@jr2/orchestrator 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +86 -9
- package/docs/concepts/architecture.md +41 -28
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +7 -5
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +4 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +50 -22
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +5 -4
- package/docs/concepts/pools-and-sources.md +187 -11
- package/docs/concepts/secrets-and-trust.md +73 -33
- package/docs/concepts/the-menu.md +11 -0
- package/docs/concepts/workspaces.md +6 -5
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/guides/add-tools-to-the-sandbox.md +55 -11
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +24 -8
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/fix-a-failed-converge.md +18 -6
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +9 -6
- package/docs/guides/look-inside-a-sandbox.md +101 -31
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-one-worker-per-item.md +15 -9
- package/docs/guides/set-an-agents-access-and-dials.md +2 -0
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +1 -1
- package/docs/guides/watch-a-run.md +127 -121
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +17 -7
- package/docs/llms.txt +81 -15
- package/docs/reference/authoring-surface/agent.md +1 -1
- package/docs/reference/authoring-surface/answer.md +1 -1
- package/docs/reference/authoring-surface/arg.md +1 -1
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +1 -1
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +1 -1
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +1 -1
- package/docs/reference/authoring-surface/file.md +1 -1
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +1 -1
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +1 -1
- package/docs/reference/authoring-surface/jr2setup.md +13 -3
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +5 -3
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +1 -1
- package/docs/reference/authoring-surface/pool.md +77 -9
- package/docs/reference/authoring-surface/push.md +1 -1
- package/docs/reference/authoring-surface/readyof.md +1 -1
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +1 -1
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +56 -1
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +14 -1
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
- package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-binding.md +1 -1
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandoutput.md +1 -1
- package/docs/reference/authoring-surface/type-customize.md +16 -1
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +1 -1
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-framepath.md +1 -1
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +1 -1
- package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +1 -1
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +16 -7
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +1 -1
- package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
- package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
- package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
- package/docs/reference/authoring-surface/workspace.md +1 -1
- package/docs/reference/authoring-surface/worktree.md +1 -1
- package/docs/reference/authoring-surface.md +88 -167
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +235 -2
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
- package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +9 -2
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +18 -14
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +32 -18
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +116 -34
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +7 -6
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -18
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +4 -3
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +5 -4
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +7 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +51 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +17 -13
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
- package/docs/reference/decisions.md +6 -4
- package/docs/reference/glossary.md +96 -42
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +976 -149
- package/docs/why.md +2 -1
- package/package.json +10 -3
- package/src/actor.ts +36 -10
- package/src/agent.ts +50 -0
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +52 -7
- package/src/config.ts +550 -1
- package/src/conversations.ts +453 -0
- package/src/custodian.ts +8 -2
- package/src/customize.ts +143 -4
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +2 -1
- package/src/effect.ts +60 -7
- package/src/effector-wire.ts +272 -27
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +10 -4
- package/src/held-secrets.ts +72 -23
- package/src/holds.ts +2 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +76 -38
- package/src/http.ts +889 -94
- package/src/index.ts +16 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +22 -3
- package/src/machine-doc.ts +26 -1
- package/src/mcp-effect.ts +424 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +64 -2
- package/src/notices.ts +138 -10
- package/src/open-body.ts +25 -11
- package/src/open-effect.ts +17 -4
- package/src/open.ts +4 -2
- package/src/parts.ts +87 -9
- package/src/party.ts +2 -1
- package/src/pick.ts +2 -2
- package/src/pool.ts +1142 -163
- package/src/registration.ts +203 -13
- package/src/run-host.ts +1509 -171
- package/src/run-list.ts +191 -0
- package/src/sandbox-kube.ts +53 -5
- package/src/secret-placement.ts +239 -33
- package/src/server.ts +53 -4
- package/src/setup.ts +72 -32
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +263 -22
- package/src/wire.ts +17 -0
- package/src/workspace.ts +50 -6
- package/src/worktree.ts +3 -3
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/src/effect.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { randomUUID } from "node:crypto";
|
|
|
39
39
|
import type { ActorLogic, AnyActorRef, EventObject, Snapshot } from "xstate";
|
|
40
40
|
import { ambientHandlesFor, type AmbientHandles } from "./ambient.ts";
|
|
41
41
|
import { boundRun, type AnyActorSystem } from "./registration.ts";
|
|
42
|
-
import type { HttpMethod, HttpOutput, TextPart, WireAuth } from "./effector-wire.ts";
|
|
42
|
+
import type { HttpMethod, HttpOutput, McpOutput, TextPart, WireAuth } from "./effector-wire.ts";
|
|
43
43
|
import type { ExecOutcome, ExecRead, ExecRequest, SyncAnswer } from "./wire.ts";
|
|
44
44
|
import { bindOpenSecretIn, type EffectorPortFactory, type OpenSecretIn } from "./http-effect.ts";
|
|
45
45
|
import { commandEnvParts, credentialedCommandWith, sandboxEffectorPort, type CommandEnv } from "./sandbox-effect.ts";
|
|
@@ -240,6 +240,51 @@ export type DispatchParts = {
|
|
|
240
240
|
readonly openSecret: boolean;
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
+
/** What an `mcp` Effect asks of its server (ADR-0079): call a tool, read a resource, or get a
|
|
244
|
+
* prompt. */
|
|
245
|
+
export type McpOp = "call" | "read" | "prompt";
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* An `mcp` Effect's plain data (ADR-0067, ADR-0079): what `jr2 up` and the Machine doc read — the
|
|
249
|
+
* operation, the server by its name in the `mcp` block of `jr2.config.ts`, the tool (a call), the
|
|
250
|
+
* resource's uri (a read) or the prompt's name (a prompt), the input's keys, the author's timeout,
|
|
251
|
+
* the bound on the output, and whether a restore may send it again. The server's url and secret
|
|
252
|
+
* are the config's, never a part: `jr2 up` reads them from there, and the Instance Effector holds
|
|
253
|
+
* them in its `effector.json` and its secrets.
|
|
254
|
+
*/
|
|
255
|
+
export type McpParts = {
|
|
256
|
+
readonly kind: "mcp";
|
|
257
|
+
readonly op: McpOp;
|
|
258
|
+
readonly server: string;
|
|
259
|
+
/** `mcp.call`'s tool. */
|
|
260
|
+
readonly tool?: string;
|
|
261
|
+
/** `mcp.read`'s resource uri. */
|
|
262
|
+
readonly uri?: string;
|
|
263
|
+
/** `mcp.prompt`'s prompt name. */
|
|
264
|
+
readonly prompt?: string;
|
|
265
|
+
/** The keys of the input schema: what a pick param may fill (ADR-0067's load walk reads them). */
|
|
266
|
+
readonly inputKeys: readonly string[];
|
|
267
|
+
/** The author's timeout, in milliseconds. Absent: the server's `timeout` in `jr2.config.ts`. */
|
|
268
|
+
readonly timeoutMs?: number;
|
|
269
|
+
/** The most bytes of `text`, and of `structured` as JSON, in the output. */
|
|
270
|
+
readonly maxBody: number;
|
|
271
|
+
/** Marked by the author: a restore that cannot find the Effect sends it again. The server's
|
|
272
|
+
* `idempotentHint` is not read (ADR-0079). */
|
|
273
|
+
readonly idempotent: boolean;
|
|
274
|
+
/** Always empty: an `mcp` Effect names no secret of its own. Its server's secret is placed in
|
|
275
|
+
* the Instance Effector by `jr2 up`, from the config (ADR-0079). */
|
|
276
|
+
readonly secrets: readonly string[];
|
|
277
|
+
/** Never: the server, not a `secret(open)`, is what a package leaves Open (ADR-0054). */
|
|
278
|
+
readonly openSecret?: never;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/** An `mcp` Effect whose server or tool a package left Open (ADR-0054, ADR-0079): the sentinel in
|
|
282
|
+
* that place, until the composer binds it with `customize()`. */
|
|
283
|
+
export type OpenMcpParts = Omit<McpParts, "server" | "tool"> & {
|
|
284
|
+
readonly server: string | typeof open;
|
|
285
|
+
readonly tool?: string | typeof open;
|
|
286
|
+
};
|
|
287
|
+
|
|
243
288
|
/**
|
|
244
289
|
* An Effect whose url or argv a package left Open (ADR-0054): the same parts with the sentinel in
|
|
245
290
|
* that one place. The kind, the placement, the timeout and the secrets the other parts name are
|
|
@@ -264,10 +309,12 @@ export type EffectParts =
|
|
|
264
309
|
| OpenCommandParts
|
|
265
310
|
| OpenHttpParts
|
|
266
311
|
| DispatchParts
|
|
267
|
-
| OpenDispatchParts
|
|
312
|
+
| OpenDispatchParts
|
|
313
|
+
| McpParts
|
|
314
|
+
| OpenMcpParts;
|
|
268
315
|
|
|
269
316
|
/** The kinds, as their parts name them. */
|
|
270
|
-
export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch"]);
|
|
317
|
+
export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch", "mcp"]);
|
|
271
318
|
|
|
272
319
|
/**
|
|
273
320
|
* Where an Effect runs, derived from its kind and what it names, never authored (ADR-0067,
|
|
@@ -275,7 +322,8 @@ export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "pu
|
|
|
275
322
|
* `http` Effect with no Worktree file in the Instance Effector; an `http` whose body is a Worktree
|
|
276
323
|
* file, a `push` and a `command` that names a secret in the Sandbox Effector of its Workspace; a
|
|
277
324
|
* `dispatch` with no target in the Orchestrator's own process, and one with a target (an Open one
|
|
278
|
-
* included: it has one once bound) in the Instance Effector (ADR-0071)
|
|
325
|
+
* included: it has one once bound) in the Instance Effector (ADR-0071); an `mcp` Effect in the
|
|
326
|
+
* Instance Effector, which calls remote servers only (ADR-0079).
|
|
279
327
|
*/
|
|
280
328
|
export type Placement = "harness" | "instance" | "sandbox" | "orchestrator";
|
|
281
329
|
export function placementOf(parts: EffectParts): Placement {
|
|
@@ -286,6 +334,8 @@ export function placementOf(parts: EffectParts): Placement {
|
|
|
286
334
|
return parts.file ? "sandbox" : "instance";
|
|
287
335
|
case "push":
|
|
288
336
|
return "sandbox";
|
|
337
|
+
case "mcp":
|
|
338
|
+
return "instance";
|
|
289
339
|
case "command":
|
|
290
340
|
return (parts.secrets?.length ?? 0) > 0 || parts.openSecret ? "sandbox" : "harness";
|
|
291
341
|
}
|
|
@@ -338,7 +388,7 @@ export function inputShapeOf(parts: EffectParts): InputShape | undefined {
|
|
|
338
388
|
if (parts.kind === "dispatch") {
|
|
339
389
|
return parts.inputKeys === undefined ? undefined : { required: [...parts.inputKeys], optional: [] };
|
|
340
390
|
}
|
|
341
|
-
if (parts.kind === "push") return { required: [...parts.inputKeys], optional: [] };
|
|
391
|
+
if (parts.kind === "push" || parts.kind === "mcp") return { required: [...parts.inputKeys], optional: [] };
|
|
342
392
|
if (parts.kind === "http") {
|
|
343
393
|
return { required: [...parts.inputKeys], optional: parts.file ? ["worktree"] : [] };
|
|
344
394
|
}
|
|
@@ -492,7 +542,7 @@ export type EffectRow =
|
|
|
492
542
|
| { state: "intent"; kind: "command"; slot: string; endpoint: string; placement: string; at: string }
|
|
493
543
|
/** An http Effect's intent names its placement and no replica (ADR-0068): a restore asks every
|
|
494
544
|
* replica. `idempotent` is the author's mark, which the restore rule reads. */
|
|
495
|
-
| { state: "intent"; kind: "http"; slot: string; placement: "instance"; idempotent: boolean; at: string }
|
|
545
|
+
| { state: "intent"; kind: "http" | "mcp"; slot: string; placement: "instance"; idempotent: boolean; at: string }
|
|
496
546
|
/** An Effect of the Sandbox Effector (ADR-0067): its Sandbox, and — for a push or a credentialed
|
|
497
547
|
* command — the commit it runs against, written once the start's answer names it. A restore
|
|
498
548
|
* re-attaches by id, or sends a push or a credentialed command again with only that commit, and
|
|
@@ -590,6 +640,9 @@ const TAILS: Record<EffectRow["kind"], readonly string[]> = {
|
|
|
590
640
|
// settled marker carries both whole. The row keeps the outcome, the run id, its status and the
|
|
591
641
|
// door's HTTP status (ADR-0071).
|
|
592
642
|
dispatch: ["output", "report"],
|
|
643
|
+
// An mcp answer's text and its structured value: the feed's settled marker carries both whole.
|
|
644
|
+
// The row keeps the outcome, `isError` and `truncated` (ADR-0079).
|
|
645
|
+
mcp: ["text", "structured"],
|
|
593
646
|
};
|
|
594
647
|
|
|
595
648
|
/** A kind's tail for one output. The NEWEST push of its Repo Slot whose sync did not finish (`busy`,
|
|
@@ -702,7 +755,7 @@ export type EffectMarker = {
|
|
|
702
755
|
effect: string;
|
|
703
756
|
stage: "started" | "output" | "settled" | "aborted";
|
|
704
757
|
text?: string;
|
|
705
|
-
output?: CommandOutput | WorktreeOutput | HttpOutput | DispatchOutput;
|
|
758
|
+
output?: CommandOutput | WorktreeOutput | HttpOutput | DispatchOutput | McpOutput;
|
|
706
759
|
};
|
|
707
760
|
|
|
708
761
|
// --- The port -----------------------------------------------------------------------------------
|
package/src/effector-wire.ts
CHANGED
|
@@ -182,37 +182,90 @@ export const DispatchedByShape = z.strictObject({
|
|
|
182
182
|
* A `dispatch` start (ADR-0071), which only the Instance Effector serves: a run of `workflow` on the
|
|
183
183
|
* Orchestrator at `url` (its origin, one text part), the body `input` whole, the Instance token as a
|
|
184
184
|
* bearer by name, `dispatchedBy` as the `jr2-dispatched-by` header — `instance` set, since the
|
|
185
|
-
* caller is foreign by construction —
|
|
185
|
+
* caller is foreign by construction — `onBehalfOf` as the `jr2-on-behalf-of` header, and, with
|
|
186
|
+
* `wait`, a poll of `GET /runs/<id>` until the run
|
|
186
187
|
* ends or `timeoutMs` passes.
|
|
188
|
+
*
|
|
189
|
+
* `keyOnly` in place of `input`: a key-only ask, sent when the slot's schema refuses the body. The
|
|
190
|
+
* Effector sends the `jr2-key-only` header and no body; the door answers the run under the key, or
|
|
191
|
+
* `{ existing: false }`, which the Effector settles as `absent`. One of the two is set, never both.
|
|
187
192
|
*/
|
|
188
|
-
export const DispatchStartShape = z
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
193
|
+
export const DispatchStartShape = z
|
|
194
|
+
.strictObject({
|
|
195
|
+
kind: z.literal("dispatch"),
|
|
196
|
+
url: TextShape.refine(
|
|
197
|
+
(parts) => parts.length === 1 && "text" in parts[0]! && ORIGIN.test(parts[0].text),
|
|
198
|
+
"a dispatch url is one text part, an origin: http(s)://host[:port]",
|
|
199
|
+
),
|
|
200
|
+
workflow: z.string().regex(WORKFLOW_NAME),
|
|
201
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
202
|
+
keyOnly: z.literal(true).optional(),
|
|
203
|
+
auth: z.strictObject({ bearer: SecretRefShape }),
|
|
204
|
+
// Every id non-empty, as the door's header shape requires (http.ts): an empty one would be the
|
|
205
|
+
// door's 400, which reads as a defect of the input. The effect is a ledger row id, the key. The Go
|
|
206
|
+
// side's `validateDispatch` holds the same rule.
|
|
207
|
+
dispatchedBy: DispatchedByShape.extend({
|
|
208
|
+
run: z.string().min(1),
|
|
209
|
+
slot: z.string().min(1),
|
|
210
|
+
effect: z
|
|
211
|
+
.string()
|
|
212
|
+
.regex(/^[A-Za-z0-9._-]{1,128}$/)
|
|
213
|
+
.refine((id) => id !== "." && id !== ".."),
|
|
214
|
+
instance: z.string().min(1),
|
|
215
|
+
}),
|
|
216
|
+
wait: z.boolean(),
|
|
217
|
+
timeoutMs: z.number().int().positive(),
|
|
218
|
+
// The first Caller of the calling run's chain (ADR-0071, ADR-0078), sent as the
|
|
219
|
+
// `jr2-on-behalf-of` header: printable ASCII, as a header value is, and at most what the door
|
|
220
|
+
// takes. The other Instance records it and does not check it.
|
|
221
|
+
onBehalfOf: z
|
|
204
222
|
.string()
|
|
205
|
-
.regex(/^[
|
|
206
|
-
.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
.regex(/^[\x20-\x7e]{1,256}$/)
|
|
224
|
+
.optional(),
|
|
225
|
+
})
|
|
226
|
+
.refine(
|
|
227
|
+
(req) => (req.input === undefined) !== (req.keyOnly === undefined),
|
|
228
|
+
"a dispatch start carries its input, or keyOnly and no input",
|
|
229
|
+
);
|
|
212
230
|
export type DispatchStartRequest = z.infer<typeof DispatchStartShape>;
|
|
213
231
|
|
|
214
|
-
/**
|
|
215
|
-
|
|
232
|
+
/** An MCP server's name (ADR-0079), as the `mcp` block of `jr2.config.ts` keys it. The Go side's
|
|
233
|
+
* `mcpServer` is the same rule. */
|
|
234
|
+
export const MCP_SERVER = /^[a-z][a-z0-9-]*$/;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* An `mcp` start (ADR-0067, ADR-0079), which only the Instance Effector serves: the server by its
|
|
238
|
+
* name — the Effector reads its url and the name of its secret from `effector.json`, so neither is
|
|
239
|
+
* on the wire — and one request: `call` a tool with its arguments, `read` a resource, or get a
|
|
240
|
+
* `prompt` with its arguments. `timeoutMs` is the author's; absent, the server's `timeout` from
|
|
241
|
+
* `effector.json` applies. `maxBody` bounds the answer's `text` and its `structured` value.
|
|
242
|
+
*/
|
|
243
|
+
export const McpStartShape = z.strictObject({
|
|
244
|
+
kind: z.literal("mcp"),
|
|
245
|
+
mcp: z
|
|
246
|
+
.strictObject({
|
|
247
|
+
server: z.string().regex(MCP_SERVER),
|
|
248
|
+
call: z.strictObject({ tool: z.string().min(1), arguments: z.record(z.string(), z.unknown()) }).optional(),
|
|
249
|
+
read: z.strictObject({ uri: z.string().min(1) }).optional(),
|
|
250
|
+
prompt: z.strictObject({ name: z.string().min(1), arguments: z.record(z.string(), z.string()) }).optional(),
|
|
251
|
+
})
|
|
252
|
+
.refine(
|
|
253
|
+
(m) => [m.call, m.read, m.prompt].filter((x) => x !== undefined).length === 1,
|
|
254
|
+
"an mcp start carries exactly one of call, read, prompt",
|
|
255
|
+
),
|
|
256
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
257
|
+
maxBody: z.number().int().positive(),
|
|
258
|
+
});
|
|
259
|
+
export type McpStartRequest = z.infer<typeof McpStartShape>;
|
|
260
|
+
|
|
261
|
+
/** Every start: the http kinds, a push, a credentialed command, a dispatch, an mcp request. */
|
|
262
|
+
export const StartRequestShape = z.union([
|
|
263
|
+
HttpStartShape,
|
|
264
|
+
PushStartShape,
|
|
265
|
+
CommandStartShape,
|
|
266
|
+
DispatchStartShape,
|
|
267
|
+
McpStartShape,
|
|
268
|
+
]);
|
|
216
269
|
export type StartRequest = z.infer<typeof StartRequestShape>;
|
|
217
270
|
|
|
218
271
|
/** The most bytes of a start's body the Effector reads (its `maxRequestBody`). A longer body
|
|
@@ -379,9 +432,92 @@ export const DispatchOutputShape = z.discriminatedUnion("outcome", [
|
|
|
379
432
|
z.strictObject({ outcome: z.literal("refused"), status: z.number().int(), report: z.array(z.string()) }),
|
|
380
433
|
z.strictObject({ outcome: z.literal("unreachable"), reason: z.string() }),
|
|
381
434
|
z.strictObject({ outcome: z.literal("unknown"), reason: z.string() }),
|
|
435
|
+
// The answer to a key-only ask when no run has the key (ADR-0071): nothing was started. Wire
|
|
436
|
+
// only: the driver makes it the invoke's error, the slot's schema refusal, and the Machine never
|
|
437
|
+
// reads it.
|
|
438
|
+
z.strictObject({ outcome: z.literal("absent") }),
|
|
382
439
|
]);
|
|
383
440
|
export type DispatchWireOutput = z.infer<typeof DispatchOutputShape>;
|
|
384
441
|
|
|
442
|
+
/**
|
|
443
|
+
* An `mcp` Effect's output on the wire (ADR-0067, ADR-0079): `returned`, with `isError` for a call
|
|
444
|
+
* alone; or a reason for `refused`, `unreachable`, `input-required` and `unknown`.
|
|
445
|
+
*/
|
|
446
|
+
export const McpOutputShape = z.union([
|
|
447
|
+
z.strictObject({
|
|
448
|
+
outcome: z.literal("returned"),
|
|
449
|
+
isError: z.boolean().optional(),
|
|
450
|
+
text: z.string(),
|
|
451
|
+
structured: z.unknown().optional(),
|
|
452
|
+
truncated: z.boolean().optional(),
|
|
453
|
+
}),
|
|
454
|
+
z.strictObject({
|
|
455
|
+
outcome: z.enum(["refused", "unreachable", "input-required", "unknown"]),
|
|
456
|
+
reason: z.string(),
|
|
457
|
+
}),
|
|
458
|
+
]);
|
|
459
|
+
|
|
460
|
+
/** The outcomes that an `mcp` Effect gives besides `returned`. Each one has a `reason` in words. */
|
|
461
|
+
type McpNotReturned =
|
|
462
|
+
| { outcome: "refused"; reason: string }
|
|
463
|
+
| { outcome: "unreachable"; reason: string }
|
|
464
|
+
| { outcome: "input-required"; reason: string }
|
|
465
|
+
| { outcome: "unknown"; reason: string }
|
|
466
|
+
| LaterOutcome;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The output of an `mcp.call` Effect: what the `onDone` of its state gets as `event.output`. Every
|
|
470
|
+
* outcome is output. Route on `outcome`:
|
|
471
|
+
*
|
|
472
|
+
* - `returned`: the server answered the call. `isError` is the tool's own `isError`: the tool ran
|
|
473
|
+
* and says that it failed. `text` joins the text of the result's content; a part that is not text
|
|
474
|
+
* is one line that names its type. `structured` is the result's `structuredContent`, when it has
|
|
475
|
+
* one. `text` and `structured` each have the byte bound of the Effect's `maxBody`: past it, jr2
|
|
476
|
+
* cuts `text`, drops `structured`, and sets `truncated`.
|
|
477
|
+
* - `refused`: the request did not run. The server answered an HTTP 3xx or 4xx status (a 401 or a
|
|
478
|
+
* 403 included) at any step, the handshake too; it refused the handshake; or it answered the
|
|
479
|
+
* request with the JSON-RPC error `-32700`, `-32600`, `-32601` or `-32602`: a request that it
|
|
480
|
+
* could not read, an unknown method, an unknown tool, or bad arguments; or it answered a read with
|
|
481
|
+
* `-32002`: the resource does not exist. It is also the Effector's
|
|
482
|
+
* own refusal, with nothing sent: the server or its secret is not in its `effector.json`.
|
|
483
|
+
* - `unreachable`: nothing of the request was sent: the name did not resolve, or the connection,
|
|
484
|
+
* TLS or the handshake failed otherwise (a 5xx, the timeout, an answer that is not MCP's, a
|
|
485
|
+
* revision of MCP that jr2 does not speak).
|
|
486
|
+
* - `input-required`: the server asked for input, which jr2 does not give. After the error
|
|
487
|
+
* `-32042`, the tool did not run. After an `elicitation/create` request, which comes while the
|
|
488
|
+
* tool runs, the tool may have begun. jr2 does not send the request again: a Machine that
|
|
489
|
+
* invokes the Effect again after a Gate can run a part of the tool two times.
|
|
490
|
+
* - `unknown`: the request was sent and no answer came (the timeout, a reset, a stream that ended
|
|
491
|
+
* first), or the server answered with a 5xx or another JSON-RPC error. The tool may have acted.
|
|
492
|
+
* jr2 does not send it again unless it is marked `idempotent`.
|
|
493
|
+
*
|
|
494
|
+
* jr2 replaces each value of an Effect secret that the Effector holds with `[secret:<name>]`, in
|
|
495
|
+
* every string of the output. The union is open ({@link LaterOutcome}): give the rest a last arm
|
|
496
|
+
* with no guard.
|
|
497
|
+
*
|
|
498
|
+
* @see docs/concepts/effects.md
|
|
499
|
+
* @see docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md
|
|
500
|
+
*/
|
|
501
|
+
export type McpCallOutput =
|
|
502
|
+
| { outcome: "returned"; isError: boolean; text: string; structured?: unknown; truncated?: boolean }
|
|
503
|
+
| McpNotReturned;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The output of an `mcp.read` or an `mcp.prompt` Effect. Its outcomes are those of
|
|
507
|
+
* {@link McpCallOutput}, with no `isError`. For a read, `text` joins the text of the resource's
|
|
508
|
+
* contents, and `structured` is the `contents` list: a blob is given only as its `mimeType` and its
|
|
509
|
+
* size. For a prompt, `text` joins the text of the prompt's messages, and `structured` is the
|
|
510
|
+
* `messages` list. Past the bound, `structured` is dropped and `truncated` is set.
|
|
511
|
+
*
|
|
512
|
+
* @see docs/concepts/effects.md
|
|
513
|
+
*/
|
|
514
|
+
export type McpReadOutput =
|
|
515
|
+
| { outcome: "returned"; text: string; structured?: unknown; truncated?: boolean }
|
|
516
|
+
| McpNotReturned;
|
|
517
|
+
|
|
518
|
+
/** The output of any `mcp` Effect: a call's, a read's or a prompt's. */
|
|
519
|
+
export type McpOutput = McpCallOutput | McpReadOutput;
|
|
520
|
+
|
|
385
521
|
/** The replica that holds an Effect, and where to poll it. */
|
|
386
522
|
export const ReplicaShape = z.strictObject({ name: z.string(), address: z.string() });
|
|
387
523
|
export type Replica = z.infer<typeof ReplicaShape>;
|
|
@@ -397,13 +533,122 @@ export const EffectStateShape = z.strictObject({
|
|
|
397
533
|
/** The run a dispatch started, once the door answered and while its wait runs: the Orchestrator
|
|
398
534
|
* writes it to the ledger's intent row, so the caller names its callee during the wait (ADR-0071). */
|
|
399
535
|
run: z.string().optional(),
|
|
400
|
-
output: z
|
|
536
|
+
output: z
|
|
537
|
+
.union([HttpOutputShape, PushOutputShape, CommandOutputShape, DispatchOutputShape, McpOutputShape])
|
|
538
|
+
.optional(),
|
|
401
539
|
});
|
|
402
540
|
export type EffectState = z.infer<typeof EffectStateShape>;
|
|
403
541
|
|
|
404
542
|
/** A refusal's body: the reason, and the replica when it names one. */
|
|
405
543
|
export const RefusalShape = z.strictObject({ error: z.string(), replica: ReplicaShape.optional() });
|
|
406
544
|
|
|
545
|
+
// --- The `mac` check (POST /mac, ADR-0068, ADR-0078) ---------------------------------------------
|
|
546
|
+
//
|
|
547
|
+
// A Hook's `mac` verifier: the Orchestrator builds the bytes that the service signed, and the
|
|
548
|
+
// Instance Effector, which holds the secret, computes the HMAC over them and compares it with the
|
|
549
|
+
// signature in constant time. The answer is "match" or "no match", and nothing more: no HMAC and no
|
|
550
|
+
// secret ever comes back. The Orchestrator makes every check that needs no secret (the window)
|
|
551
|
+
// before it asks.
|
|
552
|
+
|
|
553
|
+
/** The HMAC algorithms of a `mac` check. */
|
|
554
|
+
export const MAC_ALGS = ["sha256", "sha1", "sha512"] as const;
|
|
555
|
+
|
|
556
|
+
/** A `mac` check: the NAME of the secret, the algorithm, the signed bytes and the signature, each
|
|
557
|
+
* in base64. */
|
|
558
|
+
export const MacRequestShape = z.strictObject({
|
|
559
|
+
// A secret's name, as the Effector reads it: a file under its secrets mount.
|
|
560
|
+
secret: z
|
|
561
|
+
.string()
|
|
562
|
+
.regex(/^[A-Za-z0-9._-]{1,253}$/)
|
|
563
|
+
.refine((name) => !name.startsWith("."), "a secret's name does not start with ."),
|
|
564
|
+
alg: z.enum(MAC_ALGS),
|
|
565
|
+
bytes: z.base64(),
|
|
566
|
+
signature: z.base64(),
|
|
567
|
+
});
|
|
568
|
+
export type MacRequest = z.infer<typeof MacRequestShape>;
|
|
569
|
+
|
|
570
|
+
/** The answer to a `mac` check: whether the HMAC of the bytes with the secret is the signature. */
|
|
571
|
+
export const MacAnswerShape = z.strictObject({ match: z.boolean() });
|
|
572
|
+
export type MacAnswer = z.infer<typeof MacAnswerShape>;
|
|
573
|
+
|
|
574
|
+
/** The path of a `mac` check on the Instance Effector. */
|
|
575
|
+
export const MAC_PATH = "/mac";
|
|
576
|
+
|
|
577
|
+
/** The `mac` check, as the Hook's door asks it: `true` for a match. It rejects when no Effector can
|
|
578
|
+
* answer, and the door then answers 503. */
|
|
579
|
+
export type HookMac = (req: {
|
|
580
|
+
secret: string;
|
|
581
|
+
alg: (typeof MAC_ALGS)[number];
|
|
582
|
+
bytes: Uint8Array;
|
|
583
|
+
signature: Uint8Array;
|
|
584
|
+
}) => Promise<boolean>;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The `mac` check over the Instance Effector's signed wire (ADR-0068): one replica, any one, since
|
|
588
|
+
* each holds the same secrets. A replica that was never reached is passed over for the next. An
|
|
589
|
+
* answer is trusted only with the Effector's signature, as an Effect's is.
|
|
590
|
+
*/
|
|
591
|
+
export function createMacClient(opts: {
|
|
592
|
+
key: Buffer | string;
|
|
593
|
+
transport: EffectorTransport;
|
|
594
|
+
now?: () => number;
|
|
595
|
+
}): HookMac {
|
|
596
|
+
const now = opts.now ?? Date.now;
|
|
597
|
+
return async ({ secret, alg, bytes, signature }) => {
|
|
598
|
+
const body = JSON.stringify({
|
|
599
|
+
secret,
|
|
600
|
+
alg,
|
|
601
|
+
bytes: Buffer.from(bytes).toString("base64"),
|
|
602
|
+
signature: Buffer.from(signature).toString("base64"),
|
|
603
|
+
} satisfies MacRequest);
|
|
604
|
+
const addresses = shuffle(await opts.transport.replicas().catch(notSent));
|
|
605
|
+
if (addresses.length === 0) throw new EffectorNotSent("no Effector replica is serving");
|
|
606
|
+
let last: unknown;
|
|
607
|
+
for (const address of addresses) {
|
|
608
|
+
const time = String(now());
|
|
609
|
+
const nonce = randomBytes(16).toString("hex");
|
|
610
|
+
const headers = {
|
|
611
|
+
"content-type": "application/json",
|
|
612
|
+
[HEADER_TIME]: time,
|
|
613
|
+
[HEADER_NONCE]: nonce,
|
|
614
|
+
[HEADER_SIGNATURE]: requestMac(opts.key, "POST", address, MAC_PATH, time, nonce, body),
|
|
615
|
+
};
|
|
616
|
+
let answer: RawAnswer;
|
|
617
|
+
try {
|
|
618
|
+
answer = await opts.transport.send(address, "POST", MAC_PATH, headers, body);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
if (neverReached(err)) {
|
|
621
|
+
last = err;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
throw err;
|
|
625
|
+
}
|
|
626
|
+
const want = responseMac(opts.key, answer.status, "POST", MAC_PATH, nonce, answer.body);
|
|
627
|
+
if (!sameString(answer.headers[HEADER_SIGNATURE] ?? "", want)) {
|
|
628
|
+
throw new EffectorForged(
|
|
629
|
+
`the answer to POST ${MAC_PATH} from ${address} does not carry the Effector's signature`,
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
if (answer.status === 503) {
|
|
633
|
+
last = new EffectorNotSent(`the Effector replica at ${address} is stopping`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (answer.status !== 200) {
|
|
637
|
+
const refusal = RefusalShape.safeParse(parseJson(answer.body));
|
|
638
|
+
throw new Error(
|
|
639
|
+
`the Effector answered a mac check with ${answer.status}: ${refusal.success ? refusal.data.error : "no reason"}`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
const parsed = MacAnswerShape.safeParse(parseJson(answer.body));
|
|
643
|
+
if (!parsed.success) throw new EffectorMalformed("the Effector's answer to a mac check is not the wire's shape");
|
|
644
|
+
return parsed.data.match;
|
|
645
|
+
}
|
|
646
|
+
throw last instanceof EffectorNotSent
|
|
647
|
+
? last
|
|
648
|
+
: new EffectorNotSent(`no Effector replica answered: ${(last as Error | undefined)?.message ?? "none"}`);
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
407
652
|
// --- Signatures ----------------------------------------------------------------------------------
|
|
408
653
|
|
|
409
654
|
export const HEADER_TIME = "x-jr2-time";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The brand of a Gate slot (ADR-0011), alone in its module: the walk (setup.ts, party.ts,
|
|
2
|
+
// effect-checks.ts) and the Chart door read it, and gate.ts imports the registration table, which
|
|
3
|
+
// imports party.ts. A leaf with no imports lets each of them ask without a cycle.
|
|
4
|
+
|
|
5
|
+
/** Is this actor logic a Gate slot (`gate({ … })`)? A duck test on the brand, as `isAgent` is. */
|
|
6
|
+
export function isGateSlotLogic(logic: unknown): boolean {
|
|
7
|
+
if (typeof logic !== "object" || logic === null) return false;
|
|
8
|
+
const parts = (logic as { gateSlot?: { delivers?: unknown } }).gateSlot;
|
|
9
|
+
return typeof parts === "object" && parts !== null && Array.isArray(parts.delivers);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Does this invoke run a Gate: the plain `gate`, or a Gate slot? */
|
|
13
|
+
export function isGateInvoke(inv: { src?: unknown } | undefined, actors: Record<string, unknown>): boolean {
|
|
14
|
+
if (inv?.src === "gate") return true;
|
|
15
|
+
return typeof inv?.src === "string" && isGateSlotLogic(actors[inv.src]);
|
|
16
|
+
}
|