@jr2/orchestrator 0.4.1 → 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/console/components/app.ts +2 -0
- package/console/components/fleet.ts +14 -1
- package/console/components/machine-pane.ts +41 -2
- package/console/main.ts +13 -1
- package/console/store.ts +102 -6
- package/console/style.css +21 -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 +5 -3
- package/src/actor.ts +541 -79
- package/src/agent.ts +114 -38
- package/src/ambient.ts +2 -1
- package/src/assembly-checks.ts +189 -7
- package/src/cause.ts +234 -0
- package/src/chart.ts +169 -37
- package/src/charts.ts +34 -6
- package/src/config.ts +430 -178
- package/src/custodian.ts +74 -22
- package/src/customize.ts +188 -46
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect.ts +341 -44
- package/src/effector-wire.ts +160 -11
- package/src/fail.ts +209 -0
- package/src/harness-client.ts +541 -47
- package/src/http-effect.ts +495 -53
- package/src/http.ts +427 -16
- package/src/index.ts +60 -82
- package/src/instance.ts +68 -8
- package/src/internal.ts +84 -0
- package/src/machine-doc.ts +21 -3
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +18 -58
- package/src/notices.ts +66 -13
- package/src/open-body.ts +159 -23
- package/src/open-effect.ts +81 -8
- package/src/open.ts +59 -6
- package/src/parts.ts +184 -13
- package/src/party.ts +68 -2
- package/src/pick.ts +463 -11
- package/src/pool.ts +247 -36
- package/src/registration.ts +83 -4
- package/src/repos.ts +2 -1
- package/src/run-host.ts +565 -31
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +273 -38
- package/src/sandbox-kube.ts +3 -1
- package/src/secret-placement.ts +52 -17
- package/src/server.ts +3 -0
- package/src/setup.ts +239 -56
- package/src/size.ts +121 -9
- package/src/snapshot-store.ts +54 -10
- package/src/stub-harness.ts +44 -16
- package/src/timers.ts +14 -1
- package/src/vocabulary.ts +69 -14
- package/src/wire.ts +90 -16
- package/src/workspace.ts +697 -175
package/src/workspace.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// `workspace(body, { input, image, user, repos, resources, spec })` (ADR-0012, ADR-0049, ADR-0051,
|
|
2
2
|
// ADR-0060): the jr2-owned wrapper Machine that owns ONLY Sandbox lifecycle — provision the Sandbox
|
|
3
|
-
// (out of the STATIC `image`/`user`/`repos`/`resources` options it carries) + attach one worktree
|
|
4
|
-
// author's body Machine inside it as the named slot `body`, with
|
|
5
|
-
// appended to its input (the mechanism-facing endpoint/sandbox
|
|
6
|
-
// — ADR-0016, ambient.ts), and destroy the Sandbox when the body reaches a
|
|
3
|
+
// (out of the STATIC `image`/`user`/`repos`/`resources` options it carries) + attach one worktree
|
|
4
|
+
// per Repo Slot, run the author's body Machine inside it as the named slot `body`, with
|
|
5
|
+
// `{ workspace: { repos, branch } }` appended to its input (the mechanism-facing endpoint/sandbox
|
|
6
|
+
// are published ambiently — ADR-0016, ambient.ts), and destroy the Sandbox when the body reaches a
|
|
7
|
+
// final state or fails (ADR-0073). Teardown lives INSIDE the
|
|
7
8
|
// wrapper's own states because an xstate stop is synchronous — multi-step async cleanup must be
|
|
8
9
|
// states the machine transitions through itself, which forces the thing that provisions to also
|
|
9
10
|
// observe the body's completion (the ADR's load-bearing argument). There is no retain policy: a
|
|
@@ -18,7 +19,9 @@
|
|
|
18
19
|
//
|
|
19
20
|
// The lifecycle (ADR-0012, ADR-0064): `placing` writes the Sandbox and waits, with no deadline,
|
|
20
21
|
// for its pod to be scheduled; `provisioning` waits for Ready under a budget that starts there;
|
|
21
|
-
// `attaching` runs the Harness's attach; `running` runs the body; `teardown` destroys.
|
|
22
|
+
// `attaching` runs the Harness's attach; `running` runs the body; `teardown` destroys. A Failure from
|
|
23
|
+
// the body (ADR-0073) goes to `failing`, which destroys as `teardown` does and then sends the same
|
|
24
|
+
// Failure up; a defect has no `onError` here, so it goes up at once and its Sandbox stays as evidence.
|
|
22
25
|
//
|
|
23
26
|
// Restore-reconcile (ADR-0012, ADR-0021): every state from `placing` to `running` co-invokes the
|
|
24
27
|
// lease, and in `running` the lease judges Continuity from the Orchestrator's watch for its
|
|
@@ -41,16 +44,20 @@ import {
|
|
|
41
44
|
} from "xstate";
|
|
42
45
|
import type { z } from "zod";
|
|
43
46
|
import { registerAmbientHandles, type AmbientHandles } from "./ambient.ts";
|
|
47
|
+
import { failLogic, failurePayload, isFailure, type Failure, type FailureInput } from "./fail.ts";
|
|
48
|
+
import { refuseUnroutedLost } from "./mechanism-routes.ts";
|
|
44
49
|
import {
|
|
45
50
|
actorRoute,
|
|
46
51
|
actorSlotPath,
|
|
47
52
|
assertRepoSlot,
|
|
48
53
|
attachSandboxParts,
|
|
49
54
|
attachWrapperBody,
|
|
55
|
+
composesSandbox,
|
|
50
56
|
customizeLine,
|
|
51
57
|
open,
|
|
52
58
|
repoSlotState,
|
|
53
59
|
sandboxPartsOf,
|
|
60
|
+
wrapperBodyOf,
|
|
54
61
|
type Binding,
|
|
55
62
|
type JR2Repos,
|
|
56
63
|
type JR2Wrapper,
|
|
@@ -61,7 +68,7 @@ import {
|
|
|
61
68
|
} from "./parts.ts";
|
|
62
69
|
import { actorPath, runBindingOf, type AnyActorSystem } from "./registration.ts";
|
|
63
70
|
import { assertSize, assertUserSeat, userSeatOf, type Size, type UserContainer } from "./size.ts";
|
|
64
|
-
import { attachInputSchema,
|
|
71
|
+
import { attachInputSchema, declaresRunInput, invokingMachine, type HostInjectedInput } from "./vocabulary.ts";
|
|
65
72
|
|
|
66
73
|
/** Lease cadence when the backend names none. Well inside the 30m default idle timeout, so a
|
|
67
74
|
* few missed renewals in a row are survivable. It bounds only how long an orphan waits to be
|
|
@@ -83,75 +90,201 @@ export function leaseDelay(interval: number, which: "first" | "next", random: nu
|
|
|
83
90
|
return interval * (1 - LEASE_JITTER + 2 * LEASE_JITTER * random);
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
// What to attach, in workspace vocabulary only (ADR-0012 boundary): the one branch the body works
|
|
94
|
+
// on, the pod's work group, and the review sha. Derived PER RUN from the wrapper's input, which is
|
|
95
|
+
// what keeps it out here rather than in the options — and which is exactly why the two IMAGES and
|
|
96
|
+
// the REPOS are NOT here (ADR-0049, ADR-0051): `jr2 up` must find them by walking the Machine, and
|
|
97
|
+
// no walk can evaluate a function of run input. They are static `workspace()` options instead; a
|
|
98
|
+
// Repo that IS a function of run input is a per-run slot, a mapper the walk can see the shape of
|
|
99
|
+
// even though it cannot see the url.
|
|
100
|
+
/** What the `spec` of a `workspace()` returns: the branch that the Workspace works on, and two
|
|
101
|
+
* optional fields. jr2 calls `spec` with the run input when the run enters the Workspace. */
|
|
93
102
|
export type WorkspaceSpec = {
|
|
103
|
+
/** The branch that the Workspace works on: each Repo Slot gets a Worktree for it, at
|
|
104
|
+
* `/work/<slot>/<branch>`, with each `/` in the branch written as `-`. Required, and not
|
|
105
|
+
* `default`, which is the directory of the clone. Your Machine decides its name. */
|
|
94
106
|
branch: string;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
// The pod's work group (ADR-0005): `fsGroup`. The two writing seats may run different uids —
|
|
108
|
+
// each image's own `USER` decides — and POSIX would then make the other seat's files read-only;
|
|
109
|
+
// fsGroup (group ownership) plus the default ACL the attach stamps on each repo root (group
|
|
110
|
+
// writability, umask-proof) closes that, both inert when the uids already match. The override
|
|
111
|
+
// exists for the image whose sessions already hold a gid of their own — a root sshd's logins
|
|
112
|
+
// rebuild groups from `/etc/group`, so pointing the work group at one they have costs no
|
|
113
|
+
// rebuild. Never a config key — pod composition is the spec's business.
|
|
114
|
+
/** The group id that owns the files in `/work`. With it, the Sandbox Image and the User
|
|
115
|
+
* Container can both write those files when they run as different users. Default: 2000. Set it
|
|
116
|
+
* to a group id that the User Container's sessions already have, for example when a root sshd
|
|
117
|
+
* builds their groups from `/etc/group`. A non-negative integer. */
|
|
102
118
|
workGroup?: number;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
119
|
+
// `<branchDir>-review`, a sibling of the branch worktree, forced to exactly this sha on every
|
|
120
|
+
// attach (ADR-0028). Creation-time seat only; the per-round refresh verb (the sha moves between
|
|
121
|
+
// review rounds) is a later, workflow-driven change.
|
|
122
|
+
/** A commit to check out in a second, detached Worktree beside each branch Worktree, for a
|
|
123
|
+
* reviewer: `/work/<slot>/<branch>-review`. The body gets its paths as `workspace.review`. */
|
|
107
124
|
reviewSha?: string;
|
|
108
125
|
};
|
|
109
126
|
|
|
127
|
+
// A Workspace with no Repo Slot (ADR-0051) attaches nothing, so a branch and a review sha name
|
|
128
|
+
// nothing: `never` refuses them where the spec is written, and `assertSpec` refuses them at run
|
|
129
|
+
// time for the caller the types do not see.
|
|
130
|
+
/** What the `spec` of a `workspace()` with no Repo Slot returns: only `workGroup`. There is no
|
|
131
|
+
* branch and no review Worktree, because the Workspace has no Repo Slot. */
|
|
132
|
+
export type NoSlotSpec = Pick<WorkspaceSpec, "workGroup"> & { branch?: never; reviewSha?: never };
|
|
133
|
+
|
|
134
|
+
// What the workspace hands the BODY (ADR-0012, ADR-0016): worktree geography only, keyed by Repo
|
|
135
|
+
// Slot (ADR-0051) — so a path a prompt names is right in every Instance that consumes the Machine.
|
|
136
|
+
// `endpoint` and `sandbox` are mechanism-internal — the Agent actor resolves them ambiently from
|
|
137
|
+
// the enclosing wrapper (ambient.ts), so a workflow can no longer forget to thread them (the
|
|
138
|
+
// baba71f incident: `sandbox` omitted, every tool call 403'd, fail-closed but silent).
|
|
139
|
+
//
|
|
140
|
+
// `TSlots` has NO default on purpose (ADR-0050): a body names the slots it reads, and
|
|
141
|
+
// `workspace()` holds those names to the ones the wrapper declares. A default of `string` would
|
|
142
|
+
// type `repos` as `Record<string, string>`, under which a misspelled slot reads as a path — the
|
|
143
|
+
// silent widening the slot key exists to refuse. `string` WRITTEN is a different statement: the
|
|
144
|
+
// body names no slot at all — it enumerates them, in the order the composer wrote — which is the
|
|
145
|
+
// body under a wrapper that declares its map Open (`repos: open`, ADR-0051), where the slots are
|
|
146
|
+
// the composer's words and no body could name them.
|
|
110
147
|
/**
|
|
111
|
-
* What the workspace
|
|
112
|
-
*
|
|
113
|
-
* `endpoint` and `sandbox` are mechanism-internal — the Agent actor resolves them ambiently from
|
|
114
|
-
* the enclosing wrapper (ambient.ts), so a workflow can no longer forget to thread them (the
|
|
115
|
-
* baba71f incident: `sandbox` omitted, every tool call 403'd, fail-closed but silent).
|
|
148
|
+
* What the body of a `workspace()` gets as `input.workspace`: the path of each Worktree, and the
|
|
149
|
+
* branch. A path is correct in each Instance that registers the Machine.
|
|
116
150
|
*
|
|
117
|
-
* `TSlots`
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* under a wrapper that declares its map Open (`repos: open`, ADR-0051), where the slots are the
|
|
123
|
-
* composer's words and no body could name them.
|
|
151
|
+
* `TSlots` is the names of the Repo Slots that the body reads. `workspace()` refuses a body that
|
|
152
|
+
* names a slot that the Workspace does not have. Write `string` for a body that names no slot and
|
|
153
|
+
* reads the Worktrees in the order of `repos`, as under `repos: open`. Write `never` for a
|
|
154
|
+
* Workspace with no Repo Slot: its handles are `{ repos: {} }`, with no branch, and its Agents work
|
|
155
|
+
* in `/work`.
|
|
124
156
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
// `[TSlots] extends [never]`, not `TSlots extends never`: a bare `never` would distribute to `never`.
|
|
158
|
+
export type WorkspaceHandles<TSlots extends string> = [TSlots] extends [never]
|
|
159
|
+
? { repos: Record<never, string> }
|
|
160
|
+
: SlotHandles<TSlots>;
|
|
161
|
+
|
|
162
|
+
/** The handles of a Workspace that declares Repo Slots. @internal */
|
|
163
|
+
type SlotHandles<TSlots extends string> = {
|
|
164
|
+
// The kit gives no slot a privileged meaning — there is no `workdir` — because which tree an
|
|
165
|
+
// Agent works in is a fact about that Agent's Turn, not about the Workspace: a body that names
|
|
166
|
+
// its slots frames each Agent with its own, and a body that names none may give the order a
|
|
167
|
+
// meaning of its own (`task`: the first is the one the coder edits). The order is the kit's
|
|
168
|
+
// promise; the meaning is the Machine's (ADR-0051).
|
|
169
|
+
/** The absolute path of each Repo Slot's Worktree, `/work/<slot>/<branch>`, in the order that
|
|
170
|
+
* `repos` declares the slots. Give one to an Agent's Turn as its `cwd`. */
|
|
132
171
|
repos: Record<TSlots, string>;
|
|
172
|
+
/** The branch that the Workspace works on: the `branch` of the spec. */
|
|
133
173
|
branch: string;
|
|
134
|
-
|
|
135
|
-
|
|
174
|
+
// The review seat (ADR-0028).
|
|
175
|
+
/** The path of each Repo Slot's review Worktree. Present only when the spec has `reviewSha`.
|
|
176
|
+
* Give one to a reviewer's Turn as its `cwd`. */
|
|
136
177
|
review?: Partial<Record<TSlots, string>>;
|
|
137
178
|
};
|
|
138
179
|
|
|
180
|
+
// The composition is the whole reason the door is declared on the wrapper and not on the body
|
|
181
|
+
// (ADR-0033): `Workspaced<RunInput, Slot>` is what the body receives, `RunInput` is what a caller
|
|
182
|
+
// may send, and no caller can send `workspace` (the handles do not exist until a Sandbox is
|
|
183
|
+
// provisioned and attached). Naming it here keeps the body from hand-copying
|
|
184
|
+
// {@link WorkspaceHandles}, which drifts. The slot argument is required, never defaulted, so
|
|
185
|
+
// `workspace.repos.<slot>` is typed by the same keys the wrapper declares (ADR-0050, ADR-0051).
|
|
186
|
+
//
|
|
187
|
+
// The wrapper passes its input through UNTOUCHED, so a ROOT-placed wrapper's body also receives
|
|
188
|
+
// what the host injected beside the door — `HostInjectedInput` today (the run's `instanceId`).
|
|
189
|
+
// That is outside this type on purpose: it depends on where the wrapper sits, and `Workspaced` is
|
|
190
|
+
// the composition the WRAPPER makes.
|
|
139
191
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* `
|
|
192
|
+
* The input of a `workspace()` body: the run input, plus `workspace`, the handles that the
|
|
193
|
+
* Workspace adds (`workspace.repos`, `workspace.branch`, `workspace.review`). Use it as the type of
|
|
194
|
+
* the body's input.
|
|
195
|
+
*
|
|
196
|
+
* The first argument is the run input: the `input` schema of the `workspace()`. The second is the
|
|
197
|
+
* Repo Slots that the body reads, by name (`"app"` or `"app" | "docs"`). Under `repos: open`, write
|
|
198
|
+
* `string`: the body cannot know the names, so it reads the slots in order. For a `workspace()`
|
|
199
|
+
* with no `repos`, write `never`. The second argument has no default.
|
|
200
|
+
*
|
|
201
|
+
* A body under a root `workspace()` also gets the fields of `HostInjectedInput`. Add them when the
|
|
202
|
+
* body reads them: `Workspaced<HostInjectedInput & RunInput, "app">`.
|
|
203
|
+
*
|
|
204
|
+
* Its fields are the fields of the run input, and `workspace`. The run input can have no fields:
|
|
205
|
+
* `Workspaced<z.infer<typeof door>, "app">` for the door `z.object({})` holds only `workspace`.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* import { z } from "zod";
|
|
210
|
+
* import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
|
|
211
|
+
*
|
|
212
|
+
* const done = defineEvent({
|
|
213
|
+
* name: "done",
|
|
214
|
+
* description: "Call this when REVIEW.md is written.",
|
|
215
|
+
* input: z.object({}),
|
|
216
|
+
* });
|
|
217
|
+
* const runInput = z.object({ branch: z.string(), sha: z.string() });
|
|
218
|
+
* type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
|
|
219
|
+
*
|
|
220
|
+
* const body = jr2Setup({
|
|
221
|
+
* types: {} as { input: BodyInput; context: BodyInput },
|
|
222
|
+
* events: [done],
|
|
223
|
+
* actors: {
|
|
224
|
+
* reviewer: agent({
|
|
225
|
+
* model: "anthropic/claude-haiku-5-5",
|
|
226
|
+
* instructions: "You review one commit and write REVIEW.md.",
|
|
227
|
+
* workspace: "read",
|
|
228
|
+
* }),
|
|
229
|
+
* fail: fail(),
|
|
230
|
+
* },
|
|
231
|
+
* }).createMachine({
|
|
232
|
+
* id: "review",
|
|
233
|
+
* context: ({ input }) => input,
|
|
234
|
+
* on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
|
|
235
|
+
* initial: "reviewing",
|
|
236
|
+
* states: {
|
|
237
|
+
* reviewing: {
|
|
238
|
+
* invoke: {
|
|
239
|
+
* src: "reviewer",
|
|
240
|
+
* input: ({ context }) => ({
|
|
241
|
+
* prompt: `Review commit ${context.sha} on ${context.workspace.branch}.`,
|
|
242
|
+
* cwd: context.workspace.review?.app ?? context.workspace.repos.app,
|
|
243
|
+
* }),
|
|
244
|
+
* },
|
|
245
|
+
* on: { done: "finished" },
|
|
246
|
+
* },
|
|
247
|
+
* finished: { type: "final" },
|
|
248
|
+
* failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
|
|
249
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
250
|
+
* },
|
|
251
|
+
* });
|
|
252
|
+
*
|
|
253
|
+
* export const machine = workspace(body, {
|
|
254
|
+
* input: runInput,
|
|
255
|
+
* repos: { app: "https://github.com/you/app.git" },
|
|
256
|
+
* spec: ({ input }) => ({ branch: input.branch, reviewSha: input.sha }),
|
|
257
|
+
* });
|
|
258
|
+
* ```
|
|
148
259
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
260
|
+
* @see docs/concepts/workspaces.md
|
|
261
|
+
* @see docs/guides/write-a-workflow.md
|
|
262
|
+
* @see docs/reference/decisions/0012-workspace-wrapper-machine.md
|
|
263
|
+
* @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
|
|
153
264
|
*/
|
|
154
|
-
export type Workspaced<TInput, TSlots extends string> = TInput & {
|
|
265
|
+
export type Workspaced<TInput, TSlots extends string> = WithoutEmptyIndex<TInput> & {
|
|
266
|
+
workspace: WorkspaceHandles<TSlots>;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// `z.infer` of a door with no fields, `z.object({})`, is `Record<string, never>`: an index signature
|
|
270
|
+
// that says each key holds `never`. Intersected with the handles, it makes each assign of a body
|
|
271
|
+
// context fail (`n: number` is not `never`) with a diagnostic that does not name the cause. This
|
|
272
|
+
// removes that index signature, and only one whose value is `never`, so the named fields stay
|
|
273
|
+
// (`HostInjectedInput & Record<string, never>` keeps `instanceId: string`) and an input that is a
|
|
274
|
+
// real map (`Record<string, string>`) is not changed. The alternative, a separate type for the empty
|
|
275
|
+
// door, makes the author choose between two names for one statement.
|
|
276
|
+
// It is not `@internal`: the Reference then shows this TSDoc, so a reader of `Workspaced` sees that
|
|
277
|
+
// these are the fields of the run input. As plumbing, its page showed only the declaration.
|
|
278
|
+
/**
|
|
279
|
+
* The fields of the run input `T`. A string index signature whose value is `never` is not one of
|
|
280
|
+
* them: `z.infer` gives `Record<string, never>` for `z.object({})`, and this type makes it `{}`. The
|
|
281
|
+
* named fields of `T` stay. A different `T` does not change.
|
|
282
|
+
*/
|
|
283
|
+
type WithoutEmptyIndex<T> = string extends keyof T
|
|
284
|
+
? [T[string & keyof T]] extends [never]
|
|
285
|
+
? { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] }
|
|
286
|
+
: T
|
|
287
|
+
: T;
|
|
155
288
|
|
|
156
289
|
/**
|
|
157
290
|
* What the watch says about a workspace (ADR-0021, ADR-0063): gone, or present — and, once the
|
|
@@ -277,8 +410,10 @@ export interface SandboxPort {
|
|
|
277
410
|
* `OOMKilled` at or after `since`, the kill — its fault reason, starting with the fixed prefix
|
|
278
411
|
* `memory limit`, and the data the next Turn's notice is made of (ADR-0062) — else undefined.
|
|
279
412
|
* Asked when a Turn faults on a lost conversation; it may wait a moment for the operator's word,
|
|
280
|
-
* which can trail the Harness's restart.
|
|
281
|
-
*
|
|
413
|
+
* which can trail the Harness's restart. Asked too before an admission into the Workspace whose
|
|
414
|
+
* watch shows a last end `OOMKilled`, with `since` that end: a kill while no Turn ran (ADR-0062).
|
|
415
|
+
* A backend that cannot see the container answers undefined, and every lost conversation stays
|
|
416
|
+
* lost — never an optional method (ADR-0021).
|
|
282
417
|
*/
|
|
283
418
|
memoryFault(name: string, since: Date): Promise<MemoryKill | undefined>;
|
|
284
419
|
/** Delete the Sandbox CR. Absent is success. */
|
|
@@ -353,7 +488,7 @@ type WsContext = {
|
|
|
353
488
|
/** This invocation's stable identity (the actor id the parent invoked/spawned us under). */
|
|
354
489
|
wsId: string;
|
|
355
490
|
/** The resolved spec — computed once from input and persisted, like any other context data. */
|
|
356
|
-
spec:
|
|
491
|
+
spec: AnySpec;
|
|
357
492
|
/** Every slot's resolved Binding, by slot, in declaration order — assigned at provision. */
|
|
358
493
|
bindings?: Record<string, ResolvedBinding>;
|
|
359
494
|
endpoint?: string;
|
|
@@ -363,6 +498,9 @@ type WsContext = {
|
|
|
363
498
|
/** Persisted in context so the registrar can re-publish them on restore. */
|
|
364
499
|
handles?: MechanismHandles;
|
|
365
500
|
output?: unknown;
|
|
501
|
+
/** The body's Failure (ADR-0073), as plain data: kept while `failing` destroys, so a restore
|
|
502
|
+
* there sends the same Failure up. */
|
|
503
|
+
failure?: FailureInput;
|
|
366
504
|
};
|
|
367
505
|
|
|
368
506
|
/**
|
|
@@ -383,12 +521,14 @@ type WsContext = {
|
|
|
383
521
|
* Repo Slots it carries, because a default would widen exactly where the phantom exists to refuse
|
|
384
522
|
* — `JR2Repos<string>` offers `customize()` every key, and `AnyStateMachine` as the body offers no
|
|
385
523
|
* Agent at all. `PoolMachine` names its worker the same way.
|
|
524
|
+
*
|
|
525
|
+
* @internal
|
|
386
526
|
*/
|
|
387
527
|
export type WorkspaceMachine<TInput, TOutput, TBody extends AnyStateMachine, TSlots extends string> = StateMachine<
|
|
388
528
|
any,
|
|
389
529
|
any,
|
|
390
530
|
any,
|
|
391
|
-
WrapperActors<"body", TBody, "place" | "provision" | "attach" | "registrar" | "lease" | "destroy">,
|
|
531
|
+
WrapperActors<"body", TBody, "place" | "provision" | "attach" | "registrar" | "lease" | "destroy" | "fail">,
|
|
392
532
|
any,
|
|
393
533
|
any,
|
|
394
534
|
any,
|
|
@@ -435,8 +575,15 @@ export type WorkspaceMachine<TInput, TOutput, TBody extends AnyStateMachine, TSl
|
|
|
435
575
|
*/
|
|
436
576
|
// The handles are keyed by the wrapper's DECLARED slots (ADR-0051), so a body demanding a slot the
|
|
437
577
|
// wrapper never declared is refused here too; demanding fewer is fine, as with any other field.
|
|
578
|
+
//
|
|
579
|
+
// The provided side holds the raw door AND its `Workspaced` form. `Workspaced` removes the
|
|
580
|
+
// `Record<string, never>` that `z.infer` gives the empty door, so `Workspaced` alone no longer
|
|
581
|
+
// satisfies a body that writes that type itself (`z.infer<typeof door>`, alone or with the
|
|
582
|
+
// handles). The raw door keeps those bodies legal, and the stripped form keeps a body that
|
|
583
|
+
// declares `Workspaced<…>` legal. A key that the door does not carry is still refused.
|
|
584
|
+
/** @internal */
|
|
438
585
|
type BodyAcceptsDoor<TBody extends AnyStateMachine, TDoor, TSlots extends string> =
|
|
439
|
-
Workspaced<TDoor, TSlots> & HostInjectedInput extends InputFrom<TBody>
|
|
586
|
+
TDoor & Workspaced<TDoor, TSlots> & HostInjectedInput extends InputFrom<TBody>
|
|
440
587
|
? BodyNamesNoSlotUnderOpenMap<TBody, TSlots>
|
|
441
588
|
: { "the body's declared input must accept the door plus the injected handles": Workspaced<TDoor, TSlots> };
|
|
442
589
|
|
|
@@ -467,6 +614,8 @@ type BodyNamesNoSlotUnderOpenMap<TBody extends AnyStateMachine, TSlots extends s
|
|
|
467
614
|
* is the same one-direction assignability, on the `workspace` field alone: the handles the wrapper
|
|
468
615
|
* will inject must satisfy what the body declares for them, so a body that names fewer slots, or
|
|
469
616
|
* no handles at all, is fed a superset and passes, as ever.
|
|
617
|
+
*
|
|
618
|
+
* @internal
|
|
470
619
|
*/
|
|
471
620
|
type BodyAcceptsSlots<TBody extends AnyStateMachine, TSlots extends string> =
|
|
472
621
|
InputFrom<TBody> extends { workspace: infer THandles }
|
|
@@ -475,132 +624,426 @@ type BodyAcceptsSlots<TBody extends AnyStateMachine, TSlots extends string> =
|
|
|
475
624
|
: { "the body's declared handles must accept the Repo Slots the wrapper declares": WorkspaceHandles<TSlots> }
|
|
476
625
|
: unknown;
|
|
477
626
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
*/
|
|
627
|
+
// What the Sandbox is MADE OF (ADR-0037, ADR-0005) and which Repos it attaches (ADR-0051), as
|
|
628
|
+
// STATIC options on the wrapper rather than fields of the per-run spec (ADR-0049). Static is the
|
|
629
|
+
// whole point: `jr2 up` walks the registered Machines to find every `file:` context and build it,
|
|
630
|
+
// every bound Repo and warm it, every open slot and refuse it (parts.ts) — and a spec is a function
|
|
631
|
+
// of run input that no walk can evaluate. They are also never persisted — the provisioning state
|
|
632
|
+
// re-reads them off the Machine it was invoked as, so a restore, a `provide()` and a `customize()`
|
|
633
|
+
// all get the image and the slots the Machine carries NOW.
|
|
634
|
+
//
|
|
635
|
+
// Each image is one string in ADR-0037's two shapes: a `file:` URL to a docker context the
|
|
636
|
+
// Machine's module ships (`import.meta.resolve("./image")`), or a registry ref its owner baked and
|
|
637
|
+
// hosts.
|
|
638
|
+
//
|
|
639
|
+
// `TSlots` has no default, here and on the two option types below (ADR-0050): a value annotated
|
|
640
|
+
// with a `string` slot set types `repos` as `Record<string, RepoSlot>`, under which a body naming
|
|
641
|
+
// any slot passes the wrapper's check — the widening WorkspaceHandles refuses for the
|
|
642
|
+
// same reason. The `workspace()` overloads infer it; an annotation names it.
|
|
643
|
+
/** The options of a `workspace()` that say what its Sandbox is made of and which Repos it
|
|
644
|
+
* clones. They are the same for each run. */
|
|
496
645
|
export type SandboxOptions<TSlots extends string, TInput = unknown> = {
|
|
497
|
-
/** The Sandbox Image
|
|
646
|
+
/** The Sandbox Image: the image of the container where the Agent's Working tools and
|
|
647
|
+
* `command` Effects run. A `file:` URL to a docker context that your module ships
|
|
648
|
+
* (`import.meta.resolve("./image")`), which `jr2 up` builds, or a registry ref. Its floor is
|
|
649
|
+
* glibc and git; jr2 adds its own runtime at `/opt/jr2`. Default: the Instance's
|
|
650
|
+
* `images/default`, then jr2's Harness image. */
|
|
498
651
|
image?: string;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
*
|
|
652
|
+
// The widening ADR-0005 foresaw: `{ image, resources }` states the seat's split of the Size
|
|
653
|
+
// (ADR-0060). There is no default, because the seat's whole identity is "what jr2 does not own"
|
|
654
|
+
// and jr2 has nothing to put there. Env and ports are deliberately not forwarded.
|
|
655
|
+
/** The User Container: a third container in the Sandbox, for a service that must run beside the
|
|
656
|
+
* Agent, such as an sshd. Its image, in the same two forms as `image`, or `{ image, resources }`
|
|
657
|
+
* to give it a part of the Size. It runs its own entrypoint, with `/work` mounted. jr2 adds
|
|
658
|
+
* nothing to it. Default: none. Without `resources`, it has no limit of its own and shares the
|
|
659
|
+
* Size of the Sandbox. */
|
|
504
660
|
user?: string | UserContainer;
|
|
661
|
+
// Agent Substrate's template size (ADR-0060), so the Workspace moves there unchanged. A fact of
|
|
662
|
+
// the Machine, like its image: the author knows the toolchain, the runner, the worker count.
|
|
505
663
|
/**
|
|
506
|
-
* The Size
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
* a
|
|
511
|
-
*
|
|
664
|
+
* The Size: the memory and cpu limit of the whole Sandbox, `{ limits: { memory, cpu } }`, such as
|
|
665
|
+
* `{ limits: { memory: "4Gi", cpu: "2" } }`. jr2 reserves all of it on the node: each request
|
|
666
|
+
* equals its limit. jr2 also enforces the cpu limit. Only `limits`,
|
|
667
|
+
* and in it only `memory` and `cpu`, are accepted. Default: the Instance's `sandbox.resources`,
|
|
668
|
+
* then 2Gi and 1 cpu. Under a Size that you give, a field you leave out takes the kit's value,
|
|
669
|
+
* not the Instance's. The Instance that registers the Machine can change it with
|
|
670
|
+
* `customize(machine, { resources })`.
|
|
512
671
|
*/
|
|
513
672
|
resources?: Size;
|
|
673
|
+
// The handles keep this map's order, and the kit reads nothing into it (ADR-0051). Required
|
|
674
|
+
// here, at least one; a Workspace with no Repo Slot leaves it out, which is the
|
|
675
|
+
// `NoSlotWorkspaceOptions` overload, not an empty map. `repos: open` is the shape a packaged
|
|
676
|
+
// Machine takes when its body enumerates its checkouts rather than naming them
|
|
677
|
+
// (`@jr2/machines`'s `task`, whose first slot is the one the coder edits).
|
|
514
678
|
/**
|
|
515
|
-
* The Repo Slots
|
|
516
|
-
* `workspace.repos`
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
679
|
+
* The Repo Slots: the repositories that the Sandbox clones, each under a name of your Machine's
|
|
680
|
+
* choice. The name is the key of `workspace.repos` in the body and the directory under `/work`.
|
|
681
|
+
* At least one slot. Leave `repos` out for a Workspace with no Repo Slot (see
|
|
682
|
+
* {@link NoSlotWorkspaceOptions}). Each value is one of:
|
|
683
|
+
* - a url, or `{ url, ref }`, where `ref` is the branch or commit that the Worktree starts from
|
|
684
|
+
* (default: the Repo's default branch);
|
|
685
|
+
* - `open`, for the Instance that registers the Machine to bind with `customize()`;
|
|
686
|
+
* - a function of the run input, `({ input }) => url` or `{ url, ref }`, when each run chooses.
|
|
520
687
|
*
|
|
521
|
-
* Or
|
|
522
|
-
* one with `customize
|
|
523
|
-
* Machine takes when its body enumerates its checkouts rather than naming them (`@jr2/machines`'s
|
|
524
|
-
* `task`, whose first slot is the one the coder edits). Under it `TSlots` is `string`, and the body's handles must say so.
|
|
688
|
+
* Or `repos: open` for the whole map: the Machine names no slot, and the Instance that registers
|
|
689
|
+
* it names each one with `customize()`. The body's input is then `Workspaced<RunInput, string>`.
|
|
525
690
|
*/
|
|
526
691
|
repos: Record<TSlots, RepoSlot<TInput>> | typeof open;
|
|
692
|
+
// Naming is placement (ADR-0059): a pod gets the Held secrets something in it names — this
|
|
693
|
+
// list, and the model keys of the Agents whose Turns run here, which their providers claim
|
|
694
|
+
// without being listed.
|
|
527
695
|
/**
|
|
528
|
-
* The Held secrets this
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
696
|
+
* The Held secrets that this Sandbox can use: names from the `secrets` list in `jr2.config.ts`.
|
|
697
|
+
* The Sandbox's Custodian holds each value and puts it into requests to the hosts of that Held
|
|
698
|
+
* secret. The Agent sees only a Stand-in. You need not list the model key of an Agent that runs
|
|
699
|
+
* here: its provider claims it. Each name once.
|
|
532
700
|
*/
|
|
533
701
|
secrets?: readonly string[];
|
|
702
|
+
// The container exists only when an Effect below this `workspace()` is placed there (ADR-0068,
|
|
703
|
+
// ADR-0005). It runs as its own `USER`.
|
|
534
704
|
/**
|
|
535
|
-
* The Sandbox Effector
|
|
536
|
-
* `push`,
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
705
|
+
* The image of the Sandbox Effector. This container runs three kinds of Effect for this
|
|
706
|
+
* Workspace: a `push`, an `http` Effect with a Worktree file as its body, and a `command` with an
|
|
707
|
+
* Effect secret in its `env`. It gets the Effect secrets that those Effects name. Give an image
|
|
708
|
+
* when those Effects need tools or credentials that you keep out of the Sandbox Image, such as
|
|
709
|
+
* `terraform`. A `file:` URL or a registry ref, as for `image`. Default: the Sandbox Image,
|
|
710
|
+
* which already holds `git`.
|
|
541
711
|
*/
|
|
712
|
+
// The default is the Sandbox Image, whose floor already holds `git` (ADR-0037).
|
|
542
713
|
effector?: { image: string };
|
|
543
714
|
};
|
|
544
715
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
*/
|
|
716
|
+
// How a Workspace with a declared door is configured (ADR-0012, ADR-0033) — the wrapper's own
|
|
717
|
+
// run-input schema, what the pod is made of, and the mapping from what comes through the door to
|
|
718
|
+
// workspace vocabulary.
|
|
719
|
+
/** The options of a `workspace()` that declares the input that starts a run. */
|
|
550
720
|
export type WorkspaceOptions<TSchema extends z.ZodObject, TSlots extends string> = SandboxOptions<
|
|
551
721
|
TSlots,
|
|
552
722
|
z.infer<TSchema>
|
|
553
723
|
> & {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
*
|
|
724
|
+
// Deliberately NOT the body's schema (ADR-0033): the body is fed the run input PLUS the injected
|
|
725
|
+
// `workspace` handles ({@link Workspaced}), which no caller can send, so the body's contract is
|
|
726
|
+
// the door plus something that does not exist yet. Symmetric with `PoolSpec.input`.
|
|
727
|
+
/** The input that starts a run, as a `z.object`. jr2 checks each start against it. It also types
|
|
728
|
+
* the `input` of `spec` and of a per-run Repo Slot, and the body must accept it plus the
|
|
729
|
+
* `workspace` handles. Write it here, not on the body. */
|
|
559
730
|
input: TSchema;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
731
|
+
// ADR-0012's boundary: workflow configuration never enters the spec.
|
|
732
|
+
/**
|
|
733
|
+
* A function from the run input to the Workspace's {@link WorkspaceSpec}: the `branch` it works
|
|
734
|
+
* on, and optionally `reviewSha` and `workGroup`. jr2 calls it when the run enters the
|
|
735
|
+
* Workspace.
|
|
736
|
+
*
|
|
737
|
+
* The input of `spec` also holds `instanceId`, the seed Instance ID of the run (see
|
|
738
|
+
* {@link HostInjectedInput}). Use it to give each run a branch of its own:
|
|
739
|
+
* ``spec: ({ input }) => ({ branch: `fix/${input.instanceId}` })``. Only a root `workspace()` gets
|
|
740
|
+
* it from jr2. A `workspace()` that a different Machine invokes gets only the input that its
|
|
741
|
+
* parent gives it, so that parent must give `instanceId` when `spec` reads it.
|
|
742
|
+
*/
|
|
743
|
+
// The type says `instanceId` is present also below the root, where only the parent can supply it:
|
|
744
|
+
// the same permissive answer the body guard gives (BodyAcceptsDoor), because no type can see where
|
|
745
|
+
// the wrapper sits. The alternative, `Partial<HostInjectedInput>`, makes each root `spec` that reads
|
|
746
|
+
// it write a fallback for a value that is always there.
|
|
747
|
+
spec: (args: { input: WithoutEmptyIndex<z.infer<TSchema>> & HostInjectedInput }) => WorkspaceSpec;
|
|
563
748
|
};
|
|
564
749
|
|
|
750
|
+
// How a Workspace with NO declared door is configured: absence is permissive (ADR-0033), so jr2
|
|
751
|
+
// has nothing to infer from and says `unknown` rather than `any` — an honest "jr2 does not know",
|
|
752
|
+
// which the mapper must narrow before it reads a field.
|
|
565
753
|
/**
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
754
|
+
* The options of a `workspace()` with no `input`: it starts with any input. The `input` of `spec`
|
|
755
|
+
* is then `unknown`, and `spec` must check it before it reads a field.
|
|
756
|
+
*
|
|
757
|
+
* Use it for a Workspace that a Pool or another Machine starts. Give the parameter of `spec` a
|
|
758
|
+
* type to say what that input is: `spec: ({ input }: { input: Item }) => …`. That also types the
|
|
759
|
+
* input of the Workspace. For a Workspace that a caller starts, declare `input`.
|
|
572
760
|
*/
|
|
573
761
|
export type PermissiveWorkspaceOptions<TSlots extends string, TInput = unknown> = SandboxOptions<TSlots, TInput> & {
|
|
574
|
-
|
|
575
|
-
|
|
762
|
+
// Spelled out so a declared schema can never fall through to the permissive overload, where the
|
|
763
|
+
// body's door would go unchecked.
|
|
764
|
+
/** Not present: a `workspace()` without `input` starts with any input. */
|
|
576
765
|
input?: never;
|
|
766
|
+
/** A function from the input to the Workspace's {@link WorkspaceSpec}. Its `input` is
|
|
767
|
+
* `unknown` unless you give the parameter a type: `spec: ({ input }: { input: Item }) => …`. */
|
|
577
768
|
spec: (args: { input: TInput }) => WorkspaceSpec;
|
|
578
769
|
};
|
|
579
770
|
|
|
771
|
+
// A Workspace with no Repo Slot (ADR-0051): a Sandbox for one unit of work whose Agents and
|
|
772
|
+
// Effects share `/work`, and no Worktree is in it — the steps of one job. Its own option types, not a
|
|
773
|
+
// conditional on `TSlots`, because the slot overloads infer `TSlots` from `repos`, and no
|
|
774
|
+
// inference reaches through a conditional type.
|
|
775
|
+
/** The options of a `workspace()` with no Repo Slot that declares the input that starts a run.
|
|
776
|
+
* `repos` is left out, and `spec` is optional. */
|
|
777
|
+
export type NoSlotWorkspaceOptions<TSchema extends z.ZodObject> = Omit<SandboxOptions<never>, "repos"> & {
|
|
778
|
+
/** Not present: the Workspace has no Repo Slot. Its Agents work in `/work`. */
|
|
779
|
+
repos?: never;
|
|
780
|
+
/** The input that starts a run, as a `z.object`, as in {@link WorkspaceOptions}. */
|
|
781
|
+
input: TSchema;
|
|
782
|
+
/** A function from the run input to the Workspace's {@link NoSlotSpec}: only `workGroup`.
|
|
783
|
+
* Default: no `workGroup`. */
|
|
784
|
+
spec?: (args: { input: WithoutEmptyIndex<z.infer<TSchema>> & HostInjectedInput }) => NoSlotSpec;
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
/** The options of a `workspace()` with no Repo Slot and no `input`: it starts with any input. */
|
|
788
|
+
export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOptions<never>, "repos"> & {
|
|
789
|
+
/** Not present: the Workspace has no Repo Slot. Its Agents work in `/work`. */
|
|
790
|
+
repos?: never;
|
|
791
|
+
/** Not present: a `workspace()` without `input` starts with any input. */
|
|
792
|
+
input?: never;
|
|
793
|
+
/** A function from the input to the Workspace's {@link NoSlotSpec}: only `workGroup`. Default:
|
|
794
|
+
* no `workGroup`. */
|
|
795
|
+
spec?: (args: { input: TInput }) => NoSlotSpec;
|
|
796
|
+
};
|
|
797
|
+
|
|
580
798
|
/**
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
799
|
+
* Run a body Machine in a Sandbox: a pod of its own, with a Worktree of each Repo Slot on one
|
|
800
|
+
* branch. The result is a Machine. Register it as a Workflow, or invoke it from another Machine as
|
|
801
|
+
* an actor slot.
|
|
802
|
+
*
|
|
803
|
+
* When a run enters it, the Workspace makes the Sandbox and waits for room on a node, with no
|
|
804
|
+
* deadline. It clones each Repo Slot and makes its Worktree on the `branch` that `spec` gives. Then
|
|
805
|
+
* it runs the body with the run input plus `workspace`, the handles (see {@link Workspaced}). The
|
|
806
|
+
* Agents with Working tools and the `command` Effects in the body run in that Sandbox. An Agent
|
|
807
|
+
* with `workspace: "none"` runs on the Instance Harness. When the body reaches a final state, the
|
|
808
|
+
* Workspace deletes the Sandbox, and its output is the body's output. Push the branch before
|
|
809
|
+
* then: jr2 deletes the Sandbox's commits with it.
|
|
810
|
+
*
|
|
811
|
+
* Leave out `repos` for a Workspace with no Repo Slot: its Agents and `command` Effects share
|
|
812
|
+
* `/work`, as the steps of one job share a folder, and `spec` is optional and gives only
|
|
813
|
+
* `workGroup`. The body's input is then `Workspaced<RunInput, never>`. An `http` Effect sends a
|
|
814
|
+
* file of `/work` with `file(path)`. A `push`, and a `command` with an Effect secret in its `env`,
|
|
815
|
+
* need a Repo Slot, so jr2 refuses them there. jr2 deletes `/work` with the Sandbox: send what
|
|
816
|
+
* must stay before the body ends.
|
|
817
|
+
*
|
|
818
|
+
* Give each run a different branch, so that two runs do not push to one branch. Make it from a
|
|
819
|
+
* field of the run input, or from `instanceId` when the run input has no field that is different
|
|
820
|
+
* for each run: ``spec: ({ input }) => ({ branch: `fix/${input.instanceId}` })``. The input of
|
|
821
|
+
* `spec` holds `instanceId` with no cast, also for the door `z.object({})`. Only a root
|
|
822
|
+
* `workspace()` gets `instanceId` from jr2. A `workspace()` that a different Machine invokes gets
|
|
823
|
+
* it only when its parent gives it.
|
|
824
|
+
*
|
|
825
|
+
* The Sandbox is Lost when its pod is evicted or its node goes. Then the body gets the event
|
|
826
|
+
* `workspace.lost`, and the body decides what to do. jr2 does not make the Lost Sandbox again.
|
|
827
|
+
*
|
|
828
|
+
* When the body fails (it invokes a `fail()` slot), the Workspace deletes the Sandbox, and then the
|
|
829
|
+
* same Failure goes up to the parent. With no parent to catch it, the run ends `failed`. When the
|
|
830
|
+
* body has a defect (an error that is not a Failure), the run faults: it ends `error`, and the
|
|
831
|
+
* Sandbox stays for you to inspect, until jr2's operator removes it after an idle timeout.
|
|
832
|
+
*
|
|
833
|
+
* Give the run input to `input`, not to the body: a body with an `input` schema of its own is
|
|
834
|
+
* refused. The types refuse a body whose input does not accept the run input plus the handles,
|
|
835
|
+
* with the message `the body's declared input must accept the door plus the injected handles`.
|
|
836
|
+
*
|
|
837
|
+
* It refuses, when you call it:
|
|
838
|
+
* - a body with a state that is not final, when that state and each state above it have no
|
|
839
|
+
* `workspace.lost` route: ``workspace(): the body "<id>" has a state "<state>" that is not final,
|
|
840
|
+
* and no route for "workspace.lost" is on it or on a state above — jr2 sends workspace.lost to the
|
|
841
|
+
* body when its Sandbox is gone, and with no route the run faults: route it on the body's root,
|
|
842
|
+
* as on: { "workspace.lost": "<the state that handles it>" } (ADR-0074)``
|
|
843
|
+
* - a body with its own `input`: ``workspace(): the body "<id>" declares its own run input, which
|
|
844
|
+
* nothing will ever serve or enforce — the wrapper feeds the body the run input PLUS the injected
|
|
845
|
+
* `workspace` handles, so the body's schema is not the door. Move it to the wrapper:
|
|
846
|
+
* workspace(body, { input, spec }) (ADR-0033).``
|
|
847
|
+
* - an empty `image`: ``workspace(): `image` must be a non-empty string (got <value>) — either a
|
|
848
|
+
* `file:` URL to a docker context this module ships (`import.meta.resolve("./image")`) or a
|
|
849
|
+
* registry ref (ADR-0037).``
|
|
850
|
+
* - an empty `repos`: ``workspace(): `repos: {}` names no Repo Slot — leave `repos` out for a
|
|
851
|
+
* Workspace with no Repo Slot, or name a slot: `repos: { app: "https://…" }` (ADR-0051).``
|
|
852
|
+
* - a `repos` that is not a map or `open`: ``workspace(): `repos` must be a map of Repo Slots —
|
|
853
|
+
* `repos: { app: "https://…" }` — or `open` whole, for a map the consumer names; leave it out
|
|
854
|
+
* for a Workspace with no Repo Slot (ADR-0051).``
|
|
855
|
+
* - a bad Repo Slot name or value (see {@link RepoSlot})
|
|
856
|
+
* - a `spec` that is not a function, or no `spec` on a Workspace with Repo Slots: ``workspace():
|
|
857
|
+
* `spec` must be a function from the run input to the spec — spec: ({ input }) => ({ branch }) —
|
|
858
|
+
* and only a Workspace with no Repo Slot may leave it out (ADR-0051).``
|
|
859
|
+
* - a bad `secrets` list: ``workspace(): `secrets` must be a list of distinct names from
|
|
860
|
+
* jr2.config.ts's `secrets` list (got <value>) — secrets: ["JIRA_TOKEN"] (ADR-0059).``
|
|
861
|
+
* - a bad `effector`: ``workspace(): `effector` is { image } — a `file:` URL to a docker context
|
|
862
|
+
* or a registry ref (got <value>) — effector: { image: import.meta.resolve("./effector") }
|
|
863
|
+
* (ADR-0068).``
|
|
864
|
+
* - a `user` that is not an image or `{ image, resources? }`: ``workspace(): `user` must be a
|
|
865
|
+
* non-empty string (an image), or { image, resources? } (got <value>) — either a `file:` URL to a
|
|
866
|
+
* docker context this module ships (`import.meta.resolve("./image")`) or a registry ref
|
|
867
|
+
* (ADR-0037, ADR-0060).``, or ``workspace(): user.<key> is not accepted — the User Container is {
|
|
868
|
+
* image, resources? } (ADR-0005, ADR-0060)`` (see {@link UserContainer})
|
|
869
|
+
* - a `resources` key other than `limits` or `requests`: ``workspace(): resources.<key> is not accepted — a
|
|
870
|
+
* Size is { limits: { memory?, cpu? } } and nothing else — Agent Substrate's template size
|
|
871
|
+
* (ADR-0060)``, or the same text for `resources.limits.<key>`. The other texts are in
|
|
872
|
+
* {@link Size}.
|
|
873
|
+
*
|
|
874
|
+
* And when a run enters it, a spec without a `branch` (or with `branch: "default"`), or with a bad
|
|
875
|
+
* `reviewSha` or `workGroup`: ``workspace spec invalid: <fields> — the spec derives from run input;
|
|
876
|
+
* does `jr2 run --input` carry every field this workflow's workspace() mapping reads?``. With no
|
|
877
|
+
* Repo Slot, a spec with a `branch` or a `reviewSha`: ``workspace spec invalid: <field> (the
|
|
878
|
+
* Workspace has no Repo Slot, so it has no branch and no review Worktree; its spec gives only
|
|
879
|
+
* workGroup) …``
|
|
880
|
+
*
|
|
881
|
+
* @param body - The Machine to run in the Sandbox. Its input is `Workspaced<RunInput, Slots>`.
|
|
882
|
+
* @param options - What the Sandbox is made of (`image`, `user`, `resources`, `effector`), the
|
|
883
|
+
* Repo Slots it clones (`repos`), the Held secrets it can use (`secrets`), the run input
|
|
884
|
+
* (`input`), and the function from the run input to the branch (`spec`).
|
|
885
|
+
* @returns A Machine whose input is the run input and whose output is the body's output.
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```ts
|
|
889
|
+
* import { z } from "zod";
|
|
890
|
+
* import {
|
|
891
|
+
* command,
|
|
892
|
+
* fail,
|
|
893
|
+
* jr2Setup,
|
|
894
|
+
* workspace,
|
|
895
|
+
* type CommandOutput,
|
|
896
|
+
* type Workspaced,
|
|
897
|
+
* } from "@jr2/orchestrator";
|
|
898
|
+
*
|
|
899
|
+
* const runInput = z.object({ ticket: z.string(), repo: z.string() });
|
|
900
|
+
* type BodyInput = Workspaced<z.infer<typeof runInput>, "app" | "docs">;
|
|
901
|
+
*
|
|
902
|
+
* const passed = (output: CommandOutput): boolean =>
|
|
903
|
+
* output.outcome === "exited" && output.code === 0;
|
|
904
|
+
*
|
|
905
|
+
* const body = jr2Setup({
|
|
906
|
+
* types: {} as { input: BodyInput },
|
|
907
|
+
* actors: {
|
|
908
|
+
* build: command(["npm", "run", "build"], { slot: "app" }),
|
|
909
|
+
* fail: fail(),
|
|
910
|
+
* },
|
|
911
|
+
* }).createMachine({
|
|
912
|
+
* id: "build",
|
|
913
|
+
* on: { "workspace.lost": ".lost" },
|
|
914
|
+
* initial: "building",
|
|
915
|
+
* states: {
|
|
916
|
+
* building: {
|
|
917
|
+
* invoke: {
|
|
918
|
+
* src: "build",
|
|
919
|
+
* onDone: [
|
|
920
|
+
* { guard: ({ event }) => passed(event.output), target: "built" },
|
|
921
|
+
* { target: "broken" },
|
|
922
|
+
* ],
|
|
923
|
+
* },
|
|
924
|
+
* },
|
|
925
|
+
* built: { type: "final" },
|
|
926
|
+
* broken: { invoke: { src: "fail", input: { reason: "npm run build did not pass" } } },
|
|
927
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
928
|
+
* },
|
|
929
|
+
* });
|
|
930
|
+
*
|
|
931
|
+
* export const machine = workspace(body, {
|
|
932
|
+
* input: runInput,
|
|
933
|
+
* image: "docker.io/library/node:24",
|
|
934
|
+
* resources: { limits: { memory: "4Gi", cpu: "2" } },
|
|
935
|
+
* repos: {
|
|
936
|
+
* app: ({ input }) => input.repo,
|
|
937
|
+
* docs: { url: "https://github.com/you/docs.git", ref: "main" },
|
|
938
|
+
* },
|
|
939
|
+
* spec: ({ input }) => ({ branch: `fix/${input.ticket}` }),
|
|
940
|
+
* });
|
|
941
|
+
* ```
|
|
942
|
+
*
|
|
943
|
+
* @example
|
|
944
|
+
* ```ts
|
|
945
|
+
* import { z } from "zod";
|
|
946
|
+
* import {
|
|
947
|
+
* command,
|
|
948
|
+
* fail,
|
|
949
|
+
* jr2Setup,
|
|
950
|
+
* workspace,
|
|
951
|
+
* type CommandOutput,
|
|
952
|
+
* type Workspaced,
|
|
953
|
+
* } from "@jr2/orchestrator";
|
|
954
|
+
*
|
|
955
|
+
* const passed = (output: CommandOutput): boolean =>
|
|
956
|
+
* output.outcome === "exited" && output.code === 0;
|
|
957
|
+
*
|
|
958
|
+
* // The run input has no field that is different for each run, so the branch comes from
|
|
959
|
+
* // `instanceId`.
|
|
960
|
+
* const runInput = z.object({});
|
|
961
|
+
*
|
|
962
|
+
* const body = jr2Setup({
|
|
963
|
+
* types: {} as { input: Workspaced<z.infer<typeof runInput>, "app"> },
|
|
964
|
+
* actors: { test: command(["npm", "test"], { slot: "app" }), fail: fail() },
|
|
965
|
+
* }).createMachine({
|
|
966
|
+
* id: "nightly",
|
|
967
|
+
* on: { "workspace.lost": ".lost" },
|
|
968
|
+
* initial: "testing",
|
|
969
|
+
* states: {
|
|
970
|
+
* testing: {
|
|
971
|
+
* invoke: {
|
|
972
|
+
* src: "test",
|
|
973
|
+
* onDone: [
|
|
974
|
+
* { guard: ({ event }) => passed(event.output), target: "tested" },
|
|
975
|
+
* { target: "red" },
|
|
976
|
+
* ],
|
|
977
|
+
* },
|
|
978
|
+
* },
|
|
979
|
+
* tested: { type: "final" },
|
|
980
|
+
* red: { invoke: { src: "fail", input: { reason: "npm test did not pass" } } },
|
|
981
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
982
|
+
* },
|
|
983
|
+
* });
|
|
984
|
+
*
|
|
985
|
+
* export const machine = workspace(body, {
|
|
986
|
+
* input: runInput,
|
|
987
|
+
* image: "docker.io/library/node:24",
|
|
988
|
+
* repos: { app: "https://github.com/you/app.git" },
|
|
989
|
+
* spec: ({ input }) => ({ branch: `fix/${input.instanceId}` }),
|
|
990
|
+
* });
|
|
991
|
+
* ```
|
|
992
|
+
*
|
|
993
|
+
* @see docs/concepts/workspaces.md
|
|
994
|
+
* @see docs/guides/write-a-workflow.md
|
|
995
|
+
* @see docs/guides/size-a-workspace.md
|
|
996
|
+
* @see docs/guides/add-tools-to-the-sandbox.md
|
|
997
|
+
* @see docs/reference/decisions/0012-workspace-wrapper-machine.md
|
|
998
|
+
* @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
|
|
587
999
|
*/
|
|
588
|
-
|
|
1000
|
+
// `TSlots` defaults to `never`: with `repos` left out there is nothing to infer it from, and the
|
|
1001
|
+
// Workspace has no Repo Slot (ADR-0051). The no-slot options are a union member, not overloads of
|
|
1002
|
+
// their own, so a call with slots reports its error where it always did.
|
|
1003
|
+
export function workspace<TSchema extends z.ZodObject, TBody extends AnyStateMachine, TSlots extends string = never>(
|
|
589
1004
|
body: TBody & BodyAcceptsDoor<TBody, z.infer<TSchema>, TSlots>,
|
|
590
|
-
options: WorkspaceOptions<TSchema, TSlots>,
|
|
591
|
-
|
|
592
|
-
|
|
1005
|
+
options: WorkspaceOptions<TSchema, TSlots> | NoSlotWorkspaceOptions<TSchema>,
|
|
1006
|
+
// The input is the door without the empty door's `Record<string, never>` (`{}` for `z.object({})`), so
|
|
1007
|
+
// a parent can give `instanceId` to a nested `spec` that reads it (ADR-0033).
|
|
1008
|
+
): WorkspaceMachine<WithoutEmptyIndex<z.infer<TSchema>>, OutputFrom<TBody>, TBody, TSlots>;
|
|
1009
|
+
/**
|
|
1010
|
+
* Run a body Machine in a Sandbox, with no `input` schema: the Workspace starts with any input. Use
|
|
1011
|
+
* it for a Workspace that a Pool or another Machine starts. {@link workspace} tells the rest, and
|
|
1012
|
+
* what it refuses.
|
|
1013
|
+
*
|
|
1014
|
+
* @param body - The Machine to run in the Sandbox. Its input is the input of the Workspace plus
|
|
1015
|
+
* `workspace`, the handles.
|
|
1016
|
+
* @param options - The options of the Sandbox, and `spec`. There is no `input`.
|
|
1017
|
+
* @returns A Machine whose input is the run input and whose output is the body's output.
|
|
1018
|
+
*
|
|
1019
|
+
* @see docs/concepts/workspaces.md
|
|
1020
|
+
* @see docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md
|
|
1021
|
+
*/
|
|
1022
|
+
export function workspace<TBody extends AnyStateMachine, TSlots extends string = never, TInput = unknown>(
|
|
593
1023
|
body: TBody & BodyAcceptsSlots<TBody, TSlots>,
|
|
594
|
-
options: PermissiveWorkspaceOptions<TSlots, TInput>,
|
|
1024
|
+
options: PermissiveWorkspaceOptions<TSlots, TInput> | PermissiveNoSlotWorkspaceOptions<TInput>,
|
|
595
1025
|
): WorkspaceMachine<TInput, OutputFrom<TBody>, TBody, TSlots>;
|
|
596
1026
|
export function workspace(
|
|
597
1027
|
body: AnyStateMachine,
|
|
598
|
-
options: SandboxOptions<string, any> & {
|
|
1028
|
+
options: Omit<SandboxOptions<string, any>, "repos"> & {
|
|
1029
|
+
repos?: SandboxOptions<string, any>["repos"];
|
|
1030
|
+
input?: z.ZodObject;
|
|
1031
|
+
spec?: (args: { input: any }) => WorkspaceSpec | NoSlotSpec;
|
|
1032
|
+
},
|
|
599
1033
|
): AnyStateMachine {
|
|
1034
|
+
// `spec` maps the wrapper's input to the workspace-domain spec (ADR-0012); the body receives
|
|
1035
|
+
// the wrapper's input plus the handles — `Workspaced<TInput, TSlots>`, which is also what the
|
|
1036
|
+
// declared door checks the body against. A Failure of the body is caught: the wrapper destroys
|
|
1037
|
+
// the Sandbox and fails again with the same payload (ADR-0073). Every other body error (a defect)
|
|
1038
|
+
// is not caught: it faults the run (RunStatus.fault) and leaves the Sandbox to the operator's
|
|
1039
|
+
// idle-timeout GC, so the trail stays as evidence.
|
|
1040
|
+
//
|
|
600
1041
|
// A body that still declares its own run input is a dead declaration under the door design: the
|
|
601
1042
|
// wrapper never serves it, never validates against it, and feeds the body something it does not
|
|
602
1043
|
// describe. Silence would leave an author believing a contract that nothing enforces (ADR-0033).
|
|
603
|
-
|
|
1044
|
+
// A body that `setup()` made keeps its zod `input` in the config with no door attached, so the
|
|
1045
|
+
// check reads the config too: else the author loses the door with no message.
|
|
1046
|
+
if (declaresRunInput(body)) {
|
|
604
1047
|
throw new Error(
|
|
605
1048
|
`workspace(): the body "${body.id}" declares its own run input, which nothing will ever ` +
|
|
606
1049
|
"serve or enforce — the wrapper feeds the body the run input PLUS the injected `workspace` " +
|
|
@@ -608,6 +1051,11 @@ export function workspace(
|
|
|
608
1051
|
"workspace(body, { input, spec }) (ADR-0033).",
|
|
609
1052
|
);
|
|
610
1053
|
}
|
|
1054
|
+
// A body state that cannot route `workspace.lost` (ADR-0074): with no route, the body would show
|
|
1055
|
+
// healthy on the feed with no Sandbox. Here, because `workspace()` is the one place that knows
|
|
1056
|
+
// that a Machine is a body. A body that is the kit's own wrapper (a `workspace()`, a `pool()`) is
|
|
1057
|
+
// not judged: its states are jr2's, and the walk refuses a Workspace below a Workspace (ADR-0059).
|
|
1058
|
+
if (!composesSandbox(body) && wrapperBodyOf(body) === undefined) refuseUnroutedLost(body);
|
|
611
1059
|
// Static, so checkable NOW rather than at the first provision — an empty or non-string image is
|
|
612
1060
|
// the same derives-from-a-typo bug `assertSpec` catches for the spec, one build earlier.
|
|
613
1061
|
const image = options.image;
|
|
@@ -623,16 +1071,22 @@ export function workspace(
|
|
|
623
1071
|
// any key Substrate would refuse, is a dependency the move to it would drop.
|
|
624
1072
|
if (options.resources !== undefined) assertSize("workspace(): resources", options.resources);
|
|
625
1073
|
// The slots, checked NOW for the same reason (ADR-0051): every value is one of the three forms,
|
|
626
|
-
// every key is a directory name
|
|
627
|
-
//
|
|
628
|
-
// An Open map defers all of that to the `customize` that
|
|
629
|
-
|
|
1074
|
+
// and every key is a directory name. Left out, the Workspace has no Repo Slot and its steps share
|
|
1075
|
+
// `/work`; written empty, it is refused, so "no slot" has one spelling and an empty map never
|
|
1076
|
+
// reads as a map someone forgot to fill. An Open map defers all of that to the `customize` that
|
|
1077
|
+
// fills it, which still asks for at least one: an Open map says the composer names the slots.
|
|
1078
|
+
const repos = options.repos ?? {};
|
|
630
1079
|
if (repos !== open) {
|
|
631
|
-
if (typeof repos !== "object" || repos === null || Array.isArray(repos)
|
|
1080
|
+
if (typeof repos !== "object" || repos === null || Array.isArray(repos)) {
|
|
632
1081
|
throw new Error(
|
|
633
|
-
'workspace(): `repos` must
|
|
634
|
-
"
|
|
635
|
-
|
|
1082
|
+
'workspace(): `repos` must be a map of Repo Slots — `repos: { app: "https://…" }` — or `open` whole, ' +
|
|
1083
|
+
"for a map the consumer names; leave it out for a Workspace with no Repo Slot (ADR-0051).",
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
if (options.repos !== undefined && Object.keys(repos).length === 0) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
"workspace(): `repos: {}` names no Repo Slot — leave `repos` out for a Workspace with no Repo Slot, " +
|
|
1089
|
+
'or name a slot: `repos: { app: "https://…" }` (ADR-0051).',
|
|
636
1090
|
);
|
|
637
1091
|
}
|
|
638
1092
|
for (const [slot, value] of Object.entries(repos)) assertRepoSlot("workspace()", slot, value);
|
|
@@ -664,7 +1118,15 @@ export function workspace(
|
|
|
664
1118
|
`${JSON.stringify(effector)}) — effector: { image: import.meta.resolve("./effector") } (ADR-0068).`,
|
|
665
1119
|
);
|
|
666
1120
|
}
|
|
667
|
-
|
|
1121
|
+
// A Workspace with slots cuts a branch, so it needs `spec`; one with none may leave it out. The
|
|
1122
|
+
// types say so; this says so to a caller the types do not see, by name, before any run.
|
|
1123
|
+
if (options.spec !== undefined ? typeof options.spec !== "function" : repos === open || Object.keys(repos).length) {
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
"workspace(): `spec` must be a function from the run input to the spec — spec: ({ input }) => ({ branch }) — " +
|
|
1126
|
+
"and only a Workspace with no Repo Slot may leave it out (ADR-0051).",
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
const wrapper = buildWorkspaceMachine(body, options.spec ?? (() => ({})));
|
|
668
1130
|
// The wrapper is TRANSPARENT to its body (ADR-0049): `customize(machine, { agents })` on a
|
|
669
1131
|
// Workspace means the Machine inside, so the composer never spells `body` and never has to know
|
|
670
1132
|
// that jr2 wrapped anything.
|
|
@@ -701,16 +1163,29 @@ export function workspace(
|
|
|
701
1163
|
* unchecked, it survives until the attach script's string ops and dies as "Cannot read properties
|
|
702
1164
|
* of undefined", with a Sandbox already provisioned and nothing pointing back at the input.
|
|
703
1165
|
*/
|
|
704
|
-
function assertSpec(spec:
|
|
1166
|
+
function assertSpec(spec: AnySpec, noSlot: boolean): void {
|
|
1167
|
+
if (typeof spec !== "object" || spec === null) {
|
|
1168
|
+
throw new Error(
|
|
1169
|
+
`workspace spec invalid: the spec is ${JSON.stringify(spec)}, not an object — \`spec\` returns one, ` +
|
|
1170
|
+
"such as { branch } (ADR-0051)",
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
705
1173
|
const bad: string[] = [];
|
|
706
|
-
|
|
1174
|
+
// No Repo Slot, nothing attached (ADR-0051): a branch or a review sha would name nothing, and a
|
|
1175
|
+
// value jr2 silently ignores is the failure the refusals exist for.
|
|
1176
|
+
const nothing =
|
|
1177
|
+
"(the Workspace has no Repo Slot, so it has no branch and no review Worktree; its spec gives only workGroup)";
|
|
1178
|
+
if (noSlot) {
|
|
1179
|
+
if (spec?.branch !== undefined) bad.push(`branch ${nothing}`);
|
|
1180
|
+
if (spec?.reviewSha !== undefined) bad.push(`reviewSha ${nothing}`);
|
|
1181
|
+
} else if (typeof spec?.branch !== "string" || !spec.branch) bad.push(`branch (got ${JSON.stringify(spec?.branch)})`);
|
|
707
1182
|
// The branch Worktree is a SIBLING of the pod-local clone at `<slot>/default/` (ADR-0004), so
|
|
708
1183
|
// the one branch name that is not a worktree directory is `default`.
|
|
709
1184
|
else if (spec.branch === "default")
|
|
710
1185
|
bad.push(
|
|
711
1186
|
'branch "default" (the pod-local clone\'s own directory — a branch Worktree sits beside `default/`, ADR-0004)',
|
|
712
1187
|
);
|
|
713
|
-
if (spec?.reviewSha !== undefined && (typeof spec.reviewSha !== "string" || !spec.reviewSha))
|
|
1188
|
+
if (!noSlot && spec?.reviewSha !== undefined && (typeof spec.reviewSha !== "string" || !spec.reviewSha))
|
|
714
1189
|
bad.push(`reviewSha (got ${JSON.stringify(spec?.reviewSha)})`);
|
|
715
1190
|
// A gid, so an integer — a float or a negative becomes a pod the API server rejects at
|
|
716
1191
|
// admission, which surfaces as "never reached Ready" with nothing pointing back at the spec.
|
|
@@ -779,6 +1254,15 @@ function resolveBindings(
|
|
|
779
1254
|
return bindings;
|
|
780
1255
|
}
|
|
781
1256
|
|
|
1257
|
+
/** A spec as the wrapper holds it: a Workspace's with Repo Slots, or one with none (ADR-0051). */
|
|
1258
|
+
type AnySpec = WorkspaceSpec | NoSlotSpec;
|
|
1259
|
+
|
|
1260
|
+
/** Does this Workspace declare no Repo Slot (ADR-0051)? An Open map is not "none": its slots are
|
|
1261
|
+
* the composer's, and `customize()` names at least one. */
|
|
1262
|
+
function noSlot(slots: SandboxParts["repos"]): boolean {
|
|
1263
|
+
return slots !== open && Object.keys(slots).length === 0;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
782
1266
|
/** The port-facing view of the persisted bindings, in declaration order. */
|
|
783
1267
|
function attachedRepos(bindings: Record<string, ResolvedBinding>): Array<{ slot: string; url: string; ref?: string }> {
|
|
784
1268
|
return Object.entries(bindings).map(([slot, b]) => ({
|
|
@@ -788,7 +1272,7 @@ function attachedRepos(bindings: Record<string, ResolvedBinding>): Array<{ slot:
|
|
|
788
1272
|
}));
|
|
789
1273
|
}
|
|
790
1274
|
|
|
791
|
-
function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any }) =>
|
|
1275
|
+
function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any }) => AnySpec): AnyStateMachine {
|
|
792
1276
|
/**
|
|
793
1277
|
* `placing` (ADR-0064): write the Sandbox and wait, with no deadline, for its pod to be
|
|
794
1278
|
* scheduled. jr2 cannot tell "full now" from "never fits", so a bound is the Machine's own
|
|
@@ -807,13 +1291,15 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
807
1291
|
* delete rides the abort, after any write in flight has settled. A wait that fails on its own —
|
|
808
1292
|
* the Sandbox Lost before its node, a write that half landed — is the same end: the delete comes
|
|
809
1293
|
* before the fault, and the fault carries the reason. The host stopping the run for a restart is
|
|
810
|
-
* not an end: the restored run re-enters `placing` and keeps its place.
|
|
1294
|
+
* not an end: the restored run re-enters `placing` and keeps its place. Nor is a name that a live
|
|
1295
|
+
* Workspace of the run holds when the delete would run: a parent that re-enters the state, or
|
|
1296
|
+
* catches a Failure and invokes the same Machine again, gives its new Workspace the same invoke id,
|
|
1297
|
+
* and so the same name, which that Workspace has placed again by then (ADR-0073).
|
|
811
1298
|
*/
|
|
812
1299
|
const place = fromPromise<
|
|
813
1300
|
{ bindings: Record<string, ResolvedBinding> },
|
|
814
|
-
{ wsId: string; spec:
|
|
1301
|
+
{ wsId: string; spec: AnySpec; runInput: unknown }
|
|
815
1302
|
>(async ({ input, self, system, signal }) => {
|
|
816
|
-
assertSpec(input.spec); // before the port: a bad spec must never cost a pod
|
|
817
1303
|
const binding = runBindingOf(system);
|
|
818
1304
|
// The images and the slots come off the WRAPPER, at invoke time, not out of context and not
|
|
819
1305
|
// out of a build-time closure (ADR-0049, ADR-0051). This state re-runs on every restore, so
|
|
@@ -822,6 +1308,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
822
1308
|
// content-addressed tag, which would outlive the image it names. The RESOLVED bindings are
|
|
823
1309
|
// persisted, because a per-run mapper's answer is this run's fact.
|
|
824
1310
|
const parts = sandboxPartsOf(invokingMachine(self));
|
|
1311
|
+
assertSpec(input.spec, noSlot(parts.repos)); // before the port: a bad spec must never cost a pod
|
|
825
1312
|
const user = userSeatOf(parts.user);
|
|
826
1313
|
const path = actorSlotPath(self._parent);
|
|
827
1314
|
// Read before the port: a Workspace no walk can see placed nothing, and is refused with the slot form.
|
|
@@ -879,7 +1366,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
879
1366
|
if (binding.hostStopping) return;
|
|
880
1367
|
void placed
|
|
881
1368
|
.catch(() => {})
|
|
882
|
-
.then(() => port.destroy(name))
|
|
1369
|
+
.then(() => (binding.heldSandboxes?.().has(name) ? undefined : port.destroy(name)))
|
|
883
1370
|
.catch(() => {}); // a delete that fails leaves the CR to the idle GC, as ever
|
|
884
1371
|
},
|
|
885
1372
|
{ once: true },
|
|
@@ -910,8 +1397,11 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
910
1397
|
|
|
911
1398
|
const attach = fromPromise<
|
|
912
1399
|
{ repos: Record<string, string>; review?: Record<string, string>; stale?: Record<string, string> },
|
|
913
|
-
{ wsId: string; spec:
|
|
1400
|
+
{ wsId: string; spec: AnySpec; bindings: Record<string, ResolvedBinding> }
|
|
914
1401
|
>(async ({ input, system, signal }) => {
|
|
1402
|
+
// No Repo Slot, nothing to clone (ADR-0051): the Harness's attach would refuse an empty list,
|
|
1403
|
+
// and `/work` needs nothing from it — the pod's `fsGroup` already makes it the work group's.
|
|
1404
|
+
if (Object.keys(input.bindings).length === 0) return { repos: {} };
|
|
915
1405
|
const port = sandboxOf(system);
|
|
916
1406
|
const name = workspaceName(runBindingOf(system).runId, input.wsId);
|
|
917
1407
|
// The attach is loss-aware (ADR-0021): a Sandbox gone or Lost under it fails the attach at
|
|
@@ -930,7 +1420,9 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
930
1420
|
signal.addEventListener("abort", () => unsubscribe(), { once: true });
|
|
931
1421
|
let out: Awaited<ReturnType<typeof port.attach>>;
|
|
932
1422
|
try {
|
|
933
|
-
|
|
1423
|
+
// A Workspace with slots holds a `WorkspaceSpec`: `place` refused one without a branch.
|
|
1424
|
+
const spec = input.spec as WorkspaceSpec;
|
|
1425
|
+
out = await Promise.race([port.attach({ name, spec, repos: attachedRepos(input.bindings) }), loss]);
|
|
934
1426
|
} finally {
|
|
935
1427
|
unsubscribe();
|
|
936
1428
|
}
|
|
@@ -1049,11 +1541,12 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1049
1541
|
|
|
1050
1542
|
// Every actor this wrapper runs is a NAMED SLOT (ADR-0049), the body first among them: a Machine
|
|
1051
1543
|
// composes by invoking a declared `src`, and `body` is what `provide()`, `customize()`, Stately,
|
|
1052
|
-
// the Console's join key and the `jr2 up` parts walk all reach it by. The mechanism's own
|
|
1053
|
-
// place, provision, attach, registrar, lease, destroy — are named for the same price, and the
|
|
1054
|
-
// Console shows what each state is doing instead of "inline".
|
|
1544
|
+
// the Console's join key and the `jr2 up` parts walk all reach it by. The mechanism's own seven —
|
|
1545
|
+
// place, provision, attach, registrar, lease, destroy, fail — are named for the same price, and the
|
|
1546
|
+
// Console shows what each state is doing instead of "inline". `fail` is the unbranded logic: it
|
|
1547
|
+
// sends the body's Failure up again, and the walk does not list it as a state that fails the run.
|
|
1055
1548
|
return setup({
|
|
1056
|
-
actors: { body, place, provision, attach, registrar, lease, destroy },
|
|
1549
|
+
actors: { body, place, provision, attach, registrar, lease, destroy, fail: failLogic },
|
|
1057
1550
|
}).createMachine({
|
|
1058
1551
|
id: "workspace",
|
|
1059
1552
|
context: ({ input, self }: { input: unknown; self: { id: string } }): WsContext => ({
|
|
@@ -1141,7 +1634,8 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1141
1634
|
...(ctx.bindings
|
|
1142
1635
|
? { bindings: Object.fromEntries(Object.entries(ctx.bindings).map(([s, b]) => [s, b.url])) }
|
|
1143
1636
|
: {}),
|
|
1144
|
-
branch:
|
|
1637
|
+
// No branch with no Repo Slot (ADR-0051): nothing was cut.
|
|
1638
|
+
...(ctx.spec.branch !== undefined ? { branch: ctx.spec.branch } : {}),
|
|
1145
1639
|
...(out.review ? { review: out.review } : {}),
|
|
1146
1640
|
};
|
|
1147
1641
|
},
|
|
@@ -1167,16 +1661,25 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1167
1661
|
input: ({ context }: { context: WsContext }) => {
|
|
1168
1662
|
const ctx = context;
|
|
1169
1663
|
const { repos, branch, review } = ctx.handles!;
|
|
1170
|
-
// Body-facing subset only (ADR-0016): endpoint/sandbox are mechanism-internal.
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
};
|
|
1664
|
+
// Body-facing subset only (ADR-0016): endpoint/sandbox are mechanism-internal. With
|
|
1665
|
+
// no Repo Slot the handles are `{ repos: {} }` (WorkspaceHandles<never>).
|
|
1666
|
+
const handles: WorkspaceHandles<string> | WorkspaceHandles<never> =
|
|
1667
|
+
branch === undefined ? { repos: {} } : { repos, branch, ...(review ? { review } : {}) };
|
|
1668
|
+
return { ...ctx.runInput, workspace: handles };
|
|
1175
1669
|
},
|
|
1176
1670
|
onDone: {
|
|
1177
1671
|
target: "teardown",
|
|
1178
1672
|
actions: assign({ output: ({ event }) => (event as unknown as { output: unknown }).output }),
|
|
1179
1673
|
},
|
|
1674
|
+
// A Failure deletes the Sandbox before it goes up (ADR-0073). Guarded, so a defect
|
|
1675
|
+
// takes no transition and goes up at once, as before: its Sandbox stays as evidence.
|
|
1676
|
+
onError: {
|
|
1677
|
+
guard: ({ event }: { event: { error?: unknown } }) => isFailure(event.error),
|
|
1678
|
+
target: "failing",
|
|
1679
|
+
actions: assign({
|
|
1680
|
+
failure: ({ event }) => failurePayload((event as unknown as { error: Failure }).error),
|
|
1681
|
+
}),
|
|
1682
|
+
},
|
|
1180
1683
|
},
|
|
1181
1684
|
leaseInvoke(true),
|
|
1182
1685
|
],
|
|
@@ -1194,6 +1697,25 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1194
1697
|
},
|
|
1195
1698
|
},
|
|
1196
1699
|
done: { type: "final" },
|
|
1700
|
+
// The body failed (ADR-0073): the same delete as `teardown`, then the same Failure goes up to
|
|
1701
|
+
// the parent unchanged. No lease here: nothing judges a Sandbox that is being deleted.
|
|
1702
|
+
failing: {
|
|
1703
|
+
initial: "destroying",
|
|
1704
|
+
states: {
|
|
1705
|
+
destroying: {
|
|
1706
|
+
invoke: {
|
|
1707
|
+
src: "destroy",
|
|
1708
|
+
input: ({ context }) => ({ wsId: (context as unknown as WsContext).wsId }),
|
|
1709
|
+
onDone: "raising",
|
|
1710
|
+
// A failed delete is the operator GC's problem, and the run host's (ADR-0012 backstop).
|
|
1711
|
+
onError: "raising",
|
|
1712
|
+
},
|
|
1713
|
+
},
|
|
1714
|
+
raising: {
|
|
1715
|
+
invoke: { src: "fail", input: ({ context }) => (context as unknown as WsContext).failure! },
|
|
1716
|
+
},
|
|
1717
|
+
},
|
|
1718
|
+
},
|
|
1197
1719
|
},
|
|
1198
1720
|
// Machine output must be declared at the ROOT in xstate v5 (a final state's own `output`
|
|
1199
1721
|
// only rides the done event); the workspace's output is the body's, verbatim (ADR-0012).
|