@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
@@ -0,0 +1,238 @@
1
+ ---
2
+ title: Write a Hook kind
3
+ description: Write the kind of a service that jr2 ships no kind for, declare a Hook of it, and hear it at a Gate.
4
+ ---
5
+
6
+ # Write a Hook kind
7
+
8
+ Write a [Hook kind](../reference/glossary.md#hook-kind) for an outside service that jr2 ships no kind for, and that
9
+ [`webhook()`](../reference/hooks.md#webhook) cannot read.
10
+
11
+ `webhook()` reads a service that sends JSON and signs only the raw body. Write a kind with
12
+ [`defineHookKind()`](../reference/hooks.md#definehookkind) when the service does one of these:
13
+
14
+ - It signs more than the body, such as a time and the body.
15
+ - It sends a body that is not JSON, such as a form.
16
+ - It sends a handshake that needs an answer.
17
+ - A person reads the answer, as in a chat command.
18
+
19
+ The example is a kind for Stripe. Stripe signs `<t>.<raw body>` with the signing secret of the endpoint, and sends the
20
+ time `t` and the signature in the header `Stripe-Signature: t=<seconds>,v1=<hex>`. The type of the event is the event
21
+ key, and the id of the event is the id of the delivery.
22
+
23
+ The interface of a kind is `@jr2/orchestrator/hooks`. It is part of `@jr2/orchestrator`, so your
24
+ [Instance](../reference/glossary.md#instance) installs nothing more.
25
+
26
+ ## Write the kind
27
+
28
+ Save the kind as `hooks/stripe.ts`, beside `workflows/`. A kind is not a Workflow, so it does not go in `workflows/`:
29
+
30
+ ```ts
31
+ import { z } from "zod";
32
+ import { defineHookKind, json } from "@jr2/orchestrator/hooks";
33
+
34
+ // The payload of each event key that a handler hears. Name only the fields that the handlers read:
35
+ // a loose object keeps the other fields, so a new field from Stripe breaks nothing.
36
+ const invoice = z.looseObject({
37
+ data: z.looseObject({ object: z.looseObject({ id: z.string(), customer: z.string() }) }),
38
+ });
39
+
40
+ export const stripeEvents = {
41
+ "invoice.paid": invoice,
42
+ "invoice.payment_failed": invoice,
43
+ };
44
+
45
+ export const stripe = defineHookKind({
46
+ api: 1,
47
+ id: "stripe",
48
+ events: stripeEvents,
49
+ verify: {
50
+ mac: {
51
+ alg: "sha256",
52
+ // Stripe-Signature: t=<seconds>,v1=<hex>. Stripe signs "<t>.<raw body>". The door checks t
53
+ // against its clock, and the Instance Effector checks the HMAC.
54
+ signed: (req) => {
55
+ const fields = (req.headers["stripe-signature"] ?? "").split(",").map((f) => f.split("="));
56
+ const t = fields.find(([name]) => name === "t")?.[1];
57
+ const v1 = fields.find(([name]) => name === "v1")?.[1];
58
+ if (!t || !/^\d+$/.test(t) || !v1) return { unsigned: "no t and v1 in the Stripe-Signature header" };
59
+ return { bytes: [t, ".", req.body], signature: { hex: v1 }, at: Number(t) };
60
+ },
61
+ },
62
+ },
63
+ // The event's type is the event key, and the event's id is the id of the delivery.
64
+ read: (req) => {
65
+ const event = json(req) as { readonly id?: unknown; readonly type?: unknown } | null;
66
+ if (typeof event?.id !== "string" || typeof event.type !== "string") {
67
+ throw new Error("the body is not a Stripe event");
68
+ }
69
+ return { key: event.type, id: event.id, payload: event };
70
+ },
71
+ });
72
+
73
+ // The kind's id and payloads, for the types of the handlers.
74
+ declare module "@jr2/orchestrator/hooks" {
75
+ interface HookKinds {
76
+ stripe: typeof stripe;
77
+ }
78
+ }
79
+ ```
80
+
81
+ A kind has these parts:
82
+
83
+ - `api: 1`: the version of the interface of a kind.
84
+ - `id`: the key of the kind in the `hooks` of a Gate or a Source. It is lowercase letters, digits and `-`, and it starts
85
+ with a letter. One id is one kind in an Assembly.
86
+ - `events`: a zod schema for each event key that a handler can hear. The door parses each payload with the schema of its
87
+ key before a handler gets it. Use `z.looseObject`, and name only the fields that the handlers read. A key that ends in
88
+ `.*` gives the schema of each key with that prefix.
89
+ - `verify`: one of `mac`, `token` or `ed25519`. For `mac` and `ed25519`, `signed` gives the bytes that the service
90
+ signed, the signature without its prefix, and `at`, the time of the delivery in seconds. Give `at` only when the
91
+ signature covers it. The door refuses a time that is more than 300 seconds from its clock. When the request does not
92
+ have a signature, give `{ unsigned: "<why>" }`. For `token`, give the header of the token.
93
+ - `read`: the request after the verifier, as a delivery or as a handshake. A delivery has the event key, the id, the
94
+ payload, and `onBehalfOf` when the service names the party that acted. A handshake is `{ handshake: { body } }`, and
95
+ goes to no handler. A throw is the status 400.
96
+
97
+ A kind whose answer a person reads also has `answer`, the envelope of the text, and `maxText`, the limit of the service
98
+ on its length. Its `read` gives `readBy: "person"`. When the answer gives a follow-up post, `posts` lists the URLs that
99
+ the post can go to. See the [`slack` kind](../reference/hooks.md#slack).
100
+
101
+ A kind is pure and synchronous: it does no I/O, gives no Promise, and reads no clock. It never gets the secret. The
102
+ `declare module` block adds the kind to `HookKinds`, so that the handlers of a Gate or a Source get the types of its
103
+ payloads.
104
+
105
+ ## Declare a Hook of the kind
106
+
107
+ Add the Hook to `jr2.config.ts`, and its secret to the `secrets` list:
108
+
109
+ ```ts
110
+ import { defineConfig, secret } from "@jr2/orchestrator";
111
+ import { stripe } from "./hooks/stripe.ts";
112
+
113
+ export default defineConfig({
114
+ // The signing secret of the Stripe endpoint. jr2 up places it in the Instance Effector.
115
+ secrets: [{ name: "STRIPE_HOOK", value: process.env.STRIPE_HOOK }],
116
+ hooks: {
117
+ // POST /hooks/billing. The Hook may answer the Gates of dunning alone.
118
+ billing: stripe({ secret: secret("STRIPE_HOOK"), may: { answer: ["dunning"] } }),
119
+ },
120
+ });
121
+ ```
122
+
123
+ The kind is also the constructor of its Hooks. It takes the credential and `may.answer`. For a `mac` kind,
124
+ [`jr2 up`](../reference/cli.md#up) puts the secret in the
125
+ [Instance Effector](../reference/glossary.md#instance-effector) only, which checks each signature. Put the value in
126
+ `.env`:
127
+
128
+ ```sh
129
+ STRIPE_HOOK=<signing secret>
130
+ ```
131
+
132
+ `<signing secret>` is the signing secret that Stripe shows for the endpoint. Give the endpoint the URL
133
+ `https://<host>/hooks/billing`.
134
+
135
+ ## Hear it at a Gate
136
+
137
+ Import the kind's types in the workflow module, and give the Gate slot a handler for each event key that it hears:
138
+
139
+ ```ts
140
+ import { z } from "zod";
141
+ import { defineEvent, fail, gate, jr2Setup } from "@jr2/orchestrator";
142
+ import type {} from "../hooks/stripe.ts";
143
+
144
+ const paid = defineEvent({
145
+ name: "paid",
146
+ description: "The customer paid the invoice.",
147
+ input: z.object({}),
148
+ });
149
+
150
+ // The Gate hears the stripe kind. A payment of another invoice is not for this Gate.
151
+ const payment = gate({
152
+ meta: z.object({ invoice: z.string() }),
153
+ delivers: [paid],
154
+ hooks: {
155
+ stripe: {
156
+ "invoice.paid": ({ payload, meta }) => (payload.data.object.id === meta.invoice ? { type: "paid" } : undefined),
157
+ },
158
+ },
159
+ });
160
+
161
+ export const machine = jr2Setup({
162
+ types: {} as { input: { invoice: string }; context: { invoice: string } },
163
+ events: [paid],
164
+ actors: { payment, fail: fail() },
165
+ }).createMachine({
166
+ id: "dunning",
167
+ input: z.object({ invoice: z.string() }),
168
+ context: ({ input }) => ({ invoice: input.invoice }),
169
+ initial: "waiting",
170
+ states: {
171
+ waiting: {
172
+ invoke: { src: "payment", input: ({ context }) => ({ meta: { invoice: context.invoice } }) },
173
+ on: { paid: "paid" },
174
+ // After seven days with no payment, the run stops waiting. A lost delivery cannot stop the
175
+ // run for longer than that.
176
+ after: { 604800000: "unpaid" },
177
+ },
178
+ paid: { type: "final" },
179
+ // Not a final state: the run ends `failed`, with the reason.
180
+ unpaid: { invoke: { src: "fail", input: () => ({ reason: "the invoice is not paid after seven days" }) } },
181
+ },
182
+ });
183
+ ```
184
+
185
+ A handler for a key that the kind does not give does not compile, and a handler reads only the fields that the schema
186
+ names. [Answer a Gate from a forge](answer-a-gate-from-a-forge.md) tells the other parts of a Gate slot, such as the
187
+ look. A Source hears a kind in the same way ([Start a run for each alert](start-a-run-for-each-alert.md)).
188
+
189
+ ## Put the kind in a package
190
+
191
+ A kind in a package of its own uses the same interface. Its package has a peer dependency on `@jr2/orchestrator` and
192
+ states `api: 1`, and its `jr2.config.ts` and its workflow modules import it as they import a local file. `jr2 up`
193
+ bundles the kind into the [Assembly](../reference/glossary.md#assembly). jr2 finds no kind in a folder, and loads none
194
+ while the Instance runs.
195
+
196
+ ## Rules
197
+
198
+ - `defineHookKind()` refuses an `id` of another form:
199
+ `a Hook kind's id is lowercase letters, digits and "-", and starts with a letter: <id>`
200
+ - `defineHookKind()` refuses an `api` that it does not know:
201
+ `Hook kind <id> states api <api>; this @jr2/orchestrator/hooks knows api 1 only`
202
+ - `defineHookKind()` refuses an event key with no zod schema: `Hook kind <id>: the event key <key> has no zod schema`
203
+ - `defineHookKind()` refuses a `verify` that is not one of the three:
204
+ `Hook kind <id>: verify is one of mac, ed25519 or token`
205
+ - `defineHookKind()` refuses a `mac` verifier without an algorithm or a `signed` function:
206
+ `Hook kind <id>: a mac verifier has an alg (sha256, sha1, sha512) and a signed function`
207
+ - `defineHookKind()` refuses a `token` header that is not lowercase:
208
+ `Hook kind <id>: a token verifier names its header in lowercase`
209
+ - `defineHookKind()` refuses a `posts` entry that is not an `https://` URL that ends in `/`:
210
+ `Hook kind <id>: a posts entry is an https:// url that ends in "/": <entry>`
211
+ - `defineHookKind()` refuses a `maxText` that is not a positive integer: `Hook kind <id>: maxText is a positive integer`
212
+ - The constructor of a `mac` or `token` kind refuses a credential that is not a named secret:
213
+ `a <id> Hook takes secret: a named secret, secret("NAME")`
214
+ - The constructor of an `ed25519` kind refuses a public key that is not 64 hex characters:
215
+ `a <id> Hook takes publicKey: the Ed25519 public key in hex (64 characters)`
216
+ - The constructor refuses a `may.answer` that is not `"all"` or a list of Workflow names:
217
+ `a <id> Hook's may.answer is "all" or a list of Workflow names`
218
+ - Two kinds with one id do not compile (`Subsequent property declarations must have the same type`), and the config load
219
+ refuses them:
220
+ `<file> at hooks: two different Hook kinds have the id <id>: one id is one kind in an Assembly (ADR-0078)`
221
+ - The type of `read` refuses `readBy: "person"` in a kind that has no `answer`.
222
+ - A request that `signed` gives `unsigned` for, or whose signature does not match, gets 401. A request that `read`
223
+ throws for gets 400: `the <kind> kind cannot read the delivery: <error>`
224
+ - `jr2 up` refuses a `mac` Hook whose secret is not in the `secrets` list:
225
+ ``hooks.<name> names secret("<NAME>"), which the `secrets` list does not hold — add it: secrets: [{ name: "<NAME>", value: process.env.<NAME> }] (ADR-0068)``
226
+
227
+ ## Read next
228
+
229
+ - [Callers and Hooks](../concepts/callers-and-hooks.md)
230
+ - [Answer a Gate from a forge](answer-a-gate-from-a-forge.md) and
231
+ [Start a run for each alert](start-a-run-for-each-alert.md)
232
+ - Reference: [`@jr2/orchestrator/hooks`](../reference/hooks.md), [`HookKind`](../reference/hooks.md#type-hookkind),
233
+ [`Signed`](../reference/hooks.md#type-signed), [`Delivery`](../reference/hooks.md#type-delivery) and the
234
+ [`hooks`](../reference/config.md#hooks) field of `jr2.config.ts`
235
+ - [ADR-0078](../reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md): a
236
+ Hook kind, the three verifiers, and the window
237
+ - [ADR-0068](../reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md): the Instance
238
+ Effector holds a `mac` secret
@@ -0,0 +1,351 @@
1
+ ---
2
+ title: Write a tool as an MCP server
3
+ description: Write a small MCP server in TypeScript, put it in the Sandbox Image, and give its tool to an Agent.
4
+ ---
5
+
6
+ # Write a tool as an MCP server
7
+
8
+ Write a tool for your [Agents](../reference/glossary.md#agent) as a small MCP server in TypeScript, put it in the
9
+ [Sandbox Image](../reference/glossary.md#sandbox-image), and give it to an Agent as a stdio server.
10
+
11
+ jr2 has no tool API of its own. A tool that you write is an MCP server: a remote server that you run yourself, or a
12
+ stdio server, which is a program in the Sandbox Image. An Agent in a [Sandbox](../reference/glossary.md#sandbox) gets
13
+ each tool of a stdio server as one of its [MCP tools](../reference/glossary.md#mcp-tools).
14
+
15
+ The server on this page has one tool, `next_version`. It gives the next version number after a release. It uses only the
16
+ modules of Node, so it needs no package. Save it as `images/default/semver-mcp.ts` in your
17
+ [Instance](../reference/glossary.md#instance), beside the `Dockerfile` that `jr2 init` wrote:
18
+
19
+ ```ts
20
+ // A stdio MCP server with one tool, next_version. It reads one JSON-RPC message on each line of
21
+ // stdin, and writes each answer as one line on stdout. Node runs this file as it is.
22
+ import { createInterface } from "node:readline";
23
+
24
+ type Message = { id?: number | string; method?: string; params?: { name?: string; arguments?: unknown } };
25
+
26
+ const tools = [
27
+ {
28
+ name: "next_version",
29
+ description: "The next version number after a release. Give the current version and the kind of change.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ version: { type: "string", description: "The current version, such as 1.4.2." },
34
+ change: { type: "string", enum: ["major", "minor", "patch"] },
35
+ },
36
+ required: ["version", "change"],
37
+ },
38
+ },
39
+ ];
40
+
41
+ // The tool itself: a function of its arguments.
42
+ function nextVersion(version: string, change: string): string {
43
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
44
+ if (match === null) throw new Error(`${version} is not a version such as 1.4.2`);
45
+ const [major, minor, patch] = match.slice(1).map(Number) as [number, number, number];
46
+ if (change === "major") return `${major + 1}.0.0`;
47
+ if (change === "minor") return `${major}.${minor + 1}.0`;
48
+ if (change === "patch") return `${major}.${minor}.${patch + 1}`;
49
+ throw new Error(`${change} is not major, minor or patch`);
50
+ }
51
+
52
+ function call(name: string | undefined, args: unknown): object {
53
+ const { version, change } = (args ?? {}) as { version?: unknown; change?: unknown };
54
+ if (name !== "next_version") return { error: { code: -32602, message: `Unknown tool: ${name}` } };
55
+ try {
56
+ const text = nextVersion(String(version), String(change));
57
+ return { result: { content: [{ type: "text", text }] } };
58
+ } catch (err) {
59
+ // A tool's own error: the model reads the text, and the call is an error.
60
+ return { result: { content: [{ type: "text", text: (err as Error).message }], isError: true } };
61
+ }
62
+ }
63
+
64
+ function handle(message: Message): object {
65
+ switch (message.method) {
66
+ case "initialize":
67
+ return {
68
+ result: {
69
+ protocolVersion: "2025-11-25",
70
+ capabilities: { tools: {} },
71
+ serverInfo: { name: "semver", version: "1.0.0" },
72
+ },
73
+ };
74
+ case "tools/list":
75
+ return { result: { tools } };
76
+ case "tools/call":
77
+ return call(message.params?.name, message.params?.arguments);
78
+ default:
79
+ return { error: { code: -32601, message: `Method not found: ${message.method}` } };
80
+ }
81
+ }
82
+
83
+ createInterface({ input: process.stdin }).on("line", (line) => {
84
+ if (line.trim() === "") return;
85
+ const message = JSON.parse(line) as Message;
86
+ // A notification has no id, and gets no answer.
87
+ if (message.id === undefined) return;
88
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, ...handle(message) })}\n`);
89
+ });
90
+ ```
91
+
92
+ ## What the server does
93
+
94
+ A stdio server reads one JSON-RPC message on each line of its stdin. It writes each answer as one line on its stdout. It
95
+ answers these methods:
96
+
97
+ - `initialize`: the revision of MCP that it speaks, `2025-11-25`, and `capabilities: { tools: {} }`.
98
+ - `tools/list`: its tools. Each tool has a `name`, a `description` and an `inputSchema`, which is JSON Schema. The model
99
+ reads the description and the schema.
100
+ - `tools/call`: the result of one call, as `content`. A tool that fails gives a result with `isError: true` and the
101
+ reason as text. The model reads the reason.
102
+
103
+ A message with no `id` is a notification, such as `notifications/initialized`, and gets no answer. A method that the
104
+ server does not have gets the JSON-RPC error `-32601`. Write only messages on stdout: the Harness reads each line as a
105
+ message, and drops a line that is not one. The Harness does not read stderr.
106
+
107
+ ## Try the server on your computer
108
+
109
+ Send it the messages that the Harness sends, with Node 24 or later:
110
+
111
+ ```sh
112
+ printf '%s\n' \
113
+ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
114
+ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
115
+ '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
116
+ '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"next_version","arguments":{"version":"1.4.2","change":"minor"}}}' \
117
+ '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"next_version","arguments":{"version":"x","change":"minor"}}}' \
118
+ | node images/default/semver-mcp.ts
119
+ ```
120
+
121
+ ```text
122
+ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"semver","version":"1.0.0"}}}
123
+ {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"next_version","description":"The next version number after a release. Give the current version and the kind of change.","inputSchema":{"type":"object","properties":{"version":{"type":"string","description":"The current version, such as 1.4.2."},"change":{"type":"string","enum":["major","minor","patch"]}},"required":["version","change"]}}]}}
124
+ {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"1.5.0"}]}}
125
+ {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"x is not a version such as 1.4.2"}],"isError":true}}
126
+ ```
127
+
128
+ The notification gets no line. The last call is the tool's own error.
129
+
130
+ ## Put the server in the Sandbox Image
131
+
132
+ Copy the file into the image. The folder of the `Dockerfile` is the context of the build, so `COPY` finds the file
133
+ there:
134
+
135
+ ```dockerfile
136
+ FROM node:24-slim
137
+
138
+ RUN apt-get update \
139
+ && apt-get install -y --no-install-recommends git ca-certificates \
140
+ && rm -rf /var/lib/apt/lists/*
141
+
142
+ # Your agents' toolchain goes here — compilers, CLIs, language servers, dotfiles.
143
+
144
+ # The MCP server. Node 24 runs a .ts file as it is, so the image needs no build step.
145
+ COPY semver-mcp.ts /opt/tools/semver-mcp.ts
146
+ ```
147
+
148
+ The example does not show the comments at the start of the file. Keep them in your file. Node 24 runs a `.ts` file that
149
+ has only types that it can remove, so the image needs no build step. For an MCP server from a package, install the
150
+ package at a fixed version in the `Dockerfile` instead, such as `RUN npm install -g <package>@<version>`.
151
+
152
+ [`jr2 up`](../reference/cli.md#up) builds the image, because a file in its folder changed.
153
+ [Add tools to the Sandbox](add-tools-to-the-sandbox.md) tells how `jr2 up` builds and loads it.
154
+
155
+ ## Declare the server and give it to an Agent
156
+
157
+ Declare the server in the [`mcp`](../reference/config.md#mcp) block of `jr2.config.ts`, beside the fields that are
158
+ there. `command` and `args` are the program and its arguments in the image:
159
+
160
+ ```ts
161
+ import { defineConfig } from "@jr2/orchestrator";
162
+
163
+ export default defineConfig({
164
+ // A stdio server: a program of the Sandbox Image. The Harness starts it for each conversation.
165
+ mcp: { semver: { command: "node", args: ["/opt/tools/semver-mcp.ts"] } },
166
+ });
167
+ ```
168
+
169
+ Then name the server in the `mcp` list of an Agent that works in a Sandbox. In this
170
+ [Workflow](../reference/glossary.md#workflow), a releaser reads the changes of a release, asks `next_version` for the
171
+ next version, and picks `decided`. Save the file as `workflows/next-version.ts`:
172
+
173
+ **Anthropic**
174
+
175
+ ```ts
176
+ import { assign } from "xstate";
177
+ import { z } from "zod";
178
+ import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
179
+
180
+ const decided = defineEvent({
181
+ name: "decided",
182
+ description: "Call this with the next version, after the next_version tool gave it.",
183
+ input: z.object({ version: z.string().describe("The next version, such as 1.5.0.") }),
184
+ });
185
+
186
+ const runInput = z.object({
187
+ version: z.string().describe("The current version, such as 1.4.2."),
188
+ changes: z.string().describe("The changes since that version, one on each line."),
189
+ });
190
+ // `never`: the Workspace has no Repo Slot.
191
+ type BodyInput = Workspaced<z.infer<typeof runInput>, never>;
192
+ type Ctx = BodyInput & { next?: string };
193
+
194
+ const body = jr2Setup({
195
+ types: {} as { context: Ctx; input: BodyInput; output: { version: string } },
196
+ events: [decided],
197
+ actors: {
198
+ releaser: agent({
199
+ model: "anthropic/claude-haiku-5-5",
200
+ instructions: "You decide how large a release is: major, minor or patch. You use semantic versioning.",
201
+ workspace: "read",
202
+ // A stdio server takes no tools list: the Agent gets every tool of the server.
203
+ mcp: [{ server: "semver" }],
204
+ }),
205
+ fail: fail(),
206
+ },
207
+ }).createMachine({
208
+ id: "next-version",
209
+ context: ({ input }) => input,
210
+ on: { "agent.fault": ".failed", "workspace.lost": ".failed" },
211
+ initial: "deciding",
212
+ states: {
213
+ // The Turn frames no `cwd`, so it works in /work. The server starts there too.
214
+ deciding: {
215
+ invoke: {
216
+ src: "releaser",
217
+ input: ({ context }) => ({
218
+ prompt: `The current version is ${context.version}. The changes are:\n${context.changes}`,
219
+ }),
220
+ },
221
+ on: { decided: { target: "done", actions: assign({ next: ({ event }) => event.version }) } },
222
+ },
223
+ done: { type: "final" },
224
+ // Not a final state: the run ends `failed`, with the reason.
225
+ failed: { invoke: { src: "fail", input: { reason: "the releaser could not decide" } } },
226
+ },
227
+ output: ({ context }) => ({ version: context.next ?? "" }),
228
+ });
229
+
230
+ // No `repos`: the Workspace clones nothing. The Sandbox Image holds the server.
231
+ export const machine = workspace(body, { input: runInput });
232
+ ```
233
+
234
+ **Local**
235
+
236
+ ```ts
237
+ import { assign } from "xstate";
238
+ import { z } from "zod";
239
+ import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
240
+
241
+ const decided = defineEvent({
242
+ name: "decided",
243
+ description: "Call this with the next version, after the next_version tool gave it.",
244
+ input: z.object({ version: z.string().describe("The next version, such as 1.5.0.") }),
245
+ });
246
+
247
+ const runInput = z.object({
248
+ version: z.string().describe("The current version, such as 1.4.2."),
249
+ changes: z.string().describe("The changes since that version, one on each line."),
250
+ });
251
+ // `never`: the Workspace has no Repo Slot.
252
+ type BodyInput = Workspaced<z.infer<typeof runInput>, never>;
253
+ type Ctx = BodyInput & { next?: string };
254
+
255
+ const body = jr2Setup({
256
+ types: {} as { context: Ctx; input: BodyInput; output: { version: string } },
257
+ events: [decided],
258
+ actors: {
259
+ releaser: agent({
260
+ model: "local/qwen3.6-35b-a3b",
261
+ instructions: "You decide how large a release is: major, minor or patch. You use semantic versioning.",
262
+ workspace: "read",
263
+ // A stdio server takes no tools list: the Agent gets every tool of the server.
264
+ mcp: [{ server: "semver" }],
265
+ }),
266
+ fail: fail(),
267
+ },
268
+ }).createMachine({
269
+ id: "next-version",
270
+ context: ({ input }) => input,
271
+ on: { "agent.fault": ".failed", "workspace.lost": ".failed" },
272
+ initial: "deciding",
273
+ states: {
274
+ // The Turn frames no `cwd`, so it works in /work. The server starts there too.
275
+ deciding: {
276
+ invoke: {
277
+ src: "releaser",
278
+ input: ({ context }) => ({
279
+ prompt: `The current version is ${context.version}. The changes are:\n${context.changes}`,
280
+ }),
281
+ },
282
+ on: { decided: { target: "done", actions: assign({ next: ({ event }) => event.version }) } },
283
+ },
284
+ done: { type: "final" },
285
+ // Not a final state: the run ends `failed`, with the reason.
286
+ failed: { invoke: { src: "fail", input: { reason: "the releaser could not decide" } } },
287
+ },
288
+ output: ({ context }) => ({ version: context.next ?? "" }),
289
+ });
290
+
291
+ // No `repos`: the Workspace clones nothing. The Sandbox Image holds the server.
292
+ export const machine = workspace(body, { input: runInput });
293
+ ```
294
+
295
+ The releaser uses the model of the tab. The model reads the tool as `mcp__semver__next_version`. A stdio server takes no
296
+ `tools` list: the Agent gets each tool of the server. Run `jr2 up`, then start a run:
297
+
298
+ ```sh
299
+ jr2 run next-version --input '{"version":"1.4.2","changes":"Add the --json flag.\nFix the help text."}'
300
+ ```
301
+
302
+ ## What the server can reach
303
+
304
+ A stdio server is [Workspace code](../reference/glossary.md#workspace-code), as the Agent's shell is:
305
+
306
+ - The Harness starts it through the spawner, inside the Landlock confinement of the Sandbox, in the directory of the
307
+ [Turn](../reference/glossary.md#turn). It cannot read `/run/jr2`, and it cannot read the control Stand-in of the
308
+ Harness.
309
+ - Its env is the env of the confined programs. It holds only the [Stand-ins](../reference/glossary.md#stand-in) of the
310
+ [Held secrets](../reference/glossary.md#held-secret) that the
311
+ [`workspace()`](../reference/authoring-surface/workspace.md) lists under `secrets`. A server that calls an API sends
312
+ the Stand-in, and the [Custodian](../reference/glossary.md#custodian) puts the value in its place.
313
+ - It runs as one process for each conversation of the Agent, from the first Turn to the end of the conversation. When it
314
+ stops, its call fails, and the next call starts it again.
315
+
316
+ The Agent's other programs run in the same confinement. They can trace the server and write to its stdin. Thus trust the
317
+ result of the server as much as the output of a command that the Agent runs, and no more.
318
+ [Secrets and trust](../concepts/secrets-and-trust.md#confined-programs) tells what the confinement stops.
319
+
320
+ A Menu-only Agent cannot use a stdio server: its Turns run on the
321
+ [Instance Harness](../reference/glossary.md#instance-harness), where no Workspace code runs. An `mcp` Effect cannot use
322
+ one either. To use a tool from those, run it as a remote server, and declare its `url`.
323
+
324
+ ## Rules
325
+
326
+ - At the first Turn of a conversation, the server must answer `initialize` and `tools/list`. When it fails, the Turn
327
+ fails before the model reads anything, and the Machine gets
328
+ [`agent.fault`](../reference/mechanism-events.md#agent-fault) with the reason `<server> did not answer: <reason>`.
329
+ - A line on stdout that is longer than 16 MiB stops the server.
330
+ - `jr2 up` refuses a `tools` list on a stdio server:
331
+ `refusing: workflow "<workflow>": Agent "<slot>" gives a tools list for the stdio MCP server "<server>" — a stdio server takes none: the Agent gets every tool, because the server runs as Workspace code, and a list would guard nothing. Write { server: "<server>" } (ADR-0079)`
332
+ - `jr2 up` refuses a stdio server on a Menu-only Agent:
333
+ `refusing: workflow "<workflow>": Agent "<slot>" has workspace: "none" and names the stdio MCP server "<server>" — a stdio server runs as Workspace code, inside a Sandbox's confinement, and a "none" Agent's Turns run on the Instance Harness, which has none: name a remote server, or give the Agent a Workspace (ADR-0079, ADR-0031)`
334
+ - `jr2 up` does not check the tools of a stdio server, because the program runs only in a Sandbox.
335
+ - A tool whose name, as `mcp__<server>__<tool>`, has a character other than letters, digits, `_` and `-`, or more than
336
+ 128 characters, is not given to the model. The Harness writes one line in its log:
337
+ `the MCP server <server> lists the tool "<tool>", and "mcp__<server>__<tool>" does not match ^[A-Za-z0-9_-]{1,128}$: the tool is not offered (ADR-0079)`
338
+
339
+ [Give an Agent MCP tools](give-an-agent-mcp-tools.md#rules) gives the refusals of the `mcp` block.
340
+
341
+ ## Read next
342
+
343
+ - [Agents and Turns](../concepts/agents-and-turns.md#the-mcp-tools): the MCP tools of an Agent.
344
+ - [Workspaces](../concepts/workspaces.md): the Sandbox Image.
345
+ - [Give an Agent MCP tools](give-an-agent-mcp-tools.md): remote servers, and the allowlist.
346
+ - Reference: [`mcp`](../reference/config.md#mcp) and
347
+ [`AgentMcpServer`](../reference/authoring-surface/type-agentmcpserver.md).
348
+ - [ADR-0079](../reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md):
349
+ MCP is the one contract for tools, and a stdio server is Workspace code.
350
+ - [ADR-0076](../reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md): the
351
+ confinement of Workspace code.
@@ -184,7 +184,7 @@ jr2 run rollout --input '{"service":"billing","version":"1.4.0"}'
184
184
 
185
185
  ```text
186
186
  → context kind-jr2 / namespace my-instance
187
- run b7c3d871-e3ae-46af-8bb3-774913bcd6cc (rollout) — attached
187
+ run a5bafe11-80bf-426b-905f-6fb19952c093 (rollout) — attached
188
188
  → active "canary"
189
189
  ```
190
190
 
@@ -198,7 +198,7 @@ jr2 send <run id> --gate canary --event roll_back --input '{"reason":"The error
198
198
 
199
199
  ```text
200
200
  → context kind-jr2 / namespace my-instance
201
- delivered roll_back to gate "canary" on b7c3d871-e3ae-46af-8bb3-774913bcd6cc
201
+ delivered roll_back to gate "canary" on a5bafe11-80bf-426b-905f-6fb19952c093
202
202
  ```
203
203
 
204
204
  In the first terminal, the run ends. A `roll_back` ends the rollout, so the run ends `failed`, with the reason of the
@@ -210,7 +210,7 @@ that jr2 added, and the `reason`:
210
210
  → active "rolledBack"
211
211
  → failed "rolledBack"
212
212
  failed: The error rate went up.
213
- {"runId":"b7c3d871-e3ae-46af-8bb3-774913bcd6cc","workflow":"rollout","instanceId":"b79e7e8b-6289-4dbc-a9a3-be945f8b1173","status":"failed","value":"rolledBack","context":{"service":"billing","version":"1.4.0","canaryPercent":10,"instanceId":"b79e7e8b-6289-4dbc-a9a3-be945f8b1173","reason":"The error rate went up."},"children":[],"waiting":[],"held":[],"assembly":"83858e28b054","output":{"reason":"The error rate went up."}}
213
+ {"runId":"a5bafe11-80bf-426b-905f-6fb19952c093","workflow":"rollout","instanceId":"f779766e-49c3-49ad-92bd-10208710843b","startedAt":"2026-11-06T19:17:57.979Z","startedBy":{"kind":"caller","via":"token","name":"instance","onBehalfOf":"kubernetes-admin"},"status":"failed","value":"rolledBack","context":{"service":"billing","version":"1.4.0","canaryPercent":10,"instanceId":"f779766e-49c3-49ad-92bd-10208710843b","reason":"The error rate went up."},"children":[],"waiting":[],"held":[],"answers":[{"gate":"canary","type":"roll_back","at":"2026-11-06T19:18:06.269Z","answeredBy":{"via":"token","name":"instance","onBehalfOf":"kubernetes-admin"}}],"assembly":"bedde6f8c51c","output":{"reason":"The error rate went up."}}
214
214
  ```
215
215
 
216
216
  Your instance id and your `assembly` hash are different.
@@ -218,12 +218,22 @@ Your instance id and your `assembly` hash are different.
218
218
  [Start a run](start-a-run.md) gives the other ways to start a run. [Answer a Gate](answer-a-gate.md) gives the other
219
219
  ways to answer one.
220
220
 
221
- ## Put shared code in a file that jr2 does not register
221
+ ## Lay out the Instance
222
+
223
+ jr2 finds the Workflows in `workflows/`, and `jr2.config.ts` declares the rest. Other code goes in folders of your own
224
+ choice. `jr2 up` bundles the whole folder of the Instance, so a Workflow and the config can import from each folder:
225
+
226
+ ```
227
+ my-instance/
228
+ jr2.config.ts the config: secrets, Hooks, Callers, the Harness
229
+ workflows/ one .ts file is one Workflow
230
+ hooks/ your Hook kinds
231
+ lib/ code that two Workflows share
232
+ ```
222
233
 
223
234
  jr2 registers each `.ts` file in `workflows/`, also a file that a different Workflow imports. It does not register a
224
- file whose name starts with `_`, or a `.d.ts` file. Thus put the code that two Workflows share in a file such as
225
- `workflows/_events.ts`, or outside `workflows/`. Import it with its `.ts` extension:
226
- `import { promote } from "./_events.ts";`.
235
+ file whose name starts with `_`, or a `.d.ts` file. Thus put the code that two Workflows share outside `workflows/`,
236
+ such as in `lib/events.ts`. Import it with its `.ts` extension: `import { promote } from "../lib/events.ts";`.
227
237
 
228
238
  ## Rules
229
239
 
@@ -290,7 +300,7 @@ file whose name starts with `_`, or a `.d.ts` file. Thus put the code that two W
290
300
  not an empty one. The run ends `error`, and its fault is:
291
301
  `fail: the input must be { reason: string; detail?: unknown } with a reason that is not empty (got <value>) — invoke: { src: "fail", input: { reason: "the tests failed" } } (ADR-0073)`.
292
302
  - `workspace()` refuses a body that declares a door. Declare the door with the `input` option of `workspace()`:
293
- ``workspace(): the body "<id>" declares its own run input, which nothing will ever serve or enforce — the wrapper feeds the body the run input PLUS the injected `workspace` handles, so the body's schema is not the door. Move it to the wrapper: workspace(body, { input, spec }) (ADR-0033).``
303
+ ``workspace(): the body "<id>" declares its own run input, which nothing will ever serve or enforce — the wrapper feeds the body the run input PLUS the injected `workspace` handles, so the body's schema is not the door. Move it to the wrapper: workspace(body, { input }) (ADR-0033).``
294
304
  - `jr2 up` prints each refusal of `defineEvent`, `createMachine` and `workspace()` as `error: <refusal>`, and exits with
295
305
  `1`. It changes nothing in the cluster.
296
306
  - The Orchestrator refuses a start whose input does not agree with the door, with the status code `400`: