Skip to content

<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 ​

BoundaryDescription
value -> audit fileUntrusted tool params and outputs cross into the persisted log; secrets must not survive
tool input -> schema gateUnknown keys cross into validation; the dispatcher (Phase 4) trusts this boundary

STRIDE Threat Register ​

Threat IDCategoryComponentSeverityDispositionMitigation Plan
T-02-08I (Information Disclosure)src/audit/log.tshighmitigateRedaction 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-09T (Tampering)src/audit/redact.tsmediummitigateDepth 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)

AGPL-3.0-only. Copyright (C) 2026 Spectrum Web Co/swcstudiospace.