@hegemonart/get-design-done 1.33.0 → 1.33.6

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 (47) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +49 -0
  4. package/README.md +4 -0
  5. package/SKILL.md +1 -0
  6. package/agents/design-authority-watcher.md +4 -0
  7. package/connections/connections.md +2 -0
  8. package/connections/openrouter.md +86 -0
  9. package/hooks/budget-enforcer.ts +103 -0
  10. package/package.json +5 -2
  11. package/reference/gdd-runtime-audit.md +111 -0
  12. package/reference/gdd-threat-model.md +399 -0
  13. package/reference/openrouter-tier-mapping.md +98 -0
  14. package/reference/prices.openrouter.md +26 -0
  15. package/reference/registry.json +28 -0
  16. package/scripts/lib/authority-watcher/index.cjs +147 -0
  17. package/scripts/lib/budget-enforcer.cjs +16 -0
  18. package/scripts/lib/openrouter/catalog-fetcher.cjs +326 -0
  19. package/scripts/lib/peer-cli/acp-client.cjs +9 -1
  20. package/scripts/lib/peer-cli/asp-client.cjs +10 -1
  21. package/scripts/lib/peer-cli/sanitize-env.cjs +198 -0
  22. package/scripts/lib/redact.cjs +20 -1
  23. package/scripts/lib/tier-resolver-openrouter.cjs +343 -0
  24. package/scripts/lib/transports/ws.cjs +67 -3
  25. package/sdk/event-stream/types.ts +24 -2
  26. package/sdk/mcp/gdd-state/schemas/add_blocker.schema.json +2 -0
  27. package/sdk/mcp/gdd-state/schemas/add_decision.schema.json +1 -0
  28. package/sdk/mcp/gdd-state/schemas/add_must_have.schema.json +1 -0
  29. package/sdk/mcp/gdd-state/schemas/checkpoint.schema.json +1 -0
  30. package/sdk/mcp/gdd-state/schemas/frontmatter_update.schema.json +1 -1
  31. package/sdk/mcp/gdd-state/schemas/get.schema.json +2 -1
  32. package/sdk/mcp/gdd-state/schemas/probe_connections.schema.json +2 -0
  33. package/sdk/mcp/gdd-state/schemas/resolve_blocker.schema.json +1 -0
  34. package/sdk/mcp/gdd-state/server.js +137 -48
  35. package/sdk/mcp/gdd-state/tools/add_blocker.ts +2 -0
  36. package/sdk/mcp/gdd-state/tools/add_decision.ts +2 -0
  37. package/sdk/mcp/gdd-state/tools/add_must_have.ts +2 -0
  38. package/sdk/mcp/gdd-state/tools/checkpoint.ts +2 -0
  39. package/sdk/mcp/gdd-state/tools/frontmatter_update.ts +2 -0
  40. package/sdk/mcp/gdd-state/tools/get.ts +2 -0
  41. package/sdk/mcp/gdd-state/tools/probe_connections.ts +2 -0
  42. package/sdk/mcp/gdd-state/tools/resolve_blocker.ts +2 -0
  43. package/sdk/mcp/gdd-state/tools/set_status.ts +2 -0
  44. package/sdk/mcp/gdd-state/tools/shared.ts +117 -7
  45. package/sdk/mcp/gdd-state/tools/transition_stage.ts +2 -0
  46. package/sdk/mcp/gdd-state/tools/update_progress.ts +2 -0
  47. package/skills/openrouter-status/SKILL.md +86 -0
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Get Design Done — 5-stage agent-orchestrated design pipeline with 9 connections, handoff-first workflow, bidirectional Figma write-back, 22+ specialized agents, queryable knowledge layer (intel store, dependency analysis, learnings extraction), and a self-improvement loop (reflector, frontmatter + budget feedback, global-skills layer). v1.20.0 ships the SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream, and resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) for rate-limit + 429 + context-overflow recovery. Full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation (auto-tag + GitHub Release + release-time smoke test).",
8
- "version": "1.33.0"
8
+ "version": "1.33.6"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "get-design-done",
13
13
  "source": "./",
14
14
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), Claude Design handoff, bidirectional Figma write-back, and a queryable intel store (.design/intel/) for dependency and learnings queries. Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation. Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
15
- "version": "1.33.0",
15
+ "version": "1.33.6",
16
16
  "author": {
17
17
  "name": "hegemonart"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "get-design-done",
3
3
  "short_name": "gdd",
4
- "version": "1.33.0",
4
+ "version": "1.33.6",
5
5
  "description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), handoff-first workflow via Claude Design bundles, bidirectional Figma write-back (annotations, Code Connect), queryable intel store (`.design/intel/`) for O(1) design surface lookups, and self-improvement loop (reflector agent, frontmatter + budget feedback, global-skills layer at `~/.claude/gdd/global-skills/`). Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings, reflect, apply-reflections. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows, lint + schema + frontmatter + stale-ref + shellcheck + gitleaks + injection-scan + blocking size-budget) and release automation (auto-tag + GitHub Release + release-time smoke test). Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain. v1.27.7 ships gdd-mcp (Phase 27.7): 12 read-only MCP tools for sub-3s priming. v1.28.0 (Phase 28): Foundational References Tier 2 — 5 new reference files (color-theory, composition, proportion-systems, i18n, contrast-advanced), 2 verifier i18n probes + 1 explore i18n-readiness probe, 12 additive cross-link insertions across 10 existing references, 2 orthogonal audit-scoring lens-tags (composition_alignment + i18n_readiness).",
6
6
  "author": {
7
7
  "name": "hegemonart",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,55 @@ All notable changes to get-design-done are documented here. Versions follow [sem
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.33.6] - 2026-05-31
8
+
9
+ ### Phase 33.6 — OpenRouter Provider Adapter
10
+
11
+ Adds **OpenRouter** as a tier-resolver provider so users can route any agent's tier (`opus`/`sonnet`/`haiku`) through OpenRouter's aggregator catalog (one API key → Claude/GPT/Llama/Gemini/DeepSeek) **alongside** native provider auth. This introduces the plugin's **first plugin-side outbound REST client** — it lands under the Phase-33.5 audited outbound baseline (the catalog-fetcher's egress is explicitly allowlisted, the outbound CI gate stays green). A decimal release on the v1.33.x arc (CHANGELOG-only, D-01); no new runtime dependency (Node built-in `fetch` only, D-10). OpenRouter is **opt-in alongside** native auth, never OpenRouter-only (D-08).
12
+
13
+ ### Added
14
+
15
+ - **Dynamic OpenRouter catalog fetcher.** `scripts/lib/openrouter/catalog-fetcher.cjs` fetches `https://openrouter.ai/api/v1/models`, maps it into the cache shape, and writes it ATOMICALLY to `.design/cache/openrouter-models.json` (gitignored runtime artifact) with a **24h TTL** skip-if-fresh (D-02, configurable via `.design/config.json#openrouter_catalog_ttl_hours`). The fetch is gated behind an **injectable `fetchImpl`** (default global `fetch`) so the entire default test suite is hermetic — no live network in `npm test` (D-07) — and uses the `sdk/primitives` jittered-backoff + error-classifier + `rate-guard` for bounded transient/rate-limit retry. The `fetch(` egress is allowlisted via `scripts/lib/openrouter/**` in `scripts/security/outbound-allowlist.json`, with a matching egress entry in `reference/gdd-threat-model.md` (D-06). The `OPENROUTER_API_KEY` is sent only as an `Authorization: Bearer` header and is never persisted to the cache.
16
+ - **OpenRouter tier-resolver adapter.** `scripts/lib/tier-resolver-openrouter.cjs` — `resolve(tier) → openrouter-model-id | null` — maps GDD's `opus`/`sonnet`/`haiku` vocabulary (D-04) onto a concrete catalog id via a deterministic closed-vs-open + completion-price heuristic (opus = top closed, sonnet = mid/top-open, haiku = cheap open), with a `.design/config.json#openrouter_tier_overrides` escape hatch that wins verbatim (D-03). Never throws; an absent key / missing cache / unknown tier degrades to `null` so the caller falls back to the native provider via the existing Phase-26 tier-resolver chain (graceful-degrade — D-08). `reference/openrouter-tier-mapping.md` documents the heuristic and is registered in `reference/registry.json` in the same plan that created it (D-11).
17
+ - **OpenRouter connection + status skill.** `connections/openrouter.md` (the Phase-14 connection spec — Setup / Probe Pattern / Tools / Pipeline Integration / Fallback Behavior) and the `/gdd:openrouter-status` skill (`skills/openrouter-status/SKILL.md`) report catalog freshness, the resolved tier→model mapping, and override state.
18
+ - **Optional `cost.update` provider tag (back-compat).** The `cost.update` event payload gains an OPTIONAL `provider` field, set to `'openrouter'` when the adapter resolved the model; absent otherwise. Additive — the events JSON schema is unchanged, so existing consumers are unaffected.
19
+ - **OpenRouter price sub-table + catalog-drift watch.** `reference/prices.openrouter.md` (a catalog-derived view; the dynamic catalog stays the source of truth) and an authority-watcher catalog-drift classifier (`diffOpenRouterCatalog`) that surfaces deprecated/withdrawn models matching `openrouter_tier_overrides` (noise-controlled — new-model / pricing-change are classified but not surfaced).
20
+ - **Regression baseline.** `test/fixtures/baselines/phase-33-6/` freezes the OpenRouter surface — a golden tier-resolution snapshot (the `opus`/`sonnet`/`haiku` ids the adapter resolves from the shared fixture catalog) plus encoded TTL / fallback-no-key / drift-on-synthetic-deprecation expectations — pinned by `test/suite/phase-33-6-baseline.test.cjs` so a future change cannot silently undo the heuristic, the TTL, the graceful-degrade, or the drift classifier.
21
+
22
+ ### Notes
23
+
24
+ - **OpenRouter is a tier-RESOLUTION-layer adapter, not an install-registry runtime (D-12).** SC#5's "runtimes.cjs extension" wording is reinterpreted: `scripts/lib/install/runtimes.cjs` is the Phase-24-locked install matrix (how to install GDD *into* a runtime; guarded at exactly 16 entries) — you don't install GDD "into" OpenRouter. OpenRouter lives only in the tier-resolution adapter (`scripts/lib/tier-resolver-openrouter.cjs`), which reads the dynamic catalog rather than the install registry, so it fully delivers SC#5's intent without polluting or weakening the Phase-24 install lock. `install/runtimes.cjs` and its 16-count guard are preserved intact.
25
+ - All Phase 33.6 tests are hermetic (injected stub fetch + a fixture catalog; no network, no real `OPENROUTER_API_KEY`), so the default `npm test` stays green (D-07), and `npm run scan:outbound` stays green with the OpenRouter egress allowlisted (D-06).
26
+ - The 31.5 tarball golden (`test/fixtures/baselines/phase-31-5/tarball-manifest.txt`) was regenerated as a reviewed delta: **+6** shipped OpenRouter files (`connections/openrouter.md`, `reference/openrouter-tier-mapping.md`, `reference/prices.openrouter.md`, `scripts/lib/openrouter/catalog-fetcher.cjs`, `scripts/lib/tier-resolver-openrouter.cjs`, `skills/openrouter-status/SKILL.md`), zero removals (627 paths).
27
+ - 6-manifest lockstep at **v1.33.6** (`package.json` + `package-lock.json` (root + `packages.""`) + `.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json` (metadata.version + plugins[0].version) + `.cursor-plugin/plugin.json` + `.codex-plugin/plugin.json`). Version-sync hygiene done upfront (D-09): `OFF_CADENCE_VERSIONS.add('1.33.6')` + the 14 live-pinned `manifests-version.txt` baselines forward-propagated 1.33.5 → 1.33.6.
28
+
29
+ ---
30
+
31
+ ## [1.33.5] - 2026-05-31
32
+
33
+ ### Phase 33.5 — GDD Runtime Security Hardening
34
+
35
+ Audits and hardens GDD's **own** runtime attack surface — the multi-MCP-server, peer-CLI-spawning, WebSocket-transport-emitting SDK that grew across Phases 20–27 without a formalized security model. Phase 14.5 ships a safety floor for *user code being audited*; this phase is the equivalent for GDD's *own* runtime. Ships a STRIDE threat model + static runtime audit, an outbound-network CI gate, WebSocket bind hardening, gdd-state MCP input validation, peer-CLI env sandboxing, a secret-scan extension + fuzz, a published disclosure policy, and a regression baseline. A decimal release on the v1.33.x arc (CHANGELOG-only, D-01); no new runtime dependency (built-ins only, D-12). 6 plans across Waves A–C.
36
+
37
+ ### Added
38
+
39
+ - **STRIDE threat model + runtime audit.** `reference/gdd-threat-model.md` models the five in-scope runtime components (hooks, the `gdd-state` MCP server, the peer-CLI broker, the WebSocket transport, the issue-reporter outbound path) with a per-component Assets / Entry-points / STRIDE / Mitigations / Residual treatment and a residual-risk → closing-plan map; `reference/gdd-runtime-audit.md` records the static audit that backs it. Both are registry-tracked.
40
+ - **Outbound-network CI gate.** `scripts/scan-outbound-network.cjs` + `npm run scan:outbound`, wired into the CI `security` job, statically gates **active egress** (require/import of `node:http(s)`/`ws`/`node-fetch`/`axios`/`undici`, `fetch(`, `http(s).get/.request`, `new WebSocket(Server)`, and child-process spawn of `gh`/`curl`/`wget`/`nc`/`ssh`/`scp`) under `hooks/ scripts/ sdk/ bin/`, failing on any active-egress site not under an allowlisted glob in `scripts/security/outbound-allowlist.json` (D-06). Comment-only matches and bare-URL literals are skipped (a URL cannot exfiltrate without a call). This forces Phase 33.6's OpenRouter REST client to explicitly allowlist its new egress.
41
+ - **WebSocket localhost-default bind + timing-safe token.** `scripts/lib/transports/ws.cjs` now defaults its bind host to **127.0.0.1** (was the implicit `0.0.0.0` all-interfaces bind); a remote bind is opt-in via `.design/config.json` `event_stream.bind_host` or the `GDD_WS_BIND_HOST` env override, and `scripts/scan-ws-bind.cjs` gates that the default config would never bind `0.0.0.0`. The Bearer-token compare is upgraded to `crypto.timingSafeEqual` (was `!==`, timing-unsafe); the existing ≥8-char-token rule is retained (D-04).
42
+ - **gdd-state MCP input validation.** `sdk/mcp/gdd-state/tools/shared.ts` gains a `resolveStatePath()` path-traversal guard (rejects `..`-escape / absolute-outside / best-effort symlink-escape on the `GDD_STATE_PATH` override) and an `assertInputWithinLimits()` payload cap (64 KiB input / 8192-char string / depth-32 JSON-bomb guard) wired into all 11 tool handlers; the 11 tool input schemas are tightened with `maxLength`/`maxItems` (and already carried `additionalProperties:false`) (D-08).
43
+ - **Peer-CLI env sandbox (allowlist-forward / default-deny).** A shared `scripts/lib/peer-cli/sanitize-env.cjs` helper builds the spawned child's environment from an OS-essential baseline **plus** an explicit allowlist read from `.design/config.json` `peer_cli.env_allowlist`, applied to both the acp and asp clients. GDD's `ANTHROPIC_API_KEY` / `GH_TOKEN` / `GDD_*` and anything secret-shaped are never forwarded to a spawned peer unless explicitly allowlisted (D-03).
44
+ - **Secret-scan extension + fuzz.** `scripts/lib/redact.cjs` adds three modern token formats — Gemini/GCP `AIza…`, GitHub fine-grained `github_pat_…`, and GitHub server/oauth/user/refresh `gh[sour]_…` — bringing the redaction set to **11 patterns**, with a synthetic-secret fuzz test asserting zero leak per provider format (D-07). The existing PEM/JWT/anthropic/stripe/slack/`ghp_`/AWS/`sk-` patterns are retained.
45
+ - **`SECURITY.md` disclosure policy.** A repo-root `SECURITY.md` documents the supported-versions stance and routes vulnerability reports through **GitHub private security advisories** (the repo Security tab → "Report a vulnerability"); it publishes no email / no PII (D-02) and notes that enabling private vulnerability reporting is a one-line repo setting the maintainer must toggle (D-11).
46
+ - **Regression baseline.** `test/fixtures/baselines/phase-33-5/` freezes the hardened surface — a STRIDE-checklist snapshot, a hardening-surface invariant manifest (ws default host, redact pattern count, sanitize-env module + allowlist key, 11 gdd-state schemas, allowlist/threat-model/audit paths), and a synthetic secret-fuzz corpus — pinned by `test/suite/phase-33-5-baseline.test.cjs` so a future change cannot silently undo a hardened surface.
47
+
48
+ ### Notes
49
+
50
+ - **WebSocket event-stream now binds `127.0.0.1` by default (was `0.0.0.0`).** Opt into a remote bind via `.design/config.json` `event_stream.bind_host` or the `GDD_WS_BIND_HOST` env override. This is **not marked BREAKING** — the event stream is a token-gated observability surface (Bearer auth ships on every upgrade), and the safe-by-default localhost bind only restricts an unintended off-box exposure; the opt-in escape hatch preserves the remote-bind workflow for anyone who relied on it.
51
+ - All Phase 33.5 tests are hermetic (no network, no live peer; the WS test binds ephemeral localhost; the outbound + bind gates are static scans), so the default `npm test` stays green (D-10).
52
+ - 6-manifest lockstep at **v1.33.5** (`package.json` + `package-lock.json` (root + `packages.""`) + `.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json` (metadata.version + plugins[0].version) + `.cursor-plugin/plugin.json` + `.codex-plugin/plugin.json`). Version-sync hygiene done upfront (D-09): `OFF_CADENCE_VERSIONS.add('1.33.5')` + the 13 live-pinned `manifests-version.txt` baselines forward-propagated 1.33.0 → 1.33.5.
53
+
54
+ ---
55
+
7
56
  ## [1.33.0] - 2026-05-30
8
57
 
9
58
  ### Phase 33 — Skill Behavior Tests (Pressure-Scenario Harness)
package/README.md CHANGED
@@ -100,6 +100,10 @@ Closes the **outbound** half of multi-runtime: gdd agents now OPTIONALLY delegat
100
100
 
101
101
  See [docs/PEER-DELEGATION.md](docs/PEER-DELEGATION.md) for the ops guide (when delegation fires, fallback diagnostics, broker lifecycle, Windows quirks) and [reference/peer-protocols.md](reference/peer-protocols.md) for the ACP + ASP protocol cheat sheet.
102
102
 
103
+ ### OpenRouter provider (v1.33.6, opt-in)
104
+
105
+ You can route any agent's tier (`opus`/`sonnet`/`haiku`) through **OpenRouter** — an aggregator that fronts Claude, GPT, Llama, Gemini, DeepSeek and more behind a single API key — *alongside* your native provider auth, never instead of it. Set `OPENROUTER_API_KEY` and GDD's tier-resolver adapter dynamically fetches the OpenRouter catalog (24h TTL cache) and maps each tier onto a concrete model via a closed-vs-open + pricing heuristic, with a `.design/config.json#openrouter_tier_overrides` escape hatch for pinning exact ids. When the key is absent or the catalog is unreachable, resolution gracefully falls back to your native provider — OpenRouter is purely additive. See [`connections/openrouter.md`](connections/openrouter.md) for setup, the probe pattern, and fallback behavior, and run `/gdd:openrouter-status` to inspect catalog freshness and the resolved tier→model mapping.
106
+
103
107
  ### Previous releases
104
108
 
105
109
  - **v1.26.0** — Headless Model Resolver (per-runtime tier→model map, `resolved_models` router field, per-runtime price tables, `reasoning-class` runtime-neutral alias).
package/SKILL.md CHANGED
@@ -92,6 +92,7 @@ Each stage produces artifacts in `.design/` inside the current project.
92
92
  | `quality-gate` | `get-design-done:quality-gate` | Phase 25 — parallel lint/type/test/visual command runner; classifies failures via quality-gate-runner agent |
93
93
  | `turn-closeout` | `get-design-done:turn-closeout` | Phase 25 — Stop-hook mirror skill; finalizes per-turn STATE blocks and emits closeout events |
94
94
  | `bandit-status` | `get-design-done:bandit-status` | Phase 27.5 — read-only diagnostic surface for the bandit posterior; per-(agent, bin, delegate, tier) snapshots (alpha, beta, mean, stddev, count, last-used). Use `/gdd:bandit-reset` to mutate. |
95
+ | `openrouter-status [--refresh]` | `get-design-done:gdd-openrouter-status` | Phase 33.6 — read-only OpenRouter catalog + tier-mapping diagnostic; surfaces catalog freshness (vs 24h TTL), last-fetch, resolved opus/sonnet/haiku → model mappings, per-tier preview. `--refresh` re-fetches (needs `OPENROUTER_API_KEY`). |
95
96
  | `peers` | `get-design-done:peers` | Phase 27 — `/gdd:peers` capability matrix command; shows installed peer-CLIs (codex/gemini/cursor/copilot/qwen), allowlist status, claimed roles, posterior delta vs local |
96
97
  | `peer-cli-customize` | `get-design-done:peer-cli-customize` | Phase 27 — rewire role→peer mappings on a per-agent basis (edits frontmatter `delegate_to:` directly) |
97
98
  | `peer-cli-add` | `get-design-done:peer-cli-add` | Phase 27 — guided ladder for adding a brand-new peer (verification ladder + adapter scaffolding + capability-matrix update) |
@@ -115,6 +115,10 @@ Apply the decision table below to each new entry. Emit `{ ...entry, classificati
115
115
 
116
116
  The skip row is evaluated LAST and overrides the kind-based row — a component-system release titled "Sponsored: shipping our new sponsor tier" still ends up `skip`.
117
117
 
118
+ ### OpenRouter catalog drift (Phase 33.6, SC#8)
119
+
120
+ Beyond the design-authority feeds above, the **OpenRouter model catalog** (`.design/cache/openrouter-models.json`, fetched by `scripts/lib/openrouter/catalog-fetcher.cjs`) is a **weekly-diff feed**. Diff the prior vs current catalog via `scripts/lib/authority-watcher/index.cjs#diffOpenRouterCatalog(prevModels, currModels, { overrides })`, which classifies each delta as `new-model` / `pricing-change` / `deprecated` / `withdrawn`. To keep the report actionable and quiet, **surface ONLY `deprecated`/`withdrawn` entries whose id matches a configured `.design/config.json#openrouter_tier_overrides` pin** — i.e. the user pinned a model that is going away. `new-model` and `pricing-change` deltas are classified (returned, `surfaced:false`) but never surfaced as alerts (noise control). When OpenRouter is not configured (no catalog), this feed is silently skipped.
121
+
118
122
  ## Step 6 — Write Snapshot
119
123
 
120
124
  For each feed, merge the newly-fetched entries into `feeds[feed-id].entries`:
@@ -23,6 +23,7 @@ This directory contains connection specifications for external tools and MCPs th
23
23
  | pencil.dev | Active | [`connections/pencil-dev.md`](connections/pencil-dev.md) | File-based; `.pen` YAML specs; git-tracked; no MCP |
24
24
  | 21st.dev Magic MCP | Active | [`connections/21st-dev.md`](connections/21st-dev.md) | Uses `mcp__21st*` tools; `TWENTY_FIRST_API_KEY` required |
25
25
  | Magic Patterns | Active | [`connections/magic-patterns.md`](connections/magic-patterns.md) | Claude connector (`mcp__magic_patterns*`) + API key fallback |
26
+ | OpenRouter | Active | [`connections/openrouter.md`](connections/openrouter.md) | Model-router (no MCP); env: `OPENROUTER_API_KEY` (optional `OPENROUTER_BASE_URL`); opt-in tier-resolution overlay, graceful-degrade-to-native |
26
27
 
27
28
  ---
28
29
 
@@ -45,6 +46,7 @@ Each cell describes what the connection contributes at that pipeline stage, or `
45
46
  | pencil.dev | `.pen` discovery | `.pen` as canonical design source | — | pencil-writer: annotate/roundtrip | spec-vs-impl diff | ✓ | — |
46
47
  | 21st.dev | — | prior-art gate: marketplace search before greenfield build | — | component-generator (21st impl) | — | — | ✓ |
47
48
  | Magic Patterns | — | — | — | component-generator (magic-patterns impl) | preview_url → `? VISUAL` check | — | ✓ |
49
+ | OpenRouter | — | — | — | — | — | — | ✓ (model-router: tier→model resolution, all stages) |
48
50
 
49
51
  **Column definitions:**
50
52
 
@@ -0,0 +1,86 @@
1
+ # OpenRouter — Connection Specification
2
+
3
+ This file is the connection specification for OpenRouter within the get-design-done pipeline. OpenRouter is a **model aggregator** — one API key fronts dozens of upstream providers (Anthropic Claude, OpenAI GPT, Meta Llama, Google Gemini, DeepSeek, Qwen, Mistral, …). GDD treats it as a **tier-router**: the Phase-33.6 adapter (`scripts/lib/tier-resolver-openrouter.cjs`) maps GDD's `opus`/`sonnet`/`haiku` tiers onto a concrete OpenRouter catalog model id. It is **not** a canvas or visual-design tool — it does not read or write design surfaces. See `connections/connections.md` for the full connection index and capability matrix.
4
+
5
+ OpenRouter is **opt-in alongside** native provider auth, never OpenRouter-only (D-08). When it is not configured, tier resolution falls back to the native provider via the existing `scripts/lib/tier-resolver.cjs` fallback chain — nothing breaks.
6
+
7
+ ---
8
+
9
+ ## Setup
10
+
11
+ ### Prerequisites
12
+
13
+ - An OpenRouter account and API key at [openrouter.ai](https://openrouter.ai).
14
+ - `OPENROUTER_API_KEY` environment variable set — this is the **only** required secret. It is sent solely as an `Authorization: Bearer` header by the catalog fetcher (Phase 33.6-01) and is **never** persisted to the cache or any log.
15
+ - **OPTIONAL** `OPENROUTER_BASE_URL` environment variable — point the catalog fetch at a custom upstream (a self-hosted proxy, or a Vertex/Bedrock/Azure-fronting gateway). Defaults to `https://openrouter.ai/api/v1`. The cache always records the canonical public `source` URL regardless of `OPENROUTER_BASE_URL`.
16
+ - **OPTIONAL** opt-in flag — set `openrouter_enabled: true` in `.design/config.json` to route tiers through OpenRouter even before a key triggers a fetch. The presence of `OPENROUTER_API_KEY` also enables the consultation path. When neither is set, the OpenRouter adapter is never consulted (default behavior unchanged — D-08).
17
+ - **OPTIONAL** tier pins — `.design/config.json#openrouter_tier_overrides` (e.g. `{ "opus": "anthropic/claude-opus-4-7" }`) force a specific catalog id for a tier; the override wins over the heuristic (D-03).
18
+
19
+ ### Verification
20
+
21
+ ```
22
+ node -e "console.log(process.env.OPENROUTER_API_KEY ? 'available' : 'not_configured')"
23
+ ```
24
+
25
+ Key present → the catalog fetch (Phase 33.6-01) can run and the adapter resolves tiers from the dynamic catalog. Absent → `not_configured`; native auth stays primary and tier resolution falls back to the native provider.
26
+
27
+ To inspect the resolved catalog + tier mappings on demand, run `/gdd:openrouter-status` (read-only — see `skills/openrouter-status/SKILL.md`).
28
+
29
+ ---
30
+
31
+ ## Probe Pattern
32
+
33
+ OpenRouter has no MCP surface — it is probed by environment variable, not ToolSearch.
34
+
35
+ ```
36
+ Step OR1 — Env check:
37
+ OPENROUTER_API_KEY set → openrouter: available
38
+ OPENROUTER_API_KEY unset → openrouter: not_configured
39
+ ```
40
+
41
+ Write the result to STATE.md `<connections>`: `openrouter: <status>`.
42
+
43
+ `not_configured` is a normal, expected state — it is **not** an error. The pipeline continues with native auth.
44
+
45
+ ---
46
+
47
+ ## OpenRouter Capability
48
+
49
+ The capability classification for the `connections/connections.md` matrix:
50
+
51
+ | Capability | Value | Rationale |
52
+ |------------|-------|-----------|
53
+ | canvas | **no** | OpenRouter does not read or write a design canvas. |
54
+ | generator | **yes** | It fronts text/code generation models the pipeline can call. |
55
+ | model-router | **yes** | Its defining capability — it routes a GDD tier to a concrete upstream model id. |
56
+
57
+ OpenRouter is a model **provider/router**, not a design tool. Per D-12 it lives **only** in the tier-resolution layer (the adapter), not in the Phase-24 install registry (`scripts/lib/install/runtimes.cjs`) — you do not install GDD "into" OpenRouter.
58
+
59
+ ---
60
+
61
+ ## Pipeline Integration
62
+
63
+ | Stage | What OpenRouter provides |
64
+ |-------|--------------------------|
65
+ | (all stages, model selection) | When opted in, the tier resolver consults the OpenRouter catalog (Phase 33.6-02 adapter) to map `opus`/`sonnet`/`haiku` → a concrete catalog model id. |
66
+ | cost telemetry | Cost rows tag `provider: openrouter` when a model was resolved via the adapter (Phase 33.6-03, SC#6) — additive/back-compat with the Phase-27 `runtime_role`/`peer_id` cost-row fields. |
67
+ | drift | The authority-watcher diffs the OpenRouter catalog weekly and surfaces `deprecated`/`withdrawn` models that match a configured `openrouter_tier_overrides` pin (SC#8). |
68
+
69
+ The catalog is **dynamically fetched** with a 24h TTL cache at `.design/cache/openrouter-models.json` (gitignored runtime artifact) — there is no static catalog file. `reference/prices.openrouter.md` is a catalog-derived price snapshot (a derived view, not authority); `reference/openrouter-tier-mapping.md` documents the resolution heuristic.
70
+
71
+ ---
72
+
73
+ ## Fallback Behavior
74
+
75
+ OpenRouter resolution is **graceful-degrade-to-native** (D-08). The adapter returns `null` — and the caller falls back to the native provider via the existing `scripts/lib/tier-resolver.cjs` fallback chain — in every one of these cases:
76
+
77
+ - `openrouter: not_configured` (no `OPENROUTER_API_KEY`, opt-in flag off).
78
+ - The catalog cache is missing.
79
+ - The catalog is stale and cannot be re-fetched (no key, network failure, rate-limited).
80
+ - No catalog model matches the requested tier and no override pins it.
81
+
82
+ When `not_configured`:
83
+ - Print: `OpenRouter not configured — tier resolution uses the native provider.`
84
+ - The native provider (resolved from `reference/runtime-models.md`) remains primary.
85
+
86
+ OpenRouter is **never** the only path. Native auth is always the floor; OpenRouter is an opt-in overlay on top of it. The adapter `resolve(tier)` never throws — any error (unknown tier, corrupt config, corrupt cache) degrades to `null` and the native fallback takes over.
@@ -93,6 +93,22 @@ interface BudgetEnforcerBackend {
93
93
  reason: string | null;
94
94
  };
95
95
  modelFromResolved(resolved: unknown, agent: string): string | null;
96
+ // Plan 33.6-03 (SC#6): the canonical cost-row payload builder (the
97
+ // types.ts:237-designated emit site). Threads the optional `provider` tag
98
+ // ("openrouter" when the OpenRouter adapter resolved the model), omitting it
99
+ // when absent (back-compat).
100
+ buildCostEventPayload(args: {
101
+ runtime: string;
102
+ agent: string;
103
+ model_id: string | null;
104
+ tier: string | null;
105
+ tokens_in: number;
106
+ tokens_out: number;
107
+ cost_usd: number | null;
108
+ runtime_role?: 'host' | 'peer';
109
+ peer_id?: string | null;
110
+ provider?: string;
111
+ }): Record<string, unknown>;
96
112
  }
97
113
  const budgetBackend = nodeRequire('../scripts/lib/budget-enforcer.cjs') as BudgetEnforcerBackend;
98
114
  // Plan 26-05: runtime detection for the cost-event runtime tag. Returns
@@ -175,6 +191,51 @@ const tierResolver = nodeRequire(
175
191
  '../scripts/lib/tier-resolver.cjs',
176
192
  ) as TierResolverModule;
177
193
 
194
+ // Plan 33.6-03 (SC#6, D-08, D-12): OpenRouter tier-resolver adapter. When the
195
+ // user opts in (`.design/config.json#openrouter_enabled: true` OR
196
+ // `OPENROUTER_API_KEY` present), the hook consults this adapter FIRST for a
197
+ // resolved model; a non-null result routes to OpenRouter and tags the cost row
198
+ // `provider: "openrouter"`, a null result falls back to the native resolution
199
+ // path (unchanged default behavior). `resolve(tier, opts)` never throws.
200
+ interface TierResolverOpenRouterModule {
201
+ resolve(
202
+ tier: string,
203
+ opts?: { catalog?: unknown; models?: unknown; overrides?: unknown; cachePath?: string; configPath?: string; cwd?: string },
204
+ ): string | null;
205
+ }
206
+ const tierResolverOpenRouter = nodeRequire(
207
+ '../scripts/lib/tier-resolver-openrouter.cjs',
208
+ ) as TierResolverOpenRouterModule;
209
+
210
+ /**
211
+ * Plan 33.6-03 (SC#6 opt-in). OpenRouter is consulted ONLY when the user opts
212
+ * in — either `.design/config.json#openrouter_enabled === true` OR
213
+ * `OPENROUTER_API_KEY` is present in the environment. Best-effort + never
214
+ * throws: a missing/corrupt config degrades to "env var only". This keeps the
215
+ * default (no OpenRouter) behavior byte-identical for every existing user
216
+ * (D-08, D-12).
217
+ *
218
+ * @param cwd base dir for `.design/config.json` (default process.cwd())
219
+ */
220
+ function isOpenRouterEnabled(cwd?: string): boolean {
221
+ if (
222
+ typeof process.env.OPENROUTER_API_KEY === 'string' &&
223
+ process.env.OPENROUTER_API_KEY.length > 0
224
+ ) {
225
+ return true;
226
+ }
227
+ try {
228
+ const configPath = join(cwd ?? process.cwd(), '.design', 'config.json');
229
+ if (!existsSync(configPath)) return false;
230
+ const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as {
231
+ openrouter_enabled?: unknown;
232
+ };
233
+ return Boolean(parsed && parsed.openrouter_enabled === true);
234
+ } catch {
235
+ return false;
236
+ }
237
+ }
238
+
178
239
  // ── Types ───────────────────────────────────────────────────────────────────
179
240
 
180
241
  /**
@@ -661,6 +722,11 @@ function emitCostRecorded(
661
722
  tokens_in: number;
662
723
  tokens_out: number;
663
724
  cost_usd: number | null;
725
+ // Plan 33.6-03 SC#6 — optional resolution provider ("openrouter" when the
726
+ // OpenRouter adapter resolved the model). Additive/back-compat: omitted
727
+ // from the on-disk row when absent, so the legacy cost_recorded shape is
728
+ // preserved for every native-resolution + pre-33.6 spawn.
729
+ provider?: string;
664
730
  },
665
731
  cycle?: string,
666
732
  ): void {
@@ -677,6 +743,10 @@ function emitCostRecorded(
677
743
  tokens_in: payload.tokens_in,
678
744
  tokens_out: payload.tokens_out,
679
745
  cost_usd: payload.cost_usd,
746
+ // Omit-when-absent (mirrors the .cjs buildCostEventPayload discipline).
747
+ ...(typeof payload.provider === 'string' && payload.provider.length > 0
748
+ ? { provider: payload.provider }
749
+ : {}),
680
750
  },
681
751
  };
682
752
  try {
@@ -1149,6 +1219,36 @@ export async function main(): Promise<void> {
1149
1219
  }
1150
1220
  }
1151
1221
 
1222
+ // ── Plan 33.6-03 — OpenRouter resolution consultation (SC#6, D-08, D-12) ────
1223
+ //
1224
+ // When the user opts in (`.design/config.json#openrouter_enabled: true` OR
1225
+ // `OPENROUTER_API_KEY` present), consult the OpenRouter adapter for the
1226
+ // effective tier FIRST. A non-null result routes this spawn to OpenRouter:
1227
+ // we override the model id and tag the cost row `provider: "openrouter"`. A
1228
+ // null result (no key / catalog missing-or-stale / no match) falls through to
1229
+ // the native resolution that's already in `effectiveModelId` — so the default
1230
+ // (OpenRouter disabled) path is byte-identical to pre-33.6 behavior (D-08).
1231
+ // The adapter never throws; this whole branch is also wrapped defensively.
1232
+ let costProvider: string | undefined;
1233
+ if (isOpenRouterEnabled()) {
1234
+ try {
1235
+ const openrouterModel = tierResolverOpenRouter.resolve(effectiveTier);
1236
+ if (typeof openrouterModel === 'string' && openrouterModel.length > 0) {
1237
+ effectiveModelId = openrouterModel;
1238
+ costProvider = 'openrouter';
1239
+ // Reflect the OpenRouter pick into resolved_models so downstream
1240
+ // consumers see the actual model (mirrors the bandit override above).
1241
+ if (routerDecision !== undefined) {
1242
+ const rm = routerDecision.resolved_models ?? {};
1243
+ rm[agent] = openrouterModel;
1244
+ routerDecision.resolved_models = rm;
1245
+ }
1246
+ }
1247
+ } catch {
1248
+ // Fail open — never let OpenRouter resolution block a spawn (D-08).
1249
+ }
1250
+ }
1251
+
1152
1252
  // Compute runtime-aware cost via the shared backend. Failures return
1153
1253
  // null cost; we emit the event regardless so the cost-aggregator sees
1154
1254
  // the lookup attempt (Phase 22 events.jsonl tagging).
@@ -1169,6 +1269,9 @@ export async function main(): Promise<void> {
1169
1269
  tokens_in: Number(toolInput._tokens_in_est ?? 0),
1170
1270
  tokens_out: Number(toolInput._tokens_out_est ?? 0),
1171
1271
  cost_usd: costLookup.cost_usd,
1272
+ // Plan 33.6-03 SC#6 — tag the row when OpenRouter resolved the model.
1273
+ // Omitted (undefined) on the native path → buildCostEventPayload drops it.
1274
+ ...(costProvider !== undefined ? { provider: costProvider } : {}),
1172
1275
  },
1173
1276
  cycle,
1174
1277
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.33.0",
3
+ "version": "1.33.6",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -60,6 +60,8 @@
60
60
  "validate:frontmatter": "node --experimental-strip-types scripts/validate-frontmatter.ts agents/",
61
61
  "detect:stale-refs": "node scripts/detect-stale-refs.cjs",
62
62
  "scan:injection": "node scripts/run-injection-scanner-ci.cjs",
63
+ "scan:outbound": "node scripts/scan-outbound-network.cjs",
64
+ "scan:ws-bind": "node scripts/scan-ws-bind.cjs",
63
65
  "test:size-budget": "node --test test/suite/agent-size-budget.test.cjs",
64
66
  "release:extract-changelog": "node scripts/extract-changelog-section.cjs",
65
67
  "verify:version-sync": "node scripts/verify-version-sync.cjs",
@@ -111,6 +113,7 @@
111
113
  "ws": "^8.20.0"
112
114
  },
113
115
  "overrides": {
114
- "fast-json-patch": "^3.1.1"
116
+ "fast-json-patch": "^3.1.1",
117
+ "qs": ">=6.15.2"
115
118
  }
116
119
  }
@@ -0,0 +1,111 @@
1
+ # GDD Runtime Static Security Audit
2
+
3
+ **Phase:** 33.5 — GDD Runtime Security Hardening · **Plan:** 33.5-02 (Wave A) · **Date:** 2026-05-31
4
+ **Reference:** branch `phase/33-5-runtime-security`
5
+ **Scope:** the SHIPPED runtime surface — `hooks/`, `scripts/`, `sdk/`, `bin/`.
6
+ **Method:** static enumeration (read-only source inspection, no execution, no network).
7
+ **Companions:**
8
+
9
+ - `scripts/security/outbound-allowlist.json` — the machine-readable, CANONICAL active-egress
10
+ allowlist that Phase 33.5-04's `scripts/scan-outbound-network.cjs` gate `JSON.parse`s (this
11
+ report is its human-readable rationale).
12
+ - `reference/gdd-threat-model.md` — the STRIDE threat model (Phase 33.5-01).
13
+
14
+ This audit freezes the egress / secret / external-input picture so any *future* surface that is
15
+ not pre-approved trips the 33.5-04 gate at CI time. It is grounded in the CONTEXT real-tree sweep
16
+ ("no raw unexpected egress found"), re-verified against HEAD by reading each cited file.
17
+
18
+ ---
19
+
20
+ ## Outbound-network call sites
21
+
22
+ GDD's runtime makes outbound calls (or local IPC/server binds) from exactly the sites below. Each
23
+ maps to an `outbound-allowlist.json` directory glob (see the **Allowlisted** column). The 33.5-04
24
+ gate is **ACTIVE-egress-only** — it matches `require`/`import` of `node:http(s)`/`http(s)`/`ws`/
25
+ `node-fetch`/`axios`/`undici`, `fetch(`, `http(s).get/.request(`, `new WebSocket`/`new
26
+ WebSocketServer`/`WebSocketServer(`, and child-process `spawn`/`spawnSync`/`exec*` of
27
+ `gh|curl|wget|nc|ssh|scp` — so a bare URL string alone never trips it.
28
+
29
+ | File | What it calls | Transport | Legitimacy | Allowlisted by |
30
+ | --- | --- | --- | --- | --- |
31
+ | `scripts/lib/figma-extract/pull.cjs` | `fetch` → `https://api.figma.com/v1` (read-only) | REST (global `fetch`, injectable) | User-initiated Figma extract; token from `FIGMA_TOKEN`/`FIGMA_PERSONAL_ACCESS_TOKEN`, never persisted/logged | `scripts/lib/figma-extract/**` |
32
+ | `scripts/lib/figma-extract/styles-resolver.cjs` | Figma REST styles lookup (shares `pull.cjs` fetch path) | REST | Same extract flow; read-only | `scripts/lib/figma-extract/**` |
33
+ | `scripts/lib/figma-extract/receiver.cjs` | `require('node:http')` → `http.createServer` + `server.listen(RECEIVER_PORT, RECEIVER_HOST)` | local HTTP server | **EPHEMERAL** handshake server on `127.0.0.1`; lives for one extract run then exits (timeout-armed) | `scripts/lib/figma-extract/**` |
34
+ | `scripts/lib/transports/ws.cjs` | `require('node:http')` + `new WebSocketServer({ noServer })` + `httpServer.listen(opts.port)` | WebSocket over HTTP upgrade | Event-stream transport; **hardened in 33.5-03** (127.0.0.1 default bind, Bearer token ≥8 chars → timing-safe compare) | `scripts/lib/transports/ws.cjs` |
35
+ | `scripts/lib/issue-reporter/gh-submit.cjs` | `spawn('gh', …)` / `spawnSync` | `gh` CLI spawn | Rides `gh`'s own logged-in auth; **no raw HTTP**; frozen destination + kill-switch | `scripts/lib/issue-reporter/**` |
36
+ | `scripts/lib/issue-reporter/dedup.cjs` | `spawn('gh', …)` (3 call sites) | `gh` CLI spawn | Duplicate-issue lookup before submit; same frozen destination | `scripts/lib/issue-reporter/**` |
37
+ | `scripts/lib/issue-reporter/gh-absent-fallback.cjs` | `spawnSync('gh', ['gh'], {stdio:'ignore'})` (presence probe) | `gh` CLI spawn | Detects whether `gh` is installed; no payload | `scripts/lib/issue-reporter/**` |
38
+ | `scripts/lib/peer-cli/acp-client.cjs` | `spawn(command, args, …)` (no shell) | child-process / stdio IPC | Spawns a LOCAL peer binary over stdio (JSON-RPC); **IPC, not network**; env sandboxed (33.5-04) | `scripts/lib/peer-cli/**` |
39
+ | `scripts/lib/peer-cli/asp-client.cjs` | `spawn(...)` (no shell) | child-process / stdio IPC | Local peer over stdio; same env-allowlist sandbox | `scripts/lib/peer-cli/**` |
40
+ | `scripts/e2e/run-headless.ts` | live Anthropic API run | REST (key-gated) | Test infrastructure only; gated on `ANTHROPIC_API_KEY` + main-branch; never in default `npm test` | `scripts/e2e/**` |
41
+ | `scripts/lib/authority-watcher/index.cjs` | authority-feed classification of already-fetched records | (delegated) | The live article fetch is delegated to `agents/design-authority-watcher.md`; `index.cjs` is the pure-CommonJS classifier. Allowlisted so any direct feed fetch added here stays pre-approved | `scripts/lib/authority-watcher/**` |
42
+
43
+ **Documented but NOT hard-gated** (the scanner scope is `.js`-family + active-egress only —
44
+ these cannot exfiltrate on their own and are covered by gitleaks + the threat model):
45
+
46
+ | File / surface | What it is | Why not gated |
47
+ | --- | --- | --- |
48
+ | `scripts/lib/easings.cjs` (line 6) | React-Native `Easing.js` spec link in a comment | bare URL string, no call |
49
+ | `scripts/lib/spring.cjs` (line 6) | React-Native `SpringConfig.js` spec link in a comment | bare URL string, no call |
50
+ | `scripts/lib/install/merge.cjs` (line 85) | "Plugin repository: https://github.com/hegemonart/get-design-done" printed string | bare URL string, no call |
51
+ | `scripts/lib/issue-reporter/destination.cjs` (lines 29–30) | frozen `DESTINATION_URL` / `ISSUE_TEMPLATE_URL` constants | constants consumed only via the `gh` spawn above |
52
+ | `scripts/lint-agentskills-spec.cjs` (line 76) | spec-example URL in a comment | bare URL string, no call |
53
+ | `hooks/update-check.sh` | shell update-check egress | shell script — outside the `.{js,cjs,mjs,ts}` scanner scope |
54
+ | `scripts/bootstrap.sh` | shell bootstrap egress | shell script — outside the `.{js,cjs,mjs,ts}` scanner scope |
55
+
56
+ ---
57
+
58
+ ## Secret-handling sites
59
+
60
+ Where a token/key/credential is read, forwarded, or scrubbed across the shipped runtime.
61
+
62
+ | File | Secret touched | Action | Risk note |
63
+ | --- | --- | --- | --- |
64
+ | `scripts/lib/redact.cjs` | PEM, JWT, Anthropic `sk-ant-`, Stripe `sk_live_`, Slack `xox*`, GitHub `ghp_`, AWS `AKIA`, generic `sk-` (8 patterns) | **scrub** | Deep-walks event-stream payloads at serialize time so everything that hits disk / a bus subscriber is `[REDACTED:<type>]`. **D-07 extends** with Gemini `AIza…`, GitHub fine-grained `github_pat_`, and GitHub `ghs_/gho_/ghu_/ghr_` (currently uncovered). |
65
+ | `scripts/lib/peer-cli/acp-client.cjs` (line 102) | full `process.env` | **forward** (default) | `const env = opts.env … : process.env` → GDD's `ANTHROPIC_API_KEY`/`GH_TOKEN`/`GDD_*` leak to a spawned peer when `opts.env` is absent. **Fixed by 33.5-04** (allowlist-forward, default-deny, shared `sanitize-env`). |
66
+ | `scripts/lib/peer-cli/asp-client.cjs` (line 122) | full `process.env` | **forward** (default) | Same default-inherit gap; same 33.5-04 fix. |
67
+ | `scripts/lib/figma-extract/pull.cjs` | `FIGMA_TOKEN` / `FIGMA_PERSONAL_ACCESS_TOKEN` | **read** | Lives only inside caller-provided `headers`; NEVER written to disk, NEVER logged (diagnostics read a short body prefix, never the token — D-10 of the figma sub-plan). |
68
+ | `scripts/lib/issue-reporter/gh-submit.cjs` / `dedup.cjs` | none held by GDD | **delegate** | Authentication is `gh`'s own logged-in credential store; GDD never reads or forwards a GitHub token for these spawns. |
69
+
70
+ ---
71
+
72
+ ## External-input surfaces
73
+
74
+ Where untrusted data crosses a trust boundary into GDD's runtime.
75
+
76
+ | Surface | Untrusted source | Boundary | Risk note |
77
+ | --- | --- | --- | --- |
78
+ | WebSocket upgrade request | a remote client connecting to the event-stream WS | `scripts/lib/transports/ws.cjs` HTTP `upgrade` handler | Bearer-token gate (≥8 chars) already ships; **33.5-03** adds 127.0.0.1-default bind + timing-safe compare so the default config does not expose `0.0.0.0`. |
79
+ | gdd-state MCP tool inputs | the MCP client / model | `sdk/mcp/gdd-state/tools/*.ts` (11 tools) + `tools/shared.ts` | Each tool has a JSON schema under `sdk/mcp/gdd-state/schemas/`; **33.5-08** tightens them (`additionalProperties:false` + `maxLength`) and adds a payload-size cap (JSON-bomb guard). |
80
+ | `GDD_STATE_PATH` env override | the launching environment | `sdk/mcp/gdd-state/tools/shared.ts:resolveStatePath()` (line 60–61) | `process.env['GDD_STATE_PATH'] ?? .design/STATE.md` with **no path-traversal guard** today — `..`/absolute-outside escape is unchecked. **Closed by 33.5-08** (resolve + assert within project root / `.design/`). |
81
+ | `.design/config.json` | a repo-local config file (potentially attacker-influenced in a malicious clone) | 14 modules incl. `scripts/lib/peer-cli/registry.cjs` (line 154) | Drives `peer_cli.enabled_peers` / (33.5) `peer_cli.env_allowlist`, the WS `event_stream.bind_host`, and the issue-reporter kill-switch. Parsed defensively; opt-in by design (a peer must be explicitly enabled). |
82
+ | peer child `stdout` | a spawned LOCAL peer CLI | `scripts/lib/peer-cli/acp-client.cjs` (JSON-RPC frame parser) | A malicious/buggy peer could flood stdout — **already capped at 16 MiB un-newlined** (DoS guard). JSON-RPC frames are parsed, not `eval`'d. |
83
+
84
+ ---
85
+
86
+ ## Finding
87
+
88
+ The legitimate active-egress set is fully enumerated above and **no raw unexpected egress was
89
+ found** — every outbound call, server bind, or child spawn maps to one of six trusted modules,
90
+ each frozen into `scripts/security/outbound-allowlist.json` with a justification. The allowlist
91
+ uses **directory globs** so a new helper in an already-trusted module (e.g. another
92
+ `figma-extract` file) does not silently trip the gate, and every glob is asserted by
93
+ `test/suite/phase-33-5-audit.test.cjs` to resolve to ≥1 real file — so the 33.5-04 gate cannot be
94
+ defeated by a stale entry that matches nothing.
95
+
96
+ The residual gaps surfaced here are **closed by the remaining Phase 33.5 plans**:
97
+
98
+ - **33.5-03** — WebSocket bind hardening: 127.0.0.1 default bind (no more `0.0.0.0`), opt-in remote
99
+ via `event_stream.bind_host`, and a timing-safe (`crypto.timingSafeEqual`) token compare. Closes
100
+ the WS-upgrade external-input row.
101
+ - **33.5-04** — Outbound-network static CI gate: `scripts/scan-outbound-network.cjs` +
102
+ `npm run scan:outbound`, consuming THIS plan's `outbound-allowlist.json`. Also lands the
103
+ peer-CLI env-allowlist sandbox (shared `sanitize-env`) that closes the `acp-client.cjs` /
104
+ `asp-client.cjs` full-`process.env` forward rows.
105
+ - **33.5-05** (and 33.5-07/08) — secret-scan extension (Gemini + GitHub fine-grained/server
106
+ tokens) with a synthetic-secret fuzz, the gdd-state path-traversal guard + payload cap +
107
+ tightened schemas, `SECURITY.md`, and the regression baseline.
108
+
109
+ This report + the canonical allowlist satisfy **SEC-02** and unblock ROADMAP **SC#5** (the
110
+ outbound-network gate), as amended by **D-05** (corrected paths `reference/` + `test/suite/`) and
111
+ **D-06** (the gate mirrors the injection-scanner: a data file + a scanner that loads it).