@jr2/orchestrator 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +142 -29
- package/docs/concepts/architecture.md +60 -34
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +13 -8
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +7 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +133 -50
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +14 -4
- package/docs/concepts/pools-and-sources.md +193 -17
- package/docs/concepts/secrets-and-trust.md +186 -62
- package/docs/concepts/the-menu.md +14 -0
- package/docs/concepts/workspaces.md +135 -56
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +72 -15
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +29 -12
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +19 -7
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +14 -7
- package/docs/guides/look-inside-a-sandbox.md +115 -42
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +34 -27
- package/docs/guides/set-an-agents-access-and-dials.md +69 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +7 -1
- package/docs/guides/watch-a-run.md +146 -117
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +18 -8
- package/docs/llms.txt +110 -23
- package/docs/reference/authoring-surface/agent.md +35 -12
- package/docs/reference/authoring-surface/answer.md +20 -4
- package/docs/reference/authoring-surface/arg.md +28 -6
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +37 -22
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +18 -5
- package/docs/reference/authoring-surface/file.md +37 -17
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +109 -9
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +14 -4
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +41 -8
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +23 -5
- package/docs/reference/authoring-surface/pool.md +79 -11
- package/docs/reference/authoring-surface/push.md +31 -13
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +26 -5
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +59 -4
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- 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-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +6 -7
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
- package/docs/reference/authoring-surface/type-customize.md +20 -3
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +6 -6
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +30 -9
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- 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 +66 -38
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +88 -147
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +248 -6
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
- package/docs/reference/decisions.md +9 -4
- package/docs/reference/glossary.md +138 -45
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +988 -123
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +7 -5
- package/package.json +10 -3
- package/src/actor.ts +194 -42
- package/src/agent.ts +56 -6
- package/src/ambient.ts +3 -5
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +238 -26
- package/src/charts.ts +9 -2
- package/src/config.ts +566 -7
- package/src/conversations.ts +453 -0
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +257 -77
- package/src/customize.ts +154 -7
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +52 -4
- package/src/effect.ts +401 -132
- package/src/effector-wire.ts +278 -30
- package/src/fail.ts +17 -4
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +84 -35
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +197 -39
- package/src/holds.ts +5 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +164 -70
- package/src/http.ts +933 -99
- package/src/index.ts +30 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +23 -3
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +29 -5
- package/src/mcp-effect.ts +424 -0
- package/src/model-keys.ts +24 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +82 -7
- package/src/notices.ts +576 -59
- package/src/open-body.ts +110 -30
- package/src/open-effect.ts +17 -4
- package/src/open.ts +40 -7
- package/src/parts.ts +125 -35
- package/src/party.ts +35 -1
- package/src/pick.ts +43 -9
- package/src/pool.ts +1172 -172
- package/src/registration.ts +249 -17
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +1614 -190
- package/src/run-list.ts +191 -0
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +116 -21
- package/src/secret-placement.ts +248 -35
- package/src/server.ts +53 -4
- package/src/setup.ts +158 -38
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +362 -46
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +27 -10
- package/src/workspace.ts +314 -259
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/src/workspace.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
-
//
|
|
5
|
-
// `{ workspace: { repos
|
|
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
6
|
// are published ambiently — ADR-0016, ambient.ts), and destroy the Sandbox when the body reaches a
|
|
7
7
|
// final state or fails (ADR-0073). Teardown lives INSIDE the
|
|
8
8
|
// wrapper's own states because an xstate stop is synchronous — multi-step async cleanup must be
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
} from "xstate";
|
|
45
45
|
import type { z } from "zod";
|
|
46
46
|
import { registerAmbientHandles, type AmbientHandles } from "./ambient.ts";
|
|
47
|
+
import { isAgent } from "./agent.ts";
|
|
48
|
+
import { isEffect } from "./effect.ts";
|
|
47
49
|
import { failLogic, failurePayload, isFailure, type Failure, type FailureInput } from "./fail.ts";
|
|
48
50
|
import { refuseUnroutedLost } from "./mechanism-routes.ts";
|
|
49
51
|
import {
|
|
@@ -68,7 +70,14 @@ import {
|
|
|
68
70
|
} from "./parts.ts";
|
|
69
71
|
import { actorPath, runBindingOf, type AnyActorSystem } from "./registration.ts";
|
|
70
72
|
import { assertSize, assertUserSeat, userSeatOf, type Size, type UserContainer } from "./size.ts";
|
|
71
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
attachInputSchema,
|
|
75
|
+
declaresRunInput,
|
|
76
|
+
inputSchemaOf,
|
|
77
|
+
parsedNestedDoor,
|
|
78
|
+
invokingMachine,
|
|
79
|
+
type HostInjectedInput,
|
|
80
|
+
} from "./vocabulary.ts";
|
|
72
81
|
|
|
73
82
|
/** Lease cadence when the backend names none. Well inside the 30m default idle timeout, so a
|
|
74
83
|
* few missed renewals in a row are survivable. It bounds only how long an orphan waits to be
|
|
@@ -90,47 +99,6 @@ export function leaseDelay(interval: number, which: "first" | "next", random: nu
|
|
|
90
99
|
return interval * (1 - LEASE_JITTER + 2 * LEASE_JITTER * random);
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
// What to attach, in workspace vocabulary only (ADR-0012 boundary): the one branch the body works
|
|
94
|
-
// on, the pod's work group, and the review sha. Derived PER RUN from the wrapper's input, which is
|
|
95
|
-
// what keeps it out here rather than in the options — and which is exactly why the two IMAGES and
|
|
96
|
-
// the REPOS are NOT here (ADR-0049, ADR-0051): `jr2 up` must find them by walking the Machine, and
|
|
97
|
-
// no walk can evaluate a function of run input. They are static `workspace()` options instead; a
|
|
98
|
-
// Repo that IS a function of run input is a per-run slot, a mapper the walk can see the shape of
|
|
99
|
-
// even though it cannot see the url.
|
|
100
|
-
/** What the `spec` of a `workspace()` returns: the branch that the Workspace works on, and two
|
|
101
|
-
* optional fields. jr2 calls `spec` with the run input when the run enters the Workspace. */
|
|
102
|
-
export type WorkspaceSpec = {
|
|
103
|
-
/** The branch that the Workspace works on: each Repo Slot gets a Worktree for it, at
|
|
104
|
-
* `/work/<slot>/<branch>`, with each `/` in the branch written as `-`. Required, and not
|
|
105
|
-
* `default`, which is the directory of the clone. Your Machine decides its name. */
|
|
106
|
-
branch: string;
|
|
107
|
-
// The pod's work group (ADR-0005): `fsGroup`. The two writing seats may run different uids —
|
|
108
|
-
// each image's own `USER` decides — and POSIX would then make the other seat's files read-only;
|
|
109
|
-
// fsGroup (group ownership) plus the default ACL the attach stamps on each repo root (group
|
|
110
|
-
// writability, umask-proof) closes that, both inert when the uids already match. The override
|
|
111
|
-
// exists for the image whose sessions already hold a gid of their own — a root sshd's logins
|
|
112
|
-
// rebuild groups from `/etc/group`, so pointing the work group at one they have costs no
|
|
113
|
-
// rebuild. Never a config key — pod composition is the spec's business.
|
|
114
|
-
/** The group id that owns the files in `/work`. With it, the Sandbox Image and the User
|
|
115
|
-
* Container can both write those files when they run as different users. Default: 2000. Set it
|
|
116
|
-
* to a group id that the User Container's sessions already have, for example when a root sshd
|
|
117
|
-
* builds their groups from `/etc/group`. A non-negative integer. */
|
|
118
|
-
workGroup?: number;
|
|
119
|
-
// `<branchDir>-review`, a sibling of the branch worktree, forced to exactly this sha on every
|
|
120
|
-
// attach (ADR-0028). Creation-time seat only; the per-round refresh verb (the sha moves between
|
|
121
|
-
// review rounds) is a later, workflow-driven change.
|
|
122
|
-
/** A commit to check out in a second, detached Worktree beside each branch Worktree, for a
|
|
123
|
-
* reviewer: `/work/<slot>/<branch>-review`. The body gets its paths as `workspace.review`. */
|
|
124
|
-
reviewSha?: string;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// A Workspace with no Repo Slot (ADR-0051) attaches nothing, so a branch and a review sha name
|
|
128
|
-
// nothing: `never` refuses them where the spec is written, and `assertSpec` refuses them at run
|
|
129
|
-
// time for the caller the types do not see.
|
|
130
|
-
/** What the `spec` of a `workspace()` with no Repo Slot returns: only `workGroup`. There is no
|
|
131
|
-
* branch and no review Worktree, because the Workspace has no Repo Slot. */
|
|
132
|
-
export type NoSlotSpec = Pick<WorkspaceSpec, "workGroup"> & { branch?: never; reviewSha?: never };
|
|
133
|
-
|
|
134
102
|
// What the workspace hands the BODY (ADR-0012, ADR-0016): worktree geography only, keyed by Repo
|
|
135
103
|
// Slot (ADR-0051) — so a path a prompt names is right in every Instance that consumes the Machine.
|
|
136
104
|
// `endpoint` and `sandbox` are mechanism-internal — the Agent actor resolves them ambiently from
|
|
@@ -145,14 +113,13 @@ export type NoSlotSpec = Pick<WorkspaceSpec, "workGroup"> & { branch?: never; re
|
|
|
145
113
|
// body under a wrapper that declares its map Open (`repos: open`, ADR-0051), where the slots are
|
|
146
114
|
// the composer's words and no body could name them.
|
|
147
115
|
/**
|
|
148
|
-
* What the body of a `workspace()` gets as `input.workspace`: the
|
|
149
|
-
*
|
|
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.
|
|
150
118
|
*
|
|
151
119
|
* `TSlots` is the names of the Repo Slots that the body reads. `workspace()` refuses a body that
|
|
152
120
|
* names a slot that the Workspace does not have. Write `string` for a body that names no slot and
|
|
153
|
-
* reads the
|
|
154
|
-
* Workspace with no Repo Slot: its handles are `{ repos: {} }`,
|
|
155
|
-
* in `/work`.
|
|
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`.
|
|
156
123
|
*/
|
|
157
124
|
// `[TSlots] extends [never]`, not `TSlots extends never`: a bare `never` would distribute to `never`.
|
|
158
125
|
export type WorkspaceHandles<TSlots extends string> = [TSlots] extends [never]
|
|
@@ -161,20 +128,10 @@ export type WorkspaceHandles<TSlots extends string> = [TSlots] extends [never]
|
|
|
161
128
|
|
|
162
129
|
/** The handles of a Workspace that declares Repo Slots. @internal */
|
|
163
130
|
type SlotHandles<TSlots extends string> = {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// promise; the meaning is the Machine's (ADR-0051).
|
|
169
|
-
/** The absolute path of each Repo Slot's Worktree, `/work/<slot>/<branch>`, in the order that
|
|
170
|
-
* `repos` declares the slots. Give one to an Agent's Turn as its `cwd`. */
|
|
171
|
-
repos: Record<TSlots, string>;
|
|
172
|
-
/** The branch that the Workspace works on: the `branch` of the spec. */
|
|
173
|
-
branch: string;
|
|
174
|
-
// The review seat (ADR-0028).
|
|
175
|
-
/** The path of each Repo Slot's review Worktree. Present only when the spec has `reviewSha`.
|
|
176
|
-
* Give one to a reviewer's Turn as its `cwd`. */
|
|
177
|
-
review?: Partial<Record<TSlots, 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}` };
|
|
178
135
|
};
|
|
179
136
|
|
|
180
137
|
// The composition is the whole reason the door is declared on the wrapper and not on the body
|
|
@@ -190,7 +147,7 @@ type SlotHandles<TSlots extends string> = {
|
|
|
190
147
|
// the composition the WRAPPER makes.
|
|
191
148
|
/**
|
|
192
149
|
* The input of a `workspace()` body: the run input, plus `workspace`, the handles that the
|
|
193
|
-
* Workspace adds (`workspace.repos`,
|
|
150
|
+
* Workspace adds (`workspace.repos`, the folder of each Repo Slot). Use it as the type of
|
|
194
151
|
* the body's input.
|
|
195
152
|
*
|
|
196
153
|
* The first argument is the run input: the `input` schema of the `workspace()`. The second is the
|
|
@@ -207,16 +164,29 @@ type SlotHandles<TSlots extends string> = {
|
|
|
207
164
|
* @example
|
|
208
165
|
* ```ts
|
|
209
166
|
* import { z } from "zod";
|
|
210
|
-
* import {
|
|
167
|
+
* import {
|
|
168
|
+
* agent,
|
|
169
|
+
* defineEvent,
|
|
170
|
+
* fail,
|
|
171
|
+
* isReady,
|
|
172
|
+
* jr2Setup,
|
|
173
|
+
* workspace,
|
|
174
|
+
* worktree,
|
|
175
|
+
* type Workspaced,
|
|
176
|
+
* } from "@jr2/orchestrator";
|
|
211
177
|
*
|
|
212
178
|
* const done = defineEvent({
|
|
213
179
|
* name: "done",
|
|
214
180
|
* description: "Call this when REVIEW.md is written.",
|
|
215
181
|
* input: z.object({}),
|
|
216
182
|
* });
|
|
217
|
-
* const runInput = z.object({
|
|
183
|
+
* const runInput = z.object({ sha: z.string() });
|
|
218
184
|
* type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
|
|
219
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
|
+
*
|
|
220
190
|
* const body = jr2Setup({
|
|
221
191
|
* types: {} as { input: BodyInput; context: BodyInput },
|
|
222
192
|
* events: [done],
|
|
@@ -226,25 +196,34 @@ type SlotHandles<TSlots extends string> = {
|
|
|
226
196
|
* instructions: "You review one commit and write REVIEW.md.",
|
|
227
197
|
* workspace: "read",
|
|
228
198
|
* }),
|
|
199
|
+
* cut: worktree(),
|
|
229
200
|
* fail: fail(),
|
|
230
201
|
* },
|
|
231
202
|
* }).createMachine({
|
|
232
203
|
* id: "review",
|
|
233
204
|
* context: ({ input }) => input,
|
|
234
205
|
* on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
|
|
235
|
-
* initial: "
|
|
206
|
+
* initial: "cutting",
|
|
236
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
|
+
* },
|
|
237
215
|
* reviewing: {
|
|
238
216
|
* invoke: {
|
|
239
217
|
* src: "reviewer",
|
|
240
218
|
* input: ({ context }) => ({
|
|
241
|
-
* prompt: `Review commit ${context.sha}
|
|
242
|
-
* cwd: context
|
|
219
|
+
* prompt: `Review commit ${context.sha}.`,
|
|
220
|
+
* cwd: reviewTree(context),
|
|
243
221
|
* }),
|
|
244
222
|
* },
|
|
245
223
|
* on: { done: "finished" },
|
|
246
224
|
* },
|
|
247
225
|
* finished: { type: "final" },
|
|
226
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
248
227
|
* failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
|
|
249
228
|
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
250
229
|
* },
|
|
@@ -253,7 +232,6 @@ type SlotHandles<TSlots extends string> = {
|
|
|
253
232
|
* export const machine = workspace(body, {
|
|
254
233
|
* input: runInput,
|
|
255
234
|
* repos: { app: "https://github.com/you/app.git" },
|
|
256
|
-
* spec: ({ input }) => ({ branch: input.branch, reviewSha: input.sha }),
|
|
257
235
|
* });
|
|
258
236
|
* ```
|
|
259
237
|
*
|
|
@@ -317,12 +295,18 @@ export type PlaceRequest = {
|
|
|
317
295
|
name: string;
|
|
318
296
|
runId: string;
|
|
319
297
|
workflow: string;
|
|
298
|
+
/** The Workspace's path in the run's Machine: the wrapper's actor ids joined by `/` (ADR-0012).
|
|
299
|
+
* The port writes it on the CR, where the `jr2 workspace` verbs read it (ADR-0009). */
|
|
300
|
+
path: string;
|
|
320
301
|
/** The placement route from the registered Machine to this `workspace()` (`actorRoute`). With
|
|
321
302
|
* `workflow`, it names the pod whose Held secrets and Sandbox Effector `jr2 up` placed (ADR-0059). */
|
|
322
303
|
route: PlacementRoute;
|
|
323
304
|
image?: string;
|
|
324
305
|
user?: string;
|
|
325
306
|
workGroup?: number;
|
|
307
|
+
/** The Held secrets the `workspace()`'s `secrets` list names (ADR-0059): the ones the Harness may
|
|
308
|
+
* put in a confined child's env, by Stand-in (ADR-0076). Names, never a value. Absent → none. */
|
|
309
|
+
secrets?: string[];
|
|
326
310
|
repos: ProvisionedRepo[];
|
|
327
311
|
resources?: Size;
|
|
328
312
|
userResources?: Size;
|
|
@@ -346,9 +330,10 @@ export type ProvisionedRepo = { slot: string; url: string; ref?: string; perRun:
|
|
|
346
330
|
*/
|
|
347
331
|
export interface SandboxPort {
|
|
348
332
|
/**
|
|
349
|
-
* Write the Sandbox (its token Secret, then the CR, labeled with its run
|
|
350
|
-
* with NO deadline, until the operator says its pod is
|
|
351
|
-
*
|
|
333
|
+
* Write the Sandbox (its token Secret, then the CR, labeled with its run, its Workflow and its
|
|
334
|
+
* path for `jr2 workspace ls`) and wait, with NO deadline, until the operator says its pod is
|
|
335
|
+
* scheduled (ADR-0064). `image`/`user` are the wrapper's static image options
|
|
336
|
+
* (ADR-0037/0005/0049) — a `file:` context or a registry ref,
|
|
352
337
|
* the port resolves both, and a context the last converge did not build fails here rather than
|
|
353
338
|
* converge-time. `repos` are the wrapper's slots, resolved, in declaration order (ADR-0051): the
|
|
354
339
|
* port names each Repo on the CR so the cluster mounts its cache, and refuses a per-run url no
|
|
@@ -370,17 +355,12 @@ export interface SandboxPort {
|
|
|
370
355
|
*/
|
|
371
356
|
provision(name: string): Promise<{ endpoint: string }>;
|
|
372
357
|
/** Post-Ready attach (ADR-0004): per slot, `git clone --shared` off the node's read-only
|
|
373
|
-
* cache (the `default/` checkout)
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
|
|
377
|
-
attach(req: {
|
|
378
|
-
name: string;
|
|
379
|
-
spec: WorkspaceSpec;
|
|
380
|
-
repos: Array<{ slot: string; url: string; ref?: string }>;
|
|
381
|
-
}): Promise<{
|
|
358
|
+
* cache (the `default/` checkout). A Machine cuts each Worktree (ADR-0075). Resolves with the
|
|
359
|
+
* folder of each slot; `stale` names the slots whose cache could not be fetched before this
|
|
360
|
+
* attach, with git's own error (ADR-0051: freshness degrades, absence does not). */
|
|
361
|
+
attach(req: { name: string; repos: Array<{ slot: string; url: string; ref?: string }> }): Promise<{
|
|
382
362
|
repos: Record<string, string>;
|
|
383
|
-
|
|
363
|
+
bases?: Record<string, string>;
|
|
384
364
|
stale?: Record<string, string>;
|
|
385
365
|
}>;
|
|
386
366
|
/**
|
|
@@ -391,6 +371,20 @@ export interface SandboxPort {
|
|
|
391
371
|
* the next renewal is minutes away and the idle timeout is thirty.
|
|
392
372
|
*/
|
|
393
373
|
renew(name: string): Promise<void>;
|
|
374
|
+
/**
|
|
375
|
+
* Mark the Sandbox as held by no run (ADR-0021): the Lease stopped for the body's final state, a
|
|
376
|
+
* fault, a cancel or a parent that left the Workspace — never for a park or a process shutdown.
|
|
377
|
+
* `jr2 workspace rm` reads the mark with the cluster alone (ADR-0009). A write, and only that; a
|
|
378
|
+
* failure rejects and the caller shrugs.
|
|
379
|
+
*/
|
|
380
|
+
releaseHold(name: string): Promise<void>;
|
|
381
|
+
/**
|
|
382
|
+
* Mark every Sandbox of a run as held by no run (ADR-0021), found by its `jr2.dev/run` label: a
|
|
383
|
+
* restore ended the run `drifted` or `lost` without hosting it, so no Lease runs to release them.
|
|
384
|
+
* A write for each, and only that; a failure rejects and the caller shrugs. Optional: a port
|
|
385
|
+
* with no cluster has nothing to mark.
|
|
386
|
+
*/
|
|
387
|
+
releaseRun?(runId: string): Promise<void>;
|
|
394
388
|
/**
|
|
395
389
|
* Hear this workspace's Continuity from the watch (ADR-0021, ADR-0063): once the watch has
|
|
396
390
|
* listed, what it holds now, then again on every change the cluster reports — a Sandbox gone, or
|
|
@@ -483,12 +477,11 @@ export type WorkspaceWait = PlacingWait & { since: string };
|
|
|
483
477
|
export const WORKSPACE_WAIT_KEY = "wait";
|
|
484
478
|
|
|
485
479
|
type WsContext = {
|
|
486
|
-
/** The wrapper's
|
|
480
|
+
/** The wrapper's input, which the body gets (plus `workspace`). Under a parent, `place` replaces
|
|
481
|
+
* it with the door's parsed value. */
|
|
487
482
|
runInput: Record<string, unknown>;
|
|
488
483
|
/** This invocation's stable identity (the actor id the parent invoked/spawned us under). */
|
|
489
484
|
wsId: string;
|
|
490
|
-
/** The resolved spec — computed once from input and persisted, like any other context data. */
|
|
491
|
-
spec: AnySpec;
|
|
492
485
|
/** Every slot's resolved Binding, by slot, in declaration order — assigned at provision. */
|
|
493
486
|
bindings?: Record<string, ResolvedBinding>;
|
|
494
487
|
endpoint?: string;
|
|
@@ -581,6 +574,20 @@ export type WorkspaceMachine<TInput, TOutput, TBody extends AnyStateMachine, TSl
|
|
|
581
574
|
// satisfies a body that writes that type itself (`z.infer<typeof door>`, alone or with the
|
|
582
575
|
// handles). The raw door keeps those bodies legal, and the stripped form keeps a body that
|
|
583
576
|
// declares `Workspaced<…>` legal. A key that the door does not carry is still refused.
|
|
577
|
+
/** The input of a door-less Workspace: the body's, without the handles. `unknown` when the body
|
|
578
|
+
* asks for nothing else, and with `undefined` added when each field it asks for is optional. In
|
|
579
|
+
* both cases a parent has nothing that it must give, and xstate makes an invoke's `input` optional
|
|
580
|
+
* only for a type that holds `undefined`.
|
|
581
|
+
* @internal */
|
|
582
|
+
type DoorlessInput<TBody extends AnyStateMachine> =
|
|
583
|
+
unknown extends InputFrom<TBody>
|
|
584
|
+
? unknown
|
|
585
|
+
: [keyof Omit<InputFrom<TBody>, "workspace">] extends [never]
|
|
586
|
+
? unknown
|
|
587
|
+
: {} extends Omit<InputFrom<TBody>, "workspace">
|
|
588
|
+
? Omit<InputFrom<TBody>, "workspace"> | undefined
|
|
589
|
+
: Omit<InputFrom<TBody>, "workspace">;
|
|
590
|
+
|
|
584
591
|
/** @internal */
|
|
585
592
|
type BodyAcceptsDoor<TBody extends AnyStateMachine, TDoor, TSlots extends string> =
|
|
586
593
|
TDoor & Workspaced<TDoor, TSlots> & HostInjectedInput extends InputFrom<TBody>
|
|
@@ -625,10 +632,10 @@ type BodyAcceptsSlots<TBody extends AnyStateMachine, TSlots extends string> =
|
|
|
625
632
|
: unknown;
|
|
626
633
|
|
|
627
634
|
// What the Sandbox is MADE OF (ADR-0037, ADR-0005) and which Repos it attaches (ADR-0051), as
|
|
628
|
-
// STATIC options on the wrapper rather than
|
|
635
|
+
// STATIC options on the wrapper rather than a function of the run input (ADR-0049). Static is the
|
|
629
636
|
// whole point: `jr2 up` walks the registered Machines to find every `file:` context and build it,
|
|
630
|
-
// every bound Repo and warm it, every open slot and refuse it (parts.ts) — and
|
|
631
|
-
// of run input
|
|
637
|
+
// every bound Repo and warm it, every open slot and refuse it (parts.ts) — and no walk can evaluate a
|
|
638
|
+
// function of run input. They are also never persisted — the provisioning state
|
|
632
639
|
// re-reads them off the Machine it was invoked as, so a restore, a `provide()` and a `customize()`
|
|
633
640
|
// all get the image and the slots the Machine carries NOW.
|
|
634
641
|
//
|
|
@@ -649,6 +656,9 @@ export type SandboxOptions<TSlots extends string, TInput = unknown> = {
|
|
|
649
656
|
* glibc and git; jr2 adds its own runtime at `/opt/jr2`. Default: the Instance's
|
|
650
657
|
* `images/default`, then jr2's Harness image. */
|
|
651
658
|
image?: string;
|
|
659
|
+
// A gid is a fact of the images, so it is static.
|
|
660
|
+
/** The group id that owns the files in `/work`. Default: 2000. A non-negative integer. */
|
|
661
|
+
workGroup?: number;
|
|
652
662
|
// The widening ADR-0005 foresaw: `{ image, resources }` states the seat's split of the Size
|
|
653
663
|
// (ADR-0060). There is no default, because the seat's whole identity is "what jr2 does not own"
|
|
654
664
|
// and jr2 has nothing to put there. Env and ports are deliberately not forwarded.
|
|
@@ -680,8 +690,8 @@ export type SandboxOptions<TSlots extends string, TInput = unknown> = {
|
|
|
680
690
|
* choice. The name is the key of `workspace.repos` in the body and the directory under `/work`.
|
|
681
691
|
* At least one slot. Leave `repos` out for a Workspace with no Repo Slot (see
|
|
682
692
|
* {@link NoSlotWorkspaceOptions}). Each value is one of:
|
|
683
|
-
* - a url, or `{ url, ref }`, where `ref` is the branch or commit that the
|
|
684
|
-
* (default: the Repo's default branch);
|
|
693
|
+
* - a url, or `{ url, ref }`, where `ref` is the branch, the tag or the commit that the clone
|
|
694
|
+
* stays on, and that a new branch starts from (default: the Repo's default branch);
|
|
685
695
|
* - `open`, for the Instance that registers the Machine to bind with `customize()`;
|
|
686
696
|
* - a function of the run input, `({ input }) => url` or `{ url, ref }`, when each run chooses.
|
|
687
697
|
*
|
|
@@ -695,8 +705,11 @@ export type SandboxOptions<TSlots extends string, TInput = unknown> = {
|
|
|
695
705
|
/**
|
|
696
706
|
* The Held secrets that this Sandbox can use: names from the `secrets` list in `jr2.config.ts`.
|
|
697
707
|
* The Sandbox's Custodian holds each value and puts it into requests to the hosts of that Held
|
|
698
|
-
* secret.
|
|
699
|
-
*
|
|
708
|
+
* secret. Listing a name also gives its Stand-in to the Agent's programs — each `bash` call, test,
|
|
709
|
+
* build and `command` — in their env under that name, so code in `/work` can spend it (ADR-0076).
|
|
710
|
+
* You need not list the model key of an Agent that runs here: its provider claims it, and the
|
|
711
|
+
* Harness gives it to the model calls alone. List the model key only when code in `/work` must
|
|
712
|
+
* call the model itself: such a call is outside the Turn's usage record. Each name once.
|
|
700
713
|
*/
|
|
701
714
|
secrets?: readonly string[];
|
|
702
715
|
// The container exists only when an Effect below this `workspace()` is placed there (ADR-0068,
|
|
@@ -725,47 +738,27 @@ export type WorkspaceOptions<TSchema extends z.ZodObject, TSlots extends string>
|
|
|
725
738
|
// `workspace` handles ({@link Workspaced}), which no caller can send, so the body's contract is
|
|
726
739
|
// the door plus something that does not exist yet. Symmetric with `PoolSpec.input`.
|
|
727
740
|
/** The input that starts a run, as a `z.object`. jr2 checks each start against it. It also types
|
|
728
|
-
* the `input` of
|
|
741
|
+
* the `input` of a per-run Repo Slot, and the body must accept it plus the
|
|
729
742
|
* `workspace` handles. Write it here, not on the body. */
|
|
730
743
|
input: TSchema;
|
|
731
|
-
// ADR-0012's boundary: workflow configuration never enters the spec.
|
|
732
|
-
/**
|
|
733
|
-
* A function from the run input to the Workspace's {@link WorkspaceSpec}: the `branch` it works
|
|
734
|
-
* on, and optionally `reviewSha` and `workGroup`. jr2 calls it when the run enters the
|
|
735
|
-
* Workspace.
|
|
736
|
-
*
|
|
737
|
-
* The input of `spec` also holds `instanceId`, the seed Instance ID of the run (see
|
|
738
|
-
* {@link HostInjectedInput}). Use it to give each run a branch of its own:
|
|
739
|
-
* ``spec: ({ input }) => ({ branch: `fix/${input.instanceId}` })``. Only a root `workspace()` gets
|
|
740
|
-
* it from jr2. A `workspace()` that a different Machine invokes gets only the input that its
|
|
741
|
-
* parent gives it, so that parent must give `instanceId` when `spec` reads it.
|
|
742
|
-
*/
|
|
743
|
-
// The type says `instanceId` is present also below the root, where only the parent can supply it:
|
|
744
|
-
// the same permissive answer the body guard gives (BodyAcceptsDoor), because no type can see where
|
|
745
|
-
// the wrapper sits. The alternative, `Partial<HostInjectedInput>`, makes each root `spec` that reads
|
|
746
|
-
// it write a fallback for a value that is always there.
|
|
747
|
-
spec: (args: { input: WithoutEmptyIndex<z.infer<TSchema>> & HostInjectedInput }) => WorkspaceSpec;
|
|
748
744
|
};
|
|
749
745
|
|
|
750
746
|
// How a Workspace with NO declared door is configured: absence is permissive (ADR-0033), so jr2
|
|
751
747
|
// has nothing to infer from and says `unknown` rather than `any` — an honest "jr2 does not know",
|
|
752
748
|
// which the mapper must narrow before it reads a field.
|
|
753
749
|
/**
|
|
754
|
-
* The options of a `workspace()` with no `input`: it starts with any input. The `input` of
|
|
755
|
-
* is then `unknown`, and
|
|
750
|
+
* The options of a `workspace()` with no `input`: it starts with any input. The `input` of a
|
|
751
|
+
* per-run Repo Slot is then `unknown`, and the mapper must check it before it reads a field.
|
|
756
752
|
*
|
|
757
|
-
* Use it for a Workspace that a Pool or another Machine starts.
|
|
758
|
-
*
|
|
759
|
-
*
|
|
753
|
+
* Use it for a Workspace that a Pool or another Machine starts. The body's `input` type says what
|
|
754
|
+
* that input is, and that also types the input of the Workspace. For a Workspace that a caller
|
|
755
|
+
* starts, declare `input`.
|
|
760
756
|
*/
|
|
761
757
|
export type PermissiveWorkspaceOptions<TSlots extends string, TInput = unknown> = SandboxOptions<TSlots, TInput> & {
|
|
762
758
|
// Spelled out so a declared schema can never fall through to the permissive overload, where the
|
|
763
759
|
// body's door would go unchecked.
|
|
764
760
|
/** Not present: a `workspace()` without `input` starts with any input. */
|
|
765
761
|
input?: never;
|
|
766
|
-
/** A function from the input to the Workspace's {@link WorkspaceSpec}. Its `input` is
|
|
767
|
-
* `unknown` unless you give the parameter a type: `spec: ({ input }: { input: Item }) => …`. */
|
|
768
|
-
spec: (args: { input: TInput }) => WorkspaceSpec;
|
|
769
762
|
};
|
|
770
763
|
|
|
771
764
|
// A Workspace with no Repo Slot (ADR-0051): a Sandbox for one unit of work whose Agents and
|
|
@@ -773,53 +766,50 @@ export type PermissiveWorkspaceOptions<TSlots extends string, TInput = unknown>
|
|
|
773
766
|
// conditional on `TSlots`, because the slot overloads infer `TSlots` from `repos`, and no
|
|
774
767
|
// inference reaches through a conditional type.
|
|
775
768
|
/** The options of a `workspace()` with no Repo Slot that declares the input that starts a run.
|
|
776
|
-
* `repos` is left out
|
|
769
|
+
* `repos` is left out. */
|
|
777
770
|
export type NoSlotWorkspaceOptions<TSchema extends z.ZodObject> = Omit<SandboxOptions<never>, "repos"> & {
|
|
778
771
|
/** Not present: the Workspace has no Repo Slot. Its Agents work in `/work`. */
|
|
779
772
|
repos?: never;
|
|
780
773
|
/** The input that starts a run, as a `z.object`, as in {@link WorkspaceOptions}. */
|
|
781
774
|
input: TSchema;
|
|
782
|
-
/** A function from the run input to the Workspace's {@link NoSlotSpec}: only `workGroup`.
|
|
783
|
-
* Default: no `workGroup`. */
|
|
784
|
-
spec?: (args: { input: WithoutEmptyIndex<z.infer<TSchema>> & HostInjectedInput }) => NoSlotSpec;
|
|
785
775
|
};
|
|
786
776
|
|
|
787
777
|
/** The options of a `workspace()` with no Repo Slot and no `input`: it starts with any input. */
|
|
788
|
-
export type PermissiveNoSlotWorkspaceOptions
|
|
778
|
+
export type PermissiveNoSlotWorkspaceOptions = Omit<SandboxOptions<never>, "repos"> & {
|
|
789
779
|
/** Not present: the Workspace has no Repo Slot. Its Agents work in `/work`. */
|
|
790
780
|
repos?: never;
|
|
791
781
|
/** Not present: a `workspace()` without `input` starts with any input. */
|
|
792
782
|
input?: never;
|
|
793
|
-
/** A function from the input to the Workspace's {@link NoSlotSpec}: only `workGroup`. Default:
|
|
794
|
-
* no `workGroup`. */
|
|
795
|
-
spec?: (args: { input: TInput }) => NoSlotSpec;
|
|
796
783
|
};
|
|
797
784
|
|
|
798
785
|
/**
|
|
799
|
-
* Run a body Machine in a Sandbox: a pod of its own, with a
|
|
800
|
-
*
|
|
786
|
+
* Run a body Machine in a Sandbox: a pod of its own, with a clone of each Repo Slot. The result is
|
|
787
|
+
* a Machine. Register it as a Workflow, or invoke it from another Machine as
|
|
801
788
|
* an actor slot.
|
|
802
789
|
*
|
|
803
790
|
* When a run enters it, the Workspace makes the Sandbox and waits for room on a node, with no
|
|
804
|
-
* deadline. It clones each Repo Slot
|
|
805
|
-
*
|
|
806
|
-
*
|
|
791
|
+
* deadline. It clones each Repo Slot to `/work/<slot>/default`, on the Binding's base, and makes no
|
|
792
|
+
* Worktree: the body cuts each Worktree that it needs with `worktree()`. A slot that is only read
|
|
793
|
+
* needs no cut. No work happens in `default/`. Then it runs the body with the run input plus
|
|
794
|
+
* `workspace`, the handles (see {@link Workspaced}). The Agents with Working tools and the `command` Effects in the body run in that Sandbox. An Agent
|
|
807
795
|
* with `workspace: "none"` runs on the Instance Harness. When the body reaches a final state, the
|
|
808
|
-
* Workspace deletes the Sandbox, and its output is the body's output. Push
|
|
796
|
+
* Workspace deletes the Sandbox, and its output is the body's output. Push each branch before
|
|
809
797
|
* then: jr2 deletes the Sandbox's commits with it.
|
|
810
798
|
*
|
|
799
|
+
* The author chooses how many Agents work in a Workspace. Agents in one Worktree share git, as
|
|
800
|
+
* people in one checkout do. Each coder that needs its own branch gets its own Worktree of the one
|
|
801
|
+
* clone.
|
|
802
|
+
*
|
|
811
803
|
* Leave out `repos` for a Workspace with no Repo Slot: its Agents and `command` Effects share
|
|
812
|
-
* `/work`, as the steps of one job share a folder,
|
|
813
|
-
* `
|
|
814
|
-
*
|
|
815
|
-
* need a Repo Slot, so jr2 refuses them there. jr2 deletes `/work` with the Sandbox: send what
|
|
804
|
+
* `/work`, as the steps of one job share a folder. The body's input is then `Workspaced<RunInput, never>`. An `http` Effect sends a
|
|
805
|
+
* file of `/work` with `file(path)`. A `worktree()`, a `push`, and a `command` with an Effect secret in its
|
|
806
|
+
* `env`, need a Repo Slot, so jr2 refuses them there. jr2 deletes `/work` with the Sandbox: send what
|
|
816
807
|
* must stay before the body ends.
|
|
817
808
|
*
|
|
818
809
|
* Give each run a different branch, so that two runs do not push to one branch. Make it from a
|
|
819
810
|
* field of the run input, or from `instanceId` when the run input has no field that is different
|
|
820
|
-
* for each run: ``
|
|
821
|
-
* `
|
|
822
|
-
* `workspace()` gets `instanceId` from jr2. A `workspace()` that a different Machine invokes gets
|
|
811
|
+
* for each run: ``branch: `fix/${context.instanceId}` `` in the input of the cut. Add
|
|
812
|
+
* `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
|
|
823
813
|
* it only when its parent gives it.
|
|
824
814
|
*
|
|
825
815
|
* The Sandbox is Lost when its pod is evicted or its node goes. Then the body gets the event
|
|
@@ -843,7 +833,7 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
843
833
|
* - a body with its own `input`: ``workspace(): the body "<id>" declares its own run input, which
|
|
844
834
|
* nothing will ever serve or enforce — the wrapper feeds the body the run input PLUS the injected
|
|
845
835
|
* `workspace` handles, so the body's schema is not the door. Move it to the wrapper:
|
|
846
|
-
* workspace(body, { input
|
|
836
|
+
* workspace(body, { input }) (ADR-0033).``
|
|
847
837
|
* - an empty `image`: ``workspace(): `image` must be a non-empty string (got <value>) — either a
|
|
848
838
|
* `file:` URL to a docker context this module ships (`import.meta.resolve("./image")`) or a
|
|
849
839
|
* registry ref (ADR-0037).``
|
|
@@ -853,9 +843,7 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
853
843
|
* `repos: { app: "https://…" }` — or `open` whole, for a map the consumer names; leave it out
|
|
854
844
|
* for a Workspace with no Repo Slot (ADR-0051).``
|
|
855
845
|
* - a bad Repo Slot name or value (see {@link RepoSlot})
|
|
856
|
-
* - a `
|
|
857
|
-
* `spec` must be a function from the run input to the spec — spec: ({ input }) => ({ branch }) —
|
|
858
|
-
* and only a Workspace with no Repo Slot may leave it out (ADR-0051).``
|
|
846
|
+
* - a `workGroup` that is not a gid: ``workspace(): `workGroup` must be a gid (got <value>)``
|
|
859
847
|
* - a bad `secrets` list: ``workspace(): `secrets` must be a list of distinct names from
|
|
860
848
|
* jr2.config.ts's `secrets` list (got <value>) — secrets: ["JIRA_TOKEN"] (ADR-0059).``
|
|
861
849
|
* - a bad `effector`: ``workspace(): `effector` is { image } — a `file:` URL to a docker context
|
|
@@ -871,17 +859,14 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
871
859
|
* (ADR-0060)``, or the same text for `resources.limits.<key>`. The other texts are in
|
|
872
860
|
* {@link Size}.
|
|
873
861
|
*
|
|
874
|
-
* And when a run enters it, a
|
|
875
|
-
*
|
|
876
|
-
* does `jr2 run --input` carry every field this workflow's workspace()
|
|
877
|
-
* Repo Slot, a spec with a `branch` or a `reviewSha`: ``workspace spec invalid: <field> (the
|
|
878
|
-
* Workspace has no Repo Slot, so it has no branch and no review Worktree; its spec gives only
|
|
879
|
-
* workGroup) …``
|
|
862
|
+
* And when a run enters it, a per-run Repo Slot's mapper that gives no `url`, or a bad `ref`:
|
|
863
|
+
* ``workspace invalid: repos.<slot> mapper returned <fields> — the mapper derives from run
|
|
864
|
+
* input; does `jr2 run --input` carry every field this workflow's workspace() slot reads?``
|
|
880
865
|
*
|
|
881
866
|
* @param body - The Machine to run in the Sandbox. Its input is `Workspaced<RunInput, Slots>`.
|
|
882
867
|
* @param options - What the Sandbox is made of (`image`, `user`, `resources`, `effector`), the
|
|
883
|
-
* Repo Slots it clones (`repos`), the Held secrets it can use (`secrets`), the
|
|
884
|
-
* (`
|
|
868
|
+
* Repo Slots it clones (`repos`), the Held secrets it can use (`secrets`), the pod's `fsGroup`
|
|
869
|
+
* (`workGroup`), and the run input (`input`).
|
|
885
870
|
* @returns A Machine whose input is the run input and whose output is the body's output.
|
|
886
871
|
*
|
|
887
872
|
* @example
|
|
@@ -890,8 +875,10 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
890
875
|
* import {
|
|
891
876
|
* command,
|
|
892
877
|
* fail,
|
|
878
|
+
* isReady,
|
|
893
879
|
* jr2Setup,
|
|
894
880
|
* workspace,
|
|
881
|
+
* worktree,
|
|
895
882
|
* type CommandOutput,
|
|
896
883
|
* type Workspaced,
|
|
897
884
|
* } from "@jr2/orchestrator";
|
|
@@ -899,23 +886,36 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
899
886
|
* const runInput = z.object({ ticket: z.string(), repo: z.string() });
|
|
900
887
|
* type BodyInput = Workspaced<z.infer<typeof runInput>, "app" | "docs">;
|
|
901
888
|
*
|
|
889
|
+
* // The Worktree of this run. An Agent or a command that only reads `docs` uses `/work/docs/default`.
|
|
890
|
+
* const buildTree = (context: BodyInput) => `${context.workspace.repos.app}/build` as const;
|
|
891
|
+
*
|
|
902
892
|
* const passed = (output: CommandOutput): boolean =>
|
|
903
893
|
* output.outcome === "exited" && output.code === 0;
|
|
904
894
|
*
|
|
905
895
|
* const body = jr2Setup({
|
|
906
|
-
* types: {} as { input: BodyInput },
|
|
896
|
+
* types: {} as { input: BodyInput; context: BodyInput },
|
|
907
897
|
* actors: {
|
|
908
|
-
*
|
|
898
|
+
* cut: worktree(),
|
|
899
|
+
* build: command(["npm", "run", "build"]),
|
|
909
900
|
* fail: fail(),
|
|
910
901
|
* },
|
|
911
902
|
* }).createMachine({
|
|
912
903
|
* id: "build",
|
|
904
|
+
* context: ({ input }) => input,
|
|
913
905
|
* on: { "workspace.lost": ".lost" },
|
|
914
|
-
* initial: "
|
|
906
|
+
* initial: "cutting",
|
|
915
907
|
* states: {
|
|
908
|
+
* cutting: {
|
|
909
|
+
* invoke: {
|
|
910
|
+
* src: "cut",
|
|
911
|
+
* input: ({ context }) => ({ path: buildTree(context), branch: `fix/${context.ticket}` }),
|
|
912
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "building" }, { target: "notCut" }],
|
|
913
|
+
* },
|
|
914
|
+
* },
|
|
916
915
|
* building: {
|
|
917
916
|
* invoke: {
|
|
918
917
|
* src: "build",
|
|
918
|
+
* input: ({ context }) => ({ worktree: buildTree(context) }),
|
|
919
919
|
* onDone: [
|
|
920
920
|
* { guard: ({ event }) => passed(event.output), target: "built" },
|
|
921
921
|
* { target: "broken" },
|
|
@@ -924,6 +924,7 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
924
924
|
* },
|
|
925
925
|
* built: { type: "final" },
|
|
926
926
|
* broken: { invoke: { src: "fail", input: { reason: "npm run build did not pass" } } },
|
|
927
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
927
928
|
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
928
929
|
* },
|
|
929
930
|
* });
|
|
@@ -936,7 +937,6 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
936
937
|
* app: ({ input }) => input.repo,
|
|
937
938
|
* docs: { url: "https://github.com/you/docs.git", ref: "main" },
|
|
938
939
|
* },
|
|
939
|
-
* spec: ({ input }) => ({ branch: `fix/${input.ticket}` }),
|
|
940
940
|
* });
|
|
941
941
|
* ```
|
|
942
942
|
*
|
|
@@ -946,9 +946,12 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
946
946
|
* import {
|
|
947
947
|
* command,
|
|
948
948
|
* fail,
|
|
949
|
+
* isReady,
|
|
949
950
|
* jr2Setup,
|
|
950
951
|
* workspace,
|
|
952
|
+
* worktree,
|
|
951
953
|
* type CommandOutput,
|
|
954
|
+
* type HostInjectedInput,
|
|
952
955
|
* type Workspaced,
|
|
953
956
|
* } from "@jr2/orchestrator";
|
|
954
957
|
*
|
|
@@ -958,18 +961,30 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
958
961
|
* // The run input has no field that is different for each run, so the branch comes from
|
|
959
962
|
* // `instanceId`.
|
|
960
963
|
* const runInput = z.object({});
|
|
964
|
+
* type BodyInput = Workspaced<z.infer<typeof runInput> & HostInjectedInput, "app">;
|
|
965
|
+
*
|
|
966
|
+
* const nightlyTree = (context: BodyInput) => `${context.workspace.repos.app}/nightly` as const;
|
|
961
967
|
*
|
|
962
968
|
* const body = jr2Setup({
|
|
963
|
-
* types: {} as { input:
|
|
964
|
-
* actors: { test: command(["npm", "test"]
|
|
969
|
+
* types: {} as { input: BodyInput; context: BodyInput },
|
|
970
|
+
* actors: { cut: worktree(), test: command(["npm", "test"]), fail: fail() },
|
|
965
971
|
* }).createMachine({
|
|
966
972
|
* id: "nightly",
|
|
973
|
+
* context: ({ input }) => input,
|
|
967
974
|
* on: { "workspace.lost": ".lost" },
|
|
968
|
-
* initial: "
|
|
975
|
+
* initial: "cutting",
|
|
969
976
|
* states: {
|
|
977
|
+
* cutting: {
|
|
978
|
+
* invoke: {
|
|
979
|
+
* src: "cut",
|
|
980
|
+
* input: ({ context }) => ({ path: nightlyTree(context), branch: `nightly/${context.instanceId}` }),
|
|
981
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "testing" }, { target: "notCut" }],
|
|
982
|
+
* },
|
|
983
|
+
* },
|
|
970
984
|
* testing: {
|
|
971
985
|
* invoke: {
|
|
972
986
|
* src: "test",
|
|
987
|
+
* input: ({ context }) => ({ worktree: nightlyTree(context) }),
|
|
973
988
|
* onDone: [
|
|
974
989
|
* { guard: ({ event }) => passed(event.output), target: "tested" },
|
|
975
990
|
* { target: "red" },
|
|
@@ -978,6 +993,7 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
978
993
|
* },
|
|
979
994
|
* tested: { type: "final" },
|
|
980
995
|
* red: { invoke: { src: "fail", input: { reason: "npm test did not pass" } } },
|
|
996
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
981
997
|
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
982
998
|
* },
|
|
983
999
|
* });
|
|
@@ -986,7 +1002,6 @@ export type PermissiveNoSlotWorkspaceOptions<TInput = unknown> = Omit<SandboxOpt
|
|
|
986
1002
|
* input: runInput,
|
|
987
1003
|
* image: "docker.io/library/node:24",
|
|
988
1004
|
* repos: { app: "https://github.com/you/app.git" },
|
|
989
|
-
* spec: ({ input }) => ({ branch: `fix/${input.instanceId}` }),
|
|
990
1005
|
* });
|
|
991
1006
|
* ```
|
|
992
1007
|
*
|
|
@@ -1004,7 +1019,7 @@ export function workspace<TSchema extends z.ZodObject, TBody extends AnyStateMac
|
|
|
1004
1019
|
body: TBody & BodyAcceptsDoor<TBody, z.infer<TSchema>, TSlots>,
|
|
1005
1020
|
options: WorkspaceOptions<TSchema, TSlots> | NoSlotWorkspaceOptions<TSchema>,
|
|
1006
1021
|
// The input is the door without the empty door's `Record<string, never>` (`{}` for `z.object({})`), so
|
|
1007
|
-
// a parent can give `instanceId` to a nested
|
|
1022
|
+
// a parent can give `instanceId` to a nested body that reads it (ADR-0033).
|
|
1008
1023
|
): WorkspaceMachine<WithoutEmptyIndex<z.infer<TSchema>>, OutputFrom<TBody>, TBody, TSlots>;
|
|
1009
1024
|
/**
|
|
1010
1025
|
* Run a body Machine in a Sandbox, with no `input` schema: the Workspace starts with any input. Use
|
|
@@ -1013,26 +1028,30 @@ export function workspace<TSchema extends z.ZodObject, TBody extends AnyStateMac
|
|
|
1013
1028
|
*
|
|
1014
1029
|
* @param body - The Machine to run in the Sandbox. Its input is the input of the Workspace plus
|
|
1015
1030
|
* `workspace`, the handles.
|
|
1016
|
-
* @param options - The options of the Sandbox
|
|
1031
|
+
* @param options - The options of the Sandbox. There is no `input`.
|
|
1017
1032
|
* @returns A Machine whose input is the run input and whose output is the body's output.
|
|
1018
1033
|
*
|
|
1019
1034
|
* @see docs/concepts/workspaces.md
|
|
1020
1035
|
* @see docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md
|
|
1021
1036
|
*/
|
|
1022
|
-
export function workspace<
|
|
1037
|
+
export function workspace<
|
|
1038
|
+
TBody extends AnyStateMachine,
|
|
1039
|
+
TSlots extends string = never,
|
|
1040
|
+
// Nothing else annotates the input of a door-less Workspace, so the body says it.
|
|
1041
|
+
TInput = DoorlessInput<TBody>,
|
|
1042
|
+
>(
|
|
1023
1043
|
body: TBody & BodyAcceptsSlots<TBody, TSlots>,
|
|
1024
|
-
|
|
1044
|
+
// `undefined` is for the invoke alone: a run with no input gives a mapper `{}`.
|
|
1045
|
+
options: PermissiveWorkspaceOptions<TSlots, Exclude<TInput, undefined>> | PermissiveNoSlotWorkspaceOptions,
|
|
1025
1046
|
): WorkspaceMachine<TInput, OutputFrom<TBody>, TBody, TSlots>;
|
|
1026
1047
|
export function workspace(
|
|
1027
1048
|
body: AnyStateMachine,
|
|
1028
1049
|
options: Omit<SandboxOptions<string, any>, "repos"> & {
|
|
1029
1050
|
repos?: SandboxOptions<string, any>["repos"];
|
|
1030
1051
|
input?: z.ZodObject;
|
|
1031
|
-
spec?: (args: { input: any }) => WorkspaceSpec | NoSlotSpec;
|
|
1032
1052
|
},
|
|
1033
1053
|
): AnyStateMachine {
|
|
1034
|
-
//
|
|
1035
|
-
// the wrapper's input plus the handles — `Workspaced<TInput, TSlots>`, which is also what the
|
|
1054
|
+
// The body receives the wrapper's input plus the handles — `Workspaced<TInput, TSlots>`, which is also what the
|
|
1036
1055
|
// declared door checks the body against. A Failure of the body is caught: the wrapper destroys
|
|
1037
1056
|
// the Sandbox and fails again with the same payload (ADR-0073). Every other body error (a defect)
|
|
1038
1057
|
// is not caught: it faults the run (RunStatus.fault) and leaves the Sandbox to the operator's
|
|
@@ -1048,7 +1067,7 @@ export function workspace(
|
|
|
1048
1067
|
`workspace(): the body "${body.id}" declares its own run input, which nothing will ever ` +
|
|
1049
1068
|
"serve or enforce — the wrapper feeds the body the run input PLUS the injected `workspace` " +
|
|
1050
1069
|
"handles, so the body's schema is not the door. Move it to the wrapper: " +
|
|
1051
|
-
"workspace(body, { input
|
|
1070
|
+
"workspace(body, { input }) (ADR-0033).",
|
|
1052
1071
|
);
|
|
1053
1072
|
}
|
|
1054
1073
|
// A body state that cannot route `workspace.lost` (ADR-0074): with no route, the body would show
|
|
@@ -1057,7 +1076,7 @@ export function workspace(
|
|
|
1057
1076
|
// not judged: its states are jr2's, and the walk refuses a Workspace below a Workspace (ADR-0059).
|
|
1058
1077
|
if (!composesSandbox(body) && wrapperBodyOf(body) === undefined) refuseUnroutedLost(body);
|
|
1059
1078
|
// Static, so checkable NOW rather than at the first provision — an empty or non-string image is
|
|
1060
|
-
//
|
|
1079
|
+
// a derives-from-a-typo bug, and this catches it one build earlier.
|
|
1061
1080
|
const image = options.image;
|
|
1062
1081
|
if (image !== undefined && (typeof image !== "string" || !image)) {
|
|
1063
1082
|
throw new Error(
|
|
@@ -1118,15 +1137,10 @@ export function workspace(
|
|
|
1118
1137
|
`${JSON.stringify(effector)}) — effector: { image: import.meta.resolve("./effector") } (ADR-0068).`,
|
|
1119
1138
|
);
|
|
1120
1139
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
|
|
1125
|
-
"workspace(): `spec` must be a function from the run input to the spec — spec: ({ input }) => ({ branch }) — " +
|
|
1126
|
-
"and only a Workspace with no Repo Slot may leave it out (ADR-0051).",
|
|
1127
|
-
);
|
|
1128
|
-
}
|
|
1129
|
-
const wrapper = buildWorkspaceMachine(body, options.spec ?? (() => ({})));
|
|
1140
|
+
const workGroup = options.workGroup;
|
|
1141
|
+
assertWorkGroup(workGroup);
|
|
1142
|
+
if (repos === open || Object.keys(repos).length > 0) refuseUnnamedWorktrees(body);
|
|
1143
|
+
const wrapper = buildWorkspaceMachine(body);
|
|
1130
1144
|
// The wrapper is TRANSPARENT to its body (ADR-0049): `customize(machine, { agents })` on a
|
|
1131
1145
|
// Workspace means the Machine inside, so the composer never spells `body` and never has to know
|
|
1132
1146
|
// that jr2 wrapped anything.
|
|
@@ -1141,6 +1155,7 @@ export function workspace(
|
|
|
1141
1155
|
...(options.user !== undefined ? { user: options.user } : {}),
|
|
1142
1156
|
repos: repos === open ? open : { ...repos },
|
|
1143
1157
|
...(options.resources !== undefined ? { resources: options.resources } : {}),
|
|
1158
|
+
...(workGroup !== undefined ? { workGroup } : {}),
|
|
1144
1159
|
...(secrets !== undefined ? { secrets: [...secrets] } : {}),
|
|
1145
1160
|
...(effector !== undefined ? { effector: { image: effector.image } } : {}),
|
|
1146
1161
|
});
|
|
@@ -1158,54 +1173,75 @@ export function workspace(
|
|
|
1158
1173
|
}
|
|
1159
1174
|
|
|
1160
1175
|
/**
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1176
|
+
* Each Turn and each Effect that works in a Worktree says which, in a Workspace with Repo Slots: the
|
|
1177
|
+
* walk's half of what `jr2Setup` refuses by type, for a plain `setup()`, for a nested key, and for a
|
|
1178
|
+
* body with no typed handles. It sees an invoke with no input and a literal input. A mapper's answer
|
|
1179
|
+
* is its start's to refuse. A Machine below the body is walked too: a child, and a Pool's worker,
|
|
1180
|
+
* which is no invoke, so each Machine in `actors` is walked. A nested Workspace is not: its own
|
|
1181
|
+
* `workspace()` call walks it, by its own Repo Slots.
|
|
1165
1182
|
*/
|
|
1166
|
-
function
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
);
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
)
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1183
|
+
function refuseUnnamedWorktrees(body: AnyStateMachine): void {
|
|
1184
|
+
const faults: string[] = [];
|
|
1185
|
+
const seen = new Set<AnyStateMachine>();
|
|
1186
|
+
const walk = (machine: AnyStateMachine): void => {
|
|
1187
|
+
if (seen.has(machine) || (machine !== body && composesSandbox(machine))) return;
|
|
1188
|
+
seen.add(machine);
|
|
1189
|
+
const actors = (machine.implementations.actors ?? {}) as Record<string, unknown>;
|
|
1190
|
+
for (const logic of Object.values(actors)) if (isMachineLogic(logic)) walk(logic);
|
|
1191
|
+
const nodes = [machine.root as unknown as InvokeNode];
|
|
1192
|
+
for (const node of nodes) {
|
|
1193
|
+
nodes.push(...Object.values(node.states));
|
|
1194
|
+
const configs = [node.config.invoke ?? []].flat() as Array<{ input?: unknown } | undefined>;
|
|
1195
|
+
node.invoke.forEach((inv, i) => {
|
|
1196
|
+
const logic = typeof inv.src === "string" ? actors[inv.src] : inv.src;
|
|
1197
|
+
if (isMachineLogic(logic)) walk(logic);
|
|
1198
|
+
const key = keyOf(logic);
|
|
1199
|
+
if (key === undefined) return;
|
|
1200
|
+
const given = configs[i]?.input;
|
|
1201
|
+
const written = (given as { consumerInput?: unknown } | undefined)?.consumerInput ?? given;
|
|
1202
|
+
if (typeof written === "function") return;
|
|
1203
|
+
if (typeof written === "object" && written !== null && key in written) return;
|
|
1204
|
+
faults.push(
|
|
1205
|
+
`state "${node.id}": "${typeof inv.src === "string" ? inv.src : inv.id}" names no \`${key}\`, and a Workspace ` +
|
|
1206
|
+
'with Repo Slots says where each Turn and Effect works: a Worktree, or `cwd: "/work"` for a Turn',
|
|
1207
|
+
);
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
walk(body);
|
|
1212
|
+
if (faults.length) throw new Error(`workspace(): ${faults.join("; ")} (ADR-0057)`);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const isMachineLogic = (logic: unknown): logic is AnyStateMachine =>
|
|
1216
|
+
typeof logic === "object" && logic !== null && "implementations" in logic;
|
|
1217
|
+
|
|
1218
|
+
type InvokeNode = {
|
|
1219
|
+
id: string;
|
|
1220
|
+
states: Record<string, InvokeNode>;
|
|
1221
|
+
config: { invoke?: unknown };
|
|
1222
|
+
invoke: Array<{ src: unknown; id: string }>;
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
/** The key that says where this logic works, or undefined when it works nowhere in `/work`. */
|
|
1226
|
+
function keyOf(logic: unknown): "cwd" | "worktree" | undefined {
|
|
1227
|
+
if (isAgent(logic)) return (logic.definition as { workspace?: unknown }).workspace === "none" ? undefined : "cwd";
|
|
1228
|
+
if (!isEffect(logic)) return undefined;
|
|
1229
|
+
const parts = logic.effect;
|
|
1230
|
+
if (parts.kind === "command") return parts.cut ? undefined : "worktree";
|
|
1231
|
+
return parts.kind === "http" && parts.file ? "worktree" : undefined;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** A gid, so an integer: a float or a negative is a pod the API server rejects at admission. */
|
|
1235
|
+
function assertWorkGroup(workGroup: unknown): void {
|
|
1236
|
+
if (workGroup !== undefined && (typeof workGroup !== "number" || !Number.isInteger(workGroup) || workGroup < 0)) {
|
|
1237
|
+
throw new Error(`workspace(): \`workGroup\` must be a gid (got ${JSON.stringify(workGroup)})`);
|
|
1202
1238
|
}
|
|
1203
1239
|
}
|
|
1204
1240
|
|
|
1205
1241
|
/**
|
|
1206
1242
|
* Resolve every Repo Slot to a Binding, in declaration order (ADR-0051). A bound slot is its
|
|
1207
1243
|
* Binding; a per-run slot is its mapper called over the run input, validated like the static forms
|
|
1208
|
-
* because it derives from `jr2 run --input`
|
|
1244
|
+
* because it derives from `jr2 run --input` ; an open slot nobody bound is
|
|
1209
1245
|
* a fault BEFORE the port, naming the `customize` line that fixes it — the run-time twin of the
|
|
1210
1246
|
* refusal `jr2 up`'s walk makes for a registered Machine, reached here only by a Machine that was
|
|
1211
1247
|
* never registered as itself (a test seam, a nested invoke of an unbound import). The Machine is
|
|
@@ -1245,7 +1281,7 @@ function resolveBindings(
|
|
|
1245
1281
|
bad.push(`ref (got ${JSON.stringify(binding.ref)})`);
|
|
1246
1282
|
if (bad.length) {
|
|
1247
1283
|
throw new Error(
|
|
1248
|
-
`workspace
|
|
1284
|
+
`workspace invalid: repos.${slot} mapper returned ${bad.join("; ")} — the mapper derives from run ` +
|
|
1249
1285
|
"input; does `jr2 run --input` carry every field this workflow's workspace() slot reads?",
|
|
1250
1286
|
);
|
|
1251
1287
|
}
|
|
@@ -1254,15 +1290,6 @@ function resolveBindings(
|
|
|
1254
1290
|
return bindings;
|
|
1255
1291
|
}
|
|
1256
1292
|
|
|
1257
|
-
/** A spec as the wrapper holds it: a Workspace's with Repo Slots, or one with none (ADR-0051). */
|
|
1258
|
-
type AnySpec = WorkspaceSpec | NoSlotSpec;
|
|
1259
|
-
|
|
1260
|
-
/** Does this Workspace declare no Repo Slot (ADR-0051)? An Open map is not "none": its slots are
|
|
1261
|
-
* the composer's, and `customize()` names at least one. */
|
|
1262
|
-
function noSlot(slots: SandboxParts["repos"]): boolean {
|
|
1263
|
-
return slots !== open && Object.keys(slots).length === 0;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
1293
|
/** The port-facing view of the persisted bindings, in declaration order. */
|
|
1267
1294
|
function attachedRepos(bindings: Record<string, ResolvedBinding>): Array<{ slot: string; url: string; ref?: string }> {
|
|
1268
1295
|
return Object.entries(bindings).map(([slot, b]) => ({
|
|
@@ -1272,7 +1299,7 @@ function attachedRepos(bindings: Record<string, ResolvedBinding>): Array<{ slot:
|
|
|
1272
1299
|
}));
|
|
1273
1300
|
}
|
|
1274
1301
|
|
|
1275
|
-
function buildWorkspaceMachine(body: AnyStateMachine
|
|
1302
|
+
function buildWorkspaceMachine(body: AnyStateMachine): AnyStateMachine {
|
|
1276
1303
|
/**
|
|
1277
1304
|
* `placing` (ADR-0064): write the Sandbox and wait, with no deadline, for its pod to be
|
|
1278
1305
|
* scheduled. jr2 cannot tell "full now" from "never fits", so a bound is the Machine's own
|
|
@@ -1297,8 +1324,8 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1297
1324
|
* and so the same name, which that Workspace has placed again by then (ADR-0073).
|
|
1298
1325
|
*/
|
|
1299
1326
|
const place = fromPromise<
|
|
1300
|
-
{ bindings: Record<string, ResolvedBinding> },
|
|
1301
|
-
{ wsId: string;
|
|
1327
|
+
{ bindings: Record<string, ResolvedBinding>; runInput: Record<string, unknown> },
|
|
1328
|
+
{ wsId: string; runInput: Record<string, unknown> }
|
|
1302
1329
|
>(async ({ input, self, system, signal }) => {
|
|
1303
1330
|
const binding = runBindingOf(system);
|
|
1304
1331
|
// The images and the slots come off the WRAPPER, at invoke time, not out of context and not
|
|
@@ -1308,15 +1335,21 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1308
1335
|
// content-addressed tag, which would outlive the image it names. The RESOLVED bindings are
|
|
1309
1336
|
// persisted, because a per-run mapper's answer is this run's fact.
|
|
1310
1337
|
const parts = sandboxPartsOf(invokingMachine(self));
|
|
1311
|
-
|
|
1338
|
+
const wrapper = self._parent;
|
|
1339
|
+
// RunHost parsed a root run's door and added `instanceId`, so that value is final: a second
|
|
1340
|
+
// parse would refuse the added key under a strict door and transform a value twice. A parent's
|
|
1341
|
+
// input is parsed here, and a door value that is wrong costs no pod (ADR-0033).
|
|
1342
|
+
const door = wrapper?._parent ? inputSchemaOf(invokingMachine(self)!) : undefined;
|
|
1343
|
+
const runInput = door
|
|
1344
|
+
? parsedNestedDoor(door, input.runInput, ["instanceId"], "workspace input invalid, before any pod")
|
|
1345
|
+
: input.runInput;
|
|
1312
1346
|
const user = userSeatOf(parts.user);
|
|
1313
1347
|
const path = actorSlotPath(self._parent);
|
|
1314
1348
|
// Read before the port: a Workspace no walk can see placed nothing, and is refused with the slot form.
|
|
1315
1349
|
const route = actorRoute(self._parent);
|
|
1316
|
-
const bindings = resolveBindings({ workflow: binding.workflow, path }, parts.repos,
|
|
1350
|
+
const bindings = resolveBindings({ workflow: binding.workflow, path }, parts.repos, runInput);
|
|
1317
1351
|
const port = sandboxOf(system);
|
|
1318
1352
|
const name = workspaceName(binding.runId, input.wsId);
|
|
1319
|
-
const wrapper = self._parent;
|
|
1320
1353
|
const child = wrapper ? actorPath(wrapper).join("/") : "";
|
|
1321
1354
|
// A wait opened before a restart is the same wait: its start is in the wrapper's persisted
|
|
1322
1355
|
// context, never in this actor's input, which a restore replays as it was first invoked.
|
|
@@ -1344,12 +1377,15 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1344
1377
|
name,
|
|
1345
1378
|
runId: binding.runId,
|
|
1346
1379
|
workflow: binding.workflow,
|
|
1380
|
+
path: child || input.wsId,
|
|
1347
1381
|
route,
|
|
1348
1382
|
// The image strings straight through (ADR-0037/0005) — the port owns resolution, and the
|
|
1349
1383
|
// work group's default (ADR-0005 puts it in pod composition, where the pod is built).
|
|
1350
1384
|
...(parts.image !== undefined ? { image: parts.image } : {}),
|
|
1351
1385
|
...(user.image !== undefined ? { user: user.image } : {}),
|
|
1352
|
-
...(
|
|
1386
|
+
...(parts.workGroup !== undefined ? { workGroup: parts.workGroup } : {}),
|
|
1387
|
+
// The Held secrets the `workspace()` grants its confined children (ADR-0076): names only.
|
|
1388
|
+
...(parts.secrets?.length ? { secrets: [...parts.secrets] } : {}),
|
|
1353
1389
|
repos: Object.entries(bindings).map(([slot, b]) => ({ slot, ...b })),
|
|
1354
1390
|
// The Size as the Machine states it, re-read here like the images (ADR-0060): a redeploy
|
|
1355
1391
|
// that retuned it reaches the next provision, and no snapshot holds it.
|
|
@@ -1387,7 +1423,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1387
1423
|
openedAt !== undefined ? Math.round(performance.now() - openedAt) : Math.max(0, Date.now() - Date.parse(since));
|
|
1388
1424
|
binding.marker?.({ kind: "placed", child, since, after });
|
|
1389
1425
|
}
|
|
1390
|
-
return { bindings };
|
|
1426
|
+
return { bindings, runInput };
|
|
1391
1427
|
});
|
|
1392
1428
|
|
|
1393
1429
|
/** `provisioning`: the placed pod's Ready, under the port's budget, which starts here (ADR-0064). */
|
|
@@ -1396,8 +1432,8 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1396
1432
|
);
|
|
1397
1433
|
|
|
1398
1434
|
const attach = fromPromise<
|
|
1399
|
-
{ repos: Record<string, string>;
|
|
1400
|
-
{ wsId: string;
|
|
1435
|
+
{ repos: Record<string, string>; bases?: Record<string, string>; stale?: Record<string, string> },
|
|
1436
|
+
{ wsId: string; bindings: Record<string, ResolvedBinding> }
|
|
1401
1437
|
>(async ({ input, system, signal }) => {
|
|
1402
1438
|
// No Repo Slot, nothing to clone (ADR-0051): the Harness's attach would refuse an empty list,
|
|
1403
1439
|
// and `/work` needs nothing from it — the pod's `fsGroup` already makes it the work group's.
|
|
@@ -1420,9 +1456,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1420
1456
|
signal.addEventListener("abort", () => unsubscribe(), { once: true });
|
|
1421
1457
|
let out: Awaited<ReturnType<typeof port.attach>>;
|
|
1422
1458
|
try {
|
|
1423
|
-
|
|
1424
|
-
const spec = input.spec as WorkspaceSpec;
|
|
1425
|
-
out = await Promise.race([port.attach({ name, spec, repos: attachedRepos(input.bindings) }), loss]);
|
|
1459
|
+
out = await Promise.race([port.attach({ name, repos: attachedRepos(input.bindings) }), loss]);
|
|
1426
1460
|
} finally {
|
|
1427
1461
|
unsubscribe();
|
|
1428
1462
|
}
|
|
@@ -1507,10 +1541,22 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1507
1541
|
let timer: ReturnType<typeof setTimeout>;
|
|
1508
1542
|
const schedule = (ms: number) => {
|
|
1509
1543
|
timer = setTimeout(() => {
|
|
1544
|
+
if (stopped) return;
|
|
1510
1545
|
// A Workspace that ended in error stopped processing without stopping its children (xstate
|
|
1511
|
-
// does not), and a parent that handled the error only stops what still processes:
|
|
1512
|
-
// lease
|
|
1513
|
-
|
|
1546
|
+
// does not), and a parent that handled the error only stops what still processes: so this
|
|
1547
|
+
// lease is never stopped, and its cleanup below never runs. The lease asks, asserts nothing
|
|
1548
|
+
// for a Workspace that is over, and releases the hold mark here, once: no run holds the
|
|
1549
|
+
// Sandbox now (ADR-0021). A host that stops writes nothing, as below.
|
|
1550
|
+
if (self._parent?.getSnapshot().status !== "active") {
|
|
1551
|
+
if (runBindingOf(system).hostStopping) return;
|
|
1552
|
+
stopped = true;
|
|
1553
|
+
unsubscribe();
|
|
1554
|
+
// A parent that caught the defect can invoke the same Machine again: the new Workspace
|
|
1555
|
+
// has the same invoke id, so the same Sandbox name, and its own Lease holds it now.
|
|
1556
|
+
if (runBindingOf(system).heldSandboxes?.().has(name)) return;
|
|
1557
|
+
port.releaseHold(name).catch(() => {});
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1514
1560
|
// A failed renewal is unknown, never lost: the next one is an interval away, well inside
|
|
1515
1561
|
// the idle timeout, and loss is the watch's to report. Before the CR's write lands it is a
|
|
1516
1562
|
// 404, and the same shrug.
|
|
@@ -1524,6 +1570,19 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1524
1570
|
stopped = true;
|
|
1525
1571
|
clearTimeout(timer);
|
|
1526
1572
|
unsubscribe();
|
|
1573
|
+
// The hold mark (ADR-0021). A park writes nothing, so the CR stays `held` and the restored
|
|
1574
|
+
// run's Lease renews it. Else the stop is a step of the wrapper or its end: read the wrapper
|
|
1575
|
+
// once its stop or its transition is over. Still active, it is in a next state with a Lease
|
|
1576
|
+
// of its own, or deletes the Sandbox; over (a cancel, a fault, a parent that left it), no
|
|
1577
|
+
// run holds the Sandbox now.
|
|
1578
|
+
if (runBindingOf(system).hostStopping) return;
|
|
1579
|
+
queueMicrotask(() => {
|
|
1580
|
+
if (runBindingOf(system).hostStopping) return;
|
|
1581
|
+
if (self._parent?.getSnapshot().status === "active") return;
|
|
1582
|
+
// A live Workspace of the same name (a re-invoke after a caught defect) holds it now.
|
|
1583
|
+
if (runBindingOf(system).heldSandboxes?.().has(name)) return;
|
|
1584
|
+
port.releaseHold(name).catch(() => {});
|
|
1585
|
+
});
|
|
1527
1586
|
};
|
|
1528
1587
|
},
|
|
1529
1588
|
);
|
|
@@ -1552,7 +1611,6 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1552
1611
|
context: ({ input, self }: { input: unknown; self: { id: string } }): WsContext => ({
|
|
1553
1612
|
runInput: (input ?? {}) as Record<string, unknown>,
|
|
1554
1613
|
wsId: self.id,
|
|
1555
|
-
spec: spec({ input }),
|
|
1556
1614
|
}),
|
|
1557
1615
|
initial: "placing",
|
|
1558
1616
|
states: {
|
|
@@ -1563,7 +1621,6 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1563
1621
|
src: "place",
|
|
1564
1622
|
input: ({ context }) => ({
|
|
1565
1623
|
wsId: (context as unknown as WsContext).wsId,
|
|
1566
|
-
spec: (context as unknown as WsContext).spec,
|
|
1567
1624
|
runInput: (context as unknown as WsContext).runInput,
|
|
1568
1625
|
}),
|
|
1569
1626
|
onDone: {
|
|
@@ -1571,6 +1628,8 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1571
1628
|
actions: assign({
|
|
1572
1629
|
bindings: ({ event }) =>
|
|
1573
1630
|
(event as unknown as { output: { bindings: Record<string, ResolvedBinding> } }).output.bindings,
|
|
1631
|
+
runInput: ({ event }) =>
|
|
1632
|
+
(event as unknown as { output: { runInput: Record<string, unknown> } }).output.runInput,
|
|
1574
1633
|
wait: undefined,
|
|
1575
1634
|
}),
|
|
1576
1635
|
},
|
|
@@ -1608,7 +1667,6 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1608
1667
|
src: "attach",
|
|
1609
1668
|
input: ({ context }) => ({
|
|
1610
1669
|
wsId: (context as unknown as WsContext).wsId,
|
|
1611
|
-
spec: (context as unknown as WsContext).spec,
|
|
1612
1670
|
bindings: (context as unknown as WsContext).bindings!,
|
|
1613
1671
|
}),
|
|
1614
1672
|
onDone: {
|
|
@@ -1618,7 +1676,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1618
1676
|
const ctx = context as WsContext;
|
|
1619
1677
|
const out = (
|
|
1620
1678
|
event as unknown as {
|
|
1621
|
-
output: { repos: Record<string, string>;
|
|
1679
|
+
output: { repos: Record<string, string>; bases?: Record<string, string> };
|
|
1622
1680
|
}
|
|
1623
1681
|
).output;
|
|
1624
1682
|
return {
|
|
@@ -1634,9 +1692,8 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1634
1692
|
...(ctx.bindings
|
|
1635
1693
|
? { bindings: Object.fromEntries(Object.entries(ctx.bindings).map(([s, b]) => [s, b.url])) }
|
|
1636
1694
|
: {}),
|
|
1637
|
-
//
|
|
1638
|
-
...(
|
|
1639
|
-
...(out.review ? { review: out.review } : {}),
|
|
1695
|
+
// Each slot's base: what the attach put `default/` on, sent with every cut.
|
|
1696
|
+
...(out.bases ? { bases: out.bases } : {}),
|
|
1640
1697
|
};
|
|
1641
1698
|
},
|
|
1642
1699
|
}),
|
|
@@ -1660,12 +1717,10 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
|
|
|
1660
1717
|
src: "body",
|
|
1661
1718
|
input: ({ context }: { context: WsContext }) => {
|
|
1662
1719
|
const ctx = context;
|
|
1663
|
-
const { repos
|
|
1720
|
+
const { repos } = ctx.handles!;
|
|
1664
1721
|
// Body-facing subset only (ADR-0016): endpoint/sandbox are mechanism-internal. With
|
|
1665
1722
|
// no Repo Slot the handles are `{ repos: {} }` (WorkspaceHandles<never>).
|
|
1666
|
-
|
|
1667
|
-
branch === undefined ? { repos: {} } : { repos, branch, ...(review ? { review } : {}) };
|
|
1668
|
-
return { ...ctx.runInput, workspace: handles };
|
|
1723
|
+
return { ...ctx.runInput, workspace: { repos } };
|
|
1669
1724
|
},
|
|
1670
1725
|
onDone: {
|
|
1671
1726
|
target: "teardown",
|