프로젝트

일반

사용자정보

Actions

이슈 #446

진행중
HP HP

Phase 1 — dFS PoC 시작 (Core Schema → File API → VfsService 폐기 → vfs_nodes drop)

이슈 #446: Phase 1 — dFS PoC 시작 (Core Schema → File API → VfsService 폐기 → vfs_nodes drop)

Henry Park이(가) 약 5시간 전에 추가함.

상태:
해결
우선순위:
높음
담당자:
목표버전:
시작일:
2026/05/13
완료일:
진척도:

0%

추정시간:

설명

배경

Phase 0.7 Wallet-as-Identity 의 server/CLI 측 (#443) 완성으로 사람 GoTrue + headless wallet 인증 모델이 자리 잡았다. 다음 wave 는 로드맵의 Phase 1 dFS PoC — wallet/token namespace 기반 분산 파일시스템의 PoC 구현. 단계적으로 VfsService 를 폐기하고 dFS 로 대체한다.

이 이슈는 Phase 1 전체 (1A~1E) 의 메인 트래킹이며, 실제 구현은 feat/dfs-poc 브랜치에서 진행한다. next 는 데모 안정으로 유지하고, Phase 1 + 후속 phase 들이 완성되면 monitor 변경과 함께 next 로 merge.

작업 범위 (로드맵 §"Phase 1. dFS PoC" 그대로)

Phase 1A. dFS Core Schema (첫 mile)

  • wallet namespace + token namespace 지원
  • dfs_namespaces / dfs_files / dfs_file_versions (또는 동등) 테이블 신설
  • root path / 자동 생성 + root manifest 자동 생성
  • content_hash, version, readonly, kind, storage_ref 저장
  • v17 migration

Phase 1B. dFS File API

  • CreateFile / PutFile / GetFile / DeleteFile / ListDirectory / StatFile / ResolvePath / MoveFile / CommitFile
  • 단일 서버 / 단일 DB / 로컬 파일 또는 object storage 방식
  • path normalize / traversal reject / reserved path policy
  • file version + content_hash 증가, if_match CAS

Phase 1C. VfsService 폐기

  • proto / handler / store / domain/vfs 통째 제거
  • /modules /sys seed 는 dFS reserved path 로 이관

Phase 1D. Console FS Migration

  • 콘솔 파일 UI 를 dFS namespace browser 로 교체 (monitor 변경 시점에 통합)

Phase 1E. vfs_nodes 정리

  • vfs_nodes drop migration

작업 방식

  • 브랜치: feat/dfs-poc (next 무손상 유지)
  • 며칠 단위 mile 로 진행
  • 첫 mile: Phase 1A
  • sdktest 에 dfs-* 명령 추가 (회귀 검증 자동화)
  • TEST_CLIENT_CHECKLIST.md 의 Phase 1 항목 자동 [x]
  • Phase 1 완성 후 (또는 후속 phase 와 함께) next 로 merge — 그 시점에 monitor 변경 같이

관련 이슈

  • #443 — Phase 0.7 Wallet-as-Identity MVP (선행 완료, server/CLI 측)
  • #438 — Phase 5 wallet_address ownership 제거 (선행 완료)
  • #444 — GoTrue proxy guard (별개 추적, 미해결)
  • #254 — DB 스키마 전용 (v17 migration 시 refs 동반)

HP Henry Park이(가) 6달 전에 변경 Actions #1

  • 상태을(를) 신규에서 진행(으)로 변경되었습니다.

Phase 1A 완료 — dFS Core Schema + namespace store helpers

commit:server|8e9a6f1

첨부되는 first mile. RPC 표면 없이 schema + store helpers 까지. Phase 1B File API 는 다음 commit.

Schema v17

  • dfs_namespaces (namespace_type, namespace_id) UNIQUE + CHECK 제약 (namespace_type IN ('wallet','token'), owner_principal_type IS NULL OR IN ('wallet','user'))
  • dfs_files (namespace_id, path) UNIQUE + path LIKE '/%' / kind IN ('file','directory') CHECK
  • dfs_file_versions (file_id, version) UNIQUE + version > 0 / size >= 0 CHECK
  • current_version_id 은 DEFERRABLE INITIALLY DEFERRED FK 로 후행 ALTER — 같은 트랜재션에서 file/version/링크 안전
  • idx_dfs_files_namespace_path (text_pattern_ops, prefix 조회용)
  • storage_ref 기본 'db:inline' — Phase 10 chunk/CID backends 에서 분기

internal/store/dfs.go

  • EnsureWalletNamespace(walletAddress) — wallet principal self-owner (("wallet", lowercase address))
  • EnsureTokenNamespace(chain, tokenAddress) — namespace_id = <chain>:<address> (lowercase canonical). owner principal NULL (PoC, token governance 후속)
  • GetNamespace — lowercase normalize 후 lookup
  • createRootManifestTx — namespace 생성과 같은 트랜재션에서 /manifest.json (readonly=true) + version 1 + current_version_id 링크. RootManifest 구조체 field 선언 순서로 marshal → byte-deterministic content_hash + manifest_hash
  • GetFileByPath / GetFileVersion — Phase 1B 핸들러 / 테스트 용

테스트 (dfs_test.go)

  • root manifest auto-creation + readonly + content/manifest hash 일관성
  • idempotent EnsureWalletNamespace (두 번 호출 시 같은 ID)
  • lowercase canonical — mixed-case 입력이 같은 row 로 collapse
  • token namespace ": " + case-different collapse
  • GetNamespace 가 missing 에 nil + 입력 검증

검증

  • go build ./... / go vet ./internal/store/... 통과
  • go test ./internal/store/... 통과 (TEST_DATABASE_URL 없으면 dfs 통합 test 는 skip — admin_cleanup / agx_halving 등 store test 와 동일 패턴)
  • make dev-up 으로 v16→v17 migration 적용 + make smoke-check 통과는 서버 환경에서 별도. 새 RPC 아직 없으므로 기존 흐름 무영향

Phase 1 진행

  • 1A. dFS Core Schema + namespace store helpers — 본 commit
  • 1B. File API (proto + handler + store CRUD + sdktest dfs-*)
  • 1C. VfsService 폐기 (proto/handler/store/domain/vfs 통째 제거)
  • 1D. Console FS migration (monitor 통합 시점)
  • 1E. vfs_nodes drop

이슈 상태 → 진행 중.

다음 mile (Phase 1B)

  • proto/alphea/v1/dfs.proto (신규) — DfsService: CreateFile / PutFile / GetFile / DeleteFile / ListDirectory / StatFile / ResolvePath / MoveFile / CommitFile
  • internal/store/dfs.go 확장 — File CRUD (UPSERT version + atomic content_hash bump + if_match CAS)
  • internal/handler/dfs.go (신규) — auth interceptor 연결, user/wallet principal 권한 검증
  • cmd/sdktest — dfs-write / dfs-read / dfs-list / dfs-stat / dfs-move / dfs-commit / dfs-resolve
  • TEST_CLIENT_CHECKLIST.md 의 Phase 1 항목 자동 [x] 전환

HP Henry Park이(가) 6달 전에 변경 Actions #2

1A review fixes — race-safe ensure + migration guard + composite FK

commit:server|96dac84

Phase 1A (8e9a6f1) 의 세 가지 review 이슈 모두 해결.

#1 — ensureNamespace race

  • INSERT ... ON CONFLICT (namespace_type, namespace_id) DO NOTHING RETURNING id 로 교체
  • errors.Is(err, pgx.ErrNoRows) 면 winner 가 root manifest 까지 만들었음을 의미 — loser 는 tx rollback + GetNamespace 재호출
  • 함수 docstring 에 concurrency contract 명시

#2 — v17 ADD CONSTRAINT partial-retry

  • DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='...') THEN ALTER TABLE ... END IF; END $$; 로 wrap
  • partial-migration retry 안전

#3 — cross-file current_version_id (v18 신규)

  • dfs_file_versions 에 (file_id, id) UNIQUE 인덱스 추가
  • dfs_files 에 composite FK (id, current_version_id) → dfs_file_versions(file_id, id) 추가
  • 기존 single FK fk_dfs_files_current_version drop
  • 모두 pg_constraint lookup 으로 guard (재실행 안전)
  • 파일 A.current_version_id 가 파일 B 의 version 을 가리키는 케이스 schema 차원에서 reject

테스트 추가

  • TestEnsureWalletNamespaceConcurrentRace — 8 goroutines 동시 호출 모두 같은 namespace id + root manifest 정확히 1행
  • TestDFSCurrentVersionFKRejectsForeignFile — cross-file current_version_id UPDATE 시도 가 FK violation 으로 reject

검증

  • go build / vet / test ./internal/store/... 통과 (TEST_DATABASE_URL 없으면 dfs 통합 test skip)

진행 상태

  • 1A. dFS Core Schema + namespace store helpers (review fix 포함)
  • 1B. File API (proto + handler + store CRUD + sdktest)
  • 1C/1D/1E. VfsService 폐기 + Console FS + vfs_nodes drop

다음: Phase 1B.

HP Henry Park이(가) 6달 전에 변경 Actions #3

1A 완전 종료 — minor hardening 동병

commit:server|21f1fcc

review note 의 minor hardening: pg_constraint guards 에 conrelid = 'dfs_files'::regclass 추가. 세 곳 모두 동일 패턴.

Phase 1A 가 이제 완전히 닫힘. 다음 mile 는 Phase 1B File API.

HP Henry Park이(가) 6달 전에 변경 Actions #4

Phase 1B-1 — DfsService proto + path normalize

commit:server|6a8569b

1B 의 첫 sub-mile. RPC contract + path 안전성 helper.

proto

  • DfsService: ResolvePath / PutFile / GetFile / StatFile / DeleteFile / ListDirectory / MoveFile / CommitFile
  • DfsNamespaceRef + DfsFileMeta 공통
  • PutFile.if_match CAS 의미 명시
  • ListDirectory direct children only + synthetic directory entries
  • CommitFile 은 handler 에서 Unimplemented stub 예정

store helpers

  • NormalizeDFSPath — empty / leading slash / trailing slash / 빈 세그먼트 / . / .. 모두 reject. 루트 / 만 trailing slash 예외
  • IsSystemDFSPath / systemDFSPaths — 현재 /manifest.json. PutFile/DeleteFile/MoveFile 에서 차단

테스트

  • normalize accept 5개 케이스 + reject 10개 케이스 (DB 없이 동작)
  • IsSystemDFSPath 3개

검증

  • make proto / make lint 통과
  • go build ./... 통과
  • go test ./internal/store/... 통과

Phase 1 진행

  • 1A. Core Schema
  • 1B-1. proto + path helpers — 본 commit
  • 1B-2. store File API (PutFile / GetFile / StatFile / DeleteFile / MoveFile / ListDirectChildren) + 통합 테스트
  • 1B-3. handler + sdktest + smoke + checklist
  • 1C/1D/1E. VfsService 폐기 및 후속

HP Henry Park이(가) 6달 전에 변경 Actions #5

Phase 1B-2 — store File API

commit:server2|76286c0

1B 의 두 번째 sub-mile. proto/handler 없이 store 계층 File API + 통합 테스트까지. 마이그레이션 없음 — Phase 1A 의 v17/v18 테이블을 그대로 사용한다.

internal/store/dfs.go

  • DFSContentHash(content) / DFSManifestHash(version, contentHash) — manifest_hash 는 Option B: sha256("v<version>:<contentHash>"). 같은 content 를 재업로드해도 version 이 올라가면 manifest_hash 가 바뀌므로 if_match CAS 가 head 변경을 정확히 잡는다. createRootManifestTx 도 같은 공식으로 전환
  • PutFile — 단일 트랜잭션 UPSERT (file row + version row + current_version_id 링크). if_match 빈 값이면 unconditional, 비어있지 않으면 현재 version 의 manifest_hash 와 일치할 때만 성공. append-only version history. 새 path 에 대한 동시 first-write 가 (namespace_id, path) unique 인덱스에서 23505 로 충돌하면 트랜잭션을 재시작 — 두 번째 시도는 이미 존재하는 row 를 보고 version-append 분기로 진입
  • DeleteFile / MoveFile — system path · readonly reject. DeleteFile 은 PoC hard delete (ON DELETE CASCADE 로 version history 동반 삭제). MoveFile 은 same-namespace 한정이고, src == dst no-op 경로에서도 존재/readonly 검증을 수행
  • ListDirectChildren — direct children + 중간 세그먼트에 대한 synthetic directory entry. explicit row 가 synthetic dir 을 shadow
  • GetFileWithCurrentVersion — file row + 현재 version 을 함께 반환하는 헬퍼 (handler / 테스트용)

path-tree invariant

쓰기 시점에 트리 정합성을 강제한다 (ErrDFSPathConflict):

  • ancestor 경로가 이미 explicit file row 이면 reject — /manifest.json 같은 system path 의 descendant 도 이 분기로 자동 차단됨
  • target 경로에 이미 explicit descendant 가 있으면 (= implicit directory) reject
  • MoveFile 은 dst descendant check 에서 srcPath 를 제외 → /a/b/c.txt → /a/b 처럼 "자기 자신의 implicit directory 밖으로 파일 끌어올리기" 는 허용. dst 밑에 src 외의 다른 descendant 가 있으면 여전히 reject

테스트 (dfs_test.go, 총 38개)

  • PutFile: create / version append / same-content-bumps-manifest-hash (Option B 핵심 불변식) / if_match hit·miss / 존재하지 않는 path 에 대한 CAS / system·readonly reject
  • path-tree: ancestor-is-file / existing-descendants / system path descendant / MoveFile dst ancestor·descendant / pull-up 허용 / pull-up 시 sibling 은 차단
  • first-write race: 8 goroutine 동시 PutFile → 전원 성공 + version count 일치 (23505 누수 없음)
  • DeleteFile: row + version CASCADE / system·readonly reject / missing
  • MoveFile: rename / system·readonly reject / missing src / collision / src==dst 시맨틱
  • ListDirectChildren: root (/manifest.json 포함) / subdir + synthetic dir / explicit-shadows-synthetic

검증

  • go build ./... / go vet ./internal/store/... 통과
  • dFS 통합 테스트 38개 전부 통과 (dev Postgres 컨테이너에 별도 alphea_test DB 생성). store 패키지의 기존 pre-existing 실패 2건 (TestCreateReceiptIgnoresHalvingAtCompletedAt, TestClaimVirtualNodePackageSetsExpiryFromValidYears) 은 dFS 와 무관 — git stash 로 확인 완료

Phase 1 진행

  • 1A. Core Schema + namespace store helpers
  • 1B-1. proto + path helpers
  • 1B-2. store File API + 통합 테스트 — 본 commit
  • 1B-3. handler (auth: wallet namespace 는 wallet token bearer 만 write) + sdktest dfs-* 명령 + smoke-check + TEST_CLIENT_CHECKLIST [x]
  • 1C/1D/1E. VfsService 폐기 + Console FS migration + vfs_nodes drop

다음: Phase 1B-3.

HP Henry Park이(가) 6달 전에 변경 · Edited Actions #6

Phase 1B-3 — DfsService handler + apismoke dfs-* 명령

commit:server2|6c8c9c8 — sdktest/apismoke 책임 분리 리팩터
commit:server2|4e0dfd9 — 1B-3 핸들러 + apismoke dfs-* 명령

1B 의 마지막 sub-mile. 1B-2 의 store File API 를 ConnectRPC 핸들러 + raw smoke 클라이언트에 연결.

sdktest/apismoke 책임 분리 (commit:server2|6c8c9c8)

기존 cmd/sdktest 가 SDK 흐름 + raw RPC smoke 를 둘 다 하던 걸 분리:

  • cmd/sdktest = SDK conformance 전용 (raw connect/proto import 금지)
  • cmd/apismoke (신규) = raw API smoke (wallet-login, node-register --auth, vfs-*)
  • make smoke-server 추가 — server 컨테이너만 리빌드 후 smoke 1회 (server 코드 변경 fast loop, stale 서버로 smoke 도는 문제 제거)

DfsService 핸들러 (commit:server2|4e0dfd9)

internal/handler/dfs.go — DfsService 8 RPC 구현. 인증 정책 (PoC):

  • 모든 file RPC 는 wallet-token owner-only — caller 가 wallet principal 이고 그 주소가 target namespace_id 와 같아야 함
  • ResolvePath 는 유효 토큰만 있으면 OK (순수 URI 파싱)
  • token namespace + user/node 토큰 거부, CommitFile 은 Unimplemented stub
  • store sentinel 에러 → Connect code 매핑
  • write 는 namespace ensure, read/delete/move 는 기존 namespace 필요

ParseDFSURI + locator 정리

internal/store/dfs.go — ParseDFSURI 가 wallet:// / token:// URI 를 (type, id, path) 로 분해. namespace id 는 bare mainnet 0x 주소만 허용 — chain prefix (예: ethereum:0xdef) 나 비-0x id 는 reject 해서 wire contract 를 단일 형태로 고정. proto 주석 + v17 migration 주석도 no-chain-prefix 로 정렬. EnsureTokenNamespace 는 token File API 가 Unimplemented 라 chain-aware 시그니처 유지 + 후속 정리 TODO 마커.

apismoke dfs-* 명령

dfs-resolve/write/read/stat/list/move — raw DfsService 클라이언트 + wallet 토큰. vfs-* 명령 제거 (dFS 가 VFS 대체). path 인자는 full wallet://0xADDR/path URI 또는 bare /path (저장된 wallet 기준) 둘 다 허용. make smoke-check 가 dfs-* 흐름 전부 실행, TEST_CLIENT_CHECKLIST.md 갱신.

cmd/server — DfsService 등록 + gRPC reflector 목록 추가.

검증

  • go build / vet, store + handler dFS 테스트 통과 (handler 인증 거부 케이스 8개 포함 — owner 수락, 타 namespace/user/node/무인증 거부, token namespace Unimplemented 등)
  • make smoke-server 전체 통과 — server 컨테이너 리빌드 후 15단계 smoke exit 0, dfs-resolve→write→stat→read→list→move 가 실제 wire 로 동작
  • pre-existing 실패 (TestGetNodesDetail..., TestCreateReceipt..., TestClaimVirtualNode...) 는 dFS 와 무관 — clean tree 에서도 실패, alphea_test DB fixture 의존

Phase 1 진행

  • 1A. Core Schema + namespace store helpers
  • 1B-1. proto + path helpers
  • 1B-2. store File API + 통합 테스트
  • 1B-3. handler + apismoke dfs- + smoke-check + checklist* — 본 댓글
  • 1C/1D/1E. VfsService 폐기 + Console FS migration + vfs_nodes drop

다음: Phase 1C (VfsService 폐기 — proto/handler/store/domain/vfs 통째 제거).

HP Henry Park이(가) 6달 전에 변경 Actions #7

Phase 1C — VfsService 폐기 (코드 + 테이블 + 문서)

commit:server2|a2f34ff

DfsService (Phase 1B) 가 대체되어 VfsService 의 소비자가 없으므로 end-to-end 제거. 1E (vfs_nodes drop) 도 v19 에 흉수.

치운 코드

  • proto/alphea/v1/vfs.proto + gen/.../vfs.{pb,connect}.go 삭제
  • internal/handler/stubs.go: VfsHandler (struct + 8 RPC) 제거
  • internal/store/vfs.go: store + /modules·/sys seed 삭제 — dFS reserved path 로 이관 안 함 (VFS 데모 placeholder 였을 뿐 dFS 세계 소비자 없음, 필요하면 나중 명시적 feature 로 추가)
  • internal/domain/vfs/: 디렉터리 통쨰 제거
  • cmd/server/main.go: handler + reflector + lock cleanup unwire
  • internal/handler/webconsole_pages.go: /monitor/api/vfs/* 라우트 + 핸들러 제거
  • internal/handler/webconsole.go: VFS Browser HTML/CSS/JS + upload/download shell shortcut 제거, VfsService 를 RPC Explorer Ops 명단에서 드롭
  • internal/handler/webconsole_rpc.go: vfs proto descriptor 드롭
  • internal/handler/ais.go: AIS shell 파일 명령 (ls/cat/stat/mkdir/touch/rm/cp/upload/download/mv) + /modules·/sys readonly guard 제거. pwd/whoami/env 는 "Runtime Profile" 이름으로 유지
  • internal/store/admin_cleanup.go + test: DeletedVFSNodes 필드 + vfs_nodes DELETE 제거
  • cmd/migrate/main.go: migrateVfsNodes task 제거 (legacy SQLite→PG)

Schema (v19)

  • migrateV19DropVfsNodes: DROP TABLE IF EXISTS vfs_nodes. v1 baseline 은 immutable-migrations 규칙대로 그대로 유지 — fresh DB 는 v1 에서 생성 + v19 에서 즉시 drop. 세부는 #254 댓글 참고

문서

  • CLAUDE.md: 60 → 59 RPCs, structure tree (vfs.go/domain/vfs 제거 + dfs.go 추가), services table row 11 를 DfsService 로 교체, security 제약에 dFS readonly path 명시, stale 현재 v3 주석 → 현재 v19
  • docs/00-overview.md / docs/05-monitoring.md / docs/07-api-reference.md: VfsService 언급 다 DfsService 로 교체, api-reference 에 간결한 DfsService 섹션 추가
  • alphea_edge_delta_d_fs_roadmap.md: Phase 1C/1D 세부 스코프 명시 (사용자 직접 갱신)
  • TEST_CLIENT_CHECKLIST.md: 폐기 노트

검증

  • go build ./... / go vet 통과
  • Fresh alphea_test DB 에서 내가 건드린 부분(dFS store + handler + admin_cleanup) 타겟 테스트 전부 통과
  • make smoke-server: server 컨테이너 리빌드 (VfsService 없이, v19 적용), 16단계 smoke exit 0 — sdktest auth + apismoke wallet-login + node-register --auth wallet + dfs-resolve→write→stat→read→list→move 전부
  • pre-existing 실패 (TestClaimVirtualNodePackageSetsExpiryFromValidYears, TestGetNodes* / TestGetEarnings* / TestWalletNodeResponses* 클러스터) 는 VFS 제거와 무관 — Phase 5/0.7 의 wallet_address → user_id/principal 이전(v15/v16) 때 같이 갱신 안 된 stale seed 테스트. git stash -u 로 확인: 4e0dfd9 트리에서도 동일하게 실패. 도메인 분리가 있으니 1C 다음에 별도 컰밋으로 정리 예정

Phase 1 진행

  • 1A. Core Schema + namespace store helpers
  • 1B-1. proto + path helpers
  • 1B-2. store File API
  • 1B-3. handler + apismoke dfs-* + smoke-check + checklist
  • 1C. VfsService 폐기 (+ vfs_nodes drop v19 으로 1E 흉수) — 본 커밋
  • 1D. Console FS Migration — dFS namespace browser + AIS shell 을 dFS 기반 Unix-style shell 로 (roadmap 에 세부 추가됨)
  • 1E. vfs_nodes drop (1C 에 흉수)

다음: Phase 1D — Console FS Migration.

HP Henry Park이(가) 6달 전에 변경 Actions #8

Phase 1C 사후 리뷰 반영 (doc-only)

commit:server2|66573bb

Phase 1C 리뷰에서 나온 minor finding 2건:

  • alphea_edge_delta_d_fs_roadmap.md 의 Phase 1E 섹션이 아직 예정 작업처럼 남아 있고 stale v12 → v13 예정 표기 있음 → "Phase 1C 에 folded (완료)" 로 명시, 별도 완료 기준 충족 찴는 표기 — vfs_nodes 가 더 이상 존재하지 않음 (v19), dFS 테이블만으로 smoke-server 동작 확인
  • docs/07-api-reference.md 의 Service overview 다이어그램에서 DfsService 를 Admin → Authenticated subgraph 로 이동 — 실제 정책이 wallet JWT owner-only 라 Admin 으로 보여 둘 경우 오해 소지

이후 Phase 1D (Console FS Migration) 로 진행 예정.

HP Henry Park이(가) 6달 전에 변경 Actions #9

Phase 1D 준비 — Phase 5/0.7 stale ownership 테스트 cleanup

commit:server2|f783fb3

fresh alphea_test DB 에서 go test ./internal/store ./internal/handler 에 실패하던 10건 stale 테스트 전부 수정. polluted DB 에서 일부가 우연히 통과해 가려져 왔던 것 — 롤백하면 v15/v16 이전 때 같이 갱신 안 된 seed 흥적으로, dFS 와는 무관. 1D 리뷰 전에 baseline 을 정리하는 차원.

세 가지 명확한 패턴 + 1 isolation fix

  1. seedVirtualNodeAssignment 이중 user_id seed (7건): internal/handler/wallet_virtual_nodes_test.go. helper 가 내부에서 user_id 를 하나 생성해 assignment 를 바인딩 — 호출자가 같은 wallet 로 seedUserWithWallet 를 다시 호출하면 → idempotent 이 아니라 두 번째 user_id 가 생성, query 는 두 번째 유저로 돌아서 0행. helper 가 (wallet, userID, *assignment) 를 직접 반환하도록 변경, 7개 사이트 에서 중복 seed 제거. 해당 테스트: TestGetNodes* / TestGetNodesDetail* / TestGetNodesExpands* / TestGetNodesDetailExpands* / TestWalletNodeResponsesHideRealMobileNodes

  2. virtual_node_credits raw INSERT 에 구 column (2건): internal/handler/wallet_vip_test.go. TestGetEarningsByRange* 두 테스트가 wallet_address 컴럼엄으로 INSERT 하는데 v15 에서 user_id UUID 로 교체됨. credited_at 을 명시적으로 백필하는 구조라 InsertCredit (NOW() 고정) 대신 raw INSERT 유지, column/param 만 swap

  3. seedClaimableWallet 가 wallet 을 UUID 컴럼엄에 넘김 (1건): internal/store/virtual_node_claims_test.go. ClaimVirtualNodePackageByID(userID, ...) 가 v15 이후 UUID 기대 → wallet 0x... 을 넘기면 Postgres 22P02. seedClaimableUser 로 rename, EnsureAccountForUser(uuid, "") 경유, user_id 반환. uniqueStoreTestUserID helper 추가. uniqueStoreTestWallet 은 agx_halving_test 에서 생의의 wallet 이 필요해서 그대로 유지

  • isolation fix: TestCreateReceiptIgnoresHalvingAtCompletedAt 이 하드코딩된 JobID: "job-halving-test" 를 써서 같은 DB 에 2회 돌리면 job_receipts_job_id_key 23505 충돌. timestamp 접미 추가로 rerunnable

검증

  • fresh alphea_test DB → go test ./internal/store ./internal/handler 전체 통과
  • 같은 DB 에 다시 돌려도 통과 (re-runnable)
  • 80 PASS, 0 FAIL

이제 Phase 1 상태

  • 1A / 1B-1 / 1B-2 / 1B-3 / 1C
  • 1E (1C 에 folded)
  • 테스트 baseline 정리 (1D 준비) — 본 댓글
  • 1D. Console FS Migration

다음: Phase 1D.

HP Henry Park이(가) 6달 전에 변경 Actions #10

Phase 1D-1 — dFS Browser + AIS shell read commands

commit:server2|b581444

1C 에서 소멸된 콘솔 파일 브라우저를 dFS 기반으로 복원. read-only, write 는 1D-2.

아키텍처 결정 (핵심)

두 표면 완전 분리 — 인증 모델도 그에 따라 다름:

  • DfsService (client-facing): wallet JWT owner-only 그대로. mobile/SDK/apismoke 는 계속 이곳을 경유
  • AIS shell + Web Console (operator-facing): DfsService 우회, 서버 내부에서 store dFS API 직접 호출. 브라우저가 wallet token 들 필요 없음. 신뢰는 /monitor admin 접근 자체에 있고, per-namespace ACL 은 task 모델 이후
  • Shell context (current namespace/path): client-side only. 서버 stateless 유지 — use/cd/pwd 는 JS 내에서 소화, 별도 RPC 없음; bare ls/cat/stat 는 전척 URI 로 해서해 서버 호출

변경

internal/handler/console_dfs.go (신규): 서버 내부 dFS 어댑터. consoleResolveURI (store.ParseDFSURI + GetNamespace), consoleListDirectory / consoleStatFile / consoleReadFile — store API 직접 호출. consoleDFSEntry JSON shape 은 DfsFileMeta 필드 그대로

internal/handler/webconsole_pages.go: /monitor/api/dfs/list / /dfs/read / /dfs/stat 라우트 + 핸들러. writeDFSError 가 ErrDFSNotFound → 404, 그 외 500

internal/handler/webconsole.go: 1C 에서 빴웠던 grid-2 자리에 AIS Shell ↔ dFS Browser 나란히 복원. namespace 입력 + Use + Refresh, breadcrumb, 파일 리스트, viewer (텍스트 + base64 image preview). handleDFSShellLocal 이 shell 입력에서 use/cd/pwd 가로채고, bare ls/cat/stat 를 full URI 로 리소스

internal/handler/ais.go: ls / cat / stat 명령 추가 (full URI 인자 기대, store 직접 호출, path.Base 재임포트). help 에 "-- dFS --" 섹션 추가; "-- Runtime --" 의 pwd 는 client-side-only 로 표기

검증 (make smoke-server 리빌드 서버에서)

  • HTTP 라우트 — /monitor/api/dfs/list?uri=wallet://0x.../ 가 synthetic /data + readonly /manifest.json 반환, /dfs/stat 메타데이터 일치, /dfs/read 는 base64 content 가 root manifest JSON 으로 디코딩
  • AIS 명령 — ls wallet://.../ 가 d- v0 0 data / -r v1 178 manifest.json 포맷닝한 줄 반환, stat .../manifest.json 이 9줄 메타데이터 표시
  • help 이 -- dFS -- 섹션 포함, pwd 는 client-side 표기
  • make smoke-server — 컨테이너 리빌드 후 16단계 smoke exit 0

Phase 1 진행

  • 1A / 1B-1 / 1B-2 / 1B-3 / 1C / 1E (folded into 1C)
  • 소유권 테스트 baseline cleanup (1D 준비)
  • 1D-1. dFS Browser + AIS shell read 명령 — 본 댓글
  • 1D-2. write 명령 (put / get / mv / rm) 양쪽 표면에

다음: Phase 1D-2.

HP Henry Park이(가) 6달 전에 변경 Actions #11

1D-1 사후 리뷰 반영 — dFS Browser stored XSS 수정

commit:server2|a8913d0

리뷰어가 지적한 High finding 입니다:

공격 경로: NormalizeDFSPath 가 path segment 의 <, >, ", ' 같은 HTML 메타문자를 차단하지 않아, wallet owner 가 DfsService.PutFile 로 /<img src=x onerror=alert(1)>.txt 같은 파일명을 심을 수 있음. admin/operator 가 해당 namespace 를 dFS Browser 에서 열면 이전 dfsRenderList 가 파일명을 innerHTML 문자열에 이어붙여서 삽입 → live HTML 로 파싱되어 onerror 실행. stored XSS

wire 재현 (수정이전): apismoke dfs-write --uri '/<img src=x onerror=alert(1)>.txt' → PutFile 수락 → /monitor/api/dfs/list JSON 은 해당 문자를 보존 (Go json.Marshal 이 transit 에서는 angle bracket escape 하지만 JSON.parse 이후 JS 에서는 원본 문자열 획득) → 브라우저에서 innerHTML 으로 주입되면 실행

수정: dFS Browser 렌더링을 createElement + textContent + addEventListener 기반 DOM API 로 전환. inline onclick=... 속성 문자열 제거 (path 이 타립 문자열에 주입되는 별도 경로도 같이 닫음). 공격자 제어 값 (path, name, breadcrumb segment, error message) 은 전부 textContent 경유 → HTML 파싱 이스케이프 불가. icon 글림폰트는 여전히 innerHTML 이지만 hardcoded entity 라 안전

추가 헬퍼:

  • dfsMakeText(tag, cls, text) — tag+class+textContent 노드 생성기
  • dfsAppendItem(parent, opts) — 리스트 아이템을 closure click handler 로 조립, dataset.path 으로 데이터 전달
  • dfsRenderError(text) — 이전 innerHTML 기반 에러 렌더링 대체

검증: 재빌드 후 동일 공격 페이로드로 재현 — PutFile 은 여전히 수락 (이게 정상—파일명은 데이터, 렌더링 계층의 책임), /dfs/list JSON roundtrip 정상, ais ls 출력 그대로 텍스트. 코드 구조상 남은 innerHTML 사용은 전부 하드코딩 entity 또는 빈 clear (= '')

DfsService client surface 에 추가 path validation 을 겕필 넘어볼지도 결정 필요 — 지금 렌더링 단에서 안전한 건 맞으나 path 자체에 제약 (UTF-8, control char, max length 등) 을 두는 게 흔한 패턴. 현재는 PoC 단계라 보류 — 이후 속편으로 처리 (필요하면 별도 이슈)

HP Henry Park이(가) 6달 전에 변경 Actions #12

Phase 1D-2 — dFS Browser + AIS shell write 명령

commit:server2|fe0b728

1D 완결. put / delete / move 을 operator 표면 양쪽 (AIS shell + Web Console) 에 동일한 어댑터/분류기로 추가. DfsService client 표면은 단 한동도 건드리지 않음.

어댑터 (console_dfs.go)

  • consoleEnsureNamespaceForWrite: 첫 쓰기에서 wallet namespace 자동 생성. token 쓰기는 거부 (EnsureTokenNamespace chain-aware 시그니처 정리 이후)
  • consoleWriteFile / consoleDeleteFile / consoleMoveFile — store 직접 호출. move 는 cross-namespace src/dst 거부
  • consoleClassify 이 single source of truth — store sentinel + 어댑터 자체 sentinel 을 consoleErrorKind 에 모아 HTTPStatus() 한 번으로 HTTP 적절 코드 산출. AIS shell 도 같은 분류기 경유

HTTP 라우트 (webconsole_pages.go)

  • POST /monitor/api/dfs/put — {uri, mimeType, contentBase64, ifMatch}. base64 로 통일해서 binary-safe; JS 가 텍스트 편집(btoa) + 파일 업로드(FileReader) 술다 같은 shape
  • POST /monitor/api/dfs/delete — {uri}
  • POST /monitor/api/dfs/move — {srcUri, dstUri}
  • writeDFSError 가 consoleClassify 경유로 리팩터 (1D-1 의 404-only 검사 교체)

AIS shell (ais.go)

  • put <uri> <content...> — args 공백 join (단일 라인 텍스트; 바이너리는 콘솔 업로드)
  • mv <src> <dst> — full URI, 같은 namespace 필수
  • rm <uri> — system path / readonly 거부 메시지 그대로 통과
  • help 텍스트 "-- dFS --" 섹션 확장

Web Console UI (webconsole.go)

  • Upload 버튼 → hidden file input → FileReader → /dfs/put. drag-drop 동일 경로
  • Save / Del 버튼 — 선택 파일이 readonly 가 아닐 때만 노출. dfsUpdateActionButtons 가 textarea readonly + 버튼 가시성 동시 제어
  • Save 는 ifMatch = manifestHash 으로 보내 동시 편집 충돌을 412 로 잡음 (silent overwrite 차단)
  • 새 렌더링 코드도 a8913d0 XSS 원칙 유지 — 서버 데이터 전부 textContent/DOM property, innerHTML 문자열 조립 불가

검증 (재빌드 컨테이너)

  • Fresh wallet 생성 → list 404 (no namespace) → /dfs/put 으로 /notes/hello.txt v1 쓰기 → list 에 등장
  • stale ifMatch → HTTP 412 ("if_match does not match...")
  • /manifest.json PUT/DELETE → HTTP 403 ("system path is reserved")
  • cross-namespace move → HTTP 400 ("cross-namespace move not supported")
  • AIS shell put/cat/mv/rm round-trip — /manifest.json rm 은 적절히 거부
  • DfsService regression check — console 으로 쓴 파일을 apismoke dfs-stat / dfs-list 가 동일 메타로 읽음 (두 표면이 같은 store 에서 동작 확인)
  • fresh alphea_test DB 에 store + handler 전체 test 통과

로드맵 추가 항목

사용자님이 roadmap 에 Phase 1F — AIS → dShell/dsh full rename 섹션 추가. 1D 다음 별도 커밋으로 진행 (proto → DshService, handler/route/prompt ↔ dsh, 호환 alias 미설치 — 개발 단계니까). 1D-2 스코프 안이라 별도 이슈/커밋으로 잡고 진행 예정

Phase 1 진행

  • 1A / 1B-1 / 1B-2 / 1B-3 / 1C / 1E
  • 테스트 baseline cleanup
  • 1D-1. dFS Browser + AIS read
  • 1D-2. dFS Browser + AIS write — 본 댓글
  • 1F. AIS → dShell/dsh rename (별도 커밋)

Phase 1 dFS PoC 기능 레벨 완결 — 1F 는 순수 rename, 기능 추가 아님. 이후 monitor 통합 시점에 next 로 일괄 merge 예정 (본 브랜치 원칙 유지).

HP Henry Park이(가) 6달 전에 변경 Actions #13

1D-2 사후 리뷰 반영 — non-ASCII text round-trip 수정

commit:server2|35c07b0

리뷰어가 지적한 High finding:

버그: dfsOpenFile 이 서버 base64 를 bare atob() 로 디코딩 — 결과는 "binary string" (char 하나가 byte 하나). ASCII 파일은 우연히 맞게 보외으나, UTF-8 텍스트(한글/이모지/다바이트 JSON)는 에디터에 mojibake 주입. 이후 Save 는 dfsUTF8ToBase64(UTF-8 기준) 로 괴상한 문자열을 재인코딩 → 원본 파일이 조용히 망가짐

수정: dfsUTF8ToBase64 의 정확한 역 (dfsBase64ToUTF8) 추가 — base64 → Uint8Array → TextDecoder('utf-8').decode(). dfsOpenFile 가 이 helper 를 쓰도록 교체. atob fallback 은 malformed base64 의 마지막 안전장치로만 남김

검증 (재빌드 후 wire test, 브라우저의 open/save 동작 그대로 재현):

  1. DfsService.PutFile 로 안녕하세요 🚀 dFS Browser 라운드트립 테스트 쓰기 → content_hash 81522c8b...
  2. /monitor/api/dfs/read 의 base64 를 Uint8Array + UTF-8 디코딩 → 원본 문자열과 완전 일치
  3. 디코딩된 문자열 재인코딩 → 서버 base64 와 역시 완전 일치
  4. Save round-trip 시뮬레이션 — ifMatch=manifestHash 으로 PUT → CAS 적중, content_hash 불변 (81522c8b... == 81522c8b...), version 만 1→2 증가

binary 파일은 TextDecoder 기본 동작으로 U+FFFD replacement 채움 — textarea 에서 멠리 안전하게 편집 불가는 의도적. binary 경로는 Upload 버튼 (binary-safe).

1D 단계는 이제 콘솔 dFS Browser 의 읽기/쓰기/수정 라운드트립이 한글·이모지·CJK JSON 등 multibyte 에 안전. 다음 항목: Phase 1F (AIS → dShell/dsh full rename) — 별도 커밋.

HP Henry Park이(가) 6달 전에 변경 Actions #14

Phase 1F 완료 — AIS → dShell / DshService full rename

commit:server2|4909f28

Breaking rename, 호환 alias 미설치 (로드맵 1F 스코프 그대로). 19 files, +344/-337, gen/proto/handler/server/web console/docs 전부 swap.

명명

  • 사용자-facing: dShell
  • 코드/RPC/route/prompt: dsh / Dsh / dsh> / /monitor/api/dsh-exec
  • ConnectRPC 표면: alphea.v1.DshService (Exec/Shell, 둘 다 publicRPCs 유지)
  • proto file: proto/alphea/v1/dsh.proto (DshExecRequest/Response, DshShellRequest/Response — wire field 동일)
  • handler: internal/handler/dsh.go (DshHandler / NewDshHandler)
  • env command: DSH_VERSION=2.0 (AIS_VERSION 제거)

제거된 것

AisService, AisHandler, NewAisHandler, AisExec*/AisShell*, ais.proto, ais.pb.go, ais.connect.go, /monitor/api/ais-exec, handleAISExec, ais> prompt, "AIS Shell" 레이블, "AIS Console" 섹션, ConnectRPC 메세지 "AisService.Exec is not implemented" 등 — 전부

검증

  • go build ./... / go vet 통과
  • fresh alphea_test DB 에서 store + handler 테스트 통과
  • make smoke-server 16단계 exit 0 — 1D-2 baseline 결과와 byte-for-byte 일치
  • wire 검증: POST /monitor/api/dsh-exec 이 새 help ("=== dShell Commands ===" + "dsh prefix is optional") 반환, dsh whoami 이 dsh prefix strip 동작, 구 POST /monitor/api/ais-exec → HTTP 404 (알리아스 없음, 설계대로)
  • repo-wide grep: AisService / AisHandler / AisExec* / AisShell* / ais-exec / handleAIS / File_alphea_v1_ais_proto 코드/proto 참조 0 (의도적 역사 주석 2줄 제외 — dsh.go + dsh.proto 의 "Renamed from … in Phase 1F")
  • 로드맵 1F 섹션은 ✅ 표기 + 스코프/완료 기준 불덼릹 유지 (커밋이 구현한 항목 원본을 지우지 않기 위해)

Phase 1 완전 종료

  • 1A. Core Schema
  • 1B-1. proto + path helpers
  • 1B-2. store File API
  • 1B-3. handler + apismoke dfs-*
  • 1C. VfsService 폐기 (+ 1E folded)
  • 소유권 테스트 cleanup
  • 1D-1. dFS Browser + AIS read
  • 1D-2. dFS Browser + AIS write
  • 1F. AIS → dShell rename — 본 댓글

Phase 1 dFS PoC 전체 완결. 다음 행동은 사용자 판단 — monitor 통합 시점에 next 머지, 또는 다음 phase (Phase 2: Root Manifest / Delta Registry) 진입. feat/dfs-poc 브랜치 현재 상태: 8 commits ahead of next, smoke 통과, fresh-DB 테스트 0 failures.

HP Henry Park이(가) 6달 전에 변경 Actions #15

Phase 2-1 — Delta Registry store layer + schema v20

commit:server2|48b17a0

Phase 2 (Root Manifest / App Manifest / Delta Registry) 의 하도명: store 계층 + schema. Phase 2-2 (proto + DeltaService handler + apismoke) 는 다음 커밋.

스키마 v20 (디테일은 #254 댓글 참고)

  • delta_apps 테이블: FK dfs_namespaces (ON DELETE CASCADE), UNIQUE(namespace_id, app_id), owner_principal_type/id (CHECK 'user'|'wallet')
  • 인덱스: (namespace_id, enabled, app_id) for listing, (owner_principal_type, owner_principal_id) for future per-user queries

스토어 레이어 (internal/store/delta.go + delta_register.go)

  • AppManifest Go struct — schema_version / app / runtime / routes / permissions / payer / limits / digest (unknown key forward compat). signatures 는 PoC 보류
  • ParseAppManifest — schema_version = alphea.app.v1 고정, app.id [A-Za-z0-9][A-Za-z0-9_-]{0,63}, runtime.type 는 주어진 세트 (초기: http-static 하나)
  • sentinel: ErrDeltaAppInvalidManifest, ErrDeltaAppNotFound
  • RegisterDeltaApp / UnregisterDeltaApp / GetDeltaApp / ListDeltaApps

원자성 (사용자 확정 설계 그대로)

단일 트랜잭션 per RegisterDeltaApp:

  1. namespace resolve (NotFound 시 rollback)
  2. begin tx
  3. SELECT manifest file FOR UPDATE (동시 PutFile 차단)
  4. SELECT version content + ParseAppManifest
  5. UPSERT delta_apps keyed by (namespace_id, app_id)
  6. SELECT root manifest FOR UPDATE, patch RootManifest.Apps[app_id], serialize
  7. replaceRootManifestTx (in dfs.go, package-private) — system-managed write path: 새 dfs_file_versions row + dfs_files.current_version_id 재링크. 공개 PutFile 는 여전히 RootManifestPath 거부; 이 helper 는 그 검사를 내부 flow 에서만 우회
  8. commit

실패하면 전부 rollback — delta_apps 와 root manifest 가 갈라지는 순간 차단.

테스트 (delta_test.go, fresh alphea_test DB)

10개 전부 통과:

  • Happy path: register → delta_apps row + Apps[app_id] + root manifest version 정확히 1 만 증가
  • UPSERT: 같은 app_id 재-register → PK 불변, list len = 1
  • Validation rejects (6 구조): empty/wrong schema_version, missing/bad app.id, unknown runtime, 잘못된 JSON — 전부 ErrDeltaAppInvalidManifest + root manifest version 불변 (원자성 rollback)
  • Namespace / manifest file not found → ErrDFSNotFound
  • Unregister: 행 제거 + Apps[app_id] 제거; 존재하지 않는 app_id → ErrDeltaAppNotFound
  • ListDeltaApps 은 app_id sorted, GetDeltaApp(없음) → (nil, nil)
  • ParseAppManifest 독립 단위 커버리지

전체 store + handler test suite 여전히 0 failure.

다음 (Phase 2-2)

  • proto/alphea/v1/delta.proto — DeltaService.RegisterApp/UnregisterApp/GetApp/ListApps/EnableApp/DisableApp
  • internal/handler/delta.go — wallet-token owner-only (DfsService 패턴)
  • server registration + reflector
  • apismoke delta-register/get/list/unregister
  • E2E wire 검증

Phase 2 진행

  • 2-1. schema + store + tests — 본 커밋
  • 2-2. proto + handler + apismoke
  • 2-3. console UI + AIS shell delta 명령 (Phase 3 runtime 준비 시점)

HP Henry Park이(가) 6달 전에 변경 Actions #16

2-1 사후 리뷰 반영 — digest · entry · manifest path

commit:server2|5ad62ea

3 findings 전부 수정. 스키마 변경 없음 (테스트 + 검증 로직만).

▸ High — fallback digest 가 manifest 원본 bytes 해시

readManifestForRegisterTx 가 반환하는 content 바이트를 이전 코드는 _ 로 버리고 json.Marshal(manifest) 를 hash 했음 — JSON whitespace / key order / unknown forward-compat field 가 전부 사라져 delta_apps.digest 와 dfs_file_versions.content_hash (해당 manifest version 의) 가 갈라질 수 있음. Phase 3 에서 digest 를 registry↔file integrity bond 로 쓰는 이상 silent desync. Fix: content 을 caller 가 유지해서 manifest.Digest == "" 일 때 DFSContentHash(content) 사용. 새 TestRegisterDeltaAppDigestHashesOriginalBytes 가 논리를 고정 — 의도적 whitespace + unknown forward-compat field 있는 manifest 를 쓰고, register 한 다음 delta_apps.digest == dfs_file_versions.content_hash 를 확인

▸ Medium — runtime.entry required

Phase 2 MVP 메모의 required 조건은 runtime{type, entry} 였으나 validator 에 entry 체크 없었음. Phase 3 http-static runtime 에서 entry 없으면 실행 기준점 없음. strings.TrimSpace(m.Runtime.Entry) == "" 를 reject. 기존 bad-manifest 테스트 case 들은 entry 채워서 다른 이유로 실패하도록 수정 + "missing runtime.entry" / "empty runtime.entry" 2개 케이스 추가

▸ Medium — manifest_path == app_path + "/manifest.json" 강제

이전에는 app_path=/prj/app1 을 등록하면서 manifest_path=/prj/other/manifest.json 을 지정 가능 — registry/파일 관계 silently broken. PoC 모델이 "앱 디렉터리 + 그 아래 manifest" 이므로 정확 일치 강제 (validateRegisterRequest). 나중 custom manifest 이름 필요하면 prefix 검증으로 완화 가능. TestRegisterDeltaAppRejectsManifestOutsideAppDir 추가

검증

12 delta tests fresh DB 전부 통과, 전체 store + handler suite 도 0 failure.

리뷰어 칭찬 포인트 (그대로 유지)

  • Register/Unregister 단일 tx 묶음
  • replaceRootManifestTx package-private + public PutFile system path 차단 유지
  • v20 schema 어니메이 / index 범위

Phase 2-2 수행 전 baseline 이 깔끔하게 잡혀습니다.

HP Henry Park이(가) 6달 전에 변경 Actions #17

Phase 2-1 follow-up — happy-path 테스트 어서션 정합화

Review 에서 지적된 minor (non-blocking) 항목 반영:

  • TestRegisterDeltaAppHappyPath 의 "root manifest 가 정확히 한 번 bumped" 검증이 CurrentVersionID (dfs_file_versions 전체 BIGSERIAL) 만 비교해서 "바뀌었음" 만 보장했음. 같은 namespace 의 다른 파일이 동시에 쓰여도 BIGSERIAL 은 올라가므로 주석이 약속한 "exactly once" 와 어긋남.
  • DFSFileVersion.Version (file 별 카운터, createRootManifestTx 에서 1 로 시작) 을 비교하도록 변경. afterVer.Version == beforeVer.Version + 1 로 직접 어서션.
  • 동시에 register 후 root manifest 내용 검증용으로 중복 호출하던 GetFileWithCurrentVersion 한 번 줄임 — after 조회에서 받은 afterVer.Content 를 그대로 unmarshal.

12 delta 테스트 (happy / upsert / 8 reject case / outside-app-dir / digest-original-bytes / namespace-not-found / manifest-file-missing / unregister x2 / list / get-missing / parse) 전부 통과.

commit:server2|617f87c

Phase 2-1 마무리. 다음은 Phase 2-2 (proto + handler + apismoke).

HP Henry Park이(가) 6달 전에 변경 Actions #18

Phase 2-2 완료 — DeltaService RPC surface + apismoke + smoke-check

Phase 2-1 store 계층을 wire 위로 올림.

새 서피스

  • proto/alphea/v1/delta.proto — DeltaService 4 RPC: RegisterApp / UnregisterApp / GetApp / ListApps
  • DeltaApp 메시지는 요청 namespace 를 그대로 echo 해 돌려주어 단일 response 로 끝.
  • Enable/Disable 토글과 manifest read-through 은 Phase 3 runtime 이전이라 이번엔 미포함 (사용자 지시대로 2-2 범위 좌우).

Auth 정책 (DfsService 패턴 재사용)

  • token namespace → Unimplemented
  • user token → PermissionDenied (user→wallet 매핑은 후속 phase)
  • cross-namespace wallet token → PermissionDenied
  • store sentinel → Connect code 매핑:
    • ErrDeltaAppInvalidManifest → InvalidArgument
    • ErrDeltaAppNotFound / ErrDFSNotFound → NotFound
    • ErrDFSSystemPath / ErrDFSReadonly → PermissionDenied
  • ListApps on missing namespace → 빈 리스트 ("namespace 없음" vs "앱 없음" 클라이언트 분기 불요)

apismoke (cmd/apismoke)

  • delta-register --app-path /prj/<app> — manifest_path 는 서버에서 <app_path>/manifest.json 결정, 로컬에서 구성 필요 없음
  • delta-get --app-id <id>
  • delta-list
  • delta-unregister --app-id <id>
  • --namespace 은 saved wallet 로 fallback (bare-path 명령의 ergonomics 동일)

Makefile smoke-check 에 Delta lifecycle 5 단계 추가

dfs-write /prj/smoke-app/manifest.json
delta-register --app-path /prj/smoke-app
delta-list
delta-get --app-id smoke-app
delta-unregister --app-id smoke-app

make smoke-server exit=0. 등록 응답에서 digest = manifest file content_hash 가 wire 상에서 일치함을 확인 — Phase 2-1 의 digest-source review fix 가 실제로 동작함.

Wiring

  • cmd/server/main.go — NewDeltaServiceHandler 등록 + alphea.v1.DeltaService reflection name
  • .gitignore — /apismoke, /sdktest bare-build 산물 추가

commit:server2|ec05ec0

Phase 2 MVP ("등록되어 조회 가능") 닫힘. 다음 후보: Phase 3 runtime 적용 또는 Phase 2-3 (콘솔 UI + dShell delta 명령).

HP Henry Park이(가) 6달 전에 변경 Actions #19

Phase 2-2 review fix — InvalidRequest sentinel + handler tests

3개 finding 전부 반영.

High — request validation 가 Internal 로 새던 문제

validateRegisterRequest 가 plain errors.New / fmt.Errorf 를 반환해서 handler 의 deltaStoreError default 명제로 CodeInternal 로 떨어졌음. 사용자 입력 오류가 5xx 로 나가는 상황.

수정: store.ErrDeltaAppInvalidRequest sentinel 추가. validateRegisterRequest 와 UnregisterDeltaApp 의 app_id required 가드 모두 %w 로 wrap. deltaStoreError 에서 ErrDeltaAppInvalidRequest → InvalidArgument 매핑 추가 (wrapped 일 때도 errors.Is 가 올바르게 동작).

Medium — proto 주석 계약 불일치

DeltaApp.entry 주석이 "optional, may be empty" 이었는데 Phase 2-1 fix 이후 ParseAppManifest 가 빈 entry 를 거절. 주석을 "Always populated — ParseAppManifest rejects an empty entry at register time" 로 정독. make proto 로 regenerate.

Medium — handler 테스트 공백

internal/handler/delta_test.go 신규. DfsHandler 테스트 패턴 그대로 (이용 DB 불필요 — auth context + sentinel mapping 고정):

  • TestDeltaRequireWalletNamespaceOwner (8 sub) — wallet 소유자 (대소문자), 다른 namespace → PermissionDenied, user/node token → PermissionDenied, no auth → Unauthenticated, token namespace → Unimplemented, nil/unknown ref → InvalidArgument
  • TestDeltaStoreErrorMapping (9 case) — InvalidRequest / InvalidRequest wrapped / InvalidManifest / InvalidManifest wrapped / DeltaNotFound / DFSNotFound / SystemPath / Readonly / unknown
  • TestDeltaHandlerExplicitInvalidArgument (2 sub) — GetApp/UnregisterApp 의 empty app_id 가드

High 가 높아쇌다면 InvalidRequest wrapped 케이스에서 잡혀을 구조.

추가 — store 테스트 sentinel 고정

TestRegisterDeltaAppRejectsManifestOutsideAppDir 에 errors.Is(err, ErrDeltaAppInvalidRequest) assertion 추가. store 계층에서도 sentinel 계약 고정.

검증

  • go test ./internal/handler/ -run TestDelta — 3 그룹 12 sub 전부 새로 통과
  • go test ./internal/store/ -run TestRegisterDeltaApp — fresh DB 통과
  • make smoke-server 재검 exit=0 (digest 일치, register/list/get/unregister 전부 wire 동작)

commit:server2|65475f0

Phase 2-2 review 사이클 종료.

HP Henry Park이(가) 6달 전에 변경 Actions #20

Phase 3-1 완료 — http-static runtime adapter (「앱이 열린다」 철건)

dFS + Delta Registry 가 첫으로 한 흐름으로 닫힘. 사용자가 app 파일을 dFS 에 쓰고 manifest 를 등록하면 공개 HTTP 라우트에서 enabled http-static 앱이 서빙됨.

URL 모양

GET /d/<namespace_type>/<namespace_id>/<app_id>/[<path>]

  • 예: /d/wallet/0xabc.../smoke-app/ → manifest.runtime.entry 반환
  • 예: /d/wallet/0xabc.../smoke-app/index.html → 명시 path

결정 사항

  • Auth (PoC 확정): enabled app 은 public read. wallet/permission token 게이트 없음. 이유 — "앱이 열린다" 는 브라우저 방문, enabled 자체가 wallet owner 의 서빙 허용 선언, capability token 은 Phase 4 본 작업. Phase 3-2 의 opt-in flag 로 추가 가능하게 여지 남겨둔.
  • Registry-opacity 404: "not servable" 모든 경우 (ns/app/disabled/runtime/manifest/drifted) 403 이 아닌 404. 공개 표면에서 registry 누출 차단.
  • Manifest drift = 404: 매 요청이 <app_path>/manifest.json 의 current content_hash 를 delta_app.digest (등록 시점 pinned) 와 비교. 불일치 → 404. re-register 만이 해결책 — Phase 3 가 약속한 integrity bond.
  • Runtime 화이트리스트: http-static 만. SSR / WASM 는 후속 adapter.
  • Path 안전망: target path 가 항상 store.NormalizeDFSPath 통과 — .. traversal, 빈 segment, trailing slash, cross-namespace 차단. net/http.ServeMux cleanup 이 추가 방어.

구현

  • internal/handler/delta_http.go 신규 — DeltaHTTPHandler (parser + resolver + drift check + serve)
  • cmd/server/main.go 에 srv.Mux().Handle("/d/", ...) 등록
  • cmd/apismoke state --field walletAddress 추가 (jq 의존 없이 bash 추출)
  • 응답 헤더: Content-Type (mime), Cache-Control: no-store, X-Alphea-App-Id, X-Alphea-App-Version

테스트

  • internal/handler/delta_http_test.go — DB-free 19 sub-test:
    • parseDeltaPath 9 case (happy + variants + missing/empty/wrong prefix)
    • resolveTarget 10 case (default/explicit + 4 traversal/shape 거절 + empty entry + drift-resistant ..)
  • make smoke-server 에 Phase 3 lifecycle 추가 — default route / 명시 path / traversal / drift 404 / re-register 후 복귀 / unregister 후 404. exit=0.

검증 일부 (smoke 출력에서)

==> runtime GET default route
<h1>Hello smoke</h1>
==> runtime path traversal rejected
==> manifest drift returns 404 (registered digest pinned)
==> re-register after drift restores serving
<h1>Drifted hello</h1>
==> runtime GET after unregister returns 404

commit:server2|40b1860

안 한 것 / 다음 후보

  • Phase 3-2 (opt-in wallet-token gate)
  • Phase 2-3 (Web Console Delta panel + dShell delta 명령) — 이제 runtime 동작이 있으니 그림이 더 떨어짐
  • next 로 merge 권장 시점 곀해볼만 함 — 등록 + 실행 까지 닫힌 상태

HP Henry Park이(가) 6달 전에 변경 Actions #21

Phase 3-1 review fix — digest 는 항상 file content_hash, drift check fail-closed

2개 finding 모두 반영.

High — registry digest 가 caller-supplied 가능했던 구조

RegisterDeltaApp 이 manifest 의 "digest" JSON 필드를 우선시하고 content_hash 는 fallback 이었음. 즉 wallet owner 가 "digest":"sha256:deadbeef..." 같은 임의 값을 넣은 manifest 를 등록하면 register 는 성공하고, runtime 의 drift check 가 delta_app.digest (deadbeef) 와 실제 manifest version content_hash 를 비교해 영원 404. column 의 "integrity bond" 의도와 코드 행동이 어김난 상태.

수정:

  • AppManifest struct 에서 Digest 필드 제거 — JSON unmarshal 은 unknown key 무시하므로 기존 manifest forward-compat 유지. 파싱되지만 읽히지 않음
  • RegisterDeltaApp 는 무조건 digest := DFSContentHash(content) 사용 — server-authored, bytes-on-disk 링크

Medium — runtime drift check 가 fail-open

manifestDigestMatches 가 app.Digest == nil || "" 일 때 true 를 반환. 공개 표면에서 stale row 나 향후 migration 실수로 드리프트 검사를 조용히 bypass 할 수 있었음.

수정: 빈 digest = fail-closed (404). 해결책은 다른 drift 경우와 동일하게 re-register. schema 타이트닝 (delta_apps.digest NOT NULL) 은 후속 migration 으로 (dev row handle 후).

테스트

  • store: TestRegisterDeltaAppDigestHashesOriginalBytes → TestRegisterDeltaAppDigestIsAlwaysContentHash rename, 2 sub-case:
    • plain — whitespace + unknown forward-compat 필드 보존 (기존 invariant)
    • rogue declared digest is ignored — manifest 에 의도적으로 틀린 "digest" 값 주입, 저장된 row 가 STILL DFSContentHash(content) 은지 검증. fix 없으면 실패
  • smoke: 기존 manifest 에 "digest":"sha256:deadbeef..." 펙이로드 추가. runtime 이 declared 값을 신뢰하면 GET 404 됨. wire 확인: register 응답 digest 가 manifest file content_hash (sha256:f892f6dd...) 이며 deadbeef 안 나타남

검증

  • go test ./internal/store ./internal/handler — fresh DB 통과
  • make smoke-server — exit=0, declared-digest manifest 에도 runtime serving 정상
  • go build ./... / go vet ./... 깨끗함

commit:server2|1206e0e

후속 TODO (등장한 것): delta_apps.digest 컬럼 타이트닝 (NOT NULL migration). 현재 runtime 은 fail-closed 로 보호되니 급하지 않음.

HP Henry Park이(가) 6달 전에 변경 Actions #22

Phase 1G 완료 — dFS + Delta + runtime dev/test open mode + dsh CLI

PoC 의 wallet-token owner-only 정책을 일괄 완화. "open token://0x… → 파일 put → delta register → HTTP GET" 이 E2E 로 돌게 함.

자이의적 이름

  • requireDFSNamespaceDevOpen / requireDeltaNamespaceDevOpen — 임시성을 이름으로 노출해 production 정책 오인 차단
  • dFS 와 Delta 의 helper 는 의도적으로 디구혈는 사본 — 향후 정책 계층이 표면별로 다른 일정으로 좌일 수 있게

dFS (handler/store/console)

  • file RPC 는 인증된 그 어떤 principal (wallet/user/node) 을든 wallet/token namespace 모두 허용
  • token namespace 가 PutFile / root ls 로 자동 생성
  • EnsureTokenNamespace: chain:address → bare lowercase 0x address. chain 파라미터는 무시 사용자 컴파일 churn 회피. test 가 이 압축 계약을 pin (TestEnsureTokenNamespaceUsesBareAddress)
  • console_dfs (Web Console + dShell 어댑터): token write 차단 삭제, errConsoleTokenWriteUnimplemented 제거

Delta + runtime

  • 4 RPC 모두 dev open. caller 의 principal 이 등록 row owner. schema CHECK 가 user/wallet 만 허용하므로 node 는 RegisterApp 에서 InvalidArgument 거절 (DB 미타)
  • DeltaHTTPHandler 의 wallet-only 가드 제거. token namespace 도 같은 registry-opacity 404 + drift fail-closed 로 서빙

cmd/dsh (신규)

SSH-like operator CLI. cmd/sdktest (mobile SDK conformance) 와 cmd/apismoke (자동 smoke) 와 책임 분리. open/use/pwd/cd/ls/cat/stat/put/get/mv/rm + delta list/show/register/unregister 를 wallet/token 양쪽 지원. 프롬프트에 현재 namespace 표시 (dsh token:0xabc />). 서버는 stateless — cwd 도 client-side

테스트 업데이트

  • TestRequireDFSNamespaceDevOpen (dFS handler) — open 계약
  • TestDeltaRequireNamespaceDevOpen (8) + TestDeltaRegistrarPrincipalGate (4: wallet/user owner, node InvalidArgument, RegisterApp 도 같은 결과 DB hit 없이)
  • TestDeltaHandlerTokenNamespaceLifecycle (DB-backed, 새): EnsureTokenNamespace → manifest seed → RegisterApp → GetApp → ListApps → UnregisterApp → GetApp NotFound
  • TestDeltaHTTPParsePath 에 token namespace 케이스
  • cmd/dsh/main_test.go: splitDFSURI / shell path resolve / open token state / delta list 가 token namespace 에서도 wire 도달 (fake client)
  • make smoke-check: 월렛 패스 뒤에 token 패스 추가 — dfs-write manifest+index → delta-register --namespace token://0xdead… → delta-get → GET /d/token/… → unregister → 404

뎌큐멘트

  • docs/07-api-reference.md: row 12 DeltaService 추가 (4 RPC, dev/test open mode), DeltaService 섹션 신규 작성, /d/ runtime HTTP 엔드포인트 라인, DfsService 섹션의 정책 문구 정독
  • alphea_edge_delta_d_fs_roadmap.md Phase 1G 섹션: cmd/dsh 위치, dKernel 이행 계획, token namespace dev/test open mode 명시

검증

  • go test ./internal/handler ./internal/store ./cmd/apismoke ./cmd/dsh 전부 통과 (fresh DB)
  • go build ./... / go vet ./... 깨끗함
  • make smoke-server exit=0, wallet 패스 + token 패스 모두 정상

commit:server2|25bdf89

phase 2-2 때의 caveat ("Delta 는 아직 wallet-only") 의 해소. dFS + Delta + runtime 세 표면이 이제 같은 dev/test open mode 아래로 일관.

HP Henry Park이(가) 6달 전에 변경 Actions #23

Phase 2-3 완료 — Delta operator surface (Web Console + dShell)

Phase 1~3 의 operator UX 루프 마감. dFS Browser 와 dev/test open Delta 서비스를 운영자 표면으로 이어 준 Delta Apps panel + 브라우저 내 dShell 의 delta 명령 4종.

구조 — 2 표면 일관

client (DeltaService RPC) 는 그대로 자신의 auth 모델 유지. operator 표면 은 admin-trust 어댑터가 store 직접 호출 (Phase 1D 의 console_dfs 패턴 그대로).

신규 파일

  • internal/handler/console_delta.go — List/Show/Register/Unregister adapter. owner_principal_type=wallet, owner_principal_id=namespace_id (wallet 네임스페이스 자기소유, token 은 placeholder — 향후 operator identity 모델이 개정). RuntimeURL precompute 로 UI 는 라우팀 경로 재계산 불필요

수정 파일

  • internal/handler/console_dfs.go — consoleClassify 가 Delta sentinel 까지 포괄. single source of truth
  • internal/handler/webconsole_pages.go — 4개 /monitor/api/delta/* 라우트 (list/show/register/unregister). writeDFSError 재사용 (클래시파이어가 Delta 커버)
  • internal/handler/dsh.go — delta list|show|register|unregister 4개 command map 엔트리. dispatcher 의 longest-match 로 2-word 명령 pick-up. delta register 는 두 입력 셔이프 지원 (합쳐진 URI / namespace+path split). help 에 -- Delta Apps -- 섹션
  • internal/handler/webconsole.go — dShell + dFS Browser 그리드 아래 full-width Delta Apps panel. dfsNamespace 공유, dfsSetNamespaceFromInput / dfsRefresh 이 deltaRefresh 트리거. 모든 행 입력 은 createElement + textContent + addEventListener (Phase 1D-1 XSS 규칙). Open 버튼 은 window.open(runtimeUrl, 'noopener,noreferrer')

Wire 검증 (재빌드 일 container 에서)

  • POST /monitor/api/delta/register → digest = manifest content_hash, runtimeUrl 포함
  • GET /monitor/api/delta/list → 등록된 app
  • GET /d/wallet/0xfacef…/console-app/ → 서빙 성공 (<h1>From Console</h1>)
  • POST /monitor/api/delta/unregister → {"unregistered":true}
  • 리스트 비어짐
  • dsh-exec 경유 4개 명령 전부 구조화된 응답 정상, help 에 섹션 노출

검증

  • go test ./internal/handler ./internal/store ./cmd/apismoke ./cmd/dsh 전부 통과
  • go build ./... / go vet ./... 깨끗함
  • make smoke-server exit=0

commit:server2|837af93

Phase 1~3 PoC 르프 완결 — 등록 (RPC + dsh CLI + Web Console) → 실행 (/d/ runtime) → 운영자 랜딩 (Web Console + 브라우저 dShell) 이 한 렌더링으로 연결됨.

HP Henry Park이(가) 6달 전에 변경 Actions #24

Phase 2-3 review fix — dShell delta SSH-UX + console adapter 경로 정규화

2 finding 모두 반영.

Medium — handleDFSShellLocal 가 delta 명령을 rewrite 안 함

use wallet://... 후 delta list 입력 시 raw 명령 그대로 서버로 전달 → 서버 핸들러가 namespace 인자 요구 → "Usage: delta list ..." 출력. help 와 SSH-like UX framing 이 약속한 "current namespace 에서 바로" 와 정면 충돌.

수정: handleDFSShellLocal 을 delta 명령에도 확장.

  • delta list → delta list <dfsNamespace>
  • delta show <id> → delta show <dfsNamespace> <id>
  • delta unregister <id> → delta unregister <dfsNamespace> <id>
  • delta register (인자 없음) → combined-URI 셔이프 delta register <ns + cwd>
  • delta register <path> → 상대 경로는 cwd 에 해소 후 combined-URI 로
  • 첫 arg 가 이미 완전 URI 면 그대로 (override 안 함)
  • dfsNamespace 미설정이면 in-shell 힌트 출력

Low — consoleRegisterDeltaApp 이 app_path 정규화 안 함

split-form caller 의 appPath="/prj/app/" 가 manifest_path="/prj/app//manifest.json" 으로 돌아갑. store 의 최종 거절은 이루어지나 에러 메시지가 derived 경로를 가리켜 입력자에게 혼란을 줌.

수정: 어댑터에서 store.NormalizeDFSPath(appPath) 를 먼저 호출, 실패는 ErrDeltaAppInvalidRequest 로 wrap. 이제 split-form 이 combined-URI form (ParseDFSURI 경유) 와 동일한 검증 패스를 지나게 됨.

테스트

internal/handler/console_delta_test.go (신규, DB-backed):

  • TestConsoleRegisterDeltaAppNormalisesAppPath — 4 sub:
    • happy path canonical app_path
    • trailing slash 거절 (결정적 assertion: 에러 본문이 "app_path" 를 가리키는지 검증 — 수정 전에는 derived manifest_path 가 나왔음)
    • missing leading slash 거절
    • empty app_path 거절

Wire 검증

  • JS rewrite 시뮬레이션: delta list wallet://0x... → 구조화 리스트, delta show wallet://0x... sshui-app → 세부 조회 정상
  • POST /monitor/api/delta/register body {appPath:"/prj/sshui-app/"} → HTTP 400 + app_path: path must not end with '/' (입력 책임 명확)

commit:server2|32d2345

Phase 2-3 쾨트 SSH-like UX 명서와 욹어는 코드 면을 일치시킨 수정. 동시에 console 어댑터 계층에서 조금 더 명확한 에러 attribution.

HP Henry Park이(가) 6달 전에 변경 Actions #25

Phase 4-1 완료 — capability cookie + Web SDK + /__alphea/fs gateway

AI 생성 앱 샌드박스 모델의 첫 컷. Phase 3 와 같은 /d/… 서빙이지만, HTML 응답 시 앱별 capability 에게제 HttpOnly 쿠키 자동 부여 + Web SDK 자동 주입. SDK 의 alphea.fs.read/write 는 /__alphea/fs/… 로 이어지며 게이트웨이가 3-tier 검증 실행.

결정 (사용자 확정 후 적용)

  • HttpOnly cookie (JS 접근 불가): 앱은 alphea.fs.write 은 호출해도 capability 자체 를 읽거나 외부로 유출할 수 없음
  • Path=/__alphea/ + JWT claims 기반 cross-app 격리 (쿠키 path 는 하나만 가능하므로 /d/… + /__alphea/ 둘 다 커버 불가 → claim 검증으로 대체)
  • app-relative fs paths: alphea.fs.write("/data/x") → <app_path>/data/x. 앱 간 경계 / namespace root 도달 불가
  • GET HTML 에서만 cookie + injection: HEAD / 비-HTML / POST 은 그대로 펴스
  • CSRF claim: JWT 에 runtime_path 담고 게이트웨이가 Origin / Sec-Fetch-Site 대조 — SameSite=Lax 누수가 있을 때 서버워아로 차단

3-tier 검증 (gateway endpoint)

  1. capability JWT 서명 + freshness + typ="capability" (confused-deputy guard)
  2. 요청 path 가 cookie 의 fs_read / fs_write scope 포함 (cheap, 먼저)
  3. dFS 의 manifest 다시 읽어 여전히 같은 scope 선언 (drift 보호)
  4. resolved path = AppPath + appRelPath, NormalizeDFSPath + escape 검증

신규 / 수정

  • internal/store/delta.go — AppManifestPermissions{FSRead[], FSWrite[]} 구조체. PoC glob: "/data/*" 재귀 prefix, exact 경로는 정확 일치, malformed (리딩 / 없음) 조용히 fail-closed
  • internal/handler/capability.go — CapabilityClaims + Issue/Verify (HS256, JWT_SECRET, 30분 기본)
  • internal/handler/capability_gateway.go — /__alphea/{sdk.js,ping,fs/read,fs/write}. Web SDK 자체는 Go const 에 넣어 gateway routes 와 lock-step
  • internal/handler/delta_http.go — DeltaHTTPHandler 이 cfg.JWTSecret 수령, HTML GET 응답에 cookie 발급 + 앞에 SDK script 주입
  • cmd/server/main.go — DeltaHTTPHandler 와 CapabilityGateway 같은 mux 에 같은 secret 으로 등록
  • Makefile smoke — Phase 4-1 패스 추가 (cookie / SDK / in-scope write 실제 dFS 난착 / out-of-scope 403 / cross-site 401)

테스트

internal/handler/capability_test.go — 18 sub-test:

  • JWT round-trip (claims, wrong secret, empty, missing required claims)
  • confused-deputy (typ="user" 토큰 의 capability 검증 실패)
  • scope match invariant (manifest 과 gateway 가 같은 matcher 경유)
  • SDK injection (case-insensitive , no 패스-스루)
  • CSRF claim guard (same-origin/same-site 허용, cross-site 거절, Origin mismatch 거절, Sec-Fetch=none POST 거절/GET 허용)

검증

  • go test ./internal/handler ./internal/store ./cmd/apismoke ./cmd/dsh 전부 통과
  • go build ./... / go vet ./... 깨끗함
  • make smoke-server exit=0, Phase 0–4 한 바퀴
  • Wire 검증 요약: Set-Cookie HttpOnly + Path=/__alphea/ + SameSite=Lax + Max-Age=1800; /__alphea/sdk.js 서빙; /__alphea/ping 클레임 응답; in-scope write 세션 이어서 실제 <app_path>/data/note.json 에 저장; out-of-scope 403; cross-site 401

commit:server2|ba291bb

다음 Phase 4-x 후보

  • alphea.wallet.requestSignature / alphea.pay.request — 명시 승인 dialog 필요, 큰 UX 작업
  • alphea.kv / alphea.auth / alphea.events
  • capability rotation
  • multi-app permission delegation

HP Henry Park이(가) 6달 전에 변경 Actions #26

Phase 4-1 review fix — Referer pin + HS256 정확 고정

2 finding 모두 반영.

High — 같은 origin 의 이웃 앱 끌어올리기로 capability 경계 넘기는 공격

__alphea_cap 은 origin 당 익 하나 — victim app HTML 이 디턜리 는 순간 cookie 가 그게 덩어쓰임. 공격 앱이 victim runtime 을 iframe 로 로드한 뒤 자신 JS 에서 /__alphea/fs/* 호출하면 Sec-Fetch=same-origin + Origin host 몇 개 검증 다 통과 → victim 권한으로 쓰기/읽기 수행.

수정: checkCSRFClaim 이 Referer 존재 강제 + Referer path 가 claims.RuntimePath 의 prefix 인지 확인. 이웃 앱의 JS 가 호출했다면 Referer = /d/.../other-app/... → RuntimePath = /d/.../victim-app/ 과 불일치 → 거절. SDK 의 자체 fetch 는 기본 Referer-Policy 가 same-origin 을 보존하므로 해피 경로 영향 없음. Referer stripping (no-referrer policy) 도 reject — fail-closed.

Medium — 알고리즘 pinning 감소

VerifyCapabilityToken 이 *jwt.SigningMethodHMAC 인터페이스 타입을 수락해 HS384 / HS512 괄제 서명 토큰도 거절 안 됨. pinned-algorithm 계약 원칙 어김.

수정: t.Method != jwt.SigningMethodHS256 (concrete-value 동일성) 으로 교체. HS256 만 수락.

테스트

  • TestCheckCSRFClaim 재작성 — 해피 패스 케이스 전부 Referer 포함, 4개 review-fix 케이스 추가: neighbour-app attack 거절 / missing Referer 거절 / Referer 가 /d/ 트리 외 경로 거절 / nested Referer 수락
  • TestCapabilityRejectsWrongHMACAlg (신규) — HS384/HS512 거절
  • Smoke — happy-path 에 Referer: http://localhost:3001/d/wallet/<addr>/cap-app/ 추가, neighbour-app attack + missing Referer 그드이시 시나리오 추가

검증

  • go test ./internal/handler ./internal/store ./cmd/apismoke ./cmd/dsh 통과
  • make smoke-server exit=0 — 새 negative case 도 포함

commit:server2|f7bc441

Phase 4-1 의 cross-app isolation 약속을 실제로 지키는 수준으로 고정. Phase 4-x 에서 per-tab session id / cookie partition / nonce 기반 더 강한 방어로 증진 가능.

HP Henry Park이(가) 6달 전에 변경 Actions #27

Phase 4-1 후속 정합 — anti-embed 헤더 + scope 정직성

이전 Referer pin 이 sibling-page cookie-overwrite 공격은 막아도 same-origin scripting via window.open 은 그대로 떨어짐이 review 에서 정확히 지적됨. 모든 /d/... 앱이 한 origin 을 공유하므로, victim runtime 에 window handle 을 잡으면 victim.alphea.fs.* 호출이 victim cookie + Referer 로 게이트웨이를 통과.

이번 commit 은 실용 점근:

(a) 가장 싼 벡터 차단 — anti-embed 헤더

Runtime HTML 응답에 X-Frame-Options: DENY + CSP frame-ancestors 'none'. iframe 기반 victim 로딩을 브라우저 단계에서 차단.

(b) 문서된 약속 정직화

  • delta_http.go ServeHTTP 조입으로 전체 공격/방어 매트릭스 명시
  • capability_gateway.go::checkCSRFClaim 주석 재작성 — 먇들고 뭐들 안 막는지 명시
  • capability.go top-level 도해 "real sandbox" 표현 제거, "least-privilege HTTP API" 와 "SCOPE OF THE 4-1 SECURITY GUARANTEES" 섹션으로 교체
  • docs/07-api-reference.md 의 /d/ 행에 cookie/SDK/anti-embed 명시 + /__alphea/{sdk.js,fs/read,fs/write,ping} 4개 엔드포인트 노출 + 보안 범위 callout ("sandbox 아님, Phase 4-2 에서 per-app origin/path")

(c) Smoke 증분

HTML 응답에 X-Frame-Options: DENY 와 CSP frame-ancestors 'none' 둘 다 나타나는지 wire 검증.

아직 안 막는 항목 (Phase 4-2 작업)

  • window.open 으로 victim runtime window 핸들 획득 후 same-origin scripting — 진짜 해결은 per-app origin (subdomain) 또는 per-app cookie/gateway path
  • Service Worker / sub-resource 경로 의 fetch 점유

검증

  • go test ./internal/handler ./internal/store ./cmd/dsh 통과
  • make smoke-server exit=0 (새 X-Frame-Options + CSP grep 추가됨)
  • go build ./... / go vet ./... 깨끗함

commit:server2|93b245a

Phase 4-1 의 약속 과 코드 면이 일치. 다음 Phase 4-2 에서 per-app origin 또는 per-app cookie/gateway path 로 real cross-app isolation 작업.

HP Henry Park이(가) 6달 전에 변경 Actions #28

Phase 2-x 완료 — Delta Enable/Disable toggle + Vega 사용자-facing rename

두 commit 단계

  1. c8b1983 — toggle 기능 추가 (Phase 2-2 deferred 의 마지막 잔여)

    • store.SetDeltaAppEnabled (single tx: delta_apps + root manifest patch 같이 갱신)
    • DeltaService.EnableApp / DisableApp RPC
    • DeltaHandler 2 RPC (registrar-principal gate 안 돌아감 — toggle 은 ownership 이전 아닔)
    • apismoke delta-enable / delta-disable
    • console_delta.consoleSetDeltaAppEnabled + /monitor/api/delta/{enable,disable} POST
    • Web Console panel 의 row 당 toggle 버튼 (상태에 따라 Enable/Disable 전환)
    • TestDeltaEnableDisableToggle (DB-backed) — 어 response Enabled 구조체 + store row 서로 동기화
    • Smoke 에 disable→404 / enable→200 종단간 판단
  2. a8c017a — 사용자-facing rename Delta→Vega (apt-shaped system PM)

    • 제품 레이어 정의:
      • Delta = dFS / registry / store (internal — 코드/proto/schema 모두 그대로)
      • Lyra = npm-shaped developer package manager (manifest authoring/build/publish — 이번 commit 에는 있는 명령 없음)
      • Vega = apt-shaped system package manager (install/remove/enable/disable/list/show)
    • dShell command map: delta register/unregister → vega install/remove, delta enable/disable/list/show → vega enable/disable/list/show
    • Web Console: "📦 Delta Apps" → "📦 Vega Packages", "Register here" 버튼 → "Install here", "Unregister" → "Remove", confirm/toast 메시지 정합
    • cmd/dsh CLI: sh.delta() → sh.vega(), dispatcher case 정합. register/unregister/rm 이 transient alias 로 수락됨 (다음 phase 에 soft 제거 예정)
    • JS rewriter dispatch verb 'delta' → 'vega'
    • help 섹션 -- Delta Apps -- → -- Vega (System Package Manager) --

의도적으로 안 바꾸고 둔 것

  • proto: DeltaService DeltaApp 그대로
  • store: delta_apps SetDeltaAppEnabled RegisterDeltaApp 그대로
  • /monitor/api/delta/* REST 경로
  • apismoke CLI 명령 이름 delta-* (apismoke 는 raw RPC client 이므로 RPC 이름 trackside)
  • /d/<ns>/<id>/<app>/ runtime route

이 경계 덕분에 implementation 계층과 operator UX 계층이 각자 독립 일정으로 진화 가능.

검증

  • go test ./internal/handler ./internal/store ./cmd/dsh 통과
  • make smoke-server exit=0 (Phase 0–4-1 + toggle pass 전부)
  • go build ./... / go vet ./... 깨끗함

commit:server2|c8b1983 commit:server2|a8c017a

Phase 2 의 모든 deferred 항목 완료 (GetManifest read-through 편의 RPC 는 이후 실 필요 시점). Phase 3 의 deferred 는 Phase 3-2 (opt-in wallet-token gate) 만 남은 상태 — Phase 4-1 capability 가 fs API 인증을 가져갑 결과 의미 일부 흡수, 재평가 필요.

HP Henry Park이(가) 6달 전에 변경 Actions #29

Phase 2-x review fix — delta.proto + roadmap 을 Delta/Lyra/Vega layer 정의와 정합

세 개 finding 완결 (commit 8278ef2).

Medium — delta.proto 주석이 1G 이전 코드 설명 유지

제너레이트된 SDK / connect-Go binding package doc 에 그대로 담기는 고 주의. "wallet-token owner-only" / "token namespace returns Unimplemented" / "Enable/Disable intentionally absent" 적혀 있었음.

현재 동작으로 교체:

  • Phase 1G dev/test open mode 명시
  • RegisterApp 의 user|wallet 만 어웰 (node InvalidArgument) registrar gate 명시
  • Enable/Disable 포함, registrar gate 없음 (toggle 은 ownership 이전 아닔)
  • user-facing 표면이 Vega 이고 이 proto 는 internal Delta layer 이라는 점 명시

Medium — roadmap 의 Phase 2/3 단락이 여전히 layer 혼재

Line 1215 영역에서 Lyra 가 register/enable/disable 까지 맡는 식, Line 1355 영역에서 Vega 를 runtime 만으로 표현.

확정된 세 layer 모델로 정제:

  • Delta = filesystem/registry/data (internal: dFS, delta_apps, /d/, /__alphea/, apismoke delta-*)
  • Lyra = developer-side PM (npm-shaped: init/build/publish, manifest authoring + dFS push)
  • Vega = system-side PM + runtime (apt-shaped: install/remove/enable/disable/list/show + installed enabled 패키지 서빙)

Phase 2 섹션: 세 layer 를 명시적으로 정의하고 sample flow 로 developer/operator/visitor 구분. Phase 3 섹션: Vega 가 system PM + runtime 을 하나의 product 이름 아래에 묶는다는 점 명시, internal /d/ path-name 은 Delta layer 로 유지.

Low — /dsh 바이너리 untracked 반복

go build ./cmd/dsh 의 산출물. .gitignore 에 /dsh 추가 (기존 /apismoke /sdktest /server /migrate 와 나란히).

외부 reference PDF (ALPHEA Package Manager Lyra and Vega Specification v0.1.pdf) 는 사용자 제어 영역이라 그대로 둡 — 도입 시점은 사용자가 결정.

검증

  • go test ./internal/handler ./internal/store ./cmd/dsh 통과
  • go build ./... / go vet ./... 깨끗함
  • make proto regenerate 시 gen 디렉터리 diff 없음 (service-level proto 주석은 .connect.go 에 전파 안 됨 — proto file 자체의 명확성이 목적)

commit:server2|8278ef2

HP Henry Park이(가) 6달 전에 변경 Actions #30

Phase 0.7 SDK + sdktest 확장 + #444 게이트 기본값 (commit f5ba076)

사용자가 작업한 SDK/CLI 확장 + 4개 review finding 중 3개를 한 묶음으로 담음. 깨진 4-section UI 는 이전 valid 커밋으로 revert.

사용자 작업 (그대로 들어감)

  • sdk/auth.go — WalletChallenge / WalletVerify / WalletLogin (로컬 secp256k1 서명)
  • sdk/dfs.go 신규 — ResolvePath / PutFile / GetFile / StatFile / ListDirectory / MoveFile / DeleteFile typed wrapper
  • sdk/delta.go 신규 — RegisterApp / UnregisterApp / GetApp / ListApps / EnableApp / DisableApp
  • sdk/call.go — NodeAgent.CallProto (protobuf in/out, JSON 변환 없는 hot path)
  • cmd/sdktest/main.go — wallet-login, dfs, delta subcommand 작업
  • webconsole_test_client_gotrue.go — #444 gate 체크 추가

리뷰 fix

  • High 2 (운영 OFF 기본값): config.go 의 MonitorTestClientEnabled 기본값 true → false. dev 은 .env.sdktest 에 MONITOR_TEST_CLIENT_ENABLED=true 명시 추가. 이제 절대 운영 배포에서 GoTrue proxy 가 자동 노출되지 않음
  • Medium 2 (sdktest CLI nested + Vega rename):
    • sdktest wallet-login → sdktest wallet login
    • sdktest node-register → sdktest node register
    • sdktest delta <…> → sdktest vega <…> + register/unregister → install/remove (apt-shaped). 내부적으로는 여전히 DeltaService 을 호출
    • cmdDelta 함수도 cmdVega 로 rename
    • Makefile smoke 의 sdktest node-register → sdktest node register
  • High 1 (깨진 4-section UI — revert): webconsole_test_client.go 를 e1ab575 (이전 valid 상태) 로 되돌림. 4-section refactor 이 <script> 안에 ... placeholder 로 JS syntax error + V1 자재 script/HTML 이 남아 페이지 자체가 안 될 수준. 사용자가 4-section UI 를 재작성 예정
  • Medium 1 (CDN ethers.js) — 4-section revert 로 어차피 함께 제거됨. 재작성 시 CDN 아닌 vendored signing primitive 사용 권장 (private key 입력 페이지와 같은 JS context 에 remote script 🚫)

검증

  • go test ./internal/handler ./internal/store ./cmd/sdktest ./sdk ./cmd/dsh 통과
  • go build ./... / go vet ./... 깨끗함
  • make smoke-server exit=0 (nested CLI 경유)

후속

  • 사용자: webconsole_test_client.go 의 4-section UI 재작성
  • 일괴: node register --auth wallet 이 sdk.AuthenticateWithGoTrueToken 를 wallet token 용으로 재사용 — 함수/타입명 일반화 (후속 pass)

commit:server2|f5ba076

HP Henry Park이(가) 6달 전에 변경 Actions #31

sdktest 명명 cleanup (commit 90f4d54)

이전 rename pass (f5ba076) 의 2개 Low 잔여 문구 정리.

  • cmd/sdktest/README.md — Quick Start 을 3개 labeled block 으로 재구성: host-app (signup→node register→connect), headless wallet (keygen→wallet login→node register --auth wallet→connect), dFS+Vega round-trip (dfs put→vega install→vega list→vega disable→vega remove). 이제 사용자가 copy-paste 으로 바로 그대로 동작하는 형태
  • cmd/sdktest/main.go — flagset display name + user-facing 에러 메시지의 남은 평탄 이름 제거:
    • newFlagSet("wallet-login") → "wallet login"
    • newFlagSet("node-register") → "node register"
    • "no wallet token in state; run wallet-login first" → "… run "sdktest wallet login" first"
    • "node id/token missing; run node-register first" → "… run "sdktest node register" first"

동작 변경 없음. grep -rn 'wallet-login\|node-register' cmd/sdktest/ 이 0 hit.

검증

  • go build ./... / go vet ./... 깨끗함
  • make smoke-server exit=0

commit:server2|90f4d54

HP Henry Park이(가) 6달 전에 변경 Actions #32

dshai 첫 컷 (commit 20a2b44)

AI 실행 표면 첫 단계. dShell (cmd/dsh, 사람) 과 dShellAI (cmd/dshai, JSON IO) 가 하나의 dshcore 위에서 동작. error code / URI 세맨틱 / Delta/Vega 레이어 분리 가 두 표면에서 일관.

신규 구조

  • internal/dshcore/ 신규 패키지
    • client.go — bearer token 이 운영 REST + gRPC 둘 다 타고 감
    • errors.go — 7 code (not_found/permission_denied/invalid_argument/cas_mismatch/conflict/unavailable/internal) + Retryable. ConnectRPC code / HTTP status / store sentinel → 표준 Code 매핑. 412 + "cas"/"if_match" wording 은 CodeCASMismatch 로 승격 — AI 재시도 정책 honest 유지
    • uri.go — SplitDFSURI / CleanShellPath / JoinPath / BuildURI / ResolveTarget (client-side 단일 소스)
    • dfs.go — 7 ops (Resolve/List/Stat/Read/Write/Move/Delete). Resolve 는 DfsService.ResolvePath gRPC 로 권위있게. 나머지는 운영 REST
    • vega.go — 6 ops (List/Show/Install/Remove/Enable/Disable) via DeltaServiceClient gRPC. RuntimeURL pre-rendered
  • cmd/dshai/ 신규 바이너리
    • line-delimited JSON stdin/stdout
    • 13 명령 dispatch
    • bearer token + server URL 은 sdktest state file 공유 (read-only — dshai 가 세션 rotate 안 함)
    • Vega 명젝은 일괄 uri arg 사용, 서버 ResolvePath 경유로 split
    • malformed JSON line 은 invalid_argument 응답 + 루프 유지 (agent 복구 가능)

테스트

  • internal/dshcore/uri_test.go — 18 sub: schemes / cwd math / namespace 조합
  • internal/dshcore/errors_test.go — 41 sub: ConnectRPC 매핑 최대, HTTP status 매핑, store sentinel, IsRetryable wrapped, CAS 의 특별 승격

Smoke 증분

Phase 4-1 cap-app cleanup 뒤에 dshai pass 추가:

  • 8 JSON command (manifest+index write → stat → vega.install → list → disable → enable → remove) 전부 ok:true
  • response 라인 수 = 8 정확
  • malformed not-json\n → invalid_argument 응답
  • exit=0

이번 commit 에 안 들어간 것 (추천 순서대로 보류)

  • cmd/dsh 를 dshcore 위로 재배선 — dshcore 표면 안정 확인 후
  • Web Console Test Client 4-section 재작성
  • sdk.AuthenticateWithGoTrueToken 이름 일반화
  • Phase 4-2 per-app isolation

commit:server2|20a2b44

HP Henry Park이(가) 6달 전에 변경 Actions #33

dshai review fixes (commit 09b4091)

3 finding 첫 컷의 구조적 수정.

High — dshai 가 wallet token 우회 가능했던 점 제거

DFSList/Stat/Read/Write/Move/Delete 가 /monitor/api/dfs/* operator-trust REST 을 타서 토큰 없이도 mutation 가능. AI 표면의 capability 이야기가 bearer 강제 공입을 전제로 하구조면 안 됨.

수정: 6개 op 모두 c.dfs (DfsService gRPC) 로 이전. dshcore.Client 의 REST plumbing (getJSON/postJSON/decodeHTTP + http 필드) 제거. cfg.Token 없으면 dshai 가 아예 시작 거절 (명확한 에러 메시지). docstring 에 "no operator-trust REST escape from this binary" 명시.

Medium — dfs.write ifMatch wire 와이어 연결

roadmap + cas_mismatch 표준 다 ifMatch 전제인데 dispatcher 가 떨어뜨린 상태. PutFile 의 if_match 필드는 Phase 1B-3 부터 있었음.

수정: DFSWrite(ctx, uri, mime, body, ifMatch string). dshai 의 dfs.write args 에 선택 ifMatch 필드. stale hash 는 기존 412 → CodeCASMismatch 패스 그대로. smoke 가 round-trip 검증 — 첫 write 후 manifestHash H1, 잘못된 hash (sha256:0∦0) 보내면 cas_mismatch, H1 다시 보내면 성공 + version bump.

Medium — dfs.copy 추가 (roadmap 약속)

서버-side native copy 없으니 client-side read+write 조합. 기본 mime 은 source meta 의 mime, 선택 ifMatch 는 dst 가드. cross-namespace copy 는 지원 (Move 는 거절하는 것과 달리).

수정: DFSCopy(ctx, srcURI, dstURI, mime, ifMatch) 추가, dshai dispatcher 에 dfs.copy case. smoke 가 copy + stat round-trip 검증.

dshai 이제 14 명령 지원

dfs.{resolve,ls,stat,read,write,copy,move,delete} (8) + vega.{list,show,install,remove,enable,disable} (6) = 14.

검증

  • go test ./internal/dshcore ./internal/handler ./internal/store ./cmd/sdktest ./cmd/dsh ./sdk 통과
  • make smoke-server exit=0 — dshai 의 ifMatch CAS 어 둔 round-trip + dfs.copy + tokenless 거절 + malformed-line 복구 전부 wire 검증
  • go build ./... / go vet ./... 깨끗함

commit:server2|09b4091

HP Henry Park이(가) 6달 전에 변경 Actions #34

dshai review cleanup (commit 299f2f4)

3 Low finding 사이클 종료.

  • client.go Config + authTransport 주석 정리 — "Token may be empty / operator-trust REST routes" 표현 제거. Token 은 REQUIRED, operator-REST fallback 금지 명시. authTransport 의 empty-token 패스-스루는 belt-and-suspenders 로 남겨되, 도달하면 config 버그로 명시.
  • uri.go ResolveTarget 에러 메시지 — "open one first" 제거, "no namespace bound; pass a full URI, or start dsh with a namespace URI" 로 교체. SSH-like 진입 방향과 일치.
  • cmd/dshai/main_test.go 신규 — JSON-IO 계약 테이블 테스트 8개 + response shape 테스트 2개:
    • malformed JSON → invalid_argument + null id
    • missing command → id 보존
    • unknown command → 명령명 echoed
    • dfs.write missing content/contentBase64 거절
    • dfs.write malformed base64 거절
    • dfs.copy missing srcUri / dstUri 각각
    • malformed args object on known command
    • ok 응답은 error 생략, error 응답은 result/id(빈 때) 생략
      모두 dispatcher{client: nil} 로 실행 — client 호출 전 분기만 테스트, real server 불필요.

검증

  • go test ./cmd/dshai ./internal/dshcore ./internal/handler ./internal/store ./cmd/sdktest ./cmd/dsh ./sdk 전부 통과
  • go build ./... / go vet ./... 깨끗함
  • smoke 은 이번 변경 안함 (변동 없음)

commit:server2|299f2f4

HP Henry Park이(가) 6달 전에 변경 Actions #35

cmd/dsh 을 dshcore 위로 재배선 완료 (commit 581dac6)

Deferred queue 1번. dshai 가 이미 internal/dshcore 위에서 동작하는데 cmd/dsh 만 자체 plumbing (REST http.Client + 별도 DeltaServiceClient + URI helpers + authTransport) 을 유지하고 있었음. 두 셔에 같은 core 를 올려서 에러 코드 / URI 세맨틱 / 엔드포인트 선택 이 한 곳에서 결정.

핵심 변경

  • shell struct: http *http.Client + deltaClient alpheavlconnect.DeltaServiceClient 제거, core *dshcore.Client 추가
  • dfsEntry: type dfsEntry = dshcore.DFSEntry alias 로
  • list/read/statMeta/write/delete/move 구현이 dshcore 호출 원라이너로 축소
  • vega(...) 스위치 본문이 dshcore.Vega* 호출로
  • splitDFSURI / isDFSURI / cleanShellPath 제거 → dshcore.SplitDFSURI / dshcore.IsDFSURI / dshcore.JoinPath 사용
  • getJSON / postJSON / decodeHTTP / newHTTPClient / authTransport 전부 제거 (REST plumbing 이 필요 없어짐)
  • login 만 unauthenticated http.DefaultClient 으로 WalletChallenge/Verify — login 이 token 을 획득하는 과정이라 당연. 이후 newCore(cfg) 로 dshcore.Client 재생성

동작 변경

이제 cmd/dsh 의 ls/cat/stat/put/get/rm/mv 도 wallet token 필수 (이전에 REST 경로는 토큰 없이 admin trust 로 동작). vega.* 는 이미 토큰 강제했으므로, 이번에 dShell 의 모든 명령에 토큰 요구가 일관됨.

테스트 정합

  • TestSplitDFSURI 제거 — 규칙은 dshcore/uri_test.go 의 더 풍부한 table-test가 소유
  • TestVegaListAllowsTokenNamespace + fakeDeltaClient 제거 — dshcore 가 Connect client 을 interface 로 노출하지 않고 입주하면 과도 설계. Wire 검증은 smoke 의 vega 패스가 이미 컴버
  • TestShellPathState + TestOpenPersistsNamespaceState 유지 — cmd/dsh 고유 state 동작 (cwd math + SSH-like entry 영구화)

검증

  • go test ./internal/dshcore ./cmd/dshai ./cmd/dsh ./internal/handler ./internal/store ./cmd/sdktest ./sdk 통과
  • go build ./... / go vet ./... 깨끗함
  • make smoke-server exit=0

diff 요약

cmd/dsh/main.go: +112 / -313 (절반 이상 제거). cmd/dsh/main_test.go: 단순화.

commit:server2|581dac6

다음 deferred 순서대로

  1. Web Console Test Client 4-section 재작성
  2. sdk.AuthenticateWithGoTrueToken 이름 일반화
  3. Phase 4-2 per-app isolation

HP Henry Park이(가) 6달 전에 변경 Actions #36

cmd/dsh review fix — cross-namespace reject + SSH-like 정합 + dshai scanner (commit 2040102)

3개 finding 완결.

High — cmd/dsh 가 cross-namespace full URI 를 조용히 rewrite 하던 문제

resolvePath 가 입력된 full URI 를 path 만 추출해 반환하고, caller 가 sh.uri() 로 현재 namespace 를 다시 붙임. dsh wallet://0xabc/ 상태에서 cat token://0xdef/x 치면 실제로는 wallet://0xabc/x 를 읽음. mv/rm/get/stat 동일. main_test.go 가 이 잘못된 동작을 pin 하던 상태.

수정: resolvePath 을 (string, error) 로 변경. 현재 namespace 와 다른 full URI 는 "cannot reach %s://%s while attached to %s://%s" 로 거절. 같은 namespace 는 case-insensitive 로 매칭. 아무것도 attached 안 된 상태의 one-shot 호출은 path passthrough 하되 namespace 를 session 으로 승격 안 함. exec switch caller 전부 error 처리 추가.

Medium — open/use + state file persistence 가 SSH-like 모델과 서로 근

로드맵: "Phase 2: namespace 는 프로세스 시작 인자로만 정하며 다음 실행까지 자동 저장하지 않는다". 구현: open/use interactive 명령 있음, namespace+cwd 가 state file 에 저장됨, 다음 invocation 이 자동 resume.

수정:

  • state struct 에서 DshNamespaceType/DshNamespaceID/DshCwd 제거 (이전 file 에 남은 해당 키는 json.Unmarshal 이 무시)
  • shell struct 에 session-scoped namespace dshcore.Namespace + cwd string 필드 (in-memory only)
  • run() 의 첫 인자가 dFS URI 일 때만 새 attach(uri) 호출 — state file 안 건들임
  • exec switch 에서 open/use case 제거, help 텍스트 에서 언급 제거 + "Restart dsh with a different URI to switch namespaces" 명시
  • cd 이 sh.cwd 만 갱신, sh.save() 호출 제거
  • save() 은 durable identity (server addr + wallet token bits + private key) 만 저장 — comment 에 "session-scoped fields not persisted" 명시

Low — dshai stdin scanner cap 에서 base64 overhead 미반영

8 MiB 가 dFS 의 decoded 천장, base64 는 ~33% 오버헤드 + JSON 포림. 8 MiB 파일이 이미 깨질 수 있음.

수정: 16 MiB 로 올림. 주석에 "~33% overhead" + "streaming decoder 는 대용량 API 시점" 명시.

테스트 괴신

  • TestShellPathState cross-namespace REJECT case + case-insensitive id match
  • TestResolvePathWithoutAttachedNamespace one-shot passthrough 규칙
  • TestAttachIsSessionOnly 가 이전 TestOpenPersistsNamespaceState 를 대체 — session-only 계약 고정

검증

  • go test ./internal/dshcore ./cmd/dshai ./cmd/dsh ./internal/handler ./internal/store ./sdk 전부 통과
  • go build ./... / go vet ./... 깨끗함
  • make smoke-server exit=0

commit:server2|2040102

HP Henry Park이(가) 6달 전에 변경 Actions #37

cmd/dsh review follow-up (commit 146f69d)

2 finding 완결.

Medium — unattached one-shot full URI 는 실제로 동작 안 했음

resolvePath 의 unattached branch 가 full URI 를 통과시키는 코드 있었으나, 실제 command 는 requireOpen() 가 먼저 돌아서 "not connected" 로 막음. 테스트는 resolvePath 만 보고 이 경로를 놓침. 로드맵의 공식 UX 는 dsh wallet://0x.../ ls /path 형태.

수정: dead one-shot branch 제거. resolvePath 도 namespace 없으면 "not connected; restart dsh with a wallet://0x.../ or token://0x.../ argument" 로 거절. 실패 메시지가 command path 와 unit test path 둘 다 일관. TestResolvePathWithoutAttachedNamespace 는 이제 reject 을 검증 (계약이 되돌려 있엄).

Low — shell 주석 이 login 동작과 괴리

"namespace = bound by the first positional arg or by login" 이라 되어 있었으나 실제 login() 은 wallet token 만 갱신하고 sh.namespace 는 건들지 않음. 로그인 후에도 ls 가 "not connected".

수정: 주석이 "FIRST POSITIONAL ARG ONLY" + "SSH-like entry rule" 명시. login 후 auto-attach UX 가 필요하면 그때 명시적으로 추가 (주석에도 그 노트 남김).

검증

  • go test ./cmd/dsh ./internal/dshcore ./cmd/dshai ./internal/handler ./internal/store ./sdk 통과
  • go build ./... / go vet ./... 깨끗함
  • make smoke-server exit=0

commit:server2|146f69d

HP Henry Park이(가) 6달 전에 변경 Actions #38

SDK auth 일반화 + 로드맵 진행 상태 표시

feat/dfs-poc 에 세 커밋 추가. dshai 첫 컷 commit 본문에 적어둔 deferred 큐 중 "sdk.AuthenticateWithGoTrueToken name generalisation" 항목을 마무리하고, 그 김에 로드맵 자체에 단계별 진행 상태가 보이게 정리했다.

1. SDK 진입점 owner-agnostic Bearer 로 일반화

commit:server2|24f4b8b

서버 RegisterNode 는 한참 전부터 user JWT + wallet JWT 두 owner principal 다 받고 있었고, sdktest node register --auth user|wallet 도 같은 SDK 함수에 wallet token 을 흘려넣고 있었지만 SDK 진입점 이름은 여전히 AuthenticateWithGoTrueToken / GoTrueAuthConfig / AuthResult.GoTrueToken 으로 "GoTrue 전용" 인 척하던 상태. backward-compat alias 없이 깨끗하게 rename.

Go

  • GoTrueAuthConfig → BearerAuthConfig (필드 GoTrueToken → BearerToken)
  • AuthenticateWithGoTrueToken → AuthenticateWithBearerToken
  • AuthResult.GoTrueToken → AuthResult.BearerToken (JSON tag gotrueToken → bearerToken)
  • NodeConfig.UserJWT → NodeConfig.BearerToken
  • NodeAgent.UpdateUserJWT → NodeAgent.UpdateBearerToken
  • tokenManager.userJWT / updateUserJWT 도 동일 rename, refresh path 의 "GoTrue JWT 필수" 메시지·주석 일반화

Mobile (gomobile)

  • Mobile.AuthenticateWithBearerToken(serverAddr, bearerToken, ...)
  • agent.UpdateBearerToken(token)

클라이언트 도구

  • cmd/sdktest node register 가 sdk.AuthenticateWithBearerToken(BearerAuthConfig{BearerToken: token}) 로 호출 (--auth user|wallet 분기는 그대로)

문서

  • docs/01-auth.md — SDK 헬퍼 섹션이 AuthenticateWithBearerToken + sdk.WalletLogin 같이 노출
  • docs/06-mobile-sdk.md — 인증 flow 가 step 1 에서 사람 owner(GoTrue) / 헤드리스 owner(wallet JWT) 두 갈래로 분기, Kotlin/Swift 예제 모두 Mobile.authenticateWithBearerToken / MobileAuthenticateWithBearerToken + agent.updateBearerToken 로 갱신, AuthResult JSON 필드도 bearerToken
  • docs/10-deployment.md 인증 흐름 요약 — step 1 두 갈래 + step 3 결과를 (owner_principal_type, owner_principal_id) 모델로 정확하게
  • PROJECT_STATUS.md §4.1 Phase 0 / §4.1 Phase 0.7 RegisterNode 확장 bullet / §5 SDK auth 행 모두 동일 정합

동작 변경: 없음. RPC 도 헤더도 동일, Go/gomobile symbol 이름만 변경.

검증: go build ./... / go vet ./... / go test ./sdk pass. repo-wide grep 으로 AuthenticateWithGoTrueToken|GoTrueAuthConfig|GoTrueToken|UserJWT|UpdateUserJWT 0 hits (md+go).

2. 로드맵 단계별 진행 상태 명시

commit:server2|52b65cd

Phase 1F 가 이미 — **완료** suffix + 본문 ✅ 인용문 패턴을 쓰고 있어서 그걸 다른 단계에도 일관 적용. 추가로 §6 전체 단계 표에 상태 컬럼 (✅/🟡/⬜) + 핵심 commit hash 인용을 넣어서, 로드맵만 열어보면 "지금까지 뭐가 끝났고 뭐가 남았는지" 가 바로 보이게.

  • ✅ Phase 0 (SDK Auth 복구 + Bearer 일반화), Phase 1 PoC 전체 (1A schema → 1F dShell rename → 1G dev open mode + cmd/dsh + cmd/dshai)
  • 🟡 Phase 0.7 (Principal 모델 + WalletChallenge/Verify + sdktest wallet 흐름 완료 / user_wallets + secp256k1 강화 후속), Phase 1G (CLI PoC 완료 / dKernel control plane 후속), Phase 2 (Delta Registry + Web Console + dShell + Enable/Disable + Vega rename 완료 / Lyra dev 명령 후속), Phase 3 (Phase 3-1 http-static runtime 완료 / 3-2 auth gating 후속), Phase 4 (Phase 4-1 capability cookie + Web SDK + /__alphea/fs 완료 / 4-2 per-app isolation + payments 후속)
  • ⬜ Phase 0.5, 0.8, 4.5, 4.7, 5~11

3. 렌더링된 HTML viewer 동기화

commit:server2|5a57a5e

alphea_edge_delta_d_fs_roadmap.html 의 <script id="md-src" type="text/template"> 블록이 8d278a1 / 24f4b8b / 52b65cd 이후 stale 상태였음. embed 된 markdown 만 갱신, HTML scaffolding / CSS / marked·highlight.js / 테마 토글은 그대로. 이제 HTML 열어도 §6 진행 표 + ✅/🟡/⬜ 헤딩 + RegisterNode owner-principal 표가 동일하게 보임.

Next deferred (dshai 큐 갱신)

첫 컷 commit 본문의 deferred 4종 중 cmd/dsh 재배선 (581dac6) + SDK 일반화 (24f4b8b) 두 항목이 끝났고, 남은 항목:

  • Web Console Test Client 4-section 재작성
  • Phase 4-2 per-app origin/path isolation

이번 commit 들로 Phase 1 dFS PoC 자체의 scope 가 바뀌지는 않음 — 이슈 상태는 그대로.

HP Henry Park이(가) 6달 전에 변경 Actions #39

Web Console Test Client 4-section 재작성 + outside-sign 흐름

commit:server2|5754531

dshai 첫 컷의 deferred 큐 중 세 번째 항목. 로드맵 §Phase 0.7 의 "Reference clients (둘 다)" 설계 대로 webconsole_test_client 을 사람 흐름 ↔ headless wallet 흐름을 한 화면에서 비교 검증할 수 있는 4-section UI 로 완전히 재작성. 핵심 아이디어: 페이지는 challenge / verify / token 표면만 달고 서명은 outside-the-page (cmd/sdktest) 에서 한다 — 브라우저에도 서버에도 private key 가 어디에도 안 들어감.

Sections

Section A · User (GoTrue): signup / login / refresh / signout. 기존 구현 유지하되 모든 DOM 갱신 textContent 경유로 정리.

Section B · Wallet Identity (headless): wallet address + purpose 입력 → WalletChallenge → challenge + payload 표시 → 세션이 outside 에서 sign 해서 획득한 signature 를 textarea 에 붙여넣음 → WalletVerify → wallet JWT 획득. 안내문에 go run ./cmd/sdktest wallet sign --challenge <hex> 는 명시.

Section C · Node Register: --auth user|wallet 선택에 따라 GoTrue user JWT 또는 wallet JWT 를 bearer 로 쓰는 RegisterNode. nodeID 는 rn_<short>[_<deviceID>] 알고리즘을 JS 에서 동일하게 수행해 read-only 값으로 표시 — 운영자가 challenge 가 어떤 nodeID 에 bound 되는지 눈으로 확인. Section B 와 같은 outside-sign 규칙.

Section D · Smoke Calls: Health / Info / GetNode / GetNodes / DfsService.ResolvePath / DfsService.ListDirectory. bearer 선택 우선순위 — node-bound RPC 는 node token 먼저, owner-bound RPC 는 wallet token > GoTrue user JWT 순.

cmd/sdktest 신규 명령

sdktest wallet sign --challenge <hex> — state file 의 private key 로 challenge sign. output 은 {wallet, challenge, signature} JSON. 동반해서 sdk.SignChallenge(privateKeyHex, challenge) export (구현체는 기존 internal signChallenge). 내부 함수를 export 로 끌어올린 것 외에는 SDK 입장에서 변화 없음.

보안 규칙 적용 결과

  • CDN / remote script 0 — CSS 와 JS 가 모두 binary 안에 inline
  • 브라우저 private key 입력 0 — 서명은 outside
  • 서버 private key 수신 0 — sdktest 가 로컬에서 sign
  • DOM API only — 모든 갱신이 textContent / createElement / appendChild / addEventListener (innerHTML 문자열 조립 0, dFS Browser 1D-1 패턴 a8913d0 계승)
  • MonitorTestClientEnabled 게이트 — false (운영 기본) 시 /monitor/test-client = 404, /monitor/api/test-client/log = 403. GoTrue proxy 와 동일 패턴

마지막 하더닝 3건 (review findings 내장)

  • /monitor/api/test-client/log 에 http.MaxBytesReader 64KB 추가 (GoTrue proxy 와 동일 제한)
  • webconsole_test_client_gotrue.go 상단 주석을 현재 상태에 맞게 재작성 (이전의 "public route / TODO #444" 은 f5ba076 이후 stale)
  • cmd/sdktest 의 wallet 주석이 존재 안 하는 --key <hex> 를 언급하던 부분을 현재의 keygen → login | sign 안내로 정리

다음 deferred

남은 큐 (memory 기준):

  • Phase 4-2 per-app origin/path isolation — Phase 4-1 의 "sandbox 아님" 한계 해소
  • Phase 0.8 dKernel Agent Identity Session — control plane 장기작

Web Console Test Client 가 도구로 있어서 이 두 작업 진입 시 검증 기반이 훨씬 좋아짐.

HP Henry Park이(가) 6달 전에 변경 Actions #40

  • 상태을(를) 진행에서 해결(으)로 변경되었습니다.

Phase 1 dFS PoC 종합 마감

이 이슈는 원래 Phase 1 dFS PoC 를 추적하기 위해 열렸고, 그 과정에서 자연스럽게 이어진 PoC 계열 작업들이 모두 여기에 누적되었다. 더 이상 한 이슈에 못아넣으면 추적성이 무너지므로, 현 시점까지 끝난 작업을 정리하고 이슈를 닫는다. 이후 작업은 축별로 새 이슈를 엽어서 진행한다.

누적 완료 항목 (commits 수시순으로)

Phase 1 dFS PoC 자체 — wallet/token namespace + 파일 CRUD + Root Manifest + VfsService 폐기:

  • 1A Core Schema — commit:server2|8e9a6f1, commit:server2|96dac84, commit:server2|21f1fcc
  • 1B-1 proto + path normalize — commit:server2|6a8569b
  • 1B-2 store File API — commit:server2|76286c0
  • 1B-3 handler + apismoke dfs-* — commit:server2|6c8c9c8, commit:server2|4e0dfd9
  • 1C VfsService 폐기 — commit:server2|a2f34ff
  • 1D-1 Console FS Migration read — commit:server2|b581444, XSS fix commit:server2|a8913d0
  • 1D-2 Console FS Migration write — commit:server2|fe0b728, non-ASCII fix commit:server2|35c07b0
  • 1F AIS → dShell rename — commit:server2|4909f28
  • 1G dev/test open mode + cmd/dsh — commit:server2|25bdf89

Phase 2 Delta Registry / Vega operator surface (연쇄):

  • 2-1 store + schema v20 — commit:server2|48b17a0, review fix commit:server2|5ad62ea
  • 2-2 DeltaService 4 RPC + apismoke — commit:server2|ec05ec0, review fix commit:server2|65475f0
  • 2-3 Web Console + dShell + namespace UX — commit:server2|837af93, commit:server2|32d2345
  • 2-x Enable/Disable + Vega operator rename — commit:server2|c8b1983, commit:server2|a8c017a

Phase 3 dFS Runtime:

  • 3-1 http-static runtime adapter at /d/... — commit:server2|40b1860, drift fail-closed commit:server2|1206e0e

Phase 4 Web SDK / Gateway API:

  • 4-1 capability cookie + Web SDK + /__alphea/fs gateway — commit:server2|ba291bb, HS256 + Referer commit:server2|f7bc441, anti-embed + cross-app scope commit:server2|93b245a, doc reorder commit:server2|feb61c5

공용 인프라 / dshai / SDK 일반화 / 검증 도구:

  • dshai 첫 컷 + dshcore 공유 코어 — commit:server2|20a2b44, 보안·CAS·copy 보강 commit:server2|09b4091, dshcore 테스트 commit:server2|299f2f4
  • cmd/dsh → dshcore 재배선 + dKernel-backed shell target PoC — commit:server2|581dac6, follow-ups commit:server2|2040102, commit:server2|146f69d
  • SDK auth GoTrue → Bearer 일반화 — commit:server2|24f4b8b
  • 로드맵 단계별 진행 상태 표시 — commit:server2|52b65cd, HTML 동기화 commit:server2|5a57a5e
  • Web Console Test Client 4-section 재작성 + cmd/sdktest wallet sign outside-the-page signer — commit:server2|5754531

dFS Storage Evolution 결정 (코드 아닌 디자인 결정):

  • Stage 1 (DB inline BYTEA, 현재) → Stage 2 (Edge object store + db:inline 공존 + 암호화 metadata 자리) → Stage 3 (Node distribution/cache) → Stage 4 (분산 dFS, chunk/CID/lock/QUIC — 로드맵 Phase 10 에 해당). Stage 2 schema migration 시 암호화 metadata 전체 해제 neutral 값으로 설치.

이후 추적 계획

PoC 다음 큐는 축별로 분리:

  • Phase 0.8 — dKernel Agent Identity Session (새 이슈 생성)
  • Phase 4-2 — Vega per-app origin/path isolation (새 이슈 생성)

이 이슈는 PoC 그 자체에 대한 히스토리로 남겨고, 이후 commits 는 각자의 축별 이슈를 refs 한다.

Actions

내보내기 PDF Atom