@jr2/orchestrator 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +478 -0
- package/docs/concepts/architecture.md +243 -0
- package/docs/concepts/charts.md +146 -0
- package/docs/concepts/composition.md +357 -0
- package/docs/concepts/converge.md +257 -0
- package/docs/concepts/durable-runs.md +189 -0
- package/docs/concepts/effects.md +658 -0
- package/docs/concepts/gates.md +208 -0
- package/docs/concepts/machines-and-workflows.md +298 -0
- package/docs/concepts/pools-and-sources.md +230 -0
- package/docs/concepts/secrets-and-trust.md +306 -0
- package/docs/concepts/the-menu.md +381 -0
- package/docs/concepts/workspaces.md +756 -0
- package/docs/get-started/a-model-decides.md +563 -0
- package/docs/get-started/an-agent-in-a-sandbox.md +383 -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 +993 -0
- package/docs/get-started/install.md +130 -0
- package/docs/guides/add-tools-to-the-sandbox.md +308 -0
- package/docs/guides/answer-a-gate.md +304 -0
- package/docs/guides/bind-open-parts.md +224 -0
- package/docs/guides/cancel-a-run.md +146 -0
- package/docs/guides/clean-up.md +206 -0
- package/docs/guides/cut-a-worktree.md +560 -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 +230 -0
- package/docs/guides/look-inside-a-sandbox.md +177 -0
- package/docs/guides/mirror-the-kit-images.md +176 -0
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +318 -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 +532 -0
- package/docs/guides/run-a-chart-by-name.md +270 -0
- package/docs/guides/run-a-command-or-call-a-url.md +587 -0
- package/docs/guides/run-one-worker-per-item.md +322 -0
- package/docs/guides/set-an-agents-access-and-dials.md +420 -0
- package/docs/guides/size-a-workspace.md +217 -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 +214 -0
- package/docs/guides/use-a-local-model.md +313 -0
- package/docs/guides/watch-a-run.md +349 -0
- package/docs/guides/write-a-workflow.md +318 -0
- package/docs/index.md +44 -0
- package/docs/llms.txt +237 -0
- package/docs/reference/authoring-surface/agent.md +151 -0
- package/docs/reference/authoring-surface/answer.md +156 -0
- package/docs/reference/authoring-surface/arg.md +82 -0
- package/docs/reference/authoring-surface/basic.md +62 -0
- package/docs/reference/authoring-surface/bearer.md +53 -0
- package/docs/reference/authoring-surface/branchname.md +15 -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 +206 -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 +96 -0
- package/docs/reference/authoring-surface/file.md +142 -0
- package/docs/reference/authoring-surface/frompick.md +277 -0
- package/docs/reference/authoring-surface/http.md +259 -0
- package/docs/reference/authoring-surface/isfailure.md +58 -0
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +237 -0
- package/docs/reference/authoring-surface/open.md +105 -0
- package/docs/reference/authoring-surface/openbody.md +86 -0
- package/docs/reference/authoring-surface/paramsof.md +143 -0
- package/docs/reference/authoring-surface/pool.md +186 -0
- package/docs/reference/authoring-surface/push.md +130 -0
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/secret.md +139 -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 +15 -0
- package/docs/reference/authoring-surface/type-answeraction.md +12 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-arg.md +19 -0
- package/docs/reference/authoring-surface/type-argnames.md +12 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +61 -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 +20 -0
- package/docs/reference/authoring-surface/type-commandoptions.md +56 -0
- package/docs/reference/authoring-surface/type-commandoutput.md +122 -0
- package/docs/reference/authoring-surface/type-customize.md +111 -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 +72 -0
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -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 +37 -0
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-frominput.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +56 -0
- package/docs/reference/authoring-surface/type-httpdefinition.md +104 -0
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -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 +95 -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-lateroutcome.md +21 -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-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -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 +22 -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 +26 -0
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +60 -0
- package/docs/reference/authoring-surface/type-pushinput.md +34 -0
- package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
- package/docs/reference/authoring-surface/type-pushoutput.md +149 -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 +84 -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 +24 -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 +115 -0
- package/docs/reference/authoring-surface/type-workspacehandles.md +22 -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 +27 -0
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +287 -0
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +217 -0
- package/docs/reference/cli.md +433 -0
- package/docs/reference/config.md +765 -0
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +114 -0
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +117 -0
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +130 -0
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +240 -0
- package/docs/reference/decisions/0006-agent-control-surface.md +50 -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 +175 -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 +271 -0
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +126 -0
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +71 -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 +287 -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 +101 -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 +175 -0
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +436 -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 +245 -0
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +90 -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 +193 -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 +126 -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 +238 -0
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +86 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +123 -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 +108 -0
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +427 -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 +239 -0
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +82 -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 +552 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +458 -0
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +475 -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 +334 -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/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions.md +86 -0
- package/docs/reference/glossary.md +687 -0
- package/docs/reference/http-api.md +1386 -0
- package/docs/reference/machines.md +189 -0
- package/docs/reference/mechanism-events.md +72 -0
- package/docs/why.md +207 -0
- package/package.json +5 -3
- package/src/actor.ts +694 -106
- package/src/agent.ts +114 -38
- package/src/ambient.ts +3 -4
- package/src/assembly-checks.ts +189 -7
- package/src/cause.ts +234 -0
- package/src/chart.ts +345 -46
- package/src/charts.ts +43 -8
- package/src/config.ts +440 -178
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +325 -99
- package/src/customize.ts +199 -49
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect-checks.ts +50 -3
- package/src/effect.ts +641 -128
- package/src/effector-wire.ts +165 -13
- package/src/fail.ts +222 -0
- package/src/harness-client.ts +587 -50
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +125 -16
- package/src/holds.ts +3 -0
- package/src/http-effect.ts +556 -58
- package/src/http.ts +480 -30
- package/src/index.ts +74 -82
- package/src/instance.ts +68 -8
- package/src/internal.ts +85 -0
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +24 -7
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +42 -58
- package/src/names.ts +19 -6
- package/src/notices.ts +483 -41
- package/src/open-body.ts +242 -40
- package/src/open-effect.ts +81 -8
- package/src/open.ts +90 -6
- package/src/parts.ts +213 -30
- package/src/party.ts +101 -2
- package/src/pick.ts +497 -11
- package/src/pool.ts +282 -50
- package/src/registration.ts +133 -12
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +22 -3
- package/src/run-host.ts +665 -45
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +457 -71
- package/src/sandbox-kube.ts +66 -17
- package/src/secret-placement.ts +59 -17
- package/src/server.ts +3 -0
- package/src/setup.ts +325 -62
- 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/tokens.ts +107 -32
- package/src/vocabulary.ts +88 -14
- package/src/wire.ts +100 -26
- package/src/workspace.ts +771 -238
- package/src/worktree.ts +249 -0
package/src/workspace.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// `workspace(body, { input, image, user, repos, resources,
|
|
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) +
|
|
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
|
|
1
|
+
// `workspace(body, { input, image, user, repos, resources, workGroup })` (ADR-0012, ADR-0049, ADR-0051,
|
|
2
|
+
// ADR-0060, ADR-0075): 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) + clone each Repo
|
|
4
|
+
// Slot, run the author's body Machine inside it as the named slot `body`, with
|
|
5
|
+
// `{ workspace: { repos } }` 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,22 @@ import {
|
|
|
41
44
|
} from "xstate";
|
|
42
45
|
import type { z } from "zod";
|
|
43
46
|
import { registerAmbientHandles, type AmbientHandles } from "./ambient.ts";
|
|
47
|
+
import { isAgent } from "./agent.ts";
|
|
48
|
+
import { isEffect } from "./effect.ts";
|
|
49
|
+
import { failLogic, failurePayload, isFailure, type Failure, type FailureInput } from "./fail.ts";
|
|
50
|
+
import { refuseUnroutedLost } from "./mechanism-routes.ts";
|
|
44
51
|
import {
|
|
45
52
|
actorRoute,
|
|
46
53
|
actorSlotPath,
|
|
47
54
|
assertRepoSlot,
|
|
48
55
|
attachSandboxParts,
|
|
49
56
|
attachWrapperBody,
|
|
57
|
+
composesSandbox,
|
|
50
58
|
customizeLine,
|
|
51
59
|
open,
|
|
52
60
|
repoSlotState,
|
|
53
61
|
sandboxPartsOf,
|
|
62
|
+
wrapperBodyOf,
|
|
54
63
|
type Binding,
|
|
55
64
|
type JR2Repos,
|
|
56
65
|
type JR2Wrapper,
|
|
@@ -61,7 +70,14 @@ import {
|
|
|
61
70
|
} from "./parts.ts";
|
|
62
71
|
import { actorPath, runBindingOf, type AnyActorSystem } from "./registration.ts";
|
|
63
72
|
import { assertSize, assertUserSeat, userSeatOf, type Size, type UserContainer } from "./size.ts";
|
|
64
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
attachInputSchema,
|
|
75
|
+
declaresRunInput,
|
|
76
|
+
inputSchemaOf,
|
|
77
|
+
parsedNestedDoor,
|
|
78
|
+
invokingMachine,
|
|
79
|
+
type HostInjectedInput,
|
|
80
|
+
} from "./vocabulary.ts";
|
|
65
81
|
|
|
66
82
|
/** Lease cadence when the backend names none. Well inside the 30m default idle timeout, so a
|
|
67
83
|
* few missed renewals in a row are survivable. It bounds only how long an orphan waits to be
|
|
@@ -83,75 +99,170 @@ export function leaseDelay(interval: number, which: "first" | "next", random: nu
|
|
|
83
99
|
return interval * (1 - LEASE_JITTER + 2 * LEASE_JITTER * random);
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
// What the workspace hands the BODY (ADR-0012, ADR-0016): worktree geography only, keyed by Repo
|
|
103
|
+
// Slot (ADR-0051) — so a path a prompt names is right in every Instance that consumes the Machine.
|
|
104
|
+
// `endpoint` and `sandbox` are mechanism-internal — the Agent actor resolves them ambiently from
|
|
105
|
+
// the enclosing wrapper (ambient.ts), so a workflow can no longer forget to thread them (the
|
|
106
|
+
// baba71f incident: `sandbox` omitted, every tool call 403'd, fail-closed but silent).
|
|
107
|
+
//
|
|
108
|
+
// `TSlots` has NO default on purpose (ADR-0050): a body names the slots it reads, and
|
|
109
|
+
// `workspace()` holds those names to the ones the wrapper declares. A default of `string` would
|
|
110
|
+
// type `repos` as `Record<string, string>`, under which a misspelled slot reads as a path — the
|
|
111
|
+
// silent widening the slot key exists to refuse. `string` WRITTEN is a different statement: the
|
|
112
|
+
// body names no slot at all — it enumerates them, in the order the composer wrote — which is the
|
|
113
|
+
// body under a wrapper that declares its map Open (`repos: open`, ADR-0051), where the slots are
|
|
114
|
+
// the composer's words and no body could name them.
|
|
115
|
+
/**
|
|
116
|
+
* What the body of a `workspace()` gets as `input.workspace`: the folder of each Repo Slot. A path
|
|
117
|
+
* is correct in each Instance that registers the Machine.
|
|
118
|
+
*
|
|
119
|
+
* `TSlots` is the names of the Repo Slots that the body reads. `workspace()` refuses a body that
|
|
120
|
+
* names a slot that the Workspace does not have. Write `string` for a body that names no slot and
|
|
121
|
+
* reads the folders in the order of `repos`, as under `repos: open`. Write `never` for a
|
|
122
|
+
* Workspace with no Repo Slot: its handles are `{ repos: {} }`, and its Agents work in `/work`.
|
|
123
|
+
*/
|
|
124
|
+
// `[TSlots] extends [never]`, not `TSlots extends never`: a bare `never` would distribute to `never`.
|
|
125
|
+
export type WorkspaceHandles<TSlots extends string> = [TSlots] extends [never]
|
|
126
|
+
? { repos: Record<never, string> }
|
|
127
|
+
: SlotHandles<TSlots>;
|
|
128
|
+
|
|
129
|
+
/** The handles of a Workspace that declares Repo Slots. @internal */
|
|
130
|
+
type SlotHandles<TSlots extends string> = {
|
|
131
|
+
/** The folder of each Repo Slot, `/work/<slot>`, in the order that `repos` declares the slots.
|
|
132
|
+
* `${handle}/default` is the clone, on the Binding's base. Any other Worktree is one the Machine
|
|
133
|
+
* cut with `worktree()`. */
|
|
134
|
+
repos: { [K in TSlots]: `/work/${K}` };
|
|
108
135
|
};
|
|
109
136
|
|
|
137
|
+
// The composition is the whole reason the door is declared on the wrapper and not on the body
|
|
138
|
+
// (ADR-0033): `Workspaced<RunInput, Slot>` is what the body receives, `RunInput` is what a caller
|
|
139
|
+
// may send, and no caller can send `workspace` (the handles do not exist until a Sandbox is
|
|
140
|
+
// provisioned and attached). Naming it here keeps the body from hand-copying
|
|
141
|
+
// {@link WorkspaceHandles}, which drifts. The slot argument is required, never defaulted, so
|
|
142
|
+
// `workspace.repos.<slot>` is typed by the same keys the wrapper declares (ADR-0050, ADR-0051).
|
|
143
|
+
//
|
|
144
|
+
// The wrapper passes its input through UNTOUCHED, so a ROOT-placed wrapper's body also receives
|
|
145
|
+
// what the host injected beside the door — `HostInjectedInput` today (the run's `instanceId`).
|
|
146
|
+
// That is outside this type on purpose: it depends on where the wrapper sits, and `Workspaced` is
|
|
147
|
+
// the composition the WRAPPER makes.
|
|
110
148
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
149
|
+
* The input of a `workspace()` body: the run input, plus `workspace`, the handles that the
|
|
150
|
+
* Workspace adds (`workspace.repos`, the folder of each Repo Slot). Use it as the type of
|
|
151
|
+
* the body's input.
|
|
152
|
+
*
|
|
153
|
+
* The first argument is the run input: the `input` schema of the `workspace()`. The second is the
|
|
154
|
+
* Repo Slots that the body reads, by name (`"app"` or `"app" | "docs"`). Under `repos: open`, write
|
|
155
|
+
* `string`: the body cannot know the names, so it reads the slots in order. For a `workspace()`
|
|
156
|
+
* with no `repos`, write `never`. The second argument has no default.
|
|
157
|
+
*
|
|
158
|
+
* A body under a root `workspace()` also gets the fields of `HostInjectedInput`. Add them when the
|
|
159
|
+
* body reads them: `Workspaced<HostInjectedInput & RunInput, "app">`.
|
|
160
|
+
*
|
|
161
|
+
* Its fields are the fields of the run input, and `workspace`. The run input can have no fields:
|
|
162
|
+
* `Workspaced<z.infer<typeof door>, "app">` for the door `z.object({})` holds only `workspace`.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* import { z } from "zod";
|
|
167
|
+
* import {
|
|
168
|
+
* agent,
|
|
169
|
+
* defineEvent,
|
|
170
|
+
* fail,
|
|
171
|
+
* isReady,
|
|
172
|
+
* jr2Setup,
|
|
173
|
+
* workspace,
|
|
174
|
+
* worktree,
|
|
175
|
+
* type Workspaced,
|
|
176
|
+
* } from "@jr2/orchestrator";
|
|
116
177
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
178
|
+
* const done = defineEvent({
|
|
179
|
+
* name: "done",
|
|
180
|
+
* description: "Call this when REVIEW.md is written.",
|
|
181
|
+
* input: z.object({}),
|
|
182
|
+
* });
|
|
183
|
+
* const runInput = z.object({ sha: z.string() });
|
|
184
|
+
* type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
|
|
185
|
+
*
|
|
186
|
+
* // A handle is the slot's folder: `/work/app`. A helper returns `as const`, so that the compiler
|
|
187
|
+
* // sees the slot.
|
|
188
|
+
* const reviewTree = (context: BodyInput) => `${context.workspace.repos.app}/review` as const;
|
|
189
|
+
*
|
|
190
|
+
* const body = jr2Setup({
|
|
191
|
+
* types: {} as { input: BodyInput; context: BodyInput },
|
|
192
|
+
* events: [done],
|
|
193
|
+
* actors: {
|
|
194
|
+
* reviewer: agent({
|
|
195
|
+
* model: "anthropic/claude-haiku-5-5",
|
|
196
|
+
* instructions: "You review one commit and write REVIEW.md.",
|
|
197
|
+
* workspace: "read",
|
|
198
|
+
* }),
|
|
199
|
+
* cut: worktree(),
|
|
200
|
+
* fail: fail(),
|
|
201
|
+
* },
|
|
202
|
+
* }).createMachine({
|
|
203
|
+
* id: "review",
|
|
204
|
+
* context: ({ input }) => input,
|
|
205
|
+
* on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
|
|
206
|
+
* initial: "cutting",
|
|
207
|
+
* states: {
|
|
208
|
+
* cutting: {
|
|
209
|
+
* invoke: {
|
|
210
|
+
* src: "cut",
|
|
211
|
+
* input: ({ context }) => ({ path: reviewTree(context), detach: context.sha }),
|
|
212
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "reviewing" }, { target: "notCut" }],
|
|
213
|
+
* },
|
|
214
|
+
* },
|
|
215
|
+
* reviewing: {
|
|
216
|
+
* invoke: {
|
|
217
|
+
* src: "reviewer",
|
|
218
|
+
* input: ({ context }) => ({
|
|
219
|
+
* prompt: `Review commit ${context.sha}.`,
|
|
220
|
+
* cwd: reviewTree(context),
|
|
221
|
+
* }),
|
|
222
|
+
* },
|
|
223
|
+
* on: { done: "finished" },
|
|
224
|
+
* },
|
|
225
|
+
* finished: { type: "final" },
|
|
226
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
227
|
+
* failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
|
|
228
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
229
|
+
* },
|
|
230
|
+
* });
|
|
231
|
+
*
|
|
232
|
+
* export const machine = workspace(body, {
|
|
233
|
+
* input: runInput,
|
|
234
|
+
* repos: { app: "https://github.com/you/app.git" },
|
|
235
|
+
* });
|
|
236
|
+
* ```
|
|
237
|
+
*
|
|
238
|
+
* @see docs/concepts/workspaces.md
|
|
239
|
+
* @see docs/guides/write-a-workflow.md
|
|
240
|
+
* @see docs/reference/decisions/0012-workspace-wrapper-machine.md
|
|
241
|
+
* @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
|
|
124
242
|
*/
|
|
125
|
-
export type
|
|
126
|
-
|
|
127
|
-
* kit gives no slot a privileged meaning — there is no `workdir` — because which tree an Agent
|
|
128
|
-
* works in is a fact about that Agent's Turn, not about the Workspace: a body that names its
|
|
129
|
-
* slots frames each Agent with its own, and a body that names none may give the order a
|
|
130
|
-
* meaning of its own (`task`: the first is the one the coder edits). The order is the kit's
|
|
131
|
-
* promise; the meaning is the Machine's (ADR-0051). */
|
|
132
|
-
repos: Record<TSlots, string>;
|
|
133
|
-
branch: string;
|
|
134
|
-
/** Detached review-worktree paths by slot (ADR-0028) — present only when the spec carried
|
|
135
|
-
* `reviewSha`. The reviewer's seat: hand one of these as its cwd/prompt frame. */
|
|
136
|
-
review?: Partial<Record<TSlots, string>>;
|
|
243
|
+
export type Workspaced<TInput, TSlots extends string> = WithoutEmptyIndex<TInput> & {
|
|
244
|
+
workspace: WorkspaceHandles<TSlots>;
|
|
137
245
|
};
|
|
138
246
|
|
|
247
|
+
// `z.infer` of a door with no fields, `z.object({})`, is `Record<string, never>`: an index signature
|
|
248
|
+
// that says each key holds `never`. Intersected with the handles, it makes each assign of a body
|
|
249
|
+
// context fail (`n: number` is not `never`) with a diagnostic that does not name the cause. This
|
|
250
|
+
// removes that index signature, and only one whose value is `never`, so the named fields stay
|
|
251
|
+
// (`HostInjectedInput & Record<string, never>` keeps `instanceId: string`) and an input that is a
|
|
252
|
+
// real map (`Record<string, string>`) is not changed. The alternative, a separate type for the empty
|
|
253
|
+
// door, makes the author choose between two names for one statement.
|
|
254
|
+
// It is not `@internal`: the Reference then shows this TSDoc, so a reader of `Workspaced` sees that
|
|
255
|
+
// these are the fields of the run input. As plumbing, its page showed only the declaration.
|
|
139
256
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* may send, and no caller can send `workspace` (the handles do not exist until a Sandbox is
|
|
144
|
-
* provisioned and attached). Naming it here keeps the body from hand-copying
|
|
145
|
-
* {@link WorkspaceHandles}, which drifts. The second argument is the body's word for each Repo
|
|
146
|
-
* Slot it reads (`Workspaced<RunInput, "target">`) — required, never defaulted, so
|
|
147
|
-
* `workspace.repos.<slot>` is typed by the same keys the wrapper declares (ADR-0050, ADR-0051).
|
|
148
|
-
*
|
|
149
|
-
* The wrapper passes its input through UNTOUCHED, so a ROOT-placed wrapper's body also receives
|
|
150
|
-
* what the host injected beside the door — `HostInjectedInput` today (the run's `instanceId`).
|
|
151
|
-
* That is outside this type on purpose: it depends on where the wrapper sits, and `Workspaced` is
|
|
152
|
-
* the composition the WRAPPER makes.
|
|
257
|
+
* The fields of the run input `T`. A string index signature whose value is `never` is not one of
|
|
258
|
+
* them: `z.infer` gives `Record<string, never>` for `z.object({})`, and this type makes it `{}`. The
|
|
259
|
+
* named fields of `T` stay. A different `T` does not change.
|
|
153
260
|
*/
|
|
154
|
-
|
|
261
|
+
type WithoutEmptyIndex<T> = string extends keyof T
|
|
262
|
+
? [T[string & keyof T]] extends [never]
|
|
263
|
+
? { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] }
|
|
264
|
+
: T
|
|
265
|
+
: T;
|
|
155
266
|
|
|
156
267
|
/**
|
|
157
268
|
* What the watch says about a workspace (ADR-0021, ADR-0063): gone, or present — and, once the
|
|
@@ -190,6 +301,9 @@ export type PlaceRequest = {
|
|
|
190
301
|
image?: string;
|
|
191
302
|
user?: string;
|
|
192
303
|
workGroup?: number;
|
|
304
|
+
/** The Held secrets the `workspace()`'s `secrets` list names (ADR-0059): the ones the Harness may
|
|
305
|
+
* put in a confined child's env, by Stand-in (ADR-0076). Names, never a value. Absent → none. */
|
|
306
|
+
secrets?: string[];
|
|
193
307
|
repos: ProvisionedRepo[];
|
|
194
308
|
resources?: Size;
|
|
195
309
|
userResources?: Size;
|
|
@@ -237,17 +351,12 @@ export interface SandboxPort {
|
|
|
237
351
|
*/
|
|
238
352
|
provision(name: string): Promise<{ endpoint: string }>;
|
|
239
353
|
/** Post-Ready attach (ADR-0004): per slot, `git clone --shared` off the node's read-only
|
|
240
|
-
* cache (the `default/` checkout)
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
|
|
244
|
-
attach(req: {
|
|
245
|
-
name: string;
|
|
246
|
-
spec: WorkspaceSpec;
|
|
247
|
-
repos: Array<{ slot: string; url: string; ref?: string }>;
|
|
248
|
-
}): Promise<{
|
|
354
|
+
* cache (the `default/` checkout). A Machine cuts each Worktree (ADR-0075). Resolves with the
|
|
355
|
+
* folder of each slot; `stale` names the slots whose cache could not be fetched before this
|
|
356
|
+
* attach, with git's own error (ADR-0051: freshness degrades, absence does not). */
|
|
357
|
+
attach(req: { name: string; repos: Array<{ slot: string; url: string; ref?: string }> }): Promise<{
|
|
249
358
|
repos: Record<string, string>;
|
|
250
|
-
|
|
359
|
+
bases?: Record<string, string>;
|
|
251
360
|
stale?: Record<string, string>;
|
|
252
361
|
}>;
|
|
253
362
|
/**
|
|
@@ -277,8 +386,10 @@ export interface SandboxPort {
|
|
|
277
386
|
* `OOMKilled` at or after `since`, the kill — its fault reason, starting with the fixed prefix
|
|
278
387
|
* `memory limit`, and the data the next Turn's notice is made of (ADR-0062) — else undefined.
|
|
279
388
|
* 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
|
-
*
|
|
389
|
+
* which can trail the Harness's restart. Asked too before an admission into the Workspace whose
|
|
390
|
+
* watch shows a last end `OOMKilled`, with `since` that end: a kill while no Turn ran (ADR-0062).
|
|
391
|
+
* A backend that cannot see the container answers undefined, and every lost conversation stays
|
|
392
|
+
* lost — never an optional method (ADR-0021).
|
|
282
393
|
*/
|
|
283
394
|
memoryFault(name: string, since: Date): Promise<MemoryKill | undefined>;
|
|
284
395
|
/** Delete the Sandbox CR. Absent is success. */
|
|
@@ -348,12 +459,11 @@ export type WorkspaceWait = PlacingWait & { since: string };
|
|
|
348
459
|
export const WORKSPACE_WAIT_KEY = "wait";
|
|
349
460
|
|
|
350
461
|
type WsContext = {
|
|
351
|
-
/** The wrapper's
|
|
462
|
+
/** The wrapper's input, which the body gets (plus `workspace`). Under a parent, `place` replaces
|
|
463
|
+
* it with the door's parsed value. */
|
|
352
464
|
runInput: Record<string, unknown>;
|
|
353
465
|
/** This invocation's stable identity (the actor id the parent invoked/spawned us under). */
|
|
354
466
|
wsId: string;
|
|
355
|
-
/** The resolved spec — computed once from input and persisted, like any other context data. */
|
|
356
|
-
spec: WorkspaceSpec;
|
|
357
467
|
/** Every slot's resolved Binding, by slot, in declaration order — assigned at provision. */
|
|
358
468
|
bindings?: Record<string, ResolvedBinding>;
|
|
359
469
|
endpoint?: string;
|
|
@@ -363,6 +473,9 @@ type WsContext = {
|
|
|
363
473
|
/** Persisted in context so the registrar can re-publish them on restore. */
|
|
364
474
|
handles?: MechanismHandles;
|
|
365
475
|
output?: unknown;
|
|
476
|
+
/** The body's Failure (ADR-0073), as plain data: kept while `failing` destroys, so a restore
|
|
477
|
+
* there sends the same Failure up. */
|
|
478
|
+
failure?: FailureInput;
|
|
366
479
|
};
|
|
367
480
|
|
|
368
481
|
/**
|
|
@@ -383,12 +496,14 @@ type WsContext = {
|
|
|
383
496
|
* Repo Slots it carries, because a default would widen exactly where the phantom exists to refuse
|
|
384
497
|
* — `JR2Repos<string>` offers `customize()` every key, and `AnyStateMachine` as the body offers no
|
|
385
498
|
* Agent at all. `PoolMachine` names its worker the same way.
|
|
499
|
+
*
|
|
500
|
+
* @internal
|
|
386
501
|
*/
|
|
387
502
|
export type WorkspaceMachine<TInput, TOutput, TBody extends AnyStateMachine, TSlots extends string> = StateMachine<
|
|
388
503
|
any,
|
|
389
504
|
any,
|
|
390
505
|
any,
|
|
391
|
-
WrapperActors<"body", TBody, "place" | "provision" | "attach" | "registrar" | "lease" | "destroy">,
|
|
506
|
+
WrapperActors<"body", TBody, "place" | "provision" | "attach" | "registrar" | "lease" | "destroy" | "fail">,
|
|
392
507
|
any,
|
|
393
508
|
any,
|
|
394
509
|
any,
|
|
@@ -435,8 +550,29 @@ export type WorkspaceMachine<TInput, TOutput, TBody extends AnyStateMachine, TSl
|
|
|
435
550
|
*/
|
|
436
551
|
// The handles are keyed by the wrapper's DECLARED slots (ADR-0051), so a body demanding a slot the
|
|
437
552
|
// wrapper never declared is refused here too; demanding fewer is fine, as with any other field.
|
|
553
|
+
//
|
|
554
|
+
// The provided side holds the raw door AND its `Workspaced` form. `Workspaced` removes the
|
|
555
|
+
// `Record<string, never>` that `z.infer` gives the empty door, so `Workspaced` alone no longer
|
|
556
|
+
// satisfies a body that writes that type itself (`z.infer<typeof door>`, alone or with the
|
|
557
|
+
// handles). The raw door keeps those bodies legal, and the stripped form keeps a body that
|
|
558
|
+
// declares `Workspaced<…>` legal. A key that the door does not carry is still refused.
|
|
559
|
+
/** The input of a door-less Workspace: the body's, without the handles. `unknown` when the body
|
|
560
|
+
* asks for nothing else, and with `undefined` added when each field it asks for is optional. In
|
|
561
|
+
* both cases a parent has nothing that it must give, and xstate makes an invoke's `input` optional
|
|
562
|
+
* only for a type that holds `undefined`.
|
|
563
|
+
* @internal */
|
|
564
|
+
type DoorlessInput<TBody extends AnyStateMachine> =
|
|
565
|
+
unknown extends InputFrom<TBody>
|
|
566
|
+
? unknown
|
|
567
|
+
: [keyof Omit<InputFrom<TBody>, "workspace">] extends [never]
|
|
568
|
+
? unknown
|
|
569
|
+
: {} extends Omit<InputFrom<TBody>, "workspace">
|
|
570
|
+
? Omit<InputFrom<TBody>, "workspace"> | undefined
|
|
571
|
+
: Omit<InputFrom<TBody>, "workspace">;
|
|
572
|
+
|
|
573
|
+
/** @internal */
|
|
438
574
|
type BodyAcceptsDoor<TBody extends AnyStateMachine, TDoor, TSlots extends string> =
|
|
439
|
-
Workspaced<TDoor, TSlots> & HostInjectedInput extends InputFrom<TBody>
|
|
575
|
+
TDoor & Workspaced<TDoor, TSlots> & HostInjectedInput extends InputFrom<TBody>
|
|
440
576
|
? BodyNamesNoSlotUnderOpenMap<TBody, TSlots>
|
|
441
577
|
: { "the body's declared input must accept the door plus the injected handles": Workspaced<TDoor, TSlots> };
|
|
442
578
|
|
|
@@ -467,6 +603,8 @@ type BodyNamesNoSlotUnderOpenMap<TBody extends AnyStateMachine, TSlots extends s
|
|
|
467
603
|
* is the same one-direction assignability, on the `workspace` field alone: the handles the wrapper
|
|
468
604
|
* will inject must satisfy what the body declares for them, so a body that names fewer slots, or
|
|
469
605
|
* no handles at all, is fed a superset and passes, as ever.
|
|
606
|
+
*
|
|
607
|
+
* @internal
|
|
470
608
|
*/
|
|
471
609
|
type BodyAcceptsSlots<TBody extends AnyStateMachine, TSlots extends string> =
|
|
472
610
|
InputFrom<TBody> extends { workspace: infer THandles }
|
|
@@ -475,141 +613,452 @@ type BodyAcceptsSlots<TBody extends AnyStateMachine, TSlots extends string> =
|
|
|
475
613
|
: { "the body's declared handles must accept the Repo Slots the wrapper declares": WorkspaceHandles<TSlots> }
|
|
476
614
|
: unknown;
|
|
477
615
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
*/
|
|
616
|
+
// What the Sandbox is MADE OF (ADR-0037, ADR-0005) and which Repos it attaches (ADR-0051), as
|
|
617
|
+
// STATIC options on the wrapper rather than a function of the run input (ADR-0049). Static is the
|
|
618
|
+
// whole point: `jr2 up` walks the registered Machines to find every `file:` context and build it,
|
|
619
|
+
// every bound Repo and warm it, every open slot and refuse it (parts.ts) — and no walk can evaluate a
|
|
620
|
+
// function of run input. They are also never persisted — the provisioning state
|
|
621
|
+
// re-reads them off the Machine it was invoked as, so a restore, a `provide()` and a `customize()`
|
|
622
|
+
// all get the image and the slots the Machine carries NOW.
|
|
623
|
+
//
|
|
624
|
+
// Each image is one string in ADR-0037's two shapes: a `file:` URL to a docker context the
|
|
625
|
+
// Machine's module ships (`import.meta.resolve("./image")`), or a registry ref its owner baked and
|
|
626
|
+
// hosts.
|
|
627
|
+
//
|
|
628
|
+
// `TSlots` has no default, here and on the two option types below (ADR-0050): a value annotated
|
|
629
|
+
// with a `string` slot set types `repos` as `Record<string, RepoSlot>`, under which a body naming
|
|
630
|
+
// any slot passes the wrapper's check — the widening WorkspaceHandles refuses for the
|
|
631
|
+
// same reason. The `workspace()` overloads infer it; an annotation names it.
|
|
632
|
+
/** The options of a `workspace()` that say what its Sandbox is made of and which Repos it
|
|
633
|
+
* clones. They are the same for each run. */
|
|
496
634
|
export type SandboxOptions<TSlots extends string, TInput = unknown> = {
|
|
497
|
-
/** The Sandbox Image
|
|
635
|
+
/** The Sandbox Image: the image of the container where the Agent's Working tools and
|
|
636
|
+
* `command` Effects run. A `file:` URL to a docker context that your module ships
|
|
637
|
+
* (`import.meta.resolve("./image")`), which `jr2 up` builds, or a registry ref. Its floor is
|
|
638
|
+
* glibc and git; jr2 adds its own runtime at `/opt/jr2`. Default: the Instance's
|
|
639
|
+
* `images/default`, then jr2's Harness image. */
|
|
498
640
|
image?: string;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
641
|
+
// A gid is a fact of the images, so it is static.
|
|
642
|
+
/** The group id that owns the files in `/work`. Default: 2000. A non-negative integer. */
|
|
643
|
+
workGroup?: number;
|
|
644
|
+
// The widening ADR-0005 foresaw: `{ image, resources }` states the seat's split of the Size
|
|
645
|
+
// (ADR-0060). There is no default, because the seat's whole identity is "what jr2 does not own"
|
|
646
|
+
// and jr2 has nothing to put there. Env and ports are deliberately not forwarded.
|
|
647
|
+
/** The User Container: a third container in the Sandbox, for a service that must run beside the
|
|
648
|
+
* Agent, such as an sshd. Its image, in the same two forms as `image`, or `{ image, resources }`
|
|
649
|
+
* to give it a part of the Size. It runs its own entrypoint, with `/work` mounted. jr2 adds
|
|
650
|
+
* nothing to it. Default: none. Without `resources`, it has no limit of its own and shares the
|
|
651
|
+
* Size of the Sandbox. */
|
|
504
652
|
user?: string | UserContainer;
|
|
653
|
+
// Agent Substrate's template size (ADR-0060), so the Workspace moves there unchanged. A fact of
|
|
654
|
+
// the Machine, like its image: the author knows the toolchain, the runner, the worker count.
|
|
505
655
|
/**
|
|
506
|
-
* The Size
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
* a
|
|
511
|
-
*
|
|
656
|
+
* The Size: the memory and cpu limit of the whole Sandbox, `{ limits: { memory, cpu } }`, such as
|
|
657
|
+
* `{ limits: { memory: "4Gi", cpu: "2" } }`. jr2 reserves all of it on the node: each request
|
|
658
|
+
* equals its limit. jr2 also enforces the cpu limit. Only `limits`,
|
|
659
|
+
* and in it only `memory` and `cpu`, are accepted. Default: the Instance's `sandbox.resources`,
|
|
660
|
+
* then 2Gi and 1 cpu. Under a Size that you give, a field you leave out takes the kit's value,
|
|
661
|
+
* not the Instance's. The Instance that registers the Machine can change it with
|
|
662
|
+
* `customize(machine, { resources })`.
|
|
512
663
|
*/
|
|
513
664
|
resources?: Size;
|
|
665
|
+
// The handles keep this map's order, and the kit reads nothing into it (ADR-0051). Required
|
|
666
|
+
// here, at least one; a Workspace with no Repo Slot leaves it out, which is the
|
|
667
|
+
// `NoSlotWorkspaceOptions` overload, not an empty map. `repos: open` is the shape a packaged
|
|
668
|
+
// Machine takes when its body enumerates its checkouts rather than naming them
|
|
669
|
+
// (`@jr2/machines`'s `task`, whose first slot is the one the coder edits).
|
|
514
670
|
/**
|
|
515
|
-
* The Repo Slots
|
|
516
|
-
* `workspace.repos`
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
671
|
+
* The Repo Slots: the repositories that the Sandbox clones, each under a name of your Machine's
|
|
672
|
+
* choice. The name is the key of `workspace.repos` in the body and the directory under `/work`.
|
|
673
|
+
* At least one slot. Leave `repos` out for a Workspace with no Repo Slot (see
|
|
674
|
+
* {@link NoSlotWorkspaceOptions}). Each value is one of:
|
|
675
|
+
* - a url, or `{ url, ref }`, where `ref` is the branch, the tag or the commit that the clone
|
|
676
|
+
* stays on, and that a new branch starts from (default: the Repo's default branch);
|
|
677
|
+
* - `open`, for the Instance that registers the Machine to bind with `customize()`;
|
|
678
|
+
* - a function of the run input, `({ input }) => url` or `{ url, ref }`, when each run chooses.
|
|
520
679
|
*
|
|
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.
|
|
680
|
+
* Or `repos: open` for the whole map: the Machine names no slot, and the Instance that registers
|
|
681
|
+
* it names each one with `customize()`. The body's input is then `Workspaced<RunInput, string>`.
|
|
525
682
|
*/
|
|
526
683
|
repos: Record<TSlots, RepoSlot<TInput>> | typeof open;
|
|
684
|
+
// Naming is placement (ADR-0059): a pod gets the Held secrets something in it names — this
|
|
685
|
+
// list, and the model keys of the Agents whose Turns run here, which their providers claim
|
|
686
|
+
// without being listed.
|
|
527
687
|
/**
|
|
528
|
-
* The Held secrets this
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
688
|
+
* The Held secrets that this Sandbox can use: names from the `secrets` list in `jr2.config.ts`.
|
|
689
|
+
* The Sandbox's Custodian holds each value and puts it into requests to the hosts of that Held
|
|
690
|
+
* secret. Listing a name also gives its Stand-in to the Agent's programs — each `bash` call, test,
|
|
691
|
+
* build and `command` — in their env under that name, so code in `/work` can spend it (ADR-0076).
|
|
692
|
+
* You need not list the model key of an Agent that runs here: its provider claims it, and the
|
|
693
|
+
* Harness gives it to the model calls alone. List the model key only when code in `/work` must
|
|
694
|
+
* call the model itself: such a call is outside the Turn's usage record. Each name once.
|
|
532
695
|
*/
|
|
533
696
|
secrets?: readonly string[];
|
|
697
|
+
// The container exists only when an Effect below this `workspace()` is placed there (ADR-0068,
|
|
698
|
+
// ADR-0005). It runs as its own `USER`.
|
|
534
699
|
/**
|
|
535
|
-
* The Sandbox Effector
|
|
536
|
-
* `push`,
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
700
|
+
* The image of the Sandbox Effector. This container runs three kinds of Effect for this
|
|
701
|
+
* Workspace: a `push`, an `http` Effect with a Worktree file as its body, and a `command` with an
|
|
702
|
+
* Effect secret in its `env`. It gets the Effect secrets that those Effects name. Give an image
|
|
703
|
+
* when those Effects need tools or credentials that you keep out of the Sandbox Image, such as
|
|
704
|
+
* `terraform`. A `file:` URL or a registry ref, as for `image`. Default: the Sandbox Image,
|
|
705
|
+
* which already holds `git`.
|
|
541
706
|
*/
|
|
707
|
+
// The default is the Sandbox Image, whose floor already holds `git` (ADR-0037).
|
|
542
708
|
effector?: { image: string };
|
|
543
709
|
};
|
|
544
710
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
*/
|
|
711
|
+
// How a Workspace with a declared door is configured (ADR-0012, ADR-0033) — the wrapper's own
|
|
712
|
+
// run-input schema, what the pod is made of, and the mapping from what comes through the door to
|
|
713
|
+
// workspace vocabulary.
|
|
714
|
+
/** The options of a `workspace()` that declares the input that starts a run. */
|
|
550
715
|
export type WorkspaceOptions<TSchema extends z.ZodObject, TSlots extends string> = SandboxOptions<
|
|
551
716
|
TSlots,
|
|
552
717
|
z.infer<TSchema>
|
|
553
718
|
> & {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
*
|
|
719
|
+
// Deliberately NOT the body's schema (ADR-0033): the body is fed the run input PLUS the injected
|
|
720
|
+
// `workspace` handles ({@link Workspaced}), which no caller can send, so the body's contract is
|
|
721
|
+
// the door plus something that does not exist yet. Symmetric with `PoolSpec.input`.
|
|
722
|
+
/** The input that starts a run, as a `z.object`. jr2 checks each start against it. It also types
|
|
723
|
+
* the `input` of a per-run Repo Slot, and the body must accept it plus the
|
|
724
|
+
* `workspace` handles. Write it here, not on the body. */
|
|
559
725
|
input: TSchema;
|
|
560
|
-
/** Map what came through the door to the workspace-domain spec: what to attach, on what ref,
|
|
561
|
-
* on which branch (ADR-0012's boundary — workflow configuration never enters it). */
|
|
562
|
-
spec: (args: { input: z.infer<TSchema> }) => WorkspaceSpec;
|
|
563
726
|
};
|
|
564
727
|
|
|
728
|
+
// How a Workspace with NO declared door is configured: absence is permissive (ADR-0033), so jr2
|
|
729
|
+
// has nothing to infer from and says `unknown` rather than `any` — an honest "jr2 does not know",
|
|
730
|
+
// which the mapper must narrow before it reads a field.
|
|
565
731
|
/**
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
732
|
+
* The options of a `workspace()` with no `input`: it starts with any input. The `input` of a
|
|
733
|
+
* per-run Repo Slot is then `unknown`, and the mapper must check it before it reads a field.
|
|
734
|
+
*
|
|
735
|
+
* Use it for a Workspace that a Pool or another Machine starts. The body's `input` type says what
|
|
736
|
+
* that input is, and that also types the input of the Workspace. For a Workspace that a caller
|
|
737
|
+
* starts, declare `input`.
|
|
572
738
|
*/
|
|
573
739
|
export type PermissiveWorkspaceOptions<TSlots extends string, TInput = unknown> = SandboxOptions<TSlots, TInput> & {
|
|
574
|
-
|
|
575
|
-
|
|
740
|
+
// Spelled out so a declared schema can never fall through to the permissive overload, where the
|
|
741
|
+
// body's door would go unchecked.
|
|
742
|
+
/** Not present: a `workspace()` without `input` starts with any input. */
|
|
743
|
+
input?: never;
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// A Workspace with no Repo Slot (ADR-0051): a Sandbox for one unit of work whose Agents and
|
|
747
|
+
// Effects share `/work`, and no Worktree is in it — the steps of one job. Its own option types, not a
|
|
748
|
+
// conditional on `TSlots`, because the slot overloads infer `TSlots` from `repos`, and no
|
|
749
|
+
// inference reaches through a conditional type.
|
|
750
|
+
/** The options of a `workspace()` with no Repo Slot that declares the input that starts a run.
|
|
751
|
+
* `repos` is left out. */
|
|
752
|
+
export type NoSlotWorkspaceOptions<TSchema extends z.ZodObject> = Omit<SandboxOptions<never>, "repos"> & {
|
|
753
|
+
/** Not present: the Workspace has no Repo Slot. Its Agents work in `/work`. */
|
|
754
|
+
repos?: never;
|
|
755
|
+
/** The input that starts a run, as a `z.object`, as in {@link WorkspaceOptions}. */
|
|
756
|
+
input: TSchema;
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
/** The options of a `workspace()` with no Repo Slot and no `input`: it starts with any input. */
|
|
760
|
+
export type PermissiveNoSlotWorkspaceOptions = Omit<SandboxOptions<never>, "repos"> & {
|
|
761
|
+
/** Not present: the Workspace has no Repo Slot. Its Agents work in `/work`. */
|
|
762
|
+
repos?: never;
|
|
763
|
+
/** Not present: a `workspace()` without `input` starts with any input. */
|
|
576
764
|
input?: never;
|
|
577
|
-
spec: (args: { input: TInput }) => WorkspaceSpec;
|
|
578
765
|
};
|
|
579
766
|
|
|
580
767
|
/**
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
768
|
+
* Run a body Machine in a Sandbox: a pod of its own, with a clone of each Repo Slot. The result is
|
|
769
|
+
* a Machine. Register it as a Workflow, or invoke it from another Machine as
|
|
770
|
+
* an actor slot.
|
|
771
|
+
*
|
|
772
|
+
* When a run enters it, the Workspace makes the Sandbox and waits for room on a node, with no
|
|
773
|
+
* deadline. It clones each Repo Slot to `/work/<slot>/default`, on the Binding's base, and makes no
|
|
774
|
+
* Worktree: the body cuts each Worktree that it needs with `worktree()`. A slot that is only read
|
|
775
|
+
* needs no cut. No work happens in `default/`. Then it runs the body with the run input plus
|
|
776
|
+
* `workspace`, the handles (see {@link Workspaced}). The Agents with Working tools and the `command` Effects in the body run in that Sandbox. An Agent
|
|
777
|
+
* with `workspace: "none"` runs on the Instance Harness. When the body reaches a final state, the
|
|
778
|
+
* Workspace deletes the Sandbox, and its output is the body's output. Push each branch before
|
|
779
|
+
* then: jr2 deletes the Sandbox's commits with it.
|
|
780
|
+
*
|
|
781
|
+
* The author chooses how many Agents work in a Workspace. Agents in one Worktree share git, as
|
|
782
|
+
* people in one checkout do. Each coder that needs its own branch gets its own Worktree of the one
|
|
783
|
+
* clone.
|
|
784
|
+
*
|
|
785
|
+
* Leave out `repos` for a Workspace with no Repo Slot: its Agents and `command` Effects share
|
|
786
|
+
* `/work`, as the steps of one job share a folder. The body's input is then `Workspaced<RunInput, never>`. An `http` Effect sends a
|
|
787
|
+
* file of `/work` with `file(path)`. A `worktree()`, a `push`, and a `command` with an Effect secret in its
|
|
788
|
+
* `env`, need a Repo Slot, so jr2 refuses them there. jr2 deletes `/work` with the Sandbox: send what
|
|
789
|
+
* must stay before the body ends.
|
|
790
|
+
*
|
|
791
|
+
* Give each run a different branch, so that two runs do not push to one branch. Make it from a
|
|
792
|
+
* field of the run input, or from `instanceId` when the run input has no field that is different
|
|
793
|
+
* for each run: ``branch: `fix/${context.instanceId}` `` in the input of the cut. Add
|
|
794
|
+
* `HostInjectedInput` to the body's input type to read it. Only a root `workspace()` gets `instanceId` from jr2. A `workspace()` that a different Machine invokes gets
|
|
795
|
+
* it only when its parent gives it.
|
|
796
|
+
*
|
|
797
|
+
* The Sandbox is Lost when its pod is evicted or its node goes. Then the body gets the event
|
|
798
|
+
* `workspace.lost`, and the body decides what to do. jr2 does not make the Lost Sandbox again.
|
|
799
|
+
*
|
|
800
|
+
* When the body fails (it invokes a `fail()` slot), the Workspace deletes the Sandbox, and then the
|
|
801
|
+
* same Failure goes up to the parent. With no parent to catch it, the run ends `failed`. When the
|
|
802
|
+
* body has a defect (an error that is not a Failure), the run faults: it ends `error`, and the
|
|
803
|
+
* Sandbox stays for you to inspect, until jr2's operator removes it after an idle timeout.
|
|
804
|
+
*
|
|
805
|
+
* Give the run input to `input`, not to the body: a body with an `input` schema of its own is
|
|
806
|
+
* refused. The types refuse a body whose input does not accept the run input plus the handles,
|
|
807
|
+
* with the message `the body's declared input must accept the door plus the injected handles`.
|
|
808
|
+
*
|
|
809
|
+
* It refuses, when you call it:
|
|
810
|
+
* - a body with a state that is not final, when that state and each state above it have no
|
|
811
|
+
* `workspace.lost` route: ``workspace(): the body "<id>" has a state "<state>" that is not final,
|
|
812
|
+
* and no route for "workspace.lost" is on it or on a state above — jr2 sends workspace.lost to the
|
|
813
|
+
* body when its Sandbox is gone, and with no route the run faults: route it on the body's root,
|
|
814
|
+
* as on: { "workspace.lost": "<the state that handles it>" } (ADR-0074)``
|
|
815
|
+
* - a body with its own `input`: ``workspace(): the body "<id>" declares its own run input, which
|
|
816
|
+
* nothing will ever serve or enforce — the wrapper feeds the body the run input PLUS the injected
|
|
817
|
+
* `workspace` handles, so the body's schema is not the door. Move it to the wrapper:
|
|
818
|
+
* workspace(body, { input }) (ADR-0033).``
|
|
819
|
+
* - an empty `image`: ``workspace(): `image` must be a non-empty string (got <value>) — either a
|
|
820
|
+
* `file:` URL to a docker context this module ships (`import.meta.resolve("./image")`) or a
|
|
821
|
+
* registry ref (ADR-0037).``
|
|
822
|
+
* - an empty `repos`: ``workspace(): `repos: {}` names no Repo Slot — leave `repos` out for a
|
|
823
|
+
* Workspace with no Repo Slot, or name a slot: `repos: { app: "https://…" }` (ADR-0051).``
|
|
824
|
+
* - a `repos` that is not a map or `open`: ``workspace(): `repos` must be a map of Repo Slots —
|
|
825
|
+
* `repos: { app: "https://…" }` — or `open` whole, for a map the consumer names; leave it out
|
|
826
|
+
* for a Workspace with no Repo Slot (ADR-0051).``
|
|
827
|
+
* - a bad Repo Slot name or value (see {@link RepoSlot})
|
|
828
|
+
* - a `workGroup` that is not a gid: ``workspace(): `workGroup` must be a gid (got <value>)``
|
|
829
|
+
* - a bad `secrets` list: ``workspace(): `secrets` must be a list of distinct names from
|
|
830
|
+
* jr2.config.ts's `secrets` list (got <value>) — secrets: ["JIRA_TOKEN"] (ADR-0059).``
|
|
831
|
+
* - a bad `effector`: ``workspace(): `effector` is { image } — a `file:` URL to a docker context
|
|
832
|
+
* or a registry ref (got <value>) — effector: { image: import.meta.resolve("./effector") }
|
|
833
|
+
* (ADR-0068).``
|
|
834
|
+
* - a `user` that is not an image or `{ image, resources? }`: ``workspace(): `user` must be a
|
|
835
|
+
* non-empty string (an image), or { image, resources? } (got <value>) — either a `file:` URL to a
|
|
836
|
+
* docker context this module ships (`import.meta.resolve("./image")`) or a registry ref
|
|
837
|
+
* (ADR-0037, ADR-0060).``, or ``workspace(): user.<key> is not accepted — the User Container is {
|
|
838
|
+
* image, resources? } (ADR-0005, ADR-0060)`` (see {@link UserContainer})
|
|
839
|
+
* - a `resources` key other than `limits` or `requests`: ``workspace(): resources.<key> is not accepted — a
|
|
840
|
+
* Size is { limits: { memory?, cpu? } } and nothing else — Agent Substrate's template size
|
|
841
|
+
* (ADR-0060)``, or the same text for `resources.limits.<key>`. The other texts are in
|
|
842
|
+
* {@link Size}.
|
|
843
|
+
*
|
|
844
|
+
* And when a run enters it, a per-run Repo Slot's mapper that gives no `url`, or a bad `ref`:
|
|
845
|
+
* ``workspace invalid: repos.<slot> mapper returned <fields> — the mapper derives from run
|
|
846
|
+
* input; does `jr2 run --input` carry every field this workflow's workspace() slot reads?``
|
|
847
|
+
*
|
|
848
|
+
* @param body - The Machine to run in the Sandbox. Its input is `Workspaced<RunInput, Slots>`.
|
|
849
|
+
* @param options - What the Sandbox is made of (`image`, `user`, `resources`, `effector`), the
|
|
850
|
+
* Repo Slots it clones (`repos`), the Held secrets it can use (`secrets`), the pod's `fsGroup`
|
|
851
|
+
* (`workGroup`), and the run input (`input`).
|
|
852
|
+
* @returns A Machine whose input is the run input and whose output is the body's output.
|
|
853
|
+
*
|
|
854
|
+
* @example
|
|
855
|
+
* ```ts
|
|
856
|
+
* import { z } from "zod";
|
|
857
|
+
* import {
|
|
858
|
+
* command,
|
|
859
|
+
* fail,
|
|
860
|
+
* isReady,
|
|
861
|
+
* jr2Setup,
|
|
862
|
+
* workspace,
|
|
863
|
+
* worktree,
|
|
864
|
+
* type CommandOutput,
|
|
865
|
+
* type Workspaced,
|
|
866
|
+
* } from "@jr2/orchestrator";
|
|
867
|
+
*
|
|
868
|
+
* const runInput = z.object({ ticket: z.string(), repo: z.string() });
|
|
869
|
+
* type BodyInput = Workspaced<z.infer<typeof runInput>, "app" | "docs">;
|
|
870
|
+
*
|
|
871
|
+
* // The Worktree of this run. An Agent or a command that only reads `docs` uses `/work/docs/default`.
|
|
872
|
+
* const buildTree = (context: BodyInput) => `${context.workspace.repos.app}/build` as const;
|
|
873
|
+
*
|
|
874
|
+
* const passed = (output: CommandOutput): boolean =>
|
|
875
|
+
* output.outcome === "exited" && output.code === 0;
|
|
876
|
+
*
|
|
877
|
+
* const body = jr2Setup({
|
|
878
|
+
* types: {} as { input: BodyInput; context: BodyInput },
|
|
879
|
+
* actors: {
|
|
880
|
+
* cut: worktree(),
|
|
881
|
+
* build: command(["npm", "run", "build"]),
|
|
882
|
+
* fail: fail(),
|
|
883
|
+
* },
|
|
884
|
+
* }).createMachine({
|
|
885
|
+
* id: "build",
|
|
886
|
+
* context: ({ input }) => input,
|
|
887
|
+
* on: { "workspace.lost": ".lost" },
|
|
888
|
+
* initial: "cutting",
|
|
889
|
+
* states: {
|
|
890
|
+
* cutting: {
|
|
891
|
+
* invoke: {
|
|
892
|
+
* src: "cut",
|
|
893
|
+
* input: ({ context }) => ({ path: buildTree(context), branch: `fix/${context.ticket}` }),
|
|
894
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "building" }, { target: "notCut" }],
|
|
895
|
+
* },
|
|
896
|
+
* },
|
|
897
|
+
* building: {
|
|
898
|
+
* invoke: {
|
|
899
|
+
* src: "build",
|
|
900
|
+
* input: ({ context }) => ({ worktree: buildTree(context) }),
|
|
901
|
+
* onDone: [
|
|
902
|
+
* { guard: ({ event }) => passed(event.output), target: "built" },
|
|
903
|
+
* { target: "broken" },
|
|
904
|
+
* ],
|
|
905
|
+
* },
|
|
906
|
+
* },
|
|
907
|
+
* built: { type: "final" },
|
|
908
|
+
* broken: { invoke: { src: "fail", input: { reason: "npm run build did not pass" } } },
|
|
909
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
910
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
911
|
+
* },
|
|
912
|
+
* });
|
|
913
|
+
*
|
|
914
|
+
* export const machine = workspace(body, {
|
|
915
|
+
* input: runInput,
|
|
916
|
+
* image: "docker.io/library/node:24",
|
|
917
|
+
* resources: { limits: { memory: "4Gi", cpu: "2" } },
|
|
918
|
+
* repos: {
|
|
919
|
+
* app: ({ input }) => input.repo,
|
|
920
|
+
* docs: { url: "https://github.com/you/docs.git", ref: "main" },
|
|
921
|
+
* },
|
|
922
|
+
* });
|
|
923
|
+
* ```
|
|
924
|
+
*
|
|
925
|
+
* @example
|
|
926
|
+
* ```ts
|
|
927
|
+
* import { z } from "zod";
|
|
928
|
+
* import {
|
|
929
|
+
* command,
|
|
930
|
+
* fail,
|
|
931
|
+
* isReady,
|
|
932
|
+
* jr2Setup,
|
|
933
|
+
* workspace,
|
|
934
|
+
* worktree,
|
|
935
|
+
* type CommandOutput,
|
|
936
|
+
* type HostInjectedInput,
|
|
937
|
+
* type Workspaced,
|
|
938
|
+
* } from "@jr2/orchestrator";
|
|
939
|
+
*
|
|
940
|
+
* const passed = (output: CommandOutput): boolean =>
|
|
941
|
+
* output.outcome === "exited" && output.code === 0;
|
|
942
|
+
*
|
|
943
|
+
* // The run input has no field that is different for each run, so the branch comes from
|
|
944
|
+
* // `instanceId`.
|
|
945
|
+
* const runInput = z.object({});
|
|
946
|
+
* type BodyInput = Workspaced<z.infer<typeof runInput> & HostInjectedInput, "app">;
|
|
947
|
+
*
|
|
948
|
+
* const nightlyTree = (context: BodyInput) => `${context.workspace.repos.app}/nightly` as const;
|
|
949
|
+
*
|
|
950
|
+
* const body = jr2Setup({
|
|
951
|
+
* types: {} as { input: BodyInput; context: BodyInput },
|
|
952
|
+
* actors: { cut: worktree(), test: command(["npm", "test"]), fail: fail() },
|
|
953
|
+
* }).createMachine({
|
|
954
|
+
* id: "nightly",
|
|
955
|
+
* context: ({ input }) => input,
|
|
956
|
+
* on: { "workspace.lost": ".lost" },
|
|
957
|
+
* initial: "cutting",
|
|
958
|
+
* states: {
|
|
959
|
+
* cutting: {
|
|
960
|
+
* invoke: {
|
|
961
|
+
* src: "cut",
|
|
962
|
+
* input: ({ context }) => ({ path: nightlyTree(context), branch: `nightly/${context.instanceId}` }),
|
|
963
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "testing" }, { target: "notCut" }],
|
|
964
|
+
* },
|
|
965
|
+
* },
|
|
966
|
+
* testing: {
|
|
967
|
+
* invoke: {
|
|
968
|
+
* src: "test",
|
|
969
|
+
* input: ({ context }) => ({ worktree: nightlyTree(context) }),
|
|
970
|
+
* onDone: [
|
|
971
|
+
* { guard: ({ event }) => passed(event.output), target: "tested" },
|
|
972
|
+
* { target: "red" },
|
|
973
|
+
* ],
|
|
974
|
+
* },
|
|
975
|
+
* },
|
|
976
|
+
* tested: { type: "final" },
|
|
977
|
+
* red: { invoke: { src: "fail", input: { reason: "npm test did not pass" } } },
|
|
978
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
979
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
980
|
+
* },
|
|
981
|
+
* });
|
|
982
|
+
*
|
|
983
|
+
* export const machine = workspace(body, {
|
|
984
|
+
* input: runInput,
|
|
985
|
+
* image: "docker.io/library/node:24",
|
|
986
|
+
* repos: { app: "https://github.com/you/app.git" },
|
|
987
|
+
* });
|
|
988
|
+
* ```
|
|
989
|
+
*
|
|
990
|
+
* @see docs/concepts/workspaces.md
|
|
991
|
+
* @see docs/guides/write-a-workflow.md
|
|
992
|
+
* @see docs/guides/size-a-workspace.md
|
|
993
|
+
* @see docs/guides/add-tools-to-the-sandbox.md
|
|
994
|
+
* @see docs/reference/decisions/0012-workspace-wrapper-machine.md
|
|
995
|
+
* @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
|
|
587
996
|
*/
|
|
588
|
-
|
|
997
|
+
// `TSlots` defaults to `never`: with `repos` left out there is nothing to infer it from, and the
|
|
998
|
+
// Workspace has no Repo Slot (ADR-0051). The no-slot options are a union member, not overloads of
|
|
999
|
+
// their own, so a call with slots reports its error where it always did.
|
|
1000
|
+
export function workspace<TSchema extends z.ZodObject, TBody extends AnyStateMachine, TSlots extends string = never>(
|
|
589
1001
|
body: TBody & BodyAcceptsDoor<TBody, z.infer<TSchema>, TSlots>,
|
|
590
|
-
options: WorkspaceOptions<TSchema, TSlots>,
|
|
591
|
-
|
|
592
|
-
|
|
1002
|
+
options: WorkspaceOptions<TSchema, TSlots> | NoSlotWorkspaceOptions<TSchema>,
|
|
1003
|
+
// The input is the door without the empty door's `Record<string, never>` (`{}` for `z.object({})`), so
|
|
1004
|
+
// a parent can give `instanceId` to a nested body that reads it (ADR-0033).
|
|
1005
|
+
): WorkspaceMachine<WithoutEmptyIndex<z.infer<TSchema>>, OutputFrom<TBody>, TBody, TSlots>;
|
|
1006
|
+
/**
|
|
1007
|
+
* Run a body Machine in a Sandbox, with no `input` schema: the Workspace starts with any input. Use
|
|
1008
|
+
* it for a Workspace that a Pool or another Machine starts. {@link workspace} tells the rest, and
|
|
1009
|
+
* what it refuses.
|
|
1010
|
+
*
|
|
1011
|
+
* @param body - The Machine to run in the Sandbox. Its input is the input of the Workspace plus
|
|
1012
|
+
* `workspace`, the handles.
|
|
1013
|
+
* @param options - The options of the Sandbox. There is no `input`.
|
|
1014
|
+
* @returns A Machine whose input is the run input and whose output is the body's output.
|
|
1015
|
+
*
|
|
1016
|
+
* @see docs/concepts/workspaces.md
|
|
1017
|
+
* @see docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md
|
|
1018
|
+
*/
|
|
1019
|
+
export function workspace<
|
|
1020
|
+
TBody extends AnyStateMachine,
|
|
1021
|
+
TSlots extends string = never,
|
|
1022
|
+
// Nothing else annotates the input of a door-less Workspace, so the body says it.
|
|
1023
|
+
TInput = DoorlessInput<TBody>,
|
|
1024
|
+
>(
|
|
593
1025
|
body: TBody & BodyAcceptsSlots<TBody, TSlots>,
|
|
594
|
-
|
|
1026
|
+
// `undefined` is for the invoke alone: a run with no input gives a mapper `{}`.
|
|
1027
|
+
options: PermissiveWorkspaceOptions<TSlots, Exclude<TInput, undefined>> | PermissiveNoSlotWorkspaceOptions,
|
|
595
1028
|
): WorkspaceMachine<TInput, OutputFrom<TBody>, TBody, TSlots>;
|
|
596
1029
|
export function workspace(
|
|
597
1030
|
body: AnyStateMachine,
|
|
598
|
-
options: SandboxOptions<string, any> & {
|
|
1031
|
+
options: Omit<SandboxOptions<string, any>, "repos"> & {
|
|
1032
|
+
repos?: SandboxOptions<string, any>["repos"];
|
|
1033
|
+
input?: z.ZodObject;
|
|
1034
|
+
},
|
|
599
1035
|
): AnyStateMachine {
|
|
1036
|
+
// The body receives the wrapper's input plus the handles — `Workspaced<TInput, TSlots>`, which is also what the
|
|
1037
|
+
// declared door checks the body against. A Failure of the body is caught: the wrapper destroys
|
|
1038
|
+
// the Sandbox and fails again with the same payload (ADR-0073). Every other body error (a defect)
|
|
1039
|
+
// is not caught: it faults the run (RunStatus.fault) and leaves the Sandbox to the operator's
|
|
1040
|
+
// idle-timeout GC, so the trail stays as evidence.
|
|
1041
|
+
//
|
|
600
1042
|
// A body that still declares its own run input is a dead declaration under the door design: the
|
|
601
1043
|
// wrapper never serves it, never validates against it, and feeds the body something it does not
|
|
602
1044
|
// describe. Silence would leave an author believing a contract that nothing enforces (ADR-0033).
|
|
603
|
-
|
|
1045
|
+
// A body that `setup()` made keeps its zod `input` in the config with no door attached, so the
|
|
1046
|
+
// check reads the config too: else the author loses the door with no message.
|
|
1047
|
+
if (declaresRunInput(body)) {
|
|
604
1048
|
throw new Error(
|
|
605
1049
|
`workspace(): the body "${body.id}" declares its own run input, which nothing will ever ` +
|
|
606
1050
|
"serve or enforce — the wrapper feeds the body the run input PLUS the injected `workspace` " +
|
|
607
1051
|
"handles, so the body's schema is not the door. Move it to the wrapper: " +
|
|
608
|
-
"workspace(body, { input
|
|
1052
|
+
"workspace(body, { input }) (ADR-0033).",
|
|
609
1053
|
);
|
|
610
1054
|
}
|
|
1055
|
+
// A body state that cannot route `workspace.lost` (ADR-0074): with no route, the body would show
|
|
1056
|
+
// healthy on the feed with no Sandbox. Here, because `workspace()` is the one place that knows
|
|
1057
|
+
// that a Machine is a body. A body that is the kit's own wrapper (a `workspace()`, a `pool()`) is
|
|
1058
|
+
// not judged: its states are jr2's, and the walk refuses a Workspace below a Workspace (ADR-0059).
|
|
1059
|
+
if (!composesSandbox(body) && wrapperBodyOf(body) === undefined) refuseUnroutedLost(body);
|
|
611
1060
|
// Static, so checkable NOW rather than at the first provision — an empty or non-string image is
|
|
612
|
-
//
|
|
1061
|
+
// a derives-from-a-typo bug, and this catches it one build earlier.
|
|
613
1062
|
const image = options.image;
|
|
614
1063
|
if (image !== undefined && (typeof image !== "string" || !image)) {
|
|
615
1064
|
throw new Error(
|
|
@@ -623,16 +1072,22 @@ export function workspace(
|
|
|
623
1072
|
// any key Substrate would refuse, is a dependency the move to it would drop.
|
|
624
1073
|
if (options.resources !== undefined) assertSize("workspace(): resources", options.resources);
|
|
625
1074
|
// 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
|
-
|
|
1075
|
+
// and every key is a directory name. Left out, the Workspace has no Repo Slot and its steps share
|
|
1076
|
+
// `/work`; written empty, it is refused, so "no slot" has one spelling and an empty map never
|
|
1077
|
+
// reads as a map someone forgot to fill. An Open map defers all of that to the `customize` that
|
|
1078
|
+
// fills it, which still asks for at least one: an Open map says the composer names the slots.
|
|
1079
|
+
const repos = options.repos ?? {};
|
|
630
1080
|
if (repos !== open) {
|
|
631
|
-
if (typeof repos !== "object" || repos === null || Array.isArray(repos)
|
|
1081
|
+
if (typeof repos !== "object" || repos === null || Array.isArray(repos)) {
|
|
632
1082
|
throw new Error(
|
|
633
|
-
'workspace(): `repos` must
|
|
634
|
-
"
|
|
635
|
-
|
|
1083
|
+
'workspace(): `repos` must be a map of Repo Slots — `repos: { app: "https://…" }` — or `open` whole, ' +
|
|
1084
|
+
"for a map the consumer names; leave it out for a Workspace with no Repo Slot (ADR-0051).",
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
if (options.repos !== undefined && Object.keys(repos).length === 0) {
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
"workspace(): `repos: {}` names no Repo Slot — leave `repos` out for a Workspace with no Repo Slot, " +
|
|
1090
|
+
'or name a slot: `repos: { app: "https://…" }` (ADR-0051).',
|
|
636
1091
|
);
|
|
637
1092
|
}
|
|
638
1093
|
for (const [slot, value] of Object.entries(repos)) assertRepoSlot("workspace()", slot, value);
|
|
@@ -664,7 +1119,10 @@ export function workspace(
|
|
|
664
1119
|
`${JSON.stringify(effector)}) — effector: { image: import.meta.resolve("./effector") } (ADR-0068).`,
|
|
665
1120
|
);
|
|
666
1121
|
}
|
|
667
|
-
const
|
|
1122
|
+
const workGroup = options.workGroup;
|
|
1123
|
+
assertWorkGroup(workGroup);
|
|
1124
|
+
if (repos === open || Object.keys(repos).length > 0) refuseUnnamedWorktrees(body);
|
|
1125
|
+
const wrapper = buildWorkspaceMachine(body);
|
|
668
1126
|
// The wrapper is TRANSPARENT to its body (ADR-0049): `customize(machine, { agents })` on a
|
|
669
1127
|
// Workspace means the Machine inside, so the composer never spells `body` and never has to know
|
|
670
1128
|
// that jr2 wrapped anything.
|
|
@@ -679,6 +1137,7 @@ export function workspace(
|
|
|
679
1137
|
...(options.user !== undefined ? { user: options.user } : {}),
|
|
680
1138
|
repos: repos === open ? open : { ...repos },
|
|
681
1139
|
...(options.resources !== undefined ? { resources: options.resources } : {}),
|
|
1140
|
+
...(workGroup !== undefined ? { workGroup } : {}),
|
|
682
1141
|
...(secrets !== undefined ? { secrets: [...secrets] } : {}),
|
|
683
1142
|
...(effector !== undefined ? { effector: { image: effector.image } } : {}),
|
|
684
1143
|
});
|
|
@@ -696,41 +1155,75 @@ export function workspace(
|
|
|
696
1155
|
}
|
|
697
1156
|
|
|
698
1157
|
/**
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
1158
|
+
* Each Turn and each Effect that works in a Worktree says which, in a Workspace with Repo Slots: the
|
|
1159
|
+
* walk's half of what `jr2Setup` refuses by type, for a plain `setup()`, for a nested key, and for a
|
|
1160
|
+
* body with no typed handles. It sees an invoke with no input and a literal input. A mapper's answer
|
|
1161
|
+
* is its start's to refuse. A Machine below the body is walked too: a child, and a Pool's worker,
|
|
1162
|
+
* which is no invoke, so each Machine in `actors` is walked. A nested Workspace is not: its own
|
|
1163
|
+
* `workspace()` call walks it, by its own Repo Slots.
|
|
703
1164
|
*/
|
|
704
|
-
function
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1165
|
+
function refuseUnnamedWorktrees(body: AnyStateMachine): void {
|
|
1166
|
+
const faults: string[] = [];
|
|
1167
|
+
const seen = new Set<AnyStateMachine>();
|
|
1168
|
+
const walk = (machine: AnyStateMachine): void => {
|
|
1169
|
+
if (seen.has(machine) || (machine !== body && composesSandbox(machine))) return;
|
|
1170
|
+
seen.add(machine);
|
|
1171
|
+
const actors = (machine.implementations.actors ?? {}) as Record<string, unknown>;
|
|
1172
|
+
for (const logic of Object.values(actors)) if (isMachineLogic(logic)) walk(logic);
|
|
1173
|
+
const nodes = [machine.root as unknown as InvokeNode];
|
|
1174
|
+
for (const node of nodes) {
|
|
1175
|
+
nodes.push(...Object.values(node.states));
|
|
1176
|
+
const configs = [node.config.invoke ?? []].flat() as Array<{ input?: unknown } | undefined>;
|
|
1177
|
+
node.invoke.forEach((inv, i) => {
|
|
1178
|
+
const logic = typeof inv.src === "string" ? actors[inv.src] : inv.src;
|
|
1179
|
+
if (isMachineLogic(logic)) walk(logic);
|
|
1180
|
+
const key = keyOf(logic);
|
|
1181
|
+
if (key === undefined) return;
|
|
1182
|
+
const given = configs[i]?.input;
|
|
1183
|
+
const written = (given as { consumerInput?: unknown } | undefined)?.consumerInput ?? given;
|
|
1184
|
+
if (typeof written === "function") return;
|
|
1185
|
+
if (typeof written === "object" && written !== null && key in written) return;
|
|
1186
|
+
faults.push(
|
|
1187
|
+
`state "${node.id}": "${typeof inv.src === "string" ? inv.src : inv.id}" names no \`${key}\`, and a Workspace ` +
|
|
1188
|
+
'with Repo Slots says where each Turn and Effect works: a Worktree, or `cwd: "/work"` for a Turn',
|
|
1189
|
+
);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
walk(body);
|
|
1194
|
+
if (faults.length) throw new Error(`workspace(): ${faults.join("; ")} (ADR-0057)`);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const isMachineLogic = (logic: unknown): logic is AnyStateMachine =>
|
|
1198
|
+
typeof logic === "object" && logic !== null && "implementations" in logic;
|
|
1199
|
+
|
|
1200
|
+
type InvokeNode = {
|
|
1201
|
+
id: string;
|
|
1202
|
+
states: Record<string, InvokeNode>;
|
|
1203
|
+
config: { invoke?: unknown };
|
|
1204
|
+
invoke: Array<{ src: unknown; id: string }>;
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
/** The key that says where this logic works, or undefined when it works nowhere in `/work`. */
|
|
1208
|
+
function keyOf(logic: unknown): "cwd" | "worktree" | undefined {
|
|
1209
|
+
if (isAgent(logic)) return (logic.definition as { workspace?: unknown }).workspace === "none" ? undefined : "cwd";
|
|
1210
|
+
if (!isEffect(logic)) return undefined;
|
|
1211
|
+
const parts = logic.effect;
|
|
1212
|
+
if (parts.kind === "command") return parts.cut ? undefined : "worktree";
|
|
1213
|
+
return parts.kind === "http" && parts.file ? "worktree" : undefined;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/** A gid, so an integer: a float or a negative is a pod the API server rejects at admission. */
|
|
1217
|
+
function assertWorkGroup(workGroup: unknown): void {
|
|
1218
|
+
if (workGroup !== undefined && (typeof workGroup !== "number" || !Number.isInteger(workGroup) || workGroup < 0)) {
|
|
1219
|
+
throw new Error(`workspace(): \`workGroup\` must be a gid (got ${JSON.stringify(workGroup)})`);
|
|
727
1220
|
}
|
|
728
1221
|
}
|
|
729
1222
|
|
|
730
1223
|
/**
|
|
731
1224
|
* Resolve every Repo Slot to a Binding, in declaration order (ADR-0051). A bound slot is its
|
|
732
1225
|
* Binding; a per-run slot is its mapper called over the run input, validated like the static forms
|
|
733
|
-
* because it derives from `jr2 run --input`
|
|
1226
|
+
* because it derives from `jr2 run --input` ; an open slot nobody bound is
|
|
734
1227
|
* a fault BEFORE the port, naming the `customize` line that fixes it — the run-time twin of the
|
|
735
1228
|
* refusal `jr2 up`'s walk makes for a registered Machine, reached here only by a Machine that was
|
|
736
1229
|
* never registered as itself (a test seam, a nested invoke of an unbound import). The Machine is
|
|
@@ -770,7 +1263,7 @@ function resolveBindings(
|
|
|
770
1263
|
bad.push(`ref (got ${JSON.stringify(binding.ref)})`);
|
|
771
1264
|
if (bad.length) {
|
|
772
1265
|
throw new Error(
|
|
773
|
-
`workspace
|
|
1266
|
+
`workspace invalid: repos.${slot} mapper returned ${bad.join("; ")} — the mapper derives from run ` +
|
|
774
1267
|
"input; does `jr2 run --input` carry every field this workflow's workspace() slot reads?",
|
|
775
1268
|
);
|
|
776
1269
|
}
|
|
@@ -788,7 +1281,7 @@ function attachedRepos(bindings: Record<string, ResolvedBinding>): Array<{ slot:
|
|
|
788
1281
|
}));
|
|
789
1282
|
}
|
|
790
1283
|
|
|
791
|
-
function buildWorkspaceMachine(body: AnyStateMachine
|
|
1284
|
+
function buildWorkspaceMachine(body: AnyStateMachine): AnyStateMachine {
|
|
792
1285
|
/**
|
|
793
1286
|
* `placing` (ADR-0064): write the Sandbox and wait, with no deadline, for its pod to be
|
|
794
1287
|
* scheduled. jr2 cannot tell "full now" from "never fits", so a bound is the Machine's own
|
|
@@ -807,13 +1300,15 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
807
1300
|
* delete rides the abort, after any write in flight has settled. A wait that fails on its own —
|
|
808
1301
|
* the Sandbox Lost before its node, a write that half landed — is the same end: the delete comes
|
|
809
1302
|
* 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.
|
|
1303
|
+
* not an end: the restored run re-enters `placing` and keeps its place. Nor is a name that a live
|
|
1304
|
+
* Workspace of the run holds when the delete would run: a parent that re-enters the state, or
|
|
1305
|
+
* catches a Failure and invokes the same Machine again, gives its new Workspace the same invoke id,
|
|
1306
|
+
* and so the same name, which that Workspace has placed again by then (ADR-0073).
|
|
811
1307
|
*/
|
|
812
1308
|
const place = fromPromise<
|
|
813
|
-
{ bindings: Record<string, ResolvedBinding> },
|
|
814
|
-
{ wsId: string;
|
|
1309
|
+
{ bindings: Record<string, ResolvedBinding>; runInput: Record<string, unknown> },
|
|
1310
|
+
{ wsId: string; runInput: Record<string, unknown> }
|
|
815
1311
|
>(async ({ input, self, system, signal }) => {
|
|
816
|
-
assertSpec(input.spec); // before the port: a bad spec must never cost a pod
|
|
817
1312
|
const binding = runBindingOf(system);
|
|
818
1313
|
// The images and the slots come off the WRAPPER, at invoke time, not out of context and not
|
|
819
1314
|
// out of a build-time closure (ADR-0049, ADR-0051). This state re-runs on every restore, so
|
|
@@ -822,14 +1317,21 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
822
1317
|
// content-addressed tag, which would outlive the image it names. The RESOLVED bindings are
|
|
823
1318
|
// persisted, because a per-run mapper's answer is this run's fact.
|
|
824
1319
|
const parts = sandboxPartsOf(invokingMachine(self));
|
|
1320
|
+
const wrapper = self._parent;
|
|
1321
|
+
// RunHost parsed a root run's door and added `instanceId`, so that value is final: a second
|
|
1322
|
+
// parse would refuse the added key under a strict door and transform a value twice. A parent's
|
|
1323
|
+
// input is parsed here, and a door value that is wrong costs no pod (ADR-0033).
|
|
1324
|
+
const door = wrapper?._parent ? inputSchemaOf(invokingMachine(self)!) : undefined;
|
|
1325
|
+
const runInput = door
|
|
1326
|
+
? parsedNestedDoor(door, input.runInput, ["instanceId"], "workspace input invalid, before any pod")
|
|
1327
|
+
: input.runInput;
|
|
825
1328
|
const user = userSeatOf(parts.user);
|
|
826
1329
|
const path = actorSlotPath(self._parent);
|
|
827
1330
|
// Read before the port: a Workspace no walk can see placed nothing, and is refused with the slot form.
|
|
828
1331
|
const route = actorRoute(self._parent);
|
|
829
|
-
const bindings = resolveBindings({ workflow: binding.workflow, path }, parts.repos,
|
|
1332
|
+
const bindings = resolveBindings({ workflow: binding.workflow, path }, parts.repos, runInput);
|
|
830
1333
|
const port = sandboxOf(system);
|
|
831
1334
|
const name = workspaceName(binding.runId, input.wsId);
|
|
832
|
-
const wrapper = self._parent;
|
|
833
1335
|
const child = wrapper ? actorPath(wrapper).join("/") : "";
|
|
834
1336
|
// A wait opened before a restart is the same wait: its start is in the wrapper's persisted
|
|
835
1337
|
// context, never in this actor's input, which a restore replays as it was first invoked.
|
|
@@ -862,7 +1364,9 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
862
1364
|
// work group's default (ADR-0005 puts it in pod composition, where the pod is built).
|
|
863
1365
|
...(parts.image !== undefined ? { image: parts.image } : {}),
|
|
864
1366
|
...(user.image !== undefined ? { user: user.image } : {}),
|
|
865
|
-
...(
|
|
1367
|
+
...(parts.workGroup !== undefined ? { workGroup: parts.workGroup } : {}),
|
|
1368
|
+
// The Held secrets the `workspace()` grants its confined children (ADR-0076): names only.
|
|
1369
|
+
...(parts.secrets?.length ? { secrets: [...parts.secrets] } : {}),
|
|
866
1370
|
repos: Object.entries(bindings).map(([slot, b]) => ({ slot, ...b })),
|
|
867
1371
|
// The Size as the Machine states it, re-read here like the images (ADR-0060): a redeploy
|
|
868
1372
|
// that retuned it reaches the next provision, and no snapshot holds it.
|
|
@@ -879,7 +1383,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
879
1383
|
if (binding.hostStopping) return;
|
|
880
1384
|
void placed
|
|
881
1385
|
.catch(() => {})
|
|
882
|
-
.then(() => port.destroy(name))
|
|
1386
|
+
.then(() => (binding.heldSandboxes?.().has(name) ? undefined : port.destroy(name)))
|
|
883
1387
|
.catch(() => {}); // a delete that fails leaves the CR to the idle GC, as ever
|
|
884
1388
|
},
|
|
885
1389
|
{ once: true },
|
|
@@ -900,7 +1404,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
900
1404
|
openedAt !== undefined ? Math.round(performance.now() - openedAt) : Math.max(0, Date.now() - Date.parse(since));
|
|
901
1405
|
binding.marker?.({ kind: "placed", child, since, after });
|
|
902
1406
|
}
|
|
903
|
-
return { bindings };
|
|
1407
|
+
return { bindings, runInput };
|
|
904
1408
|
});
|
|
905
1409
|
|
|
906
1410
|
/** `provisioning`: the placed pod's Ready, under the port's budget, which starts here (ADR-0064). */
|
|
@@ -909,9 +1413,12 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
909
1413
|
);
|
|
910
1414
|
|
|
911
1415
|
const attach = fromPromise<
|
|
912
|
-
{ repos: Record<string, string>;
|
|
913
|
-
{ wsId: string;
|
|
1416
|
+
{ repos: Record<string, string>; bases?: Record<string, string>; stale?: Record<string, string> },
|
|
1417
|
+
{ wsId: string; bindings: Record<string, ResolvedBinding> }
|
|
914
1418
|
>(async ({ input, system, signal }) => {
|
|
1419
|
+
// No Repo Slot, nothing to clone (ADR-0051): the Harness's attach would refuse an empty list,
|
|
1420
|
+
// and `/work` needs nothing from it — the pod's `fsGroup` already makes it the work group's.
|
|
1421
|
+
if (Object.keys(input.bindings).length === 0) return { repos: {} };
|
|
915
1422
|
const port = sandboxOf(system);
|
|
916
1423
|
const name = workspaceName(runBindingOf(system).runId, input.wsId);
|
|
917
1424
|
// The attach is loss-aware (ADR-0021): a Sandbox gone or Lost under it fails the attach at
|
|
@@ -930,7 +1437,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
930
1437
|
signal.addEventListener("abort", () => unsubscribe(), { once: true });
|
|
931
1438
|
let out: Awaited<ReturnType<typeof port.attach>>;
|
|
932
1439
|
try {
|
|
933
|
-
out = await Promise.race([port.attach({ name,
|
|
1440
|
+
out = await Promise.race([port.attach({ name, repos: attachedRepos(input.bindings) }), loss]);
|
|
934
1441
|
} finally {
|
|
935
1442
|
unsubscribe();
|
|
936
1443
|
}
|
|
@@ -1049,17 +1556,17 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1049
1556
|
|
|
1050
1557
|
// Every actor this wrapper runs is a NAMED SLOT (ADR-0049), the body first among them: a Machine
|
|
1051
1558
|
// 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".
|
|
1559
|
+
// the Console's join key and the `jr2 up` parts walk all reach it by. The mechanism's own seven —
|
|
1560
|
+
// place, provision, attach, registrar, lease, destroy, fail — are named for the same price, and the
|
|
1561
|
+
// Console shows what each state is doing instead of "inline". `fail` is the unbranded logic: it
|
|
1562
|
+
// sends the body's Failure up again, and the walk does not list it as a state that fails the run.
|
|
1055
1563
|
return setup({
|
|
1056
|
-
actors: { body, place, provision, attach, registrar, lease, destroy },
|
|
1564
|
+
actors: { body, place, provision, attach, registrar, lease, destroy, fail: failLogic },
|
|
1057
1565
|
}).createMachine({
|
|
1058
1566
|
id: "workspace",
|
|
1059
1567
|
context: ({ input, self }: { input: unknown; self: { id: string } }): WsContext => ({
|
|
1060
1568
|
runInput: (input ?? {}) as Record<string, unknown>,
|
|
1061
1569
|
wsId: self.id,
|
|
1062
|
-
spec: spec({ input }),
|
|
1063
1570
|
}),
|
|
1064
1571
|
initial: "placing",
|
|
1065
1572
|
states: {
|
|
@@ -1070,7 +1577,6 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1070
1577
|
src: "place",
|
|
1071
1578
|
input: ({ context }) => ({
|
|
1072
1579
|
wsId: (context as unknown as WsContext).wsId,
|
|
1073
|
-
spec: (context as unknown as WsContext).spec,
|
|
1074
1580
|
runInput: (context as unknown as WsContext).runInput,
|
|
1075
1581
|
}),
|
|
1076
1582
|
onDone: {
|
|
@@ -1078,6 +1584,8 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1078
1584
|
actions: assign({
|
|
1079
1585
|
bindings: ({ event }) =>
|
|
1080
1586
|
(event as unknown as { output: { bindings: Record<string, ResolvedBinding> } }).output.bindings,
|
|
1587
|
+
runInput: ({ event }) =>
|
|
1588
|
+
(event as unknown as { output: { runInput: Record<string, unknown> } }).output.runInput,
|
|
1081
1589
|
wait: undefined,
|
|
1082
1590
|
}),
|
|
1083
1591
|
},
|
|
@@ -1115,7 +1623,6 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1115
1623
|
src: "attach",
|
|
1116
1624
|
input: ({ context }) => ({
|
|
1117
1625
|
wsId: (context as unknown as WsContext).wsId,
|
|
1118
|
-
spec: (context as unknown as WsContext).spec,
|
|
1119
1626
|
bindings: (context as unknown as WsContext).bindings!,
|
|
1120
1627
|
}),
|
|
1121
1628
|
onDone: {
|
|
@@ -1125,7 +1632,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1125
1632
|
const ctx = context as WsContext;
|
|
1126
1633
|
const out = (
|
|
1127
1634
|
event as unknown as {
|
|
1128
|
-
output: { repos: Record<string, string>;
|
|
1635
|
+
output: { repos: Record<string, string>; bases?: Record<string, string> };
|
|
1129
1636
|
}
|
|
1130
1637
|
).output;
|
|
1131
1638
|
return {
|
|
@@ -1141,8 +1648,8 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1141
1648
|
...(ctx.bindings
|
|
1142
1649
|
? { bindings: Object.fromEntries(Object.entries(ctx.bindings).map(([s, b]) => [s, b.url])) }
|
|
1143
1650
|
: {}),
|
|
1144
|
-
|
|
1145
|
-
...(out.
|
|
1651
|
+
// Each slot's base: what the attach put `default/` on, sent with every cut.
|
|
1652
|
+
...(out.bases ? { bases: out.bases } : {}),
|
|
1146
1653
|
};
|
|
1147
1654
|
},
|
|
1148
1655
|
}),
|
|
@@ -1166,17 +1673,24 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1166
1673
|
src: "body",
|
|
1167
1674
|
input: ({ context }: { context: WsContext }) => {
|
|
1168
1675
|
const ctx = context;
|
|
1169
|
-
const { repos
|
|
1170
|
-
// Body-facing subset only (ADR-0016): endpoint/sandbox are mechanism-internal.
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
workspace: { repos, branch, ...(review ? { review } : {}) } satisfies WorkspaceHandles<string>,
|
|
1174
|
-
};
|
|
1676
|
+
const { repos } = ctx.handles!;
|
|
1677
|
+
// Body-facing subset only (ADR-0016): endpoint/sandbox are mechanism-internal. With
|
|
1678
|
+
// no Repo Slot the handles are `{ repos: {} }` (WorkspaceHandles<never>).
|
|
1679
|
+
return { ...ctx.runInput, workspace: { repos } };
|
|
1175
1680
|
},
|
|
1176
1681
|
onDone: {
|
|
1177
1682
|
target: "teardown",
|
|
1178
1683
|
actions: assign({ output: ({ event }) => (event as unknown as { output: unknown }).output }),
|
|
1179
1684
|
},
|
|
1685
|
+
// A Failure deletes the Sandbox before it goes up (ADR-0073). Guarded, so a defect
|
|
1686
|
+
// takes no transition and goes up at once, as before: its Sandbox stays as evidence.
|
|
1687
|
+
onError: {
|
|
1688
|
+
guard: ({ event }: { event: { error?: unknown } }) => isFailure(event.error),
|
|
1689
|
+
target: "failing",
|
|
1690
|
+
actions: assign({
|
|
1691
|
+
failure: ({ event }) => failurePayload((event as unknown as { error: Failure }).error),
|
|
1692
|
+
}),
|
|
1693
|
+
},
|
|
1180
1694
|
},
|
|
1181
1695
|
leaseInvoke(true),
|
|
1182
1696
|
],
|
|
@@ -1194,6 +1708,25 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1194
1708
|
},
|
|
1195
1709
|
},
|
|
1196
1710
|
done: { type: "final" },
|
|
1711
|
+
// The body failed (ADR-0073): the same delete as `teardown`, then the same Failure goes up to
|
|
1712
|
+
// the parent unchanged. No lease here: nothing judges a Sandbox that is being deleted.
|
|
1713
|
+
failing: {
|
|
1714
|
+
initial: "destroying",
|
|
1715
|
+
states: {
|
|
1716
|
+
destroying: {
|
|
1717
|
+
invoke: {
|
|
1718
|
+
src: "destroy",
|
|
1719
|
+
input: ({ context }) => ({ wsId: (context as unknown as WsContext).wsId }),
|
|
1720
|
+
onDone: "raising",
|
|
1721
|
+
// A failed delete is the operator GC's problem, and the run host's (ADR-0012 backstop).
|
|
1722
|
+
onError: "raising",
|
|
1723
|
+
},
|
|
1724
|
+
},
|
|
1725
|
+
raising: {
|
|
1726
|
+
invoke: { src: "fail", input: ({ context }) => (context as unknown as WsContext).failure! },
|
|
1727
|
+
},
|
|
1728
|
+
},
|
|
1729
|
+
},
|
|
1197
1730
|
},
|
|
1198
1731
|
// Machine output must be declared at the ROOT in xstate v5 (a final state's own `output`
|
|
1199
1732
|
// only rides the done event); the workspace's output is the body's, verbatim (ADR-0012).
|