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

# Workspace Guardrail Admission 구현 계획

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

구현의 마지막 단계는 `CODE_REVIEW-cloud-G09.md` 구현 소유 섹션에 실제 변경과 stdout/stderr를 채우는 것이다. active pair를 유지하고 리뷰 준비 완료만 보고한다. blocker가 있으면 정확한 원인, 시도한 명령/출력, 재개 조건만 evidence에 기록하고 사용자 질문·user-input·stop 파일·상태 분류·archive/`complete.log`는 수행하지 않는다.

## 배경

등록 workspace라는 사실만으로 unattended provider를 호출하면 symlink escape, worktree 외부 git metadata, canonical base 직접 쓰기와 승인 fallback 위험이 남는다. provider invocation 앞의 단일 admission boundary에서 canonical grant, task writable root와 provider bypass capability를 함께 증명하고 실패 시 invocation을 0회로 유지해야 한다.

## 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-roadmap/current.md`, `agent-roadmap/priority-queue.md`, `agent-roadmap/ROADMAP.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/edge-node-runtime-wire.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-spec/index.md`, `agent-spec/runtime/edge-node-execution.md`, `agent-spec/runtime/provider-pool-config-refresh.md`.
- 구현: `apps/node/internal/adapters/cli/workspace.go`, `apps/node/internal/adapters/cli/cli.go`, `apps/node/internal/adapters/cli/command.go`, `apps/node/internal/runtime/types.go`, `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/execution_target_policy.py`, `agent-ops/skills/project/orchestrate-agent-task-loop/scripts/select_execution_target.py`.
- 테스트: `apps/node/internal/adapters/cli/cli_workspace_test.go`, `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_execution_target_policy.py`, `agent-ops/skills/project/orchestrate-agent-task-loop/tests/test_select_execution_target.py`.
- 테스트 규칙: `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/node-smoke.md`, `agent-test/local/testing-smoke.md`.

### SDD 기준

- 승인/잠금 해제 SDD의 S17과 Evidence Map S17이 기준이다.
- registered/unregistered, full clone/worktree, symlink escape, writable-root confinement 가능/불가, unattended/approval-bypass on/off matrix가 필수다.
- 허용 profile만 provider를 호출하고 모든 차단 case는 typed blocker, 설정 안내 notification, invocation 0회여야 하며 다른 project에는 영향을 주지 않아야 한다.

### 테스트 환경 규칙

- `test_env=local`; local rules 및 platform-common/node/testing profiles를 읽었다. security path tests는 fresh, process/concurrency는 race로 실행하며 cache를 허용하지 않는다.
- actual repo root `/config/workspace/iop-s0`, branch `dev`, HEAD `0565d2be66cc`, 기존 roadmap/SDD dirty 변경을 보존한다. local rules의 root 표기 불일치는 실제 root로 대체하며 rules 유지보수는 범위 밖이다.
- Go `/config/.local/bin/go`, `go1.26.2 linux/arm64`; path semantics는 Linux fixture에서 검증한다. macOS field behavior는 후속 logged smoke에서 별도 검증하므로 이 계획의 PASS는 OS-neutral API와 Linux filesystem matrix를 요구한다.
- 외부 provider를 호출하지 않는다. spy invoker가 0회/1회를 증명하므로 credential, port, external host, artifact preflight는 불필요하다.

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

- `apps/node/internal/adapters/cli/workspace.go:9-33`은 존재/디렉터리/readability만 검사하고 canonical registration, symlink containment, git common dir, writable-root를 검사하지 않는다.
- 기존 CLI workspace tests는 cwd와 missing path만 확인하고 provider unattended/bypass capability나 zero invocation을 다루지 않는다.
- Python target policy는 route 후보만 선택하며 workspace admission을 수행하지 않는다.

### 심볼 참조

- 기존 `prepareWorkspaceDir`는 Node CLI execute paths에서 호출된다. 이 계획에서 제거/대체할 경우 모든 oneshot/persistent/terminal call site와 tests를 `rg --sort path`로 갱신한다.
- `ExecutionSpec.Workspace`는 Node handler/router/CLI adapters가 참조하므로 문자열을 무단으로 재해석하지 않고 validated workspace handle을 별도 도입한다.

### 분할 판단

- stable contract는 `WorkspaceGrant + task isolation capability + ProviderProfile → Permit | typed Blocker`이고 provider 호출은 Permit 없이는 불가능해야 한다. PASS는 S17 matrix와 spy invocation count다.
- predecessor 01과 02 모두 active/archive `complete.log`가 없어 `missing`이다. 구현 전 두 completion이 필요하다.
- overlay 구현 자체는 후속 `overlay-workspace`; 이 계획은 admission에서 enforceable capability/roots를 검증하고 permit에 immutable revision을 pin한다.

### 범위 결정 근거

- user-local project registry/config storage, watcher와 grant 생성 UI/CLI는 `config-registry`/`cli-surface` 후속 범위다.
- COW overlay/worktree/clone 생성과 serial integration은 후속 epic이다. 이 계획은 전달받은 isolation descriptor와 actual canonical/VCS metadata paths를 검증한다.
- 외부 서비스 mutation 권한, provider login과 interactive fallback은 명시적으로 제외/금지한다.

### 최종 라우팅

- `evaluation_mode=first_pass`, finalizer=`finalize-task-routing` 1회.
- build `cloud/G09/routed`, review `cloud/G09/routed`; `large_indivisible_context=false`.
- positive loop risks 3개: filesystem security boundary, provider approval bypass capability, zero-invocation enforcement.
- recovery `review_rework_count=0`, `evidence_integrity_failure=false`; capability gap evidence 없음.
- canonical files: `PLAN-cloud-G09.md`, `CODE_REVIEW-cloud-G09.md`.

## 구현 체크리스트

- [ ] API-1 WorkspaceGrant/IsolationDescriptor/AdmissionStatus 계약과 typed blocker taxonomy를 확정한다.
- [ ] API-2 canonical path·symlink·full clone/worktree VCS metadata containment 검증을 구현한다.
- [ ] API-3 provider unattended/bypass와 task writable-root capability를 결합한 mandatory admission gate를 구현한다.
- [ ] API-4 S17 allowed/blocked/zero-invocation/notification matrix와 fresh/race 회귀를 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 의존 관계 및 구현 순서

1. `01_common_runtime_node_bridge/complete.log`와 `02+01_provider_catalog/complete.log`가 각각 active sibling 또는 matching archive에 정확히 하나 존재해야 한다. 현재 둘 다 missing이다.
2. API-1 → API-2 → API-3 → API-4 순서로 진행한다.

### [API-1] Admission 계약과 blocker taxonomy

- 문제: `apps/node/internal/runtime/types.go:32-45`의 `Workspace string`과 provider capability에는 immutable grant/isolation revision 및 unattended/bypass 표현이 없다.
- 해결 방법: `agent-runtime` inner contract에 grant, isolation descriptor, admission input/output, notification을 고정하고 `packages/go/agentguard` public types를 일치시킨다. blocker code는 missing grant, root escape, VCS allowance, writable confinement, unattended/bypass 각각을 구분한다.

```go
// Before: apps/node/internal/runtime/types.go:32-45 — raw workspace/policy fields
// ExecutionSpec{Workspace: string, Policy: map[string]any}
// After
// AdmissionRequest{Grant: WorkspaceGrant, Isolation: IsolationDescriptor, Profile: ProviderProfile}
// AdmissionResult{Permit *Permit, Blocker *Blocker, Notification *Notification}
```

- 수정 파일 및 체크리스트:
  - [ ] `agent-contract/inner/agent-runtime.md` guardrail/admission 섹션 보강.
  - [ ] `packages/go/agentguard/types.go`, `blocker.go`, `notification.go` 작성.
  - [ ] revision/identity가 permit에 immutable하게 포함되고 raw path 외 불필요 정보가 event로 누출되지 않게 함.
- 테스트 작성: `packages/go/agentguard/blocker_test.go`에 code/message/setup guidance 정상·unknown 경계 tests 추가.
- 중간 검증: `/config/.local/bin/go test -count=1 ./packages/go/agentguard/...`가 PASS해야 한다.

### [API-2] Canonical workspace와 VCS containment

- 문제: `apps/node/internal/adapters/cli/workspace.go:13-33`은 `os.Stat/Open`만 수행해 symlink escape 및 external git common-dir를 허용할 수 있다.
- 해결 방법: root/working/writable/VCS metadata를 절대·clean·symlink-resolved identity로 열고 component-aware containment를 검사한다. full clone `.git`은 root 내부, worktree common dir는 exact grant allowance와 일치할 때만 permit한다. 검증과 실행 사이 identity는 permit revision으로 pin한다.

```go
// Before: apps/node/internal/adapters/cli/workspace.go:13-33 — return readable directory string
// After: return CanonicalWorkspace{RootID, TaskRoot, GitMetadataRoots, GrantRevision}
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/agentguard/canonical.go`, `containment.go`, `gitmeta.go` 구현.
  - [ ] nonexistent/relative/`..`/prefix-collision/symlink-loop/escape/TOCTOU identity mismatch를 typed blocker로 반환.
  - [ ] Node/common provider bridge가 raw workspace check 대신 validated handle을 받도록 연결.
- 테스트 작성: temp full clone/worktree와 symlink fixtures로 inside, sibling-prefix, file symlink, dir symlink, common-dir allowed/denied, revision mismatch table tests 추가.
- 중간 검증: `/config/.local/bin/go test -race -count=1 ./packages/go/agentguard/...`가 PASS해야 한다.

### [API-3] Provider capability와 mandatory invocation gate

- 문제: catalog readiness만으로는 unattended/approval-bypass와 task writable-root confinement를 동시에 강제할 수 없고 호출자가 preflight를 우회할 수 있다.
- 해결 방법: provider invoker가 opaque Permit을 필수로 받고 profile capability 및 exact isolation roots/revision을 재검증한 뒤에만 process를 시작한다. 실패는 interactive fallback 없이 blocker/notification을 emit한다.

```go
// Before: apps/node/internal/runtime/types.go:199-204 — Adapter.Execute accepts no Permit
// After: admittedInvoker.Run(ctx, permit, spec, sink)
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/agentguard/admission.go`, `permit.go` 구현.
  - [ ] `packages/go/agentprovider/catalog/factory.go` 또는 공통 invocation facade가 Permit을 필수화.
  - [ ] independent project의 failure가 shared catalog/runtime을 stop하지 않도록 task-local result만 반환.
- 테스트 작성: spy invoker로 각 missing capability의 invocation 0회, allowed case 1회, forged/stale permit 0회, notification guidance를 검증한다.
- 중간 검증: `/config/.local/bin/go test -race -count=1 ./packages/go/agentguard/... ./packages/go/agentprovider/catalog/...`가 PASS해야 한다.

### [API-4] S17 matrix와 회귀

- 문제: 개별 unit test만으로 full clone/worktree×symlink×provider capability 조합과 다른 project 지속을 입증하기 어렵다.
- 해결 방법: table-driven integration fixture에 모든 S17 axes와 두 project를 구성하고 invocation ledger 및 typed event를 assert한다.
- 수정 파일 및 체크리스트:
  - [ ] `packages/go/agentguard/admission_integration_test.go`에 full matrix 추가.
  - [ ] Node workspace regression tests를 validated bridge 기준으로 갱신.
- 테스트 작성: 새 integration matrix와 기존 Node workspace tests를 실행한다.
- 중간 검증: `/config/.local/bin/go test -count=1 ./packages/go/agentguard/... ./apps/node/internal/adapters/...`가 PASS해야 한다.

## 수정 파일 요약

| 파일/영역 | 항목 |
|---|---|
| `agent-contract/inner/agent-runtime.md` | API-1 |
| `packages/go/agentguard/**` | API-1, API-2, API-3, API-4 |
| `packages/go/agentprovider/catalog/factory.go` | API-3 |
| `apps/node/internal/adapters/cli/**` | API-2, API-4 |

## 최종 검증

```bash
shopt -s nullglob
predecessor01=(agent-task/m-iop-agent-cli-runtime/01_common_runtime_node_bridge/complete.log agent-task/archive/[0-9][0-9][0-9][0-9]/[0-1][0-9]/m-iop-agent-cli-runtime/01_common_runtime_node_bridge/complete.log)
predecessor02=(agent-task/m-iop-agent-cli-runtime/02+01_provider_catalog/complete.log agent-task/archive/[0-9][0-9][0-9][0-9]/[0-1][0-9]/m-iop-agent-cli-runtime/02+01_provider_catalog/complete.log)
test "${#predecessor01[@]}" -eq 1
test "${#predecessor02[@]}" -eq 1
/config/.local/bin/go test -count=1 ./packages/go/agentguard/...
/config/.local/bin/go test -race -count=1 ./packages/go/agentguard/... ./packages/go/agentprovider/catalog/...
/config/.local/bin/go test -count=1 ./apps/node/internal/adapters/...
/config/.local/bin/go test -count=1 ./packages/go/agentruntime/... ./packages/go/agentprovider/...
rg --sort path -n 'prepareWorkspaceDir|ExecutionSpec\{[^}]*Workspace' apps/node packages/go || true
git diff --check
```

기대 결과: predecessor gates와 모든 fresh/race tests가 PASS한다. S17 matrix에서 allowed만 invocation 1회, 모든 blocked case는 0회와 구체 blocker/설정 안내를 남기며 독립 project case는 계속된다. search 잔여는 validated bridge 또는 명시 compatibility path뿐이고 `git diff --check`는 무출력이어야 한다.

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