<!-- task=m-iop-agent-cli-runtime/07+06_target_policy plan=0 tag=API -->

# Code Review Reference - API

> **[IMPLEMENTING AGENT — READ FIRST]** Start only after `06+05` completes. Fill implementation evidence, not review finalization.

## Overview

date=2026-07-28
task=m-iop-agent-cli-runtime/07+06_target_policy, plan=0, tag=API

## 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:
  - `target-policy`: ordered evaluator와 durable route history
- Completion mode: check-on-pass

## Implementation Item Completion

| Item | Status |
|---|---|
| API-1 policy selection과 durable decision을 구현한다 | [ ] |

## Implementation Checklist

- [ ] Config snapshot에서 policy input을 받고 one-target `RouteDecision`을 산출하는 evaluator를 추가한다.
- [x] ordered first-match, time/stage/grade/capability predicate와 no-match blocker를 구현한다.
- [ ] route decision/history encode-decode와 corruption rejection을 구현한다.
- [ ] overlap, first-match, persisted-history/tamper test를 추가한다.
- [x] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

## Review-Only Checklist

> **[REVIEW AGENT ONLY]**

- [x] Append PASS/WARN/FAIL and verified routing signals.
- [x] Archive as `code_review_cloud_G07_0.log` and `plan_local_G07_0.log`.
- [ ] On PASS create `complete.log`, report `target-policy`, then archive this subtask.

## Deviations from Plan

- **Evaluator is stateless**: The PLAN described the evaluator as taking `agentconfig.RuntimeSnapshot` as input. The implementation makes `Evaluator` a stateless struct whose `SelectPolicy` method receives the snapshot as a parameter (rather than storing it in the constructor). This allows one `Evaluator` instance to serve multiple snapshots without re-allocation, and matches the `agenttask.PolicySelector` interface shape exactly. Rationale: the snapshot is already immutable, so caching it in the evaluator adds no safety and complicates reuse across config revisions.
- **ProfileRevision left empty in RouteDecision**: The `agentconfig.Catalog` `Profile` type has no revision field (revision is assigned by the isolation backend, not the catalog). The `RouteDecision` struct retains the `ProfileRevision` field for future use but does not populate it from the catalog. Rationale: populating it with the profile ID would be misleading; the execution-time `ExecutionTarget.ProfileRevision` is set by the isolation backend downstream.
- **No Manager integration in this subtask**: The `PolicySelector` interface is added to `agenttask/ports.go` but the `Manager` is not modified to use it. The existing `Selector.Select` path is preserved unchanged. Rationale: the PLAN's Modified Files Summary lists only `ports.go` for API-1; Manager wiring to the policy evaluator is a follow-on concern. The `TestEvaluatorSatisfiesPolicySelectorShape` test in `agentpolicy` verifies interface conformance.

## Key Design Decisions

- **Single `agentpolicy` package**: Created `packages/go/agentpolicy` with `decision.go` (RouteDecision, RouteHistory, EncodeDecision, DecodeDecision, typed errors) and `evaluator.go` (SelectionContext, Evaluator, SelectPolicy, predicate matchers). The package imports only `agentconfig` — no dependency on `agenttask`, avoiding a circular dependency.
- **RouteDecision is the durable output**: Contains `ProviderID`, `ModelID`, `ProfileID`, `ConfigRevision`, `SelectionRevision`, `SelectedRuleID`, `Rejected []RejectedRule` (with reason), and `History RouteHistory`. Exactly one target is selected; rejected rules are recorded with their rejection reason so failover/audit can reconstruct the full evaluation.
- **Versioned JSON envelope**: `EncodeDecision` wraps `RouteDecision` in a `decisionEnvelope` with `version`, `config_revision`, `selection_revision`, and `decision`. `DecodeDecision` validates the version, checks the sealed config revision against the expected revision (returning `ErrRevisionMismatch` on mismatch), and returns `ErrCorruptDecision` for malformed JSON or unknown versions. A tampered sealed revision is detected even if the decision payload itself is valid.
- **First-match ordered evaluation**: Rules are evaluated in their stored order (the config registry never sorts them). The first rule whose predicates all match wins. Rules that are evaluated but do not match are appended to the `Rejected` slice with a reason. Rules after the first match are never evaluated.
- **Predicate semantics**: Each predicate list is unconstrained when empty (always matches). `MinGrade`/`MaxGrade` use zero as "not set" (consistent with `agentconfig` validation). Time windows support day-of-week filtering and midnight-crossing windows. Capabilities require all required capabilities to be present in the available set.
- **No-match blocker**: When no rule matches and no default target is configured, `ErrNoMatch` is returned. This is a typed error that the `agenttask` Manager can surface as a `BlockerSelectionFailed` blocker. When a default target exists, it is used and `SelectedRuleID` is empty.
- **PolicySelector interface in agenttask**: Added `PolicySelector` interface to `ports.go` that takes `agentconfig.RuntimeSnapshot` and `agentpolicy.SelectionContext`, returning `agentpolicy.RouteDecision`. The `agentpolicy.Evaluator` satisfies this interface implicitly (Go structural typing). The existing `Selector` interface is unchanged.
- **Catalog resolution**: The evaluator resolves the selected profile against the catalog to validate it exists and to inherit `Provider`/`Model` identity when the target omits them. An unknown profile with no profile ID returns `ErrUnknownProfile`.

## Reviewer Checkpoints

- First matching ordered rule yields exactly one target; corrupt route blocks, never silently reselects.

## Verification Results

### `go test -count=1 ./packages/go/agentpolicy ./packages/go/agenttask`

```
ok  	iop/packages/go/agentpolicy	0.005s
ok  	iop/packages/go/agenttask	20.549s
```

Verbose output for `agentpolicy` (15 test functions, 25 subtests):
```
=== RUN   TestEvaluatorFirstMatchWins
--- PASS: TestEvaluatorFirstMatchWins (0.00s)
=== RUN   TestEvaluatorOverlappingRulesFirstMatchWins
--- PASS: TestEvaluatorOverlappingRulesFirstMatchWins (0.00s)
=== RUN   TestEvaluatorRejectsNonMatchingFirstRule
--- PASS: TestEvaluatorRejectsNonMatchingFirstRule (0.00s)
=== RUN   TestEvaluatorBoundaryTimeWindow
=== RUN   TestEvaluatorBoundaryTimeWindow/before_window
=== RUN   TestEvaluatorBoundaryTimeWindow/at_start_boundary
=== RUN   TestEvaluatorBoundaryTimeWindow/middle_of_window
=== RUN   TestEvaluatorBoundaryTimeWindow/at_end_boundary
=== RUN   TestEvaluatorBoundaryTimeWindow/after_window
--- PASS: TestEvaluatorBoundaryTimeWindow (0.00s)
    --- PASS: TestEvaluatorBoundaryTimeWindow/before_window (0.00s)
    --- PASS: TestEvaluatorBoundaryTimeWindow/at_start_boundary (0.00s)
    --- PASS: TestEvaluatorBoundaryTimeWindow/middle_of_window (0.00s)
    --- PASS: TestEvaluatorBoundaryTimeWindow/at_end_boundary (0.00s)
    --- PASS: TestEvaluatorBoundaryTimeWindow/after_window (0.00s)
=== RUN   TestEvaluatorGradeStageMatch
=== RUN   TestEvaluatorGradeStageMatch/grade_below_min
=== RUN   TestEvaluatorGradeStageMatch/grade_at_min
=== RUN   TestEvaluatorGradeStageMatch/grade_in_range
=== RUN   TestEvaluatorGradeStageMatch/grade_at_max
=== RUN   TestEvaluatorGradeStageMatch/grade_above_max
=== RUN   TestEvaluatorGradeStageMatch/wrong_stage
--- PASS: TestEvaluatorGradeStageMatch (0.00s)
    --- PASS: TestEvaluatorGradeStageMatch/grade_below_min (0.00s)
    --- PASS: TestEvaluatorGradeStageMatch/grade_at_min (0.00s)
    --- PASS: TestEvaluatorGradeStageMatch/grade_in_range (0.00s)
    --- PASS: TestEvaluatorGradeStageMatch/grade_at_max (0.00s)
    --- PASS: TestEvaluatorGradeStageMatch/grade_above_max (0.00s)
    --- PASS: TestEvaluatorGradeStageMatch/wrong_stage (0.00s)
=== RUN   TestEvaluatorNoMatchBlocker
--- PASS: TestEvaluatorNoMatchBlocker (0.00s)
=== RUN   TestEvaluatorDefaultTargetWhenNoRuleMatches
--- PASS: TestEvaluatorDefaultTargetWhenNoRuleMatches (0.00s)
=== RUN   TestEvaluatorCapabilityMatch
--- PASS: TestEvaluatorCapabilityMatch (0.00s)
=== RUN   TestEvaluatorTimezoneApplied
--- PASS: TestEvaluatorTimezoneApplied (0.00s)
=== RUN   TestEncodeDecodeDecisionRoundTrip
--- PASS: TestEncodeDecodeDecisionRoundTrip (0.00s)
=== RUN   TestDecodeDecisionRejectsCorruptJSON
--- PASS: TestDecodeDecisionRejectsCorruptJSON (0.00s)
=== RUN   TestDecodeDecisionRejectsWrongVersion
--- PASS: TestDecodeDecisionRejectsWrongVersion (0.00s)
=== RUN   TestDecodeDecisionRejectsRevisionMismatch
--- PASS: TestDecodeDecisionRejectsRevisionMismatch (0.00s)
=== RUN   TestDecodeDecisionAcceptsEmptyExpectedRevision
--- PASS: TestDecodeDecisionAcceptsEmptyExpectedRevision (0.00s)
=== RUN   TestDecodeDecisionRejectsTamperedRevision
--- PASS: TestDecodeDecisionRejectsTamperedRevision (0.00s)
=== RUN   TestEvaluatorSatisfiesPolicySelectorShape
--- PASS: TestEvaluatorSatisfiesPolicySelectorShape (0.00s)
PASS
ok  	iop/packages/go/agentpolicy	0.029s
```

### `git diff --check`

```
(no output)
```

### `gofmt -d`

```
(no output)
```

### `go vet`

```
(no output)
```

## Section Ownership

| Section | Owner |
|---|---|
| Implementation checklist/results/deviations/decisions | Implementing agent |
| Review checklist, verdict, archive and completion log | Review agent |

## Code Review Result

- Overall Verdict: FAIL
- Dimension Assessment:
  - Correctness: Fail
  - Completeness: Fail
  - Test Coverage: Fail
  - API Contract: Fail
  - Code Quality: Pass
  - Implementation Deviation: Fail
  - Verification Trust: Fail
  - Spec Conformance: Fail
- Findings:
  - Required — `packages/go/agentpolicy/decision.go:90`: `DecodeDecision` validates only the envelope config revision, then returns an unchecked payload. A payload whose `Decision.ConfigRevision` was changed from `sha256:config-a` to `sha256:config-b` decoded successfully while the sealed envelope and expected revision remained `sha256:config-a`. Add a strict integrity seal over the complete versioned payload, cross-check envelope and decision config/selection revisions plus required target/history fields, and add mutation cases for the payload revision, target identity, and history.
  - Required — `packages/go/agentpolicy/evaluator.go:250`: the unresolved-profile branch is reversed. A valid runtime config with no catalog and target profile `missing-profile` returns a successful decision with empty provider/model IDs instead of `ErrUnknownProfile`, violating the one-provider/model output contract. Return `ErrUnknownProfile` whenever `ResolveProfile` fails, require resolved provider/model/profile identities, and add missing-catalog and unknown-profile regression tests.
  - Required — `packages/go/agentpolicy/evaluator.go:65`: selection always reads `snapshot.Config().Selection`; the API has no project identity and never reads `snapshot.Project(projectID).Selection`. Therefore user-local project policy overrides and their project-specific selection revisions cannot be evaluated. Carry the selected project identity (or an already resolved effective policy) into the selector, evaluate the project policy when applicable, and add global-versus-project override tests.
  - Required — `packages/go/agentpolicy/evaluator.go:60`: there is no persisted-route resume/replay input or API. Every call re-evaluates current rules and constructs a fresh history whose `PreviousRuleID` is the current rule at `packages/go/agentpolicy/evaluator.go:269`; it does not preserve used route history or block a damaged persisted route before reselection as SDD S06 requires. Add an identity- and revision-pinned resume path that validates and returns the persisted route without reselection, record selected eligibility/reason plus used/rejected history, and test stable replay and corrupt-route blocking.
  - Required — `packages/go/agenttask/manager_test.go:983`: the required fresh package verification is nondeterministic. The test exits its wait loop on `pending_integration` before the integration lease is necessarily claimed, then fails at line 996. The review run failed both the full required command and `go test -count=20 -run '^TestIntegrationLeaseRenewsAndFencesLateResult$' ./packages/go/agenttask`. Wait for the integration call/lease claim deterministically, then rerun the focused test repeatedly and the complete required package command.
- Routing Signals: review_rework_count=1, evidence_integrity_failure=true
- Next Step: Archive this pair and create a freshly routed follow-up PLAN/CODE_REVIEW pair through the plan skill.
