11 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 418 | [Bug] Even though "Allow" is configured, the permission system still prompts for confirmation on access requests |
Retro: #418 — Even though "Allow" is configured, the permission system still prompts
Stage: Planning (2026-06-17T14:17:37Z)
Session summary
Diagnosed the reported false external-directory prompt as a symlink-vs-pattern-matching bug: both external-directory gates resolve /tmp → /private/tmp (the macOS symlink) before matching, so the user's /tmp/* pattern never hits.
The actual firing surface in the report is the bash gate (toolName: "bash", ls -la /tmp/), driven by BashProgram.externalPaths returning the canonical path; the tool gate (describeExternalDirectoryGate) has the same defect via canonicalNormalizePathForComparison (whose own docstring says "not for pattern matching").
Produced a 6-step TDD plan that matches external_directory patterns against both the typed and the symlink-resolved forms as aliases, keeping the canonical path only for the outside-CWD boundary and infra-read checks.
Observations
- This is a third-party issue (
lipaysamart); ran theask_userdirection gate. Operator chose fix it and match both typed and resolved forms (not lexical-only). - Deliberately reused the existing resolver surface by adding an optional
surfaceparam toresolvePathPolicy/checkPathPolicyrather than adding a new method — architecture.md lines 594–595 flag resolver-surface widening as a risk, andevaluateAnyValue(last-match-wins across aliases) is already wired forPATH_SURFACES, so the alias mechanism is free. - Kept
BashProgram.externalPaths(): string[]shape (value semantics change canonical → lexical, dedup identity stays canonical) to avoid churning its 29 test references; most use synthetic non-existent paths wherecanonicalizePathno-ops. - Flagged the #393 false-green risk: the gates now resolve through
checkPathPolicy, somakeHandlermust route theexternal_directorysurface ontocheckPathPolicyormakeSurfaceCheck-driven tests silently passallow. The step-5 real-instance acceptance test (real tmpdir symlink) is the backstop. - Noted a security upside worth keeping in the commit body: the fix also closes a silent-allow hole where a symlinked deny (
/tmp/*: deny) previously fell through to the*fallback. - The tool gate gains a
resolverparameter (mirroringdescribePathGate); itsinputbecomes{}and it carries apreCheck, like the bash gate already does. - Distinct from #413 (docs-only discoverability of the
external_directoryallow-list): #418 is a genuine matching bug where the right surface and pattern were already configured.
Stage: Implementation — TDD (2026-06-17T14:53:29Z)
Session summary
Implemented the fix across 6 commits (the plan's 6 TDD steps), though steps 3 and 4 were merged into one fix: commit (see Observations).
The full suite went from 2003 to 2015 tests (+12, +1 new acceptance test file); pnpm run check, pnpm run lint, and pnpm fallow dead-code are all clean.
Both external-directory gates now match a path's typed and symlink-resolved aliases on the external_directory surface, fixing the reported /tmp/* false prompt while keeping the canonical path for the outside-CWD boundary.
Observations
- Steps 3 and 4 merged.
The plan listed the bash gate (step 3) and tool gate (step 4) as separate commits, but the
external-directory-session-deduptest couples them: a bash command approves a directory for the session, then areadmust reuse that approval. Because step 3 moved the bash approval pattern from the canonical (/private/tmp/*) to the lexical (/tmp/*) namespace, the tool gate had to move to the same namespace in the same commit or the cross-tool dedup test would fail with a green suite. Folded both into onefix:commit with the rationale in the body. - #393 false-green bit twice.
Two integration tests (
external-directory-session-dedup.test.ts,tool-call.test.ts) and the dedup shutdown test silently passedallowonce the gates routed throughcheckPathPolicy. Fixed by threading thesurfacearg throughmakeHandler'scheckPathPolicydispatcher, and by adding a delegatingcheckPathPolicymock to the two inline handlers in the dedup test that overridepermissionManager.checkPermissiondirectly (not via the session bag). The full suite — not the edited file — was the only thing that caught these, exactly as the package skill warns. - Acceptance test fixture artifact.
The real-symlink acceptance test's "allow keyed on the resolved path" case initially failed on macOS because
mkdtempreturns an unresolved/var/folders/...path whilerealpathSyncresolves/var→/private/var. Fixed by keying that one config pattern onrealpathSync(realDir). The typed-path and bash cases needed no such adjustment. - No new resolver method.
Generalized the existing
resolvePathPolicy/checkPathPolicywith an optionalsurfaceparam (default"path") rather than adding a method, honoring the architecture's resolver-surface-widening risk note.gate-fixtures.tsneeded no change — itsvi.fn<ScopedPermissionResolver["resolvePathPolicy"]>()stubs picked up the new optional param automatically. - Pre-completion reviewer: PASS — all deterministic checks green, docs/architecture/SKILL alignment verified, Mermaid diagrams validated, cross-step invariants (#352, #393, bash config-deny, canonical boundary) confirmed test-pinned. No WARN findings.
Stage: Final Retrospective (2026-06-17T15:07:00Z)
Session summary
Shipped the #418 fix end-to-end in one continuous session: plan → a mid-stream architecture design exchange → 6-step TDD implementation → ship (released pi-permission-system 13.2.0).
Both external-directory gates now match a path's typed and symlink-resolved aliases, fixing the reported /tmp/* false prompt; the full suite grew 2003 → 2015 and the pre-completion reviewer returned PASS.
The session also produced durable roadmap documentation: #418 was registered in architecture.md as the access-path probe for the deferred access-intent extraction.
Observations
What went well
- Architecture breadcrumb from a mid-stream question.
The user's "are there architectural improvements that would make this easier" question (between planning and TDD) surfaced the path-representation conflation as the root smell and connected it to the existing access-intent roadmap item.
"Bolster the documentation with this issue" turned that insight into a persistent breadcrumb (
architecture.md§ Remaining design work now cites both #393 and #418). A future session inherits the structural framing for free — a novel, durable win beyond the bug fix itself. - Real-symlink acceptance test.
external-directory-symlink-acceptance.test.tsexercises the fix through a realsymlinkSync+ realPermissionManager/PermissionResolver, not a mockedrealpathSync. This is the backstop the plan promised for the #393 false-green class and it caught the macOS/var→/private/varnesting subtlety. - Full suite as the false-green backstop. Running the full suite (not just edited files) after the shared-helper change caught three silent-allow regressions the edited test file alone would have passed — exactly the failure mode the package skill warns about.
What caused friction (agent side)
missing-context(planning) — the plan split the bash gate (step 3) and tool gate (step 4) into separate commits, but they share the session-approval namespace: step 3 moved the bash approval pattern from the canonical to the lexical form, so the cross-tool dedup test (bash approves,readreuses) could not pass until the tool gate moved too. The plan noted the namespace coupling in prose but did not translate it into step ordering. Impact: steps 3 and 4 merged into onefix:commit mid-TDD; no rework beyond the merge, caught immediately by the full suite.missing-context(planning) — the plan flagged the #393 false-green risk and namedmakeHandlerrouting, but missed the two inline test handlers that mockpermissionManager.checkPermissiondirectly (bypassing the session bag andmakeHandler's dispatcher). Impact: two extra test-helper edits discovered via full-suite failures during TDD; low rework (the suite caught them), and the gap is now documented in the package skill.instruction-violation(self-identified, recurring) — used a non-existentoldText2/newText2/oldText3shorthand on theEdittool three times (steps 1, 3, 4) instead of separateedits[]entries, despite the system-prompt rule to use one call with multipleedits[]entries. Impact: three rejectedEditcalls, each retried immediately; no downstream rework, but the same mechanical slip recurred rather than being learned after the first rejection.missing-context— the acceptance test's "allow keyed on the resolved path" case failed once becausemkdtempreturns an unresolved/var/folders/...path whilerealpathSyncresolves/var→/private/var. Impact: one test iteration; fixed by keying that pattern onrealpathSync(realDir).
What caused friction (user side)
- None.
The third-party direction gate (
ask_user: fix + match both forms) and the mid-stream design question were both well-timed and moved the work forward; the latter produced durable documentation value rather than a correction.
Diagnostic details
- Model-performance correlation — the
pre-completion-reviewersubagent ran onanthropic/claude-sonnet-4-6(its frontmatter alias), appropriate for a deterministic-check-plus-judgment-checklist review; no mismatch. The main session ran mostly onanthropic/claude-opus-4-8(judgment-heavy planning, design, and architecture work) with oneclaude-sonnet-4-6segment; no high-cost-on-mechanical or reasoning-weak-on-judgment mismatch surfaced. - Feedback-loop gap analysis — verification ran incrementally: each TDD step ran its affected test file,
pnpm run checkran after every shared-interface change before committing, and the full suite ran after step 3 (the shared-helper change), which is what caught the three #393 false-green regressions. No end-only verification gap. - Escalation-delay / unused-tool — no
rabbit-holefriction; no sequence exceeded a couple of tool calls on the same error, so no subagent-dispatch or tool-gap finding.
Changes made
.pi/skills/testing/SKILL.md— added a rule under### Step sequencing and breakage(after the "moves when a value becomes available" rule): a step changing the format of a runtime-recorded value replayed by a different consumer must fold every producer and consumer of that namespace into one commit, since only a cross-consumer runtime test catches the mismatch. Generalizes the steps-3/4 session-approval-namespace coupling from this session.packages/pi-permission-system/docs/retro/0418-external-directory-symlink-pattern-matching.md— this Final Retrospective stage entry.