8.3 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 646 | pi-permission-system: invalid higher-precedence config inherits lower-scope allow rules |
Retro: #646 — pi-permission-system: invalid higher-precedence config inherits lower-scope allow rules
Stage: Planning (2025-06-12T00:00:00Z)
Session summary
Planned the fix for the cross-scope fail-open: an invalid higher-precedence scope (project / agent / project-agent) becomes an empty scope, so mergeScopesWithOrigins inherits the lower scope's rules unchanged and a global allow survives a higher scope meant to deny/ask it.
The issue is third-party (author marcoscale98), so the ask_user gate confirmed direction and design: an always-on allow→ask flooring overlay, triggered by non-global scopes only, shipped as a breaking fix!:.
Wrote a 4-step TDD plan (0646-...) and committed it.
Observations
- The
#547strict-validation "fail-closed" is only correct for a single scope in isolation — an invalid scope's missing surfaces fall to universalask, but a lower scope's explicitallowstill wins. So #646 is a real, unfixed gap, and thestrict-config-validation.mdmigration doc's "falls back to ask — never allow" line is misleading for the cross-scope case (flagged for a doc update). - Clean symmetry hook:
rewriteAsksToYolo(ask→allow,origin: "yolo") is the exact mirror of the plannedfloorAllowsToAsk(allow→ask,origin: "fail-closed").deriveSourcekeys onrule.layer+ tool kind, notorigin, so adding a"fail-closed"RuleOrigindoes not ripple into source derivation — onlyrule.tsand thearchitecture.mdinlineRuleOrigincopy need touching. - The invalid-scope signal is carried by a new optional
ScopeConfig.invalidfield; the loader is the single decision point. For agent scopes,getFileStamp === "missing"distinguishes an absent file (not invalid) from a present-but-unreadable one (invalid) — important so a missing agent file is not mis-clamped. - Apply the overlay at composition (
resolvePermissions), not atcheck(), sogetToolPermission/getComposedConfigRulesreflect the clamp; a flooredallow→askkeeps the tool visible rather than silently allowed. - yolo neutralizes the clamp (floored
ask→allowat check time). This is intentional (yolo is an explicit full-permissive opt-in) and pinned with an invariant test rather than left implicit. - Rejected the harder "refuse to activate / universal deny" option and the opt-out config knob per the operator's
ask_useranswers — proportionateask, always-on.
Stage: Implementation — TDD (2025-06-12T18:00:00Z)
Session summary
Executed the 4-step plan across 6 commits (2 Tidy-First prep + 3 TDD + 1 docs); the full pi-permission-system suite went 2535 → 2555 tests (+20), all green, with check/lint/fallow dead-code clean.
The fix lands as designed: floorAllowsToAsk mirrors rewriteAsksToYolo, the loader marks a present-but-unloadable non-global scope ScopeConfig.invalid, and resolvePermissions floors allow→ask (origin fail-closed) when any non-global scope is invalid, with a strengthened getConfigIssues notice.
Pre-completion reviewer returned PASS.
Observations
- The
tidy-first-assessorrecommended two test-only prep commits, both taken: hoisting the sharedRulefixtures out of therewriteAsksToYolodescribeintest/rule.test.ts(renamed tooverlay*at module scope to sidestep a name collision with differently-shaped block-scoped consts elsewhere in the file), and widening the yolo test'smakeManagerto accept an optionalprojectscope for the fail-closed-under-yolo invariant test. Both kept thefeat/fix!test diffs to new assertions only. - Simulating a present-but-unreadable agent file:
mkdirSynca directory at the agent's.mdpath —statSyncsucceeds (stamp is not"missing") butreadFileSyncthrowsEISDIR, deterministically exercising the invalid-scope catch branch withoutchmodflakiness. - No deviations from the plan; every planned Module-Level Changes file was touched, no schema or lockfile changes, and
#646is not a roadmap step so no✅mark was needed. - Repo-wide
pnpm run testshows 2 unrelated FAILs in@gotgenes/pi-autoformat(test/acceptance.test.ts,test/acceptance-event-bus.test.ts) — e2e tests that spawn a realpiCLI via RPC and time out at 30s; environmental, zero overlap with this change's files. - Pre-completion reviewer: PASS (no WARN findings).
- The
#526yolo deny-preservation and#547single-scope fail-closed invariants are both pinned by tests in this change (fail-closed-under-yolo cases; the untouched#547tests still pass).
Stage: Final Retrospective (2026-07-24T16:52:42Z)
Session summary
Reviewed the Ship and Retrospective stages together.
Ship landed the code cleanly (CI green, issue closed, release-please PR merged), but the release-please CI job failed on a transient GitHub API error after it had already tagged pi-permission-system-v21.0.0 — GitHub's default needs:-skip-on-failure behavior silently dropped the downstream publish job, so the version was tagged/released on GitHub but never published to npm.
The user caught the discrepancy externally, and this session diagnosed the cascade, guided a manual pnpm publish recovery, and manually replicated the skipped last-release-sha write-back.
Observations
What went well
- The third-party protocol held end-to-end across all three prior stages: authorship check (
gh api uservs. issueauthor.login) correctly triggered the Planning-stageask_usergate, and the plan's Goals/Design were driven by the operator's answers rather than transcribed from the issue body. Confirmed this explicitly when the user asked about it mid-retro, by reading the Planning stage notes rather than relying on memory. - The CI-failure diagnosis was methodical rather than reactive: confirmed the GitHub API rate limit was healthy before concluding "transient hiccup" (not a rate-limit issue), inspected step-level (not just job-level) conclusions to isolate
release-pleaseas the failing step, and read the attempt-1 logs before choosing a non-destructive recovery (no force-push, no re-tagging, no speculative retries). - The
last-release-shawrite-back gap was caught proactively as a second-order consequence of the same root cause — not left for a future session to rediscover when the baseline drifted further.
What caused friction (agent side)
missing-context— told the usernpm loginwas "the one legitimate exception to the pnpm-only rule" without first checking whetherpnpmhas its own nativelogin/whoami/publishcommands (it does; no exception is needed). Impact: incorrect guidance the user had to correct; would have propagated a wrong command intoAGENTS.mdif not caught here.missing-context(minor, self-corrected) — reflexively rannpm view/npm whoamibefore the repo's pnpm-only guard blocked it, then switched topnpm view/pnpm whoamiin the same turn. Impact: one wasted tool call, no rework.
What caused friction (user side)
- The Ship session's final report declared the release fully landed based on
release_watchreturning the tag, but arelease-pleasejob can fail after completing its main side effect (tagging), andpublishsilently skips as a result./ship-issuehad no step that would have caught this — the user had to notice externally and open a new message to report it. This is a genuine gap in the prompt's verification coverage, not a user-process gap: the flow verified "CI passed on the shipped commit" and "tag landed," but never "the push-triggered CI run following the release-please merge itself succeeded."
Changes made
AGENTS.md— corrected the first-release manual-publish command (pnpm login+pnpm publish, dropped the incorrect--otp <code>) and added a new paragraph documenting the release-please-fails-after-tagging recovery procedure (manual publish + manuallast-release-shaadvance), referencing this issue..pi/prompts/ship-issue.md— added step6b("Verify the release-triggered CI run"): captures the release merge commit SHA, runsci_find/ci_watchon it, and stops before the Final report ifrelease-pleaseorpublishfailed or was unexpectedly skipped. Also added a matching bullet to the Constraints section.