@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/held-secrets.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
//
|
|
10
10
|
// This module is the pure half, shared by every seat that needs it:
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// heldStandIn(...) a held secret's Stand-in, minted per pod by the pod's maker — secret
|
|
13
|
+
// standIn(name) the fetch Stand-in's form — deterministic and public, for that one alone
|
|
13
14
|
// heldSecretsOf(...) `jr2 up`'s normalization of the claimed entries: every refusal (R1–R13),
|
|
14
15
|
// every warning, and the engine-neutral bindings each pod's manifest
|
|
15
16
|
// (`held-<pod>.json`) takes its own from
|
|
@@ -22,24 +23,47 @@
|
|
|
22
23
|
import { createHash } from "node:crypto";
|
|
23
24
|
import { readFile } from "node:fs/promises";
|
|
24
25
|
import { isIP } from "node:net";
|
|
25
|
-
import type
|
|
26
|
-
import {
|
|
27
|
-
|
|
26
|
+
import { isStdioServer, type JR2Config, type HarnessEnvVar } from "./config.ts";
|
|
27
|
+
import {
|
|
28
|
+
CREDENTIAL_FILES,
|
|
29
|
+
DOMAIN_WRITABLE_ROOTS,
|
|
30
|
+
MODEL_KEYS,
|
|
31
|
+
MODEL_KEY_PROVIDERS,
|
|
32
|
+
SIGNING_CREDENTIALS,
|
|
33
|
+
} from "./model-keys.ts";
|
|
34
|
+
import {
|
|
35
|
+
HELD_CA_SECRET,
|
|
36
|
+
HELD_SECRETS_SECRET,
|
|
37
|
+
HELD_TLS_SECRET,
|
|
38
|
+
INSTANCE_EFFECTOR,
|
|
39
|
+
INSTANCE_HARNESS_SERVICE,
|
|
40
|
+
INSTANCE_SECRET,
|
|
41
|
+
} from "./names.ts";
|
|
28
42
|
import type { PlacementRoute } from "./parts.ts";
|
|
43
|
+
import { sign } from "./tokens.ts";
|
|
29
44
|
|
|
30
45
|
/** The kit's Secrets no Harness container may read (R13): the Instance token and signing key, each
|
|
31
46
|
* pod's literal held values, the leaves, the CA, and every Effector's (ADR-0068) — the Instance
|
|
32
47
|
* Effector's Effect secrets and key, each Workspace's Sandbox Effector Secret (`jr2-effector-<pod>`),
|
|
33
48
|
* each Sandbox's Effector key (`<sandbox>-effector`), and each `git.credentials` entry's clone and
|
|
34
|
-
* push Secrets (`jr2-git-<hash>`, `jr2-git-push-<hash>`, ADR-0051)
|
|
35
|
-
|
|
49
|
+
* push Secrets (`jr2-git-<hash>`, `jr2-git-push-<hash>`, ADR-0051) — and each pod's own credentials
|
|
50
|
+
* (ADR-0076): the Instance Harness's (`jr2-instance-harness-token`) and each Sandbox's
|
|
51
|
+
* (`<sandbox>-token`), which hold the real control token beside the Stand-ins. */
|
|
52
|
+
const KIT_SECRETS: readonly string[] = [
|
|
53
|
+
INSTANCE_SECRET,
|
|
54
|
+
HELD_TLS_SECRET,
|
|
55
|
+
HELD_CA_SECRET,
|
|
56
|
+
INSTANCE_EFFECTOR,
|
|
57
|
+
`${INSTANCE_HARNESS_SERVICE}-token`,
|
|
58
|
+
];
|
|
36
59
|
const KIT_SECRET_PREFIXES: readonly string[] = [`${HELD_SECRETS_SECRET}-`, "jr2-effector-", "jr2-git-"];
|
|
37
60
|
const isKitSecret = (name: string): boolean =>
|
|
38
61
|
KIT_SECRETS.includes(name) ||
|
|
39
62
|
name === HELD_SECRETS_SECRET ||
|
|
40
63
|
KIT_SECRET_PREFIXES.some((p) => name.startsWith(p)) ||
|
|
41
|
-
// A Sandbox's own Effector key: every Sandbox's name starts `ws-`
|
|
42
|
-
(
|
|
64
|
+
// A Sandbox's own Effector key and its credentials: every Sandbox's name starts `ws-`
|
|
65
|
+
// (`workspaceName`).
|
|
66
|
+
(name.startsWith("ws-") && (name.endsWith("-effector") || name.endsWith("-token")));
|
|
43
67
|
|
|
44
68
|
/**
|
|
45
69
|
* The pod key of one Workspace (ADR-0059): its registered workflow and the placement route to its
|
|
@@ -70,18 +94,36 @@ export function providerClaims(config: JR2Config, models: readonly string[]): st
|
|
|
70
94
|
}
|
|
71
95
|
|
|
72
96
|
/**
|
|
73
|
-
* The Stand-in
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* something only to the Custodian and only toward a bound host. Unique per name, so one request can
|
|
77
|
-
* carry two secrets' Stand-ins and the Custodian can tell them apart. ASCII letters, digits, `-`
|
|
78
|
-
* and `_` only, so it is a valid header and env value; and it never contains `sk-ant-oat`, which pi
|
|
79
|
-
* reads as an Anthropic OAuth token and answers with different auth headers.
|
|
97
|
+
* The PUBLIC Stand-in form, `jr2-held-<NAME>`. Since ADR-0076 one Stand-in alone takes it: the fetch
|
|
98
|
+
* Stand-in, `standIn("JR2_SANDBOX_TOKEN")`, which `jr2-upload-pack` holds as a constant and the
|
|
99
|
+
* Custodian swaps on the fetch route alone. Every other Stand-in is secret ({@link heldStandIn}).
|
|
80
100
|
*/
|
|
81
101
|
export function standIn(name: string): string {
|
|
82
102
|
return `jr2-held-${name}`;
|
|
83
103
|
}
|
|
84
104
|
|
|
105
|
+
/** What a Stand-in must never contain (ADR-0059): pi reads a key with it as an Anthropic OAuth
|
|
106
|
+
* token, and answers with different auth headers. */
|
|
107
|
+
const OAUTH_MARK = "sk-ant-oat";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The SECRET Stand-in of one held secret in one pod (ADR-0059, ADR-0076): what the Harness process
|
|
111
|
+
* holds for it, as the file `/run/jr2/standins/<NAME>`, and what the Custodian of that pod swaps for
|
|
112
|
+
* the value. `jr2-held-<NAME>.<hmac>` — the readable prefix keeps a log scrub's pattern, and the
|
|
113
|
+
* HMAC under the signing key makes it unguessable, so Workspace code can spend only the secrets its
|
|
114
|
+
* `workspace()` grants. `pod` is the Sandbox's name, or `instance` for the Instance Harness. The
|
|
115
|
+
* pod's maker mints it: the Orchestrator for a Sandbox, `jr2 up` for the Instance Harness and a
|
|
116
|
+
* check pod. ASCII letters, digits, `-`, `_` and `.` only, so it is a valid header and env value,
|
|
117
|
+
* and it never contains `sk-ant-oat`: a signature that would is signed again under a counter.
|
|
118
|
+
*/
|
|
119
|
+
export function heldStandIn(key: Buffer, pod: string, name: string): string {
|
|
120
|
+
for (let n = 0; ; n++) {
|
|
121
|
+
const domain = `held-standin:${pod}/${name}${n === 0 ? "" : `#${n}`}`;
|
|
122
|
+
const value = `${standIn(name)}.${sign(key, domain)}`;
|
|
123
|
+
if (!value.includes(OAUTH_MARK)) return value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
85
127
|
/** The request headers a Stand-in is swapped in when a held secret names none — the four places a
|
|
86
128
|
* model API carries its key: `Authorization: Bearer` (OpenAI-compatible, `ANTHROPIC_AUTH_TOKEN`),
|
|
87
129
|
* `x-api-key` (Anthropic), `x-goog-api-key` (Gemini), `api-key` (Azure). */
|
|
@@ -99,10 +141,15 @@ export const CREDENTIAL_HEADERS: readonly string[] = [...DEFAULT_CREDENTIAL_HEAD
|
|
|
99
141
|
/** The env var the custom provider's key rides as (ADR-0018) — held since ADR-0059, and the kit's. */
|
|
100
142
|
export const PROVIDER_KEY_NAME = "JR2_PROVIDER_API_KEY";
|
|
101
143
|
|
|
102
|
-
/** The
|
|
103
|
-
* Orchestrator (ADR-0059).
|
|
144
|
+
/** The fetch token's held name (ADR-0013, ADR-0076) — a held secret of the kit's own, bound to the
|
|
145
|
+
* Orchestrator's fetch route (ADR-0059). Its Stand-in is the public `jr2-held-JR2_SANDBOX_TOKEN`,
|
|
146
|
+
* which `jr2-upload-pack` holds; the Custodian holds the token. */
|
|
104
147
|
export const SANDBOX_TOKEN_NAME = "JR2_SANDBOX_TOKEN";
|
|
105
148
|
|
|
149
|
+
/** The control token's held name (ADR-0076) — the kit's own, bound to the agent routes. Its Stand-in
|
|
150
|
+
* is secret: the Harness process alone holds it, as `/run/jr2/control/standin`. */
|
|
151
|
+
export const CONTROL_TOKEN_NAME = "JR2_CONTROL_TOKEN";
|
|
152
|
+
|
|
106
153
|
/** The env names the mechanism owns on a Harness container (ADR-0059): the proxy, the trust
|
|
107
154
|
* bundles, and every `JR2_` name. A held secret may not choose them, and while a held secret exists,
|
|
108
155
|
* `harness.env` may not either — the Custodian cannot chain to a second proxy yet (R11). */
|
|
@@ -233,6 +280,52 @@ export function heldSecretsOf(
|
|
|
233
280
|
catalogHosts.set(id, target.port === 443 ? target.host : `${target.host}:${target.port}`);
|
|
234
281
|
}
|
|
235
282
|
|
|
283
|
+
// R7 for an MCP server (ADR-0079): one with a `secret` needs an https url, because the Custodian
|
|
284
|
+
// carries TLS only. Its url's host is the default binding of the secret it names (`hostsOf`).
|
|
285
|
+
const serverHosts = new Map<string, Array<{ server: string; host: string; port: number; path: string }>>();
|
|
286
|
+
for (const [server, declared] of Object.entries(config.mcp ?? {})) {
|
|
287
|
+
if (isStdioServer(declared) || declared.secret === undefined || !claimed.has(declared.secret)) continue;
|
|
288
|
+
const target = httpsTarget(declared.url);
|
|
289
|
+
if (!target) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`mcp.${server}.url must be an https:// URL — the server has a secret, ${declared.secret}, and the Custodian ` +
|
|
292
|
+
"carries TLS only (ADR-0059, ADR-0079). Read it from .env: " +
|
|
293
|
+
`mcp: { "${server}": { url: process.env.${server.toUpperCase().replace(/-/g, "_")}_URL ?? "", secret: "${declared.secret}" } }`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const path = new URL(declared.url).pathname || "/";
|
|
297
|
+
serverHosts.set(declared.secret, [...(serverHosts.get(declared.secret) ?? []), { server, ...target, path }]);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// jr2's own names (ADR-0076): the Harness's start takes a `JR2_` name from jr2's composition alone,
|
|
301
|
+
// and refuses to start on an envFrom key that starts with `JR2_` (`effector nocore --harness-env`).
|
|
302
|
+
// A user's `JR2_MENU_ONLY` or `JR2_START_ENV` would turn the domain off or replace its env.
|
|
303
|
+
for (const [i, v] of env.entries()) {
|
|
304
|
+
if (v.name.startsWith("JR2_")) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`harness.env[${i}] sets ${v.name}: JR2_ names are jr2's own, and the Harness takes them from jr2 alone ` +
|
|
307
|
+
"(ADR-0076). Name the env var your program reads.",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const [i, ref] of (harness.envFrom ?? []).entries()) {
|
|
312
|
+
const lookup = ref.secretRef
|
|
313
|
+
? `secret/${ref.secretRef.name}`
|
|
314
|
+
: ref.configMapRef
|
|
315
|
+
? `configmap/${ref.configMapRef.name}`
|
|
316
|
+
: "";
|
|
317
|
+
const own = (lookups.envFromKeys?.get(lookup) ?? [])
|
|
318
|
+
.map((key) => (ref.prefix ?? "") + key)
|
|
319
|
+
.find((key) => key.startsWith("JR2_"));
|
|
320
|
+
if (own !== undefined) {
|
|
321
|
+
const where = ref.secretRef ? `Secret "${ref.secretRef.name}"` : `ConfigMap "${ref.configMapRef?.name}"`;
|
|
322
|
+
throw new Error(
|
|
323
|
+
`harness.envFrom[${i}] loads ${where}, which carries ${own}: JR2_ names are jr2's own, and the Harness ` +
|
|
324
|
+
"does not start with one from envFrom (ADR-0076). Drop the key, or give the source a prefix.",
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
236
329
|
// R1: a known model key the Agent could read.
|
|
237
330
|
for (const [i, v] of env.entries()) {
|
|
238
331
|
if (!(v.name in MODEL_KEYS)) continue;
|
|
@@ -268,6 +361,39 @@ export function heldSecretsOf(
|
|
|
268
361
|
}
|
|
269
362
|
}
|
|
270
363
|
|
|
364
|
+
// A credential file that the Harness process reads (ADR-0076): never one that the domain writes.
|
|
365
|
+
for (const [i, v] of env.entries()) {
|
|
366
|
+
if (!CREDENTIAL_FILES.includes(v.name)) continue;
|
|
367
|
+
const path = v.value;
|
|
368
|
+
if (path !== undefined) {
|
|
369
|
+
const writable = DOMAIN_WRITABLE_ROOTS.find((root) => path === root || path.startsWith(`${root}/`));
|
|
370
|
+
if (!path.startsWith("/") || writable !== undefined || path.split("/").includes("..")) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`harness.env[${i}] sets ${v.name} to "${path}", ${writable !== undefined ? `which is in ${writable}, a folder the Agent's code writes` : "which is not an absolute path"}: the Harness process reads that file, and code that writes it acts outside the Agent's confinement (ADR-0076). Point it at a file of the image that the Sandbox's user cannot write.`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
warnings.push(
|
|
377
|
+
`harness.env sets ${v.name}: the Harness process reads ${path !== undefined ? `"${path}"` : "the file it names"}, ` +
|
|
378
|
+
"so the Sandbox's user must not be able to write it — a folder of the image that the user owns is one the " +
|
|
379
|
+
"Agent's code writes (ADR-0076)",
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
for (const [i, ref] of (harness.envFrom ?? []).entries()) {
|
|
383
|
+
const lookup = ref.secretRef
|
|
384
|
+
? `secret/${ref.secretRef.name}`
|
|
385
|
+
: ref.configMapRef
|
|
386
|
+
? `configmap/${ref.configMapRef.name}`
|
|
387
|
+
: "";
|
|
388
|
+
for (const key of (lookups.envFromKeys?.get(lookup) ?? []).map((k) => (ref.prefix ?? "") + k)) {
|
|
389
|
+
if (!CREDENTIAL_FILES.includes(key)) continue;
|
|
390
|
+
warnings.push(
|
|
391
|
+
`harness.envFrom[${i}] sets ${key}: the Harness process reads the file it names, so the Sandbox's user must ` +
|
|
392
|
+
"not be able to write it — a folder of the image that the user owns is one the Agent's code writes (ADR-0076)",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
271
397
|
type Entry = {
|
|
272
398
|
at: string;
|
|
273
399
|
name: string;
|
|
@@ -344,8 +470,10 @@ export function heldSecretsOf(
|
|
|
344
470
|
const source = sourceOf(e, lookups);
|
|
345
471
|
if (source.kind === "literal") values[e.name] = e.value as string;
|
|
346
472
|
|
|
347
|
-
// R6
|
|
348
|
-
|
|
473
|
+
// R6. An MCP server's secret with no binding goes in `authorization` alone (ADR-0079): the
|
|
474
|
+
// Harness sends `Authorization: Bearer <Stand-in>`.
|
|
475
|
+
const mcpDefault = e.hosts === undefined && e.headers === undefined && serverHosts.has(e.name);
|
|
476
|
+
const headers = [...(e.headers ?? (mcpDefault ? ["authorization"] : DEFAULT_CREDENTIAL_HEADERS))];
|
|
349
477
|
const seenHeader = new Set<string>();
|
|
350
478
|
for (const [j, header] of headers.entries()) {
|
|
351
479
|
if (!HEADER_TOKEN.test(header) || FRAMING_HEADERS.has(header) || header.startsWith("proxy-")) {
|
|
@@ -358,6 +486,16 @@ export function heldSecretsOf(
|
|
|
358
486
|
if (seenHeader.has(header)) throw new Error(`${e.at}.headers lists "${header}" twice (ADR-0059)`);
|
|
359
487
|
seenHeader.add(header);
|
|
360
488
|
}
|
|
489
|
+
// R6 for an MCP server's secret (ADR-0079): the Harness sends its Stand-in in `authorization`
|
|
490
|
+
// alone, so a binding without that header removes it on each call.
|
|
491
|
+
for (const s of serverHosts.get(e.name) ?? []) {
|
|
492
|
+
if (headers.includes("authorization")) break;
|
|
493
|
+
throw new Error(
|
|
494
|
+
`${e.at}.headers leaves out "authorization", where the Harness sends the Stand-in of ${e.name} to ` +
|
|
495
|
+
`mcp.${s.server}.url — each call of that server would arrive with no credential. Add it, or drop headers ` +
|
|
496
|
+
"to take that default (ADR-0059, ADR-0079)",
|
|
497
|
+
);
|
|
498
|
+
}
|
|
361
499
|
|
|
362
500
|
// R12
|
|
363
501
|
const paths = e.paths === undefined ? undefined : [...e.paths];
|
|
@@ -369,9 +507,18 @@ export function heldSecretsOf(
|
|
|
369
507
|
);
|
|
370
508
|
}
|
|
371
509
|
}
|
|
510
|
+
// R12 for an MCP server's secret (ADR-0079): every call goes to the url's path, so `paths` must
|
|
511
|
+
// cover it, by the prefix rule the Custodian applies.
|
|
512
|
+
for (const s of serverHosts.get(e.name) ?? []) {
|
|
513
|
+
if (paths === undefined || paths.some((p) => pathCovers(p, s.path))) continue;
|
|
514
|
+
throw new Error(
|
|
515
|
+
`${e.at}.paths does not cover ${s.path}, the path of mcp.${s.server}.url, whose secret ${e.name} is — ` +
|
|
516
|
+
`each call of that server would be refused with 403. Add "${s.path}", or drop paths (ADR-0059, ADR-0079)`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
372
519
|
|
|
373
520
|
// R5 (+ the §3.4 default and R9)
|
|
374
|
-
const hosts = hostsOf(e, catalogHosts, lookups.models ?? []);
|
|
521
|
+
const hosts = hostsOf(e, catalogHosts, lookups.models ?? [], serverHosts.get(e.name) ?? []);
|
|
375
522
|
secrets.push({
|
|
376
523
|
name: e.name,
|
|
377
524
|
source,
|
|
@@ -429,24 +576,6 @@ export function heldSecretsOf(
|
|
|
429
576
|
}
|
|
430
577
|
}
|
|
431
578
|
|
|
432
|
-
// R5: one header on one host names one secret.
|
|
433
|
-
const byTarget = new Map<string, { name: string; headers: string[] }[]>();
|
|
434
|
-
for (const s of secrets) {
|
|
435
|
-
for (const h of s.hosts) {
|
|
436
|
-
const key = `${h.host}:${h.port}`;
|
|
437
|
-
for (const other of byTarget.get(key) ?? []) {
|
|
438
|
-
const shared = s.headers.filter((header) => other.headers.includes(header));
|
|
439
|
-
if (shared.length > 0) {
|
|
440
|
-
throw new Error(
|
|
441
|
-
`${s.name} and ${other.name} are both bound to ${key} in header ${shared.join(", ")} — one header on ` +
|
|
442
|
-
"one host carries one secret; give each its own `headers` (ADR-0059)",
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
byTarget.set(key, [...(byTarget.get(key) ?? []), { name: s.name, headers: s.headers }]);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
579
|
return { manifest: { version: 1, secrets }, values, warnings };
|
|
451
580
|
}
|
|
452
581
|
|
|
@@ -495,16 +624,31 @@ export function sourceOf(
|
|
|
495
624
|
|
|
496
625
|
/** R5 and R9: where one secret may go. A known model key with no `hosts` goes where its provider's
|
|
497
626
|
* calls go — the catalog override's host, else the table's — so the key and the calls read one
|
|
498
|
-
* config and cannot drift.
|
|
627
|
+
* config and cannot drift. An MCP server's secret with no `hosts` goes to the host of the url of
|
|
628
|
+
* each server that names it (ADR-0079), for the same reason. */
|
|
499
629
|
function hostsOf(
|
|
500
630
|
e: { at: string; name: string; hosts?: readonly string[] },
|
|
501
631
|
catalogHosts: ReadonlyMap<string, string>,
|
|
502
632
|
models: readonly string[],
|
|
633
|
+
servers: ReadonlyArray<{ server: string; host: string; port: number }>,
|
|
503
634
|
): Array<{ host: string; port: number }> {
|
|
504
635
|
const known = MODEL_KEYS[e.name];
|
|
505
636
|
const effective = new Map<string, string | undefined>();
|
|
506
637
|
for (const provider of known?.providers ?? []) effective.set(provider, catalogHosts.get(provider) ?? known?.host);
|
|
507
638
|
|
|
639
|
+
if (e.hosts === undefined && servers.length > 0) {
|
|
640
|
+
const out: Array<{ host: string; port: number }> = [];
|
|
641
|
+
const add = (t: { host: string; port: number }) => {
|
|
642
|
+
if (!out.some((o) => o.host === t.host && o.port === t.port)) out.push({ host: t.host, port: t.port });
|
|
643
|
+
};
|
|
644
|
+
for (const s of servers) add(s);
|
|
645
|
+
// A model key that an MCP server also names keeps its provider's host beside the servers'.
|
|
646
|
+
const defaults = new Set(effective.values());
|
|
647
|
+
const only = defaults.size === 1 ? [...defaults][0] : undefined;
|
|
648
|
+
if (only !== undefined) add(parseTarget(`${e.at}.hosts`, only));
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
651
|
+
|
|
508
652
|
if (e.hosts === undefined || e.hosts.length === 0) {
|
|
509
653
|
const defaults = new Set(effective.values());
|
|
510
654
|
const only = defaults.size === 1 ? [...defaults][0] : undefined;
|
|
@@ -527,9 +671,23 @@ function hostsOf(
|
|
|
527
671
|
`or drop hosts to take that default (ADR-0059)`,
|
|
528
672
|
);
|
|
529
673
|
}
|
|
674
|
+
for (const s of servers) {
|
|
675
|
+
if (targets.some((t) => t.host === s.host && t.port === s.port)) continue;
|
|
676
|
+
throw new Error(
|
|
677
|
+
`${e.at}.hosts leaves out ${s.host}${s.port === 443 ? "" : `:${s.port}`}, the host of mcp.${s.server}.url, ` +
|
|
678
|
+
`whose secret ${e.name} is — each call of that server would arrive with the Stand-in and fail with 401. ` +
|
|
679
|
+
"Add it, or drop hosts to take that default (ADR-0059, ADR-0079)",
|
|
680
|
+
);
|
|
681
|
+
}
|
|
530
682
|
return targets;
|
|
531
683
|
}
|
|
532
684
|
|
|
685
|
+
/** Whether the `paths` entry `prefix` lets a request to `path` through: the Custodian's rule — the
|
|
686
|
+
* same path, or `prefix` and then a `/` (or `prefix` ends in `/`). */
|
|
687
|
+
function pathCovers(prefix: string, path: string): boolean {
|
|
688
|
+
return path === prefix || (path.startsWith(prefix) && (prefix.endsWith("/") || path.charAt(prefix.length) === "/"));
|
|
689
|
+
}
|
|
690
|
+
|
|
533
691
|
/** An `https://` URL's host and port, or undefined for anything else. */
|
|
534
692
|
function httpsTarget(url: string): { host: string; port: number } | undefined {
|
|
535
693
|
let parsed: URL;
|
package/src/holds.ts
CHANGED
|
@@ -31,13 +31,16 @@ export type Hold = {
|
|
|
31
31
|
deliveryId: string;
|
|
32
32
|
/** The pick's event name. */
|
|
33
33
|
pick: string;
|
|
34
|
-
/** The Agent slot that picked — the Console's "
|
|
34
|
+
/** The Agent slot that picked — the Console's "outstanding since" names it. */
|
|
35
35
|
agent: string;
|
|
36
|
-
/** ISO time the pick was delivered: the Console's "
|
|
36
|
+
/** ISO time the pick was delivered: the Console's "outstanding since". */
|
|
37
37
|
since: string;
|
|
38
38
|
/** The Sandbox whose token may read the answer (ADR-0013) — the registration's, kept because the
|
|
39
39
|
* answer outlives a registration that a route out of the Agent's state ends. */
|
|
40
40
|
sandbox?: string;
|
|
41
|
+
/** The slot whose stated control requests may read the answer (ADR-0076) — the registration's,
|
|
42
|
+
* kept with `sandbox` for the same reason. */
|
|
43
|
+
slot?: string;
|
|
41
44
|
/** The invoke id of the Agent's state when the pick was made: a re-registration of the same
|
|
42
45
|
* address by another state is another Turn. */
|
|
43
46
|
invokeId?: string;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// `@jr2/orchestrator/hooks/discord`: jr2's Discord kind, for an application's Interactions Endpoint URL.
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { at, defineHookKind, json } from "./index.ts";
|
|
5
|
+
|
|
6
|
+
const user = z.looseObject({ id: z.string() });
|
|
7
|
+
|
|
8
|
+
/** The schema of each event key. A command's name is part of its key, so `.*` covers them all. */
|
|
9
|
+
export const discordEvents = {
|
|
10
|
+
"application_command.*": z.looseObject({
|
|
11
|
+
channel_id: z.string(),
|
|
12
|
+
/** In a guild. */
|
|
13
|
+
member: z.looseObject({ user }).optional(),
|
|
14
|
+
/** In a direct message. */
|
|
15
|
+
user: user.optional(),
|
|
16
|
+
data: z.looseObject({
|
|
17
|
+
name: z.string(),
|
|
18
|
+
options: z
|
|
19
|
+
.array(z.looseObject({ name: z.string(), value: z.union([z.string(), z.number(), z.boolean()]).optional() }))
|
|
20
|
+
.optional(),
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
message_component: z.looseObject({
|
|
24
|
+
member: z.looseObject({ user }).optional(),
|
|
25
|
+
user: user.optional(),
|
|
26
|
+
data: z.looseObject({ custom_id: z.string() }),
|
|
27
|
+
message: z.looseObject({ id: z.string() }),
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const text = (v: unknown): string | undefined => (typeof v === "string" && v !== "" ? v : undefined);
|
|
32
|
+
|
|
33
|
+
/** The interaction types that this kind reads (Discord's numbers). */
|
|
34
|
+
const PING = 1;
|
|
35
|
+
const APPLICATION_COMMAND = 2;
|
|
36
|
+
const MESSAGE_COMPONENT = 3;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* - verifier: `ed25519` over `<X-Signature-Timestamp><raw body>`, `X-Signature-Ed25519` in hex,
|
|
40
|
+
* checked against the application's PUBLIC key in the Orchestrator. The signature covers the
|
|
41
|
+
* timestamp, so the kind gives it as `at`. Discord states no window; the door's 300 s window
|
|
42
|
+
* applies to each signed `at`, so it applies here too.
|
|
43
|
+
* - handshake: `PING` (type 1) gets `{ type: 1 }` (PONG).
|
|
44
|
+
* - event keys: `application_command.<data.name>` (type 2), `message_component` (type 3). Another
|
|
45
|
+
* type (an autocomplete, a modal) gets the key `interaction.<type>`, which has no schema, so it
|
|
46
|
+
* reaches nothing.
|
|
47
|
+
* - delivery id: the interaction's `id`. `onBehalfOf`: `member.user.id`, or `user.id` in a direct
|
|
48
|
+
* message. (Discord's docs do not say that Discord sends an interaction again.)
|
|
49
|
+
* - who reads the answer: a person, always.
|
|
50
|
+
* - answer: `{ type: 4, data: { content, flags: 64 } }`, an ephemeral message. Discord shows
|
|
51
|
+
* markdown, not HTML, so the text needs no escape. Text limit: 2000 characters.
|
|
52
|
+
*/
|
|
53
|
+
export const discord = defineHookKind({
|
|
54
|
+
api: 1,
|
|
55
|
+
id: "discord",
|
|
56
|
+
events: discordEvents,
|
|
57
|
+
verify: {
|
|
58
|
+
ed25519: {
|
|
59
|
+
signed: (req) => {
|
|
60
|
+
const ts = req.headers["x-signature-timestamp"];
|
|
61
|
+
const sig = req.headers["x-signature-ed25519"];
|
|
62
|
+
if (ts === undefined || !/^\d+$/.test(ts) || sig === undefined) {
|
|
63
|
+
return { unsigned: "no Discord signature or timestamp" };
|
|
64
|
+
}
|
|
65
|
+
return { bytes: [ts, req.body], signature: { hex: sig }, at: Number(ts) };
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
read: (req) => {
|
|
70
|
+
const p = json(req);
|
|
71
|
+
const type = at(p, "type");
|
|
72
|
+
if (type === PING) return { handshake: { body: { type: 1 } } };
|
|
73
|
+
const id = text(at(p, "id"));
|
|
74
|
+
if (id === undefined) throw new Error("the interaction has no id");
|
|
75
|
+
const by = text(at(p, "member.user.id")) ?? text(at(p, "user.id"));
|
|
76
|
+
const key =
|
|
77
|
+
type === APPLICATION_COMMAND
|
|
78
|
+
? `application_command.${text(at(p, "data.name")) ?? ""}`
|
|
79
|
+
: type === MESSAGE_COMPONENT
|
|
80
|
+
? "message_component"
|
|
81
|
+
: `interaction.${String(type)}`;
|
|
82
|
+
return { key, id, payload: p, ...(by ? { onBehalfOf: by } : {}), readBy: "person" };
|
|
83
|
+
},
|
|
84
|
+
answer: (t) => ({ body: { type: 4, data: { content: t, flags: 64 } } }),
|
|
85
|
+
maxText: 2000,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
declare module "./index.ts" {
|
|
89
|
+
interface HookKinds {
|
|
90
|
+
discord: typeof discord;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// `hook-kinds/door.ts`: what the Hook's door reads from a Hook and a kind. The package does not
|
|
2
|
+
// export it, because it is NOT part of a kind's interface: a kind imports `@jr2/orchestrator/hooks` only. It is here because
|
|
3
|
+
// `defineHookKind` decides the shape of a Hook, so the code that reads a Hook is beside it.
|
|
4
|
+
|
|
5
|
+
import type { z } from "zod";
|
|
6
|
+
import { HOOK } from "./hook.ts";
|
|
7
|
+
import { HOOK_KIND_APIS, type Hook, type HookKind, type HookParts, type HookRequest, type Signed } from "./index.ts";
|
|
8
|
+
|
|
9
|
+
/** True when `value` is a Hook that a kind's constructor made. */
|
|
10
|
+
export function isHook(value: unknown): value is Hook {
|
|
11
|
+
return typeof value === "object" && value !== null && Object.hasOwn(value, HOOK);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The kind, the credential and `may` of a Hook. It throws when `hook` is not a Hook. */
|
|
15
|
+
export function hookParts<K extends HookKind>(hook: Hook<K>): HookParts<K> {
|
|
16
|
+
if (!isHook(hook)) throw new Error("not a Hook: make it with its kind, such as github({ secret, may })");
|
|
17
|
+
return hook[HOOK];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The request that the door gives a kind. Header names become lowercase. A header with more than
|
|
22
|
+
* one value keeps them joined by ", ", as `Headers` does. `strip` names a header that the door
|
|
23
|
+
* removes (the `token` verifier's header), so no code of the kind can read it.
|
|
24
|
+
*/
|
|
25
|
+
export function hookRequest(
|
|
26
|
+
headers: Headers | { readonly [name: string]: string | readonly string[] | undefined },
|
|
27
|
+
body: Uint8Array,
|
|
28
|
+
strip?: string,
|
|
29
|
+
): HookRequest {
|
|
30
|
+
const out: { [name: string]: string } = Object.create(null) as { [name: string]: string };
|
|
31
|
+
const entries: Iterable<[string, string | readonly string[] | undefined]> =
|
|
32
|
+
headers instanceof Headers ? headers.entries() : Object.entries(headers);
|
|
33
|
+
for (const [name, value] of entries) {
|
|
34
|
+
if (value === undefined) continue;
|
|
35
|
+
const lower = name.toLowerCase();
|
|
36
|
+
const text = typeof value === "string" ? value : value.join(", ");
|
|
37
|
+
out[lower] = Object.hasOwn(out, lower) ? `${out[lower]}, ${text}` : text;
|
|
38
|
+
}
|
|
39
|
+
if (strip !== undefined) delete out[strip.toLowerCase()];
|
|
40
|
+
const bytes = new Uint8Array(body);
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
headers: Object.freeze(out),
|
|
43
|
+
body: bytes,
|
|
44
|
+
text: new TextDecoder("utf-8").decode(bytes),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The schema of an event key: the exact key, else the longest `prefix.*` key that the event key
|
|
50
|
+
* starts with (`block_actions.*` for `block_actions.approve`). `undefined` when the kind names no
|
|
51
|
+
* schema for the key: the door does not parse such a delivery, and it reaches nothing.
|
|
52
|
+
*/
|
|
53
|
+
export function schemaFor(kind: HookKind, key: string): z.ZodType | undefined {
|
|
54
|
+
if (Object.hasOwn(kind.events, key)) return kind.events[key];
|
|
55
|
+
let best: { readonly length: number; readonly schema: z.ZodType } | undefined;
|
|
56
|
+
for (const [k, schema] of Object.entries(kind.events)) {
|
|
57
|
+
if (!k.endsWith(".*")) continue;
|
|
58
|
+
const prefix = k.slice(0, -1); // keeps the "."
|
|
59
|
+
if (key.length > prefix.length && key.startsWith(prefix) && (!best || prefix.length > best.length)) {
|
|
60
|
+
best = { length: prefix.length, schema };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return best?.schema;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The bytes that the service signed, joined. A string part is UTF-8. */
|
|
67
|
+
export function signedBytes(signed: Signed): Uint8Array {
|
|
68
|
+
const parts = signed.bytes.map((b) => (typeof b === "string" ? new TextEncoder().encode(b) : b));
|
|
69
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
70
|
+
let i = 0;
|
|
71
|
+
for (const p of parts) {
|
|
72
|
+
out.set(p, i);
|
|
73
|
+
i += p.length;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The signature as bytes, or `undefined` when its hex or base64 is not well formed. */
|
|
79
|
+
export function signatureBytes(signed: Signed): Uint8Array | undefined {
|
|
80
|
+
const s = signed.signature;
|
|
81
|
+
if ("hex" in s) {
|
|
82
|
+
if (!/^(?:[0-9a-fA-F]{2})+$/.test(s.hex)) return undefined;
|
|
83
|
+
return Uint8Array.from(Buffer.from(s.hex, "hex"));
|
|
84
|
+
}
|
|
85
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(s.base64) || s.base64.length % 4 !== 0) return undefined;
|
|
86
|
+
return Uint8Array.from(Buffer.from(s.base64, "base64"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The kinds of one Assembly, by id. It throws when two different kinds give one id, or when a kind
|
|
91
|
+
* states an `api` that this copy of `@jr2/orchestrator/hooks` does not know. One kind that two Hooks use is one
|
|
92
|
+
* entry.
|
|
93
|
+
*/
|
|
94
|
+
export function kindsById(kinds: Iterable<HookKind>): ReadonlyMap<string, HookKind> {
|
|
95
|
+
const out = new Map<string, HookKind>();
|
|
96
|
+
for (const kind of kinds) {
|
|
97
|
+
if (!HOOK_KIND_APIS.includes(kind.api)) {
|
|
98
|
+
throw new Error(`Hook kind ${kind.id} states api ${String(kind.api)}; this jr2 knows api 1 only`);
|
|
99
|
+
}
|
|
100
|
+
const had = out.get(kind.id);
|
|
101
|
+
if (had !== undefined && had !== kind) {
|
|
102
|
+
throw new Error(`two different Hook kinds have the id ${kind.id}: one id is one kind in an Assembly`);
|
|
103
|
+
}
|
|
104
|
+
out.set(kind.id, kind);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// `@jr2/orchestrator/hooks/github`: jr2's GitHub kind. It imports `@jr2/orchestrator/hooks` and nothing more of jr2, as a
|
|
2
|
+
// kind from another package does.
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { defineHookKind, json, type HookRequest } from "./index.ts";
|
|
6
|
+
|
|
7
|
+
const user = z.looseObject({ login: z.string() });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The schema of each event key. Each names only the fields that handlers read; a loose object keeps
|
|
11
|
+
* the rest. A value that GitHub can extend (a review's `state`) is a string, not an enum, so a new
|
|
12
|
+
* value fails no parse.
|
|
13
|
+
*/
|
|
14
|
+
export const githubEvents = {
|
|
15
|
+
"pull_request_review.submitted": z.looseObject({
|
|
16
|
+
review: z.looseObject({
|
|
17
|
+
/** Lowercase in a webhook: `approved`, `changes_requested`, `commented`. */
|
|
18
|
+
state: z.string(),
|
|
19
|
+
body: z.string().nullable(),
|
|
20
|
+
user,
|
|
21
|
+
commit_id: z.string(),
|
|
22
|
+
}),
|
|
23
|
+
pull_request: z.looseObject({ html_url: z.string() }),
|
|
24
|
+
}),
|
|
25
|
+
"issue_comment.created": z.looseObject({
|
|
26
|
+
comment: z.looseObject({ body: z.string(), user, author_association: z.string() }),
|
|
27
|
+
issue: z.looseObject({
|
|
28
|
+
/** Only when the issue is a pull request. */
|
|
29
|
+
pull_request: z.looseObject({ html_url: z.string() }).optional(),
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
32
|
+
"check_suite.completed": z.looseObject({
|
|
33
|
+
check_suite: z.looseObject({
|
|
34
|
+
head_sha: z.string(),
|
|
35
|
+
/** `success`, `failure`, `neutral`, `cancelled`, `timed_out`, …; `null` while it runs. */
|
|
36
|
+
conclusion: z.string().nullable(),
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const header = (req: HookRequest, name: string): string => {
|
|
42
|
+
const v = req.headers[name];
|
|
43
|
+
if (v === undefined || v === "") throw new Error(`no ${name} header`);
|
|
44
|
+
return v;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A GitHub repository, organization or App webhook, with the content type `application/json`. (A
|
|
49
|
+
* webhook with the content type `application/x-www-form-urlencoded` gets 400.)
|
|
50
|
+
* - verifier: `mac`, HMAC-SHA256 of the raw body, `X-Hub-Signature-256: sha256=<hex>`.
|
|
51
|
+
* - handshake: the `ping` event (sent when the webhook is made) gets an empty 200.
|
|
52
|
+
* - event key: `<X-GitHub-Event>.<action>`, or `<X-GitHub-Event>` when the body has no action.
|
|
53
|
+
* - delivery id: `X-GitHub-Delivery`. `onBehalfOf`: `sender.login`.
|
|
54
|
+
* - answer: the outcome (the default). No person reads it.
|
|
55
|
+
*/
|
|
56
|
+
export const github = defineHookKind({
|
|
57
|
+
api: 1,
|
|
58
|
+
id: "github",
|
|
59
|
+
events: githubEvents,
|
|
60
|
+
verify: {
|
|
61
|
+
mac: {
|
|
62
|
+
alg: "sha256",
|
|
63
|
+
signed: (req) => {
|
|
64
|
+
const sig = req.headers["x-hub-signature-256"];
|
|
65
|
+
if (!sig?.startsWith("sha256=")) return { unsigned: "no X-Hub-Signature-256 header" };
|
|
66
|
+
return { bytes: [req.body], signature: { hex: sig.slice("sha256=".length) } };
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
read: (req) => {
|
|
71
|
+
const event = header(req, "x-github-event");
|
|
72
|
+
if (event === "ping") return { handshake: { body: null } };
|
|
73
|
+
const p = json(req) as { readonly action?: unknown; readonly sender?: { readonly login?: unknown } } | null;
|
|
74
|
+
const action = typeof p?.action === "string" && p.action !== "" ? p.action : undefined;
|
|
75
|
+
const by = p?.sender?.login;
|
|
76
|
+
return {
|
|
77
|
+
key: action ? `${event}.${action}` : event,
|
|
78
|
+
id: header(req, "x-github-delivery"),
|
|
79
|
+
payload: p,
|
|
80
|
+
...(typeof by === "string" && by !== "" ? { onBehalfOf: by } : {}),
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
declare module "./index.ts" {
|
|
86
|
+
interface HookKinds {
|
|
87
|
+
github: typeof github;
|
|
88
|
+
}
|
|
89
|
+
}
|