<!-- task=m-iop-agent-cli-runtime/03+01,02_guardrail_admission plan=1 tag=REVIEW_API -->

# Effective Working Repository VCS Metadata Admission 보완 계획

## 이 파일을 읽는 구현 에이전트에게

구현과 검증을 완료한 뒤 `CODE_REVIEW-cloud-G04.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 명령별 stdout/stderr를 채운다. active pair는 그대로 유지하고 리뷰 준비 완료만 보고한다. blocker가 있으면 정확한 원인, 시도한 명령/출력, 재개 조건만 구현 소유 evidence에 기록하며 사용자 질문, user-input 도구, stop 파일, 상태 분류, archive, `complete.log`는 수행하지 않는다.

## 배경

첫 리뷰에서 기존 fresh/race suite는 통과했지만, clone task의 effective working directory가 grant에 없는 외부 Git metadata를 가리키는 중첩 `.git` pointer를 가져도 admission이 `permitted`가 되는 결함이 재현됐다. SDD S17의 actual VCS metadata containment와 blocked invocation 0회 조건을 닫기 위해 task root뿐 아니라 실행 cwd에 적용되는 Git repository metadata도 사전 검증해야 한다.

## Archive Evidence Snapshot

- 종료된 loop: `agent-task/m-iop-agent-cli-runtime/03+01,02_guardrail_admission/plan_cloud_G09_0.log`, `agent-task/m-iop-agent-cli-runtime/03+01,02_guardrail_admission/code_review_cloud_G09_0.log`
- 판정: `FAIL`; Required 1, Suggested 0, Nit 0.
- Required: `packages/go/agentguard/canonical.go:86`이 `taskRoot`의 `.git`만 검사해 `WorkingDir` 아래 중첩 `.git` pointer의 허용되지 않은 외부 `gitdir`/`commondir`를 놓친다.
- reviewer evidence: 기존 대상 fresh/race suite는 PASS했으나, clone task 아래 `nested/.git -> task root 밖 gitdir`, `WorkingDir=nested` fixture에서 `Admit`이 `vcs_metadata_not_allowed` 대신 `permitted`를 반환했다.
- Roadmap carryover: `guardrail-admission`은 PASS 전까지 완료 대상이 아니며 S17의 allowed/blocked/zero-invocation/notification evidence가 필요하다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
- Task ids:
  - `guardrail-admission`: canonical workspace/provider capability 사전 검증과 typed blocker
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- 규칙/테스트 환경: `agent-ops/rules/project/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-ops/rules/common/rules-agent-spec.md`, `agent-ops/rules/project/domain/platform-common/rules.md`, `agent-ops/rules/project/domain/node/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/node-smoke.md`, `agent-test/local/testing-smoke.md`.
- 로드맵/설계: `agent-roadmap/current.md`, `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`, `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`, `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`.
- 계약/스펙: `agent-contract/index.md`, `agent-contract/inner/agent-runtime.md`, `agent-spec/index.md`, `agent-spec/runtime/edge-node-execution.md`.
- 현재 loop: `agent-task/m-iop-agent-cli-runtime/03+01,02_guardrail_admission/plan_cloud_G09_0.log`, `agent-task/m-iop-agent-cli-runtime/03+01,02_guardrail_admission/code_review_cloud_G09_0.log`.
- 구현: `packages/go/agentguard/types.go`, `blocker.go`, `notification.go`, `containment.go`, `gitmeta.go`, `canonical.go`, `permit.go`, `packages/go/agentprovider/catalog/factory.go`, `discovery.go`, `readiness.go`, `packages/go/agentconfig/catalog.go`, `validate.go`, `configs/iop-agent.providers.yaml`.
- 테스트: `packages/go/agentguard/blocker_test.go`, `admission_integration_test.go`, `packages/go/agentprovider/catalog/lifecycle_conformance_test.go`, `packages/go/agentconfig/catalog_test.go`.
- 선행 완료 근거: `agent-task/archive/2026/07/m-iop-agent-cli-runtime/01_common_runtime_node_bridge/complete.log`, `agent-task/archive/2026/07/m-iop-agent-cli-runtime/02+01_provider_catalog/complete.log`.

### SDD 기준

- `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`는 `[승인됨]`, SDD 잠금 `해제`, 사용자 결정 잔여 없음이다.
- 대상은 S17/`guardrail-admission`과 Evidence Map S17이다. effective working repository의 `.git`/`gitdir`/`commondir`가 task root 내부 또는 exact grant allowance인지 검증하고, 불허 case는 typed blocker·설정 안내·provider invocation 0회로 증명하도록 체크리스트와 최종 검증을 구성했다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`와 platform-common profile을 적용한다. follow-up 수정은 `packages/go/agentguard`, catalog test와 inner contract에 한정되어 Node/user command surface를 변경하지 않으므로 node/testing full-cycle profile은 재실행 대상이 아니다.
- 환경 확인 결과: `/config/.local/bin/go` → `/config/opt/go/bin/go`, `go1.26.2 linux/arm64`, `GOROOT=/config/opt/go`. 실제 checkout root는 `/config/workspace/iop-s0`이며 local rule의 `/config/workspace/iop` 표기는 현재 checkout과 다르므로 실제 root를 사용한다.
- security path test는 `-count=1`, package race 회귀는 `-race -count=1`로 실행한다. proto schema는 바꾸지 않지만 platform-common local quick check에 따라 `make proto`를 포함한다.
- 외부 provider, remote host, credential, port가 필요한 경로는 없고 새 user entrypoint도 만들지 않으므로 external/full-cycle preflight는 적용하지 않는다.

### 테스트 커버리지 공백

- 기존 test는 task root full clone/worktree metadata와 working-directory symlink escape를 다루지만, canonical task root 내부의 중첩 working repository가 외부 Git metadata를 가리키는 경우가 없다.
- catalog facade의 ledger test는 stale revision의 invocation 0회만 검증하며, nested external Git metadata blocker의 CLI invocation 0회는 검증하지 않는다.

### 심볼 참조

- rename/remove 대상 없음.
- `discoverGitMetadata` 호출은 `packages/go/agentguard/canonical.go` 한 곳이며 새 helper는 package-private로 유지한다.

### 분할 판단

- effective working repository metadata 판정과 invocation 0회 회귀는 하나의 security invariant이므로 분할하지 않는다.
- subtask predecessor `01`은 `agent-task/archive/2026/07/m-iop-agent-cli-runtime/01_common_runtime_node_bridge/complete.log`, `02`는 `agent-task/archive/2026/07/m-iop-agent-cli-runtime/02+01_provider_catalog/complete.log`로 각각 충족됐다.

### 범위 결정 근거

- Permit HMAC/revision/filesystem identity, provider capability taxonomy와 default catalog 선언은 이번 재현 원인이 아니므로 변경하지 않는다.
- overlay/worktree/clone 생성, 전체 task tree의 COW enforcement, standalone `iop-agent` command 연결은 후속 Milestone Task 범위이므로 확장하지 않는다.
- 실제 provider 호출과 credential smoke는 facade 이전 단계의 filesystem admission bug 재현에 필요하지 않으며 금지한다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`, `finalizer=finalize-task-policy.sh`, 모든 build/review closure는 `true`, capability gap 없음.
- build score=`1+0+1+0+1=G03`, base=`local-fit`, route=`cloud/recovery-boundary`; review score=`1+0+1+1+1=G04`, route=`cloud/official-review`.
- `large_indivisible_context=false`; positive loop risks=`structured_interpretation`, `variant_product`(2개).
- recovery signals: `review_rework_count=1`, `evidence_integrity_failure=true`.
- canonical files: `PLAN-cloud-G03.md`, `CODE_REVIEW-cloud-G04.md`.

## 구현 체크리스트

- [ ] REVIEW_API-1 task root와 effective working repository의 Git metadata를 모두 canonical/exact-allowance 검증한다.
- [ ] REVIEW_API-2 nested external Git metadata의 typed blocker와 facade invocation 0회 회귀를 fresh/race 검증한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 의존 관계 및 구현 순서

1. `01_common_runtime_node_bridge`는 `agent-task/archive/2026/07/m-iop-agent-cli-runtime/01_common_runtime_node_bridge/complete.log`로 충족됐다.
2. `02+01_provider_catalog`는 `agent-task/archive/2026/07/m-iop-agent-cli-runtime/02+01_provider_catalog/complete.log`로 충족됐다.
3. REVIEW_API-1 뒤 REVIEW_API-2를 수행한다.

### [REVIEW_API-1] Effective working repository metadata 검증

- 문제: `packages/go/agentguard/canonical.go:86`은 `discoverGitMetadata(taskRoot.path, mode)`만 호출한다. 따라서 `WorkingDir`에 더 가까운 중첩 `.git` pointer가 grant에 없는 외부 metadata를 가리켜도 task root의 내부 `.git`만 보고 permit을 발급한다.
- 해결 방법: declared isolation root의 mode-specific metadata 검증은 유지하고, canonical `WorkingDir`에서 `TaskRoot`까지 Git의 parent discovery 의미로 가장 가까운 추가 `.git` entry를 찾는다. 중첩 entry가 있으면 `.git` symlink/file/directory와 `gitdir`/`commondir`를 같은 bounded parser로 canonicalize하고, task root 밖 실제 metadata는 exact `WorkspaceGrant.VCSMetadataRoots`에 있을 때만 허용한다. root/effective metadata와 identity pin은 중복 제거한다.

```go
// Before: packages/go/agentguard/canonical.go:86
actualVCS, gitErr := discoverGitMetadata(taskRoot.path, req.Isolation.Mode)

// After
rootVCS, gitErr := discoverGitMetadata(taskRoot.path, req.Isolation.Mode)
effectiveVCS, gitErr := discoverEffectiveGitMetadata(taskRoot.path, workingDir.path)
actualVCS := deduplicatePins(append(rootVCS, effectiveVCS...))
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/agentguard/gitmeta.go`에서 root mode 검증과 effective working repository discovery를 분리하되 pointer size/symlink/canonical 규칙을 재사용한다.
  - [ ] `packages/go/agentguard/canonical.go`에서 root/effective metadata를 합쳐 exact allowance와 filesystem pin을 적용한다.
  - [ ] `agent-contract/inner/agent-runtime.md`의 actual Git metadata 문구를 task root와 effective working repository 모두로 명확히 한다.
- 테스트 작성: `packages/go/agentguard/admission_integration_test.go`에 `TestAdmissionNestedWorkingRepositoryMetadata` table을 추가한다. 내부 nested `.git`은 허용하고, 외부 pointer 무허용은 `vcs_metadata_not_allowed`, exact allowance case는 허용하며 resolved metadata/pin을 확인한다.
- 중간 검증: `/config/.local/bin/go test -count=1 ./packages/go/agentguard/... -run 'NestedWorkingRepositoryMetadata'`가 PASS해야 한다.

### [REVIEW_API-2] Blocked facade zero-invocation 회귀

- 문제: 현재 `packages/go/agentprovider/catalog/lifecycle_conformance_test.go:176-342`는 facade의 stale revision과 capability 차단을 검증하지만 nested external Git metadata case에서 CLI process 0회를 증명하지 않는다.
- 해결 방법: fake provider ledger fixture에 nested working directory와 grant 밖 `.git` pointer를 구성하고 `AdmittedProfileProvider.Admit`이 `vcs_metadata_not_allowed`와 actionable notification을 반환하는지 확인한다. permit이 없으므로 `Execute` callback/process가 시작되지 않았고 ledger가 생성되지 않거나 0행임을 검증한다.

```go
// Before: no nested working repository facade regression

// After
// nested/.git -> unallowed external gitdir
// Admit => VCSMetadataNotAllowed
// provider ledger => 0 invocations
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/agentprovider/catalog/lifecycle_conformance_test.go`에 `TestAdmittedProfileProviderBlocksNestedExternalGitMetadataBeforeInvocation`을 추가한다.
  - [ ] 모든 blocked test가 non-empty setup guidance와 raw path 비노출을 함께 확인한다.
- 테스트 작성: 위 named regression을 작성한다. 실제 provider/credential 대신 기존 shell ledger fake만 사용한다.
- 중간 검증: `/config/.local/bin/go test -count=1 ./packages/go/agentprovider/catalog/... -run 'BlocksNestedExternalGitMetadataBeforeInvocation'`가 PASS해야 한다.

## 수정 파일 요약

| 파일/영역 | 항목 |
|---|---|
| `packages/go/agentguard/gitmeta.go` | REVIEW_API-1 |
| `packages/go/agentguard/canonical.go` | REVIEW_API-1 |
| `packages/go/agentguard/admission_integration_test.go` | REVIEW_API-1 |
| `packages/go/agentprovider/catalog/lifecycle_conformance_test.go` | REVIEW_API-2 |
| `agent-contract/inner/agent-runtime.md` | REVIEW_API-1 |

## 최종 검증

```bash
make proto
/config/.local/bin/go test -count=1 ./packages/go/agentguard/... -run 'NestedWorkingRepositoryMetadata'
/config/.local/bin/go test -count=1 ./packages/go/agentprovider/catalog/... -run 'BlocksNestedExternalGitMetadataBeforeInvocation'
/config/.local/bin/go test -race -count=1 ./packages/go/agentguard/... ./packages/go/agentprovider/catalog/...
/config/.local/bin/go test -count=1 ./packages/go/agentruntime/... ./packages/go/agentprovider/...
gofmt -l packages/go/agentguard packages/go/agentprovider/catalog
git diff --check
```

기대 결과: nested internal/exact-allowed metadata만 permit되고 grant 밖 `gitdir`/`commondir`는 `vcs_metadata_not_allowed`, actionable path-free notification, provider ledger 0회로 차단된다. 전체 fresh/race suite는 PASS하고 `gofmt -l`과 `git diff --check`는 무출력이어야 한다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.
