@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/assembly-checks.ts
CHANGED
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
import { isAbsolute, relative, sep } from "node:path";
|
|
8
8
|
import type { AnyStateMachine } from "xstate";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import type
|
|
10
|
+
import { isStdioServer, type AnswerGrant, type GateGrant, type JR2Config } from "./config.ts";
|
|
11
|
+
import { senderIds } from "./party.ts";
|
|
12
|
+
import type { AgentMcpServer } from "./agent.ts";
|
|
11
13
|
import { imageContextDigest, type ImageRefs } from "./images.ts";
|
|
12
14
|
import { openPartsText } from "./open-effect.ts";
|
|
13
|
-
import { customizeLine, partsOf, placedEffectsOf, workspacePodsOf } from "./parts.ts";
|
|
15
|
+
import { asMachine, customizeLine, partsOf, placedEffectsOf, workspacePodsOf } from "./parts.ts";
|
|
16
|
+
import { isGateSlot } from "./gate.ts";
|
|
17
|
+
import { isStandingPool, standingPoolOf } from "./pool.ts";
|
|
18
|
+
import { hookParts, isHook } from "./hook-kinds/door.ts";
|
|
19
|
+
import { open } from "./open.ts";
|
|
14
20
|
import { refuseEffectSecrets, type PlacementWalk } from "./secret-placement.ts";
|
|
15
21
|
import {
|
|
22
|
+
admitUnder,
|
|
16
23
|
chartFileRefusals,
|
|
17
24
|
chartName,
|
|
18
25
|
chartWrapperOf,
|
|
@@ -55,6 +62,76 @@ export function doorRefusals(workflows: readonly LoadedWorkflow[]): string[] {
|
|
|
55
62
|
return lines;
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
/**
|
|
66
|
+
* What `jr2 up` prints of the standing Pools (ADR-0017): for each one, the chain from each Hook that
|
|
67
|
+
* reaches it to its callee (`sentry → alerts → triage`; a Pool that no Hook reaches prints its own
|
|
68
|
+
* chain alone), and a warning when two standing Pools dispatch to one Workflow: `cap` is for each
|
|
69
|
+
* Pool, so one item that both Sources carry can take a slot in each, and the caps add. A Hook reaches
|
|
70
|
+
* a Pool when its kind is one that the Source's handlers hear and it may answer the Workflow.
|
|
71
|
+
*/
|
|
72
|
+
export function standingPoolLines(
|
|
73
|
+
workflows: readonly LoadedWorkflow[],
|
|
74
|
+
hooks: JR2Config["hooks"] = {},
|
|
75
|
+
): { chains: string[]; warnings: string[] } {
|
|
76
|
+
const chains: string[] = [];
|
|
77
|
+
const byCallee = new Map<string, string[]>();
|
|
78
|
+
for (const w of workflows) {
|
|
79
|
+
const info = standingPoolOf(w.machine);
|
|
80
|
+
if (!info) continue;
|
|
81
|
+
byCallee.set(info.callee, [...(byCallee.get(info.callee) ?? []), w.name]);
|
|
82
|
+
const reaching = Object.entries(hooks ?? {}).filter(([, hook]) => {
|
|
83
|
+
if (!isHook(hook)) return false;
|
|
84
|
+
const parts = hookParts(hook);
|
|
85
|
+
const may = parts.may.answer;
|
|
86
|
+
return info.kinds.includes(parts.kind.id) && (may === "all" || may.includes(w.name));
|
|
87
|
+
});
|
|
88
|
+
if (!reaching.length) chains.push(`${w.name} → ${info.callee}`);
|
|
89
|
+
for (const [name] of reaching) chains.push(`${name} → ${w.name} → ${info.callee}`);
|
|
90
|
+
}
|
|
91
|
+
const warnings = [...byCallee]
|
|
92
|
+
.filter(([, pools]) => pools.length > 1)
|
|
93
|
+
.map(
|
|
94
|
+
([callee, pools]) =>
|
|
95
|
+
`the standing Pools ${pools.map((p) => `"${p}"`).join(" and ")} each dispatch to "${callee}": \`cap\` is for ` +
|
|
96
|
+
"each Pool, so one item that two Sources carry can take a slot in each, and the caps add (ADR-0017)",
|
|
97
|
+
);
|
|
98
|
+
return { chains, warnings };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Every standing Pool that is not the top Machine of its Workflow (ADR-0017): jr2 keeps one run for
|
|
103
|
+
* each standing Workflow and hands that run over, so the Workflow is the Pool. One line per Machine
|
|
104
|
+
* that composes one, naming the fix. `pool()` itself refuses what it can see alone (a Pool of runs
|
|
105
|
+
* that does not stand, a slot with no `wait: true`, no `cap`); this is the one refusal that needs the
|
|
106
|
+
* Workflow.
|
|
107
|
+
*
|
|
108
|
+
* The line text follows `refusing: ` in `jr2 up`'s output; the prefix is the caller's.
|
|
109
|
+
*/
|
|
110
|
+
export function standingPoolRefusals(workflows: readonly LoadedWorkflow[]): string[] {
|
|
111
|
+
const lines: string[] = [];
|
|
112
|
+
for (const w of workflows) {
|
|
113
|
+
const seen = new Set<AnyStateMachine>();
|
|
114
|
+
const walk = (machine: AnyStateMachine): void => {
|
|
115
|
+
if (seen.has(machine)) return;
|
|
116
|
+
seen.add(machine);
|
|
117
|
+
for (const logic of Object.values(machine.implementations.actors as Record<string, unknown>)) {
|
|
118
|
+
const child = asMachine(logic);
|
|
119
|
+
if (!child) continue;
|
|
120
|
+
if (isStandingPool(child)) {
|
|
121
|
+
lines.push(
|
|
122
|
+
`workflow "${w.name}": machine "${machine.id}" composes the standing Pool "${child.id}" — a standing Pool ` +
|
|
123
|
+
"is the top Machine of its Workflow, because jr2 keeps one run of each standing Workflow: move the Pool " +
|
|
124
|
+
"to a file of its own in workflows/ (ADR-0017)",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
walk(child);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
walk(w.machine);
|
|
131
|
+
}
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
|
|
58
135
|
/**
|
|
59
136
|
* Every part a registered Machine leaves Open (ADR-0051, ADR-0054): a Repo Slot with no url, an
|
|
60
137
|
* Agent with no model, an Effect's url, argv or secret. One line per part, in walk order within
|
|
@@ -110,6 +187,246 @@ export function openPartRefusals(workflows: readonly LoadedWorkflow[]): string[]
|
|
|
110
187
|
return lines;
|
|
111
188
|
}
|
|
112
189
|
|
|
190
|
+
/** A tool name that a model takes (ADR-0079): Anthropic's bound, which the others' refuse no less. */
|
|
191
|
+
export const MCP_TOOL_NAME = /^[A-Za-z0-9_-]{1,128}$/;
|
|
192
|
+
|
|
193
|
+
/** The name the model reads for one MCP tool (ADR-0079). */
|
|
194
|
+
export function mcpToolName(server: string, tool: string): string {
|
|
195
|
+
return `mcp__${server}__${tool}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** One `mcp` entry as an author writes it, for a fix line. */
|
|
199
|
+
function mcpEntryText(entry: AgentMcpServer, server = entry.server): string {
|
|
200
|
+
const tools =
|
|
201
|
+
entry.tools === undefined
|
|
202
|
+
? ""
|
|
203
|
+
: entry.tools === "*"
|
|
204
|
+
? ', tools: "*"'
|
|
205
|
+
: `, tools: [${entry.tools.map((t) => JSON.stringify(t)).join(", ")}]`;
|
|
206
|
+
return `{ server: ${server.startsWith("<") ? `"${server}"` : JSON.stringify(server)}${tools} }`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The `customize()` line that rebinds an Agent's `mcp` list, nested through `actors` as
|
|
210
|
+
* `customize()` takes it (ADR-0054, ADR-0079). */
|
|
211
|
+
function mcpCustomizeLine(path: readonly string[], slot: string, list: readonly string[]): string {
|
|
212
|
+
const binds = `{ agents: { ${slot}: { mcp: [${list.join(", ")}] } } }`;
|
|
213
|
+
return `customize(<import>, ${path.reduceRight((parts, key) => `{ actors: { ${key}: ${parts} } }`, binds)})`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The refusals of the Agents' `mcp` lists against the config's `mcp` block (ADR-0079), one line per
|
|
218
|
+
* fault, in walk order: a server name the block does not declare (with the `customize()` line that
|
|
219
|
+
* binds the slot to a declared one, and the block entry that declares a new one); a remote server
|
|
220
|
+
* with no `tools`; an empty list; a stdio server with a list; a stdio server on a `"none"` Agent,
|
|
221
|
+
* whose Turns run on the Instance Harness, where no Workspace code runs; a listed tool whose model
|
|
222
|
+
* name breaks `[A-Za-z0-9_-]{1,128}`; a tool listed twice; a server named twice; and a list on a remote server whose
|
|
223
|
+
* secret Workspace code in the Agent's Sandbox can spend, where only `"*"` is honest. Needs no url,
|
|
224
|
+
* so the door runs it too.
|
|
225
|
+
*
|
|
226
|
+
* The line text follows `refusing: ` in `jr2 up`'s output; the prefix is the caller's.
|
|
227
|
+
*/
|
|
228
|
+
export function mcpRefusals(workflows: readonly LoadedWorkflow[], config: JR2Config | undefined): string[] {
|
|
229
|
+
const lines: string[] = [];
|
|
230
|
+
const declared = config?.mcp ?? {};
|
|
231
|
+
const names = Object.keys(declared);
|
|
232
|
+
const declares = names.length
|
|
233
|
+
? `the mcp block declares ${names.map((n) => `"${n}"`).join(", ")}`
|
|
234
|
+
: "the mcp block of jr2.config.ts declares no server";
|
|
235
|
+
const seen = new Set<string>();
|
|
236
|
+
for (const w of workflows) {
|
|
237
|
+
for (const agent of partsOf([w.machine]).mcpAgents) {
|
|
238
|
+
const once = JSON.stringify([w.name, agent.path ?? null, agent.slot, agent.mcp]);
|
|
239
|
+
if (seen.has(once)) continue;
|
|
240
|
+
seen.add(once);
|
|
241
|
+
const where = agent.path?.length
|
|
242
|
+
? ` (on the Machine composed as ${agent.path.map((k) => `"${k}"`).join(" → ")})`
|
|
243
|
+
: "";
|
|
244
|
+
const at = `workflow "${w.name}": Agent "${agent.slot}"${where}`;
|
|
245
|
+
const servers = new Set<string>();
|
|
246
|
+
for (const entry of agent.mcp) {
|
|
247
|
+
if (servers.has(entry.server)) {
|
|
248
|
+
lines.push(`${at} names the MCP server "${entry.server}" twice — one entry for each server (ADR-0079)`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
servers.add(entry.server);
|
|
252
|
+
const server = Object.hasOwn(declared, entry.server) ? declared[entry.server] : undefined;
|
|
253
|
+
if (server === undefined) {
|
|
254
|
+
const fix =
|
|
255
|
+
agent.path === undefined
|
|
256
|
+
? "declare that Machine under setup({ actors }) and bind it there (ADR-0049)"
|
|
257
|
+
: `bind the Agent to a declared server where the Machine is registered: export const machine = ${mcpCustomizeLine(
|
|
258
|
+
agent.path,
|
|
259
|
+
agent.slot,
|
|
260
|
+
agent.mcp.map((e) => mcpEntryText(e, e === entry ? "<server>" : e.server)),
|
|
261
|
+
)}`;
|
|
262
|
+
lines.push(
|
|
263
|
+
`${at} names the MCP server "${entry.server}", which ${declares} — ${fix}; or declare it in jr2.config.ts: ` +
|
|
264
|
+
`mcp: { "${entry.server}": { url: "https://<host>/mcp", secret: "<SECRET>" } } (ADR-0079)`,
|
|
265
|
+
);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (isStdioServer(server)) {
|
|
269
|
+
if (agent.workspace === "none") {
|
|
270
|
+
lines.push(
|
|
271
|
+
`${at} has workspace: "none" and names the stdio MCP server "${entry.server}" — a stdio server runs as ` +
|
|
272
|
+
"Workspace code, inside a Sandbox's confinement, and a \"none\" Agent's Turns run on the Instance " +
|
|
273
|
+
"Harness, which has none: name a remote server, or give the Agent a Workspace (ADR-0079, ADR-0031)",
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (entry.tools !== undefined) {
|
|
277
|
+
lines.push(
|
|
278
|
+
`${at} gives a tools list for the stdio MCP server "${entry.server}" — a stdio server takes none: the ` +
|
|
279
|
+
"Agent gets every tool, because the server runs as Workspace code, and a list would guard nothing. " +
|
|
280
|
+
`Write ${mcpEntryText({ server: entry.server })} (ADR-0079)`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (entry.tools === undefined) {
|
|
286
|
+
lines.push(
|
|
287
|
+
`${at} names the remote MCP server "${entry.server}" with no tools — a remote server needs its allowlist: ` +
|
|
288
|
+
`${mcpEntryText({ server: entry.server, tools: ["<tool>"] })}, or tools: "*" for every tool (ADR-0079)`,
|
|
289
|
+
);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (entry.tools === "*") continue;
|
|
293
|
+
if (entry.tools.length === 0) {
|
|
294
|
+
lines.push(
|
|
295
|
+
`${at} gives MCP server "${entry.server}" an empty tools list, which offers no tool — name the tools, or ` +
|
|
296
|
+
"drop the entry (ADR-0079)",
|
|
297
|
+
);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const tools = new Set<string>();
|
|
301
|
+
const twice = new Set<string>();
|
|
302
|
+
for (const tool of entry.tools) {
|
|
303
|
+
if (tools.has(tool)) {
|
|
304
|
+
if (twice.has(tool)) continue;
|
|
305
|
+
twice.add(tool);
|
|
306
|
+
lines.push(
|
|
307
|
+
`${at} lists the tool ${JSON.stringify(tool)} of MCP server "${entry.server}" twice — a model provider ` +
|
|
308
|
+
"refuses a tools block that has one name two times: list each tool once (ADR-0079)",
|
|
309
|
+
);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
tools.add(tool);
|
|
313
|
+
const name = mcpToolName(entry.server, tool);
|
|
314
|
+
if (MCP_TOOL_NAME.test(name)) continue;
|
|
315
|
+
lines.push(
|
|
316
|
+
`${at} lists the tool ${JSON.stringify(tool)} of MCP server "${entry.server}": the model would read it as ` +
|
|
317
|
+
`${JSON.stringify(name)}, and a tool name is [A-Za-z0-9_-], at most 128 characters — a model provider ` +
|
|
318
|
+
"refuses any other (ADR-0079)",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// An `mcp` Effect names a server of the block, and a remote one: it runs in the Instance
|
|
324
|
+
// Effector, which starts no program (ADR-0079). An Open server is the Open-part refusal's.
|
|
325
|
+
for (const effect of partsOf([w.machine]).effects) {
|
|
326
|
+
if (effect.kind !== "mcp" || effect.server === undefined) continue;
|
|
327
|
+
const once = JSON.stringify(["effect", w.name, effect.path ?? null, effect.name]);
|
|
328
|
+
if (seen.has(once)) continue;
|
|
329
|
+
seen.add(once);
|
|
330
|
+
const where = effect.path?.length
|
|
331
|
+
? ` (on the Machine composed as ${effect.path.map((k) => `"${k}"`).join(" → ")})`
|
|
332
|
+
: "";
|
|
333
|
+
const at = `workflow "${w.name}": the mcp Effect "${effect.name}"${where}`;
|
|
334
|
+
const entry = `mcp: { "${effect.server}": { url: "https://<host>/mcp", secret: "<SECRET>" } }`;
|
|
335
|
+
const server = Object.hasOwn(declared, effect.server) ? declared[effect.server] : undefined;
|
|
336
|
+
if (server === undefined) {
|
|
337
|
+
lines.push(
|
|
338
|
+
`${at} names the MCP server "${effect.server}", which ${declares} — declare it in jr2.config.ts: ` +
|
|
339
|
+
`${entry}, or name a declared remote server (ADR-0079)`,
|
|
340
|
+
);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (isStdioServer(server)) {
|
|
344
|
+
lines.push(
|
|
345
|
+
`${at} names the stdio MCP server "${effect.server}" — an mcp Effect runs in the Instance Effector, which ` +
|
|
346
|
+
"starts no program, so it calls a remote server only: declare one for it in jr2.config.ts, " +
|
|
347
|
+
`${entry} (ADR-0079)`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// The allowlist is a guarantee about the credential (ADR-0028, ADR-0079): where Workspace code in
|
|
352
|
+
// the Agent's Sandbox can spend the server's secret, a list would be a hint, so only "*".
|
|
353
|
+
for (const pod of workspacePodsOf(w.machine)) {
|
|
354
|
+
for (const { slot, path, entry } of pod.mcp ?? []) {
|
|
355
|
+
const server = Object.hasOwn(declared, entry.server) ? declared[entry.server] : undefined;
|
|
356
|
+
if (server === undefined || isStdioServer(server) || server.secret === undefined) continue;
|
|
357
|
+
if (!Array.isArray(entry.tools)) continue;
|
|
358
|
+
const secret = server.secret;
|
|
359
|
+
const spends = spendersOf(pod, secret);
|
|
360
|
+
if (spends === undefined) continue;
|
|
361
|
+
const where = path?.length ? ` (on the Machine composed as ${path.map((k) => `"${k}"`).join(" → ")})` : "";
|
|
362
|
+
const once = JSON.stringify(["spend", w.name, path ?? null, slot, entry.server]);
|
|
363
|
+
if (seen.has(once)) continue;
|
|
364
|
+
seen.add(once);
|
|
365
|
+
lines.push(
|
|
366
|
+
`workflow "${w.name}": Agent "${slot}"${where} gives a tools list for MCP server "${entry.server}", and ` +
|
|
367
|
+
`Workspace code in its Sandbox can spend that server's secret, ${secret}: ${spends}. A list would be a ` +
|
|
368
|
+
`hint, not a limit — write ${mcpEntryText({ server: entry.server, tools: "*" })}, or keep ${secret} out ` +
|
|
369
|
+
"of that Workspace (ADR-0079, ADR-0028)",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return lines;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The refusals of the `mcp` block's urls (ADR-0079): each remote server's `url` is an `http://` or
|
|
379
|
+
* `https://` URL with no userinfo, and `https://` when the server has a `secret`, because the
|
|
380
|
+
* Custodian puts a value on TLS only (ADR-0059). Host-side only: in-cluster the config reads no
|
|
381
|
+
* `.env`, so a url read from it is empty there, and the door does not run this.
|
|
382
|
+
*/
|
|
383
|
+
export function mcpUrlRefusals(config: JR2Config | undefined): string[] {
|
|
384
|
+
const lines: string[] = [];
|
|
385
|
+
for (const [name, server] of Object.entries(config?.mcp ?? {})) {
|
|
386
|
+
if (isStdioServer(server)) continue;
|
|
387
|
+
const env = `process.env.${name.toUpperCase().replace(/-/g, "_")}_URL`;
|
|
388
|
+
let url: URL | undefined;
|
|
389
|
+
try {
|
|
390
|
+
url = new URL(server.url);
|
|
391
|
+
} catch {
|
|
392
|
+
url = undefined;
|
|
393
|
+
}
|
|
394
|
+
if (url === undefined || (url.protocol !== "https:" && url.protocol !== "http:")) {
|
|
395
|
+
lines.push(
|
|
396
|
+
`mcp.${name}.url ${JSON.stringify(server.url)} is not an http:// or https:// URL — an unset .env variable ` +
|
|
397
|
+
`reads as empty; set it, such as url: ${env} (ADR-0079)`,
|
|
398
|
+
);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (url.username || url.password) {
|
|
402
|
+
lines.push(`mcp.${name}.url carries userinfo — put the credential in secret, never in the url (ADR-0079)`);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (server.secret !== undefined && url.protocol !== "https:") {
|
|
406
|
+
lines.push(
|
|
407
|
+
`mcp.${name}.url must be an https:// URL — the server has a secret, ${server.secret}, and the Custodian ` +
|
|
408
|
+
"carries TLS only (ADR-0059, ADR-0079)",
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return lines;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** How Workspace code of one pod can spend a secret, or undefined when it cannot (ADR-0079): its
|
|
416
|
+
* `workspace()` grants it, a credentialed `command` there names it, or an `http` Effect there that
|
|
417
|
+
* sends a Worktree file names it — a file that the Agent wrote. */
|
|
418
|
+
function spendersOf(pod: ReturnType<typeof workspacePodsOf>[number], secret: string): string | undefined {
|
|
419
|
+
if (pod.secrets.includes(secret)) return "its workspace() lists it under secrets";
|
|
420
|
+
for (const effect of pod.effects) {
|
|
421
|
+
if (!(effect.secrets ?? []).includes(secret)) continue;
|
|
422
|
+
if (effect.kind === "command") return `the command Effect "${effect.name}" names it`;
|
|
423
|
+
if (effect.kind === "http" && effect.file) {
|
|
424
|
+
return `the http Effect "${effect.name}" sends a Worktree file with it`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
113
430
|
/**
|
|
114
431
|
* What the registered Machines name for secret placement (ADR-0059, ADR-0068): every Effect and
|
|
115
432
|
* every Workspace's pod, per route, and the models the Instance Harness hosts: those of the `"none"`
|
|
@@ -124,7 +441,9 @@ export function placementWalkOf(workflows: readonly LoadedWorkflow[]): Placement
|
|
|
124
441
|
// Per route, not per Machine object: a Machine placed twice is checked where each copy runs.
|
|
125
442
|
effects: workflows.flatMap((w) => placedEffectsOf(w.machine).map((effect) => ({ workflow: w.name, effect }))),
|
|
126
443
|
workspaces: workflows.flatMap((w) => workspacePodsOf(w.machine).map((pod) => ({ workflow: w.name, pod }))),
|
|
127
|
-
...(instanceHarness
|
|
444
|
+
...(instanceHarness
|
|
445
|
+
? { instanceHarness: { models: [...instanceHarness.models], servers: [...instanceHarness.servers] } }
|
|
446
|
+
: {}),
|
|
128
447
|
};
|
|
129
448
|
}
|
|
130
449
|
|
|
@@ -261,6 +580,153 @@ function doorOf(machine: AnyStateMachine, chartFields: readonly string[]): Door
|
|
|
261
580
|
}
|
|
262
581
|
}
|
|
263
582
|
|
|
583
|
+
/**
|
|
584
|
+
* The `answer` items that name single Gates (ADR-0078), checked against the Machines that they
|
|
585
|
+
* name. Each item of `callers.tokens.<name>.may.answer` and of each issuer's `groups` map that is
|
|
586
|
+
* `{ workflow, gates }` must name a Workflow of this Assembly, and each Gate name must be the invoke
|
|
587
|
+
* id of a Gate in one Machine of that Workflow (ADR-0011): its registered Machine, each Machine that
|
|
588
|
+
* it composes, and for a Chart wrapper each of its Chart files. A name that two Machines of one
|
|
589
|
+
* Workflow have is refused too, because one name would open both Gates.
|
|
590
|
+
*
|
|
591
|
+
* A runtime Chart is not in the Assembly (ADR-0070), so its Gates cannot be checked: a name that
|
|
592
|
+
* only a runtime Chart of a wrapper can have, and an item for a Chart that no Chart file is, are
|
|
593
|
+
* warnings. The lines follow `refusing: ` and `warning: ` in `jr2 up`'s output; the prefixes are the
|
|
594
|
+
* caller's.
|
|
595
|
+
*/
|
|
596
|
+
export function gateScopeChecks(
|
|
597
|
+
workflows: readonly LoadedWorkflow[],
|
|
598
|
+
config: JR2Config | undefined,
|
|
599
|
+
charts: readonly ChartFile[] = [],
|
|
600
|
+
): { errors: string[]; warnings: string[] } {
|
|
601
|
+
const errors: string[] = [];
|
|
602
|
+
const warnings: string[] = [];
|
|
603
|
+
const grants: Array<{ at: string; grant: AnswerGrant | undefined }> = [];
|
|
604
|
+
for (const [name, token] of Object.entries(config?.callers?.tokens ?? {})) {
|
|
605
|
+
grants.push({ at: `callers.tokens.${name}.may.answer`, grant: token.may.answer });
|
|
606
|
+
}
|
|
607
|
+
(config?.callers?.issuers ?? []).forEach((issuer, i) => {
|
|
608
|
+
for (const [value, may] of Object.entries(issuer.groups)) {
|
|
609
|
+
grants.push({ at: `callers.issuers.${i}.groups.${value}.answer`, grant: may.answer });
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
for (const { at, grant } of grants) {
|
|
613
|
+
if (grant === undefined || grant === "all") continue;
|
|
614
|
+
for (const item of grant) {
|
|
615
|
+
if (typeof item === "string") continue;
|
|
616
|
+
const checked = checkGateGrant(item, workflows, charts);
|
|
617
|
+
errors.push(...checked.errors.map((line) => `${at}: ${line}`));
|
|
618
|
+
warnings.push(...checked.warnings.map((line) => `${at}: ${line}`));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return { errors, warnings };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** One Machine's Gates, named for a line: its Machine id, or a Chart file's path. */
|
|
625
|
+
type GateHolder = { label: string; gates: ReadonlySet<string> };
|
|
626
|
+
|
|
627
|
+
/** The Gates of a registered Machine and of each Machine that it composes, one holder for each
|
|
628
|
+
* Machine. A composed Machine is a key of the Machine's `actors`, or a plain child Machine written
|
|
629
|
+
* inline on a state's `invoke.src`, which runs with its own Gates as a named one does. */
|
|
630
|
+
function gateHoldersOf(root: AnyStateMachine): GateHolder[] {
|
|
631
|
+
const out: GateHolder[] = [];
|
|
632
|
+
const seen = new Set<AnyStateMachine>();
|
|
633
|
+
const walk = (machine: AnyStateMachine): void => {
|
|
634
|
+
if (seen.has(machine)) return;
|
|
635
|
+
seen.add(machine);
|
|
636
|
+
const { gates } = senderIds(machine);
|
|
637
|
+
if (gates.size) out.push({ label: `machine "${machine.id}"`, gates });
|
|
638
|
+
const actors = machine.implementations.actors as Record<string, unknown>;
|
|
639
|
+
const visit = (node: AnyStateMachine["root"]): void => {
|
|
640
|
+
const written = [node.config.invoke ?? []].flat() as Array<{ src?: unknown } | undefined>;
|
|
641
|
+
node.invoke.forEach((inv, i) => {
|
|
642
|
+
const child = asMachine(typeof inv.src === "string" ? actors[inv.src] : inv.src) ?? asMachine(written[i]?.src);
|
|
643
|
+
if (child) walk(child);
|
|
644
|
+
});
|
|
645
|
+
for (const state of Object.values(node.states)) visit(state);
|
|
646
|
+
};
|
|
647
|
+
visit(machine.root);
|
|
648
|
+
for (const logic of Object.values(actors)) {
|
|
649
|
+
const child = asMachine(logic);
|
|
650
|
+
if (child) walk(child);
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
walk(root);
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** A Chart file's Gates, or none when the Chart does not admit: `chartFileRefusals` says why. */
|
|
658
|
+
function chartHolder(file: ChartFile, machine: AnyStateMachine): GateHolder | undefined {
|
|
659
|
+
const wrapper = chartWrapperOf(machine);
|
|
660
|
+
if (!wrapper || file.error !== undefined) return undefined;
|
|
661
|
+
try {
|
|
662
|
+
return { label: file.path, gates: new Set(admitUnder(wrapper, file.text).gates) };
|
|
663
|
+
} catch {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function checkGateGrant(
|
|
669
|
+
item: GateGrant,
|
|
670
|
+
workflows: readonly LoadedWorkflow[],
|
|
671
|
+
charts: readonly ChartFile[],
|
|
672
|
+
): { errors: string[]; warnings: string[] } {
|
|
673
|
+
const errors: string[] = [];
|
|
674
|
+
const warnings: string[] = [];
|
|
675
|
+
const split = splitChartName(item.workflow);
|
|
676
|
+
const registered = workflows.find((w) => w.name === (split?.wrapper ?? item.workflow));
|
|
677
|
+
if (!registered) {
|
|
678
|
+
errors.push(
|
|
679
|
+
`{ workflow: "${item.workflow}", gates } names the Gates of a Workflow that this Assembly does not register, so ` +
|
|
680
|
+
"jr2 cannot check them: name a registered Workflow or a Chart file (ADR-0078)",
|
|
681
|
+
);
|
|
682
|
+
return { errors, warnings };
|
|
683
|
+
}
|
|
684
|
+
const own = gateHoldersOf(registered.machine);
|
|
685
|
+
const wrapper = chartWrapperOf(registered.machine);
|
|
686
|
+
const runtime = wrapper !== undefined && wrapper.chartFields.length > 0;
|
|
687
|
+
// Each Workflow that the item opens is one set of Machines: the wrapper's own, and for each Chart
|
|
688
|
+
// its Chart besides.
|
|
689
|
+
const files = charts.filter((f) => f.wrapper === registered.name && (split === undefined || f.name === split.name));
|
|
690
|
+
if (split !== undefined && files.length === 0) {
|
|
691
|
+
const line =
|
|
692
|
+
`{ workflow: "${item.workflow}", gates } names a Chart that is no Chart file of this Assembly; ` +
|
|
693
|
+
"a runtime Chart's Gates cannot be checked at jr2 up (ADR-0070, ADR-0078)";
|
|
694
|
+
if (runtime) warnings.push(line);
|
|
695
|
+
else errors.push(`${line}, and "${registered.name}" takes no runtime Chart`);
|
|
696
|
+
return { errors, warnings };
|
|
697
|
+
}
|
|
698
|
+
const sets: GateHolder[][] = split === undefined ? [own] : [];
|
|
699
|
+
// A Chart file that does not admit has Gates that jr2 cannot read: `chartFileRefusals` refuses it
|
|
700
|
+
// with its own line, and a name that only it could have gets no second line.
|
|
701
|
+
let unread = false;
|
|
702
|
+
for (const f of files) {
|
|
703
|
+
const chart = chartHolder(f, registered.machine);
|
|
704
|
+
if (chart) sets.push([...own, chart]);
|
|
705
|
+
else unread = true;
|
|
706
|
+
}
|
|
707
|
+
const known = [...new Set(sets.flat().flatMap((h) => [...h.gates]))].sort();
|
|
708
|
+
for (const gate of item.gates) {
|
|
709
|
+
const holders = sets.map((set) => set.filter((h) => h.gates.has(gate)));
|
|
710
|
+
const twice = holders.find((h) => h.length > 1);
|
|
711
|
+
if (twice) {
|
|
712
|
+
errors.push(
|
|
713
|
+
`Gate "${gate}" of Workflow "${item.workflow}" is in more than one Machine (${twice.map((h) => h.label).join(", ")}), ` +
|
|
714
|
+
"so the one name would open each of those Gates: give one of them an invoke id of its own (ADR-0078)",
|
|
715
|
+
);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (holders.some((h) => h.length === 1) || unread) continue;
|
|
719
|
+
const has = known.length ? `its Gates are ${known.map((g) => `"${g}"`).join(", ")}` : "it has no Gate";
|
|
720
|
+
const line =
|
|
721
|
+
`Workflow "${item.workflow}" has no Gate "${gate}" — a Gate's name is its invoke id: the invoke id that the ` +
|
|
722
|
+
`author wrote, or else the key path of its gated state; ${has} (ADR-0011, ADR-0078)`;
|
|
723
|
+
if (runtime && split === undefined) {
|
|
724
|
+
warnings.push(`${line}; a runtime Chart of it may have one, which jr2 up cannot check`);
|
|
725
|
+
} else errors.push(line);
|
|
726
|
+
}
|
|
727
|
+
return { errors, warnings };
|
|
728
|
+
}
|
|
729
|
+
|
|
264
730
|
export type AssemblyRefusalInput = {
|
|
265
731
|
/** The Assembly's `jr2.config.ts`, loaded; `undefined` when it ships none. */
|
|
266
732
|
config: JR2Config | undefined;
|
|
@@ -301,9 +767,17 @@ export type AssemblyRefusalInput = {
|
|
|
301
767
|
* did its cluster work, so each check is true when the Assembly lands.
|
|
302
768
|
*/
|
|
303
769
|
export async function assemblyRefusals(input: AssemblyRefusalInput): Promise<string[]> {
|
|
304
|
-
const lines = [
|
|
770
|
+
const lines = [
|
|
771
|
+
...doorRefusals(input.workflows),
|
|
772
|
+
...standingPoolRefusals(input.workflows),
|
|
773
|
+
...openPartRefusals(input.workflows),
|
|
774
|
+
...mcpRefusals(input.workflows, input.config),
|
|
775
|
+
];
|
|
305
776
|
// A Chart file is the Instance's own (ADR-0070): one that fails refuses the publish.
|
|
306
777
|
if (input.charts?.length) lines.push(...chartFileRefusals(input.charts, input.workflows));
|
|
778
|
+
// An `answer` item that names single Gates names Gates that the Assembly has (ADR-0078). Its
|
|
779
|
+
// warnings have no channel in the publish's answer: `jr2 up` printed them before the publish.
|
|
780
|
+
lines.push(...gateScopeChecks(input.workflows, input.config, input.charts ?? []).errors);
|
|
307
781
|
// A same-Instance dispatch names a Workflow this Assembly has (ADR-0071). The door sees the
|
|
308
782
|
// runtime Charts, so a miss is an error here. Its warnings (a schema "not compared") have no
|
|
309
783
|
// channel in the publish's answer, and are dropped: `jr2 up` printed them before the publish.
|
|
@@ -345,3 +819,55 @@ export async function assemblyRefusals(input: AssemblyRefusalInput): Promise<str
|
|
|
345
819
|
}
|
|
346
820
|
return lines;
|
|
347
821
|
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* The hooked Gates whose delivery a lost delivery can stop (ADR-0011, ADR-0054): a Gate slot that
|
|
825
|
+
* hears a Hook and has no look, on a state with no `after`; and a Gate slot whose look a package left
|
|
826
|
+
* `open` and nobody bound. `jr2 up` WARNS and does not refuse, because a person can still answer the
|
|
827
|
+
* Gate with `jr2 send`. A Gate with `look: "none"` hears a command, which is an edge: no warning. One
|
|
828
|
+
* line per Gate slot and state, over the registered Machine and every Machine that it composes.
|
|
829
|
+
*/
|
|
830
|
+
export function gateLookWarnings(workflows: readonly LoadedWorkflow[]): string[] {
|
|
831
|
+
const lines: string[] = [];
|
|
832
|
+
for (const w of workflows) {
|
|
833
|
+
const seen = new Set<AnyStateMachine>();
|
|
834
|
+
const walk = (machine: AnyStateMachine): void => {
|
|
835
|
+
if (seen.has(machine)) return;
|
|
836
|
+
seen.add(machine);
|
|
837
|
+
const actors = machine.implementations.actors as Record<string, unknown>;
|
|
838
|
+
const visit = (node: AnyStateMachine["root"]): void => {
|
|
839
|
+
for (const inv of node.invoke) {
|
|
840
|
+
const logic = typeof inv.src === "string" ? actors[inv.src] : inv.src;
|
|
841
|
+
const child = asMachine(logic);
|
|
842
|
+
if (child) walk(child);
|
|
843
|
+
if (!isGateSlot(logic)) continue;
|
|
844
|
+
const slot = inv.src as string;
|
|
845
|
+
const parts = logic.gateSlot;
|
|
846
|
+
if (parts.look === open) {
|
|
847
|
+
lines.push(
|
|
848
|
+
`workflow "${w.name}": Gate slot "${slot}" on "${node.id}" leaves its look open — a delivery lost in an ` +
|
|
849
|
+
"outage stops the run until a person answers the Gate: bind it with customize(machine, { gates: { " +
|
|
850
|
+
`${slot}: { look } } }), or give it look: "none" (ADR-0011, ADR-0054)`,
|
|
851
|
+
);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const hears = Object.values(parts.hooks).some((keys) => Object.keys(keys).length > 0);
|
|
855
|
+
if (!hears || parts.look !== undefined || node.after.length > 0) continue;
|
|
856
|
+
lines.push(
|
|
857
|
+
`workflow "${w.name}": Gate slot "${slot}" on "${node.id}" hears a Hook and has no look and no \`after\` on ` +
|
|
858
|
+
"its state — a delivery lost in an outage stops the run until a person answers the Gate: give it a " +
|
|
859
|
+
'look, or look: "none" for a Gate that hears a command (ADR-0011)',
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
for (const child of Object.values(node.states)) visit(child);
|
|
863
|
+
};
|
|
864
|
+
visit(machine.root);
|
|
865
|
+
for (const logic of Object.values(actors)) {
|
|
866
|
+
const child = asMachine(logic);
|
|
867
|
+
if (child) walk(child);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
walk(w.machine);
|
|
871
|
+
}
|
|
872
|
+
return lines;
|
|
873
|
+
}
|