@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/src/stub-harness.ts
CHANGED
|
@@ -5,20 +5,26 @@
|
|
|
5
5
|
// the durable stream open, and never act — the Machine parks exactly as it would against a
|
|
6
6
|
// silent real Harness, and e2e drives it by playing the agent against `/mcp/<iid>` instead.
|
|
7
7
|
//
|
|
8
|
-
// Wire (ADR-0027 — the normative model of the
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// Wire (ADR-0027 — the normative model of the conversation endpoints; `@jr2/harness` serves it for
|
|
9
|
+
// real), and the pod's memory kills, which a stub never has (ADR-0062):
|
|
10
|
+
// POST /agents/:name/:id {message, cwd?, definition, model?, thinkingLevel?, notices?, kept?} → 200 { streamUrl, offset, submissionId }
|
|
11
|
+
// (`message` and `cwd` are the Turn's Frame — what it is about and where it works, ADR-0057;
|
|
12
|
+
// `kept` admits as any POST does, because the stub keeps every conversation — the real Harness
|
|
13
|
+
// answers 404 when it keeps none, ADR-0062)
|
|
11
14
|
// GET /agents/:name/:id?offset=…[&view=updates] → 200 `[]` + Stream-Next-Offset/Up-To-Date
|
|
12
15
|
// GET /agents/:name/:id?…&live=long-poll → parked; 204 + same headers on timeout
|
|
13
|
-
// POST /agents/:name/:id/abort
|
|
16
|
+
// POST /agents/:name/:id/abort [{cause?}] → 200 { aborted }
|
|
14
17
|
// GET /agents/:name/:id?view=history → 200 { …, settlements }
|
|
18
|
+
// GET /memory-kills → 200 { kills: [] }
|
|
15
19
|
// (the client's `wait(admission)` long-polls `streamUrl?view=updates` from the admission offset;
|
|
16
20
|
// an empty stream parks it — exactly the "admitted, never settles" semantics the mechanics tier
|
|
17
21
|
// needs.) The client then re-polls calmly at the long-poll cadence. `close()` severs parked polls.
|
|
18
22
|
//
|
|
19
|
-
// A stub submission therefore ends
|
|
20
|
-
//
|
|
21
|
-
//
|
|
23
|
+
// A stub submission therefore ends one way: the sweep, when the state that asked for the turn stops
|
|
24
|
+
// waiting (ADR-0024). Its outcome follows the sweep's cause, as the real Harness's does: `ended` for
|
|
25
|
+
// the running Submission when the Turn's own pick ended it (`cause: "pick"`), `aborted` for every
|
|
26
|
+
// other Submission and every other end. That is what `history`'s `settlements` carries, and it is
|
|
27
|
+
// the only place a black-box test can see a turn end — the run itself is untouched by an abort.
|
|
22
28
|
//
|
|
23
29
|
// This can later grow scriptable behavior or be swapped for a real local Harness without
|
|
24
30
|
// touching actor code — it is only a different URL.
|
|
@@ -32,6 +38,7 @@
|
|
|
32
38
|
|
|
33
39
|
import { createServer } from "node:http";
|
|
34
40
|
import type { AddressInfo, Socket } from "node:net";
|
|
41
|
+
import type { Notice } from "./wire.ts";
|
|
35
42
|
|
|
36
43
|
/** One admission of an Agent: which slot, which durable exchange, this Turn's FRAME (the prompt
|
|
37
44
|
* and where it works — ADR-0057), the DEFINITION the Machine's slot carried here (ADR-0049), and
|
|
@@ -49,11 +56,13 @@ export type Admission = {
|
|
|
49
56
|
definition?: Record<string, unknown>;
|
|
50
57
|
model?: string;
|
|
51
58
|
thinkingLevel?: string;
|
|
59
|
+
/** The notices the admission carried (ADR-0062). Absent when it carried none. */
|
|
60
|
+
notices?: Notice[];
|
|
52
61
|
};
|
|
53
62
|
|
|
54
63
|
/** One settled submission, in the shape `history()` reports it. The stub settles submissions for
|
|
55
|
-
* exactly one reason — an abort (ADR-0024) — so `outcome`
|
|
56
|
-
export type StubSettlement = { submissionId: string; outcome: "aborted" };
|
|
64
|
+
* exactly one reason — an abort (ADR-0024) — so `outcome` is one of the two a sweep gives. */
|
|
65
|
+
export type StubSettlement = { submissionId: string; outcome: "ended" | "aborted" };
|
|
57
66
|
|
|
58
67
|
export type StubHarnessOptions = {
|
|
59
68
|
/** Listen port. Default 0 → ephemeral (read back from `url`). */
|
|
@@ -99,17 +108,39 @@ export async function startStubHarness(opts: StubHarnessOptions = {}): Promise<R
|
|
|
99
108
|
// End every in-flight and queued submission for one instance (`agents.abort` — ADR-0024).
|
|
100
109
|
// Answers `{ aborted }`: whether there was anything to end, exactly as the real Harness
|
|
101
110
|
// does for an idle instance. Settlement is recorded here rather than pushed on the stream, because nothing is
|
|
102
|
-
// listening — the actor that asked for the turn is already stopped.
|
|
111
|
+
// listening — the actor that asked for the turn is already stopped. The body's cause decides the
|
|
112
|
+
// running Submission's outcome, the first unsettled one (`AbortRequest`): `ended` for `pick` with
|
|
113
|
+
// that Submission's id; no body, another id, or any other cause, `aborted`. What is queued
|
|
114
|
+
// behind it never ran: `aborted`.
|
|
103
115
|
const aborting = req.method === "POST" && ABORT_PATH.exec(url.pathname);
|
|
104
116
|
if (aborting) {
|
|
105
117
|
const [, agentName, instanceId] = aborting as unknown as [string, string, string];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
let body = "";
|
|
119
|
+
req.on("data", (chunk: Buffer) => (body += chunk));
|
|
120
|
+
req.on("end", () => {
|
|
121
|
+
const sent = safeParse(body) as { cause?: unknown; submissionId?: unknown } | undefined;
|
|
122
|
+
const k = key(decodeURIComponent(agentName), decodeURIComponent(instanceId));
|
|
123
|
+
const ended = unsettled.get(k)?.splice(0) ?? [];
|
|
124
|
+
const byPick = sent?.cause === "pick" && sent.submissionId === ended[0];
|
|
125
|
+
const settled = settlements.get(k) ?? [];
|
|
126
|
+
settled.push(
|
|
127
|
+
...ended.map((submissionId, i) => ({
|
|
128
|
+
submissionId,
|
|
129
|
+
outcome: byPick && i === 0 ? ("ended" as const) : ("aborted" as const),
|
|
130
|
+
})),
|
|
131
|
+
);
|
|
132
|
+
settlements.set(k, settled);
|
|
133
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
134
|
+
res.end(JSON.stringify({ aborted: ended.length > 0 }));
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The pod's memory kills (ADR-0062): a stub runs no process, so none. Answered, not a 404, so a
|
|
140
|
+
// Turn the stub admits logs no failed read.
|
|
141
|
+
if (req.method === "GET" && url.pathname === "/memory-kills") {
|
|
111
142
|
res.writeHead(200, { "content-type": "application/json" });
|
|
112
|
-
res.end(JSON.stringify({
|
|
143
|
+
res.end(JSON.stringify({ kills: [] }));
|
|
113
144
|
return;
|
|
114
145
|
}
|
|
115
146
|
|
|
@@ -133,6 +164,7 @@ export async function startStubHarness(opts: StubHarnessOptions = {}): Promise<R
|
|
|
133
164
|
definition?: Record<string, unknown>;
|
|
134
165
|
model?: string;
|
|
135
166
|
thinkingLevel?: string;
|
|
167
|
+
notices?: Notice[];
|
|
136
168
|
}
|
|
137
169
|
| undefined;
|
|
138
170
|
const admission: Admission = {
|
|
@@ -145,6 +177,7 @@ export async function startStubHarness(opts: StubHarnessOptions = {}): Promise<R
|
|
|
145
177
|
...(sent?.definition ? { definition: sent.definition } : {}),
|
|
146
178
|
...(sent?.model ? { model: sent.model } : {}),
|
|
147
179
|
...(sent?.thinkingLevel ? { thinkingLevel: sent.thinkingLevel } : {}),
|
|
180
|
+
...(sent?.notices?.length ? { notices: sent.notices } : {}),
|
|
148
181
|
};
|
|
149
182
|
admissions.push(admission);
|
|
150
183
|
const submissionId = `stub-${++submissionSeq}`;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The sync's client (ADR-0067): after a push whose published commit is not the Agent's own, the
|
|
2
|
+
// Orchestrator asks the Workspace's Harness to move the Agent's branch to it (`POST /sync`), carrying the bundle the Sandbox
|
|
3
|
+
// Effector wrote. The bundle travels through here, never through a shared volume: the Worktree is
|
|
4
|
+
// read-only on the credentialed side, and nothing the Effector writes is a channel into the Agent's
|
|
5
|
+
// container but this one call, which moves one ref. The Harness's answer is a report the next push
|
|
6
|
+
// never trusts.
|
|
7
|
+
|
|
8
|
+
import type { SyncAnswer, SyncRequest } from "./wire.ts";
|
|
9
|
+
|
|
10
|
+
/** The Harness's sync, as a push drives it. It never throws: a Harness that cannot be reached, or
|
|
11
|
+
* that refuses, answers `not-synced` with the reason — the push's outcome stands either way. */
|
|
12
|
+
export interface SyncPort {
|
|
13
|
+
sync(req: SyncRequest, signal: AbortSignal): Promise<SyncAnswer>;
|
|
14
|
+
}
|
|
15
|
+
export type SyncPortFactory = (endpoint: string, bearer?: string) => SyncPort;
|
|
16
|
+
|
|
17
|
+
/** How long a sync may take before it answers `not-synced`: its git steps are bounded at 120 s each. */
|
|
18
|
+
const SYNC_TIMEOUT_MS = 5 * 60_000;
|
|
19
|
+
|
|
20
|
+
export function createSyncPort(options: {
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
/** The Workspace's Harness bearer (ADR-0058). */
|
|
23
|
+
token?: string;
|
|
24
|
+
/** Injectable for socket-free tests. Default: global `fetch`. */
|
|
25
|
+
fetch?: typeof fetch;
|
|
26
|
+
}): SyncPort {
|
|
27
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
28
|
+
return {
|
|
29
|
+
sync: async (req, signal) => {
|
|
30
|
+
const url = new URL("/sync", options.baseUrl).toString();
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetchImpl(url, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"content-type": "application/json",
|
|
36
|
+
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify(req),
|
|
39
|
+
// Bounded: a Harness that never answers is a sync that did not finish, never a push held open.
|
|
40
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(SYNC_TIMEOUT_MS)]),
|
|
41
|
+
});
|
|
42
|
+
const text = await res.text();
|
|
43
|
+
if (!res.ok) return { sync: "not-synced", reason: `the Harness refused the sync (${res.status}): ${text}` };
|
|
44
|
+
return JSON.parse(text) as SyncAnswer;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { sync: "not-synced", reason: `the Harness could not be reached: ${(err as Error).message}` };
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The real factory: the Workspace's Harness, with its bearer. */
|
|
53
|
+
export const harnessSyncPort: SyncPortFactory = (endpoint, bearer) =>
|
|
54
|
+
createSyncPort({ baseUrl: endpoint, ...(bearer ? { token: bearer } : {}) });
|
package/src/timers.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// A run's pending timers across a restart (ADR-0007).
|
|
2
|
+
//
|
|
3
|
+
// xstate 5 keeps a delayed event (`after`, a named delay, a delayed `raise` or `sendTo`) on the
|
|
4
|
+
// actor SYSTEM's scheduler, not in the Machine snapshot. `getPersistedSnapshot()` carries no trace
|
|
5
|
+
// of it, and a restored actor runs no entry actions again, so nothing schedules it again: the
|
|
6
|
+
// timer is lost, not restarted. xstate 5 has a `snapshot.scheduler` seam in `createSystem`, but
|
|
7
|
+
// the Actor constructor never passes it.
|
|
8
|
+
//
|
|
9
|
+
// So the run host records each pending timer's absolute DEADLINE beside the snapshot, in the same
|
|
10
|
+
// blob, and schedules it again on the restored system after `start()`. The author writes plain
|
|
11
|
+
// `after`. The scheduled id is xstate's own (`xstate.after.<delay>.<stateId>` for an `after`), so
|
|
12
|
+
// the state's exit `cancel(id)` still cancels a re-armed timer.
|
|
13
|
+
//
|
|
14
|
+
// This reads an xstate INTERNAL: `system.getSnapshot()._scheduledEvents` (typed in xstate's
|
|
15
|
+
// `ActorSystem`, but underscored), `system.scheduler.schedule`, and `system.scheduler.cancel`
|
|
16
|
+
// (wrapped for the length of `start()`). An xstate bump can break it; the raw-xstate tests in
|
|
17
|
+
// test/timers.test.ts are the canary. xstate 6 persists timers in the snapshot and re-arms them
|
|
18
|
+
// itself, with the same deadline and clamp: on xstate 6, delete this module.
|
|
19
|
+
|
|
20
|
+
import type { AnyActorRef, EventObject } from "xstate";
|
|
21
|
+
|
|
22
|
+
/** One pending delayed event, JSON-safe. An actor is named by its path of child ids below the
|
|
23
|
+
* run's root (`[]` is the root). */
|
|
24
|
+
export type PendingTimer = {
|
|
25
|
+
source: string[];
|
|
26
|
+
target: string[];
|
|
27
|
+
/** xstate's scheduled id. */
|
|
28
|
+
id: string;
|
|
29
|
+
event: EventObject;
|
|
30
|
+
/** The delay as last scheduled, ms: the upper bound on what remains (see {@link startAndRearm}). */
|
|
31
|
+
delay: number;
|
|
32
|
+
/** Wall-clock ms at which it is due, on the host's clock (`now`). */
|
|
33
|
+
deadline: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function childrenOf(ref: AnyActorRef): Record<string, AnyActorRef> {
|
|
37
|
+
return (ref.getSnapshot() as { children?: Record<string, AnyActorRef> }).children ?? {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pathOf(root: AnyActorRef, ref: AnyActorRef, path: string[] = []): string[] | undefined {
|
|
41
|
+
if (ref === root) return path;
|
|
42
|
+
for (const [id, child] of Object.entries(childrenOf(root))) {
|
|
43
|
+
const found = pathOf(child, ref, [...path, id]);
|
|
44
|
+
if (found) return found;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function refAt(root: AnyActorRef, path: string[]): AnyActorRef | undefined {
|
|
50
|
+
let ref: AnyActorRef | undefined = root;
|
|
51
|
+
for (const id of path) ref = ref && childrenOf(ref)[id];
|
|
52
|
+
return ref;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** xstate's key for a scheduled event: one per source actor and id. */
|
|
56
|
+
const keyOf = (source: AnyActorRef, id: string) => `${source.sessionId}.${id}`;
|
|
57
|
+
|
|
58
|
+
const clamp = (ms: number, delay: number) => Math.min(delay, Math.max(0, ms));
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The run's pending timers, read off its actor system. Call at persist time: the scheduler's table
|
|
62
|
+
* and the snapshot are read in the same tick, so they agree. A timer whose source or target is not
|
|
63
|
+
* in the run's tree (a delayed send to an actor outside it) is not recorded; jr2 has no such idiom.
|
|
64
|
+
*
|
|
65
|
+
* xstate stamps a timer's start with `Date.now()`, so the time that remains is read on that clock;
|
|
66
|
+
* the deadline is `now + remaining`, on the host's clock. With the default `now` the two are one
|
|
67
|
+
* clock and the deadline is xstate's own `startedAt + delay`.
|
|
68
|
+
*/
|
|
69
|
+
export function pendingTimers(root: AnyActorRef, now = Date.now()): PendingTimer[] {
|
|
70
|
+
const wall = Date.now();
|
|
71
|
+
const out: PendingTimer[] = [];
|
|
72
|
+
for (const s of Object.values(root.system.getSnapshot()._scheduledEvents)) {
|
|
73
|
+
const source = pathOf(root, s.source);
|
|
74
|
+
const target = pathOf(root, s.target);
|
|
75
|
+
if (!source || !target) continue;
|
|
76
|
+
const remaining = clamp(s.startedAt + s.delay - wall, s.delay);
|
|
77
|
+
out.push({ source, target, id: s.id, event: s.event, delay: s.delay, deadline: now + remaining });
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The pending delayed events whose TARGET is `ref`, from any source: what Rest asks (rest.ts) —
|
|
84
|
+
* an `after`, a named delay, a delayed `raise` or `sendTo(self)`, a child's delayed `sendParent`.
|
|
85
|
+
* The same table {@link pendingTimers} persists and {@link startAndRearm} re-arms, so Rest reads
|
|
86
|
+
* the same timers after a restore as before it.
|
|
87
|
+
*/
|
|
88
|
+
export function scheduledFor(ref: AnyActorRef): Array<{ id: string; event: EventObject }> {
|
|
89
|
+
return Object.values(ref.system.getSnapshot()._scheduledEvents)
|
|
90
|
+
.filter((s) => s.target === ref)
|
|
91
|
+
.map((s) => ({ id: s.id, event: s.event }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Watch a run's actor system for timers that FIRE: the returned function says whether one relayed
|
|
96
|
+
* event is a timer the system scheduled — an `after`, a named delay, a delayed `raise` or `sendTo` —
|
|
97
|
+
* and not an event an actor sent at once. Both reach the inspection stream from the same source with
|
|
98
|
+
* the same shape; only the scheduler knows the difference, and it forgets a timer as it fires it.
|
|
99
|
+
*
|
|
100
|
+
* So `scheduler.schedule` and `scheduler.cancel` are wrapped for the life of the system: each armed
|
|
101
|
+
* timer is kept by its key with the event object xstate relays when it fires (the same object, not
|
|
102
|
+
* a copy). A relayed event is a fired timer when its source and event object are an armed timer's
|
|
103
|
+
* and that timer is no longer in the scheduler's table; it is then forgotten. A re-armed timer
|
|
104
|
+
* (`startAndRearm`) goes through the same `schedule`, so a restored run recognises it too.
|
|
105
|
+
*
|
|
106
|
+
* Install it before `start()` (the root's creation event is early enough): a timer armed before the
|
|
107
|
+
* wrap is not seen. The same xstate internals as the rest of this module; the canary is the same.
|
|
108
|
+
*/
|
|
109
|
+
export function watchTimers(
|
|
110
|
+
system: AnyActorRef["system"],
|
|
111
|
+
hooks: TimerHooks = {},
|
|
112
|
+
): (source: unknown, event: unknown) => boolean {
|
|
113
|
+
const scheduler = system.scheduler;
|
|
114
|
+
const { schedule, cancel } = scheduler;
|
|
115
|
+
const armed = new Map<string, { source: AnyActorRef; event: EventObject }>();
|
|
116
|
+
scheduler.schedule = (source, target, event, delay, id = Math.random().toString(36).slice(2)) => {
|
|
117
|
+
hooks.arming?.(source);
|
|
118
|
+
schedule(source, target, event, delay, id);
|
|
119
|
+
armed.set(keyOf(source, id), { source, event });
|
|
120
|
+
};
|
|
121
|
+
scheduler.cancel = (source, id) => {
|
|
122
|
+
armed.delete(keyOf(source, id));
|
|
123
|
+
cancel(source, id);
|
|
124
|
+
};
|
|
125
|
+
return (source, event) => {
|
|
126
|
+
for (const [key, row] of armed) {
|
|
127
|
+
if (row.event !== event || row.source !== source) continue;
|
|
128
|
+
if (key in system.getSnapshot()._scheduledEvents) continue;
|
|
129
|
+
armed.delete(key);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** What `watchTimers` tells as the scheduler arms a timer: `arming` is called with the timer's
|
|
137
|
+
* source just before xstate schedules it, and xstate then arms it on the system's clock in the same
|
|
138
|
+
* call. xstate schedules in the deferred effects of the event whose actions armed the timer, so the
|
|
139
|
+
* source still takes that event. The run host keeps each timer's cause this way (`cause.ts`,
|
|
140
|
+
* ADR-0024). */
|
|
141
|
+
export type TimerHooks = {
|
|
142
|
+
arming?: (source: AnyActorRef) => void;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Start a restored run, then schedule each recorded timer again on it. One function, so the order
|
|
147
|
+
* cannot be got wrong: re-armed BEFORE `start()`, a timer is armed twice and fires twice, because
|
|
148
|
+
* the system's own `start()` schedules whatever is already in its table; and `start()` is where
|
|
149
|
+
* xstate builds the children, so a child's path resolves only after it.
|
|
150
|
+
*
|
|
151
|
+
* `start()` drains the mailbox: a child that sends on start (an author's `fromCallback`) can move
|
|
152
|
+
* the Machine out of a state before any timer is re-armed. That exit's `cancel(id)` finds nothing
|
|
153
|
+
* armed yet. So every cancel during `start()` is recorded, and a row it names is not re-armed: a
|
|
154
|
+
* state exit cancels the timer whether it happens during `start()` or after it.
|
|
155
|
+
*
|
|
156
|
+
* A child stopped during `start()` takes its timers with it, but xstate stops a child with
|
|
157
|
+
* `scheduler.cancelAll`, which walks the table, and the table is still empty: no cancel is seen.
|
|
158
|
+
* If its parent state is entered again, a NEW child sits at the same path. So each row's actors are
|
|
159
|
+
* resolved BEFORE `start()` (the restored tree already holds them), and a row is re-armed only on
|
|
160
|
+
* the same actors after it. A row of a stopped child then fires into nothing, not into its
|
|
161
|
+
* replacement.
|
|
162
|
+
*
|
|
163
|
+
* Remaining time is `deadline - now`, clamped to `[0, delay]`. A deadline that passed while the
|
|
164
|
+
* Orchestrator was down fires at once. A wall clock that went BACKWARDS cannot push a timer past its
|
|
165
|
+
* own delay. A wall clock that jumped forwards fires a timer early: only the wall clock survives a
|
|
166
|
+
* process, so the deadline is a wall-clock time. Rows are scheduled in deadline order, so timers
|
|
167
|
+
* that are all overdue (remaining 0) fire in the order they fell due, as in an uninterrupted run.
|
|
168
|
+
*/
|
|
169
|
+
export function startAndRearm(root: AnyActorRef, timers: readonly PendingTimer[], now = Date.now()): void {
|
|
170
|
+
const scheduler = root.system.scheduler;
|
|
171
|
+
const cancel = scheduler.cancel;
|
|
172
|
+
const cancelled = new Set<string>();
|
|
173
|
+
const rows = [...timers]
|
|
174
|
+
// `sort` is stable: equal deadlines keep the order xstate scheduled them in.
|
|
175
|
+
.sort((a, b) => a.deadline - b.deadline)
|
|
176
|
+
.map((t) => ({ t, source: refAt(root, t.source), target: refAt(root, t.target) }));
|
|
177
|
+
scheduler.cancel = (source, id) => {
|
|
178
|
+
cancelled.add(keyOf(source, id));
|
|
179
|
+
cancel(source, id);
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
root.start();
|
|
183
|
+
} finally {
|
|
184
|
+
scheduler.cancel = cancel;
|
|
185
|
+
}
|
|
186
|
+
for (const { t, source, target } of rows) {
|
|
187
|
+
if (!source || !target || cancelled.has(keyOf(source, t.id))) continue;
|
|
188
|
+
if (refAt(root, t.source) !== source || refAt(root, t.target) !== target) continue;
|
|
189
|
+
scheduler.schedule(source, target, t.event, clamp(t.deadline - now, t.delay), t.id);
|
|
190
|
+
}
|
|
191
|
+
}
|
package/src/tokens.ts
CHANGED
|
@@ -3,13 +3,20 @@
|
|
|
3
3
|
// a per-pod NetworkPolicy cannot distinguish its packets from the Custodian's. Only authentication
|
|
4
4
|
// closes this, and only the token bounds what a caller may DO (as opposed to where it may talk).
|
|
5
5
|
//
|
|
6
|
-
//
|
|
6
|
+
// Three principals. The asymmetry between the first two is the whole design; the third is a door
|
|
7
|
+
// key, not a seat:
|
|
7
8
|
//
|
|
8
9
|
// Instance token the human/CLI credential. Full trust: gates, run control, agent surfaces.
|
|
9
10
|
// From the instance Secret (`JR2_INSTANCE_TOKEN`); minted per boot when absent,
|
|
10
11
|
// announced once on stdout (fixtures capture it). An Agent never has it — it
|
|
11
12
|
// never enters a Sandbox.
|
|
12
13
|
//
|
|
14
|
+
// Publish token `jr2 up`'s credential for `PUT /assembly` alone (ADR-0069). A publish imports
|
|
15
|
+
// code into the process that holds the ServiceAccount, so it must cost what an
|
|
16
|
+
// image roll cost: cluster write access. From the instance Secret
|
|
17
|
+
// (`JR2_PUBLISH_TOKEN`), read by `jr2 up` through the kube API only. Every other
|
|
18
|
+
// route answers it 403: it is a known token, but not one that route accepts.
|
|
19
|
+
//
|
|
13
20
|
// Sandbox token the Custodian's credential. Authorizes exactly: deliver to `kind: "agent"`
|
|
14
21
|
// registrations whose Sandbox is THIS one. Never a Gate (a compromised Agent
|
|
15
22
|
// must not approve its own review), never another Sandbox (`coding.ts`'s iids
|
|
@@ -43,6 +50,9 @@ import { join } from "node:path";
|
|
|
43
50
|
export type Principal =
|
|
44
51
|
/** The instance's own operator: the CLI, a human, a webhook translator holding the token. */
|
|
45
52
|
| { kind: "instance" }
|
|
53
|
+
/** `jr2 up`, publishing an Assembly (ADR-0069). Only `PUT /assembly` accepts it: it is not an
|
|
54
|
+
* Instance principal, so it reads no run and starts none. */
|
|
55
|
+
| { kind: "publisher" }
|
|
46
56
|
/** One Sandbox's Custodian, speaking for the Agent in that pod — and for no one else. */
|
|
47
57
|
| { kind: "sandbox"; sandbox: string };
|
|
48
58
|
|
|
@@ -77,7 +87,20 @@ export function harnessTokenDigest(key: Buffer, placement: string): string {
|
|
|
77
87
|
return createHash("sha256").update(harnessToken(key, placement)).digest("base64url");
|
|
78
88
|
}
|
|
79
89
|
|
|
80
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* The signing key of one Effector placement (ADR-0068): `HMAC(signing key, "effector:" + placement)`,
|
|
92
|
+
* as the Harness bearer is derived (ADR-0058). The Orchestrator signs every request to that
|
|
93
|
+
* Effector with it and verifies every answer; the Effector reads it from a file in its own mount,
|
|
94
|
+
* which `jr2 up` writes (`instance` → the `jr2-instance-effector` Secret). Derived, not stored: a
|
|
95
|
+
* restarted Orchestrator derives the key a live Effector already holds. The `effector:` prefix
|
|
96
|
+
* keeps it apart from every Sandbox token and Harness bearer. The key's bytes are this string's.
|
|
97
|
+
*/
|
|
98
|
+
export function effectorKey(key: Buffer, placement: string): string {
|
|
99
|
+
return sign(key, `effector:${placement}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Mint the per-boot Instance token (opaque and random — it names nothing, it just IS the trust).
|
|
103
|
+
* The publish token is minted the same way when the Secret supplies none (ADR-0069). */
|
|
81
104
|
export function mintInstanceToken(): string {
|
|
82
105
|
return randomBytes(32).toString("base64url");
|
|
83
106
|
}
|
|
@@ -109,10 +132,16 @@ export async function loadSigningKey(dir: string): Promise<Buffer> {
|
|
|
109
132
|
* A token we did not mint resolves to nothing, and the caller is refused — there is no anonymous
|
|
110
133
|
* principal, and no route decides for itself whether it needs one.
|
|
111
134
|
*/
|
|
112
|
-
export function createAuthenticator(opts: {
|
|
135
|
+
export function createAuthenticator(opts: {
|
|
136
|
+
instanceToken: string;
|
|
137
|
+
signingKey: Buffer;
|
|
138
|
+
/** The publish token (ADR-0069). Absent → no bearer resolves to the publisher. */
|
|
139
|
+
publishToken?: string;
|
|
140
|
+
}): Authenticator {
|
|
113
141
|
return (bearer) => {
|
|
114
142
|
if (!bearer) return undefined;
|
|
115
143
|
if (constantTimeEqual(bearer, opts.instanceToken)) return { kind: "instance" };
|
|
144
|
+
if (opts.publishToken !== undefined && constantTimeEqual(bearer, opts.publishToken)) return { kind: "publisher" };
|
|
116
145
|
const cut = bearer.lastIndexOf(".");
|
|
117
146
|
if (cut <= 0) return undefined;
|
|
118
147
|
const sandbox = bearer.slice(0, cut);
|
|
@@ -131,6 +160,7 @@ export function createAuthenticator(opts: { instanceToken: string; signingKey: B
|
|
|
131
160
|
*/
|
|
132
161
|
export function mayDeliverToAgent(principal: Principal, registrationSandbox: string | undefined): boolean {
|
|
133
162
|
if (principal.kind === "instance") return true;
|
|
163
|
+
if (principal.kind !== "sandbox") return false;
|
|
134
164
|
return registrationSandbox !== undefined && registrationSandbox === principal.sandbox;
|
|
135
165
|
}
|
|
136
166
|
|
|
@@ -138,10 +168,11 @@ export function mayDeliverToAgent(principal: Principal, registrationSandbox: str
|
|
|
138
168
|
* May this principal ask for a fetch on this Sandbox (ADR-0053)? A Sandbox token may ask for its
|
|
139
169
|
* OWN pod and no other — the scope is the caches that pod mounts, and the route refuses a Repo it
|
|
140
170
|
* does not. The Instance token may, on the same grounds it may deliver to any agent surface: it is
|
|
141
|
-
* the operator.
|
|
171
|
+
* the operator. The publish token may not: it opens the publish door and no other (ADR-0069).
|
|
142
172
|
*/
|
|
143
173
|
export function mayAskForSandbox(principal: Principal, sandbox: string): boolean {
|
|
144
174
|
if (principal.kind === "instance") return true;
|
|
175
|
+
if (principal.kind !== "sandbox") return false;
|
|
145
176
|
return principal.sandbox === sandbox;
|
|
146
177
|
}
|
|
147
178
|
|
package/src/vocabulary.ts
CHANGED
|
@@ -54,10 +54,32 @@ export function vocabularyOf(machine: AnyStateMachine): Map<string, EventDef> |
|
|
|
54
54
|
* silently accepting a name nobody declared.
|
|
55
55
|
*/
|
|
56
56
|
export function invokingMachine(self: AnyActorRef): AnyStateMachine | undefined {
|
|
57
|
-
|
|
57
|
+
// An Open body's actor (ADR-0070) delegates to the Machine compiled from the Chart it binds, and
|
|
58
|
+
// its own logic is no Machine: the bound Machine is the scope of the Agents and Gates its states
|
|
59
|
+
// invoke, so it is asked first.
|
|
60
|
+
const parent = self._parent;
|
|
61
|
+
const bound = parent ? boundMachines.get(parent) : undefined;
|
|
62
|
+
if (bound) return bound;
|
|
63
|
+
const logic = (parent as { logic?: unknown } | undefined)?.logic as AnyStateMachine | undefined;
|
|
58
64
|
return logic?.root ? logic : undefined;
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
// The Machine an Open body's actor runs (ADR-0070): compiled from the Chart the run bound, per
|
|
68
|
+
// actor, at start and again at restore. Keyed on the ACTOR, not on a config: two runs of one
|
|
69
|
+
// wrapper bind two Charts under the same slot. Kept here, beside the vocabulary, so this module
|
|
70
|
+
// stays a leaf: open-body.ts writes it, and `invokingMachine` reads it.
|
|
71
|
+
const boundMachines = new WeakMap<AnyActorRef, AnyStateMachine>();
|
|
72
|
+
|
|
73
|
+
/** Record the Machine an Open body's actor runs. jr2-internal: the Open body's logic calls it. */
|
|
74
|
+
export function attachBoundMachine(actor: AnyActorRef, machine: AnyStateMachine): void {
|
|
75
|
+
boundMachines.set(actor, machine);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The Machine an Open body's actor runs, or undefined for any other actor. */
|
|
79
|
+
export function boundMachineFor(actor: AnyActorRef): AnyStateMachine | undefined {
|
|
80
|
+
return boundMachines.get(actor);
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
// The declared run input (ADR-0033): the one piece of a machine's vocabulary the event defs
|
|
62
84
|
// missed — what a run of it is STARTED with. Same key choice as the vocabulary above, for the
|
|
63
85
|
// same reasons (per-machine attribution, no global registry, transparent across `.provide()`).
|
|
@@ -79,22 +101,120 @@ export function inputSchemaOf(machine: AnyStateMachine): z.ZodObject | undefined
|
|
|
79
101
|
return inputSchemas.get(machine.config);
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
/** A zod schema, by its shape: the Instance can resolve a zod of its own, so `instanceof` does not
|
|
105
|
+
* tell. */
|
|
106
|
+
export function isZodSchema(value: unknown): boolean {
|
|
107
|
+
return (
|
|
108
|
+
typeof value === "object" &&
|
|
109
|
+
value !== null &&
|
|
110
|
+
"_zod" in value &&
|
|
111
|
+
typeof (value as { safeParse?: unknown }).safeParse === "function"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The run input that a machine declares: the door that `jr2Setup` attached, or a zod `input` on
|
|
116
|
+
* its `createMachine` config that no door reads (a machine that `setup()` made). */
|
|
117
|
+
export function declaresRunInput(machine: AnyStateMachine): boolean {
|
|
118
|
+
return inputSchemaOf(machine) !== undefined || isZodSchema((machine.config as { input?: unknown }).input);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Who may send what, per state (ADR-0015): the seats the menu-derivation walk computed. Same key
|
|
122
|
+
// choice as the vocabulary, for the same reasons. The Machine's doc carries them; `jr2 up` and the
|
|
123
|
+
// Console show them, so an author reads the audience rule's answer for their own Machine.
|
|
124
|
+
|
|
125
|
+
/** One party's seat on one state: the state's id, the party, and the names it may send there —
|
|
126
|
+
* for an Agent the derived set its guards narrow; for a Gate its accepted set, unless the Gate's
|
|
127
|
+
* input names `accepts:` itself. */
|
|
128
|
+
export type MachineSeat = { state: string; party: "agent" | "gate"; agent?: string; names: string[] };
|
|
129
|
+
|
|
130
|
+
const seatTables = new WeakMap<MachineKey, MachineSeat[]>();
|
|
131
|
+
|
|
132
|
+
/** Attach a Machine's seats. jr2-internal: `jr2Setup.createMachine` calls it. */
|
|
133
|
+
export function attachSeats(machine: AnyStateMachine, seats: MachineSeat[]): void {
|
|
134
|
+
seatTables.set(machine.config, seats);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The seats of a Machine built by `jr2Setup`, in walk order; empty for any other Machine. */
|
|
138
|
+
export function seatsOf(machine: AnyStateMachine): MachineSeat[] {
|
|
139
|
+
return seatTables.get(machine.config) ?? [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** One line per seat — `<state> agent:<slot>: [names]` or `<state> gate: [names]`. */
|
|
143
|
+
export function seatLine(seat: MachineSeat): string {
|
|
144
|
+
const party = seat.party === "agent" ? `agent:${seat.agent}` : "gate";
|
|
145
|
+
return `${seat.state} ${party}: [${seat.names.join(", ")}]`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const loadWarnings = new WeakMap<MachineKey, string[]>();
|
|
149
|
+
|
|
150
|
+
/** Attach the load warnings `jr2Setup.createMachine` found (ADR-0015). jr2-internal. */
|
|
151
|
+
export function attachLoadWarnings(machine: AnyStateMachine, warnings: string[]): void {
|
|
152
|
+
loadWarnings.set(machine.config, warnings);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** What the build found legal and suspect — a mode nothing enters, a pick a Gate above takes from
|
|
156
|
+
* the Agent below, one name written for two parties on one path. `jr2 up` prints them; the
|
|
157
|
+
* Machine's doc carries them. A Machine `customize()` bound an Open argv in is rebuilt around a
|
|
158
|
+
* config of its own, which carries the walk's findings for the bound argv (ADR-0054). Empty for a
|
|
159
|
+
* Machine not built by `jr2Setup`. */
|
|
160
|
+
export function loadWarningsOf(machine: AnyStateMachine): string[] {
|
|
161
|
+
return loadWarnings.get(machine.config) ?? [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// What the HOST adds to the input of the machine a run STARTS with, beside the door
|
|
165
|
+
// (`RunHost.start`). One field today: the run's seed Instance ID, minted with the run and
|
|
166
|
+
// reported by `jr2 status`. It names the RUN, never a conversation — jr2 mints every Agent
|
|
167
|
+
// Instance ID in the input mapper (ADR-0016, ADR-0057), and a Machine cannot pass one in.
|
|
168
|
+
//
|
|
169
|
+
// It is deliberately NOT door material (ADR-0033): no caller sends it — `start` overwrites
|
|
170
|
+
// whatever arrived, after the parse — and it is never served as JSON Schema, so putting it in a
|
|
171
|
+
// door schema would publish it and demand of every caller a field the host supplies anyway.
|
|
172
|
+
//
|
|
173
|
+
// It is also PLACEMENT-DEPENDENT: only the ROOT machine is started with it. A machine invoked
|
|
174
|
+
// further down is fed by its parent, which passes what it chooses. No type can see where a machine
|
|
175
|
+
// sits, so the one contract that must reason about this — `workspace()`'s body guard — takes the
|
|
176
|
+
// permissive answer and counts these keys as PROVIDED, which lets a body declare the field
|
|
177
|
+
// honestly. Counting them as provided (rather than subtracting them from what the body demands)
|
|
178
|
+
// is what keeps their TYPE checked: `instanceId` is a string, and a body asking for anything else
|
|
179
|
+
// is still rejected.
|
|
82
180
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
181
|
+
* The fields that jr2 adds to the input of the Machine that a run starts, beside the input that
|
|
182
|
+
* the caller sent. Today it is one field: `instanceId`, an id that jr2 makes for the run.
|
|
183
|
+
*
|
|
184
|
+
* Add it to the type of your root Machine's input when the Machine reads it:
|
|
185
|
+
* `HostInjectedInput & RunInput`. Do not put it in the `input` schema that a run starts with. No
|
|
186
|
+
* caller sends it. jr2 writes it over any value that a caller sends.
|
|
187
|
+
*
|
|
188
|
+
* Only the root Machine of a run gets it. A Machine that another Machine invokes gets the input
|
|
189
|
+
* that its parent gives it. A `workspace()` passes its input to its body, so the body of a root
|
|
190
|
+
* `workspace()` gets it too: `Workspaced<HostInjectedInput & RunInput, "app">`.
|
|
191
|
+
*
|
|
192
|
+
* The `spec` of a root `workspace()` also gets it, and the type of its input shows it:
|
|
193
|
+
* ``spec: ({ input }) => ({ branch: `fix/${input.instanceId}` })``.
|
|
194
|
+
*
|
|
195
|
+
* It is not the run id. Use it, for example, to give each run a branch of its own.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* import { z } from "zod";
|
|
200
|
+
* import { jr2Setup, type HostInjectedInput } from "@jr2/orchestrator";
|
|
201
|
+
*
|
|
202
|
+
* const runInput = z.object({ ticket: z.string() });
|
|
203
|
+
* type Input = HostInjectedInput & z.infer<typeof runInput>;
|
|
87
204
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
205
|
+
* export const machine = jr2Setup({
|
|
206
|
+
* types: {} as { input: Input; context: Input; output: { run: string; ticket: string } },
|
|
207
|
+
* }).createMachine({
|
|
208
|
+
* id: "ticket",
|
|
209
|
+
* input: runInput,
|
|
210
|
+
* context: ({ input }) => input,
|
|
211
|
+
* initial: "done",
|
|
212
|
+
* states: { done: { type: "final" } },
|
|
213
|
+
* output: ({ context }) => ({ run: context.instanceId, ticket: context.ticket }),
|
|
214
|
+
* });
|
|
215
|
+
* ```
|
|
91
216
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* sits, so the one contract that must reason about this — `workspace()`'s body guard — takes the
|
|
95
|
-
* permissive answer and counts these keys as PROVIDED, which lets a body declare the field
|
|
96
|
-
* honestly. Counting them as provided (rather than subtracting them from what the body demands)
|
|
97
|
-
* is what keeps their TYPE checked: `instanceId` is a string, and a body asking for anything else
|
|
98
|
-
* is still rejected.
|
|
217
|
+
* @see docs/guides/write-a-workflow.md
|
|
218
|
+
* @see docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md
|
|
99
219
|
*/
|
|
100
220
|
export type HostInjectedInput = { instanceId: string };
|