@jr2/orchestrator 0.3.0 → 0.5.1
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/README.md +20 -2
- package/bin/server.ts +7 -4
- package/console/canvas.ts +27 -14
- package/console/components/app.ts +12 -1
- package/console/components/fleet.ts +52 -6
- package/console/components/machine-pane.ts +128 -2
- package/console/components/nav.ts +1 -1
- package/console/components/schema-form.ts +12 -4
- package/console/json-field.ts +23 -0
- package/console/main.ts +122 -39
- package/console/store.ts +321 -21
- package/console/style.css +111 -0
- package/docs/agents-block.md +5 -0
- package/docs/concepts/agents-and-turns.md +442 -0
- package/docs/concepts/architecture.md +230 -0
- package/docs/concepts/charts.md +143 -0
- package/docs/concepts/composition.md +357 -0
- package/docs/concepts/converge.md +254 -0
- package/docs/concepts/durable-runs.md +189 -0
- package/docs/concepts/effects.md +603 -0
- package/docs/concepts/gates.md +208 -0
- package/docs/concepts/machines-and-workflows.md +289 -0
- package/docs/concepts/pools-and-sources.md +230 -0
- package/docs/concepts/secrets-and-trust.md +222 -0
- package/docs/concepts/the-menu.md +378 -0
- package/docs/concepts/workspaces.md +678 -0
- package/docs/get-started/a-model-decides.md +563 -0
- package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
- package/docs/get-started/connect-a-model.md +248 -0
- package/docs/get-started/create-an-instance.md +194 -0
- package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
- package/docs/get-started/install.md +126 -0
- package/docs/guides/add-tools-to-the-sandbox.md +295 -0
- package/docs/guides/answer-a-gate.md +304 -0
- package/docs/guides/bind-open-parts.md +223 -0
- package/docs/guides/cancel-a-run.md +146 -0
- package/docs/guides/clean-up.md +206 -0
- package/docs/guides/fix-a-failed-converge.md +361 -0
- package/docs/guides/give-the-cluster-git-credentials.md +222 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
- package/docs/guides/let-an-agent-write-a-chart.md +226 -0
- package/docs/guides/look-inside-a-sandbox.md +174 -0
- package/docs/guides/mirror-the-kit-images.md +176 -0
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
- package/docs/guides/open-the-console.md +91 -0
- package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
- package/docs/guides/push-and-sign-a-branch.md +486 -0
- package/docs/guides/run-a-chart-by-name.md +270 -0
- package/docs/guides/run-a-command-or-call-a-url.md +544 -0
- package/docs/guides/run-one-worker-per-item.md +321 -0
- package/docs/guides/set-an-agents-access-and-dials.md +375 -0
- package/docs/guides/size-a-workspace.md +204 -0
- package/docs/guides/start-a-run-from-a-run.md +237 -0
- package/docs/guides/start-a-run.md +207 -0
- package/docs/guides/upgrade-the-kit-version.md +240 -0
- package/docs/guides/use-a-hosted-provider.md +208 -0
- package/docs/guides/use-a-local-model.md +313 -0
- package/docs/guides/watch-a-run.md +326 -0
- package/docs/guides/write-a-workflow.md +318 -0
- package/docs/index.md +44 -0
- package/docs/llms.txt +216 -0
- package/docs/reference/authoring-surface/agent.md +128 -0
- package/docs/reference/authoring-surface/answer.md +140 -0
- package/docs/reference/authoring-surface/arg.md +60 -0
- package/docs/reference/authoring-surface/basic.md +62 -0
- package/docs/reference/authoring-surface/bearer.md +53 -0
- package/docs/reference/authoring-surface/chartschema.md +23 -0
- package/docs/reference/authoring-surface/chartschemafor.md +48 -0
- package/docs/reference/authoring-surface/command.md +191 -0
- package/docs/reference/authoring-surface/customize.md +64 -0
- package/docs/reference/authoring-surface/defineconfig.md +41 -0
- package/docs/reference/authoring-surface/defineevent.md +114 -0
- package/docs/reference/authoring-surface/dispatch.md +126 -0
- package/docs/reference/authoring-surface/fail.md +83 -0
- package/docs/reference/authoring-surface/file.md +122 -0
- package/docs/reference/authoring-surface/frompick.md +277 -0
- package/docs/reference/authoring-surface/http.md +159 -0
- package/docs/reference/authoring-surface/isfailure.md +58 -0
- package/docs/reference/authoring-surface/jr2setup.md +237 -0
- package/docs/reference/authoring-surface/open.md +74 -0
- package/docs/reference/authoring-surface/openbody.md +86 -0
- package/docs/reference/authoring-surface/paramsof.md +125 -0
- package/docs/reference/authoring-surface/pool.md +186 -0
- package/docs/reference/authoring-surface/push.md +112 -0
- package/docs/reference/authoring-surface/secret.md +118 -0
- package/docs/reference/authoring-surface/sentby.md +83 -0
- package/docs/reference/authoring-surface/source.md +51 -0
- package/docs/reference/authoring-surface/text.md +62 -0
- package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
- package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
- package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
- package/docs/reference/authoring-surface/type-agentslots.md +12 -0
- package/docs/reference/authoring-surface/type-answeraction.md +12 -0
- package/docs/reference/authoring-surface/type-arg.md +19 -0
- package/docs/reference/authoring-surface/type-argnames.md +12 -0
- package/docs/reference/authoring-surface/type-binding.md +62 -0
- package/docs/reference/authoring-surface/type-bindingof.md +12 -0
- package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
- package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
- package/docs/reference/authoring-surface/type-ceilings.md +41 -0
- package/docs/reference/authoring-surface/type-chainlink.md +19 -0
- package/docs/reference/authoring-surface/type-chartaction.md +46 -0
- package/docs/reference/authoring-surface/type-chartagent.md +28 -0
- package/docs/reference/authoring-surface/type-chartevent.md +20 -0
- package/docs/reference/authoring-surface/type-chartguard.md +20 -0
- package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
- package/docs/reference/authoring-surface/type-chartjson.md +14 -0
- package/docs/reference/authoring-surface/type-chartstate.md +46 -0
- package/docs/reference/authoring-surface/type-charttext.md +40 -0
- package/docs/reference/authoring-surface/type-charttransition.md +22 -0
- package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
- package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
- package/docs/reference/authoring-surface/type-childat.md +12 -0
- package/docs/reference/authoring-surface/type-childslots.md +12 -0
- package/docs/reference/authoring-surface/type-commandenv.md +15 -0
- package/docs/reference/authoring-surface/type-commandinput.md +19 -0
- package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
- package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
- package/docs/reference/authoring-surface/type-customize.md +109 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
- package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
- package/docs/reference/authoring-surface/type-duration.md +15 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
- package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
- package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
- package/docs/reference/authoring-surface/type-eventdef.md +45 -0
- package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
- package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
- package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
- package/docs/reference/authoring-surface/type-failslot.md +14 -0
- package/docs/reference/authoring-surface/type-failure.md +19 -0
- package/docs/reference/authoring-surface/type-failureinput.md +20 -0
- package/docs/reference/authoring-surface/type-filebody.md +41 -0
- package/docs/reference/authoring-surface/type-frominput.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
- package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
- package/docs/reference/authoring-surface/type-httpinput.md +12 -0
- package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
- package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
- package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
- package/docs/reference/authoring-surface/type-jr2config.md +126 -0
- package/docs/reference/authoring-surface/type-lends.md +38 -0
- package/docs/reference/authoring-surface/type-logicat.md +12 -0
- package/docs/reference/authoring-surface/type-markedif.md +12 -0
- package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
- package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
- package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
- package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
- package/docs/reference/authoring-surface/type-openeffect.md +12 -0
- package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
- package/docs/reference/authoring-surface/type-openof.md +12 -0
- package/docs/reference/authoring-surface/type-opensecret.md +23 -0
- package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
- package/docs/reference/authoring-surface/type-poolspec.md +60 -0
- package/docs/reference/authoring-surface/type-pushinput.md +15 -0
- package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
- package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
- package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
- package/docs/reference/authoring-surface/type-quantity.md +14 -0
- package/docs/reference/authoring-surface/type-reached.md +12 -0
- package/docs/reference/authoring-surface/type-reposlot.md +70 -0
- package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
- package/docs/reference/authoring-surface/type-secret.md +25 -0
- package/docs/reference/authoring-surface/type-sender.md +19 -0
- package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
- package/docs/reference/authoring-surface/type-size.md +85 -0
- package/docs/reference/authoring-surface/type-slothandles.md +32 -0
- package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
- package/docs/reference/authoring-surface/type-syncstate.md +12 -0
- package/docs/reference/authoring-surface/type-text.md +19 -0
- package/docs/reference/authoring-surface/type-textpart.md +24 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
- package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
- package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
- package/docs/reference/authoring-surface/type-unknown.md +21 -0
- package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
- package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
- package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
- package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
- package/docs/reference/authoring-surface/type-workspaced.md +94 -0
- package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
- package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
- package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
- package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
- package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
- package/docs/reference/authoring-surface/workspace.md +259 -0
- package/docs/reference/authoring-surface.md +197 -0
- package/docs/reference/cli.md +433 -0
- package/docs/reference/config.md +756 -0
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
- package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
- package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
- package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
- package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
- package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
- package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
- package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
- package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
- package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
- package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
- package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
- package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
- package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
- package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
- package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
- package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
- package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
- package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
- package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
- package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
- package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
- package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
- package/docs/reference/decisions.md +83 -0
- package/docs/reference/glossary.md +648 -0
- package/docs/reference/http-api.md +1348 -0
- package/docs/reference/machines.md +190 -0
- package/docs/reference/mechanism-events.md +72 -0
- package/docs/why.md +206 -0
- package/package.json +6 -4
- package/src/actor.ts +976 -61
- package/src/agent.ts +114 -38
- package/src/ambient.ts +4 -1
- package/src/assembly-checks.ts +347 -0
- package/src/assembly-tar.ts +494 -0
- package/src/assembly.ts +721 -0
- package/src/cause.ts +234 -0
- package/src/chart.ts +2182 -0
- package/src/charts.ts +748 -0
- package/src/config.ts +520 -162
- package/src/content-hash.ts +105 -0
- package/src/custodian.lua +6 -5
- package/src/custodian.ts +143 -39
- package/src/customize.ts +388 -55
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect-checks.ts +557 -0
- package/src/effect.ts +1202 -0
- package/src/effector-wire.ts +674 -0
- package/src/fail.ts +209 -0
- package/src/fingerprint.ts +4 -3
- package/src/gate.ts +8 -0
- package/src/git-clone.ts +65 -0
- package/src/git-push.ts +336 -0
- package/src/harness-client.ts +764 -48
- package/src/held-secrets.ts +82 -26
- package/src/holds.ts +156 -0
- package/src/http-effect.ts +1423 -0
- package/src/http.ts +916 -113
- package/src/images.ts +47 -21
- package/src/index.ts +62 -43
- package/src/instance.ts +505 -59
- package/src/internal.ts +84 -0
- package/src/kube-client.ts +333 -0
- package/src/machine-doc.ts +106 -6
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +18 -58
- package/src/mount-digest.ts +53 -0
- package/src/names.ts +66 -8
- package/src/notices.ts +141 -0
- package/src/open-body.ts +1580 -0
- package/src/open-effect.ts +217 -0
- package/src/open.ts +59 -6
- package/src/parts.ts +709 -41
- package/src/party.ts +534 -0
- package/src/pick.ts +761 -0
- package/src/pool.ts +248 -37
- package/src/registration.ts +382 -19
- package/src/repo-fetch.ts +60 -85
- package/src/repos.ts +65 -75
- package/src/rest.ts +133 -0
- package/src/run-host.ts +2383 -115
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +1126 -0
- package/src/sandbox-effector.ts +111 -0
- package/src/sandbox-kube.ts +1721 -0
- package/src/sandbox-watch.ts +260 -0
- package/src/secret-placement.ts +529 -0
- package/src/server.ts +284 -84
- package/src/setup.ts +819 -117
- package/src/size.ts +454 -0
- package/src/snapshot-store.ts +115 -20
- package/src/stub-harness.ts +49 -16
- package/src/sync-client.ts +54 -0
- package/src/timers.ts +191 -0
- package/src/tokens.ts +35 -4
- package/src/vocabulary.ts +135 -15
- package/src/wire.ts +238 -10
- package/src/workspace.ts +1195 -338
- package/src/sandbox-kubectl.ts +0 -1108
package/src/parts.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
// Instance Harness converges when any of them declares `workspace: "none"`, every `file:`
|
|
17
17
|
// image context a Machine ships is built and content-tagged, every BOUND Repo is known before
|
|
18
18
|
// a run can ask for it, an OPEN part — a Repo Slot with no url, an Agent with no model
|
|
19
|
-
// (ADR-0054) — is refused before anything is built,
|
|
20
|
-
//
|
|
19
|
+
// (ADR-0054) — is refused before anything is built, every Size a `workspace()` states is split
|
|
20
|
+
// and held to the Harness floor (ADR-0060), and whether any registered Machine composes a
|
|
21
|
+
// Sandbox at all is the data-plane switch. None of these can read
|
|
21
22
|
// an invoke's `input` (it is a function — dials are not statically recoverable), and none
|
|
22
23
|
// needs to: identity lives in the definition, the image and the slots are options, and all are
|
|
23
24
|
// values ON the Machine. A per-run slot is a function too, and the walk reports nothing for
|
|
@@ -33,6 +34,13 @@
|
|
|
33
34
|
// xstate rewrites to a generated key and keeps only on the raw config node. Nothing jr2 owns
|
|
34
35
|
// arrives this way any more; the walk keeps it because an AUTHOR may still write one.
|
|
35
36
|
//
|
|
37
|
+
// A PART written inline is refused, not walked (`refuseInlineParts`): an `agent()`, an Effect
|
|
38
|
+
// (ADR-0067) or a `workspace()` on an `invoke.src` has no author-written name. xstate's generated
|
|
39
|
+
// key (`xstate.invoke.<n>.<state id>`) moves when a state is renamed, so the ledger, the Console doc
|
|
40
|
+
// and `jr2 up`'s report would name it by a number, and `customize()` — xstate's `provide`, keyed on
|
|
41
|
+
// `actors` — could not reach it at all. A slot key is the one name every reader shares (ADR-0049),
|
|
42
|
+
// so a Part is a key of `actors`, and the refusal says how to write it.
|
|
43
|
+
//
|
|
36
44
|
// Deliberately NOT a `provide()`-aware read: the walk runs on the registered Machine, which is the
|
|
37
45
|
// object the run will start, so a `customize()` clone is walked as itself and a test seam's fake
|
|
38
46
|
// (unbranded) contributes nothing.
|
|
@@ -41,9 +49,16 @@ import { basename } from "node:path";
|
|
|
41
49
|
import { fileURLToPath } from "node:url";
|
|
42
50
|
import type { AnyActorRef, AnyStateMachine, StateNode, UnknownActorLogic } from "xstate";
|
|
43
51
|
import { isAgent, isOpenAgent, type AgentDeclaration } from "./agent.ts";
|
|
52
|
+
import { argvText, isEffect, placementOf, type EffectParts, type Placement } from "./effect.ts";
|
|
53
|
+
import { effectBindingLine, openPartsOf, type OpenEffectPart } from "./open-effect.ts";
|
|
54
|
+
import { isFailSlot } from "./fail.ts";
|
|
44
55
|
import { isImageContext } from "./images.ts";
|
|
56
|
+
import { isOpenBody, openBodyDeclarationOf, type OpenBodyDeclaration } from "./open-body.ts";
|
|
45
57
|
import { open } from "./open.ts";
|
|
46
58
|
import { repoIdentity } from "./repo-identity.ts";
|
|
59
|
+
import { refuseUnroutedAgentFault } from "./mechanism-routes.ts";
|
|
60
|
+
import { refuseLostOutput } from "./run-output.ts";
|
|
61
|
+
import { userSeatOf, type Size, type UserContainer } from "./size.ts";
|
|
47
62
|
|
|
48
63
|
// --- Repo Slots (ADR-0051) ---------------------------------------------------------------------
|
|
49
64
|
// A `workspace()` names each Repo it attaches under a SLOT — the Machine's own word for it, the
|
|
@@ -51,7 +66,7 @@ import { repoIdentity } from "./repo-identity.ts";
|
|
|
51
66
|
// is one of three states, and the walk tells them apart without evaluating anything. A Machine
|
|
52
67
|
// whose body names no slot — it enumerates whatever is attached — declares the whole MAP Open
|
|
53
68
|
// instead (`repos: open`): the composer names every slot, in an order the Machine may give a
|
|
54
|
-
// meaning to. Open at the map is the same word as Open at a slot, one level up (
|
|
69
|
+
// meaning to. Open at the map is the same word as Open at a slot, one level up (ADR-0051).
|
|
55
70
|
|
|
56
71
|
// The sentinel itself lives in open.ts, a module with nothing else in it: since ADR-0054 it marks
|
|
57
72
|
// an Agent's model too, and agent.ts must read it without importing this file (the walk imports
|
|
@@ -59,15 +74,103 @@ import { repoIdentity } from "./repo-identity.ts";
|
|
|
59
74
|
// meets it first, and `import { open } from "@jr2/orchestrator"` is the only spelling anyone writes.
|
|
60
75
|
export { open } from "./open.ts";
|
|
61
76
|
|
|
62
|
-
/**
|
|
63
|
-
*
|
|
64
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The repository of a Repo Slot: `{ url, ref }`. `url` is the Repo's git url. `ref` is the branch
|
|
79
|
+
* or commit that the branch Worktree starts from; absent, the Repo's default branch. Where only a
|
|
80
|
+
* url is needed, a string is enough.
|
|
81
|
+
*
|
|
82
|
+
* `workspace()` and `customize()` refuse:
|
|
83
|
+
* - an empty `url`: ``<where>: Repo Slot "<slot>" is bound to an empty url (got <value>) — a slot
|
|
84
|
+
* is `open`, a url, `{ url, ref? }`, or a mapper `({ input }) => url | { url, ref? }` over the
|
|
85
|
+
* door (ADR-0051).``
|
|
86
|
+
* - an empty `ref`: the same text, with `bound with an empty ref` in place of `bound to an empty
|
|
87
|
+
* url`.
|
|
88
|
+
* - a url that names no Repo, such as a relative path: ``<where>: Repo Slot "<slot>" binds
|
|
89
|
+
* "<url>", which names no Repo — <reason> (ADR-0051).``
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { jr2Setup, workspace, type Binding } from "@jr2/orchestrator";
|
|
94
|
+
*
|
|
95
|
+
* const app: Binding = { url: "https://github.com/you/app.git", ref: "release-2" };
|
|
96
|
+
*
|
|
97
|
+
* const body = jr2Setup().createMachine({
|
|
98
|
+
* id: "body",
|
|
99
|
+
* initial: "done",
|
|
100
|
+
* states: { done: { type: "final" } },
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* export const machine = workspace(body, {
|
|
104
|
+
* repos: { app },
|
|
105
|
+
* spec: () => ({ branch: "jr2/hotfix" }),
|
|
106
|
+
* });
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @see docs/concepts/workspaces.md
|
|
110
|
+
* @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
|
|
111
|
+
*/
|
|
112
|
+
// What a Repo Slot resolves to (GLOSSARY.md "Binding").
|
|
113
|
+
export type Binding = {
|
|
114
|
+
/** The Repo's git url. */
|
|
115
|
+
url: string;
|
|
116
|
+
/** The branch or commit that the branch Worktree starts from. Default: the Repo's default
|
|
117
|
+
* branch. */
|
|
118
|
+
ref?: string;
|
|
119
|
+
};
|
|
65
120
|
|
|
121
|
+
// Bound and open are what `jr2 up` can see; per-run is the run's business, fenced at attach by
|
|
122
|
+
// `git.credentials` (ADR-0051).
|
|
66
123
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
124
|
+
* The value of one Repo Slot in `workspace({ repos })`. One of:
|
|
125
|
+
* - a url, or a {@link Binding} `{ url, ref }`: the Machine names the Repo.
|
|
126
|
+
* - {@link open}: the Instance that registers the Machine binds it with `customize()`. `jr2 up`
|
|
127
|
+
* refuses a Workflow that leaves it open: ``refusing: workflow "<name>" leaves Repo Slot
|
|
128
|
+
* "<slot>" open — bind it where the Machine is registered: export const machine =
|
|
129
|
+
* customize(<import>, …) (ADR-0051)``, with the whole `customize` line to paste.
|
|
130
|
+
* - a function of the run input, `({ input }) => url` or a {@link Binding}: each run chooses. The
|
|
131
|
+
* url must match an entry of `git.credentials` in `jr2.config.ts`, or the run cannot use it.
|
|
132
|
+
*
|
|
133
|
+
* The key of the slot is a directory name under `/work`. It starts with a letter, and
|
|
134
|
+
* `workspace()` and `customize()` refuse a key that does not: ``<where>: Repo Slot key "<key>" is
|
|
135
|
+
* not a directory name — a slot becomes `/work/<slot>` and its order matters, so it starts with a
|
|
136
|
+
* letter and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051).`` They refuse a value of another
|
|
137
|
+
* shape: ``<where>: Repo Slot "<slot>" is <what> — a slot is `open`, a url, `{ url, ref? }`, or a
|
|
138
|
+
* mapper `({ input }) => url | { url, ref? }` over the door (ADR-0051).``
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* import { z } from "zod";
|
|
143
|
+
* import { jr2Setup, open, workspace, type RepoSlot, type Workspaced } from "@jr2/orchestrator";
|
|
144
|
+
*
|
|
145
|
+
* const runInput = z.object({ service: z.string() });
|
|
146
|
+
* type RunInput = z.infer<typeof runInput>;
|
|
147
|
+
*
|
|
148
|
+
* const service: RepoSlot<RunInput> = ({ input }) => `https://github.com/you/${input.service}.git`;
|
|
149
|
+
*
|
|
150
|
+
* const body = jr2Setup({
|
|
151
|
+
* types: {} as { input: Workspaced<RunInput, "service" | "docs" | "config"> },
|
|
152
|
+
* }).createMachine({
|
|
153
|
+
* id: "body",
|
|
154
|
+
* initial: "done",
|
|
155
|
+
* states: { done: { type: "final" } },
|
|
156
|
+
* });
|
|
157
|
+
*
|
|
158
|
+
* // A Machine for a package: the Instance that registers it binds `config`.
|
|
159
|
+
* export const deploy = workspace(body, {
|
|
160
|
+
* input: runInput,
|
|
161
|
+
* repos: {
|
|
162
|
+
* service,
|
|
163
|
+
* docs: "https://github.com/you/docs.git",
|
|
164
|
+
* config: open,
|
|
165
|
+
* },
|
|
166
|
+
* spec: ({ input }) => ({ branch: `deploy/${input.service}` }),
|
|
167
|
+
* });
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @see docs/concepts/workspaces.md
|
|
171
|
+
* @see docs/guides/bind-open-parts.md
|
|
172
|
+
* @see docs/guides/give-the-cluster-git-credentials.md
|
|
173
|
+
* @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
|
|
71
174
|
*/
|
|
72
175
|
export type RepoSlot<TInput = unknown> =
|
|
73
176
|
| typeof open
|
|
@@ -146,11 +249,20 @@ export function assertRepoSlot(where: string, slot: string, value: unknown): ass
|
|
|
146
249
|
export type SandboxParts = {
|
|
147
250
|
/** The Sandbox Image. Absent → the Instance's `images/default`, then the stock Harness. */
|
|
148
251
|
image?: string;
|
|
149
|
-
/** The User Container
|
|
150
|
-
|
|
252
|
+
/** The User Container (ADR-0005): its image, or `{ image, resources }` when the Machine states
|
|
253
|
+
* its split of the Size (ADR-0060). Absent → the pod has no third container. */
|
|
254
|
+
user?: string | UserContainer;
|
|
151
255
|
/** The Repo Slots, in declaration order — which the handles keep (ADR-0051) — or
|
|
152
256
|
* {@link open} for a map the composer fills whole. */
|
|
153
257
|
repos: Record<string, RepoSlot> | typeof open;
|
|
258
|
+
/** The Size the Machine states (ADR-0060) — the `workspace()`'s, with any `customize()` layered
|
|
259
|
+
* over it. Absent → the Instance's `sandbox.resources`, then the kit default. */
|
|
260
|
+
resources?: Size;
|
|
261
|
+
/** The Held secrets this Workspace's pod gets beside its Agents' model keys (ADR-0059): names of
|
|
262
|
+
* the `secrets` list. Naming is placement — no other pod gets a name for being listed here. */
|
|
263
|
+
secrets?: string[];
|
|
264
|
+
/** The Sandbox Effector's image (ADR-0068). Absent → the Sandbox Image. */
|
|
265
|
+
effector?: { image: string };
|
|
154
266
|
};
|
|
155
267
|
|
|
156
268
|
// The stamps below are written ON `machine.config` under `Symbol.for` keys, never held in a
|
|
@@ -178,11 +290,12 @@ export function sandboxPartsOf(machine: AnyStateMachine | undefined): SandboxPar
|
|
|
178
290
|
|
|
179
291
|
/** Does this Machine COMPOSE a Sandbox — i.e. is it a `workspace()` wrapper? Distinct from
|
|
180
292
|
* `sandboxPartsOf(m)` naming no image: a wrapper that names neither image still owns the two
|
|
181
|
-
* seats, which is what `customize({ image })` retunes (ADR-0049) — and it
|
|
182
|
-
*
|
|
183
|
-
* Sandboxes exactly when a registered Machine composes one.
|
|
293
|
+
* seats, which is what `customize({ image })` retunes (ADR-0049) — and it may carry no Repo Slot
|
|
294
|
+
* at all (ADR-0051). Also the data-plane switch, read off the walk: an Instance needs
|
|
295
|
+
* Sandboxes exactly when a registered Machine composes one. A logic with no `config` (an Effect's,
|
|
296
|
+
* ADR-0067) composes none. */
|
|
184
297
|
export function composesSandbox(machine: AnyStateMachine): boolean {
|
|
185
|
-
return (machine.config as Stamped)[SANDBOX_PARTS] !== undefined;
|
|
298
|
+
return (machine.config as Stamped | undefined)?.[SANDBOX_PARTS] !== undefined;
|
|
186
299
|
}
|
|
187
300
|
|
|
188
301
|
// The slot a jr2 WRAPPER is transparent to (ADR-0049): `workspace()`'s `body`, `pool()`'s
|
|
@@ -295,8 +408,43 @@ export type CarriedRepo = { url: string; ref?: string; identity: string; key: st
|
|
|
295
408
|
*/
|
|
296
409
|
export type OpenSlot = { slot: string | undefined; path: readonly string[] | undefined };
|
|
297
410
|
|
|
298
|
-
/**
|
|
299
|
-
|
|
411
|
+
/**
|
|
412
|
+
* One hop of a placement route (ADR-0059, ADR-0068): the `actors` slot key a child is invoked by,
|
|
413
|
+
* or — for a plain child Machine written INLINE on a state's invoke — xstate's generated invoke
|
|
414
|
+
* src for that hop (`xstate.invoke.<index>.<state id>`), which the static walk computes from the
|
|
415
|
+
* state node and the run reads off the live child. An inline hop is an object, so it can never
|
|
416
|
+
* equal a slot key.
|
|
417
|
+
*/
|
|
418
|
+
export type RouteHop = string | { readonly inline: string };
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Where a placement sits: the chain of hops from the registered root down to it, a wrapper's
|
|
422
|
+
* transparent body skipped as {@link OpenSlot}'s `path` skips it. Unlike `path`, a route goes on
|
|
423
|
+
* past an inline Machine, so every placement — a Workspace's pod, an Effect — has a route of its
|
|
424
|
+
* own, and the pod keys of two Workspaces never collide. Where `path` is defined the route is that
|
|
425
|
+
* same list of slot keys, so a pod key reached by slot keys alone is what it was.
|
|
426
|
+
*/
|
|
427
|
+
export type PlacementRoute = readonly RouteHop[];
|
|
428
|
+
|
|
429
|
+
/** The hop xstate gives the `index`-th invoke of state node `stateId` when its `src` is inline. */
|
|
430
|
+
export const inlineHop = (index: number, stateId: string): RouteHop => ({
|
|
431
|
+
inline: `xstate.invoke.${index}.${stateId}`,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
/** A route as a human reads it, for a label: ` → "review"`, ` → invoked inline in state "w.a"`. */
|
|
435
|
+
export function routeText(route: PlacementRoute): string {
|
|
436
|
+
return route
|
|
437
|
+
.map((hop) =>
|
|
438
|
+
typeof hop === "string"
|
|
439
|
+
? ` → "${hop}"`
|
|
440
|
+
: ` → invoked inline in state "${hop.inline.replace(/^xstate\.invoke\.\d+\./, "")}"`,
|
|
441
|
+
)
|
|
442
|
+
.join("");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Which kind of Open part a fix line binds — a Repo Slot (`repos`, ADR-0051), an Agent's model
|
|
446
|
+
* (`agents`, ADR-0054), or an Effect's url, argv or secret (`effects`, ADR-0054). */
|
|
447
|
+
export type OpenPart = "repo" | "agent" | OpenEffectPart;
|
|
300
448
|
|
|
301
449
|
/**
|
|
302
450
|
* The `customize` line that binds an Open part, given the identifier the composer holds the
|
|
@@ -305,22 +453,26 @@ export type OpenPart = "repo" | "agent";
|
|
|
305
453
|
* `customize(top, { actors: { review: { repos: { target: "<url>" } } } })`. The same nesting
|
|
306
454
|
* `customize()` accepts, so the line pastes.
|
|
307
455
|
*
|
|
308
|
-
* An Agent binds through `agents` instead, and the
|
|
456
|
+
* An Agent binds through `agents` instead, and the sample value is the model spelling ADR-0018
|
|
309
457
|
* demands — `<provider>/<model>`, not a bare model id, since the prefix is what picks the
|
|
310
|
-
* endpoint.
|
|
311
|
-
*
|
|
458
|
+
* endpoint. An Effect binds through `effects`: its url, its argv, its secret, or one object with two
|
|
459
|
+
* of them for an Effect with two Open parts (open-effect.ts). `repo` is the
|
|
460
|
+
* default because a Repo Slot was the first Open part and reads as the
|
|
461
|
+
* unmarked case. An Open MAP has no slot to print (`slot` undefined): the sample `<slot>`
|
|
312
462
|
* stands where the composer's own word goes, since choosing it is the composer's half of the line.
|
|
313
463
|
*/
|
|
314
464
|
export function customizeLine(
|
|
315
465
|
machine: string,
|
|
316
466
|
path: readonly string[],
|
|
317
467
|
slot: string | undefined,
|
|
318
|
-
part: OpenPart = "repo",
|
|
468
|
+
part: OpenPart | readonly OpenEffectPart[] = "repo",
|
|
319
469
|
): string {
|
|
320
470
|
const binds =
|
|
321
471
|
part === "agent"
|
|
322
472
|
? `{ agents: { ${slot}: { model: "<provider>/<model>" } } }`
|
|
323
|
-
:
|
|
473
|
+
: part === "repo"
|
|
474
|
+
? `{ repos: { ${slot ?? "<slot>"}: "<url>" } }`
|
|
475
|
+
: effectBindingLine(slot ?? "<slot>", part);
|
|
324
476
|
const inner = path.reduceRight((parts, key) => `{ actors: { ${key}: ${parts} } }`, binds);
|
|
325
477
|
return `customize(${machine}, ${inner})`;
|
|
326
478
|
}
|
|
@@ -329,8 +481,10 @@ export function customizeLine(
|
|
|
329
481
|
* The runtime twin of the walk's `path`: the actor-slot chain from a run's root actor down to
|
|
330
482
|
* `actor` (its own `src` included), read off the live actor tree — each `src` is the slot key it
|
|
331
483
|
* was invoked or spawned as, and a wrapper's transparent body is skipped by the same record
|
|
332
|
-
* ({@link wrapperBodyOf}) the walk skips it by. `undefined` past an inline-invoked actor,
|
|
333
|
-
* `
|
|
484
|
+
* ({@link wrapperBodyOf}) the walk skips it by. `undefined` past an inline-invoked actor, as the
|
|
485
|
+
* walk's `path` is: such an actor names no slot. xstate gives an inline `invoke.src` a generated
|
|
486
|
+
* string (`xstate.invoke.<n>.<state id>`) that is no key of its parent's `actors`, so the test is
|
|
487
|
+
* that registry, not the type of `src`; inline logic passed to `spawn` keeps the logic as `src`.
|
|
334
488
|
*/
|
|
335
489
|
export function actorSlotPath(actor: AnyActorRef | undefined): string[] | undefined {
|
|
336
490
|
const path: string[] = [];
|
|
@@ -338,14 +492,128 @@ export function actorSlotPath(actor: AnyActorRef | undefined): string[] | undefi
|
|
|
338
492
|
const src = (node as { src?: unknown }).src;
|
|
339
493
|
if (typeof src !== "string") return undefined;
|
|
340
494
|
const parent = asMachine((node._parent as { logic?: unknown }).logic);
|
|
495
|
+
if (parent && !Object.hasOwn(parent.implementations.actors, src)) return undefined;
|
|
341
496
|
if (!parent || wrapperBodyOf(parent) !== src) path.unshift(src);
|
|
342
497
|
}
|
|
343
498
|
return path;
|
|
344
499
|
}
|
|
345
500
|
|
|
501
|
+
/**
|
|
502
|
+
* The runtime twin of the walk's `route` (ADR-0059): the placement route from a run's root actor
|
|
503
|
+
* down to `actor`, read off the live actor tree. A child invoked by a slot key adds that key, as
|
|
504
|
+
* {@link actorSlotPath} does; a plain Machine invoked inline adds xstate's generated src as an
|
|
505
|
+
* inline hop, which the walk computes from the state node — so `jr2 up` and the provision derive
|
|
506
|
+
* one pod key. Never undefined: an actor the walk cannot see — a Machine object handed to
|
|
507
|
+
* `spawnChild`, whose `src` is the logic itself — is refused with the slot form.
|
|
508
|
+
*/
|
|
509
|
+
export function actorRoute(actor: AnyActorRef | undefined): RouteHop[] {
|
|
510
|
+
const route: RouteHop[] = [];
|
|
511
|
+
for (let node = actor; node?._parent; node = node._parent) {
|
|
512
|
+
const src = (node as { src?: unknown }).src;
|
|
513
|
+
const parent = asMachine((node._parent as { logic?: unknown }).logic);
|
|
514
|
+
if (typeof src === "string" && (!parent || Object.hasOwn(parent.implementations.actors, src))) {
|
|
515
|
+
if (!parent || wrapperBodyOf(parent) !== src) route.unshift(src);
|
|
516
|
+
} else if (typeof src === "string" && /^xstate\.invoke\.\d+\./.test(src)) {
|
|
517
|
+
route.unshift({ inline: src });
|
|
518
|
+
} else {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`actor "${node.id}" was spawned from a logic object, which no walk can see, so jr2 up placed nothing ` +
|
|
521
|
+
"for what it composes — a Part is reached through keys of actors, so it has a name that the ledger, " +
|
|
522
|
+
'the report and customize() share: actors: { <name>: <machine> }, then spawnChild("<name>") (ADR-0059)',
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return route;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* One `workspace()` a Machine composes, and the Size it states (ADR-0060) — what `jr2 up` resolves
|
|
531
|
+
* against the Instance default and splits, so a split that leaves the Harness below its floor is
|
|
532
|
+
* refused at the converge rather than at the first provision, and a Size no Sandbox node could hold
|
|
533
|
+
* is warned about. Located like an {@link OpenSlot}: `path` is the `customize()` route to it.
|
|
534
|
+
*/
|
|
535
|
+
export type CarriedSize = {
|
|
536
|
+
path: readonly string[] | undefined;
|
|
537
|
+
/** The placement route to it (ADR-0059). */
|
|
538
|
+
route: PlacementRoute;
|
|
539
|
+
/** The Machine's Size, as stated; absent when it states none. */
|
|
540
|
+
resources?: Size;
|
|
541
|
+
/** The User Container's split, as stated; absent when it states none. */
|
|
542
|
+
user?: Size;
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
/** One Effect slot a Machine carries (ADR-0067), by its slot key and the `customize()` route to its
|
|
546
|
+
* Machine, with the plain data `jr2 up` prints: the kind, where it runs, a command's argv (an argv
|
|
547
|
+
* slot reads `<name>`), its timeout, and the Repo Slot whose Worktree is its cwd, when it names one.
|
|
548
|
+
* `open` names the parts a package left Open (ADR-0054) — an http url, which then has no target
|
|
549
|
+
* yet, or a command's argv, which reads `<open>`, and a secret its auth, a header or its env names
|
|
550
|
+
* as `secret(open)`, which names no secret yet — and the walk reports them for `jr2 up` to refuse;
|
|
551
|
+
* once `customize()` binds them, the slot carries the bound Effect and `open` is absent. */
|
|
552
|
+
export type CarriedEffect = {
|
|
553
|
+
name: string;
|
|
554
|
+
path: readonly string[] | undefined;
|
|
555
|
+
/** The placement route to its Machine (ADR-0068): what its placement key reads, never `path`. */
|
|
556
|
+
route: PlacementRoute;
|
|
557
|
+
placement: Placement;
|
|
558
|
+
timeoutMs: number;
|
|
559
|
+
} &
|
|
560
|
+
/** A command; one whose env names a secret runs in the Sandbox Effector (ADR-0067). */
|
|
561
|
+
(| {
|
|
562
|
+
kind: "command";
|
|
563
|
+
argv: string[];
|
|
564
|
+
slot?: string;
|
|
565
|
+
secrets?: string[];
|
|
566
|
+
pin?: true;
|
|
567
|
+
open?: Array<"argv" | "secret">;
|
|
568
|
+
}
|
|
569
|
+
/** An http Effect (ADR-0068): its method, its fixed target, and the secrets it names — what
|
|
570
|
+
* places each secret, and what its Effector's config allows. `file`: its body is a Worktree
|
|
571
|
+
* file (ADR-0067), so it runs in its Workspace's Sandbox Effector, on Repo Slot `slot` when named.
|
|
572
|
+
* An Open url has no target, and names only the secrets its headers and auth name. */
|
|
573
|
+
| {
|
|
574
|
+
kind: "http";
|
|
575
|
+
method: string;
|
|
576
|
+
target?: { scheme: "http" | "https"; host: string; port: number };
|
|
577
|
+
secrets: string[];
|
|
578
|
+
idempotent: boolean;
|
|
579
|
+
file?: true;
|
|
580
|
+
slot?: string;
|
|
581
|
+
open?: Array<"url" | "secret">;
|
|
582
|
+
}
|
|
583
|
+
/** A push (ADR-0067, ADR-0051): the Workspace's branch to its Repo Slot's Binding. It names no
|
|
584
|
+
* secret: `jr2 up` resolves its credential through `git.credentials`. */
|
|
585
|
+
| { kind: "push"; slot?: string; sign: boolean; squash: boolean; force: boolean; pin: boolean; secrets: string[] }
|
|
586
|
+
/** A dispatch (ADR-0071): the Workflow it starts a run of, as `jr2 run` names it, whether it
|
|
587
|
+
* waits, and — toward another Orchestrator — the target's origin as an http target and its
|
|
588
|
+
* token as the one secret (placed in the Instance Effector beside the http Effects). No target:
|
|
589
|
+
* it runs in the Orchestrator's own process and names no secret. `input`: the slot's input
|
|
590
|
+
* schema as JSON Schema, what the walk compares with the callee's door; absent, the door alone
|
|
591
|
+
* checks. An Open target has no target and no secret yet. */
|
|
592
|
+
| {
|
|
593
|
+
kind: "dispatch";
|
|
594
|
+
workflow: string;
|
|
595
|
+
wait: boolean;
|
|
596
|
+
target?: { scheme: "http" | "https"; host: string; port: number };
|
|
597
|
+
secrets: string[];
|
|
598
|
+
input?: Record<string, unknown>;
|
|
599
|
+
open?: Array<"target">;
|
|
600
|
+
}
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
/** One state that can fail the run (ADR-0073): it invokes a `fail()` slot. `state` is the state's
|
|
604
|
+
* id, `machine` the id of its Machine, `slot` the slot key it invokes, and `path` the `customize()`
|
|
605
|
+
* route to its Machine, as an {@link OpenSlot}'s. Static, as a Gate or an Effect is: the walk reads
|
|
606
|
+
* it, and `jr2 up`, `jr2 check` and the Machine doc list it. */
|
|
607
|
+
export type CarriedFailure = { machine: string; state: string; slot: string; path: OpenSlot["path"] };
|
|
608
|
+
|
|
346
609
|
/** Everything the registered Machines carry that a converge or a boot must act on. */
|
|
347
610
|
export type CarriedParts = {
|
|
348
611
|
agents: CarriedAgent[];
|
|
612
|
+
/** Every state that invokes a `fail()` slot (ADR-0073), in walk order, nested Machines too. */
|
|
613
|
+
failures: CarriedFailure[];
|
|
614
|
+
/** Every Effect slot, in walk order — one per Machine and slot key, at the first route the walk
|
|
615
|
+
* reached its Machine by. The placement reads {@link placedEffectsOf}: one per route. */
|
|
616
|
+
effects: CarriedEffect[];
|
|
349
617
|
images: CarriedImage[];
|
|
350
618
|
/** Every bound Repo, deduped by identity, in walk order. */
|
|
351
619
|
repos: CarriedRepo[];
|
|
@@ -355,8 +623,32 @@ export type CarriedParts = {
|
|
|
355
623
|
/** Every Agent whose model is still Open (ADR-0054), in walk order — the same refusal, by the
|
|
356
624
|
* same route, and the reason these are two lists rather than one: the fix lines differ. */
|
|
357
625
|
openAgents: OpenSlot[];
|
|
626
|
+
/** Every Effect whose url, argv or secret is still Open (ADR-0054), in walk order — the same
|
|
627
|
+
* refusal, by the same route; `parts` says which line binds it: one object for both of two. */
|
|
628
|
+
openEffects: Array<OpenSlot & { parts: OpenEffectPart[] }>;
|
|
629
|
+
/** Every `workspace()` reached, with the Size it states, in walk order (ADR-0060). */
|
|
630
|
+
sizes: CarriedSize[];
|
|
358
631
|
/** Whether any Machine reached, at any depth, composes a Sandbox — the data-plane switch. */
|
|
359
632
|
composesSandbox: boolean;
|
|
633
|
+
/**
|
|
634
|
+
* Every Open body slot (ADR-0070), in walk order, with its declaration: what it lends, what a Chart
|
|
635
|
+
* may define, its ceilings. The one Open part a run binds, not a composer, so it is never in an
|
|
636
|
+
* Open list: an Open body whose lent names did not resolve never reaches the walk (createMachine
|
|
637
|
+
* refused it).
|
|
638
|
+
*/
|
|
639
|
+
openBodies: Array<{ slot: string; declaration: OpenBodyDeclaration; path: OpenSlot["path"] }>;
|
|
640
|
+
/**
|
|
641
|
+
* Every model a key is placed and a provider preflighted for: each bound Agent's, and each model
|
|
642
|
+
* an Open body allows a Chart's own Agents (ADR-0070: a minted model is a door refusal, never a
|
|
643
|
+
* 401 mid-run). Deduped, sorted.
|
|
644
|
+
*/
|
|
645
|
+
models: string[];
|
|
646
|
+
/**
|
|
647
|
+
* The Instance Harness is needed (ADR-0031): an Agent declares `workspace: "none"`, or an Open
|
|
648
|
+
* body allows it. `models` are the models its Turns may run: those Agents', and those Open bodies'
|
|
649
|
+
* allowed ones. Absent when nothing needs it.
|
|
650
|
+
*/
|
|
651
|
+
instanceHarness?: { models: string[] };
|
|
360
652
|
};
|
|
361
653
|
|
|
362
654
|
/** A machine actor, told apart from a promise/callback/observable one by having a state tree.
|
|
@@ -366,16 +658,49 @@ export function asMachine(logic: unknown): AnyStateMachine | undefined {
|
|
|
366
658
|
return (logic as AnyStateMachine | undefined)?.root ? (logic as AnyStateMachine) : undefined;
|
|
367
659
|
}
|
|
368
660
|
|
|
369
|
-
/** Every machine object written INLINE on one state's invokes
|
|
661
|
+
/** Every machine object written INLINE on one state's invokes, with the hop xstate gives it (its
|
|
662
|
+
* generated src, from the invoke's index on the state). A named `src` resolves through
|
|
370
663
|
* `implementations.actors` instead and is walked there. */
|
|
371
|
-
function inlineMachines(node: StateNode<any, any>): AnyStateMachine
|
|
664
|
+
function inlineMachines(node: StateNode<any, any>): Array<{ machine: AnyStateMachine; hop: RouteHop }> {
|
|
372
665
|
const config = [node.config.invoke ?? []].flat() as Array<{ src?: unknown }>;
|
|
373
|
-
return config.flatMap((inv) => {
|
|
666
|
+
return config.flatMap((inv, index) => {
|
|
374
667
|
const machine = asMachine(inv?.src);
|
|
375
|
-
return machine ? [machine] : [];
|
|
668
|
+
return machine ? [{ machine, hop: inlineHop(index, node.id) }] : [];
|
|
376
669
|
});
|
|
377
670
|
}
|
|
378
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Refuse a Part written inline on an `invoke.src` — an `agent()`, an Effect, a `fail()` or a `workspace()` that
|
|
674
|
+
* is no key of the Machine's `actors` (see the module comment). Thrown at build by
|
|
675
|
+
* `jr2Setup.createMachine` and at load by {@link partsOf}, so a Machine built with xstate's own
|
|
676
|
+
* `createMachine` is refused where `jr2 check`, `jr2 up` and the Orchestrator read it. A plain
|
|
677
|
+
* child Machine written inline is not a Part: it stays walked, with no `customize()` route (its
|
|
678
|
+
* `path` is `undefined`).
|
|
679
|
+
*/
|
|
680
|
+
export function refuseInlineParts(machine: AnyStateMachine): void {
|
|
681
|
+
const partOf = (src: unknown): { part: string; written: string } | undefined => {
|
|
682
|
+
if (isAgent(src)) return { part: "an Agent", written: "agent(...)" };
|
|
683
|
+
if (isEffect(src)) return { part: "an Effect", written: `${src.effect.kind}(...)` };
|
|
684
|
+
if (isFailSlot(src)) return { part: "a fail() Part", written: "fail()" };
|
|
685
|
+
const child = asMachine(src);
|
|
686
|
+
if (child && composesSandbox(child)) return { part: "a Workspace", written: "workspace(...)" };
|
|
687
|
+
return undefined;
|
|
688
|
+
};
|
|
689
|
+
const visit = (node: StateNode<any, any>): void => {
|
|
690
|
+
for (const inv of [node.config.invoke ?? []].flat() as Array<{ src?: unknown }>) {
|
|
691
|
+
const found = partOf(inv?.src);
|
|
692
|
+
if (found === undefined) continue;
|
|
693
|
+
throw new Error(
|
|
694
|
+
`machine "${machine.id}": state "${node.id}" invokes ${found.part} inline — a Part is a key of actors, so ` +
|
|
695
|
+
`it has a name that the ledger, the report and customize() share: actors: { <name>: ${found.written} }, ` +
|
|
696
|
+
`then invoke: { src: "<name>" }`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
for (const child of Object.values(node.states as Record<string, StateNode<any, any>>)) visit(child);
|
|
700
|
+
};
|
|
701
|
+
visit(machine.root);
|
|
702
|
+
}
|
|
703
|
+
|
|
379
704
|
/** `JSON.stringify` with object keys sorted at every depth — a value's identity, not its spelling.
|
|
380
705
|
* Symbols are spelled out rather than dropped: `JSON.stringify` silently omits a symbol-valued
|
|
381
706
|
* key, which would make an Open Agent (ADR-0054) collapse into a bound one that differs in nothing
|
|
@@ -405,10 +730,17 @@ function canonical(value: unknown): string {
|
|
|
405
730
|
*/
|
|
406
731
|
export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
|
|
407
732
|
const agents: CarriedAgent[] = [];
|
|
733
|
+
const failures: CarriedFailure[] = [];
|
|
734
|
+
const effects: CarriedEffect[] = [];
|
|
408
735
|
const images: CarriedImage[] = [];
|
|
409
736
|
const repos: CarriedRepo[] = [];
|
|
410
737
|
const openSlots: OpenSlot[] = [];
|
|
411
738
|
const openAgents: OpenSlot[] = [];
|
|
739
|
+
const openEffects: CarriedParts["openEffects"] = [];
|
|
740
|
+
const sizes: CarriedSize[] = [];
|
|
741
|
+
const openBodies: CarriedParts["openBodies"] = [];
|
|
742
|
+
// The Machine that declares each Open body: where its lent Agents are read (not carried data).
|
|
743
|
+
const bodyWrappers = new Map<CarriedParts["openBodies"][number], AnyStateMachine>();
|
|
412
744
|
let sandboxed = false;
|
|
413
745
|
const seen = new Set<string>();
|
|
414
746
|
// Cycle guard AND work saver: a Machine reached twice carries the same parts both times, and a
|
|
@@ -466,35 +798,371 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
|
|
|
466
798
|
}
|
|
467
799
|
};
|
|
468
800
|
|
|
469
|
-
const walkStates = (node: StateNode<any, any>, visit: (machine: AnyStateMachine) => void): void => {
|
|
470
|
-
for (const machine of inlineMachines(node)) visit(machine);
|
|
471
|
-
for (const child of Object.values(node.states as Record<string, StateNode<any, any>>)) walkStates(child, visit);
|
|
472
|
-
};
|
|
473
|
-
|
|
474
801
|
// `path` is the `customize()` route from the registered root to `machine` (see `OpenSlot`):
|
|
475
802
|
// a named child slot extends it, a wrapper's transparent body does not, and an inline invoke
|
|
476
803
|
// ends it — nothing downstream of an actor without a slot key can be named by a composer.
|
|
477
|
-
|
|
804
|
+
// `route` is the placement route, which an inline invoke extends by its hop (`PlacementRoute`).
|
|
805
|
+
const walk = (machine: AnyStateMachine, path: OpenSlot["path"], route: PlacementRoute): void => {
|
|
478
806
|
if (walked.has(machine)) return;
|
|
479
807
|
walked.add(machine);
|
|
808
|
+
refuseInlineParts(machine);
|
|
809
|
+
refuseLostOutput(machine);
|
|
810
|
+
refuseUnroutedAgentFault(machine);
|
|
480
811
|
const parts = sandboxPartsOf(machine);
|
|
812
|
+
const user = userSeatOf(parts.user);
|
|
481
813
|
collectImage(parts.image);
|
|
482
|
-
collectImage(
|
|
814
|
+
collectImage(user.image);
|
|
815
|
+
collectImage(parts.effector?.image);
|
|
483
816
|
if (composesSandbox(machine)) {
|
|
484
817
|
sandboxed = true;
|
|
485
818
|
collectRepos(path, parts.repos);
|
|
819
|
+
sizes.push({
|
|
820
|
+
path,
|
|
821
|
+
route,
|
|
822
|
+
...(parts.resources !== undefined ? { resources: parts.resources } : {}),
|
|
823
|
+
...(user.resources !== undefined ? { user: user.resources } : {}),
|
|
824
|
+
});
|
|
486
825
|
}
|
|
487
826
|
const body = wrapperBodyOf(machine);
|
|
827
|
+
for (const { state, slot } of failingStatesOf(machine)) failures.push({ machine: machine.id, state, slot, path });
|
|
488
828
|
for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
|
|
489
829
|
if (isAgent(logic)) collectAgent(path, name, logic.definition);
|
|
490
|
-
else {
|
|
830
|
+
else if (isOpenBody(logic)) {
|
|
831
|
+
const body = { slot: name, declaration: openBodyDeclarationOf(logic), path };
|
|
832
|
+
openBodies.push(body);
|
|
833
|
+
bodyWrappers.set(body, machine);
|
|
834
|
+
} else if (isEffect(logic)) {
|
|
835
|
+
const effect = carriedEffectOf(name, path, route, logic.effect);
|
|
836
|
+
effects.push(effect);
|
|
837
|
+
if (effect.kind !== "push" && effect.open !== undefined)
|
|
838
|
+
openEffects.push({ slot: name, path, parts: [...effect.open] });
|
|
839
|
+
} else {
|
|
491
840
|
const child = asMachine(logic);
|
|
492
|
-
if (child) walk(child, path
|
|
841
|
+
if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name]);
|
|
493
842
|
}
|
|
494
843
|
}
|
|
495
|
-
walkStates(machine.root, (inline) => walk(inline, undefined));
|
|
844
|
+
walkStates(machine.root, (inline, hop) => walk(inline, undefined, [...route, hop]));
|
|
496
845
|
};
|
|
497
846
|
|
|
498
|
-
for (const machine of machines) walk(machine, []);
|
|
499
|
-
|
|
847
|
+
for (const machine of machines) walk(machine, [], []);
|
|
848
|
+
const bound = (m: unknown): m is string => typeof m === "string";
|
|
849
|
+
const models = new Set<string>(agents.map((a) => a.definition.model).filter(bound));
|
|
850
|
+
for (const body of openBodies) for (const m of body.declaration.allows.models) models.add(m);
|
|
851
|
+
const menuOnly = agents.filter((a) => a.definition.workspace === "none");
|
|
852
|
+
const noneBodies = openBodies.filter((b) => bodyTurnsRunIn(bodyWrappers.get(b)!, b.declaration, "instance"));
|
|
853
|
+
const harnessModels = new Set<string>(menuOnly.map((a) => a.definition.model).filter(bound));
|
|
854
|
+
for (const body of noneBodies) for (const m of body.declaration.allows.models) harnessModels.add(m);
|
|
855
|
+
return {
|
|
856
|
+
agents,
|
|
857
|
+
failures,
|
|
858
|
+
effects,
|
|
859
|
+
images,
|
|
860
|
+
repos,
|
|
861
|
+
openSlots,
|
|
862
|
+
openAgents,
|
|
863
|
+
openEffects,
|
|
864
|
+
sizes,
|
|
865
|
+
composesSandbox: sandboxed,
|
|
866
|
+
openBodies,
|
|
867
|
+
models: [...models].sort(),
|
|
868
|
+
...(menuOnly.length || noneBodies.length ? { instanceHarness: { models: [...harnessModels].sort() } } : {}),
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Can a Turn under an Open body of `wrapper` run where `where` says, with a model the body allows
|
|
874
|
+
* (ADR-0070: a minted model needs a key the Assembly placed, a door refusal and never a 401)? A
|
|
875
|
+
* Chart's own Agent runs where its Workspace access says, so a body that allows "write" or "read"
|
|
876
|
+
* runs Turns in a pod, and one that allows "none" at the Instance Harness. A lent Agent runs where
|
|
877
|
+
* its own access says, and a Chart may set its `model` dial to an allowed model: a lent Agent that
|
|
878
|
+
* is not "none" is a pod Turn, and a "none" one an Instance Harness Turn.
|
|
879
|
+
*/
|
|
880
|
+
function bodyTurnsRunIn(wrapper: AnyStateMachine, decl: OpenBodyDeclaration, where: "pod" | "instance"): boolean {
|
|
881
|
+
const atInstance = (w: string | undefined) => w === "none";
|
|
882
|
+
if (decl.allows.workspace.some((w) => (where === "instance" ? atInstance(w) : !atInstance(w)))) return true;
|
|
883
|
+
const actors = wrapper.implementations.actors as Record<string, unknown>;
|
|
884
|
+
return (decl.lends.agents ?? []).some((slot) => {
|
|
885
|
+
const logic = actors[slot];
|
|
886
|
+
if (!isAgent(logic)) return false;
|
|
887
|
+
const access = (logic.definition as { workspace?: string }).workspace;
|
|
888
|
+
return where === "instance" ? atInstance(access) : !atInstance(access);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** The `customize()` route to a child of slot `name`: a wrapper's body does not extend it. */
|
|
893
|
+
function slotPath(path: OpenSlot["path"], name: string, body: string | undefined): OpenSlot["path"] {
|
|
894
|
+
return path === undefined ? undefined : name === body ? path : [...path, name];
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Every Effect the registered Machine `root` places, at every route it is reached by — one entry
|
|
899
|
+
* per route, as {@link workspacePodsOf} lists pods: a Machine invoked under two slot keys places
|
|
900
|
+
* its Effects twice, and each is checked where it is. What the placement walk reads (ADR-0068),
|
|
901
|
+
* never {@link partsOf}'s list, which holds one entry per Machine. A cycle (a recursive pool
|
|
902
|
+
* worker) stops at the Machine on its own route.
|
|
903
|
+
*/
|
|
904
|
+
export function placedEffectsOf(root: AnyStateMachine): CarriedEffect[] {
|
|
905
|
+
const effects: CarriedEffect[] = [];
|
|
906
|
+
const walk = (
|
|
907
|
+
machine: AnyStateMachine,
|
|
908
|
+
path: OpenSlot["path"],
|
|
909
|
+
route: PlacementRoute,
|
|
910
|
+
on: Set<AnyStateMachine>,
|
|
911
|
+
): void => {
|
|
912
|
+
if (on.has(machine)) return;
|
|
913
|
+
on.add(machine);
|
|
914
|
+
const body = wrapperBodyOf(machine);
|
|
915
|
+
for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
|
|
916
|
+
if (isAgent(logic)) continue;
|
|
917
|
+
if (isEffect(logic)) {
|
|
918
|
+
effects.push(carriedEffectOf(name, path, route, logic.effect));
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
const child = asMachine(logic);
|
|
922
|
+
if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name], on);
|
|
923
|
+
}
|
|
924
|
+
walkStates(machine.root, (inline, hop) => walk(inline, undefined, [...route, hop], on));
|
|
925
|
+
on.delete(machine);
|
|
926
|
+
};
|
|
927
|
+
walk(root, [], [], new Set());
|
|
928
|
+
return effects;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** One Effect slot's plain data, as `jr2 up` and the placement read it. */
|
|
932
|
+
export function carriedEffectOf(
|
|
933
|
+
name: string,
|
|
934
|
+
path: OpenSlot["path"],
|
|
935
|
+
route: PlacementRoute,
|
|
936
|
+
parts: EffectParts,
|
|
937
|
+
): CarriedEffect {
|
|
938
|
+
const placement = placementOf(parts);
|
|
939
|
+
const openParts = openPartsOf(parts);
|
|
940
|
+
if (parts.kind === "dispatch") {
|
|
941
|
+
const input = (parts as { [DISPATCH_INPUT]?: Record<string, unknown> })[DISPATCH_INPUT];
|
|
942
|
+
return {
|
|
943
|
+
name,
|
|
944
|
+
path,
|
|
945
|
+
route,
|
|
946
|
+
kind: "dispatch",
|
|
947
|
+
placement,
|
|
948
|
+
workflow: parts.workflow,
|
|
949
|
+
wait: parts.wait,
|
|
950
|
+
timeoutMs: parts.timeoutMs,
|
|
951
|
+
...(parts.target === undefined || parts.target === open ? {} : { target: originTarget(parts.target.url) }),
|
|
952
|
+
secrets: [...parts.secrets],
|
|
953
|
+
...(input !== undefined ? { input } : {}),
|
|
954
|
+
...(openParts.length ? { open: openParts as Array<"target"> } : {}),
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
if (parts.kind === "http") {
|
|
958
|
+
return {
|
|
959
|
+
name,
|
|
960
|
+
path,
|
|
961
|
+
route,
|
|
962
|
+
kind: "http",
|
|
963
|
+
placement,
|
|
964
|
+
method: parts.method,
|
|
965
|
+
...(parts.target === open ? {} : { target: { ...parts.target } }),
|
|
966
|
+
...(openParts.length ? { open: openParts as Array<"url" | "secret"> } : {}),
|
|
967
|
+
secrets: [...parts.secrets],
|
|
968
|
+
idempotent: parts.idempotent,
|
|
969
|
+
timeoutMs: parts.timeoutMs,
|
|
970
|
+
...(parts.file ? { file: true as const } : {}),
|
|
971
|
+
...(parts.file?.slot !== undefined ? { slot: parts.file.slot } : {}),
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
if (parts.kind === "push") {
|
|
975
|
+
const { sign, squash, force, pin, timeoutMs, slot } = parts;
|
|
976
|
+
return {
|
|
977
|
+
name,
|
|
978
|
+
path,
|
|
979
|
+
route,
|
|
980
|
+
kind: "push",
|
|
981
|
+
placement,
|
|
982
|
+
sign,
|
|
983
|
+
squash,
|
|
984
|
+
force,
|
|
985
|
+
pin,
|
|
986
|
+
secrets: [],
|
|
987
|
+
timeoutMs,
|
|
988
|
+
...(slot !== undefined ? { slot } : {}),
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
const { argv, timeoutMs, slot, secrets, pin } = parts;
|
|
992
|
+
return {
|
|
993
|
+
name,
|
|
994
|
+
path,
|
|
995
|
+
route,
|
|
996
|
+
kind: "command",
|
|
997
|
+
placement,
|
|
998
|
+
argv: argvText(argv),
|
|
999
|
+
timeoutMs,
|
|
1000
|
+
...(slot !== undefined ? { slot } : {}),
|
|
1001
|
+
...(secrets?.length ? { secrets: [...secrets] } : {}),
|
|
1002
|
+
...(pin ? { pin } : {}),
|
|
1003
|
+
...(openParts.length ? { open: openParts as Array<"argv" | "secret"> } : {}),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** The key a dispatch's slot schema rides its parts under (dispatch-effect.ts): `Symbol.for`, so this
|
|
1008
|
+
* walk reads it off a Machine built by another copy of the module (ADR-0071). */
|
|
1009
|
+
const DISPATCH_INPUT = Symbol.for("jr2.dispatch.input");
|
|
1010
|
+
|
|
1011
|
+
/** A dispatch target's origin as the Effector's fence reads an http target: scheme, host, port. The
|
|
1012
|
+
* origin was checked when the slot was built, so it parses. */
|
|
1013
|
+
function originTarget(url: string): { scheme: "http" | "https"; host: string; port: number } {
|
|
1014
|
+
const u = new URL(url);
|
|
1015
|
+
const scheme = u.protocol === "http:" ? "http" : "https";
|
|
1016
|
+
// As an http target's: brackets off an IPv6 literal, lower case (http-effect.ts `targetOf`).
|
|
1017
|
+
const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1018
|
+
return { scheme, host, port: u.port ? Number(u.port) : scheme === "http" ? 80 : 443 };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* One Workspace a Machine composes, as its pod's Held secrets are placed (ADR-0059): what the pod
|
|
1023
|
+
* names. Located like a {@link CarriedSize} — `path` is the `customize()` route to its
|
|
1024
|
+
* `workspace()`, and `route` its placement route, the same route the run host reads off the live
|
|
1025
|
+
* actor at provision (`actorRoute`), so `jr2 up` and the provision name one pod alike.
|
|
1026
|
+
*/
|
|
1027
|
+
export type WorkspacePod = {
|
|
1028
|
+
path: readonly string[] | undefined;
|
|
1029
|
+
/** The pod's placement route: what its pod key reads (`workspaceHeldPod`). */
|
|
1030
|
+
route: PlacementRoute;
|
|
1031
|
+
/** The `workspace()`'s own `secrets: [...]`, as written. */
|
|
1032
|
+
secrets: string[];
|
|
1033
|
+
/** The model of every bound Agent whose Turn runs in this pod: each Agent below the
|
|
1034
|
+
* `workspace()` that is not `workspace: "none"` (those run at the Instance Harness, ADR-0031),
|
|
1035
|
+
* down to the next `workspace()`, which is a pod of its own, and every model an Open body below
|
|
1036
|
+
* it allows a Chart's own Agents when it allows "write" or "read" (ADR-0070). Sorted, deduped. */
|
|
1037
|
+
models: string[];
|
|
1038
|
+
/** The Effects placed in this pod's Sandbox Effector (ADR-0068): every `push`, file-body `http` and
|
|
1039
|
+
* credentialed `command` slot below the `workspace()`, down to the next `workspace()` — read off
|
|
1040
|
+
* their plain data, statically. Empty → the pod gets no Sandbox Effector and no Secret. */
|
|
1041
|
+
effects: CarriedEffect[];
|
|
1042
|
+
/** The `workspace()`'s `effector: { image }`, as written. Absent → the Sandbox Image. */
|
|
1043
|
+
effector?: { image: string };
|
|
1044
|
+
/** The `workspace()`'s own `image`, as written — its Sandbox Image. Absent → `images/default`,
|
|
1045
|
+
* then the stock Harness (ADR-0037). */
|
|
1046
|
+
image?: string;
|
|
1047
|
+
/** Each Repo Slot's Binding url as the Machine binds it, by slot, in order; undefined for an open
|
|
1048
|
+
* or per-run slot, whose url `jr2 up` cannot read. A push resolves its credential through it. */
|
|
1049
|
+
bindings: Record<string, string | undefined>;
|
|
1050
|
+
/** The `workspace()` declares no Repo Slot (ADR-0051): it has only `/work`, which its steps
|
|
1051
|
+
* share. Absent for one with slots, and for an Open map, whose slots the composer names. */
|
|
1052
|
+
noSlot?: true;
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Every Workspace a Machine composes, at every route it is reached by — one entry per route, so a
|
|
1057
|
+
* `workspace()` composed under two slot keys is two pods. Not deduped like {@link partsOf}: a pod
|
|
1058
|
+
* is a route, not a Machine. A cycle (a recursive pool worker) stops at the Machine on its own
|
|
1059
|
+
* route.
|
|
1060
|
+
*/
|
|
1061
|
+
export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
|
|
1062
|
+
const pods: WorkspacePod[] = [];
|
|
1063
|
+
/** The bound models of the Agents in `machine` and the Machines it composes, short of a
|
|
1064
|
+
* `workspace()`. */
|
|
1065
|
+
const modelsIn = (
|
|
1066
|
+
machine: AnyStateMachine,
|
|
1067
|
+
models: Set<string>,
|
|
1068
|
+
on: Set<AnyStateMachine>,
|
|
1069
|
+
effects: Map<string, CarriedEffect>,
|
|
1070
|
+
path: OpenSlot["path"],
|
|
1071
|
+
route: PlacementRoute,
|
|
1072
|
+
): void => {
|
|
1073
|
+
if (on.has(machine)) return;
|
|
1074
|
+
on.add(machine);
|
|
1075
|
+
const body = wrapperBodyOf(machine);
|
|
1076
|
+
const visit = (child: AnyStateMachine, at: OpenSlot["path"], to: PlacementRoute) => {
|
|
1077
|
+
if (!composesSandbox(child)) modelsIn(child, models, on, effects, at, to);
|
|
1078
|
+
};
|
|
1079
|
+
for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
|
|
1080
|
+
if (isAgent(logic)) {
|
|
1081
|
+
const { model, workspace } = logic.definition;
|
|
1082
|
+
if (workspace !== "none" && typeof model === "string") models.add(model);
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
// A Turn under an Open body may run in this pod with any model the body allows (ADR-0070):
|
|
1086
|
+
// a Chart's own Agent, when the body allows a Workspace access other than "none", and a lent
|
|
1087
|
+
// Agent that is not "none", whose `model` dial a Chart may set to an allowed model.
|
|
1088
|
+
if (isOpenBody(logic)) {
|
|
1089
|
+
const decl = openBodyDeclarationOf(logic);
|
|
1090
|
+
if (bodyTurnsRunIn(machine, decl, "pod")) for (const m of decl.allows.models) models.add(m);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (isEffect(logic)) {
|
|
1094
|
+
const effect = carriedEffectOf(name, path, route, logic.effect);
|
|
1095
|
+
if (effect.placement === "sandbox") effects.set(JSON.stringify([route, name]), effect);
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
const child = asMachine(logic);
|
|
1099
|
+
if (child) visit(child, slotPath(path, name, body), name === body ? route : [...route, name]);
|
|
1100
|
+
}
|
|
1101
|
+
walkStates(machine.root, (inline, hop) => visit(inline, undefined, [...route, hop]));
|
|
1102
|
+
on.delete(machine);
|
|
1103
|
+
};
|
|
1104
|
+
const walk = (
|
|
1105
|
+
machine: AnyStateMachine,
|
|
1106
|
+
path: OpenSlot["path"],
|
|
1107
|
+
route: PlacementRoute,
|
|
1108
|
+
on: Set<AnyStateMachine>,
|
|
1109
|
+
): void => {
|
|
1110
|
+
if (on.has(machine)) return;
|
|
1111
|
+
on.add(machine);
|
|
1112
|
+
if (composesSandbox(machine)) {
|
|
1113
|
+
const models = new Set<string>();
|
|
1114
|
+
const effects = new Map<string, CarriedEffect>();
|
|
1115
|
+
modelsIn(machine, models, new Set(), effects, path, route);
|
|
1116
|
+
const parts = sandboxPartsOf(machine);
|
|
1117
|
+
const bindings: Record<string, string | undefined> = {};
|
|
1118
|
+
if (parts.repos !== open) {
|
|
1119
|
+
for (const [slot, value] of Object.entries(parts.repos)) {
|
|
1120
|
+
const state = repoSlotState(value);
|
|
1121
|
+
bindings[slot] = state.kind === "bound" ? state.binding.url : undefined;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
pods.push({
|
|
1125
|
+
path,
|
|
1126
|
+
route,
|
|
1127
|
+
secrets: [...(parts.secrets ?? [])],
|
|
1128
|
+
models: [...models].sort(),
|
|
1129
|
+
effects: [...effects.values()],
|
|
1130
|
+
...(parts.effector ? { effector: { ...parts.effector } } : {}),
|
|
1131
|
+
...(parts.image !== undefined ? { image: parts.image } : {}),
|
|
1132
|
+
bindings,
|
|
1133
|
+
...(parts.repos !== open && Object.keys(parts.repos).length === 0 ? { noSlot: true as const } : {}),
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
const body = wrapperBodyOf(machine);
|
|
1137
|
+
for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
|
|
1138
|
+
const child = isAgent(logic) || isEffect(logic) ? undefined : asMachine(logic);
|
|
1139
|
+
if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name], on);
|
|
1140
|
+
}
|
|
1141
|
+
walkStates(machine.root, (inline, hop) => walk(inline, undefined, [...route, hop], on));
|
|
1142
|
+
on.delete(machine);
|
|
1143
|
+
};
|
|
1144
|
+
walk(root, [], [], new Set());
|
|
1145
|
+
return pods;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Every state of `machine`'s own tree that invokes a `fail()` slot (ADR-0073), in document order,
|
|
1149
|
+
* with the slot key: a state that can fail the run. A child Machine's states are its own walk's. */
|
|
1150
|
+
export function failingStatesOf(machine: AnyStateMachine): Array<{ state: string; slot: string }> {
|
|
1151
|
+
const actors = machine.implementations.actors as Record<string, unknown>;
|
|
1152
|
+
const out: Array<{ state: string; slot: string }> = [];
|
|
1153
|
+
const visit = (node: StateNode<any, any>): void => {
|
|
1154
|
+
for (const inv of node.invoke) {
|
|
1155
|
+
if (typeof inv.src === "string" && isFailSlot(actors[inv.src])) out.push({ state: node.id, slot: inv.src });
|
|
1156
|
+
}
|
|
1157
|
+
const children = Object.values(node.states as Record<string, StateNode<any, any>>);
|
|
1158
|
+
for (const child of children.sort((a, b) => a.order - b.order)) visit(child);
|
|
1159
|
+
};
|
|
1160
|
+
visit(machine.root);
|
|
1161
|
+
return out;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/** Every machine object written inline below `node`, at any depth of its state tree, with its hop. */
|
|
1165
|
+
function walkStates(node: StateNode<any, any>, visit: (machine: AnyStateMachine, hop: RouteHop) => void): void {
|
|
1166
|
+
for (const { machine, hop } of inlineMachines(node)) visit(machine, hop);
|
|
1167
|
+
for (const child of Object.values(node.states as Record<string, StateNode<any, any>>)) walkStates(child, visit);
|
|
500
1168
|
}
|