@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
package/src/callers.ts ADDED
@@ -0,0 +1,245 @@
1
+ // The JWT issuers of the Caller seam, and the record of a Caller on a run (ADR-0078).
2
+ //
3
+ // The seam itself (its order, the Instance token, the named `jr2c_` tokens) is in tokens.ts. This
4
+ // file adds its third authenticator: a JWT from an identity provider that the config declares, which
5
+ // the Orchestrator checks against the provider's published keys. That fetch is the Orchestrator's
6
+ // one call out of the Instance, and it spends no secret (ADR-0068).
7
+ //
8
+ // It also holds what a run records of a Caller: the Caller without its scopes, because a run
9
+ // records who acted, not what that credential could do then.
10
+
11
+ import { createRemoteJWKSet, customFetch, decodeJwt, jwtVerify, type JWTPayload } from "jose";
12
+ import { SCOPES, type AnswerGrant, type GateGrant, type JwtIssuer, type Scopes } from "./config.ts";
13
+ import type { Caller, CallerAnswer, CallerAuthenticator } from "./tokens.ts";
14
+
15
+ // ---- What a run records ------------------------------------------------------------------------
16
+
17
+ /** A Caller as a run records it (ADR-0078): who acted, and the party it says it acted for. Never its
18
+ * scopes. */
19
+ export type CallerRecord = {
20
+ via: Caller["via"];
21
+ name: string;
22
+ /** The name that jr2 shows, when it is not `name` (a JWT's `email`). */
23
+ shown?: string;
24
+ /** The Caller's claim about the party it acts for. jr2 records it and never checks it, except for
25
+ * a Hook, whose signature covers it. */
26
+ onBehalfOf?: string;
27
+ };
28
+
29
+ /**
30
+ * Who started a run (ADR-0078): a Caller on a guarded route; a run, through a local `dispatch`
31
+ * (ADR-0071); or jr2 itself, for a standing run (ADR-0017), at `boot`, at a `publish`, at a
32
+ * `hand-over`, or at a `resume`. A run that a foreign `dispatch` started records the Caller whose
33
+ * credential the other Instance sent.
34
+ */
35
+ export type RunStarter =
36
+ | ({ kind: "caller" } & CallerRecord)
37
+ | { kind: "run"; run: string; slot: string }
38
+ | { kind: "jr2"; why: "boot" | "publish" | "hand-over" | "resume" };
39
+
40
+ /** One answer to a Gate of a run, and the Caller that gave it (ADR-0011, ADR-0078). `delivery` is
41
+ * the Hook's delivery id when a Hook gave it: the run's `(delivery, gate)` pairs, so a restore never
42
+ * gives one delivery twice to one Gate. */
43
+ export type GateAnswer = { gate: string; type: string; at: string; answeredBy: CallerRecord; delivery?: string };
44
+
45
+ /** How many Gate answers a run keeps, the last ones: a standing run's Gate can take an answer for
46
+ * each delivery, and the run's blob is saved on each transition. */
47
+ export const GATE_ANSWERS_KEPT = 100;
48
+
49
+ /** The record of a Caller: the Caller without its scopes. */
50
+ export function callerRecord(caller: Caller): CallerRecord {
51
+ return {
52
+ via: caller.via,
53
+ name: caller.name,
54
+ ...(caller.shown !== undefined ? { shown: caller.shown } : {}),
55
+ ...(caller.onBehalfOf !== undefined ? { onBehalfOf: caller.onBehalfOf } : {}),
56
+ };
57
+ }
58
+
59
+ /** A Caller as a line names it: `token/ci`, then the name that jr2 shows and the party it acts for. */
60
+ export function callerText(caller: CallerRecord): string {
61
+ return (
62
+ `${caller.via}/${caller.name}` +
63
+ (caller.shown !== undefined ? ` (${caller.shown})` : "") +
64
+ (caller.onBehalfOf !== undefined ? ` on behalf of ${caller.onBehalfOf}` : "")
65
+ );
66
+ }
67
+
68
+ // ---- The JWT authenticator ---------------------------------------------------------------------
69
+
70
+ /** The algorithms a JWT may be signed with: the asymmetric ones alone, because a published key
71
+ * must not be able to sign. */
72
+ const ALGORITHMS = ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA"];
73
+
74
+ /** How far the issuer's clock and ours may differ, for `exp` and `nbf`. */
75
+ const CLOCK_TOLERANCE_S = 30;
76
+
77
+ /** How long the keys are kept before a JWT makes a fetch again. */
78
+ const KEYS_MAX_AGE_MS = 10 * 60_000;
79
+
80
+ /** The shortest time between two fetches of the keys, for a JWT whose key is not known; also how
81
+ * long a failed fetch of the discovery document is kept before a JWT makes a fetch again. */
82
+ const KEYS_COOLDOWN_MS = 30_000;
83
+
84
+ /** How long one fetch of the discovery document or the keys may take. */
85
+ const FETCH_TIMEOUT_MS = 5_000;
86
+
87
+ /** The claims a {@link JwtIssuer} reads when its `claims` names no other. */
88
+ const DEFAULT_CLAIMS = { name: "sub", shown: "email", groups: "groups" } as const;
89
+
90
+ /** The fetch that the JWT authenticator uses: the global one, or a test's. */
91
+ export type JwksFetch = (url: string, init: { signal?: AbortSignal }) => Promise<Response>;
92
+
93
+ /** The options of {@link jwtAuthenticator}, for tests. */
94
+ export type JwtAuthenticatorOptions = {
95
+ /** The fetch for the discovery document and the keys. Default: the global `fetch`. */
96
+ fetch?: JwksFetch;
97
+ /** The shortest time between two fetches of the keys, and of a discovery that failed. Default: 30 s. */
98
+ cooldownMs?: number;
99
+ /** The clock, in milliseconds, that measures the cooldown of a failed discovery. Default: `Date.now`. */
100
+ now?: () => number;
101
+ };
102
+
103
+ type KeySet = ReturnType<typeof createRemoteJWKSet>;
104
+
105
+ /**
106
+ * The JWT issuers' authenticator (ADR-0078). It claims a bearer that is a JWT whose `iss` claim is
107
+ * the `issuer` of an entry in `issuers()`, which is read on each request, so a publish applies with
108
+ * no restart. A claimed JWT that fails a check is "mine, and bad" (401). A JWT of an issuer that no
109
+ * entry names is not claimed.
110
+ */
111
+ export function jwtAuthenticator(
112
+ issuers: () => readonly JwtIssuer[],
113
+ opts: JwtAuthenticatorOptions = {},
114
+ ): CallerAuthenticator {
115
+ const doFetch: JwksFetch = opts.fetch ?? ((url, init) => fetch(url, init));
116
+ const cooldownMs = opts.cooldownMs ?? KEYS_COOLDOWN_MS;
117
+ const now = opts.now ?? Date.now;
118
+ // The key sets, by the issuer and its keys' URL: an entry that a publish changes gets a new one.
119
+ // `failedAt`: when its discovery failed. The failure is kept for the cooldown, so a flood of JWTs
120
+ // that name an issuer whose discovery fails makes at most one fetch in each cooldown.
121
+ const keySets = new Map<string, { keySet: Promise<KeySet>; failedAt?: number }>();
122
+ const idOf = (entry: JwtIssuer): string => JSON.stringify([entry.issuer, entry.jwks ?? null]);
123
+
124
+ const keySetOf = (entry: JwtIssuer): Promise<KeySet> => {
125
+ const id = idOf(entry);
126
+ const found = keySets.get(id);
127
+ if (found && (found.failedAt === undefined || now() - found.failedAt < cooldownMs)) return found.keySet;
128
+ const slot: { keySet: Promise<KeySet>; failedAt?: number } = {
129
+ keySet: (async () => {
130
+ const url = entry.jwks ?? (await discoverJwks(entry.issuer, doFetch));
131
+ return createRemoteJWKSet(new URL(url), {
132
+ cacheMaxAge: KEYS_MAX_AGE_MS,
133
+ cooldownDuration: cooldownMs,
134
+ timeoutDuration: FETCH_TIMEOUT_MS,
135
+ [customFetch]: (u, init) => doFetch(u, { signal: init.signal }),
136
+ });
137
+ })(),
138
+ };
139
+ slot.keySet.catch(() => {
140
+ slot.failedAt = now();
141
+ });
142
+ keySets.set(id, slot);
143
+ return slot.keySet;
144
+ };
145
+
146
+ /** Drop the key sets of each issuer and keys' URL that the config no longer names: the cache
147
+ * holds at most one entry for each entry of `issuers()`. */
148
+ const prune = (current: readonly JwtIssuer[]): void => {
149
+ if (keySets.size === 0) return;
150
+ const named = new Set(current.map(idOf));
151
+ for (const id of keySets.keys()) if (!named.has(id)) keySets.delete(id);
152
+ };
153
+
154
+ return async (bearer): Promise<CallerAnswer> => {
155
+ if (bearer.split(".").length !== 3) return undefined;
156
+ let unverified: JWTPayload;
157
+ try {
158
+ unverified = decodeJwt(bearer);
159
+ } catch {
160
+ return undefined;
161
+ }
162
+ const current = issuers();
163
+ prune(current);
164
+ const entry = current.find((i) => i.issuer === unverified.iss);
165
+ if (!entry) return undefined;
166
+
167
+ let payload: JWTPayload;
168
+ try {
169
+ ({ payload } = await jwtVerify(bearer, await keySetOf(entry), {
170
+ issuer: entry.issuer,
171
+ audience: entry.audience,
172
+ algorithms: ALGORITHMS,
173
+ clockTolerance: CLOCK_TOLERANCE_S,
174
+ requiredClaims: ["exp"],
175
+ }));
176
+ } catch (err) {
177
+ return { refused: `a JWT of ${entry.issuer} did not verify: ${err instanceof Error ? err.message : err}` };
178
+ }
179
+
180
+ const claims = { ...DEFAULT_CLAIMS, ...entry.claims };
181
+ const name = payload[claims.name];
182
+ if (typeof name !== "string" || name === "") {
183
+ return { refused: `a JWT of ${entry.issuer} has no "${claims.name}" claim, which names its Caller` };
184
+ }
185
+ const shown = payload[claims.shown];
186
+ return {
187
+ via: "jwt",
188
+ name,
189
+ ...(typeof shown === "string" && shown !== "" && shown !== name ? { shown } : {}),
190
+ may: joinScopes(
191
+ valuesOf(payload[claims.groups]).flatMap((v) => (Object.hasOwn(entry.groups, v) ? [entry.groups[v]!] : [])),
192
+ ),
193
+ };
194
+ };
195
+ }
196
+
197
+ /** The `jwks_uri` of an issuer's OIDC discovery document. */
198
+ async function discoverJwks(issuer: string, doFetch: JwksFetch): Promise<string> {
199
+ const url = `${issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`;
200
+ const res = await doFetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
201
+ if (!res.ok) throw new Error(`${url} answered ${res.status}`);
202
+ const doc = (await res.json()) as { jwks_uri?: unknown };
203
+ if (typeof doc.jwks_uri !== "string") throw new Error(`${url} names no jwks_uri`);
204
+ return doc.jwks_uri;
205
+ }
206
+
207
+ /** The values of a groups claim: a list of strings, or one string. */
208
+ function valuesOf(claim: unknown): string[] {
209
+ if (typeof claim === "string") return [claim];
210
+ if (Array.isArray(claim)) return claim.filter((v): v is string => typeof v === "string");
211
+ return [];
212
+ }
213
+
214
+ /** The scopes of each grant, joined: a scope that one gives for every Workflow is for every
215
+ * Workflow, and lists join. A JWT whose groups give one team's Gate and another team's Gate of one
216
+ * Workflow may answer both. */
217
+ export function joinScopes(grants: readonly Scopes[]): Scopes {
218
+ const out: Record<string, AnswerGrant> = {};
219
+ for (const scope of SCOPES) {
220
+ let joined: AnswerGrant | undefined;
221
+ for (const may of grants) {
222
+ const grant: AnswerGrant | undefined = may[scope];
223
+ if (grant === undefined) continue;
224
+ if (grant === "all" || joined === "all") joined = "all";
225
+ else joined = joinItems(joined ?? [], grant);
226
+ }
227
+ if (joined !== undefined) out[scope] = joined;
228
+ }
229
+ return out as Scopes;
230
+ }
231
+
232
+ /** Two lists of one scope, joined: each Workflow name once, and each `answer` item that names Gates
233
+ * once (ADR-0078). An item that names Gates stays beside a bare name of its Workflow, which opens
234
+ * them anyway. */
235
+ function joinItems(a: readonly (string | GateGrant)[], b: readonly (string | GateGrant)[]): (string | GateGrant)[] {
236
+ const seen = new Set<string>();
237
+ const out: (string | GateGrant)[] = [];
238
+ for (const item of [...a, ...b]) {
239
+ const key = typeof item === "string" ? item : JSON.stringify([item.workflow, [...item.gates].sort()]);
240
+ if (seen.has(key)) continue;
241
+ seen.add(key);
242
+ out.push(item);
243
+ }
244
+ return out;
245
+ }
package/src/chart.ts CHANGED
@@ -25,6 +25,7 @@ import { defineEvent, type EventAudience, type EventDef } from "@jr2/agent-proto
25
25
  import type { AgentDefinition, ThinkingLevel, WorkspaceAccess } from "./agent.ts";
26
26
  import { inputKeysOf, inputShapeOf, isEffect, type InputShape } from "./effect.ts";
27
27
  import { FAILURE_GUARD } from "./fail.ts";
28
+ import { isGateSlot } from "./gate.ts";
28
29
  import { compileChart, durationMs, type LentParts, type OpenBodyDeclaration } from "./open-body.ts";
29
30
  import { SENT_BY } from "./party.ts";
30
31
  import { loadWarningsOf } from "./vocabulary.ts";
@@ -673,6 +674,7 @@ function lendsOf(decl: OpenBodyDeclaration) {
673
674
  agents: decl.lends.agents ?? [],
674
675
  effects: decl.lends.effects ?? [],
675
676
  machines: decl.lends.machines ?? [],
677
+ gates: decl.lends.gates ?? [],
676
678
  guards: decl.lends.guards ?? [],
677
679
  actions: decl.lends.actions ?? [],
678
680
  delays: decl.lends.delays ?? [],
@@ -695,7 +697,14 @@ export function narrowedChartSchema(decl: OpenBodyDeclaration): Record<string, u
695
697
  function narrowOf(decl: OpenBodyDeclaration): Narrow {
696
698
  const l = lendsOf(decl);
697
699
  return {
698
- srcs: [...l.agents, ...l.effects, ...l.machines, ...(l.gate ? ["gate"] : []), ...(l.fail ? ["fail"] : [])],
700
+ srcs: [
701
+ ...l.agents,
702
+ ...l.effects,
703
+ ...l.machines,
704
+ ...l.gates,
705
+ ...(l.gate ? ["gate"] : []),
706
+ ...(l.fail ? ["fail"] : []),
707
+ ],
699
708
  lentAgents: l.agents,
700
709
  lendsEffects: l.effects.length > 0,
701
710
  gate: l.gate,
@@ -853,6 +862,9 @@ export type AdmittedChart = {
853
862
  agents: Record<string, AgentDefinition>;
854
863
  /** The bytes of the text's canonical JSON. */
855
864
  size: number;
865
+ /** The invoke ids of the Chart's Gates (ADR-0011), as `jr2Setup` names them: the names that the
866
+ * `answer` scope can name them by (ADR-0078). */
867
+ gates: string[];
856
868
  };
857
869
 
858
870
  /**
@@ -1064,6 +1076,8 @@ class Door {
1064
1076
  private readonly invokeKinds = new Map<ChartInvoke, NonNullable<ReturnType<Door["srcKind"]>>>();
1065
1077
  /** Every `jr2.sentBy` id, for the party check. */
1066
1078
  private readonly sentBys: Array<{ id: string; path: string }> = [];
1079
+ /** The Gates' invoke ids, which the party check finds. */
1080
+ private readonly gateIds = new Set<string>();
1067
1081
 
1068
1082
  private readonly chart: ChartText;
1069
1083
  private readonly decl: OpenBodyDeclaration;
@@ -1126,7 +1140,14 @@ class Door {
1126
1140
  const events = Object.entries(this.chart.events ?? {}).map(([name, ev]) => this.eventDef(name, ev)!);
1127
1141
  const agents: Record<string, AgentDefinition> = {};
1128
1142
  for (const [slot, a] of Object.entries(this.chart.agents ?? {})) agents[slot] = { ...a };
1129
- const admitted: AdmittedChart = { text: this.chart, hash: chartHash(this.chart), events, agents, size };
1143
+ const admitted: AdmittedChart = {
1144
+ text: this.chart,
1145
+ hash: chartHash(this.chart),
1146
+ events,
1147
+ agents,
1148
+ size,
1149
+ gates: [...this.gateIds],
1150
+ };
1130
1151
  this.trialCompile(admitted);
1131
1152
  return admitted;
1132
1153
  }
@@ -1152,11 +1173,12 @@ class Door {
1152
1173
  // --- rule 3: names --------------------------------------------------------------------------------
1153
1174
 
1154
1175
  /** The lent actors by kind, as the lent set has them now. */
1155
- private lentActors(): Array<{ name: string; kind: "Agent" | "Effect" | "Machine" }> {
1156
- const out: Array<{ name: string; kind: "Agent" | "Effect" | "Machine" }> = [];
1176
+ private lentActors(): Array<{ name: string; kind: "Agent" | "Effect" | "Machine" | "Gate slot" }> {
1177
+ const out: Array<{ name: string; kind: "Agent" | "Effect" | "Machine" | "Gate slot" }> = [];
1157
1178
  for (const name of this.lends.agents) if (name in this.lent.actors) out.push({ name, kind: "Agent" });
1158
1179
  for (const name of this.lends.effects) if (name in this.lent.actors) out.push({ name, kind: "Effect" });
1159
1180
  for (const name of this.lends.machines) if (name in this.lent.actors) out.push({ name, kind: "Machine" });
1181
+ for (const name of this.lends.gates) if (name in this.lent.actors) out.push({ name, kind: "Gate slot" });
1160
1182
  return out;
1161
1183
  }
1162
1184
 
@@ -1170,7 +1192,7 @@ class Door {
1170
1192
  /** What an invoke's `src` names, or undefined after a refusal line. */
1171
1193
  private srcKind(src: string, path: string): InvokeKind | undefined {
1172
1194
  const lentNow = (name: string) => name in this.lent.actors;
1173
- const declared = [...this.lends.agents, ...this.lends.effects, ...this.lends.machines];
1195
+ const declared = [...this.lends.agents, ...this.lends.effects, ...this.lends.machines, ...this.lends.gates];
1174
1196
  if (declared.includes(src) && !lentNow(src)) {
1175
1197
  this.fail(
1176
1198
  3,
@@ -1181,6 +1203,22 @@ class Door {
1181
1203
  if (this.lends.agents.includes(src)) return "agent";
1182
1204
  if (this.lends.effects.includes(src)) return "effect";
1183
1205
  if (this.lends.machines.includes(src)) return "machine";
1206
+ if (this.lends.gates.includes(src)) {
1207
+ // A lent Gate slot (ADR-0011, ADR-0070): its handlers and its look give its `delivers`, which
1208
+ // the Chart's Machine must take. One that is not in the Chart's Vocabulary could never arrive.
1209
+ const slot = this.lent.actors[src];
1210
+ const outside = isGateSlot(slot)
1211
+ ? slot.gateSlot.delivers.map((d) => d.name).filter((name) => !this.eventSchemas.has(name))
1212
+ : [];
1213
+ if (outside.length) {
1214
+ this.fail(
1215
+ 3,
1216
+ `${path}: the Gate slot "${src}" delivers ${outside.map((n) => `"${n}"`).join(", ")}, which ${outside.length > 1 ? "are" : "is"} not in the Chart's \`events\` (events: ${names([...this.eventSchemas.keys()])}); declare ${outside.length > 1 ? "them" : "it"} in \`events\` and handle ${outside.length > 1 ? "them" : "it"} on this state`,
1217
+ );
1218
+ return undefined;
1219
+ }
1220
+ return "gate";
1221
+ }
1184
1222
  if (src === "gate") {
1185
1223
  if (this.lends.gate) return "gate";
1186
1224
  this.fail(
@@ -1375,7 +1413,14 @@ class Door {
1375
1413
  // --- rule 5: minted Agents ------------------------------------------------------------------------
1376
1414
 
1377
1415
  private checkAgents(): void {
1378
- const lentNames = new Set([...this.lends.agents, ...this.lends.effects, ...this.lends.machines, "gate", "fail"]);
1416
+ const lentNames = new Set([
1417
+ ...this.lends.agents,
1418
+ ...this.lends.effects,
1419
+ ...this.lends.machines,
1420
+ ...this.lends.gates,
1421
+ "gate",
1422
+ "fail",
1423
+ ]);
1379
1424
  for (const [slot, a] of Object.entries(this.chart.agents ?? {})) {
1380
1425
  const path = `agents.${slot}`;
1381
1426
  if (lentNames.has(slot)) {
@@ -1719,7 +1764,7 @@ class Door {
1719
1764
  /** The parties' rules, as `jr2Setup` applies them (ADR-0015), each with its path in the Chart. */
1720
1765
  private checkParties(): void {
1721
1766
  const agents = new Set<string>();
1722
- const gates = new Set<string>();
1767
+ const gates = this.gateIds;
1723
1768
  const slotStates = new Map<string, Node[]>();
1724
1769
  for (const node of this.nodes) {
1725
1770
  const invokes = listAt(node.state.invoke, `${node.doc}.invoke`);