@jr2/orchestrator 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +142 -29
- package/docs/concepts/architecture.md +60 -34
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +13 -8
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +7 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +133 -50
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +14 -4
- package/docs/concepts/pools-and-sources.md +193 -17
- package/docs/concepts/secrets-and-trust.md +186 -62
- package/docs/concepts/the-menu.md +14 -0
- package/docs/concepts/workspaces.md +135 -56
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +72 -15
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +29 -12
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +19 -7
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +14 -7
- package/docs/guides/look-inside-a-sandbox.md +115 -42
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +34 -27
- package/docs/guides/set-an-agents-access-and-dials.md +69 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +7 -1
- package/docs/guides/watch-a-run.md +146 -117
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +18 -8
- package/docs/llms.txt +110 -23
- package/docs/reference/authoring-surface/agent.md +35 -12
- package/docs/reference/authoring-surface/answer.md +20 -4
- package/docs/reference/authoring-surface/arg.md +28 -6
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +37 -22
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +18 -5
- package/docs/reference/authoring-surface/file.md +37 -17
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +109 -9
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +14 -4
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +41 -8
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +23 -5
- package/docs/reference/authoring-surface/pool.md +79 -11
- package/docs/reference/authoring-surface/push.md +31 -13
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +26 -5
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +59 -4
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +6 -7
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
- package/docs/reference/authoring-surface/type-customize.md +20 -3
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +6 -6
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +30 -9
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +66 -38
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +88 -147
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +248 -6
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- 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
- package/docs/reference/decisions.md +9 -4
- package/docs/reference/glossary.md +138 -45
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +988 -123
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +7 -5
- package/package.json +10 -3
- package/src/actor.ts +194 -42
- package/src/agent.ts +56 -6
- package/src/ambient.ts +3 -5
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +238 -26
- package/src/charts.ts +9 -2
- package/src/config.ts +566 -7
- package/src/conversations.ts +453 -0
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +257 -77
- package/src/customize.ts +154 -7
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +52 -4
- package/src/effect.ts +401 -132
- package/src/effector-wire.ts +278 -30
- package/src/fail.ts +17 -4
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +84 -35
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +197 -39
- package/src/holds.ts +5 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +164 -70
- package/src/http.ts +933 -99
- package/src/index.ts +30 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +23 -3
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +29 -5
- package/src/mcp-effect.ts +424 -0
- package/src/model-keys.ts +24 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +82 -7
- package/src/notices.ts +576 -59
- package/src/open-body.ts +110 -30
- package/src/open-effect.ts +17 -4
- package/src/open.ts +40 -7
- package/src/parts.ts +125 -35
- package/src/party.ts +35 -1
- package/src/pick.ts +43 -9
- package/src/pool.ts +1172 -172
- package/src/registration.ts +249 -17
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +1614 -190
- package/src/run-list.ts +191 -0
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +116 -21
- package/src/secret-placement.ts +248 -35
- package/src/server.ts +53 -4
- package/src/setup.ts +158 -38
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +362 -46
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +27 -10
- package/src/workspace.ts +314 -259
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- 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
|
+
}
|