Files
my-pi/pi-permission-system/docs/retro/0597-serving-resolves-forwarded-intent.md

124 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 597
issue_title: "pi-permission-system: serving resolves the forwarded access intent at gate parity"
---
# Retro: #597 — serving resolves the forwarded access intent at gate parity
## Stage: Planning (2026-07-18T18:40:00Z)
### Session summary
Planned Phase 12 Track A Step 3 — the serving node consumes the `ForwardedAccessIntent` Step 2 ([#596]) put on the wire, resolving the parent's recorded authority directly against the child-fixed `matchValues` (agent-scoped to `principal.agentName`) instead of re-deriving a path from a bare display string through the parent's `PathNormalizer`/cwd.
The plan is a three-step TDD sequence (additive resolver/`buildResolvedIntentFromMatchValues` infra `feat:` → the atomic serving-rework `feat:` → a `docs:` roadmap-completion step) filed at `packages/pi-permission-system/docs/plans/0597-serving-resolves-forwarded-intent.md`.
This step is the `cross-session-intent` batch tail, so it cuts the release shipping Steps 13.
### Observations
- **Issue body vs. ADR 0008 conflict surfaced via `ask_user`.**
The issue body said "keep the legacy `(surface, value)` fallback for version skew," but ADR 0008 §4 explicitly retired that branch (rejected the "Tolerant dual-path" alternative) and floors a missing-intent request to `ask`.
Operator confirmed: **follow ADR 0008** — retire the legacy branch, `ask`-floor on absence.
Second `ask_user` decision: ship agent-scoped serving as **`feat:`** (non-breaking), per the [#557] serving-is-resolution precedent, not `feat!:`.
- **The resolver must accept a pre-fixed `path-values` intent.**
Serving cannot go through the `access-path` variant (that would rebuild an `AccessPath` and re-derive — the exact flaw ADR 0008 removes).
Resolution: widen the *concrete* `PermissionResolver.resolve` to `AccessIntent | PathValuesAccessIntent` (passthrough in `toResolvedIntent`'s else-branch), keeping the gate-facing `ScopedPermissionResolver` interface narrow.
The forwarded-serving wire becomes a second legitimate producer of pre-fixed match values — coherent with ADR-0002 (strings in, manager never imports `AccessPath`).
- **Wire→intent mapping takes primitives, not `ForwardedAccessIntent`.**
`buildResolvedIntentFromMatchValues(surface, matchValues, agentName)` lives in `input-normalizer.ts` (sibling of `buildAccessIntentForSurface`), so `access-intent/` stays decoupled from `authority/`.
- **`boundaryValue` is not needed for rule matching** — `matchValues()` already contains the canonical alias, so `evaluateAnyValue` matches a parent `/tmp/**` rule against the child's aliases directly; `boundaryValue`/`requesterCwd` ride for provenance/disclosure only.
- **Atomic Step 2 is unavoidable** — the `ServingPolicy` `check``resolve` rename cascades through the server, `index.ts`, `makeServerDeps`, and the `forwarded-request-server.test.ts` suite; folded into one `feat:` commit.
The test rewrite is mechanical (mock rename + attach `accessIntent`), not a full large-file rewrite.
- **Dead-code gate respected** — the additive infra (Step 1) is exercised by its own new tests; `buildResolvedIntentFromMatchValues` is consumed by `index.ts` in Step 2, never landing unused.
- **Composition-root round-trip tests are the behavior-parity anchor** — they exercise the real serving path (real `ParentAuthorizer` stamps `accessIntent`, real server resolves), use `demo`/no-per-agent-rules so agent-scoped lands on the same `ask`, and stay green unchanged.
- **[#565] does not close here** — Step 3 structurally dissolves items 23, but [#565] stays open until Phase 12 end per roadmap decision.
- **Release**: ship now — `cross-session-intent` batch tail.
- Next stage is `/tdd-plan` (the plan has red→green→commit cycles).
### Diagnostic details
- **Feedback-loop gap analysis** — grounded every design claim in source before writing: read `forwarded-request-server.ts`, `permission-forwarding.ts`, `permission-resolver.ts`, `access-intent.ts`, `input-normalizer.ts`, `forwarding-io.ts`, `index.ts` (serving closure), the manager `check`, the full `forwarded-request-server.test.ts`, and the composition-root serving round-trip.
Confirmed `asForwardedAccessIntent` already reconstructs `accessIntent` (Step 2), so no `forwarding-io.ts` change is needed.
## Stage: Implementation — TDD (2026-07-18T19:15:00Z)
### Session summary
Executed all three plan steps as planned, no Tidy-First preparatory refactors landed (the `tidy-first-assessor` found the target files already shaped for the change).
Four commits: additive `feat:` (resolver `path-values` acceptance + `buildResolvedIntentFromMatchValues`), the atomic `feat:` serving rework (`ServingPolicy` intent-shaped, legacy branch retired), a `docs:` roadmap-completion commit, and a small follow-up `docs:` commit fixing a stale doc comment the pre-completion reviewer flagged.
Test count 2491 → 2499 (+8).
Pre-completion reviewer returned WARN (one cosmetic finding), fixed before finishing.
### Observations
- **The Step-1 `permission-resolver.test.ts` red was hollow at the runtime level, real at the type level.**
`toResolvedIntent`'s existing `else`-branch already returns any non-`access-path` intent unchanged, so a `path-values` object passed to `resolver.resolve` ran correctly through the *runtime* path before the type widening — the new test passed immediately under `vitest` (esbuild, no typecheck).
The real red was `tsc`: `pnpm run check` failed with `TS2322` on the `path-values` literal until `PermissionResolver.resolve`/`toResolvedIntent` were widened to `AccessIntent | PathValuesAccessIntent`.
Confirmed both reds explicitly (`vitest run` for the runtime file, `pnpm --filter ... run check` for the type-level file) before calling it Red, per the testing skill's "hollow red" warning.
- **Step 2 test rewrite was exactly as mechanical as planned.**
Renamed every `policy: { check }` mock to `policy: { resolve }`, added `accessIntent` to the requests that needed to reach the resolve branch (via the new `makeForwardedAccessIntent` fixture), and split the single "floors a request without display fields" test into two: a fully-empty legacy request and a version-skew request that has `surface`/`value` but no `accessIntent` — the literal ADR 0008 §4 scenario, which the original single test didn't distinguish.
No large-file rewrite needed a lift-and-shift; the whole file fit one atomic commit.
- **Composition-root round-trip anchors held unchanged, as predicted.**
Both `forwarded grant-scope selection round-trip` tests (real `ParentAuthorizer` stamping `accessIntent`, real `ForwardedRequestServer` resolving it, `demo` surface with no per-agent rules) passed without any edit — confirming agent-scoped resolution is a true superset of the agent-neutral serving it replaced.
- **Pre-completion reviewer: WARN, one finding** — a stale doc comment on `ForwardedRequestServerDeps.policy` (`"Recorded-authority resolution for \`(surface, value)\` requests."`) that survived the `ServingPolicy` interface rename.
Fixed in a trivial follow-up `docs:` commit (not amended into the feat commit, since the feat commit was already several commits back and AGENTS.md discourages non-interactive rebase for reordering).
Reviewer explicitly verified the implementation matched the plan's two operator-confirmed deviations from the issue body (retire-outright over dual-path; `feat:` over `feat!:`) rather than flagging them as drift.
- **Reviewer note (non-issue):** the same pre-existing pi-autoformat acceptance-test flake noted in the #596 retro recurred (`pnpm run test` root pass, 2 RPC timeouts, unrelated package, standalone re-run green both before and after this session's changes).
- **Release**: ship now — `cross-session-intent` batch tail (Steps 13).
Next step is `/ship-issue`.
## Stage: Final Retrospective (2026-07-18T22:57:31Z)
### Session summary
Shipped issue #597 end to end in one continuous session — planned Phase 12 Track A Step 3, executed three TDD cycles (no Tidy-First prep needed), handled the pre-completion reviewer's one WARN, then pushed, verified CI, closed the issue, and merged the release-please PR to cut `pi-permission-system` 20.8.0 (the `cross-session-intent` batch, Steps 13).
The change makes the serving node resolve a forwarded permission request against the child-fixed `ForwardedAccessIntent.matchValues` (agent-scoped), retiring the legacy `(surface, value)` re-derivation and structurally dissolving [#565] items 23.
An exceptionally low-friction session — the only substantive user input across all four stages was the two `ask_user` design confirmations at planning time.
### Observations
#### What went well
- **The planning `ask_user` gate caught a real issue-body-vs-accepted-ADR contradiction (novel win).**
The issue body said "keep the legacy `(surface, value)` fallback for version skew," but the operator's own accepted ADR 0008 §4 had already retired that branch (rejecting the "Tolerant dual-path" alternative with rationale).
Surfacing it rather than transcribing the issue produced a materially different — and correct — plan: the operator confirmed "follow the ADR," which drove the whole implementation (sole-resolution-path, `ask`-floor on absence).
This is the "issue body is a hypothesis, not a spec" principle plus "accepted ADR is authority" working exactly as intended.
- **The testing skill's "hollow red" guidance paid off at Step 1.**
The new `permission-resolver.test.ts` case passed immediately under `vitest` (esbuild does not typecheck, and `toResolvedIntent`'s existing `else`-branch already passed a `path-values` intent through), so the runtime red was hollow.
The agent recognized this and confirmed the *real* red via `pnpm run check` (`TS2322` on the `path-values` literal) before implementing — no false-green slipped through.
- **Verification was incremental at every stage.**
Green baseline before any edit; `pnpm run check` after each shared-signature change (the resolver widening in Step 1, the interface rename in Step 2); the target test file after every red→green; full suite + root `lint` + `fallow dead-code` after the last step and again pre-push.
The feedback-loop-gap lens found no deferred-verification gap.
- **The plan's Module-Level Changes matched the actual diff exactly** — the atomic Step 2 scope (server + `index.ts` + fixtures + test suite in one commit) was correctly predicted, and the composition-root round-trip anchors stayed green unchanged, confirming agent-scoped serving is a true superset of the agent-neutral serving it replaced.
#### What caused friction (agent side)
- `other` (doc-comment staleness during an interface rename) — the `ServingPolicy` `check``resolve` rename left the `ForwardedRequestServerDeps.policy` field's doc comment describing the retired `(surface, value)` check.
The plan enumerated two doc comments to update (class-level and `resolveDecision`) but missed the field-level one, and the implementation followed the plan.
Impact: one pre-completion-reviewer WARN and one trivial follow-up `docs:` commit (`64b1b8e1`); no behavior rework.
Self-caught by the reviewer doing exactly its job — the right backstop for cosmetic doc staleness.
- `other` (recurring environmental flake) — the pi-autoformat real-CLI acceptance tests (`test/acceptance.test.ts`) timed out under the concurrent `pnpm run test` root run (2 RPC timeouts), the same flake the #596 retro recorded.
Impact: ~3 confirmation tool-calls per occurrence (standalone re-run to prove it unrelated); no rework, resolved correctly using the #596 retro context.
Now observed across two consecutive sessions — filed as [#618] for a durable fix rather than per-session re-confirmation (see Changes made).
#### What caused friction (user side)
- **None — oversight was minimal and well-placed.**
The operator's only substantive interventions were the two planning-stage `ask_user` confirmations (the legacy-fallback and commit-type decisions), exactly the strategic-judgment calls the workflow reserves for a human; everything else ran unattended from a plan the operator had reviewed.
### Diagnostic details
- **Model-performance correlation** — every rendered parent-session turn ran on `anthropic/claude-opus-4-8` (Planning + Retrospective, judgment-heavy) or `anthropic/claude-sonnet-5` (TDD + Ship, implementation/mechanical) — appropriate allocation, no mismatch.
The `model_change` stream also showed several switches (`deepseek-v4-flash`, `claude-fable-5`, `claude-haiku-4-5`) that never ran a turn (no `[provider/model]` label appears on any rendered turn for them), so no reasoning-weak model executed judgment-heavy work.
The two subagent dispatches (`tidy-first-assessor`, `pre-completion-reviewer`) ran on their configured models for judgment tasks — appropriate.
- **Escalation-delay tracking** — no `rabbit-hole` friction; the hollow-red at Step 1 resolved in a single follow-up tool call (`tsc`), well under the 5-call threshold.
- **Unused-tool detection** — no `missing-context` gaps; planning read every relevant source file directly (the design was fully grounded before the plan was written), so implementation needed no exploratory search.
- **Feedback-loop gap analysis** — verification was incremental at every stage (see "What went well"); no lens found a gap.
### Changes made
1. Filed [#618] (`pkg:pi-autoformat`) to harden the real-CLI acceptance tests against the concurrent-workspace-run RPC timeout flake — the one durable cross-session pattern (#596 + #597).
No `AGENTS.md`/prompt rule was added: the doc-comment miss was a self-caught one-off (reviewer backstop), and documenting the flake as a workaround was rejected in favor of the root-cause fix.
2. `packages/pi-permission-system/docs/retro/0597-serving-resolves-forwarded-intent.md` — appended this Final Retrospective stage entry.
[#618]: https://github.com/gotgenes/pi-packages/issues/618