@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,347 @@
1
+ # An Agent's control credential lives only where no Workspace code runs
2
+
3
+ [ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) scoped an Agent's control
4
+ credential to its Sandbox. That stops a coder in one Sandbox from delivering a pick to a reviewer in another Sandbox.
5
+ [ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md) made several Agents in one Sandbox normal:
6
+ concurrent coders in one clone, or a coder beside an independent reviewer. In one Sandbox the Sandbox scope is not
7
+ sufficient. Every Agent's `bash` is a child of the one Harness process, at the one uid, and it inherits the env of that
8
+ process. So a coder's `bash`, driven by a prompt injection, can read the Stand-in of the control credential, send
9
+ `POST /agents/<reviewer-iid>/events` through the Custodian, and approve its own review. The reviewer did nothing.
10
+
11
+ The attack does not need the coder's own `bash`. The Agents of one Workspace share a writable `/work`. Code that one
12
+ Agent writes there — a test file, a git hook, `core.fsmonitor`, a smudge filter — runs when another process runs on
13
+ `/work`: the reviewer's `bash`, its `node --test` or `git diff`, or a `command` Effect. That code then has whatever
14
+ credential its process can read. So the question is not which container holds the credential. It is which processes run
15
+ code from `/work`.
16
+
17
+ ## Decision
18
+
19
+ - **The boundary.** A credential that lets a request act for an Agent lives only where no Workspace code runs.
20
+ **Workspace code** is each process whose program or input comes from `/work`: the Working tools, tests, builds, git
21
+ (its hooks, its filters, fsmonitor, textconv and each repository-local config), `command` Effects and stdio MCP
22
+ servers. The boundary needs no separate container for each Agent.
23
+ - **One Harness container for each Sandbox.** It serves every workspace-ful Agent below the `workspace()`
24
+ ([ADR-0005](0005-sandbox-pod-composition.md)). A Menu-only Agent has no Sandbox, and it runs on the Instance Harness,
25
+ as before ([ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md)).
26
+ - **The Harness process is trusted, and every program that a model drives runs confined.** The Harness process runs
27
+ jr2's code only. It runs the engine loop for each conversation, and it holds the control credential. Each other
28
+ process in the Harness container runs in one Landlock domain, which the Harness is not in:
29
+ - each `bash` call, and so each test, build, git hook and planted program that it runs;
30
+ - each process of a search tool (`rg`, `grep`);
31
+ - the I/O of the file tools (read, write, edit);
32
+ - jr2's own git for the attach, the cut and the sync
33
+ ([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md),
34
+ [ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)), because a smudge filter or a textconv
35
+ in the shared `.git/config` runs in git's process;
36
+ - each `command` Effect that is placed in the Harness, which is each `command` that names no secret;
37
+ - each stdio MCP server of an Agent
38
+ ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)),
39
+ which the Harness starts through the spawner, never with its MCP client's own spawn.
40
+
41
+ So the trust boundary is inside the Harness container: the Harness process on one side, the domain on the other.
42
+ [ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md) makes Landlock a requirement of jr2, so no path runs a
43
+ program unconfined.
44
+
45
+ - **One spawner makes the domain.** At its start, the Harness starts the **spawner**, `effector spawner` on the runtime
46
+ volume ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), confined once. The spawner
47
+ starts each process that the Harness asks for, and it does each file tool's I/O and each read, list and remove of a
48
+ path that the Harness needs in the Agent's files (the sync's git folders and its bundle). It sends back the output,
49
+ the exit status, the file's bytes and the names in a folder. A process inherits its parent's domain, so every program
50
+ of every Agent of the Sandbox is in the one domain. They see, signal and trace each other, as one user's processes do
51
+ on a machine: a later `bash` call can `kill` or `lsof` a server that an earlier call started. The tools that the model
52
+ calls do not change: `bash` takes a command, and `read`, `write` and `edit` take a path. If the spawner stops, the
53
+ Harness starts it again, with the same rules. The new spawner makes a new domain, a sibling of the old one, and the
54
+ processes that the old spawner started run on in the old domain. A process of the new domain cannot trace one of the
55
+ old domain, or read its `/proc/<pid>/environ` or its open files. Below ABI 6 it can still signal it, so a later `bash`
56
+ call can end a server from before the restart; at ABI 6 or later it cannot. The confiner never follows a symbolic link
57
+ to find what a rule allows: a link gets no rule (what it names gets its own rule where it is allowed, and the kernel
58
+ checks the path that a link resolves to), and the confiner refuses to start when a rule's path has a link on it. A
59
+ rule on a link would allow what the link names at each restart, and a confined process can change that.
60
+ - **A person's shell is a process of the domain.** `jr2 workspace shell`
61
+ ([ADR-0009](0009-cli-and-instance-interface.md)) is `kubectl exec -c harness` of a small client in `/opt/jr2`, which
62
+ starts as the Harness starts (`effector nocore --harness-env`). The client starts nothing itself. It asks the Harness,
63
+ on a Unix socket in `/run/jr2/shell`, for the shell or for the one command after `--`, and the Harness asks the
64
+ spawner. So the shell is in the one domain, with the domain's env (the Stand-ins that the Workspace grants), its umask
65
+ and git identity, and the Workspace's cwd. It sees, signals and traces the Agents' processes, as a later `bash` call
66
+ does: a person can stop a dev server that an Agent left. With a terminal, the spawner opens a pseudo-terminal for the
67
+ shell, as its controlling terminal in a session of its own. The client sends the keys, the terminal's size and the
68
+ hang-up, and the spawner closes the terminal when the shell exits. The output has flow control: while the socket to
69
+ the client is full, the Harness pauses the spawn, and the spawner reads none of its output until the resume. So the
70
+ terminal fills and the shell's writes block, as on a slow terminal, and a fast shell does not fill the memory of the
71
+ Harness that serves the Agents' Turns. `/run/jr2/shell` is an emptyDir of the Harness container alone, and the domain
72
+ can make no file under `/run/jr2`, so no process of the domain listens in the Harness's place. A process of the domain
73
+ can connect to the socket, because Landlock does not control a connect. So the client proves that it is not one: its
74
+ first frame carries a digest of the control Stand-in, which it reads from the file and which the domain cannot read.
75
+ The Stand-in itself never goes on the socket. Without the proof, the Harness starts nothing.
76
+ - **The domain can do what the container's uid can do, except reach jr2's secrets.** The rules give every right on every
77
+ path, with one exception: `/run/jr2`. There the domain can list the names in each folder, and do nothing else.
78
+ Landlock only narrows the Unix permissions, so a confined program can do everything that the uid can do without
79
+ Landlock, except:
80
+ - reach the contents of `/run/jr2`: read, write, truncate, make, remove, move or link a file or a folder there.
81
+ `/run/jr2` holds the control Stand-in, each held secret's Stand-in, the folder of the Custodian's control socket,
82
+ the folder of the shell's socket, and the Harness's conversations. Every path that jr2 holds for the Harness alone
83
+ is under `/run/jr2`. Landlock allows a path only by its rules, so the confiner allows every sibling of `/run/jr2`
84
+ and of each folder above it, one level at a time. It also gives the root the right to list names on its whole tree,
85
+ so `ls /`, `ls /run` and `find /` work as they do outside jr2. That right lists the names under `/run/jr2` too: a
86
+ held secret's name, a conversation's file name. These names are not secrets.
87
+ - trace the Harness process, or read its `/proc/<pid>/environ` or `/proc/<pid>/mem`. A confined process cannot ptrace
88
+ a process outside its domain.
89
+ - at Landlock ABI 6 or later, connect to an abstract Unix socket that a process outside the domain made. The
90
+ containers of the pod share one network namespace, so this keeps the domain off the abstract sockets of the Harness
91
+ and of the Custodian.
92
+
93
+ Landlock does not limit a connect to a named Unix socket. So a confined program can connect to the Custodian's control
94
+ socket. There the Custodian is the guard: it refuses a request that does not carry the secret control Stand-in, and
95
+ the fetch Stand-in, which every confined program holds, is valid only on the fetch route. Each listener answers 404 to
96
+ the other's routes, so the fetch Stand-in gets nothing on the control socket.
97
+
98
+ The kernel, not the rules, keeps what other pods share. The Repo caches under `/repos`, the runtime under `/opt/jr2`
99
+ and the CA bundles are mounted read-only, and the pod mounts no ServiceAccount token. The domain does not limit the
100
+ network. A request reaches a held credential only through the Custodian
101
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)).
102
+
103
+ - **No Harness pod reaches a cloud metadata address.** Where the metadata service of a node answers pods, it gives the
104
+ node's cloud credential, which no `workspace()` grants. The Custodian's dial guard (ADR-0059) applies only to a
105
+ request that goes through the Custodian, and a program of the Agent can connect directly. So `jr2 up` converges an
106
+ egress NetworkPolicy for each Harness pod: `jr2-sandbox-egress` selects each Sandbox pod,
107
+ `jr2-instance-harness-egress` selects the Instance Harness, and each check pod of the provider preflight gets one of
108
+ its own, before the pod. Each policy allows all egress except `169.254.169.254/32`, `fd00:ec2::254/128`,
109
+ `169.254.170.2/32`, `169.254.170.23/32` and `100.100.100.200/32`: the metadata service of AWS, GCP and Azure, the
110
+ credential agents of ECS and EKS Pod Identity, and the metadata service of Alibaba Cloud.
111
+ - NetworkPolicy has no deny rule. So the rule allows `0.0.0.0/0` and `::/0`, each with its addresses as `except`, and
112
+ it also allows every pod of the cluster (`namespaceSelector: {}`).
113
+ - The Instance Harness and the check pod run no Workspace code. They get the policy because the check pod stands for
114
+ every Harness pod ([ADR-0019](0019-one-converging-command-against-the-current-context.md)): a model that only the
115
+ node's credential reaches must fail the check, not a Turn.
116
+ - The block has two limits. The list is not complete for every cloud provider, and only a CNI that enforces
117
+ NetworkPolicy applies it. So the shared-cluster guide tells the admin to check the metadata address of their
118
+ provider, and to give pods no node credential: Workload Identity, or IMDSv2 with a hop limit of 1.
119
+
120
+ - **The env of the domain is the container's own env, with the granted Stand-ins.** A program in the domain gets the env
121
+ that the container started with: the image's `ENV`, `harness.env` and `envFrom`, the proxy and CA variables. jr2's own
122
+ `JR2_*` names are removed, except `JR2_CUSTODIAN_URL`. It also gets `HOME`, `TMPDIR`, and the Stand-ins of the secrets
123
+ that the `workspace()` grants (its `secrets` list), each under its own name. It never gets what the Harness process
124
+ added to its own env after the start, and so it never gets the control Stand-in, or the Stand-in of a model key that
125
+ the `secrets` list does not name. A toolchain and a bring-your-own image work as they work outside jr2.
126
+ - **The Harness process runs with jr2's env alone.** node, glibc and OpenSSL read names from the env that load code or
127
+ configuration from a path: `NODE_OPTIONS=--require /app/x.js`, `LD_PRELOAD`, `OPENSSL_CONF`. The image's `ENV` can
128
+ name a file that the domain writes. So the Harness starts as `effector nocore --harness-env -- node …`. The effector
129
+ and tini are static binaries, so no `LD_*` name changes them. Before it starts node, the effector:
130
+ - gives node only the names that jr2 composed for the container: each name of its `env`, which the Orchestrator lists
131
+ in `JR2_HARNESS_ENV_NAMES`, and each key of its `envFrom`, to which the Orchestrator gives the prefix
132
+ `JR2_ENVFROM_`. The image's `ENV` can also hold a name with that prefix, and node then gets it. The marks keep out a
133
+ name that an image sets by accident, not one that its author sets to attack: the image's author already controls the
134
+ Harness process through the image's glibc
135
+ ([ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md)).
136
+ - stops, and starts nothing, on an `envFrom` key that starts with `JR2_`. jr2's own names, such as `JR2_MENU_ONLY`,
137
+ which turns the domain off, come from the container's `env` alone, so a shared Secret cannot set one. `jr2 up`
138
+ refuses a `JR2_` name in `harness.env`, and a `JR2_` key of a Secret or a ConfigMap that `harness.envFrom` loads.
139
+ - removes from them each name that loads code or configuration from a path, or writes to one: every `LD_*` and
140
+ `MALLOC_*` name, `NODE_OPTIONS`, `NODE_PATH`, `NODE_COMPILE_CACHE`, `OPENSSL_CONF`, `GCONV_PATH` and the others of
141
+ its list, and a `TZ` that names a file.
142
+ - gives node `HOME=/run/jr2/home`, whatever the start env holds. node reads `HOME` once, at its start, into the
143
+ folders where a bare `require()` looks last: `$HOME/.node_modules` and `$HOME/.node_libraries`. A provider module
144
+ that pi-ai loads on its first Turn can miss in `node_modules` and fall through to them. So a Harness process with a
145
+ `HOME` that the domain writes runs a module that the domain put there.
146
+ - puts the container's whole start env, each `envFrom` key under its own name, in `JR2_START_ENV`, as base64. The
147
+ domain's env is made from it, so the image's `ENV` and the container's `HOME` reach each program of the domain. No
148
+ program reads that name. A `JR2_START_ENV` that the start env already holds is dropped. Without
149
+ `JR2_HARNESS_ENV_NAMES` the effector starts nothing.
150
+
151
+ At its start, before it loads any other module, the Harness refuses to run when a folder where node looks for a module
152
+ is outside `/opt/jr2` and `/run/jr2`, and it checks that its `HOME` is `/run/jr2/home`. A change of `HOME` after node
153
+ started is too late, so the Harness never makes one.
154
+
155
+ A name of jr2's env that names a credential file which the Harness process reads (`AWS_CONFIG_FILE`,
156
+ `GOOGLE_APPLICATION_CREDENTIALS`, and the others) is the admin's choice. `jr2 up` refuses such a path in a folder that
157
+ the domain writes in every pod (`/work`, `/tmp`, `/dev/shm`, `/home/jr2`), and warns on any other path: whether the
158
+ Sandbox's uid can write a folder of the image is the image's, and jr2 cannot read it.
159
+
160
+ - **The file tools reach what `bash` reaches.** Their I/O runs in the spawner, so the kernel applies the domain's rules
161
+ to them. A path under `/run/jr2`, or a symbolic link to one, fails with the error that an unreadable file gives. `~`
162
+ in a path is the domain's `HOME`, as it is for `bash`.
163
+ - **The Harness process touches no path that the domain can write.** Each path in the Agent's files that the Harness
164
+ reads, lists or removes goes through the spawner. The one exception is the file that holds the full output of a `bash`
165
+ call. The Harness makes it in the domain's `TMPDIR`, with a name that must be new and must not be a link, and it
166
+ writes through that open file alone. If the file did not land in `TMPDIR`, the Harness removes it and refuses. The
167
+ Harness process has its own `HOME`, `/run/jr2/home`, from its start (above), which does not exist and which no process
168
+ can make, because code in the Harness process reads files under `HOME`: node looks for modules in `~/.node_modules`,
169
+ the AWS SDK reads `~/.aws/config`, which can name a `credential_process` to run, and Google's library reads
170
+ `~/.config/gcloud`. The domain keeps the Workspace's `HOME`.
171
+ - **One secret control Stand-in for each Sandbox.** Only the Harness process holds it, as a file under `/run/jr2`. It is
172
+ in no env of the domain. The Custodian puts the Sandbox's control token in its place on the control routes (ADR-0059),
173
+ and the real token never leaves the Custodian.
174
+ - **The control routes are a Unix socket under `/run/jr2`.** The Harness sends each control request to the Custodian's
175
+ control socket, not to a loopback port. A confined program cannot make a socket under `/run/jr2`, so it cannot take
176
+ the control listener's place while the Custodian restarts, and it cannot receive the control Stand-in. It can connect
177
+ to the socket, and the Custodian refuses what it sends (above). The fetch route stays on loopback, because its
178
+ Stand-in is public.
179
+ - **The Harness states the slot.** On each control request, the Harness states the Agent that the conversation belongs
180
+ to: its slot. It takes the slot from its own record of the conversation, never from the URL. The slot is the Agent's
181
+ full identity below the run: the actor path of its Machine and its agent name, which is a continued conversation's
182
+ Instance ID without the run id ([ADR-0057](0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md)). So two
183
+ Agents with one key in two child Machines are two slots. The Orchestrator checks that the stated slot is the slot of
184
+ the target registration, on the surface read, the pick, the delivery poll and the acknowledgment of a notice
185
+ ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)). This check finds a routing fault. It is not
186
+ the security boundary: the boundary is that only the Harness holds the credential.
187
+ - **Two tokens, each checked on its own routes.** This is defence in depth:
188
+ - The **control token** is what the Custodian puts in place of the secret control Stand-in. It is valid only on the
189
+ agent routes (`/agents/<iid>/surface`, `/agents/<iid>/events`, `/agents/<iid>/deliveries/<id>`, the route on which
190
+ the Harness acknowledges a notice, and `/agents/<iid>/conversation`, on which it sends the entries of the
191
+ conversation, [ADR-0023](0023-the-harness-prints-the-conversation.md)), and it carries the stated slot.
192
+ - The **fetch token** is what the Custodian puts in place of the public Sandbox Stand-in that `jr2-upload-pack` holds
193
+ as a constant. It is valid only on `POST /sandboxes/<sandbox>/fetch`
194
+ ([ADR-0053](0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md)). The Custodian
195
+ accepts the public Stand-in on the fetch route only.
196
+ - The Orchestrator accepts each token only on its own routes. A Gate accepts neither, and no other principal of an
197
+ Agent: an Agent never reaches a Gate (ADR-0013).
198
+ - **An engine runs its loop in the Harness process.** pi does: its loop is in the Harness process, and only its tools
199
+ run in the domain. Each engine adapter states where its loop runs, and `engine.ts` refuses an adapter whose engine
200
+ runs in the domain. Every process of the one domain can see, trace and signal every other one, so another Agent's code
201
+ could write such an engine's stdin, read its environ and forge its Menu calls.
202
+ - **An engine with its own process, such as Claude Code, needs a domain for each Agent first.** The root domain then
203
+ holds only the spawner, which no process can trace or read (`PR_SET_DUMPABLE 0`). For each conversation, the spawner
204
+ starts a sub-spawner one Landlock layer deeper, with a socket pair as its stdio and as the subreaper
205
+ (`PR_SET_CHILD_SUBREAPER`) of what it starts. Every process of that Agent comes from its sub-spawner: the engine, its
206
+ `bash` and its git. Landlock denies a process of one nested domain the `/proc/<pid>/fd` and the environ of a process
207
+ in a sibling domain, so an Agent's code cannot drive another Agent's engine. They still share `/work`. Such an engine
208
+ also needs these, as mechanisms:
209
+ - **Authority stays in the Harness.** The engine holds no token in its env, its argv or a file. The Harness knows the
210
+ engine by the socket that it gave it. The Harness accepts a Menu call only when it matches a tool call of a model
211
+ answer that the Harness relayed, and only the first pick of that answer
212
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). The Turn's safety hooks become decisions
213
+ of the relay and of the Menu server.
214
+ - **The model endpoint is not a proxy.** It sets the path, the model and the Dials, and refuses every other API of the
215
+ provider (files, batches, fine-tuning). It counts the usage into the Settlement, and it refuses a request outside a
216
+ Turn. The context of each request comes from jr2's record: the engine's own session file is a cache that jr2 does
217
+ not trust. It is never on the Harness's wire port.
218
+ - **Spawns go through the spawner.** An adapter starts its engine through the spawner, with the domain's env, never
219
+ with the Harness process's env.
220
+ - **No such engine on the Instance Harness.** The Instance Harness has no domain, and its control Stand-in is valid
221
+ for each Menu-only Agent of the Instance. A Menu-only Agent's engine runs its loop in the Harness process
222
+ ([ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md)).
223
+ - **Chart Agents and Pool workers need no provisioning.** The Agents that a Chart makes
224
+ ([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)) and each Turn of a Pool worker run in the one Harness, and
225
+ the Harness states their slot. The provision knows nothing of the Agents of the Sandbox.
226
+
227
+ ## Considered options
228
+
229
+ - **A raw `kubectl exec -c harness` shell (the old `jr2 ssh`).** Rejected: it runs outside the Landlock domain and can
230
+ read `/run/jr2`, so a person's shell, and any code that the person runs in it, reaches the control Stand-in.
231
+ `jr2 workspace shell` is a process of the spawner's domain ([ADR-0009](0009-cli-and-instance-interface.md)).
232
+ - **A person's shell in a sibling domain with the same rules**, which the client makes again with the confiner, so the
233
+ spawner needs no pseudo-terminal and the Harness needs no socket. Rejected: the shell reaches what an Agent's `bash`
234
+ reaches and nothing more, but it is not in the Agents' domain. It cannot trace their processes, and at ABI 6 or later
235
+ it cannot signal them, so a person cannot stop a runaway dev server, which is a usual reason to open a shell.
236
+ - **One Harness container for each agent slot, each with its own secret Stand-in.** Rejected. Kubernetes isolates one
237
+ container from another, but all the slots share the writable `/work`. Code that one Agent plants there runs in a
238
+ sibling's container when the sibling's `bash`, git or tests run, or when a `command` Effect runs there, and that code
239
+ reads the credential of the container that runs it. So container isolation between Agents gives nothing against an
240
+ attack that goes through `/work`. It also costs a container, a port, a Secret and a share of the Size for each slot,
241
+ and it has no container for an Agent that a Chart makes at run time.
242
+ - **A trusted control container and a tools container with no credential.** Rejected. It gives the same protection as
243
+ confinement: no Workspace code runs where the credential is. But it adds a container, it sends each tool call across a
244
+ container edge, and it changes [ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md) (the
245
+ Harness is one server). jr2 already confines the code that an Agent wrote in the Effector (ADR-0068). One mechanism
246
+ serves one problem.
247
+ - **A second uid inside the Harness container**, so that `bash` runs as a uid that cannot read the credential. Rejected.
248
+ Under `runAsNonRoot`, `allowPrivilegeEscalation: false` and `capabilities: drop ALL`, no process can `setuid` without
249
+ `CAP_SETUID`, and under `no_new_privs` that likely also needs `allowPrivilegeEscalation: true`. It adds privilege to
250
+ the container that runs code a model controls.
251
+ - **A user namespace**, so that the Harness maps a second uid with no host privilege. Rejected: the default seccomp
252
+ profile (`RuntimeDefault`) blocks `unshare(CLONE_NEWUSER)` from a process with no privilege. It needs a custom seccomp
253
+ profile and a kernel that permits it, so it is not portable to a self-hosted cluster.
254
+ - **No right to list `/` and the folders above `/run/jr2`.** Rejected. `ls /`, `ls /run`, `find /` and a glob such as
255
+ `/*` then fail, which stops normal work. Landlock can only add rights, and a right on a folder holds on its whole
256
+ tree, so a right to list `/` also lists `/run/jr2`. The names there are not secrets.
257
+ - **The Harness process inherits the image's env.** Rejected: the image's `ENV` can hold
258
+ `NODE_OPTIONS=--require /app/x.js`, and `/app` can be a folder that the Sandbox's uid owns. Workspace code writes
259
+ `/app/x.js`, ends the Harness (below ABI 6 a signal can), and the restarted Harness runs that file with the control
260
+ Stand-in in its memory. A deny-list alone on the image's env is not sufficient: the next name that loads code is not
261
+ on it.
262
+ - **The Harness process keeps the domain's `HOME`, or takes its own after node starts.** Rejected: the domain writes
263
+ that folder, and code in the Harness process reads files there. A planted `~/.aws/config` with a `credential_process`
264
+ would run a program outside the domain. node reads `HOME` once, at its start: a module planted in
265
+ `$HOME/.node_modules` or `$HOME/.node_libraries` would run in the Harness process, as one named by `NODE_OPTIONS` or
266
+ `NODE_PATH` would. So the Harness gets its `HOME` when it is started.
267
+ - **Rules that allow only `/work`, `HOME`, `TMPDIR` and the system paths.** Rejected. It stops what an Agent does on a
268
+ machine: a write to a folder of the image that the uid owns (`/app`, a virtualenv, `/usr/local`), a read of `/var` or
269
+ `/srv`, a write to `/proc/self/oom_score_adj`. A bring-your-own image breaks in ways that its author cannot see. The
270
+ secrets are under one path, so a rule that denies that path is sufficient.
271
+ - **A new domain for each process that the Harness starts.** Rejected. Each `bash` call is then its own domain, so a
272
+ later call cannot signal, trace or `lsof` the server that an earlier call started. That is normal behaviour for an
273
+ Agent.
274
+ - **One spawner for each conversation, today.** Rejected while every engine runs its loop in the Harness process. Then
275
+ an Agent's processes hold nothing that another Agent's code can use to act as that Agent: the credential and the loop
276
+ are in the Harness, and each process gets the one env of the Sandbox. Through `/work`, one Agent can only try to
277
+ persuade another Agent's model. The separation costs a process and a layer for each conversation. It is the path for
278
+ an engine with its own process (above): there, a sibling in the one domain forges the engine's Menu calls at once.
279
+ - **The file tools do their I/O in the Harness, with a check of the path.** Rejected. It puts the rule in two places,
280
+ the kernel and jr2's code, and the check in code must handle each symbolic link and each race.
281
+ - **The control routes on a loopback port.** Rejected. A confined program can bind `127.0.0.1:<port>` while the
282
+ Custodian restarts, and it then receives the secret control Stand-in on the Harness's next request.
283
+ - **The Custodian authenticates the caller.** Rejected alone: every process reaches the Custodian over the one pod
284
+ loopback. The Custodian can tell the Harness from a child only by a secret that the child cannot read, which is this
285
+ decision.
286
+ - **Keep the Sandbox-scoped credential, and call the Agents of one Workspace mutually trusting.** Rejected: a coder
287
+ beside an independent reviewer in one Workspace is a motivating case.
288
+ - **Refuse all of `169.254.0.0/16` at the egress.** Rejected: NodeLocal DNSCache answers on `169.254.20.10`, so the pods
289
+ of a cluster that runs it lose DNS.
290
+ - **An egress policy with `ipBlock` peers only.** Rejected: a CNI that applies an `ipBlock` to the address of a pod
291
+ (Cilium) then cuts DNS, the Orchestrator and each Service. So the rule also allows every pod of the cluster.
292
+ - **The egress policy on the Sandbox pods only.** Rejected: the check pod of the provider preflight stands for every
293
+ Harness pod. Without the policy there, a model that only the node's credential reaches passes the check and fails in a
294
+ Turn.
295
+
296
+ ## Consequences
297
+
298
+ - **No Workspace code can act as an Agent.** Planted code finds no control credential: not in its env, not in a file
299
+ that it can read, not through the Harness's `/proc`. It cannot deliver a pick for any Agent, its own included. Only
300
+ the Harness process delivers a pick, for the conversation whose model called the pick.
301
+ - **What remains.**
302
+ - An Agent can still try to persuade another Agent through what that Agent reads: a file, a commit message, the output
303
+ of a test. That is an attack on content, not a forged pick. The other Agent's model chooses each pick, and the
304
+ Orchestrator judges it against that Turn's Allowed picks
305
+ ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)).
306
+ - A confined child can spend only a held secret that its `workspace()` grants. Every Stand-in but the fetch Stand-in
307
+ is secret, and the child's env holds only the granted ones (ADR-0059). So code in `/work` reaches a model key only
308
+ when the author grants that key.
309
+ - Below Landlock ABI 6, a confined process can still send a signal to the Harness process, which has the same uid. So
310
+ the Harness starts with no core file (`effector nocore`, a core limit of zero), and with Node's `--disable-sigusr1`.
311
+ Without them, a dumping signal writes the Harness's memory, with its Stand-ins, to a file in its working folder, and
312
+ SIGUSR1 opens Node's inspector on the pod loopback, which runs the child's code in the Harness. What a signal can
313
+ still do is stop or end the Harness. That is a denial of service in the Agent's own Sandbox, not an impersonation.
314
+ The Harness names its process `jr2-harness`, so an Agent's `pkill node` does not end it by accident. ABI 6 scopes
315
+ signals and abstract Unix sockets to the domain, and the confiner uses both scopes when the kernel offers them
316
+ (ADR-0077): the Harness is then outside each signal from the domain.
317
+ - The domain can connect to the Custodian's control socket. The Custodian refuses each request that does not carry the
318
+ control Stand-in, which no process of the domain holds.
319
+ - The domain can see that a path under `/run/jr2` exists, by its name or by `stat`, and its size. It cannot read what
320
+ is in it.
321
+ - The node's cloud credential is closed only at the metadata addresses that jr2 knows, and only on a CNI that enforces
322
+ NetworkPolicy (the decision above).
323
+ - **The pod is ADR-0005's pod**: one Harness, a Custodian, and an optional User Container and Sandbox Effector. The
324
+ count of Agents does not change it, and the Size splits as before
325
+ ([ADR-0060](0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md)).
326
+ - **The Instance Harness runs no Workspace code.** A Menu-only Agent has no Working tools (ADR-0031), so the credential
327
+ of the Instance Harness is already where no Workspace code runs.
328
+ - **Tests.** The `@kind` tier proves the boundary from the model's own `bash`: it finds no Stand-in of the pod in its
329
+ env, in a file or through the Harness's `/proc`; a test that the coder plants and the reviewer's own `node --test`
330
+ runs cannot deliver a pick as the reviewer, and the run does not settle; a granted held secret works, and no model key
331
+ is in the env; a second Agent runs git on its Worktree, and the step checks the output. It also proves that the domain
332
+ does not stop normal work: a server that one `bash` call starts is found and ended by a later call; a write to a
333
+ folder of the image that the uid owns, outside `/work`, succeeds; a move of a file into another folder succeeds;
334
+ `ls /` and `ls /run` succeed; and `read` reads the full output of a `bash` call. It proves the metadata block: kind
335
+ has no metadata service, so the step gives `169.254.169.254` to the node, and a pod that the policy does not select
336
+ gets the node's refusal there. The Agent's `bash` does not, and it still connects to the model's host and to the
337
+ Orchestrator's Service. A unit test pins the policy that `jr2 up` composes. The unit and conformance tests refuse the
338
+ control token on the fetch route, the fetch token on an agent route, a stated slot that is not the registration's, and
339
+ both tokens at a Gate, and they check the slot on the delivery poll. A test reads the Harness container's mounts from
340
+ the pod spec. It fails on a mount that is not read-only, not under `/run/jr2`, and not the Workspace's own (`/work`,
341
+ `HOME`), so a new mount is classified before it ships. Unit tests start node through `effector nocore --harness-env`
342
+ with a `HOME` that holds a planted `.node_modules`, and check that node looks for no module there; they also retarget
343
+ a link onto `/run/jr2`, restart the spawner, and check that the new domain reads nothing there. The `@kind` tier reads
344
+ the Harness process's environ from outside the pod and checks its `HOME`.
345
+ - **The `@model` tier runs after the build.** The change alters what the Working tools can do and where a notice is
346
+ delivered ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)), so it is a `@model`-tier change
347
+ ([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)).
@@ -0,0 +1,79 @@
1
+ # jr2 requires Landlock on every Sandbox node
2
+
3
+ Two boundaries of jr2 are Landlock domains. [ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)
4
+ confines the child of a credentialed `command` in the Effector, so that the Agent's code cannot read the Effector's key
5
+ or a secret that the command does not name.
6
+ [ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md) confines each process that the
7
+ Harness starts in `/work`, so that no Workspace code can read the control credential and act as an Agent. On a node with
8
+ no Landlock, neither boundary exists. A path that runs there with less confinement is a second jr2, with a weaker
9
+ boundary, to support and to test.
10
+
11
+ ## Decision
12
+
13
+ - **Landlock is a requirement of jr2 on every Sandbox node.** It holds where Workspace code runs, which is also where
14
+ the check reaches. No component has an unconfined branch, anywhere.
15
+ - **The Repo cache agent detects it, on each Sandbox node.** The cache agent's DaemonSet has one pod on each Sandbox
16
+ node, by the same `nodeSelector` and `tolerations` as the Sandboxes
17
+ ([ADR-0052](0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md)). The agent probes Landlock
18
+ on its node. On a node without it, the agent is NotReady, and its status gives the reason.
19
+ - **`jr2 up` refuses to converge while a Sandbox node lacks Landlock.** It reads the DaemonSet's pods (one list call),
20
+ and it names each Sandbox node whose cache agent reports no Landlock
21
+ ([ADR-0019](0019-one-converging-command-against-the-current-context.md)). The admin keeps a bad node out of the set
22
+ with `sandbox.nodeSelector` or a taint, as ADR-0052 says. There is still no jr2 node label.
23
+ - **The refusal is an ordinary converge failure** (ADR-0019). It comes within the cache agent's rollout wait, which
24
+ `jr2 up` already makes. It names each Sandbox node without Landlock, the probe's reason, and the fix: keep the node
25
+ out with `sandbox.nodeSelector` or a taint (ADR-0052), or enable Landlock in its kernel. A run of `jr2 up` after the
26
+ fix converges.
27
+ - **Each confining component probes at its start, and refuses to serve without it.** These are the Harness of a Sandbox
28
+ and the Sandbox Effector. The probe is `landlock_create_ruleset` with `LANDLOCK_CREATE_RULESET_VERSION`. This is the
29
+ backstop for a node that joins after the converge.
30
+ - **No component keeps a path for a node that cannot confine a child.** ADR-0068's Effector refuses to start on such a
31
+ node, whatever its Workspace holds.
32
+ - **The minimum ABI is 3.** Under ABI 1, a confined process can never move or link a file into another folder, whatever
33
+ the rules allow, and package managers, git and `mv` do that. ABI 2 adds the `REFER` right, so the domain can do it
34
+ (ADR-0076). Under ABI 2, Landlock does not handle `truncate(2)`, so a confined process can truncate a file that the
35
+ rules let it do nothing else to: a conversation under `/run/jr2`, which the Harness rebuilds an Agent's record from.
36
+ ABI 3 adds the `TRUNCATE` right, and the confiner always handles it. The confiner uses the scopes of abstract Unix
37
+ sockets and of signals (ABI 6) when the kernel offers them. Each probe refuses a kernel below ABI 3, with the ABI that
38
+ it found.
39
+ - **The components that probe.** The cache agent probes as the detector, though it confines nothing. The Harness of a
40
+ Sandbox and the Sandbox Effector probe because they confine. The Instance Harness and the Instance Effector confine
41
+ nothing: a Menu-only Agent has no Working tools ([ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md)),
42
+ and the Instance Effector runs no `command`. They do not probe.
43
+
44
+ ## Considered options
45
+
46
+ - **A minimum of ABI 1.** Rejected: under it no confined process can move a file into another folder, which stops normal
47
+ work.
48
+ - **A minimum of ABI 2.** Rejected: under it the domain can truncate a file under `/run/jr2`. One Agent's code can then
49
+ erase or roll back another Agent's conversation, and below ABI 6 it can end the Harness so that the Harness rebuilds
50
+ from the cut file. It admits Linux 5.19 to 6.1 (Debian 12's stock kernel among them), for a boundary with a hole.
51
+ - **Refuse only a Sandbox with more than one Agent on a node without Landlock.** Rejected: it keeps a less secure
52
+ branch, which jr2 must then support and test.
53
+ - **Run unconfined, with a warning.** Rejected: Workspace code can then read the control credential, and a warning does
54
+ not stop it.
55
+ - **A check pod on each node at each `jr2 up`.** Rejected: it starts one pod for each node on every converge, and it
56
+ misses a node that joins later.
57
+ - **One check pod, with the start-up probe as the backstop.** Rejected: a bad node shows only when a run lands there and
58
+ fails.
59
+ - **jr2 labels the nodes that have Landlock, and a Sandbox requires the label.** Rejected: it reverses ADR-0052, and it
60
+ needs a cluster-wide permission to patch Nodes.
61
+
62
+ ## Consequences
63
+
64
+ - **The kernels that this excludes.** Landlock ABI 3 is in Linux 6.2 and later. A node with an older kernel cannot run
65
+ jr2: for example RHEL 8 (4.18), Debian 11 (5.10), Debian 12 on its stock kernel (6.1), Ubuntu 20.04 and 22.04 on their
66
+ first kernels (5.4, 5.15), and Amazon Linux 2 (4.14 or 5.10). A distribution kernel can carry a backport: the probe,
67
+ not the version, decides. A newer kernel must also enable Landlock: its list of security modules (`CONFIG_LSM`, or the
68
+ `lsm=` boot parameter) must name `landlock`. On a node, `/sys/kernel/security/lsm` lists the modules that run.
69
+ Measured from a hardened pod with no capabilities: kind on WSL2 (6.6) offers ABI 3; the home cluster's arm64 nodes
70
+ (6.8), ABI 4; spark (6.11, arm64), ABI 5.
71
+ - **kind on a host inherits the host's kernel.** A kind node is a container, so the kernel of the host decides. On macOS
72
+ or Windows, Docker runs the nodes in a virtual machine, and the kernel of that machine decides.
73
+ - **A mixed cluster.** `jr2 up` names each Sandbox node without Landlock and refuses until the admin keeps it out of the
74
+ set. A node that joins after the converge shows as a NotReady cache agent. A Sandbox placed on it fails at its start,
75
+ because its Harness refuses to serve.
76
+ - **The check runs when a Machine uses a Sandbox.** `jr2 up` converges the cache agent then
77
+ ([ADR-0051](0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md)). With no Sandbox, no node needs
78
+ Landlock, and there is nothing to check.
79
+ - **The docs list the requirement** where they list what jr2 needs from a cluster and from a host.