@jr2/orchestrator 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +142 -29
  14. package/docs/concepts/architecture.md +60 -34
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +13 -8
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +7 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +133 -50
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +14 -4
  23. package/docs/concepts/pools-and-sources.md +193 -17
  24. package/docs/concepts/secrets-and-trust.md +186 -62
  25. package/docs/concepts/the-menu.md +14 -0
  26. package/docs/concepts/workspaces.md +135 -56
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
  32. package/docs/get-started/install.md +4 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +72 -15
  34. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  35. package/docs/guides/answer-a-gate.md +74 -37
  36. package/docs/guides/bind-open-parts.md +29 -12
  37. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  38. package/docs/guides/cancel-a-run.md +26 -16
  39. package/docs/guides/cut-a-worktree.md +560 -0
  40. package/docs/guides/fix-a-failed-converge.md +19 -7
  41. package/docs/guides/give-a-caller-a-scope.md +203 -0
  42. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  43. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  44. package/docs/guides/let-an-agent-write-a-chart.md +14 -7
  45. package/docs/guides/look-inside-a-sandbox.md +115 -42
  46. package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
  47. package/docs/guides/open-the-console.md +63 -14
  48. package/docs/guides/push-and-sign-a-branch.md +67 -21
  49. package/docs/guides/read-a-conversation.md +225 -0
  50. package/docs/guides/run-a-chart-by-name.md +17 -16
  51. package/docs/guides/run-a-command-or-call-a-url.md +84 -41
  52. package/docs/guides/run-one-worker-per-item.md +34 -27
  53. package/docs/guides/set-an-agents-access-and-dials.md +69 -22
  54. package/docs/guides/size-a-workspace.md +19 -6
  55. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  56. package/docs/guides/start-a-run-from-a-run.md +18 -16
  57. package/docs/guides/start-a-run.md +24 -18
  58. package/docs/guides/steer-an-agent.md +332 -0
  59. package/docs/guides/use-a-hosted-provider.md +7 -1
  60. package/docs/guides/watch-a-run.md +146 -117
  61. package/docs/guides/write-a-hook-kind.md +238 -0
  62. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  63. package/docs/guides/write-a-workflow.md +18 -8
  64. package/docs/llms.txt +110 -23
  65. package/docs/reference/authoring-surface/agent.md +35 -12
  66. package/docs/reference/authoring-surface/answer.md +20 -4
  67. package/docs/reference/authoring-surface/arg.md +28 -6
  68. package/docs/reference/authoring-surface/basic.md +1 -1
  69. package/docs/reference/authoring-surface/bearer.md +1 -1
  70. package/docs/reference/authoring-surface/branchname.md +15 -0
  71. package/docs/reference/authoring-surface/carry.md +23 -0
  72. package/docs/reference/authoring-surface/chartschema.md +1 -1
  73. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  74. package/docs/reference/authoring-surface/command.md +37 -22
  75. package/docs/reference/authoring-surface/customize.md +4 -1
  76. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  77. package/docs/reference/authoring-surface/defineevent.md +1 -1
  78. package/docs/reference/authoring-surface/dispatch.md +5 -4
  79. package/docs/reference/authoring-surface/fail.md +18 -5
  80. package/docs/reference/authoring-surface/file.md +37 -17
  81. package/docs/reference/authoring-surface/frompick.md +1 -1
  82. package/docs/reference/authoring-surface/gate.md +118 -0
  83. package/docs/reference/authoring-surface/group-agents.md +15 -0
  84. package/docs/reference/authoring-surface/group-charts.md +16 -0
  85. package/docs/reference/authoring-surface/group-composition.md +25 -0
  86. package/docs/reference/authoring-surface/group-config.md +17 -0
  87. package/docs/reference/authoring-surface/group-effects.md +48 -0
  88. package/docs/reference/authoring-surface/group-failure.md +13 -0
  89. package/docs/reference/authoring-surface/group-gates.md +16 -0
  90. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  91. package/docs/reference/authoring-surface/group-steering.md +12 -0
  92. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  93. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  94. package/docs/reference/authoring-surface/http.md +109 -9
  95. package/docs/reference/authoring-surface/isfailure.md +1 -1
  96. package/docs/reference/authoring-surface/isready.md +14 -0
  97. package/docs/reference/authoring-surface/jr2setup.md +14 -4
  98. package/docs/reference/authoring-surface/mcp.md +107 -0
  99. package/docs/reference/authoring-surface/open.md +41 -8
  100. package/docs/reference/authoring-surface/openbody.md +1 -1
  101. package/docs/reference/authoring-surface/paramsof.md +23 -5
  102. package/docs/reference/authoring-surface/pool.md +79 -11
  103. package/docs/reference/authoring-surface/push.md +31 -13
  104. package/docs/reference/authoring-surface/readyof.md +21 -0
  105. package/docs/reference/authoring-surface/reread.md +19 -0
  106. package/docs/reference/authoring-surface/secret.md +26 -5
  107. package/docs/reference/authoring-surface/sentby.md +1 -1
  108. package/docs/reference/authoring-surface/skip.md +22 -0
  109. package/docs/reference/authoring-surface/source.md +59 -4
  110. package/docs/reference/authoring-surface/steer.md +93 -0
  111. package/docs/reference/authoring-surface/text.md +1 -1
  112. package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
  113. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  114. package/docs/reference/authoring-surface/type-agentslots.md +4 -1
  115. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  116. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  117. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  118. package/docs/reference/authoring-surface/type-binding.md +6 -7
  119. package/docs/reference/authoring-surface/type-caller.md +40 -0
  120. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  121. package/docs/reference/authoring-surface/type-carry.md +19 -0
  122. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  123. package/docs/reference/authoring-surface/type-commandinput.md +4 -3
  124. package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
  125. package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
  126. package/docs/reference/authoring-surface/type-customize.md +20 -3
  127. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  128. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  129. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  130. package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
  131. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  132. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  133. package/docs/reference/authoring-surface/type-filebody.md +4 -8
  134. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  135. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  136. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  137. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  138. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  139. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  140. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  141. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  142. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  143. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  144. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  145. package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
  146. package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
  147. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
  149. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  150. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  151. package/docs/reference/authoring-surface/type-lends.md +5 -1
  152. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  153. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  154. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  155. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  156. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  157. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  158. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  159. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  160. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  161. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  162. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  163. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
  164. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  165. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  166. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  167. package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
  168. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
  169. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
  170. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  171. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  172. package/docs/reference/authoring-surface/type-pushinput.md +22 -3
  173. package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
  174. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  175. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  176. package/docs/reference/authoring-surface/type-reposlot.md +6 -6
  177. package/docs/reference/authoring-surface/type-reread.md +18 -0
  178. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  179. package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
  180. package/docs/reference/authoring-surface/type-sender.md +26 -1
  181. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  182. package/docs/reference/authoring-surface/type-size.md +1 -1
  183. package/docs/reference/authoring-surface/type-skip.md +19 -0
  184. package/docs/reference/authoring-surface/type-slothandles.md +5 -13
  185. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  186. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  187. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  188. package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
  189. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  190. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  191. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  192. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  193. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  194. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  195. package/docs/reference/authoring-surface/type-workspaced.md +30 -9
  196. package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
  197. package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
  198. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  199. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  200. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  201. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  202. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  203. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  204. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  205. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  206. package/docs/reference/authoring-surface/workspace.md +66 -38
  207. package/docs/reference/authoring-surface/worktree.md +71 -0
  208. package/docs/reference/authoring-surface.md +88 -147
  209. package/docs/reference/cli.md +413 -74
  210. package/docs/reference/config.md +248 -6
  211. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
  212. package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
  213. package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
  214. package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
  215. package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
  216. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  217. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  218. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  219. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  220. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
  221. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
  222. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  223. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
  224. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
  225. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
  226. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  227. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
  228. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
  229. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  230. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
  231. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
  232. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  233. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
  234. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
  235. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  236. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  237. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
  238. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  239. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
  240. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  241. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
  242. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
  243. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  244. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
  245. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
  246. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
  247. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  248. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  249. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
  250. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
  251. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
  252. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
  253. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
  254. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  255. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  256. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
  257. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
  258. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
  259. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  260. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
  261. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  262. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  263. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
  264. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
  265. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
  266. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  267. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  268. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  269. package/docs/reference/decisions.md +9 -4
  270. package/docs/reference/glossary.md +138 -45
  271. package/docs/reference/hooks.md +1077 -0
  272. package/docs/reference/http-api.md +988 -123
  273. package/docs/reference/machines.md +0 -1
  274. package/docs/why.md +7 -5
  275. package/package.json +10 -3
  276. package/src/actor.ts +194 -42
  277. package/src/agent.ts +56 -6
  278. package/src/ambient.ts +3 -5
  279. package/src/assembly-checks.ts +530 -4
  280. package/src/callers.ts +245 -0
  281. package/src/chart.ts +238 -26
  282. package/src/charts.ts +9 -2
  283. package/src/config.ts +566 -7
  284. package/src/conversations.ts +453 -0
  285. package/src/custodian.lua +17 -5
  286. package/src/custodian.ts +257 -77
  287. package/src/customize.ts +154 -7
  288. package/src/dispatch-effect.ts +142 -26
  289. package/src/durability.ts +2 -0
  290. package/src/duration.ts +20 -0
  291. package/src/effect-checks.ts +52 -4
  292. package/src/effect.ts +401 -132
  293. package/src/effector-wire.ts +278 -30
  294. package/src/fail.ts +17 -4
  295. package/src/gate-brand.ts +16 -0
  296. package/src/gate.ts +634 -48
  297. package/src/harness-client.ts +84 -35
  298. package/src/harness-env.ts +33 -0
  299. package/src/held-secrets.ts +197 -39
  300. package/src/holds.ts +5 -2
  301. package/src/hook-kinds/discord.ts +92 -0
  302. package/src/hook-kinds/door.ts +107 -0
  303. package/src/hook-kinds/github.ts +89 -0
  304. package/src/hook-kinds/gitlab.ts +72 -0
  305. package/src/hook-kinds/hook.ts +7 -0
  306. package/src/hook-kinds/index.ts +465 -0
  307. package/src/hook-kinds/slack.ts +151 -0
  308. package/src/hook-kinds/webhook.ts +113 -0
  309. package/src/hooks.ts +506 -0
  310. package/src/http-effect.ts +164 -70
  311. package/src/http.ts +933 -99
  312. package/src/index.ts +30 -5
  313. package/src/instance.ts +196 -26
  314. package/src/internal.ts +23 -3
  315. package/src/kube-client.ts +6 -3
  316. package/src/machine-doc.ts +29 -5
  317. package/src/mcp-effect.ts +424 -0
  318. package/src/model-keys.ts +24 -0
  319. package/src/mount-digest.ts +41 -7
  320. package/src/names.ts +82 -7
  321. package/src/notices.ts +576 -59
  322. package/src/open-body.ts +110 -30
  323. package/src/open-effect.ts +17 -4
  324. package/src/open.ts +40 -7
  325. package/src/parts.ts +125 -35
  326. package/src/party.ts +35 -1
  327. package/src/pick.ts +43 -9
  328. package/src/pool.ts +1172 -172
  329. package/src/registration.ts +249 -17
  330. package/src/repo-identity.ts +7 -3
  331. package/src/repos.ts +20 -2
  332. package/src/run-host.ts +1614 -190
  333. package/src/run-list.ts +191 -0
  334. package/src/sandbox-effect.ts +217 -66
  335. package/src/sandbox-kube.ts +116 -21
  336. package/src/secret-placement.ts +248 -35
  337. package/src/server.ts +53 -4
  338. package/src/setup.ts +158 -38
  339. package/src/snapshot-store.ts +565 -48
  340. package/src/steer.ts +111 -0
  341. package/src/stub-harness.ts +6 -2
  342. package/src/tokens.ts +362 -46
  343. package/src/vocabulary.ts +23 -4
  344. package/src/wire.ts +27 -10
  345. package/src/workspace.ts +314 -259
  346. package/src/worktree.ts +249 -0
  347. package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
  348. package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
  349. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  350. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
@@ -0,0 +1,453 @@
1
+ // The kept conversations (ADR-0023, ADR-0065): what the Harness sends of each conversation while
2
+ // its Turn runs, kept in `conversations.db` beside `state.db` on the state volume, for as long as
3
+ // the run is kept. A second SQLite file, because its writes are larger and more frequent than a
4
+ // snapshot's, and its sweep frees pages: neither may slow the snapshot's synchronous write.
5
+ //
6
+ // conversations one row per conversation: its run, its Agent, the bytes kept, `cut`, `swept`
7
+ // entries one row per entry, keyed by (instance_id, seq): a batch sent again adds nothing
8
+ //
9
+ // The rows are plain JSON (`entry` is the entry as the Harness sent it, with the Agent from the
10
+ // ledger), so a person or a tool can query the file with SQLite alone. The file is made with
11
+ // `auto_vacuum = INCREMENTAL`, and each delete frees its pages, so the file gets smaller after a
12
+ // sweep. There is no compression now: when it comes, it is Node's own zstd on the entry text, with
13
+ // the queryable columns kept.
14
+ //
15
+ // The cap is per conversation: an entry that would take it past `capBytes` is not kept. One marker
16
+ // entry takes its place, and every later entry of that conversation is dropped. The cap keeps the
17
+ // start of the conversation, which tells why the Agent did what it did.
18
+
19
+ import { readdirSync, statfsSync, statSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { DatabaseSync } from "node:sqlite";
22
+ import type { SnapshotStore } from "./snapshot-store.ts";
23
+
24
+ /** One entry of a kept conversation (ADR-0023): one body of the print, its label as `kind` and the
25
+ * rest as `text`. `seq` goes up for as long as the Instance ID is kept; a new record starts it at
26
+ * the clock (the Harness's keep.ts). */
27
+ export type ConversationEntry = {
28
+ id: string;
29
+ seq: number;
30
+ at: string;
31
+ agent: string;
32
+ kind: string;
33
+ text: string;
34
+ };
35
+
36
+ /** What an entry is, as the Harness sends it: the Agent comes from the ledger, not the body. */
37
+ export type SentEntry = Omit<ConversationEntry, "agent">;
38
+
39
+ /** One kept conversation, as `GET /runs/:runId/conversations` lists it (without `open`, which is the
40
+ * host's to say). */
41
+ export type KeptConversation = {
42
+ agent: string;
43
+ instanceId: string;
44
+ bytes: number;
45
+ cut: boolean;
46
+ /** Why the conversation's entries are gone while its run is kept: the space guard swept them. */
47
+ swept?: typeof SWEPT_NO_SPACE;
48
+ };
49
+
50
+ /** The one reason the guard sweeps a conversation (ADR-0065). */
51
+ export const SWEPT_NO_SPACE = "no space";
52
+
53
+ /** What a run says when the guard swept its conversations (ADR-0065). */
54
+ export const CONVERSATION_SWEPT = "conversation swept: no space" as const;
55
+
56
+ /** The cap of one kept conversation (ADR-0023). */
57
+ export const CONVERSATION_CAP_BYTES = 8 * 1024 * 1024;
58
+
59
+ /** The `kind` of the marker entry that a cut leaves (ADR-0023). */
60
+ export const CUT_KIND = "cut";
61
+
62
+ /** An append's outcome: how many entries are new, and whether the conversation is cut. */
63
+ export type AppendResult = { kept: number; cut: boolean };
64
+
65
+ type ConversationRow = {
66
+ run_id: string;
67
+ agent: string;
68
+ bytes: number;
69
+ cut: number;
70
+ swept: string | null;
71
+ };
72
+
73
+ /** The listener of one conversation's new entries (the SSE follow). */
74
+ type EntryListener = (entries: ConversationEntry[]) => void;
75
+
76
+ export class ConversationStore {
77
+ private readonly db: DatabaseSync;
78
+ private readonly capBytes: number;
79
+ private readonly listeners = new Map<string, Set<EntryListener>>();
80
+ /** Set by the guard: the volume is past its last bound, and no entry is kept (ADR-0065). */
81
+ #full = false;
82
+
83
+ constructor(path: string, opts: { capBytes?: number } = {}) {
84
+ this.db = new DatabaseSync(path);
85
+ this.capBytes = opts.capBytes ?? CONVERSATION_CAP_BYTES;
86
+ }
87
+
88
+ init(): void {
89
+ // Before the first table, or it has no effect: a file's vacuum mode is set when it is made.
90
+ this.db.exec(`PRAGMA auto_vacuum = INCREMENTAL`);
91
+ this.db.exec(`PRAGMA journal_mode = WAL`);
92
+ this.db.exec(`PRAGMA synchronous = NORMAL`);
93
+ this.db.exec(`
94
+ CREATE TABLE IF NOT EXISTS conversations (
95
+ instance_id TEXT PRIMARY KEY,
96
+ run_id TEXT NOT NULL,
97
+ agent TEXT NOT NULL,
98
+ bytes INTEGER NOT NULL DEFAULT 0,
99
+ cut INTEGER NOT NULL DEFAULT 0,
100
+ swept TEXT,
101
+ created_at TEXT NOT NULL,
102
+ updated_at TEXT NOT NULL
103
+ )
104
+ `);
105
+ this.db.exec(`CREATE INDEX IF NOT EXISTS conversations_run ON conversations (run_id, created_at)`);
106
+ this.db.exec(`
107
+ CREATE TABLE IF NOT EXISTS entries (
108
+ instance_id TEXT NOT NULL,
109
+ seq INTEGER NOT NULL,
110
+ id TEXT NOT NULL,
111
+ entry TEXT NOT NULL,
112
+ PRIMARY KEY (instance_id, seq)
113
+ ) WITHOUT ROWID
114
+ `);
115
+ // The entry id dedupes (ADR-0023): a batch sent again adds nothing, and an entry of a new
116
+ // record whose seq meets an old one (a pod on a node whose clock is behind) is still kept.
117
+ this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS entries_id ON entries (instance_id, id)`);
118
+ }
119
+
120
+ /** The volume is past the guard's last bound: no entry is kept, and each conversation that gets
121
+ * one more is cut with a marker, as at its cap (ADR-0065). */
122
+ get full(): boolean {
123
+ return this.#full;
124
+ }
125
+ set full(value: boolean) {
126
+ this.#full = value;
127
+ }
128
+
129
+ /**
130
+ * Keep a batch of one conversation's entries. An entry whose `(instanceId, seq)` is kept already
131
+ * adds nothing, so a batch sent again is harmless. An entry that would take the conversation past
132
+ * its cap, or that comes while the volume is full, is replaced by one marker, and the conversation
133
+ * is cut: no later entry is kept. A conversation that the guard swept keeps nothing more.
134
+ */
135
+ append(runId: string, instanceId: string, agent: string, sent: readonly SentEntry[]): AppendResult {
136
+ const now = new Date().toISOString();
137
+ const added: ConversationEntry[] = [];
138
+ let kept = 0;
139
+ this.db.exec("BEGIN IMMEDIATE");
140
+ let row: ConversationRow;
141
+ try {
142
+ const found = this.row(instanceId);
143
+ if (!found) {
144
+ this.db
145
+ .prepare(
146
+ `INSERT INTO conversations (instance_id, run_id, agent, bytes, cut, created_at, updated_at)
147
+ VALUES (?, ?, ?, 0, 0, ?, ?)`,
148
+ )
149
+ .run(instanceId, runId, agent, now, now);
150
+ }
151
+ row = found ?? { run_id: runId, agent, bytes: 0, cut: 0, swept: null };
152
+ if (row.cut || row.swept !== null) {
153
+ this.db.exec("COMMIT");
154
+ return { kept: 0, cut: row.cut === 1 };
155
+ }
156
+ const hasId = this.db.prepare(`SELECT 1 FROM entries WHERE instance_id = ? AND id = ?`);
157
+ const hasSeq = this.db.prepare(`SELECT 1 FROM entries WHERE instance_id = ? AND seq = ?`);
158
+ const last = this.db.prepare(`SELECT MAX(seq) AS seq FROM entries WHERE instance_id = ?`);
159
+ const insertRow = this.db.prepare(`INSERT INTO entries (instance_id, seq, id, entry) VALUES (?, ?, ?, ?)`);
160
+ for (const e of [...sent].sort((a, b) => a.seq - b.seq)) {
161
+ if (hasId.get(instanceId, e.id)) continue;
162
+ // Another entry has this seq: a new record whose clock is behind. Keep it after the last.
163
+ const seq = hasSeq.get(instanceId, e.seq)
164
+ ? ((last.get(instanceId) as { seq: number | null }).seq ?? 0) + 1
165
+ : e.seq;
166
+ const entry: ConversationEntry = { id: e.id, seq, at: e.at, agent, kind: e.kind, text: e.text };
167
+ const json = JSON.stringify(entry);
168
+ const size = Buffer.byteLength(json);
169
+ if (this.#full || row.bytes + size > this.capBytes) {
170
+ const marker: ConversationEntry = {
171
+ id: `${CUT_KIND}-${seq}`,
172
+ seq,
173
+ at: now,
174
+ agent,
175
+ kind: CUT_KIND,
176
+ text: this.#full
177
+ ? "the Orchestrator keeps no more of this conversation: its state volume is full (ADR-0065)"
178
+ : `the Orchestrator keeps no more of this conversation: it is at its cap of ${capText(this.capBytes)} (ADR-0023)`,
179
+ };
180
+ insertRow.run(instanceId, seq, marker.id, JSON.stringify(marker));
181
+ added.push(marker);
182
+ row.cut = 1;
183
+ break;
184
+ }
185
+ insertRow.run(instanceId, seq, e.id, json);
186
+ added.push(entry);
187
+ kept += 1;
188
+ row.bytes += size;
189
+ }
190
+ this.db
191
+ .prepare(`UPDATE conversations SET bytes = ?, cut = ?, updated_at = ? WHERE instance_id = ?`)
192
+ .run(row.bytes, row.cut, now, instanceId);
193
+ this.db.exec("COMMIT");
194
+ } catch (err) {
195
+ this.db.exec("ROLLBACK");
196
+ throw err;
197
+ }
198
+ if (added.length > 0) for (const listener of this.listeners.get(instanceId) ?? []) listener(added);
199
+ return { kept, cut: row.cut === 1 };
200
+ }
201
+
202
+ /** A run's kept conversations, the first kept first. */
203
+ list(runId: string): KeptConversation[] {
204
+ const rows = this.db
205
+ .prepare(
206
+ `SELECT instance_id, agent, bytes, cut, swept FROM conversations WHERE run_id = ?
207
+ ORDER BY created_at, instance_id`,
208
+ )
209
+ .all(runId) as Array<{ instance_id: string; agent: string; bytes: number; cut: number; swept: string | null }>;
210
+ return rows.map((r) => ({
211
+ agent: r.agent,
212
+ instanceId: r.instance_id,
213
+ bytes: r.bytes,
214
+ cut: r.cut === 1,
215
+ ...(r.swept !== null ? { swept: SWEPT_NO_SPACE } : {}),
216
+ }));
217
+ }
218
+
219
+ /** One kept conversation of a run, or undefined when the run has none of that id. */
220
+ conversation(runId: string, instanceId: string): KeptConversation | undefined {
221
+ return this.list(runId).find((c) => c.instanceId === instanceId);
222
+ }
223
+
224
+ /** The entries of a conversation after `after` (-1: from the first), in order, at most `limit`. */
225
+ entries(instanceId: string, after: number, limit: number): ConversationEntry[] {
226
+ const rows = this.db
227
+ .prepare(`SELECT entry FROM entries WHERE instance_id = ? AND seq > ? ORDER BY seq LIMIT ?`)
228
+ .all(instanceId, after, limit) as Array<{ entry: string }>;
229
+ return rows.map((r) => JSON.parse(r.entry) as ConversationEntry);
230
+ }
231
+
232
+ /** The runs, of `runIds`, whose conversations the guard swept (ADR-0065). */
233
+ sweptRuns(runIds: readonly string[]): Set<string> {
234
+ const swept = new Set<string>();
235
+ const query = this.db.prepare(`SELECT 1 FROM conversations WHERE run_id = ? AND swept IS NOT NULL LIMIT 1`);
236
+ for (const runId of runIds) if (query.get(runId)) swept.add(runId);
237
+ return swept;
238
+ }
239
+
240
+ /** Hear the new entries of one conversation. Returns the unsubscribe. */
241
+ follow(instanceId: string, listener: EntryListener): () => void {
242
+ let set = this.listeners.get(instanceId);
243
+ if (!set) this.listeners.set(instanceId, (set = new Set()));
244
+ set.add(listener);
245
+ return () => {
246
+ set.delete(listener);
247
+ if (set.size === 0 && this.listeners.get(instanceId) === set) this.listeners.delete(instanceId);
248
+ };
249
+ }
250
+
251
+ /** Delete the conversations of runs that the retention sweep deleted (ADR-0065), and free their
252
+ * pages. */
253
+ deleteRuns(runIds: readonly string[]): void {
254
+ if (runIds.length === 0) return;
255
+ this.db.exec("BEGIN IMMEDIATE");
256
+ try {
257
+ const entries = this.db.prepare(
258
+ `DELETE FROM entries WHERE instance_id IN (SELECT instance_id FROM conversations WHERE run_id = ?)`,
259
+ );
260
+ const rows = this.db.prepare(`DELETE FROM conversations WHERE run_id = ?`);
261
+ for (const runId of runIds) {
262
+ entries.run(runId);
263
+ rows.run(runId);
264
+ }
265
+ this.db.exec("COMMIT");
266
+ } catch (err) {
267
+ this.db.exec("ROLLBACK");
268
+ throw err;
269
+ }
270
+ this.vacuum();
271
+ }
272
+
273
+ /** Each run that has a kept conversation, swept or not. */
274
+ runIds(): string[] {
275
+ const rows = this.db.prepare(`SELECT DISTINCT run_id FROM conversations ORDER BY run_id`).all() as Array<{
276
+ run_id: string;
277
+ }>;
278
+ return rows.map((r) => r.run_id);
279
+ }
280
+
281
+ /** The runs that have conversations with entries kept, the run whose first conversation is
282
+ * oldest first: the order in which the guard sweeps (ADR-0065). */
283
+ runsOldestFirst(): string[] {
284
+ const rows = this.db
285
+ .prepare(`SELECT run_id FROM conversations WHERE swept IS NULL GROUP BY run_id ORDER BY MIN(created_at), run_id`)
286
+ .all() as Array<{ run_id: string }>;
287
+ return rows.map((r) => r.run_id);
288
+ }
289
+
290
+ /** Delete the entries of a run's conversations for space (ADR-0065). The rows stay, marked
291
+ * swept, so the list says why the conversation is gone. */
292
+ sweepForSpace(runId: string): void {
293
+ this.db.exec("BEGIN IMMEDIATE");
294
+ try {
295
+ this.db
296
+ .prepare(`DELETE FROM entries WHERE instance_id IN (SELECT instance_id FROM conversations WHERE run_id = ?)`)
297
+ .run(runId);
298
+ this.db
299
+ .prepare(`UPDATE conversations SET swept = ?, bytes = 0, updated_at = ? WHERE run_id = ?`)
300
+ .run(SWEPT_NO_SPACE, new Date().toISOString(), runId);
301
+ this.db.exec("COMMIT");
302
+ } catch (err) {
303
+ this.db.exec("ROLLBACK");
304
+ throw err;
305
+ }
306
+ this.vacuum();
307
+ }
308
+
309
+ /** Give the freed pages back to the file system: the file, and the WAL beside it, get smaller. */
310
+ private vacuum(): void {
311
+ this.db.exec(`PRAGMA incremental_vacuum`);
312
+ this.db.exec(`PRAGMA wal_checkpoint(TRUNCATE)`);
313
+ }
314
+
315
+ private row(instanceId: string): ConversationRow | undefined {
316
+ return this.db
317
+ .prepare(`SELECT run_id, agent, bytes, cut, swept FROM conversations WHERE instance_id = ?`)
318
+ .get(instanceId) as ConversationRow | undefined;
319
+ }
320
+
321
+ close(): void {
322
+ this.listeners.clear();
323
+ this.db.close();
324
+ }
325
+ }
326
+
327
+ function capText(bytes: number): string {
328
+ return bytes % (1024 * 1024) === 0 ? `${bytes / (1024 * 1024)} MiB` : `${bytes} bytes`;
329
+ }
330
+
331
+ // ---- The space guard (ADR-0065) ------------------------------------------------------------------
332
+
333
+ /** Above this part of the volume, the guard sweeps the conversations of ended runs. */
334
+ export const GUARD_SWEEP_AT = 0.8;
335
+ /** Above this part, with no conversation of an ended run left, no entry is kept. */
336
+ export const GUARD_FULL_AT = 0.95;
337
+
338
+ /** What the guard reads and does. Injected, so a test sets the sizes. */
339
+ export type SpaceGuardPort = {
340
+ conversations: ConversationStore;
341
+ /** The bytes of the files on the state volume: both databases with their `-wal` files, the
342
+ * Assemblies and the Charts. */
343
+ used(): number;
344
+ /** The size of the state volume: the smaller of the claim and what `statfs` says. */
345
+ capacity(): number;
346
+ /** Whether a run is live. A live run's conversations are never swept for space. */
347
+ isLive(runId: string): Promise<boolean>;
348
+ /** Hears each run whose conversations went. */
349
+ onSwept?: (runId: string) => void;
350
+ };
351
+
352
+ /**
353
+ * One pass of the space guard (ADR-0065). Above 80% of the volume, it deletes the kept
354
+ * conversations of ended runs, the oldest run first, until the volume is below 80%. A live run's
355
+ * conversations are never swept. Above 95% with no ended run's conversation left, it sets the
356
+ * store full, and each open conversation is cut at its next entry; below that, the store keeps
357
+ * entries again. Answers the runs whose conversations went.
358
+ */
359
+ export async function guardSpace(port: SpaceGuardPort): Promise<string[]> {
360
+ const capacity = port.capacity();
361
+ if (!(capacity > 0)) return [];
362
+ const swept: string[] = [];
363
+ let used = port.used();
364
+ if (used > GUARD_SWEEP_AT * capacity) {
365
+ for (const runId of port.conversations.runsOldestFirst()) {
366
+ if (used < GUARD_SWEEP_AT * capacity) break;
367
+ if (await port.isLive(runId)) continue;
368
+ port.conversations.sweepForSpace(runId);
369
+ swept.push(runId);
370
+ port.onSwept?.(runId);
371
+ used = port.used();
372
+ }
373
+ }
374
+ port.conversations.full = used > GUARD_FULL_AT * capacity;
375
+ return swept;
376
+ }
377
+
378
+ /** The bytes of every file under `dir`, at any depth. A file that goes while it is read counts 0. */
379
+ export function bytesUnder(dir: string): number {
380
+ let total = 0;
381
+ let names: import("node:fs").Dirent[];
382
+ try {
383
+ names = readdirSync(dir, { withFileTypes: true });
384
+ } catch {
385
+ return 0;
386
+ }
387
+ for (const entry of names) {
388
+ const path = join(dir, entry.name);
389
+ if (entry.isDirectory()) total += bytesUnder(path);
390
+ else if (entry.isFile()) {
391
+ try {
392
+ total += statSync(path).size;
393
+ } catch {
394
+ // Gone between the read and the stat.
395
+ }
396
+ }
397
+ }
398
+ return total;
399
+ }
400
+
401
+ /** The size of the file system that holds `dir` (`statfs`), or Infinity when it cannot be read. */
402
+ export function fileSystemBytes(dir: string): number {
403
+ try {
404
+ const fs = statfsSync(dir);
405
+ return fs.blocks * fs.bsize;
406
+ } catch {
407
+ return Number.POSITIVE_INFINITY;
408
+ }
409
+ }
410
+
411
+ const QUANTITY = /^(\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$/;
412
+ const UNIT: Record<string, number> = {
413
+ "": 1,
414
+ Ki: 2 ** 10,
415
+ Mi: 2 ** 20,
416
+ Gi: 2 ** 30,
417
+ Ti: 2 ** 40,
418
+ Pi: 2 ** 50,
419
+ Ei: 2 ** 60,
420
+ k: 1e3,
421
+ M: 1e6,
422
+ G: 1e9,
423
+ T: 1e12,
424
+ P: 1e15,
425
+ E: 1e18,
426
+ };
427
+
428
+ /** A Kubernetes storage quantity (`"5Gi"`) in bytes, or undefined for one that does not parse. */
429
+ export function quantityBytes(quantity: string): number | undefined {
430
+ const m = QUANTITY.exec(quantity);
431
+ if (!m) return undefined;
432
+ return Number.parseFloat(m[1]!) * UNIT[m[2] ?? ""]!;
433
+ }
434
+
435
+ /**
436
+ * Delete the kept conversations whose run has no row in the run store, and answer those run ids.
437
+ * Such a conversation stays when a crash comes between the store's sweep and `deleteRuns`, or when
438
+ * a batch is placed just before the sweep deletes its row. The retention sweep never names that
439
+ * run again, so each sweep calls this (ADR-0065).
440
+ */
441
+ export async function deleteOrphanConversations(
442
+ conversations: ConversationStore,
443
+ runs: Pick<SnapshotStore, "findIdsByPrefix">,
444
+ ): Promise<string[]> {
445
+ const orphans: string[] = [];
446
+ for (const runId of conversations.runIds()) {
447
+ // The ids that share a prefix come sorted, so the run's own id, when it exists, is first.
448
+ const [first] = await runs.findIdsByPrefix(runId, 1);
449
+ if (first !== runId) orphans.push(runId);
450
+ }
451
+ conversations.deleteRuns(orphans);
452
+ return orphans;
453
+ }
package/src/custodian.lua CHANGED
@@ -10,15 +10,18 @@
10
10
  --
11
11
  -- Four roles, one per route family, read from the route's metadata:
12
12
  --
13
- -- control the Harness's Menu and the ask, plain HTTP on loopback, toward the Orchestrator
13
+ -- control toward the Orchestrator, plain HTTP: the agent routes on the control socket, and
14
+ -- the ask on the loopback fetch listener (ADR-0076). Each listener has only its own
15
+ -- routes, so a route of the other one gets the deny route's 404
14
16
  -- egress the CONNECT listener: dial guard, then tunnel or hand to an intercept listener
15
17
  -- intercept one bound host, TLS already ended: swap the Stand-in, strip the rest
16
18
  -- health the kubelet's probe
17
19
 
18
20
  local VALUES = "/etc/jr2/custodian/values/"
19
21
 
20
- -- The value of one held secret, read once. One trailing line end is taken off, because
21
- -- `kubectl create secret --from-file` writes one; anything else a header cannot carry stops us.
22
+ -- The value of one held secret, or its Stand-in, read once. One trailing line end is taken off,
23
+ -- because `kubectl create secret --from-file` writes one; anything else a header cannot carry stops
24
+ -- us. The line names the file, never what it holds.
22
25
  local function load(name)
23
26
  local f = io.open(VALUES .. name, "rb")
24
27
  if f == nil then
@@ -36,13 +39,20 @@ local function load(name)
36
39
  return v
37
40
  end
38
41
 
42
+ -- Every Stand-in is secret but the fetch Stand-in (ADR-0059, ADR-0076): the pod's maker minted it,
43
+ -- and it is a value too, read from `<NAME>.standin` beside the secret's own. The one public Stand-in
44
+ -- rides the table (`standIn`), because `jr2-upload-pack` holds it as a constant.
39
45
  local STAND_IN = {}
40
46
  local VALUE = {}
41
47
  for _, s in ipairs(HELD.secrets) do
42
- STAND_IN[s.name] = "jr2-held-" .. s.name
48
+ STAND_IN[s.name] = s.standIn or load(s.name .. ".standin")
43
49
  VALUE[s.name] = load(s.name)
44
50
  end
45
51
 
52
+ -- The slot the Harness states on a control request (ADR-0076). It goes to the Orchestrator on a
53
+ -- control route, unchanged, and to no other host.
54
+ local SLOT_HEADER = "x-jr2-slot"
55
+
46
56
  -- The headers every bound target treats as a credential (`CREDENTIAL_HEADERS`, held-secrets.ts).
47
57
  local CREDENTIAL = {}
48
58
  for _, h in ipairs(HELD.credential) do CREDENTIAL[h] = true end
@@ -258,7 +268,7 @@ local function intercept(handle, headers, target)
258
268
  if path:lower():find("%%2[ef]") then
259
269
  return refuse(handle, 400, "path", "an encoded / or . in the path")
260
270
  end
261
- for _, h in ipairs({ "te", "trailer", "keep-alive", "proxy-connection" }) do headers:remove(h) end
271
+ for _, h in ipairs({ "te", "trailer", "keep-alive", "proxy-connection", SLOT_HEADER }) do headers:remove(h) end
262
272
  return hold(handle, headers, HELD.targets[target], target, path)
263
273
  end
264
274
 
@@ -273,6 +283,8 @@ local function control(handle, headers, route)
273
283
  '{"error":"this pod serves no Sandbox, so it mounts no Repo to fetch (ADR-0053)"}')
274
284
  end
275
285
  headers:replace(":path", "/sandboxes/" .. sandbox .. "/fetch")
286
+ -- The fetch acts for the Sandbox, not for an Agent: no slot rides it.
287
+ headers:remove(SLOT_HEADER)
276
288
  end
277
289
  return hold(handle, headers, HELD.control, "orchestrator", headers:get(":path"))
278
290
  end