@jr2/orchestrator 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +86 -9
- package/docs/concepts/architecture.md +41 -28
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +7 -5
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +4 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +50 -22
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +5 -4
- package/docs/concepts/pools-and-sources.md +187 -11
- package/docs/concepts/secrets-and-trust.md +73 -33
- package/docs/concepts/the-menu.md +11 -0
- package/docs/concepts/workspaces.md +6 -5
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/guides/add-tools-to-the-sandbox.md +55 -11
- 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 +24 -8
- 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/fix-a-failed-converge.md +18 -6
- 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 +9 -6
- package/docs/guides/look-inside-a-sandbox.md +101 -31
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-one-worker-per-item.md +15 -9
- package/docs/guides/set-an-agents-access-and-dials.md +2 -0
- 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 +1 -1
- package/docs/guides/watch-a-run.md +127 -121
- 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 +17 -7
- package/docs/llms.txt +81 -15
- package/docs/reference/authoring-surface/agent.md +1 -1
- package/docs/reference/authoring-surface/answer.md +1 -1
- package/docs/reference/authoring-surface/arg.md +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/file.md +1 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +1 -1
- package/docs/reference/authoring-surface/jr2setup.md +13 -3
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +5 -3
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +1 -1
- package/docs/reference/authoring-surface/pool.md +77 -9
- package/docs/reference/authoring-surface/push.md +1 -1
- package/docs/reference/authoring-surface/readyof.md +1 -1
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +1 -1
- 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 +56 -1
- 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 +14 -1
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
- package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-binding.md +1 -1
- 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-commandoutput.md +1 -1
- package/docs/reference/authoring-surface/type-customize.md +16 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-framepath.md +1 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
- 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-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
- 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 +1 -1
- 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-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-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 +16 -7
- 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 +1 -1
- package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
- package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
- package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
- package/docs/reference/authoring-surface/workspace.md +1 -1
- package/docs/reference/authoring-surface/worktree.md +1 -1
- package/docs/reference/authoring-surface.md +88 -167
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +235 -2
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
- package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
- 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 +9 -2
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
- 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 +18 -14
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
- 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 +32 -18
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
- 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 +116 -34
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
- 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 +7 -6
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
- 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 -18
- 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 +4 -3
- 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 +5 -4
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- 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 +7 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
- 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 +51 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
- 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 +17 -13
- 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/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
- 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 +6 -4
- package/docs/reference/glossary.md +96 -42
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +976 -149
- package/docs/why.md +2 -1
- package/package.json +10 -3
- package/src/actor.ts +36 -10
- package/src/agent.ts +50 -0
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +52 -7
- package/src/config.ts +550 -1
- package/src/conversations.ts +453 -0
- package/src/custodian.ts +8 -2
- package/src/customize.ts +143 -4
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +2 -1
- package/src/effect.ts +60 -7
- package/src/effector-wire.ts +272 -27
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +10 -4
- package/src/held-secrets.ts +72 -23
- package/src/holds.ts +2 -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 +76 -38
- package/src/http.ts +889 -94
- package/src/index.ts +16 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +22 -3
- package/src/machine-doc.ts +26 -1
- package/src/mcp-effect.ts +424 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +64 -2
- package/src/notices.ts +138 -10
- package/src/open-body.ts +25 -11
- package/src/open-effect.ts +17 -4
- package/src/open.ts +4 -2
- package/src/parts.ts +87 -9
- package/src/party.ts +2 -1
- package/src/pick.ts +2 -2
- package/src/pool.ts +1142 -163
- package/src/registration.ts +203 -13
- package/src/run-host.ts +1509 -171
- package/src/run-list.ts +191 -0
- package/src/sandbox-kube.ts +53 -5
- package/src/secret-placement.ts +239 -33
- package/src/server.ts +53 -4
- package/src/setup.ts +72 -32
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +263 -22
- package/src/wire.ts +17 -0
- package/src/workspace.ts +50 -6
- package/src/worktree.ts +3 -3
- 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[];
|
|
@@ -184,7 +213,7 @@ export function secretPlacementsOf(
|
|
|
184
213
|
walk: PlacementWalk,
|
|
185
214
|
lookups: Pick<HeldLookups, "secretKeys" | "valueLength"> = {},
|
|
186
215
|
): SecretPlacements {
|
|
187
|
-
const { entries, namedBy, instancePlaced } = namesOf(config, walk);
|
|
216
|
+
const { entries, namedBy, instancePlaced, hooks, tokenHooks } = namesOf(config, walk);
|
|
188
217
|
const { pods, redundant } = heldPodsOf(config, walk, new Set(entries.map((e) => e.name)));
|
|
189
218
|
const claimed = new Set(pods.flatMap((p) => p.names));
|
|
190
219
|
|
|
@@ -192,6 +221,7 @@ export function secretPlacementsOf(
|
|
|
192
221
|
const warnings: string[] = [];
|
|
193
222
|
const secrets: EffectSecret[] = [];
|
|
194
223
|
const values: Record<string, string> = {};
|
|
224
|
+
const hookTokens: HookTokenSecret[] = [];
|
|
195
225
|
for (const e of entries) {
|
|
196
226
|
const named = namedBy.get(e.name) ?? [];
|
|
197
227
|
if (named.length > 0) {
|
|
@@ -215,10 +245,21 @@ export function secretPlacementsOf(
|
|
|
215
245
|
)})`,
|
|
216
246
|
);
|
|
217
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
|
+
}
|
|
218
259
|
if (where.length === 0) {
|
|
219
260
|
warnings.push(
|
|
220
261
|
`${e.at} (${e.name}) is named by nothing; goes nowhere — no workspace() lists it, no Agent's provider ` +
|
|
221
|
-
"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)",
|
|
222
263
|
);
|
|
223
264
|
continue;
|
|
224
265
|
}
|
|
@@ -231,21 +272,32 @@ export function secretPlacementsOf(
|
|
|
231
272
|
);
|
|
232
273
|
}
|
|
233
274
|
for (const p of pods) {
|
|
234
|
-
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
|
+
);
|
|
235
282
|
report.push(`${p.label}: Custodian holds ${holds.length ? holds.join(", ") : "nothing"}`);
|
|
236
283
|
}
|
|
237
284
|
for (const { label, name } of redundant) {
|
|
238
285
|
report.push(`${label}: secrets lists ${name}, which its Agents' provider claims already — redundant`);
|
|
239
286
|
}
|
|
240
287
|
|
|
241
|
-
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
|
+
]);
|
|
242
292
|
const instance =
|
|
243
|
-
instancePlaced.length === 0
|
|
293
|
+
instancePlaced.length === 0 && hooks.length === 0
|
|
244
294
|
? undefined
|
|
245
295
|
: effectorPlan(
|
|
246
296
|
instancePlaced,
|
|
297
|
+
hooks,
|
|
247
298
|
secrets.filter((s) => instanceSecrets.has(s.name)),
|
|
248
299
|
values,
|
|
300
|
+
config,
|
|
249
301
|
);
|
|
250
302
|
if (instance) {
|
|
251
303
|
report.push(
|
|
@@ -275,7 +327,7 @@ export function secretPlacementsOf(
|
|
|
275
327
|
`${pushes.length ? `; pushes ${pushes.join(", ")}` : ""}`,
|
|
276
328
|
);
|
|
277
329
|
}
|
|
278
|
-
return { held: pods, claimed, ...(instance ? { instance } : {}), sandbox, report, warnings };
|
|
330
|
+
return { held: pods, claimed, ...(instance ? { instance } : {}), sandbox, hookTokens, report, warnings };
|
|
279
331
|
}
|
|
280
332
|
|
|
281
333
|
/** A Workspace's Sandbox Effector: the secrets its Effects name, the targets its file-body https call, its
|
|
@@ -354,34 +406,123 @@ function pushesOf(config: JR2Config, ws: PlacedWorkspace): SandboxPush[] {
|
|
|
354
406
|
return out;
|
|
355
407
|
}
|
|
356
408
|
|
|
357
|
-
function effectorPlan(
|
|
409
|
+
function effectorPlan(
|
|
410
|
+
placed: InstancePlaced[],
|
|
411
|
+
hooks: InstanceHook[],
|
|
412
|
+
secrets: EffectSecret[],
|
|
413
|
+
values: Record<string, string>,
|
|
414
|
+
config: JR2Config,
|
|
415
|
+
): EffectorPlan {
|
|
358
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[] = [];
|
|
359
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);
|
|
360
437
|
// An Open url or an Open dispatch target has no target, so it allows nothing: `jr2 up` refuses
|
|
361
438
|
// it before this (ADR-0054). A dispatch's target is its origin, its one secret the token
|
|
362
439
|
// (ADR-0071).
|
|
363
440
|
if (effect.target === undefined) continue;
|
|
364
|
-
|
|
365
|
-
const same = (x: EffectorTargetConfig) =>
|
|
366
|
-
x.scheme === t.scheme && x.host === t.host && x.port === t.port && x.secrets.join() === t.secrets.join();
|
|
367
|
-
if (!targets.some(same)) targets.push(t);
|
|
441
|
+
add({ ...effect.target, secrets: [...effect.secrets].sort() });
|
|
368
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: [] });
|
|
369
445
|
return {
|
|
370
446
|
placement: "instance",
|
|
371
447
|
secrets,
|
|
372
448
|
values,
|
|
373
449
|
targets,
|
|
374
|
-
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
|
|
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),
|
|
378
456
|
effects: placed.map((p) => `${p.workflow}/${p.effect.name}`),
|
|
457
|
+
hooks: hooks.map((h) => h.name),
|
|
379
458
|
};
|
|
380
459
|
}
|
|
381
460
|
|
|
382
|
-
/**
|
|
383
|
-
*
|
|
384
|
-
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
|
+
}
|
|
385
526
|
|
|
386
527
|
/** A Workspace as a human reads it: its workflow and the placement route to it, an inline hop as
|
|
387
528
|
* `invoked inline in state <id>`. */
|
|
@@ -424,6 +565,19 @@ function heldPodsOf(
|
|
|
424
565
|
};
|
|
425
566
|
const pods: HeldPodPlan[] = [];
|
|
426
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
|
+
};
|
|
427
581
|
for (const ws of walk.workspaces) {
|
|
428
582
|
const label = workspaceLabel(ws);
|
|
429
583
|
for (const name of ws.pod.secrets) {
|
|
@@ -435,18 +589,27 @@ function heldPodsOf(
|
|
|
435
589
|
}
|
|
436
590
|
const byProvider = providerClaims(config, ws.pod.models);
|
|
437
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));
|
|
438
593
|
const plan: HeldPodPlan = {
|
|
439
594
|
pod: workspaceHeldPod({ workflow: ws.workflow, route: ws.pod.route }),
|
|
440
595
|
label,
|
|
441
|
-
names: ordered([...ws.pod.secrets, ...byProvider]),
|
|
596
|
+
names: ordered([...ws.pod.secrets, ...byProvider, ...Object.keys(byServer)]),
|
|
442
597
|
byProvider: ordered(byProvider),
|
|
598
|
+
...(Object.keys(byServer).length ? { byServer } : {}),
|
|
443
599
|
};
|
|
444
600
|
if (pods.some((p) => p.pod === plan.pod)) throw sharedPodKey(plan.pod, label);
|
|
445
601
|
pods.push(plan);
|
|
446
602
|
}
|
|
447
603
|
if (walk.instanceHarness) {
|
|
448
604
|
const byProvider = ordered(providerClaims(config, walk.instanceHarness.models));
|
|
449
|
-
|
|
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
|
+
});
|
|
450
613
|
}
|
|
451
614
|
return { pods, redundant };
|
|
452
615
|
}
|
|
@@ -469,7 +632,8 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
|
|
|
469
632
|
const namedBy = new Map<string, Array<{ label: string; where: string }>>();
|
|
470
633
|
const instancePlaced = walk.effects.filter(
|
|
471
634
|
(p): p is InstancePlaced =>
|
|
472
|
-
(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",
|
|
473
637
|
);
|
|
474
638
|
// Keyed by the Effect's placement route and slot name, as workspacePodsOf keys a pod's Effects:
|
|
475
639
|
// two slots of one name in one workflow — one in a workspace(), one outside it — are two Effects,
|
|
@@ -489,23 +653,52 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
|
|
|
489
653
|
"workspace()'s body, or send a json or text body (ADR-0068)",
|
|
490
654
|
);
|
|
491
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
|
+
}
|
|
492
680
|
for (const { workflow, effect } of walk.effects) {
|
|
493
681
|
const label = `${workflow}/${effect.name}`;
|
|
494
682
|
const where =
|
|
495
683
|
effect.placement === "instance"
|
|
496
684
|
? "the Instance Effector"
|
|
497
685
|
: (sandboxOf.get(keyOf(workflow, effect)) ?? "the Sandbox Effector of the Workspace that invokes it");
|
|
498
|
-
for (const name of effect
|
|
686
|
+
for (const name of effectSecretsOf(config, effect)) {
|
|
499
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`;
|
|
500
693
|
throw new Error(
|
|
501
|
-
`workflow "${workflow}": the ${effect.kind} Effect "${effect.name}"
|
|
694
|
+
`workflow "${workflow}": the ${effect.kind} Effect "${effect.name}" ${names} the ` +
|
|
502
695
|
`\`secrets\` list does not hold — add it: secrets: [{ name: "${name}", value: process.env.${envish(name)} }] (ADR-0068)`,
|
|
503
696
|
);
|
|
504
697
|
}
|
|
505
698
|
namedBy.set(name, [...(namedBy.get(name) ?? []), { label, where }]);
|
|
506
699
|
}
|
|
507
700
|
}
|
|
508
|
-
return { entries, namedBy, instancePlaced };
|
|
701
|
+
return { entries, namedBy, instancePlaced, hooks, tokenHooks };
|
|
509
702
|
}
|
|
510
703
|
|
|
511
704
|
/** An Effect secret shorter than 8 characters is refused, and the refusal says why. */
|
|
@@ -515,10 +708,23 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
|
|
|
515
708
|
* length leaves; `jr2 up` keeps no value of the user's Secret (ADR-0068).
|
|
516
709
|
*/
|
|
517
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 {
|
|
518
724
|
let bytes = Buffer.from(base64, "base64");
|
|
519
725
|
if (bytes.at(-1) === 0x0a) bytes = bytes.subarray(0, -1);
|
|
520
726
|
if (bytes.at(-1) === 0x0d) bytes = bytes.subarray(0, -1);
|
|
521
|
-
return bytes
|
|
727
|
+
return bytes;
|
|
522
728
|
}
|
|
523
729
|
|
|
524
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;
|