@jr2/orchestrator 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/console/components/app.ts +2 -0
- package/console/components/fleet.ts +14 -1
- package/console/components/machine-pane.ts +41 -2
- package/console/main.ts +13 -1
- package/console/store.ts +102 -6
- package/console/style.css +21 -0
- package/docs/agents-block.md +5 -0
- package/docs/concepts/agents-and-turns.md +478 -0
- package/docs/concepts/architecture.md +243 -0
- package/docs/concepts/charts.md +146 -0
- package/docs/concepts/composition.md +357 -0
- package/docs/concepts/converge.md +257 -0
- package/docs/concepts/durable-runs.md +189 -0
- package/docs/concepts/effects.md +658 -0
- package/docs/concepts/gates.md +208 -0
- package/docs/concepts/machines-and-workflows.md +298 -0
- package/docs/concepts/pools-and-sources.md +230 -0
- package/docs/concepts/secrets-and-trust.md +306 -0
- package/docs/concepts/the-menu.md +381 -0
- package/docs/concepts/workspaces.md +756 -0
- package/docs/get-started/a-model-decides.md +563 -0
- package/docs/get-started/an-agent-in-a-sandbox.md +383 -0
- package/docs/get-started/connect-a-model.md +248 -0
- package/docs/get-started/create-an-instance.md +194 -0
- package/docs/get-started/have-your-assistant-write-a-machine.md +993 -0
- package/docs/get-started/install.md +130 -0
- package/docs/guides/add-tools-to-the-sandbox.md +308 -0
- package/docs/guides/answer-a-gate.md +304 -0
- package/docs/guides/bind-open-parts.md +224 -0
- package/docs/guides/cancel-a-run.md +146 -0
- package/docs/guides/clean-up.md +206 -0
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +361 -0
- package/docs/guides/give-the-cluster-git-credentials.md +222 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
- package/docs/guides/let-an-agent-write-a-chart.md +230 -0
- package/docs/guides/look-inside-a-sandbox.md +177 -0
- package/docs/guides/mirror-the-kit-images.md +176 -0
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +318 -0
- package/docs/guides/open-the-console.md +91 -0
- package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
- package/docs/guides/push-and-sign-a-branch.md +532 -0
- package/docs/guides/run-a-chart-by-name.md +270 -0
- package/docs/guides/run-a-command-or-call-a-url.md +587 -0
- package/docs/guides/run-one-worker-per-item.md +322 -0
- package/docs/guides/set-an-agents-access-and-dials.md +420 -0
- package/docs/guides/size-a-workspace.md +217 -0
- package/docs/guides/start-a-run-from-a-run.md +237 -0
- package/docs/guides/start-a-run.md +207 -0
- package/docs/guides/upgrade-the-kit-version.md +240 -0
- package/docs/guides/use-a-hosted-provider.md +214 -0
- package/docs/guides/use-a-local-model.md +313 -0
- package/docs/guides/watch-a-run.md +349 -0
- package/docs/guides/write-a-workflow.md +318 -0
- package/docs/index.md +44 -0
- package/docs/llms.txt +237 -0
- package/docs/reference/authoring-surface/agent.md +151 -0
- package/docs/reference/authoring-surface/answer.md +156 -0
- package/docs/reference/authoring-surface/arg.md +82 -0
- package/docs/reference/authoring-surface/basic.md +62 -0
- package/docs/reference/authoring-surface/bearer.md +53 -0
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/chartschema.md +23 -0
- package/docs/reference/authoring-surface/chartschemafor.md +48 -0
- package/docs/reference/authoring-surface/command.md +206 -0
- package/docs/reference/authoring-surface/customize.md +64 -0
- package/docs/reference/authoring-surface/defineconfig.md +41 -0
- package/docs/reference/authoring-surface/defineevent.md +114 -0
- package/docs/reference/authoring-surface/dispatch.md +126 -0
- package/docs/reference/authoring-surface/fail.md +96 -0
- package/docs/reference/authoring-surface/file.md +142 -0
- package/docs/reference/authoring-surface/frompick.md +277 -0
- package/docs/reference/authoring-surface/http.md +259 -0
- package/docs/reference/authoring-surface/isfailure.md +58 -0
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +237 -0
- package/docs/reference/authoring-surface/open.md +105 -0
- package/docs/reference/authoring-surface/openbody.md +86 -0
- package/docs/reference/authoring-surface/paramsof.md +143 -0
- package/docs/reference/authoring-surface/pool.md +186 -0
- package/docs/reference/authoring-surface/push.md +130 -0
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/secret.md +139 -0
- package/docs/reference/authoring-surface/sentby.md +83 -0
- package/docs/reference/authoring-surface/source.md +51 -0
- package/docs/reference/authoring-surface/text.md +62 -0
- package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
- package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
- package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
- package/docs/reference/authoring-surface/type-agentslots.md +15 -0
- package/docs/reference/authoring-surface/type-answeraction.md +12 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-arg.md +19 -0
- package/docs/reference/authoring-surface/type-argnames.md +12 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +61 -0
- package/docs/reference/authoring-surface/type-bindingof.md +12 -0
- package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
- package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
- package/docs/reference/authoring-surface/type-ceilings.md +41 -0
- package/docs/reference/authoring-surface/type-chainlink.md +19 -0
- package/docs/reference/authoring-surface/type-chartaction.md +46 -0
- package/docs/reference/authoring-surface/type-chartagent.md +28 -0
- package/docs/reference/authoring-surface/type-chartevent.md +20 -0
- package/docs/reference/authoring-surface/type-chartguard.md +20 -0
- package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
- package/docs/reference/authoring-surface/type-chartjson.md +14 -0
- package/docs/reference/authoring-surface/type-chartstate.md +46 -0
- package/docs/reference/authoring-surface/type-charttext.md +40 -0
- package/docs/reference/authoring-surface/type-charttransition.md +22 -0
- package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
- package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
- package/docs/reference/authoring-surface/type-childat.md +12 -0
- package/docs/reference/authoring-surface/type-childslots.md +12 -0
- package/docs/reference/authoring-surface/type-commandenv.md +15 -0
- package/docs/reference/authoring-surface/type-commandinput.md +20 -0
- package/docs/reference/authoring-surface/type-commandoptions.md +56 -0
- package/docs/reference/authoring-surface/type-commandoutput.md +122 -0
- package/docs/reference/authoring-surface/type-customize.md +111 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
- package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchoutput.md +72 -0
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-duration.md +15 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
- package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
- package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
- package/docs/reference/authoring-surface/type-eventdef.md +45 -0
- package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
- package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
- package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
- package/docs/reference/authoring-surface/type-failslot.md +14 -0
- package/docs/reference/authoring-surface/type-failure.md +19 -0
- package/docs/reference/authoring-surface/type-failureinput.md +20 -0
- package/docs/reference/authoring-surface/type-filebody.md +37 -0
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-frominput.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +56 -0
- package/docs/reference/authoring-surface/type-httpdefinition.md +104 -0
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpinput.md +12 -0
- package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
- package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +95 -0
- package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
- package/docs/reference/authoring-surface/type-jr2config.md +126 -0
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +38 -0
- package/docs/reference/authoring-surface/type-logicat.md +12 -0
- package/docs/reference/authoring-surface/type-markedif.md +12 -0
- package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
- package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
- package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
- package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +22 -0
- package/docs/reference/authoring-surface/type-openeffect.md +12 -0
- package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
- package/docs/reference/authoring-surface/type-openof.md +12 -0
- package/docs/reference/authoring-surface/type-opensecret.md +23 -0
- package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +26 -0
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +60 -0
- package/docs/reference/authoring-surface/type-pushinput.md +34 -0
- package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
- package/docs/reference/authoring-surface/type-pushoutput.md +149 -0
- package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
- package/docs/reference/authoring-surface/type-quantity.md +14 -0
- package/docs/reference/authoring-surface/type-reached.md +12 -0
- package/docs/reference/authoring-surface/type-reposlot.md +70 -0
- package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +84 -0
- package/docs/reference/authoring-surface/type-secret.md +25 -0
- package/docs/reference/authoring-surface/type-sender.md +19 -0
- package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
- package/docs/reference/authoring-surface/type-size.md +85 -0
- package/docs/reference/authoring-surface/type-slothandles.md +24 -0
- package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
- package/docs/reference/authoring-surface/type-syncstate.md +12 -0
- package/docs/reference/authoring-surface/type-text.md +19 -0
- package/docs/reference/authoring-surface/type-textpart.md +24 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
- package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
- package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
- package/docs/reference/authoring-surface/type-unknown.md +21 -0
- package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
- package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
- package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
- package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
- package/docs/reference/authoring-surface/type-workspaced.md +115 -0
- package/docs/reference/authoring-surface/type-workspacehandles.md +22 -0
- package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
- package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
- package/docs/reference/authoring-surface/type-workspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +287 -0
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +217 -0
- package/docs/reference/cli.md +433 -0
- package/docs/reference/config.md +765 -0
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +114 -0
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +117 -0
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +130 -0
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +240 -0
- package/docs/reference/decisions/0006-agent-control-surface.md +50 -0
- package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
- package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
- package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +175 -0
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +271 -0
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +126 -0
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +71 -0
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +287 -0
- package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +101 -0
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +175 -0
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +436 -0
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
- package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +245 -0
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +90 -0
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +193 -0
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +126 -0
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
- package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
- package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
- package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
- package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
- package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
- package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
- package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
- package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
- package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
- package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
- package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
- package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +238 -0
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +86 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +123 -0
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
- package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +108 -0
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +427 -0
- package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
- package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +239 -0
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +82 -0
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +552 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +458 -0
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +475 -0
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +334 -0
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
- package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions.md +86 -0
- package/docs/reference/glossary.md +687 -0
- package/docs/reference/http-api.md +1386 -0
- package/docs/reference/machines.md +189 -0
- package/docs/reference/mechanism-events.md +72 -0
- package/docs/why.md +207 -0
- package/package.json +5 -3
- package/src/actor.ts +694 -106
- package/src/agent.ts +114 -38
- package/src/ambient.ts +3 -4
- package/src/assembly-checks.ts +189 -7
- package/src/cause.ts +234 -0
- package/src/chart.ts +345 -46
- package/src/charts.ts +43 -8
- package/src/config.ts +440 -178
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +325 -99
- package/src/customize.ts +199 -49
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect-checks.ts +50 -3
- package/src/effect.ts +641 -128
- package/src/effector-wire.ts +165 -13
- package/src/fail.ts +222 -0
- package/src/harness-client.ts +587 -50
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +125 -16
- package/src/holds.ts +3 -0
- package/src/http-effect.ts +556 -58
- package/src/http.ts +480 -30
- package/src/index.ts +74 -82
- package/src/instance.ts +68 -8
- package/src/internal.ts +85 -0
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +24 -7
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +42 -58
- package/src/names.ts +19 -6
- package/src/notices.ts +483 -41
- package/src/open-body.ts +242 -40
- package/src/open-effect.ts +81 -8
- package/src/open.ts +90 -6
- package/src/parts.ts +213 -30
- package/src/party.ts +101 -2
- package/src/pick.ts +497 -11
- package/src/pool.ts +282 -50
- package/src/registration.ts +133 -12
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +22 -3
- package/src/run-host.ts +665 -45
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +457 -71
- package/src/sandbox-kube.ts +66 -17
- package/src/secret-placement.ts +59 -17
- package/src/server.ts +3 -0
- package/src/setup.ts +325 -62
- package/src/size.ts +121 -9
- package/src/snapshot-store.ts +54 -10
- package/src/stub-harness.ts +44 -16
- package/src/timers.ts +14 -1
- package/src/tokens.ts +107 -32
- package/src/vocabulary.ts +88 -14
- package/src/wire.ts +100 -26
- package/src/workspace.ts +771 -238
- package/src/worktree.ts +249 -0
package/src/custodian.lua
CHANGED
|
@@ -10,15 +10,18 @@
|
|
|
10
10
|
--
|
|
11
11
|
-- Four roles, one per route family, read from the route's metadata:
|
|
12
12
|
--
|
|
13
|
-
-- control
|
|
13
|
+
-- control toward the Orchestrator, plain HTTP: the agent routes on the control socket, and
|
|
14
|
+
-- the ask on the loopback fetch listener (ADR-0076). Each listener has only its own
|
|
15
|
+
-- routes, so a route of the other one gets the deny route's 404
|
|
14
16
|
-- egress the CONNECT listener: dial guard, then tunnel or hand to an intercept listener
|
|
15
17
|
-- intercept one bound host, TLS already ended: swap the Stand-in, strip the rest
|
|
16
18
|
-- health the kubelet's probe
|
|
17
19
|
|
|
18
20
|
local VALUES = "/etc/jr2/custodian/values/"
|
|
19
21
|
|
|
20
|
-
-- The value of one held secret, read once. One trailing line end is taken off,
|
|
21
|
-
-- `kubectl create secret --from-file` writes one; anything else a header cannot carry stops
|
|
22
|
+
-- The value of one held secret, or its Stand-in, read once. One trailing line end is taken off,
|
|
23
|
+
-- because `kubectl create secret --from-file` writes one; anything else a header cannot carry stops
|
|
24
|
+
-- us. The line names the file, never what it holds.
|
|
22
25
|
local function load(name)
|
|
23
26
|
local f = io.open(VALUES .. name, "rb")
|
|
24
27
|
if f == nil then
|
|
@@ -36,13 +39,20 @@ local function load(name)
|
|
|
36
39
|
return v
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
-- Every Stand-in is secret but the fetch Stand-in (ADR-0059, ADR-0076): the pod's maker minted it,
|
|
43
|
+
-- and it is a value too, read from `<NAME>.standin` beside the secret's own. The one public Stand-in
|
|
44
|
+
-- rides the table (`standIn`), because `jr2-upload-pack` holds it as a constant.
|
|
39
45
|
local STAND_IN = {}
|
|
40
46
|
local VALUE = {}
|
|
41
47
|
for _, s in ipairs(HELD.secrets) do
|
|
42
|
-
STAND_IN[s.name] =
|
|
48
|
+
STAND_IN[s.name] = s.standIn or load(s.name .. ".standin")
|
|
43
49
|
VALUE[s.name] = load(s.name)
|
|
44
50
|
end
|
|
45
51
|
|
|
52
|
+
-- The slot the Harness states on a control request (ADR-0076). It goes to the Orchestrator on a
|
|
53
|
+
-- control route, unchanged, and to no other host.
|
|
54
|
+
local SLOT_HEADER = "x-jr2-slot"
|
|
55
|
+
|
|
46
56
|
-- The headers every bound target treats as a credential (`CREDENTIAL_HEADERS`, held-secrets.ts).
|
|
47
57
|
local CREDENTIAL = {}
|
|
48
58
|
for _, h in ipairs(HELD.credential) do CREDENTIAL[h] = true end
|
|
@@ -258,7 +268,7 @@ local function intercept(handle, headers, target)
|
|
|
258
268
|
if path:lower():find("%%2[ef]") then
|
|
259
269
|
return refuse(handle, 400, "path", "an encoded / or . in the path")
|
|
260
270
|
end
|
|
261
|
-
for _, h in ipairs({ "te", "trailer", "keep-alive", "proxy-connection" }) do headers:remove(h) end
|
|
271
|
+
for _, h in ipairs({ "te", "trailer", "keep-alive", "proxy-connection", SLOT_HEADER }) do headers:remove(h) end
|
|
262
272
|
return hold(handle, headers, HELD.targets[target], target, path)
|
|
263
273
|
end
|
|
264
274
|
|
|
@@ -273,6 +283,8 @@ local function control(handle, headers, route)
|
|
|
273
283
|
'{"error":"this pod serves no Sandbox, so it mounts no Repo to fetch (ADR-0053)"}')
|
|
274
284
|
end
|
|
275
285
|
headers:replace(":path", "/sandboxes/" .. sandbox .. "/fetch")
|
|
286
|
+
-- The fetch acts for the Sandbox, not for an Agent: no slot rides it.
|
|
287
|
+
headers:remove(SLOT_HEADER)
|
|
276
288
|
end
|
|
277
289
|
return hold(handle, headers, HELD.control, "orchestrator", headers:get(":path"))
|
|
278
290
|
end
|
package/src/custodian.ts
CHANGED
|
@@ -21,14 +21,17 @@
|
|
|
21
21
|
// so the two pod shapes cannot drift
|
|
22
22
|
//
|
|
23
23
|
// The Custodian is a separate container from the Harness, and that is the point: the Agent
|
|
24
|
-
// executes code in the Harness container and nothing here (ADR-0005). What it holds — the
|
|
25
|
-
//
|
|
26
|
-
// `shareProcessNamespace` stays off, so the Agent cannot read its env, its files or its
|
|
24
|
+
// executes code in the Harness container and nothing here (ADR-0005). What it holds — the pod's
|
|
25
|
+
// control and fetch tokens and every held secret its pod claims — is mounted into this container
|
|
26
|
+
// alone, and `shareProcessNamespace` stays off, so the Agent cannot read its env, its files or its
|
|
27
|
+
// `/proc`. The Harness container gets the Stand-ins, every one but the public fetch Stand-in
|
|
28
|
+
// secret, as files under `/run/jr2/` that no child the Harness confines can read (ADR-0076).
|
|
27
29
|
|
|
28
30
|
import { readFileSync } from "node:fs";
|
|
29
31
|
import type { HarnessEnvVar } from "./config.ts";
|
|
30
32
|
import type { ResourceRequirements } from "./size.ts";
|
|
31
33
|
import {
|
|
34
|
+
CONTROL_TOKEN_NAME,
|
|
32
35
|
CREDENTIAL_HEADERS,
|
|
33
36
|
SANDBOX_TOKEN_NAME,
|
|
34
37
|
standIn,
|
|
@@ -37,10 +40,12 @@ import {
|
|
|
37
40
|
} from "./held-secrets.ts";
|
|
38
41
|
import {
|
|
39
42
|
CA_CONFIGMAP,
|
|
40
|
-
|
|
43
|
+
CUSTODIAN_CONTROL_SOCKET,
|
|
44
|
+
CUSTODIAN_FETCH_PORT,
|
|
41
45
|
CUSTODIAN_HEALTH_PORT,
|
|
42
46
|
CUSTODIAN_PORT,
|
|
43
47
|
CUSTODIAN_PROGRAM_KEY,
|
|
48
|
+
CUSTODIAN_SOCKET_DIR,
|
|
44
49
|
HELD_CONFIGMAP,
|
|
45
50
|
HELD_TLS_SECRET,
|
|
46
51
|
heldSecretsSecret,
|
|
@@ -96,8 +101,60 @@ export const CUSTODIAN_ARGS: readonly string[] = [
|
|
|
96
101
|
"warning",
|
|
97
102
|
];
|
|
98
103
|
|
|
99
|
-
/** The address
|
|
100
|
-
|
|
104
|
+
/** The address of the Custodian's fetch listener: the ask alone (ADR-0053), which
|
|
105
|
+
* `jr2-upload-pack` makes from the domain. The Harness container gets it as `JR2_CUSTODIAN_URL`,
|
|
106
|
+
* the one `JR2_*` name that a confined process keeps (ADR-0076). The agent routes are not here. */
|
|
107
|
+
export const CUSTODIAN_URL = `http://127.0.0.1:${CUSTODIAN_FETCH_PORT}`;
|
|
108
|
+
|
|
109
|
+
/** The Harness env that names the Custodian's control socket ({@link CUSTODIAN_CONTROL_SOCKET}):
|
|
110
|
+
* where the Harness sends each control request — the surface read, a pick, a delivery's poll and a
|
|
111
|
+
* notice's acknowledgment (ADR-0076). A path, never a value; the domain's env does not keep it. */
|
|
112
|
+
export const CUSTODIAN_SOCKET_ENV = "JR2_CUSTODIAN_SOCKET";
|
|
113
|
+
/** The volume of the control socket's folder ({@link CUSTODIAN_SOCKET_DIR}): an emptyDir in the
|
|
114
|
+
* Harness container and the Custodian container, and in no other container of the pod. */
|
|
115
|
+
export const CUSTODIAN_SOCKET_VOLUME = "custodian-socket";
|
|
116
|
+
|
|
117
|
+
/** Where the Harness container holds its Stand-ins (ADR-0059, ADR-0076): a read-only mount that no
|
|
118
|
+
* child the Harness confines can read — the confiner denies all of `/run/jr2`. Never env. */
|
|
119
|
+
export const HARNESS_CREDENTIALS = "/run/jr2";
|
|
120
|
+
/** The secret control Stand-in, as the Harness reads it once at start (C1). */
|
|
121
|
+
export const CONTROL_STANDIN_FILE = `${HARNESS_CREDENTIALS}/control/standin`;
|
|
122
|
+
/** The directory of the held secrets' Stand-ins, one file per name: `<dir>/<NAME>`. */
|
|
123
|
+
export const STANDINS_DIR = `${HARNESS_CREDENTIALS}/standins`;
|
|
124
|
+
/** The Harness env that names the control Stand-in's file; its default is {@link CONTROL_STANDIN_FILE}. */
|
|
125
|
+
export const CONTROL_STANDIN_FILE_ENV = "JR2_CONTROL_STANDIN_FILE";
|
|
126
|
+
/** The Harness env that names the held secrets the `workspace()` grants a confined child (its
|
|
127
|
+
* `secrets` list, ADR-0076), comma-separated: names, never a value. Absent → none is granted. */
|
|
128
|
+
export const GRANTED_SECRETS_ENV = "JR2_GRANTED_SECRETS";
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The keys of a pod's credentials Secret (a Sandbox's `<sandbox>-token`): the fetch token, the
|
|
132
|
+
* control token, the secret control Stand-in, and one secret Stand-in per held secret
|
|
133
|
+
* ({@link heldStandInKey}). The pod's maker mints them all. The tokens are projected into the
|
|
134
|
+
* Custodian alone; each Stand-in into the Custodian, for its swap, and the Harness container.
|
|
135
|
+
*/
|
|
136
|
+
export const CREDENTIAL_KEYS = {
|
|
137
|
+
fetchToken: "token",
|
|
138
|
+
controlToken: "control-token",
|
|
139
|
+
controlStandIn: "control-standin",
|
|
140
|
+
} as const;
|
|
141
|
+
|
|
142
|
+
/** The key of one held secret's secret Stand-in in its pod's credentials Secret. */
|
|
143
|
+
export function heldStandInKey(name: string): string {
|
|
144
|
+
return `standin-${name}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The file of a Stand-in among the Custodian's values: beside its value, `<NAME>.standin`. A held
|
|
148
|
+
* name is an env var's, so it holds no `.` and never meets another value's file. */
|
|
149
|
+
export function standInValueFile(name: string): string {
|
|
150
|
+
return `${name}.standin`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The path prefixes each control credential may ride, after the control listener's rewrite
|
|
154
|
+
* (ADR-0076, C3): the control token on the agent routes alone, the fetch token on the fetch route
|
|
155
|
+
* alone. The program's path guard refuses either one elsewhere, before any swap. */
|
|
156
|
+
export const CONTROL_PATHS = "/agents/";
|
|
157
|
+
export const FETCH_PATHS = "/sandboxes/";
|
|
101
158
|
|
|
102
159
|
/** Where the Orchestrator's Service is, from inside a Harness pod. */
|
|
103
160
|
export type OrchestratorAddress = { host: string; port: number };
|
|
@@ -217,30 +274,72 @@ function meta(fields: Record<string, string>): Json {
|
|
|
217
274
|
return { filter_metadata: { [LUA_FILTER]: fields } };
|
|
218
275
|
}
|
|
219
276
|
|
|
220
|
-
/**
|
|
221
|
-
*
|
|
222
|
-
|
|
277
|
+
/** Envoy's flags for a request it did not send to the target (ADR-0059): the connection failed, no
|
|
278
|
+
* host was healthy, the DNS lookup failed. */
|
|
279
|
+
export const NOT_SENT_FLAGS = ["UF", "UH", "DF"] as const;
|
|
280
|
+
|
|
281
|
+
/** Envoy's flags for a request that may have reached the target before the stream ended (ADR-0059):
|
|
282
|
+
* the target closed the connection or reset the stream, Envoy reset it, a protocol error on either
|
|
283
|
+
* side (a body framing error can come after the headers went). `UMSDR` (the maximum stream
|
|
284
|
+
* duration) ends a stream that was sent. `URX` (the retry limit) comes with the last attempt's own
|
|
285
|
+
* flag (`UF,URX`, `UC,URX`), so alone it says nothing of the send; the mapper for
|
|
286
|
+
* {@link NOT_SENT_FLAGS} comes first, so `UF,URX` still reads "could not be reached". These routes
|
|
287
|
+
* set no retry policy and no `max_stream_duration`, so Envoy sets neither flag on them; they keep
|
|
288
|
+
* the union equal to ORCHESTRATOR_UNREACHABLE's. */
|
|
289
|
+
export const MAY_HAVE_SENT_FLAGS = ["UC", "DPE", "UMSDR", "URX", "UPE", "UR", "LR"] as const;
|
|
290
|
+
|
|
291
|
+
/** One of the Custodian's own 502s for a target that gave no answer (§5.4): the target, what
|
|
292
|
+
* jr2 knows about the request, and Envoy's reason — never a retry without verification. */
|
|
293
|
+
function noAnswer(target: string, flags: readonly string[], words: string): Json {
|
|
223
294
|
return {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
text_format_source: {
|
|
230
|
-
inline_string: `jr2 custodian: ${target} could not be reached: %RESPONSE_CODE_DETAILS% %UPSTREAM_TRANSPORT_FAILURE_REASON%\n`,
|
|
231
|
-
},
|
|
232
|
-
},
|
|
295
|
+
filter: { response_flag_filter: { flags: [...flags] } },
|
|
296
|
+
status_code: 502,
|
|
297
|
+
body_format_override: {
|
|
298
|
+
text_format_source: {
|
|
299
|
+
inline_string: `jr2 custodian: ${target} ${words}: %RESPONSE_CODE_DETAILS% %UPSTREAM_TRANSPORT_FAILURE_REASON%\n`,
|
|
233
300
|
},
|
|
234
|
-
|
|
301
|
+
},
|
|
235
302
|
};
|
|
236
303
|
}
|
|
237
304
|
|
|
305
|
+
const NOT_REACHED = "could not be reached";
|
|
306
|
+
const MAY_HAVE_REACHED = "did not answer; the request may have reached it";
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* An intercepted target that gave no answer (ADR-0059). Two mappers, because the words must be
|
|
310
|
+
* true: after a `NOT_SENT_FLAGS` failure the target did not get the request, and after a
|
|
311
|
+
* `MAY_HAVE_SENT_FLAGS` one it may have it — so an Agent that sends a POST again can write twice,
|
|
312
|
+
* and the text must not tell it the first one went nowhere. One reply can carry several flags,
|
|
313
|
+
* and Envoy uses the first mapper that matches, in order: the `NOT_SENT_FLAGS` mapper is first, so
|
|
314
|
+
* a reply with `UF`, `UH` or `DF` reads "could not be reached" whatever else it carries. Without
|
|
315
|
+
* the mappers, Envoy answers each of these flags itself (503 for most, 502 for `UPE`) with a body
|
|
316
|
+
* that names no target, which a caller can read as the target's answer. No caller reads the flags;
|
|
317
|
+
* the Orchestrator route's go to the Harness, which tells "never reached" from "not known"
|
|
318
|
+
* (ADR-0002).
|
|
319
|
+
*/
|
|
320
|
+
function unreachable(target: string): Json {
|
|
321
|
+
return {
|
|
322
|
+
mappers: [noAnswer(target, NOT_SENT_FLAGS, NOT_REACHED), noAnswer(target, MAY_HAVE_SENT_FLAGS, MAY_HAVE_REACHED)],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The tunnel's 502 (ADR-0059): every flag, one text. Envoy answers a `CONNECT` with 200 only after
|
|
327
|
+
* its TCP connection to the target is up, and no byte of the Agent's request goes before that 200.
|
|
328
|
+
* So a 502 on the tunnel always comes before the request reached the target, whatever its flag. */
|
|
329
|
+
function tunnelUnreachable(target: string): Json {
|
|
330
|
+
return { mappers: [noAnswer(target, [...NOT_SENT_FLAGS, ...MAY_HAVE_SENT_FLAGS], NOT_REACHED)] };
|
|
331
|
+
}
|
|
332
|
+
|
|
238
333
|
/** The Orchestrator unreachable, said so the Harness can tell it from an answer and retry the
|
|
239
334
|
* surface read the way ADR-0042 asks — the one route on this pod that retries. */
|
|
240
335
|
const ORCHESTRATOR_UNREACHABLE: Json = {
|
|
241
336
|
mappers: [
|
|
242
337
|
{
|
|
243
|
-
|
|
338
|
+
// Envoy's own answers for a stream it reset (`UPE`, `UR`, `LR`: 503, or 502 for `UPE`) are said
|
|
339
|
+
// here too, so the Harness reads them as "not known" and not as a stopping Orchestrator's 503
|
|
340
|
+
// (ADR-0002). One text for
|
|
341
|
+
// both lists: the Harness reads the flags, not the words.
|
|
342
|
+
filter: { response_flag_filter: { flags: [...NOT_SENT_FLAGS, ...MAY_HAVE_SENT_FLAGS] } },
|
|
244
343
|
status_code: 502,
|
|
245
344
|
headers_to_add: [
|
|
246
345
|
{ header: { key: "x-jr2-custodian", value: "unreachable" }, append_action: "OVERWRITE_IF_EXISTS_OR_ADD" },
|
|
@@ -333,7 +432,7 @@ function egressReplies(): Json {
|
|
|
333
432
|
},
|
|
334
433
|
},
|
|
335
434
|
},
|
|
336
|
-
...(
|
|
435
|
+
...(tunnelUnreachable("%REQ(:AUTHORITY)%").mappers as Json[]),
|
|
337
436
|
],
|
|
338
437
|
};
|
|
339
438
|
}
|
|
@@ -367,9 +466,14 @@ function isIpLiteral(host: string): boolean {
|
|
|
367
466
|
* The Custodian's static bootstrap (ADR-0059): Envoy's whole configuration, rendered once per
|
|
368
467
|
* converge by `jr2 up`, identical for every Harness pod of the Instance.
|
|
369
468
|
*
|
|
370
|
-
* control
|
|
371
|
-
* four routes
|
|
372
|
-
*
|
|
469
|
+
* control /run/jr2/custodian/control.sock
|
|
470
|
+
* the four agent routes — the Menu, a pick, a delivery's poll and a
|
|
471
|
+
* notice's acknowledgment — plain HTTP, to the Orchestrator's Service.
|
|
472
|
+
* They swap the control Stand-in for the control token. A Unix socket
|
|
473
|
+
* in a folder that no confined process can write, so no process in
|
|
474
|
+
* the domain can take this listener's place (ADR-0013, ADR-0076)
|
|
475
|
+
* fetch 127.0.0.1:8081 the ask alone, plain HTTP, to the same Service: it swaps the public
|
|
476
|
+
* fetch Stand-in for the fetch token (ADR-0053, ADR-0076)
|
|
373
477
|
* health 0.0.0.0:15021 GET /healthz, for the kubelet, which cannot reach loopback
|
|
374
478
|
* egress 127.0.0.1:15001 CONNECT only — present when a held secret exists; a bound target
|
|
375
479
|
* goes to its intercept listener, every other one is tunneled
|
|
@@ -386,55 +490,76 @@ export function custodianBootstrap(manifest: HeldManifest, orchestrator: Orchest
|
|
|
386
490
|
metadata: meta({ role: "control", route: name }),
|
|
387
491
|
});
|
|
388
492
|
|
|
493
|
+
// One listener toward the Orchestrator, with its routes and the deny route after them: a route
|
|
494
|
+
// of the other listener gets the deny route's 404 here.
|
|
495
|
+
const toOrchestrator = (name: string, address: Json, routes: Json[]): Json => ({
|
|
496
|
+
name,
|
|
497
|
+
address,
|
|
498
|
+
filter_chains: [
|
|
499
|
+
{
|
|
500
|
+
filters: [
|
|
501
|
+
hcm(name, {
|
|
502
|
+
target: orchestratorTarget,
|
|
503
|
+
idleSeconds: 3600,
|
|
504
|
+
lua: true,
|
|
505
|
+
local: ORCHESTRATOR_UNREACHABLE,
|
|
506
|
+
routes: {
|
|
507
|
+
virtual_hosts: [
|
|
508
|
+
{
|
|
509
|
+
name: "orchestrator",
|
|
510
|
+
domains: ["*"],
|
|
511
|
+
routes: [
|
|
512
|
+
...routes,
|
|
513
|
+
{
|
|
514
|
+
name: "deny",
|
|
515
|
+
match: { prefix: "/" },
|
|
516
|
+
direct_response: { status: 404 },
|
|
517
|
+
metadata: meta({ role: "deny" }),
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
},
|
|
523
|
+
}),
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
});
|
|
528
|
+
|
|
389
529
|
const listeners: Json[] = [
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
{
|
|
424
|
-
name: "deny",
|
|
425
|
-
match: { prefix: "/" },
|
|
426
|
-
direct_response: { status: 404 },
|
|
427
|
-
metadata: meta({ role: "deny" }),
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
},
|
|
431
|
-
],
|
|
432
|
-
},
|
|
433
|
-
}),
|
|
434
|
-
],
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
},
|
|
530
|
+
// The agent routes, on the control socket (ADR-0076). A Unix socket, not a loopback port: a
|
|
531
|
+
// confined process can bind a free loopback port while the Custodian restarts, and it then gets
|
|
532
|
+
// the control Stand-in on the Harness's next request. It cannot make a socket under `/run/jr2`.
|
|
533
|
+
// Envoy removes a socket that a stopped Envoy left, before it binds. Mode 0666, because the
|
|
534
|
+
// Harness runs as a uid that is not Envoy's: the folder, not the mode, keeps the domain out.
|
|
535
|
+
toOrchestrator("control", { pipe: { path: CUSTODIAN_CONTROL_SOCKET, mode: 0o666 } }, [
|
|
536
|
+
// A read retries any reset. A pick retries only a reset before it was sent: one the
|
|
537
|
+
// Orchestrator may have read is not sent twice.
|
|
538
|
+
route("surface", "GET", "/agents/[^/]+/surface", {
|
|
539
|
+
retry_policy: { retry_on: "reset,connect-failure", num_retries: 2 },
|
|
540
|
+
}),
|
|
541
|
+
route("events", "POST", "/agents/[^/]+/events", {
|
|
542
|
+
retry_policy: { retry_on: "reset-before-request,connect-failure", num_retries: 2 },
|
|
543
|
+
}),
|
|
544
|
+
// An outstanding pick's answer, long-polled (ADR-0024): a read, so any reset is retried — an
|
|
545
|
+
// Orchestrator restart mid-poll costs a retry, not the pick.
|
|
546
|
+
route("deliveries", "GET", "/agents/[^/]+/deliveries/[^/]+", {
|
|
547
|
+
retry_policy: { retry_on: "reset,connect-failure", num_retries: 2 },
|
|
548
|
+
}),
|
|
549
|
+
// The acknowledgment of the notices a step boundary appended (ADR-0062, ADR-0076): marking
|
|
550
|
+
// them heard twice changes nothing, so any reset is retried.
|
|
551
|
+
route("notices", "POST", "/agents/[^/]+/notices", {
|
|
552
|
+
retry_policy: { retry_on: "reset,connect-failure", num_retries: 2 },
|
|
553
|
+
}),
|
|
554
|
+
]),
|
|
555
|
+
// The ask (ADR-0053), on loopback: `jr2-upload-pack` runs in the domain, and the fetch Stand-in
|
|
556
|
+
// is public, so a process that takes this port gets nothing secret. Like a pick, it retries only
|
|
557
|
+
// a reset before it was sent.
|
|
558
|
+
toOrchestrator("fetch", { socket_address: { address: "127.0.0.1", port_value: CUSTODIAN_FETCH_PORT } }, [
|
|
559
|
+
route("fetch", "POST", "/fetch", {
|
|
560
|
+
retry_policy: { retry_on: "reset-before-request,connect-failure", num_retries: 2 },
|
|
561
|
+
}),
|
|
562
|
+
]),
|
|
438
563
|
{
|
|
439
564
|
name: "health",
|
|
440
565
|
address: { socket_address: { address: "0.0.0.0", port_value: CUSTODIAN_HEALTH_PORT } },
|
|
@@ -750,24 +875,40 @@ function luaList(values: readonly string[]): string {
|
|
|
750
875
|
/**
|
|
751
876
|
* The script the Custodian runs: a generated `HELD` table, then `dofile` of the fixed program
|
|
752
877
|
* (`CUSTODIAN_PROGRAM`, mounted beside it from `jr2-held`). The table names every secret this pod
|
|
753
|
-
* holds — the
|
|
754
|
-
* and the targets each is bound to. It never holds a value
|
|
755
|
-
* mounted files at start.
|
|
878
|
+
* holds — the control credentials first, then the manifest's — with the headers and paths each may
|
|
879
|
+
* ride and the targets each is bound to. It never holds a value, and never a secret Stand-in: the
|
|
880
|
+
* program reads both from the mounted files at start (ADR-0059).
|
|
881
|
+
*
|
|
882
|
+
* The control credentials (ADR-0076, C3): the control token, path-restricted to the agent routes,
|
|
883
|
+
* whose secret Stand-in the program reads from its values; and, on a pod that serves a Sandbox
|
|
884
|
+
* (`fetch`), the fetch token, path-restricted to the fetch route, whose Stand-in is the one public
|
|
885
|
+
* one and so rides the table. The Instance Harness and a check pod serve no fetch, and hold the
|
|
886
|
+
* control token alone.
|
|
756
887
|
*/
|
|
757
|
-
export function custodianScript(manifest: HeldManifest): string {
|
|
888
|
+
export function custodianScript(manifest: HeldManifest, opts: { fetch: boolean }): string {
|
|
889
|
+
const control = [
|
|
890
|
+
`{ name = ${lua(CONTROL_TOKEN_NAME)}, headers = { "authorization" }, paths = ${luaList([CONTROL_PATHS])} }`,
|
|
891
|
+
...(opts.fetch
|
|
892
|
+
? [
|
|
893
|
+
`{ name = ${lua(SANDBOX_TOKEN_NAME)}, headers = { "authorization" }, standIn = ${lua(standIn(SANDBOX_TOKEN_NAME))}, ` +
|
|
894
|
+
`paths = ${luaList([FETCH_PATHS])} }`,
|
|
895
|
+
]
|
|
896
|
+
: []),
|
|
897
|
+
];
|
|
758
898
|
const lines = [
|
|
759
899
|
"-- Generated by `jr2 up` from its pod's held manifest (ADR-0059). Names, headers, paths, targets; never a value.",
|
|
760
900
|
`HELD = { secrets = {}, targets = {}, hosts = ${targetsOf(manifest).length} }`,
|
|
761
901
|
`HELD.credential = ${luaList(CREDENTIAL_HEADERS)}`,
|
|
762
|
-
`HELD.secrets[1] =
|
|
763
|
-
|
|
902
|
+
...control.map((entry, i) => `HELD.secrets[${i + 1}] = ${entry}`),
|
|
903
|
+
`HELD.control = { ${control.map((_, i) => `HELD.secrets[${i + 1}]`).join(", ")} }`,
|
|
764
904
|
];
|
|
905
|
+
const offset = control.length + 1;
|
|
765
906
|
manifest.secrets.forEach((s, i) => {
|
|
766
907
|
const paths = s.paths ? `, paths = ${luaList(s.paths)}` : "";
|
|
767
|
-
lines.push(`HELD.secrets[${i +
|
|
908
|
+
lines.push(`HELD.secrets[${i + offset}] = { name = ${lua(s.name)}, headers = ${luaList(s.headers)}${paths} }`);
|
|
768
909
|
});
|
|
769
910
|
for (const t of targetsOf(manifest)) {
|
|
770
|
-
const refs = t.secrets.map((s) => `HELD.secrets[${manifest.secrets.indexOf(s) +
|
|
911
|
+
const refs = t.secrets.map((s) => `HELD.secrets[${manifest.secrets.indexOf(s) + offset}]`);
|
|
771
912
|
lines.push(`HELD.targets[${lua(t.key)}] = { ${refs.join(", ")} }`);
|
|
772
913
|
}
|
|
773
914
|
// `dofile`, not a copy: Envoy's Lua opens the standard libraries (the program already reads its
|
|
@@ -788,12 +929,32 @@ export type CustodianCompositionOptions = {
|
|
|
788
929
|
* makes the pod: the Orchestrator at provision (`sandboxCustodianConfigMap`, owned by the CR),
|
|
789
930
|
* `jr2 up` for the Instance Harness (`INSTANCE_CUSTODIAN_CONFIGMAP`). */
|
|
790
931
|
config: string;
|
|
791
|
-
/**
|
|
792
|
-
*
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
*
|
|
932
|
+
/**
|
|
933
|
+
* The pod's credentials Secret (ADR-0076): its control token, its secret control Stand-in, its
|
|
934
|
+
* fetch token (a Sandbox alone) and one secret Stand-in per held secret, under
|
|
935
|
+
* {@link CREDENTIAL_KEYS} and {@link heldStandInKey} — a key may be renamed here. The pod's maker
|
|
936
|
+
* mints them: the Orchestrator a Sandbox's (`<sandbox>-token`), `jr2 up` the Instance Harness's
|
|
937
|
+
* and a check pod's. The tokens are mounted into the Custodian alone (ADR-0013); each Stand-in
|
|
938
|
+
* into the Custodian and the Harness container, as files.
|
|
939
|
+
*/
|
|
940
|
+
credentials: {
|
|
941
|
+
secret: string;
|
|
942
|
+
keys?: Partial<Record<keyof typeof CREDENTIAL_KEYS, string>>;
|
|
943
|
+
};
|
|
944
|
+
/** This pod's Sandbox — the name the ask is addressed by (ADR-0053), and the reason the pod holds
|
|
945
|
+
* a fetch token. Absent on the Instance Harness and a check pod, which mount no Repo and answer an
|
|
946
|
+
* ask by saying so. */
|
|
796
947
|
sandbox?: string;
|
|
948
|
+
/** The held secrets the `workspace()` grants its confined children (its `secrets` list, ADR-0076):
|
|
949
|
+
* the Harness puts their Stand-ins, and only theirs, in a child's env. Names the manifest does not
|
|
950
|
+
* hold are left out. Absent → none. */
|
|
951
|
+
granted?: readonly string[];
|
|
952
|
+
/** The names of the Instance objects that this Custodian reads in place of the defaults. The
|
|
953
|
+
* defaults are the Instance's own: `pod`'s literal values (`heldSecretsSecret(pod)`),
|
|
954
|
+
* `jr2-held-tls`, `jr2-ca`, and the program in `jr2-held`.
|
|
955
|
+
* The check pod (ADR-0019) reads its own copies, so a check pod writes nothing that a running pod
|
|
956
|
+
* of the Instance reads. */
|
|
957
|
+
reads?: { literals: string; tls: string; trust: string; program: string };
|
|
797
958
|
/** The Instance ships a `harness.caBundle` (ADR-0020). */
|
|
798
959
|
caBundle: boolean;
|
|
799
960
|
/** The Custodian container's resources (ADR-0060) — the placement's to choose: a Sandbox's fixed
|
|
@@ -802,15 +963,24 @@ export type CustodianCompositionOptions = {
|
|
|
802
963
|
};
|
|
803
964
|
|
|
804
965
|
type Container = Record<string, unknown>;
|
|
966
|
+
|
|
967
|
+
/** The Harness container's credentials volume: its Stand-ins, mounted at {@link HARNESS_CREDENTIALS}. */
|
|
968
|
+
export const HARNESS_CREDENTIALS_VOLUME = "harness-credentials";
|
|
805
969
|
type Volume = Record<string, unknown>;
|
|
806
970
|
type VolumeMount = { name: string; mountPath: string; readOnly?: boolean };
|
|
807
971
|
|
|
808
972
|
/** What one Harness pod gains from the Custodian. */
|
|
809
973
|
export type CustodianComposition = {
|
|
810
|
-
/** The Harness container's env — in `env`, after `harness.env`, before the wire's gate (§5.1)
|
|
811
|
-
*
|
|
812
|
-
*
|
|
974
|
+
/** The Harness container's env — in `env`, after `harness.env`, before the wire's gate (§5.1):
|
|
975
|
+
* the Custodian's fetch address and control socket, the control Stand-in's file, the granted
|
|
976
|
+
* names, the proxy and the
|
|
977
|
+
* trust. Never a Stand-in: those are files ({@link HARNESS_CREDENTIALS}), so no `envFrom` key is
|
|
978
|
+
* hidden by one, and `jr2 up` refuses a held name that is a key of an `envFrom` Secret (R14). */
|
|
813
979
|
harnessEnv: HarnessEnvVar[];
|
|
980
|
+
/** The Harness container's mounts: the trust bundles, its Stand-ins at {@link HARNESS_CREDENTIALS},
|
|
981
|
+
* and the control socket's folder below it. Each is read-only. A placement mounts the
|
|
982
|
+
* conversations' volume at `/run/jr2/conversations`, below the Stand-ins too: the kubelet makes
|
|
983
|
+
* the mount point of a volume that is below a projected volume (ADR-0076). */
|
|
814
984
|
harnessMounts: VolumeMount[];
|
|
815
985
|
custodianContainer: Container;
|
|
816
986
|
volumes: Volume[];
|
|
@@ -837,16 +1007,29 @@ const CUSTODIAN_SECURITY = {
|
|
|
837
1007
|
export function custodianComposition(manifest: HeldManifest, opts: CustodianCompositionOptions): CustodianComposition {
|
|
838
1008
|
const held = manifest.secrets.length > 0;
|
|
839
1009
|
const literal = manifest.secrets.filter((s) => s.source.kind === "literal").map((s) => s.name);
|
|
1010
|
+
const {
|
|
1011
|
+
literals = heldSecretsSecret(opts.pod),
|
|
1012
|
+
tls = HELD_TLS_SECRET,
|
|
1013
|
+
trust: trustMap = CA_CONFIGMAP,
|
|
1014
|
+
program = HELD_CONFIGMAP,
|
|
1015
|
+
} = opts.reads ?? {};
|
|
1016
|
+
|
|
1017
|
+
const keys = { ...CREDENTIAL_KEYS, ...opts.credentials.keys };
|
|
1018
|
+
const fetch = opts.sandbox !== undefined;
|
|
1019
|
+
const heldNames = new Set(manifest.secrets.map((s) => s.name));
|
|
1020
|
+
const granted = (opts.granted ?? []).filter((n) => heldNames.has(n));
|
|
840
1021
|
|
|
1022
|
+
// No Stand-in rides env (ADR-0059, ADR-0076): each is a file under `/run/jr2/`, below.
|
|
841
1023
|
const harnessEnv: HarnessEnvVar[] = [
|
|
842
1024
|
{ name: "JR2_CUSTODIAN_URL", value: CUSTODIAN_URL },
|
|
843
|
-
{ name:
|
|
1025
|
+
{ name: CUSTODIAN_SOCKET_ENV, value: CUSTODIAN_CONTROL_SOCKET },
|
|
1026
|
+
{ name: CONTROL_STANDIN_FILE_ENV, value: CONTROL_STANDIN_FILE },
|
|
1027
|
+
...(granted.length ? [{ name: GRANTED_SECRETS_ENV, value: granted.join(",") }] : []),
|
|
844
1028
|
];
|
|
845
1029
|
if (held) {
|
|
846
1030
|
const proxy = `http://127.0.0.1:${CUSTODIAN_PORT}`;
|
|
847
1031
|
const noProxy = "localhost,127.0.0.1,::1";
|
|
848
1032
|
harnessEnv.push(
|
|
849
|
-
...manifest.secrets.map((s) => ({ name: s.name, value: standIn(s.name) })),
|
|
850
1033
|
{ name: "HTTPS_PROXY", value: proxy },
|
|
851
1034
|
{ name: "https_proxy", value: proxy },
|
|
852
1035
|
{ name: "NO_PROXY", value: noProxy },
|
|
@@ -869,10 +1052,21 @@ export function custodianComposition(manifest: HeldManifest, opts: CustodianComp
|
|
|
869
1052
|
// Group-readable, for the pod's fsGroup: the Custodian runs as Envoy's own uid.
|
|
870
1053
|
defaultMode: 0o440,
|
|
871
1054
|
sources: [
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1055
|
+
// The control credentials (ADR-0076): the control token beside its secret Stand-in, and
|
|
1056
|
+
// on a Sandbox the fetch token, whose Stand-in is public and rides the script. Then each
|
|
1057
|
+
// held secret's secret Stand-in, beside the value the Custodian swaps it for.
|
|
1058
|
+
{
|
|
1059
|
+
secret: {
|
|
1060
|
+
name: opts.credentials.secret,
|
|
1061
|
+
items: [
|
|
1062
|
+
{ key: keys.controlToken, path: CONTROL_TOKEN_NAME },
|
|
1063
|
+
{ key: keys.controlStandIn, path: standInValueFile(CONTROL_TOKEN_NAME) },
|
|
1064
|
+
...(fetch ? [{ key: keys.fetchToken, path: SANDBOX_TOKEN_NAME }] : []),
|
|
1065
|
+
...manifest.secrets.map((s) => ({ key: heldStandInKey(s.name), path: standInValueFile(s.name) })),
|
|
1066
|
+
],
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
...(literal.length ? [{ secret: { name: literals, items: literal.map((n) => ({ key: n, path: n })) } }] : []),
|
|
876
1070
|
...manifest.secrets.flatMap((s) =>
|
|
877
1071
|
s.source.kind === "secret"
|
|
878
1072
|
? [{ secret: { name: s.source.secret, items: [{ key: s.source.key, path: s.name }] } }]
|
|
@@ -896,20 +1090,45 @@ export function custodianComposition(manifest: HeldManifest, opts: CustodianComp
|
|
|
896
1090
|
],
|
|
897
1091
|
},
|
|
898
1092
|
},
|
|
899
|
-
{ configMap: { name:
|
|
1093
|
+
{ configMap: { name: program, items: [{ key: CUSTODIAN_PROGRAM_KEY, path: CUSTODIAN_PROGRAM_KEY }] } },
|
|
900
1094
|
],
|
|
901
1095
|
},
|
|
902
1096
|
},
|
|
903
1097
|
...(held
|
|
904
1098
|
? [
|
|
905
|
-
{ name: "custodian-tls", secret: { secretName:
|
|
1099
|
+
{ name: "custodian-tls", secret: { secretName: tls, defaultMode: 0o440 } },
|
|
906
1100
|
{
|
|
907
1101
|
name: "custodian-trust",
|
|
908
|
-
configMap: { name:
|
|
1102
|
+
configMap: { name: trustMap, items: [{ key: "upstream.crt", path: "upstream.crt" }] },
|
|
909
1103
|
},
|
|
910
1104
|
]
|
|
911
1105
|
: []),
|
|
912
|
-
...(trust ? [{ name: "ca", configMap: { name:
|
|
1106
|
+
...(trust ? [{ name: "ca", configMap: { name: trustMap } }] : []),
|
|
1107
|
+
// The control socket's folder (ADR-0076): the Custodian makes the socket, and the Harness
|
|
1108
|
+
// connects to it. It goes with the pod, and it outlives a restart of either container, so the
|
|
1109
|
+
// socket's path never changes.
|
|
1110
|
+
{ name: CUSTODIAN_SOCKET_VOLUME, emptyDir: {} },
|
|
1111
|
+
// The Harness container's Stand-ins (ADR-0076): the secret control Stand-in and each held
|
|
1112
|
+
// secret's, as files under `/run/jr2/`, which the confiner denies to every child the Harness
|
|
1113
|
+
// starts. Group-readable for the pod's fsGroup — the Harness runs as no fixed uid, and the
|
|
1114
|
+
// kubelet writes the files as root — so 0440 is the least mode the Harness can read.
|
|
1115
|
+
{
|
|
1116
|
+
name: HARNESS_CREDENTIALS_VOLUME,
|
|
1117
|
+
projected: {
|
|
1118
|
+
defaultMode: 0o440,
|
|
1119
|
+
sources: [
|
|
1120
|
+
{
|
|
1121
|
+
secret: {
|
|
1122
|
+
name: opts.credentials.secret,
|
|
1123
|
+
items: [
|
|
1124
|
+
{ key: keys.controlStandIn, path: "control/standin" },
|
|
1125
|
+
...manifest.secrets.map((s) => ({ key: heldStandInKey(s.name), path: `standins/${s.name}` })),
|
|
1126
|
+
],
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
],
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
913
1132
|
];
|
|
914
1133
|
|
|
915
1134
|
const custodianContainer: Container = {
|
|
@@ -922,6 +1141,8 @@ export function custodianComposition(manifest: HeldManifest, opts: CustodianComp
|
|
|
922
1141
|
volumeMounts: [
|
|
923
1142
|
{ name: "custodian-values", mountPath: CUSTODIAN_VALUES, readOnly: true },
|
|
924
1143
|
{ name: "custodian-config", mountPath: CUSTODIAN_CONFIG, readOnly: true },
|
|
1144
|
+
// The one folder that the Custodian writes: where it makes its control socket.
|
|
1145
|
+
{ name: CUSTODIAN_SOCKET_VOLUME, mountPath: CUSTODIAN_SOCKET_DIR },
|
|
925
1146
|
...(held
|
|
926
1147
|
? [
|
|
927
1148
|
{ name: "custodian-tls", mountPath: CUSTODIAN_TLS, readOnly: true },
|
|
@@ -944,7 +1165,12 @@ export function custodianComposition(manifest: HeldManifest, opts: CustodianComp
|
|
|
944
1165
|
|
|
945
1166
|
return {
|
|
946
1167
|
harnessEnv,
|
|
947
|
-
harnessMounts:
|
|
1168
|
+
harnessMounts: [
|
|
1169
|
+
...(trust ? [{ name: "ca", mountPath: CA_MOUNT, readOnly: true }] : []),
|
|
1170
|
+
{ name: HARNESS_CREDENTIALS_VOLUME, mountPath: HARNESS_CREDENTIALS, readOnly: true },
|
|
1171
|
+
// Read-only: the Harness connects to the socket, and nothing in its container writes here.
|
|
1172
|
+
{ name: CUSTODIAN_SOCKET_VOLUME, mountPath: CUSTODIAN_SOCKET_DIR, readOnly: true },
|
|
1173
|
+
],
|
|
948
1174
|
custodianContainer,
|
|
949
1175
|
volumes,
|
|
950
1176
|
};
|