이슈 #451
진행중이슈 #448: Phase 0.8 — dKernel Agent Identity Session
Phase 0.8-2 — command_audit 스키마 + dshcore audit hook
0%
설명
범위¶
gap-report.md §6, §7 Option B, §9. 구조화된 audit 테이블 — 현재 access_logs 이 free-form details 240-char 이라 AI agent 도 운영자도 replay 불가능한 증상 해소.
작업¶
-
command_audit테이블 신규 —id,source,principal_type,principal_id,command,namespace,args_hash,result_hash,error_code,status,latency_ms,agent_session_id(0.8-3 일찍 남겨둔 컬럼),created_at - v22 마이그레이션 (
internal/store/migrations.go) -
internal/dshcore/client.go에 per-command audit hook — command 이름과 args hash 기록 -
internal/server/interceptors.go의AccessLogInterceptor를 분리 또는 업그레이드 — RPC 레벨 기록은command_audit으로 -
error_code는dshcore.Code장건 (not_found / permission_denied / invalid_argument / cas_mismatch / conflict / unavailable / internal) — ConnectRPC code 는 다른 컬럼 또는 details 로 - handler·store 테스트 + sdktest agent-conformance 명령 확장 (gap-report §10 참고)
DoD¶
-
command_audit테이블 + v22 마이그레이션 - dshcore client.go 에 audit hook — 매 command 당 행 1개 기록 (transport 실패 처리 규칙 명시)
- RPC 레벨 audit (DfsService, DeltaService, AuthService …) 가 같은 테이블로 안내짐
-
access_logs는 넘어가지 않음 (공존 또는 명시적 deprecation) - store + handler tests
-
smoke 에
command_audit행 검증 단계 추가
관련¶
HP Henry Park이(가) 5달 전에 변경
- 상태을(를) 신규에서 해결(으)로 변경되었습니다.
0.8-2 command_audit 구현 완료¶
commit:server2|6ee2aa3
Phase 0.8 두 번째 슬라이스 마감. AI replay / audit 소비자가 RPC 단위로 fingerprint + agent intent 를 읽을 수 있는 구조 완성.
구현¶
v21 마이그레이션 + command_audit 테이블
- columns: id, surface, command, rpc_procedure, request_id, principal_*, namespace, args_hash, result_hash, error_code, error_message, status (CHECK ok|error), latency_ms, agent_session_id, created_at
- 3 인덱스: principal lookup / surface dashboard / agent_session_id partial WHERE NOT NULL
- agent_session_id 은 0.8-3 까지 NULL
- access_logs 는 그대로 — 다른 소비자, 다른 표면
Server-side CommandAuditInterceptor
-
AccessLogInterceptor과 peer로 구동 -
proto.MarshalOptions{Deterministic: true}.Marshal()결과 바이트 → sha256 →sha256:<hex>. 동일한 논리 메시지는 항상 같은 hash - ConnectRPC code 를
dshcore.Code7종 vocabulary 로 collapse (not_found / permission_denied / invalid_argument / cas_mismatch / conflict / unavailable / internal) - off-request goroutine INSERT — DB stall 이 RPC 응답 latency 에 추가되지 않음. 5s timeout
- streaming RPC 는 unary interceptor 이므로 자연스럽게 skip (rolling chunk hash 는 후속)
Client-side dshcore.Client + 3 헤더
-
X-Alphea-Surface(Config 고정): "dshai" / "dsh" / "sdk" / "webconsole" / fallback="api" -
X-Alphea-Command(per-call ctx): 명령 이름 (e.g. "dfs.copy") -
X-Alphea-Request-Id(per-call ctx): dshai JSON 요청 ID -
WithCommandMeta(ctx, command, requestID)helper - 헤더 부재시 server 이
surface="api"/command=<procedure>로 backfill — raw 호출자 호환 - 헤더 상수는 dshcore + server 양쪽 정의 +
TestAuditHeaderConstantsMatchServer가 sync 검증
헤더 = advisory only — permission 결정은 JWT principal 만. 더러운 클라이언트가 command 이름을 댌사구해도 row 의 procedure 컬럼 에 진실 있음
Surface wiring
-
cmd/dshai: Surface="dshai", dispatcher 가 매 request 마다WithCommandMeta(req.Command, req.ID)박음. composite 명령dfs.copy→ (GetFile + PutFile) 두 row 가 같은 command + request_id 공유 -
cmd/dsh: Surface="dsh". per-call meta 없음 (operator 명령 1:1 RPC 매핑) -
cmd/sdktest/cmd/apismoke: dshcore 경유 안 함, surface="api" fallback
테스트¶
internal/dshcore/audit_header_test.go (신규):
- TestAuditHeaderConstantsMatchServer — dshcore ↔ server 상수 sync
- TestAuditMetadataInterceptorStampsHeaders — httptest 서버 경유 wire 검증
- TestAuditMetadataInterceptorOmitsEmpty — empty meta 는 헤더 생략 (backfill 경로 활용)
internal/server/command_audit_test.go (신규):
- TestMapCommandAuditErrorCode — 15 sub-test, 모든 ConnectRPC code mapping
- TestHashProtoMessage — 포멃, 길이, 재현성, empty-on-non-proto
- TestTruncateAuditMessage — 512 bytes cap + ellipsis marker
검증¶
go test ./sdk ./internal/dshcore ./internal/server \
./internal/store ./internal/handler \
./cmd/dshai ./cmd/dsh ./cmd/sdktest — all clean
go vet ./... — clean
go build ./... — clean
이 슬라이스가 포함하지 않는 항목¶
- agent_sessions 엔티티 + bootstrap RPC (#452) → command_audit.agent_session_id 채움
- namespace 컬럼 채움 (per-procedure mapping 또는 proto reflection 가 필요, 후속 슬라이스)
- streaming RPC audit (chunk rolling hash)
- DKernelTaskService routing (#453)
- 문서 정리 (#454)
다음 진입: #452 (0.8-3 Agent session entity + bootstrap RPC).
HP Henry Park이(가) 5달 전에 변경
0.8-2 review findings 먹음¶
commit:server2|b69a259
첫 컷 (6ee2aa3) 후 리뷰에서 나온 세 건 (High 1 + Medium 2) 모두 같은 슬라이스에 접어 넣음.
1) High — interceptor chain order. Connect 가 chain 을 outer→inner 로 실행하는데 AuthInterceptor 가 마지막 (innermost) 이었음. CommandAuditInterceptor 가 outer 이라 next() 다른 ctx 를 못 봅. 결과 protected RPC 의 audit row 가 principal 빈 채 들어감. Fix: AuthInterceptor 를 access/audit 보다 outer 로 이동. Metrics + Logging 은 그대로 outermost — auth 실패도 관찰 가능. Public-RPC best-effort 동반: publicRPCs 분기에도 token 있으면 verify-and-attach. 충관 시에는 그냥 anonymous 로 통과 (public RPC 는 token 필요 안 함)
2) Medium — namespace extraction. 첫 컷에서는 perma-NULL + TODO. 이제 모든 dFS / Delta request 가 관습적으로 노출하는 GetNamespace() *DfsNamespaceRef 와 ResolvePathRequest 의 GetUri() 를 interface dispatch 로 읽음. 환경 추출 → "wallet:0xabc" / "token:0xdef" 등. 새 namespace-bearing RPC 도 자동 attribution. Wallet/Node 챌린지는 의도적 NULL — principal_id 가 그 정보 carry
3) Low — DB-free integration tests. 첫 컷 테스트가 helper 는 잘 잡는데 실제 chain output 은 못 잡은 점이 High 버그를 뚫은 원인. 신규:
-
TestExtractAuditNamespace(8 sub-case) +TestParseURINamespace(7 sub-case) — namespace 추출 하나하나 -
TestInterceptorChainAttributesProtectedRPC— 전체 chain 을runChain으로 이어서. channel-backedfakeAuditWriter로 off-request goroutine 을 결정론적 수신. principal + namespace + surface + command + request_id + args/result hash 전부 검증. High regression 가 다시 들어오면 이 테스트가 잡음 -
TestInterceptorChainAttributesPublicRPCWithBearer— public RPC + bearer → audit row 에 principal -
TestInterceptorChainProtectedRPCRejectsAnonymous— anonymous protected call → Unauthenticated short-circuit + 200ms 안 row 안 들어감 검증 -
specRequestwrapper — connect.NewRequest 가 비워둘 Spec().Procedure 를 테스트에서 제어
검증¶
go test ./sdk ./internal/dshcore ./internal/server \
./internal/store ./internal/handler \
./cmd/dshai ./cmd/dsh ./cmd/sdktest — all clean
go vet ./... — clean
go build ./... — clean
decision-log.md 의 2026-05-20 follow-up entry 에 세 건의 "왜 그 첨 구현에서 이 올가미가 나왔는지" 설명. 다음 슬라이스에서 같은 함정 다시 빠지지 않게.
HP Henry Park이(가) 5달 전에 변경
0.8-2 second-round review findings 먹음¶
commit:server2|bbe43db
지난 라운드 (b69a259) 의 chain-order fix 가 protected RPC 의 principal 는 채웠지만, AuthInterceptor 가 outer 가 되어 거절한 RPC (만료 wallet token, bad bearer, missing Authorization) 가 audit 에 아예 안 남는 새 regression 을 만듦. 세 건 (Medium 1 + Low 2) 모두 같은 슬라이스에 접어 넣음.
1) Medium — authentication failures 도 audit 에 남아야. Phase 0.8 audit 의 의미는 "모든 시도된 RPC 가 흔적을 남김" — expired token / bad bearer 도 row 있어야. Fix: chain 순서를 원래대로 (AccessLog / Audit 가 Auth 를 감셜) 되돌리고, audit logger 가 자체 best-effort verify(token) 으로 principal 채움. principalFromAuthorization helper 신규 — 명시적 attribution-only path (permission 결정에 임팝트 없음)
New chain (원래 의도):
MetricsInterceptor (outer)
LoggingInterceptor
AccessLogInterceptor — best-effort verify
CommandAuditInterceptor — best-effort verify
AuthInterceptor (inner — 유일 게이트)
Anonymous / bad-bearer protected 가 이제 row 남김: status="error", error_code="permission_denied", empty principal (best-effort verify 도 실패), populated namespace + procedure + args_hash. handler 는 여전히 안 실행.
2) Low — namespace scheme + address gating. parseURINamespace 이 알려진 스키마·호스트 필터링 없이 https://example/x 도 namespace="https:example" 로 기록. 컬럼 의미 오염. Fix: isDFSNamespaceScheme(s) 가 wallet/token 만 허용, isHexAddress(s) 가 lowercase 0x + 40 hex 강제. DfsNamespaceRef-holder 도 같은 validation. column 이 이제 엄격하게 "dFS namespace key 또는 NULL"
3) Low — test housekeeping. var _ sync.Mutex placeholder + sync import 제거. 필요해지면 그때 import.
테스트 변경¶
-
TestInterceptorChainProtectedRPCRejectsAnonymous(이전) → 제거 (잘못된 assertion). 대체 2개:-
TestInterceptorChainAuditsAnonymousProtectedRPC— row 가 남고 status=error / error_code=permission_denied / namespace+procedure+args_hash 채움 -
TestInterceptorChainAuditsBadBearerProtectedRPC— expired wallet token 시나리오
-
-
TestInterceptorChainAttributesProtectedRPC40-hex 주소 fixture 로 -
TestExtractAuditNamespace+TestParseURINamespace에 non-dFS scheme + bad address 케이스 추가
검증¶
go test ./sdk ./internal/dshcore ./internal/server \
./internal/store ./internal/handler \
./cmd/dshai ./cmd/dsh ./cmd/sdktest — all clean
go vet ./... — clean
go build ./... — clean
decision-log 에 세 번째 2026-05-20 entry — "왜 이전 라운드의 fix 가 틀렸는지". 다음 슬라이스에서 같은 trap 재현하지 않게.