@jr2/orchestrator 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/console/components/app.ts +2 -0
- package/console/components/fleet.ts +14 -1
- package/console/components/machine-pane.ts +41 -2
- package/console/main.ts +13 -1
- package/console/store.ts +102 -6
- package/console/style.css +21 -0
- package/docs/agents-block.md +5 -0
- package/docs/concepts/agents-and-turns.md +478 -0
- package/docs/concepts/architecture.md +243 -0
- package/docs/concepts/charts.md +146 -0
- package/docs/concepts/composition.md +357 -0
- package/docs/concepts/converge.md +257 -0
- package/docs/concepts/durable-runs.md +189 -0
- package/docs/concepts/effects.md +658 -0
- package/docs/concepts/gates.md +208 -0
- package/docs/concepts/machines-and-workflows.md +298 -0
- package/docs/concepts/pools-and-sources.md +230 -0
- package/docs/concepts/secrets-and-trust.md +306 -0
- package/docs/concepts/the-menu.md +381 -0
- package/docs/concepts/workspaces.md +756 -0
- package/docs/get-started/a-model-decides.md +563 -0
- package/docs/get-started/an-agent-in-a-sandbox.md +383 -0
- package/docs/get-started/connect-a-model.md +248 -0
- package/docs/get-started/create-an-instance.md +194 -0
- package/docs/get-started/have-your-assistant-write-a-machine.md +993 -0
- package/docs/get-started/install.md +130 -0
- package/docs/guides/add-tools-to-the-sandbox.md +308 -0
- package/docs/guides/answer-a-gate.md +304 -0
- package/docs/guides/bind-open-parts.md +224 -0
- package/docs/guides/cancel-a-run.md +146 -0
- package/docs/guides/clean-up.md +206 -0
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +361 -0
- package/docs/guides/give-the-cluster-git-credentials.md +222 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
- package/docs/guides/let-an-agent-write-a-chart.md +230 -0
- package/docs/guides/look-inside-a-sandbox.md +177 -0
- package/docs/guides/mirror-the-kit-images.md +176 -0
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +318 -0
- package/docs/guides/open-the-console.md +91 -0
- package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
- package/docs/guides/push-and-sign-a-branch.md +532 -0
- package/docs/guides/run-a-chart-by-name.md +270 -0
- package/docs/guides/run-a-command-or-call-a-url.md +587 -0
- package/docs/guides/run-one-worker-per-item.md +322 -0
- package/docs/guides/set-an-agents-access-and-dials.md +420 -0
- package/docs/guides/size-a-workspace.md +217 -0
- package/docs/guides/start-a-run-from-a-run.md +237 -0
- package/docs/guides/start-a-run.md +207 -0
- package/docs/guides/upgrade-the-kit-version.md +240 -0
- package/docs/guides/use-a-hosted-provider.md +214 -0
- package/docs/guides/use-a-local-model.md +313 -0
- package/docs/guides/watch-a-run.md +349 -0
- package/docs/guides/write-a-workflow.md +318 -0
- package/docs/index.md +44 -0
- package/docs/llms.txt +237 -0
- package/docs/reference/authoring-surface/agent.md +151 -0
- package/docs/reference/authoring-surface/answer.md +156 -0
- package/docs/reference/authoring-surface/arg.md +82 -0
- package/docs/reference/authoring-surface/basic.md +62 -0
- package/docs/reference/authoring-surface/bearer.md +53 -0
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/chartschema.md +23 -0
- package/docs/reference/authoring-surface/chartschemafor.md +48 -0
- package/docs/reference/authoring-surface/command.md +206 -0
- package/docs/reference/authoring-surface/customize.md +64 -0
- package/docs/reference/authoring-surface/defineconfig.md +41 -0
- package/docs/reference/authoring-surface/defineevent.md +114 -0
- package/docs/reference/authoring-surface/dispatch.md +126 -0
- package/docs/reference/authoring-surface/fail.md +96 -0
- package/docs/reference/authoring-surface/file.md +142 -0
- package/docs/reference/authoring-surface/frompick.md +277 -0
- package/docs/reference/authoring-surface/http.md +259 -0
- package/docs/reference/authoring-surface/isfailure.md +58 -0
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +237 -0
- package/docs/reference/authoring-surface/open.md +105 -0
- package/docs/reference/authoring-surface/openbody.md +86 -0
- package/docs/reference/authoring-surface/paramsof.md +143 -0
- package/docs/reference/authoring-surface/pool.md +186 -0
- package/docs/reference/authoring-surface/push.md +130 -0
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/secret.md +139 -0
- package/docs/reference/authoring-surface/sentby.md +83 -0
- package/docs/reference/authoring-surface/source.md +51 -0
- package/docs/reference/authoring-surface/text.md +62 -0
- package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
- package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
- package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
- package/docs/reference/authoring-surface/type-agentslots.md +15 -0
- package/docs/reference/authoring-surface/type-answeraction.md +12 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-arg.md +19 -0
- package/docs/reference/authoring-surface/type-argnames.md +12 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +61 -0
- package/docs/reference/authoring-surface/type-bindingof.md +12 -0
- package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
- package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
- package/docs/reference/authoring-surface/type-ceilings.md +41 -0
- package/docs/reference/authoring-surface/type-chainlink.md +19 -0
- package/docs/reference/authoring-surface/type-chartaction.md +46 -0
- package/docs/reference/authoring-surface/type-chartagent.md +28 -0
- package/docs/reference/authoring-surface/type-chartevent.md +20 -0
- package/docs/reference/authoring-surface/type-chartguard.md +20 -0
- package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
- package/docs/reference/authoring-surface/type-chartjson.md +14 -0
- package/docs/reference/authoring-surface/type-chartstate.md +46 -0
- package/docs/reference/authoring-surface/type-charttext.md +40 -0
- package/docs/reference/authoring-surface/type-charttransition.md +22 -0
- package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
- package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
- package/docs/reference/authoring-surface/type-childat.md +12 -0
- package/docs/reference/authoring-surface/type-childslots.md +12 -0
- package/docs/reference/authoring-surface/type-commandenv.md +15 -0
- package/docs/reference/authoring-surface/type-commandinput.md +20 -0
- package/docs/reference/authoring-surface/type-commandoptions.md +56 -0
- package/docs/reference/authoring-surface/type-commandoutput.md +122 -0
- package/docs/reference/authoring-surface/type-customize.md +111 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
- package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchoutput.md +72 -0
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-duration.md +15 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
- package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
- package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
- package/docs/reference/authoring-surface/type-eventdef.md +45 -0
- package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
- package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
- package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
- package/docs/reference/authoring-surface/type-failslot.md +14 -0
- package/docs/reference/authoring-surface/type-failure.md +19 -0
- package/docs/reference/authoring-surface/type-failureinput.md +20 -0
- package/docs/reference/authoring-surface/type-filebody.md +37 -0
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-frominput.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +56 -0
- package/docs/reference/authoring-surface/type-httpdefinition.md +104 -0
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpinput.md +12 -0
- package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
- package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +95 -0
- package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
- package/docs/reference/authoring-surface/type-jr2config.md +126 -0
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +38 -0
- package/docs/reference/authoring-surface/type-logicat.md +12 -0
- package/docs/reference/authoring-surface/type-markedif.md +12 -0
- package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
- package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
- package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
- package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +22 -0
- package/docs/reference/authoring-surface/type-openeffect.md +12 -0
- package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
- package/docs/reference/authoring-surface/type-openof.md +12 -0
- package/docs/reference/authoring-surface/type-opensecret.md +23 -0
- package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +26 -0
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +60 -0
- package/docs/reference/authoring-surface/type-pushinput.md +34 -0
- package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
- package/docs/reference/authoring-surface/type-pushoutput.md +149 -0
- package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
- package/docs/reference/authoring-surface/type-quantity.md +14 -0
- package/docs/reference/authoring-surface/type-reached.md +12 -0
- package/docs/reference/authoring-surface/type-reposlot.md +70 -0
- package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +84 -0
- package/docs/reference/authoring-surface/type-secret.md +25 -0
- package/docs/reference/authoring-surface/type-sender.md +19 -0
- package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
- package/docs/reference/authoring-surface/type-size.md +85 -0
- package/docs/reference/authoring-surface/type-slothandles.md +24 -0
- package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
- package/docs/reference/authoring-surface/type-syncstate.md +12 -0
- package/docs/reference/authoring-surface/type-text.md +19 -0
- package/docs/reference/authoring-surface/type-textpart.md +24 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
- package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
- package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
- package/docs/reference/authoring-surface/type-unknown.md +21 -0
- package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
- package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
- package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
- package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
- package/docs/reference/authoring-surface/type-workspaced.md +115 -0
- package/docs/reference/authoring-surface/type-workspacehandles.md +22 -0
- package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
- package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
- package/docs/reference/authoring-surface/type-workspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +287 -0
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +217 -0
- package/docs/reference/cli.md +433 -0
- package/docs/reference/config.md +765 -0
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +114 -0
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +117 -0
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +130 -0
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +240 -0
- package/docs/reference/decisions/0006-agent-control-surface.md +50 -0
- package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
- package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
- package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +175 -0
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +271 -0
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +126 -0
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +71 -0
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +287 -0
- package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +101 -0
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +175 -0
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +436 -0
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
- package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +245 -0
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +90 -0
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +193 -0
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +126 -0
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
- package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
- package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
- package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
- package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
- package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
- package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
- package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
- package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
- package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
- package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
- package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
- package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +238 -0
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +86 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +123 -0
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
- package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +108 -0
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +427 -0
- package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
- package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +239 -0
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +82 -0
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +552 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +458 -0
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +475 -0
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +334 -0
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
- package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions.md +86 -0
- package/docs/reference/glossary.md +687 -0
- package/docs/reference/http-api.md +1386 -0
- package/docs/reference/machines.md +189 -0
- package/docs/reference/mechanism-events.md +72 -0
- package/docs/why.md +207 -0
- package/package.json +5 -3
- package/src/actor.ts +694 -106
- package/src/agent.ts +114 -38
- package/src/ambient.ts +3 -4
- package/src/assembly-checks.ts +189 -7
- package/src/cause.ts +234 -0
- package/src/chart.ts +345 -46
- package/src/charts.ts +43 -8
- package/src/config.ts +440 -178
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +325 -99
- package/src/customize.ts +199 -49
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect-checks.ts +50 -3
- package/src/effect.ts +641 -128
- package/src/effector-wire.ts +165 -13
- package/src/fail.ts +222 -0
- package/src/harness-client.ts +587 -50
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +125 -16
- package/src/holds.ts +3 -0
- package/src/http-effect.ts +556 -58
- package/src/http.ts +480 -30
- package/src/index.ts +74 -82
- package/src/instance.ts +68 -8
- package/src/internal.ts +85 -0
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +24 -7
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +42 -58
- package/src/names.ts +19 -6
- package/src/notices.ts +483 -41
- package/src/open-body.ts +242 -40
- package/src/open-effect.ts +81 -8
- package/src/open.ts +90 -6
- package/src/parts.ts +213 -30
- package/src/party.ts +101 -2
- package/src/pick.ts +497 -11
- package/src/pool.ts +282 -50
- package/src/registration.ts +133 -12
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +22 -3
- package/src/run-host.ts +665 -45
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +457 -71
- package/src/sandbox-kube.ts +66 -17
- package/src/secret-placement.ts +59 -17
- package/src/server.ts +3 -0
- package/src/setup.ts +325 -62
- package/src/size.ts +121 -9
- package/src/snapshot-store.ts +54 -10
- package/src/stub-harness.ts +44 -16
- package/src/timers.ts +14 -1
- package/src/tokens.ts +107 -32
- package/src/vocabulary.ts +88 -14
- package/src/wire.ts +100 -26
- package/src/workspace.ts +771 -238
- package/src/worktree.ts +249 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# BDD acceptance tests via Cucumber.js (black-box, scenario-isolated)
|
|
2
|
+
|
|
3
|
+
jr2's per-package tests are `node --test` suites that run in-process and socket-free (e.g. the CLI's `cli.test.ts` calls
|
|
4
|
+
`main(argv, io)` with `fetch` bound to `app.request`). Those are fast and cover unit and integration behavior, but
|
|
5
|
+
nothing exercises the _system_ the way a user does — the real `jr2` binary, a real HTTP socket, exit codes, signals.
|
|
6
|
+
This ADR adds a second, complementary tier: **black-box end-to-end acceptance tests, written in Gherkin and run by
|
|
7
|
+
Cucumber.js**, living in a top-level `./features/` workspace package (`@jr2/e2e`).
|
|
8
|
+
|
|
9
|
+
## What we decided
|
|
10
|
+
|
|
11
|
+
- **Two tiers, not one.** In-process `node --test` stays the unit/integration tier (fast, no sockets, per package).
|
|
12
|
+
Cucumber.js is the **e2e** tier: each step spawns the real `jr2` binary as a child process against a real orchestrator
|
|
13
|
+
server on an ephemeral port, and asserts only on what a shell sees — captured stdout (the one machine-readable
|
|
14
|
+
result), stderr (human activity), and the exit code. The e2e tier does not re-test what the in-process tier already
|
|
15
|
+
covers.
|
|
16
|
+
- **The per-scenario orchestrator is the image entrypoint, not a CLI verb.** There is no host dev mode (ADR-0019), but
|
|
17
|
+
the Orchestrator's server entrypoint is an ordinary Node process — the test fixture boots exactly that process on the
|
|
18
|
+
host, with `--url`/token env pointing the spawned `jr2` binary at it. Same real server the cluster runs, no cluster,
|
|
19
|
+
no user-facing verb. The wire-compatible **stub Harness** (ADR-0011) is a fixture owned by this tier and served the
|
|
20
|
+
same way.
|
|
21
|
+
- **`Feature` → `Rule` → `Scenario` maps to criteria.** A `Rule` is one acceptance criterion; a `Scenario` is an example
|
|
22
|
+
of it. This is organizational only — `Rule`s carry no shared state.
|
|
23
|
+
- **`Scenario` is the isolation unit — and the environment is part of it.** Each scenario gets its own scaffolded temp
|
|
24
|
+
instance (via the real `jr2 init`), its own server process + sqlite store, torn down in an `After` hook. It also gets
|
|
25
|
+
its own **environment**: the World spawns nothing from the ambient one, because a tier that inherits the developer's
|
|
26
|
+
shell has made that shell a silent participant in every scenario. Two classes are sealed. Every `JR2_*` variable is
|
|
27
|
+
scrubbed, since they say where to talk and as whom — an exported `JR2_URL` would point a whole suite at someone's
|
|
28
|
+
deployed instance, and `JR2_NAMESPACE` reaches the entrypoint's data-plane switch
|
|
29
|
+
(`composesSandbox && namespace !== undefined`), which would turn `workspace.feature`'s "no cluster, so it faults
|
|
30
|
+
pointedly" into a real Sandbox provisioned on whatever cluster was configured. Each tier adds back exactly what it
|
|
31
|
+
means to set, and the variables a scenario's own config reads arrive through the instance's `.env`, a file the step
|
|
32
|
+
wrote. `KUBECONFIG` is pinned to `/dev/null` wherever there is no cluster in the story, so a run-verb falling through
|
|
33
|
+
to the kube path finds nothing rather than the last cluster a human used; `@kind` and `@dist` own a namespace on a
|
|
34
|
+
real cluster and keep the real one. (The leak this closes was not theoretical: `cli-contract.feature`'s exit-code
|
|
35
|
+
scenario hung for its full 120s timeout against an unreachable home cluster, where it should fail in milliseconds —
|
|
36
|
+
and it hung only on the machine that had one.) Nothing is shared across scenarios, so the suite is safe under
|
|
37
|
+
`cucumber-js --parallel`.
|
|
38
|
+
- **An opt-in `@kind` tier for the data plane** (added once the `workspace()` slice landed — ADR-0012). Workspaces are
|
|
39
|
+
always real Sandboxes, so the only way to test them is against a real cluster: the `@kind` scenarios drive the
|
|
40
|
+
operator's Sandbox CR, a pod, the node's read-only Repo cache, an in-pod git worktree, and the Harness endpoint —
|
|
41
|
+
faking only the LLM. (Originally the Sandbox ran a **dev Harness image**, a wire-compatible stub in a container with
|
|
42
|
+
`git`; [ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md) retired it. The pod runs the STOCK Harness now — real
|
|
43
|
+
pi, the real Menu through the Custodian, real Working tools — and the substitution moved to the provider: a scripted
|
|
44
|
+
OpenAI-compatible endpoint the World serves from the host. Which is what makes this tier a second pi canary.) They are
|
|
45
|
+
tagged `@kind` and **excluded from the default profile**, so the everyday suite needs no docker. Bring-up is the
|
|
46
|
+
product's own path (ADR-0019): one shared VANILLA kind cluster and nothing else, then **`jr2 up` per scenario into a
|
|
47
|
+
fresh namespace** — which since ADR-0038 builds and loads every image it deploys, so no image is pre-loaded by hand.
|
|
48
|
+
Namespace-as-identity makes the scenario the isolation unit here too, so nothing is instance-bound to the cluster.
|
|
49
|
+
**The host address the pods dial back on is discovered, not assumed.** The scripted provider runs host-side, so the
|
|
50
|
+
tier needs the one address that routes from a pod back to this process — and it is not the same on every machine. On
|
|
51
|
+
Linux the daemon IS the host, so the kind bridge's gateway is the answer. Under a VM-backed daemon (colima, Docker
|
|
52
|
+
Desktop) that gateway is the VM: measured on colima, `172.18.0.1` reaches nothing while `host.docker.internal`,
|
|
53
|
+
`host.lima.internal` and `192.168.5.2` all answer — and on Linux those names mean nothing. So the candidates are
|
|
54
|
+
ordered by platform and the winner is VERIFIED, by `curl`ing the provider that is already listening from inside a kind
|
|
55
|
+
node (a pod's egress routes through it), for the price of one `docker exec` memoized per worker process. Taking the
|
|
56
|
+
bridge network's first IPAM entry is what broke: docker lists the IPv6 subnet first on some hosts and it carries no
|
|
57
|
+
gateway at all, so the tier now reads every entry that HAS one, IPv4 first. Failing to find any address is one error
|
|
58
|
+
naming every candidate tried, not an assertion in each scenario's `Before` hook. Measured: **22/22 on macOS/colima,
|
|
59
|
+
~3m20 warm at degree 4** against the ~1m35 Linux figure below — the VM is slower, not different. **The tier runs
|
|
60
|
+
parallel, and three properties are what make that safe.** Scenario isolation (own namespace, own scripted model port);
|
|
61
|
+
no shared mutable image name between concurrent converges of one checkout — a Sandbox Image builds straight to its
|
|
62
|
+
content tag, with no intermediate, because the Harness arrives by volume at pod time
|
|
63
|
+
([ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md)) — while
|
|
64
|
+
[ADR-0041](0041-a-build-the-host-already-holds-is-not-spent-again.md) makes a warm scenario's converge build nothing
|
|
65
|
+
(~13s fresh-namespace converge, every build disk-skipped); and connection retries at the two seats that dial a Sandbox
|
|
66
|
+
before anything else ever has — [ADR-0042](0042-ready-is-not-routable-an-admission-retries-its-connection.md) has the
|
|
67
|
+
diagnosis (a Sandbox CR at `phase: Ready` is not yet ROUTABLE, so an unretried first dial is a lost turn; the Menu
|
|
68
|
+
read was a second instance of the same defect). Parallelism does not create those windows, it only samples them more
|
|
69
|
+
often — which is why any tier flake that vanishes at `--parallel 1` should be read as a routability-class defect
|
|
70
|
+
first, not as an isolation bug. Measured: **degree 4 → 8 consecutive runs, 88/88 scenarios, 1m30–1m43s each** on a
|
|
71
|
+
sealed tree (serial: ~5m). **The wired default is now `--parallel 4`** (in the `kind` profile, so a direct
|
|
72
|
+
`npx cucumber-js --profile kind` gets it too); pass `--parallel 1` to bisect a suspected isolation bug. Three tier
|
|
73
|
+
changes came out of the hunt and stay. A FAILED `@kind` scenario dumps its evidence (the provider's request count,
|
|
74
|
+
`jr2 status`, the Harness's `?view=history`, every pod log with timestamps, events, EndpointSlices) to
|
|
75
|
+
`features/.tmp/kind-failures/` before its namespace is deleted. The tier's own workflow ROUTES `agent.fault` instead
|
|
76
|
+
of ignoring it — an ignored terminal fault is an invisible one, and a tier that hangs where it could name the reason
|
|
77
|
+
is a tier that costs a session per defect. And **every** scenario, passing or not, is now scraped for what ADR-0042's
|
|
78
|
+
retries COST (`jr2.routability seat=… attempts=… ms=… last=…`), against a budget of 30s or 8 attempts judged per
|
|
79
|
+
worker in `AfterAll` — two meters because a REJECT spends attempts and a dropped SYN spends time, and the tier
|
|
80
|
+
measured one of each in its first two runs (2 attempts / 10667ms, then 3 attempts / 502ms / `ECONNREFUSED`). That last
|
|
81
|
+
one is the tier watching the fix rather than merely enjoying it: the retry is absorbed by design, so without it a
|
|
82
|
+
cluster drifting toward the 90s window would keep passing — slightly slower, silently — until the day it did not.
|
|
83
|
+
- **An opt-in `@console` tier for the Console's UX** (added with
|
|
84
|
+
[ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)). The Console's risky behavior is interaction —
|
|
85
|
+
token-mode switching, the frame-triggered gate inbox, selection vs. folding — which no reducer test sees and no CLI
|
|
86
|
+
step drives. These scenarios hold a **Playwright page inside ordinary Cucumber steps** (Playwright the _library_, not
|
|
87
|
+
the `@playwright/test` runner): the browser is a second driver of the same black box, reusing the per-scenario
|
|
88
|
+
orchestrator fixture unchanged, and a scenario may drive both (the CLI starts a run; the browser sees it park). Tagged
|
|
89
|
+
`@console` and **excluded from the default profile** exactly as `@kind` is — the everyday suite must not require a
|
|
90
|
+
Chromium install — but they need no docker: a host-booted orchestrator serves the Console fine. Scope discipline: they
|
|
91
|
+
assert what a user does and sees, never pixels or layout; what the page _believes_ stays in `console-store.test.ts`.
|
|
92
|
+
- **Harness conformance, in `@jr2/harness`, in the default gate**
|
|
93
|
+
([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)). The three tiers above all reach the
|
|
94
|
+
Harness through the **stub Harness**, which is a hand-written model of the _wire_ — it holds no conversation, so it
|
|
95
|
+
can report a turn settled `ended` or `aborted` without ever having had a turn to settle. That is the right fixture for
|
|
96
|
+
testing jr2, and it is blind by construction to what the real runtime keeps, drops, or sends onward. Those claims are
|
|
97
|
+
covered by the conformance suite in `packages/harness/test/`, driven through the real turn loop — pi at the exact pin,
|
|
98
|
+
the Menu over a real socket to the Custodian's address, a **scripted provider** — so a turn's shape (where it stalls,
|
|
99
|
+
what it half-emits) is chosen by the test rather than by a model. The witness is the message array the provider
|
|
100
|
+
receives on the _next_ turn, which is the only place these claims are visible. Not a separate tier: it runs as part of
|
|
101
|
+
`pnpm -r test` (no docker, no cluster), and it is the canary for pi bumps — **run it before bumping the pin**. (A
|
|
102
|
+
predecessor tier that ran the retired foreign harness runtime at its pin dissolved into this suite — ADR-0027.)
|
|
103
|
+
- **The Custodian suite, opt-in, against the real engine**
|
|
104
|
+
([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The Custodian's claims — the swap on
|
|
105
|
+
the wire, the strip, the refusals, the dial guard, TLS verification upstream, SSE timing, abort, and a real SDK
|
|
106
|
+
through `HTTPS_PROXY` — are Envoy's behaviour under jr2's configuration, so only the pinned Envoy image can prove
|
|
107
|
+
them. `just custodian-test` runs `node --test` (`packages/cli/test/custodian.docker.ts`) against it under docker, on
|
|
108
|
+
the host network with free ports, with local HTTPS upstreams under their own CA; about 23 seconds. Opt-in for the
|
|
109
|
+
`@kind` reason — it needs docker — and NOT part of `pnpm -r test`. What the default gate keeps is the rendering:
|
|
110
|
+
`custodian.test.ts` pins the bootstrap and script a pod's maker writes, socket-free. The `@kind` tier then proves the
|
|
111
|
+
Custodian in a real pod, in both placements, against a model provider that refuses any request without the key.
|
|
112
|
+
- **An opt-in `@model` tier, against a real model**
|
|
113
|
+
([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)). Every tier above
|
|
114
|
+
fakes the model, so none can see a change in what a model DOES with what jr2 shows it — and ADR-0029/0062 measured
|
|
115
|
+
that such changes are large. This tier is `@kind` with the last fake removed: the same cluster and bring-up, `jr2 up`
|
|
116
|
+
per scenario into a fresh namespace, and behind the Custodian a real endpoint named by a profile
|
|
117
|
+
(`features/model-endpoints/<name>.json`, selected by `JR2_MODEL_ENDPOINT`). Its scenarios are CLAIMS — an ADR's
|
|
118
|
+
measured claim as N trials with a floor, scored into `features/.tmp/model-scorecard.json` against a committed baseline
|
|
119
|
+
— and THE RUN, one workflow from a failing test to a commit verified in the pod. It runs the endpoint profile's
|
|
120
|
+
`workers` scenarios at once, and `workers` × the profile's `parallel` runs must stay within what the endpoint serves
|
|
121
|
+
and what the host holds. Not in CI and not in `pnpm -r test`: `just e2e-model` probes the endpoint and says `skipped:`
|
|
122
|
+
when nothing answers, and whoever changes what the model reads runs it before calling the change done. A third pi
|
|
123
|
+
canary.
|
|
124
|
+
- **Shared cluster state is the one thing a scenario cannot own, and the pool is PROCESSES.** `--parallel` runs cucumber
|
|
125
|
+
workers as separate processes, so a fixture memoized "once per process" runs once per WORKER — four times,
|
|
126
|
+
concurrently, against one cluster. The tier has exactly one such fixture: the git seed (`steps/seed.ts`) it serves
|
|
127
|
+
in-cluster, which is cluster-scoped where everything else is namespaced per scenario. `kubectl apply` creates with
|
|
128
|
+
GET-then-POST, so the workers that lose that race are told `AlreadyExists` — which an `ensure` treats as success,
|
|
129
|
+
because the object being there is the whole ask. The rollout wait that follows is the real rendezvous, and it is
|
|
130
|
+
idempotent for every worker. (The same property is why the `@dist` profile is serial: its publish would otherwise
|
|
131
|
+
stand up one verdaccio per worker on a single port.)
|
|
132
|
+
- **A workspace package, not a bare folder.** `./features/` is `@jr2/e2e` so it owns its own `xstate` (+ cucumber)
|
|
133
|
+
dependency: a scaffolded instance's `workflows/*.ts` `import "xstate"`, resolved by walking up from the temp dir, and
|
|
134
|
+
the repo root has no `xstate`. The package's `node_modules` satisfies it. It is top-level (not under `packages/cli/`)
|
|
135
|
+
because an e2e test exercises most packages at once — the CLI is the common entry point, not the only conceivable one.
|
|
136
|
+
- **Zero-build, like the rest of the repo.** Cucumber.js v13 runs `.ts` ESM step definitions directly under Node 24's
|
|
137
|
+
native type-stripping — no `ts-node`, no transpile step. (Verified before adopting it.)
|
|
138
|
+
|
|
139
|
+
## Considered options
|
|
140
|
+
|
|
141
|
+
- **Express BDD as a convention in `node --test`** (a Given/When/Then helper, no `.feature` files): keeps zero deps, but
|
|
142
|
+
loses the plain-English acceptance specs that are the point of writing them at the user boundary. Rejected — we want
|
|
143
|
+
the `.feature` files as readable, reviewable criteria.
|
|
144
|
+
- **In-process e2e** (drive `main(argv, io)` instead of a subprocess): faster, but it is integration, not e2e — it can't
|
|
145
|
+
catch bin/shebang, exit-code, signal, or attach bugs. That's already the job of the in-process tier; the e2e tier
|
|
146
|
+
exists precisely to cover the subprocess boundary.
|
|
147
|
+
- **Requiring a cluster for all e2e** (every scenario through `jr2 up`): honest but slow, and it gates the everyday
|
|
148
|
+
suite on docker. Rejected — the control-plane surface is fully exercisable against the bare server process; only the
|
|
149
|
+
data plane needs kind, and it has its own tier.
|
|
150
|
+
|
|
151
|
+
## Consequences
|
|
152
|
+
|
|
153
|
+
- `@cucumber/cucumber` is the **first test-framework dependency** in a repo that otherwise uses only `node --test` — a
|
|
154
|
+
deliberate deviation, scoped to `@jr2/e2e` (it does not leak into the kit packages). `playwright` joins it there, same
|
|
155
|
+
scoping, browser binaries installed only where `@console` runs (CI, or `npx playwright install chromium` locally).
|
|
156
|
+
- The suite is run with `pnpm --filter @jr2/e2e test:e2e`; `@jr2/e2e` exposes **no** `test` script, so `pnpm -r test`
|
|
157
|
+
stays fast and unit-only.
|
|
158
|
+
- Human-in-the-loop delivery is covered at the gates API (`features/agent-and-gates.feature` — the agent played over
|
|
159
|
+
`/agents/<iid>/*`, the human over `/runs/:id/gates/:gate/events`); the `jr2 send --gate` CLI verb is covered at the
|
|
160
|
+
in-process tier. A CLI-driven e2e gate scenario remains a candidate follow-up `Rule`.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Control events are workflow-defined and delivered by closure, not routed
|
|
2
|
+
|
|
3
|
+
Two decisions, out of the jr-parity design exercise: **jr2 ships the event mechanism and zero events**, and **an event
|
|
4
|
+
binds to a Machine state through the actor that exposes it** — delivery is a closure created at invoke time, so no
|
|
5
|
+
routing layer exists anywhere.
|
|
6
|
+
|
|
7
|
+
## `defineEvent`: mechanism, not policy
|
|
8
|
+
|
|
9
|
+
`defineEvent({ name, input, audience?, semantics? })` produces a pure-data definition: a name, a zod input schema (flat
|
|
10
|
+
tagged objects, never `oneOf` — the ADR-0006 encoding rule), an optional audience tag (`agent` | `external` | `any`;
|
|
11
|
+
unset, each writing of the event belongs to the party its state, or the nearest state above it, invokes — ADR-0015), and
|
|
12
|
+
an optional semantics tag (`ack`, or the reserved `poll`; a def declares no output — ADR-0006). It is a **pure factory —
|
|
13
|
+
no import-time side effects, no global registry**. Because actor **inputs are serializable** (ADR-0007) they carry event
|
|
14
|
+
_names_, so resolution needs a name→def scope; that scope is **per-Machine**: `jr2Setup` takes the defs as values and
|
|
15
|
+
attaches the vocabulary to the machine (ADR-0015 — discovery reads it there; the module contract is
|
|
16
|
+
`export const machine` alone), and `gate`/`agentRun` resolve names against **the Machine that invoked them** —
|
|
17
|
+
`self._parent.logic`, public xstate API — never a run-wide set. Names are local to their Machine: `coding`'s `approve`
|
|
18
|
+
and `release`'s `approve` may differ, and one run may hold both, because a Machine nested inside another resolves
|
|
19
|
+
against its own defs and no map is ever merged. That is what makes a Machine composable by plain `invoke` (ADR-0049):
|
|
20
|
+
the importing Machine neither re-declares nor sees the nested one's events, and `workspace()`/`pool()` propagate
|
|
21
|
+
nothing. An unlisted name fails at invoke time with an error naming the Machine and its declared set. The attachment is
|
|
22
|
+
keyed on the machine's `config`, which xstate's `.provide()` passes through unchanged, so the host's per-run provide and
|
|
23
|
+
a test's `.provide()` both keep it. We rejected a global name registry (import = registration): it couples every
|
|
24
|
+
workflow in an instance — and any future npm-distributed one — through one namespace, and attribution can't be inferred
|
|
25
|
+
at import time. We also retired the run-scoped set this ADR first specified (`RunBinding.events`, the root's
|
|
26
|
+
vocabulary): it made every nested Machine's events the root's problem to re-declare, and turned a
|
|
27
|
+
same-name-different-payload pair into a collision that the per-Machine defs had already avoided.
|
|
28
|
+
|
|
29
|
+
`defineEvent` also carries the TypeScript side: `jr2Setup` derives the machine's event union from the defs — one source
|
|
30
|
+
of truth, no schema/type drift, and the machine stays fully typed for xstate tooling.
|
|
31
|
+
|
|
32
|
+
A def's `input` is what its sender supplies, and nothing the Machine adds. Where a pick starts work,
|
|
33
|
+
`fromPick(def, fill)` carries those params into the invoked Effect's input by name, and `fill` adds the Machine's part;
|
|
34
|
+
the load walk refuses a param nothing reads and a `fill` that overwrites one (ADR-0067, ADR-0015).
|
|
35
|
+
|
|
36
|
+
There is no jr2-blessed event vocabulary. `request_review`, `review_verdict`, `approve` are the _coding workflow's_
|
|
37
|
+
words; another workflow defines others. jr2 owns only definition, transport, validation, and delivery.
|
|
38
|
+
|
|
39
|
+
## Agent side: invoke-scoped registration, delivery via `sendBack`
|
|
40
|
+
|
|
41
|
+
On start, the `agentRun` actor registers, for its instance id, a toolset — the menu derived from its invoking state's
|
|
42
|
+
transitions (ADR-0015) — whose handlers **close over that invocation's `sendBack`**; on stop it deregisters. A tool call
|
|
43
|
+
→ validate against the named schema → `sendBack` a typed event, plus its sender → the event lands on **the state that
|
|
44
|
+
invoked the agent** (bubbling to ancestor states per statechart semantics — a machine can handle a
|
|
45
|
+
`report_blocked`-style event once at its root). The binding _is_ the closure.
|
|
46
|
+
|
|
47
|
+
- **No routing.** There is no instance→actor table and no workflow-side forwarding boilerplate; delivery works at any
|
|
48
|
+
nesting depth (workspace wrappers, pool children) with zero authored code.
|
|
49
|
+
- **State-scoped menus for free.** A transition swaps actors, which swaps registrations; the surface an Agent sees is
|
|
50
|
+
exactly the live registration.
|
|
51
|
+
- **One catch point.** A call for an unregistered iid (settled run, exited state) is rejected at the demux.
|
|
52
|
+
|
|
53
|
+
**Transport: the Agent's registration is read from the Sandbox** (ADR-0013). The Orchestrator speaks no MCP: it serves
|
|
54
|
+
`GET /agents/:iid/surface` + `POST /agents/:iid/events` under the Sandbox token, and the pod's Harness reads them
|
|
55
|
+
through the Custodian on `localhost`, which holds the token, and presents the surface to its model as the Menu (the
|
|
56
|
+
Harness re-reads per Submission, so the iid in the path is all the addressing needed). Lookup, validation, delivery, and
|
|
57
|
+
lifecycle stay implemented once, in the registration table.
|
|
58
|
+
|
|
59
|
+
## External side: the same primitive over HTTP (`gate`), and each gate is a resource
|
|
60
|
+
|
|
61
|
+
`gate` is the symmetric actor for **every non-agent caller** — humans (`jr2 send`, a UI), webhook translators, CI, other
|
|
62
|
+
systems. "Human" is policy, not mechanism, so the actor is not named for one caller. Each invocation is an addressable
|
|
63
|
+
**gate**: input is `{ gate?, meta? }` — `gate` an optional authored id (the id is derived when absent — below), `meta`
|
|
64
|
+
serializable caller/integration context (PR URL, title); its accepted set derives from the gated state's transitions,
|
|
65
|
+
its ancestors' and its plain children's (ADR-0015). Registration is per-gate: `GET /runs/:runId` lists open gates
|
|
66
|
+
(`{ gate, accepts (names + schemas), acceptsNow, meta }` — `acceptsNow` the names the run would take from this Gate now,
|
|
67
|
+
read the way an Agent's Allowed picks are, ADR-0029), `POST /runs/:runId/gates/:gate/events` validates the body against
|
|
68
|
+
the named schema and delivers via `sendBack` into the gated state — or refuses a send the run would not take (ADR-0013)
|
|
69
|
+
— and leaving the state destroys the gate. Gate ids are **run-scoped by mechanism** (the same `system`→run mapping that
|
|
70
|
+
scopes event resolution), so `gate: "F-12"` in two concurrent runs cannot collide.
|
|
71
|
+
|
|
72
|
+
Gate-as-resource is forced by concurrency: several children park simultaneously (three features in `humanReview` is the
|
|
73
|
+
_normal_ case), all accepting `approve` — a run-level POST is ambiguous. And every real external surface already thinks
|
|
74
|
+
in gates: a CLI listing shows pending decisions and acts on one; a UI inbox renders one card per gate (schemas drive the
|
|
75
|
+
forms, `meta` drives the copy); a forge webhook translator finds its gate by matching `meta` (e.g. `prUrl`), which is
|
|
76
|
+
why `meta` exists. Generalizing beyond humans also closes a hole hard-coded event types left: a push seam like "a
|
|
77
|
+
work-source webhook wakes discovery" is just an idle state holding a gate accepting `work_ready` (the `wake` seam of
|
|
78
|
+
ADR-0017). Two obvious later additions, both deferred: a cross-run inbox (`GET /gates`), and CLI segment-matching over
|
|
79
|
+
derived gate ids (`--gate F-12` resolving an unambiguous segment, git-style like run-id prefixes).
|
|
80
|
+
|
|
81
|
+
**The gate id is derived; authoring one is the exception.** Absent an authored `gate`, the id is the gate actor's own
|
|
82
|
+
path below the run root, and jr2Setup's menu-derivation walk names an unnamed gate invoke with its state key path so the
|
|
83
|
+
leaf segment is readable — `F-12.body.humanReview`, not xstate's `0.body.humanReview` default. This is unique wherever
|
|
84
|
+
concurrently live siblings have distinct actor ids — the invariant any correct fan-out already maintains, because xstate
|
|
85
|
+
keys children by id (a duplicate silently shadows the children-map entry and breaks `xstate.done.actor.<id>`
|
|
86
|
+
correlation). Authored ids exist for meaningful flat names (jr's feature id) and collision on them is a loud invoke-time
|
|
87
|
+
error. Derivation runs at actor start, not the input mapper — deterministic from structure, so recomputation on every
|
|
88
|
+
(re)start is restore-stable, and one mechanism serves jr2Setup and plain-`setup` machines alike (the walk contributes id
|
|
89
|
+
_quality_ only, never correctness). Rejected: a workflow-computed id as the default (`gate: context.feature.id`) — it
|
|
90
|
+
quietly violated the address doctrine (Consequences below), making the gate id the one caller-facing address a workflow
|
|
91
|
+
computed by hand, and a machine with a static id (`"humanReview"`) was correct standalone but a latent, timing-dependent
|
|
92
|
+
collision once composed under a pool, firing only when two children park concurrently. Also rejected: deriving only in
|
|
93
|
+
the walk (a second dialect and an error case for plain-setup callers), and xstate's default invoke ids as the name
|
|
94
|
+
(invoke-index noise no caller cares about).
|
|
95
|
+
|
|
96
|
+
The full symmetry: **agents deliver through their per-iid registration (via the Custodian); everything else delivers
|
|
97
|
+
over HTTP (per-gate registration).** Two dialects, one primitive, zero routing. Internally that is literal structure:
|
|
98
|
+
one registration table (`address → { accepted event defs, deliver closure, meta, sender }`) that `agentRun` and `gate`
|
|
99
|
+
register into, with the agent surface and the gates HTTP API as dialect adapters over it — a future caller class (a
|
|
100
|
+
Slack bridge, an email reply) is another adapter, not a third mechanism. The table is implementation structure, not
|
|
101
|
+
vocabulary — workflows speak only `defineEvent` / `agentRun` / `gate`.
|
|
102
|
+
|
|
103
|
+
**Every delivered event carries its sender.** The table writes `sender: { party: "agent" | "gate", id }` on each event
|
|
104
|
+
it delivers, after the schema parse and from the registration alone: `id` is the Agent's slot, or the Gate's invoke id
|
|
105
|
+
(static in the Machine, the same after a restore — not the caller-facing gate id, which a workflow may compute). It is
|
|
106
|
+
plain data on the event, so it rides the mailbox, a delayed event the run host persists beside the snapshot, and a
|
|
107
|
+
restore; a restored registration writes it again from the persisted input and the invoke id. The Machine's own events
|
|
108
|
+
(`raise`, `sendTo` itself) carry none. `defineEvent` refuses an input that declares `sender`, and both surfaces refuse a
|
|
109
|
+
body that carries it (ADR-0013): no caller and no def can write it. What an author does with it is ADR-0015's.
|
|
110
|
+
|
|
111
|
+
`CANCEL` stays reserved as the run-level infra interrupt. jr's "exit 3 and let the human drive the bash script" becomes
|
|
112
|
+
"park in a durable state that advertises its human surface."
|
|
113
|
+
|
|
114
|
+
## Static imports; live objects constructed per-invocation from input
|
|
115
|
+
|
|
116
|
+
Everything above is reachable by plain `import`. We rejected two alternatives:
|
|
117
|
+
|
|
118
|
+
- **Injected closures / a `defineWorkflow(kit)` wrapper** — xstate's `provide()` fills only the machine it is called on;
|
|
119
|
+
it cannot reach actors inside child machines, so host-side injection cannot compose past one level (the finding that
|
|
120
|
+
retired the templates-with-injected-providers model — see ADR-0015).
|
|
121
|
+
- **Adapter singletons** — a module-global "current adapters" holder cross-contaminates in-process multi-instance use
|
|
122
|
+
and turns mocking into global state. (The demux registry above is not this: it is rendezvous keyed by globally unique
|
|
123
|
+
ids, not a swappable adapter.)
|
|
124
|
+
|
|
125
|
+
The doctrine that makes static import work, sharpening ADR-0007: **actor logic is code; everything live is constructed
|
|
126
|
+
per-invocation from serializable input** — the Harness client from an endpoint, the kube client from ambient config.
|
|
127
|
+
Tests mock by `machine.provide()` at the layer under test.
|
|
128
|
+
|
|
129
|
+
## Consequences
|
|
130
|
+
|
|
131
|
+
- Addresses are computed by jr2, never by the workflow — iids always (ADR-0016): fresh per invocation by default (the
|
|
132
|
+
lossy handoff), or `<run>/<machine actor path>/<agent>` plus its epoch under `continue: true` (ADR-0057); gate ids
|
|
133
|
+
derived from the gate's actor path by default, authored only to give external callers a meaningful name. Durable
|
|
134
|
+
handles live in the host ledger (ADR-0016), not in machine contexts.
|
|
135
|
+
- **Test stubbing happens at the wire, not in the actor.** The e2e tier (ADR-0010) hosts a wire-compatible stub Harness
|
|
136
|
+
beside its orchestrator fixture (admits the agent, holds the stream open, never acts). An endpoint is just a URL, so
|
|
137
|
+
`agentRun` keeps a single code path and cannot tell it is talking to a fake. Scope: **workspace-less test workflows
|
|
138
|
+
only**, which pass `endpoint` directly in run input — e2e drives their Machines by playing the Harness's Menu against
|
|
139
|
+
`/agents/<iid>/*` with no cluster. Workflows that invoke `workspace()` always get real Sandboxes (the data plane is
|
|
140
|
+
never faked — ADR-0009); their e2e tier requires kind.
|
|
141
|
+
- Unsolicited mid-turn steering remains the residual hard case (ADR-0002/0006).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# `workspace()` wraps a body Machine and owns only Sandbox lifecycle
|
|
2
|
+
|
|
3
|
+
jr2 provides Workspaces as a helper so workflows never provision Sandboxes themselves:
|
|
4
|
+
`workspace(body, { input, repos })` is a statically-imported factory (per [ADR-0011](0011-workflow-defined-events.md)'s
|
|
5
|
+
import doctrine) returning a Machine that provisions a Sandbox and clones its Repos, runs the author's **body** Machine
|
|
6
|
+
inside it, and tears the Sandbox down when the body finishes. This makes GLOSSARY.md's "Workspace = child Machine bound
|
|
7
|
+
to a unit of work" concrete.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
placing: create Sandbox CR → await the pod scheduled, no deadline (ADR-0064)
|
|
11
|
+
provisioning: await phase: Ready (watched, ADR-0063) → Harness /attach repos (ADR-0004)
|
|
12
|
+
running: invoke body; input = parent input + { workspace: { repos } }
|
|
13
|
+
teardown: destroy the CR
|
|
14
|
+
done: final; workspace output = body output
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Why a wrapper Machine and not actors in the workflow
|
|
18
|
+
|
|
19
|
+
Teardown forces the shape. In xstate v5 a stop is synchronous — a stopped machine cannot run async work or transition
|
|
20
|
+
through cleanup states — so reliable multi-step teardown must be **internal states the machine reaches by its own
|
|
21
|
+
transitions**. The thing that provisions must therefore also observe the body's completion, which means it wraps the
|
|
22
|
+
body. The persisted `teardown` state is also **durable intent**: a crash mid-teardown restores into it and re-runs the
|
|
23
|
+
idempotent destroy, which host-side lifecycle could only replicate by rebuilding the same ledger the state machine
|
|
24
|
+
already is. When the guarantee still fails (`kill -9`, hard cancel), the operator's heartbeat-lease GC is the backstop
|
|
25
|
+
(ADR-0001) — the reason ADR-0001 made the CR the durable desired state. Author-assembled provisioning/cleanup states
|
|
26
|
+
(the obvious alternative) put the guarantee in every workflow's hands to forget, on every error path.
|
|
27
|
+
|
|
28
|
+
## Boundary: the options speak workspace vocabulary only
|
|
29
|
+
|
|
30
|
+
The wrapper's options are workspace-domain. Which Repos to attach is not per-run at all: they are slots on the options
|
|
31
|
+
(`repos: { target: open, docs: { url, ref } }`, ADR-0051), static so `jr2 up` can see them, each bound by the Machine,
|
|
32
|
+
by a composer's `customize`, or by a mapper over the door. The Workspace only clones. A branch and a Worktree are the
|
|
33
|
+
body's: their names and their granularity (per feature, per task, from a ticket, from run input) are the Machine's
|
|
34
|
+
policy, not jr2's, and the body cuts each Worktree with `worktree()`
|
|
35
|
+
([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)). Workflow configuration (review rounds,
|
|
36
|
+
budgets, ticket data) passes through to the body untouched; the workspace never sees it. In return the body gets what it
|
|
37
|
+
needs to operate inside the Sandbox: `{ repos: Record<slot, path> }`, the handles keyed by the slots the wrapper
|
|
38
|
+
declared, in declaration order. A handle is the slot's folder, `/work/<slot>`, and not a Worktree. No handle names a
|
|
39
|
+
primary checkout: which tree an Agent works in is a fact about that Agent's Turn, which the body states (ADR-0051). The
|
|
40
|
+
Harness `endpoint` and the Sandbox name are mechanism-facing, not body-facing — `agentRun` resolves them **ambiently**
|
|
41
|
+
from the enclosing wrapper (a registrar actor co-invoked in `running`, ADR-0016), so the body cannot mis-thread them.
|
|
42
|
+
Keeping this boundary is what lets one helper serve any workflow rather than coupling to coding-shaped ones.
|
|
43
|
+
|
|
44
|
+
The wrapper's `input` is its door — the wrapper's own declared run-input schema, which also types a per-run slot's
|
|
45
|
+
mapper ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)). It sits on the wrapper and not on the body
|
|
46
|
+
precisely because of the injection above: the body's input is the door plus `workspace`, which no caller sends. That
|
|
47
|
+
same composition is what the door checks the body against, one way only — the body may not demand more than the wrapper
|
|
48
|
+
hands it; demanding less is safe — so the injection is a compile-time contract, not just a comment. The host's own
|
|
49
|
+
injection into a root machine's input (`instanceId`) counts as handed over for that check, because whether it actually
|
|
50
|
+
reaches this body depends on where the wrapper sits and no type can see that
|
|
51
|
+
([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)).
|
|
52
|
+
|
|
53
|
+
## An Effect runs in its Workspace: the Harness container or the Sandbox Effector
|
|
54
|
+
|
|
55
|
+
An Effect in the body (ADR-0067) resolves the same ambient handles an Agent does. A `command` that names no secret runs
|
|
56
|
+
in the Harness container of the enclosing Workspace's Sandbox, as the Agent's user. A `push`, an `http` whose body is a
|
|
57
|
+
Worktree file and a `command` that names a secret run in that Workspace's Sandbox Effector, the pod's `effector`
|
|
58
|
+
container ([ADR-0005](0005-sandbox-pod-composition.md),
|
|
59
|
+
[ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), which the provision adds only when an
|
|
60
|
+
Effect below the `workspace()` is placed there; its image is the Sandbox Image unless the `workspace()` names
|
|
61
|
+
`effector: { image }`. Each one's Worktree follows the Turn's rule (ADR-0057): its input names it, and under Repo Slots
|
|
62
|
+
it must. A push goes to the Binding of its Repo Slot, the one slot or the one its static `slot` names, and after every
|
|
63
|
+
push whose published commit is not the Agent's own — signed copies, or one squashed commit, signed or not — the
|
|
64
|
+
Workspace's Harness moves the Agent's branch to the published commits (the sync, ADR-0067). An Effect of these kinds
|
|
65
|
+
outside any `workspace()` is refused at invoke, as an error, never as output. A `command` that names a secret runs
|
|
66
|
+
against a pinned commit in a scratch of the Effector's own, and its child is confined with Landlock from the Effector's
|
|
67
|
+
key, the secrets it does not name, the push credentials and the Worktree (ADR-0067, ADR-0068).
|
|
68
|
+
|
|
69
|
+
## There is no retain policy: parking is retention
|
|
70
|
+
|
|
71
|
+
Teardown fires **only when the body reaches a final state or fails**. A Failure from the body
|
|
72
|
+
([ADR-0073](0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)) goes to a `failing` state,
|
|
73
|
+
which runs the same `destroy` and then sends the Failure up unchanged; a defect goes up with no `onError`, and its
|
|
74
|
+
Sandbox stays as evidence. A body that needs its Sandbox kept alive — escalation, a human inspecting the worktree, a
|
|
75
|
+
human-review gate — simply parks in a non-final state (typically holding a `gate`, ADR-0011). We considered and rejected
|
|
76
|
+
an `onSettled → retain | destroy` policy knob: parking expresses retention with no new API, and keeps the run visibly
|
|
77
|
+
"waiting" in `jr2 runs`. A parked run's Sandbox stays alive as long as the run does — each live workspace renews its own
|
|
78
|
+
lease, and the operator reaps only Sandboxes whose lease has lapsed (abandonment, not parking — ADR-0001).
|
|
79
|
+
|
|
80
|
+
## Commits leave via the workflow, never the workspace
|
|
81
|
+
|
|
82
|
+
Getting commits out of the pod-local clone (push, PR) is **body flow**, decided by the workflow — a coding workflow
|
|
83
|
+
pushes the branch / opens a PR before its human gate, so review is PR-based and the forge owns merging. The workspace
|
|
84
|
+
attaches and destroys; it never touches remotes. A body that finishes without publishing loses its commits by design —
|
|
85
|
+
the same contract as a temp directory.
|
|
86
|
+
|
|
87
|
+
## Consequences
|
|
88
|
+
|
|
89
|
+
- The body chooses how many Agents work in its Workspace (ADR-0004, ADR-0075). The wrapper has no lock machinery for
|
|
90
|
+
them.
|
|
91
|
+
- The workspace labels the CR with its run (`jr2.dev/run`, `jr2.dev/workflow`), which is what `jr2 ls` groups by
|
|
92
|
+
(ADR-0009).
|
|
93
|
+
- **Lost workspace: the wrapper emits, the body decides.** While the body runs, a lease actor beside it renews the
|
|
94
|
+
Sandbox's keepalive and follows the Sandbox watch for whether the workspace it attached to is still there (ADR-0021,
|
|
95
|
+
ADR-0063) — continuously, and on snapshot restore as the first tick of that same loop rather than a separate
|
|
96
|
+
restore-time path. Intact → carry on (same endpoint — deterministic per Sandbox name; in-flight turns re-attach from
|
|
97
|
+
the host ledger, ADR-0016). Lost — reaped, deleted, or a replacement pod after an eviction or node loss — → the
|
|
98
|
+
pod-local clone and any unpushed commits are gone, so silently re-provisioning would resume into an inconsistent world
|
|
99
|
+
(tickets closed, commits vanished); instead the wrapper delivers a **`workspace.lost`** event into the body — same
|
|
100
|
+
channel as `agent.fault` — and the body's policy decides (route it to escalation, fail the run, or choose to
|
|
101
|
+
re-provision and restart). The body must route it: `workspace()` refuses a body with a non-final state that has no
|
|
102
|
+
route on itself or an ancestor, and an event that no transition takes faults the run
|
|
103
|
+
([ADR-0074](0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md)).
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# The Agent reaches its Machine through a container it cannot read
|
|
2
|
+
|
|
3
|
+
An earlier cut put the Agent's tool surface on the Orchestrator (`/mcp/:instanceId`), which meant an Agent must dial
|
|
4
|
+
**into** the control plane to drive its Machine. Validating the workspace slice on kind (2026-07-12) showed that leg did
|
|
5
|
+
not exist and could not be safely built as specified: nothing passed a callback URL into a Sandbox, `jr2 dev` bound
|
|
6
|
+
loopback, and — the real problem — an Agent that can reach the delivery API can deliver to **any** registration in its
|
|
7
|
+
run, including its own human-review Gate. The fix is not to hide the API from the Agent. Working tools give the Agent
|
|
8
|
+
code execution in the Harness container, which shares the pod's network namespace, so it can reach any address the pod
|
|
9
|
+
can. The fix is where the credential lives: in a container of the same pod that the Agent executes nothing in — the
|
|
10
|
+
**Custodian** ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The Orchestrator speaks
|
|
11
|
+
no MCP, and keeps one HTTP surface over the registration table it already has
|
|
12
|
+
([ADR-0011](0011-workflow-defined-events.md)).
|
|
13
|
+
|
|
14
|
+
## The enabling fact: the Harness reads its Menu per Submission
|
|
15
|
+
|
|
16
|
+
Each Submission, the Harness reads `GET /agents/<iid>/surface`: the Agent's Menu, which it presents to its model as
|
|
17
|
+
tools named `mcp__jr2__<event>` (shipped instructions and the printer's prefix-stripping depend on that name;
|
|
18
|
+
[ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md) states it as explicit jr2 code, never
|
|
19
|
+
written by users). The Harness names the iid itself, so nothing has to _learn_ which turn is live — no push channel, no
|
|
20
|
+
long-poll, no `sandbox → active turn` index, no second inbound port on the pod. The same read carries the Turn's Allowed
|
|
21
|
+
picks, which the Harness states in the Briefing; the Menu itself is fixed for a conversation
|
|
22
|
+
([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)), so no `list_changed` push
|
|
23
|
+
is needed either (ADR-0006).
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
- **The Custodian carries the Agent's Menu, and holds the credential that makes it work.** It is a jr2-composed
|
|
28
|
+
container in every Harness pod ([ADR-0005](0005-sandbox-pod-composition.md)). A Sandbox has two tokens, its **Sandbox
|
|
29
|
+
tokens**: the **control token** and the **fetch token**, mounted into the Custodian as files, and into no other
|
|
30
|
+
container. The Harness process holds the control token's Stand-in, which is secret: a file that no process it confines
|
|
31
|
+
can read, in no child's env
|
|
32
|
+
([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). The fetch token's Stand-in
|
|
33
|
+
is public, `jr2-held-JR2_SANDBOX_TOKEN`, and `jr2-upload-pack` holds it as a constant. The Harness reaches the
|
|
34
|
+
Orchestrator only through the Custodian: the agent routes on its control socket, `/run/jr2/custodian/control.sock`,
|
|
35
|
+
which no confined program can replace (ADR-0076), and the fetch on `127.0.0.1:8081`. The Custodian forwards five
|
|
36
|
+
routes, with each route's own token put in place of its Stand-in, and answers every other route itself:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
GET /agents/<iid>/surface the Menu → the Orchestrator
|
|
40
|
+
POST /agents/<iid>/events a pick → the Orchestrator
|
|
41
|
+
GET /agents/<iid>/deliveries/<id> an outstanding pick's answer → the Orchestrator
|
|
42
|
+
POST /agents/<iid>/notices a step boundary's notices heard → the Orchestrator
|
|
43
|
+
POST /fetch the ask (ADR-0053), for THIS pod's Sandbox → POST /sandboxes/<name>/fetch
|
|
44
|
+
anything else 404
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
So the Agent's sole control-plane peer is a container it can reach but whose credential it cannot read. That is what
|
|
48
|
+
makes ADR-0006's "the Agent never steers the workflow" **enforced** rather than advertised.
|
|
49
|
+
|
|
50
|
+
- **The Harness presents the Menu to its model itself.** There is no MCP server and no MCP client. `menu.ts` turns each
|
|
51
|
+
accepted event into a Menu item, with the event's JSON Schema as its parameters, and turns a pick into one
|
|
52
|
+
`POST …/events` — and, for an outstanding pick, the polls of its delivery, inside the same tool call. The engine runs
|
|
53
|
+
one model answer's calls in the model's order
|
|
54
|
+
([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)), and only the first pick of a model
|
|
55
|
+
answer runs. It imports nothing from pi: `menu-tools.ts` gives each item to pi as a tool. A harness that only speaks
|
|
56
|
+
MCP would get a second presenter over the same `menu.ts`, not a second Menu. The receipt comes back as prose
|
|
57
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). A 404 on the surface is a Turn that is
|
|
58
|
+
over, and it is never prompted ([ADR-0026](0026-a-turn-that-is-over-is-never-prompted.md)).
|
|
59
|
+
- **The Orchestrator hosts no MCP, and judges every pick.** The registration table stays the one internal primitive; it
|
|
60
|
+
keeps two thin HTTP surfaces, neither of them MCP:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
# human / webhook / CI — the Gate resource of ADR-0011, authenticated
|
|
64
|
+
GET /runs/:id → open gates: accepts + schemas + acceptsNow + meta [Instance token]
|
|
65
|
+
POST /runs/:id/gates/:gate/events → validate + deliver, or 409 [Instance token]
|
|
66
|
+
|
|
67
|
+
# the Agent's, through its pod's Custodian
|
|
68
|
+
GET /agents/:iid/surface → Menu + Allowed picks + schemas + semantics [control token]
|
|
69
|
+
POST /agents/:iid/events → validate + deliver → the receipt, or 409 [control token]
|
|
70
|
+
GET /agents/:iid/deliveries/:id → an outstanding pick's answer, long-polled [control token]
|
|
71
|
+
POST /agents/:iid/notices → the notices a step boundary appended, heard (ADR-0062) [control token]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The receipt is `{ delivered, event, moved, turnComplete, allowed, deliveryId }`. A pick the workflow is still running
|
|
75
|
+
when the POST answers — it moved the workflow to work inside the Agent's state
|
|
76
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)) — is `outstanding`, and `deliveryId`
|
|
77
|
+
addresses its answer: `GET /agents/:iid/deliveries/:id` holds up to about 25 s and answers either `outstanding` (ask
|
|
78
|
+
again) or the receipt with `answered`, the Allowed picks read after the route, and the answer. A child that replied
|
|
79
|
+
inside the pick's own send and left the workflow at Rest makes the POST receipt itself the `answered` one. A stopping
|
|
80
|
+
or parked Orchestrator answers the poll 503, never 404, which says the Turn is over; the delivery is scoped to the
|
|
81
|
+
Sandbox its registration had when the pick was made, because the answer outlives a registration that a route out of
|
|
82
|
+
the Agent's state ends. A pick while one is outstanding, or while the Agent's state is busy, is answered
|
|
83
|
+
`409 { refused: "outstanding", outstanding?, allowed: [] }` and nothing is delivered. Lookup, validation, delivery and
|
|
84
|
+
lifecycle are implemented once, in the table. The token's scope, the live registration, and the event's name and
|
|
85
|
+
payload are judged here and nowhere else; the Custodian forwards and decides nothing about a pick. Neither surface
|
|
86
|
+
takes a body that carries `sender`: the table writes it on every delivered event (ADR-0011), so a client that posts it
|
|
87
|
+
— the Harness, `jr2 send --input` — is answered 400. The gates surface answers `{ ok: true }` when the run took the
|
|
88
|
+
send, and only then; a send the run would not take now — its selection holds another party's writing, or no writing
|
|
89
|
+
would move on this payload (ADR-0015, ADR-0029) — is answered `409 { error, acceptsNow }` and nothing is delivered.
|
|
90
|
+
`jr2 send --gate` prints that error, which names what the Gate takes now, and exits 1.
|
|
91
|
+
|
|
92
|
+
- **Bearer tokens, because the boundary is otherwise theater.** The Harness container shares the pod's network
|
|
93
|
+
namespace, so an Agent can `curl` the Orchestrator directly; a per-pod NetworkPolicy cannot distinguish it from the
|
|
94
|
+
Custodian. Only authentication closes this.
|
|
95
|
+
- **Control token and fetch token** — minted per Sandbox at provision (signed names, so re-provision after a restart
|
|
96
|
+
yields the same tokens), delivered as a Secret mounted into the **Custodian container only**. User
|
|
97
|
+
`harness.env`/`envFrom` land on the Harness container, never the Custodian. The control token authorizes exactly:
|
|
98
|
+
read and deliver to agent registrations **whose Sandbox is this one** and whose slot is the slot the Harness states
|
|
99
|
+
for the conversation (ADR-0076). The fetch token authorizes exactly: ask for this Sandbox's fetch. The Orchestrator
|
|
100
|
+
accepts each token only on its own routes. Neither reaches a Gate: a Gate refuses every principal of an Agent.
|
|
101
|
+
Neither reaches another Sandbox. A Stand-in is refused on every route.
|
|
102
|
+
- **Instance token** — the human/CLI credential for Gates and run control. Minted into an in-cluster Secret at
|
|
103
|
+
`jr2 up`; the CLI reads it over the kube API (RBAC is the gate) while port-forwarding the Service (ADR-0019).
|
|
104
|
+
- **The Instance-token door.** The Instance token may also read any Agent's surface and deliver to it —
|
|
105
|
+
`GET /agents/:iid/surface`, `POST /agents/:iid/events` — because it is the operator. That is the operator speaking
|
|
106
|
+
AS the Agent: it reads the Agent's Menu and Allowed picks and its picks are judged as the Agent's — they carry the
|
|
107
|
+
Agent's sender (ADR-0011) — and no audience filter applies, so it can send a pick no Gate's caller may send
|
|
108
|
+
([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)). It is deliberate — it is how the e2e tier stands in
|
|
109
|
+
for an Agent — and it adds no party: neither `jr2 send` nor the Console offers it, and a control token never opens
|
|
110
|
+
it for a registration that is not its own.
|
|
111
|
+
- An agent registration records its Sandbox **ambiently**: `agentRun` resolves the enclosing `workspace()` and records
|
|
112
|
+
the same deterministic name the token was minted for (ADR-0016), so forgetting to pass it is unrepresentable (the
|
|
113
|
+
pre-ambient version shipped exactly that footgun: `sandbox` omitted, every tool call 403'd — fail-closed but
|
|
114
|
+
silent). A run-scoped token instead of a Sandbox-scoped one would be genuinely exploitable: derivable iids plus
|
|
115
|
+
readable ticket ids would let one feature's coder inject a `review_verdict` into another feature's reviewer. That
|
|
116
|
+
holds across Sandboxes, where another feature's Agents are in another pod. Within one Sandbox several Agents can
|
|
117
|
+
share it ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)); there the Sandbox scope is
|
|
118
|
+
not enough, and ADR-0076 keeps the control credential where no Workspace code runs, so a coder's code cannot act as
|
|
119
|
+
the reviewer beside it.
|
|
120
|
+
- **Reachability, narrowed to one caller.** The Orchestrator is always in-cluster (ADR-0019), so the route home is
|
|
121
|
+
simply **Service DNS**, which the pod's maker writes into the Custodian's configuration — the Orchestrator at a
|
|
122
|
+
Sandbox's provision, `jr2 up` for the Instance Harness. The Harness is told the Custodian's loopback address
|
|
123
|
+
(`JR2_CUSTODIAN_URL`) and nothing else.
|
|
124
|
+
- **A Machine-answered tool result is the held answer, for every pick that runs work.** No def declares it: a pick whose
|
|
125
|
+
target runs something inside the Agent's state is outstanding until the workflow is back at Rest, and the Harness
|
|
126
|
+
polls its delivery inside the one tool call (ADR-0002, ADR-0024). The gates surface answers `{ ok: true }` for a send
|
|
127
|
+
the run took and 409 for one it would not — external callers need no receipt. `deferred` is retired (ADR-0006). `poll`
|
|
128
|
+
is still in the model and the surface listing still ships each def's `semantics`; registering a `poll` def **fails
|
|
129
|
+
loudly as unimplemented** — a silent downgrade to `ack` would be a lying tool contract — and the Harness refuses a
|
|
130
|
+
turn whose surface offers one.
|
|
131
|
+
|
|
132
|
+
## Considered options
|
|
133
|
+
|
|
134
|
+
- **Agent talks to the Orchestrator directly** (the original cut, plus a callback URL). Rejected: the credential lands
|
|
135
|
+
in a container where the Agent has code execution, so the Agent can deliver `approve` to its own human-review Gate —
|
|
136
|
+
it merges its own PR. The tool menu is advisory once you hold the key to the API.
|
|
137
|
+
- **A jr2-owned Adapter container that serves the Menu over MCP on loopback and holds the token.** It meets the claim,
|
|
138
|
+
but a whole container, image and package exist to translate two HTTP calls into MCP and back. Model keys need a
|
|
139
|
+
container the Agent executes nothing in anyway (ADR-0059); one container can hold every credential, and the
|
|
140
|
+
translation then has no reason to live outside the Harness.
|
|
141
|
+
- **Translation inside the Harness, with the credential there too.** Rejected — same container, same code execution,
|
|
142
|
+
same credential. The translation is inside the Harness now; the credential is not.
|
|
143
|
+
- **The Orchestrator serves MCP, and the Custodian forwards it.** Rejected: it keeps the MCP transport and its session
|
|
144
|
+
handling in the control plane and on both ends, to carry two plain HTTP calls.
|
|
145
|
+
- **A uniform `/registrations/:address` API** for every caller. Rejected: it dissolves the run-scoped, discoverable Gate
|
|
146
|
+
resource ADR-0011 designed on purpose (a webhook translator finds its Gate by `meta.prUrl`; `GET /runs/:id` lists open
|
|
147
|
+
decisions) in exchange for aesthetic symmetry.
|
|
148
|
+
|
|
149
|
+
## Consequences
|
|
150
|
+
|
|
151
|
+
- **The operator needs no change.** ADR-0001 made sidecars generic container fragments the operator schedules without
|
|
152
|
+
understanding; the Custodian is exactly that. `kubeSandbox` composes it (plus the token Secret, written right after
|
|
153
|
+
the CR with the CR as its owner from birth, so Kubernetes reaps it with the Sandbox and no path leaks it — ADR-0001).
|
|
154
|
+
- **Nothing in jr2 speaks MCP.** The Orchestrator, the Harness and the CLI carry no MCP dependency.
|
|
155
|
+
- **One container holds the Sandbox's tokens and every model key.** A fault in the Custodian's engine is a fault in the
|
|
156
|
+
one container that holds both. ADR-0059 records the trade.
|
|
157
|
+
- **The `@kind` e2e tier owns the pod → Orchestrator leg** — the pod's Harness actually calls its tool through the
|
|
158
|
+
Custodian, so a broken leg is a red test, and a Harness container that posts a pick straight to the Orchestrator with
|
|
159
|
+
the Stand-in it holds is refused. The mechanics tier plays `/agents/:iid/*` from the host, which is honestly
|
|
160
|
+
simulating the Harness's Menu, not an Agent.
|
|
161
|
+
- **Per-agent identity within one Sandbox is ADR-0076.** A Sandbox-scoped credential that an Agent's code could read
|
|
162
|
+
would let one Agent in a shared Sandbox deliver a pick as another, and ADR-0075 makes a shared Sandbox normal. Only
|
|
163
|
+
the Harness process holds the control credential, and each process it starts in `/work` runs confined; the Harness
|
|
164
|
+
states each conversation's slot, and the Orchestrator checks it on the whole `/agents/:iid/*` set. The `@kind` tier
|
|
165
|
+
gains the sibling cases: a coder's `bash` finds no control credential, and code it plants that the reviewer's own
|
|
166
|
+
`bash` runs cannot deliver a pick as the reviewer.
|
|
167
|
+
- **NetworkPolicy is a second layer, not the control.** It bounds where a pod may talk; only the token bounds what it
|
|
168
|
+
may _do_. `jr2 up` converges ingress policies that admit only the Orchestrator to a Harness pod
|
|
169
|
+
([ADR-0058](0058-a-harness-answers-the-orchestrator-alone.md)); the Orchestrator itself is left reachable, because a
|
|
170
|
+
Custodian in every Harness pod must reach it and no policy can tell the Custodian's packets from the Agent's.
|
|
171
|
+
- **The Harness wire is authenticated in the other direction.** The Orchestrator bears a token derived for the placement
|
|
172
|
+
on every Harness call, and the Harness holds only its digest (ADR-0058). Same signed-name idea, reversed caller.
|
|
173
|
+
- **Open: authn for non-kube callers.** The Instance token rides kube RBAC for anyone with cluster creds; what an
|
|
174
|
+
ingress-exposed jr2 Application uses for human/webhook callers without them (`--url` mode) is out of scope here
|
|
175
|
+
(ADR-0014 inherits the same question).
|