@retinue/agentkit 0.1.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 (405) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +310 -0
  3. package/dist/adapters/bullmq/consumer.d.ts +33 -0
  4. package/dist/adapters/bullmq/consumer.js +41 -0
  5. package/dist/adapters/bullmq/dispatcher.d.ts +74 -0
  6. package/dist/adapters/bullmq/dispatcher.js +160 -0
  7. package/dist/adapters/bullmq/export.d.ts +31 -0
  8. package/dist/adapters/bullmq/export.js +53 -0
  9. package/dist/adapters/bullmq/extraction.d.ts +42 -0
  10. package/dist/adapters/bullmq/extraction.js +63 -0
  11. package/dist/adapters/bullmq/index.d.ts +13 -0
  12. package/dist/adapters/bullmq/index.js +13 -0
  13. package/dist/adapters/bullmq/lock.d.ts +77 -0
  14. package/dist/adapters/bullmq/lock.js +126 -0
  15. package/dist/adapters/bullmq/queue.d.ts +50 -0
  16. package/dist/adapters/bullmq/queue.js +81 -0
  17. package/dist/adapters/memory/artifact-exports.d.ts +11 -0
  18. package/dist/adapters/memory/artifact-exports.js +102 -0
  19. package/dist/adapters/memory/artifacts.d.ts +15 -0
  20. package/dist/adapters/memory/artifacts.js +134 -0
  21. package/dist/adapters/memory/blobs.d.ts +7 -0
  22. package/dist/adapters/memory/blobs.js +27 -0
  23. package/dist/adapters/memory/evaluation.d.ts +18 -0
  24. package/dist/adapters/memory/evaluation.js +148 -0
  25. package/dist/adapters/memory/files.d.ts +27 -0
  26. package/dist/adapters/memory/files.js +0 -0
  27. package/dist/adapters/memory/flows.d.ts +16 -0
  28. package/dist/adapters/memory/flows.js +117 -0
  29. package/dist/adapters/memory/hitl.d.ts +9 -0
  30. package/dist/adapters/memory/hitl.js +130 -0
  31. package/dist/adapters/memory/idempotency.d.ts +13 -0
  32. package/dist/adapters/memory/idempotency.js +32 -0
  33. package/dist/adapters/memory/index.d.ts +39 -0
  34. package/dist/adapters/memory/index.js +107 -0
  35. package/dist/adapters/memory/knowledge.d.ts +43 -0
  36. package/dist/adapters/memory/knowledge.js +248 -0
  37. package/dist/adapters/memory/mcp.d.ts +9 -0
  38. package/dist/adapters/memory/mcp.js +37 -0
  39. package/dist/adapters/memory/message-store.d.ts +17 -0
  40. package/dist/adapters/memory/message-store.js +70 -0
  41. package/dist/adapters/memory/principal-memory.d.ts +7 -0
  42. package/dist/adapters/memory/principal-memory.js +83 -0
  43. package/dist/adapters/memory/runtime.d.ts +29 -0
  44. package/dist/adapters/memory/runtime.js +0 -0
  45. package/dist/adapters/memory/sessions.d.ts +29 -0
  46. package/dist/adapters/memory/sessions.js +0 -0
  47. package/dist/adapters/memory/skills.d.ts +10 -0
  48. package/dist/adapters/memory/skills.js +41 -0
  49. package/dist/adapters/memory/thread-summary.d.ts +7 -0
  50. package/dist/adapters/memory/thread-summary.js +29 -0
  51. package/dist/adapters/memory/usage-limits.d.ts +13 -0
  52. package/dist/adapters/memory/usage-limits.js +72 -0
  53. package/dist/adapters/memory/usage.d.ts +16 -0
  54. package/dist/adapters/memory/usage.js +279 -0
  55. package/dist/adapters/otel/index.d.ts +111 -0
  56. package/dist/adapters/otel/index.js +133 -0
  57. package/dist/adapters/postgres/artifact-exports.d.ts +12 -0
  58. package/dist/adapters/postgres/artifact-exports.js +117 -0
  59. package/dist/adapters/postgres/artifacts.d.ts +16 -0
  60. package/dist/adapters/postgres/artifacts.js +172 -0
  61. package/dist/adapters/postgres/checkpoint-store.d.ts +16 -0
  62. package/dist/adapters/postgres/checkpoint-store.js +34 -0
  63. package/dist/adapters/postgres/config.d.ts +15 -0
  64. package/dist/adapters/postgres/config.js +187 -0
  65. package/dist/adapters/postgres/conversation-store.d.ts +4 -0
  66. package/dist/adapters/postgres/conversation-store.js +82 -0
  67. package/dist/adapters/postgres/evaluation.d.ts +17 -0
  68. package/dist/adapters/postgres/evaluation.js +193 -0
  69. package/dist/adapters/postgres/file-content.d.ts +30 -0
  70. package/dist/adapters/postgres/file-content.js +111 -0
  71. package/dist/adapters/postgres/files.d.ts +19 -0
  72. package/dist/adapters/postgres/files.js +209 -0
  73. package/dist/adapters/postgres/flows.d.ts +20 -0
  74. package/dist/adapters/postgres/flows.js +206 -0
  75. package/dist/adapters/postgres/hitl.d.ts +5 -0
  76. package/dist/adapters/postgres/hitl.js +247 -0
  77. package/dist/adapters/postgres/index.d.ts +35 -0
  78. package/dist/adapters/postgres/index.js +35 -0
  79. package/dist/adapters/postgres/knowledge.d.ts +48 -0
  80. package/dist/adapters/postgres/knowledge.js +255 -0
  81. package/dist/adapters/postgres/memory.d.ts +14 -0
  82. package/dist/adapters/postgres/memory.js +194 -0
  83. package/dist/adapters/postgres/message-store.d.ts +11 -0
  84. package/dist/adapters/postgres/message-store.js +145 -0
  85. package/dist/adapters/postgres/migrations.d.ts +69 -0
  86. package/dist/adapters/postgres/migrations.js +1594 -0
  87. package/dist/adapters/postgres/pg-executor.d.ts +19 -0
  88. package/dist/adapters/postgres/pg-executor.js +32 -0
  89. package/dist/adapters/postgres/retention.d.ts +26 -0
  90. package/dist/adapters/postgres/retention.js +59 -0
  91. package/dist/adapters/postgres/rollups.d.ts +17 -0
  92. package/dist/adapters/postgres/rollups.js +267 -0
  93. package/dist/adapters/postgres/run-coordinator.d.ts +5 -0
  94. package/dist/adapters/postgres/run-coordinator.js +98 -0
  95. package/dist/adapters/postgres/run-event-log.d.ts +26 -0
  96. package/dist/adapters/postgres/run-event-log.js +30 -0
  97. package/dist/adapters/postgres/run-store.d.ts +4 -0
  98. package/dist/adapters/postgres/run-store.js +199 -0
  99. package/dist/adapters/postgres/schema.d.ts +39 -0
  100. package/dist/adapters/postgres/schema.js +70 -0
  101. package/dist/adapters/postgres/session-state.d.ts +7 -0
  102. package/dist/adapters/postgres/session-state.js +99 -0
  103. package/dist/adapters/postgres/sql.d.ts +8 -0
  104. package/dist/adapters/postgres/sql.js +2 -0
  105. package/dist/adapters/postgres/transaction.d.ts +37 -0
  106. package/dist/adapters/postgres/transaction.js +93 -0
  107. package/dist/adapters/postgres/unit-of-work.d.ts +18 -0
  108. package/dist/adapters/postgres/unit-of-work.js +8 -0
  109. package/dist/adapters/postgres/usage-limits.d.ts +15 -0
  110. package/dist/adapters/postgres/usage-limits.js +136 -0
  111. package/dist/adapters/postgres/usage.d.ts +15 -0
  112. package/dist/adapters/postgres/usage.js +226 -0
  113. package/dist/adapters/redis/index.d.ts +9 -0
  114. package/dist/adapters/redis/index.js +9 -0
  115. package/dist/adapters/redis/realtime.d.ts +74 -0
  116. package/dist/adapters/redis/realtime.js +112 -0
  117. package/dist/adapters/supabase/index.d.ts +88 -0
  118. package/dist/adapters/supabase/index.js +84 -0
  119. package/dist/adapters/supabase/realtime.d.ts +30 -0
  120. package/dist/adapters/supabase/realtime.js +53 -0
  121. package/dist/adapters/supabase/rls.d.ts +99 -0
  122. package/dist/adapters/supabase/rls.js +216 -0
  123. package/dist/adapters/supabase/storage.d.ts +50 -0
  124. package/dist/adapters/supabase/storage.js +207 -0
  125. package/dist/agents/agent.d.ts +66 -0
  126. package/dist/agents/agent.js +209 -0
  127. package/dist/agents/define.d.ts +21 -0
  128. package/dist/agents/define.js +63 -0
  129. package/dist/agents/engine.d.ts +98 -0
  130. package/dist/agents/engine.js +462 -0
  131. package/dist/agents/index.d.ts +50 -0
  132. package/dist/agents/index.js +17 -0
  133. package/dist/artifacts/index.d.ts +114 -0
  134. package/dist/artifacts/index.js +219 -0
  135. package/dist/authorization/index.d.ts +76 -0
  136. package/dist/authorization/index.js +76 -0
  137. package/dist/capabilities/index.d.ts +120 -0
  138. package/dist/capabilities/index.js +167 -0
  139. package/dist/capabilities/runtime.d.ts +89 -0
  140. package/dist/capabilities/runtime.js +84 -0
  141. package/dist/citations/index.d.ts +161 -0
  142. package/dist/citations/index.js +182 -0
  143. package/dist/context/assembler.d.ts +82 -0
  144. package/dist/context/assembler.js +129 -0
  145. package/dist/context/compaction.d.ts +45 -0
  146. package/dist/context/compaction.js +55 -0
  147. package/dist/context/index.d.ts +75 -0
  148. package/dist/context/index.js +17 -0
  149. package/dist/core/content-parts.d.ts +194 -0
  150. package/dist/core/content-parts.js +23 -0
  151. package/dist/core/context.d.ts +51 -0
  152. package/dist/core/context.js +9 -0
  153. package/dist/core/env.d.ts +25 -0
  154. package/dist/core/env.js +41 -0
  155. package/dist/core/errors.d.ts +30 -0
  156. package/dist/core/errors.js +65 -0
  157. package/dist/core/events.d.ts +139 -0
  158. package/dist/core/events.js +99 -0
  159. package/dist/core/ids.d.ts +52 -0
  160. package/dist/core/ids.js +13 -0
  161. package/dist/core/index.d.ts +9 -0
  162. package/dist/core/index.js +9 -0
  163. package/dist/core/tokens.d.ts +22 -0
  164. package/dist/core/tokens.js +22 -0
  165. package/dist/core/validation.d.ts +34 -0
  166. package/dist/core/validation.js +176 -0
  167. package/dist/documents/extraction.d.ts +121 -0
  168. package/dist/documents/extraction.js +293 -0
  169. package/dist/documents/index.d.ts +199 -0
  170. package/dist/documents/index.js +65 -0
  171. package/dist/documents/parsers/pdf.d.ts +47 -0
  172. package/dist/documents/parsers/pdf.js +508 -0
  173. package/dist/documents/parsers/text.d.ts +59 -0
  174. package/dist/documents/parsers/text.js +325 -0
  175. package/dist/documents/read-tool.d.ts +52 -0
  176. package/dist/documents/read-tool.js +109 -0
  177. package/dist/documents/render.d.ts +29 -0
  178. package/dist/documents/render.js +59 -0
  179. package/dist/documents/vision.d.ts +159 -0
  180. package/dist/documents/vision.js +214 -0
  181. package/dist/entries/adapters-bullmq.d.ts +8 -0
  182. package/dist/entries/adapters-bullmq.js +8 -0
  183. package/dist/entries/adapters-otel.d.ts +13 -0
  184. package/dist/entries/adapters-otel.js +13 -0
  185. package/dist/entries/adapters-postgres.d.ts +10 -0
  186. package/dist/entries/adapters-postgres.js +10 -0
  187. package/dist/entries/adapters-redis.d.ts +3 -0
  188. package/dist/entries/adapters-redis.js +3 -0
  189. package/dist/entries/context.d.ts +20 -0
  190. package/dist/entries/context.js +20 -0
  191. package/dist/entries/flows.d.ts +15 -0
  192. package/dist/entries/flows.js +15 -0
  193. package/dist/entries/hitl.d.ts +10 -0
  194. package/dist/entries/hitl.js +10 -0
  195. package/dist/entries/knowledge.d.ts +18 -0
  196. package/dist/entries/knowledge.js +19 -0
  197. package/dist/entries/mcp.d.ts +10 -0
  198. package/dist/entries/mcp.js +10 -0
  199. package/dist/entries/observability.d.ts +14 -0
  200. package/dist/entries/observability.js +16 -0
  201. package/dist/entries/persistence.d.ts +11 -0
  202. package/dist/entries/persistence.js +11 -0
  203. package/dist/entries/providers.d.ts +14 -0
  204. package/dist/entries/providers.js +14 -0
  205. package/dist/entries/runtime.d.ts +13 -0
  206. package/dist/entries/runtime.js +13 -0
  207. package/dist/entries/server.d.ts +24 -0
  208. package/dist/entries/server.js +24 -0
  209. package/dist/entries/tools.d.ts +21 -0
  210. package/dist/entries/tools.js +21 -0
  211. package/dist/entries/usage.d.ts +10 -0
  212. package/dist/entries/usage.js +10 -0
  213. package/dist/evaluation/gate.d.ts +168 -0
  214. package/dist/evaluation/gate.js +180 -0
  215. package/dist/evaluation/graders.d.ts +125 -0
  216. package/dist/evaluation/graders.js +203 -0
  217. package/dist/evaluation/index.d.ts +120 -0
  218. package/dist/evaluation/index.js +183 -0
  219. package/dist/evaluation/judge.d.ts +75 -0
  220. package/dist/evaluation/judge.js +111 -0
  221. package/dist/export/index.d.ts +162 -0
  222. package/dist/export/index.js +363 -0
  223. package/dist/export/markdown.d.ts +19 -0
  224. package/dist/export/markdown.js +29 -0
  225. package/dist/export/pdf.d.ts +73 -0
  226. package/dist/export/pdf.js +407 -0
  227. package/dist/files/context.d.ts +97 -0
  228. package/dist/files/context.js +185 -0
  229. package/dist/files/index.d.ts +210 -0
  230. package/dist/files/index.js +338 -0
  231. package/dist/files/read-tool.d.ts +81 -0
  232. package/dist/files/read-tool.js +163 -0
  233. package/dist/files/turn-parts.d.ts +96 -0
  234. package/dist/files/turn-parts.js +171 -0
  235. package/dist/flows/index.d.ts +270 -0
  236. package/dist/flows/index.js +62 -0
  237. package/dist/flows/interpreter.d.ts +146 -0
  238. package/dist/flows/interpreter.js +426 -0
  239. package/dist/flows/runner.d.ts +145 -0
  240. package/dist/flows/runner.js +270 -0
  241. package/dist/graphql/index.d.ts +8 -0
  242. package/dist/graphql/index.js +8 -0
  243. package/dist/graphql/resolvers.d.ts +237 -0
  244. package/dist/graphql/resolvers.js +253 -0
  245. package/dist/graphql/schema.d.ts +11 -0
  246. package/dist/graphql/schema.js +258 -0
  247. package/dist/graphql/sse.d.ts +77 -0
  248. package/dist/graphql/sse.js +100 -0
  249. package/dist/hitl/approved-execution.d.ts +127 -0
  250. package/dist/hitl/approved-execution.js +177 -0
  251. package/dist/hitl/index.d.ts +79 -0
  252. package/dist/hitl/index.js +12 -0
  253. package/dist/hitl/service.d.ts +221 -0
  254. package/dist/hitl/service.js +268 -0
  255. package/dist/idempotency/index.d.ts +70 -0
  256. package/dist/idempotency/index.js +59 -0
  257. package/dist/index.d.ts +103 -0
  258. package/dist/index.js +60 -0
  259. package/dist/knowledge/chunking.d.ts +57 -0
  260. package/dist/knowledge/chunking.js +158 -0
  261. package/dist/knowledge/index.d.ts +119 -0
  262. package/dist/knowledge/index.js +166 -0
  263. package/dist/knowledge/retrieval.d.ts +146 -0
  264. package/dist/knowledge/retrieval.js +170 -0
  265. package/dist/loadtest/harness.d.ts +168 -0
  266. package/dist/loadtest/harness.js +507 -0
  267. package/dist/loadtest/index.d.ts +13 -0
  268. package/dist/loadtest/index.js +13 -0
  269. package/dist/loadtest/injection.d.ts +89 -0
  270. package/dist/loadtest/injection.js +147 -0
  271. package/dist/loadtest/metrics.d.ts +197 -0
  272. package/dist/loadtest/metrics.js +160 -0
  273. package/dist/loadtest/runbooks.d.ts +28 -0
  274. package/dist/loadtest/runbooks.js +159 -0
  275. package/dist/loadtest/scenario.d.ts +104 -0
  276. package/dist/loadtest/scenario.js +208 -0
  277. package/dist/mcp/egress.d.ts +53 -0
  278. package/dist/mcp/egress.js +115 -0
  279. package/dist/mcp/index.d.ts +93 -0
  280. package/dist/mcp/index.js +33 -0
  281. package/dist/mcp/provider.d.ts +62 -0
  282. package/dist/mcp/provider.js +0 -0
  283. package/dist/models/index.d.ts +98 -0
  284. package/dist/models/index.js +74 -0
  285. package/dist/models/pricing.d.ts +24 -0
  286. package/dist/models/pricing.js +37 -0
  287. package/dist/models/provider-factory.d.ts +31 -0
  288. package/dist/models/provider-factory.js +67 -0
  289. package/dist/models/streaming.d.ts +145 -0
  290. package/dist/models/streaming.js +272 -0
  291. package/dist/models/vision.d.ts +38 -0
  292. package/dist/models/vision.js +62 -0
  293. package/dist/persistence/index.d.ts +1654 -0
  294. package/dist/persistence/index.js +226 -0
  295. package/dist/principal-memory/index.d.ts +106 -0
  296. package/dist/principal-memory/index.js +89 -0
  297. package/dist/retention/index.d.ts +89 -0
  298. package/dist/retention/index.js +70 -0
  299. package/dist/runtime/checkpoint.d.ts +37 -0
  300. package/dist/runtime/checkpoint.js +22 -0
  301. package/dist/runtime/index.d.ts +118 -0
  302. package/dist/runtime/index.js +69 -0
  303. package/dist/runtime/retry.d.ts +95 -0
  304. package/dist/runtime/retry.js +126 -0
  305. package/dist/runtime/serialization.d.ts +85 -0
  306. package/dist/runtime/serialization.js +95 -0
  307. package/dist/runtime/streaming.d.ts +54 -0
  308. package/dist/runtime/streaming.js +115 -0
  309. package/dist/runtime/worker.d.ts +130 -0
  310. package/dist/runtime/worker.js +405 -0
  311. package/dist/security/checklist.d.ts +53 -0
  312. package/dist/security/checklist.js +204 -0
  313. package/dist/security/findings.d.ts +56 -0
  314. package/dist/security/findings.js +168 -0
  315. package/dist/security/index.d.ts +14 -0
  316. package/dist/security/index.js +14 -0
  317. package/dist/security/prompt-safety.d.ts +100 -0
  318. package/dist/security/prompt-safety.js +133 -0
  319. package/dist/server/boot.d.ts +32 -0
  320. package/dist/server/boot.js +36 -0
  321. package/dist/server/cli-worker.d.ts +37 -0
  322. package/dist/server/cli-worker.js +151 -0
  323. package/dist/server/cli.d.ts +27 -0
  324. package/dist/server/cli.js +74 -0
  325. package/dist/server/config.d.ts +42 -0
  326. package/dist/server/config.js +127 -0
  327. package/dist/server/health.d.ts +59 -0
  328. package/dist/server/health.js +90 -0
  329. package/dist/server/host.d.ts +39 -0
  330. package/dist/server/host.js +124 -0
  331. package/dist/server/index.d.ts +15 -0
  332. package/dist/server/index.js +15 -0
  333. package/dist/server/main.d.ts +16 -0
  334. package/dist/server/main.js +31 -0
  335. package/dist/server/sse-route.d.ts +21 -0
  336. package/dist/server/sse-route.js +282 -0
  337. package/dist/skills/index.d.ts +67 -0
  338. package/dist/skills/index.js +31 -0
  339. package/dist/skills/resolver.d.ts +54 -0
  340. package/dist/skills/resolver.js +121 -0
  341. package/dist/teams/index.d.ts +93 -0
  342. package/dist/teams/index.js +207 -0
  343. package/dist/telemetry/index.d.ts +157 -0
  344. package/dist/telemetry/index.js +71 -0
  345. package/dist/telemetry/instrument.d.ts +108 -0
  346. package/dist/telemetry/instrument.js +232 -0
  347. package/dist/telemetry/log-events.d.ts +17 -0
  348. package/dist/telemetry/log-events.js +58 -0
  349. package/dist/telemetry/metrics.d.ts +123 -0
  350. package/dist/telemetry/metrics.js +135 -0
  351. package/dist/telemetry/noop.d.ts +39 -0
  352. package/dist/telemetry/noop.js +143 -0
  353. package/dist/telemetry/redaction.d.ts +64 -0
  354. package/dist/telemetry/redaction.js +153 -0
  355. package/dist/telemetry/spans.d.ts +56 -0
  356. package/dist/telemetry/spans.js +78 -0
  357. package/dist/telemetry/trace-context.d.ts +55 -0
  358. package/dist/telemetry/trace-context.js +60 -0
  359. package/dist/toolkit/compute.d.ts +53 -0
  360. package/dist/toolkit/compute.js +152 -0
  361. package/dist/toolkit/data.d.ts +98 -0
  362. package/dist/toolkit/data.js +235 -0
  363. package/dist/toolkit/http.d.ts +113 -0
  364. package/dist/toolkit/http.js +205 -0
  365. package/dist/toolkit/index.d.ts +21 -0
  366. package/dist/toolkit/index.js +17 -0
  367. package/dist/toolkit/web.d.ts +107 -0
  368. package/dist/toolkit/web.js +147 -0
  369. package/dist/tools/define.d.ts +25 -0
  370. package/dist/tools/define.js +45 -0
  371. package/dist/tools/delegating.d.ts +132 -0
  372. package/dist/tools/delegating.js +211 -0
  373. package/dist/tools/index.d.ts +129 -0
  374. package/dist/tools/index.js +33 -0
  375. package/dist/tools/library/compute.d.ts +11 -0
  376. package/dist/tools/library/compute.js +46 -0
  377. package/dist/tools/library/data.d.ts +16 -0
  378. package/dist/tools/library/data.js +92 -0
  379. package/dist/tools/library/http.d.ts +28 -0
  380. package/dist/tools/library/http.js +71 -0
  381. package/dist/tools/library/index.d.ts +97 -0
  382. package/dist/tools/library/index.js +134 -0
  383. package/dist/tools/library/knowledge.d.ts +39 -0
  384. package/dist/tools/library/knowledge.js +58 -0
  385. package/dist/tools/library/web.d.ts +19 -0
  386. package/dist/tools/library/web.js +65 -0
  387. package/dist/tools/meta-tools.d.ts +19 -0
  388. package/dist/tools/meta-tools.js +36 -0
  389. package/dist/tools/registry.d.ts +146 -0
  390. package/dist/tools/registry.js +291 -0
  391. package/dist/usage/index.d.ts +105 -0
  392. package/dist/usage/index.js +20 -0
  393. package/dist/usage/quota.d.ts +258 -0
  394. package/dist/usage/quota.js +510 -0
  395. package/dist/usage/recorder.d.ts +29 -0
  396. package/dist/usage/recorder.js +96 -0
  397. package/dist/usage/rollups.d.ts +121 -0
  398. package/dist/usage/rollups.js +157 -0
  399. package/dist/worker/export.d.ts +57 -0
  400. package/dist/worker/export.js +81 -0
  401. package/dist/worker/extraction.d.ts +57 -0
  402. package/dist/worker/extraction.js +84 -0
  403. package/dist/worker/main.d.ts +103 -0
  404. package/dist/worker/main.js +159 -0
  405. package/package.json +187 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The runbooks — AC-5: "runbooks exist for every failure mode observed in testing".
3
+ *
4
+ * Kept as data next to the failure matrix, so a test can assert the two agree. A runbook in a wiki is a runbook
5
+ * that drifts from the code silently, and the drift is discovered during the incident it was written for.
6
+ *
7
+ * Each one is written for someone woken at 3am: what they will see, what to check first, what to do, and — the
8
+ * part usually missing — **what not to do**. Most of the damage in an incident comes from a well-intentioned
9
+ * action that fights the recovery mechanism, and "do not restart the workers, the reaper is already handling it"
10
+ * is the sentence that saves the night.
11
+ */
12
+ import { FAILURE_MODES } from "./injection.js";
13
+ export const RUNBOOKS = {
14
+ "provider-timeout": {
15
+ mode: "provider-timeout",
16
+ symptoms: [
17
+ "agentkit_model_latency_ms p99 pinned at the provider timeout",
18
+ "agentkit_retries_total climbing",
19
+ "agentkit_run_duration_ms following, with throughput falling",
20
+ ],
21
+ confirm: [
22
+ "model.failed log lines with errorCode=timeout, not rate-limited — the two need different responses",
23
+ "agentkit_model_calls_total{outcome=error} rising while tool metrics stay flat, which locates it at the provider",
24
+ ],
25
+ action: [
26
+ "Nothing, first: retries are bounded and runs will fail cleanly rather than hang.",
27
+ "If it persists beyond the provider's own status page, fail over the model configuration to a second provider.",
28
+ "Runs that failed are safe to re-drive — the checkpoint means a resumed run does not repeat a completed tool call.",
29
+ ],
30
+ doNot: [
31
+ "Do not raise the retry count to ride it out. Retries hold a worker slot, so more retries against a dead provider converts a provider outage into a queue backlog.",
32
+ "Do not raise the provider timeout. A longer timeout holds the same slot for longer and reaches the same place more slowly.",
33
+ ],
34
+ expectedRecovery: "Immediate once the provider answers; in-flight runs fail after their retry budget and can be re-driven.",
35
+ },
36
+ "provider-rate-limit": {
37
+ mode: "provider-rate-limit",
38
+ symptoms: [
39
+ "agentkit_retries_total with reason=rate-limited",
40
+ "throughput drops while error rate stays near zero — the shape of working backoff",
41
+ ],
42
+ confirm: ["run.retry-scheduled log lines", "the provider's own quota dashboard"],
43
+ action: [
44
+ "Reduce worker concurrency. Fewer parallel calls means fewer rejections and, counter-intuitively, more completed work.",
45
+ "If sustained, raise the provider quota or spread across provider accounts.",
46
+ ],
47
+ doNot: [
48
+ "Do not add workers. More workers means more concurrent calls against the same quota, so throughput falls as the rejection rate rises.",
49
+ "Do not disable the retry. A rate limit is the one failure that genuinely does resolve by waiting.",
50
+ ],
51
+ expectedRecovery: "Throughput returns as the provider's window rolls; no run is lost.",
52
+ },
53
+ "redis-unavailable": {
54
+ mode: "redis-unavailable",
55
+ symptoms: [
56
+ "enqueue errors with code=unavailable, immediately rather than after a hang",
57
+ "queue depth unreadable; no new claims",
58
+ "runs already claimed continue to completion",
59
+ ],
60
+ confirm: [
61
+ "run.enqueue-failed log lines with errorCode=unavailable",
62
+ "in-flight runs still reaching terminal states, which distinguishes this from a database problem",
63
+ ],
64
+ action: [
65
+ "Restore Redis. The reaper re-enqueues anything whose lease expired while it was down.",
66
+ "Callers that received the typed error may retry; the job id is derived from tenant and run, so a repeat enqueue collapses into one job.",
67
+ ],
68
+ doNot: [
69
+ "Do not re-drive runs by hand while Redis is down; there is nowhere for the job to go and the attempt fails.",
70
+ "Do not clear the queue to 'start clean'. Those jobs are the record of admitted work.",
71
+ ],
72
+ expectedRecovery: "Claims resume immediately; expired leases are re-enqueued within one reap interval.",
73
+ },
74
+ "database-unavailable": {
75
+ mode: "database-unavailable",
76
+ symptoms: [
77
+ "claims stop entirely; runs fail with code=unavailable",
78
+ "queue depth rises because nothing can be claimed",
79
+ "connection errors in the worker logs",
80
+ ],
81
+ confirm: ["run.failed with errorCode=unavailable across every worker at once, which is what separates this from one bad worker"],
82
+ action: [
83
+ "Restore the database. Runs resume from their last checkpoint, so a multi-step run does not restart from the beginning.",
84
+ "Expect a burst of reaper activity as leases that expired during the outage are collected.",
85
+ ],
86
+ doNot: [
87
+ "Do not truncate or reset anything to clear the errors. The run rows and checkpoints are what makes resumption possible.",
88
+ "Do not restart workers repeatedly; a worker with no database cannot claim, and restarting only loses its in-memory state.",
89
+ ],
90
+ expectedRecovery: "Claims resume immediately; checkpointed runs continue from their last committed step.",
91
+ },
92
+ "database-failover": {
93
+ mode: "database-failover",
94
+ symptoms: ["a burst of connection errors, then recovery", "a spike in reaped runs shortly after"],
95
+ confirm: ["the failover event in the database provider's own log, correlated with the error burst"],
96
+ action: [
97
+ "Wait. The compare-and-set claim and the per-step checkpoint mean a half-applied step is never observable, so recovery is automatic.",
98
+ "Check that the connection pool reconnected — a pool holding dead connections looks exactly like a database still being down.",
99
+ ],
100
+ doNot: [
101
+ "Do not re-drive runs during the failover window. They are still claimed until their leases expire, and a manual re-drive races the reaper.",
102
+ ],
103
+ expectedRecovery: "Seconds to the provider's failover time, plus one lease interval for runs that were mid-step.",
104
+ },
105
+ "worker-kill": {
106
+ mode: "worker-kill",
107
+ symptoms: [
108
+ "one worker disappears; its runs sit in 'running' with no progress",
109
+ "claim latency p99 rises by roughly one lease interval",
110
+ ],
111
+ confirm: ["run.reaped log lines naming the runs, one lease interval after the worker went", "the killed worker's workerId absent from recent run.claimed lines"],
112
+ action: [
113
+ "Nothing. The lease expires, the reaper re-enqueues, another worker resumes from the checkpoint.",
114
+ "If the worker was killed by the OOM killer, reduce concurrency before bringing it back or it will be killed again.",
115
+ ],
116
+ doNot: [
117
+ "Do not re-drive the runs by hand before the lease expires. The claim is still held, so the manual attempt is rejected — and if it were not, it would be the duplicate-external-action bug this platform is built to prevent.",
118
+ "Do not shorten the lease to speed recovery. A lease shorter than a slow step causes a *live* worker to lose its claim mid-run, which is a much worse failure.",
119
+ ],
120
+ expectedRecovery: "One lease interval plus one reap interval.",
121
+ },
122
+ "slow-consumer": {
123
+ mode: "slow-consumer",
124
+ symptoms: ["a subscriber falling behind its run's event sequence", "no producer-side memory growth"],
125
+ confirm: ["the subscriber's last acknowledged sequence lagging the run's current sequence"],
126
+ action: [
127
+ "Nothing on the platform side: the durable event log means a slow reader holds no producer buffer and resumes from its own sequence.",
128
+ "Investigate the client. A consistently slow subscriber is usually rendering every event rather than batching.",
129
+ ],
130
+ doNot: [
131
+ "Do not raise a buffer size to 'give it room'. A producer-side buffer is precisely what turns one slow client into the platform's memory problem.",
132
+ ],
133
+ expectedRecovery: "The subscriber catches up at its own pace; nothing is lost while it is behind.",
134
+ },
135
+ overload: {
136
+ mode: "overload",
137
+ symptoms: [
138
+ "queue depth at its bound",
139
+ "admissions refused with code=resource-exhausted",
140
+ "RSS flat — which is the point",
141
+ ],
142
+ confirm: [
143
+ "run.refused-quota or a resource-exhausted rate rising while error rate stays low",
144
+ "flat memory alongside the refusals; memory growth instead means the bound is not being enforced",
145
+ ],
146
+ action: [
147
+ "Add workers if the database can take the extra connections; otherwise raise capacity upstream.",
148
+ "Refusals are the correct behaviour. Communicate them as capacity, not as errors.",
149
+ ],
150
+ doNot: [
151
+ "Do not raise maxQueueDepth to stop the refusals. That converts an honest 'no' into an unbounded backlog, and the queue then fails by exhausting memory instead of by saying no.",
152
+ "Do not disable admission checks to 'let the work through'. A refused run holds no slot and no job; an admitted one it cannot serve holds both.",
153
+ ],
154
+ expectedRecovery: "Immediate once offered load falls below capacity; the backlog drains at the sustainable rate.",
155
+ },
156
+ };
157
+ /** Every mode has a runbook. Asserted, so a new injector cannot ship undocumented. */
158
+ export const modesWithoutRunbook = () => FAILURE_MODES.filter((mode) => RUNBOOKS[mode] === undefined);
159
+ //# sourceMappingURL=runbooks.js.map
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The traffic mix and the synthetic engine — the load the harness actually offers.
3
+ *
4
+ * **Why the engine is synthetic, and why that is not a compromise.** A load test cannot drive a real model
5
+ * provider: it would cost money proportional to the load, the provider's own rate limits would become the thing
6
+ * under test, and a provider outage would look like a platform bug. Nobody load-tests through a paid third
7
+ * party. What *is* under test is the platform — the claim, the lease, the checkpoint, the queue, the event log,
8
+ * the idempotency of external effects — and every one of those is real here.
9
+ *
10
+ * The engine's job is therefore to be a *believable* source of latency and failure, and its knobs are exactly
11
+ * the failure modes #144 lists: a timeout, a rate limit, a tool that performs an external effect, and an
12
+ * approval that suspends the run.
13
+ *
14
+ * The one thing it must get right is the **external effect**: it performs a side effect under an idempotency
15
+ * key, so "no duplicated external action" becomes a count of effects against a count of distinct keys rather
16
+ * than a reading of logs.
17
+ */
18
+ import type { AgentEngine } from "../runtime/worker.js";
19
+ export type TrafficShape = {
20
+ /** Steps a run takes before completing. More steps means more checkpoints and more to lose on a kill. */
21
+ readonly steps: number;
22
+ /** Simulated model latency per step. */
23
+ readonly modelLatencyMs: number;
24
+ /** Fraction of runs that call a tool with an external effect. */
25
+ readonly externalActionRate: number;
26
+ /** Fraction of runs that pause for an approval. */
27
+ readonly approvalRate: number;
28
+ /** Fraction of steps where the provider times out before eventually succeeding. */
29
+ readonly providerTimeoutRate: number;
30
+ /** Fraction of steps the provider rate-limits. */
31
+ readonly rateLimitRate: number;
32
+ };
33
+ /**
34
+ * The default mix.
35
+ *
36
+ * Chosen to look like the product rather than to look good: most runs are a few steps of model output, a
37
+ * meaningful minority calls a tool that touches the outside world, and a smaller minority stops for a human. A
38
+ * mix with no approvals would miss the longest-lived state the platform has, and a mix with no external actions
39
+ * would make the one assertion that matters untestable.
40
+ */
41
+ export declare const DEFAULT_TRAFFIC: TrafficShape;
42
+ /** Where the synthetic external effects are counted. Shared by the engine and the assertions. */
43
+ export type EffectLedger = {
44
+ /** Every effect performed, in order, with the key it was performed under. */
45
+ readonly performed: {
46
+ readonly key: string;
47
+ readonly atMs: number;
48
+ }[];
49
+ perform(key: string, atMs: number): void;
50
+ /** Distinct keys. Compared against `performed.length` — equality is "no duplicate". */
51
+ distinctKeys(): number;
52
+ };
53
+ export declare const createEffectLedger: () => EffectLedger;
54
+ /**
55
+ * Deterministic pseudo-randomness.
56
+ *
57
+ * A seeded generator, so a run that found a failure can be replayed exactly. `Math.random()` would make the
58
+ * interesting runs — the ones that broke — unrepeatable, which is the opposite of what a load test is for.
59
+ */
60
+ export declare const seededRandom: (seed: number) => (() => number);
61
+ /**
62
+ * Two stable, well-distributed fractions from a run id.
63
+ *
64
+ * Stable because a resumed run must draw the same fate as its first attempt — `Math.random()` and a shared
65
+ * generator both fail at that, and a scenario that changes under a resume can make no claim about resumption.
66
+ *
67
+ * Well-distributed because the first version was not, and it mattered. Plain FNV-1a with two different offset
68
+ * bases, taken as a raw fraction, gave 0.03 external-action rate for one id prefix and 0.50 for another against a
69
+ * configured 0.30 — the low bits are poorly mixed and two bases correlate. So: FNV-1a over a *salted* string,
70
+ * then murmur3's `fmix32` avalanche. A biased traffic mix is a load test that measures a different workload than
71
+ * the one it reports, and nothing in the output would have said so.
72
+ */
73
+ export declare const runFate: (runId: string) => {
74
+ readonly a: number;
75
+ readonly b: number;
76
+ };
77
+ export type SyntheticEngineDeps = {
78
+ readonly traffic: TrafficShape;
79
+ readonly effects: EffectLedger;
80
+ readonly random: () => number;
81
+ readonly now?: () => number;
82
+ /** Injected so a test does not actually wait. Real runs pass a real sleep. */
83
+ readonly sleep?: (ms: number) => Promise<void>;
84
+ };
85
+ /** A rate-limit failure shaped like a provider's, so the retry path treats it the way it would in production. */
86
+ export declare class SyntheticRateLimit extends Error {
87
+ readonly code = "rate-limited";
88
+ readonly retryable = true;
89
+ constructor();
90
+ }
91
+ export declare class SyntheticTimeout extends Error {
92
+ readonly code = "timeout";
93
+ readonly retryable = true;
94
+ constructor();
95
+ }
96
+ /**
97
+ * The engine.
98
+ *
99
+ * Resumable, because the whole point of the worker-kill case is that a restarted run must not redo work. It
100
+ * reads `resume` and skips the steps already in the checkpoint — and the external effect is keyed on the run and
101
+ * the step, so a step that *is* redone reuses its key and the duplicate is detectable rather than invisible.
102
+ */
103
+ export declare const createSyntheticEngine: (deps: SyntheticEngineDeps) => AgentEngine;
104
+ //# sourceMappingURL=scenario.d.ts.map
@@ -0,0 +1,208 @@
1
+ /**
2
+ * The traffic mix and the synthetic engine — the load the harness actually offers.
3
+ *
4
+ * **Why the engine is synthetic, and why that is not a compromise.** A load test cannot drive a real model
5
+ * provider: it would cost money proportional to the load, the provider's own rate limits would become the thing
6
+ * under test, and a provider outage would look like a platform bug. Nobody load-tests through a paid third
7
+ * party. What *is* under test is the platform — the claim, the lease, the checkpoint, the queue, the event log,
8
+ * the idempotency of external effects — and every one of those is real here.
9
+ *
10
+ * The engine's job is therefore to be a *believable* source of latency and failure, and its knobs are exactly
11
+ * the failure modes #144 lists: a timeout, a rate limit, a tool that performs an external effect, and an
12
+ * approval that suspends the run.
13
+ *
14
+ * The one thing it must get right is the **external effect**: it performs a side effect under an idempotency
15
+ * key, so "no duplicated external action" becomes a count of effects against a count of distinct keys rather
16
+ * than a reading of logs.
17
+ */
18
+ import { asId } from "../core/ids.js";
19
+ /**
20
+ * The default mix.
21
+ *
22
+ * Chosen to look like the product rather than to look good: most runs are a few steps of model output, a
23
+ * meaningful minority calls a tool that touches the outside world, and a smaller minority stops for a human. A
24
+ * mix with no approvals would miss the longest-lived state the platform has, and a mix with no external actions
25
+ * would make the one assertion that matters untestable.
26
+ */
27
+ export const DEFAULT_TRAFFIC = {
28
+ steps: 3,
29
+ modelLatencyMs: 40,
30
+ externalActionRate: 0.3,
31
+ approvalRate: 0.1,
32
+ providerTimeoutRate: 0,
33
+ rateLimitRate: 0,
34
+ };
35
+ export const createEffectLedger = () => {
36
+ const performed = [];
37
+ return {
38
+ performed,
39
+ perform(key, atMs) {
40
+ // Deliberately **not** deduplicated here. The platform's idempotency is what is under test, so a ledger
41
+ // that refused a repeat would be answering the question on the platform's behalf and every run would pass.
42
+ performed.push({ key, atMs });
43
+ },
44
+ distinctKeys: () => new Set(performed.map((p) => p.key)).size,
45
+ };
46
+ };
47
+ /**
48
+ * Deterministic pseudo-randomness.
49
+ *
50
+ * A seeded generator, so a run that found a failure can be replayed exactly. `Math.random()` would make the
51
+ * interesting runs — the ones that broke — unrepeatable, which is the opposite of what a load test is for.
52
+ */
53
+ export const seededRandom = (seed) => {
54
+ let state = seed >>> 0 || 1;
55
+ return () => {
56
+ // xorshift32. Small, fast, and good enough to pick a branch; it is choosing traffic, not generating keys.
57
+ state ^= state << 13;
58
+ state ^= state >>> 17;
59
+ state ^= state << 5;
60
+ return ((state >>> 0) % 1_000_000) / 1_000_000;
61
+ };
62
+ };
63
+ /**
64
+ * Two stable, well-distributed fractions from a run id.
65
+ *
66
+ * Stable because a resumed run must draw the same fate as its first attempt — `Math.random()` and a shared
67
+ * generator both fail at that, and a scenario that changes under a resume can make no claim about resumption.
68
+ *
69
+ * Well-distributed because the first version was not, and it mattered. Plain FNV-1a with two different offset
70
+ * bases, taken as a raw fraction, gave 0.03 external-action rate for one id prefix and 0.50 for another against a
71
+ * configured 0.30 — the low bits are poorly mixed and two bases correlate. So: FNV-1a over a *salted* string,
72
+ * then murmur3's `fmix32` avalanche. A biased traffic mix is a load test that measures a different workload than
73
+ * the one it reports, and nothing in the output would have said so.
74
+ */
75
+ export const runFate = (runId) => {
76
+ const fnv = (text) => {
77
+ let h = 0x811c9dc5;
78
+ for (let i = 0; i < text.length; i += 1) {
79
+ h ^= text.charCodeAt(i);
80
+ h = Math.imul(h, 0x01000193) >>> 0;
81
+ }
82
+ return h >>> 0;
83
+ };
84
+ // murmur3 fmix32. Without it the low bits stay correlated with the input and sequential ids cluster.
85
+ const mix = (h0) => {
86
+ let h = h0 >>> 0;
87
+ h ^= h >>> 16;
88
+ h = Math.imul(h, 0x85ebca6b) >>> 0;
89
+ h ^= h >>> 13;
90
+ h = Math.imul(h, 0xc2b2ae35) >>> 0;
91
+ h ^= h >>> 16;
92
+ return (h >>> 0) / 0x1_0000_0000;
93
+ };
94
+ // Two different *strings*, not two different bases: salting the input decorrelates the pair, changing the
95
+ // basis does not.
96
+ return { a: mix(fnv(`effect:${runId}`)), b: mix(fnv(`approval:${runId}`)) };
97
+ };
98
+ /** A rate-limit failure shaped like a provider's, so the retry path treats it the way it would in production. */
99
+ export class SyntheticRateLimit extends Error {
100
+ code = "rate-limited";
101
+ retryable = true;
102
+ constructor() {
103
+ super("synthetic rate limit");
104
+ this.name = "SyntheticRateLimit";
105
+ }
106
+ }
107
+ export class SyntheticTimeout extends Error {
108
+ code = "timeout";
109
+ retryable = true;
110
+ constructor() {
111
+ super("synthetic provider timeout");
112
+ this.name = "SyntheticTimeout";
113
+ }
114
+ }
115
+ /**
116
+ * The engine.
117
+ *
118
+ * Resumable, because the whole point of the worker-kill case is that a restarted run must not redo work. It
119
+ * reads `resume` and skips the steps already in the checkpoint — and the external effect is keyed on the run and
120
+ * the step, so a step that *is* redone reuses its key and the duplicate is detectable rather than invisible.
121
+ */
122
+ export const createSyntheticEngine = (deps) => {
123
+ const now = deps.now ?? Date.now;
124
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
125
+ return {
126
+ async *run(input) {
127
+ const { traffic } = deps;
128
+ /**
129
+ * How far a previous attempt got — counted from the engine's **own** step parts.
130
+ *
131
+ * Not `parts.length`, and not `resume.step`. `parts.length` was the first version and it was wrong: the
132
+ * approval marker is a part too, so a run that paused for approval came back with `alreadyDone` already
133
+ * equal to `steps`, skipped the loop entirely, never yielded `run.completed`, and sat in
134
+ * `waiting-for-approval` forever. Sixty-five of a hundred and sixty runs did exactly that, and the
135
+ * staircase reported it as an error rate rather than as a resume bug — which is what a `stuckByStatus`
136
+ * breakdown was worth adding for.
137
+ *
138
+ * `resume.step` is no good either: the runtime derives it from *tool-call* parts, and this engine emits
139
+ * none. It would be zero forever and every resumed run would redo all its work — including its external
140
+ * action, which is precisely the duplicate this whole exercise exists to detect.
141
+ */
142
+ const stepPart = new RegExp(`^${input.run.id}-p\\d+$`);
143
+ const alreadyDone = (input.resume?.parts ?? []).filter((part) => stepPart.test(part.id)).length;
144
+ // Derived from the **run id**, not from the shared generator.
145
+ //
146
+ // A real bug the first time: `deps.random()` advances global state, so a resumed run drew *different*
147
+ // values than its first attempt — a run that had performed its external action could come back deciding it
148
+ // never does one, and a run that paused for approval could resume deciding to pause again, forever. A load
149
+ // test whose scenario is not stable across a resume cannot make any claim about resumption at all.
150
+ const fate = runFate(input.run.id);
151
+ const doesExternalAction = fate.a < traffic.externalActionRate;
152
+ const pausesForApproval = fate.b < traffic.approvalRate;
153
+ for (let step = alreadyDone; step < traffic.steps; step += 1) {
154
+ if (input.signal.isCancelled())
155
+ return;
156
+ if (deps.random() < traffic.rateLimitRate)
157
+ throw new SyntheticRateLimit();
158
+ if (deps.random() < traffic.providerTimeoutRate)
159
+ throw new SyntheticTimeout();
160
+ await sleep(traffic.modelLatencyMs);
161
+ yield {
162
+ type: "part.added",
163
+ part: {
164
+ id: `${input.run.id}-p${step}`,
165
+ type: "text",
166
+ schemaVersion: 1,
167
+ createdAt: new Date(now()).toISOString(),
168
+ text: `step ${step}`,
169
+ },
170
+ };
171
+ // The external effect on the *last* step, so a kill mid-run lands before it and recovery has to decide
172
+ // whether to redo it. Placing it on the first step would make the interesting case unreachable.
173
+ if (doesExternalAction && step === traffic.steps - 1) {
174
+ // Keyed on run and step, which is what the platform's own idempotency key would be derived from. A key
175
+ // including an attempt number would make every retry unique and the duplicate check vacuous.
176
+ deps.effects.perform(`${input.run.id}:${step}`, now());
177
+ }
178
+ // The approval is requested once, on the step *after* it is recorded, so `alreadyDone` on resume is past
179
+ // it. Without the marker part the resumed run reaches the same step, requests approval again, and pauses
180
+ // forever — which looks exactly like a platform that cannot resume.
181
+ if (pausesForApproval && step === Math.floor(traffic.steps / 2)) {
182
+ yield {
183
+ type: "part.added",
184
+ part: {
185
+ id: `${input.run.id}-approval-mark`,
186
+ type: "text",
187
+ schemaVersion: 1,
188
+ createdAt: new Date(now()).toISOString(),
189
+ text: "approval requested",
190
+ },
191
+ };
192
+ yield {
193
+ type: "approval.requested",
194
+ interactionId: asId(`${input.run.id}-a`),
195
+ toolName: "publish_post",
196
+ summary: "publish",
197
+ riskCategory: "external-write",
198
+ };
199
+ // Suspend. The harness decides the approval, which is what makes approval wait time measurable and
200
+ // what exercises the longest-lived state the platform holds.
201
+ return;
202
+ }
203
+ }
204
+ yield { type: "run.completed" };
205
+ },
206
+ };
207
+ };
208
+ //# sourceMappingURL=scenario.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * MCP egress policy — `docs/10-mcp-integration.md` → Egress and trust.
3
+ *
4
+ * A tenant's MCP endpoint is validated *before any handshake*, at registration and again at connect.
5
+ * HTTP endpoints are checked against scheme/host rules and blocked from reaching private/loopback
6
+ * addresses (SSRF defense); stdio commands are checked against an explicit allow-list. Anything not
7
+ * allowed is rejected — the safe default is deny.
8
+ */
9
+ import type { McpTransport } from "./index.js";
10
+ export type EgressPolicy = {
11
+ /** If set, only these HTTP hosts are permitted (allow-list). */
12
+ readonly allowedHttpHosts?: readonly string[];
13
+ /** Permitted URL schemes for HTTP transports. Defaults to `["https"]`. */
14
+ readonly allowedSchemes?: readonly string[];
15
+ /** Explicit allow-list of stdio commands. Stdio is denied unless a command matches. */
16
+ readonly allowedStdioCommands?: readonly string[];
17
+ /** Allow private/loopback addresses (dev only). Defaults to false. */
18
+ readonly allowPrivateNetworks?: boolean;
19
+ };
20
+ /** Normalize a host for policy checks: lowercase, strip IPv6 brackets and a trailing dot (`a.` ≡ `a`). */
21
+ export declare const normalizeHost: (host: string) => string;
22
+ /**
23
+ * Private/loopback/link-local detection for a host (name or IP literal). Blocks the obvious private
24
+ * ranges, and — importantly — every IPv6 literal by default: an IPv4-mapped form like
25
+ * `::ffff:169.254.169.254` otherwise slips a metadata address past a naive v4-only check. A specific
26
+ * public IPv6 endpoint must instead be permitted explicitly via `allowedHttpHosts`.
27
+ */
28
+ export declare const isPrivateHost: (host: string) => boolean;
29
+ /**
30
+ * Validate an endpoint against the policy. Throws `forbidden` when disallowed. For stdio the endpoint
31
+ * is the command; for HTTP transports it is a URL.
32
+ */
33
+ export declare const validateEndpoint: (policy: EgressPolicy, transport: McpTransport, endpoint: string) => void;
34
+ /**
35
+ * The HTTP half of the policy, on its own — #176.
36
+ *
37
+ * Extracted because the check is about **HTTP egress**, not about MCP: an agent tool that fetches a URL needs
38
+ * exactly these rules, and the alternatives were both bad. Calling `validateEndpoint(policy, "streamable-http",
39
+ * url)` would be passing a transport the caller does not have, and writing the checks again in the tool would be
40
+ * a second SSRF defence that drifts from the first — and the one that drifts is the one nobody re-reads.
41
+ *
42
+ * Everything here was already load-bearing for MCP. Stated again because it is now reachable from a tool the
43
+ * *model* chooses the argument for, which is a materially more hostile position than an operator-configured
44
+ * endpoint:
45
+ *
46
+ * - Credentials in userinfo are **refused**, not stripped.
47
+ * - Every IPv6 literal is denied by default, because `::ffff:169.254.169.254` slips a cloud metadata address
48
+ * past any v4-only check.
49
+ * - An explicit host allow-list is authoritative, so an operator can permit something they trust.
50
+ * - Absent an allow-list, private, loopback, link-local and `.internal`/`.local` targets are blocked.
51
+ */
52
+ export declare const validateHttpEgress: (policy: EgressPolicy, endpoint: string) => URL;
53
+ //# sourceMappingURL=egress.d.ts.map
@@ -0,0 +1,115 @@
1
+ /**
2
+ * MCP egress policy — `docs/10-mcp-integration.md` → Egress and trust.
3
+ *
4
+ * A tenant's MCP endpoint is validated *before any handshake*, at registration and again at connect.
5
+ * HTTP endpoints are checked against scheme/host rules and blocked from reaching private/loopback
6
+ * addresses (SSRF defense); stdio commands are checked against an explicit allow-list. Anything not
7
+ * allowed is rejected — the safe default is deny.
8
+ */
9
+ import { AgentPlatformError } from "../core/errors.js";
10
+ const forbidden = (message) => new AgentPlatformError({ code: "forbidden", message, retryable: false });
11
+ const PRIVATE_HOSTNAMES = new Set(["localhost", "ip6-localhost", "metadata.google.internal"]);
12
+ /** Normalize a host for policy checks: lowercase, strip IPv6 brackets and a trailing dot (`a.` ≡ `a`). */
13
+ export const normalizeHost = (host) => host.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
14
+ const isPrivateV4 = (h) => {
15
+ const v4 = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
16
+ if (!v4)
17
+ return false;
18
+ const [a, b] = [Number(v4[1]), Number(v4[2])];
19
+ if (a === 10 || a === 127 || a === 0)
20
+ return true;
21
+ if (a === 169 && b === 254)
22
+ return true; // link-local incl. cloud metadata 169.254.169.254
23
+ if (a === 172 && b >= 16 && b <= 31)
24
+ return true;
25
+ if (a === 192 && b === 168)
26
+ return true;
27
+ return false;
28
+ };
29
+ /**
30
+ * Private/loopback/link-local detection for a host (name or IP literal). Blocks the obvious private
31
+ * ranges, and — importantly — every IPv6 literal by default: an IPv4-mapped form like
32
+ * `::ffff:169.254.169.254` otherwise slips a metadata address past a naive v4-only check. A specific
33
+ * public IPv6 endpoint must instead be permitted explicitly via `allowedHttpHosts`.
34
+ */
35
+ export const isPrivateHost = (host) => {
36
+ const h = normalizeHost(host);
37
+ if (h.length === 0)
38
+ return true;
39
+ if (h === "localhost" || PRIVATE_HOSTNAMES.has(h) || h.endsWith(".local") || h.endsWith(".internal"))
40
+ return true;
41
+ if (h.includes(":"))
42
+ return true; // any IPv6 literal (incl. ::, ::1, ::ffff:… mapped) — deny by default
43
+ return isPrivateV4(h);
44
+ };
45
+ /**
46
+ * Validate an endpoint against the policy. Throws `forbidden` when disallowed. For stdio the endpoint
47
+ * is the command; for HTTP transports it is a URL.
48
+ */
49
+ export const validateEndpoint = (policy, transport, endpoint) => {
50
+ if (transport === "stdio") {
51
+ const allowed = policy.allowedStdioCommands ?? [];
52
+ const command = endpoint.trim().split(/\s+/)[0] ?? "";
53
+ if (!allowed.includes(command))
54
+ throw forbidden(`stdio command "${command}" is not on the egress allow-list`);
55
+ return;
56
+ }
57
+ validateHttpEgress(policy, endpoint);
58
+ };
59
+ /**
60
+ * The HTTP half of the policy, on its own — #176.
61
+ *
62
+ * Extracted because the check is about **HTTP egress**, not about MCP: an agent tool that fetches a URL needs
63
+ * exactly these rules, and the alternatives were both bad. Calling `validateEndpoint(policy, "streamable-http",
64
+ * url)` would be passing a transport the caller does not have, and writing the checks again in the tool would be
65
+ * a second SSRF defence that drifts from the first — and the one that drifts is the one nobody re-reads.
66
+ *
67
+ * Everything here was already load-bearing for MCP. Stated again because it is now reachable from a tool the
68
+ * *model* chooses the argument for, which is a materially more hostile position than an operator-configured
69
+ * endpoint:
70
+ *
71
+ * - Credentials in userinfo are **refused**, not stripped.
72
+ * - Every IPv6 literal is denied by default, because `::ffff:169.254.169.254` slips a cloud metadata address
73
+ * past any v4-only check.
74
+ * - An explicit host allow-list is authoritative, so an operator can permit something they trust.
75
+ * - Absent an allow-list, private, loopback, link-local and `.internal`/`.local` targets are blocked.
76
+ */
77
+ export const validateHttpEgress = (policy, endpoint) => {
78
+ let url;
79
+ try {
80
+ url = new URL(endpoint);
81
+ }
82
+ catch {
83
+ throw forbidden(`Invalid URL`);
84
+ }
85
+ /**
86
+ * A URL carrying userinfo is a **credential**, and it is refused (#145).
87
+ *
88
+ * `https://user:sk-live-abc@allowed.host` passed every check here: the scheme is permitted, the host is on the
89
+ * allow-list, and the endpoint was then stored verbatim in `mcp_connections.endpoint`. A secret in a database
90
+ * column — and in every log line, error message and support ticket that ever quotes the endpoint.
91
+ *
92
+ * The whole point of `McpAuth.credentialRef` is that a secret is *referenced*, never inlined. A URL is a second,
93
+ * unguarded way to inline one, which is why this is a refusal and not a strip: silently removing the userinfo
94
+ * would connect without the credential the operator believed they had configured, and the failure would look
95
+ * like the remote server rejecting them.
96
+ */
97
+ if (url.username !== "" || url.password !== "")
98
+ throw forbidden("MCP endpoint URL must not contain credentials in its userinfo; use McpAuth.credentialRef instead");
99
+ const schemes = policy.allowedSchemes ?? ["https"];
100
+ const scheme = url.protocol.replace(/:$/, "");
101
+ if (!schemes.includes(scheme))
102
+ throw forbidden(`scheme "${scheme}" is not permitted for MCP egress`);
103
+ const host = normalizeHost(url.hostname);
104
+ // An explicit host allow-list is authoritative — it lets an operator permit a specific internal or
105
+ // IPv6-literal host they trust. Without one, private/loopback/metadata targets are blocked.
106
+ if (policy.allowedHttpHosts) {
107
+ if (!policy.allowedHttpHosts.map(normalizeHost).includes(host))
108
+ throw forbidden(`host "${url.hostname}" is not on the egress allow-list`);
109
+ return url;
110
+ }
111
+ if (!policy.allowPrivateNetworks && isPrivateHost(url.hostname))
112
+ throw forbidden(`endpoint host "${url.hostname}" resolves to a private/loopback address`);
113
+ return url;
114
+ };
115
+ //# sourceMappingURL=egress.js.map