@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/secret-placement.ts
CHANGED
|
@@ -3,13 +3,20 @@
|
|
|
3
3
|
//
|
|
4
4
|
// - a Workspace's pod: the Held secrets its `workspace()` lists under `secrets: [...]`, and the
|
|
5
5
|
// model keys of the Agents whose Turns run there — a model key is claimed by the PROVIDER;
|
|
6
|
-
// - the Instance Harness: the model keys of the `"none"` Agents it hosts, and
|
|
7
|
-
//
|
|
6
|
+
// - the Instance Harness: the model keys of the `"none"` Agents it hosts, and the secrets of the
|
|
7
|
+
// remote MCP servers they name (ADR-0079), and nothing else (they cannot execute code, ADR-0031,
|
|
8
|
+
// so nothing else is theirs to use);
|
|
9
|
+
// - a Workspace's pod also holds the secret of each MCP server an Agent there names (ADR-0079):
|
|
10
|
+
// "a Custodian holds what its pod's Agents name";
|
|
8
11
|
// - an Effector: what its Effects name with `secret("name")` — the Instance Effector what its
|
|
9
12
|
// `http` Effects name and the token of each `dispatch` with a target (ADR-0071; a dispatch with
|
|
10
13
|
// no target runs in the Orchestrator's own process and names nothing); a Workspace's Sandbox Effector what its file-body `http`s and
|
|
11
14
|
// credentialed `command`s name. The push credential its `push`es spend is not placed here: the provision
|
|
12
|
-
// names the entry the slot's url matches on the Sandbox, by reference (git-push.ts, ADR-0051)
|
|
15
|
+
// names the entry the slot's url matches on the Sandbox, by reference (git-push.ts, ADR-0051);
|
|
16
|
+
// - the Instance Effector also holds the secret of each Hook with a `mac` verifier, and its fence
|
|
17
|
+
// takes each host that a Hook's kind declares in `posts`, with no secret (ADR-0068, ADR-0078);
|
|
18
|
+
// - the Orchestrator gets only the sha-256 digest of the secret of each Hook with a `token`
|
|
19
|
+
// verifier, in `jr2-hook-digests`, and never the value (ADR-0078).
|
|
13
20
|
//
|
|
14
21
|
// `hosts`, `headers` and `paths` are an entry's BINDING — where a Custodian may put the value — and
|
|
15
22
|
// never its placement. Both is both, and `jr2 up` reports it per pod; an entry nothing names goes
|
|
@@ -19,6 +26,7 @@
|
|
|
19
26
|
// the list does not hold, and an Effect secret shorter than 8 characters (ADR-0068): the Effector
|
|
20
27
|
// replaces every match of a secret in what it returns, and a short one would match ordinary text.
|
|
21
28
|
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
22
30
|
import {
|
|
23
31
|
providerClaims,
|
|
24
32
|
sourceOf,
|
|
@@ -27,7 +35,8 @@ import {
|
|
|
27
35
|
type HeldBinding,
|
|
28
36
|
type HeldLookups,
|
|
29
37
|
} from "./held-secrets.ts";
|
|
30
|
-
import type
|
|
38
|
+
import { isStdioServer, mcpTimeoutMs, type JR2Config, type McpRemoteServer } from "./config.ts";
|
|
39
|
+
import { hookParts } from "./hook-kinds/door.ts";
|
|
31
40
|
import { needsASlot } from "./effect.ts";
|
|
32
41
|
import { declaredPushKinds, pushRefusal } from "./git-push.ts";
|
|
33
42
|
import { INSTANCE_HARNESS_HELD_POD } from "./names.ts";
|
|
@@ -42,6 +51,10 @@ export type EffectSecret = { name: string; source: HeldBinding["source"] };
|
|
|
42
51
|
/** One target an Effect placed in an Effector may call, with the secrets it names there. */
|
|
43
52
|
export type EffectorTargetConfig = { scheme: "http" | "https"; host: string; port: number; secrets: string[] };
|
|
44
53
|
|
|
54
|
+
/** One MCP server an `mcp` Effect placed in the Instance Effector calls (ADR-0079): `effector.json`'s
|
|
55
|
+
* `mcp` entry — its url, the name of its secret, and its `timeout` in milliseconds. Never a value. */
|
|
56
|
+
export type EffectorMcpServer = { url: string; secret?: string; timeoutMs: number };
|
|
57
|
+
|
|
45
58
|
/** What one Effector placement holds and allows — what `jr2 up` converges from. */
|
|
46
59
|
export type EffectorPlan = {
|
|
47
60
|
placement: "instance";
|
|
@@ -51,12 +64,22 @@ export type EffectorPlan = {
|
|
|
51
64
|
values: Record<string, string>;
|
|
52
65
|
/** `effector.json`'s targets: one per distinct (target, secrets) an Effect placed there names. */
|
|
53
66
|
targets: EffectorTargetConfig[];
|
|
67
|
+
/** `effector.json`'s `mcp`: each server an `mcp` Effect placed there names, by its name in the
|
|
68
|
+
* `mcp` block (ADR-0079). Empty when there is none. */
|
|
69
|
+
mcp: Record<string, EffectorMcpServer>;
|
|
54
70
|
/** The longest timeout among the Effects placed there, in milliseconds. */
|
|
55
71
|
longestTimeoutMs: number;
|
|
56
72
|
/** The Effects placed there, as `<workflow>/<slot>`. */
|
|
57
73
|
effects: string[];
|
|
74
|
+
/** The Hooks it serves (ADR-0068): each one with a `mac` verifier, or whose kind declares
|
|
75
|
+
* `posts`, by name. */
|
|
76
|
+
hooks: string[];
|
|
58
77
|
};
|
|
59
78
|
|
|
79
|
+
/** The secret of a Hook with a `token` verifier (ADR-0078): `jr2 up` writes the sha-256 digest of
|
|
80
|
+
* its value into `jr2-hook-digests`, from the literal or from the key of the user's Secret. */
|
|
81
|
+
export type HookTokenSecret = { name: string } & ({ value: string } | { secret: string; key: string });
|
|
82
|
+
|
|
60
83
|
/** One registered Machine's Effect, as `jr2 up` walked it. */
|
|
61
84
|
export type PlacedEffect = { workflow: string; effect: CarriedEffect };
|
|
62
85
|
|
|
@@ -67,8 +90,9 @@ export type PlacedWorkspace = { workflow: string; pod: WorkspacePod };
|
|
|
67
90
|
export type PlacementWalk = {
|
|
68
91
|
effects: readonly PlacedEffect[];
|
|
69
92
|
workspaces: readonly PlacedWorkspace[];
|
|
70
|
-
/** The models of the `"none"` Agents
|
|
71
|
-
|
|
93
|
+
/** The models of the `"none"` Agents, and the MCP servers they name (ADR-0079) — present when
|
|
94
|
+
* the Instance Harness is converged. */
|
|
95
|
+
instanceHarness?: { models: readonly string[]; servers?: readonly string[] };
|
|
72
96
|
};
|
|
73
97
|
|
|
74
98
|
/** One Harness pod's Held secrets: what its Custodian holds. */
|
|
@@ -79,8 +103,11 @@ export type HeldPodPlan = {
|
|
|
79
103
|
label: string;
|
|
80
104
|
/** The names it holds, in list order; the custom provider's key last. */
|
|
81
105
|
names: string[];
|
|
82
|
-
/** Of `names`, those a provider claims (the rest the `workspace()` lists). */
|
|
106
|
+
/** Of `names`, those a provider claims (the rest the `workspace()` lists, or an MCP server). */
|
|
83
107
|
byProvider: string[];
|
|
108
|
+
/** Of `names`, the secrets of the MCP servers its Agents name (ADR-0079), each with those
|
|
109
|
+
* servers. Absent when there is none. */
|
|
110
|
+
byServer?: Record<string, string[]>;
|
|
84
111
|
};
|
|
85
112
|
|
|
86
113
|
/** One push of a Workspace (ADR-0067, ADR-0051): its Repo Slot, whether it signs, and — for a slot the
|
|
@@ -120,10 +147,12 @@ export type SecretPlacements = {
|
|
|
120
147
|
held: HeldPodPlan[];
|
|
121
148
|
/** Every name some pod holds — what `heldSecretsOf` normalizes. */
|
|
122
149
|
claimed: Set<string>;
|
|
123
|
-
/** The Instance Effector, when an Effect is placed there; absent otherwise. */
|
|
150
|
+
/** The Instance Effector, when an Effect is placed there or a Hook needs it; absent otherwise. */
|
|
124
151
|
instance?: EffectorPlan;
|
|
125
152
|
/** Each Workspace whose body places an Effect in its Sandbox Effector. */
|
|
126
153
|
sandbox: SandboxEffectorPlan[];
|
|
154
|
+
/** The secrets of the `token` Hooks, in list order: only their digests reach the Orchestrator. */
|
|
155
|
+
hookTokens: HookTokenSecret[];
|
|
127
156
|
/** One line per secret, then one per pod: where each goes, what each holds. */
|
|
128
157
|
report: string[];
|
|
129
158
|
warnings: string[];
|
|
@@ -151,8 +180,8 @@ export function refuseEffectSecrets(config: JR2Config, walk: PlacementWalk): voi
|
|
|
151
180
|
/**
|
|
152
181
|
* A Workspace with no Repo Slot (ADR-0051) has no branch and no commit, so the two Sandbox Effector kinds that
|
|
153
182
|
* run on a commit of a slot's branch have none: a `push`, and a `command` with an Effect secret
|
|
154
|
-
* (ADR-0067).
|
|
155
|
-
* file-body `http` reads `/work` and stays.
|
|
183
|
+
* (ADR-0067). A `worktree()` has no clone to cut from (ADR-0075). Refused here, before anything is
|
|
184
|
+
* built, as the invoke would refuse them mid-run. A file-body `http` reads `/work` and stays.
|
|
156
185
|
*/
|
|
157
186
|
function refuseWithNoSlot(ws: PlacedWorkspace): void {
|
|
158
187
|
if (!ws.pod.noSlot) return;
|
|
@@ -165,6 +194,13 @@ function refuseWithNoSlot(ws: PlacedWorkspace): void {
|
|
|
165
194
|
`Slot: ${why} (ADR-0051)`,
|
|
166
195
|
);
|
|
167
196
|
}
|
|
197
|
+
const [cut] = ws.pod.cuts ?? [];
|
|
198
|
+
if (cut !== undefined) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`workflow "${ws.workflow}": worktree "${cut}" runs in ${workspaceLabel(ws)}, which has no Repo Slot: ` +
|
|
201
|
+
`${needsASlot("worktree")} (ADR-0075)`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
168
204
|
}
|
|
169
205
|
|
|
170
206
|
/**
|
|
@@ -177,7 +213,7 @@ export function secretPlacementsOf(
|
|
|
177
213
|
walk: PlacementWalk,
|
|
178
214
|
lookups: Pick<HeldLookups, "secretKeys" | "valueLength"> = {},
|
|
179
215
|
): SecretPlacements {
|
|
180
|
-
const { entries, namedBy, instancePlaced } = namesOf(config, walk);
|
|
216
|
+
const { entries, namedBy, instancePlaced, hooks, tokenHooks } = namesOf(config, walk);
|
|
181
217
|
const { pods, redundant } = heldPodsOf(config, walk, new Set(entries.map((e) => e.name)));
|
|
182
218
|
const claimed = new Set(pods.flatMap((p) => p.names));
|
|
183
219
|
|
|
@@ -185,6 +221,7 @@ export function secretPlacementsOf(
|
|
|
185
221
|
const warnings: string[] = [];
|
|
186
222
|
const secrets: EffectSecret[] = [];
|
|
187
223
|
const values: Record<string, string> = {};
|
|
224
|
+
const hookTokens: HookTokenSecret[] = [];
|
|
188
225
|
for (const e of entries) {
|
|
189
226
|
const named = namedBy.get(e.name) ?? [];
|
|
190
227
|
if (named.length > 0) {
|
|
@@ -208,10 +245,21 @@ export function secretPlacementsOf(
|
|
|
208
245
|
)})`,
|
|
209
246
|
);
|
|
210
247
|
const where = [...(heldIn.length ? [`Held in ${heldIn.join(", ")}`] : []), ...effectIn];
|
|
248
|
+
// A `token` Hook names its secret: the Orchestrator gets its digest, never its value (ADR-0078).
|
|
249
|
+
const byToken = tokenHooks.get(e.name);
|
|
250
|
+
if (byToken !== undefined) {
|
|
251
|
+
const source = sourceOf(e, lookups);
|
|
252
|
+
hookTokens.push(
|
|
253
|
+
source.kind === "literal"
|
|
254
|
+
? { name: e.name, value: e.value as string }
|
|
255
|
+
: { name: e.name, secret: source.secret, key: source.key },
|
|
256
|
+
);
|
|
257
|
+
where.push(`a digest in the Orchestrator (${byToken.map((h) => `hook ${h}`).join(", ")})`);
|
|
258
|
+
}
|
|
211
259
|
if (where.length === 0) {
|
|
212
260
|
warnings.push(
|
|
213
261
|
`${e.at} (${e.name}) is named by nothing; goes nowhere — no workspace() lists it, no Agent's provider ` +
|
|
214
|
-
"reads it, and no Effect names it (ADR-0059, ADR-0068)",
|
|
262
|
+
"reads it, and no Effect or Hook names it (ADR-0059, ADR-0068)",
|
|
215
263
|
);
|
|
216
264
|
continue;
|
|
217
265
|
}
|
|
@@ -224,21 +272,32 @@ export function secretPlacementsOf(
|
|
|
224
272
|
);
|
|
225
273
|
}
|
|
226
274
|
for (const p of pods) {
|
|
227
|
-
const holds = p.names.map((n) =>
|
|
275
|
+
const holds = p.names.map((n) =>
|
|
276
|
+
p.byProvider.includes(n)
|
|
277
|
+
? `${n} (provider)`
|
|
278
|
+
: p.byServer?.[n] !== undefined
|
|
279
|
+
? `${n} (mcp ${p.byServer[n]!.join(", ")})`
|
|
280
|
+
: n,
|
|
281
|
+
);
|
|
228
282
|
report.push(`${p.label}: Custodian holds ${holds.length ? holds.join(", ") : "nothing"}`);
|
|
229
283
|
}
|
|
230
284
|
for (const { label, name } of redundant) {
|
|
231
285
|
report.push(`${label}: secrets lists ${name}, which its Agents' provider claims already — redundant`);
|
|
232
286
|
}
|
|
233
287
|
|
|
234
|
-
const instanceSecrets = new Set(
|
|
288
|
+
const instanceSecrets = new Set([
|
|
289
|
+
...instancePlaced.flatMap((p) => effectSecretsOf(config, p.effect)),
|
|
290
|
+
...hooks.flatMap((h) => (h.secret === undefined ? [] : [h.secret])),
|
|
291
|
+
]);
|
|
235
292
|
const instance =
|
|
236
|
-
instancePlaced.length === 0
|
|
293
|
+
instancePlaced.length === 0 && hooks.length === 0
|
|
237
294
|
? undefined
|
|
238
295
|
: effectorPlan(
|
|
239
296
|
instancePlaced,
|
|
297
|
+
hooks,
|
|
240
298
|
secrets.filter((s) => instanceSecrets.has(s.name)),
|
|
241
299
|
values,
|
|
300
|
+
config,
|
|
242
301
|
);
|
|
243
302
|
if (instance) {
|
|
244
303
|
report.push(
|
|
@@ -268,7 +327,7 @@ export function secretPlacementsOf(
|
|
|
268
327
|
`${pushes.length ? `; pushes ${pushes.join(", ")}` : ""}`,
|
|
269
328
|
);
|
|
270
329
|
}
|
|
271
|
-
return { held: pods, claimed, ...(instance ? { instance } : {}), sandbox, report, warnings };
|
|
330
|
+
return { held: pods, claimed, ...(instance ? { instance } : {}), sandbox, hookTokens, report, warnings };
|
|
272
331
|
}
|
|
273
332
|
|
|
274
333
|
/** A Workspace's Sandbox Effector: the secrets its Effects name, the targets its file-body https call, its
|
|
@@ -347,34 +406,123 @@ function pushesOf(config: JR2Config, ws: PlacedWorkspace): SandboxPush[] {
|
|
|
347
406
|
return out;
|
|
348
407
|
}
|
|
349
408
|
|
|
350
|
-
function effectorPlan(
|
|
409
|
+
function effectorPlan(
|
|
410
|
+
placed: InstancePlaced[],
|
|
411
|
+
hooks: InstanceHook[],
|
|
412
|
+
secrets: EffectSecret[],
|
|
413
|
+
values: Record<string, string>,
|
|
414
|
+
config: JR2Config,
|
|
415
|
+
): EffectorPlan {
|
|
351
416
|
const targets: EffectorTargetConfig[] = [];
|
|
417
|
+
const add = (t: EffectorTargetConfig): void => {
|
|
418
|
+
const same = (x: EffectorTargetConfig) =>
|
|
419
|
+
x.scheme === t.scheme && x.host === t.host && x.port === t.port && x.secrets.join() === t.secrets.join();
|
|
420
|
+
if (!targets.some(same)) targets.push(t);
|
|
421
|
+
};
|
|
422
|
+
const mcp: Record<string, EffectorMcpServer> = {};
|
|
423
|
+
const timeouts: number[] = [];
|
|
352
424
|
for (const { effect } of placed) {
|
|
425
|
+
if (effect.kind === "mcp") {
|
|
426
|
+
// The server's url and secret are the config's (ADR-0079). An Open server, and one the block
|
|
427
|
+
// does not declare as a remote server, are refused before this (`mcpRefusals`).
|
|
428
|
+
const server = remoteServerOf(config, effect.server);
|
|
429
|
+
if (server === undefined) continue;
|
|
430
|
+
const timeoutMs = mcpTimeoutMs(server.timeout) ?? 0;
|
|
431
|
+
mcp[effect.server!] = { url: server.url, ...(server.secret ? { secret: server.secret } : {}), timeoutMs };
|
|
432
|
+
add({ ...originOf(server.url), secrets: server.secret ? [server.secret] : [] });
|
|
433
|
+
timeouts.push(effect.timeoutMs > 0 ? effect.timeoutMs : timeoutMs);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (effect.kind === "http") timeouts.push(effect.timeoutMs);
|
|
353
437
|
// An Open url or an Open dispatch target has no target, so it allows nothing: `jr2 up` refuses
|
|
354
438
|
// it before this (ADR-0054). A dispatch's target is its origin, its one secret the token
|
|
355
439
|
// (ADR-0071).
|
|
356
440
|
if (effect.target === undefined) continue;
|
|
357
|
-
|
|
358
|
-
const same = (x: EffectorTargetConfig) =>
|
|
359
|
-
x.scheme === t.scheme && x.host === t.host && x.port === t.port && x.secrets.join() === t.secrets.join();
|
|
360
|
-
if (!targets.some(same)) targets.push(t);
|
|
441
|
+
add({ ...effect.target, secrets: [...effect.secrets].sort() });
|
|
361
442
|
}
|
|
443
|
+
// A Hook's follow-up is one `http` call with no secret, to a host its kind declares (ADR-0068).
|
|
444
|
+
for (const hook of hooks) for (const post of hook.posts) add({ ...post, secrets: [] });
|
|
362
445
|
return {
|
|
363
446
|
placement: "instance",
|
|
364
447
|
secrets,
|
|
365
448
|
values,
|
|
366
449
|
targets,
|
|
367
|
-
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
|
|
450
|
+
mcp,
|
|
451
|
+
// The grace a rollout gives a replica covers the http and mcp Effects alone (ADR-0071): a
|
|
452
|
+
// dispatch is idempotent by construction, so a `wait` a rollout cuts is sent again under its
|
|
453
|
+
// key and its poll resumes, and a wait of hours never stretches the Effector's grace. An mcp
|
|
454
|
+
// Effect with no timeout of its own waits for its server's (ADR-0079).
|
|
455
|
+
longestTimeoutMs: Math.max(0, ...timeouts),
|
|
371
456
|
effects: placed.map((p) => `${p.workflow}/${p.effect.name}`),
|
|
457
|
+
hooks: hooks.map((h) => h.name),
|
|
372
458
|
};
|
|
373
459
|
}
|
|
374
460
|
|
|
375
|
-
/**
|
|
376
|
-
*
|
|
377
|
-
type
|
|
461
|
+
/** A Hook the Instance Effector serves (ADR-0068): its `mac` secret's name, when its verifier is
|
|
462
|
+
* `mac`, and the targets of its kind's `posts`. */
|
|
463
|
+
type InstanceHook = { name: string; secret?: string; posts: Array<Omit<EffectorTargetConfig, "secrets">> };
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* The Hooks the Instance Effector serves (ADR-0068, ADR-0078): each one with a `mac` verifier, whose
|
|
467
|
+
* secret only that Effector holds, and each one whose kind declares `posts`, whose follow-up it
|
|
468
|
+
* sends. A `token` Hook's secret is a digest in the Orchestrator, and an `ed25519` Hook holds a
|
|
469
|
+
* public key: neither needs the Effector unless its kind posts.
|
|
470
|
+
*/
|
|
471
|
+
function instanceHooksOf(config: JR2Config): InstanceHook[] {
|
|
472
|
+
const out: InstanceHook[] = [];
|
|
473
|
+
for (const [name, hook] of Object.entries(config.hooks ?? {})) {
|
|
474
|
+
const { kind, credential } = hookParts(hook);
|
|
475
|
+
const secret = "mac" in kind.verify && "secret" in credential ? credential.secret.secret : undefined;
|
|
476
|
+
const posts = (kind.posts ?? []).map((prefix) => {
|
|
477
|
+
const url = new URL(prefix);
|
|
478
|
+
return { scheme: "https" as const, host: url.hostname, port: url.port === "" ? 443 : Number(url.port) };
|
|
479
|
+
});
|
|
480
|
+
if (secret === undefined && posts.length === 0) continue;
|
|
481
|
+
out.push({ name, ...(secret !== undefined ? { secret } : {}), posts });
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** The Hooks with a `token` verifier, by the name of the secret each names (ADR-0078). */
|
|
487
|
+
function tokenHooksOf(config: JR2Config): Map<string, string[]> {
|
|
488
|
+
const out = new Map<string, string[]>();
|
|
489
|
+
for (const [name, hook] of Object.entries(config.hooks ?? {})) {
|
|
490
|
+
const { kind, credential } = hookParts(hook);
|
|
491
|
+
if (!("token" in kind.verify) || !("secret" in credential)) continue;
|
|
492
|
+
const secret = credential.secret.secret;
|
|
493
|
+
out.set(secret, [...(out.get(secret) ?? []), name]);
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** An Effect the Instance Effector runs: an http one with no Worktree file, a dispatch with a
|
|
499
|
+
* target, or an mcp one (ADR-0068, ADR-0071, ADR-0079). */
|
|
500
|
+
type InstancePlaced = { workflow: string; effect: Extract<CarriedEffect, { kind: "http" | "dispatch" | "mcp" }> };
|
|
501
|
+
|
|
502
|
+
/** The remote server of the `mcp` block that an `mcp` Effect names, or undefined: an Open server,
|
|
503
|
+
* one the block does not declare, or a stdio one (ADR-0079). */
|
|
504
|
+
function remoteServerOf(config: JR2Config, name: string | undefined): McpRemoteServer | undefined {
|
|
505
|
+
if (name === undefined || !Object.hasOwn(config.mcp ?? {}, name)) return undefined;
|
|
506
|
+
const server = config.mcp![name]!;
|
|
507
|
+
return isStdioServer(server) ? undefined : server;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** The secrets an Effect names, as the placement reads them: its own, or for an `mcp` Effect its
|
|
511
|
+
* server's, from the config (ADR-0079). */
|
|
512
|
+
function effectSecretsOf(config: JR2Config, effect: CarriedEffect): string[] {
|
|
513
|
+
if (effect.kind !== "mcp") return [...(effect.secrets ?? [])];
|
|
514
|
+
const secret = remoteServerOf(config, effect.server)?.secret;
|
|
515
|
+
return secret === undefined ? [] : [secret];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** An MCP server url's origin as the Effector's fence reads it: scheme, host, port. `jr2 up` has
|
|
519
|
+
* checked the url (`mcpUrlRefusals`), so it parses. */
|
|
520
|
+
function originOf(url: string): { scheme: "http" | "https"; host: string; port: number } {
|
|
521
|
+
const u = new URL(url);
|
|
522
|
+
const scheme = u.protocol === "http:" ? "http" : "https";
|
|
523
|
+
const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
524
|
+
return { scheme, host, port: u.port ? Number(u.port) : scheme === "http" ? 80 : 443 };
|
|
525
|
+
}
|
|
378
526
|
|
|
379
527
|
/** A Workspace as a human reads it: its workflow and the placement route to it, an inline hop as
|
|
380
528
|
* `invoked inline in state <id>`. */
|
|
@@ -417,6 +565,19 @@ function heldPodsOf(
|
|
|
417
565
|
};
|
|
418
566
|
const pods: HeldPodPlan[] = [];
|
|
419
567
|
const redundant: Array<{ label: string; name: string }> = [];
|
|
568
|
+
// The secret of each remote MCP server that the pod's Agents name (ADR-0079), with the servers:
|
|
569
|
+
// the Agent claims it by naming the server, as it claims a model key by naming a model. A name
|
|
570
|
+
// the `mcp` block does not declare is refused by the walk's own check (`mcpRefusals`), not here.
|
|
571
|
+
const serverClaims = (servers: Iterable<string>): Record<string, string[]> => {
|
|
572
|
+
const out: Record<string, string[]> = {};
|
|
573
|
+
for (const server of new Set(servers)) {
|
|
574
|
+
const declared = config.mcp?.[server];
|
|
575
|
+
const secret = declared && !isStdioServer(declared) ? declared.secret : undefined;
|
|
576
|
+
if (secret === undefined) continue;
|
|
577
|
+
out[secret] = [...(out[secret] ?? []), server].sort();
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
580
|
+
};
|
|
420
581
|
for (const ws of walk.workspaces) {
|
|
421
582
|
const label = workspaceLabel(ws);
|
|
422
583
|
for (const name of ws.pod.secrets) {
|
|
@@ -428,18 +589,27 @@ function heldPodsOf(
|
|
|
428
589
|
}
|
|
429
590
|
const byProvider = providerClaims(config, ws.pod.models);
|
|
430
591
|
for (const name of ws.pod.secrets) if (byProvider.includes(name)) redundant.push({ label, name });
|
|
592
|
+
const byServer = serverClaims((ws.pod.mcp ?? []).map((m) => m.entry.server));
|
|
431
593
|
const plan: HeldPodPlan = {
|
|
432
594
|
pod: workspaceHeldPod({ workflow: ws.workflow, route: ws.pod.route }),
|
|
433
595
|
label,
|
|
434
|
-
names: ordered([...ws.pod.secrets, ...byProvider]),
|
|
596
|
+
names: ordered([...ws.pod.secrets, ...byProvider, ...Object.keys(byServer)]),
|
|
435
597
|
byProvider: ordered(byProvider),
|
|
598
|
+
...(Object.keys(byServer).length ? { byServer } : {}),
|
|
436
599
|
};
|
|
437
600
|
if (pods.some((p) => p.pod === plan.pod)) throw sharedPodKey(plan.pod, label);
|
|
438
601
|
pods.push(plan);
|
|
439
602
|
}
|
|
440
603
|
if (walk.instanceHarness) {
|
|
441
604
|
const byProvider = ordered(providerClaims(config, walk.instanceHarness.models));
|
|
442
|
-
|
|
605
|
+
const byServer = serverClaims(walk.instanceHarness.servers ?? []);
|
|
606
|
+
pods.push({
|
|
607
|
+
pod: INSTANCE_HARNESS_HELD_POD,
|
|
608
|
+
label: "the Instance Harness",
|
|
609
|
+
names: ordered([...byProvider, ...Object.keys(byServer)]),
|
|
610
|
+
byProvider,
|
|
611
|
+
...(Object.keys(byServer).length ? { byServer } : {}),
|
|
612
|
+
});
|
|
443
613
|
}
|
|
444
614
|
return { pods, redundant };
|
|
445
615
|
}
|
|
@@ -462,7 +632,8 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
|
|
|
462
632
|
const namedBy = new Map<string, Array<{ label: string; where: string }>>();
|
|
463
633
|
const instancePlaced = walk.effects.filter(
|
|
464
634
|
(p): p is InstancePlaced =>
|
|
465
|
-
(p.effect.kind === "http" || p.effect.kind === "dispatch"
|
|
635
|
+
(p.effect.kind === "http" || p.effect.kind === "dispatch" || p.effect.kind === "mcp") &&
|
|
636
|
+
p.effect.placement === "instance",
|
|
466
637
|
);
|
|
467
638
|
// Keyed by the Effect's placement route and slot name, as workspacePodsOf keys a pod's Effects:
|
|
468
639
|
// two slots of one name in one workflow — one in a workspace(), one outside it — are two Effects,
|
|
@@ -482,23 +653,52 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
|
|
|
482
653
|
"workspace()'s body, or send a json or text body (ADR-0068)",
|
|
483
654
|
);
|
|
484
655
|
}
|
|
656
|
+
// A Hook with a `mac` verifier names its secret as an Effect names one, and it is placed in the
|
|
657
|
+
// Instance Effector alone (ADR-0068).
|
|
658
|
+
const hooks = instanceHooksOf(config);
|
|
659
|
+
const tokenHooks = tokenHooksOf(config);
|
|
660
|
+
for (const [secret, names] of tokenHooks) {
|
|
661
|
+
if (byName.has(secret)) continue;
|
|
662
|
+
throw new Error(
|
|
663
|
+
`hooks.${names[0]} names secret("${secret}"), which the \`secrets\` list does not hold — add it: ` +
|
|
664
|
+
`secrets: [{ name: "${secret}", value: process.env.${envish(secret)} }] (ADR-0078)`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
for (const hook of hooks) {
|
|
668
|
+
if (hook.secret === undefined) continue;
|
|
669
|
+
if (!byName.has(hook.secret)) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`hooks.${hook.name} names secret("${hook.secret}"), which the \`secrets\` list does not hold — add it: ` +
|
|
672
|
+
`secrets: [{ name: "${hook.secret}", value: process.env.${envish(hook.secret)} }] (ADR-0068)`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
namedBy.set(hook.secret, [
|
|
676
|
+
...(namedBy.get(hook.secret) ?? []),
|
|
677
|
+
{ label: `hook ${hook.name}`, where: "the Instance Effector" },
|
|
678
|
+
]);
|
|
679
|
+
}
|
|
485
680
|
for (const { workflow, effect } of walk.effects) {
|
|
486
681
|
const label = `${workflow}/${effect.name}`;
|
|
487
682
|
const where =
|
|
488
683
|
effect.placement === "instance"
|
|
489
684
|
? "the Instance Effector"
|
|
490
685
|
: (sandboxOf.get(keyOf(workflow, effect)) ?? "the Sandbox Effector of the Workspace that invokes it");
|
|
491
|
-
for (const name of effect
|
|
686
|
+
for (const name of effectSecretsOf(config, effect)) {
|
|
492
687
|
if (!byName.has(name)) {
|
|
688
|
+
// An mcp Effect never writes secret(): the server it calls, in the `mcp` block, names it.
|
|
689
|
+
const names =
|
|
690
|
+
effect.kind === "mcp"
|
|
691
|
+
? `calls the MCP server "${effect.server}", whose secret "${name}"`
|
|
692
|
+
: `names secret("${name}"), which`;
|
|
493
693
|
throw new Error(
|
|
494
|
-
`workflow "${workflow}": the ${effect.kind} Effect "${effect.name}"
|
|
694
|
+
`workflow "${workflow}": the ${effect.kind} Effect "${effect.name}" ${names} the ` +
|
|
495
695
|
`\`secrets\` list does not hold — add it: secrets: [{ name: "${name}", value: process.env.${envish(name)} }] (ADR-0068)`,
|
|
496
696
|
);
|
|
497
697
|
}
|
|
498
698
|
namedBy.set(name, [...(namedBy.get(name) ?? []), { label, where }]);
|
|
499
699
|
}
|
|
500
700
|
}
|
|
501
|
-
return { entries, namedBy, instancePlaced };
|
|
701
|
+
return { entries, namedBy, instancePlaced, hooks, tokenHooks };
|
|
502
702
|
}
|
|
503
703
|
|
|
504
704
|
/** An Effect secret shorter than 8 characters is refused, and the refusal says why. */
|
|
@@ -508,10 +708,23 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
|
|
|
508
708
|
* length leaves; `jr2 up` keeps no value of the user's Secret (ADR-0068).
|
|
509
709
|
*/
|
|
510
710
|
export function storedValueLength(base64: string): number {
|
|
711
|
+
return storedBytes(base64).length;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* The sha-256, in hex, of a value a Kubernetes Secret holds, trimmed as `storedValueLength` trims
|
|
716
|
+
* it: what `jr2-hook-digests` holds for a `token` Hook whose secret is a key of the user's Secret
|
|
717
|
+
* (ADR-0078). Only the digest leaves.
|
|
718
|
+
*/
|
|
719
|
+
export function storedValueSha256(base64: string): string {
|
|
720
|
+
return createHash("sha256").update(storedBytes(base64)).digest("hex");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function storedBytes(base64: string): Buffer {
|
|
511
724
|
let bytes = Buffer.from(base64, "base64");
|
|
512
725
|
if (bytes.at(-1) === 0x0a) bytes = bytes.subarray(0, -1);
|
|
513
726
|
if (bytes.at(-1) === 0x0d) bytes = bytes.subarray(0, -1);
|
|
514
|
-
return bytes
|
|
727
|
+
return bytes;
|
|
515
728
|
}
|
|
516
729
|
|
|
517
730
|
function shortRefusal(e: { at: string; name: string }, length: number): void {
|
package/src/server.ts
CHANGED
|
@@ -16,6 +16,17 @@
|
|
|
16
16
|
// a publish door anyone outside it can open.
|
|
17
17
|
// JR2_SIGNING_KEY base64 key Sandbox tokens are signed with; from the Secret so live Sandboxes
|
|
18
18
|
// survive a pod restart. Absent → minted into `<dir>/.jr2/secret` (dev-grade).
|
|
19
|
+
// JR2_CALLER_DIGESTS the file of the named Caller tokens' digests (ADR-0078). Default, deployed:
|
|
20
|
+
// the `jr2-caller-digests` mount; host-booted: none. Read again when it
|
|
21
|
+
// changes, so a rotation needs no restart.
|
|
22
|
+
// JR2_HOOK_DIGESTS the file of the `token` Hooks' secrets' digests (ADR-0078). Default,
|
|
23
|
+
// deployed: the `jr2-hook-digests` mount; host-booted: none. Read again when
|
|
24
|
+
// it changes.
|
|
25
|
+
// JR2_INSTANCE_EFFECTOR the Instance Effector's address, `host:port` (ADR-0068). Default,
|
|
26
|
+
// deployed: its Service; host-booted: none. A host-booted fixture names its
|
|
27
|
+
// own, which speaks the same signed wire (the e2e tier's `mac` Hook check).
|
|
28
|
+
// JR2_STATE_VOLUME the size of the state volume claim that `jr2 up` applied (ADR-0065): the
|
|
29
|
+
// space guard measures against it. Absent: `runs.volume`, or `5Gi`.
|
|
19
30
|
// JR2_NAMESPACE the pod's own namespace (Deployment fieldRef) — presence = "deployed":
|
|
20
31
|
// Sandboxes are driven in it, and the Custodians' route home is Service DNS.
|
|
21
32
|
//
|
|
@@ -46,7 +57,7 @@ import { join } from "node:path";
|
|
|
46
57
|
import { resolveAssembly, type ResolvedAssembly } from "./assembly.ts";
|
|
47
58
|
import { KIT_VERSION, loadConfig, type JR2Config } from "./config.ts";
|
|
48
59
|
import { readImageRefs } from "./images.ts";
|
|
49
|
-
import { readMountDigests } from "./mount-digest.ts";
|
|
60
|
+
import { loadedDigests, readMountDigests } from "./mount-digest.ts";
|
|
50
61
|
import { GIT_CLONE_MANIFEST_KEY, readGitCloneManifest } from "./git-clone.ts";
|
|
51
62
|
import { loadWorkflows, startInstance, type RunningInstance } from "./instance.ts";
|
|
52
63
|
import {
|
|
@@ -60,6 +71,12 @@ import {
|
|
|
60
71
|
INSTANCE_HARNESS_SERVICE,
|
|
61
72
|
INSTANCE_EFFECTOR,
|
|
62
73
|
EFFECTOR_PORT,
|
|
74
|
+
CALLER_DIGESTS_KEY,
|
|
75
|
+
CALLER_DIGESTS_MOUNT,
|
|
76
|
+
CALLER_DIGESTS_SECRET,
|
|
77
|
+
HOOK_DIGESTS_KEY,
|
|
78
|
+
HOOK_DIGESTS_MOUNT,
|
|
79
|
+
HOOK_DIGESTS_SECRET,
|
|
63
80
|
} from "./names.ts";
|
|
64
81
|
import type { AnyStateMachine } from "xstate";
|
|
65
82
|
import { partsOf, type CarriedRepo } from "./parts.ts";
|
|
@@ -69,7 +86,7 @@ import { kubeRepos, type RepoResources } from "./repos.ts";
|
|
|
69
86
|
import { SANDBOX_EFFECTOR_PORT, kubeSandbox } from "./sandbox-kube.ts";
|
|
70
87
|
import { sandboxEffectorService } from "./sandbox-effector.ts";
|
|
71
88
|
import { watchSandboxes, type SandboxWatch } from "./sandbox-watch.ts";
|
|
72
|
-
import { loadSigningKey, mintInstanceToken } from "./tokens.ts";
|
|
89
|
+
import { loadSigningKey, mintInstanceToken, watchCallerDigests } from "./tokens.ts";
|
|
73
90
|
import type { SandboxPort } from "./workspace.ts";
|
|
74
91
|
|
|
75
92
|
export type ServerMainOptions = {
|
|
@@ -100,6 +117,18 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
|
|
|
100
117
|
// From the instance Secret when deployed; per-boot for a host-booted fixture.
|
|
101
118
|
const instanceToken = env.JR2_INSTANCE_TOKEN ?? mintInstanceToken();
|
|
102
119
|
const publishToken = env.JR2_PUBLISH_TOKEN ?? mintInstanceToken();
|
|
120
|
+
// The named Callers' digests (ADR-0078), from the mount `jr2 up` writes. A host-booted process
|
|
121
|
+
// has none unless the env names a file.
|
|
122
|
+
const digestsPath =
|
|
123
|
+
env.JR2_CALLER_DIGESTS ?? (namespace !== undefined ? join(CALLER_DIGESTS_MOUNT, CALLER_DIGESTS_KEY) : undefined);
|
|
124
|
+
const digests = digestsPath !== undefined ? watchCallerDigests(digestsPath) : undefined;
|
|
125
|
+
// The `token` Hooks' digests (ADR-0078): the same form, another mount.
|
|
126
|
+
const hookDigestsPath =
|
|
127
|
+
env.JR2_HOOK_DIGESTS ?? (namespace !== undefined ? join(HOOK_DIGESTS_MOUNT, HOOK_DIGESTS_KEY) : undefined);
|
|
128
|
+
const hookDigests = hookDigestsPath !== undefined ? watchCallerDigests(hookDigestsPath) : undefined;
|
|
129
|
+
const instanceEffector =
|
|
130
|
+
env.JR2_INSTANCE_EFFECTOR ??
|
|
131
|
+
(namespace !== undefined ? `${INSTANCE_EFFECTOR}.${namespace}.svc:${EFFECTOR_PORT}` : undefined);
|
|
103
132
|
|
|
104
133
|
// The current Assembly (ADR-0069), and its config and Workflows read from its ROOT, never from
|
|
105
134
|
// the folder directly. A published Assembly that does not load (a Kit bump that broke an import,
|
|
@@ -247,15 +276,27 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
|
|
|
247
276
|
assembly: resolved,
|
|
248
277
|
port,
|
|
249
278
|
hostname: env.HOST ?? "0.0.0.0",
|
|
279
|
+
...(env.JR2_STATE_VOLUME ? { stateVolume: env.JR2_STATE_VOLUME } : {}),
|
|
250
280
|
instanceToken,
|
|
251
281
|
publishToken,
|
|
282
|
+
...(digests ? { callerDigests: digests.current } : {}),
|
|
283
|
+
...(hookDigests ? { hookDigests: hookDigests.current } : {}),
|
|
252
284
|
signingKey,
|
|
253
285
|
sandbox,
|
|
254
286
|
dataPlane,
|
|
255
287
|
// The door reads the same image map a provision reads (ADR-0069): a `file:` context whose
|
|
256
288
|
// digest it lacks is refused at the publish rather than at the first provision.
|
|
257
289
|
...(dataPlane
|
|
258
|
-
? {
|
|
290
|
+
? {
|
|
291
|
+
imageMap: () => readImageRefs(join(IMAGES_MOUNT, IMAGES_KEY)),
|
|
292
|
+
// The ConfigMaps as the mounts hold them, and the digests Secrets as this process LOADED
|
|
293
|
+
// them (ADR-0078): a token works when the authenticator holds its digest, not when the
|
|
294
|
+
// file has it.
|
|
295
|
+
mounts: async () => ({
|
|
296
|
+
...(await readMountDigests()),
|
|
297
|
+
...loadedDigests({ [CALLER_DIGESTS_SECRET]: digests, [HOOK_DIGESTS_SECRET]: hookDigests }),
|
|
298
|
+
}),
|
|
299
|
+
}
|
|
259
300
|
: {}),
|
|
260
301
|
// A publish landed (ADR-0069): its config is the one that applies from now on, and its
|
|
261
302
|
// Machines may bind other Repos, so the Repo pass runs again. Synchronous: the walk is over
|
|
@@ -300,7 +341,7 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
|
|
|
300
341
|
...(namespace ? { orchestratorName: namespace } : {}),
|
|
301
342
|
// Where an http Effect runs (ADR-0068): the Instance Effector's headless Service. `jr2 up`
|
|
302
343
|
// converges it whenever a registered Machine carries an http Effect.
|
|
303
|
-
...(
|
|
344
|
+
...(instanceEffector !== undefined ? { instanceEffector } : {}),
|
|
304
345
|
// Where a push, a file-body http or a credentialed command runs (ADR-0068): a Sandbox's Effector, by
|
|
305
346
|
// the headless Service the provision writes beside it when `jr2 up` placed an Effect there.
|
|
306
347
|
...(namespace
|
|
@@ -311,6 +352,14 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
|
|
|
311
352
|
: {}),
|
|
312
353
|
});
|
|
313
354
|
held = inst.heldMachines;
|
|
355
|
+
for (const watched of [digests, hookDigests]) {
|
|
356
|
+
if (!watched) continue;
|
|
357
|
+
const close = inst.close;
|
|
358
|
+
inst.close = async () => {
|
|
359
|
+
watched.stop();
|
|
360
|
+
await close();
|
|
361
|
+
};
|
|
362
|
+
}
|
|
314
363
|
// The watch ends with the instance: its stream is the one thing here that never settles alone.
|
|
315
364
|
if (watch) {
|
|
316
365
|
const close = inst.close;
|