@jr2/orchestrator 0.4.1 → 0.6.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 (361) hide show
  1. package/README.md +20 -2
  2. package/console/components/app.ts +2 -0
  3. package/console/components/fleet.ts +14 -1
  4. package/console/components/machine-pane.ts +41 -2
  5. package/console/main.ts +13 -1
  6. package/console/store.ts +102 -6
  7. package/console/style.css +21 -0
  8. package/docs/agents-block.md +5 -0
  9. package/docs/concepts/agents-and-turns.md +478 -0
  10. package/docs/concepts/architecture.md +243 -0
  11. package/docs/concepts/charts.md +146 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +257 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +658 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +298 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +306 -0
  20. package/docs/concepts/the-menu.md +381 -0
  21. package/docs/concepts/workspaces.md +756 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +383 -0
  24. package/docs/get-started/connect-a-model.md +248 -0
  25. package/docs/get-started/create-an-instance.md +194 -0
  26. package/docs/get-started/have-your-assistant-write-a-machine.md +993 -0
  27. package/docs/get-started/install.md +130 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +308 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +224 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/cut-a-worktree.md +560 -0
  34. package/docs/guides/fix-a-failed-converge.md +361 -0
  35. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  36. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  37. package/docs/guides/let-an-agent-write-a-chart.md +230 -0
  38. package/docs/guides/look-inside-a-sandbox.md +177 -0
  39. package/docs/guides/mirror-the-kit-images.md +176 -0
  40. package/docs/guides/move-from-kind-to-a-shared-cluster.md +318 -0
  41. package/docs/guides/open-the-console.md +91 -0
  42. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  43. package/docs/guides/push-and-sign-a-branch.md +532 -0
  44. package/docs/guides/run-a-chart-by-name.md +270 -0
  45. package/docs/guides/run-a-command-or-call-a-url.md +587 -0
  46. package/docs/guides/run-one-worker-per-item.md +322 -0
  47. package/docs/guides/set-an-agents-access-and-dials.md +420 -0
  48. package/docs/guides/size-a-workspace.md +217 -0
  49. package/docs/guides/start-a-run-from-a-run.md +237 -0
  50. package/docs/guides/start-a-run.md +207 -0
  51. package/docs/guides/upgrade-the-kit-version.md +240 -0
  52. package/docs/guides/use-a-hosted-provider.md +214 -0
  53. package/docs/guides/use-a-local-model.md +313 -0
  54. package/docs/guides/watch-a-run.md +349 -0
  55. package/docs/guides/write-a-workflow.md +318 -0
  56. package/docs/index.md +44 -0
  57. package/docs/llms.txt +237 -0
  58. package/docs/reference/authoring-surface/agent.md +151 -0
  59. package/docs/reference/authoring-surface/answer.md +156 -0
  60. package/docs/reference/authoring-surface/arg.md +82 -0
  61. package/docs/reference/authoring-surface/basic.md +62 -0
  62. package/docs/reference/authoring-surface/bearer.md +53 -0
  63. package/docs/reference/authoring-surface/branchname.md +15 -0
  64. package/docs/reference/authoring-surface/chartschema.md +23 -0
  65. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  66. package/docs/reference/authoring-surface/command.md +206 -0
  67. package/docs/reference/authoring-surface/customize.md +64 -0
  68. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  69. package/docs/reference/authoring-surface/defineevent.md +114 -0
  70. package/docs/reference/authoring-surface/dispatch.md +126 -0
  71. package/docs/reference/authoring-surface/fail.md +96 -0
  72. package/docs/reference/authoring-surface/file.md +142 -0
  73. package/docs/reference/authoring-surface/frompick.md +277 -0
  74. package/docs/reference/authoring-surface/http.md +259 -0
  75. package/docs/reference/authoring-surface/isfailure.md +58 -0
  76. package/docs/reference/authoring-surface/isready.md +14 -0
  77. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  78. package/docs/reference/authoring-surface/open.md +105 -0
  79. package/docs/reference/authoring-surface/openbody.md +86 -0
  80. package/docs/reference/authoring-surface/paramsof.md +143 -0
  81. package/docs/reference/authoring-surface/pool.md +186 -0
  82. package/docs/reference/authoring-surface/push.md +130 -0
  83. package/docs/reference/authoring-surface/readyof.md +21 -0
  84. package/docs/reference/authoring-surface/secret.md +139 -0
  85. package/docs/reference/authoring-surface/sentby.md +83 -0
  86. package/docs/reference/authoring-surface/source.md +51 -0
  87. package/docs/reference/authoring-surface/text.md +62 -0
  88. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  89. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  90. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  91. package/docs/reference/authoring-surface/type-agentslots.md +15 -0
  92. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  93. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  94. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  95. package/docs/reference/authoring-surface/type-arg.md +19 -0
  96. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  97. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  98. package/docs/reference/authoring-surface/type-binding.md +61 -0
  99. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  100. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  101. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  102. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  103. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  104. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  105. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  106. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  107. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  108. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  109. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  110. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  111. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  112. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  113. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  114. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  115. package/docs/reference/authoring-surface/type-childat.md +12 -0
  116. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  117. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  118. package/docs/reference/authoring-surface/type-commandinput.md +20 -0
  119. package/docs/reference/authoring-surface/type-commandoptions.md +56 -0
  120. package/docs/reference/authoring-surface/type-commandoutput.md +122 -0
  121. package/docs/reference/authoring-surface/type-customize.md +111 -0
  122. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  123. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  124. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  125. package/docs/reference/authoring-surface/type-dispatchoutput.md +72 -0
  126. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  127. package/docs/reference/authoring-surface/type-duration.md +15 -0
  128. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  129. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  130. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  131. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  132. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  133. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  134. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  135. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  136. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  137. package/docs/reference/authoring-surface/type-failure.md +19 -0
  138. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  139. package/docs/reference/authoring-surface/type-filebody.md +37 -0
  140. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  141. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  142. package/docs/reference/authoring-surface/type-hostinjectedinput.md +56 -0
  143. package/docs/reference/authoring-surface/type-httpdefinition.md +104 -0
  144. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  145. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  146. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  147. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +95 -0
  149. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  150. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  151. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  152. package/docs/reference/authoring-surface/type-lends.md +38 -0
  153. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  154. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  155. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  156. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  157. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  158. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +27 -0
  159. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  160. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  161. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  162. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  163. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  164. package/docs/reference/authoring-surface/type-opencommandinput.md +22 -0
  165. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  166. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  167. package/docs/reference/authoring-surface/type-openof.md +12 -0
  168. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  169. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  170. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +26 -0
  171. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +27 -0
  172. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  173. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  174. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  175. package/docs/reference/authoring-surface/type-pushinput.md +34 -0
  176. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  177. package/docs/reference/authoring-surface/type-pushoutput.md +149 -0
  178. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  179. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  180. package/docs/reference/authoring-surface/type-reached.md +12 -0
  181. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  182. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  183. package/docs/reference/authoring-surface/type-sandboxoptions.md +84 -0
  184. package/docs/reference/authoring-surface/type-secret.md +25 -0
  185. package/docs/reference/authoring-surface/type-sender.md +19 -0
  186. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  187. package/docs/reference/authoring-surface/type-size.md +85 -0
  188. package/docs/reference/authoring-surface/type-slothandles.md +24 -0
  189. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  190. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  191. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  192. package/docs/reference/authoring-surface/type-text.md +19 -0
  193. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  194. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  195. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  196. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  197. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  198. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  199. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  200. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  201. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  202. package/docs/reference/authoring-surface/type-workspaced.md +115 -0
  203. package/docs/reference/authoring-surface/type-workspacehandles.md +22 -0
  204. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  205. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  206. package/docs/reference/authoring-surface/type-workspaceoptions.md +27 -0
  207. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  208. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  209. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  210. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  211. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  212. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  213. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  214. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  215. package/docs/reference/authoring-surface/workspace.md +287 -0
  216. package/docs/reference/authoring-surface/worktree.md +71 -0
  217. package/docs/reference/authoring-surface.md +217 -0
  218. package/docs/reference/cli.md +433 -0
  219. package/docs/reference/config.md +765 -0
  220. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +114 -0
  221. package/docs/reference/decisions/0002-duplex-streaming-actor.md +117 -0
  222. package/docs/reference/decisions/0004-worktree-provisioning-model.md +130 -0
  223. package/docs/reference/decisions/0005-sandbox-pod-composition.md +240 -0
  224. package/docs/reference/decisions/0006-agent-control-surface.md +50 -0
  225. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  226. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  227. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  228. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  229. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  230. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  231. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +175 -0
  232. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  233. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +271 -0
  234. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +126 -0
  235. package/docs/reference/decisions/0017-pool-over-a-source-port.md +71 -0
  236. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  237. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +287 -0
  238. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  239. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +101 -0
  240. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  241. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +175 -0
  242. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +436 -0
  243. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  244. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  245. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +245 -0
  246. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +90 -0
  247. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  248. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  249. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +193 -0
  250. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  251. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +126 -0
  252. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  253. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  254. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  255. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  256. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  257. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  258. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  259. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  260. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  261. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  262. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  263. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  264. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  265. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  266. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  267. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  268. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +238 -0
  269. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +86 -0
  270. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +123 -0
  271. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  272. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  273. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  274. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  275. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +108 -0
  276. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +427 -0
  277. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  278. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  279. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +239 -0
  280. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +82 -0
  281. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  282. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  283. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +552 -0
  284. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +458 -0
  285. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +475 -0
  286. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  287. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +334 -0
  288. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  289. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  290. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  291. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  292. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
  293. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
  294. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  295. package/docs/reference/decisions.md +86 -0
  296. package/docs/reference/glossary.md +687 -0
  297. package/docs/reference/http-api.md +1386 -0
  298. package/docs/reference/machines.md +189 -0
  299. package/docs/reference/mechanism-events.md +72 -0
  300. package/docs/why.md +207 -0
  301. package/package.json +5 -3
  302. package/src/actor.ts +694 -106
  303. package/src/agent.ts +114 -38
  304. package/src/ambient.ts +3 -4
  305. package/src/assembly-checks.ts +189 -7
  306. package/src/cause.ts +234 -0
  307. package/src/chart.ts +345 -46
  308. package/src/charts.ts +43 -8
  309. package/src/config.ts +440 -178
  310. package/src/custodian.lua +17 -5
  311. package/src/custodian.ts +325 -99
  312. package/src/customize.ts +199 -49
  313. package/src/dispatch-effect.ts +945 -0
  314. package/src/door-compat.ts +217 -0
  315. package/src/durability.ts +11 -4
  316. package/src/effect-checks.ts +50 -3
  317. package/src/effect.ts +641 -128
  318. package/src/effector-wire.ts +165 -13
  319. package/src/fail.ts +222 -0
  320. package/src/harness-client.ts +587 -50
  321. package/src/harness-env.ts +33 -0
  322. package/src/held-secrets.ts +125 -16
  323. package/src/holds.ts +3 -0
  324. package/src/http-effect.ts +556 -58
  325. package/src/http.ts +480 -30
  326. package/src/index.ts +74 -82
  327. package/src/instance.ts +68 -8
  328. package/src/internal.ts +85 -0
  329. package/src/kube-client.ts +6 -3
  330. package/src/machine-doc.ts +24 -7
  331. package/src/mechanism-routes.ts +168 -0
  332. package/src/model-keys.generated.ts +53 -0
  333. package/src/model-keys.ts +42 -58
  334. package/src/names.ts +19 -6
  335. package/src/notices.ts +483 -41
  336. package/src/open-body.ts +242 -40
  337. package/src/open-effect.ts +81 -8
  338. package/src/open.ts +90 -6
  339. package/src/parts.ts +213 -30
  340. package/src/party.ts +101 -2
  341. package/src/pick.ts +497 -11
  342. package/src/pool.ts +282 -50
  343. package/src/registration.ts +133 -12
  344. package/src/repo-identity.ts +7 -3
  345. package/src/repos.ts +22 -3
  346. package/src/run-host.ts +665 -45
  347. package/src/run-output.ts +38 -0
  348. package/src/sandbox-effect.ts +457 -71
  349. package/src/sandbox-kube.ts +66 -17
  350. package/src/secret-placement.ts +59 -17
  351. package/src/server.ts +3 -0
  352. package/src/setup.ts +325 -62
  353. package/src/size.ts +121 -9
  354. package/src/snapshot-store.ts +54 -10
  355. package/src/stub-harness.ts +44 -16
  356. package/src/timers.ts +14 -1
  357. package/src/tokens.ts +107 -32
  358. package/src/vocabulary.ts +88 -14
  359. package/src/wire.ts +100 -26
  360. package/src/workspace.ts +771 -238
  361. package/src/worktree.ts +249 -0
package/src/size.ts CHANGED
@@ -23,15 +23,73 @@
23
23
  /** A Kubernetes quantity: `"2Gi"`, `"1500m"`, `"2"`, or a plain number (`2` cpus, bytes of memory). */
24
24
  export type Quantity = string | number;
25
25
 
26
+ // A Size (ADR-0060, GLOSSARY.md "Size"): `limits.memory` and `limits.cpu`, nothing else. A field
27
+ // left out resolves down the chain (`resolveSize`) — the Instance's `sandbox.resources`, then the
28
+ // kit default (`KIT_SIZE`). The same shape is the User Container's split (`userSeatOf`,
29
+ // `splitSize`), where a field left out means the seat has no limit of its own for it and shares
30
+ // the pod's budget.
26
31
  /**
27
- * A Size (ADR-0060, GLOSSARY.md "Size"): `limits.memory` and `limits.cpu`, nothing else. A field
28
- * left out resolves down the chain (`resolveSize`) — the Instance's `sandbox.resources`, then the
29
- * kit default of 2Gi and 1 cpu.
32
+ * The Size of a Workspace: the most memory and cpu that its whole Sandbox can use. jr2 reserves
33
+ * all of it on the node. Thus a Sandbox never gets less than its Size and never uses more.
30
34
  *
31
- * The same shape is the User Container's split (`user: { image, resources }`), where a field left
32
- * out means the seat has no limit of its own for it and shares the pod's budget.
35
+ * Write it as `{ limits: { memory, cpu } }`. You can write it in three places: the `resources`
36
+ * option of `workspace()`, `customize(machine, { resources })`, and `sandbox.resources` in
37
+ * `jr2.config.ts`. The Size is a fact of the Machine. The Instance's `sandbox.resources` applies
38
+ * only to a Workspace that states no Size. A field that neither states comes from the kit default:
39
+ * memory `2Gi`, cpu `1`.
40
+ *
41
+ * The same shape is the User Container's part of the Size, in `user: { image, resources }`. There,
42
+ * a field you leave out gives the User Container no limit of its own: it shares the pod's budget.
43
+ *
44
+ * It refuses any key but `limits.memory` and `limits.cpu`, where you write it. The message names
45
+ * the field: `<where>` is where you wrote the Size, for example `workspace(): resources`.
46
+ *
47
+ * - `<where>.requests is not accepted — jr2 reserves the whole Size, so every request equals its
48
+ * limit — a Size is { limits: { memory?, cpu? } } and nothing else — Agent Substrate's template
49
+ * size (ADR-0060)`
50
+ * - `<where>.<key> is not accepted — a Size is { limits: { memory?, cpu? } } and nothing else —
51
+ * Agent Substrate's template size (ADR-0060)`
52
+ * - `<where>.limits.<key> is not accepted — a Size is { limits: { memory?, cpu? } } and nothing
53
+ * else — Agent Substrate's template size (ADR-0060)`
54
+ * - `<where>.limits.memory: <value> is not a positive quantity (e.g. "2Gi", "512Mi")`, and for
55
+ * cpu, `<where>.limits.cpu: <value> is not a positive quantity (e.g. "2", "1500m")`
56
+ *
57
+ * `jr2 up` also refuses a Size that leaves the Harness container less than its floor. The message
58
+ * starts with `the Size leaves the Harness container <memory> memory, below its 256Mi floor:` or
59
+ * with `the Size leaves the Harness container no cpu:`. The text that follows names each number
60
+ * and where it came from.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * import { customize, type Size } from "@jr2/orchestrator";
65
+ * import { task } from "@jr2/machines";
66
+ *
67
+ * const size: Size = { limits: { memory: "4Gi", cpu: "2" } };
68
+ *
69
+ * export const machine = customize(task, {
70
+ * repos: { app: { url: "https://github.com/you/app.git" } },
71
+ * agents: { coder: { model: "anthropic/claude-haiku-5-5" } },
72
+ * resources: size,
73
+ * });
74
+ * ```
75
+ *
76
+ * @see docs/guides/size-a-workspace.md
77
+ * @see docs/concepts/workspaces.md
78
+ * @see docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md
33
79
  */
34
- export type Size = { limits: { memory?: Quantity; cpu?: Quantity } };
80
+ export type Size = {
81
+ /** The two limits. Write `limits: {}` to state no field: each field then comes from the next
82
+ * source. */
83
+ limits: {
84
+ /** The memory limit, as a Kubernetes quantity: `"4Gi"`, `"512Mi"`, or a number of bytes.
85
+ * It must be more than zero. When you leave it out, it comes from the next source, as above. */
86
+ memory?: Quantity;
87
+ /** The cpu limit, as a Kubernetes quantity: `"2"`, `"1500m"`, or a number of cpus. It must be
88
+ * more than zero. jr2 enforces it. When you leave it out, it comes from the next source, as
89
+ * above. */
90
+ cpu?: Quantity;
91
+ };
92
+ };
35
93
 
36
94
  /** The kit's default Size (ADR-0060). Node gives each process a V8 heap of about 55% of its
37
95
  * container limit, and `tsc` on a mid-size repo aborted below about 1.1Gi, so 1Gi fails a common
@@ -303,11 +361,65 @@ export function splitSize(size: ResolvedSize, user?: Size): SizeSplit {
303
361
 
304
362
  // --- the User Container seat -------------------------------------------------------------------
305
363
 
364
+ // The User Container as a Machine states it (ADR-0005, ADR-0060): an image in ADR-0037's two
365
+ // shapes, and optionally its split of the Size. The object form exists for the split alone.
306
366
  /**
307
- * The User Container as a Machine states it (ADR-0005, ADR-0060): an image in ADR-0037's two
308
- * shapes, and optionally its split of the Size. The object form exists for the split alone.
367
+ * The User Container in its object form: its image, and its part of the Workspace's Size. The User
368
+ * Container is an optional container of your own in the Sandbox pod. It runs its own entrypoint
369
+ * with `/work` mounted, and jr2 never builds into it, probes it or sends it commands. Use it for a
370
+ * service that must run without an Agent, such as an sshd.
371
+ *
372
+ * Write it in the `user` option of `workspace()` or of `customize()`. That option also takes the
373
+ * image as a string; use this object form only to give the User Container a part of the Size. A
374
+ * `customize()` that names `user` replaces the whole User Container, its part of the Size
375
+ * included.
376
+ *
377
+ * It refuses, where you write it. `<where>` is `workspace()` or `customize()`:
378
+ *
379
+ * - an image that is not a non-empty string. The message starts with
380
+ * `` <where>: `user` must be a non-empty string (an image), or { image, resources? } ``.
381
+ * - `<where>: user.<key> is not accepted — the User Container is { image, resources? } (ADR-0005,
382
+ * ADR-0060)`
383
+ *
384
+ * `resources` is checked as a {@link Size}, with the same refusals.
385
+ *
386
+ * `jr2 up` refuses a part that leaves the Harness container less than its floor. The message starts
387
+ * with `the Size leaves the Harness container <memory> memory, below its 256Mi floor:` or with
388
+ * `the Size leaves the Harness container no cpu:`. The text that follows names each number and
389
+ * where it came from.
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * import { customize, type UserContainer } from "@jr2/orchestrator";
394
+ * import { task } from "@jr2/machines";
395
+ *
396
+ * const sshd: UserContainer = {
397
+ * image: "registry.example.com/sshd:1.0",
398
+ * resources: { limits: { memory: "256Mi", cpu: "250m" } },
399
+ * };
400
+ *
401
+ * export const machine = customize(task, {
402
+ * repos: { app: { url: "https://github.com/you/app.git" } },
403
+ * agents: { coder: { model: "anthropic/claude-haiku-5-5" } },
404
+ * user: sshd,
405
+ * });
406
+ * ```
407
+ *
408
+ * @see docs/concepts/workspaces.md
409
+ * @see docs/guides/size-a-workspace.md
410
+ * @see docs/reference/decisions/0005-sandbox-pod-composition.md
411
+ * @see docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md
309
412
  */
310
- export type UserContainer = { image: string; resources?: Size };
413
+ export type UserContainer = {
414
+ /** The image: a `file:` URL to a docker context that your module ships
415
+ * (`import.meta.resolve("./sshd")`), which `jr2 up` builds, or a registry ref. It must not be
416
+ * empty. */
417
+ image: string;
418
+ /** This container's part of the Workspace's Size, as a Size. jr2 takes it from the Harness
419
+ * container's part. A field you leave out gives this container no limit of its own for it: it
420
+ * shares the pod's budget. Default: no part. */
421
+ resources?: Size;
422
+ };
311
423
 
312
424
  /** The `user` option in either shape, read as its two halves. */
313
425
  export function userSeatOf(user: string | UserContainer | undefined): { image?: string; resources?: Size } {
@@ -4,7 +4,8 @@
4
4
  // store is owed — with `:memory:` for tests. A persist survives a process crash, not a power loss
5
5
  // (WAL, `synchronous=NORMAL`), and the write stays synchronous so the row is on disk before the
6
6
  // Machine's next side effect runs. `markLost` records a run whose live state could not be
7
- // re-hydrated (e.g. its flue handle is gone); a non-live row lives a week, then `sweep` deletes it.
7
+ // re-hydrated (e.g. its flue handle is gone), keeping any snapshot the row had; a non-live row
8
+ // lives a week, then `sweep` deletes it.
8
9
 
9
10
  import { DatabaseSync } from "node:sqlite";
10
11
  import type { StoredSnapshot } from "./durability.ts";
@@ -37,18 +38,31 @@ export interface SnapshotStore {
37
38
  * silently to the live one, which makes resolution unsound.
38
39
  */
39
40
  findIdsByPrefix(prefix: string, limit: number): Promise<string[]>;
41
+ /**
42
+ * The run whose blob carries the idempotency key `effect` (`dispatchedBy.effect`, ADR-0071), in
43
+ * any status, or undefined: what answers a re-sent dispatch with the run it already started. A
44
+ * row `markLost` wrote with no save before it has no snapshot, and so no key; a swept row is gone
45
+ * with its key.
46
+ */
47
+ findByDispatch(effect: string): Promise<string | undefined>;
40
48
  save(runId: string, snapshot: unknown, status?: string): Promise<void>;
49
+ /**
50
+ * Record a run whose live state could not be re-hydrated. An upsert: a row that was saved KEEPS
51
+ * its snapshot and its idempotency key, because a lost dispatched run must keep answering its
52
+ * key — a re-send from its caller would otherwise start a second run (ADR-0071). Only a run with
53
+ * no row before gets a row with no snapshot.
54
+ */
41
55
  markLost(runId: string, reason: string): Promise<void>;
42
56
  /**
43
57
  * Record a run this Orchestrator can no longer read (ADR-0030, ADR-0069) — refused, not resumed:
44
58
  * the Assembly its snapshot names is not on the volume, was published for another Kit version or
45
59
  * does not load, or its Machine's shape does not match the snapshot's stamp.
46
60
  *
47
- * Unlike {@link markLost} this KEEPS the snapshot. A lost run has nothing left to look at, so
48
- * nulling it costs nothing; a drifted run is intact and only unreadable by this Orchestrator,
49
- * and it is the one a human most needs to inspect. Nulling it would also hide the run
50
- * entirely: `read()` returns undefined for a null blob, so `jr2 status <id>` would answer
51
- * `no run` — a refusal indistinguishable from a run that never existed.
61
+ * An UPDATE of a row that is there, and it KEEPS the snapshot: a drifted run is intact and only
62
+ * unreadable by this Orchestrator, and it is the one a human most needs to inspect. Nulling it
63
+ * would also hide the run entirely: `read()` returns undefined for a null blob, so
64
+ * `jr2 status <id>` would answer `no run` — a refusal indistinguishable from a run that never
65
+ * existed. It keeps the row's idempotency key too (ADR-0071).
52
66
  */
53
67
  markDrifted(runId: string, reason: string): Promise<void>;
54
68
  close(): Promise<void>;
@@ -62,6 +76,20 @@ type Row = {
62
76
  updated_at: string;
63
77
  };
64
78
 
79
+ /** The lookup `findByDispatch` runs, exported so a test can ask the planner about THIS statement,
80
+ * not a copy of it: the column is indexed, so the plan is a seek. */
81
+ export const FIND_BY_DISPATCH_SQL = `SELECT run_id FROM machine_snapshots
82
+ WHERE dispatch_effect = ? ORDER BY rowid LIMIT 1`;
83
+
84
+ /** The idempotency key a blob carries at `dispatchedBy.effect` (ADR-0071), or null. */
85
+ function dispatchEffectOf(snapshot: unknown): string | null {
86
+ if (typeof snapshot !== "object" || snapshot === null) return null;
87
+ const by = (snapshot as { dispatchedBy?: unknown }).dispatchedBy;
88
+ if (typeof by !== "object" || by === null) return null;
89
+ const effect = (by as { effect?: unknown }).effect;
90
+ return typeof effect === "string" ? effect : null;
91
+ }
92
+
65
93
  export class SqliteSnapshotStore implements SnapshotStore {
66
94
  private readonly db: DatabaseSync;
67
95
 
@@ -83,6 +111,16 @@ export class SqliteSnapshotStore implements SnapshotStore {
83
111
  updated_at TEXT
84
112
  )
85
113
  `);
114
+ // The idempotency key of a dispatched run (ADR-0071) is copied out of its blob into a column of
115
+ // its own when `save` writes the row, and the column is indexed. Not an expression index over
116
+ // `json_extract(snapshot, …)`: that runs on every row and THROWS on a blob SQLite's JSON parser
117
+ // refuses (nested about a thousand deep), so one such row would fail its own save and every
118
+ // later `init()` over the volume. A column also costs no JSON parse a save.
119
+ const columns = this.db.prepare(`PRAGMA table_info(machine_snapshots)`).all() as Array<{ name: string }>;
120
+ if (!columns.some((c) => c.name === "dispatch_effect")) {
121
+ this.db.exec(`ALTER TABLE machine_snapshots ADD COLUMN dispatch_effect TEXT`);
122
+ }
123
+ this.db.exec(`CREATE INDEX IF NOT EXISTS machine_snapshots_dispatch_effect ON machine_snapshots(dispatch_effect)`);
86
124
  }
87
125
 
88
126
  async load(runId: string): Promise<StoredSnapshot | null> {
@@ -151,18 +189,24 @@ export class SqliteSnapshotStore implements SnapshotStore {
151
189
  return rows.map((row) => row.run_id);
152
190
  }
153
191
 
192
+ async findByDispatch(effect: string): Promise<string | undefined> {
193
+ const row = this.db.prepare(FIND_BY_DISPATCH_SQL).get(effect) as { run_id: string } | undefined;
194
+ return row?.run_id;
195
+ }
196
+
154
197
  async save(runId: string, snapshot: unknown, status = "live"): Promise<void> {
155
198
  this.db
156
199
  .prepare(
157
- `INSERT INTO machine_snapshots (run_id, status, snapshot, reason, updated_at)
158
- VALUES (?, ?, ?, NULL, ?)
200
+ `INSERT INTO machine_snapshots (run_id, status, snapshot, reason, updated_at, dispatch_effect)
201
+ VALUES (?, ?, ?, NULL, ?, ?)
159
202
  ON CONFLICT(run_id) DO UPDATE SET
160
203
  status = excluded.status,
161
204
  snapshot = excluded.snapshot,
162
205
  reason = NULL,
163
- updated_at = excluded.updated_at`,
206
+ updated_at = excluded.updated_at,
207
+ dispatch_effect = excluded.dispatch_effect`,
164
208
  )
165
- .run(runId, status, JSON.stringify(snapshot), new Date().toISOString());
209
+ .run(runId, status, JSON.stringify(snapshot), new Date().toISOString(), dispatchEffectOf(snapshot));
166
210
  }
167
211
 
168
212
  async markLost(runId: string, reason: string): Promise<void> {
@@ -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.
@@ -55,8 +61,8 @@ export type Admission = {
55
61
  };
56
62
 
57
63
  /** One settled submission, in the shape `history()` reports it. The stub settles submissions for
58
- * exactly one reason — an abort (ADR-0024) — so `outcome` has exactly one value here. */
59
- 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" };
60
66
 
61
67
  export type StubHarnessOptions = {
62
68
  /** Listen port. Default 0 → ephemeral (read back from `url`). */
@@ -102,17 +108,39 @@ export async function startStubHarness(opts: StubHarnessOptions = {}): Promise<R
102
108
  // End every in-flight and queued submission for one instance (`agents.abort` — ADR-0024).
103
109
  // Answers `{ aborted }`: whether there was anything to end, exactly as the real Harness
104
110
  // does for an idle instance. Settlement is recorded here rather than pushed on the stream, because nothing is
105
- // 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`.
106
115
  const aborting = req.method === "POST" && ABORT_PATH.exec(url.pathname);
107
116
  if (aborting) {
108
117
  const [, agentName, instanceId] = aborting as unknown as [string, string, string];
109
- const k = key(decodeURIComponent(agentName), decodeURIComponent(instanceId));
110
- const ended = unsettled.get(k)?.splice(0) ?? [];
111
- const settled = settlements.get(k) ?? [];
112
- settled.push(...ended.map((submissionId) => ({ submissionId, outcome: "aborted" as const })));
113
- 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") {
114
142
  res.writeHead(200, { "content-type": "application/json" });
115
- res.end(JSON.stringify({ aborted: ended.length > 0 }));
143
+ res.end(JSON.stringify({ kills: [] }));
116
144
  return;
117
145
  }
118
146
 
package/src/timers.ts CHANGED
@@ -106,11 +106,15 @@ export function scheduledFor(ref: AnyActorRef): Array<{ id: string; event: Event
106
106
  * Install it before `start()` (the root's creation event is early enough): a timer armed before the
107
107
  * wrap is not seen. The same xstate internals as the rest of this module; the canary is the same.
108
108
  */
109
- export function watchTimers(system: AnyActorRef["system"]): (source: unknown, event: unknown) => boolean {
109
+ export function watchTimers(
110
+ system: AnyActorRef["system"],
111
+ hooks: TimerHooks = {},
112
+ ): (source: unknown, event: unknown) => boolean {
110
113
  const scheduler = system.scheduler;
111
114
  const { schedule, cancel } = scheduler;
112
115
  const armed = new Map<string, { source: AnyActorRef; event: EventObject }>();
113
116
  scheduler.schedule = (source, target, event, delay, id = Math.random().toString(36).slice(2)) => {
117
+ hooks.arming?.(source);
114
118
  schedule(source, target, event, delay, id);
115
119
  armed.set(keyOf(source, id), { source, event });
116
120
  };
@@ -129,6 +133,15 @@ export function watchTimers(system: AnyActorRef["system"]): (source: unknown, ev
129
133
  };
130
134
  }
131
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
+
132
145
  /**
133
146
  * Start a restored run, then schedule each recorded timer again on it. One function, so the order
134
147
  * cannot be got wrong: re-armed BEFORE `start()`, a timer is armed twice and fires twice, because
package/src/tokens.ts CHANGED
@@ -17,17 +17,20 @@
17
17
  // (`JR2_PUBLISH_TOKEN`), read by `jr2 up` through the kube API only. Every other
18
18
  // route answers it 403: it is a known token, but not one that route accepts.
19
19
  //
20
- // Sandbox token the Custodian's credential. Authorizes exactly: deliver to `kind: "agent"`
21
- // registrations whose Sandbox is THIS one. Never a Gate (a compromised Agent
22
- // must not approve its own review), never another Sandbox (`coding.ts`'s iids
23
- // are derivable and feature ids are readable from the Work Source, so a merely
24
- // run-scoped token would let one feature's coder inject a verdict into another
25
- // feature's reviewer). Delivered as a Secret mounted into the Custodian
26
- // container ALONE, which the Agent cannot read; the Harness holds its
27
- // Stand-in (ADR-0059).
28
- // The signed name is a POD hosting Turns, not a Sandbox CR per se: the Instance
29
- // Harness's Custodian holds one signed for that placement's Service name
30
- // (ADR-0031), scoping it to the Menu-only registrations placed there.
20
+ // Sandbox token the Custodian's credentials, two of them, each valid on its own routes alone
21
+ // (ADR-0076). Both are signed names of a POD hosting Turns — a Sandbox, or the
22
+ // Instance Harness's Service name (ADR-0031) — and both live in the Custodian
23
+ // container ALONE, which no Workspace code reaches (ADR-0059):
24
+ // control token `agent:<pod>.<sig>`. Delivers to `kind: "agent"` registrations whose pod is
25
+ // THIS one, and only for the slot the Harness states (`X-Jr2-Slot`), on the
26
+ // four agent routes. The Harness process alone holds its secret Stand-in.
27
+ // fetch token `<pod>.<sig>`. Asks for a fetch for THIS Sandbox (ADR-0053), on
28
+ // `POST /sandboxes/<pod>/fetch` alone. Its Stand-in is public: it can do
29
+ // nothing else.
30
+ // Neither is ever a Gate's (a compromised Agent must not approve its own review),
31
+ // and neither reaches another pod (`coding.ts`'s iids are derivable and feature
32
+ // ids are readable from the Work Source, so a merely run-scoped token would let
33
+ // one feature's coder inject a verdict into another feature's reviewer).
31
34
  //
32
35
  // And one bearer the other way (ADR-0058), which is no principal here because this server never
33
36
  // checks it:
@@ -53,17 +56,53 @@ export type Principal =
53
56
  /** `jr2 up`, publishing an Assembly (ADR-0069). Only `PUT /assembly` accepts it: it is not an
54
57
  * Instance principal, so it reads no run and starts none. */
55
58
  | { kind: "publisher" }
56
- /** One Sandbox's Custodian, speaking for the Agent in that pod — and for no one else. */
57
- | { kind: "sandbox"; sandbox: string };
59
+ /** One Sandbox's fetch token (ADR-0053, ADR-0076): asks for a fetch for that Sandbox, and does
60
+ * nothing else. */
61
+ | { kind: "sandbox"; sandbox: string }
62
+ /** One pod's control token (ADR-0076), with the slot the Harness stated for the conversation
63
+ * (`X-Jr2-Slot`): speaks on the agent routes for that slot's registrations in that pod, and for no
64
+ * other. */
65
+ | { kind: "agent"; sandbox: string; slot: string };
58
66
 
59
- /** Resolve a bearer token to a principal; undefined = not a token we minted (→ 401). */
60
- export type Authenticator = (bearer: string | undefined) => Principal | undefined;
67
+ /** The header the Harness states a control request's slot in (ADR-0076): the Agent's actor path
68
+ * and agent name, its continued conversation's Instance ID without the run id. */
69
+ export const SLOT_HEADER = "x-jr2-slot";
61
70
 
62
- /** The Sandbox token for one Sandbox: its name, signed. Same name + key → same token, always. */
71
+ /** Resolve a bearer token, and the slot a control request states, to a principal; undefined = not a
72
+ * token we minted, or a control token with no slot (→ 401). */
73
+ export type Authenticator = (bearer: string | undefined, slot?: string) => Principal | undefined;
74
+
75
+ /** The fetch token for one Sandbox: its name, signed. Same name + key → same token, always. The
76
+ * bare form of ADR-0013's Sandbox token; since ADR-0076 it opens the fetch route alone. */
63
77
  export function sandboxToken(key: Buffer, sandbox: string): string {
64
78
  return `${sandbox}.${sign(key, sandbox)}`;
65
79
  }
66
80
 
81
+ /** The domain of a control token's signed name (ADR-0076). No DNS label holds a `:`, so a control
82
+ * token is never read as a fetch token, nor the reverse. */
83
+ const CONTROL_PREFIX = "agent:";
84
+
85
+ /**
86
+ * The control token for one pod (ADR-0076): what the Custodian puts in place of the secret control
87
+ * Stand-in on the agent routes. A signed name like the fetch token — same key, same deterministic
88
+ * re-mint — in its own `agent:` domain. No slot is inside it: the Harness states the slot on each
89
+ * request, and the Orchestrator checks it against the registration's.
90
+ */
91
+ export function controlToken(key: Buffer, pod: string): string {
92
+ const name = `${CONTROL_PREFIX}${pod}`;
93
+ return `${name}.${sign(key, name)}`;
94
+ }
95
+
96
+ /**
97
+ * The secret control Stand-in for one pod (ADR-0076, ADR-0059): the bearer the Harness process
98
+ * alone holds, as a file under `/run/jr2/` that no confined child can read, and that the Custodian
99
+ * swaps for {@link controlToken} on the agent routes. A 43-character base64url HMAC, so no
100
+ * Workspace code can guess it. Its own `control-standin:` domain keeps it apart from every token.
101
+ */
102
+ export function controlStandIn(key: Buffer, pod: string): string {
103
+ return sign(key, `control-standin:${pod}`);
104
+ }
105
+
67
106
  /**
68
107
  * The Harness bearer for one placement (ADR-0058): what the Orchestrator presents on every Harness
69
108
  * wire call — admit, stream, abort, echo — to the Harness in the pod `placement` names (a
@@ -138,37 +177,65 @@ export function createAuthenticator(opts: {
138
177
  /** The publish token (ADR-0069). Absent → no bearer resolves to the publisher. */
139
178
  publishToken?: string;
140
179
  }): Authenticator {
141
- return (bearer) => {
180
+ return (bearer, slot) => {
142
181
  if (!bearer) return undefined;
143
182
  if (constantTimeEqual(bearer, opts.instanceToken)) return { kind: "instance" };
144
183
  if (opts.publishToken !== undefined && constantTimeEqual(bearer, opts.publishToken)) return { kind: "publisher" };
145
184
  const cut = bearer.lastIndexOf(".");
146
185
  if (cut <= 0) return undefined;
147
- const sandbox = bearer.slice(0, cut);
148
- if (!constantTimeEqual(bearer.slice(cut + 1), sign(opts.signingKey, sandbox))) return undefined;
149
- return { kind: "sandbox", sandbox };
186
+ const name = bearer.slice(0, cut);
187
+ if (!constantTimeEqual(bearer.slice(cut + 1), sign(opts.signingKey, name))) return undefined;
188
+ // A control token (ADR-0076), read BEFORE the bare form. A control request states its slot; one
189
+ // that states none is refused like an unknown token, so no route ever sees a control principal
190
+ // without a slot.
191
+ if (name.startsWith(CONTROL_PREFIX)) {
192
+ const pod = name.slice(CONTROL_PREFIX.length);
193
+ if (!isPodName(pod) || !slot) return undefined;
194
+ return { kind: "agent", sandbox: pod, slot };
195
+ }
196
+ // The bare fetch token, in its own domain alone: a name in any other domain (`harness:`,
197
+ // `control-standin:`, `held-standin:` …) is not a Sandbox's, whatever key signed it.
198
+ if (!isPodName(name)) return undefined;
199
+ return { kind: "sandbox", sandbox: name };
150
200
  };
151
201
  }
152
202
 
203
+ /** A pod's name as a token signs it: a DNS-1123 name, so it holds no `:` and no `/` — the marks of
204
+ * every other signed domain (ADR-0076). */
205
+ function isPodName(name: string): boolean {
206
+ return /^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$/.test(name);
207
+ }
208
+
153
209
  /**
154
- * May this principal deliver to this agent registration? The Instance token may (it is the
155
- * operator). A Sandbox token may only when the registration records ITS name — which is why
156
- * an Agent registration carries `sandbox` at all (ADR-0013). The recorded name is the pod hosting the Turn:
157
- * a Workspace's Sandbox, or `jr2-instance-harness` for a Menu-only registration (ADR-0031). An
158
- * agent registration with NO name at all is an explicit-`endpoint` run (the stub Harness on the
159
- * host, in no pod): no Sandbox token can claim it.
210
+ * May this principal speak on this agent registration's routes — its surface, a pick, a delivery's
211
+ * poll, a notice's acknowledgment? The Instance token may (it is the operator). A control token may
212
+ * only when the registration records ITS pod — which is why an Agent registration carries `sandbox`
213
+ * at all (ADR-0013) — AND the slot the Harness stated is the registration's (ADR-0076). The recorded
214
+ * pod is the one hosting the Turn: a Workspace's Sandbox, or `jr2-instance-harness` for a Menu-only
215
+ * registration (ADR-0031). An agent registration with NO pod at all is an explicit-`endpoint` run
216
+ * (the stub Harness on the host, in no pod): no pod's token can claim it. A fetch token never may:
217
+ * it opens the fetch route alone.
218
+ *
219
+ * The slot check finds a routing fault. It is not the boundary: only the Harness process holds the
220
+ * control Stand-in, so only the Harness can state a slot at all (ADR-0076).
160
221
  */
161
- export function mayDeliverToAgent(principal: Principal, registrationSandbox: string | undefined): boolean {
222
+ export function mayDeliverToAgent(principal: Principal, registration: { sandbox?: string; slot?: string }): boolean {
162
223
  if (principal.kind === "instance") return true;
163
- if (principal.kind !== "sandbox") return false;
164
- return registrationSandbox !== undefined && registrationSandbox === principal.sandbox;
224
+ if (principal.kind !== "agent") return false;
225
+ return (
226
+ registration.sandbox !== undefined &&
227
+ registration.sandbox === principal.sandbox &&
228
+ registration.slot !== undefined &&
229
+ registration.slot === principal.slot
230
+ );
165
231
  }
166
232
 
167
233
  /**
168
- * May this principal ask for a fetch on this Sandbox (ADR-0053)? A Sandbox token may ask for its
234
+ * May this principal ask for a fetch on this Sandbox (ADR-0053)? A fetch token may ask for its
169
235
  * OWN pod and no other — the scope is the caches that pod mounts, and the route refuses a Repo it
170
236
  * does not. The Instance token may, on the same grounds it may deliver to any agent surface: it is
171
- * the operator. The publish token may not: it opens the publish door and no other (ADR-0069).
237
+ * the operator. A control token may not: it opens the agent routes alone (ADR-0076). The publish
238
+ * token may not: it opens the publish door and no other (ADR-0069).
172
239
  */
173
240
  export function mayAskForSandbox(principal: Principal, sandbox: string): boolean {
174
241
  if (principal.kind === "instance") return true;
@@ -176,7 +243,15 @@ export function mayAskForSandbox(principal: Principal, sandbox: string): boolean
176
243
  return principal.sandbox === sandbox;
177
244
  }
178
245
 
179
- function sign(key: Buffer, value: string): string {
246
+ /** Whether a principal is an Agent's — a control token or a fetch token. A Gate refuses every one
247
+ * (ADR-0013, ADR-0076): an Agent never reaches a Gate. */
248
+ export function isAgentPrincipal(principal: Principal): boolean {
249
+ return principal.kind === "agent" || principal.kind === "sandbox";
250
+ }
251
+
252
+ /** An HMAC of `value` under the signing key, base64url (43 characters). Exported so every signed
253
+ * name and Stand-in derives one way. */
254
+ export function sign(key: Buffer, value: string): string {
180
255
  return createHmac("sha256", key).update(value).digest("base64url");
181
256
  }
182
257