Files
my-pi/pi-permission-system/docs/retro/0502-per-tool-gate-access-path.md
T

88 lines
8.1 KiB
Markdown
Raw 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: 502
issue_title: "pi-permission-system: migrate the per-tool path-bearing gate onto AccessPath (Phase 7 Step 1)"
---
# Retro: #502 — Migrate the per-tool path-bearing gate onto AccessPath (Phase 7 Step 1)
## Stage: Planning (2026-06-29T00:00:00Z)
### Session summary
Planned Phase 7 Step 1: route the per-tool path-bearing gate (`read`/`write`/`edit`/`grep`/`find`/`ls`) onto `AccessPath` so per-tool rules match lexical canonical, closing the symlink-evasion asymmetry against the `path` surface.
The change is mechanically parallel to [#486] (the `path`-surface migration): the resolver already unwraps `access-path``path-values` and `PATH_SURFACES` already routes the per-tool surfaces through `evaluateAnyValue`, so the only behavior change is the canonical alias joining the match set.
Produced a three-step plan (breaking `feat!:` behavior change, a `refactor:` accessor removal, then docs) at `docs/plans/0502-per-tool-gate-access-path.md`.
### Observations
- **`getPlatform()` removal is forced, not optional, and resolves [#513].**
[#511] is already merged, so [#502] is the *second* of the two `getPlatform()` consumers to fold.
Once the per-tool gate stops reading `platform` (it threaded it only to feed `deriveSuggestionValue`'s `normalizePathForComparison`), the pipeline's `getPlatform()` read is dead and the accessor would trip the `pnpm fallow dead-code` CI gate.
So Step 2 removes `getPlatform()` from `ToolCallGateInputs` + `PermissionSession` + `makeGateInputs`, and [#513] should be closed at ship with a pointer to the [#502] SHA.
- **Scope discriminator:** keyed the `access-path` branch off `getPathBearingToolPath(...) !== null` (built-in six with a present `input.path`).
This deliberately keeps the missing-path case on the `tool` intent so the `normalizeInput` `["*"]` fallback is preserved, and keeps MCP/extension tools on `tool` (their path is already symlink-resistant via the cross-cutting `path` gate since [#486]).
- **Suggestion value is provably unchanged:** `accessPath.value()` equals the old `normalizePathForComparison(path, tcc.cwd, platform)` because the pipeline normalizer is built from the same session `cwd` + `platform`.
Flagged the two [#438] cwd-bounding tests as the invariants to keep green (now passing an injected `AccessPath` built via `new PathNormalizer(...)`).
- **Structural win:** the change removes a parameter relay — `platform` threaded session → pipeline → `describeToolGate` solely to feed one derivation the `AccessPath` the gate already builds now owns (Tell-Don't-Ask via `value()`).
- **Release:** Step 1 of batch "symlink-resistant-path-matching" (tail = Step 3, [#504]); mid-batch → defer.
The breaking `feat!:` lands on `main` and auto-batches; the major-bump release cuts when Step 3 lands.
- Skipped the `ask_user` gate: operator-authored issue, unambiguous proposal, and the only scope addition (`getPlatform()` removal) is forced by the dead-code gate + [#513], not a genuine design choice.
## Stage: Implementation — TDD (2026-06-29T10:15:00Z)
### Session summary
Implemented all three planned TDD steps: the breaking `feat!:` per-tool gate migration to `access-path`, the `refactor:` removal of the dead `getPlatform()` accessor (resolving [#513]), and the `docs:` updates marking Phase 7 Step 1 complete.
Four new tests added (suite 2211 → 2215); full suite, `tsc`, root lint, and `pnpm fallow dead-code` all green.
Pre-completion reviewer returned PASS.
### Observations
- **One unplanned deviation — a stale fallow suppression.**
Adding the explicit `bashProgram: BashProgram | null` annotation in the new `resolvePerToolCheck` helper gave fallow a resolvable receiver for `BashProgram.commands()`, which retired the long-standing `unused-class-member` false-positive suppression in `program.ts`.
Removed it as a focused fourth `refactor:` commit (not in the plan; the plan only listed the touched files).
This is why `fallow dead-code` must be run — the baseline-green checks (check/lint/test) do not catch a now-stale suppression.
- **`describeToolGate` signature change was structurally improved, not just mechanical.**
Swapping the `platform: NodeJS.Platform` parameter for an optional `accessPath?: AccessPath` removed a three-layer parameter relay (Tell-Don't-Ask via `value()`), exactly as the plan predicted.
- **The `tool.test.ts` red was weak; the pipeline test carried the real red.**
Passing an `AccessPath` where the old code expected a `platform` string coincidentally behaved like posix (an object `!== "win32"`), so `tool.test.ts` passed against old code.
The meaningful behavioral red (per-tool gate emits `access-path`; symlink-canonical match blocks) lived in `tool-call-gate-pipeline.test.ts`, which used the established `node:fs` `realpathSync` mock from `path.test.ts`.
- **Pre-completion reviewer: PASS** — no warnings; all cross-step invariants ([#486], [#438], [#510], missing-path fallback) verified preserved by their pinning tests.
- **Remaining for ship:** close [#513] with a pointer to the [#502] SHA (its `getPlatform()` removal is folded into this work); confirm the mid-batch release deferral (batch "symlink-resistant-path-matching", tail = Step 3 [#504]).
## Stage: Final Retrospective (2026-06-29T14:26:40Z)
### Session summary
Shipped Phase 7 Step 1 across plan → TDD → ship in one continuous session: a breaking `feat!:` per-tool gate migration to `access-path`, the folded-in [#513] `getPlatform()` removal, an unplanned stale-suppression cleanup, and docs.
The operator deferred the release (mid-batch); commits landed on `main`, CI passed, and [#513] was closed with a pointer to the [#502] SHA while [#502] stays open until the batch tail (Step 3, [#504]) ships.
A notably clean run — no rework, no rabbit-holes, pre-completion PASS.
### Observations
#### What went well
- **Reading the [#486] plan as a template made planning fast and accurate.**
[#502] was "mechanically parallel to [#486]", so loading the prior plan and the already-migrated `path.ts` / `path.test.ts` gave a ready-made design (the `access-path` intent shape, the `node:fs` `realpathSync` mock convention) and a correct prediction of every invariant at risk.
- **Verifying related-issue state caught the [#513] fold-in.**
Checking that [#511] was already `CLOSED` made [#502] the *second* `getPlatform()` consumer to fold, so the dead-code gate forced the accessor removal into this issue — a scope point that would have surfaced as a CI failure if planned around instead.
- **The fallow gate did its job.**
The post-step `pnpm fallow dead-code` run flagged a now-stale suppression that the baseline check/lint/test triad cannot see; one focused `refactor:` commit cleared it.
#### What caused friction (agent side)
- `other` (weak red) — the Step 1 `tool.test.ts` changes passed against the *old* `describeToolGate`: an `AccessPath` passed where the old signature expected a `platform` string flowed through esbuild untypechecked and coincidentally behaved like posix (an object `!== "win32"`), so the unit test gave a hollow red.
Impact: no rework — the genuine behavioral red lived in `tool-call-gate-pipeline.test.ts` (access-path emission + symlink-canonical block), which was noticed and relied on at the time.
But the `tool.test.ts` red phase was not truly validating the change.
- `missing-context` (minor) — the plan's Module-Level Changes did not anticipate that adding the `bashProgram: BashProgram | null` annotation in `resolvePerToolCheck` would give fallow a resolvable receiver and retire the `program.ts` suppression.
Impact: one extra unplanned `refactor:` commit; self-identified by the gate, no rework.
#### What caused friction (user side)
- None.
The single decision point (mid-batch release deferral) was surfaced early from the plan's `**Release:**` marker via `ask_user` and answered cleanly — the intended handshake.
### Changes made
1. `.pi/skills/testing/SKILL.md` — added a rule under `### Interface and type changes`: a TDD step that changes a parameter's *type* can produce a hollow red (esbuild does not typecheck, so the new-typed argument may coincidentally satisfy the old runtime path); confirm the red exercises the new *behavior*, not just the new signature.