patchwork-os 0.2.0-beta.9.canary.92 → 1.1.0-beta.1

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 (739) hide show
  1. package/README.bridge.md +15 -16
  2. package/README.md +46 -458
  3. package/dist/activationMetrics.js +2 -3
  4. package/dist/activationMetrics.js.map +1 -1
  5. package/dist/activityLog.d.ts +1 -0
  6. package/dist/activityLog.js +8 -3
  7. package/dist/activityLog.js.map +1 -1
  8. package/dist/adapters/gemini.js +2 -2
  9. package/dist/adapters/gemini.js.map +1 -1
  10. package/dist/adapters/grok.js +6 -1
  11. package/dist/adapters/grok.js.map +1 -1
  12. package/dist/adapters/openai.js +4 -0
  13. package/dist/adapters/openai.js.map +1 -1
  14. package/dist/approvalHttp.d.ts +9 -0
  15. package/dist/approvalHttp.js +210 -171
  16. package/dist/approvalHttp.js.map +1 -1
  17. package/dist/approvalKpi.d.ts +102 -0
  18. package/dist/approvalKpi.js +282 -0
  19. package/dist/approvalKpi.js.map +1 -0
  20. package/dist/approvalQueue.d.ts +1 -1
  21. package/dist/approvalQueue.js.map +1 -1
  22. package/dist/automation.d.ts +45 -4
  23. package/dist/automation.js +123 -15
  24. package/dist/automation.js.map +1 -1
  25. package/dist/bridge.d.ts +10 -0
  26. package/dist/bridge.js +225 -46
  27. package/dist/bridge.js.map +1 -1
  28. package/dist/bridgeLockDiscovery.d.ts +16 -1
  29. package/dist/bridgeLockDiscovery.js +38 -4
  30. package/dist/bridgeLockDiscovery.js.map +1 -1
  31. package/dist/bridgeToolsRules.js +3 -18
  32. package/dist/bridgeToolsRules.js.map +1 -1
  33. package/dist/claudeAuthHttp.d.ts +25 -0
  34. package/dist/claudeAuthHttp.js +219 -0
  35. package/dist/claudeAuthHttp.js.map +1 -0
  36. package/dist/claudeDriver.js +4 -1
  37. package/dist/claudeDriver.js.map +1 -1
  38. package/dist/claudeOrchestrator.d.ts +15 -0
  39. package/dist/claudeOrchestrator.js +44 -0
  40. package/dist/claudeOrchestrator.js.map +1 -1
  41. package/dist/commands/connect.d.ts +47 -0
  42. package/dist/commands/connect.js +419 -0
  43. package/dist/commands/connect.js.map +1 -0
  44. package/dist/commands/install.js +3 -10
  45. package/dist/commands/install.js.map +1 -1
  46. package/dist/commands/launchd.d.ts +7 -0
  47. package/dist/commands/launchd.js +20 -2
  48. package/dist/commands/launchd.js.map +1 -1
  49. package/dist/commands/patchworkInit.d.ts +7 -0
  50. package/dist/commands/patchworkInit.js +26 -0
  51. package/dist/commands/patchworkInit.js.map +1 -1
  52. package/dist/commands/recipe.d.ts +59 -4
  53. package/dist/commands/recipe.js +206 -5
  54. package/dist/commands/recipe.js.map +1 -1
  55. package/dist/commands/recipeInstall.d.ts +9 -0
  56. package/dist/commands/recipeInstall.js +56 -3
  57. package/dist/commands/recipeInstall.js.map +1 -1
  58. package/dist/commands/task.d.ts +25 -0
  59. package/dist/commands/task.js +61 -41
  60. package/dist/commands/task.js.map +1 -1
  61. package/dist/commands/tokenEfficiency.d.ts +4 -0
  62. package/dist/commands/tokenEfficiency.js +23 -26
  63. package/dist/commands/tokenEfficiency.js.map +1 -1
  64. package/dist/commands/tracesExport.d.ts +15 -1
  65. package/dist/commands/tracesExport.js +39 -5
  66. package/dist/commands/tracesExport.js.map +1 -1
  67. package/dist/config.js +16 -0
  68. package/dist/config.js.map +1 -1
  69. package/dist/connectorRoutes.js +613 -62
  70. package/dist/connectorRoutes.js.map +1 -1
  71. package/dist/connectors/asana.js +9 -3
  72. package/dist/connectors/asana.js.map +1 -1
  73. package/dist/connectors/baseConnector.d.ts +16 -0
  74. package/dist/connectors/baseConnector.js +2 -0
  75. package/dist/connectors/baseConnector.js.map +1 -1
  76. package/dist/connectors/caldiy.js +34 -0
  77. package/dist/connectors/caldiy.js.map +1 -1
  78. package/dist/connectors/confluence.js +9 -2
  79. package/dist/connectors/confluence.js.map +1 -1
  80. package/dist/connectors/connectorActivity.d.ts +24 -0
  81. package/dist/connectors/connectorActivity.js +32 -0
  82. package/dist/connectors/connectorActivity.js.map +1 -0
  83. package/dist/connectors/connectorRedirectUri.js +22 -2
  84. package/dist/connectors/connectorRedirectUri.js.map +1 -1
  85. package/dist/connectors/connectorRegistry.d.ts +0 -3
  86. package/dist/connectors/connectorRegistry.js +8 -8
  87. package/dist/connectors/connectorRegistry.js.map +1 -1
  88. package/dist/connectors/datadog.js +8 -1
  89. package/dist/connectors/datadog.js.map +1 -1
  90. package/dist/connectors/discord.js +4 -3
  91. package/dist/connectors/discord.js.map +1 -1
  92. package/dist/connectors/elasticsearch.js +22 -0
  93. package/dist/connectors/elasticsearch.js.map +1 -1
  94. package/dist/connectors/github.d.ts +38 -0
  95. package/dist/connectors/github.js +91 -5
  96. package/dist/connectors/github.js.map +1 -1
  97. package/dist/connectors/gitlab.js +19 -16
  98. package/dist/connectors/gitlab.js.map +1 -1
  99. package/dist/connectors/gmail.d.ts +5 -0
  100. package/dist/connectors/gmail.js +53 -11
  101. package/dist/connectors/gmail.js.map +1 -1
  102. package/dist/connectors/googleCalendar.js +5 -4
  103. package/dist/connectors/googleCalendar.js.map +1 -1
  104. package/dist/connectors/googleDocs.js +7 -6
  105. package/dist/connectors/googleDocs.js.map +1 -1
  106. package/dist/connectors/googleDrive.js +15 -8
  107. package/dist/connectors/googleDrive.js.map +1 -1
  108. package/dist/connectors/grafana.js +39 -3
  109. package/dist/connectors/grafana.js.map +1 -1
  110. package/dist/connectors/jira.d.ts +1 -1
  111. package/dist/connectors/jira.js +19 -4
  112. package/dist/connectors/jira.js.map +1 -1
  113. package/dist/connectors/linear.js +2 -2
  114. package/dist/connectors/linear.js.map +1 -1
  115. package/dist/connectors/mcpClient.d.ts +29 -1
  116. package/dist/connectors/mcpClient.js +114 -44
  117. package/dist/connectors/mcpClient.js.map +1 -1
  118. package/dist/connectors/mcpOAuth.d.ts +1 -0
  119. package/dist/connectors/mcpOAuth.js +49 -4
  120. package/dist/connectors/mcpOAuth.js.map +1 -1
  121. package/dist/connectors/monday.js +5 -4
  122. package/dist/connectors/monday.js.map +1 -1
  123. package/dist/connectors/mongodb.js +32 -5
  124. package/dist/connectors/mongodb.js.map +1 -1
  125. package/dist/connectors/notion.d.ts +1 -1
  126. package/dist/connectors/notion.js +22 -6
  127. package/dist/connectors/notion.js.map +1 -1
  128. package/dist/connectors/oauthError.d.ts +17 -0
  129. package/dist/connectors/oauthError.js +50 -0
  130. package/dist/connectors/oauthError.js.map +1 -0
  131. package/dist/connectors/postgres.d.ts +7 -0
  132. package/dist/connectors/postgres.js +32 -2
  133. package/dist/connectors/postgres.js.map +1 -1
  134. package/dist/connectors/posthog.js +26 -0
  135. package/dist/connectors/posthog.js.map +1 -1
  136. package/dist/connectors/redis.d.ts +1 -0
  137. package/dist/connectors/redis.js +47 -20
  138. package/dist/connectors/redis.js.map +1 -1
  139. package/dist/connectors/salesforce.js +66 -5
  140. package/dist/connectors/salesforce.js.map +1 -1
  141. package/dist/connectors/sentry.js +2 -2
  142. package/dist/connectors/sentry.js.map +1 -1
  143. package/dist/connectors/slack.js +37 -14
  144. package/dist/connectors/slack.js.map +1 -1
  145. package/dist/connectors/snowflake.js +24 -0
  146. package/dist/connectors/snowflake.js.map +1 -1
  147. package/dist/connectors/stripe.js +9 -2
  148. package/dist/connectors/stripe.js.map +1 -1
  149. package/dist/connectors/supabase.js +34 -0
  150. package/dist/connectors/supabase.js.map +1 -1
  151. package/dist/connectors/tokenStorage.d.ts +20 -0
  152. package/dist/connectors/tokenStorage.js +196 -64
  153. package/dist/connectors/tokenStorage.js.map +1 -1
  154. package/dist/connectors/woocommerce.js +34 -0
  155. package/dist/connectors/woocommerce.js.map +1 -1
  156. package/dist/copilot/parseIntent.d.ts +58 -0
  157. package/dist/copilot/parseIntent.js +149 -0
  158. package/dist/copilot/parseIntent.js.map +1 -0
  159. package/dist/decisionReplay.d.ts +8 -6
  160. package/dist/decisionReplay.js +35 -11
  161. package/dist/decisionReplay.js.map +1 -1
  162. package/dist/decisionTraceLog.d.ts +9 -0
  163. package/dist/decisionTraceLog.js +6 -0
  164. package/dist/decisionTraceLog.js.map +1 -1
  165. package/dist/drivers/claude/api.js +49 -7
  166. package/dist/drivers/claude/api.js.map +1 -1
  167. package/dist/drivers/claude/envSanitizer.d.ts +9 -1
  168. package/dist/drivers/claude/envSanitizer.js +41 -3
  169. package/dist/drivers/claude/envSanitizer.js.map +1 -1
  170. package/dist/drivers/claude/streamParser.d.ts +13 -0
  171. package/dist/drivers/claude/streamParser.js.map +1 -1
  172. package/dist/drivers/claude/subprocess.js +131 -16
  173. package/dist/drivers/claude/subprocess.js.map +1 -1
  174. package/dist/drivers/claude/subprocessSettings.d.ts +1 -1
  175. package/dist/drivers/claude/subprocessSettings.js +18 -4
  176. package/dist/drivers/claude/subprocessSettings.js.map +1 -1
  177. package/dist/drivers/gemini/index.d.ts +13 -0
  178. package/dist/drivers/gemini/index.js +211 -67
  179. package/dist/drivers/gemini/index.js.map +1 -1
  180. package/dist/drivers/local/index.d.ts +2 -17
  181. package/dist/drivers/local/index.js +5 -92
  182. package/dist/drivers/local/index.js.map +1 -1
  183. package/dist/drivers/openai/index.js +69 -9
  184. package/dist/drivers/openai/index.js.map +1 -1
  185. package/dist/drivers/outputCap.d.ts +27 -0
  186. package/dist/drivers/outputCap.js +50 -0
  187. package/dist/drivers/outputCap.js.map +1 -0
  188. package/dist/embeddings/cosine.d.ts +23 -0
  189. package/dist/embeddings/cosine.js +59 -0
  190. package/dist/embeddings/cosine.js.map +1 -0
  191. package/dist/embeddings/index.d.ts +19 -0
  192. package/dist/embeddings/index.js +35 -0
  193. package/dist/embeddings/index.js.map +1 -0
  194. package/dist/embeddings/localEmbeddings.d.ts +29 -0
  195. package/dist/embeddings/localEmbeddings.js +103 -0
  196. package/dist/embeddings/localEmbeddings.js.map +1 -0
  197. package/dist/embeddings/localEndpoints.d.ts +24 -0
  198. package/dist/embeddings/localEndpoints.js +37 -0
  199. package/dist/embeddings/localEndpoints.js.map +1 -0
  200. package/dist/embeddings/types.d.ts +32 -0
  201. package/dist/embeddings/types.js +12 -0
  202. package/dist/embeddings/types.js.map +1 -0
  203. package/dist/featureFlags.d.ts +18 -0
  204. package/dist/featureFlags.js +32 -0
  205. package/dist/featureFlags.js.map +1 -1
  206. package/dist/fp/automationInterpreter.js +118 -39
  207. package/dist/fp/automationInterpreter.js.map +1 -1
  208. package/dist/fp/automationProgram.d.ts +10 -0
  209. package/dist/fp/automationProgram.js.map +1 -1
  210. package/dist/fp/automationState.d.ts +6 -0
  211. package/dist/fp/automationState.js +38 -0
  212. package/dist/fp/automationState.js.map +1 -1
  213. package/dist/fp/commandDescription.d.ts +6 -0
  214. package/dist/fp/commandDescription.js +18 -5
  215. package/dist/fp/commandDescription.js.map +1 -1
  216. package/dist/fp/interpreterContext.d.ts +16 -5
  217. package/dist/fp/interpreterContext.js +59 -3
  218. package/dist/fp/interpreterContext.js.map +1 -1
  219. package/dist/fp/policyParser.js +18 -8
  220. package/dist/fp/policyParser.js.map +1 -1
  221. package/dist/haltPushDispatch.d.ts +4 -0
  222. package/dist/haltPushDispatch.js +7 -22
  223. package/dist/haltPushDispatch.js.map +1 -1
  224. package/dist/inboxRoutes.js +2 -2
  225. package/dist/inboxRoutes.js.map +1 -1
  226. package/dist/index.js +672 -135
  227. package/dist/index.js.map +1 -1
  228. package/dist/installGuard.js +6 -0
  229. package/dist/installGuard.js.map +1 -1
  230. package/dist/localEndpointGuard.d.ts +25 -0
  231. package/dist/localEndpointGuard.js +101 -0
  232. package/dist/localEndpointGuard.js.map +1 -0
  233. package/dist/mcpRoutes.js +1 -1
  234. package/dist/mcpRoutes.js.map +1 -1
  235. package/dist/oauth.d.ts +13 -0
  236. package/dist/oauth.js +53 -9
  237. package/dist/oauth.js.map +1 -1
  238. package/dist/orchestrator/childBridgeClient.d.ts +7 -0
  239. package/dist/orchestrator/childBridgeClient.js +42 -1
  240. package/dist/orchestrator/childBridgeClient.js.map +1 -1
  241. package/dist/orchestrator/orchestratorBridge.d.ts +33 -0
  242. package/dist/orchestrator/orchestratorBridge.js +75 -9
  243. package/dist/orchestrator/orchestratorBridge.js.map +1 -1
  244. package/dist/patchworkConfig.d.ts +4 -0
  245. package/dist/patchworkConfig.js +33 -4
  246. package/dist/patchworkConfig.js.map +1 -1
  247. package/dist/probe.js +39 -5
  248. package/dist/probe.js.map +1 -1
  249. package/dist/recipeOrchestration.d.ts +59 -0
  250. package/dist/recipeOrchestration.js +314 -60
  251. package/dist/recipeOrchestration.js.map +1 -1
  252. package/dist/recipeRoutes.d.ts +93 -0
  253. package/dist/recipeRoutes.js +664 -27
  254. package/dist/recipeRoutes.js.map +1 -1
  255. package/dist/recipes/RecipeOrchestrator.d.ts +14 -0
  256. package/dist/recipes/RecipeOrchestrator.js +42 -2
  257. package/dist/recipes/RecipeOrchestrator.js.map +1 -1
  258. package/dist/recipes/agentExecutor.d.ts +51 -2
  259. package/dist/recipes/agentExecutor.js +86 -13
  260. package/dist/recipes/agentExecutor.js.map +1 -1
  261. package/dist/recipes/chainedRunner.d.ts +111 -3
  262. package/dist/recipes/chainedRunner.js +440 -45
  263. package/dist/recipes/chainedRunner.js.map +1 -1
  264. package/dist/recipes/compiler.js +42 -29
  265. package/dist/recipes/compiler.js.map +1 -1
  266. package/dist/recipes/connectorPreflight.js +31 -0
  267. package/dist/recipes/connectorPreflight.js.map +1 -1
  268. package/dist/recipes/dependencyGraph.d.ts +16 -0
  269. package/dist/recipes/dependencyGraph.js +37 -2
  270. package/dist/recipes/dependencyGraph.js.map +1 -1
  271. package/dist/recipes/eventTriggerPrograms.d.ts +36 -0
  272. package/dist/recipes/eventTriggerPrograms.js +158 -0
  273. package/dist/recipes/eventTriggerPrograms.js.map +1 -0
  274. package/dist/recipes/githubInstallSource.d.ts +9 -2
  275. package/dist/recipes/githubInstallSource.js +36 -6
  276. package/dist/recipes/githubInstallSource.js.map +1 -1
  277. package/dist/recipes/haltCategory.d.ts +10 -0
  278. package/dist/recipes/haltCategory.js +11 -1
  279. package/dist/recipes/haltCategory.js.map +1 -1
  280. package/dist/recipes/idempotencyKey.d.ts +30 -3
  281. package/dist/recipes/idempotencyKey.js +89 -5
  282. package/dist/recipes/idempotencyKey.js.map +1 -1
  283. package/dist/recipes/installer.js +12 -35
  284. package/dist/recipes/installer.js.map +1 -1
  285. package/dist/recipes/judgeVerdict.d.ts +11 -1
  286. package/dist/recipes/judgeVerdict.js +47 -7
  287. package/dist/recipes/judgeVerdict.js.map +1 -1
  288. package/dist/recipes/names.d.ts +5 -0
  289. package/dist/recipes/names.js +10 -5
  290. package/dist/recipes/names.js.map +1 -1
  291. package/dist/recipes/parser.js +74 -10
  292. package/dist/recipes/parser.js.map +1 -1
  293. package/dist/recipes/pricing/costRouter.d.ts +43 -0
  294. package/dist/recipes/pricing/costRouter.js +44 -0
  295. package/dist/recipes/pricing/costRouter.js.map +1 -0
  296. package/dist/recipes/pricing/priceTable.d.ts +76 -0
  297. package/dist/recipes/pricing/priceTable.js +144 -0
  298. package/dist/recipes/pricing/priceTable.js.map +1 -0
  299. package/dist/recipes/replayRun.js +5 -2
  300. package/dist/recipes/replayRun.js.map +1 -1
  301. package/dist/recipes/resolveRecipePath.js +15 -1
  302. package/dist/recipes/resolveRecipePath.js.map +1 -1
  303. package/dist/recipes/runBudget.d.ts +93 -32
  304. package/dist/recipes/runBudget.js +213 -49
  305. package/dist/recipes/runBudget.js.map +1 -1
  306. package/dist/recipes/runRegistry.d.ts +32 -0
  307. package/dist/recipes/runRegistry.js +54 -0
  308. package/dist/recipes/runRegistry.js.map +1 -0
  309. package/dist/recipes/scheduler.js +6 -2
  310. package/dist/recipes/scheduler.js.map +1 -1
  311. package/dist/recipes/schema.d.ts +59 -6
  312. package/dist/recipes/schemaGenerator.d.ts +9 -0
  313. package/dist/recipes/schemaGenerator.js +446 -77
  314. package/dist/recipes/schemaGenerator.js.map +1 -1
  315. package/dist/recipes/simulation/aggregateRunRisk.d.ts +46 -0
  316. package/dist/recipes/simulation/aggregateRunRisk.js +120 -0
  317. package/dist/recipes/simulation/aggregateRunRisk.js.map +1 -0
  318. package/dist/recipes/simulation/costProjector.d.ts +32 -0
  319. package/dist/recipes/simulation/costProjector.js +194 -0
  320. package/dist/recipes/simulation/costProjector.js.map +1 -0
  321. package/dist/recipes/simulation/sideEffects.d.ts +32 -0
  322. package/dist/recipes/simulation/sideEffects.js +62 -0
  323. package/dist/recipes/simulation/sideEffects.js.map +1 -0
  324. package/dist/recipes/simulation/simulate.d.ts +36 -0
  325. package/dist/recipes/simulation/simulate.js +264 -0
  326. package/dist/recipes/simulation/simulate.js.map +1 -0
  327. package/dist/recipes/simulation/simulateMockedRun.d.ts +52 -0
  328. package/dist/recipes/simulation/simulateMockedRun.js +84 -0
  329. package/dist/recipes/simulation/simulateMockedRun.js.map +1 -0
  330. package/dist/recipes/simulation/synthesizeMockedOutputs.d.ts +31 -0
  331. package/dist/recipes/simulation/synthesizeMockedOutputs.js +50 -0
  332. package/dist/recipes/simulation/synthesizeMockedOutputs.js.map +1 -0
  333. package/dist/recipes/simulation/types.d.ts +198 -0
  334. package/dist/recipes/simulation/types.js +30 -0
  335. package/dist/recipes/simulation/types.js.map +1 -0
  336. package/dist/recipes/stepObservation.d.ts +12 -0
  337. package/dist/recipes/stepObservation.js +21 -1
  338. package/dist/recipes/stepObservation.js.map +1 -1
  339. package/dist/recipes/toolRegistry.js +61 -24
  340. package/dist/recipes/toolRegistry.js.map +1 -1
  341. package/dist/recipes/tools/airtable.d.ts +15 -0
  342. package/dist/recipes/tools/airtable.js +240 -0
  343. package/dist/recipes/tools/airtable.js.map +1 -0
  344. package/dist/recipes/tools/caldiy.d.ts +13 -0
  345. package/dist/recipes/tools/caldiy.js +215 -0
  346. package/dist/recipes/tools/caldiy.js.map +1 -0
  347. package/dist/recipes/tools/circleci.d.ts +10 -0
  348. package/dist/recipes/tools/circleci.js +204 -0
  349. package/dist/recipes/tools/circleci.js.map +1 -0
  350. package/dist/recipes/tools/cloudflare.d.ts +13 -0
  351. package/dist/recipes/tools/cloudflare.js +211 -0
  352. package/dist/recipes/tools/cloudflare.js.map +1 -0
  353. package/dist/recipes/tools/confluence.js +11 -10
  354. package/dist/recipes/tools/confluence.js.map +1 -1
  355. package/dist/recipes/tools/datadog.js +13 -12
  356. package/dist/recipes/tools/datadog.js.map +1 -1
  357. package/dist/recipes/tools/docs.d.ts +18 -0
  358. package/dist/recipes/tools/docs.js +95 -0
  359. package/dist/recipes/tools/docs.js.map +1 -0
  360. package/dist/recipes/tools/elasticsearch.d.ts +11 -0
  361. package/dist/recipes/tools/elasticsearch.js +157 -0
  362. package/dist/recipes/tools/elasticsearch.js.map +1 -0
  363. package/dist/recipes/tools/fanOut.js +4 -4
  364. package/dist/recipes/tools/fanOut.js.map +1 -1
  365. package/dist/recipes/tools/figma.d.ts +12 -0
  366. package/dist/recipes/tools/figma.js +195 -0
  367. package/dist/recipes/tools/figma.js.map +1 -0
  368. package/dist/recipes/tools/file.js +43 -22
  369. package/dist/recipes/tools/file.js.map +1 -1
  370. package/dist/recipes/tools/github.js +162 -5
  371. package/dist/recipes/tools/github.js.map +1 -1
  372. package/dist/recipes/tools/gmail.js +45 -33
  373. package/dist/recipes/tools/gmail.js.map +1 -1
  374. package/dist/recipes/tools/grafana.d.ts +11 -0
  375. package/dist/recipes/tools/grafana.js +216 -0
  376. package/dist/recipes/tools/grafana.js.map +1 -0
  377. package/dist/recipes/tools/http.d.ts +1 -1
  378. package/dist/recipes/tools/http.js +10 -28
  379. package/dist/recipes/tools/http.js.map +1 -1
  380. package/dist/recipes/tools/hubspot.js +13 -12
  381. package/dist/recipes/tools/hubspot.js.map +1 -1
  382. package/dist/recipes/tools/index.d.ts +28 -0
  383. package/dist/recipes/tools/index.js +28 -0
  384. package/dist/recipes/tools/index.js.map +1 -1
  385. package/dist/recipes/tools/intercom.js +11 -10
  386. package/dist/recipes/tools/intercom.js.map +1 -1
  387. package/dist/recipes/tools/jira.js +13 -0
  388. package/dist/recipes/tools/jira.js.map +1 -1
  389. package/dist/recipes/tools/meetingNotes.js +2 -2
  390. package/dist/recipes/tools/monday.d.ts +22 -0
  391. package/dist/recipes/tools/monday.js +242 -0
  392. package/dist/recipes/tools/monday.js.map +1 -0
  393. package/dist/recipes/tools/mongodb.d.ts +19 -0
  394. package/dist/recipes/tools/mongodb.js +194 -0
  395. package/dist/recipes/tools/mongodb.js.map +1 -0
  396. package/dist/recipes/tools/notion.js +11 -10
  397. package/dist/recipes/tools/notion.js.map +1 -1
  398. package/dist/recipes/tools/obsidian.d.ts +15 -0
  399. package/dist/recipes/tools/obsidian.js +173 -0
  400. package/dist/recipes/tools/obsidian.js.map +1 -0
  401. package/dist/recipes/tools/outcomes.d.ts +15 -0
  402. package/dist/recipes/tools/outcomes.js +111 -0
  403. package/dist/recipes/tools/outcomes.js.map +1 -0
  404. package/dist/recipes/tools/paystack.d.ts +11 -0
  405. package/dist/recipes/tools/paystack.js +212 -0
  406. package/dist/recipes/tools/paystack.js.map +1 -0
  407. package/dist/recipes/tools/pipedrive.d.ts +16 -0
  408. package/dist/recipes/tools/pipedrive.js +234 -0
  409. package/dist/recipes/tools/pipedrive.js.map +1 -0
  410. package/dist/recipes/tools/postgres.d.ts +15 -0
  411. package/dist/recipes/tools/postgres.js +186 -0
  412. package/dist/recipes/tools/postgres.js.map +1 -0
  413. package/dist/recipes/tools/posthog.d.ts +16 -0
  414. package/dist/recipes/tools/posthog.js +219 -0
  415. package/dist/recipes/tools/posthog.js.map +1 -0
  416. package/dist/recipes/tools/redis.d.ts +9 -0
  417. package/dist/recipes/tools/redis.js +141 -0
  418. package/dist/recipes/tools/redis.js.map +1 -0
  419. package/dist/recipes/tools/resend.d.ts +8 -0
  420. package/dist/recipes/tools/resend.js +153 -0
  421. package/dist/recipes/tools/resend.js.map +1 -0
  422. package/dist/recipes/tools/salesforce.d.ts +16 -0
  423. package/dist/recipes/tools/salesforce.js +184 -0
  424. package/dist/recipes/tools/salesforce.js.map +1 -0
  425. package/dist/recipes/tools/sendgrid.d.ts +9 -0
  426. package/dist/recipes/tools/sendgrid.js +175 -0
  427. package/dist/recipes/tools/sendgrid.js.map +1 -0
  428. package/dist/recipes/tools/shopify.d.ts +16 -0
  429. package/dist/recipes/tools/shopify.js +266 -0
  430. package/dist/recipes/tools/shopify.js.map +1 -0
  431. package/dist/recipes/tools/snowflake.d.ts +16 -0
  432. package/dist/recipes/tools/snowflake.js +173 -0
  433. package/dist/recipes/tools/snowflake.js.map +1 -0
  434. package/dist/recipes/tools/stripe.js +13 -12
  435. package/dist/recipes/tools/stripe.js.map +1 -1
  436. package/dist/recipes/tools/supabase.d.ts +9 -0
  437. package/dist/recipes/tools/supabase.js +133 -0
  438. package/dist/recipes/tools/supabase.js.map +1 -0
  439. package/dist/recipes/tools/todoist.d.ts +15 -0
  440. package/dist/recipes/tools/todoist.js +228 -0
  441. package/dist/recipes/tools/todoist.js.map +1 -0
  442. package/dist/recipes/tools/twilio.d.ts +11 -0
  443. package/dist/recipes/tools/twilio.js +180 -0
  444. package/dist/recipes/tools/twilio.js.map +1 -0
  445. package/dist/recipes/tools/vercel.d.ts +9 -0
  446. package/dist/recipes/tools/vercel.js +146 -0
  447. package/dist/recipes/tools/vercel.js.map +1 -0
  448. package/dist/recipes/tools/webflow.d.ts +11 -0
  449. package/dist/recipes/tools/webflow.js +244 -0
  450. package/dist/recipes/tools/webflow.js.map +1 -0
  451. package/dist/recipes/tools/woocommerce.d.ts +18 -0
  452. package/dist/recipes/tools/woocommerce.js +260 -0
  453. package/dist/recipes/tools/woocommerce.js.map +1 -0
  454. package/dist/recipes/tools/wrapConnectorExecute.d.ts +25 -0
  455. package/dist/recipes/tools/wrapConnectorExecute.js +37 -0
  456. package/dist/recipes/tools/wrapConnectorExecute.js.map +1 -0
  457. package/dist/recipes/tools/zendesk.js +11 -10
  458. package/dist/recipes/tools/zendesk.js.map +1 -1
  459. package/dist/recipes/triggerVars.d.ts +15 -0
  460. package/dist/recipes/triggerVars.js +57 -0
  461. package/dist/recipes/triggerVars.js.map +1 -0
  462. package/dist/recipes/validation.d.ts +1 -1
  463. package/dist/recipes/validation.js +536 -12
  464. package/dist/recipes/validation.js.map +1 -1
  465. package/dist/recipes/yamlPositions.d.ts +1 -1
  466. package/dist/recipes/yamlPositions.js +7 -2
  467. package/dist/recipes/yamlPositions.js.map +1 -1
  468. package/dist/recipes/yamlRunner.d.ts +216 -4
  469. package/dist/recipes/yamlRunner.js +1030 -96
  470. package/dist/recipes/yamlRunner.js.map +1 -1
  471. package/dist/recipesHttp.d.ts +5 -0
  472. package/dist/recipesHttp.js +68 -6
  473. package/dist/recipesHttp.js.map +1 -1
  474. package/dist/resources.js +14 -1
  475. package/dist/resources.js.map +1 -1
  476. package/dist/riskSignals.d.ts +42 -0
  477. package/dist/riskSignals.js +197 -0
  478. package/dist/riskSignals.js.map +1 -0
  479. package/dist/riskTier.d.ts +9 -0
  480. package/dist/riskTier.js +48 -0
  481. package/dist/riskTier.js.map +1 -1
  482. package/dist/runLog.d.ts +53 -0
  483. package/dist/runLog.js +75 -16
  484. package/dist/runLog.js.map +1 -1
  485. package/dist/schemas/recipe.v1.json +360 -16
  486. package/dist/server.d.ts +60 -0
  487. package/dist/server.js +192 -31
  488. package/dist/server.js.map +1 -1
  489. package/dist/sessionCheckpoint.js +4 -1
  490. package/dist/sessionCheckpoint.js.map +1 -1
  491. package/dist/sessionDetail.d.ts +43 -0
  492. package/dist/sessionDetail.js +59 -0
  493. package/dist/sessionDetail.js.map +1 -0
  494. package/dist/settingsEnv.d.ts +47 -0
  495. package/dist/settingsEnv.js +88 -0
  496. package/dist/settingsEnv.js.map +1 -0
  497. package/dist/streamableHttp.js +44 -10
  498. package/dist/streamableHttp.js.map +1 -1
  499. package/dist/ta/backtest/abnday.d.ts +53 -0
  500. package/dist/ta/backtest/abnday.js +123 -0
  501. package/dist/ta/backtest/abnday.js.map +1 -0
  502. package/dist/ta/backtest/directional.d.ts +48 -0
  503. package/dist/ta/backtest/directional.js +103 -0
  504. package/dist/ta/backtest/directional.js.map +1 -0
  505. package/dist/ta/backtest/fundx.d.ts +67 -0
  506. package/dist/ta/backtest/fundx.js +264 -0
  507. package/dist/ta/backtest/fundx.js.map +1 -0
  508. package/dist/ta/backtest/fvg.d.ts +62 -0
  509. package/dist/ta/backtest/fvg.js +236 -0
  510. package/dist/ta/backtest/fvg.js.map +1 -0
  511. package/dist/ta/backtest/ichimoku.d.ts +41 -0
  512. package/dist/ta/backtest/ichimoku.js +146 -0
  513. package/dist/ta/backtest/ichimoku.js.map +1 -0
  514. package/dist/ta/backtest/ingest.d.ts +20 -0
  515. package/dist/ta/backtest/ingest.js +104 -0
  516. package/dist/ta/backtest/ingest.js.map +1 -0
  517. package/dist/ta/backtest/msb.d.ts +49 -0
  518. package/dist/ta/backtest/msb.js +162 -0
  519. package/dist/ta/backtest/msb.js.map +1 -0
  520. package/dist/ta/backtest/scoring.d.ts +33 -0
  521. package/dist/ta/backtest/scoring.js +82 -0
  522. package/dist/ta/backtest/scoring.js.map +1 -0
  523. package/dist/ta/backtest/summary.d.ts +20 -0
  524. package/dist/ta/backtest/summary.js +42 -0
  525. package/dist/ta/backtest/summary.js.map +1 -0
  526. package/dist/ta/backtest/tsmom.d.ts +72 -0
  527. package/dist/ta/backtest/tsmom.js +202 -0
  528. package/dist/ta/backtest/tsmom.js.map +1 -0
  529. package/dist/ta/backtest/volsq.d.ts +34 -0
  530. package/dist/ta/backtest/volsq.js +154 -0
  531. package/dist/ta/backtest/volsq.js.map +1 -0
  532. package/dist/ta/backtest/walkForward.d.ts +45 -0
  533. package/dist/ta/backtest/walkForward.js +136 -0
  534. package/dist/ta/backtest/walkForward.js.map +1 -0
  535. package/dist/ta/cycles.d.ts +15 -0
  536. package/dist/ta/cycles.js +31 -0
  537. package/dist/ta/cycles.js.map +1 -0
  538. package/dist/ta/desk/accrualEmitter.d.ts +59 -0
  539. package/dist/ta/desk/accrualEmitter.js +136 -0
  540. package/dist/ta/desk/accrualEmitter.js.map +1 -0
  541. package/dist/ta/desk/cellBacktest.d.ts +179 -0
  542. package/dist/ta/desk/cellBacktest.js +514 -0
  543. package/dist/ta/desk/cellBacktest.js.map +1 -0
  544. package/dist/ta/desk/cells/wpMaRejection.d.ts +49 -0
  545. package/dist/ta/desk/cells/wpMaRejection.js +141 -0
  546. package/dist/ta/desk/cells/wpMaRejection.js.map +1 -0
  547. package/dist/ta/desk/cells/wpVolumeClimax.d.ts +57 -0
  548. package/dist/ta/desk/cells/wpVolumeClimax.js +193 -0
  549. package/dist/ta/desk/cells/wpVolumeClimax.js.map +1 -0
  550. package/dist/ta/desk/collectors.d.ts +18 -0
  551. package/dist/ta/desk/collectors.js +450 -0
  552. package/dist/ta/desk/collectors.js.map +1 -0
  553. package/dist/ta/desk/contract.d.ts +51 -0
  554. package/dist/ta/desk/contract.js +261 -0
  555. package/dist/ta/desk/contract.js.map +1 -0
  556. package/dist/ta/desk/deskLedger.d.ts +36 -0
  557. package/dist/ta/desk/deskLedger.js +239 -0
  558. package/dist/ta/desk/deskLedger.js.map +1 -0
  559. package/dist/ta/desk/liqTape.d.ts +37 -0
  560. package/dist/ta/desk/liqTape.js +99 -0
  561. package/dist/ta/desk/liqTape.js.map +1 -0
  562. package/dist/ta/desk/post.d.ts +20 -0
  563. package/dist/ta/desk/post.js +65 -0
  564. package/dist/ta/desk/post.js.map +1 -0
  565. package/dist/ta/desk/render.d.ts +25 -0
  566. package/dist/ta/desk/render.js +398 -0
  567. package/dist/ta/desk/render.js.map +1 -0
  568. package/dist/ta/desk/surfaces.d.ts +216 -0
  569. package/dist/ta/desk/surfaces.js +472 -0
  570. package/dist/ta/desk/surfaces.js.map +1 -0
  571. package/dist/ta/desk/types.d.ts +174 -0
  572. package/dist/ta/desk/types.js +104 -0
  573. package/dist/ta/desk/types.js.map +1 -0
  574. package/dist/ta/index.d.ts +23 -0
  575. package/dist/ta/index.js +54 -0
  576. package/dist/ta/index.js.map +1 -0
  577. package/dist/ta/ledger.d.ts +44 -0
  578. package/dist/ta/ledger.js +95 -0
  579. package/dist/ta/ledger.js.map +1 -0
  580. package/dist/ta/levels.d.ts +21 -0
  581. package/dist/ta/levels.js +41 -0
  582. package/dist/ta/levels.js.map +1 -0
  583. package/dist/ta/swings.d.ts +30 -0
  584. package/dist/ta/swings.js +83 -0
  585. package/dist/ta/swings.js.map +1 -0
  586. package/dist/ta/types.d.ts +81 -0
  587. package/dist/ta/types.js +20 -0
  588. package/dist/ta/types.js.map +1 -0
  589. package/dist/telemetry.js +20 -9
  590. package/dist/telemetry.js.map +1 -1
  591. package/dist/tokenUsageTracker.d.ts +2 -1
  592. package/dist/tokenUsageTracker.js +38 -20
  593. package/dist/tokenUsageTracker.js.map +1 -1
  594. package/dist/tools/contextBundle.js +13 -9
  595. package/dist/tools/contextBundle.js.map +1 -1
  596. package/dist/tools/ctxGetTaskContext.js +8 -3
  597. package/dist/tools/ctxGetTaskContext.js.map +1 -1
  598. package/dist/tools/ctxQueryTraces.d.ts +18 -1
  599. package/dist/tools/ctxQueryTraces.js +176 -27
  600. package/dist/tools/ctxQueryTraces.js.map +1 -1
  601. package/dist/tools/enrichCommit.js +5 -2
  602. package/dist/tools/enrichCommit.js.map +1 -1
  603. package/dist/tools/getDependencyTree.js +2 -2
  604. package/dist/tools/getDependencyTree.js.map +1 -1
  605. package/dist/tools/getDiagnostics.js +3 -0
  606. package/dist/tools/getDiagnostics.js.map +1 -1
  607. package/dist/tools/getDocumentSymbols.js +5 -1
  608. package/dist/tools/getDocumentSymbols.js.map +1 -1
  609. package/dist/tools/getGitHotspots.js +12 -1
  610. package/dist/tools/getGitHotspots.js.map +1 -1
  611. package/dist/tools/getGitLog.js +5 -2
  612. package/dist/tools/getGitLog.js.map +1 -1
  613. package/dist/tools/getGitStatus.js +6 -1
  614. package/dist/tools/getGitStatus.js.map +1 -1
  615. package/dist/tools/getProjectContext.js +3 -1
  616. package/dist/tools/getProjectContext.js.map +1 -1
  617. package/dist/tools/getToolCapabilities.js +39 -4
  618. package/dist/tools/getToolCapabilities.js.map +1 -1
  619. package/dist/tools/gitWrite.js +12 -3
  620. package/dist/tools/gitWrite.js.map +1 -1
  621. package/dist/tools/github/composite.d.ts +1 -1
  622. package/dist/tools/github/composite.js +7 -7
  623. package/dist/tools/github/composite.js.map +1 -1
  624. package/dist/tools/github/pr.d.ts +6 -6
  625. package/dist/tools/github/pr.js +18 -11
  626. package/dist/tools/github/pr.js.map +1 -1
  627. package/dist/tools/httpClient.js +55 -3
  628. package/dist/tools/httpClient.js.map +1 -1
  629. package/dist/tools/index.d.ts +2 -1
  630. package/dist/tools/index.js +19 -12
  631. package/dist/tools/index.js.map +1 -1
  632. package/dist/tools/lsp.d.ts +6 -0
  633. package/dist/tools/lsp.js +81 -12
  634. package/dist/tools/lsp.js.map +1 -1
  635. package/dist/tools/previewEdit.js +16 -1
  636. package/dist/tools/previewEdit.js.map +1 -1
  637. package/dist/tools/resumeClaudeTask.js +17 -0
  638. package/dist/tools/resumeClaudeTask.js.map +1 -1
  639. package/dist/tools/searchAndReplace.js +9 -5
  640. package/dist/tools/searchAndReplace.js.map +1 -1
  641. package/dist/tools/searchTools.d.ts +26 -0
  642. package/dist/tools/searchTools.js +42 -2
  643. package/dist/tools/searchTools.js.map +1 -1
  644. package/dist/tools/terminal.js +23 -51
  645. package/dist/tools/terminal.js.map +1 -1
  646. package/dist/tools/transaction.d.ts +9 -1
  647. package/dist/tools/transaction.js +59 -4
  648. package/dist/tools/transaction.js.map +1 -1
  649. package/dist/tools/utils.d.ts +16 -0
  650. package/dist/tools/utils.js +77 -7
  651. package/dist/tools/utils.js.map +1 -1
  652. package/dist/transport.d.ts +31 -0
  653. package/dist/transport.js +117 -14
  654. package/dist/transport.js.map +1 -1
  655. package/dist/wireHaltPushDispatch.js +12 -0
  656. package/dist/wireHaltPushDispatch.js.map +1 -1
  657. package/dist/workerGateDecisionLog.d.ts +141 -0
  658. package/dist/workerGateDecisionLog.js +361 -0
  659. package/dist/workerGateDecisionLog.js.map +1 -0
  660. package/dist/workers/actionClass.d.ts +38 -0
  661. package/dist/workers/actionClass.js +155 -0
  662. package/dist/workers/actionClass.js.map +1 -0
  663. package/dist/workers/backtest.d.ts +68 -0
  664. package/dist/workers/backtest.js +113 -0
  665. package/dist/workers/backtest.js.map +1 -0
  666. package/dist/workers/contextRisk.d.ts +35 -0
  667. package/dist/workers/contextRisk.js +20 -0
  668. package/dist/workers/contextRisk.js.map +1 -0
  669. package/dist/workers/contextRiskScorer.d.ts +48 -0
  670. package/dist/workers/contextRiskScorer.js +94 -0
  671. package/dist/workers/contextRiskScorer.js.map +1 -0
  672. package/dist/workers/graduation.d.ts +61 -0
  673. package/dist/workers/graduation.js +112 -0
  674. package/dist/workers/graduation.js.map +1 -0
  675. package/dist/workers/outcomeStore.d.ts +98 -0
  676. package/dist/workers/outcomeStore.js +146 -0
  677. package/dist/workers/outcomeStore.js.map +1 -0
  678. package/dist/workers/outcomesCli.d.ts +34 -0
  679. package/dist/workers/outcomesCli.js +91 -0
  680. package/dist/workers/outcomesCli.js.map +1 -0
  681. package/dist/workers/runWorkerShadow.d.ts +90 -0
  682. package/dist/workers/runWorkerShadow.js +323 -0
  683. package/dist/workers/runWorkerShadow.js.map +1 -0
  684. package/dist/workers/shadowGate.d.ts +30 -0
  685. package/dist/workers/shadowGate.js +58 -0
  686. package/dist/workers/shadowGate.js.map +1 -0
  687. package/dist/workers/shadowObserver.d.ts +165 -0
  688. package/dist/workers/shadowObserver.js +203 -0
  689. package/dist/workers/shadowObserver.js.map +1 -0
  690. package/dist/workers/shadowReport.d.ts +16 -0
  691. package/dist/workers/shadowReport.js +61 -0
  692. package/dist/workers/shadowReport.js.map +1 -0
  693. package/dist/workers/shadowRun.d.ts +38 -0
  694. package/dist/workers/shadowRun.js +44 -0
  695. package/dist/workers/shadowRun.js.map +1 -0
  696. package/dist/workers/trustLevel.d.ts +82 -0
  697. package/dist/workers/trustLevel.js +100 -0
  698. package/dist/workers/trustLevel.js.map +1 -0
  699. package/dist/workers/worker.d.ts +49 -0
  700. package/dist/workers/worker.js +88 -0
  701. package/dist/workers/worker.js.map +1 -0
  702. package/dist/workers/workerGate.d.ts +101 -0
  703. package/dist/workers/workerGate.js +210 -0
  704. package/dist/workers/workerGate.js.map +1 -0
  705. package/dist/workers/workerLevelStore.d.ts +33 -0
  706. package/dist/workers/workerLevelStore.js +87 -0
  707. package/dist/workers/workerLevelStore.js.map +1 -0
  708. package/dist/workers/workerLoader.d.ts +7 -0
  709. package/dist/workers/workerLoader.js +31 -0
  710. package/dist/workers/workerLoader.js.map +1 -0
  711. package/dist/writeFileAtomic.js +40 -13
  712. package/dist/writeFileAtomic.js.map +1 -1
  713. package/package.json +14 -6
  714. package/scripts/mcp-stdio-shim.cjs +247 -87
  715. package/templates/CLAUDE.bridge.md +1 -1
  716. package/templates/recipes/dependency-bump.yaml +33 -0
  717. package/templates/recipes/incident-to-pr.yaml +187 -0
  718. package/templates/recipes/outcome-ingester.yaml +53 -0
  719. package/templates/recipes/release-notes.yaml +30 -0
  720. package/templates/recipes/triage-failing-tests-autofile.yaml +152 -0
  721. package/templates/recipes/triage-failing-tests.yaml +38 -0
  722. package/templates/workers/dependency-upkeep.worker.yaml +26 -0
  723. package/templates/workers/release-notes.worker.yaml +17 -0
  724. package/templates/workers/test-guardian.worker.yaml +24 -0
  725. package/deploy/README.md +0 -172
  726. package/deploy/bootstrap-new-vps.sh +0 -364
  727. package/deploy/bootstrap-vps.sh +0 -192
  728. package/deploy/claude-ide-bridge.service.template +0 -67
  729. package/deploy/claude-ide-bridge@.service +0 -31
  730. package/deploy/deploy-dashboard.sh +0 -198
  731. package/deploy/deploy-landing.sh +0 -136
  732. package/deploy/ecosystem.config.js.example +0 -36
  733. package/deploy/install-vps-service.sh +0 -240
  734. package/deploy/macos/README.md +0 -153
  735. package/deploy/macos/com.patchwork.bridge.plist.template +0 -54
  736. package/deploy/macos/com.patchwork.tunnel.plist.template +0 -76
  737. package/deploy/macos/install-mac-bridge.sh +0 -244
  738. package/deploy/macos/uninstall-mac-bridge.sh +0 -22
  739. package/deploy/nginx-claude-bridge.conf.template +0 -129
@@ -29,11 +29,32 @@
29
29
  import os from "node:os";
30
30
  import path from "node:path";
31
31
  import { computeSummary as computeActivationSummary, loadMetrics as loadActivationMetrics, } from "./activationMetrics.js";
32
+ import { getApprovalQueue } from "./approvalQueue.js";
33
+ import { buildCopilotReply, parseCopilotIntent, } from "./copilot/parseIntent.js";
32
34
  import { isWriteKillSwitchActive } from "./featureFlags.js";
33
35
  import { consumeToken, refillBucket, } from "./fp/tokenBucket.js";
34
36
  import { respondIfUnknownBodyKeys } from "./httpBodyValidation.js";
35
37
  import { respond500 } from "./httpErrorResponse.js";
38
+ import { cancelRun } from "./recipes/runRegistry.js";
39
+ import { invalidateRecipesCache } from "./recipesHttp.js";
36
40
  import { validateSafeUrl } from "./ssrfGuard.js";
41
+ /**
42
+ * Sanitize error messages from storage operations before including them in
43
+ * HTTP responses. Node.js filesystem errors (ENOENT, EACCES, etc.) often
44
+ * contain absolute paths (e.g. "ENOENT: no such file or directory, open
45
+ * '/home/user/.patchwork/recipes/foo.yaml'"). These must not be returned
46
+ * verbatim to clients. Returns "Storage error" for any message that looks
47
+ * like a filesystem error (identified by Node.js errno code prefix);
48
+ * otherwise returns the message unchanged.
49
+ */
50
+ function sanitizeStorageError(msg) {
51
+ // Node.js filesystem errors always start with an errno code such as
52
+ // "ENOENT:", "EACCES:", "EPERM:", "EBUSY:", "EISDIR:", etc.
53
+ if (/^E[A-Z]+:/.test(msg)) {
54
+ return "Storage error";
55
+ }
56
+ return msg;
57
+ }
37
58
  // 5-minute cache of the public template registry from the patchworkos/recipes
38
59
  // GitHub repo. Process-wide; hoisted out of Server class state.
39
60
  // Sentinel `false` marks a negative-cache entry (fetch failed) — distinct from
@@ -41,20 +62,86 @@ import { validateSafeUrl } from "./ssrfGuard.js";
41
62
  let templatesCache = null;
42
63
  let templatesCacheTs = 0;
43
64
  /**
44
- * #605: shape-validate the upstream templates payload before caching.
65
+ * Canonical trust enums the dashboard understands. Anything outside these
66
+ * sets must NOT survive into the cached registry — an out-of-enum
67
+ * `risk_level` evaluates falsy downstream (silently reading as "safe") and
68
+ * `RISK_PILL_CLASS[risk_level]` resolves to `undefined`.
69
+ */
70
+ const TEMPLATE_RISK_LEVELS = new Set(["low", "medium", "high"]);
71
+ const TEMPLATE_APPROVAL_BEHAVIORS = new Set([
72
+ "always_ask",
73
+ "ask_on_novel",
74
+ "auto_approve",
75
+ ]);
76
+ /**
77
+ * #605 / B3-A: shape-validate AND sanitize the upstream templates payload
78
+ * before caching.
79
+ *
45
80
  * The minimal contract used by the dashboard marketplace is `{recipes:
46
- * Array}` (other fields are optional). Anything else (an error page
47
- * JSON, a tampered file with a flipped key, a future GitHub schema
48
- * change) is rejected so we don't serve garbage for 5 minutes to every
49
- * dashboard client.
81
+ * Array}` (other fields are optional). A bare array is accepted too (legacy).
82
+ * Anything that fails that contract (an error page JSON, a tampered file with
83
+ * a flipped top-level key, a future GitHub schema change) is rejected so we
84
+ * don't serve garbage for 5 minutes to every dashboard client.
85
+ *
86
+ * B3-A SECURITY change: rather than reject the WHOLE payload when a single
87
+ * entry carries a malformed trust field, we SANITIZE each entry IN PLACE so
88
+ * one bad entry can't poison the registry for every client. For each entry:
89
+ *
90
+ * - `risk_level` not in {low,medium,high} → stripped (set undefined)
91
+ * - `approval_behavior` not in the canonical set → stripped (set undefined)
92
+ * - `downloads` not a finite number → coerced if it parses to a
93
+ * finite number, else dropped
94
+ *
95
+ * Conservative by design: stripping an out-of-enum `risk_level` means the
96
+ * dashboard treats trust as UNKNOWN (which the default-deny gate renders as
97
+ * elevated), never silently as "low"/"safe".
50
98
  */
51
- function isWellFormedTemplatesPayload(raw) {
99
+ export function isWellFormedTemplatesPayload(raw) {
52
100
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
53
- // Some legacy callers used a bare array; accept that too.
54
- return Array.isArray(raw);
101
+ // Some legacy callers used a bare array; accept (and sanitize) that too.
102
+ if (Array.isArray(raw)) {
103
+ for (const entry of raw)
104
+ sanitizeTemplateEntry(entry);
105
+ return true;
106
+ }
107
+ return false;
55
108
  }
56
109
  const r = raw;
57
- return Array.isArray(r.recipes);
110
+ if (!Array.isArray(r.recipes))
111
+ return false;
112
+ for (const entry of r.recipes)
113
+ sanitizeTemplateEntry(entry);
114
+ return true;
115
+ }
116
+ /**
117
+ * Strip/coerce untrusted trust fields on a single registry entry, mutating it
118
+ * in place. Non-object entries are left untouched (the dashboard tolerates
119
+ * them; they simply won't render trust pills). Kept conservative: we only
120
+ * touch the three fields with a downstream safety impact.
121
+ */
122
+ function sanitizeTemplateEntry(entry) {
123
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
124
+ return;
125
+ }
126
+ const e = entry;
127
+ // risk_level: must be one of the canonical levels, else undefined.
128
+ if ("risk_level" in e && !TEMPLATE_RISK_LEVELS.has(e.risk_level)) {
129
+ e.risk_level = undefined;
130
+ }
131
+ // approval_behavior: must be one of the canonical behaviors, else undefined.
132
+ if ("approval_behavior" in e &&
133
+ !TEMPLATE_APPROVAL_BEHAVIORS.has(e.approval_behavior)) {
134
+ e.approval_behavior = undefined;
135
+ }
136
+ // downloads: must be a finite number. Coerce a numeric string; otherwise
137
+ // drop. A non-numeric value (e.g. "abc") must never pass through as-is.
138
+ if ("downloads" in e && typeof e.downloads !== "number") {
139
+ const n = Number(e.downloads);
140
+ e.downloads = Number.isFinite(n) ? n : undefined;
141
+ }
142
+ else if (typeof e.downloads === "number" && !Number.isFinite(e.downloads)) {
143
+ e.downloads = undefined;
144
+ }
58
145
  }
59
146
  /**
60
147
  * Per-process token bucket guarding `/recipes/generate`. Every call to the
@@ -117,7 +204,7 @@ export function _resetRepairRateLimitForTests() {
117
204
  // per R3 amendment 4 / I-3, prevents JSON.stringify smuggling
118
205
  // a `..` segment into a coerced value at render time).
119
206
  const VARS_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/;
120
- const VARS_VALUE_RE = /^[\w\-. :+@,]+$/u;
207
+ const VARS_VALUE_RE = /^(?!.*\.\.)([\w\-. :+@,]+)$/u;
121
208
  /** Validate the HTTP-supplied `vars` object. Returns null on success. */
122
209
  export function validateRecipeVars(vars) {
123
210
  if (vars == null)
@@ -188,6 +275,28 @@ export const RECIPE_ROUTE_BODY_CAPS = {
188
275
  * (256 KB matches the existing PUT/POST/lint caps).
189
276
  */
190
277
  repair: 256 * 1024,
278
+ /**
279
+ * PATCH /recipes/:name — only `{ "enabled": true/false }` (≈ 15 bytes).
280
+ * 1 KB is 66× that maximum; 256 KB was a trivial DoS vector.
281
+ */
282
+ toggle: 1 * 1024,
283
+ /**
284
+ * POST /outcomes — `{ issueUrl, disposition, recipeName?, workerClass? }`.
285
+ * A URL + short enum + two audit strings; 2 KB is generous.
286
+ */
287
+ outcomes: 2 * 1024,
288
+ /**
289
+ * POST /traces/decision — `{ ref, problem, solution, workspace?, tags?,
290
+ * sessionId?, source? }`. problem/solution are each capped at 500 chars
291
+ * by DecisionTraceLog.record(); 8 KB covers that plus tags/metadata with
292
+ * headroom, matching the shape of the ctxSaveTrace MCP tool's inputs.
293
+ */
294
+ decisionTrace: 8 * 1024,
295
+ /**
296
+ * POST /copilot/message — `{ text: string }`, a single chat-input line.
297
+ * 4 KB is generous for free text while blocking a pathological paste.
298
+ */
299
+ copilotMessage: 4 * 1024,
191
300
  };
192
301
  /**
193
302
  * Read an HTTP request body up to `max` bytes. Returns the raw string or
@@ -212,11 +321,14 @@ export function readBodyWithCap(req, max) {
212
321
  total += chunk.byteLength;
213
322
  if (total > max) {
214
323
  aborted = true;
324
+ // Remove the data listener immediately so no further chunks accumulate
325
+ // in `chunks` after the promise resolves. The `aborted` guard already
326
+ // prevents adding to `chunks`, but leaving the listener attached causes
327
+ // unnecessary listener accumulation when the same socket is reused.
328
+ req.removeListener("data", onData);
215
329
  // Resolve immediately so the route can write 413; do NOT destroy the
216
330
  // socket here — destroying mid-upload races with the response write
217
331
  // and the client sees EPIPE/ECONNRESET before reading the body.
218
- // Subsequent chunks land in `onData` again but the `aborted` guard
219
- // discards them, draining the upload until the client emits `end`.
220
332
  resolve({ ok: false, code: "too_large" });
221
333
  // Force the underlying stream to keep flowing so buffered upload
222
334
  // data drains naturally. Without this Node may pause the stream
@@ -234,6 +346,7 @@ export function readBodyWithCap(req, max) {
234
346
  const onEnd = () => {
235
347
  if (aborted)
236
348
  return;
349
+ req.removeListener("data", onData);
237
350
  const bytes = Buffer.concat(chunks);
238
351
  // `bytes` is the raw on-the-wire body; `body` is the utf-8 decode used
239
352
  // by JSON parsers. HMAC consumers must use `bytes` to avoid the
@@ -244,6 +357,7 @@ export function readBodyWithCap(req, max) {
244
357
  if (aborted)
245
358
  return;
246
359
  aborted = true;
360
+ req.removeListener("data", onData);
247
361
  // Treat aborted/error mid-stream as a malformed read. Callers that
248
362
  // care about the distinction can check the `code` on the
249
363
  // readJsonBody result; here we collapse to "too_large" to keep
@@ -304,6 +418,73 @@ function respondInvalidJson(res) {
304
418
  res.writeHead(400, { "Content-Type": "application/json" });
305
419
  res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
306
420
  }
421
+ // END A-PR2 EDIT BLOCK
422
+ // ---------------------------------------------------------------------
423
+ // B3-A — post-redirect SSRF re-check for the non-github HTTPS install path.
424
+ //
425
+ // `validateSafeUrl` only validates the FIRST hop and explicitly does NOT pin
426
+ // the resolved IP across redirects (see src/ssrfGuard.ts header comment). A
427
+ // bare `fetch(url, { redirect: "follow" })` therefore lets a 302 → private
428
+ // IP / IMDS slip past the upfront guard. This helper replays the manual
429
+ // redirect-loop pattern from the CLI `httpsGet` in
430
+ // src/commands/recipeInstall.ts: every hop's host is re-validated with the
431
+ // canonical async guard (`validateSafeUrl`, which re-resolves DNS), the chain
432
+ // is capped, and relative `Location` values are resolved against the current
433
+ // URL before re-validation.
434
+ // ---------------------------------------------------------------------
435
+ const INSTALL_FETCH_MAX_REDIRECTS = 5;
436
+ /**
437
+ * Fetch `startUrl` following HTTP redirects manually, re-validating every hop
438
+ * against the SSRF guard. Throws on a blocked host (private/loopback),
439
+ * unsupported protocol, malformed `Location`, or exceeding the hop cap.
440
+ * Returns the terminal (non-redirect) `Response`.
441
+ *
442
+ * `fetchImpl` is injectable for tests; defaults to the global `fetch`.
443
+ * `signal` is forwarded to every hop so the install timeout still applies.
444
+ */
445
+ export async function fetchFollowingRedirectsSafely(startUrl, opts = {}) {
446
+ const doFetch = opts.fetchImpl ?? fetch;
447
+ let currentUrl = startUrl;
448
+ for (let hop = 0; hop <= INSTALL_FETCH_MAX_REDIRECTS; hop++) {
449
+ // Re-validate the host of THIS hop before issuing the request. The first
450
+ // hop was already validated by the route, but re-checking is cheap and
451
+ // keeps this helper self-contained / correct in isolation.
452
+ const ssrf = await validateSafeUrl(currentUrl);
453
+ if (!ssrf.ok) {
454
+ throw new Error(`SSRF guard blocked redirect target: ${ssrf.detail ?? ssrf.reason ?? "unknown"}`);
455
+ }
456
+ const res = await doFetch(currentUrl, {
457
+ ...(opts.signal ? { signal: opts.signal } : {}),
458
+ redirect: "manual",
459
+ });
460
+ const isRedirect = res.status >= 300 && res.status < 400;
461
+ // Defensive: a non-redirect response with no `Location` is terminal even
462
+ // if a stub omits the `headers` object entirely.
463
+ const location = typeof res.headers?.get === "function"
464
+ ? res.headers.get("location")
465
+ : null;
466
+ if (!isRedirect || !location) {
467
+ // Terminal response — host already validated above.
468
+ return res;
469
+ }
470
+ if (hop >= INSTALL_FETCH_MAX_REDIRECTS) {
471
+ throw new Error(`Too many redirects (>${INSTALL_FETCH_MAX_REDIRECTS}) installing recipe`);
472
+ }
473
+ // Resolve relative redirects against the current URL so a relative
474
+ // `Location: /foo` isn't treated as an empty hostname.
475
+ let nextUrl;
476
+ try {
477
+ nextUrl = new URL(location, currentUrl);
478
+ }
479
+ catch {
480
+ throw new Error(`Invalid redirect location: "${location}"`);
481
+ }
482
+ currentUrl = nextUrl.toString();
483
+ // Loop re-validates currentUrl at the top before fetching it.
484
+ }
485
+ // Unreachable: the loop either returns a terminal response or throws.
486
+ throw new Error("Too many redirects installing recipe");
487
+ }
307
488
  /**
308
489
  * Best-effort fire of the recipe-changed notification. Wraps the
309
490
  * callback in try/catch + console.error so a misbehaving notifier
@@ -313,6 +494,7 @@ function respondInvalidJson(res) {
313
494
  * routes after their respective success paths.
314
495
  */
315
496
  function fireOnRecipesChanged(deps) {
497
+ invalidateRecipesCache();
316
498
  if (!deps.onRecipesChangedFn)
317
499
  return;
318
500
  try {
@@ -521,6 +703,270 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
521
703
  }
522
704
  return true;
523
705
  }
706
+ // GET /workers/shadow — read-only worker trust dial + ramp-vs-gate report.
707
+ // Replays the run log + gate decision log through the (worker × action-class)
708
+ // ramp; changes nothing. Backs the dashboard /workers page.
709
+ if (parsedUrl.pathname === "/workers/shadow" && req.method === "GET") {
710
+ void (async () => {
711
+ try {
712
+ const data = (await deps.workerShadowFn?.()) ?? {
713
+ workers: [],
714
+ runsScanned: 0,
715
+ decisionsScanned: 0,
716
+ };
717
+ res.writeHead(200, { "Content-Type": "application/json" });
718
+ res.end(JSON.stringify({ ...data, generatedAt: new Date().toISOString() }));
719
+ }
720
+ catch (err) {
721
+ respond500(res, err);
722
+ }
723
+ })();
724
+ return true;
725
+ }
726
+ // GET /gate/decisions — read-only query over the persisted Decision Record
727
+ // (worker_gate_decisions.jsonl). Backs `patchwork gate explain`. Requires
728
+ // BOTH workerId and classKey (there is no useful "explain everything"
729
+ // default; this is the one-decision/one-history lookup, not a dashboard
730
+ // feed like /workers/shadow).
731
+ if (parsedUrl.pathname === "/gate/decisions" && req.method === "GET") {
732
+ try {
733
+ const sp = parsedUrl.searchParams;
734
+ const workerId = sp.get("workerId") ?? undefined;
735
+ const classKey = sp.get("classKey") ?? undefined;
736
+ const limitRaw = sp.get("limit");
737
+ const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined;
738
+ const decisions = deps.gateDecisionsFn?.({
739
+ ...(workerId && { workerId }),
740
+ ...(classKey && { classKey }),
741
+ ...(Number.isFinite(limit) && { limit }),
742
+ }) ?? [];
743
+ res.writeHead(200, { "Content-Type": "application/json" });
744
+ res.end(JSON.stringify({ decisions }));
745
+ }
746
+ catch (err) {
747
+ respond500(res, err);
748
+ }
749
+ return true;
750
+ }
751
+ // POST /traces/decision — record a Decision Record trace over HTTP. The
752
+ // HTTP twin of the `ctxSaveTrace` MCP tool: same `DecisionTraceLog.record()`
753
+ // writer, same JSONL file, same validation (ref/problem/solution required,
754
+ // 500-char caps). Exists so future non-MCP callers (dashboard copilot, CI,
755
+ // scripts) can write decision traces without an MCP client. Bearer-gated —
756
+ // all recipe routes run after the auth gate in server.ts.
757
+ if (parsedUrl.pathname === "/traces/decision" && req.method === "POST") {
758
+ void (async () => {
759
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.decisionTrace);
760
+ if (!parsedBody.ok) {
761
+ if (parsedBody.code === "too_large") {
762
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.decisionTrace);
763
+ }
764
+ else {
765
+ respondInvalidJson(res);
766
+ }
767
+ return;
768
+ }
769
+ const respond400 = (error) => {
770
+ res.writeHead(400, { "Content-Type": "application/json" });
771
+ res.end(JSON.stringify({ ok: false, error }));
772
+ };
773
+ try {
774
+ const parsed = parsedBody.value ?? {};
775
+ if (respondIfUnknownBodyKeys(res, parsed, [
776
+ "ref",
777
+ "problem",
778
+ "solution",
779
+ "workspace",
780
+ "tags",
781
+ "sessionId",
782
+ "source",
783
+ ]))
784
+ return;
785
+ const { ref, problem, solution, workspace, tags, sessionId, source } = parsed;
786
+ if (typeof ref !== "string" || !ref.trim()) {
787
+ respond400("`ref` is required (issue ref, PR ref, commit SHA, or free text).");
788
+ return;
789
+ }
790
+ if (typeof problem !== "string" || !problem.trim()) {
791
+ respond400("`problem` is required.");
792
+ return;
793
+ }
794
+ if (typeof solution !== "string" || !solution.trim()) {
795
+ respond400("`solution` is required.");
796
+ return;
797
+ }
798
+ if (workspace !== undefined && typeof workspace !== "string") {
799
+ respond400("`workspace` must be a string when present.");
800
+ return;
801
+ }
802
+ if (tags !== undefined &&
803
+ (!Array.isArray(tags) || !tags.every((t) => typeof t === "string"))) {
804
+ respond400("`tags` must be an array of strings when present.");
805
+ return;
806
+ }
807
+ if (sessionId !== undefined && typeof sessionId !== "string") {
808
+ respond400("`sessionId` must be a string when present.");
809
+ return;
810
+ }
811
+ if (source !== undefined && typeof source !== "string") {
812
+ respond400("`source` must be a string when present.");
813
+ return;
814
+ }
815
+ if (!deps.saveDecisionTraceFn) {
816
+ res.writeHead(503, { "Content-Type": "application/json" });
817
+ res.end(JSON.stringify({
818
+ ok: false,
819
+ error: "Decision Record unavailable on this bridge.",
820
+ }));
821
+ return;
822
+ }
823
+ const trace = deps.saveDecisionTraceFn({
824
+ ref,
825
+ problem,
826
+ solution,
827
+ ...(workspace && { workspace }),
828
+ ...(tags && { tags }),
829
+ ...(sessionId && { sessionId }),
830
+ ...(source && { source }),
831
+ });
832
+ res.writeHead(200, { "Content-Type": "application/json" });
833
+ res.end(JSON.stringify({ ok: true, trace }));
834
+ }
835
+ catch (err) {
836
+ respond400(err instanceof Error ? err.message : "Invalid decision trace input.");
837
+ }
838
+ })();
839
+ return true;
840
+ }
841
+ // GET /outcomes — read-only list of operator outcome dispositions
842
+ // (~/.patchwork/outcome-log.jsonl, deduped last-writer-wins). Backs the
843
+ // dashboard "Filed outcomes" card + mirrors `patchwork outcomes list`.
844
+ // Bearer-gated (all recipe routes run after the auth gate in server.ts).
845
+ if (parsedUrl.pathname === "/outcomes" && req.method === "GET") {
846
+ try {
847
+ const outcomes = deps.outcomeStoreFn?.().readAll() ?? [];
848
+ res.writeHead(200, { "Content-Type": "application/json" });
849
+ res.end(JSON.stringify({ outcomes }));
850
+ }
851
+ catch (err) {
852
+ respond500(res, err);
853
+ }
854
+ return true;
855
+ }
856
+ // GET /outcomes/pending — the confirm queue: worker filings awaiting an
857
+ // operator disposition. Read-only join over the run log + dispositions;
858
+ // backs the dashboard "awaiting confirmation" badge + mirrors
859
+ // `patchwork outcomes pending`. (Exact-path match, so it never shadows the
860
+ // bare /outcomes routes above.)
861
+ if (parsedUrl.pathname === "/outcomes/pending" && req.method === "GET") {
862
+ try {
863
+ const pending = deps.pendingConfirmationsFn?.() ?? [];
864
+ res.writeHead(200, { "Content-Type": "application/json" });
865
+ res.end(JSON.stringify({ pending }));
866
+ }
867
+ catch (err) {
868
+ respond500(res, err);
869
+ }
870
+ return true;
871
+ }
872
+ // POST /outcomes — operator positive-act: record a `confirmed`|`junk`
873
+ // disposition for a worker-filed issue/PR URL. The HTTP twin of `patchwork
874
+ // outcomes confirm|reject` (src/workers/outcomesCli.ts) and the only write
875
+ // path the dashboard uses. `unknown` is ingester-only and rejected here.
876
+ // Same URL validation as the CLI. SELF-CONFIRM PROHIBITION: exposed over
877
+ // Bearer-gated HTTP + CLI only — never as a recipe step or MCP tool — so a
878
+ // worker cannot confirm its own filing. Append-only supersede makes a
879
+ // mis-click recoverable by a corrected write.
880
+ if (parsedUrl.pathname === "/outcomes" && req.method === "POST") {
881
+ void (async () => {
882
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.outcomes);
883
+ if (!parsedBody.ok) {
884
+ if (parsedBody.code === "too_large") {
885
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.outcomes);
886
+ }
887
+ else {
888
+ respondInvalidJson(res);
889
+ }
890
+ return;
891
+ }
892
+ try {
893
+ const parsed = parsedBody.value ?? {};
894
+ if (respondIfUnknownBodyKeys(res, parsed, [
895
+ "issueUrl",
896
+ "disposition",
897
+ "recipeName",
898
+ "workerClass",
899
+ ]))
900
+ return;
901
+ const respond400 = (error) => {
902
+ res.writeHead(400, { "Content-Type": "application/json" });
903
+ res.end(JSON.stringify({ ok: false, error }));
904
+ };
905
+ const { issueUrl, disposition, recipeName, workerClass } = parsed;
906
+ if (typeof issueUrl !== "string" || !/^https?:\/\//.test(issueUrl)) {
907
+ respond400("`issueUrl` must be an http(s) URL (the filed issue/PR URL).");
908
+ return;
909
+ }
910
+ // `unknown` is ingester-only — an operator either confirms or rejects.
911
+ if (disposition !== "confirmed" && disposition !== "junk") {
912
+ respond400('`disposition` must be "confirmed" or "junk" ("unknown" is ingester-only).');
913
+ return;
914
+ }
915
+ if (recipeName !== undefined && typeof recipeName !== "string") {
916
+ respond400("`recipeName` must be a string when present.");
917
+ return;
918
+ }
919
+ if (workerClass !== undefined && typeof workerClass !== "string") {
920
+ respond400("`workerClass` must be a string when present.");
921
+ return;
922
+ }
923
+ const store = deps.outcomeStoreFn?.();
924
+ if (!store) {
925
+ res.writeHead(503, { "Content-Type": "application/json" });
926
+ res.end(JSON.stringify({
927
+ ok: false,
928
+ error: "Outcome store unavailable on this bridge.",
929
+ }));
930
+ return;
931
+ }
932
+ store.upsert({
933
+ issueUrl,
934
+ disposition,
935
+ checkedAt: Date.now(),
936
+ ...(recipeName ? { recipeName } : {}),
937
+ ...(workerClass ? { workerClass } : {}),
938
+ });
939
+ res.writeHead(200, { "Content-Type": "application/json" });
940
+ res.end(JSON.stringify({ ok: true, issueUrl, disposition }));
941
+ }
942
+ catch (err) {
943
+ respond500(res, err);
944
+ }
945
+ })();
946
+ return true;
947
+ }
948
+ // GET /approvals/kpi — considered-approval KPI (reject rate, latency-to-
949
+ // decision, abandoned, dashboard-vs-phone). Read-only over the persisted
950
+ // approval-decision log; the sibling of /workers/shadow that answers "was the
951
+ // trust climbing the dial EARNED or rubber-stamped". Backs the dashboard
952
+ // approval panel. Optional ?sinceMs=<epoch-ms> windows it. Fail-soft.
953
+ if (parsedUrl.pathname === "/approvals/kpi" && req.method === "GET") {
954
+ void (async () => {
955
+ try {
956
+ const sinceRaw = parsedUrl.searchParams.get("sinceMs");
957
+ const sinceMs = sinceRaw ? Number.parseInt(sinceRaw, 10) : Number.NaN;
958
+ const { readConsideredDecisions, computeConsideredApprovalKpi } = await import("./approvalKpi.js");
959
+ const decisions = readConsideredDecisions(Number.isFinite(sinceMs) ? { sinceMs } : {});
960
+ const kpi = computeConsideredApprovalKpi(decisions);
961
+ res.writeHead(200, { "Content-Type": "application/json" });
962
+ res.end(JSON.stringify({ ...kpi, generatedAt: new Date().toISOString() }));
963
+ }
964
+ catch (err) {
965
+ respond500(res, err);
966
+ }
967
+ })();
968
+ return true;
969
+ }
524
970
  // GET /recipes/doctor?recipe=<name> — one-call recipe health diagnosis.
525
971
  // Server-side home for the `recipe doctor` CLI: composes the static
526
972
  // preflight check (lint + write-policy + plan) with the recipe-scoped
@@ -639,6 +1085,28 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
639
1085
  })();
640
1086
  return true;
641
1087
  }
1088
+ // POST /runs/:seq/cancel — cancel an in-flight recipe run. Aborts the run's
1089
+ // registered AbortController so not-yet-started steps are skipped (and agent
1090
+ // steps that honor the signal abort). 200 {cancelled:true} when a live run
1091
+ // was found, 404 {cancelled:false} when the seq isn't currently running
1092
+ // (already finished, unknown, or ran on a different process). (run-cancel)
1093
+ const runCancelMatch = req.method === "POST"
1094
+ ? /^\/runs\/(\d+)\/cancel$/.exec(parsedUrl.pathname)
1095
+ : null;
1096
+ if (runCancelMatch?.[1]) {
1097
+ const seq = Number.parseInt(runCancelMatch[1], 10);
1098
+ try {
1099
+ const cancelled = cancelRun(seq);
1100
+ res.writeHead(cancelled ? 200 : 404, {
1101
+ "Content-Type": "application/json",
1102
+ });
1103
+ res.end(JSON.stringify({ cancelled, seq }));
1104
+ }
1105
+ catch (err) {
1106
+ respond500(res, err, "runs/:seq/cancel");
1107
+ }
1108
+ return true;
1109
+ }
642
1110
  // GET /runs/:seq/plan — dry-run plan for the recipe that produced this run
643
1111
  const runPlanMatch = req.method === "GET"
644
1112
  ? /^\/runs\/(\d+)\/plan$/.exec(parsedUrl.pathname)
@@ -658,6 +1126,14 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
658
1126
  res.end(JSON.stringify({ error: "plan_unavailable" }));
659
1127
  return;
660
1128
  }
1129
+ // Guard: recipeName may be absent on runs created before the field was
1130
+ // added (audit LOW #36). An unsafe cast + immediate .replace() would
1131
+ // throw TypeError — return 404 instead.
1132
+ if (!run.recipeName) {
1133
+ res.writeHead(404, { "Content-Type": "application/json" });
1134
+ res.end(JSON.stringify({ error: "recipe_name_missing" }));
1135
+ return;
1136
+ }
661
1137
  // triggerSource appends ":agent" suffix — strip before file lookup
662
1138
  const recipeName = run.recipeName.replace(/:agent$/, "");
663
1139
  const plan = await deps.runPlanFn(recipeName);
@@ -887,7 +1363,7 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
887
1363
  })();
888
1364
  return true;
889
1365
  }
890
- if (req.url === "/recipes" && req.method === "POST") {
1366
+ if (parsedUrl.pathname === "/recipes" && req.method === "POST") {
891
1367
  // A-PR2: bounded JSON read at RECIPE_ROUTE_BODY_CAPS.content (256 KB).
892
1368
  void (async () => {
893
1369
  const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.content);
@@ -970,13 +1446,14 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
970
1446
  }
971
1447
  const recipePatchMatch = /^\/recipes\/([^/]+)$/.exec(parsedUrl.pathname);
972
1448
  if (recipePatchMatch && req.method === "PATCH") {
973
- // A-PR2: bounded JSON read at RECIPE_ROUTE_BODY_CAPS.content (256 KB).
1449
+ // A-PR2: tight 1 KB cap — the only valid payload is `{"enabled":bool}`.
1450
+ // Using the 256 KB content cap here was a trivial DoS vector (audit LOW #34).
974
1451
  const name = decodeURIComponent(recipePatchMatch[1] ?? "");
975
1452
  void (async () => {
976
- const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.content);
1453
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.toggle);
977
1454
  if (!parsedBody.ok) {
978
1455
  if (parsedBody.code === "too_large") {
979
- respond413(res, RECIPE_ROUTE_BODY_CAPS.content);
1456
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.toggle);
980
1457
  }
981
1458
  else {
982
1459
  respondInvalidJson(res);
@@ -1089,6 +1566,41 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1089
1566
  })();
1090
1567
  return true;
1091
1568
  }
1569
+ // GET /recipes/:name/simulate — What-If Preview: static counterfactual
1570
+ // simulation of a recipe by name (projected actions, side-effect taxonomy,
1571
+ // blast-radius risk, tier-only approval projection honestly flagged as NOT
1572
+ // gated on recipe steps today, low-confidence cost, undetermined branches).
1573
+ // Executes nothing; superset of the dry-run plan. See runRecipeSimulate.
1574
+ const recipeSimulateMatch = req.method === "GET"
1575
+ ? /^\/recipes\/([^/]+)\/simulate$/.exec(parsedUrl.pathname)
1576
+ : null;
1577
+ if (recipeSimulateMatch?.[1]) {
1578
+ const name = decodeURIComponent(recipeSimulateMatch[1]);
1579
+ void (async () => {
1580
+ try {
1581
+ if (!deps.simulateFn) {
1582
+ res.writeHead(503, { "Content-Type": "application/json" });
1583
+ res.end(JSON.stringify({ error: "simulate_unavailable" }));
1584
+ return;
1585
+ }
1586
+ const report = await deps.simulateFn(name);
1587
+ res.writeHead(200, { "Content-Type": "application/json" });
1588
+ res.end(JSON.stringify({ report }));
1589
+ }
1590
+ catch (err) {
1591
+ // #605: classify by code, not substring (see /recipes/:name/plan).
1592
+ const code = err?.code;
1593
+ if (code === "ENOENT" || code === "RECIPE_NOT_FOUND") {
1594
+ res.writeHead(404, { "Content-Type": "application/json" });
1595
+ res.end(JSON.stringify({ error: "Recipe not found" }));
1596
+ }
1597
+ else {
1598
+ respond500(res, err, "recipes simulate");
1599
+ }
1600
+ }
1601
+ })();
1602
+ return true;
1603
+ }
1092
1604
  const recipeContentMatch = /^\/recipes\/([^/]+)$/.exec(parsedUrl.pathname);
1093
1605
  if (recipeContentMatch && req.method === "GET") {
1094
1606
  const name = decodeURIComponent(recipeContentMatch[1] ?? "");
@@ -1148,7 +1660,10 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1148
1660
  res.writeHead(result.ok ? 200 : 400, {
1149
1661
  "Content-Type": "application/json",
1150
1662
  });
1151
- res.end(JSON.stringify(result));
1663
+ const safeResult = result.error
1664
+ ? { ...result, error: sanitizeStorageError(result.error) }
1665
+ : result;
1666
+ res.end(JSON.stringify(safeResult));
1152
1667
  }
1153
1668
  catch {
1154
1669
  respondInvalidJson(res);
@@ -1177,7 +1692,10 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1177
1692
  ? 404
1178
1693
  : 400;
1179
1694
  res.writeHead(status, { "Content-Type": "application/json" });
1180
- res.end(JSON.stringify(result));
1695
+ const safeDeleteResult = result.error
1696
+ ? { ...result, error: sanitizeStorageError(result.error) }
1697
+ : result;
1698
+ res.end(JSON.stringify(safeDeleteResult));
1181
1699
  return true;
1182
1700
  }
1183
1701
  // POST /recipes/:name/archive — move recipe (+ sidecar) into <recipesDir>/.archive/
@@ -1200,7 +1718,10 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1200
1718
  ? 404
1201
1719
  : 400;
1202
1720
  res.writeHead(status, { "Content-Type": "application/json" });
1203
- res.end(JSON.stringify(result));
1721
+ const safeArchiveResult = result.error
1722
+ ? { ...result, error: sanitizeStorageError(result.error) }
1723
+ : result;
1724
+ res.end(JSON.stringify(safeArchiveResult));
1204
1725
  return true;
1205
1726
  }
1206
1727
  // POST /recipes/:name/duplicate — copy recipe as next available variant name
@@ -1223,7 +1744,10 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1223
1744
  ? 404
1224
1745
  : 400;
1225
1746
  res.writeHead(status, { "Content-Type": "application/json" });
1226
- res.end(JSON.stringify(result));
1747
+ const safeDuplicateResult = result.error
1748
+ ? { ...result, error: sanitizeStorageError(result.error) }
1749
+ : result;
1750
+ res.end(JSON.stringify(safeDuplicateResult));
1227
1751
  return true;
1228
1752
  }
1229
1753
  // POST /recipes/:name/promote — promote variant to canonical name.
@@ -1288,7 +1812,7 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1288
1812
  })();
1289
1813
  return true;
1290
1814
  }
1291
- if (req.url === "/recipes" && req.method === "GET") {
1815
+ if (parsedUrl.pathname === "/recipes" && req.method === "GET") {
1292
1816
  try {
1293
1817
  const data = deps.recipesFn?.() ?? { recipesDir: null, recipes: [] };
1294
1818
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -1299,12 +1823,83 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1299
1823
  }
1300
1824
  return true;
1301
1825
  }
1826
+ // POST /copilot/message — Tier 1 lever-action copilot (Overview deck's
1827
+ // 7:copilot pane). Deterministic intent matching only, NEVER an LLM
1828
+ // call and NEVER an executed action — this route only proposes
1829
+ // {reply, action?}; the dashboard's action-card Confirm button is what
1830
+ // actually calls the (separately gated) pause/enable/run endpoints.
1831
+ // "Chat proposes, buttons dispose" — see parseIntent.ts's module doc.
1832
+ if (parsedUrl.pathname === "/copilot/message" && req.method === "POST") {
1833
+ void (async () => {
1834
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.copilotMessage);
1835
+ if (!parsedBody.ok) {
1836
+ if (parsedBody.code === "too_large") {
1837
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.copilotMessage);
1838
+ }
1839
+ else {
1840
+ respondInvalidJson(res);
1841
+ }
1842
+ return;
1843
+ }
1844
+ try {
1845
+ const parsed = parsedBody.value ?? {};
1846
+ if (respondIfUnknownBodyKeys(res, parsed, ["text"]))
1847
+ return;
1848
+ const text = typeof parsed.text === "string" ? parsed.text : "";
1849
+ const recipesData = deps.recipesFn?.() ?? { recipes: [] };
1850
+ const recipeList = Array.isArray(recipesData.recipes)
1851
+ ? recipesData.recipes
1852
+ : [];
1853
+ const intent = parseCopilotIntent(text, recipeList);
1854
+ let haltReason = null;
1855
+ if (intent.kind === "explain_halt") {
1856
+ const runs = deps.runsFn?.({
1857
+ recipe: intent.recipe?.name,
1858
+ limit: 20,
1859
+ }) ?? [];
1860
+ const lastHalt = runs.find((r) => typeof r.haltReason === "string");
1861
+ haltReason = lastHalt?.haltReason ?? null;
1862
+ }
1863
+ // Read-only status Q&A — same singletons the /approvals and
1864
+ // /kill-switch routes already read from, so the copilot's answer
1865
+ // can never drift from what those pages show.
1866
+ const result = buildCopilotReply(intent, {
1867
+ haltReason,
1868
+ approvalsPending: intent.kind === "approvals_status"
1869
+ ? getApprovalQueue().list().length
1870
+ : undefined,
1871
+ killSwitchEngaged: intent.kind === "kill_switch_status"
1872
+ ? isWriteKillSwitchActive()
1873
+ : undefined,
1874
+ });
1875
+ res.writeHead(200, { "Content-Type": "application/json" });
1876
+ res.end(JSON.stringify(result));
1877
+ }
1878
+ catch (err) {
1879
+ respond500(res, err);
1880
+ }
1881
+ })();
1882
+ return true;
1883
+ }
1302
1884
  if (parsedUrl.pathname === "/templates" && req.method === "GET") {
1303
1885
  void (async () => {
1304
1886
  try {
1305
1887
  const now = Date.now();
1306
1888
  if (templatesCache === null || now - templatesCacheTs > 5 * 60 * 1000) {
1307
- const ghRes = await fetch("https://raw.githubusercontent.com/patchworkos/recipes/main/index.json");
1889
+ // Abort a stalled CDN connection — without this a hung socket
1890
+ // parks the handler forever (the negative-cache sentinel only
1891
+ // fires on rejection, which a hung connection never produces).
1892
+ // Matches the AbortController idiom used by the other fetch
1893
+ // sites in this file.
1894
+ const ctl = new AbortController();
1895
+ const timeout = setTimeout(() => ctl.abort(), 30_000);
1896
+ let ghRes;
1897
+ try {
1898
+ ghRes = await fetch("https://raw.githubusercontent.com/patchworkos/recipes/main/index.json", { signal: ctl.signal });
1899
+ }
1900
+ finally {
1901
+ clearTimeout(timeout);
1902
+ }
1308
1903
  if (!ghRes.ok) {
1309
1904
  throw new Error(`GitHub returned ${ghRes.status}`);
1310
1905
  }
@@ -1563,8 +2158,10 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1563
2158
  // recipe inside one millisecond don't collide.
1564
2159
  const { randomBytes: randomBytesFn } = await import("node:crypto");
1565
2160
  const tmpFile = path.join(os.tmpdir(), `patchwork-bundle-install-${process.pid}-${Date.now()}-${randomBytesFn(6).toString("hex")}-${r}.yaml`);
1566
- writeFileSync(tmpFile, yamlText, "utf-8");
2161
+ // Audit LOW #35: writeFileSync is inside the try so that the
2162
+ // finally block's unlinkSync runs even when the write throws.
1567
2163
  try {
2164
+ writeFileSync(tmpFile, yamlText, "utf-8");
1568
2165
  const installResult = installRecipeFromFile(tmpFile, {
1569
2166
  recipesDir,
1570
2167
  });
@@ -1745,7 +2342,22 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1745
2342
  }
1746
2343
  fetchUrl = source;
1747
2344
  const urlParts = fetchUrl.split("/");
1748
- recipeName = (urlParts[urlParts.length - 1] ?? "recipe").replace(/\.ya?ml$/i, "");
2345
+ const rawRecipeName = (urlParts[urlParts.length - 1] ?? "").replace(/\.ya?ml$/i, "");
2346
+ // Audit 2026-06-03 MEDIUM #17: the derived recipe name was written
2347
+ // into the recipes directory without any sanitisation, opening a
2348
+ // path-injection vector (e.g. `bad%20name`, `..evil`, etc.).
2349
+ // Validate against the same SEGMENT_RE used for github: sources.
2350
+ recipeName = rawRecipeName.toLowerCase();
2351
+ const { SEGMENT_RE: installSegRE } = await import("./recipes/githubInstallSource.js");
2352
+ if (!recipeName || !installSegRE.test(recipeName)) {
2353
+ res.writeHead(400, { "Content-Type": "application/json" });
2354
+ res.end(JSON.stringify({
2355
+ ok: false,
2356
+ error: "Invalid recipe name in source URL",
2357
+ code: "invalid_recipe_name",
2358
+ }));
2359
+ return;
2360
+ }
1749
2361
  }
1750
2362
  else {
1751
2363
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -1806,13 +2418,34 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1806
2418
  }
1807
2419
  else {
1808
2420
  try {
1809
- yamlRes = await fetch(fetchUrl, {
2421
+ // B3-A: manual redirect loop with per-hop SSRF re-validation —
2422
+ // the upfront validateSafeUrl above only pins the first hop, so a
2423
+ // 302 → private IP / IMDS would otherwise bypass it under
2424
+ // `redirect: "follow"`.
2425
+ yamlRes = await fetchFollowingRedirectsSafely(fetchUrl, {
1810
2426
  signal: fetchCtl.signal,
1811
- redirect: "follow",
1812
2427
  });
1813
2428
  }
1814
2429
  catch (err) {
1815
2430
  clearTimeout(fetchTimeout);
2431
+ // A redirect to a blocked host is an SSRF attempt, not a generic
2432
+ // network failure — surface it as 403 ssrf_blocked so it isn't
2433
+ // confused with an unreachable upstream.
2434
+ const msg = err instanceof Error ? err.message : String(err);
2435
+ if (/^SSRF guard blocked/.test(msg)) {
2436
+ // Log the full guard detail server-side, but return a STATIC
2437
+ // message — never echo the caught error text to the client (it is
2438
+ // error/stack-derived; CodeQL js/stack-trace-exposure). The
2439
+ // `code` carries the machine-readable signal.
2440
+ console.error(`[recipes/install] SSRF guard blocked redirect:`, msg);
2441
+ res.writeHead(403, { "Content-Type": "application/json" });
2442
+ res.end(JSON.stringify({
2443
+ ok: false,
2444
+ error: "Install blocked: the source redirected to a disallowed host (SSRF guard).",
2445
+ code: "ssrf_blocked",
2446
+ }));
2447
+ return;
2448
+ }
1816
2449
  console.error(`[recipes/install] fetch failed:`, err);
1817
2450
  // Network-level error → 502 (upstream unreachable), not 500.
1818
2451
  res.writeHead(502, { "Content-Type": "application/json" });
@@ -1887,9 +2520,13 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
1887
2520
  const { randomBytes } = await import("node:crypto");
1888
2521
  const tmpFile = path.join(os.tmpdir(), `patchwork-install-${process.pid}-${Date.now()}-${randomBytes(6).toString("hex")}-${recipeName}.yaml`);
1889
2522
  const { writeFileSync, mkdirSync, unlinkSync } = await import("node:fs");
1890
- writeFileSync(tmpFile, yamlText, "utf-8");
1891
2523
  let result;
2524
+ // Audit LOW #35: writeFileSync is now inside the try so that the
2525
+ // finally block's unlinkSync runs even when the write itself throws
2526
+ // (e.g. ENOSPC, EROFS). Previously writeFileSync was called before
2527
+ // the try, orphaning the temp file on any write error.
1892
2528
  try {
2529
+ writeFileSync(tmpFile, yamlText, "utf-8");
1893
2530
  const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
1894
2531
  mkdirSync(recipesDir, { recursive: true });
1895
2532
  const { installRecipeFromFile } = await import("./recipes/installer.js");