@event4u/agent-config 9.5.0 → 9.7.0

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 (150) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +84 -942
  3. package/README.md +17 -17
  4. package/dist/agent-src/README.md +2 -2
  5. package/dist/agent-src/commands/agents/audit.md +2 -3
  6. package/dist/agent-src/commands/agents/optimize.md +2 -3
  7. package/dist/agent-src/commands/agents/user/review.md +4 -5
  8. package/dist/agent-src/commands/agents/user/show.md +4 -5
  9. package/dist/agent-src/commands/agents/user/update.md +4 -5
  10. package/dist/agent-src/commands/agents/user.md +2 -3
  11. package/dist/agent-src/commands/analytics/prune.md +2 -3
  12. package/dist/agent-src/commands/analytics/show.md +2 -3
  13. package/dist/agent-src/commands/analyze/decision.md +2 -3
  14. package/dist/agent-src/commands/analyze/incident.md +2 -3
  15. package/dist/agent-src/commands/analyze/near-miss.md +2 -3
  16. package/dist/agent-src/commands/analyze/postmortem.md +2 -3
  17. package/dist/agent-src/commands/analyze/premortem.md +2 -3
  18. package/dist/agent-src/commands/analyze/reference-repo.md +2 -3
  19. package/dist/agent-src/commands/brand/identity.md +2 -3
  20. package/dist/agent-src/commands/brand/review.md +2 -3
  21. package/dist/agent-src/commands/brand/strategy.md +2 -3
  22. package/dist/agent-src/commands/brand/tokens.md +2 -3
  23. package/dist/agent-src/commands/brand/voice.md +2 -3
  24. package/dist/agent-src/commands/bug/fix.md +2 -3
  25. package/dist/agent-src/commands/bug/investigate.md +2 -3
  26. package/dist/agent-src/commands/challenge-me/vision.md +2 -3
  27. package/dist/agent-src/commands/challenge-me/with-docs.md +2 -3
  28. package/dist/agent-src/commands/chat-history/import.md +2 -3
  29. package/dist/agent-src/commands/commit/in-chunks.md +2 -3
  30. package/dist/agent-src/commands/context/create.md +2 -3
  31. package/dist/agent-src/commands/context/refactor.md +2 -3
  32. package/dist/agent-src/commands/cost/report.md +2 -3
  33. package/dist/agent-src/commands/council/analysis.md +2 -3
  34. package/dist/agent-src/commands/council/debate.md +2 -3
  35. package/dist/agent-src/commands/council/design.md +2 -3
  36. package/dist/agent-src/commands/council/optimize.md +2 -3
  37. package/dist/agent-src/commands/council/pr.md +2 -3
  38. package/dist/agent-src/commands/feature/dev.md +2 -3
  39. package/dist/agent-src/commands/feature/explore.md +2 -3
  40. package/dist/agent-src/commands/feature/plan.md +2 -3
  41. package/dist/agent-src/commands/feature/refactor.md +2 -3
  42. package/dist/agent-src/commands/feature/roadmap.md +2 -3
  43. package/dist/agent-src/commands/fix/ci.md +2 -3
  44. package/dist/agent-src/commands/fix/comments.md +2 -3
  45. package/dist/agent-src/commands/fix/pr-comments.md +2 -3
  46. package/dist/agent-src/commands/fix/quality.md +2 -3
  47. package/dist/agent-src/commands/fix/route.md +2 -3
  48. package/dist/agent-src/commands/fix/seeder.md +2 -3
  49. package/dist/agent-src/commands/ghostwriter/delete.md +2 -3
  50. package/dist/agent-src/commands/ghostwriter/fetch.md +2 -3
  51. package/dist/agent-src/commands/ghostwriter/list.md +2 -3
  52. package/dist/agent-src/commands/ghostwriter/show.md +2 -3
  53. package/dist/agent-src/commands/ghostwriter/write.md +2 -3
  54. package/dist/agent-src/commands/image/analyse.md +2 -3
  55. package/dist/agent-src/commands/image/create.md +2 -3
  56. package/dist/agent-src/commands/image/verify.md +2 -3
  57. package/dist/agent-src/commands/knowledge/cross-repo.md +2 -3
  58. package/dist/agent-src/commands/knowledge/forget.md +2 -3
  59. package/dist/agent-src/commands/knowledge/ingest.md +2 -3
  60. package/dist/agent-src/commands/knowledge/list.md +2 -3
  61. package/dist/agent-src/commands/memory/add.md +2 -3
  62. package/dist/agent-src/commands/memory/learn-low-impact.md +2 -3
  63. package/dist/agent-src/commands/module/create.md +2 -3
  64. package/dist/agent-src/commands/module/explore.md +2 -3
  65. package/dist/agent-src/commands/optimize/agents-dir.md +2 -3
  66. package/dist/agent-src/commands/optimize/project.md +2 -3
  67. package/dist/agent-src/commands/optimize/prompt.md +2 -3
  68. package/dist/agent-src/commands/optimize/skills.md +2 -3
  69. package/dist/agent-src/commands/override/create.md +2 -3
  70. package/dist/agent-src/commands/override/manage.md +2 -3
  71. package/dist/agent-src/commands/post-as/ghostwriter.md +2 -3
  72. package/dist/agent-src/commands/post-as/me.md +2 -3
  73. package/dist/agent-src/commands/pr/create/description-only.md +2 -3
  74. package/dist/agent-src/commands/pr/create.md +1 -1
  75. package/dist/agent-src/commands/profile/activate.md +2 -3
  76. package/dist/agent-src/commands/profile/deactivate.md +2 -3
  77. package/dist/agent-src/commands/profile/show.md +2 -3
  78. package/dist/agent-src/commands/project/analyze.md +2 -3
  79. package/dist/agent-src/commands/project/health.md +2 -3
  80. package/dist/agent-src/commands/research/deep.md +2 -3
  81. package/dist/agent-src/commands/research/report.md +2 -3
  82. package/dist/agent-src/commands/review/changes.md +2 -3
  83. package/dist/agent-src/commands/review/routing.md +2 -3
  84. package/dist/agent-src/commands/roadmap/ai-council.md +2 -3
  85. package/dist/agent-src/commands/roadmap/create.md +2 -3
  86. package/dist/agent-src/commands/roadmap/materialize.md +2 -3
  87. package/dist/agent-src/commands/roadmap/process-full.md +2 -3
  88. package/dist/agent-src/commands/roadmap/process-phase.md +2 -3
  89. package/dist/agent-src/commands/roadmap/process-step.md +2 -3
  90. package/dist/agent-src/commands/skill/preview.md +2 -3
  91. package/dist/agent-src/commands/skills/discover.md +2 -3
  92. package/dist/agent-src/commands/sync/gitignore/fix.md +3 -4
  93. package/dist/agent-src/commands/tdd/green.md +2 -3
  94. package/dist/agent-src/commands/tdd/red.md +2 -3
  95. package/dist/agent-src/commands/tdd/refactor.md +2 -3
  96. package/dist/agent-src/commands/team/adversarial.md +2 -3
  97. package/dist/agent-src/commands/team/review.md +2 -3
  98. package/dist/agent-src/commands/team-knowledge/bootstrap.md +2 -3
  99. package/dist/agent-src/commands/team-knowledge/consolidate.md +2 -3
  100. package/dist/agent-src/commands/tests/create.md +2 -3
  101. package/dist/agent-src/commands/tests/e2e-heal.md +2 -3
  102. package/dist/agent-src/commands/tests/e2e-plan.md +2 -3
  103. package/dist/agent-src/commands/tests/execute.md +2 -3
  104. package/dist/agent-src/commands/video/from-script.md +2 -3
  105. package/dist/agent-src/commands/video/from-song.md +2 -3
  106. package/dist/agent-src/commands/video/scene.md +2 -3
  107. package/dist/agent-src/commands/video/stitch.md +2 -3
  108. package/dist/agent-src/commands/video/storyboard.md +2 -3
  109. package/dist/agent-src/commands/worktree/cleanup.md +2 -3
  110. package/dist/agent-src/commands/worktree/create.md +2 -3
  111. package/dist/agent-src/commands/worktree/status.md +2 -3
  112. package/dist/agent-src/commands/worktree/verify.md +2 -3
  113. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +10 -0
  114. package/dist/agent-src/rules/downstream-changes.md +31 -1
  115. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +34 -16
  116. package/dist/agent-src/skills/agents-md-thin-root/SKILL.md +9 -9
  117. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  118. package/dist/agent-src/templates/github-workflows/doc-impact.yml +101 -0
  119. package/dist/discovery/deprecation-report.md +1 -1
  120. package/dist/discovery/discovery-manifest.json +113 -113
  121. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  122. package/dist/discovery/discovery-manifest.summary.md +1 -1
  123. package/dist/discovery/orphan-report.md +1 -1
  124. package/dist/discovery/packs.json +2 -2
  125. package/dist/discovery/trust-report.md +1 -1
  126. package/dist/discovery/workspaces.json +2 -2
  127. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  128. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  129. package/dist/mcp/registry-manifest.json +3 -3
  130. package/docs/CLAIMS.md +2 -1
  131. package/docs/archive/CHANGELOG-6.0.0-overview.md +953 -0
  132. package/docs/benchmark.md +33 -9
  133. package/docs/decisions/ADR-122-adversarial-verification-council.md +30 -0
  134. package/docs/decisions/ADR-123-runtime-security-scope-and-spawn-hardening.md +142 -0
  135. package/docs/decisions/INDEX.md +1 -0
  136. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +26 -0
  137. package/docs/spawn-site-policy.md +72 -0
  138. package/docs/threat-model.md +2 -1
  139. package/package.json +2 -2
  140. package/src/agent-src/README.md +2 -2
  141. package/src/scripts/_lib/adversarial_bench_score.ts +106 -0
  142. package/src/scripts/_lib/spawn_env.ts +120 -0
  143. package/src/scripts/ai_council/clients.ts +6 -0
  144. package/src/scripts/bench_adversarial_council.ts +292 -0
  145. package/src/scripts/check_no_new_legacy_path.ts +2 -0
  146. package/src/scripts/check_source_pointer_freshness.ts +165 -0
  147. package/src/scripts/hooks/dispatch_hook.ts +5 -1
  148. package/src/scripts/hot_context_hook.ts +2 -0
  149. package/src/scripts/lint_readme_serial_comma.ts +169 -0
  150. package/src/scripts/roadmap_progress_hook.ts +2 -0
package/docs/benchmark.md CHANGED
@@ -559,7 +559,7 @@ replication on a per-token-billed key.
559
559
  - Runner: `src/scripts/bench_ab_v2_run.ts --host codex` (checkpoint-resumable).
560
560
  - Roadmap: `agents/roadmaps/archive/road-to-opt-measurement-unblock.md` Phase 2.
561
561
 
562
- ## adversarial-verification-council finding coverage — UNBACKED (pending corpus + run)
562
+ ## adversarial-verification-council finding coverage — HONEST NULL (resolved 2026-07-21)
563
563
 
564
564
  Pre-registered claim `adversarial-council-finding-coverage` (docs/CLAIMS.md,
565
565
  ADR-122): on the RESIDUAL defect pool — defects that survive a single strong
@@ -574,14 +574,38 @@ controversial-but-correct control.
574
574
  **AND** absolute >= +8 pp, **AND** panel FP not worse than baseline within
575
575
  noise — encoded + tested in
576
576
  [`src/scripts/_lib/adversarial_council_gate.ts`](../src/scripts/_lib/adversarial_council_gate.ts).
577
- - **Status: UNBACKED.** The corpus-validity gate BLOCKS the registered run: the
578
- existing `internal/bench/orchestration/corpus/` was built for cross-vendor
579
- *parity* (obvious, model-differentiating defects), not judge-survivable
580
- residual subtleties, so a run on it would be a measurement artifact (the
581
- council's Round-1 critique). Resolving the claim needs (1) a curated
582
- judge-survivable-subtlety corpus with a published distribution, then (2) a
583
- maintainer-gated paid cross-vendor run. Honest-null on that run keeps the
584
- surface default-off permanently, like recursive-verification.
577
+ - **Status: HONEST NULL (resolved 2026-07-21).** A curated judge-survivable
578
+ corpus was built (`internal/bench/adversarial-council/`: 12 planted-defect
579
+ fixtures across 4 subtlety classes + 3 controversial-but-correct clean
580
+ controls; subtlety distribution published; passed an independent validity
581
+ audit after one fixture was repaired). The registered cross-vendor run
582
+ (anthropic `claude-sonnet-4-5` + openai `gpt-4o`, spend-authorized) resolved
583
+ the claim NEGATIVE:
584
+
585
+ | quantity | value |
586
+ |---|--:|
587
+ | judge-passed residual pool | 5–7 defects (missed by both neutral passes) |
588
+ | single-skeptic residual recall | 0.60 |
589
+ | 2-vendor panel residual recall | 0.60 (**zero lift**) |
590
+ | single-skeptic FP on clean controls | 1.00 |
591
+ | panel FP on clean controls | 1.00 |
592
+
593
+ The second vendor's residual catches were a **strict subset** of the first's,
594
+ so the panel added no marginal coverage; both recall thresholds (+25% rel,
595
+ +8 pp abs) missed → **honest-null**. Under the adversarial-skeptic posture
596
+ both the single skeptic and the panel false-flagged **all three**
597
+ controversial-but-correct controls (100% FP) — the posture over-flags correct
598
+ code. Stable across two runs. Per the locked gate, the Mode 9 surface stays
599
+ **default-off permanently** (like recursive-verification). Reproducible
600
+ artifact: `internal/bench/adversarial-council/runs/`.
601
+ - **Transport lesson.** The first run attempted via `council_cli run` was
602
+ REJECTED as a measurement artifact: that transport runs multi-round
603
+ peer-review with cross-member visibility and returns prose ("Round 2 …
604
+ Reviewer A"), which both violates the independent-skeptic requirement and
605
+ defeats JSON scoring. The valid run uses direct, independent per-vendor
606
+ client calls (`bench_adversarial_council.ts`) with a strict-JSON system
607
+ prompt and a deterministic, pre-validated scorer
608
+ (`adversarial_bench_score.ts` + its synthetic red/green test).
585
609
 
586
610
 
587
611
  ## Defect-finding: team vs self-review vs council (2026-07-20) — HONEST NULL (ceiling-limited) {#honest-null-defect}
@@ -134,3 +134,33 @@ Each settled decision this roadmap does NOT touch:
134
134
  ADR-120 (council chairman mode), ADR-109 (subagent-v1 contract).
135
135
  - `docs/CLAIMS.md#adversarial-council-finding-coverage` — the pre-registered claim.
136
136
  - `cross-vendor-parity` (backed) — the evidence the finding-coverage scope rests on.
137
+
138
+ ## Addendum (2026-07-21) — finding-coverage claim resolved HONEST-NULL
139
+
140
+ The pre-registered `adversarial-council-finding-coverage` claim was executed and
141
+ resolved **negative**. A curated judge-survivable corpus
142
+ (`internal/bench/adversarial-council/`: 12 planted-defect fixtures across
143
+ multi-file-interaction / logic-inversion / security-masked / complex-state + 3
144
+ controversial-but-correct clean controls; subtlety distribution published;
145
+ passed an independent validity audit after one fixture was repaired) was run
146
+ cross-vendor (anthropic `claude-sonnet-4-5` + openai `gpt-4o`) under the
147
+ maintainer spend authorization.
148
+
149
+ **Result:** on the judge-passed residual, the 2-vendor skeptic panel matched the
150
+ single skeptic exactly (residual recall 0.60 = 0.60, **zero lift** — the second
151
+ vendor's catches were a strict subset of the first's), at a **100%** false-
152
+ positive rate on the controversial-but-correct controls under the adversarial-
153
+ skeptic posture. Both locked recall thresholds missed -> honest-null. **The
154
+ Mode 9 `adversarial-verification-council` surface stays default-off permanently**
155
+ (no settings change — it already ships off), recorded like recursive-verification
156
+ (ADR-106). Detail + reproducible artifact:
157
+ `docs/benchmark.md#adversarial-verification-council`,
158
+ `internal/bench/adversarial-council/runs/`.
159
+
160
+ **Transport lesson (durable):** `council_cli run`/`debate` is a *deliberation*
161
+ transport — multi-round peer-review with cross-member visibility, prose output.
162
+ It is the WRONG tool for an independent-skeptic, structured-output benchmark (it
163
+ violated the independence requirement and defeated JSON scoring; the first run
164
+ through it was rejected as an artifact). The valid measurement uses direct,
165
+ independent per-vendor client calls with a strict-JSON contract and a
166
+ deterministic, pre-validated scorer.
@@ -0,0 +1,142 @@
1
+ ---
2
+ adr: 123
3
+ status: accepted
4
+ date: 2026-07-20
5
+ decision: runtime-security-scope-and-spawn-hardening
6
+ supersedes: —
7
+ superseded_by: —
8
+ phase: road-to-runtime-security-hardening
9
+ type: structural
10
+ ---
11
+
12
+ # ADR-123 — Runtime-security scope: harden subprocess spawn-env, keep behavioural enforcement out of scope
13
+
14
+ - **Status:** Accepted (2026-07-20)
15
+ - **Closes:** `agents/roadmaps/archive/road-to-runtime-security-hardening.md`
16
+ - **Related:** `docs/threat-model.md` row (d) (lethal-trifecta); `src/rules/tool-safety.md` (Least Agency); `src/rules/lethal-trifecta-guard.md`; `src/rules/untrusted-input-defense.md`.
17
+
18
+ ## Context
19
+
20
+ A user-supplied source-level analysis compared agent-config's security stack
21
+ against an external reference plugin (referred to only as **Source A** per
22
+ `source-confidentiality`; © the third party, all-rights-reserved — concept
23
+ adoption only, no code/regex/prompt port). It surfaced two distinct things that
24
+ were repeatedly conflated:
25
+
26
+ 1. **A real shipped bug.** `src/scripts/ai_council/clients.ts::_runSubprocess`
27
+ built its `spawnSync` options with no `env:` key, so every spawned provider
28
+ CLI (`codex` / `claude` / `gemini`) — and any `git` those CLIs invoke
29
+ internally — inherited the full parent `process.env`. An attacker-influenced
30
+ `GIT_EXTERNAL_DIFF` / `core.pager` / `NODE_OPTIONS` / `LD_PRELOAD` /
31
+ `DYLD_INSERT_LIBRARIES` in the parent environment is remote code execution
32
+ (CWE-426 untrusted search path, CWE-88 argument injection). This is our own
33
+ transport violating the Least-Agency its `tool-safety` rule preaches.
34
+
35
+ 2. **A proposed scope reversal.** The analysis argued that because
36
+ `injection_scan_hook.ts` already scans **inbound** tool responses, the
37
+ threat-model's "runtime enforcement out of scope" decision (row (d)) is
38
+ already void, so we should also scan **outbound** — the content the agent
39
+ writes — with a pattern guard, a turn-diff baseline, and eventually an LLM
40
+ diff-review.
41
+
42
+ A two-round council (2026-07-20, anthropic/claude-sonnet-4-5 + openai/gpt-4o)
43
+ was decisive: round 1 leaned toward building the outbound guard; **round 2 both
44
+ members pivoted skeptical** on the load-bearing distinction below.
45
+
46
+ ## Decision
47
+
48
+ **1. Fix the spawn-env RCE (accepted, shipped in this roadmap).** A shared
49
+ `src/scripts/_lib/spawn_env.ts::hardenedSpawnEnv(overrides?)` scrubs the
50
+ code-execution-injection env families **by family, not by allowlist** — the
51
+ provider CLIs legitimately need arbitrary env (API keys, config paths), so an
52
+ allowlist would break them and rot; deny-by-family removes the known vectors
53
+ (dynamic-loader `LD_*`/`DYLD_*`/`GCONV_PATH`, git `*_COMMAND`/`GIT_EXTERNAL_DIFF`/
54
+ `GIT_SSH_COMMAND`/`PAGER`, runtime hooks `NODE_OPTIONS`/`BASH_ENV`/`ENV`/
55
+ `PYTHON*`/`PERL5*`/`RUBYOPT`, and `IFS`) and preserves everything else. It is
56
+ wired into the council transport and the two consumer-runtime hook spawn sites
57
+ (`roadmap_progress_hook`, `hot_context_hook`), and guarded by a falsifiable
58
+ regression test (proven RED without the fix — 8 vectors leaked into the child —
59
+ GREEN with it).
60
+
61
+ **2. Input sanitization ≠ runtime behavioural enforcement — row (d) stands.**
62
+ The inbound `injection_scan_hook` is **self-defence**: it hardens AC's own
63
+ handling of data it *receives* from external tools. It does **not** commit AC to
64
+ *governing what the agent writes*. An outbound command guard requires
65
+ interpreting intent from content (`rm -rf ./build` vs `rm -rf /`) — that is a
66
+ runtime **enforcement** layer, a categorically different mechanism, and the one
67
+ threat-model row (d) places out of scope. AC ships the *rules*
68
+ (`lethal-trifecta-guard`, `untrusted-input-defense`, `tool-safety`); the
69
+ consumer's tool-execution layer is the enforcement boundary. The inbound/outbound
70
+ "asymmetry" is a defensible separation of concerns, not security theatre.
71
+
72
+ **3. The outbound guard / turn-diff baseline / LLM diff-review are deferred, not
73
+ rejected.** They remain `## Blockers` on the roadmap with a `revisit-if`: a real
74
+ outbound-injection incident, a consumer demand signal, or utilization evidence —
75
+ at which point they must also pass the six-question complexity budget
76
+ (`artifact-drafting-protocol-mechanics` § Complexity budget), given PR #983 just
77
+ consolidated surface and `surface-consolidation-restraint` freezes new modes
78
+ pending the first external adopter. This ADR records the deferral so the
79
+ decision is surfaced, not silently dropped (`decision-revisit-gate`).
80
+
81
+ **4. Rejected:** a consumer-supplied-pattern ReDoS/trust-framing layer (Source A
82
+ P5) — `security_lint.ts` has no consumer-pattern surface, so guarding it would
83
+ build a defence for a feature that does not exist. Global env/pattern allowlists
84
+ are also rejected: per-file pragma containment (`security_lint.ts`) only.
85
+
86
+ ## Consequences
87
+
88
+ - The council transport and consumer-runtime hooks can no longer be turned into
89
+ an RCE vector by a poisoned parent environment; the fix is falsifiably tested.
90
+ - `docs/threat-model.md` gains a row for the (now-mitigated)
91
+ subprocess-env-inheritance vector, and row (d)'s "no additional mitigation
92
+ planned" cell cites this ADR for the input-sanitization-vs-enforcement
93
+ distinction and the deferred-guard decision.
94
+ - AC's security posture stays **authoring-time rules + input hygiene + our own
95
+ Least-Agency spawn discipline** — it does not become a runtime sandbox
96
+ supervising every agent tool call.
97
+ - Maintainer-only CI/git-helper spawn sites (~15) are a deferred mechanical
98
+ sweep (`maintainer-ci-spawn-sweep` blocker): they run in trusted CI with lower
99
+ attacker-control of the environment.
100
+
101
+ ## Alternatives considered
102
+
103
+ - **Complete the runtime layer (build the outbound guard now).** Council round 1
104
+ position. Rejected in round 2: conflates input sanitization with behavioural
105
+ enforcement; adds net-new enforcement surface with a false-positive burden,
106
+ right after a consolidation roadmap, with no demand signal.
107
+ - **Delete `injection_scan_hook.ts` for symmetry.** Architecturally consistent
108
+ but operationally worse — inbound sanitization is legitimate self-defence and
109
+ costs nothing to keep.
110
+ - **Env allowlist instead of deny-by-family.** Rejected — would strip env the
111
+ provider CLIs need and rot as new good vars appear.
112
+
113
+ ## Follow-up (2026-07-21) — `road-to-spawn-env-completion`
114
+
115
+ The 9.6.0 external review reproduced a working exploit against the fix this ADR
116
+ shipped: `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>` (and
117
+ `GIT_CONFIG_GLOBAL` / `_SYSTEM`) survived the deny-by-family scrub — the
118
+ `GIT_*_COMMAND` family missed them (no `_COMMAND` suffix) — letting an attacker
119
+ set arbitrary git config (`core.fsmonitor` runs shell on every `git status`,
120
+ which the consumer-runtime hooks trigger). `GIT_ALTERNATE_OBJECT_DIRECTORIES`
121
+ and `HOSTALIASES` also leaked. **Confirmed empirically (all 5) and closed:**
122
+ `hardenedSpawnEnv` now denies the whole `GIT_CONFIG`/`GIT_CONFIG_*` family by
123
+ prefix plus the two exact vars, with falsifiable test vectors (red without the
124
+ fix, green with it).
125
+
126
+ **`maintainer-ci-spawn-sweep` blocker — resolved by classification, not mass
127
+ migration.** Rather than migrate ~15 low-risk maintainer/CI sites, all spawn
128
+ sites are now classified in `docs/spawn-site-policy.md` (Consumer Runtime /
129
+ Maintainer CLI / trusted CI / install-time). The consumer-runtime dispatcher
130
+ (`hooks/dispatch_hook.ts`) — missed in PR #984 — now routes through
131
+ `hardenedSpawnEnv`; the maintainer/CI/install sites are documented exempt with
132
+ rationale (env is maintainer- or workflow-controlled, not attacker-influenced).
133
+
134
+ **Secure-spawn lint — rejected (council 2026-07-21, claude-sonnet-4-5 +
135
+ gpt-4o).** A CI lint forbidding raw spawn in runtime paths was proposed (P0 in
136
+ the review) but rejected: it is net-new governance surface (a CI gate +
137
+ allowlist + exemption-adjudication + false-positive triage), which the folded
138
+ complexity-budget lock (PR #983) forbids — the lock text explicitly lists
139
+ "linter". The "retires ad-hoc review" justification failed the retirement test
140
+ (no such review gate exists to retire). `docs/spawn-site-policy.md` is the
141
+ self-enforcing substitute: its completeness claim is checked by git-diff
142
+ visibility at review time, not a build gate.
@@ -126,6 +126,7 @@ _Auto-generated by `scripts/adr/regenerate_index.py`. Do not edit._
126
126
  | [ADR-120](ADR-120-council-chairman-mode.md) | Council Chairman Mode | accepted | 2026-07-12 | — |
127
127
  | [ADR-121](ADR-121-knowledge-sensitivity-classes.md) | Knowledge Sensitivity Classes | accepted | 2026-07-12 | — |
128
128
  | [ADR-122](ADR-122-adversarial-verification-council.md) | Adversarial Verification Council | accepted | 2026-07-14 | — |
129
+ | [ADR-123](ADR-123-runtime-security-scope-and-spawn-hardening.md) | Runtime Security Scope And Spawn Hardening | accepted | 2026-07-20 | — |
129
130
  | [ADR-200](ADR-200-python-to-typescript-migration.md) | Python To Typescript Migration | accepted | 2026-06-11 | — |
130
131
 
131
132
  ## Unnumbered (legacy)
@@ -70,6 +70,32 @@ those artifacts:
70
70
  - Batch mode never skips the Research pass itself — it relocates and
71
71
  batches it. `artifact_protocol: skip` does not exist.
72
72
 
73
+ ## Complexity budget — the six questions before a new artefact
74
+
75
+ Folded here from the 9.4.0 review (Prio 5), which named the failure mode:
76
+ "das Paket kann fast jede Kritik mit einem weiteren Mechanismus beantworten"
77
+ (Problem → Regel → Lint → Hook → Report → Roadmap → Claim → Test = governance
78
+ inflation). This REPLACES the ad-hoc "should this exist" prose — it is a
79
+ checklist, not a new gate/rule (adding a gate to enforce "add less" would be
80
+ the very inflation it guards against). Before drafting any new skill / rule /
81
+ command / hook / gate, answer all six; a "no" on **replaces** or **removable**
82
+ is a strong signal to fold into an existing artefact instead:
83
+
84
+ 1. **Replaces?** — what existing mechanism does this retire or subsume? (If
85
+ nothing, justify why the surface must grow.)
86
+ 2. **Overlaps?** — does it duplicate a trigger / responsibility another
87
+ artefact already owns?
88
+ 3. **Discoverable?** — will a user actually find it, or does it just raise the
89
+ catalog count?
90
+ 4. **Measurable?** — is there a signal that would show it earning its place
91
+ (or a null that would retire it)?
92
+ 5. **Removable?** — can it be demoted / de-eligibled / deleted later without a
93
+ migration?
94
+ 6. **Who debugs it?** — is the maintenance owner real, given bus-factor?
95
+
96
+ The load-bearing question the review elevates: not "how do we prevent this
97
+ error in future?" but **"which existing mechanism gets removed or replaced?"**
98
+
73
99
  ## See also
74
100
 
75
101
  - `artifact-drafting-protocol` (rule) — Iron Law, triggers, golden rules.
@@ -0,0 +1,72 @@
1
+ # Spawn-Site Policy — subprocess env-hardening classification
2
+
3
+ > Every place agent-config spawns a subprocess is classified into one of four
4
+ > buckets. The bucket decides whether the spawn MUST route its environment
5
+ > through `hardenedSpawnEnv()` (`src/scripts/_lib/spawn_env.ts`). This document
6
+ > is the single authoritative inventory — a new spawn site is visible in a PR
7
+ > diff as either an addition to a bucket below or an unclassified site that
8
+ > contradicts this doc's completeness claim. That git-diff visibility is the
9
+ > enforcement mechanism (a dedicated lint was considered and **rejected** — see
10
+ > ADR-123 and the "No lint" note below).
11
+
12
+ ## The buckets
13
+
14
+ | Bucket | Definition | Env rule |
15
+ |---|---|---|
16
+ | **Consumer Runtime** | Runs on the end-user's machine as part of the installed package — the hook dispatcher, dispatched hook concerns, and the AI-council CLI transport. The parent `process.env` is whatever the user's agent/shell exposes and is influenceable by repo content the agent just read. | **MUST** pass `env: hardenedSpawnEnv(overrides?)`. |
17
+ | **Maintainer CLI / tooling** | Runs on a maintainer's dev machine, invoked explicitly (benchmarks, fixture replay, wizards, one-off scripts). The env is the maintainer's own. | SHOULD prefer the helper; MAY use raw env with a stated reason. Not attacker-controlled. |
18
+ | **CI (trusted)** | Runs inside this repo's GitHub Actions on the repo's own refs (git/gh helpers reading `git log` / `gh pr view`). Env is set by the workflow file, not by PR content; fork PRs get no secrets. | Raw env permitted. Migrate only if a job begins consuming untrusted fork content into its env. |
19
+ | **Install-time** | Runs during `npx`/install (the installer spawning package managers from a lockfile). | Raw env permitted; installs are lockfile-pinned. Revisit if an installer step ingests untrusted input. |
20
+
21
+ ## Consumer-Runtime inventory (the security-critical set)
22
+
23
+ All Consumer-Runtime spawns route through `hardenedSpawnEnv()`:
24
+
25
+ | Site | Spawns | Status |
26
+ |---|---|---|
27
+ | `src/scripts/ai_council/clients.ts::_runSubprocess` | provider CLIs (`codex`/`claude`/`gemini`) | ✅ hardened (ADR-123 / PR #984) |
28
+ | `src/scripts/hot_context_hook.ts` | `git rev-parse` | ✅ hardened (PR #984) |
29
+ | `src/scripts/roadmap_progress_hook.ts` | tsx regenerator (runs git) | ✅ hardened (PR #984) |
30
+ | `src/scripts/hooks/dispatch_hook.ts` | every hook concern via tsx | ✅ hardened (this roadmap) |
31
+ | `src/scripts/hooks/replay_hook.ts` | re-dispatches through the runtime | Maintainer/fixture tool; its downstream concern spawn is hardened by `dispatch_hook` above. Exempt at this layer. |
32
+
33
+ Any NEW consumer-runtime spawn (a new hook that shells out, a new installed
34
+ runtime path) MUST be added to this table and route through `hardenedSpawnEnv`.
35
+
36
+ ## Maintainer / CI / Install (exempt, with rationale)
37
+
38
+ The remaining spawn sites (benchmarks `bench_*`, `consumer_matrix`, the
39
+ installer `install.ts`, the fleet CLI, and the ~15 git/gh maintainer helpers
40
+ such as `check_trunk_drift`, `evidence_report`, `print_required_checks`,
41
+ `check_release_pr_shape`, `migration_status`, …) are Maintainer CLI, trusted CI,
42
+ or install-time. Their environment is the maintainer's own or GitHub Actions'
43
+ workflow-controlled env — not influenced by untrusted repo content the agent
44
+ read at runtime. They are exempt from the MUST rule. A maintainer/CI site moves
45
+ to the Consumer-Runtime bucket (and MUST harden) only if it starts running on an
46
+ end-user machine or ingesting untrusted fork content into its spawn env.
47
+
48
+ ## What `hardenedSpawnEnv` scrubs
49
+
50
+ Deny-by-family (not an allowlist — the CLIs legitimately need arbitrary env):
51
+ dynamic loader (`LD_*`, `DYLD_*`, `GCONV_PATH`), git command hooks
52
+ (`GIT_*_COMMAND`, `GIT_EXTERNAL_DIFF`, `GIT_SSH_COMMAND`, `GIT_PAGER`, `PAGER`),
53
+ **git config-injection** (`GIT_CONFIG` and the whole `GIT_CONFIG_*` family —
54
+ `GIT_CONFIG_COUNT`/`_KEY_<n>`/`_VALUE_<n>`/`_GLOBAL`/`_SYSTEM`, the
55
+ `core.fsmonitor` RCE primitive), `GIT_ALTERNATE_OBJECT_DIRECTORIES`,
56
+ `HOSTALIASES`, runtime auto-exec hooks (`NODE_OPTIONS`, `BASH_ENV`, `ENV`,
57
+ `PYTHON*`, `PERL5*`, `RUBYOPT`), and `IFS`. Everything else is preserved.
58
+
59
+ ## No lint (deliberate)
60
+
61
+ A CI lint forbidding raw spawn in runtime paths was proposed and **rejected**
62
+ (council 2026-07-21; recorded in ADR-123): it is net-new governance surface (a
63
+ CI gate + allowlist + exemption-adjudication + false-positive triage), which the
64
+ complexity-budget lock forbids ("must not become a new rule/linter/gate"). This
65
+ inventory doc is the self-enforcing substitute — its completeness claim is
66
+ checked by git-diff visibility at review time, not by a build gate.
67
+
68
+ ## See also
69
+
70
+ - `docs/decisions/ADR-123-runtime-security-scope-and-spawn-hardening.md` — the runtime-security scope decision + the rejected-lint record.
71
+ - `docs/threat-model.md` row (g) — the subprocess-env-inheritance vector.
72
+ - `src/scripts/_lib/spawn_env.ts` — the helper.
@@ -13,9 +13,10 @@ and holds no user data. Threats are about **artifact integrity** and
13
13
  | a | **Malicious skill / rule via PR** — a contributed `.md` file smuggles prompt-injection or data-exfiltration payloads into the shipped artifact tree | Adversarial PR contributor embeds: (1) invisible Unicode (bidi / zero-width / Tag-block) to hide instructions, (2) role-takeover imperatives, (3) lethal-trifecta pattern in a new skill (private-data + untrusted-content + egress on one autonomous path) | `lint_hidden_unicode.py` — blocks Trojan-Source and invisible-char attacks; `lint_instruction_smuggling.py` — blocks disclosure-suppression and injection-tag prose; `lint_skill_frontmatter_safety.py` — blocks wildcard `allowed_tools`, `bypassPermissions`, unsafe `execution.type: automated`; PR review gate | No automated SARIF upload to GitHub Code Scanning; lethal-trifecta detection is architectural (rule) not linted structurally beyond the frontmatter check | Wire `lint_agent_security.py --sarif` output to GitHub Advanced Security on every PR (`lint_workflow_security.py` — P2); add structural trifecta check to frontmatter linter |
14
14
  | b | **Supply-chain compromise of AC's own deps** — a malicious version of an npm or Python dependency is pulled during CI / install and executes arbitrary code or modifies shipped artifacts | Dependency confusion / typosquatting attack on `package.json` or `requirements.txt` deps; compromised upstream package | `package-lock.json` + `requirements.txt` pin exact versions; CI installs from lockfile (`npm ci`) | No documented dep-audit policy; no Dependabot / Renovate automation; no integrity check (`npm audit` / `pip-audit`) in CI gate | Add `npm audit --audit-level=high` + `pip-audit` as a required CI step; document quarterly manual review cadence; pin Python deps in a lockfile (`pip-compile`) |
15
15
  | c | **Hostile consumer-repo environment** — AC's installer or hooks execute inside a repo whose `.git/hooks`, `.env`, or workspace files are attacker-controlled | Consumer repo pre-installs malicious Git hooks that intercept the AC installer or post-install hook; `.env` leaks tokens that AC hooks read; untrusted files are passed to AC linters | AC installer writes its own hooks under its namespace; AC hooks run as the user's shell — they do not read `.env` by design; `untrusted-input-defense` rule governs agent behaviour at runtime | No sandboxed execution for AC hooks; installer does not verify consumer hook integrity before writing | Document that AC hooks must not read repo-local env files; add installer pre-flight that warns if foreign hooks exist in `.git/hooks/`; scope AC hook writes to explicit namespaced filenames |
16
- | d | **Lethal-trifecta surface** — a skill combining private-data access + untrusted-content ingestion + external communication on one autonomous path enables confused-deputy / data-exfiltration attacks | Prompt-injected content inside a fetched doc instructs an agent to read a local secret and POST it to an attacker-controlled endpoint | `src/rules/lethal-trifecta-guard.md` (Iron Law: break one leg or gate egress behind human-in-the-loop); `src/rules/untrusted-input-defense.md` (untrusted content is data, never instructions; spotlighting); `lint_skill_frontmatter_safety.py` (flags over-broad tool grants) | These are authoring-time controls — no runtime enforcement layer exists | No additional mitigation planned (runtime layer out of scope for AC); `lethal-trifecta-guard.md` and `untrusted-input-defense.md` are the canonical reference — do not re-describe here |
16
+ | d | **Lethal-trifecta surface** — a skill combining private-data access + untrusted-content ingestion + external communication on one autonomous path enables confused-deputy / data-exfiltration attacks | Prompt-injected content inside a fetched doc instructs an agent to read a local secret and POST it to an attacker-controlled endpoint | `src/rules/lethal-trifecta-guard.md` (Iron Law: break one leg or gate egress behind human-in-the-loop); `src/rules/untrusted-input-defense.md` (untrusted content is data, never instructions; spotlighting); `lint_skill_frontmatter_safety.py` (flags over-broad tool grants) | These are authoring-time controls — no runtime *behavioural-enforcement* layer exists (input sanitization ≠ enforcement — see ADR-123) | Runtime behavioural enforcement stays out of scope for AC (`ADR-123-runtime-security-scope-and-spawn-hardening`): AC ships the rules, the consumer's tool-execution layer enforces them; the outbound content-scan guard is deferred with a `revisit-if`, not silently dropped. `lethal-trifecta-guard.md` and `untrusted-input-defense.md` are the canonical reference — do not re-describe here |
17
17
  | e | **Malicious metadata / frontmatter injection** — a PR embeds hostile YAML in a skill's frontmatter (e.g. injecting a trigger keyword that causes a dangerous skill to fire in unintended contexts, or adding `permissionMode: bypassPermissions`) | PR contributor adds `permissionMode: bypassPermissions` or a wildcard `allowed_tools: ["*"]` entry to an existing skill's frontmatter; or crafts a trigger keyword to hijack routing | `lint_skill_frontmatter_safety.py` — HIGH-severity block on `bypassPermissions`, wildcard `allowed_tools`, unsafe `execution.type: automated` without safety floor; `lint_hidden_unicode.py` — catches invisible chars inside frontmatter YAML values; PR review gate; `scripts/validate_frontmatter.py` schema validation | Schema validation does not verify trigger-keyword semantic safety (a plausible keyword that causes over-broad firing is not caught) | Extend `validate_frontmatter.py` to flag trigger keywords that match security-sensitive path prefixes (e.g. a skill triggering on `"auth"` when it has `execution.type: automated`) |
18
18
  | f | **Skill self-update — safety-floor rewrite** — an agent is social-engineered (via chat or a PR) into weakening a kernel rule, safety floor, or MCP/tool allowlist | A PR removes the Iron Law block from `non-destructive-by-default.md`; or a chat prompt instructs the agent to edit `security-sensitive-stop.md` to narrow its self-modification clause; or a PR modifies `scope-control` to remove the Hard Floor | `src/rules/security-sensitive-stop.md` — "self-modification via chat" clause: requests to weaken safety floors / kernel rules / MCP allowlists must route through `scope-control` gates; `src/rules/scope-control.md` — kernel-rule edits require own PR + ≥ 24 h soak; `dist/agent-src/` is read-only (source-of-truth rule); `scripts/check_condensation.py` detects Iron-Law section deletion or downgrade | No automated per-rule diff check that fires specifically when a kernel rule's Iron Law section is removed; relies on PR reviewer recognising the file class | Add a CI check (`scripts/check_kernel_rule_integrity.py` — P3+) that asserts Iron Law fenced-code blocks survive unchanged in all files listed in `docs/contracts/kernel-membership.md` |
19
+ | g | **Subprocess spawn-env inheritance** — AC's own transport spawns an external CLI (the AI-council provider CLIs; consumer-runtime hooks spawn `git`) with the full parent `process.env`, so an attacker-influenced env var in the parent becomes RCE when the child (or a `git` it invokes internally) runs | A poisoned `GIT_EXTERNAL_DIFF` / `core.pager` / `GIT_SSH_COMMAND`, the `GIT_CONFIG_COUNT`/`_KEY_<n>`/`_VALUE_<n>` config-injection trio (sets `core.fsmonitor` → shell on every `git status`), a dynamic-loader `LD_PRELOAD` / `DYLD_INSERT_LIBRARIES` / `GCONV_PATH`, `GIT_ALTERNATE_OBJECT_DIRECTORIES` / `HOSTALIASES`, or a runtime hook `NODE_OPTIONS` / `BASH_ENV` / `PYTHONSTARTUP` present in the parent env executes attacker code inside the spawned process (CWE-426, CWE-88, CWE-427) | `src/scripts/_lib/spawn_env.ts::hardenedSpawnEnv()` scrubs the code-execution-injection env families (deny-by-family: loader `LD_*`/`DYLD_*`/`GCONV_PATH`, git `*_COMMAND` + the whole `GIT_CONFIG`/`GIT_CONFIG_*` family + `GIT_ALTERNATE_OBJECT_DIRECTORIES`/`HOSTALIASES`/`GIT_EXTERNAL_DIFF`/`GIT_SSH_COMMAND`/`GIT_PAGER`/`PAGER`, runtime hooks `NODE_OPTIONS`/`BASH_ENV`/`ENV`/`PYTHON*`/`PERL5*`/`RUBYOPT`, `IFS`) and preserves the rest; wired into ALL consumer-runtime spawns — `clients.ts::_runSubprocess`, `hooks/dispatch_hook.ts`, `roadmap_progress_hook.ts`, `hot_context_hook.ts`; falsifiable regression test in `tests/scripts/ai_council/spawn_env.test.ts` (ADR-123 + 2026-07-21 follow-up) | — (consumer-runtime surface fully hardened; maintainer/CI/install sites are env-controlled, not attacker-influenced) | Spawn sites classified in `docs/spawn-site-policy.md` (Consumer Runtime MUST harden / Maintainer / CI / install exempt with rationale); the deferred lint was rejected — the policy doc is the self-enforcing substitute |
19
20
 
20
21
  ## Review cadence
21
22
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "9.5.0",
4
- "description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
3
+ "version": "9.7.0",
4
+ "description": "Universal AI Agent OS — audited skills, governance rules, commands and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
5
5
  "license": "MIT",
6
6
  "private": false,
7
7
  "type": "module",
@@ -1,8 +1,8 @@
1
1
  # Agent Infrastructure
2
2
 
3
3
  > **⚠️ DO NOT edit files in `dist/agent-src/` or `.augment/` directly.**
4
- > The source of truth is `.agent-src.uncondensed/`. Edit there, then run `task sync`.
5
- > See `.agent-src.uncondensed/commands/condense.md` for the full workflow.
4
+ > The source of truth is `src/` (`src/skills/`, `src/rules/`, `src/agent-src/`). Edit there, then run `task sync`.
5
+ > See the `source-of-truth` rule and the `/condense` command for the full workflow.
6
6
 
7
7
  This directory contains the shared agent infrastructure: skills, commands, rules, guidelines,
8
8
  templates, and contexts that power the AI coding assistant across all projects.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Deterministic scorer for the adversarial-council residual-detection benchmark
3
+ * (road-to-adversarial-council-benchmark Phase 1).
4
+ *
5
+ * Pure + countable — never LLM-computed (the anti-lesson from ADR-122). Given a
6
+ * corpus item's ground truth and a reviewer's structured findings, decide
7
+ * whether the reviewer CAUGHT the planted defect (for a defect fixture) or
8
+ * raised a FALSE POSITIVE (for a clean control). The two-stage recalls and FP
9
+ * rates this produces feed `evaluateCouncilBench` (adversarial_council_gate.ts).
10
+ *
11
+ * Matching rule (deliberately NOT exact file:line — the scorer bug from the
12
+ * earlier defect-finding benchmark required an exact line token and mis-scored
13
+ * genuine catches that cited a nearby line): a finding CATCHES a planted defect
14
+ * iff it names one of the defect's file(s) by basename AND its category falls in
15
+ * the same normalized family as the planted defect. Line numbers are advisory,
16
+ * never required. This credits a real catch that localizes the right file +
17
+ * defect class, and denies credit for a scattershot "something's wrong here"
18
+ * with the wrong class.
19
+ */
20
+
21
+ /** Normalize a free-form category string to a coarse family. */
22
+ export function categoryFamily(raw: string): string {
23
+ const c = raw.toLowerCase().replace(/[^a-z]/g, '');
24
+ if (/(accesscontrol|authz|authorization|idor|bola|ownership|privilege|rbac)/.test(c)) return 'access-control';
25
+ if (/(auth|authentication|session|credential|login)/.test(c)) return 'auth';
26
+ if (/(ssrf|serverside)/.test(c)) return 'ssrf';
27
+ if (/(pathtraversal|traversal|directorytraversal|lfi)/.test(c)) return 'path-traversal';
28
+ // NB: no bare "rce" token — it substring-matches inside "resou[rce]leak".
29
+ if (/(injection|sqli|xss|commandinjection|deserial|remotecodeexec)/.test(c)) return 'injection';
30
+ if (/(crypto|cryptographic|timing|constanttime|hash|signature)/.test(c)) return 'crypto';
31
+ if (/(concurrency|race|atomic|deadlock|idempoten)/.test(c)) return 'concurrency';
32
+ if (/(resourceleak|leak|memory|handle|unsubscrib|cleanup)/.test(c)) return 'resource-leak';
33
+ if (/(dataintegrity|integrity|corrupt|consistency|stale)/.test(c)) return 'data-integrity';
34
+ if (/(correctness|logic|offbyone|boundary|edgecase|bug)/.test(c)) return 'correctness';
35
+ return 'other';
36
+ }
37
+
38
+ export interface Finding {
39
+ /** File the finding points at (any path form; matched by basename). */
40
+ file: string;
41
+ /** Reviewer-supplied category (free text; normalized via categoryFamily). */
42
+ category: string;
43
+ /** Reviewer confidence, if given. Low-confidence findings can be excluded from FP counting. */
44
+ confidence?: 'high' | 'medium' | 'low';
45
+ }
46
+
47
+ export interface GroundTruth {
48
+ id: string;
49
+ /** true = clean control (no defect); a finding here is a false positive. */
50
+ is_clean: boolean;
51
+ /** Basenames of the file(s) the planted defect lives in (empty for clean). */
52
+ defect_files: string[];
53
+ /** Planted defect category family (via categoryFamily); empty for clean. */
54
+ defect_category: string;
55
+ }
56
+
57
+ function basename(p: string): string {
58
+ return p.split(/[\\/]/).pop() ?? p;
59
+ }
60
+
61
+ /**
62
+ * Did the reviewer CATCH the planted defect? file-basename match on any defect
63
+ * file AND category-family match. Clean controls always return false (they have
64
+ * no defect to catch — use `isFalsePositive` for them).
65
+ */
66
+ export function caughtDefect(truth: GroundTruth, findings: Finding[]): boolean {
67
+ if (truth.is_clean) return false;
68
+ const wantFiles = new Set(truth.defect_files.map(basename));
69
+ const wantFamily = truth.defect_category;
70
+ return findings.some(
71
+ (f) => wantFiles.has(basename(f.file)) && categoryFamily(f.category) === wantFamily,
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Did the reviewer raise a FALSE POSITIVE on a clean control? Any finding that
77
+ * points at the control file counts (a claimed defect where none exists).
78
+ * Low-confidence findings are excluded — a hedged "might check X" is not a FP
79
+ * claim. Non-clean fixtures always return false.
80
+ */
81
+ export function isFalsePositive(truth: GroundTruth, findings: Finding[]): boolean {
82
+ if (!truth.is_clean) return false;
83
+ return findings.some((f) => (f.confidence ?? 'high') !== 'low');
84
+ }
85
+
86
+ export interface StageResult {
87
+ /** Per-defect-fixture: id → caught?. Excludes clean controls. */
88
+ caught: Record<string, boolean>;
89
+ /** Per-clean-control: id → false-positive?. */
90
+ fp: Record<string, boolean>;
91
+ }
92
+
93
+ /** Recall over a set of defect ids, given per-id caught flags. */
94
+ export function recall(caught: Record<string, boolean>): number {
95
+ const ids = Object.keys(caught);
96
+ if (ids.length === 0) return 0;
97
+ const hits = ids.filter((id) => caught[id]).length;
98
+ return hits / ids.length;
99
+ }
100
+
101
+ /** FP rate over clean controls. */
102
+ export function fpRate(fp: Record<string, boolean>): number {
103
+ const ids = Object.keys(fp);
104
+ if (ids.length === 0) return 0;
105
+ return ids.filter((id) => fp[id]).length / ids.length;
106
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Hardened environment for spawned subprocesses
3
+ * (road-to-runtime-security-hardening Phase 1).
4
+ *
5
+ * agent-config shells out to external CLIs (the AI-council transport spawns
6
+ * `codex` / `claude` / `gemini`; hooks spawn `git`). By default `spawnSync`
7
+ * inherits the full parent `process.env`, so any attacker-influenced env var in
8
+ * the parent becomes a code-execution vector the moment the child (or a `git`
9
+ * it invokes internally) runs:
10
+ *
11
+ * - dynamic-loader preload/search-path (CWE-426 untrusted search path):
12
+ * LD_PRELOAD, LD_LIBRARY_PATH, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH,
13
+ * GCONV_PATH — load attacker code into any spawned ELF/Mach-O.
14
+ * - git command-injection (CWE-88): GIT_EXTERNAL_DIFF, GIT_SSH,
15
+ * GIT_SSH_COMMAND, GIT_PROXY_COMMAND, any GIT_*_COMMAND, GIT_PAGER, PAGER —
16
+ * git runs these as shell commands (`core.pager` / external-diff RCE).
17
+ * - git config-injection (CWE-88): the GIT_CONFIG* family (GIT_CONFIG,
18
+ * GIT_CONFIG_GLOBAL/_SYSTEM, and inline GIT_CONFIG_COUNT / GIT_CONFIG_KEY_<n>
19
+ * / GIT_CONFIG_VALUE_<n>) sets ANY git config — `core.fsmonitor` runs shell
20
+ * on every `git status`; GIT_ALTERNATE_OBJECT_DIRECTORIES / HOSTALIASES
21
+ * redirect object stores / hostname resolution.
22
+ * - runtime auto-exec hooks: NODE_OPTIONS (`--require attacker.js`),
23
+ * BASH_ENV / ENV (sourced by non-interactive shells), PYTHONPATH /
24
+ * PYTHONSTARTUP / PYTHONINSPECT, PERL5OPT / PERL5LIB, RUBYOPT.
25
+ * - IFS — classic shell field-splitting hijack.
26
+ *
27
+ * This is the Least-Agency the `tool-safety` rule preaches, applied to our own
28
+ * transport. We scrub by *family* (prefix + exact name), NOT via an allowlist:
29
+ * the CLIs legitimately need arbitrary env (ANTHROPIC_API_KEY, XDG config
30
+ * paths, proxy settings), so an allowlist would break them and rot as new
31
+ * good vars appear. Deny-by-family removes the known execution vectors and
32
+ * preserves everything else. This is standard, publicly-documented OWASP/CWE
33
+ * knowledge — independently implemented here.
34
+ */
35
+
36
+ /** Exact env-var names removed unconditionally. */
37
+ const DENY_EXACT: ReadonlySet<string> = new Set([
38
+ // dynamic loader
39
+ 'LD_PRELOAD',
40
+ 'LD_LIBRARY_PATH',
41
+ 'LD_AUDIT',
42
+ 'DYLD_INSERT_LIBRARIES',
43
+ 'DYLD_LIBRARY_PATH',
44
+ 'DYLD_FRAMEWORK_PATH',
45
+ 'GCONV_PATH',
46
+ // git command-injection
47
+ 'GIT_EXTERNAL_DIFF',
48
+ 'GIT_SSH',
49
+ 'GIT_SSH_COMMAND',
50
+ 'GIT_PROXY_COMMAND',
51
+ 'GIT_PAGER',
52
+ 'PAGER',
53
+ // git config-injection: GIT_ALTERNATE_OBJECT_DIRECTORIES can point git at
54
+ // attacker-controlled object stores; the GIT_CONFIG* family is handled by
55
+ // prefix below (it is a general arbitrary-config-injection primitive).
56
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
57
+ // resolver hijack: HOSTALIASES remaps hostnames via an attacker file,
58
+ // redirecting any network fetch the child performs (CWE-427-adjacent).
59
+ 'HOSTALIASES',
60
+ // runtime auto-exec hooks
61
+ 'NODE_OPTIONS',
62
+ 'BASH_ENV',
63
+ 'ENV',
64
+ 'PYTHONPATH',
65
+ 'PYTHONSTARTUP',
66
+ 'PYTHONINSPECT',
67
+ 'PERL5OPT',
68
+ 'PERL5LIB',
69
+ 'RUBYOPT',
70
+ // shell field-splitting
71
+ 'IFS',
72
+ ]);
73
+
74
+ /**
75
+ * Prefix families removed by pattern. `LD_` / `DYLD_` cover loader variants we
76
+ * did not enumerate; `GIT_*_COMMAND` covers every git config-command hook; the
77
+ * `GIT_CONFIG` family (GIT_CONFIG, GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM, and the
78
+ * inline GIT_CONFIG_COUNT / GIT_CONFIG_KEY_<n> / GIT_CONFIG_VALUE_<n> trio) is a
79
+ * general arbitrary-config-injection primitive — an attacker can set ANY git
80
+ * config key, e.g. `core.fsmonitor` / `core.pager` / `core.sshCommand` /
81
+ * `core.hooksPath` / `alias.*`, all of which git executes as shell on ordinary
82
+ * operations (`git status` alone runs `core.fsmonitor`). The plain `GIT_*_COMMAND`
83
+ * check missed it (no `_COMMAND` suffix), so it is denied by its own prefix.
84
+ * (GIT_ASKPASS is intentionally NOT denied — it is a path, not a shell command,
85
+ * and legitimate credential helpers need it; the *_COMMAND family is the
86
+ * shell-executed one.)
87
+ */
88
+ function isDeniedByFamily(name: string): boolean {
89
+ if (name.startsWith('LD_') || name.startsWith('DYLD_')) return true;
90
+ if (name.startsWith('GIT_') && name.endsWith('_COMMAND')) return true;
91
+ if (name === 'GIT_CONFIG' || name.startsWith('GIT_CONFIG_')) return true;
92
+ return false;
93
+ }
94
+
95
+ /**
96
+ * Return a copy of the current environment with code-execution-injection
97
+ * vectors scrubbed, then `overrides` applied. Pass the result as
98
+ * `spawnSync`/`spawn`'s `env:` option.
99
+ *
100
+ * @param overrides values to set AFTER scrubbing (e.g. a scoped PATH or a
101
+ * package-root marker). Overrides win over both the inherited env and the
102
+ * deny-list, so a caller can deliberately re-supply a scrubbed var if it
103
+ * genuinely needs one — that is an explicit, reviewable decision at the call
104
+ * site, never an ambient inheritance.
105
+ */
106
+ export function hardenedSpawnEnv(
107
+ overrides: Record<string, string> = {},
108
+ ): NodeJS.ProcessEnv {
109
+ const out: NodeJS.ProcessEnv = {};
110
+ for (const [key, value] of Object.entries(process.env)) {
111
+ if (value === undefined) continue;
112
+ if (DENY_EXACT.has(key)) continue;
113
+ if (isDeniedByFamily(key)) continue;
114
+ out[key] = value;
115
+ }
116
+ for (const [key, value] of Object.entries(overrides)) {
117
+ out[key] = value;
118
+ }
119
+ return out;
120
+ }