10 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 352 | Add access intent extractors for path-aware extension tools |
Retro: #352 — Add access intent extractors for path-aware extension tools
Stage: Planning (2026-06-12T02:53:21Z)
Session summary
Planned the response to third-party PR #352 (moekyo) — the issue number is itself an open PR (branch feature/path-aware-extension-tools, +1105/−52, 29 files).
The operator used /plan-issue as a stand-in for PR review.
After a four-question ask_user dialogue, the agreed direction is to adopt the capability with a simplified design, default-on, minimal scope: close the path-gating bypass for extension/MCP tools via the cross-cutting path and external_directory surfaces, expose registerToolAccessExtractor() on the cross-extension service, and defer per-tool path maps.
Produced docs/plans/0352-access-intent-extractors-path-aware-tools.md with five TDD cycles.
Observations
- The real gap: path gating only recognizes six hardcoded built-ins (
PATH_BEARING_TOOLS);getPathBearingToolPathreturnsnullfor everything else, so thepathandexternal_directorygates skip extension/MCP path tools — a genuine permission bypass. - Key design critique of the PR that drove the simplification: its
ToolAccessIntentenvelope carriesresource/operation/confidence/source/toolName, but only.valueis ever consumed by a gate (resourceandconfidenceeach have a single inhabitant). The package skill flags exactly this ("any declared config field not read at runtime is a maintenance trap"), so the plan collapses the envelope to a value-only extractor(input) => string | undefined. - Detection is by convention, not registration: replacing
getPathBearingToolPath's earlyreturn nullwith aninput.pathfallback makes any non-bash tool path-aware automatically;registerToolAccessExtractoris only the escape hatch for non-standard input shapes (MCParguments.pathis handled inline). Confirmed with the operator that this needs no cooperation from extensions. - Scope decision (operator-driven): start with the cross-cutting
path/external_directorysurfaces only and defer per-tool path maps ("ffgrep": { "*.env": "deny" }). Verified this is cleanly additive later — the per-tool feature only adds threading throughnormalizeInput/PermissionManager, reusing the same registry, extractor type, and service API with no rework. This keeps the change out of the hotnormalizeInput/PermissionManagerpath and its large test surface. - Minimal-scope blast radius is concentrated:
getPathBearingToolPathstays unchanged (still used bytool.tsfor cosmetic suggestion/log values); a newgetToolInputPathis consumed only by the two cross-cutting gates, threaded via the pipeline exactly like the existingcustomFormatters. - The registry mirrors the proven
ToolInputFormatterRegistryone-for-one (ISPLookup/Registrarsplit, dup-throw, identity-guarded disposer, one instance shared betweenLocalPermissionsServiceregistrar and pipeline lookup) — idiomatic, low-risk plumbing. - Classified breaking (
feat!:+BREAKING CHANGE:footer): extension/MCP path tools previously ungated become gated on upgrade without a user edit. It is a security fix; the schemamarkdownDescriptionanddocs/configuration.mddocument it. - No new config field (registration is a runtime API), so the loader /
PermissionSystemExtensionConfig/ merge intermediate are untouched — avoids the #332/#347 merge-drop bug class entirely. - Follow-up to file: per-tool path maps for extension tools (deferred); reference it in the PR #352 close comment.
- Attribution is required and encoded in the plan's
## Attributionsection: since we re-implement rather than merge, every implementation/docs commit carriesCo-authored-by: moekyo <shigotods@outlook.com>(from the PR's commit authorship), and the ship-stage close comment thanks@moekyoby name and links the implementing SHA(s).
Stage: Implementation — TDD (2026-06-12T03:20:40Z)
Session summary
Implemented all five TDD cycles cleanly: the lean ToolAccessExtractorRegistry, getToolInputPath, the default-on feat! gate change threaded into ToolCallGatePipeline + both cross-cutting gates, the registerToolAccessExtractor service API, and the docs/schema updates.
Test count went 1922 → 1951 (+29); full suite, check, lint, and fallow dead-code all green.
Every implementation/docs commit carries the Co-authored-by: moekyo <shigotods@outlook.com> trailer.
Observations
- The design held exactly as planned — no rework.
Keeping
getPathBearingToolPath(built-in only, fortool.ts's cosmetic suggestion/log values) and adding a separategetToolInputPathfor the two cross-cutting gates kept the blast radius tight and left the per-tool surface (normalizeInput/PermissionManager) untouched as scoped. - One deviation beyond the plan's file list:
test/service-lifecycle.test.tsalso constructs aPermissionsServicefake, so addingregisterToolAccessExtractorto the interface required addingregisterToolAccessExtractor: vi.fn()there too (folded into Cycle 4). The plan listed the other fakes but missed this one — the interface-breaks-all-implementers rule caught it. - Cycle 3 commit churn: the pre-commit
eslinthook auto-removed(x as string)assertions (unnecessary aftertypeofnarrowing) andbiomereflowed the resulting ternary, aborting the commit twice. Resolved by removing the redundant parens and re-staging the formatted output. Worth pre-empting next time: writetypeof x === "string" ? x : undefined(no cast) from the start. - The full suite (not just the affected files) was run before the
feat!Cycle 3 commit since it changes shared gate behavior — caught nothing, but the right call for a breaking change. - Pre-completion reviewer verdict: WARN (no FAILs).
Two non-blocking findings: (1) the
package-pi-permission-systemskill was stale — fixed in commit30824366(added the extractor-registry / default-on note + testing bullet); (2) the two planning-stage docs commits (d7e881ac,1eece29b) lack theCo-authored-bytrailer — accepted as-is, since moekyo did not author the plan prose and all five implementation commits plus the eventual close comment carry the attribution.
Stage: Final Retrospective (2026-06-12T03:33:14Z)
Session summary
Shipped #352 end-to-end (TDD → Ship): closed the path-gating bypass for extension/MCP tools, released pi-permission-system-v12.0.0 (a major bump from the feat!), and closed the PR-issue with an explicit @moekyo credit comment.
The ship stage was clean (CI green, release-please UNSTABLE-no-checks case handled correctly).
The attribution mechanics worked: every implementation/docs commit carried the Co-authored-by: moekyo <shigotods@outlook.com> trailer and the close comment thanked @moekyo by name and linked the SHAs.
Observations
What went well
- The plan's simplified design held through implementation and ship with zero rework — the lean value-only extractor and the decision to leave
normalizeInput/PermissionManageruntouched kept the diff tight and reviewable for a security-sensitive package. - The third-party attribution workflow (encode
Co-authored-byin the plan → carry on every commit → credit in the close comment) executed cleanly and is a reusable template for adopting external contributions without merging them. - Release automation handled the major version bump (
12.0.0) correctly from thefeat!+BREAKING CHANGE:footer; theUNSTABLE/empty-rollupGITHUB_TOKENmerge case was recognized and merged without blocking.
What caused friction (agent side)
other(Edit tool-schema misuse) — repeatedly included a non-schemaendTextproperty inEditcalls (4+ times this session, plus earlier in the broader work); each was rejected and retried. Impact: ~1–2 wasted tool calls per occurrence, no code impact. No project-doc remedy — this is tool-call hygiene, not a convention gap.other(linter/hook churn) — in Cycle 3, a redundant(x as string)after atypeof x === "string"narrowing in test mocks was auto-stripped by ESLint'sno-unnecessary-type-assertion, thenbiomereflowed the resulting ternary, aborting the pre-commit hook twice. Impact: ~4 extra tool calls (paren cleanup + two re-stage/re-commit cycles). Repeatable gotcha worth a one-line AGENTS.md note (proposed).
What caused friction (user side)
- None blocking.
The earlier "no
/pr-reviewtemplate" remark surfaced a real structural gap (below) rather than friction.
Follow-ups
- A dedicated
/pr-review(or contribution-triage) prompt template: third-party PRs arriving as issues is now a recurring pattern (#389graelo,#352moekyo), and/plan-issueis being overloaded as a stand-in. A purpose-built template would standardize the evaluate-vs-adopt decision, the attribution mechanics, and the simplified-re-implementation path. Substantive (a new prompt) — open an issue and run/plan-issue, do not add inline here.
Diagnostic details
- Feedback-loop — clean and incremental:
pnpm run check/biomeran after each interface-touching cycle (3, 4), and the full suite ran before thefeat!Cycle 3 commit (correct for a breaking change). No end-only verification gap. - Escalation-delay — the Cycle 3 hook churn was 2 aborted commits / ~4 tool calls, under the 5-call threshold; resolved by removing the cast rather than fighting the hook.
- Model-performance — one subagent (pre-completion-reviewer) on
claude-sonnet-4-6, appropriate for judgment-heavy review; returned WARN with two correct findings.
Changes made
- Created
.pi/prompts/pr-review.md— a new/pr-reviewslash-command template (operator-directed, using #352 as the worked example). It evaluates a third-party PR, separates the underlying problem from the implementation, runs a required third-partyask-userdirection gate (adopt-as-is / adopt-with-simplified-design / decline), bakes in theCo-authored-by+ close-comment attribution mechanics, and — in the common case — records a triage note and hands off to/plan-issuerather than merging. - Added a
PR reviewrow to the session-naming table inAGENTS.md(#N PR Review — <title>) so the convention table stays consistent with the new template'sset_session_namecall. - Did not add the redundant-cast gotcha to
AGENTS.md(the other ask-user option) — the operator redirected to building/pr-review; left here as a recorded observation only.