프로젝트

일반

사용자정보

Actions

이슈 #451

진행중
HP HP

이슈 #448: Phase 0.8 — dKernel Agent Identity Session

Phase 0.8-2 — command_audit 스키마 + dshcore audit hook

이슈 #451: Phase 0.8-2 — command_audit 스키마 + dshcore audit hook

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

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

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 행 검증 단계 추가

관련

  • depends on / follows #450 (0.8-1 Signer)
  • precedes #448 자식 0.8-3 (Agent session 이 이 컬럼 을 채움)

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

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

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.Code 7종 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달 전에 변경 Actions #2

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-backed fakeAuditWriter 로 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 안 들어감 검증
  • specRequest wrapper — 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달 전에 변경 Actions #3

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 시나리오
  • TestInterceptorChainAttributesProtectedRPC 40-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 재현하지 않게.

Actions

내보내기 PDF Atom