@jr2/orchestrator 0.6.0 → 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 (301) 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 +86 -9
  14. package/docs/concepts/architecture.md +41 -28
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +7 -5
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +4 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +50 -22
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +5 -4
  23. package/docs/concepts/pools-and-sources.md +187 -11
  24. package/docs/concepts/secrets-and-trust.md +73 -33
  25. package/docs/concepts/the-menu.md +11 -0
  26. package/docs/concepts/workspaces.md +6 -5
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
  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/guides/add-tools-to-the-sandbox.md +55 -11
  32. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  33. package/docs/guides/answer-a-gate.md +74 -37
  34. package/docs/guides/bind-open-parts.md +24 -8
  35. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  36. package/docs/guides/cancel-a-run.md +26 -16
  37. package/docs/guides/fix-a-failed-converge.md +18 -6
  38. package/docs/guides/give-a-caller-a-scope.md +203 -0
  39. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  41. package/docs/guides/let-an-agent-write-a-chart.md +9 -6
  42. package/docs/guides/look-inside-a-sandbox.md +101 -31
  43. package/docs/guides/open-the-console.md +63 -14
  44. package/docs/guides/read-a-conversation.md +225 -0
  45. package/docs/guides/run-a-chart-by-name.md +17 -16
  46. package/docs/guides/run-one-worker-per-item.md +15 -9
  47. package/docs/guides/set-an-agents-access-and-dials.md +2 -0
  48. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  49. package/docs/guides/start-a-run-from-a-run.md +18 -16
  50. package/docs/guides/start-a-run.md +24 -18
  51. package/docs/guides/steer-an-agent.md +332 -0
  52. package/docs/guides/use-a-hosted-provider.md +1 -1
  53. package/docs/guides/watch-a-run.md +127 -121
  54. package/docs/guides/write-a-hook-kind.md +238 -0
  55. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  56. package/docs/guides/write-a-workflow.md +17 -7
  57. package/docs/llms.txt +81 -15
  58. package/docs/reference/authoring-surface/agent.md +1 -1
  59. package/docs/reference/authoring-surface/answer.md +1 -1
  60. package/docs/reference/authoring-surface/arg.md +1 -1
  61. package/docs/reference/authoring-surface/basic.md +1 -1
  62. package/docs/reference/authoring-surface/bearer.md +1 -1
  63. package/docs/reference/authoring-surface/branchname.md +1 -1
  64. package/docs/reference/authoring-surface/carry.md +23 -0
  65. package/docs/reference/authoring-surface/chartschema.md +1 -1
  66. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  67. package/docs/reference/authoring-surface/command.md +1 -1
  68. package/docs/reference/authoring-surface/customize.md +4 -1
  69. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  70. package/docs/reference/authoring-surface/defineevent.md +1 -1
  71. package/docs/reference/authoring-surface/dispatch.md +5 -4
  72. package/docs/reference/authoring-surface/fail.md +1 -1
  73. package/docs/reference/authoring-surface/file.md +1 -1
  74. package/docs/reference/authoring-surface/frompick.md +1 -1
  75. package/docs/reference/authoring-surface/gate.md +118 -0
  76. package/docs/reference/authoring-surface/group-agents.md +15 -0
  77. package/docs/reference/authoring-surface/group-charts.md +16 -0
  78. package/docs/reference/authoring-surface/group-composition.md +25 -0
  79. package/docs/reference/authoring-surface/group-config.md +17 -0
  80. package/docs/reference/authoring-surface/group-effects.md +48 -0
  81. package/docs/reference/authoring-surface/group-failure.md +13 -0
  82. package/docs/reference/authoring-surface/group-gates.md +16 -0
  83. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  84. package/docs/reference/authoring-surface/group-steering.md +12 -0
  85. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  86. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  87. package/docs/reference/authoring-surface/http.md +1 -1
  88. package/docs/reference/authoring-surface/isfailure.md +1 -1
  89. package/docs/reference/authoring-surface/isready.md +1 -1
  90. package/docs/reference/authoring-surface/jr2setup.md +13 -3
  91. package/docs/reference/authoring-surface/mcp.md +107 -0
  92. package/docs/reference/authoring-surface/open.md +5 -3
  93. package/docs/reference/authoring-surface/openbody.md +1 -1
  94. package/docs/reference/authoring-surface/paramsof.md +1 -1
  95. package/docs/reference/authoring-surface/pool.md +77 -9
  96. package/docs/reference/authoring-surface/push.md +1 -1
  97. package/docs/reference/authoring-surface/readyof.md +1 -1
  98. package/docs/reference/authoring-surface/reread.md +19 -0
  99. package/docs/reference/authoring-surface/secret.md +1 -1
  100. package/docs/reference/authoring-surface/sentby.md +1 -1
  101. package/docs/reference/authoring-surface/skip.md +22 -0
  102. package/docs/reference/authoring-surface/source.md +56 -1
  103. package/docs/reference/authoring-surface/steer.md +93 -0
  104. package/docs/reference/authoring-surface/text.md +1 -1
  105. package/docs/reference/authoring-surface/type-agentdefinition.md +14 -1
  106. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  107. package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
  108. package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
  109. package/docs/reference/authoring-surface/type-binding.md +1 -1
  110. package/docs/reference/authoring-surface/type-caller.md +40 -0
  111. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  112. package/docs/reference/authoring-surface/type-carry.md +19 -0
  113. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  114. package/docs/reference/authoring-surface/type-commandoutput.md +1 -1
  115. package/docs/reference/authoring-surface/type-customize.md +16 -1
  116. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  117. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  118. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  119. package/docs/reference/authoring-surface/type-dispatchoutput.md +1 -1
  120. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  121. package/docs/reference/authoring-surface/type-framepath.md +1 -1
  122. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  123. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  124. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  125. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  126. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  127. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  128. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  129. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  130. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  131. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  132. package/docs/reference/authoring-surface/type-hostinjectedinput.md +1 -1
  133. package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
  134. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  135. package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
  136. package/docs/reference/authoring-surface/type-lends.md +5 -1
  137. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  138. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  139. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  140. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  141. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  142. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  143. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  144. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  145. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  146. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  147. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  148. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  149. package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
  150. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  151. package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
  152. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  153. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  154. package/docs/reference/authoring-surface/type-reposlot.md +1 -1
  155. package/docs/reference/authoring-surface/type-reread.md +18 -0
  156. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  157. package/docs/reference/authoring-surface/type-sender.md +26 -1
  158. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  159. package/docs/reference/authoring-surface/type-size.md +1 -1
  160. package/docs/reference/authoring-surface/type-skip.md +19 -0
  161. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  162. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  163. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  164. package/docs/reference/authoring-surface/type-sourcespec.md +16 -7
  165. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  166. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  167. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  168. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  169. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  170. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  171. package/docs/reference/authoring-surface/type-workspaced.md +1 -1
  172. package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
  173. package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
  174. package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
  175. package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
  176. package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
  177. package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
  178. package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
  179. package/docs/reference/authoring-surface/workspace.md +1 -1
  180. package/docs/reference/authoring-surface/worktree.md +1 -1
  181. package/docs/reference/authoring-surface.md +88 -167
  182. package/docs/reference/cli.md +413 -74
  183. package/docs/reference/config.md +235 -2
  184. package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
  185. package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
  186. package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
  187. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  188. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  189. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  190. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  191. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +9 -2
  192. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
  193. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  194. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +18 -14
  195. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
  196. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  197. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +32 -18
  198. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
  199. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  200. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +116 -34
  201. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
  202. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  203. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +7 -6
  204. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
  205. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  206. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  207. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -18
  208. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  209. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +4 -3
  210. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  211. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +5 -4
  212. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
  213. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  214. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  215. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  216. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +7 -0
  217. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
  218. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
  219. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
  220. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
  221. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  222. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  223. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +51 -23
  224. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
  225. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
  226. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  227. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +17 -13
  228. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  229. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  230. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
  231. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
  232. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  233. 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
  234. package/docs/reference/decisions.md +6 -4
  235. package/docs/reference/glossary.md +96 -42
  236. package/docs/reference/hooks.md +1077 -0
  237. package/docs/reference/http-api.md +976 -149
  238. package/docs/why.md +2 -1
  239. package/package.json +10 -3
  240. package/src/actor.ts +36 -10
  241. package/src/agent.ts +50 -0
  242. package/src/assembly-checks.ts +530 -4
  243. package/src/callers.ts +245 -0
  244. package/src/chart.ts +52 -7
  245. package/src/config.ts +550 -1
  246. package/src/conversations.ts +453 -0
  247. package/src/custodian.ts +8 -2
  248. package/src/customize.ts +143 -4
  249. package/src/dispatch-effect.ts +142 -26
  250. package/src/durability.ts +2 -0
  251. package/src/duration.ts +20 -0
  252. package/src/effect-checks.ts +2 -1
  253. package/src/effect.ts +60 -7
  254. package/src/effector-wire.ts +272 -27
  255. package/src/gate-brand.ts +16 -0
  256. package/src/gate.ts +634 -48
  257. package/src/harness-client.ts +10 -4
  258. package/src/held-secrets.ts +72 -23
  259. package/src/holds.ts +2 -2
  260. package/src/hook-kinds/discord.ts +92 -0
  261. package/src/hook-kinds/door.ts +107 -0
  262. package/src/hook-kinds/github.ts +89 -0
  263. package/src/hook-kinds/gitlab.ts +72 -0
  264. package/src/hook-kinds/hook.ts +7 -0
  265. package/src/hook-kinds/index.ts +465 -0
  266. package/src/hook-kinds/slack.ts +151 -0
  267. package/src/hook-kinds/webhook.ts +113 -0
  268. package/src/hooks.ts +506 -0
  269. package/src/http-effect.ts +76 -38
  270. package/src/http.ts +889 -94
  271. package/src/index.ts +16 -5
  272. package/src/instance.ts +196 -26
  273. package/src/internal.ts +22 -3
  274. package/src/machine-doc.ts +26 -1
  275. package/src/mcp-effect.ts +424 -0
  276. package/src/mount-digest.ts +41 -7
  277. package/src/names.ts +64 -2
  278. package/src/notices.ts +138 -10
  279. package/src/open-body.ts +25 -11
  280. package/src/open-effect.ts +17 -4
  281. package/src/open.ts +4 -2
  282. package/src/parts.ts +87 -9
  283. package/src/party.ts +2 -1
  284. package/src/pick.ts +2 -2
  285. package/src/pool.ts +1142 -163
  286. package/src/registration.ts +203 -13
  287. package/src/run-host.ts +1509 -171
  288. package/src/run-list.ts +191 -0
  289. package/src/sandbox-kube.ts +53 -5
  290. package/src/secret-placement.ts +239 -33
  291. package/src/server.ts +53 -4
  292. package/src/setup.ts +72 -32
  293. package/src/snapshot-store.ts +565 -48
  294. package/src/steer.ts +111 -0
  295. package/src/stub-harness.ts +6 -2
  296. package/src/tokens.ts +263 -22
  297. package/src/wire.ts +17 -0
  298. package/src/workspace.ts +50 -6
  299. package/src/worktree.ts +3 -3
  300. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  301. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
@@ -0,0 +1,101 @@
1
+ # The Console unlocks with a Caller's credential, and shows what its scopes allow
2
+
3
+ [ADR-0014](0014-observation-is-open-run-state-is-not.md) built the HTTP surface on one premise: the visualizer page is a
4
+ browser, and a browser holds no token — so it got a projection instead of a credential, and "give the page a token" was
5
+ explicitly rejected. That page is now the **Console**: it starts runs, answers Gates and cancels runs, which are writes
6
+ on the guarded surface. This ADR records how a browser earns those writes without moving the open band. The person
7
+ presents a credential; the page never ships with one.
8
+
9
+ ## Decision
10
+
11
+ - **The person presents a Caller's credential; the page never ships with one.** The Console's nav carries a password
12
+ field, and a login button for each JWT issuer that the Instance declares with `login: true`
13
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)). Empty, the page is
14
+ precisely ADR-0014's observer: open structure and observation, no control rendered. With a credential, the page sends
15
+ `Authorization: Bearer` on the guarded surface. The credential is any Caller's: the Instance token, a named Caller
16
+ token, or a JWT. The login gets a JWT from the issuer with OIDC (the authorization code flow with PKCE, as a public
17
+ client), and the page then uses it as it uses a typed token. The page as _served_ stays open and inert; ADR-0014's
18
+ rejection stands, because what it rejected was baking the credential into the page.
19
+ - **The login's facts come from an open route, and its client id is the issuer's `audience`.** Before a login, the page
20
+ reads `GET /console/config`: `{ login: [{ issuer, clientId }] }`, from the issuers of the current Assembly that have
21
+ `login: true`. The route is in the open band (ADR-0014), because the page reads it before it holds a credential, and
22
+ both values are public: a public client has no secret, and the issuer's URL is in every JWT that it signs. The client
23
+ id is the issuer entry's `audience`, because the JWT that the login gives names its client as its audience, and that
24
+ is what the JWT authenticator checks. A login is opt-in for each issuer, because an issuer for machines (a CI
25
+ provider's tokens) has no login page. The page reads the issuer's discovery document, asks for `openid`, `profile`,
26
+ `email` and `groups` where the issuer lists each, and sends the ID token as the bearer: an access token's audience and
27
+ form differ between providers. The return address is always the Console's root (`/`), so the client registers one
28
+ redirect URI; the page then puts the address back to where the login began. The verifier, the state and that address
29
+ wait in `sessionStorage` until the return, and a return whose state does not match is refused. PKCE needs
30
+ `crypto.subtle`, so a login needs the Console on https or on localhost, and the issuer must let the Console's origin
31
+ call its discovery document and its token endpoint (CORS).
32
+ - **The page shows only what the Caller's scopes allow.** On entry the page asks `GET /whoami` for the Caller record:
33
+ its name, the name that jr2 shows, and its scopes for each Workflow. It renders a control only where a scope opens its
34
+ route: the start form with `start` for that Workflow, Gate cards with `answer`, cancel with `cancel`, a run's full
35
+ state with `read`, and the Steering box with `steer`. The page re-fetches a run on a frame only for a Caller with
36
+ `answer` or `read` on its Workflow. The nav shows who the page acts as: the name that jr2 shows, else `<via>/<name>`,
37
+ with the scopes in its title, and a sign-out button in place of the field. The server still checks each request
38
+ (ADR-0014); the page hides a control so that a person does not meet a 403, not as a guard.
39
+ - **The credential lives in `sessionStorage`** — survives a reload, dies with the tab, never a cookie (no ambient
40
+ credential, no CSRF surface). A typed token and a login's JWT live there alike. A later 401 (an expired JWT, a rotated
41
+ token) drops the UI back to observer mode rather than failing per widget.
42
+ - **Gate discovery is a frame-triggered guarded read, not an open-band extension.** A Gate exists exactly while its
43
+ invoking state is entered ([ADR-0011](0011-workflow-defined-events.md)), and every entry/exit lands a status frame on
44
+ the open workflow feed (ADR-0022) — so a Console whose Caller has `answer` reads `GET /gates` on entry and again on
45
+ each frame, and folds the Gates into its store. `GET /gates` lists only the Gates that the Caller may answer, across
46
+ every Workflow (ADR-0078), so the Gate cards are the person's inbox and never show a Gate the person cannot answer.
47
+ Synchronized, no polling loop, and the observation projection is untouched. A delivery's success is likewise confirmed
48
+ by the next frame emptying the card, never by local bookkeeping.
49
+ - **The rail lists ended runs too.** It lists the live runs and, under them, the ended runs of the selected Workflow
50
+ from `GET /runs?status=ended&workflow=<name>` (ADR-0009), each with its status, and a `drifted` run with its reason
51
+ (ADR-0030). An ended run opens as a live one does: its graph, its last status and its conversations, for as long as
52
+ jr2 keeps the run (ADR-0065). A run on an older Assembly carries the mark "older Assembly · <hash>" and is drawn on
53
+ its own Machine, live or ended: an ended run's row names the Machine it ran on
54
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). A `drifted` run has no
55
+ Machine to draw, so it shows its drift reason in place of the graph. The Console shows each outstanding pick as
56
+ "outstanding since".
57
+ - **A conversation opens in a bottom drawer.** A full-width drawer across the bottom of the page shows the run's
58
+ conversations ([ADR-0023](0023-the-harness-prints-the-conversation.md)). A person can change its height, and the page
59
+ keeps the height for that viewer. Closed, it is a bar of tabs, one for each Agent slot, with a dot on a slot whose
60
+ Turn is live. Open, it shows the selected slot's conversation and follows a live Turn, with the Steering box beside it
61
+ for a Caller with `steer`. It reads the conversation routes, so it needs `read`. The right drawer keeps Attention and
62
+ Activity.
63
+ - **Page addresses content-negotiate; JSON is the default dialect.** `/` and `/workflows/:name` serve the Console shell
64
+ to `Accept: text/html` and JSON to everything else — so the workflow-detail resource keeps its natural address (the
65
+ run-input schema's home, [ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)) and no future page has to
66
+ squat on a mangled sibling path. Nothing existing moves: EventSource asks `text/event-stream`, and the Harness's Menu
67
+ and the CLI never ask for HTML. `/viz/*` is deleted (greenfield, no redirects); assets move to `/assets/*`, which also
68
+ retires the "a workflow named `assets` is shadowed" hazard.
69
+
70
+ ## Considered options
71
+
72
+ - **`jr2 ui` — the CLI opens the browser through an authenticated local proxy.** A convenience layer worth building _on
73
+ top of_ the token field later, not a substitute: it has no answer for the deployed jr2 Application behind an ingress,
74
+ where the browser talks to the Service directly and the CLI is not in the path.
75
+ - **The Instance token alone.** Rejected (ADR-0078): a person who must answer Gates and holds no cluster RBAC would then
76
+ hold every act on every run. A scoped Caller's credential is what that person holds.
77
+ - **The conversation in the right drawer.** Rejected: a conversation needs the page's width, and the right drawer keeps
78
+ Attention and Activity.
79
+ - **Announce gates on the open observation feed** (even just `{runId, gate}`). Rejected: it widens the projection for a
80
+ convenience the credential already solves, and the open band has an unresolved exposure question as it stands (spawn
81
+ ids — the ADR-0014 open call). The projection stays the one place the line is drawn.
82
+ - **A `/ui` path prefix, or moving the API under `/api`.** Rejected for negotiation: the first is ceremony on the
83
+ address a human types, the second churns the Harness's Menu, the Custodian's route allowlist, the CLI, and the
84
+ ADR-0009/0013 route tables for cosmetics.
85
+
86
+ ## Consequences
87
+
88
+ - **No new band.** The server-side diff of the Console itself is: negotiate two addresses, serve the shell, retire
89
+ `/viz/*`, and serve `GET /console/config` in the open band. The scopes, `GET /whoami` and the login's issuers are
90
+ ADR-0078's.
91
+ - **Possession follows the credential.** The Instance token stays one `kubectl get secret` away for whoever holds
92
+ cluster RBAC; a named token is what its holder was given; a login is what the identity provider's groups grant.
93
+ - **The XSS caveat is the real one and is bounded by what the page is:** a credential in `sessionStorage` is readable by
94
+ scripts on the page, so the Console must keep serving only its own assets — no CDN, no third-party script, exactly the
95
+ constraint the viz page already lives under.
96
+ - **GLOSSARY.md**: **Console** is a glossary term (visualizer, dashboard, UI, viz on its Avoid list).
97
+ - **The Console's UX gets a browser tier** — `@console`-tagged Cucumber scenarios holding a Playwright page, in
98
+ `@jr2/e2e`, excluded from the default profile ([ADR-0010](0010-bdd-acceptance-tests.md) as amended). What the page
99
+ _believes_ stays unit-tested in `console-store.test.ts`, and the login's pure half in `console-login.test.ts`; the
100
+ browser tier asserts what a user does and sees, and the login against Dex in a container (ADR-0078). The login
101
+ scenario is also tagged `@dex`, because it needs docker, which the rest of the tier does not.
@@ -3,7 +3,7 @@
3
3
  `POST /workflows/:name/runs` takes a JSON body and hands it to the machine as xstate `input` — unvalidated, and
4
4
  undescribed: the module contract is `export const machine` alone (ADR-0011/0015), so nothing at runtime says what a run
5
5
  of a given workflow should be started _with_. The Console's start form
6
- ([ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)) forced the question, but the gap predates it —
6
+ ([ADR-0032](0032-the-console-unlocks-with-a-callers-credential.md)) forced the question, but the gap predates it —
7
7
  `jr2 run` has the same blindness, and a typo'd input surfaces as a confusing mid-run failure instead of a refusal at the
8
8
  door. Meanwhile every event a machine _accepts_ already carries a zod schema, attached to the machine itself
9
9
  ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)'s vocabulary-on-the-machine). The input that starts a
@@ -93,8 +93,9 @@ machine is the one piece of its vocabulary that pattern missed.
93
93
  - **The door takes an idempotency key, out of band.** A start may carry one in a header, never in the body (a body field
94
94
  would be one every door had to reserve); a repeat with the same key answers with the run the first one made. A
95
95
  `dispatch` Effect sends its ledger row's id ([ADR-0071](0071-a-run-starts-a-run-through-a-dispatch-effect.md)), so a
96
- start whose answer was lost is sent once where it may have arrived (ADR-0067) and makes one run. `jr2 run` and the
97
- Console send none.
96
+ start whose answer was lost is sent once where it may have arrived (ADR-0067) and makes one run. With a second header,
97
+ a start is a key-only ask: the door answers the run under the key, or that no run has it, and starts nothing.
98
+ `jr2 run` and the Console send none.
98
99
 
99
100
  ## Considered options
100
101
 
@@ -1,11 +1,11 @@
1
1
  # The Console renders with Preact, and the server erases the types
2
2
 
3
- The Console ([ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)) outgrew its framework-free form: `main.js`
4
- reached ~1,500 lines, much of it hand-rolled DOM reconciliation (`memo()`, `paint()`, manual list diffing) — work a view
5
- library exists to own. Three standing constraints shaped the replacement: the repo is zero-build (Node 24 strips types;
6
- nothing compiles), the Console serves only its own assets (ADR-0032), and `@jr2/orchestrator` is a library that serves
7
- its files from disk wherever the process starts. A fourth arrived with this decision: Console source is TypeScript like
8
- everything else.
3
+ The Console ([ADR-0032](0032-the-console-unlocks-with-a-callers-credential.md)) outgrew its framework-free form:
4
+ `main.js` reached ~1,500 lines, much of it hand-rolled DOM reconciliation (`memo()`, `paint()`, manual list diffing) —
5
+ work a view library exists to own. Three standing constraints shaped the replacement: the repo is zero-build (Node 24
6
+ strips types; nothing compiles), the Console serves only its own assets (ADR-0032), and `@jr2/orchestrator` is a library
7
+ that serves its files from disk wherever the process starts. A fourth arrived with this decision: Console source is
8
+ TypeScript like everything else.
9
9
 
10
10
  ## Decision
11
11
 
@@ -81,10 +81,11 @@ every build) and promotes incidental image properties into contract items.
81
81
  because jr2's file supplies one; such an image sets `GIT_CONFIG_SYSTEM=/etc/gitconfig` itself, and jr2 then adds
82
82
  nothing. With the variable set, `git config --system <key> <value>` writes jr2's file on the read-only volume and
83
83
  fails (exit 4); without it, an Agent that runs as root wrote `/etc/gitconfig`. A `--global` or repo-local write works
84
- as before. A human's `kubectl exec` shell does not get the variable, as it does not get the PATH append. jr2's other
85
- git is out of scope: the sync runs under `GIT_CONFIG_NOSYSTEM` (ADR-0067); the Sandbox Effector is another container
86
- with its own committer rules (ADR-0068), and a credentialed `command` Effect runs there with `PATH`, `HOME` and
87
- `TMPDIR` alone, so it gets no default identity.
84
+ as before. A raw `kubectl exec` shell does not get the variable, as it does not get the PATH append;
85
+ `jr2 workspace shell` starts through the spawner, as an Agent's `bash` does (ADR-0009). jr2's other git is out of
86
+ scope: the sync runs under `GIT_CONFIG_NOSYSTEM` (ADR-0067); the Sandbox Effector is another container with its own
87
+ committer rules (ADR-0068), and a credentialed `command` Effect runs there with `PATH`, `HOME` and `TMPDIR` alone, so
88
+ it gets no default identity.
88
89
 
89
90
  - **The contract is the floor, and the preflight proves it.** What is inherent to "jr2's runtime in your filesystem" and
90
91
  nothing more: a **glibc** base no older than the one jr2's node was built against (musl is out entirely), **`git`** on
@@ -47,27 +47,27 @@ because they are one problem: **what a Machine depends on that is not inside it.
47
47
  ADR-0011 adopted for Vocabulary.
48
48
  - **Customization is `customize(machine, parts)`**, a plain function in the family of `workspace()` and `pool()`,
49
49
  returning a plain `StateMachine`. `parts` has the declaration's own shape, recursively partial:
50
- `{ agents?: { <slot>?: Partial<AgentDefinition> }, effects?: { <slot>?: { url } | { argv } | { secret } }, image?, user?, repos?: { <slot>?: Binding }, actors?: { <child>?: parts } }`
50
+ `{ agents?: { <slot>?: Partial<AgentDefinition> }, effects?: { <slot>?: { url } | { argv } | { secret } | { server } | { tool } }, image?, user?, repos?: { <slot>?: Binding }, actors?: { <child>?: parts } }`
51
51
  — the two image seats together, since ADR-0005 gives the User Container the same static option, and the Repo Slots
52
52
  beside them, since ADR-0051 makes a Repo a `workspace()` slot a composer binds. `effects` binds an Effect's Open url,
53
- argv or secret name, once — an Effect with an Open secret and an Open url or argv takes one object with both keys,
54
- `{ url, secret }` or `{ argv, secret }`
55
- ([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)): the slot's logic is built
56
- again with the bound part and substituted as an Agent override is. Internally an Agent override is xstate's
57
- `machine.provide({ actors: { researcher: agent(merged) } })`, with the override layered over the stock definition; a
58
- child override is the same call one level down, recursing — each level is exactly the one-level reach ADR-0015 found
59
- `provide` has, held by the composer who owns the child object, never host-side injection. jr2's wrappers are
60
- transparent: `customize(research, { agents })` reaches the body through the `body` slot (and `pool()`'s `worker`, at
61
- any depth), so a consumer never writes `body`. The images and the Repo bindings are the parts `provide` cannot carry
62
- (xstate copies only implementations, and every part is keyed on the config it passes through by reference), so those
63
- fields clone the wrapper's config and rebuild it with the same implementations, re-attaching what the original
64
- carried. A bound argv's load warnings are such a part too: `customize()` rebuilds the Machine the author wrote the
65
- same way, with the vocabulary, the seats and the door it carried and the warnings the walk gives for the bound argv,
66
- so no attachment is keyed on anything but the config. Only DECLARED parts can be retuned, and the compiler is what
67
- says so: the slots are read off xstate's own `TActor` parameter, so an Agent, a Repo Slot, or a child the Machine does
68
- not carry is a type error (ADR-0050), and the runtime refuses the same call naming the Machine's own slots. Rejected:
69
- a `.with()` method on the machine — it needs a jr2-owned machine type over xstate's, which ADR-0015 avoided, and it
70
- vanishes after any `.provide()`; a callable-machine hybrid — verified to work, reads as a trick.
53
+ argv, MCP server and tool, or secret name, once — an Effect with an Open secret and an Open url or argv takes one
54
+ object with both keys, `{ url, secret }` or `{ argv, secret }`, and an `mcp` Effect with an Open server and tool takes
55
+ `{ server, tool }` ([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)): the
56
+ slot's logic is built again with the bound part and substituted as an Agent override is. Internally an Agent override
57
+ is xstate's `machine.provide({ actors: { researcher: agent(merged) } })`, with the override layered over the stock
58
+ definition; a child override is the same call one level down, recursing — each level is exactly the one-level reach
59
+ ADR-0015 found `provide` has, held by the composer who owns the child object, never host-side injection. jr2's
60
+ wrappers are transparent: `customize(research, { agents })` reaches the body through the `body` slot (and `pool()`'s
61
+ `worker`, at any depth), so a consumer never writes `body`. The images and the Repo bindings are the parts `provide`
62
+ cannot carry (xstate copies only implementations, and every part is keyed on the config it passes through by
63
+ reference), so those fields clone the wrapper's config and rebuild it with the same implementations, re-attaching what
64
+ the original carried. A bound argv's load warnings are such a part too: `customize()` rebuilds the Machine the author
65
+ wrote the same way, with the vocabulary, the seats and the door it carried and the warnings the walk gives for the
66
+ bound argv, so no attachment is keyed on anything but the config. Only DECLARED parts can be retuned, and the compiler
67
+ is what says so: the slots are read off xstate's own `TActor` parameter, so an Agent, a Repo Slot, or a child the
68
+ Machine does not carry is a type error (ADR-0050), and the runtime refuses the same call naming the Machine's own
69
+ slots. Rejected: a `.with()` method on the machine — it needs a jr2-owned machine type over xstate's, which ADR-0015
70
+ avoided, and it vanishes after any `.provide()`; a callable-machine hybrid — verified to work, reads as a trick.
71
71
  - **A package exports a Machine, nothing beside it.** The door (ADR-0033), the vocabulary, the Agents, and the image all
72
72
  ride the exported object. The three uses:
73
73
 
@@ -17,7 +17,12 @@ a definition lives, and the gate that enforces it.
17
17
  an Effect names it with `secret("name")`, and the Instance declares it in the top-level `secrets` list
18
18
  ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). The name is a string, not a type: the
19
19
  walk after the typecheck refuses an Effect that names a secret the list does not hold, before anything is built.
20
- `jr2.config.ts` holds reach and credentials (`harness`, `secrets`, `git.credentials`, `registry`).
20
+ `jr2.config.ts` holds reach and credentials (`harness`, `secrets`, `git.credentials`, `registry`, and the Callers'
21
+ `callers`, `hooks` and `cors`,
22
+ [ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)). A Machine names no Hook: a
23
+ Gate's or a Source's handlers are keyed by the Hook's kind ([ADR-0011](0011-workflow-defined-events.md)), and only a
24
+ Hook's address names it. A Hook kind is code that the config and the Workflows import by name, so it is neither
25
+ declared nor discovered: it comes in by `import`, as a nested Machine does, and `jr2 up` bundles it into the Assembly.
21
26
  - **The slot key types a Repo.** A `workspace()` declares its Repos as slots —
22
27
  `repos: { target: open, docs: { url, ref } }` — and the keys are a phantom on the wrapper type, read through
23
28
  `pool()`'s `worker` and `workspace()`'s `body` as the image is (ADR-0049). A
@@ -31,48 +31,63 @@ the one part ADR-0049/0051 left a package unable to leave unsaid: **the model.**
31
31
  who never reads the file spends money on a model they did not choose, silently, which is the failure class `jr2 up`
32
32
  exists to make loud. Rejected: a Machine factory `task({ model })` — ADR-0049 settled that a package exports a Machine
33
33
  and retunes with `customize`, and a factory reopens that.
34
- - **Open widens to an Effect's url, argv and secret, bound once.** A package cannot know the host its `http` Effect
35
- calls, the program its `command` runs
34
+ - **Open widens to an Effect's url, argv, MCP server and tool, and secret, bound once.** A package cannot know the host
35
+ its `http` Effect calls, the program its `command` runs, the MCP server and tool its `mcp` Effect calls
36
36
  ([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)), or the secret its
37
37
  composer spends, so it may leave each Open: `http({ method, url: open, auth, input, request })`,
38
- `http({ method: "PUT", url: open, body: file(path) })`, `command(open, { env, pin })`, and `secret(open)` in place of
39
- a secret's name. The url, the argv and the secret's name are the Open parts of an Effect that exist; everything else
40
- the definition says is the package's as written — the method, the headers, the auth and the secrets they name, the
41
- input, `request`, the env, `pin` — and is checked when the Machine is built. `secret(open)` is taken by `bearer`,
42
- `basic` (the user or the password), a header's value, and a command's env, as its value or its `.path`; in a url it is
43
- refused when the Machine is built ("leave the whole url Open"), since a url that carries a secret is left Open whole
44
- (below). An Effect takes one Open secret: a second `secret(open)` in one definition is refused when the Machine is
45
- built ("an Effect takes one Open secret"); the one `secret(open)` may stand in several places, a value and its
46
- `.path`. An Effect that names an Open secret is placed as one that names a secret is — a credentialed command in the
47
- Sandbox Effector, an `http` where its body places it — before the name is known. The composer binds the parts with
48
- `customize(machine, { effects: { <slot>: { url } } })`, `{ argv }` or `{ secret }`, and an Effect with an Open url or
49
- argv beside an Open secret takes one object with both keys (`{ url, secret }`, `{ argv, secret }`): a url is a string
50
- or a `text` with a secret in its path, an argv is the usual list, and a secret is a name that obeys `secret()`'s own
51
- rule — it is the secret's file in the Effector's mount (ADR-0068). The type of `effects` is the intersection of the
52
- Open parts' bindings, so a binding that leaves one of them unbound is a compile error, as an unbound url is; at run
53
- time, a binding that leaves one unbound gives an Effect still Open in it, which a later `customize()` may bind. Each
54
- is bound ONCE, to a value: never a mapper over the door, so a host is fixed in the definition or bound before any run,
55
- and no run chooses one, nor a secret. Binding builds the Effect again with its constructor, with the bound name in
56
- every place `secret(open)` stood, so a bound part meets every rule the constructor checks — a literal host, no secret
57
- in a query, a non-empty argv, a secret's name — and the bound Effect is the one the same definition written with that
58
- name gives; `jr2 up` reads it as written: the placement, the secrets it names, and the Effector's fence
59
- ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)) are the bound ones, and the bound secret
60
- is placed only in that Effect's Effector, as a named secret is. Only an Open part is offered: a part the author fixed
61
- is no key of `effects` and is refused at the call, and a part bound once is fixed from then on, so a second composer
62
- cannot move it. `effects` reaches through `workspace()`'s body, `pool()`'s worker and a composed Machine's `actors`,
63
- as `agents` does, typed off the slot's logic. An Open argv's slots are the composer's, so the load walk counts every
64
- pick param as read by it when the Machine is built, and `customize()` runs the walk's pick-param check again once the
65
- argv is bound — a param the bound argv does not read is refused there, and a warning it gives for the bound argv is
66
- the bound Machine's, as it is the same Machine's written with that argv: the bound Machine gets a config of its own,
67
- which carries those warnings ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)), so a copy
68
- `provide()` makes of it — a run's — keeps them, and the Open Machine keeps its own. The compiler's check does not
69
- reach a bound argv: an Open argv's input is one string per key, so nothing at build or at `customize()` checks that
70
- the invoke supplies each slot the bound argv names, and a slot it does not supply faults when the Effect starts — its
71
- state's `onError`, with nothing run. The `jr2 up` walk reports an Open Effect beside Open Repo Slots and Agents and
72
- refuses the converge naming the `effects` line — `{ effects: { <slot>: { secret: "<name>" } } }` for an Open secret,
73
- one object with both keys for an Effect with two Open parts; the Effect refuses to run while Open, as the second fence
74
- — its state's `onError`, with nothing sent and no ledger row. The Open part is the whole url, the whole argv, or the
75
- secret's name.
38
+ `http({ method: "PUT", url: open, body: file(path) })`, `command(open, { env, pin })`,
39
+ `mcp.call(open, open, { input })`, and `secret(open)` in place of a secret's name. The url, the argv, an `mcp`
40
+ Effect's server and tool, and the secret's name are the Open parts of an Effect that exist; everything else the
41
+ definition says is the package's as written — the method, the headers, the auth and the secrets they name, the input,
42
+ `request`, the env, `pin` — and is checked when the Machine is built. `secret(open)` is taken by `bearer`, `basic`
43
+ (the user or the password), a header's value, and a command's env, as its value or its `.path`; in a url it is refused
44
+ when the Machine is built ("leave the whole url Open"), since a url that carries a secret is left Open whole (below).
45
+ An Effect takes one Open secret: a second `secret(open)` in one definition is refused when the Machine is built ("an
46
+ Effect takes one Open secret"); the one `secret(open)` may stand in several places, a value and its `.path`. An Effect
47
+ that names an Open secret is placed as one that names a secret is — a credentialed command in the Sandbox Effector, an
48
+ `http` where its body places it — before the name is known. The composer binds the parts with
49
+ `customize(machine, { effects: { <slot>: { url } } })`, `{ argv }`, `{ secret }`, `{ server }` or `{ tool }`, and an
50
+ Effect with two Open parts takes one object with both keys (`{ url, secret }`, `{ argv, secret }`,
51
+ `{ server, tool }`): a url is a string or a `text` with a secret in its path, an argv is the usual list, and a secret
52
+ is a name that obeys `secret()`'s own rule — it is the secret's file in the Effector's mount (ADR-0068) — and a server
53
+ is a name of the `mcp` block of `jr2.config.ts`, whose secret is the server's own, so an `mcp` Effect takes no
54
+ `secret(open)`
55
+ ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)). The
56
+ type of `effects` is the intersection of the Open parts' bindings, so a binding that leaves one of them unbound is a
57
+ compile error, as an unbound url is; at run time, a binding that leaves one unbound gives an Effect still Open in it,
58
+ which a later `customize()` may bind. Each is bound ONCE, to a value: never a mapper over the door, so a host is fixed
59
+ in the definition or bound before any run, and no run chooses one, nor a secret. Binding builds the Effect again with
60
+ its constructor, with the bound name in every place `secret(open)` stood, so a bound part meets every rule the
61
+ constructor checks — a literal host, no secret in a query, a non-empty argv, a secret's name — and the bound Effect is
62
+ the one the same definition written with that name gives; `jr2 up` reads it as written: the placement, the secrets it
63
+ names, and the Effector's fence ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)) are the
64
+ bound ones, and the bound secret is placed only in that Effect's Effector, as a named secret is. Only an Open part is
65
+ offered: a part the author fixed is no key of `effects` and is refused at the call, and a part bound once is fixed
66
+ from then on, so a second composer cannot move it. `effects` reaches through `workspace()`'s body, `pool()`'s worker
67
+ and a composed Machine's `actors`, as `agents` does, typed off the slot's logic. An Open argv's slots are the
68
+ composer's, so the load walk counts every pick param as read by it when the Machine is built, and `customize()` runs
69
+ the walk's pick-param check again once the argv is bound — a param the bound argv does not read is refused there, and
70
+ a warning it gives for the bound argv is the bound Machine's, as it is the same Machine's written with that argv: the
71
+ bound Machine gets a config of its own, which carries those warnings
72
+ ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)), so a copy `provide()` makes of it — a run's
73
+ — keeps them, and the Open Machine keeps its own. The compiler's check does not reach a bound argv: an Open argv's
74
+ input is one string per key, so nothing at build or at `customize()` checks that the invoke supplies each slot the
75
+ bound argv names, and a slot it does not supply faults when the Effect starts — its state's `onError`, with nothing
76
+ run. The `jr2 up` walk reports an Open Effect beside Open Repo Slots and Agents and refuses the converge naming the
77
+ `effects` line — `{ effects: { <slot>: { secret: "<name>" } } }` for an Open secret, one object with both keys for an
78
+ Effect with two Open parts, `{ effects: { <slot>: { server: "<name>" } } }`, `{ tool: "<tool>" }` or both for an `mcp`
79
+ Effect, with only its Open keys; the Effect refuses to run while Open, as the second fence — its state's `onError`,
80
+ with nothing sent and no ledger row. The Open part is the whole url, the whole argv, an MCP server's name or a tool's
81
+ name, or the secret's name.
82
+ - **Open widens to a hooked Gate's look, which only warns.** A package writes the Hook handlers of its Gate slots for
83
+ each forge that it supports, because a forge's payload is public, so a handler is never Open. It imports the Hook kind
84
+ of each forge that it hears, for the payload types, and it has `@jr2/orchestrator`, which holds the kinds, as a peer
85
+ dependency ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)). Which Hooks of
86
+ those kinds exist is the Instance's config. A look needs a url and a token, so a package leaves `look: open`, and the
87
+ composer binds it once, to a look or `"none"`, with `customize(machine, { gates: { <slot>: { look } } })`, which can
88
+ also add, replace or remove a handler by kind and event key ([ADR-0011](0011-workflow-defined-events.md)). An Open
89
+ look that nobody binds is the one Open part that `jr2 up` does not refuse: it warns, as it warns about a hooked Gate
90
+ with no look, because the Gate still works when a person answers it with `jr2 send`.
76
91
  - **The per-run Dial is the escape hatch, not the default.** A shipped Machine's door may carry `model` and
77
92
  `thinkingLevel` (ADR-0018's two Dials) and fold them into each invoke's input; the bound definition is what `jr2 up`
78
93
  preflights, the door value overrides it for one run's Turns. Nothing in jr2 changes for this — it is the Machine's own
@@ -16,10 +16,10 @@ version, images, gate, trigger.
16
16
  an npmjs credential is the release job, and it holds no token: it publishes with npm trusted publishing (OIDC from
17
17
  GitHub Actions), so nothing long-lived exists to leak. A `pnpm -r publish` on a dev box reaches npmjs and fails
18
18
  `ENEEDAUTH`, which is the guard — an absence that cannot be edited past, where the manifest line was a presence that
19
- had to be. `publishConfig.access: public` stays on all four, because a scoped package's first publish is 402 without
20
- it. `scripts/dist-publish.sh` passes `--registry` again, and `publish.test.ts` inverts: it asserts that **no** public
21
- manifest names a registry, so the old guard cannot be smuggled back in one package and silently pin a release to
22
- localhost.
19
+ had to be. `publishConfig.access: public` stays on every public package, because a scoped package's first publish is
20
+ 402 without it. `scripts/dist-publish.sh` passes `--registry` again, and `publish.test.ts` inverts: it asserts that
21
+ **no** public manifest names a registry, so the old guard cannot be smuggled back in one package and silently pin a
22
+ release to localhost.
23
23
  - **Lockstep, and `main` carries the next version.** One number across `packages/*` and `templates/default`, because
24
24
  [ADR-0019](0019-one-converging-command-against-the-current-context.md)'s one release train already reads a single
25
25
  `KIT_VERSION`, ADR-0043's scaffold pins two packages at one exact number, and `workspace:*` packs to an exact
@@ -50,11 +50,11 @@ version, images, gate, trigger.
50
50
  ([ADR-0072](0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md)). It does not wait for the
51
51
  npm approval, so the site can be ahead of the live packages until the maintainer approves them.
52
52
  - **The job stages; the maintainer approves.** Each trusted publisher is configured for `npm stage publish` only, so the
53
- tag produces four staged tarballs and one 2FA approval per package (`npm stage approve`, dependencies first) makes
54
- them live. This is the one human act a release keeps, and it is kept on purpose: the job's identity is bound to the
55
- repository and the workflow file, so a stolen GitHub session or a bad edit to that file could otherwise publish under
56
- the project's name; staged, it can stage and nothing more. It is not a runbook step — nothing has to be remembered in
57
- order, the staged tarballs wait.
53
+ tag produces one staged tarball per public package and one 2FA approval per package (`npm stage approve`, dependencies
54
+ first) makes them live. This is the one human act a release keeps, and it is kept on purpose: the job's identity is
55
+ bound to the repository and the workflow file, so a stolen GitHub session or a bad edit to that file could otherwise
56
+ publish under the project's name; staged, it can stage and nothing more. It is not a runbook step — nothing has to be
57
+ remembered in order, the staged tarballs wait.
58
58
  - **Two executors of one train.** `scripts/publish.sh` is the irreversible half — images, then packages, dependencies
59
59
  first, live versions skipped — and the job runs it with `--stage`. A maintainer runs it as `just publish` from the
60
60
  pushed tag when a 0.x release cannot wait the job's hour: `npm publish` with a 2FA prompt per package in place of an
@@ -94,14 +94,15 @@ version, images, gate, trigger.
94
94
  configuration" no longer holds; the rest of that list does.
95
95
  - **Bootstrap, once:** create the `jr2` org on npmjs (a scope is a user or an org), publish the first version by hand
96
96
  with a token — npm attaches a trusted publisher only to a package that already exists — then configure the trusted
97
- publisher on all four packages and revoke the token. After that no token that publishes on its own exists anywhere:
98
- the job's identity stages, a maintainer's login needs its second factor. Likewise once: make the three GHCR packages
99
- public after the first push, because GHCR creates them private.
97
+ publisher on each public package and revoke the token. Each new public package needs the same step once: publish its
98
+ first version by hand before the first tag that holds it, or that tag's `npm stage publish` of it fails. After that no
99
+ token that publishes on its own exists anywhere: the job's identity stages, a maintainer's login needs its second
100
+ factor. Likewise once: make the three GHCR packages public after the first push, because GHCR creates them private.
100
101
  - `publish.test.ts` asserts no registry, `access: public`, and `engines`; `dist-publish.sh` drops its manifest read-back
101
102
  and passes `--registry`; the `@dist` fixture takes its registry from `JR2_DIST_PORT`'s default rather than the
102
103
  manifest; `kit-push.sh`'s cli-vs-orchestrator cross-check widens to every manifest; `just release` is new;
103
104
  `.github/workflows/` gains `ci.yml` and `release.yml`.
104
105
  - A release costs a runner kind cluster and roughly ten minutes of docker. Accepted: it is the only time the installed
105
106
  path must be proven on the exact tree that ships.
106
- - The four tarballs gain the npm-page fields (`description`, `repository` with `directory`, `homepage`, `bugs`,
107
+ - The public tarballs gain the npm-page fields (`description`, `repository` with `directory`, `homepage`, `bugs`,
107
108
  `keywords`) and a README each; the root README's "only scaffolding exists" line goes.
@@ -52,6 +52,13 @@ ADR-0018 and GLOSSARY.md said `cwd` is identity, never per Turn. The decided thi
52
52
  - **Fan-out is safe by construction.** A Pool spawns each worker with the item id as its actor id (pool.ts), so three
53
53
  `task` children each mint their own `…/backlog.worker.<item>/coder`. The packaged Machine never learns it is under a
54
54
  Pool.
55
+ - **Each segment of the actor path is encoded, then the segments are joined with `.`.** An actor id can be any text
56
+ (an item id is the source's), and with plain `.` joints the paths `a.b`, `c` and `a`, `b.c` were one conversation. A
57
+ letter, a digit, `-` and `_` stay as they are; each other character becomes `~` and two upper-case hex digits for
58
+ each of its UTF-8 bytes (`F.12` is `F~2E12`). So a segment has no `.` and no `/`, the iid's two joints, and nothing
59
+ that a URL path or a header must escape. The root Machine's path is `root`. Two segments are escaped although each
60
+ of their characters may stay: `root` is `~72oot`, and the empty id is `~`. So no other path is `root` or empty, and
61
+ two paths never have one spelling.
55
62
  - **States of one Machine share by saying `continue`.** States differ by Menu — a coder in `implement` offers
56
63
  `finish`, the same coder in `fix` offers `finish` and `dispute` — and the same conversation is wanted across them. A
57
64
  state that wants a clean context leaves `continue` off. A Turn joins only a conversation that an earlier Turn
@@ -42,7 +42,7 @@ and we add an ingress NetworkPolicy that admits only the Orchestrator to a Harne
42
42
  bearer. A 401 comes before a 404, so a refused caller does not learn which conversations exist. A refused admission
43
43
  creates no conversation. The Harness requires `JR2_HARNESS_TOKEN_SHA256` at boot and fails loudly without it.
44
44
  - **The echo uses the same bearer.** `JR2_ECHO_TOKEN_SHA256` is removed. The Instance token no longer enters any pod,
45
- which is what [ADR-0032](0032-the-console-unlocks-with-the-instance-token.md) and
45
+ which is what [ADR-0032](0032-the-console-unlocks-with-a-callers-credential.md) and
46
46
  [Secrets and trust](../../concepts/secrets-and-trust.md) say.
47
47
  - **A refused stream is a fault, not a reconnect.** The bearer is derived, so a Harness that refuses it once refuses it
48
48
  on every retry. `wait` settles a 401 or 403 as a `SettlementFault` and does not park the Turn forever.
@@ -52,9 +52,11 @@ and we add an ingress NetworkPolicy that admits only the Orchestrator to a Harne
52
52
  Orchestrator pods (`app: jr2-orchestrator`, `jr2.dev/instance: <name>`). They are converged unconditionally, so a
53
53
  Harness pod is never created before its policy exists. The Instance Effector gets a third of the same form,
54
54
  `jr2-instance-effector`, applied with it in one List (ADR-0068).
55
- - **Starting a run needs the Instance token.** `POST /workflows/:name/runs` moves from `authenticated` to
56
- `instanceOnly`. The scope of a Sandbox token is its own agent surface. The Custodian's route allowlist does not
57
- forward this route, but that is not a control.
55
+ - **Starting a run needs a Caller with `start`.** `POST /workflows/:name/runs` moves from `authenticated` to the guarded
56
+ band, where a Caller's `start` scope opens it
57
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)). A Sandbox token is no
58
+ Caller: its scope is its own agent surface. The Custodian's route allowlist does not forward this route, but that is
59
+ not a control.
58
60
 
59
61
  ## Considered options
60
62
 
@@ -84,10 +86,14 @@ and we add an ingress NetworkPolicy that admits only the Orchestrator to a Harne
84
86
  transport failure, not a status, because a 401 would mean the bearer stopped it and the policy did not.
85
87
  - **Port-forward and probes are not affected.** `kubectl port-forward` enters the pod's network namespace through the
86
88
  kubelet, and the kubelet's probes come from the node. On kind, neither is filtered by an ingress policy. So the CLI,
87
- the Console, and a human's shell into the User Container ([ADR-0005](0005-sandbox-pod-composition.md)) keep working.
88
- If routable access to the User Container is added later (ADR-0005 leaves it open), it must add its own ingress rule.
89
+ the Console, `jr2 workspace forward` and `jr2 workspace shell --user` into the User Container
90
+ ([ADR-0005](0005-sandbox-pod-composition.md), [ADR-0009](0009-cli-and-instance-interface.md)) keep working: they reach
91
+ the pod by port-forward and exec through the kube API, and no Service routes to the User Container. If routable access
92
+ to the User Container is added later (ADR-0005 leaves it open), it must add its own ingress rule.
89
93
  - **The `@kind` history reads present the bearer.** The steps derive it from the instance Secret's signing key, as the
90
- CLI reads the Instance token: over the kube API, with RBAC as the gate. A human reading a conversation does the same.
94
+ CLI reads the Instance token: over the kube API, with RBAC as the gate. A human reads a conversation from the
95
+ Orchestrator's record with `jr2 logs`, behind `read` ([ADR-0023](0023-the-harness-prints-the-conversation.md)), and
96
+ needs no bearer of a Harness.
91
97
  - **Model keys are not in the Harness container.** `JR2_PROVIDER_API_KEY` and every other held secret live in the
92
98
  Custodian, and the Harness container's env holds their Stand-ins (ADR-0059). `jr2 up` refuses a known model key in
93
99
  `harness.env` or in an `envFrom` Secret.