<objective> Audit redaction and append-only log plus the tool-input gate gap coverage. Runs in wave 1 parallel with 02-01 and 02-02; touches only audit and contract-test files.
Purpose: Prove no host secret reaches the audit file and that the schema boundary rejects every D-40 shape the Phase 1 tests do not already cover. Output: Two source files and three test files covering AUD-01 and TOOL-01. </objective>
<execution_context> @~/.claude/gsd-core/workflows/execute-plan.md @~/.claude/gsd-core/templates/summary.md </execution_context>
<context> @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/02-policy-state-and-filesystem-lockdown/02-CONTEXT.md @.planning/phases/02-policy-state-and-filesystem-lockdown/02-PATTERNS.md </context>
<tasks>
<task type="checkpoint:decision"> <name>Checkpoint: confirm the audit log entry format (D-42 one-way)</name> <files></files> <read_first> - .planning/phases/02-policy-state-and-filesystem-lockdown/02-CONTEXT.md (D-41 redaction layers, D-42 entry fields and modes) </read_first> <action>Confirm before the log writer lands: entries append to <stateDir>/audit/<sessionId>.jsonl with mode 0600 via O_APPEND, one JSON object per line, carrying at, sessionId, tool, outcome (ok or an error code), path as canonical relative path when present, bytes, and sha256; write entries never include file content; exec entries record argv redacted or shell script length and sha256 only; redaction runs before the append. Phase 7 review consumes this format, so later changes need agreement, which is why this gate exists.</action> <verify> <automated>MISSING — human decision required before the audit log writer lands</automated> </verify> <done>Human confirms the D-42 entry format, location, and modes, or records an amendment.</done> </task>
<task type="tdd"> <name>Task 1: Redaction and append-only audit log</name> <files>src/audit/redact.ts, src/audit/log.ts, test/audit/redaction.test.ts, test/audit/log.test.ts</files> <read_first> - src/contract/validate.ts (privacy-rule header: name keys and pointers, never values; redact.ts follows the same model) - src/core/states.ts (freezeEdges pattern for the frozen redaction pattern constants) </read_first> <behavior> - Test: key-based replacement for token, secret, password, passwd, api-key, authorization, cookie, credential, private-key shapes; value-based replacement for Bearer tokens, PEM blocks, AKIA keys, gh[pousr]_ tokens of 20 or more characters, sk- tokens of 20 or more characters, and NAME=value assignments for TOKEN, SECRET, PASSWORD, KEY names - Test: recursion depth limit fails closed replacing the deeper subtree with [REDACTED]; unusual values including circular structures never throw - Test: one JSON object per line; redaction applied before append verified by asserting sentinel absence in the file; no content field on write entries; file mode 0600 - Test titles cite AUD-01 </behavior> <action>Write the failing tests first per behavior, then implement: redactValue in src/audit/redact.ts applying the D-41 key layer then the value layer, recursing through arrays and objects with a depth limit that substitutes [REDACTED] for deeper subtrees, catching every error class including circular input and returning [REDACTED] instead of throwing, with frozen pattern constants following the section 2 convention; appendAudit in src/audit/log.ts taking stateDir, sessionId, tool, outcome, and optional path, bytes, sha256, execSummary, redacting the whole entry before serialization, appending with O_APPEND at mode 0600, and never accepting a content field. Carry the one-way rationale on the log task: the persisted JSONL format is consumed by the Phase 7 review, so shape changes need agreement. Assumption Q-3 carried: temp-file or partial-line cleanup after a killed process is deferred, a Phase 4 reaper candidate.</action> <acceptance_criteria> - test/audit/redaction.test.ts asserts sentinel absence in outputs, both redaction layers, depth fail-closed, and never-throws including circular input - test/audit/log.test.ts asserts one object per line, redaction-before-append via sentinel absence, no content field, and mode 0600 - src/audit/log.ts imports core only for error codes where needed and never imports fs write paths (audit appends O_APPEND on its own file per D-20) </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/audit/redaction.test.js dist/test/audit/log.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Redaction and audit log exist with tests green; no secret shape survives to disk.</done> </task>
<task type="auto"> <name>Task 2: Tool-input gate gap cases</name> <files>test/contract/tool-input-gate.test.ts</files> <read_first> - test/contract/tools.schema.test.ts:44-245 (full existing coverage; add only what is missing per F-6) - src/contract/tools.schema.json (frozen six-tool map; read-only reference for pointer expectations) - src/contract/validate.ts (value and rejection helper shapes to reuse) </read_first> <action>Add to test/contract/tool-input-gate.test.ts only the D-40 cases not already covered by tools.schema.test.ts: unknown keys on write, list, and the shell exec form; shell form with shell true and no script key; write with missing content; list with missing depth; status and retry each holding the other tool params shape; and an envelope-level unknown key alongside a valid tool. Reuse the value and rejection helpers and pointer-matching style from the analog file. Assert SCHEMA_INVALID with the offending key or pointer named for every case. Test titles cite TOOL-01.</action> <acceptance_criteria> - Every D-40 shape (unknown keys each level, unknown tool names, both exec forms, shell without script, absolute and dot-dot paths, over-large maxBytes) is covered either here or in tools.schema.test.ts with no duplication - Each new test names the offending key or pointer in the detail match </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/contract/tool-input-gate.test.js</automated> <fails_when>non-zero exit, or the summary shows 0 passed</fails_when> </verify> <done>Gate gap cases green with zero overlap against the Phase 1 suite.</done> </task>
</tasks>
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| value -> audit file | Untrusted tool params and outputs cross into the persisted log; secrets must not survive |
| tool input -> schema gate | Unknown keys cross into validation; the dispatcher (Phase 4) trusts this boundary |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-02-08 | I (Information Disclosure) | src/audit/log.ts | high | mitigate | Redaction runs on the whole entry before serialization; tests assert sentinel absence in the file, not just key replacement; write entries carry no content |
| T-02-09 | T (Tampering) | src/audit/redact.ts | medium | mitigate | Depth limit fails closed to [REDACTED]; never-throws guarantee including circular input; key regex and value patterns pinned as frozen constants |
| </threat_model> |
<verification> npx tsc -p tsconfig.json && node --test dist/test/audit/redaction.test.js dist/test/audit/log.test.js dist/test/contract/tool-input-gate.test.js; grep the three test files for their requirement IDs. </verification>
<success_criteria> Redaction removes every credential shape before append, the log is append-only JSONL at 0600 with no content field, and every D-40 gate shape is rejected SCHEMA_INVALID. </success_criteria>
<output> Create .planning/phases/02-policy-state-and-filesystem-lockdown/02-04-SUMMARY.md when done </output>
Artifacts this phase produces
- src/audit/redact.ts (redactValue: two-layer redaction, depth-limited fail-closed, never throws)
- src/audit/log.ts (appendAudit: O_APPEND JSONL at <stateDir>/audit/<sessionId>.jsonl, mode 0600, redaction before append, no content field)
- test/audit/redaction.test.ts (AUD-01: key and value layers, depth fail-closed, spec n5 credential-leakage case)
- test/audit/log.test.ts (AUD-01: line format, redaction before append, no file content, mode 0600)
- test/contract/tool-input-gate.test.ts (TOOL-01: only the D-40 cases missing from tools.schema.test.ts per F-6)