@jr2/orchestrator 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +20 -2
  2. package/bin/server.ts +7 -4
  3. package/console/canvas.ts +27 -14
  4. package/console/components/app.ts +12 -1
  5. package/console/components/fleet.ts +52 -6
  6. package/console/components/machine-pane.ts +128 -2
  7. package/console/components/nav.ts +1 -1
  8. package/console/components/schema-form.ts +12 -4
  9. package/console/json-field.ts +23 -0
  10. package/console/main.ts +122 -39
  11. package/console/store.ts +321 -21
  12. package/console/style.css +111 -0
  13. package/docs/agents-block.md +5 -0
  14. package/docs/concepts/agents-and-turns.md +442 -0
  15. package/docs/concepts/architecture.md +230 -0
  16. package/docs/concepts/charts.md +143 -0
  17. package/docs/concepts/composition.md +357 -0
  18. package/docs/concepts/converge.md +254 -0
  19. package/docs/concepts/durable-runs.md +189 -0
  20. package/docs/concepts/effects.md +603 -0
  21. package/docs/concepts/gates.md +208 -0
  22. package/docs/concepts/machines-and-workflows.md +289 -0
  23. package/docs/concepts/pools-and-sources.md +230 -0
  24. package/docs/concepts/secrets-and-trust.md +222 -0
  25. package/docs/concepts/the-menu.md +378 -0
  26. package/docs/concepts/workspaces.md +678 -0
  27. package/docs/get-started/a-model-decides.md +563 -0
  28. package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
  29. package/docs/get-started/connect-a-model.md +248 -0
  30. package/docs/get-started/create-an-instance.md +194 -0
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
  32. package/docs/get-started/install.md +126 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  34. package/docs/guides/answer-a-gate.md +304 -0
  35. package/docs/guides/bind-open-parts.md +223 -0
  36. package/docs/guides/cancel-a-run.md +146 -0
  37. package/docs/guides/clean-up.md +206 -0
  38. package/docs/guides/fix-a-failed-converge.md +361 -0
  39. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  41. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  42. package/docs/guides/look-inside-a-sandbox.md +174 -0
  43. package/docs/guides/mirror-the-kit-images.md +176 -0
  44. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  45. package/docs/guides/open-the-console.md +91 -0
  46. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  47. package/docs/guides/push-and-sign-a-branch.md +486 -0
  48. package/docs/guides/run-a-chart-by-name.md +270 -0
  49. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  50. package/docs/guides/run-one-worker-per-item.md +321 -0
  51. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  52. package/docs/guides/size-a-workspace.md +204 -0
  53. package/docs/guides/start-a-run-from-a-run.md +237 -0
  54. package/docs/guides/start-a-run.md +207 -0
  55. package/docs/guides/upgrade-the-kit-version.md +240 -0
  56. package/docs/guides/use-a-hosted-provider.md +208 -0
  57. package/docs/guides/use-a-local-model.md +313 -0
  58. package/docs/guides/watch-a-run.md +326 -0
  59. package/docs/guides/write-a-workflow.md +318 -0
  60. package/docs/index.md +44 -0
  61. package/docs/llms.txt +216 -0
  62. package/docs/reference/authoring-surface/agent.md +128 -0
  63. package/docs/reference/authoring-surface/answer.md +140 -0
  64. package/docs/reference/authoring-surface/arg.md +60 -0
  65. package/docs/reference/authoring-surface/basic.md +62 -0
  66. package/docs/reference/authoring-surface/bearer.md +53 -0
  67. package/docs/reference/authoring-surface/chartschema.md +23 -0
  68. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  69. package/docs/reference/authoring-surface/command.md +191 -0
  70. package/docs/reference/authoring-surface/customize.md +64 -0
  71. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  72. package/docs/reference/authoring-surface/defineevent.md +114 -0
  73. package/docs/reference/authoring-surface/dispatch.md +126 -0
  74. package/docs/reference/authoring-surface/fail.md +83 -0
  75. package/docs/reference/authoring-surface/file.md +122 -0
  76. package/docs/reference/authoring-surface/frompick.md +277 -0
  77. package/docs/reference/authoring-surface/http.md +159 -0
  78. package/docs/reference/authoring-surface/isfailure.md +58 -0
  79. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  80. package/docs/reference/authoring-surface/open.md +74 -0
  81. package/docs/reference/authoring-surface/openbody.md +86 -0
  82. package/docs/reference/authoring-surface/paramsof.md +125 -0
  83. package/docs/reference/authoring-surface/pool.md +186 -0
  84. package/docs/reference/authoring-surface/push.md +112 -0
  85. package/docs/reference/authoring-surface/secret.md +118 -0
  86. package/docs/reference/authoring-surface/sentby.md +83 -0
  87. package/docs/reference/authoring-surface/source.md +51 -0
  88. package/docs/reference/authoring-surface/text.md +62 -0
  89. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  90. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  91. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  92. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  93. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  94. package/docs/reference/authoring-surface/type-arg.md +19 -0
  95. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  96. package/docs/reference/authoring-surface/type-binding.md +62 -0
  97. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  98. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  99. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  100. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  101. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  102. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  103. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  104. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  105. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  106. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  107. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  108. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  109. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  110. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  111. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  112. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  113. package/docs/reference/authoring-surface/type-childat.md +12 -0
  114. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  115. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  116. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  117. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  118. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  119. package/docs/reference/authoring-surface/type-customize.md +109 -0
  120. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  121. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  122. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  124. package/docs/reference/authoring-surface/type-duration.md +15 -0
  125. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  126. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  127. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  128. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  129. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  130. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  131. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  132. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  133. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  134. package/docs/reference/authoring-surface/type-failure.md +19 -0
  135. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  136. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  137. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  138. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  139. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  140. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  141. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  142. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  143. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  144. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  145. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  146. package/docs/reference/authoring-surface/type-lends.md +38 -0
  147. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  148. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  149. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  150. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  151. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  152. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  153. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  154. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  155. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  156. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  157. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  158. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  159. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  160. package/docs/reference/authoring-surface/type-openof.md +12 -0
  161. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  162. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  163. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  164. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  165. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  166. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  167. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  168. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  169. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  170. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  171. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  172. package/docs/reference/authoring-surface/type-reached.md +12 -0
  173. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  174. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  175. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  176. package/docs/reference/authoring-surface/type-secret.md +25 -0
  177. package/docs/reference/authoring-surface/type-sender.md +19 -0
  178. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  179. package/docs/reference/authoring-surface/type-size.md +85 -0
  180. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  181. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  182. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  183. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  184. package/docs/reference/authoring-surface/type-text.md +19 -0
  185. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  186. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  187. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  188. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  189. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  190. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  191. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  192. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  193. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  194. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  195. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  196. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  197. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  198. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  199. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  200. package/docs/reference/authoring-surface/workspace.md +259 -0
  201. package/docs/reference/authoring-surface.md +197 -0
  202. package/docs/reference/cli.md +433 -0
  203. package/docs/reference/config.md +756 -0
  204. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  205. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  206. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  207. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  208. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  209. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  210. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  211. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  212. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  213. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  214. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  215. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  216. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  217. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  218. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  219. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  220. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  221. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  222. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  223. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  224. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  225. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  226. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  227. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  228. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  229. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  230. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  231. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  232. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  233. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  234. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  235. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  236. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  237. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  238. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  239. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  240. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  241. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  242. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  243. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  244. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  245. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  246. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  247. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  248. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  249. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  250. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  251. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  252. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  253. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  254. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  255. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  256. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  257. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  258. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  259. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  260. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  261. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  262. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  263. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  264. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  265. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  266. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  267. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
  268. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  269. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  270. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  271. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  272. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  273. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  274. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  275. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  276. package/docs/reference/decisions.md +83 -0
  277. package/docs/reference/glossary.md +648 -0
  278. package/docs/reference/http-api.md +1348 -0
  279. package/docs/reference/machines.md +190 -0
  280. package/docs/reference/mechanism-events.md +72 -0
  281. package/docs/why.md +206 -0
  282. package/package.json +6 -4
  283. package/src/actor.ts +976 -61
  284. package/src/agent.ts +114 -38
  285. package/src/ambient.ts +4 -1
  286. package/src/assembly-checks.ts +347 -0
  287. package/src/assembly-tar.ts +494 -0
  288. package/src/assembly.ts +721 -0
  289. package/src/cause.ts +234 -0
  290. package/src/chart.ts +2182 -0
  291. package/src/charts.ts +748 -0
  292. package/src/config.ts +520 -162
  293. package/src/content-hash.ts +105 -0
  294. package/src/custodian.lua +6 -5
  295. package/src/custodian.ts +143 -39
  296. package/src/customize.ts +388 -55
  297. package/src/dispatch-effect.ts +945 -0
  298. package/src/door-compat.ts +217 -0
  299. package/src/durability.ts +11 -4
  300. package/src/effect-checks.ts +557 -0
  301. package/src/effect.ts +1202 -0
  302. package/src/effector-wire.ts +674 -0
  303. package/src/fail.ts +209 -0
  304. package/src/fingerprint.ts +4 -3
  305. package/src/gate.ts +8 -0
  306. package/src/git-clone.ts +65 -0
  307. package/src/git-push.ts +336 -0
  308. package/src/harness-client.ts +764 -48
  309. package/src/held-secrets.ts +82 -26
  310. package/src/holds.ts +156 -0
  311. package/src/http-effect.ts +1423 -0
  312. package/src/http.ts +916 -113
  313. package/src/images.ts +47 -21
  314. package/src/index.ts +62 -43
  315. package/src/instance.ts +505 -59
  316. package/src/internal.ts +84 -0
  317. package/src/kube-client.ts +333 -0
  318. package/src/machine-doc.ts +106 -6
  319. package/src/mechanism-routes.ts +168 -0
  320. package/src/model-keys.generated.ts +53 -0
  321. package/src/model-keys.ts +18 -58
  322. package/src/mount-digest.ts +53 -0
  323. package/src/names.ts +66 -8
  324. package/src/notices.ts +141 -0
  325. package/src/open-body.ts +1580 -0
  326. package/src/open-effect.ts +217 -0
  327. package/src/open.ts +59 -6
  328. package/src/parts.ts +709 -41
  329. package/src/party.ts +534 -0
  330. package/src/pick.ts +761 -0
  331. package/src/pool.ts +248 -37
  332. package/src/registration.ts +382 -19
  333. package/src/repo-fetch.ts +60 -85
  334. package/src/repos.ts +65 -75
  335. package/src/rest.ts +133 -0
  336. package/src/run-host.ts +2383 -115
  337. package/src/run-output.ts +38 -0
  338. package/src/sandbox-effect.ts +1126 -0
  339. package/src/sandbox-effector.ts +111 -0
  340. package/src/sandbox-kube.ts +1721 -0
  341. package/src/sandbox-watch.ts +260 -0
  342. package/src/secret-placement.ts +529 -0
  343. package/src/server.ts +284 -84
  344. package/src/setup.ts +819 -117
  345. package/src/size.ts +454 -0
  346. package/src/snapshot-store.ts +115 -20
  347. package/src/stub-harness.ts +49 -16
  348. package/src/sync-client.ts +54 -0
  349. package/src/timers.ts +191 -0
  350. package/src/tokens.ts +35 -4
  351. package/src/vocabulary.ts +135 -15
  352. package/src/wire.ts +238 -10
  353. package/src/workspace.ts +1195 -338
  354. package/src/sandbox-kubectl.ts +0 -1108
@@ -5,20 +5,26 @@
5
5
  // the durable stream open, and never act — the Machine parks exactly as it would against a
6
6
  // silent real Harness, and e2e drives it by playing the agent against `/mcp/<iid>` instead.
7
7
  //
8
- // Wire (ADR-0027 — the normative model of the five endpoints; `@jr2/harness` serves it for real):
9
- // POST /agents/:name/:id {message, cwd?, definition, model?, thinkingLevel?} → 200 { streamUrl, offset, submissionId }
10
- // (`message` and `cwd` are the Turn's Frame — what it is about and where it works, ADR-0057)
8
+ // Wire (ADR-0027 — the normative model of the conversation endpoints; `@jr2/harness` serves it for
9
+ // real), and the pod's memory kills, which a stub never has (ADR-0062):
10
+ // POST /agents/:name/:id {message, cwd?, definition, model?, thinkingLevel?, notices?, kept?} → 200 { streamUrl, offset, submissionId }
11
+ // (`message` and `cwd` are the Turn's Frame — what it is about and where it works, ADR-0057;
12
+ // `kept` admits as any POST does, because the stub keeps every conversation — the real Harness
13
+ // answers 404 when it keeps none, ADR-0062)
11
14
  // GET /agents/:name/:id?offset=…[&view=updates] → 200 `[]` + Stream-Next-Offset/Up-To-Date
12
15
  // GET /agents/:name/:id?…&live=long-poll → parked; 204 + same headers on timeout
13
- // POST /agents/:name/:id/abort → 200 { aborted }
16
+ // POST /agents/:name/:id/abort [{cause?}] → 200 { aborted }
14
17
  // GET /agents/:name/:id?view=history → 200 { …, settlements }
18
+ // GET /memory-kills → 200 { kills: [] }
15
19
  // (the client's `wait(admission)` long-polls `streamUrl?view=updates` from the admission offset;
16
20
  // an empty stream parks it — exactly the "admitted, never settles" semantics the mechanics tier
17
21
  // needs.) The client then re-polls calmly at the long-poll cadence. `close()` severs parked polls.
18
22
  //
19
- // A stub submission therefore ends exactly one way: ABORTED, when the state that asked for the
20
- // turn stops waiting (ADR-0024). That is what `history`'s `settlements` carries, and it is the
21
- // only place a black-box test can see a turn end — the run itself is untouched by an abort.
23
+ // A stub submission therefore ends one way: the sweep, when the state that asked for the turn stops
24
+ // waiting (ADR-0024). Its outcome follows the sweep's cause, as the real Harness's does: `ended` for
25
+ // the running Submission when the Turn's own pick ended it (`cause: "pick"`), `aborted` for every
26
+ // other Submission and every other end. That is what `history`'s `settlements` carries, and it is
27
+ // the only place a black-box test can see a turn end — the run itself is untouched by an abort.
22
28
  //
23
29
  // This can later grow scriptable behavior or be swapped for a real local Harness without
24
30
  // touching actor code — it is only a different URL.
@@ -32,6 +38,7 @@
32
38
 
33
39
  import { createServer } from "node:http";
34
40
  import type { AddressInfo, Socket } from "node:net";
41
+ import type { Notice } from "./wire.ts";
35
42
 
36
43
  /** One admission of an Agent: which slot, which durable exchange, this Turn's FRAME (the prompt
37
44
  * and where it works — ADR-0057), the DEFINITION the Machine's slot carried here (ADR-0049), and
@@ -49,11 +56,13 @@ export type Admission = {
49
56
  definition?: Record<string, unknown>;
50
57
  model?: string;
51
58
  thinkingLevel?: string;
59
+ /** The notices the admission carried (ADR-0062). Absent when it carried none. */
60
+ notices?: Notice[];
52
61
  };
53
62
 
54
63
  /** One settled submission, in the shape `history()` reports it. The stub settles submissions for
55
- * exactly one reason — an abort (ADR-0024) — so `outcome` has exactly one value here. */
56
- export type StubSettlement = { submissionId: string; outcome: "aborted" };
64
+ * exactly one reason — an abort (ADR-0024) — so `outcome` is one of the two a sweep gives. */
65
+ export type StubSettlement = { submissionId: string; outcome: "ended" | "aborted" };
57
66
 
58
67
  export type StubHarnessOptions = {
59
68
  /** Listen port. Default 0 → ephemeral (read back from `url`). */
@@ -99,17 +108,39 @@ export async function startStubHarness(opts: StubHarnessOptions = {}): Promise<R
99
108
  // End every in-flight and queued submission for one instance (`agents.abort` — ADR-0024).
100
109
  // Answers `{ aborted }`: whether there was anything to end, exactly as the real Harness
101
110
  // does for an idle instance. Settlement is recorded here rather than pushed on the stream, because nothing is
102
- // listening — the actor that asked for the turn is already stopped.
111
+ // listening — the actor that asked for the turn is already stopped. The body's cause decides the
112
+ // running Submission's outcome, the first unsettled one (`AbortRequest`): `ended` for `pick` with
113
+ // that Submission's id; no body, another id, or any other cause, `aborted`. What is queued
114
+ // behind it never ran: `aborted`.
103
115
  const aborting = req.method === "POST" && ABORT_PATH.exec(url.pathname);
104
116
  if (aborting) {
105
117
  const [, agentName, instanceId] = aborting as unknown as [string, string, string];
106
- const k = key(decodeURIComponent(agentName), decodeURIComponent(instanceId));
107
- const ended = unsettled.get(k)?.splice(0) ?? [];
108
- const settled = settlements.get(k) ?? [];
109
- settled.push(...ended.map((submissionId) => ({ submissionId, outcome: "aborted" as const })));
110
- settlements.set(k, settled);
118
+ let body = "";
119
+ req.on("data", (chunk: Buffer) => (body += chunk));
120
+ req.on("end", () => {
121
+ const sent = safeParse(body) as { cause?: unknown; submissionId?: unknown } | undefined;
122
+ const k = key(decodeURIComponent(agentName), decodeURIComponent(instanceId));
123
+ const ended = unsettled.get(k)?.splice(0) ?? [];
124
+ const byPick = sent?.cause === "pick" && sent.submissionId === ended[0];
125
+ const settled = settlements.get(k) ?? [];
126
+ settled.push(
127
+ ...ended.map((submissionId, i) => ({
128
+ submissionId,
129
+ outcome: byPick && i === 0 ? ("ended" as const) : ("aborted" as const),
130
+ })),
131
+ );
132
+ settlements.set(k, settled);
133
+ res.writeHead(200, { "content-type": "application/json" });
134
+ res.end(JSON.stringify({ aborted: ended.length > 0 }));
135
+ });
136
+ return;
137
+ }
138
+
139
+ // The pod's memory kills (ADR-0062): a stub runs no process, so none. Answered, not a 404, so a
140
+ // Turn the stub admits logs no failed read.
141
+ if (req.method === "GET" && url.pathname === "/memory-kills") {
111
142
  res.writeHead(200, { "content-type": "application/json" });
112
- res.end(JSON.stringify({ aborted: ended.length > 0 }));
143
+ res.end(JSON.stringify({ kills: [] }));
113
144
  return;
114
145
  }
115
146
 
@@ -133,6 +164,7 @@ export async function startStubHarness(opts: StubHarnessOptions = {}): Promise<R
133
164
  definition?: Record<string, unknown>;
134
165
  model?: string;
135
166
  thinkingLevel?: string;
167
+ notices?: Notice[];
136
168
  }
137
169
  | undefined;
138
170
  const admission: Admission = {
@@ -145,6 +177,7 @@ export async function startStubHarness(opts: StubHarnessOptions = {}): Promise<R
145
177
  ...(sent?.definition ? { definition: sent.definition } : {}),
146
178
  ...(sent?.model ? { model: sent.model } : {}),
147
179
  ...(sent?.thinkingLevel ? { thinkingLevel: sent.thinkingLevel } : {}),
180
+ ...(sent?.notices?.length ? { notices: sent.notices } : {}),
148
181
  };
149
182
  admissions.push(admission);
150
183
  const submissionId = `stub-${++submissionSeq}`;
@@ -0,0 +1,54 @@
1
+ // The sync's client (ADR-0067): after a push whose published commit is not the Agent's own, the
2
+ // Orchestrator asks the Workspace's Harness to move the Agent's branch to it (`POST /sync`), carrying the bundle the Sandbox
3
+ // Effector wrote. The bundle travels through here, never through a shared volume: the Worktree is
4
+ // read-only on the credentialed side, and nothing the Effector writes is a channel into the Agent's
5
+ // container but this one call, which moves one ref. The Harness's answer is a report the next push
6
+ // never trusts.
7
+
8
+ import type { SyncAnswer, SyncRequest } from "./wire.ts";
9
+
10
+ /** The Harness's sync, as a push drives it. It never throws: a Harness that cannot be reached, or
11
+ * that refuses, answers `not-synced` with the reason — the push's outcome stands either way. */
12
+ export interface SyncPort {
13
+ sync(req: SyncRequest, signal: AbortSignal): Promise<SyncAnswer>;
14
+ }
15
+ export type SyncPortFactory = (endpoint: string, bearer?: string) => SyncPort;
16
+
17
+ /** How long a sync may take before it answers `not-synced`: its git steps are bounded at 120 s each. */
18
+ const SYNC_TIMEOUT_MS = 5 * 60_000;
19
+
20
+ export function createSyncPort(options: {
21
+ baseUrl: string;
22
+ /** The Workspace's Harness bearer (ADR-0058). */
23
+ token?: string;
24
+ /** Injectable for socket-free tests. Default: global `fetch`. */
25
+ fetch?: typeof fetch;
26
+ }): SyncPort {
27
+ const fetchImpl = options.fetch ?? fetch;
28
+ return {
29
+ sync: async (req, signal) => {
30
+ const url = new URL("/sync", options.baseUrl).toString();
31
+ try {
32
+ const res = await fetchImpl(url, {
33
+ method: "POST",
34
+ headers: {
35
+ "content-type": "application/json",
36
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
37
+ },
38
+ body: JSON.stringify(req),
39
+ // Bounded: a Harness that never answers is a sync that did not finish, never a push held open.
40
+ signal: AbortSignal.any([signal, AbortSignal.timeout(SYNC_TIMEOUT_MS)]),
41
+ });
42
+ const text = await res.text();
43
+ if (!res.ok) return { sync: "not-synced", reason: `the Harness refused the sync (${res.status}): ${text}` };
44
+ return JSON.parse(text) as SyncAnswer;
45
+ } catch (err) {
46
+ return { sync: "not-synced", reason: `the Harness could not be reached: ${(err as Error).message}` };
47
+ }
48
+ },
49
+ };
50
+ }
51
+
52
+ /** The real factory: the Workspace's Harness, with its bearer. */
53
+ export const harnessSyncPort: SyncPortFactory = (endpoint, bearer) =>
54
+ createSyncPort({ baseUrl: endpoint, ...(bearer ? { token: bearer } : {}) });
package/src/timers.ts ADDED
@@ -0,0 +1,191 @@
1
+ // A run's pending timers across a restart (ADR-0007).
2
+ //
3
+ // xstate 5 keeps a delayed event (`after`, a named delay, a delayed `raise` or `sendTo`) on the
4
+ // actor SYSTEM's scheduler, not in the Machine snapshot. `getPersistedSnapshot()` carries no trace
5
+ // of it, and a restored actor runs no entry actions again, so nothing schedules it again: the
6
+ // timer is lost, not restarted. xstate 5 has a `snapshot.scheduler` seam in `createSystem`, but
7
+ // the Actor constructor never passes it.
8
+ //
9
+ // So the run host records each pending timer's absolute DEADLINE beside the snapshot, in the same
10
+ // blob, and schedules it again on the restored system after `start()`. The author writes plain
11
+ // `after`. The scheduled id is xstate's own (`xstate.after.<delay>.<stateId>` for an `after`), so
12
+ // the state's exit `cancel(id)` still cancels a re-armed timer.
13
+ //
14
+ // This reads an xstate INTERNAL: `system.getSnapshot()._scheduledEvents` (typed in xstate's
15
+ // `ActorSystem`, but underscored), `system.scheduler.schedule`, and `system.scheduler.cancel`
16
+ // (wrapped for the length of `start()`). An xstate bump can break it; the raw-xstate tests in
17
+ // test/timers.test.ts are the canary. xstate 6 persists timers in the snapshot and re-arms them
18
+ // itself, with the same deadline and clamp: on xstate 6, delete this module.
19
+
20
+ import type { AnyActorRef, EventObject } from "xstate";
21
+
22
+ /** One pending delayed event, JSON-safe. An actor is named by its path of child ids below the
23
+ * run's root (`[]` is the root). */
24
+ export type PendingTimer = {
25
+ source: string[];
26
+ target: string[];
27
+ /** xstate's scheduled id. */
28
+ id: string;
29
+ event: EventObject;
30
+ /** The delay as last scheduled, ms: the upper bound on what remains (see {@link startAndRearm}). */
31
+ delay: number;
32
+ /** Wall-clock ms at which it is due, on the host's clock (`now`). */
33
+ deadline: number;
34
+ };
35
+
36
+ function childrenOf(ref: AnyActorRef): Record<string, AnyActorRef> {
37
+ return (ref.getSnapshot() as { children?: Record<string, AnyActorRef> }).children ?? {};
38
+ }
39
+
40
+ function pathOf(root: AnyActorRef, ref: AnyActorRef, path: string[] = []): string[] | undefined {
41
+ if (ref === root) return path;
42
+ for (const [id, child] of Object.entries(childrenOf(root))) {
43
+ const found = pathOf(child, ref, [...path, id]);
44
+ if (found) return found;
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ function refAt(root: AnyActorRef, path: string[]): AnyActorRef | undefined {
50
+ let ref: AnyActorRef | undefined = root;
51
+ for (const id of path) ref = ref && childrenOf(ref)[id];
52
+ return ref;
53
+ }
54
+
55
+ /** xstate's key for a scheduled event: one per source actor and id. */
56
+ const keyOf = (source: AnyActorRef, id: string) => `${source.sessionId}.${id}`;
57
+
58
+ const clamp = (ms: number, delay: number) => Math.min(delay, Math.max(0, ms));
59
+
60
+ /**
61
+ * The run's pending timers, read off its actor system. Call at persist time: the scheduler's table
62
+ * and the snapshot are read in the same tick, so they agree. A timer whose source or target is not
63
+ * in the run's tree (a delayed send to an actor outside it) is not recorded; jr2 has no such idiom.
64
+ *
65
+ * xstate stamps a timer's start with `Date.now()`, so the time that remains is read on that clock;
66
+ * the deadline is `now + remaining`, on the host's clock. With the default `now` the two are one
67
+ * clock and the deadline is xstate's own `startedAt + delay`.
68
+ */
69
+ export function pendingTimers(root: AnyActorRef, now = Date.now()): PendingTimer[] {
70
+ const wall = Date.now();
71
+ const out: PendingTimer[] = [];
72
+ for (const s of Object.values(root.system.getSnapshot()._scheduledEvents)) {
73
+ const source = pathOf(root, s.source);
74
+ const target = pathOf(root, s.target);
75
+ if (!source || !target) continue;
76
+ const remaining = clamp(s.startedAt + s.delay - wall, s.delay);
77
+ out.push({ source, target, id: s.id, event: s.event, delay: s.delay, deadline: now + remaining });
78
+ }
79
+ return out;
80
+ }
81
+
82
+ /**
83
+ * The pending delayed events whose TARGET is `ref`, from any source: what Rest asks (rest.ts) —
84
+ * an `after`, a named delay, a delayed `raise` or `sendTo(self)`, a child's delayed `sendParent`.
85
+ * The same table {@link pendingTimers} persists and {@link startAndRearm} re-arms, so Rest reads
86
+ * the same timers after a restore as before it.
87
+ */
88
+ export function scheduledFor(ref: AnyActorRef): Array<{ id: string; event: EventObject }> {
89
+ return Object.values(ref.system.getSnapshot()._scheduledEvents)
90
+ .filter((s) => s.target === ref)
91
+ .map((s) => ({ id: s.id, event: s.event }));
92
+ }
93
+
94
+ /**
95
+ * Watch a run's actor system for timers that FIRE: the returned function says whether one relayed
96
+ * event is a timer the system scheduled — an `after`, a named delay, a delayed `raise` or `sendTo` —
97
+ * and not an event an actor sent at once. Both reach the inspection stream from the same source with
98
+ * the same shape; only the scheduler knows the difference, and it forgets a timer as it fires it.
99
+ *
100
+ * So `scheduler.schedule` and `scheduler.cancel` are wrapped for the life of the system: each armed
101
+ * timer is kept by its key with the event object xstate relays when it fires (the same object, not
102
+ * a copy). A relayed event is a fired timer when its source and event object are an armed timer's
103
+ * and that timer is no longer in the scheduler's table; it is then forgotten. A re-armed timer
104
+ * (`startAndRearm`) goes through the same `schedule`, so a restored run recognises it too.
105
+ *
106
+ * Install it before `start()` (the root's creation event is early enough): a timer armed before the
107
+ * wrap is not seen. The same xstate internals as the rest of this module; the canary is the same.
108
+ */
109
+ export function watchTimers(
110
+ system: AnyActorRef["system"],
111
+ hooks: TimerHooks = {},
112
+ ): (source: unknown, event: unknown) => boolean {
113
+ const scheduler = system.scheduler;
114
+ const { schedule, cancel } = scheduler;
115
+ const armed = new Map<string, { source: AnyActorRef; event: EventObject }>();
116
+ scheduler.schedule = (source, target, event, delay, id = Math.random().toString(36).slice(2)) => {
117
+ hooks.arming?.(source);
118
+ schedule(source, target, event, delay, id);
119
+ armed.set(keyOf(source, id), { source, event });
120
+ };
121
+ scheduler.cancel = (source, id) => {
122
+ armed.delete(keyOf(source, id));
123
+ cancel(source, id);
124
+ };
125
+ return (source, event) => {
126
+ for (const [key, row] of armed) {
127
+ if (row.event !== event || row.source !== source) continue;
128
+ if (key in system.getSnapshot()._scheduledEvents) continue;
129
+ armed.delete(key);
130
+ return true;
131
+ }
132
+ return false;
133
+ };
134
+ }
135
+
136
+ /** What `watchTimers` tells as the scheduler arms a timer: `arming` is called with the timer's
137
+ * source just before xstate schedules it, and xstate then arms it on the system's clock in the same
138
+ * call. xstate schedules in the deferred effects of the event whose actions armed the timer, so the
139
+ * source still takes that event. The run host keeps each timer's cause this way (`cause.ts`,
140
+ * ADR-0024). */
141
+ export type TimerHooks = {
142
+ arming?: (source: AnyActorRef) => void;
143
+ };
144
+
145
+ /**
146
+ * Start a restored run, then schedule each recorded timer again on it. One function, so the order
147
+ * cannot be got wrong: re-armed BEFORE `start()`, a timer is armed twice and fires twice, because
148
+ * the system's own `start()` schedules whatever is already in its table; and `start()` is where
149
+ * xstate builds the children, so a child's path resolves only after it.
150
+ *
151
+ * `start()` drains the mailbox: a child that sends on start (an author's `fromCallback`) can move
152
+ * the Machine out of a state before any timer is re-armed. That exit's `cancel(id)` finds nothing
153
+ * armed yet. So every cancel during `start()` is recorded, and a row it names is not re-armed: a
154
+ * state exit cancels the timer whether it happens during `start()` or after it.
155
+ *
156
+ * A child stopped during `start()` takes its timers with it, but xstate stops a child with
157
+ * `scheduler.cancelAll`, which walks the table, and the table is still empty: no cancel is seen.
158
+ * If its parent state is entered again, a NEW child sits at the same path. So each row's actors are
159
+ * resolved BEFORE `start()` (the restored tree already holds them), and a row is re-armed only on
160
+ * the same actors after it. A row of a stopped child then fires into nothing, not into its
161
+ * replacement.
162
+ *
163
+ * Remaining time is `deadline - now`, clamped to `[0, delay]`. A deadline that passed while the
164
+ * Orchestrator was down fires at once. A wall clock that went BACKWARDS cannot push a timer past its
165
+ * own delay. A wall clock that jumped forwards fires a timer early: only the wall clock survives a
166
+ * process, so the deadline is a wall-clock time. Rows are scheduled in deadline order, so timers
167
+ * that are all overdue (remaining 0) fire in the order they fell due, as in an uninterrupted run.
168
+ */
169
+ export function startAndRearm(root: AnyActorRef, timers: readonly PendingTimer[], now = Date.now()): void {
170
+ const scheduler = root.system.scheduler;
171
+ const cancel = scheduler.cancel;
172
+ const cancelled = new Set<string>();
173
+ const rows = [...timers]
174
+ // `sort` is stable: equal deadlines keep the order xstate scheduled them in.
175
+ .sort((a, b) => a.deadline - b.deadline)
176
+ .map((t) => ({ t, source: refAt(root, t.source), target: refAt(root, t.target) }));
177
+ scheduler.cancel = (source, id) => {
178
+ cancelled.add(keyOf(source, id));
179
+ cancel(source, id);
180
+ };
181
+ try {
182
+ root.start();
183
+ } finally {
184
+ scheduler.cancel = cancel;
185
+ }
186
+ for (const { t, source, target } of rows) {
187
+ if (!source || !target || cancelled.has(keyOf(source, t.id))) continue;
188
+ if (refAt(root, t.source) !== source || refAt(root, t.target) !== target) continue;
189
+ scheduler.schedule(source, target, t.event, clamp(t.deadline - now, t.delay), t.id);
190
+ }
191
+ }
package/src/tokens.ts CHANGED
@@ -3,13 +3,20 @@
3
3
  // a per-pod NetworkPolicy cannot distinguish its packets from the Custodian's. Only authentication
4
4
  // closes this, and only the token bounds what a caller may DO (as opposed to where it may talk).
5
5
  //
6
- // Two principals, and the asymmetry between them is the whole design:
6
+ // Three principals. The asymmetry between the first two is the whole design; the third is a door
7
+ // key, not a seat:
7
8
  //
8
9
  // Instance token the human/CLI credential. Full trust: gates, run control, agent surfaces.
9
10
  // From the instance Secret (`JR2_INSTANCE_TOKEN`); minted per boot when absent,
10
11
  // announced once on stdout (fixtures capture it). An Agent never has it — it
11
12
  // never enters a Sandbox.
12
13
  //
14
+ // Publish token `jr2 up`'s credential for `PUT /assembly` alone (ADR-0069). A publish imports
15
+ // code into the process that holds the ServiceAccount, so it must cost what an
16
+ // image roll cost: cluster write access. From the instance Secret
17
+ // (`JR2_PUBLISH_TOKEN`), read by `jr2 up` through the kube API only. Every other
18
+ // route answers it 403: it is a known token, but not one that route accepts.
19
+ //
13
20
  // Sandbox token the Custodian's credential. Authorizes exactly: deliver to `kind: "agent"`
14
21
  // registrations whose Sandbox is THIS one. Never a Gate (a compromised Agent
15
22
  // must not approve its own review), never another Sandbox (`coding.ts`'s iids
@@ -43,6 +50,9 @@ import { join } from "node:path";
43
50
  export type Principal =
44
51
  /** The instance's own operator: the CLI, a human, a webhook translator holding the token. */
45
52
  | { kind: "instance" }
53
+ /** `jr2 up`, publishing an Assembly (ADR-0069). Only `PUT /assembly` accepts it: it is not an
54
+ * Instance principal, so it reads no run and starts none. */
55
+ | { kind: "publisher" }
46
56
  /** One Sandbox's Custodian, speaking for the Agent in that pod — and for no one else. */
47
57
  | { kind: "sandbox"; sandbox: string };
48
58
 
@@ -77,7 +87,20 @@ export function harnessTokenDigest(key: Buffer, placement: string): string {
77
87
  return createHash("sha256").update(harnessToken(key, placement)).digest("base64url");
78
88
  }
79
89
 
80
- /** Mint the per-boot Instance token (opaque and random — it names nothing, it just IS the trust). */
90
+ /**
91
+ * The signing key of one Effector placement (ADR-0068): `HMAC(signing key, "effector:" + placement)`,
92
+ * as the Harness bearer is derived (ADR-0058). The Orchestrator signs every request to that
93
+ * Effector with it and verifies every answer; the Effector reads it from a file in its own mount,
94
+ * which `jr2 up` writes (`instance` → the `jr2-instance-effector` Secret). Derived, not stored: a
95
+ * restarted Orchestrator derives the key a live Effector already holds. The `effector:` prefix
96
+ * keeps it apart from every Sandbox token and Harness bearer. The key's bytes are this string's.
97
+ */
98
+ export function effectorKey(key: Buffer, placement: string): string {
99
+ return sign(key, `effector:${placement}`);
100
+ }
101
+
102
+ /** Mint the per-boot Instance token (opaque and random — it names nothing, it just IS the trust).
103
+ * The publish token is minted the same way when the Secret supplies none (ADR-0069). */
81
104
  export function mintInstanceToken(): string {
82
105
  return randomBytes(32).toString("base64url");
83
106
  }
@@ -109,10 +132,16 @@ export async function loadSigningKey(dir: string): Promise<Buffer> {
109
132
  * A token we did not mint resolves to nothing, and the caller is refused — there is no anonymous
110
133
  * principal, and no route decides for itself whether it needs one.
111
134
  */
112
- export function createAuthenticator(opts: { instanceToken: string; signingKey: Buffer }): Authenticator {
135
+ export function createAuthenticator(opts: {
136
+ instanceToken: string;
137
+ signingKey: Buffer;
138
+ /** The publish token (ADR-0069). Absent → no bearer resolves to the publisher. */
139
+ publishToken?: string;
140
+ }): Authenticator {
113
141
  return (bearer) => {
114
142
  if (!bearer) return undefined;
115
143
  if (constantTimeEqual(bearer, opts.instanceToken)) return { kind: "instance" };
144
+ if (opts.publishToken !== undefined && constantTimeEqual(bearer, opts.publishToken)) return { kind: "publisher" };
116
145
  const cut = bearer.lastIndexOf(".");
117
146
  if (cut <= 0) return undefined;
118
147
  const sandbox = bearer.slice(0, cut);
@@ -131,6 +160,7 @@ export function createAuthenticator(opts: { instanceToken: string; signingKey: B
131
160
  */
132
161
  export function mayDeliverToAgent(principal: Principal, registrationSandbox: string | undefined): boolean {
133
162
  if (principal.kind === "instance") return true;
163
+ if (principal.kind !== "sandbox") return false;
134
164
  return registrationSandbox !== undefined && registrationSandbox === principal.sandbox;
135
165
  }
136
166
 
@@ -138,10 +168,11 @@ export function mayDeliverToAgent(principal: Principal, registrationSandbox: str
138
168
  * May this principal ask for a fetch on this Sandbox (ADR-0053)? A Sandbox token may ask for its
139
169
  * OWN pod and no other — the scope is the caches that pod mounts, and the route refuses a Repo it
140
170
  * does not. The Instance token may, on the same grounds it may deliver to any agent surface: it is
141
- * the operator. Nothing else can hold either, so there is no third case.
171
+ * the operator. The publish token may not: it opens the publish door and no other (ADR-0069).
142
172
  */
143
173
  export function mayAskForSandbox(principal: Principal, sandbox: string): boolean {
144
174
  if (principal.kind === "instance") return true;
175
+ if (principal.kind !== "sandbox") return false;
145
176
  return principal.sandbox === sandbox;
146
177
  }
147
178
 
package/src/vocabulary.ts CHANGED
@@ -54,10 +54,32 @@ export function vocabularyOf(machine: AnyStateMachine): Map<string, EventDef> |
54
54
  * silently accepting a name nobody declared.
55
55
  */
56
56
  export function invokingMachine(self: AnyActorRef): AnyStateMachine | undefined {
57
- const logic = (self._parent as { logic?: unknown } | undefined)?.logic as AnyStateMachine | undefined;
57
+ // An Open body's actor (ADR-0070) delegates to the Machine compiled from the Chart it binds, and
58
+ // its own logic is no Machine: the bound Machine is the scope of the Agents and Gates its states
59
+ // invoke, so it is asked first.
60
+ const parent = self._parent;
61
+ const bound = parent ? boundMachines.get(parent) : undefined;
62
+ if (bound) return bound;
63
+ const logic = (parent as { logic?: unknown } | undefined)?.logic as AnyStateMachine | undefined;
58
64
  return logic?.root ? logic : undefined;
59
65
  }
60
66
 
67
+ // The Machine an Open body's actor runs (ADR-0070): compiled from the Chart the run bound, per
68
+ // actor, at start and again at restore. Keyed on the ACTOR, not on a config: two runs of one
69
+ // wrapper bind two Charts under the same slot. Kept here, beside the vocabulary, so this module
70
+ // stays a leaf: open-body.ts writes it, and `invokingMachine` reads it.
71
+ const boundMachines = new WeakMap<AnyActorRef, AnyStateMachine>();
72
+
73
+ /** Record the Machine an Open body's actor runs. jr2-internal: the Open body's logic calls it. */
74
+ export function attachBoundMachine(actor: AnyActorRef, machine: AnyStateMachine): void {
75
+ boundMachines.set(actor, machine);
76
+ }
77
+
78
+ /** The Machine an Open body's actor runs, or undefined for any other actor. */
79
+ export function boundMachineFor(actor: AnyActorRef): AnyStateMachine | undefined {
80
+ return boundMachines.get(actor);
81
+ }
82
+
61
83
  // The declared run input (ADR-0033): the one piece of a machine's vocabulary the event defs
62
84
  // missed — what a run of it is STARTED with. Same key choice as the vocabulary above, for the
63
85
  // same reasons (per-machine attribution, no global registry, transparent across `.provide()`).
@@ -79,22 +101,120 @@ export function inputSchemaOf(machine: AnyStateMachine): z.ZodObject | undefined
79
101
  return inputSchemas.get(machine.config);
80
102
  }
81
103
 
104
+ /** A zod schema, by its shape: the Instance can resolve a zod of its own, so `instanceof` does not
105
+ * tell. */
106
+ export function isZodSchema(value: unknown): boolean {
107
+ return (
108
+ typeof value === "object" &&
109
+ value !== null &&
110
+ "_zod" in value &&
111
+ typeof (value as { safeParse?: unknown }).safeParse === "function"
112
+ );
113
+ }
114
+
115
+ /** The run input that a machine declares: the door that `jr2Setup` attached, or a zod `input` on
116
+ * its `createMachine` config that no door reads (a machine that `setup()` made). */
117
+ export function declaresRunInput(machine: AnyStateMachine): boolean {
118
+ return inputSchemaOf(machine) !== undefined || isZodSchema((machine.config as { input?: unknown }).input);
119
+ }
120
+
121
+ // Who may send what, per state (ADR-0015): the seats the menu-derivation walk computed. Same key
122
+ // choice as the vocabulary, for the same reasons. The Machine's doc carries them; `jr2 up` and the
123
+ // Console show them, so an author reads the audience rule's answer for their own Machine.
124
+
125
+ /** One party's seat on one state: the state's id, the party, and the names it may send there —
126
+ * for an Agent the derived set its guards narrow; for a Gate its accepted set, unless the Gate's
127
+ * input names `accepts:` itself. */
128
+ export type MachineSeat = { state: string; party: "agent" | "gate"; agent?: string; names: string[] };
129
+
130
+ const seatTables = new WeakMap<MachineKey, MachineSeat[]>();
131
+
132
+ /** Attach a Machine's seats. jr2-internal: `jr2Setup.createMachine` calls it. */
133
+ export function attachSeats(machine: AnyStateMachine, seats: MachineSeat[]): void {
134
+ seatTables.set(machine.config, seats);
135
+ }
136
+
137
+ /** The seats of a Machine built by `jr2Setup`, in walk order; empty for any other Machine. */
138
+ export function seatsOf(machine: AnyStateMachine): MachineSeat[] {
139
+ return seatTables.get(machine.config) ?? [];
140
+ }
141
+
142
+ /** One line per seat — `<state> agent:<slot>: [names]` or `<state> gate: [names]`. */
143
+ export function seatLine(seat: MachineSeat): string {
144
+ const party = seat.party === "agent" ? `agent:${seat.agent}` : "gate";
145
+ return `${seat.state} ${party}: [${seat.names.join(", ")}]`;
146
+ }
147
+
148
+ const loadWarnings = new WeakMap<MachineKey, string[]>();
149
+
150
+ /** Attach the load warnings `jr2Setup.createMachine` found (ADR-0015). jr2-internal. */
151
+ export function attachLoadWarnings(machine: AnyStateMachine, warnings: string[]): void {
152
+ loadWarnings.set(machine.config, warnings);
153
+ }
154
+
155
+ /** What the build found legal and suspect — a mode nothing enters, a pick a Gate above takes from
156
+ * the Agent below, one name written for two parties on one path. `jr2 up` prints them; the
157
+ * Machine's doc carries them. A Machine `customize()` bound an Open argv in is rebuilt around a
158
+ * config of its own, which carries the walk's findings for the bound argv (ADR-0054). Empty for a
159
+ * Machine not built by `jr2Setup`. */
160
+ export function loadWarningsOf(machine: AnyStateMachine): string[] {
161
+ return loadWarnings.get(machine.config) ?? [];
162
+ }
163
+
164
+ // What the HOST adds to the input of the machine a run STARTS with, beside the door
165
+ // (`RunHost.start`). One field today: the run's seed Instance ID, minted with the run and
166
+ // reported by `jr2 status`. It names the RUN, never a conversation — jr2 mints every Agent
167
+ // Instance ID in the input mapper (ADR-0016, ADR-0057), and a Machine cannot pass one in.
168
+ //
169
+ // It is deliberately NOT door material (ADR-0033): no caller sends it — `start` overwrites
170
+ // whatever arrived, after the parse — and it is never served as JSON Schema, so putting it in a
171
+ // door schema would publish it and demand of every caller a field the host supplies anyway.
172
+ //
173
+ // It is also PLACEMENT-DEPENDENT: only the ROOT machine is started with it. A machine invoked
174
+ // further down is fed by its parent, which passes what it chooses. No type can see where a machine
175
+ // sits, so the one contract that must reason about this — `workspace()`'s body guard — takes the
176
+ // permissive answer and counts these keys as PROVIDED, which lets a body declare the field
177
+ // honestly. Counting them as provided (rather than subtracting them from what the body demands)
178
+ // is what keeps their TYPE checked: `instanceId` is a string, and a body asking for anything else
179
+ // is still rejected.
82
180
  /**
83
- * What the HOST adds to the input of the machine a run STARTS with, beside the door
84
- * (`RunHost.start`). One field today: the run's seed Instance ID, minted with the run and
85
- * reported by `jr2 status`. It names the RUN, never a conversation — jr2 mints every Agent
86
- * Instance ID in the input mapper (ADR-0016, ADR-0057), and a Machine cannot pass one in.
181
+ * The fields that jr2 adds to the input of the Machine that a run starts, beside the input that
182
+ * the caller sent. Today it is one field: `instanceId`, an id that jr2 makes for the run.
183
+ *
184
+ * Add it to the type of your root Machine's input when the Machine reads it:
185
+ * `HostInjectedInput & RunInput`. Do not put it in the `input` schema that a run starts with. No
186
+ * caller sends it. jr2 writes it over any value that a caller sends.
187
+ *
188
+ * Only the root Machine of a run gets it. A Machine that another Machine invokes gets the input
189
+ * that its parent gives it. A `workspace()` passes its input to its body, so the body of a root
190
+ * `workspace()` gets it too: `Workspaced<HostInjectedInput & RunInput, "app">`.
191
+ *
192
+ * The `spec` of a root `workspace()` also gets it, and the type of its input shows it:
193
+ * ``spec: ({ input }) => ({ branch: `fix/${input.instanceId}` })``.
194
+ *
195
+ * It is not the run id. Use it, for example, to give each run a branch of its own.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * import { z } from "zod";
200
+ * import { jr2Setup, type HostInjectedInput } from "@jr2/orchestrator";
201
+ *
202
+ * const runInput = z.object({ ticket: z.string() });
203
+ * type Input = HostInjectedInput & z.infer<typeof runInput>;
87
204
  *
88
- * It is deliberately NOT door material (ADR-0033): no caller sends it — `start` overwrites
89
- * whatever arrived, after the parse — and it is never served as JSON Schema, so putting it in a
90
- * door schema would publish it and demand of every caller a field the host supplies anyway.
205
+ * export const machine = jr2Setup({
206
+ * types: {} as { input: Input; context: Input; output: { run: string; ticket: string } },
207
+ * }).createMachine({
208
+ * id: "ticket",
209
+ * input: runInput,
210
+ * context: ({ input }) => input,
211
+ * initial: "done",
212
+ * states: { done: { type: "final" } },
213
+ * output: ({ context }) => ({ run: context.instanceId, ticket: context.ticket }),
214
+ * });
215
+ * ```
91
216
  *
92
- * It is also PLACEMENT-DEPENDENT: only the ROOT machine is started with it. A machine invoked
93
- * further down is fed by its parent, which passes what it chooses. No type can see where a machine
94
- * sits, so the one contract that must reason about this — `workspace()`'s body guard — takes the
95
- * permissive answer and counts these keys as PROVIDED, which lets a body declare the field
96
- * honestly. Counting them as provided (rather than subtracting them from what the body demands)
97
- * is what keeps their TYPE checked: `instanceId` is a string, and a body asking for anything else
98
- * is still rejected.
217
+ * @see docs/guides/write-a-workflow.md
218
+ * @see docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md
99
219
  */
100
220
  export type HostInjectedInput = { instanceId: string };