@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,424 @@
|
|
|
1
|
+
// The `mcp` Effect (ADR-0067, ADR-0079): call a tool of an MCP server, read one of its resources,
|
|
2
|
+
// or get one of its prompts, from the Instance Effector, with the server's secret that the Agent
|
|
3
|
+
// never reads. It has the shape of `http`, with a server and a tool in place of a url:
|
|
4
|
+
//
|
|
5
|
+
// post: mcp.call("slack-acme", "post_message", { input: z.object({ channel: z.string(), text: z.string() }) })
|
|
6
|
+
//
|
|
7
|
+
// The server is a name of the `mcp` block of `jr2.config.ts`: its url and its secret are the
|
|
8
|
+
// config's, never a part and never a param. `jr2 up` places the secret in the Instance Effector as
|
|
9
|
+
// an Effect secret and writes the server into `effector.json`; the start names the server alone,
|
|
10
|
+
// so the Effector reads the url and the secret from what `jr2 up` wrote. An Agent's param reaches
|
|
11
|
+
// the tool's arguments only (or a prompt's), through the input.
|
|
12
|
+
//
|
|
13
|
+
// The driver is the Instance Effector's, shared with `http` (`instanceEffectLogic`): the
|
|
14
|
+
// write-ahead intent row, the restore rule (at most once unless the author marks it idempotent;
|
|
15
|
+
// the server's `idempotentHint` is not read), a forged answer, and D1.
|
|
16
|
+
|
|
17
|
+
import type { z } from "zod";
|
|
18
|
+
import type { Duration, EffectLogic, McpOp, McpParts, OpenMcpParts } from "./effect.ts";
|
|
19
|
+
import {
|
|
20
|
+
createEffectorClient,
|
|
21
|
+
MCP_SERVER,
|
|
22
|
+
nodeEffectorTransport,
|
|
23
|
+
type McpCallOutput,
|
|
24
|
+
type McpReadOutput,
|
|
25
|
+
type McpStartRequest,
|
|
26
|
+
} from "./effector-wire.ts";
|
|
27
|
+
import { instanceEffectLogic, type EffectorPortFactory } from "./http-effect.ts";
|
|
28
|
+
import { open } from "./open.ts";
|
|
29
|
+
import { effectBindingShape, openEffectLogic, openPartsOf, type OpenOf } from "./open-effect.ts";
|
|
30
|
+
|
|
31
|
+
/** The options of an `mcp` Effect. */
|
|
32
|
+
export type McpOptions<S extends z.ZodObject | undefined = undefined> = {
|
|
33
|
+
/**
|
|
34
|
+
* The zod schema of the input of the Effect: what the `input` of the `invoke`, or `fromPick`,
|
|
35
|
+
* gives it. For `mcp.call` the parsed input is the tool's arguments; for `mcp.prompt`, the
|
|
36
|
+
* prompt's arguments, each a string. jr2 parses the input with it before the call. When it
|
|
37
|
+
* starts, input that does not agree with it makes the Effect fail (its `onError` route): `mcp
|
|
38
|
+
* "<slot>": its input does not match the definition's input schema — <path>: <message>`. With no
|
|
39
|
+
* schema, the input of the `invoke` is the arguments as it is, and `{}` when it has none.
|
|
40
|
+
*/
|
|
41
|
+
input?: S;
|
|
42
|
+
/**
|
|
43
|
+
* How long jr2 waits for the answer: a number and a unit, such as `"30s"` or `"2m"`. Default:
|
|
44
|
+
* the server's `timeout` in `jr2.config.ts` (60 seconds when it sets none). With no answer in
|
|
45
|
+
* that time, the output is `unknown`. It refuses a value that is not a positive duration: `mcp:
|
|
46
|
+
* timeout "<timeout>" is not a positive duration (like "30s", "2m")`.
|
|
47
|
+
*/
|
|
48
|
+
timeout?: Duration;
|
|
49
|
+
/**
|
|
50
|
+
* The largest number of bytes of `text`, and of `structured` as JSON, that the output keeps.
|
|
51
|
+
* Default 65536. Past it, jr2 cuts `text`, drops `structured`, and sets `truncated`. It refuses
|
|
52
|
+
* a value that is not a positive whole number: `mcp({ maxBody }): a positive number of bytes`.
|
|
53
|
+
*/
|
|
54
|
+
maxBody?: number;
|
|
55
|
+
// The restore rule (ADR-0067): the author's mark alone. The server's `idempotentHint` is the
|
|
56
|
+
// server's word, not the author's (ADR-0079).
|
|
57
|
+
/**
|
|
58
|
+
* `true` when it is safe to send the request two times: the tool only reads, or it has its own
|
|
59
|
+
* idempotency key. Default `false`. After an Orchestrator restart, jr2 cannot always tell if the
|
|
60
|
+
* request went out. Then it sends an idempotent one again, and gives any other the output
|
|
61
|
+
* `unknown`. jr2 does not read the tool's `idempotentHint`.
|
|
62
|
+
*/
|
|
63
|
+
idempotent?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** @internal */
|
|
67
|
+
export type McpInput<S> = S extends z.ZodObject ? z.infer<S> : void;
|
|
68
|
+
|
|
69
|
+
/** Which parts of an `mcp` Effect are Open, by the values the author wrote. @internal */
|
|
70
|
+
export type McpOpenParts<V, T> = (V extends typeof open ? "server" : never) | (T extends typeof open ? "tool" : never);
|
|
71
|
+
|
|
72
|
+
const DEFAULT_MAX_BODY = 65_536;
|
|
73
|
+
|
|
74
|
+
/** The definition of one `mcp` Effect, as the three constructors give it. */
|
|
75
|
+
type McpDefinition = {
|
|
76
|
+
op: McpOp;
|
|
77
|
+
server: string | typeof open;
|
|
78
|
+
/** The tool (a call), the resource's uri (a read) or the prompt's name (a prompt). */
|
|
79
|
+
name: string | typeof open;
|
|
80
|
+
options: McpOptions<z.ZodObject | undefined>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const WHAT: Record<McpOp, string> = { call: "tool", read: "uri", prompt: "prompt name" };
|
|
84
|
+
|
|
85
|
+
/** The definition's plain data, checked: every refusal names what to write instead. */
|
|
86
|
+
function mcpPartsOf(def: McpDefinition): OpenMcpParts {
|
|
87
|
+
const { op, server, name, options } = def;
|
|
88
|
+
if (server !== open && (typeof server !== "string" || !MCP_SERVER.test(server) || server === "jr2")) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`mcp.${op}(server): ${JSON.stringify(server)} is not a server name — a name of the mcp block of ` +
|
|
91
|
+
'jr2.config.ts, lowercase letters, digits and -, not "jr2"; or `open`, which the composer binds (ADR-0079)',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (name === open && op !== "call") {
|
|
95
|
+
throw new Error(`mcp.${op}(): its ${WHAT[op]} is the author's, never Open — only a call's tool is (ADR-0079)`);
|
|
96
|
+
}
|
|
97
|
+
if (name !== open && (typeof name !== "string" || name === "")) {
|
|
98
|
+
throw new Error(`mcp.${op}(): the ${WHAT[op]} is a non-empty string`);
|
|
99
|
+
}
|
|
100
|
+
const maxBody = options.maxBody ?? DEFAULT_MAX_BODY;
|
|
101
|
+
if (!Number.isInteger(maxBody) || maxBody <= 0) throw new Error("mcp({ maxBody }): a positive number of bytes");
|
|
102
|
+
const timeoutMs = options.timeout === undefined ? undefined : parseDuration(options.timeout);
|
|
103
|
+
const shape = (options.input as { shape?: unknown } | undefined)?.shape;
|
|
104
|
+
if (op === "prompt" && shape && typeof shape === "object") {
|
|
105
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
106
|
+
if (!isStringSchema(field)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`mcp.prompt(${JSON.stringify(name)}): the input field "${key}" is not a string schema, and a prompt's ` +
|
|
109
|
+
"arguments are strings (MCP's prompts/get)",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
kind: "mcp",
|
|
116
|
+
op,
|
|
117
|
+
server,
|
|
118
|
+
...(op === "call" ? { tool: name } : op === "read" ? { uri: name as string } : { prompt: name as string }),
|
|
119
|
+
inputKeys: shape && typeof shape === "object" ? Object.keys(shape) : [],
|
|
120
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
121
|
+
maxBody,
|
|
122
|
+
idempotent: options.idempotent === true,
|
|
123
|
+
secrets: [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The wire request for one run: the server, the operation and its arguments.
|
|
129
|
+
*
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
export function mcpStartOf(parts: McpParts, args: Record<string, unknown>): McpStartRequest {
|
|
133
|
+
const request =
|
|
134
|
+
parts.op === "call"
|
|
135
|
+
? { call: { tool: parts.tool!, arguments: args } }
|
|
136
|
+
: parts.op === "read"
|
|
137
|
+
? { read: { uri: parts.uri! } }
|
|
138
|
+
: { prompt: { name: parts.prompt!, arguments: promptArguments(args) } };
|
|
139
|
+
return {
|
|
140
|
+
kind: "mcp",
|
|
141
|
+
mcp: { server: parts.server, ...request },
|
|
142
|
+
...(parts.timeoutMs !== undefined ? { timeoutMs: parts.timeoutMs } : {}),
|
|
143
|
+
maxBody: parts.maxBody,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The schema types whose value is a string, and the wrappers that leave it one when present. */
|
|
148
|
+
const STRING_TYPES = new Set(["string", "enum", "literal", "template_literal"]);
|
|
149
|
+
const WRAPPERS = new Set(["optional", "default", "prefault", "catch", "readonly", "nonoptional"]);
|
|
150
|
+
|
|
151
|
+
/** Whether a zod field's value is a string (or absent): what a prompt's argument may be. */
|
|
152
|
+
function isStringSchema(field: unknown): boolean {
|
|
153
|
+
let def = (field as { _zod?: { def?: { type?: string; innerType?: unknown; values?: unknown[] } } })?._zod?.def;
|
|
154
|
+
while (def?.type !== undefined && WRAPPERS.has(def.type)) {
|
|
155
|
+
def = (def.innerType as { _zod?: { def?: typeof def } } | undefined)?._zod?.def;
|
|
156
|
+
}
|
|
157
|
+
if (def?.type === undefined || !STRING_TYPES.has(def.type)) return false;
|
|
158
|
+
return def.values === undefined || def.values.every((v) => typeof v === "string");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A prompt's arguments are strings (MCP's `prompts/get`): another value is a defect of the Machine. */
|
|
162
|
+
function promptArguments(args: Record<string, unknown>): Record<string, string> {
|
|
163
|
+
for (const [key, value] of Object.entries(args)) {
|
|
164
|
+
if (typeof value !== "string") {
|
|
165
|
+
throw new Error(`mcp.prompt: the argument "${key}" is ${typeof value}, and a prompt's arguments are strings`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return args as Record<string, string>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The slot logic of an `mcp` Effect over an injected port factory: the seam the constructors and
|
|
173
|
+
* the unit tests share, as `httpWith` is for an http Effect.
|
|
174
|
+
*
|
|
175
|
+
* @internal
|
|
176
|
+
*/
|
|
177
|
+
export function mcpWith<O>(portFactory: EffectorPortFactory, def: McpDefinition): EffectLogic<unknown, O> {
|
|
178
|
+
const parts = mcpPartsOf(def);
|
|
179
|
+
const opened = openPartsOf(parts);
|
|
180
|
+
if (opened.length > 0) {
|
|
181
|
+
return openEffectLogic(parts, opened, (binding) => {
|
|
182
|
+
const keys = Object.keys(binding).filter((k) => (binding as Record<string, unknown>)[k] !== undefined);
|
|
183
|
+
if (keys.some((k) => k !== "server" && k !== "tool")) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`an mcp Effect's Open parts are its server and its tool — bind it with ${effectBindingShape(opened)}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return mcpWith(portFactory, {
|
|
189
|
+
...def,
|
|
190
|
+
...(binding.server !== undefined ? { server: binding.server } : {}),
|
|
191
|
+
...(binding.tool !== undefined ? { name: binding.tool } : {}),
|
|
192
|
+
}) as EffectLogic<unknown, unknown>;
|
|
193
|
+
}) as unknown as EffectLogic<unknown, O>;
|
|
194
|
+
}
|
|
195
|
+
const bound = parts as McpParts;
|
|
196
|
+
const target = bound.op === "call" ? `${bound.server}/${bound.tool}` : `${bound.server} ${bound.op}`;
|
|
197
|
+
return instanceEffectLogic<unknown, O>(
|
|
198
|
+
bound,
|
|
199
|
+
{
|
|
200
|
+
kind: "mcp",
|
|
201
|
+
idempotent: bound.idempotent,
|
|
202
|
+
label: `mcp ${target}`,
|
|
203
|
+
request: (input, slot) => {
|
|
204
|
+
let args: unknown = input ?? {};
|
|
205
|
+
const schema = def.options.input;
|
|
206
|
+
if (schema !== undefined) {
|
|
207
|
+
const parsed = schema.safeParse(input);
|
|
208
|
+
if (!parsed.success) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`mcp "${slot}": its input does not match the definition's input schema — ` +
|
|
211
|
+
parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
args = parsed.data;
|
|
215
|
+
}
|
|
216
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
217
|
+
throw new Error(`mcp "${slot}": its input is the request's arguments, an object`);
|
|
218
|
+
}
|
|
219
|
+
return mcpStartOf(bound, args as Record<string, unknown>);
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
portFactory,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The real port: the Instance Effector's headless Service, over its signed wire. */
|
|
227
|
+
const instancePort: EffectorPortFactory = (target) =>
|
|
228
|
+
createEffectorClient({ key: target.key, transport: nodeEffectorTransport(target.service) });
|
|
229
|
+
|
|
230
|
+
/** A duration as milliseconds, a whole number of 1 or more. */
|
|
231
|
+
function parseDuration(d: Duration): number {
|
|
232
|
+
const m = /^(\d+(?:\.\d+)?)(s|m|h)$/.exec(d);
|
|
233
|
+
const ms = m === null ? 0 : Math.round(Number(m[1]) * (m[2] === "h" ? 3_600_000 : m[2] === "m" ? 60_000 : 1000));
|
|
234
|
+
if (!(ms >= 1)) {
|
|
235
|
+
throw new Error(`mcp: timeout "${d}" is not a positive duration (like "30s", "2m")`);
|
|
236
|
+
}
|
|
237
|
+
return ms;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Call a tool of an MCP server: an Effect that the Machine runs, with no model. Put it in
|
|
242
|
+
* `actors`, and invoke it by its key: `actors: { post: mcp.call("slack-acme", "post_message", {
|
|
243
|
+
* input }) }`, then `invoke: { src: "post" }`.
|
|
244
|
+
*
|
|
245
|
+
* `server` is a name of the `mcp` block of `jr2.config.ts`: a remote server, whose url and secret
|
|
246
|
+
* are the config's. The Effect runs in the Instance Effector, which holds that secret as an Effect
|
|
247
|
+
* secret and sends it as `Authorization: Bearer <value>`. The Agent and the Orchestrator never read
|
|
248
|
+
* it. `jr2 up` refuses a server that the `mcp` block does not declare, and a stdio server: an `mcp`
|
|
249
|
+
* Effect calls a remote server only.
|
|
250
|
+
*
|
|
251
|
+
* `server` and `tool` can each be `open`: a packaged Machine that cannot know them leaves them
|
|
252
|
+
* Open, and the composer binds them one time with `customize(machine, { effects: { <slot>: {
|
|
253
|
+
* server, tool } } })`.
|
|
254
|
+
*
|
|
255
|
+
* Every outcome is output ({@link McpCallOutput}): `returned`, `refused`, `unreachable`,
|
|
256
|
+
* `input-required` or `unknown`. A tool's own error is `returned` with `isError: true`. jr2 sends
|
|
257
|
+
* the call at most one time, unless you mark it `idempotent`. When its state exits while the call
|
|
258
|
+
* runs, jr2 stops waiting for it. The Effector can still send it.
|
|
259
|
+
*
|
|
260
|
+
* This is how an Agent's request gets an approval: the Agent picks, a Gate waits for a person,
|
|
261
|
+
* and the Effect makes the call. The Effect's output is the pick's answer.
|
|
262
|
+
*
|
|
263
|
+
* @param server - A server name of the `mcp` block of `jr2.config.ts`, or `open`.
|
|
264
|
+
* @param tool - The tool's name, or `open`.
|
|
265
|
+
* @param options - The input schema, the timeout, the output bound, and `idempotent`.
|
|
266
|
+
* @returns An Effect, for `actors`.
|
|
267
|
+
*
|
|
268
|
+
* @see docs/concepts/effects.md
|
|
269
|
+
* @see docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md
|
|
270
|
+
* @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
|
|
271
|
+
*/
|
|
272
|
+
function call<
|
|
273
|
+
const V extends string | typeof open,
|
|
274
|
+
const T extends string | typeof open,
|
|
275
|
+
S extends z.ZodObject | undefined = undefined,
|
|
276
|
+
>(
|
|
277
|
+
server: V,
|
|
278
|
+
tool: T,
|
|
279
|
+
options: McpOptions<S> = {},
|
|
280
|
+
): EffectLogic<McpInput<S>, McpCallOutput> & OpenOf<McpOpenParts<V, T>> {
|
|
281
|
+
return mcpWith(instancePort, { op: "call", server, name: tool, options }) as EffectLogic<McpInput<S>, McpCallOutput> &
|
|
282
|
+
OpenOf<McpOpenParts<V, T>>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Read a resource of an MCP server: an Effect that the Machine runs, with no model. `server` is
|
|
287
|
+
* as {@link mcp}`.call` takes it, and can be `open`. `uri` is the author's, never Open and never a
|
|
288
|
+
* param. The output is {@link McpReadOutput}: `text` joins the text of the resource's contents,
|
|
289
|
+
* and `structured` is the `contents` list. jr2 sends it at most one time, unless you mark it
|
|
290
|
+
* `idempotent`.
|
|
291
|
+
*
|
|
292
|
+
* @param server - A server name of the `mcp` block of `jr2.config.ts`, or `open`.
|
|
293
|
+
* @param uri - The resource's uri.
|
|
294
|
+
* @param options - The timeout, the output bound, and `idempotent`.
|
|
295
|
+
* @returns An Effect, for `actors`.
|
|
296
|
+
*
|
|
297
|
+
* @see docs/concepts/effects.md
|
|
298
|
+
*/
|
|
299
|
+
function read<const V extends string | typeof open>(
|
|
300
|
+
server: V,
|
|
301
|
+
uri: string,
|
|
302
|
+
options: Omit<McpOptions, "input"> = {},
|
|
303
|
+
): EffectLogic<void, McpReadOutput> & OpenOf<McpOpenParts<V, string>> {
|
|
304
|
+
return mcpWith(instancePort, { op: "read", server, name: uri, options }) as EffectLogic<void, McpReadOutput> &
|
|
305
|
+
OpenOf<McpOpenParts<V, string>>;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get a prompt of an MCP server: an Effect that the Machine runs, with no model. `server` is as
|
|
310
|
+
* {@link mcp}`.call` takes it, and can be `open`. `name` is the author's. The input is the
|
|
311
|
+
* prompt's arguments, each a string. The output is {@link McpReadOutput}: `text` joins the text of
|
|
312
|
+
* the prompt's messages, and `structured` is the `messages` list.
|
|
313
|
+
*
|
|
314
|
+
* @param server - A server name of the `mcp` block of `jr2.config.ts`, or `open`.
|
|
315
|
+
* @param name - The prompt's name.
|
|
316
|
+
* @param options - The input schema, the timeout, the output bound, and `idempotent`.
|
|
317
|
+
* @returns An Effect, for `actors`.
|
|
318
|
+
*
|
|
319
|
+
* @see docs/concepts/effects.md
|
|
320
|
+
*/
|
|
321
|
+
function prompt<const V extends string | typeof open, S extends z.ZodObject | undefined = undefined>(
|
|
322
|
+
server: V,
|
|
323
|
+
name: string,
|
|
324
|
+
options: McpOptions<S> = {},
|
|
325
|
+
): EffectLogic<McpInput<S>, McpReadOutput> & OpenOf<McpOpenParts<V, string>> {
|
|
326
|
+
return mcpWith(instancePort, { op: "prompt", server, name, options }) as EffectLogic<McpInput<S>, McpReadOutput> &
|
|
327
|
+
OpenOf<McpOpenParts<V, string>>;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The `mcp` Effects (ADR-0067, ADR-0079): `mcp.call(server, tool, { input })` calls a tool,
|
|
332
|
+
* `mcp.read(server, uri)` reads a resource, and `mcp.prompt(server, name, { input })` gets a
|
|
333
|
+
* prompt. Each one is an Effect that the Machine runs, with no model, in the Instance Effector.
|
|
334
|
+
* Put it in `actors`, and invoke it by its key.
|
|
335
|
+
*
|
|
336
|
+
* `server` is a name of the `mcp` block of `jr2.config.ts`: a remote server, whose url and secret
|
|
337
|
+
* are the config's. The Instance Effector holds that secret as an Effect secret, and sends it as
|
|
338
|
+
* `Authorization: Bearer <value>`. The Agent and the Orchestrator never read it. `jr2 up` refuses
|
|
339
|
+
* a server that the `mcp` block does not declare, and a stdio server. `server`, and a call's
|
|
340
|
+
* `tool`, can each be `open`: the composer binds them one time with `customize(machine, { effects:
|
|
341
|
+
* { <slot>: { server, tool } } })`.
|
|
342
|
+
*
|
|
343
|
+
* Every outcome is output ({@link McpCallOutput}, {@link McpReadOutput}): `returned`, `refused`,
|
|
344
|
+
* `unreachable`, `input-required` or `unknown`. jr2 sends the request at most one time, unless you
|
|
345
|
+
* mark it `idempotent`. jr2 does not read the tool's `idempotentHint`.
|
|
346
|
+
*
|
|
347
|
+
* An Agent that holds a server's tools (its `mcp` list) calls them with no approval. When a call
|
|
348
|
+
* needs an approval, the Machine makes it: the Agent picks, a Gate waits for a person, and the
|
|
349
|
+
* Effect makes the call. The Effect's output is the pick's answer.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* import { assign } from "xstate";
|
|
354
|
+
* import { z } from "zod";
|
|
355
|
+
* import { agent, defineEvent, fail, gate, jr2Setup, mcp } from "@jr2/orchestrator";
|
|
356
|
+
*
|
|
357
|
+
* const postMessage = defineEvent({
|
|
358
|
+
* name: "post_message",
|
|
359
|
+
* description: "Post one message to the team's channel. A person approves it first.",
|
|
360
|
+
* input: z.object({ text: z.string() }),
|
|
361
|
+
* });
|
|
362
|
+
* const approve = defineEvent({ name: "approve", input: z.object({ by: z.string() }) });
|
|
363
|
+
* const reject = defineEvent({ name: "reject", input: z.object({ by: z.string() }) });
|
|
364
|
+
*
|
|
365
|
+
* export const machine = jr2Setup({
|
|
366
|
+
* types: {} as { context: { text: string } },
|
|
367
|
+
* events: [postMessage, approve, reject],
|
|
368
|
+
* actors: {
|
|
369
|
+
* writer: agent({
|
|
370
|
+
* model: "anthropic/claude-haiku-5-5",
|
|
371
|
+
* instructions: "You write the release note for the team.",
|
|
372
|
+
* workspace: "none",
|
|
373
|
+
* }),
|
|
374
|
+
* // A person answers it, in the Console or with `jr2 answer`.
|
|
375
|
+
* approval: gate({ meta: z.object({ text: z.string() }), delivers: [approve, reject], look: "none" }),
|
|
376
|
+
* // "slack-acme" is a remote server of the mcp block of jr2.config.ts.
|
|
377
|
+
* post: mcp.call("slack-acme", "post_message", {
|
|
378
|
+
* input: z.object({ channel: z.string(), text: z.string() }),
|
|
379
|
+
* }),
|
|
380
|
+
* fail: fail(),
|
|
381
|
+
* },
|
|
382
|
+
* }).createMachine({
|
|
383
|
+
* id: "release-note",
|
|
384
|
+
* context: { text: "" },
|
|
385
|
+
* on: { "agent.fault": ".failed" },
|
|
386
|
+
* initial: "writing",
|
|
387
|
+
* states: {
|
|
388
|
+
* writing: {
|
|
389
|
+
* invoke: { src: "writer", input: { prompt: "Write the release note, then post it." } },
|
|
390
|
+
* initial: "ready",
|
|
391
|
+
* states: {
|
|
392
|
+
* ready: {
|
|
393
|
+
* on: {
|
|
394
|
+
* post_message: { target: "approving", actions: assign({ text: ({ event }) => event.text }) },
|
|
395
|
+
* },
|
|
396
|
+
* },
|
|
397
|
+
* approving: {
|
|
398
|
+
* invoke: { src: "approval", input: ({ context }) => ({ meta: { text: context.text } }) },
|
|
399
|
+
* on: { approve: "posting", reject: "ready" },
|
|
400
|
+
* },
|
|
401
|
+
* posting: {
|
|
402
|
+
* invoke: {
|
|
403
|
+
* src: "post",
|
|
404
|
+
* input: ({ context }) => ({ channel: "#releases", text: context.text }),
|
|
405
|
+
* // On another outcome, the Agent reads the output as the answer to its pick.
|
|
406
|
+
* onDone: [
|
|
407
|
+
* { guard: ({ event }) => event.output.outcome === "returned" && !event.output.isError, target: "#posted" },
|
|
408
|
+
* { target: "ready" },
|
|
409
|
+
* ],
|
|
410
|
+
* },
|
|
411
|
+
* },
|
|
412
|
+
* },
|
|
413
|
+
* },
|
|
414
|
+
* posted: { id: "posted", type: "final" },
|
|
415
|
+
* failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
|
|
416
|
+
* },
|
|
417
|
+
* });
|
|
418
|
+
* ```
|
|
419
|
+
*
|
|
420
|
+
* @see docs/concepts/effects.md
|
|
421
|
+
* @see docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md
|
|
422
|
+
*/
|
|
423
|
+
export const mcp: { readonly call: typeof call; readonly read: typeof read; readonly prompt: typeof prompt } =
|
|
424
|
+
Object.freeze({ call, read, prompt });
|
package/src/model-keys.ts
CHANGED
|
@@ -50,3 +50,27 @@ export const SIGNING_CREDENTIALS: readonly string[] = [
|
|
|
50
50
|
"AWS_PROFILE",
|
|
51
51
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
52
52
|
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Names whose value is the path of a credential or trust file that the Harness PROCESS reads (the
|
|
56
|
+
* AWS SDK, Google's library), when `harness.env` or `harness.envFrom` sets one (ADR-0076). The
|
|
57
|
+
* Harness runs with the control Stand-in in its memory, so such a file must be one that the Agent's
|
|
58
|
+
* code cannot write: `~/.aws/config` can name a `credential_process`, which the SDK runs. `jr2 up`
|
|
59
|
+
* refuses a path that the domain writes for certain (`DOMAIN_WRITABLE_ROOTS`), and warns on any
|
|
60
|
+
* other, because whether the Sandbox's uid can write a path of the image is the image's, which jr2
|
|
61
|
+
* cannot read. An image's own `ENV` never reaches the Harness process.
|
|
62
|
+
*/
|
|
63
|
+
export const CREDENTIAL_FILES: readonly string[] = [
|
|
64
|
+
"AWS_CONFIG_FILE",
|
|
65
|
+
"AWS_SHARED_CREDENTIALS_FILE",
|
|
66
|
+
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
|
67
|
+
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
|
|
68
|
+
"AWS_CA_BUNDLE",
|
|
69
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
70
|
+
"GOOGLE_API_CERTIFICATE_CONFIG",
|
|
71
|
+
"CLOUDSDK_CONFIG",
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/** Folders of a Sandbox's Harness container that the domain writes in every pod: the Worktree root,
|
|
75
|
+
* the temporary folders, `/dev/shm`, and the HOME that jr2 gives an image with no `USER`. */
|
|
76
|
+
export const DOMAIN_WRITABLE_ROOTS: readonly string[] = ["/work", "/tmp", "/var/tmp", "/dev/shm", "/home/jr2"];
|
package/src/mount-digest.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
// What a mounted ConfigMap holds, as one digest (ADR-0038, ADR-0069). A
|
|
2
|
-
// the Orchestrator reads `jr2-images
|
|
3
|
-
// own period. `jr2 up` digests the data it
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// What a mounted ConfigMap or digests Secret holds, as one digest (ADR-0038, ADR-0069, ADR-0078). A
|
|
2
|
+
// write to one rolls no pod, so the Orchestrator reads `jr2-images`, `jr2-held` and the two digests
|
|
3
|
+
// Secrets through mounts that the kubelet refreshes on its own period. `jr2 up` digests the data it
|
|
4
|
+
// applied, `GET /assembly` reports the digest of what the process reads, and `jr2 up` waits until
|
|
5
|
+
// the two agree. Then the door's checks and the provisions after the converge read the data that
|
|
6
|
+
// this converge wrote, and a Caller token or a Hook secret that it wrote works.
|
|
6
7
|
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
9
|
import { readdir, readFile } from "node:fs/promises";
|
|
9
10
|
import { join } from "node:path";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
CALLER_DIGESTS_KEY,
|
|
13
|
+
CALLER_DIGESTS_SECRET,
|
|
14
|
+
HELD_CONFIGMAP,
|
|
15
|
+
HELD_MOUNT,
|
|
16
|
+
HOOK_DIGESTS_KEY,
|
|
17
|
+
HOOK_DIGESTS_SECRET,
|
|
18
|
+
IMAGES_CONFIGMAP,
|
|
19
|
+
IMAGES_MOUNT,
|
|
20
|
+
} from "./names.ts";
|
|
11
21
|
|
|
12
22
|
/** The ConfigMaps the Orchestrator reads through a mount, by name, and where each is mounted. */
|
|
13
23
|
export const MOUNTED_CONFIGMAPS: Readonly<Record<string, string>> = {
|
|
@@ -15,7 +25,18 @@ export const MOUNTED_CONFIGMAPS: Readonly<Record<string, string>> = {
|
|
|
15
25
|
[HELD_CONFIGMAP]: HELD_MOUNT,
|
|
16
26
|
};
|
|
17
27
|
|
|
18
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* The digests Secrets (ADR-0078), by name, and the key of each. For these, `GET /assembly` reports
|
|
30
|
+
* the digest of the file content that the process LOADED (`watchCallerDigests`), not of the file:
|
|
31
|
+
* the authenticator and the Hook door use what is loaded, and the poll can be some seconds behind
|
|
32
|
+
* the file. The content is digests of tokens, never a value.
|
|
33
|
+
*/
|
|
34
|
+
export const DIGEST_SECRETS: Readonly<Record<string, string>> = {
|
|
35
|
+
[CALLER_DIGESTS_SECRET]: CALLER_DIGESTS_KEY,
|
|
36
|
+
[HOOK_DIGESTS_SECRET]: HOOK_DIGESTS_KEY,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** One digest of a mounted object's `data`: the same bytes give the same digest at both ends. */
|
|
19
40
|
export function configMapDigest(data: Readonly<Record<string, string>>): string {
|
|
20
41
|
const entries = Object.keys(data)
|
|
21
42
|
.sort()
|
|
@@ -51,3 +72,16 @@ export async function readMountDigests(
|
|
|
51
72
|
}
|
|
52
73
|
return out;
|
|
53
74
|
}
|
|
75
|
+
|
|
76
|
+
/** The digest of what each digests Secret's watcher has loaded, by name (ADR-0078). A Secret with
|
|
77
|
+
* no watcher, or whose file was not read, is left out, as an absent mount is. */
|
|
78
|
+
export function loadedDigests(
|
|
79
|
+
watched: Readonly<Record<string, { loaded(): string | undefined } | undefined>>,
|
|
80
|
+
): Record<string, string> {
|
|
81
|
+
const out: Record<string, string> = {};
|
|
82
|
+
for (const [name, watcher] of Object.entries(watched)) {
|
|
83
|
+
const digest = watcher?.loaded();
|
|
84
|
+
if (digest !== undefined) out[name] = digest;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
package/src/names.ts
CHANGED
|
@@ -18,18 +18,27 @@ export const INSTANCE_HARNESS_SERVICE = "jr2-instance-harness";
|
|
|
18
18
|
export const INSTANCE_HARNESS_PORT = 8080;
|
|
19
19
|
|
|
20
20
|
/** The Instance Effector (ADR-0068): its Deployment, headless Service, NetworkPolicy and Secret all
|
|
21
|
-
* carry this name. `jr2 up` converges them when a registered Machine places an Effect there
|
|
21
|
+
* carry this name. `jr2 up` converges them when a registered Machine places an Effect there, or a
|
|
22
|
+
* Hook needs it (a `mac` verifier, or a kind that declares `posts`). The
|
|
22
23
|
* port is the Effector's own (`JR2_EFFECTOR_PORT` default) — none of the Harness pod's ports. */
|
|
23
24
|
export const INSTANCE_EFFECTOR = "jr2-instance-effector";
|
|
24
25
|
export const EFFECTOR_PORT = 8082;
|
|
25
26
|
|
|
26
27
|
/** Where a Harness persists its conversations, and the env var that tells it so (ADR-0031): one
|
|
27
28
|
* fixed path in both placements — the Instance Harness's PersistentVolumeClaim (deploy.ts) and a
|
|
28
|
-
* Sandbox's emptyDir (sandbox-kube.ts), each mounted into the Harness container alone.
|
|
29
|
-
|
|
29
|
+
* Sandbox's emptyDir (sandbox-kube.ts), each mounted into the Harness container alone. It is under
|
|
30
|
+
* `/run/jr2`, as each path that the Harness holds alone is, so no process that the Harness confines
|
|
31
|
+
* can read an Agent's history or write into it (ADR-0076). */
|
|
32
|
+
export const CONVERSATIONS_DIR = "/run/jr2/conversations";
|
|
30
33
|
export const CONVERSATIONS_DIR_ENV = "JR2_CONVERSATIONS_DIR";
|
|
31
34
|
export const CONVERSATIONS_VOLUME = "conversations";
|
|
32
35
|
|
|
36
|
+
/** Where the Harness of a Sandbox listens for a person's shell (`jr2 workspace shell`, ADR-0076):
|
|
37
|
+
* a folder under `/run/jr2`, an emptyDir mounted into the Harness container alone. No process that
|
|
38
|
+
* the Harness confines can make a file there, so none can listen in the Harness's place. */
|
|
39
|
+
export const SHELL_DIR = "/run/jr2/shell";
|
|
40
|
+
export const SHELL_VOLUME = "shell";
|
|
41
|
+
|
|
33
42
|
/** The harness config ConfigMap the stock Harness boots from (ADR-0018): what this instance can
|
|
34
43
|
* REACH — the custom model provider minus its key, and the catalog providers' gateways (ADR-0059). No Agents ride it: a Machine carries its own
|
|
35
44
|
* (ADR-0049), and the definition rides each admission, so this holds deployment facts alone
|
|
@@ -51,6 +60,33 @@ export const IMAGES_MOUNT = "/etc/jr2/images";
|
|
|
51
60
|
/** The ConfigMap key, hence the filename under the mount — the two-sided contract's other half. */
|
|
52
61
|
export const IMAGES_KEY = "images.json";
|
|
53
62
|
|
|
63
|
+
/** The digests of the named Caller tokens (ADR-0078): one key, `CALLER_DIGESTS_KEY`, a JSON object
|
|
64
|
+
* `{ "<name>": "<sha-256 hex of the value>" }`. `jr2 up` writes it, and the Orchestrator reads it
|
|
65
|
+
* through a MOUNT and reads it again when it changes, so a rotation rolls no pod. Until the kubelet
|
|
66
|
+
* updates the mount, about a minute, the old value still works, so `jr2 up` and `jr2 caller rotate`
|
|
67
|
+
* wait until `GET /assembly` reports the new content as loaded. No value is here: each value is in
|
|
68
|
+
* its own Secret (`callerTokenSecret`), which no pod mounts. */
|
|
69
|
+
export const CALLER_DIGESTS_SECRET = "jr2-caller-digests";
|
|
70
|
+
/** Where the Orchestrator reads the digests. */
|
|
71
|
+
export const CALLER_DIGESTS_MOUNT = "/etc/jr2/callers";
|
|
72
|
+
/** The Secret's key, hence the filename under the mount. */
|
|
73
|
+
export const CALLER_DIGESTS_KEY = "digests.json";
|
|
74
|
+
/** The digests of the `token` Hooks' secrets (ADR-0078): one key, `HOOK_DIGESTS_KEY`, a JSON object
|
|
75
|
+
* `{ "<secret name>": "<sha-256 hex of the value>" }`. The door compares the sha-256 of a delivery's
|
|
76
|
+
* token with it, so the Orchestrator holds no Hook secret. Its own Secret, not the Caller digests':
|
|
77
|
+
* `jr2 caller rotate` writes that one whole. The Orchestrator reads it through a mount, again when
|
|
78
|
+
* it changes. */
|
|
79
|
+
export const HOOK_DIGESTS_SECRET = "jr2-hook-digests";
|
|
80
|
+
/** Where the Orchestrator reads the Hook digests. */
|
|
81
|
+
export const HOOK_DIGESTS_MOUNT = "/etc/jr2/hooks";
|
|
82
|
+
/** The Secret's key, hence the filename under the mount. */
|
|
83
|
+
export const HOOK_DIGESTS_KEY = "digests.json";
|
|
84
|
+
/** The Secret that holds one named Caller token's value (ADR-0078), which `jr2 caller token <name>`
|
|
85
|
+
* reads over the kube API. */
|
|
86
|
+
export function callerTokenSecret(name: string): string {
|
|
87
|
+
return `jr2-caller-${name}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
54
90
|
/** The HARNESS containers' env Secret — the literal `harness.env` values, never the Instance token
|
|
55
91
|
* (ADR-0013) and never a model key (ADR-0059: those are held). */
|
|
56
92
|
export const HARNESS_ENV_SECRET = "jr2-harness-env";
|
|
@@ -98,12 +134,23 @@ export const sandboxCustodianConfigMap = (sandbox: string): string => `${sandbox
|
|
|
98
134
|
* (`workspaceHeldPod`), so the two cannot collide. */
|
|
99
135
|
export const INSTANCE_HARNESS_HELD_POD = "instance";
|
|
100
136
|
|
|
101
|
-
/** The Custodian's
|
|
102
|
-
* HTTPS proxy on loopback, the kubelet's probe, and the
|
|
103
|
-
*
|
|
137
|
+
/** The Custodian's ports (ADR-0059), all taken on every Harness pod (ADR-0005): the Harness's
|
|
138
|
+
* HTTPS proxy on loopback, the kubelet's probe, and the fetch listener that the ask rides to the
|
|
139
|
+
* Orchestrator, at the address `jr2-upload-pack` defaults to (ADR-0053). The agent routes are not
|
|
140
|
+
* on a port: they are on the control socket ({@link CUSTODIAN_CONTROL_SOCKET}, ADR-0076). */
|
|
104
141
|
export const CUSTODIAN_PORT = 15001;
|
|
105
142
|
export const CUSTODIAN_HEALTH_PORT = 15021;
|
|
106
|
-
export const
|
|
143
|
+
export const CUSTODIAN_FETCH_PORT = 8081;
|
|
144
|
+
|
|
145
|
+
/** The folder of the Custodian's control socket (ADR-0076): an emptyDir that the Harness container
|
|
146
|
+
* and the Custodian container alone mount, at this path in both. It is under `/run/jr2`, so no
|
|
147
|
+
* process that the Harness confines can make a socket here, and none can take the control
|
|
148
|
+
* listener's place while the Custodian restarts. The Custodian writes it; the Harness mounts it
|
|
149
|
+
* read-only, which still lets it connect to the socket. */
|
|
150
|
+
export const CUSTODIAN_SOCKET_DIR = "/run/jr2/custodian";
|
|
151
|
+
/** The control socket: the Custodian's listener for the five agent routes, which carry the control
|
|
152
|
+
* Stand-in (ADR-0059, ADR-0076). */
|
|
153
|
+
export const CUSTODIAN_CONTROL_SOCKET = `${CUSTODIAN_SOCKET_DIR}/control.sock`;
|
|
107
154
|
|
|
108
155
|
/** The scaffold's default deploy-key Secret name (ADR-0047/0051): what `jr2 init`'s wildcard
|
|
109
156
|
* `git.credentials` entry names as its `sshKey`, and what `jr2 up`'s ssh offer generates into when
|
|
@@ -130,6 +177,34 @@ export const LABEL_REPO_BOUND = "jr2.dev/bound";
|
|
|
130
177
|
export const ANNOTATION_REPO_IDENTITY = "jr2.dev/identity";
|
|
131
178
|
export const ANNOTATION_REPO_LAST_ATTACHED = "jr2.dev/last-attached";
|
|
132
179
|
|
|
180
|
+
/** What a Workspace writes on its Sandbox CR (ADR-0012, ADR-0021). The labels carry its run, its
|
|
181
|
+
* Workflow and a selector form of its path; the annotation carries the exact path, which the
|
|
182
|
+
* `jr2 workspace` verbs compare a handle with (ADR-0009). The keepalive is the Lease's stamp, and
|
|
183
|
+
* the hold mark says whether a run still holds the Sandbox: `held` from the Lease's first write,
|
|
184
|
+
* `released` when the Lease stops for the body's final state, a fault, a cancel or a parent that
|
|
185
|
+
* leaves the Workspace's state. */
|
|
186
|
+
export const LABEL_SANDBOX_RUN = "jr2.dev/run";
|
|
187
|
+
export const LABEL_SANDBOX_WORKFLOW = "jr2.dev/workflow";
|
|
188
|
+
export const LABEL_SANDBOX_WORKSPACE = "jr2.dev/workspace";
|
|
189
|
+
export const ANNOTATION_WORKSPACE_PATH = "jr2.dev/workspace-path";
|
|
190
|
+
export const ANNOTATION_KEEPALIVE = "jr2.dev/keepalive";
|
|
191
|
+
export const ANNOTATION_HOLD = "jr2.dev/hold";
|
|
192
|
+
export const HOLD_HELD = "held";
|
|
193
|
+
export const HOLD_RELEASED = "released";
|
|
194
|
+
|
|
195
|
+
/** The selector form of a Workspace path for the `jr2.dev/workspace` label (ADR-0012): a label
|
|
196
|
+
* value holds at most 63 characters from `[A-Za-z0-9._-]` and starts and ends with an alphanumeric.
|
|
197
|
+
* Each other character becomes `-`; a value above 63 characters is cut and ends with a hash of the
|
|
198
|
+
* whole path. For `kubectl -l` alone: no jr2 reader selects by it. */
|
|
199
|
+
export function workspaceLabel(path: string): string {
|
|
200
|
+
const form = path.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, "");
|
|
201
|
+
if (form.length <= 63) return form;
|
|
202
|
+
let h = 0;
|
|
203
|
+
for (const ch of path) h = (h * 31 + ch.charCodeAt(0)) | 0;
|
|
204
|
+
const hash = (h >>> 0).toString(36);
|
|
205
|
+
return `${form.slice(0, 62 - hash.length).replace(/[^A-Za-z0-9]+$/, "")}-${hash}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
133
208
|
/** The mark one ask leaves on a Sandbox CR, per Repo key (ADR-0053): a timestamp the Orchestrator
|
|
134
209
|
* writes when something inside the pod asks for a fetch. The operator copies it onto the pod, and
|
|
135
210
|
* the node's cache agent takes `asked` as the later of the pod's creation and this — so a fetch
|