@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,285 @@
1
+ # A Caller comes from one authenticator seam and acts within its scopes
2
+
3
+ The Instance token is the operator's credential for everything that acts on an Instance from outside its Machines. The
4
+ CLI reads it from its Secret over the kube API, so kube RBAC is the gate
5
+ ([ADR-0019](0019-one-converging-command-against-the-current-context.md)). That serves the person who runs `jr2 up`. It
6
+ does not serve the other Callers of the v1 target user: a CI job that starts runs, a bridge to a chat service that
7
+ answers Gates, a forge that sends deliveries, and a teammate with a browser and no cluster access. Each needs less than
8
+ the Instance token, and with one token each holds all of it. A run must also record who acted on it. So this decision
9
+ gives jr2 a way to know a Caller with no kube credentials, a scope for each act, and a record of each act.
10
+
11
+ ## Decision
12
+
13
+ - **One seam: an ordered list of authenticators.** Each authenticator takes the credential of a request and gives one of
14
+ two answers: a **Caller** `{ via, name, may, onBehalfOf? }`, or "not mine". The first authenticator that claims the
15
+ credential decides. A credential that an authenticator claims and then finds bad is 401; it never falls through to the
16
+ next one. A credential that no authenticator claims is 401. `via` names the authenticator (`token`, `jwt`, `hook`),
17
+ `name` is the Caller's name for that authenticator, `may` is its scopes (below), and `onBehalfOf` is the Caller's
18
+ claim about the party it acts for. The principals inside the Orchestrator stay outside the seam: an Agent's control
19
+ token, a Sandbox's fetch token and the publish token
20
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)) are not Callers, and no
21
+ authenticator makes a Caller from one. So a Sandbox token still reaches no Gate (ADR-0013).
22
+ - **The Instance token is one authenticator.** It gives the Caller `{ via: "token", name: "instance" }`, with every
23
+ scope for every Workflow. Its meaning does not change: it is the operator's credential.
24
+ - **Named Caller tokens.** The Instance declares them in the `callers` block of `jr2.config.ts`, each with a name and
25
+ its scopes (`callers.tokens.<name>.may`). A name is a DNS label of at most 40 characters, because it is part of the
26
+ name of its Secret, and `instance` is the Instance token's name, so the config load refuses it. `jr2 up` makes the
27
+ value of each one, once, in a Secret of its own, and gives the Orchestrator only the sha-256 of each value, in hex. A
28
+ value is `jr2c_` and 32 random bytes in base64url. The prefix lets the seam say "not mine" with no lookup, and lets a
29
+ secret scanner find a value that leaked. The digests go in one Secret, `jr2-caller-digests`, as one JSON object from
30
+ name to digest, and the Orchestrator reads it through a mount. It reads the file again when it changes, so a new token
31
+ or a rotation needs no restart. The kubelet updates a mounted Secret on its own period, about a minute. Thus
32
+ `GET /assembly` reports a digest of the file that the Orchestrator holds now, beside the digests of its mounted
33
+ ConfigMaps, and `jr2 up` and `jr2 caller rotate` wait until it is the file that they applied: when `jr2 up` says
34
+ `converged`, a new token works, and when `rotate` returns, the old value no longer works. After 150 s, each stops with
35
+ exit code 1 and says that the change does not work yet. The scopes come from the config of the current Assembly: a
36
+ name that has a digest and no entry in the config opens nothing, and a file that does not parse keeps the digests that
37
+ the Orchestrator read last. `jr2 caller token <name>` prints the value, which it reads from the Secret over the kube
38
+ API (RBAC is the gate, as for the Instance token), so it takes `-n` and `--context` and never `--url`.
39
+ `jr2 caller rotate <name>` makes a new value, and the old value stops working. It writes the value's Secret first and
40
+ the digests after, from every token Secret of the namespace, so a rotate that fails between the two runs again and
41
+ loses no value. A name that the config no longer declares stops working at the next `jr2 up`, which deletes its
42
+ Secret. Each token Secret carries its name in the label `jr2.dev/caller-token`, which is how `jr2 up` finds the
43
+ undeclared ones.
44
+ - **JWTs from an identity provider.** The `callers` block also declares issuers
45
+ (`callers.issuers: [{ issuer, audience, jwks?, claims?, groups, login? }]`). The JWT authenticator claims a bearer
46
+ that is a JWT whose `iss` is the `issuer` of an entry; a JWT of another issuer is not its. For each issuer, jr2 checks
47
+ a JWT's signature against the issuer's published keys, and its issuer, audience and expiry, with 30 seconds for the
48
+ clocks to differ. It takes only asymmetric signatures, because a published key must not be able to sign. The keys come
49
+ from `jwks`, or else from the `jwks_uri` of the issuer's `/.well-known/openid-configuration`. The Orchestrator keeps
50
+ them for 10 minutes, and for a JWT with a key that it does not know it fetches them again at once, at most once in 30
51
+ seconds, so a key that the issuer rotated works with no restart. When the fetch of the discovery document fails, the
52
+ Orchestrator keeps the failure for 30 seconds, so many JWTs of that issuer cause at most one fetch in 30 seconds. It
53
+ keeps keys only for the issuers that the current config names. The Caller's `name` is the `sub` claim and the name
54
+ that jr2 shows is the `email` claim; `claims` names other claims for each. The scopes come from the values of one
55
+ claim (a list of groups or roles, `groups` by default), through the map `groups` from a value to scopes; a JWT with
56
+ more than one value gets the scopes of each, joined. A JWT that gets no scope from the map is 403 on every route that
57
+ needs a scope. jr2 never treats it as read-only. `GET /whoami` answers it, with no scope, so that a person can see why
58
+ the other routes refuse it.
59
+ - **Login in the Console's browser.** The Console logs a person in against a JWT issuer with `login: true`, with OIDC
60
+ (the authorization code flow with PKCE, as a public client whose client id is the issuer's `audience`), and sends the
61
+ ID token that it gets as the bearer. So login adds no authenticator: it is a way to get a JWT credential. The Console
62
+ keeps the JWT as it keeps a typed token ([ADR-0032](0032-the-console-unlocks-with-a-callers-credential.md)). A login
63
+ that jr2 hosts on the server side, with a session, comes later through the seam.
64
+ - **Five scopes, each one for every Workflow or for named Workflows.** `start` starts a run, `answer` answers a Gate,
65
+ `cancel` cancels a run, `read` reads a run's state, and `steer` sends a Steering message
66
+ ([ADR-0002](0002-duplex-streaming-actor.md)). A scope's value is `"all"` or a list of Workflow names. A Chart
67
+ wrapper's name in the list also opens each named Chart of that wrapper (`plan` opens `plan/weekly`), because a named
68
+ Chart runs its wrapper's Machine ([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)); the name `plan/weekly`
69
+ opens that Chart alone. For a route that addresses a run, jr2 checks the scope against that run's Workflow. Which
70
+ scope opens which route is [ADR-0014](0014-observation-is-open-run-state-is-not.md)'s table. A 403 names the scope and
71
+ the Workflow that the request needed. A Caller with the scope for no Workflow at all is refused before the Workflow is
72
+ read, and the 403 names the scope.
73
+ - **`answer` can name single Gates.** Its list takes an item `{ workflow, gates: [<name>] }` beside a bare Workflow
74
+ name, which still means every Gate of that Workflow: `answer: ["triage", { workflow: "deploy", gates: ["approve"] }]`.
75
+ The same form is a value of the `groups` map, so a team's group answers its own Gate of a Workflow and not the others.
76
+ A Hook's `may.answer` stays per Workflow: the Gate slot's handlers already say which Gates hear its kind. A Gate's
77
+ name is its invoke id, its static name in the Machine that invokes it ([ADR-0011](0011-workflow-defined-events.md)).
78
+ One name covers every Gate of the Workflow with that id, and `jr2 up` refuses a name that the Workflow's Machines do
79
+ not have, so a typo fails at the door and not at the first answer, and a name that matches Gates in more than one
80
+ Machine of the Workflow, so one name never opens two Gates without the author's word. An item for a Chart wrapper,
81
+ `{ workflow: "plan", gates }`, opens those Gates in each named Chart of `plan` too, and `jr2 up` checks its names
82
+ against the wrapper's Machine and each named Chart of the Assembly; an item for `plan/weekly` opens and checks that
83
+ Chart alone. A runtime Chart's Gates do not exist at `jr2 up`, so `jr2 up` cannot check them: a name that no Machine
84
+ and no Chart file of the wrapper has, and an item for a Chart that is no Chart file, are warnings and not refusals,
85
+ and at run time the item opens the Gates of that name in the runtime Charts too. `GET /gates`, `GET /runs/:id` and the
86
+ 404 of a gate POST list only the Gates that the Caller may answer. A gate POST to an open Gate that the Caller may not
87
+ answer is a 403 that names the scope, the Workflow and the Gate. The check and the delivery are one step, so a Gate
88
+ that opens while the request is in flight is checked too. So a 403 and a 404 tell a Caller which Gate is open. That is
89
+ not a secret: which state a run is in is observation, and the open run projections show it
90
+ ([ADR-0014](0014-observation-is-open-run-state-is-not.md)). The other four scopes stay per Workflow: each of them acts
91
+ on a whole run.
92
+ - **The CLI is the Caller `token/instance`.** It reads the Instance token as before, and it sends `onBehalfOf`: the user
93
+ name that a kube `SelfSubjectReview` gives for the current context. It asks for the name beside the port-forward, and
94
+ a server that does not answer gives no claim and no failure. A name that is not printable ASCII is not sent, because a
95
+ header cannot carry it, and a longer name is cut to the bound. A Caller sends `onBehalfOf` in the header
96
+ `jr2-on-behalf-of`, at most 256 characters. A Hook's `onBehalfOf` comes from its signed payload, and the door ignores
97
+ the header for a Hook. With `--url` and `JR2_TOKEN`, the CLI sends any Caller's credential, and it is that Caller.
98
+ - **`GET /whoami` and `jr2 whoami` give the request's Caller record**: `via`, `name`, the name that jr2 shows, `may` and
99
+ `onBehalfOf`. A person uses it to see what a credential may do before it fails.
100
+ - **`cors` is an allowlist of origins in `jr2.config.ts`, empty by default.** The Console is served from the
101
+ Orchestrator's own origin and needs no entry. A browser app on another origin needs its origin in the list. Each entry
102
+ is an origin alone (`https://ops.example.com`, no path, no pattern), and the config load refuses any other text. The
103
+ Orchestrator reads the list of the current Assembly on each request.
104
+ - **The run records the Caller of each act on a guarded route.** A Caller in a record is
105
+ `{ via, name, shown?, onBehalfOf? }`: never its scopes. The run's record holds `startedBy`, which is
106
+ `{ kind: "caller", via, name, shown?, onBehalfOf? }`; `{ kind: "run", run, slot }` for a run that a local `dispatch`
107
+ started (ADR-0071); or `{ kind: "jr2", why }` for a standing run, which has no Caller because jr2 starts it, at
108
+ `boot`, at a `publish`, at a `hand-over` or at a `resume` ([ADR-0017](0017-pool-over-a-source-port.md)). A resume also
109
+ records the Caller of the `jr2 run` that resumed the run, as `resumedBy`. A run that a foreign `dispatch` started
110
+ records the Caller whose credential the other Instance sent. The record also holds `cancelledBy`, and `answers`: the
111
+ last 100 answers to the run's Gates, each `{ gate, type, at, answeredBy }`. It keeps the last 100 because a standing
112
+ run's Gate can take an answer for each delivery, and the record is saved on each transition. A gate event's Sender
113
+ carries the Caller too (`via`, `name`, `onBehalfOf`), and `checked` names which of its fields jr2 checked
114
+ ([ADR-0011](0011-workflow-defined-events.md)). `jr2 status` walks the `startedBy` chain to the first Caller and prints
115
+ it, and the Console shows it. The walk reads one run at a time, as the Caller that asks, and names the runs between; a
116
+ run it may not read, or that is no longer known, ends the walk, and the line says so. It reads at most 16 runs. The
117
+ record is on the guarded routes only, never on the open observation projection (ADR-0014).
118
+ - **`onBehalfOf` is a claim.** jr2 records it and shows it, and it never checks it. The one exception is a Hook (below):
119
+ there the signature covers the payload that `onBehalfOf` comes from.
120
+ - **A Caller's scope does not limit what its run dispatches.** A Caller with `start` on `triage` starts a run whose
121
+ Machine can dispatch `deploy`. The Machine is the author's, and it is the boundary of what its run does, as it is for
122
+ every Effect. The docs say this where they describe scopes.
123
+ - **A Hook is a Caller with `via: "hook"`.** The `hooks` block of `jr2.config.ts` declares each Hook by its name (the
124
+ last segment of its address) and its Hook kind, which the config imports. A Hook's name has the form of a Caller
125
+ token's name. The config load refuses another name, a value that no kind's constructor made, and two kinds with one
126
+ id. The kind's constructor takes only the credential and the Workflows whose Gates the Hook may answer (`may.answer`,
127
+ every Workflow by default): `github({ secret: secret("GH_HOOK"), may })`,
128
+ `slack({ secret: secret("SLACK_SIGNING"), may })`, `discord({ publicKey: "…", may })`. A Hook has the `answer` scope
129
+ only. A Hook never starts a run. To bring items to a Pool, it needs `answer` on the Pool's Workflow, because it
130
+ reaches the Pool's `source` Gate ([ADR-0017](0017-pool-over-a-source-port.md)). The Hook's route has its own
131
+ authenticator, its verifier, which gives `{ via: "hook", name, may: { answer }, onBehalfOf }`.
132
+ - **A Hook kind owns the service's protocol.** The kind gives what the service signed, the handshake, the decoding of
133
+ the body (Slack sends a form), a zod schema for each event key, the event key, the delivery id, `onBehalfOf` and the
134
+ envelope of a text that a person reads. The author writes none of them. Each is a fact of the service, the same for
135
+ every Instance. jr2 ships a kind for each service that has a handshake, an envelope or a scheme of its own:
136
+ - `github`: the event key is the event name and its action (`pull_request_review.submitted`), the id is
137
+ `X-GitHub-Delivery`, and `onBehalfOf` is `sender.login`.
138
+ - `gitlab`: the `token` verifier on `X-Gitlab-Token`. The event key is `object_kind` and the noteable type for a
139
+ `note`, else the action or the status (`merge_request.approved`, `note.MergeRequest`, `pipeline.success`), the id is
140
+ `Idempotency-Key` (else `X-Gitlab-Event-UUID`), and `onBehalfOf` is `user.username`.
141
+ - `slack`: the event keys are `slash_command./jr2` (the command is in the key, so a Source for `/jr2` never hears
142
+ `/deploy`), `block_actions.<action_id>` and `event_callback.<event type>`. The id is `event_id` for an event and
143
+ `trigger_id` for a command or a click. `onBehalfOf` is the Slack user id, never a display name. The handshake
144
+ answers `url_verification` with its `challenge`.
145
+ - `discord`: the event keys are `application_command.<name>` and `message_component`, the id is the interaction's
146
+ `id`, and `onBehalfOf` is `member.user.id`, or `user.id` in a direct message. The handshake answers `PING` with
147
+ `PONG`. Another type of interaction (an autocomplete, a modal) gets the key `interaction.<type>`, which has no
148
+ schema, so it reaches nothing.
149
+
150
+ `onBehalfOf` comes from the verified body, so the signature covers it. The handshake, the parse and the envelope are
151
+ [ADR-0011](0011-workflow-defined-events.md)'s door.
152
+
153
+ - **`webhook()` makes a kind from config data, for a service that has no kind.** Its config gives the verifier (`mac`
154
+ over the raw body, with the header, its prefix and the algorithm; or `token` in a header), the event name, the
155
+ delivery id and `onBehalfOf` (each by a header or a JSON path), and a zod schema for each event name. It has no
156
+ handshake, and its answer is the outcome JSON. It makes a kind, not a Hook: the Instance writes it in a module of its
157
+ own, which the config imports for its Hooks and the Workflows import for their handlers. A service that needs a
158
+ handshake, a body that is not JSON, a signature over more than the body, a signed timestamp or an answer that a person
159
+ reads needs a kind module. Sentry, the alert service of the standing Pool
160
+ ([ADR-0017](0017-pool-over-a-source-port.md)), fits `webhook()` or a kind from another package.
161
+
162
+ - **A Hook kind is a module of the `@jr2/orchestrator/hooks` interface, from any package or a local file.**
163
+ `jr2.config.ts` declares it by `import`, and `jr2 up` bundles it into the Assembly
164
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). jr2 loads no kind at run
165
+ time and discovers none from a folder. jr2's own kinds (`@jr2/orchestrator/hooks/github` and the others) use the same
166
+ interface, and nothing more. The interface and the kinds are subpath exports of `@jr2/orchestrator`, so the
167
+ Orchestrator Kit image holds their one copy, and each kind in an Assembly uses that copy. The door reads a Hook and a
168
+ kind through a module that the package does not export, because it is not part of a kind's interface. It sits beside
169
+ the module that makes a Hook, because that module decides the shape of a Hook.
170
+ - **A kind never gets the credential.** jr2 makes the kind's constructor, so the credential goes from the config to
171
+ the door and never through code that the kind wrote. For `token`, the door removes the header before any code of the
172
+ kind runs.
173
+ - **A kind is pure and synchronous**: no I/O, no Promise, no clock. It is trusted code in the Orchestrator, as is
174
+ every package that a Workflow imports. Its types refuse a Promise, but they cannot stop I/O. The firm guarantee is
175
+ that no kind holds a `mac` secret, because the Orchestrator holds none
176
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)).
177
+ - **A kind's id is short** (`sentry`): lowercase letters, digits and `-`, and it starts with a letter. One id is one
178
+ kind in an Assembly. Two kinds with one id fail the typecheck and `jr2 up`. `defineHookKind` refuses an id of
179
+ another form, an `api` that it does not know, and a `posts` entry that is not an `https://` url that ends in `/`. A
180
+ kind's constructor refuses a credential of the wrong form: a `publicKey` that is not 64 hex characters, or a
181
+ `secret` that is not a named secret.
182
+ - **A follow-up is data.** A kind's answer to a person can give one POST for the door to make after the answer, with
183
+ its url and its JSON (the reply to a Slack click). The kind declares the hosts of its posts, and `jr2 up` adds them
184
+ to the Instance Effector's fence (ADR-0068).
185
+ - **The interface is a public surface of v1.** A kind states `api: 1`, and a kind's package has a peer dependency on
186
+ `@jr2/orchestrator`. The `api` is the contract, not the package version: the door refuses a kind whose `api` it does
187
+ not know. Within `api: 1`, a new field of the interface is optional. A change that breaks a kind is `api: 2`.
188
+
189
+ - **Three verifiers, a closed set, and the Effector knows no Hook kind.** A kind chooses one, and gives the door what to
190
+ verify: the bytes that the service signed, the signature, and the signed timestamp `at` when the signature covers one.
191
+ The door runs the verifier:
192
+ - `mac`: the kind gives the bytes (GitHub: the raw body; Slack: `v0:{timestamp}:{body}`), and the door makes every
193
+ check that needs no secret, such as the window on `at`. Then the Instance Effector computes an HMAC over those bytes
194
+ with the named secret and algorithm, compares it with the signature in constant time, and answers only "match" or
195
+ "no match" ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). A secret that verifies a
196
+ delivery can also forge one, so the Orchestrator never holds it.
197
+ - `token`: the service sends a shared token in a header (GitLab). The door, in the Orchestrator, compares its sha-256
198
+ with the digest that `jr2 up` gave it, and then removes the header. The digests go in one Secret,
199
+ `jr2-hook-digests`, as one JSON object from the secret's name to its digest. It is not `jr2-caller-digests`, because
200
+ `jr2 caller rotate` writes that Secret whole. The Orchestrator reads it through a mount, and again when it changes,
201
+ and `jr2 up` waits for it, as for the Caller digests. `jr2 up` applies it at each converge, empty when no Hook is
202
+ `token`. It computes each digest on the host, from the entry of the `secrets` list: a literal, or the key of the
203
+ user's Secret with one trailing newline removed, as the Effector reads a value. The value goes into no object that
204
+ `jr2 up` writes.
205
+ - `ed25519`: the door, in the Orchestrator, checks the signature over the timestamp and the body (Discord) against the
206
+ application's public key. A public key can verify a signature and cannot make one, so it is not a secret, and the
207
+ config holds it as a literal.
208
+
209
+ **One window: 300 seconds, for every signed timestamp.** The door refuses a delivery whose `at` is more than 300
210
+ seconds from its clock. A kind gives the timestamp and never the window, and no config changes it. Slack asks for 5
211
+ minutes. Discord states no window, and its signature covers its timestamp, so the same window applies. The window
212
+ closes a replay after the hook log forgets the delivery, `runs.retain` later
213
+ ([ADR-0065](0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md)). A failed check, or a timestamp
214
+ out of the window, is 401 (Discord sends bad signatures on purpose and expects 401). A new scheme is a change to the
215
+ set. What a delivery does after the verifier is ADR-0011's: the handshake, the parse, the route, the repeat check, the
216
+ fan-in and the answer.
217
+
218
+ - **Tests.** The unit tier checks the JWT authenticator against an in-process issuer made with `jose`: a good token, a
219
+ wrong issuer or audience, an expired token, a key that rotated, and a token with no scope. The `@console` tier checks
220
+ the browser login against Dex in a container. Before each release, a person checks one login against a real provider
221
+ by hand. The default tier checks the `token` and `ed25519` verifiers end to end, with a key pair that the test makes,
222
+ and the window, the handshakes, the decoding, the parse and the envelopes of each kind. It has no Instance Effector,
223
+ so a `mac` check runs there only through the injected transport (the bytes the Orchestrator builds, and its window);
224
+ the Effector's Go tests check the comparison, and the `@kind` tier checks a `mac` Hook end to end through the Instance
225
+ Effector ([ADR-0011](0011-workflow-defined-events.md)). The build must check what the services' docs do not settle:
226
+ whether Slack signs `url_verification` (the design verifies it first), whether `trigger_id` is unique for each request
227
+ (the body's digest catches a repeat anyway), whether Discord sends an interaction again, whether a Discord follow-up
228
+ needs a bot token (the design does not use one), and Slack's limit on the length of a text. The `slack` kind gives the
229
+ door a limit of 4000 characters, the length that Slack's docs advise for a message's `text`; no one checked the limit
230
+ of the answer to a command or of a post to a `response_url`. GitLab's `Idempotency-Key` is from GitLab's docs, and no
231
+ one checked it against a real delivery.
232
+
233
+ ## Considered options
234
+
235
+ - **One Instance token for every Caller**, the state before this decision. Rejected: a CI job or a bridge holds every
236
+ act on every run, and the run cannot say who acted.
237
+ - **Headers from an authenticating proxy** (`X-Forwarded-User` from a proxy in front of the Orchestrator). Not refused:
238
+ it adds later as an authenticator. Not now, because jr2 can trust the header only when nothing but the proxy reaches
239
+ the Orchestrator, and the v1 target user has no such proxy.
240
+ - **A kube TokenReview of the bearer.** Not refused: it adds later as an authenticator. Not now, because a Caller with
241
+ kube credentials already has the CLI, and kube RBAC gives jr2 no scope to map.
242
+ - **A login that jr2 hosts on the server side**, with a session cookie. Not refused: it adds later through the seam. Not
243
+ now, because it brings a cookie, CSRF and a session store. Browser login with a JWT gives the Console a login without
244
+ them.
245
+ - **Other ways to read a JWT.** The `email` claim as the Caller's name: rejected, because a provider can change or
246
+ reassign an email, and `sub` is stable for each issuer. Scopes from several claims, or from an expression over claims:
247
+ rejected, because the groups or roles claim of a provider is one list. A JWT with no scope as `read`: rejected,
248
+ because a default grant opens run state to every account of the provider. Opaque tokens with introspection: rejected,
249
+ because each request then calls the provider, in a way that is different for each provider.
250
+ - **The token's value in the Orchestrator.** Rejected: the Orchestrator keeps only a digest, as it does for a Harness
251
+ token ([ADR-0058](0058-a-harness-answers-the-orchestrator-alone.md)). A digest that leaks starts nothing.
252
+ - **A Caller's scope as a limit on its run's dispatches.** Rejected: a run's Effects are the author's Machine, not the
253
+ Caller's. A limit there makes the same Machine do different things for different Callers.
254
+ - **`answer` for whole Workflows only.** Rejected: one Workflow can wait on Gates that different people answer (a team's
255
+ review, a release manager's approval), and a scope for the whole Workflow lets each of them answer the other's Gate.
256
+ - **A scope for each route.** Rejected: the five scopes are the five acts of a Caller. A finer scope names a route,
257
+ which can change, and not an act.
258
+ - **`may.start` on a Hook**, so that a Hook can start runs of a Workflow. Rejected: a Hook never starts a run
259
+ (ADR-0017). The authority to start is the `dispatch` slot in the author's code, as for any dispatch, and the Hook only
260
+ answers the Pool's Gate.
261
+ - **The protocol in the author's config, for a service that jr2 ships a kind for**: the config says where a delivery
262
+ holds its id, its event name and `onBehalfOf`, and which header holds the signature. Rejected: each of these is a fact
263
+ of the service, the same for every Instance, so each author writes the service's protocol again; and a handshake or an
264
+ envelope is not a field. For a service with no kind, this is `webhook()` (above).
265
+ - **A verifier for each service in the Instance Effector** (a `slack` scheme that knows `v0:`, the header names and the
266
+ window). Rejected: every new service is then a change to the Go binary that is the trust boundary, and nothing in the
267
+ bytes to check or in the window is secret. The Effector needs only the one operation that needs the secret: an HMAC
268
+ over given bytes.
269
+ - **A package of its own for the interface and the kinds** (`@jr2/hooks`). Rejected: the Kit releases every package at
270
+ one version, so a separate package gives the interface no version line of its own, and `api` already is its contract.
271
+ A kind's package has a peer dependency on the kit either way, because every Instance that loads a kind has
272
+ `@jr2/orchestrator`. A separate package only adds a public package to publish, pin and stage at each release.
273
+
274
+ ## Consequences
275
+
276
+ - ADR-0013's open question, the authentication of a Caller without kube credentials, is closed: a named token, a JWT, or
277
+ a browser login.
278
+ - [ADR-0014](0014-observation-is-open-run-state-is-not.md)'s guarded band becomes a scope check for each route.
279
+ [ADR-0032](0032-the-console-unlocks-with-a-callers-credential.md)'s Console takes any Caller's credential, and shows
280
+ only what its scopes allow. ADR-0071's foreign dispatch takes any token with `start` for that Workflow.
281
+ - `@jr2/orchestrator` gains the subpath exports `@jr2/orchestrator/hooks` (the interface of a Hook kind) and
282
+ `@jr2/orchestrator/hooks/github`, `/gitlab`, `/slack`, `/discord` and `/webhook` (jr2's kinds and `webhook()`).
283
+ - `jr2.config.ts` gains `callers`, `hooks` and `cors`. The CLI gains `jr2 caller token|rotate <name>` and `jr2 whoami`
284
+ ([ADR-0009](0009-cli-and-instance-interface.md)).
285
+ - GLOSSARY.md: **Caller** names the Steering message and the Hook. **Hook** and **Hook kind** are new terms.
@@ -0,0 +1,198 @@
1
+ # An Agent holds MCP tools, a Machine calls them as an Effect, and the Machine is the approval
2
+
3
+ An Agent of the v1 target user works with services: it reads a Jira ticket, posts to Slack, opens a GitHub issue. Before
4
+ this decision an Agent had two kinds of tool, its Working tools and its Menu, and no third
5
+ ([ADR-0028](0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md)). It could reach a service only
6
+ from Workspace code, with `curl` and a Held secret that its `workspace()` grants, or through a Machine's `http` Effect.
7
+ The first gives the Agent the whole credential: Workspace code can spend it on every API that the token opens. The
8
+ second makes the author write each call of the service again as an Effect. The services ship MCP servers, and each agent
9
+ framework that we read speaks MCP. So jr2 takes MCP as the one contract for tools beyond its own.
10
+
11
+ Each of those frameworks (Claude Code, the OpenAI Agents SDK, LangGraph, Mastra, the Vercel AI SDK) puts an approval in
12
+ the harness: a mode or a callback that stops the loop before a tool call and asks a person. jr2 has an approval already.
13
+ A Machine's Gate stops the run, keeps it across a restart, names who answered
14
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)) and routes on the answer. A
15
+ second approval in the Harness would be a state that the Machine cannot see.
16
+
17
+ ## Decision
18
+
19
+ - **Two ways to use an MCP server, both in v1.** An Agent holds **MCP tools**: its definition names a server, and the
20
+ server's tools join the Agent's tools. The Harness is the MCP client, and the server's credential is a Held secret
21
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). A Machine calls a server as an
22
+ Effect: the `mcp` Effect runs in the Instance Effector, with an Effect secret and no model
23
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)).
24
+ - **The Machine is the approval.** An Agent's MCP tool runs when the model calls it, within its allowlist (below). The
25
+ Harness has no `requireApproval`, no ask mode and no callback. A call that needs approval is the Machine's: the Agent
26
+ makes a pick, a Gate takes the approval, and an `mcp` Effect makes the call. The Effect's output is the pick's answer
27
+ ([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)). So the approval is in
28
+ the run's record, it survives a restart, and a Caller with the `answer` scope gives it.
29
+ - **MCP is the one extension contract for tools.** jr2 has no tool API of its own. An author's own tool is an MCP
30
+ server: a remote server that the author deploys, or a stdio server in the Sandbox Image. A guide in the docs shows a
31
+ small TypeScript server in the Sandbox Image. An Instance `tools/` folder and a server that the Effector hosts are
32
+ Later.
33
+ - **The config declares each server, by the author's name.** The `mcp` block of `jr2.config.ts` maps a name to a server:
34
+ `{ url, secret?, timeout? }` for a remote server (Streamable HTTP), or `{ command, args?, timeout? }` for a stdio
35
+ server. The name is the author's, so one Instance can declare two servers of one service with two secrets
36
+ (`slack-acme`, `slack-partner`). A name matches `[a-z][a-z0-9-]*`. `jr2` is reserved, because the Menu's tools are
37
+ `mcp__jr2__<event>` ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)), and the config
38
+ load refuses it. `secret` names an entry of the top-level `secrets` list. With no `hosts`, that entry is bound to the
39
+ host of the server's `url`, as a model key is bound to its provider's host. The Harness sends it as
40
+ `Authorization: Bearer <Stand-in>`, so `jr2 up` refuses explicit `headers` on that entry that leave out
41
+ `authorization`, explicit `hosts` that leave out the url's host, and explicit `paths` that do not cover the url's
42
+ path. A server with a `secret` needs an `https://` url, as a model endpoint with a bound key does
43
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)); a server with no secret can use
44
+ `http://`, as a keyless model endpoint can. In v1 a credential is a static bearer token, a Held secret or an Effect
45
+ secret. OAuth grants are the next round. `timeout` is for one call, 60 seconds by default.
46
+ - **An Agent names the servers and the tools that it uses.** `AgentDefinition` gains `mcp?: Array<{ server, tools? }>`.
47
+ `server` is a name of the config's `mcp` block, and `tools` is the allowlist. The field is plain data, as the
48
+ definition is ([ADR-0018](0018-instance-agents-are-definitions-jr2-assembles-the-harness.md)). Each entry is an
49
+ object, so that `resources` can be added beside `tools` later. A packaged Machine's Agent names the server that it
50
+ needs, and the composer rebinds the list with the existing `customize(machine, { agents: { <slot>: { mcp } } })`
51
+ ([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)). `jr2 up` refuses a server
52
+ name that the config does not declare. It prints the `customize(machine, { agents: { <slot>: { mcp } } })` line that
53
+ binds the slot to a declared server, and the `mcp` block entry that declares a new one. A Menu-only Agent can name a
54
+ remote server only: `jr2 up` refuses a stdio server on a `"none"` Agent (below).
55
+ - **The allowlist is offered only where jr2 can keep it.** It guards the server's credential:
56
+ - A remote server needs `tools`: a list of tool names, or `"*"`. The list is a guarantee. The Harness offers only
57
+ those tools and runs only those tools, and no Workspace code can spend the server's secret.
58
+ - A remote server whose secret Workspace code of the Sandbox where the Agent's Turns run can spend takes only `"*"`,
59
+ and `jr2 up` refuses a list. That is the case when the `workspace()` of that Sandbox grants the secret, when a
60
+ credentialed `command` of that Workspace names it, or when an `http` Effect of that Workspace whose body is a
61
+ Worktree file names it ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). Each lets a
62
+ file that the Agent wrote call every tool of the server, so a list would be a hint, and jr2 does not give a hint the
63
+ form of a guarantee. A `"none"` Agent's Turns run on the Instance Harness, where no Workspace code runs, so it keeps
64
+ its list.
65
+ - A stdio server has no `tools` field. The Agent gets every tool of the server. The server's version is fixed in the
66
+ Sandbox Image, and it runs as Workspace code (below), so a list would guard nothing.
67
+
68
+ The token's own scopes are the outer limit: the allowlist narrows what the Agent can do with a token, and never widens
69
+ it. One token under two secret names is the author's own act, and jr2 cannot see it.
70
+
71
+ - **The tool list is fixed for a conversation.** `jr2 up` checks each allowlisted name against the live server, from the
72
+ check pod and through its Custodian, as the provider preflight checks a model
73
+ ([ADR-0019](0019-one-converging-command-against-the-current-context.md)). It refuses a name that the server does not
74
+ list, and it fails when the server refuses the request (a 401 or a 403: a wrong or expired token), as it fails on a
75
+ model key that the provider refuses. It only warns when the server does not answer. The Harness reads `tools/list` at
76
+ the first Turn of a conversation. It keeps the allowed tools, sorted by name, records them with the conversation, and
77
+ offers the same tools until the conversation ends, across a park and a Harness restart. It never reads the list again
78
+ for a Turn, because a tools block that changes misses the provider's prompt cache
79
+ ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). It ignores a
80
+ `listChanged` notice. A tool that the server took away fails its call with jr2's text. The tools block is the Working
81
+ tools, then the MCP tools, then the Menu.
82
+ - **Names.** An MCP tool is `mcp__<server>__<tool>`, and the name must match `[A-Za-z0-9_-]{1,128}`, Anthropic's bound.
83
+ `jr2 up` refuses an allowlisted tool whose name breaks it. A tool that `"*"` reaches and whose name breaks it is not
84
+ offered, and the Harness writes one log line that names it. A name is in the tools block one time: `jr2 up` refuses an
85
+ allowlist that names a tool twice, and the Harness keeps the first tool of a name that a server lists twice. A server
86
+ name has no `_`, so the prefix is never ambiguous.
87
+ - **A server that does not answer at the first Turn fails the Turn.** The Turn fails with a named error,
88
+ `slack-acme did not answer: <reason>`, before the model reads anything. The Machine gets it as `agent.fault`, with
89
+ that text as its `reason` ([ADR-0016](0016-agent-turn-mechanics-are-internal.md),
90
+ [ADR-0074](0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md)). A server that does not answer
91
+ at a later call fails that call with jr2's text, and the model reads it.
92
+ - **A stdio server is Workspace code.** The Harness starts it through the spawner, inside the Landlock domain
93
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)), with the domain's env:
94
+ only the Stand-ins that the `workspace()` grants. Its program comes from the Sandbox Image. A stdio server has no
95
+ `secret` of its own: the secrets that it can spend are those that its Workspace grants. The Harness never starts a
96
+ server with pi-mcp's own spawn: that process would be outside the domain, and it would inherit the env of the Harness
97
+ process, every Stand-in and the path to the control Stand-in with it. The Instance Harness has no domain
98
+ ([ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md)), so a Menu-only Agent gets remote servers only.
99
+ - **Lifetimes.** A stdio server runs one process for each Agent conversation, from its first Turn to the end of the
100
+ conversation or of the Harness. Two Agents never share a process. When the process stops, the call fails with jr2's
101
+ text, and the next call starts a new process. A remote server has one connection for each Harness, which every
102
+ conversation of that pod uses.
103
+ - **What an Agent gets: tools, and the text and images of each result.** The model reads a result's text content and its
104
+ images. The engine replaces an image with a line of text for a model that reads no image, and an image larger than
105
+ about 5 MiB is one line of jr2's text. A result with `structuredContent` and no text gives the model the JSON as text.
106
+ Other content (audio, a resource) is replaced by one line of jr2's text that names its kind. A result with `isError`
107
+ reaches the model as an error. A result longer than 64 KiB is cut, and the model reads
108
+ `[jr2 cut this result at 64 KiB]` at the cut. The text of a failure that holds the server's words, such as a JSON-RPC
109
+ error's message, is cut in the same way. A call past the server's `timeout` is cancelled, and the model reads jr2's
110
+ text. The Harness gives the server no elicitation capability, and a call that asks for input fails with jr2's text.
111
+ Not in v1: elicitation, tool search, sampling, roots, and an Agent's resources and prompts (Later).
112
+ - **The `mcp` Effect has the shape of `http`.** `mcp.call(server, tool, { input })`, `mcp.read(server, uri)` and
113
+ `mcp.prompt(server, name, { input })`. `server` and `tool` can each be Open and bind under
114
+ `customize(machine, { effects: { <slot>: { server } | { tool } } })`, one object with both keys when both are Open, as
115
+ `url` and `secret` bind for `http`. The `server` of `mcp.read` and `mcp.prompt` can be Open too; their `uri` and
116
+ `name` are the author's. `fromPick` gives a pick's params to `input`. The server's secret is its Effect secret, placed
117
+ in the Instance Effector. `jr2 up` refuses an `mcp` Effect whose server the config's `mcp` block does not declare, or
118
+ whose server is a stdio server. It prints the `mcp` block entry that declares a remote server, and the
119
+ `customize(machine, { effects: { <slot>: { server } } })` line for an Open server. An Effect's `timeout` replaces the
120
+ server's `timeout` for that Effect. Outputs: `returned { isError, text, structured?, truncated? }` for a call
121
+ (`{ text, structured?, truncated? }` for a read and a prompt), `refused`, `unreachable`, `input-required` and
122
+ `unknown`. A call's `structuredContent` is `structured`, typed `unknown`; a read's `structured` is its `contents`
123
+ list, and a prompt's is its `messages` list. An `output:` schema is Later. The Effector bounds `text` and `structured`
124
+ as it bounds an `http` body, and sets `truncated` when it cuts one. It cuts a JSON-RPC error's message in a reason at
125
+ 1 KiB, with a jr2 marker at the cut. The client speaks the revisions `2025-11-25`, `2025-06-18` and `2025-03-26`; a
126
+ server that answers `initialize` with another revision is `unreachable`. When a server closes the event stream of the
127
+ request before its answer, after an event id, the client opens the stream again with a GET and `Last-Event-ID` (within
128
+ the timeout, and at most five times in a row that bring no new event). It does not send the request again. `refused`
129
+ means that the request did not run: an HTTP 3xx or 4xx at any step (a 401 or a 403 included), a refused handshake, or
130
+ one of the JSON-RPC errors `-32700`, `-32600`, `-32601` and `-32602`, or `-32002` (resource not found) for a read,
131
+ which does not act. Another JSON-RPC error, or a 5xx, after the request was sent is `unknown`, because the tool may
132
+ have acted. `input-required` is a server that asked for input: the error `-32042` (URL elicitation required), before
133
+ the tool ran, or an `elicitation/create` request, which the client declared no capability for. That request comes
134
+ while the tool runs, so the tool may have begun. jr2 does not send the request again. It is a named outcome that the
135
+ Machine can route. The client reads an event stream whose lines end with CRLF, LF or a lone CR. It scrubs a server's
136
+ words in a reason before it cuts them, so that no part of a secret stays at the cut. ADR-0067 states each outcome. An
137
+ `mcp` Effect runs at most once unless the author marks it `idempotent` (ADR-0067). The server's `idempotentHint` is
138
+ ignored: the server is not the author. In v1 an `mcp` Effect calls a remote server only, from the Instance Effector. A
139
+ stdio server for an Effect is Later.
140
+ - **Two clients, one protocol.** The Harness uses `@earendil-works/pi-mcp` behind a jr2 interface, with a jr2 stdio
141
+ transport over the spawner. The interface does not depend on the engine, so another engine keeps the MCP tools. The
142
+ Effector has a small MCP client of its own in Go, with the standard library only, over Streamable HTTP. Both speak
143
+ protocol `2025-11-25`, the revision that pi-mcp speaks.
144
+ - **The printed conversation shows an MCP call as `<server>/<tool>`**, with its input bounded as every call's is
145
+ ([ADR-0023](0023-the-harness-prints-the-conversation.md)). A pick prints by its event's name, as before, so the two
146
+ never print alike. The result of an MCP call does not print, as the result of a Working tool does not.
147
+ - **The Briefing says that MCP tools are not picks.** An Agent with MCP tools reads one more line in the standing part
148
+ ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)).
149
+
150
+ ## Considered options
151
+
152
+ - **An approval mode in the Harness**, as every other framework has: a `requireApproval` on a tool, and a Turn that
153
+ stops until a person answers. Rejected. The stop would be a state in the Harness that the Machine cannot see, keep or
154
+ route on. A Gate already does each of these, and the Machine already decides which act needs a person. A Turn that
155
+ waits on a person also holds a conversation, a Sandbox and its Size for that time.
156
+ - **Servers on the Agent definition** (the shape of 2026-10-27: `mcp: { slack: { url, secret, tools? } }` in `agent()`).
157
+ Rejected. A url and a secret are facts of the Instance, not of the Agent's definition, so a packaged Machine would
158
+ ship them. Two Agents of one server would declare it twice, and an Effect could not name the same server.
159
+ - **Read the tool list again on each Turn.** Rejected. A changed tools block misses the provider's prefix cache
160
+ (ADR-0029), and the history then calls tools that the request no longer defines.
161
+ - **An allowlist that is only a hint**, offered on every server. Rejected. Where Workspace code holds the Stand-in, or a
162
+ stdio server holds the token, the list stops nothing. A field that reads as a limit and is not one is worse than no
163
+ field.
164
+ - **A tool API of jr2's own** (a TypeScript function and a schema, run by the Harness). Rejected. It is a second
165
+ contract beside MCP, its code would run in the Harness process or in the domain with a new seat, and an engine change
166
+ would carry it again. An MCP server is the same function behind a contract that every engine speaks.
167
+ - **One secret for each host and header**, which `jr2 up` refused before this decision (ADR-0059's R5). Rejected: two
168
+ servers of one service (two Slack workspaces) send their tokens to one host in one header, and the Menu-only Agents of
169
+ an Instance share one Custodian. The Custodian already chose the value by the Stand-in in the header, so only the
170
+ refusal went.
171
+ - **Start a stdio server with pi-mcp's own spawn.** Rejected: the process would be outside the domain, with every
172
+ Stand-in in its env (ADR-0076).
173
+ - **Protocol `2026-07-28`.** Not now: pi-mcp speaks `2025-11-25`, and the servers that the target user runs speak it.
174
+
175
+ ## Consequences
176
+
177
+ - `jr2.config.ts` gains the `mcp` block, and `AgentDefinition` gains `mcp`. The Effect kinds gain `mcp` (ADR-0067).
178
+ - A Custodian holds what its pod's Agents name, the Instance Harness's too (ADR-0059): an Agent's MCP server secret is
179
+ held in the pod where its Turns run. The Custodian chooses a secret by the Stand-in that a request carries, so two
180
+ secrets can share one host and one header.
181
+ - An Agent's MCP tools reach the model, so the `@model` tier gains the claim `mcp-then-pick` (ADR-0066).
182
+ - A stdio server shares the domain with the other Workspace code of its Sandbox. Other Agents' code can trace it and
183
+ write to its stdin. It holds no control credential, but its results are as trusted as the output of `bash`, and no
184
+ more.
185
+ - An `elicitation/create` request does not name the call that sent it. When it comes on a connection with one call in
186
+ flight, that call fails with jr2's text. When more calls are in flight, which a remote server's one connection for
187
+ each Harness makes possible, jr2 cannot tell which call asked: no call is marked, and each fails with the server's own
188
+ error.
189
+ - A remote server's results come from outside the Instance. They can carry a prompt injection, as a web page can. The
190
+ allowlist bounds what the Agent can do with the server's token; the Machine bounds what the run does.
191
+ - Tests: the unit tier checks the config and the allowlist rules, and the Harness's client against an in-memory server.
192
+ The Harness's tests drive a stdio server through the spawner, and check that it holds only the granted Stand-ins. The
193
+ default e2e tier checks the refusals of `jr2 up` and of the publish door. The `@kind` tier checks the live tool list
194
+ from the check pod, an Agent's calls to two servers on one host, each through the Custodian with its own value, and an
195
+ `mcp` Effect from the Instance Effector; the server sees each value and never a Stand-in. The Effector's Go tests
196
+ check the `mcp` kind against a test server. The Custodian suite checks two secrets on one host in one header.
197
+ - The OAuth round must look again at client registration: the `2026-07-28` spec deprecates dynamic client registration
198
+ for Client ID Metadata Documents.
@@ -21,7 +21,7 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
21
21
  | 0011 | [Control events are workflow-defined and delivered by closure, not routed](decisions/0011-workflow-defined-events.md) |
22
22
  | 0012 | [`workspace()` wraps a body Machine and owns only Sandbox lifecycle](decisions/0012-workspace-wrapper-machine.md) |
23
23
  | 0013 | [The Agent reaches its Machine through a container it cannot read](decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) |
24
- | 0014 | [Observation is open; run state and control need the Instance token](decisions/0014-observation-is-open-run-state-is-not.md) |
24
+ | 0014 | [Observation is open; run state and control need a Caller's scope](decisions/0014-observation-is-open-run-state-is-not.md) |
25
25
  | 0015 | [Workflows are authored through `jr2Setup`; vocabulary and agent menus derive from the machine](decisions/0015-authoring-surface-absorbs-the-mechanism.md) |
26
26
  | 0016 | [Agent-turn mechanics are jr2-internal: ambient coordinates, host-side offsets, absorbed retries, fresh sessions](decisions/0016-agent-turn-mechanics-are-internal.md) |
27
27
  | 0017 | [The run loop is a jr2 `pool` over a generalized source port](decisions/0017-pool-over-a-source-port.md) |
@@ -30,7 +30,7 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
30
30
  | 0020 | [Private-CA trust is a named concept, not volume plumbing](decisions/0020-private-ca-trust-is-a-named-concept.md) |
31
31
  | 0021 | [Workspace continuity is watched; the lease asserts](decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md) |
32
32
  | 0022 | [Observation is a level-triggered feed, and an API before it is a page](decisions/0022-observation-is-a-level-triggered-feed.md) |
33
- | 0023 | [The Harness prints the conversation, and the reasoning is the part that prints](decisions/0023-the-harness-prints-the-conversation.md) |
33
+ | 0023 | [The Harness prints the conversation and the Orchestrator keeps it, and the reasoning is the part that prints](decisions/0023-the-harness-prints-the-conversation.md) |
34
34
  | 0024 | [An Agent's turn ends with the state that asked for it](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) |
35
35
  | 0025 | [CANCEL ends a run; stopping one parks it](decisions/0025-cancel-ends-a-run-stop-parks-it.md) |
36
36
  | 0026 | [A Turn that is over is never prompted, and only a Turn that is over answers 404](decisions/0026-a-turn-that-is-over-is-never-prompted.md) |
@@ -39,7 +39,7 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
39
39
  | 0029 | [A Menu is fixed for a conversation, and a Turn is told its Allowed picks](decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md) |
40
40
  | 0030 | [A snapshot names the Machine it was written under](decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md) |
41
41
  | 0031 | [Menu-only Agents run on the Instance Harness](decisions/0031-menu-only-agents-run-on-the-instance-harness.md) |
42
- | 0032 | [The Console unlocks with the Instance token, and the bands do not move](decisions/0032-the-console-unlocks-with-the-instance-token.md) |
42
+ | 0032 | [The Console unlocks with a Caller's credential, and shows what its scopes allow](decisions/0032-the-console-unlocks-with-a-callers-credential.md) |
43
43
  | 0033 | [A Machine declares the input that starts it](decisions/0033-a-machine-declares-the-input-that-starts-it.md) |
44
44
  | 0034 | [The Console renders with Preact, and the server erases the types](decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md) |
45
45
  | 0035 | [A runaway turn is ended by the Harness, rerolled once, then a fault](decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md) |
@@ -71,7 +71,7 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
71
71
  | 0062 | [jr2 briefs the Agent on its seat, never on its task](decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md) |
72
72
  | 0063 | [The Orchestrator watches the cluster](decisions/0063-the-orchestrator-watches-the-cluster.md) |
73
73
  | 0064 | [A Workspace waits for capacity](decisions/0064-a-workspace-waits-for-capacity.md) |
74
- | 0065 | [A persist survives a crash, and history is a week](decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md) |
74
+ | 0065 | [A persist survives a crash, and history is kept for a set time](decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md) |
75
75
  | 0066 | [A `@model` tier runs a real model, and a change to what the model reads runs the tier](decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md) |
76
76
  | 0067 | [An Effect is an actor slot, and runs at most once unless marked idempotent](decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md) |
77
77
  | 0068 | [An Effector holds an Effect's secrets and signs its wire](decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md) |
@@ -84,3 +84,5 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
84
84
  | 0075 | [A Machine cuts a Worktree, and the Workspace only clones](decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md) |
85
85
  | 0076 | [An Agent's control credential lives only where no Workspace code runs](decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md) |
86
86
  | 0077 | [jr2 requires Landlock on every Sandbox node](decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md) |
87
+ | 0078 | [A Caller comes from one authenticator seam and acts within its scopes](decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md) |
88
+ | 0079 | [An Agent holds MCP tools, a Machine calls them as an Effect, and the Machine is the approval](decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md) |