Files
my-pi/pi-permission-system/docs/retro/0493-symlink-traversal-path-rules.md
T

4.1 KiB

issue, issue_title
issue issue_title
493 fix: symlink traversal bypasses path-based permission rules

Retro: #493 — fix: symlink traversal bypasses path-based permission rules

Stage: Final Retrospective (2026-06-28T01:06:38Z)

Session summary

A /plan-issue #493 session that turned into a triage-and-evaluate session: the third-party report claimed symlink traversal bypasses path-based permission rules, but investigation showed the path / external_directory / bash surfaces already match the symlink-resolved (canonical) form via the AccessPath dual-match design (#418, #486), confirmed by 41 passing tests and a live end-to-end demo against the running extension. No plan was committed; instead #493 was closed as already-addressed with an evidence-backed comment, and the one genuinely actionable residual — a misleading external-directory prompt that names the typed path while asserting it is "outside working directory" — was filed as #507.

Observations

What went well

  1. The third-party ask_user direction gate worked exactly as intended: because the issue author (gr3enarr0w) was not the operator, the session treated the proposal as a hypothesis to evaluate rather than a spec, and the codebase evidence (the Phase 7 roadmap, closed #486/#418, and the package skill's own "symlink-resolved aliases" language) short-circuited a redundant plan before any commit.
  2. Novel win — the live reproduction against the running extension was a notably effective verification: creating an actual ./demo-symlink-passwd -> /etc/passwd symlink (lexical path inside CWD, canonical path outside) and attempting a read produced a real deny from the live gate, which is stronger evidence than unit tests alone and directly surfaced the actionable message bug (#507). Using the live extension as the test harness is a pattern worth reaching for on bypass claims.
  3. Filing #507 with the github-voice skill and the captured live-repro evidence kept the misleading-message concern cleanly separated from the (already-solved) bypass claim, rather than conflating them under #493.

What caused friction (agent side)

  1. missing-context — on a claimed security bypass, the agent first leaned on docs and the architecture roadmap and offered the operator a close-vs-plan disposition decision before running the strongest available evidence (the live repro). The operator had to push twice ("So for sure, we've tested this case?" then "Let's just try it here") to escalate from docs-claims → unit tests → live reproduction. Impact: two extra ask_user round-trips before the actionable bug (#507) surfaced; no rework, but the strongest evidence and the real finding both came late and operator-prompted.

What caused friction (user side)

  1. The operator's "Let's just try it here. Create the link. Then try to access it." was the highest-value intervention of the session — it converted an abstract "is this already handled?" into a concrete repro that both proved the protection and surfaced #507. Framed as opportunity: the agent should reach for live reproduction proactively on bypass/vulnerability claims, so this strategic nudge is not needed next time.

Diagnostic details

  1. Feedback-loop gap analysis — the decisive verification (the live symlink repro) ran only after the operator requested it, not on the agent's initiative; the agent's self-driven verification stopped at the unit-test suite. For a "X can be bypassed" claim, the live repro is the primary feedback loop and should run early, before asking for a disposition.
  2. Model-performance correlation — no subagents were dispatched; all work ran in the main session. Exploration was efficient via grep plus the architecture doc, so no Explore/Plan dispatch was warranted (unused-tool and escalation-delay lenses found nothing notable).

Changes made

  1. Added Debugging bullet 5 to .pi/skills/package-pi-permission-system/SKILL.md — reproduce a claimed path/permission bypass against the running extension before concluding it is already handled (a live deny beats unit tests and can surface adjacent bugs, e.g. #493 → #507).