@jr2/orchestrator 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +142 -29
  14. package/docs/concepts/architecture.md +60 -34
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +13 -8
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +7 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +133 -50
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +14 -4
  23. package/docs/concepts/pools-and-sources.md +193 -17
  24. package/docs/concepts/secrets-and-trust.md +186 -62
  25. package/docs/concepts/the-menu.md +14 -0
  26. package/docs/concepts/workspaces.md +135 -56
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
  32. package/docs/get-started/install.md +4 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +72 -15
  34. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  35. package/docs/guides/answer-a-gate.md +74 -37
  36. package/docs/guides/bind-open-parts.md +29 -12
  37. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  38. package/docs/guides/cancel-a-run.md +26 -16
  39. package/docs/guides/cut-a-worktree.md +560 -0
  40. package/docs/guides/fix-a-failed-converge.md +19 -7
  41. package/docs/guides/give-a-caller-a-scope.md +203 -0
  42. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  43. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  44. package/docs/guides/let-an-agent-write-a-chart.md +14 -7
  45. package/docs/guides/look-inside-a-sandbox.md +115 -42
  46. package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
  47. package/docs/guides/open-the-console.md +63 -14
  48. package/docs/guides/push-and-sign-a-branch.md +67 -21
  49. package/docs/guides/read-a-conversation.md +225 -0
  50. package/docs/guides/run-a-chart-by-name.md +17 -16
  51. package/docs/guides/run-a-command-or-call-a-url.md +84 -41
  52. package/docs/guides/run-one-worker-per-item.md +34 -27
  53. package/docs/guides/set-an-agents-access-and-dials.md +69 -22
  54. package/docs/guides/size-a-workspace.md +19 -6
  55. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  56. package/docs/guides/start-a-run-from-a-run.md +18 -16
  57. package/docs/guides/start-a-run.md +24 -18
  58. package/docs/guides/steer-an-agent.md +332 -0
  59. package/docs/guides/use-a-hosted-provider.md +7 -1
  60. package/docs/guides/watch-a-run.md +146 -117
  61. package/docs/guides/write-a-hook-kind.md +238 -0
  62. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  63. package/docs/guides/write-a-workflow.md +18 -8
  64. package/docs/llms.txt +110 -23
  65. package/docs/reference/authoring-surface/agent.md +35 -12
  66. package/docs/reference/authoring-surface/answer.md +20 -4
  67. package/docs/reference/authoring-surface/arg.md +28 -6
  68. package/docs/reference/authoring-surface/basic.md +1 -1
  69. package/docs/reference/authoring-surface/bearer.md +1 -1
  70. package/docs/reference/authoring-surface/branchname.md +15 -0
  71. package/docs/reference/authoring-surface/carry.md +23 -0
  72. package/docs/reference/authoring-surface/chartschema.md +1 -1
  73. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  74. package/docs/reference/authoring-surface/command.md +37 -22
  75. package/docs/reference/authoring-surface/customize.md +4 -1
  76. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  77. package/docs/reference/authoring-surface/defineevent.md +1 -1
  78. package/docs/reference/authoring-surface/dispatch.md +5 -4
  79. package/docs/reference/authoring-surface/fail.md +18 -5
  80. package/docs/reference/authoring-surface/file.md +37 -17
  81. package/docs/reference/authoring-surface/frompick.md +1 -1
  82. package/docs/reference/authoring-surface/gate.md +118 -0
  83. package/docs/reference/authoring-surface/group-agents.md +15 -0
  84. package/docs/reference/authoring-surface/group-charts.md +16 -0
  85. package/docs/reference/authoring-surface/group-composition.md +25 -0
  86. package/docs/reference/authoring-surface/group-config.md +17 -0
  87. package/docs/reference/authoring-surface/group-effects.md +48 -0
  88. package/docs/reference/authoring-surface/group-failure.md +13 -0
  89. package/docs/reference/authoring-surface/group-gates.md +16 -0
  90. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  91. package/docs/reference/authoring-surface/group-steering.md +12 -0
  92. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  93. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  94. package/docs/reference/authoring-surface/http.md +109 -9
  95. package/docs/reference/authoring-surface/isfailure.md +1 -1
  96. package/docs/reference/authoring-surface/isready.md +14 -0
  97. package/docs/reference/authoring-surface/jr2setup.md +14 -4
  98. package/docs/reference/authoring-surface/mcp.md +107 -0
  99. package/docs/reference/authoring-surface/open.md +41 -8
  100. package/docs/reference/authoring-surface/openbody.md +1 -1
  101. package/docs/reference/authoring-surface/paramsof.md +23 -5
  102. package/docs/reference/authoring-surface/pool.md +79 -11
  103. package/docs/reference/authoring-surface/push.md +31 -13
  104. package/docs/reference/authoring-surface/readyof.md +21 -0
  105. package/docs/reference/authoring-surface/reread.md +19 -0
  106. package/docs/reference/authoring-surface/secret.md +26 -5
  107. package/docs/reference/authoring-surface/sentby.md +1 -1
  108. package/docs/reference/authoring-surface/skip.md +22 -0
  109. package/docs/reference/authoring-surface/source.md +59 -4
  110. package/docs/reference/authoring-surface/steer.md +93 -0
  111. package/docs/reference/authoring-surface/text.md +1 -1
  112. package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
  113. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  114. package/docs/reference/authoring-surface/type-agentslots.md +4 -1
  115. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  116. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  117. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  118. package/docs/reference/authoring-surface/type-binding.md +6 -7
  119. package/docs/reference/authoring-surface/type-caller.md +40 -0
  120. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  121. package/docs/reference/authoring-surface/type-carry.md +19 -0
  122. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  123. package/docs/reference/authoring-surface/type-commandinput.md +4 -3
  124. package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
  125. package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
  126. package/docs/reference/authoring-surface/type-customize.md +20 -3
  127. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  128. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  129. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  130. package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
  131. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  132. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  133. package/docs/reference/authoring-surface/type-filebody.md +4 -8
  134. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  135. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  136. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  137. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  138. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  139. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  140. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  141. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  142. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  143. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  144. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  145. package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
  146. package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
  147. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
  149. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  150. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  151. package/docs/reference/authoring-surface/type-lends.md +5 -1
  152. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  153. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  154. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  155. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  156. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  157. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  158. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  159. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  160. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  161. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  162. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  163. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
  164. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  165. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  166. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  167. package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
  168. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
  169. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
  170. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  171. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  172. package/docs/reference/authoring-surface/type-pushinput.md +22 -3
  173. package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
  174. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  175. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  176. package/docs/reference/authoring-surface/type-reposlot.md +6 -6
  177. package/docs/reference/authoring-surface/type-reread.md +18 -0
  178. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  179. package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
  180. package/docs/reference/authoring-surface/type-sender.md +26 -1
  181. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  182. package/docs/reference/authoring-surface/type-size.md +1 -1
  183. package/docs/reference/authoring-surface/type-skip.md +19 -0
  184. package/docs/reference/authoring-surface/type-slothandles.md +5 -13
  185. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  186. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  187. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  188. package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
  189. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  190. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  191. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  192. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  193. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  194. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  195. package/docs/reference/authoring-surface/type-workspaced.md +30 -9
  196. package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
  197. package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
  198. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  199. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  200. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  201. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  202. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  203. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  204. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  205. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  206. package/docs/reference/authoring-surface/workspace.md +66 -38
  207. package/docs/reference/authoring-surface/worktree.md +71 -0
  208. package/docs/reference/authoring-surface.md +88 -147
  209. package/docs/reference/cli.md +413 -74
  210. package/docs/reference/config.md +248 -6
  211. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
  212. package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
  213. package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
  214. package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
  215. package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
  216. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  217. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  218. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  219. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  220. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
  221. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
  222. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  223. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
  224. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
  225. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
  226. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  227. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
  228. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
  229. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  230. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
  231. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
  232. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  233. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
  234. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
  235. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  236. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  237. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
  238. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  239. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
  240. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  241. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
  242. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
  243. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  244. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
  245. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
  246. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
  247. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  248. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  249. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
  250. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
  251. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
  252. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
  253. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
  254. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  255. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  256. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
  257. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
  258. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
  259. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  260. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
  261. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  262. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  263. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
  264. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
  265. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
  266. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  267. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  268. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  269. package/docs/reference/decisions.md +9 -4
  270. package/docs/reference/glossary.md +138 -45
  271. package/docs/reference/hooks.md +1077 -0
  272. package/docs/reference/http-api.md +988 -123
  273. package/docs/reference/machines.md +0 -1
  274. package/docs/why.md +7 -5
  275. package/package.json +10 -3
  276. package/src/actor.ts +194 -42
  277. package/src/agent.ts +56 -6
  278. package/src/ambient.ts +3 -5
  279. package/src/assembly-checks.ts +530 -4
  280. package/src/callers.ts +245 -0
  281. package/src/chart.ts +238 -26
  282. package/src/charts.ts +9 -2
  283. package/src/config.ts +566 -7
  284. package/src/conversations.ts +453 -0
  285. package/src/custodian.lua +17 -5
  286. package/src/custodian.ts +257 -77
  287. package/src/customize.ts +154 -7
  288. package/src/dispatch-effect.ts +142 -26
  289. package/src/durability.ts +2 -0
  290. package/src/duration.ts +20 -0
  291. package/src/effect-checks.ts +52 -4
  292. package/src/effect.ts +401 -132
  293. package/src/effector-wire.ts +278 -30
  294. package/src/fail.ts +17 -4
  295. package/src/gate-brand.ts +16 -0
  296. package/src/gate.ts +634 -48
  297. package/src/harness-client.ts +84 -35
  298. package/src/harness-env.ts +33 -0
  299. package/src/held-secrets.ts +197 -39
  300. package/src/holds.ts +5 -2
  301. package/src/hook-kinds/discord.ts +92 -0
  302. package/src/hook-kinds/door.ts +107 -0
  303. package/src/hook-kinds/github.ts +89 -0
  304. package/src/hook-kinds/gitlab.ts +72 -0
  305. package/src/hook-kinds/hook.ts +7 -0
  306. package/src/hook-kinds/index.ts +465 -0
  307. package/src/hook-kinds/slack.ts +151 -0
  308. package/src/hook-kinds/webhook.ts +113 -0
  309. package/src/hooks.ts +506 -0
  310. package/src/http-effect.ts +164 -70
  311. package/src/http.ts +933 -99
  312. package/src/index.ts +30 -5
  313. package/src/instance.ts +196 -26
  314. package/src/internal.ts +23 -3
  315. package/src/kube-client.ts +6 -3
  316. package/src/machine-doc.ts +29 -5
  317. package/src/mcp-effect.ts +424 -0
  318. package/src/model-keys.ts +24 -0
  319. package/src/mount-digest.ts +41 -7
  320. package/src/names.ts +82 -7
  321. package/src/notices.ts +576 -59
  322. package/src/open-body.ts +110 -30
  323. package/src/open-effect.ts +17 -4
  324. package/src/open.ts +40 -7
  325. package/src/parts.ts +125 -35
  326. package/src/party.ts +35 -1
  327. package/src/pick.ts +43 -9
  328. package/src/pool.ts +1172 -172
  329. package/src/registration.ts +249 -17
  330. package/src/repo-identity.ts +7 -3
  331. package/src/repos.ts +20 -2
  332. package/src/run-host.ts +1614 -190
  333. package/src/run-list.ts +191 -0
  334. package/src/sandbox-effect.ts +217 -66
  335. package/src/sandbox-kube.ts +116 -21
  336. package/src/secret-placement.ts +248 -35
  337. package/src/server.ts +53 -4
  338. package/src/setup.ts +158 -38
  339. package/src/snapshot-store.ts +565 -48
  340. package/src/steer.ts +111 -0
  341. package/src/stub-harness.ts +6 -2
  342. package/src/tokens.ts +362 -46
  343. package/src/vocabulary.ts +23 -4
  344. package/src/wire.ts +27 -10
  345. package/src/workspace.ts +314 -259
  346. package/src/worktree.ts +249 -0
  347. package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
  348. package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
  349. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  350. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
@@ -3,13 +3,20 @@
3
3
  //
4
4
  // - a Workspace's pod: the Held secrets its `workspace()` lists under `secrets: [...]`, and the
5
5
  // model keys of the Agents whose Turns run there — a model key is claimed by the PROVIDER;
6
- // - the Instance Harness: the model keys of the `"none"` Agents it hosts, and nothing else (they
7
- // cannot execute code, ADR-0031, so nothing else is theirs to use);
6
+ // - the Instance Harness: the model keys of the `"none"` Agents it hosts, and the secrets of the
7
+ // remote MCP servers they name (ADR-0079), and nothing else (they cannot execute code, ADR-0031,
8
+ // so nothing else is theirs to use);
9
+ // - a Workspace's pod also holds the secret of each MCP server an Agent there names (ADR-0079):
10
+ // "a Custodian holds what its pod's Agents name";
8
11
  // - an Effector: what its Effects name with `secret("name")` — the Instance Effector what its
9
12
  // `http` Effects name and the token of each `dispatch` with a target (ADR-0071; a dispatch with
10
13
  // no target runs in the Orchestrator's own process and names nothing); a Workspace's Sandbox Effector what its file-body `http`s and
11
14
  // credentialed `command`s name. The push credential its `push`es spend is not placed here: the provision
12
- // names the entry the slot's url matches on the Sandbox, by reference (git-push.ts, ADR-0051).
15
+ // names the entry the slot's url matches on the Sandbox, by reference (git-push.ts, ADR-0051);
16
+ // - the Instance Effector also holds the secret of each Hook with a `mac` verifier, and its fence
17
+ // takes each host that a Hook's kind declares in `posts`, with no secret (ADR-0068, ADR-0078);
18
+ // - the Orchestrator gets only the sha-256 digest of the secret of each Hook with a `token`
19
+ // verifier, in `jr2-hook-digests`, and never the value (ADR-0078).
13
20
  //
14
21
  // `hosts`, `headers` and `paths` are an entry's BINDING — where a Custodian may put the value — and
15
22
  // never its placement. Both is both, and `jr2 up` reports it per pod; an entry nothing names goes
@@ -19,6 +26,7 @@
19
26
  // the list does not hold, and an Effect secret shorter than 8 characters (ADR-0068): the Effector
20
27
  // replaces every match of a secret in what it returns, and a short one would match ordinary text.
21
28
 
29
+ import { createHash } from "node:crypto";
22
30
  import {
23
31
  providerClaims,
24
32
  sourceOf,
@@ -27,7 +35,8 @@ import {
27
35
  type HeldBinding,
28
36
  type HeldLookups,
29
37
  } from "./held-secrets.ts";
30
- import type { JR2Config } from "./config.ts";
38
+ import { isStdioServer, mcpTimeoutMs, type JR2Config, type McpRemoteServer } from "./config.ts";
39
+ import { hookParts } from "./hook-kinds/door.ts";
31
40
  import { needsASlot } from "./effect.ts";
32
41
  import { declaredPushKinds, pushRefusal } from "./git-push.ts";
33
42
  import { INSTANCE_HARNESS_HELD_POD } from "./names.ts";
@@ -42,6 +51,10 @@ export type EffectSecret = { name: string; source: HeldBinding["source"] };
42
51
  /** One target an Effect placed in an Effector may call, with the secrets it names there. */
43
52
  export type EffectorTargetConfig = { scheme: "http" | "https"; host: string; port: number; secrets: string[] };
44
53
 
54
+ /** One MCP server an `mcp` Effect placed in the Instance Effector calls (ADR-0079): `effector.json`'s
55
+ * `mcp` entry — its url, the name of its secret, and its `timeout` in milliseconds. Never a value. */
56
+ export type EffectorMcpServer = { url: string; secret?: string; timeoutMs: number };
57
+
45
58
  /** What one Effector placement holds and allows — what `jr2 up` converges from. */
46
59
  export type EffectorPlan = {
47
60
  placement: "instance";
@@ -51,12 +64,22 @@ export type EffectorPlan = {
51
64
  values: Record<string, string>;
52
65
  /** `effector.json`'s targets: one per distinct (target, secrets) an Effect placed there names. */
53
66
  targets: EffectorTargetConfig[];
67
+ /** `effector.json`'s `mcp`: each server an `mcp` Effect placed there names, by its name in the
68
+ * `mcp` block (ADR-0079). Empty when there is none. */
69
+ mcp: Record<string, EffectorMcpServer>;
54
70
  /** The longest timeout among the Effects placed there, in milliseconds. */
55
71
  longestTimeoutMs: number;
56
72
  /** The Effects placed there, as `<workflow>/<slot>`. */
57
73
  effects: string[];
74
+ /** The Hooks it serves (ADR-0068): each one with a `mac` verifier, or whose kind declares
75
+ * `posts`, by name. */
76
+ hooks: string[];
58
77
  };
59
78
 
79
+ /** The secret of a Hook with a `token` verifier (ADR-0078): `jr2 up` writes the sha-256 digest of
80
+ * its value into `jr2-hook-digests`, from the literal or from the key of the user's Secret. */
81
+ export type HookTokenSecret = { name: string } & ({ value: string } | { secret: string; key: string });
82
+
60
83
  /** One registered Machine's Effect, as `jr2 up` walked it. */
61
84
  export type PlacedEffect = { workflow: string; effect: CarriedEffect };
62
85
 
@@ -67,8 +90,9 @@ export type PlacedWorkspace = { workflow: string; pod: WorkspacePod };
67
90
  export type PlacementWalk = {
68
91
  effects: readonly PlacedEffect[];
69
92
  workspaces: readonly PlacedWorkspace[];
70
- /** The models of the `"none"` Agents — present when the Instance Harness is converged. */
71
- instanceHarness?: { models: readonly string[] };
93
+ /** The models of the `"none"` Agents, and the MCP servers they name (ADR-0079) — present when
94
+ * the Instance Harness is converged. */
95
+ instanceHarness?: { models: readonly string[]; servers?: readonly string[] };
72
96
  };
73
97
 
74
98
  /** One Harness pod's Held secrets: what its Custodian holds. */
@@ -79,8 +103,11 @@ export type HeldPodPlan = {
79
103
  label: string;
80
104
  /** The names it holds, in list order; the custom provider's key last. */
81
105
  names: string[];
82
- /** Of `names`, those a provider claims (the rest the `workspace()` lists). */
106
+ /** Of `names`, those a provider claims (the rest the `workspace()` lists, or an MCP server). */
83
107
  byProvider: string[];
108
+ /** Of `names`, the secrets of the MCP servers its Agents name (ADR-0079), each with those
109
+ * servers. Absent when there is none. */
110
+ byServer?: Record<string, string[]>;
84
111
  };
85
112
 
86
113
  /** One push of a Workspace (ADR-0067, ADR-0051): its Repo Slot, whether it signs, and — for a slot the
@@ -120,10 +147,12 @@ export type SecretPlacements = {
120
147
  held: HeldPodPlan[];
121
148
  /** Every name some pod holds — what `heldSecretsOf` normalizes. */
122
149
  claimed: Set<string>;
123
- /** The Instance Effector, when an Effect is placed there; absent otherwise. */
150
+ /** The Instance Effector, when an Effect is placed there or a Hook needs it; absent otherwise. */
124
151
  instance?: EffectorPlan;
125
152
  /** Each Workspace whose body places an Effect in its Sandbox Effector. */
126
153
  sandbox: SandboxEffectorPlan[];
154
+ /** The secrets of the `token` Hooks, in list order: only their digests reach the Orchestrator. */
155
+ hookTokens: HookTokenSecret[];
127
156
  /** One line per secret, then one per pod: where each goes, what each holds. */
128
157
  report: string[];
129
158
  warnings: string[];
@@ -151,8 +180,8 @@ export function refuseEffectSecrets(config: JR2Config, walk: PlacementWalk): voi
151
180
  /**
152
181
  * A Workspace with no Repo Slot (ADR-0051) has no branch and no commit, so the two Sandbox Effector kinds that
153
182
  * run on a commit of a slot's branch have none: a `push`, and a `command` with an Effect secret
154
- * (ADR-0067). Refused here, before anything is built, as the invoke would refuse them mid-run. A
155
- * file-body `http` reads `/work` and stays.
183
+ * (ADR-0067). A `worktree()` has no clone to cut from (ADR-0075). Refused here, before anything is
184
+ * built, as the invoke would refuse them mid-run. A file-body `http` reads `/work` and stays.
156
185
  */
157
186
  function refuseWithNoSlot(ws: PlacedWorkspace): void {
158
187
  if (!ws.pod.noSlot) return;
@@ -165,6 +194,13 @@ function refuseWithNoSlot(ws: PlacedWorkspace): void {
165
194
  `Slot: ${why} (ADR-0051)`,
166
195
  );
167
196
  }
197
+ const [cut] = ws.pod.cuts ?? [];
198
+ if (cut !== undefined) {
199
+ throw new Error(
200
+ `workflow "${ws.workflow}": worktree "${cut}" runs in ${workspaceLabel(ws)}, which has no Repo Slot: ` +
201
+ `${needsASlot("worktree")} (ADR-0075)`,
202
+ );
203
+ }
168
204
  }
169
205
 
170
206
  /**
@@ -177,7 +213,7 @@ export function secretPlacementsOf(
177
213
  walk: PlacementWalk,
178
214
  lookups: Pick<HeldLookups, "secretKeys" | "valueLength"> = {},
179
215
  ): SecretPlacements {
180
- const { entries, namedBy, instancePlaced } = namesOf(config, walk);
216
+ const { entries, namedBy, instancePlaced, hooks, tokenHooks } = namesOf(config, walk);
181
217
  const { pods, redundant } = heldPodsOf(config, walk, new Set(entries.map((e) => e.name)));
182
218
  const claimed = new Set(pods.flatMap((p) => p.names));
183
219
 
@@ -185,6 +221,7 @@ export function secretPlacementsOf(
185
221
  const warnings: string[] = [];
186
222
  const secrets: EffectSecret[] = [];
187
223
  const values: Record<string, string> = {};
224
+ const hookTokens: HookTokenSecret[] = [];
188
225
  for (const e of entries) {
189
226
  const named = namedBy.get(e.name) ?? [];
190
227
  if (named.length > 0) {
@@ -208,10 +245,21 @@ export function secretPlacementsOf(
208
245
  )})`,
209
246
  );
210
247
  const where = [...(heldIn.length ? [`Held in ${heldIn.join(", ")}`] : []), ...effectIn];
248
+ // A `token` Hook names its secret: the Orchestrator gets its digest, never its value (ADR-0078).
249
+ const byToken = tokenHooks.get(e.name);
250
+ if (byToken !== undefined) {
251
+ const source = sourceOf(e, lookups);
252
+ hookTokens.push(
253
+ source.kind === "literal"
254
+ ? { name: e.name, value: e.value as string }
255
+ : { name: e.name, secret: source.secret, key: source.key },
256
+ );
257
+ where.push(`a digest in the Orchestrator (${byToken.map((h) => `hook ${h}`).join(", ")})`);
258
+ }
211
259
  if (where.length === 0) {
212
260
  warnings.push(
213
261
  `${e.at} (${e.name}) is named by nothing; goes nowhere — no workspace() lists it, no Agent's provider ` +
214
- "reads it, and no Effect names it (ADR-0059, ADR-0068)",
262
+ "reads it, and no Effect or Hook names it (ADR-0059, ADR-0068)",
215
263
  );
216
264
  continue;
217
265
  }
@@ -224,21 +272,32 @@ export function secretPlacementsOf(
224
272
  );
225
273
  }
226
274
  for (const p of pods) {
227
- const holds = p.names.map((n) => (p.byProvider.includes(n) ? `${n} (provider)` : n));
275
+ const holds = p.names.map((n) =>
276
+ p.byProvider.includes(n)
277
+ ? `${n} (provider)`
278
+ : p.byServer?.[n] !== undefined
279
+ ? `${n} (mcp ${p.byServer[n]!.join(", ")})`
280
+ : n,
281
+ );
228
282
  report.push(`${p.label}: Custodian holds ${holds.length ? holds.join(", ") : "nothing"}`);
229
283
  }
230
284
  for (const { label, name } of redundant) {
231
285
  report.push(`${label}: secrets lists ${name}, which its Agents' provider claims already — redundant`);
232
286
  }
233
287
 
234
- const instanceSecrets = new Set(instancePlaced.flatMap((p) => p.effect.secrets));
288
+ const instanceSecrets = new Set([
289
+ ...instancePlaced.flatMap((p) => effectSecretsOf(config, p.effect)),
290
+ ...hooks.flatMap((h) => (h.secret === undefined ? [] : [h.secret])),
291
+ ]);
235
292
  const instance =
236
- instancePlaced.length === 0
293
+ instancePlaced.length === 0 && hooks.length === 0
237
294
  ? undefined
238
295
  : effectorPlan(
239
296
  instancePlaced,
297
+ hooks,
240
298
  secrets.filter((s) => instanceSecrets.has(s.name)),
241
299
  values,
300
+ config,
242
301
  );
243
302
  if (instance) {
244
303
  report.push(
@@ -268,7 +327,7 @@ export function secretPlacementsOf(
268
327
  `${pushes.length ? `; pushes ${pushes.join(", ")}` : ""}`,
269
328
  );
270
329
  }
271
- return { held: pods, claimed, ...(instance ? { instance } : {}), sandbox, report, warnings };
330
+ return { held: pods, claimed, ...(instance ? { instance } : {}), sandbox, hookTokens, report, warnings };
272
331
  }
273
332
 
274
333
  /** A Workspace's Sandbox Effector: the secrets its Effects name, the targets its file-body https call, its
@@ -347,34 +406,123 @@ function pushesOf(config: JR2Config, ws: PlacedWorkspace): SandboxPush[] {
347
406
  return out;
348
407
  }
349
408
 
350
- function effectorPlan(placed: InstancePlaced[], secrets: EffectSecret[], values: Record<string, string>): EffectorPlan {
409
+ function effectorPlan(
410
+ placed: InstancePlaced[],
411
+ hooks: InstanceHook[],
412
+ secrets: EffectSecret[],
413
+ values: Record<string, string>,
414
+ config: JR2Config,
415
+ ): EffectorPlan {
351
416
  const targets: EffectorTargetConfig[] = [];
417
+ const add = (t: EffectorTargetConfig): void => {
418
+ const same = (x: EffectorTargetConfig) =>
419
+ x.scheme === t.scheme && x.host === t.host && x.port === t.port && x.secrets.join() === t.secrets.join();
420
+ if (!targets.some(same)) targets.push(t);
421
+ };
422
+ const mcp: Record<string, EffectorMcpServer> = {};
423
+ const timeouts: number[] = [];
352
424
  for (const { effect } of placed) {
425
+ if (effect.kind === "mcp") {
426
+ // The server's url and secret are the config's (ADR-0079). An Open server, and one the block
427
+ // does not declare as a remote server, are refused before this (`mcpRefusals`).
428
+ const server = remoteServerOf(config, effect.server);
429
+ if (server === undefined) continue;
430
+ const timeoutMs = mcpTimeoutMs(server.timeout) ?? 0;
431
+ mcp[effect.server!] = { url: server.url, ...(server.secret ? { secret: server.secret } : {}), timeoutMs };
432
+ add({ ...originOf(server.url), secrets: server.secret ? [server.secret] : [] });
433
+ timeouts.push(effect.timeoutMs > 0 ? effect.timeoutMs : timeoutMs);
434
+ continue;
435
+ }
436
+ if (effect.kind === "http") timeouts.push(effect.timeoutMs);
353
437
  // An Open url or an Open dispatch target has no target, so it allows nothing: `jr2 up` refuses
354
438
  // it before this (ADR-0054). A dispatch's target is its origin, its one secret the token
355
439
  // (ADR-0071).
356
440
  if (effect.target === undefined) continue;
357
- const t = { ...effect.target, secrets: [...effect.secrets].sort() };
358
- const same = (x: EffectorTargetConfig) =>
359
- x.scheme === t.scheme && x.host === t.host && x.port === t.port && x.secrets.join() === t.secrets.join();
360
- if (!targets.some(same)) targets.push(t);
441
+ add({ ...effect.target, secrets: [...effect.secrets].sort() });
361
442
  }
443
+ // A Hook's follow-up is one `http` call with no secret, to a host its kind declares (ADR-0068).
444
+ for (const hook of hooks) for (const post of hook.posts) add({ ...post, secrets: [] });
362
445
  return {
363
446
  placement: "instance",
364
447
  secrets,
365
448
  values,
366
449
  targets,
367
- // The grace a rollout gives a replica covers the http Effects alone (ADR-0071): a dispatch is
368
- // idempotent by construction, so a `wait` a rollout cuts is sent again under its key and its
369
- // poll resumes, and a wait of hours never stretches the Effector's grace.
370
- longestTimeoutMs: Math.max(0, ...placed.flatMap((p) => (p.effect.kind === "http" ? [p.effect.timeoutMs] : []))),
450
+ mcp,
451
+ // The grace a rollout gives a replica covers the http and mcp Effects alone (ADR-0071): a
452
+ // dispatch is idempotent by construction, so a `wait` a rollout cuts is sent again under its
453
+ // key and its poll resumes, and a wait of hours never stretches the Effector's grace. An mcp
454
+ // Effect with no timeout of its own waits for its server's (ADR-0079).
455
+ longestTimeoutMs: Math.max(0, ...timeouts),
371
456
  effects: placed.map((p) => `${p.workflow}/${p.effect.name}`),
457
+ hooks: hooks.map((h) => h.name),
372
458
  };
373
459
  }
374
460
 
375
- /** An Effect the Instance Effector runs: an http one with no Worktree file, or a dispatch with a
376
- * target (ADR-0068, ADR-0071). */
377
- type InstancePlaced = { workflow: string; effect: Extract<CarriedEffect, { kind: "http" | "dispatch" }> };
461
+ /** A Hook the Instance Effector serves (ADR-0068): its `mac` secret's name, when its verifier is
462
+ * `mac`, and the targets of its kind's `posts`. */
463
+ type InstanceHook = { name: string; secret?: string; posts: Array<Omit<EffectorTargetConfig, "secrets">> };
464
+
465
+ /**
466
+ * The Hooks the Instance Effector serves (ADR-0068, ADR-0078): each one with a `mac` verifier, whose
467
+ * secret only that Effector holds, and each one whose kind declares `posts`, whose follow-up it
468
+ * sends. A `token` Hook's secret is a digest in the Orchestrator, and an `ed25519` Hook holds a
469
+ * public key: neither needs the Effector unless its kind posts.
470
+ */
471
+ function instanceHooksOf(config: JR2Config): InstanceHook[] {
472
+ const out: InstanceHook[] = [];
473
+ for (const [name, hook] of Object.entries(config.hooks ?? {})) {
474
+ const { kind, credential } = hookParts(hook);
475
+ const secret = "mac" in kind.verify && "secret" in credential ? credential.secret.secret : undefined;
476
+ const posts = (kind.posts ?? []).map((prefix) => {
477
+ const url = new URL(prefix);
478
+ return { scheme: "https" as const, host: url.hostname, port: url.port === "" ? 443 : Number(url.port) };
479
+ });
480
+ if (secret === undefined && posts.length === 0) continue;
481
+ out.push({ name, ...(secret !== undefined ? { secret } : {}), posts });
482
+ }
483
+ return out;
484
+ }
485
+
486
+ /** The Hooks with a `token` verifier, by the name of the secret each names (ADR-0078). */
487
+ function tokenHooksOf(config: JR2Config): Map<string, string[]> {
488
+ const out = new Map<string, string[]>();
489
+ for (const [name, hook] of Object.entries(config.hooks ?? {})) {
490
+ const { kind, credential } = hookParts(hook);
491
+ if (!("token" in kind.verify) || !("secret" in credential)) continue;
492
+ const secret = credential.secret.secret;
493
+ out.set(secret, [...(out.get(secret) ?? []), name]);
494
+ }
495
+ return out;
496
+ }
497
+
498
+ /** An Effect the Instance Effector runs: an http one with no Worktree file, a dispatch with a
499
+ * target, or an mcp one (ADR-0068, ADR-0071, ADR-0079). */
500
+ type InstancePlaced = { workflow: string; effect: Extract<CarriedEffect, { kind: "http" | "dispatch" | "mcp" }> };
501
+
502
+ /** The remote server of the `mcp` block that an `mcp` Effect names, or undefined: an Open server,
503
+ * one the block does not declare, or a stdio one (ADR-0079). */
504
+ function remoteServerOf(config: JR2Config, name: string | undefined): McpRemoteServer | undefined {
505
+ if (name === undefined || !Object.hasOwn(config.mcp ?? {}, name)) return undefined;
506
+ const server = config.mcp![name]!;
507
+ return isStdioServer(server) ? undefined : server;
508
+ }
509
+
510
+ /** The secrets an Effect names, as the placement reads them: its own, or for an `mcp` Effect its
511
+ * server's, from the config (ADR-0079). */
512
+ function effectSecretsOf(config: JR2Config, effect: CarriedEffect): string[] {
513
+ if (effect.kind !== "mcp") return [...(effect.secrets ?? [])];
514
+ const secret = remoteServerOf(config, effect.server)?.secret;
515
+ return secret === undefined ? [] : [secret];
516
+ }
517
+
518
+ /** An MCP server url's origin as the Effector's fence reads it: scheme, host, port. `jr2 up` has
519
+ * checked the url (`mcpUrlRefusals`), so it parses. */
520
+ function originOf(url: string): { scheme: "http" | "https"; host: string; port: number } {
521
+ const u = new URL(url);
522
+ const scheme = u.protocol === "http:" ? "http" : "https";
523
+ const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
524
+ return { scheme, host, port: u.port ? Number(u.port) : scheme === "http" ? 80 : 443 };
525
+ }
378
526
 
379
527
  /** A Workspace as a human reads it: its workflow and the placement route to it, an inline hop as
380
528
  * `invoked inline in state <id>`. */
@@ -417,6 +565,19 @@ function heldPodsOf(
417
565
  };
418
566
  const pods: HeldPodPlan[] = [];
419
567
  const redundant: Array<{ label: string; name: string }> = [];
568
+ // The secret of each remote MCP server that the pod's Agents name (ADR-0079), with the servers:
569
+ // the Agent claims it by naming the server, as it claims a model key by naming a model. A name
570
+ // the `mcp` block does not declare is refused by the walk's own check (`mcpRefusals`), not here.
571
+ const serverClaims = (servers: Iterable<string>): Record<string, string[]> => {
572
+ const out: Record<string, string[]> = {};
573
+ for (const server of new Set(servers)) {
574
+ const declared = config.mcp?.[server];
575
+ const secret = declared && !isStdioServer(declared) ? declared.secret : undefined;
576
+ if (secret === undefined) continue;
577
+ out[secret] = [...(out[secret] ?? []), server].sort();
578
+ }
579
+ return out;
580
+ };
420
581
  for (const ws of walk.workspaces) {
421
582
  const label = workspaceLabel(ws);
422
583
  for (const name of ws.pod.secrets) {
@@ -428,18 +589,27 @@ function heldPodsOf(
428
589
  }
429
590
  const byProvider = providerClaims(config, ws.pod.models);
430
591
  for (const name of ws.pod.secrets) if (byProvider.includes(name)) redundant.push({ label, name });
592
+ const byServer = serverClaims((ws.pod.mcp ?? []).map((m) => m.entry.server));
431
593
  const plan: HeldPodPlan = {
432
594
  pod: workspaceHeldPod({ workflow: ws.workflow, route: ws.pod.route }),
433
595
  label,
434
- names: ordered([...ws.pod.secrets, ...byProvider]),
596
+ names: ordered([...ws.pod.secrets, ...byProvider, ...Object.keys(byServer)]),
435
597
  byProvider: ordered(byProvider),
598
+ ...(Object.keys(byServer).length ? { byServer } : {}),
436
599
  };
437
600
  if (pods.some((p) => p.pod === plan.pod)) throw sharedPodKey(plan.pod, label);
438
601
  pods.push(plan);
439
602
  }
440
603
  if (walk.instanceHarness) {
441
604
  const byProvider = ordered(providerClaims(config, walk.instanceHarness.models));
442
- pods.push({ pod: INSTANCE_HARNESS_HELD_POD, label: "the Instance Harness", names: byProvider, byProvider });
605
+ const byServer = serverClaims(walk.instanceHarness.servers ?? []);
606
+ pods.push({
607
+ pod: INSTANCE_HARNESS_HELD_POD,
608
+ label: "the Instance Harness",
609
+ names: ordered([...byProvider, ...Object.keys(byServer)]),
610
+ byProvider,
611
+ ...(Object.keys(byServer).length ? { byServer } : {}),
612
+ });
443
613
  }
444
614
  return { pods, redundant };
445
615
  }
@@ -462,7 +632,8 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
462
632
  const namedBy = new Map<string, Array<{ label: string; where: string }>>();
463
633
  const instancePlaced = walk.effects.filter(
464
634
  (p): p is InstancePlaced =>
465
- (p.effect.kind === "http" || p.effect.kind === "dispatch") && p.effect.placement === "instance",
635
+ (p.effect.kind === "http" || p.effect.kind === "dispatch" || p.effect.kind === "mcp") &&
636
+ p.effect.placement === "instance",
466
637
  );
467
638
  // Keyed by the Effect's placement route and slot name, as workspacePodsOf keys a pod's Effects:
468
639
  // two slots of one name in one workflow — one in a workspace(), one outside it — are two Effects,
@@ -482,23 +653,52 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
482
653
  "workspace()'s body, or send a json or text body (ADR-0068)",
483
654
  );
484
655
  }
656
+ // A Hook with a `mac` verifier names its secret as an Effect names one, and it is placed in the
657
+ // Instance Effector alone (ADR-0068).
658
+ const hooks = instanceHooksOf(config);
659
+ const tokenHooks = tokenHooksOf(config);
660
+ for (const [secret, names] of tokenHooks) {
661
+ if (byName.has(secret)) continue;
662
+ throw new Error(
663
+ `hooks.${names[0]} names secret("${secret}"), which the \`secrets\` list does not hold — add it: ` +
664
+ `secrets: [{ name: "${secret}", value: process.env.${envish(secret)} }] (ADR-0078)`,
665
+ );
666
+ }
667
+ for (const hook of hooks) {
668
+ if (hook.secret === undefined) continue;
669
+ if (!byName.has(hook.secret)) {
670
+ throw new Error(
671
+ `hooks.${hook.name} names secret("${hook.secret}"), which the \`secrets\` list does not hold — add it: ` +
672
+ `secrets: [{ name: "${hook.secret}", value: process.env.${envish(hook.secret)} }] (ADR-0068)`,
673
+ );
674
+ }
675
+ namedBy.set(hook.secret, [
676
+ ...(namedBy.get(hook.secret) ?? []),
677
+ { label: `hook ${hook.name}`, where: "the Instance Effector" },
678
+ ]);
679
+ }
485
680
  for (const { workflow, effect } of walk.effects) {
486
681
  const label = `${workflow}/${effect.name}`;
487
682
  const where =
488
683
  effect.placement === "instance"
489
684
  ? "the Instance Effector"
490
685
  : (sandboxOf.get(keyOf(workflow, effect)) ?? "the Sandbox Effector of the Workspace that invokes it");
491
- for (const name of effect.secrets ?? []) {
686
+ for (const name of effectSecretsOf(config, effect)) {
492
687
  if (!byName.has(name)) {
688
+ // An mcp Effect never writes secret(): the server it calls, in the `mcp` block, names it.
689
+ const names =
690
+ effect.kind === "mcp"
691
+ ? `calls the MCP server "${effect.server}", whose secret "${name}"`
692
+ : `names secret("${name}"), which`;
493
693
  throw new Error(
494
- `workflow "${workflow}": the ${effect.kind} Effect "${effect.name}" names secret("${name}"), which the ` +
694
+ `workflow "${workflow}": the ${effect.kind} Effect "${effect.name}" ${names} the ` +
495
695
  `\`secrets\` list does not hold — add it: secrets: [{ name: "${name}", value: process.env.${envish(name)} }] (ADR-0068)`,
496
696
  );
497
697
  }
498
698
  namedBy.set(name, [...(namedBy.get(name) ?? []), { label, where }]);
499
699
  }
500
700
  }
501
- return { entries, namedBy, instancePlaced };
701
+ return { entries, namedBy, instancePlaced, hooks, tokenHooks };
502
702
  }
503
703
 
504
704
  /** An Effect secret shorter than 8 characters is refused, and the refusal says why. */
@@ -508,10 +708,23 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
508
708
  * length leaves; `jr2 up` keeps no value of the user's Secret (ADR-0068).
509
709
  */
510
710
  export function storedValueLength(base64: string): number {
711
+ return storedBytes(base64).length;
712
+ }
713
+
714
+ /**
715
+ * The sha-256, in hex, of a value a Kubernetes Secret holds, trimmed as `storedValueLength` trims
716
+ * it: what `jr2-hook-digests` holds for a `token` Hook whose secret is a key of the user's Secret
717
+ * (ADR-0078). Only the digest leaves.
718
+ */
719
+ export function storedValueSha256(base64: string): string {
720
+ return createHash("sha256").update(storedBytes(base64)).digest("hex");
721
+ }
722
+
723
+ function storedBytes(base64: string): Buffer {
511
724
  let bytes = Buffer.from(base64, "base64");
512
725
  if (bytes.at(-1) === 0x0a) bytes = bytes.subarray(0, -1);
513
726
  if (bytes.at(-1) === 0x0d) bytes = bytes.subarray(0, -1);
514
- return bytes.length;
727
+ return bytes;
515
728
  }
516
729
 
517
730
  function shortRefusal(e: { at: string; name: string }, length: number): void {
package/src/server.ts CHANGED
@@ -16,6 +16,17 @@
16
16
  // a publish door anyone outside it can open.
17
17
  // JR2_SIGNING_KEY base64 key Sandbox tokens are signed with; from the Secret so live Sandboxes
18
18
  // survive a pod restart. Absent → minted into `<dir>/.jr2/secret` (dev-grade).
19
+ // JR2_CALLER_DIGESTS the file of the named Caller tokens' digests (ADR-0078). Default, deployed:
20
+ // the `jr2-caller-digests` mount; host-booted: none. Read again when it
21
+ // changes, so a rotation needs no restart.
22
+ // JR2_HOOK_DIGESTS the file of the `token` Hooks' secrets' digests (ADR-0078). Default,
23
+ // deployed: the `jr2-hook-digests` mount; host-booted: none. Read again when
24
+ // it changes.
25
+ // JR2_INSTANCE_EFFECTOR the Instance Effector's address, `host:port` (ADR-0068). Default,
26
+ // deployed: its Service; host-booted: none. A host-booted fixture names its
27
+ // own, which speaks the same signed wire (the e2e tier's `mac` Hook check).
28
+ // JR2_STATE_VOLUME the size of the state volume claim that `jr2 up` applied (ADR-0065): the
29
+ // space guard measures against it. Absent: `runs.volume`, or `5Gi`.
19
30
  // JR2_NAMESPACE the pod's own namespace (Deployment fieldRef) — presence = "deployed":
20
31
  // Sandboxes are driven in it, and the Custodians' route home is Service DNS.
21
32
  //
@@ -46,7 +57,7 @@ import { join } from "node:path";
46
57
  import { resolveAssembly, type ResolvedAssembly } from "./assembly.ts";
47
58
  import { KIT_VERSION, loadConfig, type JR2Config } from "./config.ts";
48
59
  import { readImageRefs } from "./images.ts";
49
- import { readMountDigests } from "./mount-digest.ts";
60
+ import { loadedDigests, readMountDigests } from "./mount-digest.ts";
50
61
  import { GIT_CLONE_MANIFEST_KEY, readGitCloneManifest } from "./git-clone.ts";
51
62
  import { loadWorkflows, startInstance, type RunningInstance } from "./instance.ts";
52
63
  import {
@@ -60,6 +71,12 @@ import {
60
71
  INSTANCE_HARNESS_SERVICE,
61
72
  INSTANCE_EFFECTOR,
62
73
  EFFECTOR_PORT,
74
+ CALLER_DIGESTS_KEY,
75
+ CALLER_DIGESTS_MOUNT,
76
+ CALLER_DIGESTS_SECRET,
77
+ HOOK_DIGESTS_KEY,
78
+ HOOK_DIGESTS_MOUNT,
79
+ HOOK_DIGESTS_SECRET,
63
80
  } from "./names.ts";
64
81
  import type { AnyStateMachine } from "xstate";
65
82
  import { partsOf, type CarriedRepo } from "./parts.ts";
@@ -69,7 +86,7 @@ import { kubeRepos, type RepoResources } from "./repos.ts";
69
86
  import { SANDBOX_EFFECTOR_PORT, kubeSandbox } from "./sandbox-kube.ts";
70
87
  import { sandboxEffectorService } from "./sandbox-effector.ts";
71
88
  import { watchSandboxes, type SandboxWatch } from "./sandbox-watch.ts";
72
- import { loadSigningKey, mintInstanceToken } from "./tokens.ts";
89
+ import { loadSigningKey, mintInstanceToken, watchCallerDigests } from "./tokens.ts";
73
90
  import type { SandboxPort } from "./workspace.ts";
74
91
 
75
92
  export type ServerMainOptions = {
@@ -100,6 +117,18 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
100
117
  // From the instance Secret when deployed; per-boot for a host-booted fixture.
101
118
  const instanceToken = env.JR2_INSTANCE_TOKEN ?? mintInstanceToken();
102
119
  const publishToken = env.JR2_PUBLISH_TOKEN ?? mintInstanceToken();
120
+ // The named Callers' digests (ADR-0078), from the mount `jr2 up` writes. A host-booted process
121
+ // has none unless the env names a file.
122
+ const digestsPath =
123
+ env.JR2_CALLER_DIGESTS ?? (namespace !== undefined ? join(CALLER_DIGESTS_MOUNT, CALLER_DIGESTS_KEY) : undefined);
124
+ const digests = digestsPath !== undefined ? watchCallerDigests(digestsPath) : undefined;
125
+ // The `token` Hooks' digests (ADR-0078): the same form, another mount.
126
+ const hookDigestsPath =
127
+ env.JR2_HOOK_DIGESTS ?? (namespace !== undefined ? join(HOOK_DIGESTS_MOUNT, HOOK_DIGESTS_KEY) : undefined);
128
+ const hookDigests = hookDigestsPath !== undefined ? watchCallerDigests(hookDigestsPath) : undefined;
129
+ const instanceEffector =
130
+ env.JR2_INSTANCE_EFFECTOR ??
131
+ (namespace !== undefined ? `${INSTANCE_EFFECTOR}.${namespace}.svc:${EFFECTOR_PORT}` : undefined);
103
132
 
104
133
  // The current Assembly (ADR-0069), and its config and Workflows read from its ROOT, never from
105
134
  // the folder directly. A published Assembly that does not load (a Kit bump that broke an import,
@@ -247,15 +276,27 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
247
276
  assembly: resolved,
248
277
  port,
249
278
  hostname: env.HOST ?? "0.0.0.0",
279
+ ...(env.JR2_STATE_VOLUME ? { stateVolume: env.JR2_STATE_VOLUME } : {}),
250
280
  instanceToken,
251
281
  publishToken,
282
+ ...(digests ? { callerDigests: digests.current } : {}),
283
+ ...(hookDigests ? { hookDigests: hookDigests.current } : {}),
252
284
  signingKey,
253
285
  sandbox,
254
286
  dataPlane,
255
287
  // The door reads the same image map a provision reads (ADR-0069): a `file:` context whose
256
288
  // digest it lacks is refused at the publish rather than at the first provision.
257
289
  ...(dataPlane
258
- ? { imageMap: () => readImageRefs(join(IMAGES_MOUNT, IMAGES_KEY)), mounts: () => readMountDigests() }
290
+ ? {
291
+ imageMap: () => readImageRefs(join(IMAGES_MOUNT, IMAGES_KEY)),
292
+ // The ConfigMaps as the mounts hold them, and the digests Secrets as this process LOADED
293
+ // them (ADR-0078): a token works when the authenticator holds its digest, not when the
294
+ // file has it.
295
+ mounts: async () => ({
296
+ ...(await readMountDigests()),
297
+ ...loadedDigests({ [CALLER_DIGESTS_SECRET]: digests, [HOOK_DIGESTS_SECRET]: hookDigests }),
298
+ }),
299
+ }
259
300
  : {}),
260
301
  // A publish landed (ADR-0069): its config is the one that applies from now on, and its
261
302
  // Machines may bind other Repos, so the Repo pass runs again. Synchronous: the walk is over
@@ -300,7 +341,7 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
300
341
  ...(namespace ? { orchestratorName: namespace } : {}),
301
342
  // Where an http Effect runs (ADR-0068): the Instance Effector's headless Service. `jr2 up`
302
343
  // converges it whenever a registered Machine carries an http Effect.
303
- ...(namespace ? { instanceEffector: `${INSTANCE_EFFECTOR}.${namespace}.svc:${EFFECTOR_PORT}` } : {}),
344
+ ...(instanceEffector !== undefined ? { instanceEffector } : {}),
304
345
  // Where a push, a file-body http or a credentialed command runs (ADR-0068): a Sandbox's Effector, by
305
346
  // the headless Service the provision writes beside it when `jr2 up` placed an Effect there.
306
347
  ...(namespace
@@ -311,6 +352,14 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
311
352
  : {}),
312
353
  });
313
354
  held = inst.heldMachines;
355
+ for (const watched of [digests, hookDigests]) {
356
+ if (!watched) continue;
357
+ const close = inst.close;
358
+ inst.close = async () => {
359
+ watched.stop();
360
+ await close();
361
+ };
362
+ }
314
363
  // The watch ends with the instance: its stream is the one thing here that never settles alone.
315
364
  if (watch) {
316
365
  const close = inst.close;