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

14 KiB
Raw Blame History

issue, issue_title
issue issue_title
597 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 matchingmatchValues() 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 checkresolve 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 ServingPolicyinterface rename. Fixed in a trivial follow-updocs: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:overfeat!:`) 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 checkresolve 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.