mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
158 lines
16 KiB
Markdown
158 lines
16 KiB
Markdown
---
|
|
issue: 647
|
|
issue_title: "pi-permission-system: permission review logs may persist secrets with inherited file modes"
|
|
---
|
|
|
|
# Retro: #647 — pi-permission-system: permission review logs may persist secrets with inherited file modes
|
|
|
|
## Stage: Planning (2026-07-25T19:52:14Z)
|
|
|
|
### Session summary
|
|
|
|
Planned the response to a third-party security report from `marcoscale98` claiming the permission review log persists secrets with umask-inherited file modes.
|
|
The direction gate ran twice: an initial `ask_user` settled the file-mode half and the mode scope, and the operator deferred the redaction half to free-form discussion, which converged on key-name masking plus ADR 0010.
|
|
Produced `packages/pi-permission-system/docs/plans/0647-owner-only-log-modes-key-name-redaction.md` with six TDD cycles.
|
|
|
|
### Observations
|
|
|
|
- **Measuring the live corpus changed the design.**
|
|
The operator's own review log (6.7 MB, 8380 lines, mode 0644) was probed for the shapes a value-based redactor would target.
|
|
`sk-` had 403 hits, of which 356 were the tail of `task-approval` and 275 of `task-user`; `xox` matched inside a tool-use id.
|
|
True positives: zero.
|
|
That evidence, not an argument from principle, is what retired the provider-prefix list — and it belongs in the ADR so the next reporter is triaged against data.
|
|
- **A structural fact invalidated the obvious design.**
|
|
The first instinct was a single redaction choke point at `writeLine` in `logging.ts`.
|
|
Reading `permission-prompter.ts:135` showed `toolInputPreview` arrives there already flattened to a string by `serializeToolInputPreview`, so that choke point would have missed the reporter's literal repro (`authorization: "Bearer TEST_VALUE"` as a tool-input field).
|
|
The plan therefore redacts at two points, and the reason is written down rather than left to be rediscovered.
|
|
- **Ecosystem precedent settled a question the operator flagged as outside their experience.**
|
|
Pino's `redact`, Winston's formats, and Serilog's destructuring policies are all declarative key-path masking; secret *detection* is a separate product category (gitleaks, trufflehog).
|
|
Naming that precedent turned "I have no experience engineering this" into a bounded, fifteen-line decision.
|
|
- **A downstream `registerLogRedactor` seam was considered and declined.**
|
|
It would have mirrored the three existing registries exactly, so novelty was low — but it would ship with zero consumers, which the package skill's maintenance-trap rule explicitly targets, and the operator confirmed they would not consume it.
|
|
- **Grammar-anchored bash redaction was costed, not filed.**
|
|
Masking the value side of a `variable_assignment` in the existing tree-sitter parse would extend coverage to `FOO_TOKEN=abc deploy` with near-zero false positives, reusing #481/#645 machinery.
|
|
Recorded in the ADR as the option a future report reopens; deliberately not filed as an issue, to avoid a speculative backlog entry.
|
|
- **Two grep findings would have bitten implementation.**
|
|
`test/tool-input-preview.test.ts` and `test/tool-preview-formatter.test.ts` mock `safeJsonStringify` by **relative** specifier (`../src/logging.js`), not the `#src/` alias — an alias-only grep misses both, and a missed retarget fails at run time rather than under `tsc`.
|
|
Separately, `safeJsonStringify`'s cycle / `Error` / `bigint` handling has no test at all, so the step-1 move needs characterization tests written first.
|
|
- **Scope held.**
|
|
`config-store.ts`'s config write was offered in the mode-scope gate and not selected; forwarding request/response files were.
|
|
Those files get modes but not redaction, since the parent reads them to render the ask-prompt — the same reason the prompt path itself stays unredacted.
|
|
- Classified non-breaking (`fix:`): the review log is a diagnostic artifact with no documented consumer contract, and the docs never guaranteed verbatim payloads.
|
|
|
|
## Stage: Implementation — TDD (2026-07-25T20:25:08Z)
|
|
|
|
### Session summary
|
|
|
|
Executed all six planned TDD cycles plus one Tidy-First preparatory commit, landing owner-only file modes for both JSONL logs and the permission-forwarding artifacts, key-name redaction at two application points, and ADR 0010.
|
|
Eight commits total; test count went from 2603 to 2665 (+62) across 127 → 130 files.
|
|
Pre-completion reviewer returned PASS on every section.
|
|
|
|
### Observations
|
|
|
|
- **The reds were real measurements, not ceremony.**
|
|
Step 4's red reported `expected 420 to be 384` and `expected 493 to be 448` — that is `0o644` and `0o755`, reproducing the reporter's exact claim about umask-inherited modes before a line of the fix existed.
|
|
Step 3's red reproduced the literal repro from the issue body.
|
|
- **The two-application-point design was load-bearing, and the plan was right to insist on it.**
|
|
Redaction at `writeLine` alone would have left the reporter's own repro unfixed, because `getToolInputPreviewForLog` flattens tool input to a string before the writer ever sees its keys.
|
|
The reviewer independently confirmed both points are covered and that no log write path bypasses `writeLine`.
|
|
- **The `vi.mock` partial-module trap fired exactly where the testing skill warns.**
|
|
`test/tool-preview-formatter.test.ts` replaced `#src/json-safe-stringify` with a literal factory exporting only `safeJsonStringify`, which would have blanked out the `createJsonSafeReplacer` that `log-redaction.ts` builds on.
|
|
Fixed with an `importActual` spread.
|
|
Three existing `formatGenericToolInputForLog` tests also had to move to real serialization, since the log path no longer routes through the mocked prompt-path serializer — a net improvement, as they now assert real behavior.
|
|
- **Characterization tests before the move paid off immediately.**
|
|
`safeJsonStringify`'s cycle / `Error` / `bigint` handling had zero coverage because both consumers mocked it away.
|
|
Writing the eight tests first surfaced an undocumented quirk worth pinning: a repeated *non-cyclic* reference is also marked `[Circular]`, because `seen` entries are never released.
|
|
- **Deviation: `test/extension-config.test.ts` was touched but not named in the plan.**
|
|
The logs-directory mode assertions had to live there, because `ensurePermissionSystemLogsDirectory` is in `src/extension-config.ts` and `test/logging.test.ts` supplies its own `ensureLogsDirectory` callback, so it cannot exercise the real one.
|
|
The plan's Module-Level Changes should have caught this.
|
|
- **Deviation: an extra `docs:` commit for a distribution gap the plan missed.**
|
|
`configuration.md` and `troubleshooting.md` ship in the npm tarball; `docs/decisions/` does not, so the ADR links would have been dead for anyone reading the installed package.
|
|
Resolved by following the absolute-GitHub-URL precedent already set in `docs/subagent-integration.md` rather than adding `docs/decisions` to the `files` allowlist, which would ship ten internal design records to serve one user-facing reference.
|
|
Verified with `pnpm pack` + `tar tzf`.
|
|
- **The permission gate caught an agent mistake mid-session.**
|
|
An `Edit` call dropped the `pi-packages/packages/` prefix from a path; the `external_directory` gate blocked it and named the correct location.
|
|
A live demonstration of the thing being hardened.
|
|
- **Tidy-First assessor was well-scoped.**
|
|
One recommendation (extract a shared temp-dir fixture in `test/logging.test.ts`, which was about to gain two new scenarios), and its rejected list correctly declined three in-scope-but-unobstructive modules.
|
|
It also recognized that the plan's own step-1 sequencing already *was* the tidy-first move for the riskiest friction rather than re-proposing it.
|
|
- Pre-completion reviewer: **PASS**, no warnings.
|
|
It verified `isSensitiveLogKey` against every real key name the package logs and found no false positive — including confirming that the bash parser's internal `token` field is never logged directly.
|
|
|
|
## Stage: Final Retrospective (2026-07-26T00:58:10Z)
|
|
|
|
### Session summary
|
|
|
|
Single session carrying #647 from a third-party security report through planning, six TDD cycles, and release as `pi-permission-system-v23.0.2`.
|
|
Nine implementation commits landed owner-only file modes, key-name log redaction at two application points, and ADR 0010; the pre-completion reviewer returned PASS and both CI runs were green.
|
|
The decisive moment was not in the code but in the design gate, where the operator declined a four-option menu and asked a question that produced a better answer than any option on it.
|
|
|
|
### Observations
|
|
|
|
#### What went well
|
|
|
|
- **Measurement replaced argument at the design gate.**
|
|
Rather than reasoning about whether a secret-shape redactor would work, the planning stage probed the operator's live 6.7 MB review log: 403 `sk-` hits of which 356 were the tail of `task-approval`, `xox` inside a tool-use id, and zero true positives.
|
|
That single command retired an entire design direction and became the ADR's evidence table.
|
|
The same instinct carried into the reds — `expected 420 to be 384` is `0o644` versus `0o600`, so the failing test *was* the bug report.
|
|
- **A structural reading of the code invalidated the obvious design before it was built.**
|
|
Reading `permission-prompter.ts:135` showed `toolInputPreview` arrives at the writer already flattened to a string, so the natural single-choke-point design would have shipped without fixing the reporter's own repro.
|
|
Catching this at plan time rather than at review time is what made the two-application-point design deliberate instead of a patch.
|
|
- **`pnpm pack` caught a distribution bug that every other gate missed.**
|
|
`check`, `lint`, `test`, `fallow`, and the pre-completion reviewer were all green with two dead documentation links in the shipped tarball.
|
|
Only unpacking the artifact surfaced it.
|
|
- **The permission gate under test blocked a real agent mistake.**
|
|
An `Edit` dropped the `pi-packages/packages/` prefix and the `external_directory` gate refused it, naming the correct path.
|
|
A live demonstration of the subject matter, mid-implementation.
|
|
|
|
#### What caused friction (agent side)
|
|
|
|
- `missing-context` — the first redaction `ask_user` offered four options (no redaction, grammar-anchored bash redaction, grammar-plus-shape-list, metadata-only logging), all constructed from first principles.
|
|
None of them was key-name masking — the boring, fifteen-line, zero-maintenance technique that pino's `redact`, Winston's formats, and Serilog's destructuring policies all implement, and the one that actually shipped.
|
|
The ecosystem precedent was never checked before the option set was built.
|
|
Impact: one extra `ask_user` round-trip, and the correct answer arrived only because the operator asked "is there a low-hanging technical implementation that is very common?"
|
|
No rework — but the design gate was one question away from converging on a worse option.
|
|
- `instruction-violation` (self-identified, at retro) — `/plan-issue` directs loading the `colgrep` skill before code exploration and the `design-review` skill before finalizing any design that extracts or changes shared interfaces.
|
|
Neither was loaded; the plan extracted `safeJsonStringify` into a new module, which is squarely a `design-review` trigger.
|
|
Impact: no rework.
|
|
Exploration was mostly exact-symbol tracing where `grep` was the right tool, and the `tidy-first-assessor` plus the pre-completion reviewer independently covered the structural ground `design-review` would have.
|
|
The gates were skipped without consequence, which is precisely why it is worth noting.
|
|
- `other` — an `Edit` call constructed the path `/Users/chris/development/pi/pi-permission-system/test/extension-config.test.ts`, dropping the `pi-packages/packages/` segment.
|
|
Impact: one rejected tool call, corrected immediately; zero rework.
|
|
The `external_directory` gate caught it, so the blast radius was a single retry rather than a file written outside the repo.
|
|
- `other` — the plan's Module-Level Changes did not name `test/extension-config.test.ts`, which had to absorb the logs-directory mode assertions because `test/logging.test.ts` stubs `ensureLogsDirectory` and cannot exercise the real one.
|
|
Impact: a noted deviation, no rework.
|
|
The plan reasoned about which `src/` module changed but not about which test file could actually reach it.
|
|
|
|
#### What caused friction (user side)
|
|
|
|
- Nothing that cost time — and one intervention worth naming as a model.
|
|
When the redaction `ask_user` presented four options, the reply was "I don't know yet, let's discuss more free-form for now," followed by a question about what a downstream package could do and whether a common low-hanging implementation existed.
|
|
That refusal to pick from a bad menu is what surfaced key-name masking.
|
|
The generalizable lesson is agent-side, not user-side: an option set is itself a design artifact and can be wrong in ways none of its options reveal.
|
|
|
|
### Diagnostic details
|
|
|
|
- **Model-performance correlation** — planning and TDD ran on `anthropic/claude-opus-5`, ship on `anthropic/claude-sonnet-5`, retro on `anthropic/claude-opus-5`; that split matches task weight (judgment-heavy design and implementation on the stronger model, deterministic push/CI/merge choreography on the cheaper one).
|
|
Two subagents: `tidy-first-assessor` and `pre-completion-reviewer`, both judgment tasks, both appropriately modelled.
|
|
The session's model-change log also records switches to `opencode-go/deepseek-v4-flash`, `anthropic/claude-fable-5`, and `anthropic/claude-haiku-4-5`, which fall in the session's earliest segment — outside the readable transcript window, so which turns they ran could not be confirmed.
|
|
Worth checking if planning quality ever regresses: the initial issue triage is judgment-heavy and is exactly where a flash-tier model would hurt.
|
|
- **Escalation-delay tracking** — no `rabbit-hole` friction points, so no escalation delay to measure.
|
|
The longest same-topic sequence was five calls polling the release PR's `statusCheckRollup`; that was correct blocking behavior on a genuinely in-progress check, not a stall.
|
|
- **Unused-tool detection** — `colgrep` was available and never invoked.
|
|
Most exploration was exact-symbol tracing (`safeJsonStringify`, `appendFileSync`, `writeJsonFileAtomic`) where `grep` is the correct choice per the `colgrep` skill's own decision table.
|
|
The exception is the question that drove the whole design — "where does tool input get serialized on the way to the log?"
|
|
— which is intent-shaped and was answered by manually reading four files.
|
|
- **Feedback-loop gap analysis** — no gap.
|
|
`pnpm run check` plus the full package suite ran after every one of the five code-bearing TDD steps, not just at the end; each cycle confirmed a red before implementing; `pnpm run lint` ran at baseline, after the extraction, after the docs commit, and again pre-push.
|
|
The one gate that ran only once was `pnpm pack`, and it was the one that found a bug — an argument for running it whenever a shipped doc gains a link.
|
|
|
|
### Changes made
|
|
|
|
1. `.pi/prompts/plan-issue.md` — appended a sentence to "Gather context" step 6: when a change introduces a mechanism a mature ecosystem already standardizes, check what established libraries do before building the `ask_user` option set.
|
|
Step 6 already covered internal convention discovery (sibling packages, SDK terms); this is its external analog.
|
|
2. `AGENTS.md` — added a sentence to the docs-in-distribution convention: a link from a shipped doc into a non-shipped path resolves to nothing in the tarball, so use an absolute GitHub URL or add the target to `files`.
|
|
3. `packages/pi-permission-system/docs/retro/0647-owner-only-log-modes-key-name-redaction.md` — this Final Retrospective stage entry.
|
|
|
|
Considered and deliberately not landed: a prompt nudge to actually load the six skills `/plan-issue` lists (compliance failure, not a clarity failure), another "when a step changes X, grep Y" rule for the missed test file (that section already carries ~20), and a rule about running `pnpm pack` routinely (AGENTS.md already directs it; the real gap was link direction, which change 2 covers).
|