@jr2/orchestrator 0.3.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/bin/server.ts +7 -4
- package/console/canvas.ts +27 -14
- package/console/components/app.ts +12 -1
- package/console/components/fleet.ts +52 -6
- package/console/components/machine-pane.ts +128 -2
- package/console/components/nav.ts +1 -1
- package/console/components/schema-form.ts +12 -4
- package/console/json-field.ts +23 -0
- package/console/main.ts +122 -39
- package/console/store.ts +321 -21
- package/console/style.css +111 -0
- package/docs/agents-block.md +5 -0
- package/docs/concepts/agents-and-turns.md +442 -0
- package/docs/concepts/architecture.md +230 -0
- package/docs/concepts/charts.md +143 -0
- package/docs/concepts/composition.md +357 -0
- package/docs/concepts/converge.md +254 -0
- package/docs/concepts/durable-runs.md +189 -0
- package/docs/concepts/effects.md +603 -0
- package/docs/concepts/gates.md +208 -0
- package/docs/concepts/machines-and-workflows.md +289 -0
- package/docs/concepts/pools-and-sources.md +230 -0
- package/docs/concepts/secrets-and-trust.md +222 -0
- package/docs/concepts/the-menu.md +378 -0
- package/docs/concepts/workspaces.md +678 -0
- package/docs/get-started/a-model-decides.md +563 -0
- package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
- package/docs/get-started/connect-a-model.md +248 -0
- package/docs/get-started/create-an-instance.md +194 -0
- package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
- package/docs/get-started/install.md +126 -0
- package/docs/guides/add-tools-to-the-sandbox.md +295 -0
- package/docs/guides/answer-a-gate.md +304 -0
- package/docs/guides/bind-open-parts.md +223 -0
- package/docs/guides/cancel-a-run.md +146 -0
- package/docs/guides/clean-up.md +206 -0
- package/docs/guides/fix-a-failed-converge.md +361 -0
- package/docs/guides/give-the-cluster-git-credentials.md +222 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
- package/docs/guides/let-an-agent-write-a-chart.md +226 -0
- package/docs/guides/look-inside-a-sandbox.md +174 -0
- package/docs/guides/mirror-the-kit-images.md +176 -0
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
- package/docs/guides/open-the-console.md +91 -0
- package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
- package/docs/guides/push-and-sign-a-branch.md +486 -0
- package/docs/guides/run-a-chart-by-name.md +270 -0
- package/docs/guides/run-a-command-or-call-a-url.md +544 -0
- package/docs/guides/run-one-worker-per-item.md +321 -0
- package/docs/guides/set-an-agents-access-and-dials.md +375 -0
- package/docs/guides/size-a-workspace.md +204 -0
- package/docs/guides/start-a-run-from-a-run.md +237 -0
- package/docs/guides/start-a-run.md +207 -0
- package/docs/guides/upgrade-the-kit-version.md +240 -0
- package/docs/guides/use-a-hosted-provider.md +208 -0
- package/docs/guides/use-a-local-model.md +313 -0
- package/docs/guides/watch-a-run.md +326 -0
- package/docs/guides/write-a-workflow.md +318 -0
- package/docs/index.md +44 -0
- package/docs/llms.txt +216 -0
- package/docs/reference/authoring-surface/agent.md +128 -0
- package/docs/reference/authoring-surface/answer.md +140 -0
- package/docs/reference/authoring-surface/arg.md +60 -0
- package/docs/reference/authoring-surface/basic.md +62 -0
- package/docs/reference/authoring-surface/bearer.md +53 -0
- package/docs/reference/authoring-surface/chartschema.md +23 -0
- package/docs/reference/authoring-surface/chartschemafor.md +48 -0
- package/docs/reference/authoring-surface/command.md +191 -0
- package/docs/reference/authoring-surface/customize.md +64 -0
- package/docs/reference/authoring-surface/defineconfig.md +41 -0
- package/docs/reference/authoring-surface/defineevent.md +114 -0
- package/docs/reference/authoring-surface/dispatch.md +126 -0
- package/docs/reference/authoring-surface/fail.md +83 -0
- package/docs/reference/authoring-surface/file.md +122 -0
- package/docs/reference/authoring-surface/frompick.md +277 -0
- package/docs/reference/authoring-surface/http.md +159 -0
- package/docs/reference/authoring-surface/isfailure.md +58 -0
- package/docs/reference/authoring-surface/jr2setup.md +237 -0
- package/docs/reference/authoring-surface/open.md +74 -0
- package/docs/reference/authoring-surface/openbody.md +86 -0
- package/docs/reference/authoring-surface/paramsof.md +125 -0
- package/docs/reference/authoring-surface/pool.md +186 -0
- package/docs/reference/authoring-surface/push.md +112 -0
- package/docs/reference/authoring-surface/secret.md +118 -0
- package/docs/reference/authoring-surface/sentby.md +83 -0
- package/docs/reference/authoring-surface/source.md +51 -0
- package/docs/reference/authoring-surface/text.md +62 -0
- package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
- package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
- package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
- package/docs/reference/authoring-surface/type-agentslots.md +12 -0
- package/docs/reference/authoring-surface/type-answeraction.md +12 -0
- package/docs/reference/authoring-surface/type-arg.md +19 -0
- package/docs/reference/authoring-surface/type-argnames.md +12 -0
- package/docs/reference/authoring-surface/type-binding.md +62 -0
- package/docs/reference/authoring-surface/type-bindingof.md +12 -0
- package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
- package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
- package/docs/reference/authoring-surface/type-ceilings.md +41 -0
- package/docs/reference/authoring-surface/type-chainlink.md +19 -0
- package/docs/reference/authoring-surface/type-chartaction.md +46 -0
- package/docs/reference/authoring-surface/type-chartagent.md +28 -0
- package/docs/reference/authoring-surface/type-chartevent.md +20 -0
- package/docs/reference/authoring-surface/type-chartguard.md +20 -0
- package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
- package/docs/reference/authoring-surface/type-chartjson.md +14 -0
- package/docs/reference/authoring-surface/type-chartstate.md +46 -0
- package/docs/reference/authoring-surface/type-charttext.md +40 -0
- package/docs/reference/authoring-surface/type-charttransition.md +22 -0
- package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
- package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
- package/docs/reference/authoring-surface/type-childat.md +12 -0
- package/docs/reference/authoring-surface/type-childslots.md +12 -0
- package/docs/reference/authoring-surface/type-commandenv.md +15 -0
- package/docs/reference/authoring-surface/type-commandinput.md +19 -0
- package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
- package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
- package/docs/reference/authoring-surface/type-customize.md +109 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
- package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
- package/docs/reference/authoring-surface/type-duration.md +15 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
- package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
- package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
- package/docs/reference/authoring-surface/type-eventdef.md +45 -0
- package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
- package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
- package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
- package/docs/reference/authoring-surface/type-failslot.md +14 -0
- package/docs/reference/authoring-surface/type-failure.md +19 -0
- package/docs/reference/authoring-surface/type-failureinput.md +20 -0
- package/docs/reference/authoring-surface/type-filebody.md +41 -0
- package/docs/reference/authoring-surface/type-frominput.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
- package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
- package/docs/reference/authoring-surface/type-httpinput.md +12 -0
- package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
- package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
- package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
- package/docs/reference/authoring-surface/type-jr2config.md +126 -0
- package/docs/reference/authoring-surface/type-lends.md +38 -0
- package/docs/reference/authoring-surface/type-logicat.md +12 -0
- package/docs/reference/authoring-surface/type-markedif.md +12 -0
- package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
- package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
- package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
- package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
- package/docs/reference/authoring-surface/type-openeffect.md +12 -0
- package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
- package/docs/reference/authoring-surface/type-openof.md +12 -0
- package/docs/reference/authoring-surface/type-opensecret.md +23 -0
- package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
- package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
- package/docs/reference/authoring-surface/type-poolspec.md +60 -0
- package/docs/reference/authoring-surface/type-pushinput.md +15 -0
- package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
- package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
- package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
- package/docs/reference/authoring-surface/type-quantity.md +14 -0
- package/docs/reference/authoring-surface/type-reached.md +12 -0
- package/docs/reference/authoring-surface/type-reposlot.md +70 -0
- package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
- package/docs/reference/authoring-surface/type-secret.md +25 -0
- package/docs/reference/authoring-surface/type-sender.md +19 -0
- package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
- package/docs/reference/authoring-surface/type-size.md +85 -0
- package/docs/reference/authoring-surface/type-slothandles.md +32 -0
- package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
- package/docs/reference/authoring-surface/type-syncstate.md +12 -0
- package/docs/reference/authoring-surface/type-text.md +19 -0
- package/docs/reference/authoring-surface/type-textpart.md +24 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
- package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
- package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
- package/docs/reference/authoring-surface/type-unknown.md +21 -0
- package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
- package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
- package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
- package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
- package/docs/reference/authoring-surface/type-workspaced.md +94 -0
- package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
- package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
- package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
- package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
- package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
- package/docs/reference/authoring-surface/workspace.md +259 -0
- package/docs/reference/authoring-surface.md +197 -0
- package/docs/reference/cli.md +433 -0
- package/docs/reference/config.md +756 -0
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
- package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
- package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
- package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
- package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
- package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
- package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
- package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
- package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
- package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
- package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
- package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
- package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
- package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
- package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
- package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
- package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
- package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
- package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
- package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
- package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
- package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
- package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
- package/docs/reference/decisions.md +83 -0
- package/docs/reference/glossary.md +648 -0
- package/docs/reference/http-api.md +1348 -0
- package/docs/reference/machines.md +190 -0
- package/docs/reference/mechanism-events.md +72 -0
- package/docs/why.md +206 -0
- package/package.json +6 -4
- package/src/actor.ts +976 -61
- package/src/agent.ts +114 -38
- package/src/ambient.ts +4 -1
- package/src/assembly-checks.ts +347 -0
- package/src/assembly-tar.ts +494 -0
- package/src/assembly.ts +721 -0
- package/src/cause.ts +234 -0
- package/src/chart.ts +2182 -0
- package/src/charts.ts +748 -0
- package/src/config.ts +520 -162
- package/src/content-hash.ts +105 -0
- package/src/custodian.lua +6 -5
- package/src/custodian.ts +143 -39
- package/src/customize.ts +388 -55
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect-checks.ts +557 -0
- package/src/effect.ts +1202 -0
- package/src/effector-wire.ts +674 -0
- package/src/fail.ts +209 -0
- package/src/fingerprint.ts +4 -3
- package/src/gate.ts +8 -0
- package/src/git-clone.ts +65 -0
- package/src/git-push.ts +336 -0
- package/src/harness-client.ts +764 -48
- package/src/held-secrets.ts +82 -26
- package/src/holds.ts +156 -0
- package/src/http-effect.ts +1423 -0
- package/src/http.ts +916 -113
- package/src/images.ts +47 -21
- package/src/index.ts +62 -43
- package/src/instance.ts +505 -59
- package/src/internal.ts +84 -0
- package/src/kube-client.ts +333 -0
- package/src/machine-doc.ts +106 -6
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +18 -58
- package/src/mount-digest.ts +53 -0
- package/src/names.ts +66 -8
- package/src/notices.ts +141 -0
- package/src/open-body.ts +1580 -0
- package/src/open-effect.ts +217 -0
- package/src/open.ts +59 -6
- package/src/parts.ts +709 -41
- package/src/party.ts +534 -0
- package/src/pick.ts +761 -0
- package/src/pool.ts +248 -37
- package/src/registration.ts +382 -19
- package/src/repo-fetch.ts +60 -85
- package/src/repos.ts +65 -75
- package/src/rest.ts +133 -0
- package/src/run-host.ts +2383 -115
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +1126 -0
- package/src/sandbox-effector.ts +111 -0
- package/src/sandbox-kube.ts +1721 -0
- package/src/sandbox-watch.ts +260 -0
- package/src/secret-placement.ts +529 -0
- package/src/server.ts +284 -84
- package/src/setup.ts +819 -117
- package/src/size.ts +454 -0
- package/src/snapshot-store.ts +115 -20
- package/src/stub-harness.ts +49 -16
- package/src/sync-client.ts +54 -0
- package/src/timers.ts +191 -0
- package/src/tokens.ts +35 -4
- package/src/vocabulary.ts +135 -15
- package/src/wire.ts +238 -10
- package/src/workspace.ts +1195 -338
- package/src/sandbox-kubectl.ts +0 -1108
package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# A Repo is a slot on the `workspace()` and a cache on the node
|
|
2
|
+
|
|
3
|
+
ADR-0049 moved Agents and the Sandbox Image into the Machine and left one dependency outside it: the Repo, catalogued in
|
|
4
|
+
`jr2.config.ts` and named by string from a `workspace()` spec, with the name typed back through a `Register` interface
|
|
5
|
+
(ADR-0050). The grill of 2026-09-13 found two faults. A packaged Machine still could not say which repository it works
|
|
6
|
+
on, nor could a consumer bind one, except by the consumer's config holding the right NAME and the door threading it —
|
|
7
|
+
the same encapsulation gap ADR-0049 closed for Agents. And the catalog's shape was dictated by storage: one read-only
|
|
8
|
+
volume of `default/` checkouts, which on a multi-node cluster pins every Sandbox to the node holding it. The two are one
|
|
9
|
+
decision: **what a Machine says about a repository, and what the cluster does with that.**
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
- **A Repo is a slot on the `workspace()`**, beside `image` and `user`, keyed by the Machine's own word for it:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
export const codeReview = workspace(body, {
|
|
17
|
+
input,
|
|
18
|
+
image: import.meta.resolve("./image"),
|
|
19
|
+
repos: {
|
|
20
|
+
target: open, // the consumer's — must be bound
|
|
21
|
+
docs: { url: "https://github.com/acme/handbook.git", ref: "v3" }, // the package's own
|
|
22
|
+
},
|
|
23
|
+
spec: ({ input }) => ({ branch: input.branch }),
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A slot is in one of three states, and the walk tells them apart without evaluating anything: **bound** (a url, or
|
|
28
|
+
`{ url, ref? }`), **open** (the `open` sentinel — someone downstream binds it), or **per-run** (a mapper over the
|
|
29
|
+
wrapper's door: `feature: ({ input }) => ({ url: input.feature.repo, ref: input.feature.baseRef })`). `ref` is the
|
|
30
|
+
base the branch worktree is cut from; absent, the repository's own default branch. The spec no longer carries repos —
|
|
31
|
+
it is `{ branch, workGroup?, reviewSha? }`, or `{ workGroup? }` for a Workspace with no slot (below). The body's
|
|
32
|
+
handles are keyed by slot (`workspace.repos.target`), and so are the in-pod paths (`/work/<slot>/<branch>`): a slot
|
|
33
|
+
key is collision-free by construction and the same in every Instance that consumes the package, so a prompt can name a
|
|
34
|
+
path and be right everywhere.
|
|
35
|
+
|
|
36
|
+
- **The handles are the slot map and the branch — no slot is the `workdir`.** `workspace.repos` keeps the slots in
|
|
37
|
+
declaration order, and jr2 reads nothing into that order. Which checkout an Agent works in is a fact about that
|
|
38
|
+
Agent's Turn, not about the Workspace: a body with a `backend` and a `frontend` slot frames one Agent with the first
|
|
39
|
+
path and another with the second, and either may read the other's tree. A body that names no slot may give the order a
|
|
40
|
+
meaning of its own and document it (`task`: the first slot is the one the coder edits). The order is the kit's promise
|
|
41
|
+
— a slot key starts with a letter, because JS reads an integer-like key first wherever it was written — and the
|
|
42
|
+
meaning is the Machine's. Rejected: a `workdir` handle, the first declared slot's worktree — it made a singular
|
|
43
|
+
"primary repo" a property of every Workspace, which is wrong for a body whose Agents work in different slots or in
|
|
44
|
+
more than one, and it hid a rule in key order that the body never chose.
|
|
45
|
+
|
|
46
|
+
- **A Machine whose body names no slot declares the whole map Open: `repos: open`.** A body that enumerates whatever is
|
|
47
|
+
attached — `@jr2/machines`'s `task` — knows nothing a slot name could say, and a `target: open` on it would claim "one
|
|
48
|
+
repository" where the body claims nothing. So the map is the Open part, one level up from a slot: the composer names
|
|
49
|
+
every slot with `customize`, in the order the Machine documents. The body's handles are typed `Workspaced<…, string>`,
|
|
50
|
+
which is its statement that it names no slot; `workspace()` refuses a body that names one under an Open map, since the
|
|
51
|
+
composer may never write that word. The walk reports an Open map as one Open part with no slot, and `jr2 up` refuses
|
|
52
|
+
it by the same route, with `<slot>` left in the line for the composer to fill. Rejected: `customize` adding keys to a
|
|
53
|
+
declared map — a slot a Machine never declared is a compile error on purpose (a typo cannot mint a clone), and a body
|
|
54
|
+
that named `target` could say nothing about a `reference` it never knew of; the map-level Open says what the body
|
|
55
|
+
actually knows. Rejected: a second Machine of the consumer's own, wrapping the exported body with more slots —
|
|
56
|
+
composition is the right act for a different BODY, but the same body with more checkouts beside it is a binding, and
|
|
57
|
+
`customize` is where bindings go.
|
|
58
|
+
|
|
59
|
+
- **A Workspace with no Repo Slot leaves `repos` out.** A Workspace is a Sandbox bound to one unit of work, and a
|
|
60
|
+
repository is something it may attach, not what makes it one. Some work has no repository: fetch data, write a report,
|
|
61
|
+
have a second Agent edit it, upload it. Its steps share `/work`, as the steps of one GitHub Actions job share that
|
|
62
|
+
job's folder; two Workspaces are two jobs and share nothing. So `workspace(body, { input })` with no `repos` is a
|
|
63
|
+
Workspace with no Repo Slot: the attach is skipped (there is nothing to clone, and the pod's `fsGroup` already makes
|
|
64
|
+
`/work` the work group's), the body's handles are `{ repos: {} }` with no branch, typed `Workspaced<…, never>`, and a
|
|
65
|
+
Turn that frames no `cwd` and a `command` both work in `/work`. The spec has nothing to cut a branch in: `spec` is
|
|
66
|
+
optional, gives only `workGroup`, and a `branch` or a `reviewSha` in it is refused — at the type, and when the run
|
|
67
|
+
enters, since a value jr2 silently ignores is the failure the refusals exist for. `repos: {}` is refused with the
|
|
68
|
+
pointer to leaving it out, so "none" has one spelling and an empty map never reads as one somebody forgot to fill. An
|
|
69
|
+
Open map still asks `customize` for at least one slot: Open says the composer names the slots, and a body that
|
|
70
|
+
enumerates its checkouts (`task`) has nothing to do with none. Two Effects need a slot and are refused there, by
|
|
71
|
+
`jr2 up` and at invoke, with one reason: a `push` publishes a slot's branch, and a `command` with an Effect secret
|
|
72
|
+
runs against one commit of it (ADR-0067). A file-body `http` reads `/work` and stays. `/work` is an `emptyDir`, so a
|
|
73
|
+
Lost Sandbox takes everything with it — with a repository, pushed commits outlive the pod; here only what an Effect
|
|
74
|
+
sent out does, and the body hears `workspace.lost` as any body does. Storage that outlives the pod, the job's
|
|
75
|
+
"artifacts", is a decision of its own. Rejected: a placeholder repository bound to satisfy the rule — a clone nobody
|
|
76
|
+
uses, a branch that means nothing, a Worktree that becomes every Agent's default `cwd`, and a cache entry to warm.
|
|
77
|
+
Rejected: a new term for the repository-less Sandbox — the Glossary's Workspace never named a repository, so none was
|
|
78
|
+
needed.
|
|
79
|
+
|
|
80
|
+
- **The consumer binds with `customize`**, in the same call as Agents and the image, nested through `actors` like every
|
|
81
|
+
other part: `customize(codeReview, { repos: { target: "git@github.com:ourorg/app.git" } })`. Any of the three forms is
|
|
82
|
+
accepted, so a consumer can also bind a mapper over the child's door. A slot the Machine does not declare is a compile
|
|
83
|
+
error (a phantom on the wrapper type, read through `pool()`'s `worker` and `workspace()`'s `body` as the image is). A
|
|
84
|
+
registered Machine with an open slot nobody bound is refused by `jr2 up`'s walk, before it builds anything, naming the
|
|
85
|
+
Machine, the slot, and the `customize` line that fixes it. That is the one check that is converge-time and not
|
|
86
|
+
compile-time: a `workflows/` export has no type to hang it on (ADR-0050).
|
|
87
|
+
- **The url is the identity.** There is no repo name. `jr2.config.ts` loses `repos`; `Register`, `RepoName`,
|
|
88
|
+
`repoNames()`, and the `const` on `defineConfig` retire. ADR-0050's rule holds vacuously: the string a Machine writes
|
|
89
|
+
is the thing, not a reference into someone else's file. Two spellings of one repository (`https://`, `git@…:`,
|
|
90
|
+
`ssh://`, with or without `.git`) resolve to one identity — host plus path, scheme and user dropped — and therefore
|
|
91
|
+
one cache. A door that offers a menu of repositories is the Instance's own `z.enum` of urls.
|
|
92
|
+
- **The cluster keeps a bare cache per node, not a volume.** Each repository is a `Repo` custom resource the operator
|
|
93
|
+
reconciles (spec: url, refresh interval; status: per node — synced, generation, last error). A cache agent runs as a
|
|
94
|
+
DaemonSet per Instance over its Sandbox nodes (ADR-0052) with a hostPath directory: it clones a repository onto its
|
|
95
|
+
node the first time a Sandbox there needs it, fetches in place on the interval and on demand before an attach, and
|
|
96
|
+
pins gc exactly as ADR-0004 requires. The Sandbox CR names the repositories it needs by identity; the operator mounts
|
|
97
|
+
each node cache read-only, sets a soft node affinity toward nodes whose status holds them, and reports `Ready` only
|
|
98
|
+
once every one is present and has been fetched since the CR asked. The agent works per Repo, four at once on a node (a
|
|
99
|
+
kit value; a fetch is network and CPU beside the Sandboxes it serves), so one Repo's cold clone never holds another's
|
|
100
|
+
on-demand fetch; every cache speaks git protocol v2, so a fetch advertises only the refs it asks for; and the refresh
|
|
101
|
+
interval carries ±20% jitter, like the Lease, so R Repos on N nodes do not hit a remote on one beat (scaling review
|
|
102
|
+
R13, 2026-09-30). The agent also consolidates a cache's packs without deleting an object (ADR-0004). The
|
|
103
|
+
Orchestrator's attach then runs unchanged: `git clone --shared` off the mount, worktree beside it. A Sandbox that
|
|
104
|
+
lands on a cold node pays one clone there, once — the same economics as an image pull, and the reason a Sandbox can
|
|
105
|
+
run on any node.
|
|
106
|
+
- **The Orchestrator creates `Repo` CRs; it does not sync them.** At boot it walks its registered Machines and creates
|
|
107
|
+
one CR per bound identity, so a statically known repository is KNOWN before a run can ask: the cache agent on every
|
|
108
|
+
node probes it (`git ls-remote`) as soon as the CR exists, so a wrong url or a credential that does not reach shows in
|
|
109
|
+
`jr2 status` right after converge, not at the first run. Known is not warm: the clone happens on a node the first time
|
|
110
|
+
a Sandbox there needs the repository, never on every node at boot — cloning a large repository onto every node that
|
|
111
|
+
may never run a Sandbox for it is the cost the image-pull economics above exist to avoid. A per-run url creates its CR
|
|
112
|
+
at first attach, and every later attach anywhere finds it. The boot restates a bound CR's url and `secretRef` at every
|
|
113
|
+
deploy; an attach restates the `secretRef` of a CR nothing binds, since no boot will — so a `git.credentials` entry
|
|
114
|
+
fixed after a failed clone reaches the cache at the next run either way. Only an attach moves the eviction clock: a
|
|
115
|
+
boot is not an attach.
|
|
116
|
+
- **Credentials are matched by prefix and carried by the CR, the Argo and Flux shape.** `jr2.config.ts` holds
|
|
117
|
+
`git.credentials`, a list of `{ match, token?, sshKey? }`: `match` is a prefix on the identity (`github.com/ourorg/`,
|
|
118
|
+
or `*`), `token` names an env var whose value `jr2 up` writes into that entry's own clone Secret,
|
|
119
|
+
`jr2-git-<hash of match>` (labelled, and deleted when the entry no longer has one; an unset variable writes none),
|
|
120
|
+
listing the Secrets it wrote in `git-clone.json` in `jr2-held`, names only; `sshKey` names a Secret holding a deploy
|
|
121
|
+
key; the url's scheme picks which field applies, and the longest match wins. When the Orchestrator creates a `Repo` CR
|
|
122
|
+
it resolves the entry and writes a `secretRef`, the name of a Secret `jr2 up` wrote or the user made — a token entry
|
|
123
|
+
whose clone Secret `git-clone.json` does not list gets none, and the clone is anonymous; the cache agent reads only
|
|
124
|
+
that Secret, and the operator matches nothing. The Orchestrator writes and reads no git credential (ADR-0068). The
|
|
125
|
+
Secret uses Flux's key names (`username`/`password`; `identity`, `identity.pub`, `known_hosts`), so a Flux or Argo
|
|
126
|
+
user reuses the Secret they have. **The list is also the fence.** A per-run url — a run input, a ticket field — is
|
|
127
|
+
otherwise a way to spend the cluster's credential against any host: one that matches no entry is refused at attach,
|
|
128
|
+
naming the list. A static binding is code the Instance typechecked and deployed, so it is admitted without a match and
|
|
129
|
+
cloned anonymously; a private one then fails at the cache with git's error. `jr2 init` scaffolds one wildcard entry —
|
|
130
|
+
`{ match: "*", token: "JR2_GIT_TOKEN", sshKey: "jr2-git-ssh" }`, today's two implicit defaults made visible, with a
|
|
131
|
+
comment saying to narrow it before anything untrusted can start a run, and that it also pushes: a `push()` whose Repo
|
|
132
|
+
Slot url it matches, a per-run url included, spends the same token or key — so the open fence is in the user's file
|
|
133
|
+
and commit, never implied.
|
|
134
|
+
- **An entry may carry a push credential and a signing key** (ADR-0067), checked by shape when the config loads:
|
|
135
|
+
`push?: { sshKey } | { token }`, the credential a `push()` to a Binding under the entry spends — absent, the clone
|
|
136
|
+
credential pushes, and an entry with no credential admits a clone, never a push; a token goes Basic over https through
|
|
137
|
+
a credential helper only, which answers for the Binding's own host alone, and the push follows no redirect, so the
|
|
138
|
+
token never reaches an origin outside the entry. `sign?: { sshKey, name, email } | { gpgKey, name, email }`, the key a
|
|
139
|
+
`push({ sign: true })` signs with and the committer it signs as — explicit, so an auth key is never reused as a
|
|
140
|
+
signing key; an ssh key is the default format, a GPG key is a Secret with `private.asc` and no passphrase. `name` and
|
|
141
|
+
`email` are required: the Effector re-creates the commits, and a forge verifies a signature only when the committer
|
|
142
|
+
email belongs to the account that holds the key. The author line stays the Agent's. `committer?: { name, email }`, who
|
|
143
|
+
an unsigned `push({ squash: true })` is committed by, at the Effector's clock (ADR-0067): a squash is a new commit,
|
|
144
|
+
and without `sign` no signer names its committer; `jr2 up` refuses an unsigned squash whose entry has neither `sign`
|
|
145
|
+
nor `committer`.
|
|
146
|
+
- **The pod's maker places the push credential, by reference, when the url is known.** When a registered Machine pushes,
|
|
147
|
+
`jr2 up` writes each entry's push and sign credentials into that entry's own Secret, `jr2-git-push-<hash of match>`
|
|
148
|
+
(labelled, and deleted when the entry no longer has one), never per Workspace: the token from `.env` in Flux's key
|
|
149
|
+
names (`username`, `password`); the keys of the Secret its ssh key names, copied as data and never decoded
|
|
150
|
+
(`identity`, `identity.pub`, `known_hosts`); the signing key as `sign.identity` or `sign.private.asc`; and
|
|
151
|
+
`entry.json`, what the entry is (its match, what it pushes with, the committer it signs as). An entry with no `push`
|
|
152
|
+
field pushes with its clone credentials, each over its own scheme, so the scaffold's wildcard holds both. An entry
|
|
153
|
+
with nothing it can place — its token unset, its key's Secret absent — gets no Secret, and `jr2 up` says why. It
|
|
154
|
+
writes `git-push.json` in `jr2-held` beside them: which entries have a Secret and what each can spend, names only. At
|
|
155
|
+
provision the Orchestrator matches each pushed slot's url — static, per-run or `customize()`-bound, one path — against
|
|
156
|
+
the fence (longest prefix, the attach match), and the Sandbox CR carries the matched entry's Secret name
|
|
157
|
+
(`gitCredentials`, the `secretRef` shape); the operator projects it into the Sandbox Effector alone (ADR-0005). The
|
|
158
|
+
Orchestrator reads no value. The Effector holds only the entries named there and matches a push's url as the fence
|
|
159
|
+
does, longest match first, and refuses a url outside the entry.
|
|
160
|
+
- **A push is refused with a fix that works, at `jr2 up` for a static Binding and at provision for any url.** One rule
|
|
161
|
+
(`pushRefusal`), and the fix each refusal names is one the same rule then accepts: a url no entry matches (the entry
|
|
162
|
+
to add, its `match` the url's host and directories, so always a prefix of the url); a local path (git's local
|
|
163
|
+
transport runs that repository's config and hooks) or `git://`; an `http://` url — a push credential goes over https
|
|
164
|
+
or ssh only: a token over http is the token in the clear, and no ssh key is spent over http — told the url its entry
|
|
165
|
+
can push to (https for a token, ssh for an ssh key) or, when no entry or no credential is there, the https url and the
|
|
166
|
+
token to add; an entry that holds no credential the url's scheme can spend (a token over https, an ssh key over ssh):
|
|
167
|
+
the clone field to add when it holds a clone credential and no `push` field, since a `push` field would replace that
|
|
168
|
+
credential, the `push` field when it holds none, and the other scheme's url when its `push` field already names the
|
|
169
|
+
other credential, since a `push` field names one; and `sign: true` on an entry with no `sign` field (the field to
|
|
170
|
+
add). `jr2 up` reads a static Binding's entry as written, then refuses an https Binding whose entry's token variable
|
|
171
|
+
is unset. A key's Secret, or a signing key's, that is absent at `jr2 up` is a warning, not a refusal: it may be
|
|
172
|
+
written after (ADR-0047's prompt, a human); the entry's Secret is written without that key, and the provision refuses
|
|
173
|
+
the push that needs it, naming the Secret to create and `jr2 up` to run again. The provision reads the same entry as
|
|
174
|
+
`jr2 up` placed it, so a per-run url is refused at attach for the same reasons, as a url outside the fence is, before
|
|
175
|
+
anything is written. Accepted costs: `jr2 up`'s report is exact for a static Binding and says "the entry its url
|
|
176
|
+
matches" for a per-run slot; a wildcard entry gives a per-run push the catch-all credential, the fence's existing
|
|
177
|
+
choice.
|
|
178
|
+
- **Freshness degrades, absence does not.** A fetch that fails on a warm cache lets the attach proceed on the objects it
|
|
179
|
+
has, announced as stale with git's own error. A stale attach is stale until the next fetch anyone inside the pod runs:
|
|
180
|
+
the worktree's `origin` asks the cache, and the cache asks the remote (ADR-0053) — no container the Agent executes in
|
|
181
|
+
holds a credential (ADR-0005), and the same failure lets that fetch fall through to the cache's objects with a warning
|
|
182
|
+
on stderr. A clone that fails on a cold node fails that provision pointedly, naming the repository and the error, and
|
|
183
|
+
the CR status carries it for `jr2 status` (ADR-0048's pattern).
|
|
184
|
+
- **Eviction is reachability plus age.** `jr2 gc` deletes a `Repo` CR that no Machine of a held Assembly binds (the
|
|
185
|
+
current one's, and every one a live run holds,
|
|
186
|
+
[ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)) and no run has attached
|
|
187
|
+
within its TTL; the cache agent removes the node copy on CR deletion, once no pod on that node mounts it.
|
|
188
|
+
|
|
189
|
+
## Considered options
|
|
190
|
+
|
|
191
|
+
- **Image volumes** (each repository generation packed as an OCI artifact, mounted by kubelet): gives per-node cache,
|
|
192
|
+
scheduling, and GC for free, and immutability makes the gc invariant structural. Rejected: a refresh is a new layer
|
|
193
|
+
per fetch, so packs accumulate until a repack, and a repack is a full re-upload and a full re-pull on every node — the
|
|
194
|
+
re-clone cost a mutable bare repo never pays. Immutability bought only what gc pinning already buys.
|
|
195
|
+
- **A per-repo PersistentVolume.** Rejected: an RWO volume still binds to one node at a time; it changes nothing about
|
|
196
|
+
pinning, only the number of volumes.
|
|
197
|
+
- **Keep the shared PVC, require RWX storage for multi-node.** Rejected: scales the storage, not the fetch, and makes
|
|
198
|
+
the kit's first cluster requirement a storage class most small clusters lack.
|
|
199
|
+
- **A Flux-style `GitRepository` serving an artifact over HTTP.** Rejected: an artifact download is a clone per Sandbox
|
|
200
|
+
— the cost the cache exists to avoid.
|
|
201
|
+
- **Urls kept in the spec, not the slot** (the shape before this decision, with a url in place of a name). Rejected: the
|
|
202
|
+
walk cannot see inside the spec, so a bound repository would be invisible to `jr2 up` and to prewarm, and `customize`
|
|
203
|
+
would have nothing to bind.
|
|
204
|
+
|
|
205
|
+
## Consequences
|
|
206
|
+
|
|
207
|
+
- The operator becomes git-aware, which ADR-0001 kept it from being: the `Repo` CRD and the cache agent are its. The
|
|
208
|
+
Sandbox CRD stays free of git semantics beyond a list of identities; clone and worktree remain the Orchestrator's
|
|
209
|
+
post-`Ready` step (ADR-0004).
|
|
210
|
+
- `workspace()`'s `repos` names at least one slot, or is `open` whole — in which case the `customize` that names the map
|
|
211
|
+
is held to the same rule — or is left out, for a Workspace with no Repo Slot; a Machine that composes no Sandbox has
|
|
212
|
+
no repos to customize, and the data-plane switch ("does this Instance need Sandboxes") is "a registered Machine
|
|
213
|
+
composes a Sandbox", read off the walk, not "config has repos". That switch decides what `jr2 up` DEPLOYS for
|
|
214
|
+
Sandboxes, the cache agent and the Sandbox Images; a deployed Orchestrator wires its kube client whatever its current
|
|
215
|
+
Assembly composes, because a publish can bring the first `workspace()` without a roll
|
|
216
|
+
([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)).
|
|
217
|
+
- Every slot gets a branch worktree cut at the run's branch, a reference checkout included. A slot has no read-only
|
|
218
|
+
mode; that is a decision of its own if it is ever wanted.
|
|
219
|
+
- The Console's start form loses the catalog dropdown: a per-run repository is a url field unless the Machine's door
|
|
220
|
+
enumerates its own.
|
|
221
|
+
- The volume paths change: the in-pod source mount is `/repos/<key>` where `<key>` is derived from the identity, and the
|
|
222
|
+
pod-local layout is `/work/<slot>/{default,<branch>}`. Existing checkouts on a pre-0051 PVC are not migrated.
|
|
223
|
+
- ADR-0047's deploy-key prompt reads ssh urls off the walk. A per-run ssh url first seen at attach was never prompted
|
|
224
|
+
for; its first attach fails with the key hint, as any unregistered key does.
|
|
225
|
+
- A `customize` of a per-run slot with a static url is legal and makes it bound; a Machine that wants a slot to stay the
|
|
226
|
+
run's declares it per-run and the consumer leaves it alone. Which slots a consumer may fix is the package author's
|
|
227
|
+
call, expressed by the slot's state.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# A Sandbox lands where an ordinary pod lands, and the cache follows it
|
|
2
|
+
|
|
3
|
+
[ADR-0051](0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md) made a Sandbox schedulable on any node and
|
|
4
|
+
put a Repo cache agent on every node as a DaemonSet that tolerates every taint. The grill of 2026-09-13 found the two
|
|
5
|
+
halves disagreeing about which nodes matter: the Sandbox pod carries no tolerations and no selector, so it lands only
|
|
6
|
+
where an ordinary pod lands, while the cache agent probes and clones on control-plane and GPU nodes no Sandbox can reach
|
|
7
|
+
— and a `present` cache there scores a node the scheduler cannot use. Meanwhile
|
|
8
|
+
[ADR-0045](0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md) read a third set (every node not
|
|
9
|
+
cordoned) to choose build platforms, and nothing in `jr2.config.ts` could say which nodes an Instance's Sandboxes may
|
|
10
|
+
use. **Which nodes are an Instance's Sandbox nodes, and who follows that set** is one decision.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
- **A Sandbox node is where an ordinary pod lands. There is no jr2 node label.** A node is a Sandbox node when it is not
|
|
15
|
+
cordoned, matches the Instance's `sandbox.nodeSelector`, and carries no taint the Instance's `sandbox.tolerations` do
|
|
16
|
+
not tolerate. With no config that is exactly the default scheduler's answer: taints already express "not for ordinary
|
|
17
|
+
work" for every workload on the cluster, so jr2 reads them instead of asking for a second, jr2-only opt-in that every
|
|
18
|
+
kind cluster and every fresh pool would have to perform before `jr2 up` could work.
|
|
19
|
+
- **The override is Instance-level and raw Kubernetes**: `sandbox: { nodeSelector?, tolerations? }` in `jr2.config.ts`,
|
|
20
|
+
pod-spec shapes verbatim, the stance `harness.env` takes for `EnvVar`. A node label or taint is a deployment fact a
|
|
21
|
+
Machine cannot know (ADR-0050), so it lives in the file that holds deployment facts. No `affinity` key: the operator
|
|
22
|
+
owns the Sandbox's affinity (the soft preference toward nodes holding its Repo caches), and a user preference beside
|
|
23
|
+
it would need a merge rule for a case no cluster has asked for. A requirement (`nodeSelector`) never conflicts with a
|
|
24
|
+
preference, so the two sit side by side untouched.
|
|
25
|
+
- **One predicate, three readers.** The Sandbox pod, the cache-agent DaemonSet, and `jr2 up`'s summary all use the same
|
|
26
|
+
`nodeSelector` and `tolerations`. The DaemonSet loses its blanket `operator: Exists` toleration: one cache agent per
|
|
27
|
+
Sandbox node, so a cache is only ever present where a Sandbox can land, and the affinity it feeds only ever names a
|
|
28
|
+
usable node. The build set (ADR-0045) is the union of the nodes an ordinary pod lands on — the Orchestrator
|
|
29
|
+
Deployment's own placement — and the Sandbox nodes, intersected with the supported set as before.
|
|
30
|
+
- **The Sandbox CR carries the placement.** `Sandbox.spec` gains `nodeSelector` and `tolerations` beside `resources`,
|
|
31
|
+
`initContainers`, and `volumes`; the Orchestrator writes them from its baked config at provision and the operator
|
|
32
|
+
copies them onto the pod, merging nothing. The operator stays config-blind (ADR-0001), and every Instance's placement
|
|
33
|
+
is its own: the CR is namespaced, the DaemonSet is per Instance with a per-namespace hostPath, and the shared operator
|
|
34
|
+
holds no placement state. Version skew is ADR-0019's contract: an older operator would place a pod without the new
|
|
35
|
+
fields, and the `jr2 up` that introduces them is the one that upgrades the operator.
|
|
36
|
+
- **An empty set warns; it never refuses.** `jr2 up` prints the Sandbox nodes it sees, and when there are none names
|
|
37
|
+
each node with the taint or selector that excluded it and the `sandbox.*` line that would admit it — then converges.
|
|
38
|
+
The set moves: a pool autoscales from zero, a node is cordoned mid-converge, a GPU box joins tomorrow. A refusal would
|
|
39
|
+
turn `jr2 up` into a point-in-time check of a moving set. The run-time truth is the operator's: a `Pending` Sandbox
|
|
40
|
+
reports the scheduler's own `FailedScheduling` message through its condition, so `jr2 status` shows the taint or
|
|
41
|
+
selector while the run waits (ADR-0048's pattern).
|
|
42
|
+
- **A Machine says nothing about placement — for now.** A packaged `workspace()` that needs a GPU cannot write the
|
|
43
|
+
consumer's label (the encapsulation gap ADR-0049/0051 closed for Agents and Repos). The extension, when a Machine
|
|
44
|
+
needs it, is a named class: `workspace(body, { node: "gpu" })` and
|
|
45
|
+
`sandbox: { classes: { gpu: { nodeSelector, tolerations } } }`, refused by `jr2 up`'s walk when unmapped — the Repo
|
|
46
|
+
Slot pattern. `sandbox.nodeSelector` and `sandbox.tolerations` are that shape's default class, so the extension adds
|
|
47
|
+
`classes` beside them and renames nothing; and because placement rides the CR, a per-Machine class needs no new
|
|
48
|
+
plumbing. A class is placement only: how big a Sandbox is belongs to the Machine, not the cluster, and rides the
|
|
49
|
+
`workspace()` as its Size ([ADR-0060](0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md)).
|
|
50
|
+
`sandbox.resources` sits beside `nodeSelector` and `tolerations` as the Instance's default Size, for a Workspace that
|
|
51
|
+
states none.
|
|
52
|
+
|
|
53
|
+
## Considered options
|
|
54
|
+
|
|
55
|
+
- **A jr2 label** (`jr2.dev/sandbox=true` on nodes that opt in). Rejected: every cluster pays a labeling step before the
|
|
56
|
+
first `jr2 up`, a single-node kind cluster included, and a labeled node is a candidate for every Sandbox — a GPU node
|
|
57
|
+
labeled for one Machine hosts all of them. Taints already say what the label would, for every workload at once.
|
|
58
|
+
- **A jr2 vocabulary** (`sandbox.nodes: ["node-4"]`, or `pool: "workers"`). Rejected: simpler to type and wrong on the
|
|
59
|
+
first autoscaled pool, where node names are not stable and "pool" is a cloud provider's word with a different label on
|
|
60
|
+
each.
|
|
61
|
+
- **The operator reads placement from a per-Instance object** (a ConfigMap, or the DaemonSet's own template) instead of
|
|
62
|
+
the CR. Rejected: a second read per reconcile and a cross-object dependency the CR does not declare, for no gain — the
|
|
63
|
+
CR already carries everything else the pod needs.
|
|
64
|
+
- **Refuse `jr2 up` on an empty set**, the ADR-0051 stance for an unbound slot. Rejected: a slot is a static fact of the
|
|
65
|
+
code; the node set is a fact of the cluster right now.
|
|
66
|
+
- **Keep the DaemonSet on every node.** Rejected: a cache on a node no Sandbox reaches is a clone nobody reads, a probe
|
|
67
|
+
that fails on a control-plane node's egress rules, and an affinity term the scheduler cannot honor.
|
|
68
|
+
|
|
69
|
+
## Consequences
|
|
70
|
+
|
|
71
|
+
- ADR-0045 is rewritten in place: "schedulable nodes" is the union above, and the `platforms` escape hatch's "polluted
|
|
72
|
+
set" case narrows to an untainted pool of a foreign arch — a tainted GPU pool is no longer counted.
|
|
73
|
+
- ADR-0051 is rewritten in place: the cache agent is a DaemonSet over the Sandbox nodes, not every node.
|
|
74
|
+
- The DaemonSet controller auto-tolerates `node.kubernetes.io/unschedulable`, so a cache agent also sits on a cordoned
|
|
75
|
+
Sandbox node. Harmless: the scheduler ignores the soft affinity toward a node it cannot use.
|
|
76
|
+
- A Sandbox node of a NEW architecture that joins after a converge meets a Sandbox Image the cluster never built for it
|
|
77
|
+
and fails with `exec format error` until the next `jr2 up` — as any node did before this ADR; `platforms` is the
|
|
78
|
+
answer for a pool that autoscales from zero.
|
|
79
|
+
- The Orchestrator and operator Deployments keep their own placement; `sandbox.*` names Sandboxes only.
|
|
80
|
+
- A `NoExecute` taint an Instance tolerates on the Sandbox pod is tolerated verbatim: jr2 adds no `tolerationSeconds`,
|
|
81
|
+
so a Sandbox stays through the taint for as long as its Lease is renewed (ADR-0021).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# A fetch inside the pod asks the node cache, and the cache asks the remote
|
|
2
|
+
|
|
3
|
+
[ADR-0051](0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md) made the node cache the only thing that
|
|
4
|
+
fetches from a remote, and left one consequence standing: stale stays stale from inside the pod until the interval or
|
|
5
|
+
the next attach. The only ask a cache agent understood was a pod's birth — `asked` is the newest pod's creation time,
|
|
6
|
+
and a Sandbox is `Ready` once its caches were fetched after its CR was created. Nothing after birth could ask: no CLI,
|
|
7
|
+
no route out of the pod, no body primitive. The grill of 2026-09-13 took the concrete case — a human pushes a fix to
|
|
8
|
+
`master`, the coder at `request_changes` must rebase onto it now, and the human in the User Container of the same pod
|
|
9
|
+
wants the same commit — and found that a five-minute interval is not a freshness model, it is the absence of one. **How
|
|
10
|
+
a fetch inside a pod reaches the remote, and who may ask** is one decision.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
- **`origin`'s fetch url is a command, not a path.** The attach sets it to git's built-in `ext::` transport, an absolute
|
|
15
|
+
program on the runtime volume with the Repo's identity as its argument:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
ext::/opt/jr2/bin/jr2-upload-pack %S github.com/acme/app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Git runs the program and bridges stdio, so the program owes no remote-helper protocol: it asks for a fetch, waits for
|
|
22
|
+
the landing, then execs `git upload-pack /repos/<key>` and gets out of the way. Git speaks its ordinary protocol and
|
|
23
|
+
prints its ordinary output; refs update once, at the end. What the caller sees is a fetch on a slow handshake, never
|
|
24
|
+
an early return and never a partial state. `git fetch`, `git pull`, `git ls-remote origin`, and `git fetch --dry-run`
|
|
25
|
+
all pass through it, so every fetch inside a pod is a fetch of the remote's now. `git archive --remote=origin` is the
|
|
26
|
+
one other read git asks a remote for, and the program serves it the same way — the same ask, then `git upload-archive`
|
|
27
|
+
against the same cache — because the path url served it and a decision about fetching may not quietly take it away.
|
|
28
|
+
Nothing else in git talks to a remote, so `status`, `log`, and `rebase` stay local. The push url is unchanged: the
|
|
29
|
+
Binding's own spelling, the caller's own credential, never the Agent's (ADR-0005).
|
|
30
|
+
|
|
31
|
+
- **The ask rides the Sandbox CR and reaches the agent through the pod.** The program asks the Custodian on `localhost`
|
|
32
|
+
(`POST /fetch`, with the Sandbox token's Stand-in — a constant, because the User Container gets no env); the Custodian
|
|
33
|
+
puts the token it holds in place of the Stand-in and asks the Orchestrator for THIS pod's Sandbox
|
|
34
|
+
(`POST /sandboxes/<name>/fetch` — the program cannot name another,
|
|
35
|
+
[ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)); the Orchestrator patches one
|
|
36
|
+
annotation on the Sandbox CR, per Repo key, timestamp value — the Lease's shape, an annotation the Orchestrator
|
|
37
|
+
writes. The operator copies it onto the pod as it copies everything else the pod needs from the CR, and the cache
|
|
38
|
+
agent's `asked` becomes the later of the pod's creation and its ask for that key. Demand stays read off pods alone.
|
|
39
|
+
The mark is a coalescer: however many fetches are in flight before one lands, the remote is fetched once, and a fetch
|
|
40
|
+
that started before the ask does not satisfy it — the agent stamps `lastFetched` with the attempt's start, which is
|
|
41
|
+
what makes that true.
|
|
42
|
+
- **The landing is reported on the Sandbox, standing and per key.** The verdict the operator already computes on the way
|
|
43
|
+
to `Ready` — fresh, stale with git's error, or still cloning, per Repo, against the CR's creation — becomes a standing
|
|
44
|
+
entry on the Sandbox status computed against the later of creation and the ask. `Ready` itself stays as it was:
|
|
45
|
+
sticky, once per pod life. The Orchestrator's route waits on that entry — a Sandbox watch event, never a poll
|
|
46
|
+
([ADR-0063](0063-the-orchestrator-watches-the-cluster.md)) — then answers the Custodian, which answers the program.
|
|
47
|
+
One wait, and it is the wait an attach already makes.
|
|
48
|
+
- **The scope is the Sandbox token's scope, and no seat gains a credential.** A Sandbox may ask for the caches it mounts
|
|
49
|
+
and nothing else; the remote is reached by the cache agent with the Repo CR's `secretRef`, the credential that cloned
|
|
50
|
+
the cache. The pod holds none before and none after. The Menu is untouched — a fetch is not something the Agent
|
|
51
|
+
_says_, it is something it _does_ — and no Machine declares anything: the ability is ambient in every Workspace pod,
|
|
52
|
+
the way Working tools are, and absent on the Instance Harness, which mounts no Repo.
|
|
53
|
+
- **Freshness degrades, absence does not, and the caller is told.** A remote fetch that fails or outruns the on-demand
|
|
54
|
+
budget lets the program fall through to the objects the cache holds — ADR-0051's stance for attach — and writes one
|
|
55
|
+
line to stderr, which git passes through verbatim:
|
|
56
|
+
`warning: jr2: remote fetch failed (<git's error>); serving the cache as of <lastFetched>`. The budget is the cache
|
|
57
|
+
agent's one on-demand number; the program's wait is that plus watch slack, and nothing else owns a timeout.
|
|
58
|
+
- **Every seat that holds the checkouts holds the program.** The fetch url lives in the shared `default/.git/config`, so
|
|
59
|
+
a seat without the program has checkouts whose `git fetch` dies. That is ADR-0005's own argument for `/repos` — half
|
|
60
|
+
of one exception, not a second — applied once more: the pod's `/opt/jr2` volume is mounted read-only into the User
|
|
61
|
+
Container beside `/repos`. `ext::` names the program by absolute path, so the seat needs no PATH and jr2 injects no
|
|
62
|
+
env; the program is a static binary, the `work-acl` rule (ADR-0037), because it executes on a libc jr2 does not
|
|
63
|
+
control. A human in the User Container gets the same `git fetch` as the Agent, with no credential of their own; the
|
|
64
|
+
human `exec`'d into the Harness container already had it.
|
|
65
|
+
- **Names.** `fetch` is what an Agent, a human, and a cache do; `refresh` is what the interval does and stays on
|
|
66
|
+
`refreshInterval`; an _ask_ is the mark, the word the agent and the operator already use. The url carries the Repo's
|
|
67
|
+
identity, never the cache key — a key is a derived directory name, never chosen by a human (ADR-0004), and not the
|
|
68
|
+
name a human should read in `git remote -v`.
|
|
69
|
+
- **What stays.** An attach still fetches before it runs, because a creation is an ask. The interval still runs, so a
|
|
70
|
+
cold attach and `jr2 status` stay honest without anyone asking. There is no CLI verb: the human who wants the fetch is
|
|
71
|
+
in the pod, where `git fetch` is the verb, and the mark makes a `jr2` verb additive if a caller outside the pod ever
|
|
72
|
+
needs one.
|
|
73
|
+
|
|
74
|
+
## Considered options
|
|
75
|
+
|
|
76
|
+
- **The run asks** — a body primitive before a Turn, or on `request_changes` re-entry, the way attach asks on entry.
|
|
77
|
+
Rejected as the only path: it covers what the author foresaw and nothing an Agent discovers mid-Turn. Not foreclosed:
|
|
78
|
+
the mark is the substrate, and a body primitive that sets it is additive.
|
|
79
|
+
- **A Menu pick** (`need_fresh`, declared with `defineEvent`, a `fetching` state the pick transitions to). Rejected: a
|
|
80
|
+
Turn boundary per fetch, an author opt-in per state, and a transition for something that moves the Machine nowhere. A
|
|
81
|
+
fetch is not part of a Machine's Vocabulary.
|
|
82
|
+
- **A named Menu tool** (`mcp__jr2__fetch` beside the Menu's events). Rejected: the model must learn a tool and remember
|
|
83
|
+
to call it before the verb it already knows, and a human at a shell gets nothing from it.
|
|
84
|
+
- **Shorten the interval.** Rejected: polling per node per Repo, and the latency is still the interval.
|
|
85
|
+
- **The ask on the Repo CR.** Rejected: every node holding the cache fetches, and the wait must then name a node.
|
|
86
|
+
- **The cache agent reads Sandbox CRs.** Rejected: two sources of demand where one exists, and a new RBAC grant for the
|
|
87
|
+
DaemonSet, to save one copy the operator makes for every other field.
|
|
88
|
+
- **A `jr2::` remote helper found on PATH.** Rejected: PATH is env, the User Container gets none (ADR-0005), and an owed
|
|
89
|
+
image line where `ext::` costs nothing but a longer line in `git remote -v`.
|
|
90
|
+
- **The human's credentialed fetch pushed into the cache.** Rejected: the cache is read-only in the pod by design, the
|
|
91
|
+
structural half of the gc invariant (ADR-0004). And moot: with this decision the human's own credential is not needed
|
|
92
|
+
for a fetch at all.
|
|
93
|
+
- **Fail the fetch when the remote fails.** Rejected: attach chose availability (ADR-0051), and the Agent's `git fetch`
|
|
94
|
+
would become less reliable than today, where it always succeeds against the cache.
|
|
95
|
+
|
|
96
|
+
## Consequences
|
|
97
|
+
|
|
98
|
+
- ADR-0051 is rewritten in place: "stale stays stale from inside the pod" is retired; a stale attach is stale until the
|
|
99
|
+
next fetch anyone inside the pod runs. ADR-0005 is rewritten in place: "`git fetch origin` reads the node cache" is
|
|
100
|
+
now "asks the node cache", the User Container mounts `/opt/jr2` read-only beside `/repos`, and "nothing injected"
|
|
101
|
+
narrows to the seat's process — no env, no command, no probe. ADR-0004's refresh section gains the in-pod ask.
|
|
102
|
+
- The runtime volume carries a second static binary, and so a jr2 program is present in every seat of every Workspace
|
|
103
|
+
pod, User Container included. It is the natural home for in-pod jr2 functionality that does not yet exist.
|
|
104
|
+
- A fetch inside a pod is a remote round trip through the node agent, seconds rather than milliseconds. Every git
|
|
105
|
+
command that fetches pays it; none that does not.
|
|
106
|
+
- The Sandbox status grows a standing per-key Repo entry; the Custodian carries one loopback route for it; the
|
|
107
|
+
Orchestrator gains a Sandbox-token route. No new credential, no new watch in the cache agent, no git knowledge in the
|
|
108
|
+
operator beyond the timestamp comparison it already makes.
|
|
109
|
+
- The rewind gotcha vanishes: a human who fetched around the cache with their own credential advanced a remote-tracking
|
|
110
|
+
ref the Agent's next cache fetch forced back. Nobody fetches around the cache now.
|
|
111
|
+
- The Harness `bash` tool's own timeout still bounds a fetch the way it bounds any slow command.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# The kit ships Machines, and a shipped Machine leaves its parts Open
|
|
2
|
+
|
|
3
|
+
The kit's only worked Machines lived in `examples/`: two model Instances, one of them (`coding`) four workflows deep,
|
|
4
|
+
written against surfaces three ADRs old, runnable by nobody but the author, and installable by no one — an example is
|
|
5
|
+
copied, never imported. ADR-0049 had already settled that "a package exports a Machine, nothing beside it" and ADR-0019
|
|
6
|
+
that "reusable work is published as npm packages", yet the kit itself shipped none, so the packaged-Machine story had no
|
|
7
|
+
first instance to hold it honest. The grill of 2026-09-14 replaced the examples with a package, and in doing so found
|
|
8
|
+
the one part ADR-0049/0051 left a package unable to leave unsaid: **the model.**
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
- **The kit ships Machines as `@jr2/machines`.** A fourth published package beside
|
|
13
|
+
`@jr2/{cli,orchestrator,agent-protocol}` (ADR-0043), on the same release train, exporting named Machines from its
|
|
14
|
+
index: `import { task } from "@jr2/machines"`. It ships Machines and never Workflows — a Workflow is the name an
|
|
15
|
+
Instance registers a Machine under, and only the Instance's `workflows/` file can do that (GLOSSARY.md, ADR-0009).
|
|
16
|
+
Rejected: `@jr2/workflows` — it names exactly the thing the kit does not ship, and `workflow` is on Machine's
|
|
17
|
+
avoid-list for the reason ADR-0049/0050 lean on the distinction.
|
|
18
|
+
- **`@jr2/machines` authors against the consumer's `@jr2/orchestrator`: peer dependencies, no regular ones.** Every part
|
|
19
|
+
a Machine carries is keyed in maps `@jr2/orchestrator` holds (vocabulary.ts, parts.ts) and in `xstate`'s own `provide`
|
|
20
|
+
and `isMachine`; a second copy of either is a Machine whose Menus are empty and whose Gates accept nothing, with no
|
|
21
|
+
error anywhere. `peerDependencies: { "@jr2/orchestrator": <exact kit version>, xstate, zod }` states the fact and is
|
|
22
|
+
the shape a user's own Machine package copies. Rejected: regular deps at the exact version — dedupe makes it work by
|
|
23
|
+
the accident of equal versions and installs two copies silently on skew.
|
|
24
|
+
- **A shipped Machine leaves its parts Open, and Open widens from the Repo Slot to the Agent's model.** ADR-0051's
|
|
25
|
+
`open` sentinel — "a consumer's to bind, with `customize`" — applies unchanged to the one part a package cannot
|
|
26
|
+
honestly fill: `agent({ model: open, instructions })`. The `jr2 up` walk reports an Open model beside Open Repo Slots
|
|
27
|
+
and refuses the converge naming the `customize` line that binds it; the Agent actor refuses to admit a Turn whose
|
|
28
|
+
definition is still Open, as the second fence. `AgentDefinition` stays the wire type with `model: string` — a Symbol
|
|
29
|
+
does not ride a Turn — and `agent()` takes a declaration whose `model` is `string | typeof open`. The sentinel becomes
|
|
30
|
+
`Symbol.for("jr2.open")`. Rejected: a stock default model — the kit picks a vendor in a published package, and a user
|
|
31
|
+
who never reads the file spends money on a model they did not choose, silently, which is the failure class `jr2 up`
|
|
32
|
+
exists to make loud. Rejected: a Machine factory `task({ model })` — ADR-0049 settled that a package exports a Machine
|
|
33
|
+
and retunes with `customize`, and a factory reopens that.
|
|
34
|
+
- **Open widens to an Effect's url, argv and secret, bound once.** A package cannot know the host its `http` Effect
|
|
35
|
+
calls, the program its `command` runs
|
|
36
|
+
([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)), or the secret its
|
|
37
|
+
composer spends, so it may leave each Open: `http({ method, url: open, auth, input, request })`,
|
|
38
|
+
`http({ method: "PUT", url: open, body: file(path) })`, `command(open, { env, pin })`, and `secret(open)` in place of
|
|
39
|
+
a secret's name. The url, the argv and the secret's name are the Open parts of an Effect that exist; everything else
|
|
40
|
+
the definition says is the package's as written — the method, the headers, the auth and the secrets they name, the
|
|
41
|
+
input, `request`, the env, `pin` — and is checked when the Machine is built. `secret(open)` is taken by `bearer`,
|
|
42
|
+
`basic` (the user or the password), a header's value, and a command's env, as its value or its `.path`; in a url it is
|
|
43
|
+
refused when the Machine is built ("leave the whole url Open"), since a url that carries a secret is left Open whole
|
|
44
|
+
(below). An Effect takes one Open secret: a second `secret(open)` in one definition is refused when the Machine is
|
|
45
|
+
built ("an Effect takes one Open secret"); the one `secret(open)` may stand in several places, a value and its
|
|
46
|
+
`.path`. An Effect that names an Open secret is placed as one that names a secret is — a credentialed command in the
|
|
47
|
+
Sandbox Effector, an `http` where its body places it — before the name is known. The composer binds the parts with
|
|
48
|
+
`customize(machine, { effects: { <slot>: { url } } })`, `{ argv }` or `{ secret }`, and an Effect with an Open url or
|
|
49
|
+
argv beside an Open secret takes one object with both keys (`{ url, secret }`, `{ argv, secret }`): a url is a string
|
|
50
|
+
or a `text` with a secret in its path, an argv is the usual list, and a secret is a name that obeys `secret()`'s own
|
|
51
|
+
rule — it is the secret's file in the Effector's mount (ADR-0068). The type of `effects` is the intersection of the
|
|
52
|
+
Open parts' bindings, so a binding that leaves one of them unbound is a compile error, as an unbound url is; at run
|
|
53
|
+
time, a binding that leaves one unbound gives an Effect still Open in it, which a later `customize()` may bind. Each
|
|
54
|
+
is bound ONCE, to a value: never a mapper over the door, so a host is fixed in the definition or bound before any run,
|
|
55
|
+
and no run chooses one, nor a secret. Binding builds the Effect again with its constructor, with the bound name in
|
|
56
|
+
every place `secret(open)` stood, so a bound part meets every rule the constructor checks — a literal host, no secret
|
|
57
|
+
in a query, a non-empty argv, a secret's name — and the bound Effect is the one the same definition written with that
|
|
58
|
+
name gives; `jr2 up` reads it as written: the placement, the secrets it names, and the Effector's fence
|
|
59
|
+
([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)) are the bound ones, and the bound secret
|
|
60
|
+
is placed only in that Effect's Effector, as a named secret is. Only an Open part is offered: a part the author fixed
|
|
61
|
+
is no key of `effects` and is refused at the call, and a part bound once is fixed from then on, so a second composer
|
|
62
|
+
cannot move it. `effects` reaches through `workspace()`'s body, `pool()`'s worker and a composed Machine's `actors`,
|
|
63
|
+
as `agents` does, typed off the slot's logic. An Open argv's slots are the composer's, so the load walk counts every
|
|
64
|
+
pick param as read by it when the Machine is built, and `customize()` runs the walk's pick-param check again once the
|
|
65
|
+
argv is bound — a param the bound argv does not read is refused there, and a warning it gives for the bound argv is
|
|
66
|
+
the bound Machine's, as it is the same Machine's written with that argv: the bound Machine gets a config of its own,
|
|
67
|
+
which carries those warnings ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)), so a copy
|
|
68
|
+
`provide()` makes of it — a run's — keeps them, and the Open Machine keeps its own. The compiler's check does not
|
|
69
|
+
reach a bound argv: an Open argv's input is one string per key, so nothing at build or at `customize()` checks that
|
|
70
|
+
the invoke supplies each slot the bound argv names, and a slot it does not supply faults when the Effect starts — its
|
|
71
|
+
state's `onError`, with nothing run. The `jr2 up` walk reports an Open Effect beside Open Repo Slots and Agents and
|
|
72
|
+
refuses the converge naming the `effects` line — `{ effects: { <slot>: { secret: "<name>" } } }` for an Open secret,
|
|
73
|
+
one object with both keys for an Effect with two Open parts; the Effect refuses to run while Open, as the second fence
|
|
74
|
+
— its state's `onError`, with nothing sent and no ledger row. The Open part is the whole url, the whole argv, or the
|
|
75
|
+
secret's name.
|
|
76
|
+
- **The per-run Dial is the escape hatch, not the default.** A shipped Machine's door may carry `model` and
|
|
77
|
+
`thinkingLevel` (ADR-0018's two Dials) and fold them into each invoke's input; the bound definition is what `jr2 up`
|
|
78
|
+
preflights, the door value overrides it for one run's Turns. Nothing in jr2 changes for this — it is the Machine's own
|
|
79
|
+
four lines.
|
|
80
|
+
- **The first Machine is `task`: one prompt, one Workspace, one human says done.** Door
|
|
81
|
+
`{ prompt, branch?, model?, thinkingLevel? }`, branch defaulting to `jr2/task-<instance id>`, where `<instance id>` is
|
|
82
|
+
the seed Instance ID of the run (`instanceId` in `jr2 status`), not the run id; the Repo Slots Open as a map
|
|
83
|
+
(`repos: open`, ADR-0051) — the consumer names them, and the order is `task`'s own convention, since jr2 gives no slot
|
|
84
|
+
a meaning: the first is where the coder works, any others are checkouts the first Turn frames for the coder to read;
|
|
85
|
+
one Agent `coder` with an Open model. `working` invokes `coder`; its `finish { summary }` parks at the `review` Gate
|
|
86
|
+
(meta: summary, branch, worktree), as does a terminal `agent.fault` (meta: reason). `approve` reaches the final state
|
|
87
|
+
and tears the Workspace down; `request_changes { notes }` **continues the same conversation** (`continue: true`,
|
|
88
|
+
ADR-0057) — one human steering one Agent wants the Agent to remember what it did, unlike ADR-0049's coder⇄reviewer
|
|
89
|
+
handoff, which is lossy on purpose. No round cap: the human is the cap. Rejected: a per-run Repo on the door — a
|
|
90
|
+
packaged door cannot enumerate the Instance's repos, and "a Workflow is a name" reads best when the name means "a
|
|
91
|
+
prompt against THIS repo". Rejected: a named `target: open` slot — the body reads the first checkout the handles
|
|
92
|
+
carry, never `repos.target`, so the name claimed a shape the body did not have and shut out the one thing a task
|
|
93
|
+
commonly wants beside its repository: another checkout to read.
|
|
94
|
+
- **The Machine does not push.** ADR-0005/0053: the Agent holds no credential and the push url is the caller's own
|
|
95
|
+
spelling with the caller's own credential. The Gate park is the inspection window — exec in, review, push, then
|
|
96
|
+
`approve`, and unpushed commits go with the pod, as ADR-0012 always said. A credentialed push out of the pod is a
|
|
97
|
+
decision of its own, not this one.
|
|
98
|
+
- **`examples/` retires; `templates/default` is the one Instance in the checkout.** The scaffold's model moves from
|
|
99
|
+
`examples/starter` to `templates/default` — the folder documents what an Instance looks like and is what `jr2 init`
|
|
100
|
+
renders, kept byte-equal to the CLI's inline templates by the existing drift test, since an installed CLI cannot read
|
|
101
|
+
a repo-root folder (ADR-0043). `templates/` is plural because a later `jr2 init --template <name>` is the obvious next
|
|
102
|
+
step. The scaffold stays `ping`-only — the `@dist` tier's contract is a fresh instance that runs before any provider
|
|
103
|
+
or repo exists — and `ping.ts`'s prose shows the `@jr2/machines` move as the next step instead of an inline `agent()`
|
|
104
|
+
sketch. `examples/coding` is deleted whole, `jr.ts` included;
|
|
105
|
+
[An Agent in a Sandbox](../../get-started/an-agent-in-a-sandbox.md) shows `task` in its place.
|
|
106
|
+
|
|
107
|
+
## Consequences
|
|
108
|
+
|
|
109
|
+
- **Every Machine in `@jr2/machines` ships with a `@kind` scenario.** These are for end users, and the only proof a
|
|
110
|
+
shipped Machine works in jr2 is the stock Harness driving it in a real Sandbox. `features/kind-instance` registers
|
|
111
|
+
each one the way a consumer would — `customize(task, { repos, agents })` over the tier's fixture repo and scripted
|
|
112
|
+
model — and the scenario walks its Gates. The socket-free mechanics test beside the Machine covers the default
|
|
113
|
+
`pnpm -r test` gate; it does not stand in for the scenario.
|
|
114
|
+
- `@jr2/machines` joins the `PUBLIC` list in `publish.test.ts` and the guard loop in `scripts/dist-publish.sh`; the
|
|
115
|
+
`@dist` tier publishes it to verdaccio, so its pack and `files:` list execute there even though no `@dist` scenario
|
|
116
|
+
runs it.
|
|
117
|
+
- GLOSSARY.md gains **Open** as a term, and the Workflow entry says the kit ships Machines. Eleven earlier ADRs cite
|
|
118
|
+
`examples/`; the citations are retargeted in place.
|
|
119
|
+
- A `customize` that binds a model is now the common consumer line, not the exotic one; `jr2 up`'s refusal for an Open
|
|
120
|
+
Agent prints it, as it does for an Open Repo Slot and an Open Effect (its `effects` line).
|
|
121
|
+
- No Machine in `@jr2/machines` leaves an Effect Open yet; `task` carries no Effect.
|
|
122
|
+
- Two copies of one Machine spend two teams' secrets: each composer binds the Open secret, or a url that carries one in
|
|
123
|
+
its path, with its own. A labelled form for two Open secrets in one Effect can be added later without changing the
|
|
124
|
+
unlabelled one.
|
|
125
|
+
- The `triaged-task` mechanics test went with `examples/coding`, and the cross-Machine conversation pin it exercised
|
|
126
|
+
went with ADR-0057; the `@kind` `consulted`/`advised` fixtures hold Instance Harness placement.
|