@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
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# A Caller comes from one authenticator seam and acts within its scopes
|
|
2
|
+
|
|
3
|
+
The Instance token is the operator's credential for everything that acts on an Instance from outside its Machines. The
|
|
4
|
+
CLI reads it from its Secret over the kube API, so kube RBAC is the gate
|
|
5
|
+
([ADR-0019](0019-one-converging-command-against-the-current-context.md)). That serves the person who runs `jr2 up`. It
|
|
6
|
+
does not serve the other Callers of the v1 target user: a CI job that starts runs, a bridge to a chat service that
|
|
7
|
+
answers Gates, a forge that sends deliveries, and a teammate with a browser and no cluster access. Each needs less than
|
|
8
|
+
the Instance token, and with one token each holds all of it. A run must also record who acted on it. So this decision
|
|
9
|
+
gives jr2 a way to know a Caller with no kube credentials, a scope for each act, and a record of each act.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
- **One seam: an ordered list of authenticators.** Each authenticator takes the credential of a request and gives one of
|
|
14
|
+
two answers: a **Caller** `{ via, name, may, onBehalfOf? }`, or "not mine". The first authenticator that claims the
|
|
15
|
+
credential decides. A credential that an authenticator claims and then finds bad is 401; it never falls through to the
|
|
16
|
+
next one. A credential that no authenticator claims is 401. `via` names the authenticator (`token`, `jwt`, `hook`),
|
|
17
|
+
`name` is the Caller's name for that authenticator, `may` is its scopes (below), and `onBehalfOf` is the Caller's
|
|
18
|
+
claim about the party it acts for. The principals inside the Orchestrator stay outside the seam: an Agent's control
|
|
19
|
+
token, a Sandbox's fetch token and the publish token
|
|
20
|
+
([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)) are not Callers, and no
|
|
21
|
+
authenticator makes a Caller from one. So a Sandbox token still reaches no Gate (ADR-0013).
|
|
22
|
+
- **The Instance token is one authenticator.** It gives the Caller `{ via: "token", name: "instance" }`, with every
|
|
23
|
+
scope for every Workflow. Its meaning does not change: it is the operator's credential.
|
|
24
|
+
- **Named Caller tokens.** The Instance declares them in the `callers` block of `jr2.config.ts`, each with a name and
|
|
25
|
+
its scopes (`callers.tokens.<name>.may`). A name is a DNS label of at most 40 characters, because it is part of the
|
|
26
|
+
name of its Secret, and `instance` is the Instance token's name, so the config load refuses it. `jr2 up` makes the
|
|
27
|
+
value of each one, once, in a Secret of its own, and gives the Orchestrator only the sha-256 of each value, in hex. A
|
|
28
|
+
value is `jr2c_` and 32 random bytes in base64url. The prefix lets the seam say "not mine" with no lookup, and lets a
|
|
29
|
+
secret scanner find a value that leaked. The digests go in one Secret, `jr2-caller-digests`, as one JSON object from
|
|
30
|
+
name to digest, and the Orchestrator reads it through a mount. It reads the file again when it changes, so a new token
|
|
31
|
+
or a rotation needs no restart. The kubelet updates a mounted Secret on its own period, about a minute. Thus
|
|
32
|
+
`GET /assembly` reports a digest of the file that the Orchestrator holds now, beside the digests of its mounted
|
|
33
|
+
ConfigMaps, and `jr2 up` and `jr2 caller rotate` wait until it is the file that they applied: when `jr2 up` says
|
|
34
|
+
`converged`, a new token works, and when `rotate` returns, the old value no longer works. After 150 s, each stops with
|
|
35
|
+
exit code 1 and says that the change does not work yet. The scopes come from the config of the current Assembly: a
|
|
36
|
+
name that has a digest and no entry in the config opens nothing, and a file that does not parse keeps the digests that
|
|
37
|
+
the Orchestrator read last. `jr2 caller token <name>` prints the value, which it reads from the Secret over the kube
|
|
38
|
+
API (RBAC is the gate, as for the Instance token), so it takes `-n` and `--context` and never `--url`.
|
|
39
|
+
`jr2 caller rotate <name>` makes a new value, and the old value stops working. It writes the value's Secret first and
|
|
40
|
+
the digests after, from every token Secret of the namespace, so a rotate that fails between the two runs again and
|
|
41
|
+
loses no value. A name that the config no longer declares stops working at the next `jr2 up`, which deletes its
|
|
42
|
+
Secret. Each token Secret carries its name in the label `jr2.dev/caller-token`, which is how `jr2 up` finds the
|
|
43
|
+
undeclared ones.
|
|
44
|
+
- **JWTs from an identity provider.** The `callers` block also declares issuers
|
|
45
|
+
(`callers.issuers: [{ issuer, audience, jwks?, claims?, groups, login? }]`). The JWT authenticator claims a bearer
|
|
46
|
+
that is a JWT whose `iss` is the `issuer` of an entry; a JWT of another issuer is not its. For each issuer, jr2 checks
|
|
47
|
+
a JWT's signature against the issuer's published keys, and its issuer, audience and expiry, with 30 seconds for the
|
|
48
|
+
clocks to differ. It takes only asymmetric signatures, because a published key must not be able to sign. The keys come
|
|
49
|
+
from `jwks`, or else from the `jwks_uri` of the issuer's `/.well-known/openid-configuration`. The Orchestrator keeps
|
|
50
|
+
them for 10 minutes, and for a JWT with a key that it does not know it fetches them again at once, at most once in 30
|
|
51
|
+
seconds, so a key that the issuer rotated works with no restart. When the fetch of the discovery document fails, the
|
|
52
|
+
Orchestrator keeps the failure for 30 seconds, so many JWTs of that issuer cause at most one fetch in 30 seconds. It
|
|
53
|
+
keeps keys only for the issuers that the current config names. The Caller's `name` is the `sub` claim and the name
|
|
54
|
+
that jr2 shows is the `email` claim; `claims` names other claims for each. The scopes come from the values of one
|
|
55
|
+
claim (a list of groups or roles, `groups` by default), through the map `groups` from a value to scopes; a JWT with
|
|
56
|
+
more than one value gets the scopes of each, joined. A JWT that gets no scope from the map is 403 on every route that
|
|
57
|
+
needs a scope. jr2 never treats it as read-only. `GET /whoami` answers it, with no scope, so that a person can see why
|
|
58
|
+
the other routes refuse it.
|
|
59
|
+
- **Login in the Console's browser.** The Console logs a person in against a JWT issuer with `login: true`, with OIDC
|
|
60
|
+
(the authorization code flow with PKCE, as a public client whose client id is the issuer's `audience`), and sends the
|
|
61
|
+
ID token that it gets as the bearer. So login adds no authenticator: it is a way to get a JWT credential. The Console
|
|
62
|
+
keeps the JWT as it keeps a typed token ([ADR-0032](0032-the-console-unlocks-with-a-callers-credential.md)). A login
|
|
63
|
+
that jr2 hosts on the server side, with a session, comes later through the seam.
|
|
64
|
+
- **Five scopes, each one for every Workflow or for named Workflows.** `start` starts a run, `answer` answers a Gate,
|
|
65
|
+
`cancel` cancels a run, `read` reads a run's state, and `steer` sends a Steering message
|
|
66
|
+
([ADR-0002](0002-duplex-streaming-actor.md)). A scope's value is `"all"` or a list of Workflow names. A Chart
|
|
67
|
+
wrapper's name in the list also opens each named Chart of that wrapper (`plan` opens `plan/weekly`), because a named
|
|
68
|
+
Chart runs its wrapper's Machine ([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)); the name `plan/weekly`
|
|
69
|
+
opens that Chart alone. For a route that addresses a run, jr2 checks the scope against that run's Workflow. Which
|
|
70
|
+
scope opens which route is [ADR-0014](0014-observation-is-open-run-state-is-not.md)'s table. A 403 names the scope and
|
|
71
|
+
the Workflow that the request needed. A Caller with the scope for no Workflow at all is refused before the Workflow is
|
|
72
|
+
read, and the 403 names the scope.
|
|
73
|
+
- **`answer` can name single Gates.** Its list takes an item `{ workflow, gates: [<name>] }` beside a bare Workflow
|
|
74
|
+
name, which still means every Gate of that Workflow: `answer: ["triage", { workflow: "deploy", gates: ["approve"] }]`.
|
|
75
|
+
The same form is a value of the `groups` map, so a team's group answers its own Gate of a Workflow and not the others.
|
|
76
|
+
A Hook's `may.answer` stays per Workflow: the Gate slot's handlers already say which Gates hear its kind. A Gate's
|
|
77
|
+
name is its invoke id, its static name in the Machine that invokes it ([ADR-0011](0011-workflow-defined-events.md)).
|
|
78
|
+
One name covers every Gate of the Workflow with that id, and `jr2 up` refuses a name that the Workflow's Machines do
|
|
79
|
+
not have, so a typo fails at the door and not at the first answer, and a name that matches Gates in more than one
|
|
80
|
+
Machine of the Workflow, so one name never opens two Gates without the author's word. An item for a Chart wrapper,
|
|
81
|
+
`{ workflow: "plan", gates }`, opens those Gates in each named Chart of `plan` too, and `jr2 up` checks its names
|
|
82
|
+
against the wrapper's Machine and each named Chart of the Assembly; an item for `plan/weekly` opens and checks that
|
|
83
|
+
Chart alone. A runtime Chart's Gates do not exist at `jr2 up`, so `jr2 up` cannot check them: a name that no Machine
|
|
84
|
+
and no Chart file of the wrapper has, and an item for a Chart that is no Chart file, are warnings and not refusals,
|
|
85
|
+
and at run time the item opens the Gates of that name in the runtime Charts too. `GET /gates`, `GET /runs/:id` and the
|
|
86
|
+
404 of a gate POST list only the Gates that the Caller may answer. A gate POST to an open Gate that the Caller may not
|
|
87
|
+
answer is a 403 that names the scope, the Workflow and the Gate. The check and the delivery are one step, so a Gate
|
|
88
|
+
that opens while the request is in flight is checked too. So a 403 and a 404 tell a Caller which Gate is open. That is
|
|
89
|
+
not a secret: which state a run is in is observation, and the open run projections show it
|
|
90
|
+
([ADR-0014](0014-observation-is-open-run-state-is-not.md)). The other four scopes stay per Workflow: each of them acts
|
|
91
|
+
on a whole run.
|
|
92
|
+
- **The CLI is the Caller `token/instance`.** It reads the Instance token as before, and it sends `onBehalfOf`: the user
|
|
93
|
+
name that a kube `SelfSubjectReview` gives for the current context. It asks for the name beside the port-forward, and
|
|
94
|
+
a server that does not answer gives no claim and no failure. A name that is not printable ASCII is not sent, because a
|
|
95
|
+
header cannot carry it, and a longer name is cut to the bound. A Caller sends `onBehalfOf` in the header
|
|
96
|
+
`jr2-on-behalf-of`, at most 256 characters. A Hook's `onBehalfOf` comes from its signed payload, and the door ignores
|
|
97
|
+
the header for a Hook. With `--url` and `JR2_TOKEN`, the CLI sends any Caller's credential, and it is that Caller.
|
|
98
|
+
- **`GET /whoami` and `jr2 whoami` give the request's Caller record**: `via`, `name`, the name that jr2 shows, `may` and
|
|
99
|
+
`onBehalfOf`. A person uses it to see what a credential may do before it fails.
|
|
100
|
+
- **`cors` is an allowlist of origins in `jr2.config.ts`, empty by default.** The Console is served from the
|
|
101
|
+
Orchestrator's own origin and needs no entry. A browser app on another origin needs its origin in the list. Each entry
|
|
102
|
+
is an origin alone (`https://ops.example.com`, no path, no pattern), and the config load refuses any other text. The
|
|
103
|
+
Orchestrator reads the list of the current Assembly on each request.
|
|
104
|
+
- **The run records the Caller of each act on a guarded route.** A Caller in a record is
|
|
105
|
+
`{ via, name, shown?, onBehalfOf? }`: never its scopes. The run's record holds `startedBy`, which is
|
|
106
|
+
`{ kind: "caller", via, name, shown?, onBehalfOf? }`; `{ kind: "run", run, slot }` for a run that a local `dispatch`
|
|
107
|
+
started (ADR-0071); or `{ kind: "jr2", why }` for a standing run, which has no Caller because jr2 starts it, at
|
|
108
|
+
`boot`, at a `publish`, at a `hand-over` or at a `resume` ([ADR-0017](0017-pool-over-a-source-port.md)). A resume also
|
|
109
|
+
records the Caller of the `jr2 run` that resumed the run, as `resumedBy`. A run that a foreign `dispatch` started
|
|
110
|
+
records the Caller whose credential the other Instance sent. The record also holds `cancelledBy`, and `answers`: the
|
|
111
|
+
last 100 answers to the run's Gates, each `{ gate, type, at, answeredBy }`. It keeps the last 100 because a standing
|
|
112
|
+
run's Gate can take an answer for each delivery, and the record is saved on each transition. A gate event's Sender
|
|
113
|
+
carries the Caller too (`via`, `name`, `onBehalfOf`), and `checked` names which of its fields jr2 checked
|
|
114
|
+
([ADR-0011](0011-workflow-defined-events.md)). `jr2 status` walks the `startedBy` chain to the first Caller and prints
|
|
115
|
+
it, and the Console shows it. The walk reads one run at a time, as the Caller that asks, and names the runs between; a
|
|
116
|
+
run it may not read, or that is no longer known, ends the walk, and the line says so. It reads at most 16 runs. The
|
|
117
|
+
record is on the guarded routes only, never on the open observation projection (ADR-0014).
|
|
118
|
+
- **`onBehalfOf` is a claim.** jr2 records it and shows it, and it never checks it. The one exception is a Hook (below):
|
|
119
|
+
there the signature covers the payload that `onBehalfOf` comes from.
|
|
120
|
+
- **A Caller's scope does not limit what its run dispatches.** A Caller with `start` on `triage` starts a run whose
|
|
121
|
+
Machine can dispatch `deploy`. The Machine is the author's, and it is the boundary of what its run does, as it is for
|
|
122
|
+
every Effect. The docs say this where they describe scopes.
|
|
123
|
+
- **A Hook is a Caller with `via: "hook"`.** The `hooks` block of `jr2.config.ts` declares each Hook by its name (the
|
|
124
|
+
last segment of its address) and its Hook kind, which the config imports. A Hook's name has the form of a Caller
|
|
125
|
+
token's name. The config load refuses another name, a value that no kind's constructor made, and two kinds with one
|
|
126
|
+
id. The kind's constructor takes only the credential and the Workflows whose Gates the Hook may answer (`may.answer`,
|
|
127
|
+
every Workflow by default): `github({ secret: secret("GH_HOOK"), may })`,
|
|
128
|
+
`slack({ secret: secret("SLACK_SIGNING"), may })`, `discord({ publicKey: "…", may })`. A Hook has the `answer` scope
|
|
129
|
+
only. A Hook never starts a run. To bring items to a Pool, it needs `answer` on the Pool's Workflow, because it
|
|
130
|
+
reaches the Pool's `source` Gate ([ADR-0017](0017-pool-over-a-source-port.md)). The Hook's route has its own
|
|
131
|
+
authenticator, its verifier, which gives `{ via: "hook", name, may: { answer }, onBehalfOf }`.
|
|
132
|
+
- **A Hook kind owns the service's protocol.** The kind gives what the service signed, the handshake, the decoding of
|
|
133
|
+
the body (Slack sends a form), a zod schema for each event key, the event key, the delivery id, `onBehalfOf` and the
|
|
134
|
+
envelope of a text that a person reads. The author writes none of them. Each is a fact of the service, the same for
|
|
135
|
+
every Instance. jr2 ships a kind for each service that has a handshake, an envelope or a scheme of its own:
|
|
136
|
+
- `github`: the event key is the event name and its action (`pull_request_review.submitted`), the id is
|
|
137
|
+
`X-GitHub-Delivery`, and `onBehalfOf` is `sender.login`.
|
|
138
|
+
- `gitlab`: the `token` verifier on `X-Gitlab-Token`. The event key is `object_kind` and the noteable type for a
|
|
139
|
+
`note`, else the action or the status (`merge_request.approved`, `note.MergeRequest`, `pipeline.success`), the id is
|
|
140
|
+
`Idempotency-Key` (else `X-Gitlab-Event-UUID`), and `onBehalfOf` is `user.username`.
|
|
141
|
+
- `slack`: the event keys are `slash_command./jr2` (the command is in the key, so a Source for `/jr2` never hears
|
|
142
|
+
`/deploy`), `block_actions.<action_id>` and `event_callback.<event type>`. The id is `event_id` for an event and
|
|
143
|
+
`trigger_id` for a command or a click. `onBehalfOf` is the Slack user id, never a display name. The handshake
|
|
144
|
+
answers `url_verification` with its `challenge`.
|
|
145
|
+
- `discord`: the event keys are `application_command.<name>` and `message_component`, the id is the interaction's
|
|
146
|
+
`id`, and `onBehalfOf` is `member.user.id`, or `user.id` in a direct message. The handshake answers `PING` with
|
|
147
|
+
`PONG`. Another type of interaction (an autocomplete, a modal) gets the key `interaction.<type>`, which has no
|
|
148
|
+
schema, so it reaches nothing.
|
|
149
|
+
|
|
150
|
+
`onBehalfOf` comes from the verified body, so the signature covers it. The handshake, the parse and the envelope are
|
|
151
|
+
[ADR-0011](0011-workflow-defined-events.md)'s door.
|
|
152
|
+
|
|
153
|
+
- **`webhook()` makes a kind from config data, for a service that has no kind.** Its config gives the verifier (`mac`
|
|
154
|
+
over the raw body, with the header, its prefix and the algorithm; or `token` in a header), the event name, the
|
|
155
|
+
delivery id and `onBehalfOf` (each by a header or a JSON path), and a zod schema for each event name. It has no
|
|
156
|
+
handshake, and its answer is the outcome JSON. It makes a kind, not a Hook: the Instance writes it in a module of its
|
|
157
|
+
own, which the config imports for its Hooks and the Workflows import for their handlers. A service that needs a
|
|
158
|
+
handshake, a body that is not JSON, a signature over more than the body, a signed timestamp or an answer that a person
|
|
159
|
+
reads needs a kind module. Sentry, the alert service of the standing Pool
|
|
160
|
+
([ADR-0017](0017-pool-over-a-source-port.md)), fits `webhook()` or a kind from another package.
|
|
161
|
+
|
|
162
|
+
- **A Hook kind is a module of the `@jr2/orchestrator/hooks` interface, from any package or a local file.**
|
|
163
|
+
`jr2.config.ts` declares it by `import`, and `jr2 up` bundles it into the Assembly
|
|
164
|
+
([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). jr2 loads no kind at run
|
|
165
|
+
time and discovers none from a folder. jr2's own kinds (`@jr2/orchestrator/hooks/github` and the others) use the same
|
|
166
|
+
interface, and nothing more. The interface and the kinds are subpath exports of `@jr2/orchestrator`, so the
|
|
167
|
+
Orchestrator Kit image holds their one copy, and each kind in an Assembly uses that copy. The door reads a Hook and a
|
|
168
|
+
kind through a module that the package does not export, because it is not part of a kind's interface. It sits beside
|
|
169
|
+
the module that makes a Hook, because that module decides the shape of a Hook.
|
|
170
|
+
- **A kind never gets the credential.** jr2 makes the kind's constructor, so the credential goes from the config to
|
|
171
|
+
the door and never through code that the kind wrote. For `token`, the door removes the header before any code of the
|
|
172
|
+
kind runs.
|
|
173
|
+
- **A kind is pure and synchronous**: no I/O, no Promise, no clock. It is trusted code in the Orchestrator, as is
|
|
174
|
+
every package that a Workflow imports. Its types refuse a Promise, but they cannot stop I/O. The firm guarantee is
|
|
175
|
+
that no kind holds a `mac` secret, because the Orchestrator holds none
|
|
176
|
+
([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)).
|
|
177
|
+
- **A kind's id is short** (`sentry`): lowercase letters, digits and `-`, and it starts with a letter. One id is one
|
|
178
|
+
kind in an Assembly. Two kinds with one id fail the typecheck and `jr2 up`. `defineHookKind` refuses an id of
|
|
179
|
+
another form, an `api` that it does not know, and a `posts` entry that is not an `https://` url that ends in `/`. A
|
|
180
|
+
kind's constructor refuses a credential of the wrong form: a `publicKey` that is not 64 hex characters, or a
|
|
181
|
+
`secret` that is not a named secret.
|
|
182
|
+
- **A follow-up is data.** A kind's answer to a person can give one POST for the door to make after the answer, with
|
|
183
|
+
its url and its JSON (the reply to a Slack click). The kind declares the hosts of its posts, and `jr2 up` adds them
|
|
184
|
+
to the Instance Effector's fence (ADR-0068).
|
|
185
|
+
- **The interface is a public surface of v1.** A kind states `api: 1`, and a kind's package has a peer dependency on
|
|
186
|
+
`@jr2/orchestrator`. The `api` is the contract, not the package version: the door refuses a kind whose `api` it does
|
|
187
|
+
not know. Within `api: 1`, a new field of the interface is optional. A change that breaks a kind is `api: 2`.
|
|
188
|
+
|
|
189
|
+
- **Three verifiers, a closed set, and the Effector knows no Hook kind.** A kind chooses one, and gives the door what to
|
|
190
|
+
verify: the bytes that the service signed, the signature, and the signed timestamp `at` when the signature covers one.
|
|
191
|
+
The door runs the verifier:
|
|
192
|
+
- `mac`: the kind gives the bytes (GitHub: the raw body; Slack: `v0:{timestamp}:{body}`), and the door makes every
|
|
193
|
+
check that needs no secret, such as the window on `at`. Then the Instance Effector computes an HMAC over those bytes
|
|
194
|
+
with the named secret and algorithm, compares it with the signature in constant time, and answers only "match" or
|
|
195
|
+
"no match" ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). A secret that verifies a
|
|
196
|
+
delivery can also forge one, so the Orchestrator never holds it.
|
|
197
|
+
- `token`: the service sends a shared token in a header (GitLab). The door, in the Orchestrator, compares its sha-256
|
|
198
|
+
with the digest that `jr2 up` gave it, and then removes the header. The digests go in one Secret,
|
|
199
|
+
`jr2-hook-digests`, as one JSON object from the secret's name to its digest. It is not `jr2-caller-digests`, because
|
|
200
|
+
`jr2 caller rotate` writes that Secret whole. The Orchestrator reads it through a mount, and again when it changes,
|
|
201
|
+
and `jr2 up` waits for it, as for the Caller digests. `jr2 up` applies it at each converge, empty when no Hook is
|
|
202
|
+
`token`. It computes each digest on the host, from the entry of the `secrets` list: a literal, or the key of the
|
|
203
|
+
user's Secret with one trailing newline removed, as the Effector reads a value. The value goes into no object that
|
|
204
|
+
`jr2 up` writes.
|
|
205
|
+
- `ed25519`: the door, in the Orchestrator, checks the signature over the timestamp and the body (Discord) against the
|
|
206
|
+
application's public key. A public key can verify a signature and cannot make one, so it is not a secret, and the
|
|
207
|
+
config holds it as a literal.
|
|
208
|
+
|
|
209
|
+
**One window: 300 seconds, for every signed timestamp.** The door refuses a delivery whose `at` is more than 300
|
|
210
|
+
seconds from its clock. A kind gives the timestamp and never the window, and no config changes it. Slack asks for 5
|
|
211
|
+
minutes. Discord states no window, and its signature covers its timestamp, so the same window applies. The window
|
|
212
|
+
closes a replay after the hook log forgets the delivery, `runs.retain` later
|
|
213
|
+
([ADR-0065](0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md)). A failed check, or a timestamp
|
|
214
|
+
out of the window, is 401 (Discord sends bad signatures on purpose and expects 401). A new scheme is a change to the
|
|
215
|
+
set. What a delivery does after the verifier is ADR-0011's: the handshake, the parse, the route, the repeat check, the
|
|
216
|
+
fan-in and the answer.
|
|
217
|
+
|
|
218
|
+
- **Tests.** The unit tier checks the JWT authenticator against an in-process issuer made with `jose`: a good token, a
|
|
219
|
+
wrong issuer or audience, an expired token, a key that rotated, and a token with no scope. The `@console` tier checks
|
|
220
|
+
the browser login against Dex in a container. Before each release, a person checks one login against a real provider
|
|
221
|
+
by hand. The default tier checks the `token` and `ed25519` verifiers end to end, with a key pair that the test makes,
|
|
222
|
+
and the window, the handshakes, the decoding, the parse and the envelopes of each kind. It has no Instance Effector,
|
|
223
|
+
so a `mac` check runs there only through the injected transport (the bytes the Orchestrator builds, and its window);
|
|
224
|
+
the Effector's Go tests check the comparison, and the `@kind` tier checks a `mac` Hook end to end through the Instance
|
|
225
|
+
Effector ([ADR-0011](0011-workflow-defined-events.md)). The build must check what the services' docs do not settle:
|
|
226
|
+
whether Slack signs `url_verification` (the design verifies it first), whether `trigger_id` is unique for each request
|
|
227
|
+
(the body's digest catches a repeat anyway), whether Discord sends an interaction again, whether a Discord follow-up
|
|
228
|
+
needs a bot token (the design does not use one), and Slack's limit on the length of a text. The `slack` kind gives the
|
|
229
|
+
door a limit of 4000 characters, the length that Slack's docs advise for a message's `text`; no one checked the limit
|
|
230
|
+
of the answer to a command or of a post to a `response_url`. GitLab's `Idempotency-Key` is from GitLab's docs, and no
|
|
231
|
+
one checked it against a real delivery.
|
|
232
|
+
|
|
233
|
+
## Considered options
|
|
234
|
+
|
|
235
|
+
- **One Instance token for every Caller**, the state before this decision. Rejected: a CI job or a bridge holds every
|
|
236
|
+
act on every run, and the run cannot say who acted.
|
|
237
|
+
- **Headers from an authenticating proxy** (`X-Forwarded-User` from a proxy in front of the Orchestrator). Not refused:
|
|
238
|
+
it adds later as an authenticator. Not now, because jr2 can trust the header only when nothing but the proxy reaches
|
|
239
|
+
the Orchestrator, and the v1 target user has no such proxy.
|
|
240
|
+
- **A kube TokenReview of the bearer.** Not refused: it adds later as an authenticator. Not now, because a Caller with
|
|
241
|
+
kube credentials already has the CLI, and kube RBAC gives jr2 no scope to map.
|
|
242
|
+
- **A login that jr2 hosts on the server side**, with a session cookie. Not refused: it adds later through the seam. Not
|
|
243
|
+
now, because it brings a cookie, CSRF and a session store. Browser login with a JWT gives the Console a login without
|
|
244
|
+
them.
|
|
245
|
+
- **Other ways to read a JWT.** The `email` claim as the Caller's name: rejected, because a provider can change or
|
|
246
|
+
reassign an email, and `sub` is stable for each issuer. Scopes from several claims, or from an expression over claims:
|
|
247
|
+
rejected, because the groups or roles claim of a provider is one list. A JWT with no scope as `read`: rejected,
|
|
248
|
+
because a default grant opens run state to every account of the provider. Opaque tokens with introspection: rejected,
|
|
249
|
+
because each request then calls the provider, in a way that is different for each provider.
|
|
250
|
+
- **The token's value in the Orchestrator.** Rejected: the Orchestrator keeps only a digest, as it does for a Harness
|
|
251
|
+
token ([ADR-0058](0058-a-harness-answers-the-orchestrator-alone.md)). A digest that leaks starts nothing.
|
|
252
|
+
- **A Caller's scope as a limit on its run's dispatches.** Rejected: a run's Effects are the author's Machine, not the
|
|
253
|
+
Caller's. A limit there makes the same Machine do different things for different Callers.
|
|
254
|
+
- **`answer` for whole Workflows only.** Rejected: one Workflow can wait on Gates that different people answer (a team's
|
|
255
|
+
review, a release manager's approval), and a scope for the whole Workflow lets each of them answer the other's Gate.
|
|
256
|
+
- **A scope for each route.** Rejected: the five scopes are the five acts of a Caller. A finer scope names a route,
|
|
257
|
+
which can change, and not an act.
|
|
258
|
+
- **`may.start` on a Hook**, so that a Hook can start runs of a Workflow. Rejected: a Hook never starts a run
|
|
259
|
+
(ADR-0017). The authority to start is the `dispatch` slot in the author's code, as for any dispatch, and the Hook only
|
|
260
|
+
answers the Pool's Gate.
|
|
261
|
+
- **The protocol in the author's config, for a service that jr2 ships a kind for**: the config says where a delivery
|
|
262
|
+
holds its id, its event name and `onBehalfOf`, and which header holds the signature. Rejected: each of these is a fact
|
|
263
|
+
of the service, the same for every Instance, so each author writes the service's protocol again; and a handshake or an
|
|
264
|
+
envelope is not a field. For a service with no kind, this is `webhook()` (above).
|
|
265
|
+
- **A verifier for each service in the Instance Effector** (a `slack` scheme that knows `v0:`, the header names and the
|
|
266
|
+
window). Rejected: every new service is then a change to the Go binary that is the trust boundary, and nothing in the
|
|
267
|
+
bytes to check or in the window is secret. The Effector needs only the one operation that needs the secret: an HMAC
|
|
268
|
+
over given bytes.
|
|
269
|
+
- **A package of its own for the interface and the kinds** (`@jr2/hooks`). Rejected: the Kit releases every package at
|
|
270
|
+
one version, so a separate package gives the interface no version line of its own, and `api` already is its contract.
|
|
271
|
+
A kind's package has a peer dependency on the kit either way, because every Instance that loads a kind has
|
|
272
|
+
`@jr2/orchestrator`. A separate package only adds a public package to publish, pin and stage at each release.
|
|
273
|
+
|
|
274
|
+
## Consequences
|
|
275
|
+
|
|
276
|
+
- ADR-0013's open question, the authentication of a Caller without kube credentials, is closed: a named token, a JWT, or
|
|
277
|
+
a browser login.
|
|
278
|
+
- [ADR-0014](0014-observation-is-open-run-state-is-not.md)'s guarded band becomes a scope check for each route.
|
|
279
|
+
[ADR-0032](0032-the-console-unlocks-with-a-callers-credential.md)'s Console takes any Caller's credential, and shows
|
|
280
|
+
only what its scopes allow. ADR-0071's foreign dispatch takes any token with `start` for that Workflow.
|
|
281
|
+
- `@jr2/orchestrator` gains the subpath exports `@jr2/orchestrator/hooks` (the interface of a Hook kind) and
|
|
282
|
+
`@jr2/orchestrator/hooks/github`, `/gitlab`, `/slack`, `/discord` and `/webhook` (jr2's kinds and `webhook()`).
|
|
283
|
+
- `jr2.config.ts` gains `callers`, `hooks` and `cors`. The CLI gains `jr2 caller token|rotate <name>` and `jr2 whoami`
|
|
284
|
+
([ADR-0009](0009-cli-and-instance-interface.md)).
|
|
285
|
+
- GLOSSARY.md: **Caller** names the Steering message and the Hook. **Hook** and **Hook kind** are new terms.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# An Agent holds MCP tools, a Machine calls them as an Effect, and the Machine is the approval
|
|
2
|
+
|
|
3
|
+
An Agent of the v1 target user works with services: it reads a Jira ticket, posts to Slack, opens a GitHub issue. Before
|
|
4
|
+
this decision an Agent had two kinds of tool, its Working tools and its Menu, and no third
|
|
5
|
+
([ADR-0028](0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md)). It could reach a service only
|
|
6
|
+
from Workspace code, with `curl` and a Held secret that its `workspace()` grants, or through a Machine's `http` Effect.
|
|
7
|
+
The first gives the Agent the whole credential: Workspace code can spend it on every API that the token opens. The
|
|
8
|
+
second makes the author write each call of the service again as an Effect. The services ship MCP servers, and each agent
|
|
9
|
+
framework that we read speaks MCP. So jr2 takes MCP as the one contract for tools beyond its own.
|
|
10
|
+
|
|
11
|
+
Each of those frameworks (Claude Code, the OpenAI Agents SDK, LangGraph, Mastra, the Vercel AI SDK) puts an approval in
|
|
12
|
+
the harness: a mode or a callback that stops the loop before a tool call and asks a person. jr2 has an approval already.
|
|
13
|
+
A Machine's Gate stops the run, keeps it across a restart, names who answered
|
|
14
|
+
([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)) and routes on the answer. A
|
|
15
|
+
second approval in the Harness would be a state that the Machine cannot see.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
- **Two ways to use an MCP server, both in v1.** An Agent holds **MCP tools**: its definition names a server, and the
|
|
20
|
+
server's tools join the Agent's tools. The Harness is the MCP client, and the server's credential is a Held secret
|
|
21
|
+
([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). A Machine calls a server as an
|
|
22
|
+
Effect: the `mcp` Effect runs in the Instance Effector, with an Effect secret and no model
|
|
23
|
+
([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)).
|
|
24
|
+
- **The Machine is the approval.** An Agent's MCP tool runs when the model calls it, within its allowlist (below). The
|
|
25
|
+
Harness has no `requireApproval`, no ask mode and no callback. A call that needs approval is the Machine's: the Agent
|
|
26
|
+
makes a pick, a Gate takes the approval, and an `mcp` Effect makes the call. The Effect's output is the pick's answer
|
|
27
|
+
([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)). So the approval is in
|
|
28
|
+
the run's record, it survives a restart, and a Caller with the `answer` scope gives it.
|
|
29
|
+
- **MCP is the one extension contract for tools.** jr2 has no tool API of its own. An author's own tool is an MCP
|
|
30
|
+
server: a remote server that the author deploys, or a stdio server in the Sandbox Image. A guide in the docs shows a
|
|
31
|
+
small TypeScript server in the Sandbox Image. An Instance `tools/` folder and a server that the Effector hosts are
|
|
32
|
+
Later.
|
|
33
|
+
- **The config declares each server, by the author's name.** The `mcp` block of `jr2.config.ts` maps a name to a server:
|
|
34
|
+
`{ url, secret?, timeout? }` for a remote server (Streamable HTTP), or `{ command, args?, timeout? }` for a stdio
|
|
35
|
+
server. The name is the author's, so one Instance can declare two servers of one service with two secrets
|
|
36
|
+
(`slack-acme`, `slack-partner`). A name matches `[a-z][a-z0-9-]*`. `jr2` is reserved, because the Menu's tools are
|
|
37
|
+
`mcp__jr2__<event>` ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)), and the config
|
|
38
|
+
load refuses it. `secret` names an entry of the top-level `secrets` list. With no `hosts`, that entry is bound to the
|
|
39
|
+
host of the server's `url`, as a model key is bound to its provider's host. The Harness sends it as
|
|
40
|
+
`Authorization: Bearer <Stand-in>`, so `jr2 up` refuses explicit `headers` on that entry that leave out
|
|
41
|
+
`authorization`, explicit `hosts` that leave out the url's host, and explicit `paths` that do not cover the url's
|
|
42
|
+
path. A server with a `secret` needs an `https://` url, as a model endpoint with a bound key does
|
|
43
|
+
([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)); a server with no secret can use
|
|
44
|
+
`http://`, as a keyless model endpoint can. In v1 a credential is a static bearer token, a Held secret or an Effect
|
|
45
|
+
secret. OAuth grants are the next round. `timeout` is for one call, 60 seconds by default.
|
|
46
|
+
- **An Agent names the servers and the tools that it uses.** `AgentDefinition` gains `mcp?: Array<{ server, tools? }>`.
|
|
47
|
+
`server` is a name of the config's `mcp` block, and `tools` is the allowlist. The field is plain data, as the
|
|
48
|
+
definition is ([ADR-0018](0018-instance-agents-are-definitions-jr2-assembles-the-harness.md)). Each entry is an
|
|
49
|
+
object, so that `resources` can be added beside `tools` later. A packaged Machine's Agent names the server that it
|
|
50
|
+
needs, and the composer rebinds the list with the existing `customize(machine, { agents: { <slot>: { mcp } } })`
|
|
51
|
+
([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)). `jr2 up` refuses a server
|
|
52
|
+
name that the config does not declare. It prints the `customize(machine, { agents: { <slot>: { mcp } } })` line that
|
|
53
|
+
binds the slot to a declared server, and the `mcp` block entry that declares a new one. A Menu-only Agent can name a
|
|
54
|
+
remote server only: `jr2 up` refuses a stdio server on a `"none"` Agent (below).
|
|
55
|
+
- **The allowlist is offered only where jr2 can keep it.** It guards the server's credential:
|
|
56
|
+
- A remote server needs `tools`: a list of tool names, or `"*"`. The list is a guarantee. The Harness offers only
|
|
57
|
+
those tools and runs only those tools, and no Workspace code can spend the server's secret.
|
|
58
|
+
- A remote server whose secret Workspace code of the Sandbox where the Agent's Turns run can spend takes only `"*"`,
|
|
59
|
+
and `jr2 up` refuses a list. That is the case when the `workspace()` of that Sandbox grants the secret, when a
|
|
60
|
+
credentialed `command` of that Workspace names it, or when an `http` Effect of that Workspace whose body is a
|
|
61
|
+
Worktree file names it ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). Each lets a
|
|
62
|
+
file that the Agent wrote call every tool of the server, so a list would be a hint, and jr2 does not give a hint the
|
|
63
|
+
form of a guarantee. A `"none"` Agent's Turns run on the Instance Harness, where no Workspace code runs, so it keeps
|
|
64
|
+
its list.
|
|
65
|
+
- A stdio server has no `tools` field. The Agent gets every tool of the server. The server's version is fixed in the
|
|
66
|
+
Sandbox Image, and it runs as Workspace code (below), so a list would guard nothing.
|
|
67
|
+
|
|
68
|
+
The token's own scopes are the outer limit: the allowlist narrows what the Agent can do with a token, and never widens
|
|
69
|
+
it. One token under two secret names is the author's own act, and jr2 cannot see it.
|
|
70
|
+
|
|
71
|
+
- **The tool list is fixed for a conversation.** `jr2 up` checks each allowlisted name against the live server, from the
|
|
72
|
+
check pod and through its Custodian, as the provider preflight checks a model
|
|
73
|
+
([ADR-0019](0019-one-converging-command-against-the-current-context.md)). It refuses a name that the server does not
|
|
74
|
+
list, and it fails when the server refuses the request (a 401 or a 403: a wrong or expired token), as it fails on a
|
|
75
|
+
model key that the provider refuses. It only warns when the server does not answer. The Harness reads `tools/list` at
|
|
76
|
+
the first Turn of a conversation. It keeps the allowed tools, sorted by name, records them with the conversation, and
|
|
77
|
+
offers the same tools until the conversation ends, across a park and a Harness restart. It never reads the list again
|
|
78
|
+
for a Turn, because a tools block that changes misses the provider's prompt cache
|
|
79
|
+
([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). It ignores a
|
|
80
|
+
`listChanged` notice. A tool that the server took away fails its call with jr2's text. The tools block is the Working
|
|
81
|
+
tools, then the MCP tools, then the Menu.
|
|
82
|
+
- **Names.** An MCP tool is `mcp__<server>__<tool>`, and the name must match `[A-Za-z0-9_-]{1,128}`, Anthropic's bound.
|
|
83
|
+
`jr2 up` refuses an allowlisted tool whose name breaks it. A tool that `"*"` reaches and whose name breaks it is not
|
|
84
|
+
offered, and the Harness writes one log line that names it. A name is in the tools block one time: `jr2 up` refuses an
|
|
85
|
+
allowlist that names a tool twice, and the Harness keeps the first tool of a name that a server lists twice. A server
|
|
86
|
+
name has no `_`, so the prefix is never ambiguous.
|
|
87
|
+
- **A server that does not answer at the first Turn fails the Turn.** The Turn fails with a named error,
|
|
88
|
+
`slack-acme did not answer: <reason>`, before the model reads anything. The Machine gets it as `agent.fault`, with
|
|
89
|
+
that text as its `reason` ([ADR-0016](0016-agent-turn-mechanics-are-internal.md),
|
|
90
|
+
[ADR-0074](0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md)). A server that does not answer
|
|
91
|
+
at a later call fails that call with jr2's text, and the model reads it.
|
|
92
|
+
- **A stdio server is Workspace code.** The Harness starts it through the spawner, inside the Landlock domain
|
|
93
|
+
([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)), with the domain's env:
|
|
94
|
+
only the Stand-ins that the `workspace()` grants. Its program comes from the Sandbox Image. A stdio server has no
|
|
95
|
+
`secret` of its own: the secrets that it can spend are those that its Workspace grants. The Harness never starts a
|
|
96
|
+
server with pi-mcp's own spawn: that process would be outside the domain, and it would inherit the env of the Harness
|
|
97
|
+
process, every Stand-in and the path to the control Stand-in with it. The Instance Harness has no domain
|
|
98
|
+
([ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md)), so a Menu-only Agent gets remote servers only.
|
|
99
|
+
- **Lifetimes.** A stdio server runs one process for each Agent conversation, from its first Turn to the end of the
|
|
100
|
+
conversation or of the Harness. Two Agents never share a process. When the process stops, the call fails with jr2's
|
|
101
|
+
text, and the next call starts a new process. A remote server has one connection for each Harness, which every
|
|
102
|
+
conversation of that pod uses.
|
|
103
|
+
- **What an Agent gets: tools, and the text and images of each result.** The model reads a result's text content and its
|
|
104
|
+
images. The engine replaces an image with a line of text for a model that reads no image, and an image larger than
|
|
105
|
+
about 5 MiB is one line of jr2's text. A result with `structuredContent` and no text gives the model the JSON as text.
|
|
106
|
+
Other content (audio, a resource) is replaced by one line of jr2's text that names its kind. A result with `isError`
|
|
107
|
+
reaches the model as an error. A result longer than 64 KiB is cut, and the model reads
|
|
108
|
+
`[jr2 cut this result at 64 KiB]` at the cut. The text of a failure that holds the server's words, such as a JSON-RPC
|
|
109
|
+
error's message, is cut in the same way. A call past the server's `timeout` is cancelled, and the model reads jr2's
|
|
110
|
+
text. The Harness gives the server no elicitation capability, and a call that asks for input fails with jr2's text.
|
|
111
|
+
Not in v1: elicitation, tool search, sampling, roots, and an Agent's resources and prompts (Later).
|
|
112
|
+
- **The `mcp` Effect has the shape of `http`.** `mcp.call(server, tool, { input })`, `mcp.read(server, uri)` and
|
|
113
|
+
`mcp.prompt(server, name, { input })`. `server` and `tool` can each be Open and bind under
|
|
114
|
+
`customize(machine, { effects: { <slot>: { server } | { tool } } })`, one object with both keys when both are Open, as
|
|
115
|
+
`url` and `secret` bind for `http`. The `server` of `mcp.read` and `mcp.prompt` can be Open too; their `uri` and
|
|
116
|
+
`name` are the author's. `fromPick` gives a pick's params to `input`. The server's secret is its Effect secret, placed
|
|
117
|
+
in the Instance Effector. `jr2 up` refuses an `mcp` Effect whose server the config's `mcp` block does not declare, or
|
|
118
|
+
whose server is a stdio server. It prints the `mcp` block entry that declares a remote server, and the
|
|
119
|
+
`customize(machine, { effects: { <slot>: { server } } })` line for an Open server. An Effect's `timeout` replaces the
|
|
120
|
+
server's `timeout` for that Effect. Outputs: `returned { isError, text, structured?, truncated? }` for a call
|
|
121
|
+
(`{ text, structured?, truncated? }` for a read and a prompt), `refused`, `unreachable`, `input-required` and
|
|
122
|
+
`unknown`. A call's `structuredContent` is `structured`, typed `unknown`; a read's `structured` is its `contents`
|
|
123
|
+
list, and a prompt's is its `messages` list. An `output:` schema is Later. The Effector bounds `text` and `structured`
|
|
124
|
+
as it bounds an `http` body, and sets `truncated` when it cuts one. It cuts a JSON-RPC error's message in a reason at
|
|
125
|
+
1 KiB, with a jr2 marker at the cut. The client speaks the revisions `2025-11-25`, `2025-06-18` and `2025-03-26`; a
|
|
126
|
+
server that answers `initialize` with another revision is `unreachable`. When a server closes the event stream of the
|
|
127
|
+
request before its answer, after an event id, the client opens the stream again with a GET and `Last-Event-ID` (within
|
|
128
|
+
the timeout, and at most five times in a row that bring no new event). It does not send the request again. `refused`
|
|
129
|
+
means that the request did not run: an HTTP 3xx or 4xx at any step (a 401 or a 403 included), a refused handshake, or
|
|
130
|
+
one of the JSON-RPC errors `-32700`, `-32600`, `-32601` and `-32602`, or `-32002` (resource not found) for a read,
|
|
131
|
+
which does not act. Another JSON-RPC error, or a 5xx, after the request was sent is `unknown`, because the tool may
|
|
132
|
+
have acted. `input-required` is a server that asked for input: the error `-32042` (URL elicitation required), before
|
|
133
|
+
the tool ran, or an `elicitation/create` request, which the client declared no capability for. That request comes
|
|
134
|
+
while the tool runs, so the tool may have begun. jr2 does not send the request again. It is a named outcome that the
|
|
135
|
+
Machine can route. The client reads an event stream whose lines end with CRLF, LF or a lone CR. It scrubs a server's
|
|
136
|
+
words in a reason before it cuts them, so that no part of a secret stays at the cut. ADR-0067 states each outcome. An
|
|
137
|
+
`mcp` Effect runs at most once unless the author marks it `idempotent` (ADR-0067). The server's `idempotentHint` is
|
|
138
|
+
ignored: the server is not the author. In v1 an `mcp` Effect calls a remote server only, from the Instance Effector. A
|
|
139
|
+
stdio server for an Effect is Later.
|
|
140
|
+
- **Two clients, one protocol.** The Harness uses `@earendil-works/pi-mcp` behind a jr2 interface, with a jr2 stdio
|
|
141
|
+
transport over the spawner. The interface does not depend on the engine, so another engine keeps the MCP tools. The
|
|
142
|
+
Effector has a small MCP client of its own in Go, with the standard library only, over Streamable HTTP. Both speak
|
|
143
|
+
protocol `2025-11-25`, the revision that pi-mcp speaks.
|
|
144
|
+
- **The printed conversation shows an MCP call as `<server>/<tool>`**, with its input bounded as every call's is
|
|
145
|
+
([ADR-0023](0023-the-harness-prints-the-conversation.md)). A pick prints by its event's name, as before, so the two
|
|
146
|
+
never print alike. The result of an MCP call does not print, as the result of a Working tool does not.
|
|
147
|
+
- **The Briefing says that MCP tools are not picks.** An Agent with MCP tools reads one more line in the standing part
|
|
148
|
+
([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)).
|
|
149
|
+
|
|
150
|
+
## Considered options
|
|
151
|
+
|
|
152
|
+
- **An approval mode in the Harness**, as every other framework has: a `requireApproval` on a tool, and a Turn that
|
|
153
|
+
stops until a person answers. Rejected. The stop would be a state in the Harness that the Machine cannot see, keep or
|
|
154
|
+
route on. A Gate already does each of these, and the Machine already decides which act needs a person. A Turn that
|
|
155
|
+
waits on a person also holds a conversation, a Sandbox and its Size for that time.
|
|
156
|
+
- **Servers on the Agent definition** (the shape of 2026-10-27: `mcp: { slack: { url, secret, tools? } }` in `agent()`).
|
|
157
|
+
Rejected. A url and a secret are facts of the Instance, not of the Agent's definition, so a packaged Machine would
|
|
158
|
+
ship them. Two Agents of one server would declare it twice, and an Effect could not name the same server.
|
|
159
|
+
- **Read the tool list again on each Turn.** Rejected. A changed tools block misses the provider's prefix cache
|
|
160
|
+
(ADR-0029), and the history then calls tools that the request no longer defines.
|
|
161
|
+
- **An allowlist that is only a hint**, offered on every server. Rejected. Where Workspace code holds the Stand-in, or a
|
|
162
|
+
stdio server holds the token, the list stops nothing. A field that reads as a limit and is not one is worse than no
|
|
163
|
+
field.
|
|
164
|
+
- **A tool API of jr2's own** (a TypeScript function and a schema, run by the Harness). Rejected. It is a second
|
|
165
|
+
contract beside MCP, its code would run in the Harness process or in the domain with a new seat, and an engine change
|
|
166
|
+
would carry it again. An MCP server is the same function behind a contract that every engine speaks.
|
|
167
|
+
- **One secret for each host and header**, which `jr2 up` refused before this decision (ADR-0059's R5). Rejected: two
|
|
168
|
+
servers of one service (two Slack workspaces) send their tokens to one host in one header, and the Menu-only Agents of
|
|
169
|
+
an Instance share one Custodian. The Custodian already chose the value by the Stand-in in the header, so only the
|
|
170
|
+
refusal went.
|
|
171
|
+
- **Start a stdio server with pi-mcp's own spawn.** Rejected: the process would be outside the domain, with every
|
|
172
|
+
Stand-in in its env (ADR-0076).
|
|
173
|
+
- **Protocol `2026-07-28`.** Not now: pi-mcp speaks `2025-11-25`, and the servers that the target user runs speak it.
|
|
174
|
+
|
|
175
|
+
## Consequences
|
|
176
|
+
|
|
177
|
+
- `jr2.config.ts` gains the `mcp` block, and `AgentDefinition` gains `mcp`. The Effect kinds gain `mcp` (ADR-0067).
|
|
178
|
+
- A Custodian holds what its pod's Agents name, the Instance Harness's too (ADR-0059): an Agent's MCP server secret is
|
|
179
|
+
held in the pod where its Turns run. The Custodian chooses a secret by the Stand-in that a request carries, so two
|
|
180
|
+
secrets can share one host and one header.
|
|
181
|
+
- An Agent's MCP tools reach the model, so the `@model` tier gains the claim `mcp-then-pick` (ADR-0066).
|
|
182
|
+
- A stdio server shares the domain with the other Workspace code of its Sandbox. Other Agents' code can trace it and
|
|
183
|
+
write to its stdin. It holds no control credential, but its results are as trusted as the output of `bash`, and no
|
|
184
|
+
more.
|
|
185
|
+
- An `elicitation/create` request does not name the call that sent it. When it comes on a connection with one call in
|
|
186
|
+
flight, that call fails with jr2's text. When more calls are in flight, which a remote server's one connection for
|
|
187
|
+
each Harness makes possible, jr2 cannot tell which call asked: no call is marked, and each fails with the server's own
|
|
188
|
+
error.
|
|
189
|
+
- A remote server's results come from outside the Instance. They can carry a prompt injection, as a web page can. The
|
|
190
|
+
allowlist bounds what the Agent can do with the server's token; the Machine bounds what the run does.
|
|
191
|
+
- Tests: the unit tier checks the config and the allowlist rules, and the Harness's client against an in-memory server.
|
|
192
|
+
The Harness's tests drive a stdio server through the spawner, and check that it holds only the granted Stand-ins. The
|
|
193
|
+
default e2e tier checks the refusals of `jr2 up` and of the publish door. The `@kind` tier checks the live tool list
|
|
194
|
+
from the check pod, an Agent's calls to two servers on one host, each through the Custodian with its own value, and an
|
|
195
|
+
`mcp` Effect from the Instance Effector; the server sees each value and never a Stand-in. The Effector's Go tests
|
|
196
|
+
check the `mcp` kind against a test server. The Custodian suite checks two secrets on one host in one header.
|
|
197
|
+
- The OAuth round must look again at client registration: the `2026-07-28` spec deprecates dynamic client registration
|
|
198
|
+
for Client ID Metadata Documents.
|
|
@@ -21,7 +21,7 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
|
|
|
21
21
|
| 0011 | [Control events are workflow-defined and delivered by closure, not routed](decisions/0011-workflow-defined-events.md) |
|
|
22
22
|
| 0012 | [`workspace()` wraps a body Machine and owns only Sandbox lifecycle](decisions/0012-workspace-wrapper-machine.md) |
|
|
23
23
|
| 0013 | [The Agent reaches its Machine through a container it cannot read](decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) |
|
|
24
|
-
| 0014 | [Observation is open; run state and control need
|
|
24
|
+
| 0014 | [Observation is open; run state and control need a Caller's scope](decisions/0014-observation-is-open-run-state-is-not.md) |
|
|
25
25
|
| 0015 | [Workflows are authored through `jr2Setup`; vocabulary and agent menus derive from the machine](decisions/0015-authoring-surface-absorbs-the-mechanism.md) |
|
|
26
26
|
| 0016 | [Agent-turn mechanics are jr2-internal: ambient coordinates, host-side offsets, absorbed retries, fresh sessions](decisions/0016-agent-turn-mechanics-are-internal.md) |
|
|
27
27
|
| 0017 | [The run loop is a jr2 `pool` over a generalized source port](decisions/0017-pool-over-a-source-port.md) |
|
|
@@ -30,7 +30,7 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
|
|
|
30
30
|
| 0020 | [Private-CA trust is a named concept, not volume plumbing](decisions/0020-private-ca-trust-is-a-named-concept.md) |
|
|
31
31
|
| 0021 | [Workspace continuity is watched; the lease asserts](decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md) |
|
|
32
32
|
| 0022 | [Observation is a level-triggered feed, and an API before it is a page](decisions/0022-observation-is-a-level-triggered-feed.md) |
|
|
33
|
-
| 0023 | [The Harness prints the conversation, and the reasoning is the part that prints](decisions/0023-the-harness-prints-the-conversation.md) |
|
|
33
|
+
| 0023 | [The Harness prints the conversation and the Orchestrator keeps it, and the reasoning is the part that prints](decisions/0023-the-harness-prints-the-conversation.md) |
|
|
34
34
|
| 0024 | [An Agent's turn ends with the state that asked for it](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) |
|
|
35
35
|
| 0025 | [CANCEL ends a run; stopping one parks it](decisions/0025-cancel-ends-a-run-stop-parks-it.md) |
|
|
36
36
|
| 0026 | [A Turn that is over is never prompted, and only a Turn that is over answers 404](decisions/0026-a-turn-that-is-over-is-never-prompted.md) |
|
|
@@ -39,7 +39,7 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
|
|
|
39
39
|
| 0029 | [A Menu is fixed for a conversation, and a Turn is told its Allowed picks](decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md) |
|
|
40
40
|
| 0030 | [A snapshot names the Machine it was written under](decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md) |
|
|
41
41
|
| 0031 | [Menu-only Agents run on the Instance Harness](decisions/0031-menu-only-agents-run-on-the-instance-harness.md) |
|
|
42
|
-
| 0032 | [The Console unlocks with
|
|
42
|
+
| 0032 | [The Console unlocks with a Caller's credential, and shows what its scopes allow](decisions/0032-the-console-unlocks-with-a-callers-credential.md) |
|
|
43
43
|
| 0033 | [A Machine declares the input that starts it](decisions/0033-a-machine-declares-the-input-that-starts-it.md) |
|
|
44
44
|
| 0034 | [The Console renders with Preact, and the server erases the types](decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md) |
|
|
45
45
|
| 0035 | [A runaway turn is ended by the Harness, rerolled once, then a fault](decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md) |
|
|
@@ -71,7 +71,7 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
|
|
|
71
71
|
| 0062 | [jr2 briefs the Agent on its seat, never on its task](decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md) |
|
|
72
72
|
| 0063 | [The Orchestrator watches the cluster](decisions/0063-the-orchestrator-watches-the-cluster.md) |
|
|
73
73
|
| 0064 | [A Workspace waits for capacity](decisions/0064-a-workspace-waits-for-capacity.md) |
|
|
74
|
-
| 0065 | [A persist survives a crash, and history is a
|
|
74
|
+
| 0065 | [A persist survives a crash, and history is kept for a set time](decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md) |
|
|
75
75
|
| 0066 | [A `@model` tier runs a real model, and a change to what the model reads runs the tier](decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md) |
|
|
76
76
|
| 0067 | [An Effect is an actor slot, and runs at most once unless marked idempotent](decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md) |
|
|
77
77
|
| 0068 | [An Effector holds an Effect's secrets and signs its wire](decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md) |
|
|
@@ -81,3 +81,8 @@ Each architecture decision record (ADR) says what jr2 decided, why, and what it
|
|
|
81
81
|
| 0072 | [The docs ship inside the Kit, and an Instance's `AGENTS.md` indexes them](decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md) |
|
|
82
82
|
| 0073 | [A Machine fails a run with `fail()`, and an uncaught Failure ends it `failed`](decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md) |
|
|
83
83
|
| 0074 | [An unrouted mechanism event is refused at load and faults the run](decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md) |
|
|
84
|
+
| 0075 | [A Machine cuts a Worktree, and the Workspace only clones](decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md) |
|
|
85
|
+
| 0076 | [An Agent's control credential lives only where no Workspace code runs](decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md) |
|
|
86
|
+
| 0077 | [jr2 requires Landlock on every Sandbox node](decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md) |
|
|
87
|
+
| 0078 | [A Caller comes from one authenticator seam and acts within its scopes](decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md) |
|
|
88
|
+
| 0079 | [An Agent holds MCP tools, a Machine calls them as an Effect, and the Machine is the approval](decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md) |
|