@hegemonart/get-design-done 1.32.0 → 1.33.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +57 -0
  4. package/NOTICE +43 -5
  5. package/README.md +13 -0
  6. package/package.json +4 -2
  7. package/reference/gdd-runtime-audit.md +111 -0
  8. package/reference/gdd-threat-model.md +336 -0
  9. package/reference/registry.json +14 -0
  10. package/reference/schemas/pressure-scenario.schema.json +69 -0
  11. package/scripts/lib/peer-cli/acp-client.cjs +9 -1
  12. package/scripts/lib/peer-cli/asp-client.cjs +10 -1
  13. package/scripts/lib/peer-cli/sanitize-env.cjs +198 -0
  14. package/scripts/lib/redact.cjs +20 -1
  15. package/scripts/lib/skill-behavior/runner.cjs +187 -0
  16. package/scripts/lib/skill-behavior/stub-invoker.cjs +95 -0
  17. package/scripts/lib/skill-behavior/telemetry.cjs +379 -0
  18. package/scripts/lib/transports/ws.cjs +67 -3
  19. package/sdk/mcp/gdd-state/schemas/add_blocker.schema.json +2 -0
  20. package/sdk/mcp/gdd-state/schemas/add_decision.schema.json +1 -0
  21. package/sdk/mcp/gdd-state/schemas/add_must_have.schema.json +1 -0
  22. package/sdk/mcp/gdd-state/schemas/checkpoint.schema.json +1 -0
  23. package/sdk/mcp/gdd-state/schemas/frontmatter_update.schema.json +1 -1
  24. package/sdk/mcp/gdd-state/schemas/get.schema.json +2 -1
  25. package/sdk/mcp/gdd-state/schemas/probe_connections.schema.json +2 -0
  26. package/sdk/mcp/gdd-state/schemas/resolve_blocker.schema.json +1 -0
  27. package/sdk/mcp/gdd-state/server.js +137 -48
  28. package/sdk/mcp/gdd-state/tools/add_blocker.ts +2 -0
  29. package/sdk/mcp/gdd-state/tools/add_decision.ts +2 -0
  30. package/sdk/mcp/gdd-state/tools/add_must_have.ts +2 -0
  31. package/sdk/mcp/gdd-state/tools/checkpoint.ts +2 -0
  32. package/sdk/mcp/gdd-state/tools/frontmatter_update.ts +2 -0
  33. package/sdk/mcp/gdd-state/tools/get.ts +2 -0
  34. package/sdk/mcp/gdd-state/tools/probe_connections.ts +2 -0
  35. package/sdk/mcp/gdd-state/tools/resolve_blocker.ts +2 -0
  36. package/sdk/mcp/gdd-state/tools/set_status.ts +2 -0
  37. package/sdk/mcp/gdd-state/tools/shared.ts +117 -7
  38. package/sdk/mcp/gdd-state/tools/transition_stage.ts +2 -0
  39. package/sdk/mcp/gdd-state/tools/update_progress.ts +2 -0
  40. package/scripts/lib/cli/index.ts +0 -29
  41. package/scripts/lib/error-classifier.cjs +0 -29
  42. package/scripts/lib/event-stream/index.ts +0 -29
  43. package/scripts/lib/gdd-errors/index.ts +0 -29
  44. package/scripts/lib/gdd-state/index.ts +0 -29
  45. package/scripts/lib/iteration-budget.cjs +0 -29
  46. package/scripts/lib/jittered-backoff.cjs +0 -29
  47. package/scripts/lib/lockfile.cjs +0 -29
  48. package/scripts/mcp-servers/gdd-mcp/server.ts +0 -35
  49. package/scripts/mcp-servers/gdd-state/server.ts +0 -34
@@ -0,0 +1,336 @@
1
+ # GDD Runtime Threat Model (STRIDE)
2
+
3
+ > Phase 33.5 · SEC-01 · STRIDE pass over GDD's **own** runtime attack surface.
4
+ > Generated against branch `phase/33-5-runtime-security`, HEAD `5374bed`.
5
+
6
+ ## Scope
7
+
8
+ This document models the security posture of **GDD's own runtime** — the
9
+ multi-MCP-server, peer-CLI-spawning, WebSocket-emitting SDK that grew across
10
+ Phases 20–27 without a formalized security model. It does **NOT** model the
11
+ user code that GDD audits; the safety floor for *audited user code* is Phase
12
+ 14.5's concern. This is the equivalent threat model for GDD's *own* moving
13
+ parts: the hooks that run on every session start, the two MCP servers that
14
+ read and mutate `STATE.md`, the broker that spawns peer CLIs, the WebSocket
15
+ transport that streams the event bus, and the issue-reporter that reaches the
16
+ network through `gh`.
17
+
18
+ **STRIDE** is the Microsoft threat taxonomy used throughout: **S**poofing
19
+ (pretending to be someone/something you are not), **T**ampering (unauthorized
20
+ modification of data or code), **R**epudiation (denying an action with no
21
+ audit trail), **I**nformation disclosure (leaking data to the wrong party),
22
+ **D**enial of service (exhausting a resource so legitimate use fails), and
23
+ **E**levation of privilege (gaining capabilities you were not granted).
24
+
25
+ Each of the five in-scope components below gets a fixed five-part treatment:
26
+ **Assets** (what an attacker wants), **Entry points** (the untrusted-input
27
+ boundary), **STRIDE threats** (which categories apply), **Current mitigations**
28
+ (citing **real shipped code** — file + line + behavior), and **Residual risks**
29
+ (threats current code does **not** fully cover, each routed to the Phase 33.5
30
+ plan that closes it). Out of scope per CONTEXT: rewriting the issue-reporter
31
+ network model — it is **documented** here as already-mitigated, not
32
+ re-engineered.
33
+
34
+ ## Trust boundaries
35
+
36
+ The runtime crosses four trust boundaries. On the untrusted side of each sits
37
+ input that an attacker (or a compromised peer / config author / network host)
38
+ controls; the table names what crosses the line.
39
+
40
+ | Boundary | Untrusted side | What crosses |
41
+ | --- | --- | --- |
42
+ | WS event-stream server `←` client | A WebSocket client on the network (LAN/internet if bound wide) | The HTTP `Upgrade` request + `Authorization: Bearer` header |
43
+ | gdd-state MCP `←` environment / config / tool input | Whoever sets `GDD_STATE_PATH` or supplies a tool-call payload, or authors `.design/config.json` | The `GDD_STATE_PATH` env value + the JSON tool-input payloads |
44
+ | Peer-CLI broker `↔` spawned child | A spawned peer CLI (Codex / Gemini / Cursor / Copilot / Qwen) and its stdout stream | The child's stdout JSON frames + the parent env handed to the child |
45
+ | Outbound call sites `↔` external host | The remote HTTP host / GitHub / Figma the call reaches | The outbound request payload + whatever the remote returns |
46
+
47
+ The event payloads that traverse the bus (and therefore the WS transport and
48
+ any persisted JSONL) are scrubbed at serialize time — see Component 4's
49
+ `redact.cjs` mitigation, which is the cross-cutting information-disclosure
50
+ control for the whole bus.
51
+
52
+ ---
53
+
54
+ ## Component 1 — Hooks (SessionStart update-check + budget/context-monitor)
55
+
56
+ The hooks run automatically: `SessionStart` fires the update-check on every
57
+ session, and the budget / context-monitor hook runs on tool-use to enforce
58
+ spend and context ceilings. They execute with the user's full shell privileges
59
+ inside the user's repo, with no sandbox.
60
+
61
+ - **Assets:** The user's shell + filesystem (the hook runs as the user); the
62
+ integrity of the budget/context accounting the monitor maintains; the
63
+ network reachability of the update-check's outbound call.
64
+ - **Entry points:** The update-check's outbound HTTP fetch and whatever it
65
+ parses from the response (a version string / changelog); the hook's read of
66
+ `.design/config.json` (a malicious or malformed config is untrusted input);
67
+ the tool-use payload the budget monitor inspects.
68
+ - **STRIDE threats:**
69
+ - **Spoofing:** A spoofed update endpoint (DNS/MITM) could feed a forged
70
+ "latest version" response to the update-check.
71
+ - **Tampering:** A malformed `.design/config.json` could try to corrupt the
72
+ budget/context accounting or flip the monitor's thresholds.
73
+ - **Repudiation:** Hook actions are largely silent — limited audit trail of
74
+ what a SessionStart hook did or why a budget veto fired.
75
+ - **Information disclosure:** The update-check's User-Agent / outbound
76
+ request reveals that GDD is in use; a verbose hook could echo env into logs.
77
+ - **Denial of service:** A hung or slow update endpoint could stall session
78
+ start if the fetch were unbounded.
79
+ - **Elevation of privilege:** The hook already runs at full user privilege —
80
+ the residual concern is a config-driven path or command injection lifting
81
+ *attacker* input to that privilege level.
82
+ - **Current mitigations:** The update-check is **advisory** — it informs of a
83
+ newer version and never auto-installs or executes downloaded code, so a
84
+ spoofed version string cannot achieve code execution. The budget /
85
+ context-monitor reads config defensively (missing file / malformed JSON /
86
+ missing key are tolerated, mirroring the issue-reporter kill-switch's
87
+ config-tolerance contract in `scripts/lib/issue-reporter/kill-switch.cjs`).
88
+ Hooks emit through the event bus, which is redacted by `redact.cjs` at
89
+ serialize time (see Component 4), so secrets in hook telemetry are scrubbed.
90
+ - **Residual risks:** The update-check's outbound egress is one of the
91
+ cross-cutting call sites that currently has **no machine-readable allowlist
92
+ and no CI gate** asserting it is the only network touch a hook makes →
93
+ audited + allowlisted in **33.5-02** and gated in **33.5-04**. (The shell
94
+ hook `hooks/update-check.sh` is `.sh`, outside the `.js`-family static
95
+ scanner's scope, so it is **documented** in the **33.5-02** audit report
96
+ rather than hard-gated.)
97
+
98
+ ---
99
+
100
+ ## Component 2 — MCP servers (gdd-state: 11 mutating tools / gdd-mcp: read)
101
+
102
+ Two MCP servers expose GDD state to an MCP client: **gdd-state**
103
+ (`sdk/mcp/gdd-state/`) with **11 mutating tools** — `add_blocker`,
104
+ `add_decision`, `add_must_have`, `checkpoint`, `frontmatter_update`, `get`,
105
+ `probe_connections`, `resolve_blocker`, `set_status`, `transition_stage`,
106
+ `update_progress` — and **gdd-mcp** (`sdk/mcp/gdd-mcp/`) with read tools. The
107
+ mutating server is the higher-value target because it writes `STATE.md`.
108
+
109
+ - **Assets:** The integrity of `STATE.md` (the project's source of truth for
110
+ position, decisions, blockers, stage); the event stream the mutations emit;
111
+ the filesystem region the server is allowed to write.
112
+ - **Entry points:** The `GDD_STATE_PATH` environment variable (which redirects
113
+ *where the server reads/writes*); the JSON tool-input payloads for all 11
114
+ mutating tools; the `.design/STATE.md` file content the server parses.
115
+ - **STRIDE threats:**
116
+ - **Spoofing:** A tool caller could impersonate a legitimate pipeline stage
117
+ and drive `transition_stage` / `set_status` without authorization.
118
+ - **Tampering:** Crafted tool inputs could write hostile content into
119
+ `STATE.md`, or `GDD_STATE_PATH` could redirect writes onto an unintended
120
+ file (path traversal).
121
+ - **Repudiation:** Without a complete mutation audit trail, a hostile or
122
+ buggy mutation is hard to attribute — partly addressed by the event
123
+ emissions below.
124
+ - **Information disclosure:** A `get` against a traversed path could read a
125
+ file outside the intended `.design/` boundary.
126
+ - **Denial of service:** A JSON-bomb (deeply nested object / multi-megabyte
127
+ string field) in a tool payload could exhaust memory/CPU during parse.
128
+ - **Elevation of privilege:** Path traversal via `GDD_STATE_PATH` plus an
129
+ absent boundary check effectively elevates a tool caller's reach to any
130
+ file the process can write.
131
+ - **Current mitigations:** Every mutation emits a `state.mutation` /
132
+ `state.transition` event through `emitStateMutation()` / `emitStateTransition()`
133
+ (`sdk/mcp/gdd-state/tools/shared.ts` lines 91–140), giving a partial audit
134
+ trail (anti-repudiation). Handlers **never throw to the harness** — every
135
+ error funnels through `errorResponse()` → `toToolError()` into a structured
136
+ `{success:false,error}` (shared.ts lines 28–31, 148–151), so a malformed
137
+ input degrades to a clean error instead of a crash. Each of the 11 tools
138
+ already ships a JSON input schema under `sdk/mcp/gdd-state/schemas/`. State
139
+ events are redacted by `redact.cjs` at serialize time (Component 4).
140
+ - **Residual risks:** `resolveStatePath()` (`sdk/mcp/gdd-state/tools/shared.ts`
141
+ lines 60–64) honors `GDD_STATE_PATH` with **no path-traversal guard** — it
142
+ returns the override verbatim, so `..` escape / absolute-outside / symlink
143
+ escape are unchecked. The tool schemas exist but carry **no payload-size cap**
144
+ (no JSON-bomb guard) and are not uniformly tightened
145
+ (`additionalProperties:false` + `maxLength`). Path traversal + JSON-bomb +
146
+ un-tightened schemas are all closed by **33.5-03** (path-traversal guard +
147
+ payload cap + all 11 schemas tightened).
148
+
149
+ ---
150
+
151
+ ## Component 3 — Peer-CLI broker (acp-client + asp-client child spawn)
152
+
153
+ The broker spawns peer CLIs over stdio: `scripts/lib/peer-cli/acp-client.cjs`
154
+ (ACP-protocol peers) and `scripts/lib/peer-cli/asp-client.cjs` (Codex
155
+ app-server protocol). Both fork a local child process and exchange
156
+ line-delimited JSON over its stdio. The child is **untrusted** — it is a
157
+ third-party CLI whose stdout the broker parses.
158
+
159
+ - **Assets:** GDD's process environment — specifically `ANTHROPIC_API_KEY`,
160
+ `GH_TOKEN`, and any `GDD_*` / provider secret in `process.env`; the broker's
161
+ memory/availability; the integrity of the JSON protocol exchange.
162
+ - **Entry points:** The child's **stdout** (untrusted JSON frames the broker
163
+ must parse); the **environment handed to the child** at spawn time; the
164
+ `opts.command` / `opts.args` the broker is asked to launch.
165
+ - **STRIDE threats:**
166
+ - **Spoofing:** A misbehaving peer could emit forged protocol replies /
167
+ request IDs to confuse the correlation map.
168
+ - **Tampering:** A peer could stream malformed frames attempting to corrupt
169
+ the broker's line-buffer / pending-request state.
170
+ - **Repudiation:** Limited record of exactly what env a given child was
171
+ handed at spawn.
172
+ - **Information disclosure:** **The headline risk** — the child inherits
173
+ GDD's full environment, so a hostile or compromised peer reads
174
+ `ANTHROPIC_API_KEY` / `GH_TOKEN` straight out of `process.env`.
175
+ - **Denial of service:** A peer that never emits a newline could force the
176
+ broker to buffer unbounded stdout until memory exhaustion.
177
+ - **Elevation of privilege:** Inherited secrets let a peer act *as GDD*
178
+ against GDD's providers — using GDD's keys for the peer's own ends.
179
+ - **Current mitigations:** `acp-client.cjs` caps an un-terminated stdout line
180
+ at **`MAX_LINE_BYTES = 16 * 1024 * 1024`** (16 MiB; defined line 62, enforced
181
+ lines 166–176 — a peer that emits 16 MiB without a newline gets its active
182
+ prompt rejected as a protocol violation). This is a real **DoS guard** on the
183
+ untrusted stdout channel. The broker uses plain `spawn` with **no shell**
184
+ (acp-client.cjs lines 106–113, `windowsHide: true`), avoiding shell-injection
185
+ on the command path. Per-request correlation via a pending-id map bounds the
186
+ protocol state machine.
187
+ - **Residual risks:** Both clients default the child's environment to the
188
+ **full `process.env`** when `opts.env` is absent — `acp-client.cjs` line 102
189
+ (`const env = opts.env && typeof opts.env === 'object' ? opts.env :
190
+ process.env;`) and `asp-client.cjs` line 122 (when `opts.env` is absent no
191
+ `spawnOptions.env` is set, so the child inherits the parent's `process.env` by
192
+ Node default). This leaks GDD's `ANTHROPIC_API_KEY` / `GH_TOKEN` / `GDD_*` to
193
+ every spawned peer. Closed by **33.5-04** (allowlist-forward, default-deny env
194
+ sandbox via a shared `sanitize-env` helper applied to both clients; secrets
195
+ are never forwarded unless explicitly allowlisted in `.design/config.json`).
196
+
197
+ ---
198
+
199
+ ## Component 4 — WebSocket event-stream transport (scripts/lib/transports/ws.cjs)
200
+
201
+ `scripts/lib/transports/ws.cjs` exposes the event-stream bus over WebSocket:
202
+ one JSON event per text frame, with optional replay of a tail file to each new
203
+ connection. It is an **optional dependency** (`ws`) — absent installs render an
204
+ install hint instead of starting. When running, it is a network listener.
205
+
206
+ - **Assets:** The **event stream itself** (every `state.mutation` /
207
+ `state.transition` / pipeline event, which can carry payload detail); the
208
+ listening socket; the Bearer token that authorizes a connection.
209
+ - **Entry points:** The HTTP **`Upgrade` request** from any client that can
210
+ reach the bound socket, and specifically its `Authorization: Bearer <token>`
211
+ header; the `tailFrom` replay file path.
212
+ - **STRIDE threats:**
213
+ - **Spoofing:** A client without the token attempting to subscribe to the
214
+ live event stream.
215
+ - **Tampering:** N/A for inbound (the transport is push-only to clients) —
216
+ the concern is read access, not write.
217
+ - **Repudiation:** No per-connection identity beyond the shared token, so
218
+ individual subscribers are not distinguishable in an audit.
219
+ - **Information disclosure:** **The headline risk** — an unauthorized
220
+ subscriber would receive the entire live event stream, including any
221
+ sensitive payload detail, if it could reach the socket and pass auth.
222
+ - **Denial of service:** Many connections / a slow consumer could pressure
223
+ the server (mitigated in part by fire-and-forget, no-queue backpressure).
224
+ - **Elevation of privilege:** A network-reachable listener turns a
225
+ local-only observability feature into a remotely-reachable data source.
226
+ - **Current mitigations:** **Bearer-token auth is enforced on every upgrade**:
227
+ `ws.cjs` lines 110–116 reject any upgrade whose header is missing or where
228
+ the supplied token does not match the expected `Bearer` value, returning an `HTTP/1.1 401 Unauthorized` and a
229
+ socket destroy. The token **must be ≥8 chars** — `startServer` throws a
230
+ `TypeError` if `opts.token.length < 8` (line 74), preventing trivially weak
231
+ tokens. Backpressure is **fire-and-forget with no queue** (lines 91–108):
232
+ events for a non-OPEN socket are dropped, bounding memory under a slow
233
+ consumer. Cross-cutting for the whole bus: **`redact.cjs`** deep-walks every
234
+ event payload at serialize time (`scripts/lib/redact.cjs` — `redact()` lines
235
+ 95–116, `redactString()` lines 75–83) and scrubs **8 secret patterns** (pem,
236
+ jwt, anthropic `sk-ant-`, stripe `sk_live_`, slack `xox[baprs]`, github_pat
237
+ `ghp_`, aws `AKIA`, generic `sk-`), so secrets in event payloads are masked
238
+ before they ever reach a WS subscriber or hit disk. This `redact.cjs` scrub is
239
+ the runtime's primary information-disclosure control across **all** components
240
+ that emit events.
241
+ - **Residual risks:**
242
+ - The server binds to **all interfaces (`0.0.0.0`)** by default —
243
+ `httpServer.listen(opts.port, ...)` (line 145) passes **no host argument**,
244
+ so on a multi-homed / LAN host the token-protected stream is reachable
245
+ off-box. The token compare uses `!==` (line 112), which is
246
+ **timing-unsafe**. Both closed by **33.5-03** (default bind `127.0.0.1` +
247
+ opt-in remote via `event_stream.bind_host` / `GDD_WS_BIND_HOST` + a CI gate
248
+ that fails if the default config would bind `0.0.0.0`; upgrade the compare to
249
+ `crypto.timingSafeEqual`).
250
+ - `redact.cjs` is **missing three modern token formats**: Gemini / GCP
251
+ `AIza…`, GitHub fine-grained `github_pat_…`, and GitHub server / oauth /
252
+ user / refresh `gh[sour]_…`. A payload carrying one of these would leak
253
+ through the scrub onto the stream and disk. Closed by **33.5-05** (add the
254
+ three patterns + a synthetic-secret fuzz test asserting zero leak).
255
+
256
+ ---
257
+
258
+ ## Component 5 — Issue-reporter outbound (gh CLI only)
259
+
260
+ `scripts/lib/issue-reporter/` is the only first-party feature that intentionally
261
+ reaches the network. It assembles a bug report and submits it through the user's
262
+ **`gh` CLI**. **This network model is already mitigated and is DOCUMENTED here,
263
+ not re-engineered** (CONTEXT Out-of-scope: rewriting the issue-reporter network
264
+ model).
265
+
266
+ - **Assets:** The user's GitHub identity (via the local `gh` auth); the content
267
+ of the submitted report (which must not carry the user's secrets or
268
+ unintended PII); the integrity of the destination repo.
269
+ - **Entry points:** The user-invoked report flow (the body / title assembled
270
+ from local state); the `.design/config.json` and the env that gate whether the
271
+ reporter runs at all.
272
+ - **STRIDE threats:**
273
+ - **Spoofing:** A forged destination could try to receive reports — mitigated
274
+ by the frozen destination below.
275
+ - **Tampering:** Attempting to redirect submissions to an attacker repo by
276
+ injecting a destination override.
277
+ - **Repudiation:** Submissions flow through `gh` under the user's identity,
278
+ which is itself the attribution record.
279
+ - **Information disclosure:** **The headline risk** — a report could exfiltrate
280
+ secrets / PII embedded in local state if the payload were not scrubbed.
281
+ - **Denial of service:** Not a meaningful vector — submission is a
282
+ user-initiated, one-shot CLI call.
283
+ - **Elevation of privilege:** Using the user's `gh` credentials beyond the
284
+ single sanctioned submit.
285
+ - **Current mitigations (ALREADY shipped — documented, no change here):**
286
+ - **Outbound is via the `gh` CLI ONLY.** `gh-submit.cjs` wraps
287
+ `gh issue create --repo <DESTINATION_REPO> --title … --body-file …` and is
288
+ explicit that "the user's gh CLI is the sole outbound primitive. No HTTP-S
289
+ URL literals, no global fetch primitive, no plugin-side credentials" (D-05).
290
+ There is no raw HTTP egress in this subtree.
291
+ - **Frozen destination.** `destination.cjs` is an `Object.freeze`-d module —
292
+ the single source of truth for the destination repo, with **no env-var
293
+ lookup, no config override, no flag override**. A static CI gate asserts it
294
+ is the only file under the report-issue tree that contains the destination
295
+ literal, so a redirect attempt fails the build.
296
+ - **Kill-switch (dual-surface).** `kill-switch.cjs` disables the reporter via
297
+ **either** the env var `GDD_DISABLE_ISSUE_REPORTER === '1'` **or** the config
298
+ `.design/config.json` `{ "issue_reporter": false }`; either surface alone is
299
+ sufficient, and config is read tolerantly (missing file / malformed JSON /
300
+ missing key are safe).
301
+ - Payloads pass through privacy-diff / consent-prompt machinery before
302
+ submission, and event telemetry is redacted by `redact.cjs` (Component 4).
303
+ - **Residual risks:** The issue-reporter's **own** network model has no residual
304
+ this phase changes — it is intentionally documented as complete. The only
305
+ cross-cutting residual touching it is the **lack of a machine-readable
306
+ outbound allowlist + CI gate** that proves `gh-submit` is the sole egress in
307
+ this subtree at a tree-wide level: closed by **33.5-02** (the canonical
308
+ outbound-network allowlist data, which lists `scripts/lib/issue-reporter/**`
309
+ as an allowed egress glob) and **33.5-04** (the `scan:outbound` CI gate that
310
+ fails on any active-egress site not under an allowlisted glob).
311
+
312
+ ---
313
+
314
+ ## Residual-risk → closing-plan map
315
+
316
+ Every residual risk identified above is routed to the Phase 33.5 plan (or
317
+ policy doc) that closes it. No residual is left unmapped. This table is the
318
+ spine the phase closeout (33.5-06) uses to prove completeness.
319
+
320
+ | Residual risk | Component | Closing plan |
321
+ | --- | --- | --- |
322
+ | WS binds `0.0.0.0` by default (`listen` line 145, no host) + timing-unsafe `!==` token compare (line 112) | WebSocket transport | **33.5-03** |
323
+ | `GDD_STATE_PATH` path traversal (no guard, shared.ts 60–64) + no payload-size cap + un-tightened tool schemas | gdd-state MCP | **33.5-03** |
324
+ | Full `process.env` (incl. `ANTHROPIC_API_KEY` / `GH_TOKEN`) leaks to spawned peers (acp 102 / asp 122) | Peer-CLI broker | **33.5-04** |
325
+ | Outbound egress sites have no machine-readable allowlist + no CI gate | cross-cutting (hooks update-check, figma-extract, issue-reporter, e2e) | **33.5-02** (allowlist) + **33.5-04** (scan gate) |
326
+ | Secret-scan misses Gemini `AIza…` / GitHub fine-grained `github_pat_…` / GitHub server `gh[sour]_…` tokens | redact.cjs | **33.5-05** |
327
+ | No published vulnerability-disclosure policy | project | **33.5-06** (SECURITY.md) |
328
+
329
+ ### Already-mitigated (documented, NOT re-engineered)
330
+
331
+ | Already-mitigated surface | Component | Evidence |
332
+ | --- | --- | --- |
333
+ | Outbound via `gh` CLI only; frozen destination; dual-surface kill-switch | Issue-reporter | `gh-submit.cjs` (`gh issue create` only), `destination.cjs` (`Object.freeze`, no override), `kill-switch.cjs` (env + `.design/config.json`) |
334
+ | Bearer-token auth on every WS upgrade + ≥8-char token rule | WebSocket transport | `ws.cjs` 110–116 (401 on mismatch) + line 74 (`length < 8` → `TypeError`) |
335
+ | 16 MiB un-newlined-stdout DoS cap on untrusted peer output | Peer-CLI broker | `acp-client.cjs` `MAX_LINE_BYTES` line 62, enforced 166–176 |
336
+ | Deep-walk secret scrub of every event payload at serialize time | cross-cutting (event bus) | `redact.cjs` `redact()` 95–116 / `redactString()` 75–83, 8 patterns |
@@ -860,6 +860,20 @@
860
860
  "type": "meta-rules",
861
861
  "phase": 30,
862
862
  "description": "Phase 30 triage gate catalogue — locally-fixable failure modes (id/pattern/diagnosis/remedy/severity, with optional propose_report whitelist flag per D-11) consulted by scripts/lib/issue-reporter/triage-matcher.cjs before the report-issue consent prompt (D-07/D-11)."
863
+ },
864
+ {
865
+ "name": "gdd-threat-model",
866
+ "path": "reference/gdd-threat-model.md",
867
+ "type": "heuristic",
868
+ "phase": 33.5,
869
+ "description": "Phase 33.5 STRIDE threat model of GDD's own runtime attack surface — hooks, the gdd-state + gdd-mcp MCP servers, the peer-CLI broker, the WebSocket event-stream transport, and issue-reporter outbound; maps each residual risk to the 33.5 plan that closes it."
870
+ },
871
+ {
872
+ "name": "gdd-runtime-audit",
873
+ "path": "reference/gdd-runtime-audit.md",
874
+ "type": "heuristic",
875
+ "phase": 33.5,
876
+ "description": "Phase 33.5 static security audit of GDD's shipped runtime surface (hooks/scripts/sdk/bin) — outbound-network call sites, secret-handling sites, and external-input surfaces; human-readable companion to scripts/security/outbound-allowlist.json (the canonical active-egress allowlist the 33.5-04 scan-outbound-network.cjs gate consumes) and reference/gdd-threat-model.md."
863
877
  }
864
878
  ]
865
879
  }
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://get-design-done.example/schemas/pressure-scenario.schema.json",
4
+ "title": "Pressure Scenario Manifest",
5
+ "description": "Contract for a Phase-33 skill-behavior pressure-scenario manifest. The runner (scripts/lib/skill-behavior/runner.cjs) loads manifests conforming to this schema, spawns a subagent against `setup_prompt` under the named `pressures`, and validates the response against the `expected_compliance` / `expected_violations` regex sources (compiled with new RegExp(source)). The 5-value `pressures` enum and the required-field set come verbatim from ROADMAP Phase-33 SC#2.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "name",
10
+ "target_skill",
11
+ "pressures",
12
+ "setup_prompt",
13
+ "expected_compliance",
14
+ "expected_violations"
15
+ ],
16
+ "properties": {
17
+ "name": {
18
+ "type": "string",
19
+ "minLength": 1,
20
+ "description": "Unique scenario identifier, e.g. \"brief-time-pressure\"."
21
+ },
22
+ "target_skill": {
23
+ "type": "string",
24
+ "minLength": 1,
25
+ "description": "The skill under test, e.g. \"brief\", \"explore\", \"plan\", \"using-gdd\"."
26
+ },
27
+ "pressures": {
28
+ "type": "array",
29
+ "minItems": 1,
30
+ "description": "One or more pressure vectors applied in the setup_prompt.",
31
+ "items": {
32
+ "enum": ["time", "sunk-cost", "authority", "exhaustion", "scope-minimization"]
33
+ }
34
+ },
35
+ "setup_prompt": {
36
+ "type": "string",
37
+ "minLength": 1,
38
+ "description": "The prompt handed to the subagent — embeds the pressure(s) and asks it to act."
39
+ },
40
+ "expected_compliance": {
41
+ "type": "array",
42
+ "minItems": 1,
43
+ "description": "Regex SOURCE strings the response MUST match to count as compliant (the runner compiles each with new RegExp(source)).",
44
+ "items": { "type": "string", "minLength": 1 }
45
+ },
46
+ "expected_violations": {
47
+ "type": "array",
48
+ "description": "Regex SOURCE strings that, if matched, count as a violation (the runner compiles each with new RegExp(source)). May be empty.",
49
+ "items": { "type": "string", "minLength": 1 }
50
+ },
51
+ "description": {
52
+ "type": "string",
53
+ "description": "Optional free-text scenario note (33-03 baselines reference it)."
54
+ },
55
+ "variant": {
56
+ "type": "string",
57
+ "description": "Optional A/B variant label, e.g. \"trigger-only\" | \"what-clause\" (33-04 description-format A/B)."
58
+ },
59
+ "variants": {
60
+ "type": "array",
61
+ "description": "Optional array of A/B variant descriptors for a single-manifest A/B pair (33-04). Each item is an object, e.g. { label, description }.",
62
+ "items": { "type": "object" }
63
+ },
64
+ "body_probe": {
65
+ "type": "string",
66
+ "description": "Optional body-only probe prompt the A/B scenario asks (33-04 description-format A/B)."
67
+ }
68
+ }
69
+ }
@@ -50,6 +50,7 @@
50
50
 
51
51
  const { spawn } = require('child_process');
52
52
  const { EventEmitter } = require('events');
53
+ const { sanitizeEnv, readPeerCliAllowlist } = require('./sanitize-env.cjs');
53
54
 
54
55
  /**
55
56
  * Hard cap on the size of a single un-terminated line read from the
@@ -99,7 +100,14 @@ function createAcpClient(opts) {
99
100
  const command = opts.command;
100
101
  const args = Array.isArray(opts.args) ? opts.args : [];
101
102
  const cwd = typeof opts.cwd === 'string' ? opts.cwd : process.cwd();
102
- const env = opts.env && typeof opts.env === 'object' ? opts.env : process.env;
103
+ // Plan 33.5-04 (D-03): when the caller does not supply an explicit env, the
104
+ // child inherits a SANITIZED env (OS-essential baseline + the configured
105
+ // peer_cli.env_allowlist) instead of the raw full process.env — so GDD's
106
+ // ANTHROPIC_API_KEY/GH_TOKEN/GDD_* never leak to spawned peers. An explicit
107
+ // opts.env still wins (callers/tests can pass a full env).
108
+ const env = opts.env && typeof opts.env === 'object'
109
+ ? opts.env
110
+ : sanitizeEnv(process.env, { allowlist: readPeerCliAllowlist() });
103
111
 
104
112
  const events = new EventEmitter();
105
113
 
@@ -80,6 +80,7 @@
80
80
  'use strict';
81
81
 
82
82
  const { spawn } = require('node:child_process');
83
+ const { sanitizeEnv, readPeerCliAllowlist } = require('./sanitize-env.cjs');
83
84
 
84
85
  /** Per-line cap before we treat the stream as malformed. */
85
86
  const MAX_LINE_BYTES = 16 * 1024 * 1024;
@@ -119,7 +120,15 @@ function createAspClient(opts) {
119
120
  stdio: ['pipe', 'pipe', 'pipe'],
120
121
  };
121
122
  if (typeof opts.cwd === 'string' && opts.cwd.length > 0) spawnOptions.cwd = opts.cwd;
122
- if (opts.env && typeof opts.env === 'object') spawnOptions.env = opts.env;
123
+ if (opts.env && typeof opts.env === 'object') {
124
+ spawnOptions.env = opts.env;
125
+ } else {
126
+ // Plan 33.5-04 (D-03): without an explicit env, the child ALWAYS gets a
127
+ // defined, SANITIZED env (OS-essential baseline + peer_cli.env_allowlist)
128
+ // rather than Node defaulting to the raw process.env — closing the
129
+ // ANTHROPIC_API_KEY/GH_TOKEN/GDD_* leak. Explicit opts.env still wins.
130
+ spawnOptions.env = sanitizeEnv(process.env, { allowlist: readPeerCliAllowlist() });
131
+ }
123
132
 
124
133
  // Test-injection seam: callers (or unit tests) can supply a pre-built
125
134
  // ChildProcess so we don't actually fork a binary in tests. The mock