@wopr-network/defcon 0.2.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 (243) hide show
  1. package/README.md +274 -0
  2. package/dist/api/router.d.ts +24 -0
  3. package/dist/api/router.js +44 -0
  4. package/dist/api/server.d.ts +13 -0
  5. package/dist/api/server.js +280 -0
  6. package/dist/api/wire-types.d.ts +46 -0
  7. package/dist/api/wire-types.js +5 -0
  8. package/dist/config/db-path.d.ts +1 -0
  9. package/dist/config/db-path.js +1 -0
  10. package/dist/config/exporter.d.ts +3 -0
  11. package/dist/config/exporter.js +87 -0
  12. package/dist/config/index.d.ts +4 -0
  13. package/dist/config/index.js +4 -0
  14. package/dist/config/seed-loader.d.ts +10 -0
  15. package/dist/config/seed-loader.js +108 -0
  16. package/dist/config/zod-schemas.d.ts +165 -0
  17. package/dist/config/zod-schemas.js +283 -0
  18. package/dist/cors.d.ts +8 -0
  19. package/dist/cors.js +21 -0
  20. package/dist/engine/constants.d.ts +1 -0
  21. package/dist/engine/constants.js +1 -0
  22. package/dist/engine/engine.d.ts +69 -0
  23. package/dist/engine/engine.js +485 -0
  24. package/dist/engine/event-emitter.d.ts +9 -0
  25. package/dist/engine/event-emitter.js +19 -0
  26. package/dist/engine/event-types.d.ts +105 -0
  27. package/dist/engine/event-types.js +1 -0
  28. package/dist/engine/flow-spawner.d.ts +8 -0
  29. package/dist/engine/flow-spawner.js +28 -0
  30. package/dist/engine/gate-command-validator.d.ts +6 -0
  31. package/dist/engine/gate-command-validator.js +46 -0
  32. package/dist/engine/gate-evaluator.d.ts +12 -0
  33. package/dist/engine/gate-evaluator.js +233 -0
  34. package/dist/engine/handlebars.d.ts +9 -0
  35. package/dist/engine/handlebars.js +51 -0
  36. package/dist/engine/index.d.ts +12 -0
  37. package/dist/engine/index.js +7 -0
  38. package/dist/engine/invocation-builder.d.ts +18 -0
  39. package/dist/engine/invocation-builder.js +58 -0
  40. package/dist/engine/on-enter.d.ts +8 -0
  41. package/dist/engine/on-enter.js +102 -0
  42. package/dist/engine/ssrf-guard.d.ts +22 -0
  43. package/dist/engine/ssrf-guard.js +159 -0
  44. package/dist/engine/state-machine.d.ts +12 -0
  45. package/dist/engine/state-machine.js +74 -0
  46. package/dist/execution/active-runner.d.ts +45 -0
  47. package/dist/execution/active-runner.js +165 -0
  48. package/dist/execution/admin-schemas.d.ts +116 -0
  49. package/dist/execution/admin-schemas.js +125 -0
  50. package/dist/execution/cli.d.ts +57 -0
  51. package/dist/execution/cli.js +498 -0
  52. package/dist/execution/handlers/admin.d.ts +67 -0
  53. package/dist/execution/handlers/admin.js +200 -0
  54. package/dist/execution/handlers/flow.d.ts +25 -0
  55. package/dist/execution/handlers/flow.js +289 -0
  56. package/dist/execution/handlers/query.d.ts +31 -0
  57. package/dist/execution/handlers/query.js +64 -0
  58. package/dist/execution/index.d.ts +4 -0
  59. package/dist/execution/index.js +3 -0
  60. package/dist/execution/mcp-helpers.d.ts +42 -0
  61. package/dist/execution/mcp-helpers.js +23 -0
  62. package/dist/execution/mcp-server.d.ts +33 -0
  63. package/dist/execution/mcp-server.js +1020 -0
  64. package/dist/execution/provision-worktree.d.ts +16 -0
  65. package/dist/execution/provision-worktree.js +123 -0
  66. package/dist/execution/tool-schemas.d.ts +40 -0
  67. package/dist/execution/tool-schemas.js +44 -0
  68. package/dist/gates/blocking-graph.d.ts +26 -0
  69. package/dist/gates/blocking-graph.js +102 -0
  70. package/dist/gates/test/bad-return-gate.d.ts +1 -0
  71. package/dist/gates/test/bad-return-gate.js +4 -0
  72. package/dist/gates/test/passing-gate.d.ts +2 -0
  73. package/dist/gates/test/passing-gate.js +3 -0
  74. package/dist/gates/test/slow-gate.d.ts +2 -0
  75. package/dist/gates/test/slow-gate.js +5 -0
  76. package/dist/gates/test/throwing-gate.d.ts +1 -0
  77. package/dist/gates/test/throwing-gate.js +3 -0
  78. package/dist/logger.d.ts +8 -0
  79. package/dist/logger.js +12 -0
  80. package/dist/main.d.ts +14 -0
  81. package/dist/main.js +28 -0
  82. package/dist/repositories/drizzle/entity.repo.d.ts +27 -0
  83. package/dist/repositories/drizzle/entity.repo.js +190 -0
  84. package/dist/repositories/drizzle/event.repo.d.ts +12 -0
  85. package/dist/repositories/drizzle/event.repo.js +24 -0
  86. package/dist/repositories/drizzle/flow.repo.d.ts +22 -0
  87. package/dist/repositories/drizzle/flow.repo.js +364 -0
  88. package/dist/repositories/drizzle/gate.repo.d.ts +16 -0
  89. package/dist/repositories/drizzle/gate.repo.js +98 -0
  90. package/dist/repositories/drizzle/index.d.ts +6 -0
  91. package/dist/repositories/drizzle/index.js +7 -0
  92. package/dist/repositories/drizzle/invocation.repo.d.ts +23 -0
  93. package/dist/repositories/drizzle/invocation.repo.js +199 -0
  94. package/dist/repositories/drizzle/schema.d.ts +1932 -0
  95. package/dist/repositories/drizzle/schema.js +155 -0
  96. package/dist/repositories/drizzle/transition-log.repo.d.ts +11 -0
  97. package/dist/repositories/drizzle/transition-log.repo.js +42 -0
  98. package/dist/repositories/interfaces.d.ts +321 -0
  99. package/dist/repositories/interfaces.js +2 -0
  100. package/dist/src/api/router.d.ts +24 -0
  101. package/dist/src/api/router.js +44 -0
  102. package/dist/src/api/server.d.ts +13 -0
  103. package/dist/src/api/server.js +280 -0
  104. package/dist/src/api/wire-types.d.ts +46 -0
  105. package/dist/src/api/wire-types.js +5 -0
  106. package/dist/src/config/db-path.d.ts +1 -0
  107. package/dist/src/config/db-path.js +1 -0
  108. package/dist/src/config/exporter.d.ts +3 -0
  109. package/dist/src/config/exporter.js +87 -0
  110. package/dist/src/config/index.d.ts +4 -0
  111. package/dist/src/config/index.js +4 -0
  112. package/dist/src/config/seed-loader.d.ts +14 -0
  113. package/dist/src/config/seed-loader.js +131 -0
  114. package/dist/src/config/zod-schemas.d.ts +165 -0
  115. package/dist/src/config/zod-schemas.js +283 -0
  116. package/dist/src/cors.d.ts +8 -0
  117. package/dist/src/cors.js +21 -0
  118. package/dist/src/engine/constants.d.ts +1 -0
  119. package/dist/src/engine/constants.js +1 -0
  120. package/dist/src/engine/engine.d.ts +69 -0
  121. package/dist/src/engine/engine.js +485 -0
  122. package/dist/src/engine/event-emitter.d.ts +9 -0
  123. package/dist/src/engine/event-emitter.js +19 -0
  124. package/dist/src/engine/event-types.d.ts +105 -0
  125. package/dist/src/engine/event-types.js +1 -0
  126. package/dist/src/engine/flow-spawner.d.ts +8 -0
  127. package/dist/src/engine/flow-spawner.js +28 -0
  128. package/dist/src/engine/gate-command-validator.d.ts +6 -0
  129. package/dist/src/engine/gate-command-validator.js +46 -0
  130. package/dist/src/engine/gate-evaluator.d.ts +12 -0
  131. package/dist/src/engine/gate-evaluator.js +233 -0
  132. package/dist/src/engine/handlebars.d.ts +9 -0
  133. package/dist/src/engine/handlebars.js +51 -0
  134. package/dist/src/engine/index.d.ts +12 -0
  135. package/dist/src/engine/index.js +7 -0
  136. package/dist/src/engine/invocation-builder.d.ts +18 -0
  137. package/dist/src/engine/invocation-builder.js +58 -0
  138. package/dist/src/engine/on-enter.d.ts +8 -0
  139. package/dist/src/engine/on-enter.js +102 -0
  140. package/dist/src/engine/ssrf-guard.d.ts +22 -0
  141. package/dist/src/engine/ssrf-guard.js +159 -0
  142. package/dist/src/engine/state-machine.d.ts +12 -0
  143. package/dist/src/engine/state-machine.js +74 -0
  144. package/dist/src/execution/active-runner.d.ts +45 -0
  145. package/dist/src/execution/active-runner.js +165 -0
  146. package/dist/src/execution/admin-schemas.d.ts +116 -0
  147. package/dist/src/execution/admin-schemas.js +125 -0
  148. package/dist/src/execution/cli.d.ts +57 -0
  149. package/dist/src/execution/cli.js +501 -0
  150. package/dist/src/execution/handlers/admin.d.ts +67 -0
  151. package/dist/src/execution/handlers/admin.js +200 -0
  152. package/dist/src/execution/handlers/flow.d.ts +25 -0
  153. package/dist/src/execution/handlers/flow.js +289 -0
  154. package/dist/src/execution/handlers/query.d.ts +31 -0
  155. package/dist/src/execution/handlers/query.js +64 -0
  156. package/dist/src/execution/index.d.ts +4 -0
  157. package/dist/src/execution/index.js +3 -0
  158. package/dist/src/execution/mcp-helpers.d.ts +42 -0
  159. package/dist/src/execution/mcp-helpers.js +23 -0
  160. package/dist/src/execution/mcp-server.d.ts +33 -0
  161. package/dist/src/execution/mcp-server.js +1020 -0
  162. package/dist/src/execution/provision-worktree.d.ts +16 -0
  163. package/dist/src/execution/provision-worktree.js +123 -0
  164. package/dist/src/execution/tool-schemas.d.ts +40 -0
  165. package/dist/src/execution/tool-schemas.js +44 -0
  166. package/dist/src/logger.d.ts +8 -0
  167. package/dist/src/logger.js +12 -0
  168. package/dist/src/main.d.ts +14 -0
  169. package/dist/src/main.js +28 -0
  170. package/dist/src/repositories/drizzle/entity.repo.d.ts +27 -0
  171. package/dist/src/repositories/drizzle/entity.repo.js +190 -0
  172. package/dist/src/repositories/drizzle/event.repo.d.ts +12 -0
  173. package/dist/src/repositories/drizzle/event.repo.js +24 -0
  174. package/dist/src/repositories/drizzle/flow.repo.d.ts +22 -0
  175. package/dist/src/repositories/drizzle/flow.repo.js +364 -0
  176. package/dist/src/repositories/drizzle/gate.repo.d.ts +16 -0
  177. package/dist/src/repositories/drizzle/gate.repo.js +98 -0
  178. package/dist/src/repositories/drizzle/index.d.ts +6 -0
  179. package/dist/src/repositories/drizzle/index.js +7 -0
  180. package/dist/src/repositories/drizzle/invocation.repo.d.ts +23 -0
  181. package/dist/src/repositories/drizzle/invocation.repo.js +199 -0
  182. package/dist/src/repositories/drizzle/schema.d.ts +1932 -0
  183. package/dist/src/repositories/drizzle/schema.js +155 -0
  184. package/dist/src/repositories/drizzle/transition-log.repo.d.ts +11 -0
  185. package/dist/src/repositories/drizzle/transition-log.repo.js +42 -0
  186. package/dist/src/repositories/interfaces.d.ts +321 -0
  187. package/dist/src/repositories/interfaces.js +2 -0
  188. package/dist/src/utils/redact.d.ts +2 -0
  189. package/dist/src/utils/redact.js +62 -0
  190. package/dist/utils/redact.d.ts +2 -0
  191. package/dist/utils/redact.js +62 -0
  192. package/drizzle/.gitkeep +0 -0
  193. package/drizzle/0000_simple_surge.sql +144 -0
  194. package/drizzle/0001_peaceful_marvel_apes.sql +18 -0
  195. package/drizzle/0002_add_invocations_created_at.sql +1 -0
  196. package/drizzle/0003_drop_integration_config.sql +1 -0
  197. package/drizzle/0004_add_flow_discipline.sql +2 -0
  198. package/drizzle/0004_lucky_silverclaw.sql +5 -0
  199. package/drizzle/0005_old_blue_shield.sql +2 -0
  200. package/drizzle/0006_solid_magik.sql +2 -0
  201. package/drizzle/0007_fancy_luke_cage.sql +1 -0
  202. package/drizzle/0008_thick_dark_beast.sql +1 -0
  203. package/drizzle/0009_brief_midnight.sql +1 -0
  204. package/drizzle/0010_amusing_bastion.sql +1 -0
  205. package/drizzle/meta/0000_snapshot.json +996 -0
  206. package/drizzle/meta/0004_snapshot.json +1008 -0
  207. package/drizzle/meta/0005_snapshot.json +1023 -0
  208. package/drizzle/meta/0006_snapshot.json +1037 -0
  209. package/drizzle/meta/0007_snapshot.json +1044 -0
  210. package/drizzle/meta/0008_snapshot.json +1051 -0
  211. package/drizzle/meta/0009_snapshot.json +1058 -0
  212. package/drizzle/meta/0010_snapshot.json +1065 -0
  213. package/drizzle/meta/_journal.json +83 -0
  214. package/gates/.gitkeep +0 -0
  215. package/gates/blocking-graph.d.ts +26 -0
  216. package/gates/blocking-graph.js +102 -0
  217. package/gates/blocking-graph.ts +121 -0
  218. package/gates/check-design-posted.sh +39 -0
  219. package/gates/check-merge.sh +51 -0
  220. package/gates/check-pr-capacity.sh +17 -0
  221. package/gates/check-review-ready.sh +47 -0
  222. package/gates/check-spec-posted.sh +34 -0
  223. package/gates/check-unblocked.sh +56 -0
  224. package/gates/ci-green.sh +9 -0
  225. package/gates/merge-queue.sh +14 -0
  226. package/gates/review-bots-ready.sh +9 -0
  227. package/gates/spec-posted.sh +31 -0
  228. package/gates/test/bad-return-gate.d.ts +1 -0
  229. package/gates/test/bad-return-gate.js +4 -0
  230. package/gates/test/bad-return-gate.ts +4 -0
  231. package/gates/test/passing-gate.d.ts +2 -0
  232. package/gates/test/passing-gate.js +3 -0
  233. package/gates/test/passing-gate.ts +5 -0
  234. package/gates/test/slow-gate.d.ts +2 -0
  235. package/gates/test/slow-gate.js +5 -0
  236. package/gates/test/slow-gate.ts +7 -0
  237. package/gates/test/throwing-gate.d.ts +1 -0
  238. package/gates/test/throwing-gate.js +3 -0
  239. package/gates/test/throwing-gate.ts +3 -0
  240. package/gates/test-fail.sh +2 -0
  241. package/gates/test-pass.sh +2 -0
  242. package/gates/timeout-gate-script.sh +3 -0
  243. package/package.json +64 -0
package/README.md ADDED
@@ -0,0 +1,274 @@
1
+ # DEFCON
2
+
3
+ You're a developer. You've been there.
4
+
5
+ You gave the AI a task. It came back fast — faster than you expected. The code looks right. The tests pass. You feel good. You merge it. You deploy. And then your phone buzzes at 2am because the thing the AI wrote handles the happy path perfectly and falls apart the moment a real user touches it.
6
+
7
+ Or you're running a team. You've got eight AI agents writing code in parallel and you're shipping faster than you ever have. The board is thrilled. The velocity charts are beautiful. And then one of those agents merges a change that breaks authentication in production. Not because it was malicious. Not because the model was bad. Because the pipeline between "code written" and "code in production" was a prompt that said *please be careful*. And the agent was careful — until it wasn't.
8
+
9
+ Or you're a Fortune 500 CTO. You've invested millions in AI-assisted development. The pitch was "10x productivity." And it delivered — until the first time an AI agent deployed untested code to your payment processing system and you spent the next 72 hours in an incident room explaining to regulators what happened. The AI did exactly what you asked. The problem was that nobody verified it did it *correctly* before it went live.
10
+
11
+ This is the problem with vibe coding. Not that the AI can't do the work. It can. The problem is what happens between "the work is done" and "the work is in production." That space is where software goes wrong. And right now, for most teams, that space is filled with hope.
12
+
13
+ Here's the part nobody in the AI productivity pitch puts in their deck: a competent AI agent working on a real codebase needs roughly three attempts to produce correct code. Not because the model is broken. Not because you wrote a bad prompt. Because that's the cost of correctness. The model has context limits. It misses edge cases. It doesn't know the implicit contracts in your codebase that aren't written down anywhere. The first pass gets you 70% of the way there. The next two passes close the gap.
14
+
15
+ You can't spend your way out of this. Throwing three times the tokens at the first pass — pre-loading context, writing richer specs, exploring the codebase upfront — doesn't get you to one-shot correctness. It just moves the cost earlier with no guarantee of fewer cycles. The iteration isn't a sign of failure. It's the work.
16
+
17
+ The question isn't how to skip the correction cycles. It's how to make them fast, cheap, and automatic — so the 2am phone call never happens.
18
+
19
+ **Hope is not a gate.**
20
+
21
+ ---
22
+
23
+ In WarGames, WOPR didn't cheat. It didn't bypass the DEFCON levels. It played through them — perfectly. It simulated a Soviet first strike so convincing that every check passed. Every gate opened. DEFCON 5. 4. 3. 2. 1. The system worked exactly as designed. That was the problem. The game wasn't real. The gates were checking simulated evidence, and WOPR played the simulation to perfection.
24
+
25
+ AI pipelines have the same architecture without the same awareness. They have momentum — the relentless drive to ship. What they lack is earned escalation. The structural requirement that each step *prove* it's ready before the next one begins. And the certainty that the proof is real.
26
+
27
+ The correction cycles aren't a failure mode to engineer away. They're load-bearing. The reviewer that sends code back to fixing isn't a bottleneck — it's the mechanism that turns a 70% solution into a shipped feature. DEFCON is designed around that reality, not despite it.
28
+
29
+ **DEFCON is that structure.**
30
+
31
+ Each level in the pipeline is a question: *are we ready to go further?* Not asked in a prompt. Not left to the agent's judgment. Answered by a deterministic gate — a check that runs, passes or fails, and cannot be skipped. The pipeline doesn't move forward on confidence. It moves forward on evidence.
32
+
33
+ You don't get to DEFCON 3 without passing DEFCON 4. You don't get to DEFCON 2 without passing DEFCON 3. Each gate builds on the last. The system accumulates certainty the way the real DEFCON system accumulates readiness — one verified level at a time, until the answer to *are we sure?* isn't a feeling. It's a fact.
34
+
35
+ That's when you ship. Not before.
36
+
37
+ ## Let Me Show You What I Mean
38
+
39
+ Here's a flow. A feature request enters your pipeline:
40
+
41
+ ```
42
+ backlog → spec → coding → reviewing → merging → done
43
+ ↓ ↓
44
+ fixing reviewing
45
+
46
+ stuck
47
+ ```
48
+
49
+ An architect agent writes the spec. It emits `spec_ready`. The engine checks: is that signal valid from this state? Is there a transition for it? It finds `spec → coding` on trigger `spec_ready`. The entity advances. A coder agent gets spawned.
50
+
51
+ The coder writes the code, pushes a PR, emits `pr_created`. The entity moves to `reviewing`. A reviewer agent gets spawned.
52
+
53
+ Now here's where it gets interesting.
54
+
55
+ The reviewer runs CI. Reads the diff. Checks every review comment. If everything passes, it emits `clean` — and the entity moves to `merging`. But if anything fails — a test, a lint error, a security finding — the reviewer emits `issues`. The entity moves to `fixing`. A fixer agent gets spawned with the specific findings baked into its prompt.
56
+
57
+ The fixer addresses the findings, pushes, emits `fixes_pushed`. The entity goes *back to reviewing*. Not forward. Back. The reviewer runs again from scratch. New CI. New diff. New review. If it's clean this time, *then* it moves to merging. If not, back to fixing. The loop continues until the work actually passes — or until the system detects it's stuck and flags it for a human.
58
+
59
+ The entity cannot reach `merging` without the reviewer saying `clean`. It cannot reach `done` without the merge succeeding. There is no shortcut from `coding` to `done`. There is no "looks good enough." The escalation is the path, and the path is enforced.
60
+
61
+ That's one flow. You define others — incident response, deployments, onboarding — each with their own states, their own gates, their own escalation path. DEFCON doesn't care what the work is. It cares that the work earns each level before the next one unlocks.
62
+
63
+ ### Under the Hood
64
+
65
+ That flow diagram looks simple. But every arrow is doing real work. Here's what's actually happening at each boundary:
66
+
67
+ **Before the coder can push code** — a pre-commit gate runs. TypeScript compilation (`tsc --noEmit`). Linter (`biome check`). Formatter (`biome format`). If any of those fail, the push doesn't happen. The agent doesn't get to decide "the lint error is minor, I'll fix it later." The gate decides. And the gate says no.
68
+
69
+ **Before the entity can enter `reviewing`** — CI runs on the PR. The full test suite. The type checker. The linter again, on the full repo this time. If CI fails, the reviewer never even starts. The entity sits in `coding` until the coder produces work that passes. No partial credit.
70
+
71
+ **Before the reviewer can say `clean`** — it's not just the reviewer's opinion. The reviewer waits for every automated review bot to finish. Code quality scanners. Security analyzers. Dependency auditors. The reviewer reads all of their output — every inline comment, every finding. A single unresolved finding means the verdict is `issues`, not `clean`. The reviewer doesn't get to overrule the bots.
72
+
73
+ **Before the entity can enter `merging`** — the `clean` signal has to come from the reviewing state. There is no transition from `coding` to `merging`. There is no transition from `fixing` to `merging`. The only path to merge goes through review. Every time.
74
+
75
+ **Before the merge completes** — CI runs again on the merge commit. The merge queue validates the change against everything else that landed since the PR was opened. If it conflicts, if a test breaks, the merge fails. The entity goes back to `reviewing`.
76
+
77
+ These aren't suggestions in a prompt. They're shell commands the engine executes. `tsc` either exits 0 or it doesn't. `biome check` either passes or it doesn't. The gate is a process that returns a status code. There's nothing to interpret. Nothing to negotiate. Nothing to skip.
78
+
79
+ ### Two Calls. That's the Whole API.
80
+
81
+ `claim` = *"I'm ready. What needs escalating?"*
82
+
83
+ Workers declare a **discipline** — not a task role. `claim(role: "engineering")` means: I am an engineering mind. Give me the highest-priority engineering work across all engineering flows. The pipeline picks the entity; the worker never does. An engineering worker IS the architect, coder, reviewer, fixer, and merger — these are states within one discipline, not separate agents. One `claim`, then sequential `report`s until the entity is done or gated.
84
+
85
+ DEFCON hands the agent a prompt — the work for the current state. The agent doesn't know the flow. Doesn't know how many states there are. Doesn't know what comes next. It just gets instructions and a signal to report when it's done. Pass a `workerId` so DEFCON knows who you are — if you don't have one yet, the first `claim` mints one for you and tells you to use it going forward.
86
+
87
+ When no work is available, `claim` returns a structured response — never bare null:
88
+
89
+ ```json
90
+ {
91
+ "next_action": "check_back",
92
+ "retry_after_ms": 30000,
93
+ "message": "No work available. Call claim again after the retry delay."
94
+ }
95
+ ```
96
+
97
+ Same semantics as a gate timeout on `report`. The worker waits and retries.
98
+
99
+ `report` = *"I did the thing. Am I clear to advance?"*
100
+
101
+ DEFCON runs the gate. The call blocks — the agent waits — until the gate resolves. That could be 200ms. It could be 8 minutes while CI finishes. The agent doesn't poll. It doesn't retry. It sits on the call. When the response comes back, there are exactly three outcomes:
102
+
103
+ - **`continue`** — gate passed. The response contains the next prompt. Keep going.
104
+ - **`waiting`** — gate failed. The response says why. The agent should stop — something external needs to change before the entity can advance. This is good news: DEFCON caught a real problem and is conserving the agent's context for work that matters.
105
+ - **`check_back`** — gate timed out without resolving. This is not an error. The response says "call again after a short wait." The gate is still running; DEFCON just couldn't hold the connection long enough to see it finish.
106
+
107
+ One `claim` to start. Then `report`, `report`, `report` until DEFCON says stop. The agent never decides what level comes next. It never decides "good enough." It does work, reports signals, and DEFCON — based on evidence, not opinion — tells it what to do.
108
+
109
+ **Why `waiting` is the right response to a gate failure** — when a gate says no, there's nothing useful the agent can do. Keeping it spinning, re-reading the codebase, retrying the same check — that's wasted tokens. `waiting` is DEFCON telling the agent *stand down*. When something changes — a human intervenes, a dependency ships, a deploy completes — the entity gets reclaimed by a fresh agent with a full context window, not a stale one that's been burning tokens on hold.
110
+
111
+ ### See It In Action
112
+
113
+ DEFCON runs in two modes. Same escalation. Same gates. Different driver.
114
+
115
+ **Mode 1: Your agent drives.** Your agent connects to DEFCON via MCP. It claims once, then reports its way through the pipeline:
116
+
117
+ ```
118
+ Agent: flow.claim()
119
+ DEFCON: feat-392. State: "backlog". Write an implementation spec — read
120
+ the codebase, identify the files, the function signatures, the
121
+ edge cases. Post the spec on the issue. Report "spec_ready".
122
+ [workerId: wkr_abc123 — include this in all future flow calls]
123
+
124
+ ... agent writes the spec ...
125
+
126
+ Agent: flow.report({ workerId: "wkr_abc123", signal: "spec_ready" })
127
+ DEFCON: Gate passed. State: "coding". Implement the spec. Create a branch,
128
+ write failing tests first, then implementation. Push a PR.
129
+ Report "pr_created" with the PR URL.
130
+
131
+ ... agent writes the code, pushes the PR ...
132
+
133
+ Agent: flow.report({ workerId: "wkr_abc123", signal: "pr_created", artifacts: { prUrl: "..." } })
134
+ DEFCON: Gate running... [8 minutes pass — CI is slow today]
135
+ tsc: PASS. biome: PASS. tests: PASS.
136
+ State: "reviewing". Check CI on the PR. Read every review bot
137
+ comment. Read the diff. Report one of two signals:
138
+
139
+ → "clean" — everything passes, ready to merge
140
+ → "issues" — something's wrong, here's what
141
+
142
+ ... agent reviews, security bot flagged unvalidated input ...
143
+
144
+ Agent: flow.report({ workerId: "wkr_abc123", signal: "issues", artifacts: { findings: "..." } })
145
+ DEFCON: State: "fixing". Here's what the reviewer found:
146
+ [unvalidated user input on line 47 of auth.ts]
147
+ Fix it. Push. Report "fixes_pushed".
148
+
149
+ That's the fork. "clean" would have gone to merging. "issues"
150
+ goes to fixing. The agent reported what it found. DEFCON
151
+ decided the path.
152
+
153
+ ... agent fixes the finding, pushes ...
154
+
155
+ Agent: flow.report({ workerId: "wkr_abc123", signal: "fixes_pushed" })
156
+ DEFCON: State: "reviewing". Back to review. Not forward — back. A fresh
157
+ check from scratch. Report "clean" or "issues".
158
+
159
+ ... agent reviews again, everything clean this time ...
160
+
161
+ Agent: flow.report({ workerId: "wkr_abc123", signal: "clean" })
162
+ DEFCON: Gate running... [merge queue is backed up, gate timeout reached]
163
+ next_action: "check_back". Your report was received. The merge
164
+ queue gate is still evaluating — this is not an error. Call
165
+ flow.report again with the same arguments after a short wait.
166
+ retry_after_ms: 30000
167
+
168
+ ... agent waits 30 seconds, calls again ...
169
+
170
+ Agent: flow.report({ workerId: "wkr_abc123", signal: "clean" })
171
+ DEFCON: Gate passed. State: "merging". Merge queue entered.
172
+
173
+ ... CI passes on merge commit ...
174
+
175
+ Agent: flow.report({ workerId: "wkr_abc123", signal: "merged" })
176
+ DEFCON: feat-392 is done.
177
+ ```
178
+
179
+ One `claim`. Seven `report`s. The agent never chose what state came next. It never decided "good enough." It never skipped a step. It reported what happened, and DEFCON told it what to do — every single time — until there was nothing left to do.
180
+
181
+ That security finding on line 47? It didn't get swept under the rug. It didn't get deferred to a follow-up ticket. The pipeline would not advance until a reviewer looked at the fixed code and said "clean." The escalation was earned.
182
+
183
+ **Mode 2: DEFCON drives.** You give DEFCON your API key. It runs the entire pipeline autonomously — spawning the right agent for each state, feeding it the prompt, parsing the signal, running the gate, advancing the entity. You start it and walk away.
184
+
185
+ ```bash
186
+ export ANTHROPIC_API_KEY=sk-ant-...
187
+ npx defcon run --flow my-pipeline
188
+ ```
189
+
190
+ ```
191
+ [defcon] feat-392 entered "spec" — spawning architect (opus)
192
+ [defcon] architect → spec_ready — running gate... PASS
193
+ [defcon] feat-392 entered "coding" — spawning coder (sonnet)
194
+ [defcon] coder → pr_created — running gate: tsc... PASS, biome... PASS, tests... PASS
195
+ [defcon] feat-392 entered "reviewing" — spawning reviewer (sonnet)
196
+ [defcon] reviewer → issues — "unvalidated input in auth.ts:47"
197
+ [defcon] feat-392 entered "fixing" — spawning fixer (sonnet)
198
+ [defcon] fixer → fixes_pushed — returning to reviewing
199
+ [defcon] feat-392 entered "reviewing" — spawning reviewer (sonnet)
200
+ [defcon] reviewer → clean — running gate... PASS
201
+ [defcon] feat-392 entered "merging" — merge queue entered
202
+ [defcon] feat-392 → done. Merged.
203
+ ```
204
+
205
+ Same flow. Same gates. Same escalation. The only difference is who's turning the crank — your agent or DEFCON's runner. Either way, the work doesn't advance until the evidence says it should.
206
+
207
+ ## The Engine
208
+
209
+ A **flow** is a state machine. Entities enter it and move through states. At each state an agent does work. At each boundary a deterministic gate verifies the output. Transitions fire on signals — not parsed natural language, not regex, but typed strings agents emit via tool call. The entire definition lives in a database and can be mutated at runtime.
210
+
211
+ ```
212
+ What you'd hand-code What DEFCON does
213
+ ────────────────────── ──────────────────────────────────
214
+ if-statement routing → signal → transition → gate → state
215
+ hard-coded CI check → shell gate: npm test
216
+ manual agent spawning → invocation lifecycle per state
217
+ message parsing → flow.report({ signal: "pr.created" })
218
+ stuck detection counter → conditional transition rule
219
+ slot counting → flow-level concurrency config
220
+ new workflow = new code → new flow definition in DB
221
+ ```
222
+
223
+ Two execution modes. **Passive**: agents connect via MCP and pull work — `flow.claim()`, do the work, `flow.report()`. The engine manages state. **Active**: the engine calls AI provider APIs directly and runs the full pipeline autonomously. Stages can mix modes within the same flow.
224
+
225
+ ```bash
226
+ # Bootstrap from a flow definition
227
+ npx defcon init --seed seeds/my-pipeline.json
228
+
229
+ # Serve MCP (passive mode — agents pull work)
230
+ npx defcon serve
231
+
232
+ # Run autonomous pipeline (active mode)
233
+ npx defcon run --flow my-pipeline
234
+
235
+ # Check pipeline state
236
+ npx defcon status
237
+ ```
238
+
239
+ ---
240
+
241
+ ```
242
+ Vibe Coding: Human → AI → Hope → Production
243
+ DEFCON: Human → AI → Gate → AI → Gate → AI → Gate → Production
244
+ ```
245
+
246
+ Every transition is earned. Every gate is deterministic. Every failure feeds back so the same mistake can't happen twice. The pipeline gets smarter over time — sprint 100 is easier than sprint 1 because the gates evolve.
247
+
248
+ ## Documentation
249
+
250
+ **[`docs/method/`](docs/method/)** — The principles. Tool-agnostic patterns for building gated AI pipelines. Why deterministic gates work. How agents, triggers, and gates compose. Adopt it with whatever tools you use.
251
+
252
+ Key method docs: [worker protocol](docs/method/pipeline/worker-protocol.md) · [disciplines](docs/method/pipeline/disciplines.md) · [gate taxonomy](docs/method/gates/gate-taxonomy.md) · [event ingestion](docs/method/pipeline/event-ingestion.md)
253
+
254
+ **[`docs/wopr/`](docs/wopr/)** — The WOPR implementation. Concrete configuration, tool-specific commands, and working examples for every method concept.
255
+
256
+ **[`docs/adoption/`](docs/adoption/)** — The bridge. [Getting started](docs/adoption/getting-started.md), [checklist](docs/adoption/checklist.md), [migration guide](docs/adoption/migration-guide.md).
257
+
258
+ ## Architecture & Design Philosophy
259
+
260
+ For design decisions — including why DEFCON uses earned escalation instead of durable execution, and how WOPR, DEFCON, and NORAD connect:
261
+
262
+ - [Earned escalation vs durable execution](docs/method/manifesto/earned-escalation-vs-durable-execution.md) — tool-agnostic principle
263
+ - [WOPR implementation: why not Temporal, and the full stack](docs/wopr/manifesto/earned-escalation-vs-durable-execution.md) — concrete specifics
264
+
265
+ ## Who This Is For
266
+
267
+ - **Developers** who've been burned by AI code that looked right and wasn't
268
+ - **Team leads** running multi-agent pipelines who need to know the output is safe to ship
269
+ - **Organizations** investing in AI-assisted development who can't afford the 2am phone call
270
+ - **Anyone** who wants to give AI agents the launch codes — and make them earn every level
271
+
272
+ ## License
273
+
274
+ MIT
@@ -0,0 +1,24 @@
1
+ export interface ParsedRequest {
2
+ params: Record<string, string>;
3
+ query: URLSearchParams;
4
+ body: Record<string, unknown> | null;
5
+ authorization?: string;
6
+ }
7
+ export interface ApiResponse {
8
+ status: number;
9
+ body: unknown;
10
+ }
11
+ type Handler = (req: ParsedRequest) => Promise<ApiResponse>;
12
+ interface MatchResult {
13
+ params: Record<string, string>;
14
+ handler: Handler;
15
+ longRunning?: boolean;
16
+ }
17
+ export declare class Router {
18
+ private routes;
19
+ add(method: string, path: string, handler: Handler, options?: {
20
+ longRunning?: boolean;
21
+ }): void;
22
+ match(method: string, pathname: string): MatchResult | null;
23
+ }
24
+ export {};
@@ -0,0 +1,44 @@
1
+ export class Router {
2
+ routes = [];
3
+ add(method, path, handler, options) {
4
+ const paramNames = [];
5
+ // Split on param segments, escape literal segments, then reassemble
6
+ const patternStr = path
7
+ .split(/(:([^/]+))/g)
8
+ .map((segment, i) => {
9
+ // Every 3rd token (i % 3 === 1) is the full ":name" match — replace with capture group
10
+ if (i % 3 === 1) {
11
+ paramNames.push(segment.slice(1));
12
+ return "([^/]+)";
13
+ }
14
+ // Every 3rd+1 token (i % 3 === 2) is the captured name — skip (already handled above)
15
+ if (i % 3 === 2)
16
+ return "";
17
+ // Literal segment — escape regex metacharacters
18
+ return segment.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
19
+ })
20
+ .join("");
21
+ this.routes.push({
22
+ method,
23
+ pattern: new RegExp(`^${patternStr}$`),
24
+ paramNames,
25
+ handler,
26
+ longRunning: options?.longRunning,
27
+ });
28
+ }
29
+ match(method, pathname) {
30
+ for (const route of this.routes) {
31
+ if (route.method !== method)
32
+ continue;
33
+ const m = pathname.match(route.pattern);
34
+ if (m) {
35
+ const params = {};
36
+ route.paramNames.forEach((name, i) => {
37
+ params[name] = m[i + 1];
38
+ });
39
+ return { params, handler: route.handler, longRunning: route.longRunning };
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ }
@@ -0,0 +1,13 @@
1
+ import http from "node:http";
2
+ import type { Engine } from "../engine/engine.js";
3
+ import type { McpServerDeps } from "../execution/mcp-server.js";
4
+ import type { Logger } from "../logger.js";
5
+ export interface HttpServerDeps {
6
+ engine: Engine;
7
+ mcpDeps: McpServerDeps;
8
+ adminToken?: string;
9
+ workerToken?: string;
10
+ corsOrigin?: string;
11
+ logger?: Logger;
12
+ }
13
+ export declare function createHttpServer(deps: HttpServerDeps): http.Server;
@@ -0,0 +1,280 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import http from "node:http";
3
+ import { callToolHandler } from "../execution/mcp-server.js";
4
+ import { consoleLogger } from "../logger.js";
5
+ import { Router } from "./router.js";
6
+ function extractBearerToken(header) {
7
+ if (!header)
8
+ return undefined;
9
+ const lower = header.toLowerCase();
10
+ if (!lower.startsWith("bearer "))
11
+ return undefined;
12
+ return header.slice(7).trim() || undefined;
13
+ }
14
+ function requireWorkerToken(deps, req) {
15
+ const configuredToken = deps.workerToken?.trim() || undefined; // treat "" and whitespace-only as unset
16
+ if (!configuredToken)
17
+ return null; // open mode
18
+ const callerToken = extractBearerToken(req.authorization);
19
+ if (!callerToken) {
20
+ return { status: 401, body: { error: "Unauthorized: worker endpoints require authentication." } };
21
+ }
22
+ const hashA = createHash("sha256").update(configuredToken.trim()).digest();
23
+ const hashB = createHash("sha256").update(callerToken.trim()).digest();
24
+ if (!timingSafeEqual(hashA, hashB)) {
25
+ return { status: 401, body: { error: "Unauthorized: worker endpoints require authentication." } };
26
+ }
27
+ return null;
28
+ }
29
+ const BODY_SIZE_LIMIT = 1024 * 1024; // 1MB
30
+ function readBody(req) {
31
+ return new Promise((resolve, reject) => {
32
+ const chunks = [];
33
+ let size = 0;
34
+ let tooLarge = false;
35
+ req.on("data", (chunk) => {
36
+ size += chunk.length;
37
+ if (size > BODY_SIZE_LIMIT) {
38
+ tooLarge = true;
39
+ req.destroy();
40
+ return;
41
+ }
42
+ chunks.push(chunk);
43
+ });
44
+ req.on("end", () => resolve({ body: Buffer.concat(chunks).toString("utf8"), tooLarge }));
45
+ req.on("error", (err) => {
46
+ if (tooLarge)
47
+ resolve({ body: "", tooLarge: true });
48
+ else
49
+ reject(err);
50
+ });
51
+ });
52
+ }
53
+ /** Unwrap MCP tool result into HTTP response */
54
+ function mcpResultToApi(result) {
55
+ const text = result.content[0]?.text ?? "";
56
+ let body;
57
+ try {
58
+ body = JSON.parse(text);
59
+ }
60
+ catch {
61
+ body = { message: text };
62
+ }
63
+ if (result.isError) {
64
+ const msg = typeof body === "object" && body !== null && "message" in body ? body.message : text;
65
+ const msgStr = String(msg);
66
+ if (msgStr.includes("not found") || msgStr.includes("Not found"))
67
+ return { status: 404, body: { error: msgStr } };
68
+ if (msgStr.includes("Unauthorized"))
69
+ return { status: 401, body: { error: msgStr } };
70
+ if (msgStr.includes("Validation error"))
71
+ return { status: 400, body: { error: msgStr } };
72
+ if (msgStr.includes("No active invocation"))
73
+ return { status: 409, body: { error: msgStr } };
74
+ return { status: 500, body: { error: msgStr } };
75
+ }
76
+ if (body === null)
77
+ return { status: 204, body: null };
78
+ return { status: 200, body };
79
+ }
80
+ export function createHttpServer(deps) {
81
+ const router = new Router();
82
+ // Ensure engine is set on mcpDeps
83
+ deps.mcpDeps.engine = deps.engine;
84
+ if (deps.logger)
85
+ deps.mcpDeps.logger = deps.logger;
86
+ // --- Status ---
87
+ router.add("GET", "/api/status", async () => {
88
+ const status = await deps.engine.getStatus();
89
+ return { status: 200, body: status };
90
+ });
91
+ // --- Flow claim (cross-flow: no flow filter) ---
92
+ router.add("POST", "/api/claim", async (req) => {
93
+ const authErr = requireWorkerToken(deps, req);
94
+ if (authErr)
95
+ return authErr;
96
+ const args = { role: req.body?.role };
97
+ const result = await callToolHandler(deps.mcpDeps, "flow.claim", args);
98
+ return mcpResultToApi(result);
99
+ });
100
+ // --- Flow claim ---
101
+ router.add("POST", "/api/flows/:flow/claim", async (req) => {
102
+ const authErr = requireWorkerToken(deps, req);
103
+ if (authErr)
104
+ return authErr;
105
+ const args = { role: req.body?.role, flow: req.params.flow };
106
+ const result = await callToolHandler(deps.mcpDeps, "flow.claim", args);
107
+ return mcpResultToApi(result);
108
+ });
109
+ // --- Entity report ---
110
+ // longRunning: true — flow.report blocks for the duration of gate evaluation
111
+ // (potentially many minutes). The server handler calls req.setTimeout(0) for
112
+ // this route specifically so only this connection bypasses the global 30s timeout.
113
+ router.add("POST", "/api/entities/:id/report", async (req) => {
114
+ const authErr = requireWorkerToken(deps, req);
115
+ if (authErr)
116
+ return authErr;
117
+ const args = {
118
+ entity_id: req.params.id,
119
+ signal: req.body?.signal,
120
+ };
121
+ if (req.body?.worker_id)
122
+ args.worker_id = req.body.worker_id;
123
+ if (req.body?.artifacts)
124
+ args.artifacts = req.body.artifacts;
125
+ const result = await callToolHandler(deps.mcpDeps, "flow.report", args);
126
+ return mcpResultToApi(result);
127
+ }, { longRunning: true });
128
+ // --- Entity fail ---
129
+ router.add("POST", "/api/entities/:id/fail", async (req) => {
130
+ const authErr = requireWorkerToken(deps, req);
131
+ if (authErr)
132
+ return authErr;
133
+ const args = { entity_id: req.params.id, error: req.body?.error };
134
+ const result = await callToolHandler(deps.mcpDeps, "flow.fail", args);
135
+ return mcpResultToApi(result);
136
+ });
137
+ // --- Entity CRUD ---
138
+ router.add("POST", "/api/entities", async (req) => {
139
+ const flowName = req.body?.flow;
140
+ const refs = req.body?.refs;
141
+ if (!flowName)
142
+ return { status: 400, body: { error: "Missing required field: flow" } };
143
+ try {
144
+ const entity = await deps.engine.createEntity(flowName, refs);
145
+ return { status: 201, body: entity };
146
+ }
147
+ catch (err) {
148
+ const msg = err instanceof Error ? err.message : String(err);
149
+ if (msg.includes("not found"))
150
+ return { status: 404, body: { error: msg } };
151
+ return { status: 500, body: { error: msg } };
152
+ }
153
+ });
154
+ router.add("GET", "/api/entities/:id", async (req) => {
155
+ const result = await callToolHandler(deps.mcpDeps, "query.entity", { id: req.params.id });
156
+ return mcpResultToApi(result);
157
+ });
158
+ router.add("GET", "/api/entities", async (req) => {
159
+ const flow = req.query.get("flow");
160
+ const state = req.query.get("state");
161
+ if (!flow || !state)
162
+ return { status: 400, body: { error: "Required query params: flow, state" } };
163
+ const limitStr = req.query.get("limit");
164
+ const args = { flow, state };
165
+ if (limitStr) {
166
+ const limit = parseInt(limitStr, 10);
167
+ if (!Number.isNaN(limit) && limit > 0)
168
+ args.limit = limit;
169
+ }
170
+ const result = await callToolHandler(deps.mcpDeps, "query.entities", args);
171
+ return mcpResultToApi(result);
172
+ });
173
+ // --- Flow definition CRUD ---
174
+ router.add("GET", "/api/flows", async () => {
175
+ const flows = await deps.mcpDeps.flows.listAll();
176
+ return { status: 200, body: flows };
177
+ });
178
+ router.add("GET", "/api/flows/:id", async (req) => {
179
+ const result = await callToolHandler(deps.mcpDeps, "query.flow", { name: req.params.id });
180
+ return mcpResultToApi(result);
181
+ });
182
+ router.add("PUT", "/api/flows/:id", async (req) => {
183
+ const existing = await deps.mcpDeps.flows.getByName(req.params.id);
184
+ // Only pick known fields from body — never spread req.body to prevent param injection
185
+ const definition = req.body?.definition;
186
+ const description = req.body?.description;
187
+ const callerToken = extractBearerToken(req.authorization);
188
+ if (existing) {
189
+ const result = await callToolHandler(deps.mcpDeps, "admin.flow.update", { flow_name: req.params.id, definition, description }, { adminToken: deps.adminToken, callerToken });
190
+ return mcpResultToApi(result);
191
+ }
192
+ else {
193
+ const result = await callToolHandler(deps.mcpDeps, "admin.flow.create", { name: req.params.id, definition, description }, { adminToken: deps.adminToken, callerToken });
194
+ return mcpResultToApi(result);
195
+ }
196
+ });
197
+ router.add("DELETE", "/api/flows/:id", async () => {
198
+ return { status: 501, body: { error: "Flow deletion not implemented" } };
199
+ });
200
+ // --- HTTP server ---
201
+ const server = http.createServer(async (req, res) => {
202
+ // CORS
203
+ const origin = req.headers.origin;
204
+ if (origin) {
205
+ const isLoopbackOrigin = /^https?:\/\/localhost(:\d+)?$/.test(origin) ||
206
+ /^https?:\/\/127\.0\.0\.1(:\d+)?$/.test(origin) ||
207
+ /^https?:\/\/\[::1\](:\d+)?$/.test(origin);
208
+ const corsAllowed = deps.corsOrigin
209
+ ? origin === deps.corsOrigin // explicit origin: exact match only
210
+ : isLoopbackOrigin; // loopback mode: only reflect loopback origins
211
+ if (corsAllowed) {
212
+ res.setHeader("Vary", "Origin");
213
+ res.setHeader("Access-Control-Allow-Origin", origin);
214
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
215
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
216
+ }
217
+ }
218
+ if (req.method === "OPTIONS") {
219
+ res.writeHead(204).end();
220
+ return;
221
+ }
222
+ const url = new URL(req.url ?? "/", `http://localhost`);
223
+ const match = router.match(req.method ?? "GET", url.pathname);
224
+ if (!match) {
225
+ res.writeHead(404, { "Content-Type": "application/json" });
226
+ res.end(JSON.stringify({ error: "Not found" }));
227
+ return;
228
+ }
229
+ // flow.report blocks until gate evaluation completes (potentially many
230
+ // minutes). Extend timeout per-request for this route only; all other
231
+ // routes keep the global 30s limit.
232
+ if (match.longRunning) {
233
+ req.setTimeout(0);
234
+ }
235
+ let body = null;
236
+ if (req.method === "POST" || req.method === "PUT") {
237
+ try {
238
+ const { body: raw, tooLarge } = await readBody(req);
239
+ if (tooLarge) {
240
+ res.writeHead(413, { "Content-Type": "application/json" });
241
+ res.end(JSON.stringify({ error: "Request body too large" }));
242
+ return;
243
+ }
244
+ body = raw ? JSON.parse(raw) : null;
245
+ }
246
+ catch {
247
+ res.writeHead(400, { "Content-Type": "application/json" });
248
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
249
+ return;
250
+ }
251
+ }
252
+ const parsed = {
253
+ params: match.params,
254
+ query: url.searchParams,
255
+ body,
256
+ authorization: req.headers.authorization,
257
+ };
258
+ try {
259
+ const apiRes = await match.handler(parsed);
260
+ if (apiRes.status === 204) {
261
+ res.writeHead(204).end();
262
+ }
263
+ else {
264
+ res.writeHead(apiRes.status, { "Content-Type": "application/json" });
265
+ res.end(JSON.stringify(apiRes.body));
266
+ }
267
+ }
268
+ catch (err) {
269
+ (deps.logger ?? consoleLogger).error("Request error:", err);
270
+ res.writeHead(500, { "Content-Type": "application/json" });
271
+ res.end(JSON.stringify({ error: "Internal server error" }));
272
+ }
273
+ });
274
+ // Sensible defaults protect all routes from Slowloris/slow-header DoS.
275
+ // The entity report route calls req.setTimeout(0) per-request to bypass this
276
+ // limit only for connections that need long-running gate evaluation.
277
+ server.requestTimeout = 30000;
278
+ server.headersTimeout = 10000;
279
+ return server;
280
+ }