@jr2/orchestrator 0.4.1 → 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/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 +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 +5 -3
- package/src/actor.ts +541 -79
- package/src/agent.ts +114 -38
- package/src/ambient.ts +2 -1
- package/src/assembly-checks.ts +189 -7
- package/src/cause.ts +234 -0
- package/src/chart.ts +169 -37
- package/src/charts.ts +34 -6
- package/src/config.ts +430 -178
- package/src/custodian.ts +74 -22
- package/src/customize.ts +188 -46
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect.ts +341 -44
- package/src/effector-wire.ts +160 -11
- package/src/fail.ts +209 -0
- package/src/harness-client.ts +541 -47
- package/src/http-effect.ts +495 -53
- package/src/http.ts +427 -16
- package/src/index.ts +60 -82
- package/src/instance.ts +68 -8
- package/src/internal.ts +84 -0
- package/src/machine-doc.ts +21 -3
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +18 -58
- package/src/notices.ts +66 -13
- package/src/open-body.ts +159 -23
- package/src/open-effect.ts +81 -8
- package/src/open.ts +59 -6
- package/src/parts.ts +184 -13
- package/src/party.ts +68 -2
- package/src/pick.ts +463 -11
- package/src/pool.ts +247 -36
- package/src/registration.ts +83 -4
- package/src/repos.ts +2 -1
- package/src/run-host.ts +565 -31
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +273 -38
- package/src/sandbox-kube.ts +3 -1
- package/src/secret-placement.ts +52 -17
- package/src/server.ts +3 -0
- package/src/setup.ts +239 -56
- 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/vocabulary.ts +69 -14
- package/src/wire.ts +90 -16
- package/src/workspace.ts +697 -175
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Image garbage collects by reachability
|
|
2
|
+
|
|
3
|
+
[ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md) made every tag a content address, which made garbage a
|
|
4
|
+
by-product of normal work: ten Dockerfile iterations leave ten full images, and the prune it added collects only some of
|
|
5
|
+
them — kind nodes only (the host daemon that built every image is never touched), at `jr2 down` only (the accumulation
|
|
6
|
+
comes from iterating _while up_, which end-of-life pruning never sees), and scoped by tag **grammar**, which is
|
|
7
|
+
ambiguous: `jr2-sandbox-<instance>-<name>` has no reserved delimiter, so instance `my` + image `extra-default` and
|
|
8
|
+
instance `my-extra` + image `default` collide on one repo, and deleting `images/<x>/` orphans that image's tags because
|
|
9
|
+
nothing derives their names any more. Each of those is a patch on the same wrong primitive: deciding ownership by
|
|
10
|
+
parsing names.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
- **Ownership is a label, not a naming convention.** Every image `jr2 up` builds is stamped at build time —
|
|
15
|
+
`jr2.dev/kind` (`instance` | `sandbox` | `kit`) plus `jr2.dev/instance=<name>` on instance-owned ones — via
|
|
16
|
+
`docker build --label`, never in a Dockerfile (the user's file keeps zero jr2 knowledge, ADR-0037; the committed kit
|
|
17
|
+
Dockerfiles stay plain). Labels ride the image config through `kind load` into containerd, so both sides of the
|
|
18
|
+
transport can read provenance back. **The sweep touches labeled images and nothing else.** Name grammar stops being
|
|
19
|
+
load-bearing: the `my`/`my-extra` collision class and the orphaned-tags consequence in ADR-0038 both dissolve, because
|
|
20
|
+
deletion no longer needs to reconstruct a name — the image says who built it.
|
|
21
|
+
- **Garbage is defined by reachability, not age or naming.** An image is needed iff a live root names it:
|
|
22
|
+
1. the `jr2-images` ConfigMap of **every** jr2 instance on the cluster (namespaces labeled `jr2.dev/instance`) — what
|
|
23
|
+
future Sandboxes will run;
|
|
24
|
+
2. every Sandbox CR's `spec.image` in those namespaces — a parked Workspace must survive a pod restart (`IfNotPresent`
|
|
25
|
+
cannot re-pull a local tag);
|
|
26
|
+
3. every pod's container images in those namespaces plus `jr2-system` — the orchestrator, Instance Harness, Custodian,
|
|
27
|
+
and operator actually running, mid-roll pods included, without naming Deployments one by one. The keep set is the
|
|
28
|
+
union, matched by whole ref after the one containerd normalization ADR-0038 already fixed (strip
|
|
29
|
+
`docker.io/library/`). A labeled image none of it names is garbage — user Sandbox Images and `jr2-*` kit images by
|
|
30
|
+
the same rule. **"Kit images are never pruned" dissolves into reachability**: a kit ref is kept because some
|
|
31
|
+
instance's map names it, and when the last instance leaves the cluster, kit images collect like everything else
|
|
32
|
+
instead of being permanent by fiat. A Pinned image — the Custodian's Envoy
|
|
33
|
+
([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)) — is never taken at all: jr2 did
|
|
34
|
+
not build it, so it carries no stamp, and what jr2 did not stamp, jr2 does not sweep. It is a root besides (the
|
|
35
|
+
map's `custodian` key, every Sandbox CR's sidecars, every Harness pod), so no sweep would take it from under a live
|
|
36
|
+
pod even if it were stamped.
|
|
37
|
+
- **`jr2 up` sweeps after a successful converge** — the moment the root set moves, which is where the iteration garbage
|
|
38
|
+
comes from. Nodes get a **one-generation grace**: refs named by the map this converge replaced stay one more round, so
|
|
39
|
+
the ConfigMap's kubelet propagation window cannot provision a just-swept ref. The **host daemon is swept
|
|
40
|
+
aggressively** (labeled ∧ not in the keep set ∪ this converge's refs): nothing runs from the host, its images are
|
|
41
|
+
scratch awaiting delivery, and BuildKit's cache — a separate store `docker rmi` does not touch — makes regenerating a
|
|
42
|
+
swept tag cost seconds. A failed converge sweeps nothing.
|
|
43
|
+
- **`jr2 down` sweeps host and nodes after the namespace delete.** This instance's roots are gone, so its images are
|
|
44
|
+
unreachable by construction; another instance's roots still protect everything it shares, kit refs included. The
|
|
45
|
+
operator's image stays a root through its running pod unless `--all` takes the operator too. The exact-prefix matching
|
|
46
|
+
this replaces (`prunePlan`, the derived `jr2-sandbox-<name>-<x>:` list) is deleted, not kept as a fallback.
|
|
47
|
+
- **Node policy is unchanged from ADR-0038's fix**: `crictl` cannot untag, so a node image id is removed (once) only
|
|
48
|
+
when **every** tag on it is unreachable; a mixed id is kept whole and reported. `docker rmi` on the host does untag,
|
|
49
|
+
so the host sweeps per tag with no id grouping. Both sides are delete-if-present, and a failed removal is reported and
|
|
50
|
+
skipped, never allowed to abandon the rest. The node **mechanics** are not unchanged: a `kind load`ed image is also
|
|
51
|
+
held under an `import-<date>@<digest>` ref CRI does not know, so `crictl rmi` alone exits 0 having reclaimed nothing —
|
|
52
|
+
removal reaches through to `ctr -n k8s.io images rm` for those names, and the report is confirmed by a re-list of the
|
|
53
|
+
node rather than by an exit code.
|
|
54
|
+
- **The sweep narrates bytes, not counts** — `swept 4 image(s) (2.1 GB)` — because disk is the quantity the user feels,
|
|
55
|
+
and a silent GC plus one visible number is the entire intended interface. **`jr2 gc [--dry-run]`** is the same sweep
|
|
56
|
+
run off-cycle: an escape hatch for "disk is full now", not a step in any workflow; `--dry-run` prints the plan and
|
|
57
|
+
removes nothing. It never needs confirmation, because by construction it removes only what jr2 built and nothing
|
|
58
|
+
running or provisionable names.
|
|
59
|
+
- **Registry-delivered copies are cache and sweep like everything else.** A pulled tag on a node (or the host's
|
|
60
|
+
just-pushed copy) is re-pullable from the registry, so unreachable copies go; the registry's own retention stays the
|
|
61
|
+
registry's business, as ADR-0038 drew the line.
|
|
62
|
+
|
|
63
|
+
## Considered options
|
|
64
|
+
|
|
65
|
+
- **Keep name-prefix pruning and extend it to the host** (the shape ADR-0038 grew fix by fix). Rejected: every fix
|
|
66
|
+
patched the same wrong primitive. Exact prefixes still collide (`my`+`extra-default` ≡ `my-extra`+`default`), deleting
|
|
67
|
+
an image folder still orphans tags, the kit exemption stays a fiat rule, and `up`-time iteration garbage stays
|
|
68
|
+
invisible. Reachability plus labels answers all four with one mechanism.
|
|
69
|
+
- **A kind-local registry** so images live in one place and GC becomes registry retention. Rejected for now: real infra
|
|
70
|
+
(a registry container, containerd config on every node) bought mostly to avoid a sweep the CLI can do in a few
|
|
71
|
+
subprocess calls; the two-store shape (host builds, nodes hold copies) is a kind artifact this ADR manages rather than
|
|
72
|
+
rebuilds. Revisitable if a real registry becomes the default transport anyway.
|
|
73
|
+
- **Age- or count-based GC** (`keep last N`, `older than 30d`). Rejected: wrong axis. Old is not unneeded (a parked
|
|
74
|
+
Workspace can hold a month-old ref alive) and new is not needed (a failed converge's tag is garbage at minutes old).
|
|
75
|
+
Reachability is the fact the others approximate.
|
|
76
|
+
- **Sweeping at `down` only** (the ADR-0038 status quo). Rejected: the accumulation the prune exists for is produced by
|
|
77
|
+
iterating while up, so the collector must run where the garbage is made — after each converge.
|
|
78
|
+
|
|
79
|
+
## Consequences
|
|
80
|
+
|
|
81
|
+
- **Images built before this ADR carry no labels and are invisible to the sweep** — deliberately, since sweeping
|
|
82
|
+
unlabeled images means guessing by name again. The pre-existing strays (`jr2-workspace-*`, `:local` kit tags, old e2e
|
|
83
|
+
iterations) are cleaned once by hand or with the cluster; `jr2 up --force` re-tags current images with labels.
|
|
84
|
+
- **The sweep reads cluster-wide** (namespaces labeled `jr2.dev/instance`, their ConfigMaps, Sandboxes, and pods), so
|
|
85
|
+
the CLI needs list access beyond its own namespace — true of the admin kubeconfig `jr2 up` already requires.
|
|
86
|
+
- **A converge to another cluster can take a host image this converge built and has not used yet** — a second checkout's
|
|
87
|
+
converge, or this checkout's — because the host keep set only sees the current context's roots. `jr2 up` answers it
|
|
88
|
+
itself: a ref it counted on, its own build or a host-held skip
|
|
89
|
+
([ADR-0041](0041-a-build-the-host-already-holds-is-not-spent-again.md)), that the daemon no longer holds at its use
|
|
90
|
+
(the `USER` read, the Effector image read, the `kind load` or the push) is built again from BuildKit's cache, in
|
|
91
|
+
seconds, and the use is retried, at most twice. A tag gone again after two rebuilds fails with docker's own error.
|
|
92
|
+
- **CRI's image list can outlive containerd's refs**, because a removal that goes through `ctr` does not resync the CRI
|
|
93
|
+
image store: `crictl images` keeps answering for ids whose refs and content are gone. So the node read is filtered
|
|
94
|
+
against `ctr -n k8s.io images ls` — containerd's refs decide what is present, CRI's list only mirrors them — and
|
|
95
|
+
without that filter every id the sweep took would come back on the next plan, forever. The residual rows are cosmetic
|
|
96
|
+
(nothing can run from them, nothing is reclaimed by taking them again) and clear on a containerd restart; jr2 ignores
|
|
97
|
+
them by construction rather than reporting them.
|
|
98
|
+
- **A converge in flight is not a root.** An image is needed iff a LIVE root names it — and a converge that has resolved
|
|
99
|
+
its refs but not yet applied them names them nowhere the cluster can see, so a sweep finishing in another namespace
|
|
100
|
+
can take a ref between its build and its delivery. On the host the rebuild above answers it; on the nodes nothing
|
|
101
|
+
does, and a ref taken after its `kind load` and before its map lands is an `ImagePullBackOff` at the next provision.
|
|
102
|
+
In the `@kind` tier under `--parallel` this is mostly out of reach: every scenario converges the same checkout, so
|
|
103
|
+
each worker's own `extraKeep` already holds the identical refs the others are building
|
|
104
|
+
([ADR-0041](0041-a-build-the-host-already-holds-is-not-spent-again.md)'s build skip narrows the window further — a
|
|
105
|
+
warm converge builds nothing). It bites two instances on one cluster, or one instance converged twice across a source
|
|
106
|
+
edit. Recorded as a known limit; the fix — a claim the converge publishes BEFORE it builds, making an in-flight ref a
|
|
107
|
+
root like any other — is not taken here.
|
|
108
|
+
- ADR-0038's `jr2 down` prune clause is **superseded by this ADR**; its `imagePullPolicy`/content-address reasoning
|
|
109
|
+
stands untouched, and ADR-0038's seal is what finally makes it true of the Instance's own artifact, its Assembly since
|
|
110
|
+
ADR-0069.
|
|
111
|
+
- **Docker label inheritance makes derived images sweepable.** An image built `FROM` a jr2-labeled base inherits the
|
|
112
|
+
`jr2.dev/*` labels through the image config, so the sweep treats it as jr2's own — observed live with a brought ref
|
|
113
|
+
layered on a built Sandbox Image, collected by the next `jr2 gc`. A truly foreign brought image carries no such labels
|
|
114
|
+
and is untouchable as designed; one derived from a jr2 build should clear them (`LABEL jr2.dev/kind=`) or accept being
|
|
115
|
+
swept when no live root names it.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# A build the host already holds is not spent again
|
|
2
|
+
|
|
3
|
+
[ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md) keyed the build skip on the cluster's record — the
|
|
4
|
+
`jr2.dev/images` annotation the previous converge stamped — deliberately: _"a claim about the CLUSTER'S RECORD, not the
|
|
5
|
+
node's disk"_, because when tags were mutable, an image being present said nothing about it being current. A fresh
|
|
6
|
+
namespace has no record, so it re-runs every `docker build` even when the host daemon holds every ref byte-for-byte —
|
|
7
|
+
pure waste on a real cluster whenever a namespace is recreated, and ~7.4s per scenario across the `@kind` tier. The
|
|
8
|
+
premise expired with ADR-0038's own seal: every tag is now an honest content address, so on the host that built it,
|
|
9
|
+
**present implies current** is true rather than aspirational — asking the daemon is no longer trusting a guess, it is
|
|
10
|
+
reading the fact the record only ever approximated.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
- **Two records, each answering the question it can vouch for.** The cluster's annotation answers for the _cluster_: a
|
|
15
|
+
ref it names was built and delivered by a converge that fully succeeded — skip the build **and** the delivery, exactly
|
|
16
|
+
as before. When the record is silent, the host daemon answers for the _host_: a labeled image whose tag equals the
|
|
17
|
+
resolved ref IS the build (the tag is a content address of the same inputs and platform set —
|
|
18
|
+
[ADR-0045](0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md); an address silent about platform
|
|
19
|
+
let a daemon-default flip make one tag name two artifacts) — skip the build, **run the delivery**. A multi-arch ref
|
|
20
|
+
never lands in the daemon (buildx pushes straight to the registry), so the host skip cannot answer for it and that
|
|
21
|
+
build is spent. Only when both records are silent is docker spent. `--force` overrides both, unchanged in meaning:
|
|
22
|
+
rebuild and redeliver regardless of what anything claims.
|
|
23
|
+
- **The disk answers the build question only, never the delivery one.** Delivery is already cheap-idempotent on both
|
|
24
|
+
transports — `kind load` skips a node holding the image id, and a push of layers the registry holds is a no-op per
|
|
25
|
+
layer — so the skip that would need _node_ or _registry_ observation buys seconds at the cost of a second stale-state
|
|
26
|
+
channel. The record keeps that job.
|
|
27
|
+
- **The skip proves the build, not the floor.** The ADR-0037 floor is proven where the seat is known — the `preflight`
|
|
28
|
+
init step at provision, in the user's own image — so a converge, skipped or not, holds no preflight obligation and the
|
|
29
|
+
disk path re-proves nothing beyond tag-equals-content.
|
|
30
|
+
- **One read, lazy, and advisory.** The daemon is asked once per converge at most (`hostImages()` — the same labeled
|
|
31
|
+
listing the sweep reads, ~13ms against the ~7.4s it saves), and only when some record check missed. A steady-state
|
|
32
|
+
converge still spends directory walks and no docker at all; ADR-0038's bullet stands. A listing that _fails_ reads as
|
|
33
|
+
"holds nothing": the check may only ever save a build, never fail a converge the build itself could survive — a truly
|
|
34
|
+
dead daemon fails at the build that follows, with docker's own error naming it.
|
|
35
|
+
- **An unlabeled image never answers.** The listing is label-filtered, so a hand-built image that happens to wear the
|
|
36
|
+
right name is invisible and the build proceeds — the safe direction, and the same rule the sweep lives by
|
|
37
|
+
([ADR-0039](0039-image-garbage-collects-by-reachability.md)): what jr2 did not stamp, jr2 does not trust or touch.
|
|
38
|
+
|
|
39
|
+
## Considered options
|
|
40
|
+
|
|
41
|
+
- **Replace the record with disk truth outright.** Rejected: the record answers a question disk cannot — what the
|
|
42
|
+
cluster was _delivered_ — and it is free (one kube read the converge already makes). Disk truth alone would re-run
|
|
43
|
+
every delivery every converge.
|
|
44
|
+
- **Node-disk truth too** (`nodeImages()` before each `kind load`). Rejected: it duplicates the check `kind load`
|
|
45
|
+
already performs, at a `crictl` round trip per node, to skip a step measured fast.
|
|
46
|
+
- **Registry-truth for the push path** (HEAD the manifest before pushing). Rejected for now: the push is idempotent, the
|
|
47
|
+
registry path has the record for steady state, and a registry API client is a new dependency for a case nothing
|
|
48
|
+
measured as slow.
|
|
49
|
+
|
|
50
|
+
## Consequences
|
|
51
|
+
|
|
52
|
+
- **The `@kind` tier's per-scenario converge stops paying the build tax.** A fresh namespace on a warm host skips all
|
|
53
|
+
five builds; the measured savings ride ADR-0010's numbers. It also narrows ADR-0039's sweeper-vs-builder window: a
|
|
54
|
+
converge that skips its builds holds nothing in flight for a sweep to take.
|
|
55
|
+
- **The ADR-0039 hole is not widened.** The vulnerable window — a ref present on the host that no live root names,
|
|
56
|
+
between the skip decision and delivery — is the same window that already existed between a _build_ and its delivery,
|
|
57
|
+
and it fails the same way (the delivery fails loudly; re-run). In the `@kind` tier the delivered refs are identical
|
|
58
|
+
across scenarios and ride every sweeper's `extraKeep`. The two-instance case stays ADR-0039's recorded limit, and the
|
|
59
|
+
claim-published-before-building fix stays untaken.
|
|
60
|
+
- **Recorded-but-absent is unchanged**: a record hit still skips everything, so a ref someone `docker rmi`'d _and_
|
|
61
|
+
removed from the nodes still surfaces as `ImagePullBackOff`, and `--force` is still the way back. The disk is
|
|
62
|
+
consulted only where the record was silent, never to audit it.
|
|
63
|
+
- ADR-0038's skip bullet is **amended by this ADR**, not superseded: the record remains the first and cheapest answer;
|
|
64
|
+
what changes is that its silence stops being read as "the build never happened".
|
package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Ready is not routable: the two calls a turn starts with re-ask a Service that has not answered
|
|
2
|
+
|
|
3
|
+
[ADR-0016](0016-agent-turn-mechanics-are-internal.md) sorted an Agent run's failures into classes and said which ones
|
|
4
|
+
jr2 absorbs. Its **infra** class is stated as already-covered: _"provider-stream retry lives inside the turn in the
|
|
5
|
+
Harness, and `wait` reconnects indefinitely from the offset ledger; a dead Harness surfaces as a fault"_. That sentence
|
|
6
|
+
walks the turn from its stream backwards and never reaches the two steps in front of it. **Both legs that run BEFORE a
|
|
7
|
+
model is ever asked were single, unretried `fetch`es** — the Orchestrator's admission POST to the Workspace Harness, and
|
|
8
|
+
the Harness's `GET /agents/:iid/surface` that builds the turn's Menu — and either one rejecting settles the turn, which
|
|
9
|
+
`actor.ts` reports as the terminal `agent.fault`. One dropped packet, one lost turn.
|
|
10
|
+
|
|
11
|
+
The gap is not theoretical and not rare, because both of those calls dial a **Service that has only just started
|
|
12
|
+
mattering**. The admission is the FIRST HTTP request jr2 ever sends over a Workspace's Service: the wrapper waits on the
|
|
13
|
+
Sandbox CR's `phase: Ready`, which the operator computes from the POD's readiness, and the attach then reaches into the
|
|
14
|
+
pod through the API server (`kubectl exec`) — neither proves the Service dialable. The surface read dials the
|
|
15
|
+
Orchestrator's Service, which is between EndpointSlices every time the Orchestrator restarts, and restarting is ordinary
|
|
16
|
+
([ADR-0007](0007-durable-machine-state.md)'s restore is built on it). Kubernetes programs the EndpointSlice behind a
|
|
17
|
+
ClusterIP **after** the pod passes its probe, and until it does, kube-proxy REJECTs. **Ready is a statement about a pod;
|
|
18
|
+
routable is a statement about a Service, and jr2 was reading the first as the second — at both ends of the same turn.**
|
|
19
|
+
|
|
20
|
+
Measured: this is what cost the `@kind` tier its parallel default ([ADR-0010](0010-bdd-acceptance-tests.md)). Roughly
|
|
21
|
+
one parallel run in three lost a scenario whose first turn never reached the scripted model, and the symptom was read
|
|
22
|
+
for a long time as "a slow first turn". It is not. It is a lost turn, and the tier's failure dump caught both legs
|
|
23
|
+
losing it:
|
|
24
|
+
|
|
25
|
+
- **The admission**, three times. The pod's Harness answered `?view=history` with **404 — no conversation** (POST is
|
|
26
|
+
what creates one), the scripted model had received **zero** streaming requests, and the run settled
|
|
27
|
+
`{"outcome":"faulted","reason":"fetch failed"}`. In the same window the run-narrative echo — un-retried, and the only
|
|
28
|
+
other thing that dials that Service — logged the same `fetch failed` against the same address. Nothing had reached the
|
|
29
|
+
pod at all.
|
|
30
|
+
- **The surface read**, once the admission was fixed and the tier ran again:
|
|
31
|
+
`submission settled failed: … {"error":"fetch failed"}` — the surface read's failure, in the scenario that RESTARTS
|
|
32
|
+
the Orchestrator. Which is the same defect one hop over: the Menu leg looks pod-local, and it is not — its hop leaves
|
|
33
|
+
the pod for the Orchestrator's Service.
|
|
34
|
+
|
|
35
|
+
Parallelism causes neither; it multiplies the number of Services crossing into service at once, so it samples both
|
|
36
|
+
windows more often. That is why the flake was degree-independent and serial runs never saw it.
|
|
37
|
+
|
|
38
|
+
## Decision
|
|
39
|
+
|
|
40
|
+
- **A never-delivered admission is re-sent.** `send` re-POSTs while the request demonstrably never left the host,
|
|
41
|
+
backing off on the same capped ladder `wait` uses. This is `wait`'s own rule applied one step earlier and for the same
|
|
42
|
+
reason: a request that could not connect is not a Harness that refused the prompt.
|
|
43
|
+
- **Only never-delivered.** Admission is accept-and-queue
|
|
44
|
+
([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)): a POST the Harness received but
|
|
45
|
+
could not answer has already queued a Submission, so re-sending it would run the turn **twice** — a worse failure than
|
|
46
|
+
losing it. The test is therefore the narrow structural question "did any byte reach the wire" (the errno's
|
|
47
|
+
`syscall`/`code`, read down the `cause` chain), never the broad "does this look transient". A reset connection is not
|
|
48
|
+
retried.
|
|
49
|
+
- **One answer is re-sent too: a draining Harness's 503.** A Harness that received SIGTERM refuses an admission before
|
|
50
|
+
it reads or queues anything, and closes the connection
|
|
51
|
+
([ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md)), so that 503 is as undelivered as a refused
|
|
52
|
+
connection — nothing ran, and the re-send dials the Service anew. It rides the same ladder and window. Every other
|
|
53
|
+
status is the Harness's word about this admission, and stays a fault.
|
|
54
|
+
- **The window is bounded, where `wait`'s is not.** `wait` may reconnect forever because its Submission is already
|
|
55
|
+
admitted, so the lease owns the reporting ([ADR-0021](0021-workspace-continuity-is-a-lease-that-answers-back.md)'s
|
|
56
|
+
`workspace.lost`). Nothing is admitted yet at this seat, so there is no turn for a lease to be about: an address that
|
|
57
|
+
never answers is a fault this call must name itself, and it names the address it kept trying. **90 seconds — a
|
|
58
|
+
measurement, not a derivation.** The mechanism argues for far less: kube-proxy programs the EndpointSlice in well
|
|
59
|
+
under a second, and CoreDNS's 30s negative TTL is the only other obvious floor. Reasoning from the mechanism alone is
|
|
60
|
+
how this ADR first arrived at 60s, and 60s clears neither number the field has actually recorded —
|
|
61
|
+
[kubernetes#88986](https://github.com/kubernetes/kubernetes/issues/88986) measures **63s** on bare metal (a
|
|
62
|
+
SYN-retransmit ladder, 1-2-4-8-16-32, so those packets were DROPPED rather than refused), and
|
|
63
|
+
[kind#2280](https://github.com/kubernetes-sigs/kind/issues/2280) measures **up to 77s** with the EndpointSlices
|
|
64
|
+
already populated. The bound covers both. Its cost is paid only by an address that is genuinely wrong, and that one
|
|
65
|
+
faults with the address in hand either way.
|
|
66
|
+
- **The ladder is jittered, because the callers arrive together.** Sandboxes that converge together cross the same
|
|
67
|
+
window together, and every Custodian in the cluster dials the same Orchestrator Service — so an un-jittered ladder
|
|
68
|
+
does not merely fail to help, it organizes the callers into a herd that retries and misses in lockstep. Each sleep is
|
|
69
|
+
drawn from the TOP HALF of its rung (equal jitter). The random half decorrelates them; the half that stays a floor is
|
|
70
|
+
the deliberate part, and it is what full jitter would give away — it is what still holds four clients off a Service
|
|
71
|
+
that is genuinely down.
|
|
72
|
+
- **A transport failure says what it was.** `fetch` reports every one of them as the same three words, and
|
|
73
|
+
`reason: "fetch failed"` on a run is a diagnosis of nothing — the errno one level down (`connect ECONNREFUSED …`,
|
|
74
|
+
`getaddrinfo ENOTFOUND …`) is the whole answer. Both the admission fault and the echo's log line now carry it.
|
|
75
|
+
- **The Harness re-asks an unanswered surface read, on ANY transport failure.** An unanswered read is the Harness's own
|
|
76
|
+
transport failure, or the Custodian's word that the Orchestrator never answered it (a 502 it marks as its own,
|
|
77
|
+
[ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)), or no answer within 10 seconds. The
|
|
78
|
+
Custodian itself retries only a connection reset: any reset under the read, and under a pick or an ask only one before
|
|
79
|
+
the request was sent. It also drops a pooled connection after 4 seconds idle, before Node's 5-second keep-alive closes
|
|
80
|
+
it. A GET is idempotent, so the narrow never-delivered test the admission needs buys nothing here — there is no second
|
|
81
|
+
turn to accidentally start, so the rule is simply "an answered request is an answer". A 503 is the Orchestrator's word
|
|
82
|
+
that it is stopping, so it is re-asked too ([ADR-0026](0026-a-turn-that-is-over-is-never-prompted.md)). A 404 stays
|
|
83
|
+
ADR-0026's turn-is-over and a 403 stays a scope refusal: both are answers, and neither is re-asked.
|
|
84
|
+
- **`deliver` does not retry, and that is not an oversight.** A failed pick reaches the model as a tool error it can act
|
|
85
|
+
on — pick again, or pick differently — so the turn survives one; and a POST that may have been delivered must never be
|
|
86
|
+
re-sent, because a duplicate pick is a duplicate transition. The asymmetry between the Menu's two calls is the same
|
|
87
|
+
one between `send` and `wait`: what may be re-asked is decided by what a second copy would do, never by how transient
|
|
88
|
+
the failure looks.
|
|
89
|
+
- **The fault class is unchanged.** Exhausting either window is still ADR-0016's single terminal `agent.fault`, and
|
|
90
|
+
where it routes is still workflow policy. What changes is that it now means "this endpoint never answered", not "the
|
|
91
|
+
EndpointSlice was a few hundred milliseconds behind".
|
|
92
|
+
|
|
93
|
+
## What this does not fix
|
|
94
|
+
|
|
95
|
+
**An unrouted `agent.fault` is not this ADR's.** This ADR does not change what a workflow with no `agent.fault` route
|
|
96
|
+
does with a lost Turn. [ADR-0074](0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md) decides it:
|
|
97
|
+
the walk refuses a state that invokes an Agent with no route on it or above it, and a fault that no transition takes at
|
|
98
|
+
runtime ends the run `error`, with a `fault` that names the event, the state and the reason.
|
|
99
|
+
|
|
100
|
+
## Consequences
|
|
101
|
+
|
|
102
|
+
- The `@kind` tier's wired default becomes `--parallel 4` (ADR-0010 carries the measurements).
|
|
103
|
+
- A genuinely wrong address at either seat now costs up to its window before it faults, instead of failing at once. That
|
|
104
|
+
is the intended trade: the address is right and the Service is late far more often than the address is wrong, and the
|
|
105
|
+
fault message names the address either way.
|
|
106
|
+
- **The rule generalizes, and the next seat to need it should say so out loud.** Two hops needed this and were found one
|
|
107
|
+
at a time, each by a failure. The claim is not "retry the network"; it is that jr2 dials Services at the exact moment
|
|
108
|
+
they come into existence, and that a call which never got an answer has learned nothing worth acting on. Any new
|
|
109
|
+
jr2→Service call made at a lifecycle edge starts with that exposure until someone decides otherwise.
|
|
110
|
+
- The retry is invisible to the workflow by construction (ADR-0016's absorption principle): no new event, no new budget
|
|
111
|
+
on the authoring surface, no bookkeeping in Machine context. **But absorbed is not the same as unmeasured, and the
|
|
112
|
+
difference is load-bearing.** Absorption alone would leave the tier benefiting from a window it cannot see: if
|
|
113
|
+
routability got twice as slow tomorrow, all 88 scenarios would still pass, a little slower, in silence, until the day
|
|
114
|
+
the window finally closed and the suite went red as a fresh mystery. Being unseeable is precisely how this class
|
|
115
|
+
survived three sessions, so each seat emits ONE line when a retry actually cost something —
|
|
116
|
+
`jr2.routability seat=… attempts=… ms=… last=… url=…` — and the `@kind` tier holds a budget against it, judged once
|
|
117
|
+
per worker in `AfterAll`. A log line, not a run-feed event: the authoring surface is untouched, so nothing above
|
|
118
|
+
contradicts itself. What the tier now asserts is not that the retry worked — the scenarios already assert that — but
|
|
119
|
+
**how much of the window is being spent**, which is what turns 90s from a number read off two GitHub issues into a
|
|
120
|
+
measurement of the cluster in front of us.
|
|
121
|
+
- **The first live reading immediately corrected this ADR, which is the argument for having taken it.** The budget was
|
|
122
|
+
going to be attempts alone. The tier's first measurement was **2 attempts costing 10667ms** — one dropped SYN sitting
|
|
123
|
+
on undici's 10s connect timeout, not a ladder being climbed. So the refusal that dominates the cost here is a DROP,
|
|
124
|
+
not the kube-proxy REJECT this ADR leads with; both are real, and they cost in opposite currencies. A REJECT is
|
|
125
|
+
instant, so it spends attempts and almost no time. A DROP spends time and almost no attempts. An attempts-only budget
|
|
126
|
+
of 4 would therefore have waved through ~40s of a 90s window without a word. The very next run measured the other mode
|
|
127
|
+
— **3 attempts, 502ms, `ECONNREFUSED`** — which settled the shape: neither meter sees the other's failure, so the
|
|
128
|
+
budget is **30s or 8 attempts**, whichever trips first, and the line carries the final errno so nobody has to infer
|
|
129
|
+
the mode from the arithmetic. The two are calibrated to trip at comparable severity, which is what stops them being
|
|
130
|
+
one meter written twice: against the jittered 250ms→5s ladder, 8 attempts is ~9–18s of refusal, and 30s is three
|
|
131
|
+
connect timeouts. The first draft budgeted 4 attempts against an observed 3 — which is not a budget, it is a flake —
|
|
132
|
+
and only the second reading showed it.
|
|
133
|
+
- **This is the ecosystem's answer, not a jr2 workaround**, which is worth recording because "retry" reads as a patch.
|
|
134
|
+
[kind#2280](https://github.com/kubernetes-sigs/kind/issues/2280) is this defect exactly — EndpointSlices populated,
|
|
135
|
+
connection still refused, up to 77s — and it establishes that waiting for the EndpointSlice is NOT sufficient, which
|
|
136
|
+
retires gating the CR's `phase: Ready` on it as a narrowing rather than a fix. Knative's activator arrives at the same
|
|
137
|
+
place from the other side: it
|
|
138
|
+
[probes the pod itself and treats its own successful probe as authoritative](https://knative.dev/blog/articles/demystifying-activator-on-path/)
|
|
139
|
+
regardless of what Kubernetes says, exactly so traffic can start before the control plane catches up. jr2's version is
|
|
140
|
+
that with one fewer moving part — the first real request IS the probe — which is only safe because `neverDelivered` is
|
|
141
|
+
what makes it re-sendable. A separate probe would add a round trip and put the race back (probe succeeds, the rule
|
|
142
|
+
changes, the real request lands).
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# The kit is tested as installed: a local registry stands in for npm
|
|
2
|
+
|
|
3
|
+
jr2's product shape ([ADR-0009](0009-cli-and-instance-interface.md)) is a globally installed CLI and a standalone
|
|
4
|
+
Instance folder that just works — scaffold anywhere (`/tmp` included), install with whatever package manager the user
|
|
5
|
+
already has, `jr2 up`. None of that path existed: the scaffold wrote `workspace:*` deps only the kit's own pnpm
|
|
6
|
+
workspace can resolve, the bundle step was `pnpm deploy` (workspace members only), every package was `private: true`,
|
|
7
|
+
and installed mode's kit image refs name published tags that exist nowhere. Every test tier ran checkout mode, so the
|
|
8
|
+
mode users will actually run was the one mode nothing exercised. This ADR fixes the loop that closes that gap — and the
|
|
9
|
+
loop is **permanent, not a pre-1.0 crutch**: "test 1.3.0 before it exists on npm" is the same problem forever.
|
|
10
|
+
|
|
11
|
+
## The rule: fake the registry, never the mechanism
|
|
12
|
+
|
|
13
|
+
The loop feeds the real machinery a local upstream — the same philosophy as
|
|
14
|
+
[ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md)'s scripted model provider (only the LLM is faked; here, only
|
|
15
|
+
the registries are local). Concretely: an ephemeral [verdaccio](https://verdaccio.org) npm registry per loop run;
|
|
16
|
+
`pnpm -r publish --registry` into it; `npm i -g @jr2/cli --registry` into a temp npm prefix; `jr2 init` in a temp dir
|
|
17
|
+
outside any checkout and any git repo; the user's own package manager installs; `jr2 up` converges in installed mode.
|
|
18
|
+
|
|
19
|
+
`npm link` was the tempting shortcut and is rejected for cause: a linked package is a symlink into the checkout, Node
|
|
20
|
+
resolves modules by real path, so `detectKitCheckout()` finds the checkout and the CLI runs **checkout mode** — the
|
|
21
|
+
branch under test never executes. Link also skips `npm pack` entirely (no `files:` check, no `workspace:*` rewrite),
|
|
22
|
+
which is exactly the works-linked-broken-published failure class a release loop exists to catch. Tarball installs fail
|
|
23
|
+
differently: npm cannot resolve inter-package deps (`@jr2/cli` → `@jr2/orchestrator`) to tarballs. A registry is the
|
|
24
|
+
only stand-in that exercises the real chain.
|
|
25
|
+
|
|
26
|
+
**An ephemeral registry deletes version bookkeeping.** Fresh verdaccio per run means `0.0.0` publishes cleanly every
|
|
27
|
+
time — no `-dev.N` stamping, no manifest mutation before publish (a loop that edits manifests tests edited manifests).
|
|
28
|
+
|
|
29
|
+
## What publishes, and the guard
|
|
30
|
+
|
|
31
|
+
`@jr2/cli`, `@jr2/orchestrator`, `@jr2/agent-protocol` flip to `private: false` — the prod-resolution chain an instance
|
|
32
|
+
pulls. `@jr2/harness` stays private: it reaches users as a **Kit image**, never via npm install
|
|
33
|
+
([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md),
|
|
34
|
+
[ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md)). This amends ADR-0009's "all
|
|
35
|
+
`packages/*` publish to npm" — that line wrote down a mechanism, not the intent.
|
|
36
|
+
|
|
37
|
+
Losing `private: true` loses the accidental-publish guard, so the guard moves into `publishConfig`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
"publishConfig": { "registry": "http://localhost:4873" }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The manifest names the only registry it may publish to. `publishConfig` outranks CLI and env registry settings at
|
|
44
|
+
publish time, so a stray `--registry` cannot leak past it, and a publish with no verdaccio up fails connection-refused.
|
|
45
|
+
Publishing to npmjs becomes a deliberate, reviewable **edit** — changing that line is the "we are ready" commit — never
|
|
46
|
+
an absence-of-flag accident. (**Amended by
|
|
47
|
+
[ADR-0055](0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md)**: the same precedence that made
|
|
48
|
+
the line a guard made it unreleasable — the edit broke every reader of the guard at once. The line is gone; the guard is
|
|
49
|
+
that no dev box holds an npmjs credential, and the release job publishes by trusted publishing on a pushed tag.)
|
|
50
|
+
|
|
51
|
+
In the loop, Kit images need no registry at all on kind: build from the checkout, tag with the published names
|
|
52
|
+
(`jr2-harness:<version>`), `kind load` — the same delivery checkout mode already uses. Registry-prefixed kit refs for
|
|
53
|
+
non-kind clusters stay deferred (ADR-0038). (**Amended by
|
|
54
|
+
[ADR-0044](0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md)**: `kind load` bypassed the pull path, so
|
|
55
|
+
the mode users run had no tier — the `@dist` bring-up now adds a local OCI registry, seeds it with `just kit-push`, and
|
|
56
|
+
scenarios pull Kit images through `kitRegistry` like any self-host.)
|
|
57
|
+
|
|
58
|
+
## The binary is the kit's one `.js` file
|
|
59
|
+
|
|
60
|
+
Running the loop found what running checkout mode forever had hidden: Node's type stripping **refuses files under
|
|
61
|
+
`node_modules`**, and an installed kit is nothing else — `jr2 --help` died on its own shebang. The image bundle already
|
|
62
|
+
knew (its entrypoint runs through `tsx`, ADR-0038); the host-side binary had nowhere to learn it.
|
|
63
|
+
|
|
64
|
+
So `bin/jr2.js` replaces `bin/jr2.ts`: it registers a `module.registerHooks` load hook that erases types with
|
|
65
|
+
`ts-blank-space` — the same erasure ADR-0034 already makes for the Console — and then imports `../src/cli.ts`. What
|
|
66
|
+
`ts-blank-space` cannot erase is what Node's own stripping rejects too, so this widens where the kit runs, never what it
|
|
67
|
+
may be written in, and the repo stays zero-build. **One entry for both worlds**, for the same reason `jr2 init` grows no
|
|
68
|
+
mode branch: a checkout-only `.ts` bin beside an installed-only `.js` bin would mean the binary users run is not the
|
|
69
|
+
binary the tiers run.
|
|
70
|
+
|
|
71
|
+
The loop's own state — registry storage, npmrc, throwaway global prefix — therefore lives **outside** the checkout. A
|
|
72
|
+
prefix under the kit root is `npm link`'s rejected failure in another costume: `detectKitCheckout()` walks up from the
|
|
73
|
+
CLI's real path, finds the kit above the prefix, and converges in **checkout mode** while reporting success. The publish
|
|
74
|
+
script refuses a `JR2_DIST_DIR` inside the checkout, and the `@dist` converge asserts the mode line it expects.
|
|
75
|
+
|
|
76
|
+
## The scaffold pins the kit that scaffolded it
|
|
77
|
+
|
|
78
|
+
`jr2 init` renders `"@jr2/orchestrator"` and `"@jr2/cli"` at the **exact** running `KIT_VERSION` — exact, not caret,
|
|
79
|
+
because 0.x minors break (the same reasoning that pinned pi exact, ADR-0027). One template serves both worlds; init
|
|
80
|
+
grows no checkout/installed branch, because a branch means the tested output and the shipped output diverge.
|
|
81
|
+
`templates/default` carries the same literal, so the byte-for-byte mirror test survives and becomes the version-bump
|
|
82
|
+
tripwire. The checkout resolves the exact version to its own packages via `linkWorkspacePackages: true`; kit inter-deps
|
|
83
|
+
stay `workspace:*` (publish rewrites them at pack). The scaffold names no package manager — no `packageManager` field, a
|
|
84
|
+
PM-neutral install hint — and instances upgrade the kit by editing two dep lines, which pre-1.0 is a feature. (**Amended
|
|
85
|
+
by [ADR-0056](0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md)**: the two lines
|
|
86
|
+
must name one number — a global `jr2` hands off to the Instance's own `@jr2/cli`, and a CLI whose orchestrator is not
|
|
87
|
+
the Instance's refuses.)
|
|
88
|
+
|
|
89
|
+
## The bundle installs from the lockfile, with the lockfile's own package manager
|
|
90
|
+
|
|
91
|
+
The instance-image bundle ([ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md)) is keyed off the **instance's own
|
|
92
|
+
shape** — and not off the mode `jr2 up` detects for its Kit image refs. Asked in order: does the instance carry a
|
|
93
|
+
lockfile of its own; if not, is there a `pnpm-workspace.yaml` at or above it. The instance's shape, not the CLI's
|
|
94
|
+
provenance, is what says whether `pnpm deploy` can run at all, and the two keys disagree in both directions: a checkout
|
|
95
|
+
CLI legitimately drives a standalone instance (a developer's `/tmp` folder), and an installed kit legitimately drives an
|
|
96
|
+
instance nested in the user's own pnpm monorepo — which carries no lockfile of its own, because the workspace root holds
|
|
97
|
+
it.
|
|
98
|
+
|
|
99
|
+
_Amended:_ the key was first written as the workspace file alone — "is there a `pnpm-workspace.yaml` above me" — which
|
|
100
|
+
is a fact about an ancestor, not the instance. A standalone instance committed inside a checkout whose
|
|
101
|
+
`pnpm-workspace.yaml` does not list it went to `pnpm deploy`, which answers a `--filter` naming no member with "No
|
|
102
|
+
projects matched the filters" and **exit 0**: no bundle, and the failure surfaced as an ENOENT from the seal. The
|
|
103
|
+
lockfile is asked first because a member never has one and a standalone instance always does; and the deploy branch now
|
|
104
|
+
checks the bundle exists, naming both ways out (join the workspace, or commit a lockfile) when it does not.
|
|
105
|
+
|
|
106
|
+
- **Workspace member**: `pnpm deploy --legacy`, hoisted (`node-linker=hoisted`) like the standalone path, so the
|
|
107
|
+
Assembly of [ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md) is one flat
|
|
108
|
+
tree whichever path staged it. Its job — materializing workspace symlinks — only exists in a workspace. pnpm is a
|
|
109
|
+
**contributor prerequisite** (like go for the operator), and in the one installed-mode shape that reaches this branch
|
|
110
|
+
it is a prerequisite the user already met: they wrote the `pnpm-workspace.yaml` that selects it.
|
|
111
|
+
- **Standalone**: stage a copy of the instance (minus `node_modules/`, `.jr2/`, `.git/`, and the credential files
|
|
112
|
+
`.env*`/`.npmrc` — `jr2 up` reads `.env` host-side into the Secrets it writes for the pods that spend each value,
|
|
113
|
+
ADR-0019 and ADR-0068, so a copy would bake a key into an image layer and into the tag addressing it) and run a
|
|
114
|
+
**frozen install from the lockfile**, dispatched on which lockfile is present: `package-lock.json` →
|
|
115
|
+
`npm ci --omit=dev`; `pnpm-lock.yaml` → `pnpm install --prod --frozen-lockfile` (with `node-linker=hoisted`, so the
|
|
116
|
+
bundle is flat real files regardless of PM); `bun.lock[b]` → `bun install --production --frozen-lockfile`. A private
|
|
117
|
+
registry reaches that install through the environment (`npm_config_registry`) or the user-level npmrc, never through
|
|
118
|
+
image content.
|
|
119
|
+
|
|
120
|
+
The lockfile — not the user's `node_modules/` — is the input, and that is forced, not stylistic: the GitOps/CI path
|
|
121
|
+
(ADR-0008's instance repo, the planned `jr2 build`) runs from a clean checkout where **no `node_modules` exists**, a
|
|
122
|
+
copied tree bakes in accidents rather than declarations, and prod-pruning a copied tree means reimplementing resolution.
|
|
123
|
+
Lockfile-as-input is also [ADR-0019](0019-one-converging-command-against-the-current-context.md)'s derivability rule
|
|
124
|
+
applied to dependencies: the deployed bundle is a function of what `up` can see committed. The lockfile is therefore
|
|
125
|
+
part of the instance contract; no lockfile is a hard error naming the supported three, two lockfiles is an ambiguity
|
|
126
|
+
error.
|
|
127
|
+
|
|
128
|
+
The package-manager "dependency matrix" collapses to nothing: lockfiles are proprietary formats, so supporting any PM
|
|
129
|
+
mechanically means invoking the binary that speaks the lockfile present — and that binary's presence is guaranteed by
|
|
130
|
+
the very thing that selects it (the user wrote the lockfile with it). jr2 itself depends on no package manager.
|
|
131
|
+
|
|
132
|
+
**v1 ships npm, pnpm, and bun. Yarn is deliberately out**: one filename hides two incompatible generations (classic
|
|
133
|
+
`--frozen-lockfile` vs berry `--immutable`), and berry defaults to PnP — no `node_modules` at all, which the image's
|
|
134
|
+
resolution model cannot host. A `yarn.lock` gets a named rejection ("use npm, pnpm, or bun"), and adding it later is one
|
|
135
|
+
dispatch row plus its tests, no design change.
|
|
136
|
+
|
|
137
|
+
## The loop's two faces
|
|
138
|
+
|
|
139
|
+
- **`@dist`**: a new opt-in e2e tag beside `@kind` (docker + kind + verdaccio), whose Rule is literally ADR-0009's
|
|
140
|
+
claim: a globally installed CLI converges a standalone instance. Publish happens once per suite run; scenarios isolate
|
|
141
|
+
by namespace; scenarios cover npm and pnpm instances (bun rides unit coverage on the dispatch until the tier needs
|
|
142
|
+
it), scaffolded in temp dirs with no git, converged, blown away.
|
|
143
|
+
- **The manual loop**: a `just` recipe leaves verdaccio and the temp global install standing, so a developer plays the
|
|
144
|
+
way a user lives — `jr2 init /tmp/anything && npm install && jr2 up`, entirely outside the repo.
|
|
145
|
+
|
|
146
|
+
## Consequences
|
|
147
|
+
|
|
148
|
+
- ADR-0009's consequences: the instance-facing packages publish to npm; the Harness ships in a Kit image.
|
|
149
|
+
- **No `@dist` scenario deploys a Custodian.** `ping` runs on no Harness pod, so the tier checks that the image map
|
|
150
|
+
re-homes the Custodian's Pinned image onto `kitRegistry`
|
|
151
|
+
([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)) and pulls nothing. The fixture seeds
|
|
152
|
+
the registry with `just kit-push`, which builds Kit images only; a scenario that ran a Harness pod would need the
|
|
153
|
+
Envoy image mirrored there too (`jr2 kit push` does it).
|
|
154
|
+
- Scaffolded instances pin exact and bump manually; a `jr2 upgrade` verb can exist later if that ever hurts.
|
|
155
|
+
- The checkout's `pnpm-workspace.yaml` gains `linkWorkspacePackages: true`; `templates/default` trades `workspace:*` for
|
|
156
|
+
the exact version literal.
|
|
157
|
+
- The npmjs `@jr2` scope must be claimed early regardless of readiness — the localhost guard protects against accidents,
|
|
158
|
+
not squatters.
|
|
159
|
+
- **`jr2 up` still shells out to pnpm in one installed-mode shape**: an instance nested in the user's own pnpm
|
|
160
|
+
workspace. That is not the product dependency creeping back, because the thing that selects pnpm is a file the user
|
|
161
|
+
wrote with pnpm — the same self-selecting argument the lockfile dispatch already rests on.
|
|
162
|
+
- **The published packages carry `files:` allowlists, and the over-include half is a unit assertion**
|
|
163
|
+
(`packages/cli/test/publish.test.ts`). A `files:` list fails loudly only when it OMITS something, so the `@dist` tier
|
|
164
|
+
is structurally incapable of catching a package that ships its test tree — and these are prod deps, unpacked into
|
|
165
|
+
every Assembly.
|
|
166
|
+
- The staged bundle drops `node_modules/.pnpm-workspace-state.json` beside `.modules.yaml` (ADR-0038's seal): it is a
|
|
167
|
+
`lastValidatedTimestamp` and nothing else, so leaving it in re-addressed every pnpm bundle on every converge — a
|
|
168
|
+
pod-template change, and a snapshot restore for every live run, on a no-op `up`.
|
|
169
|
+
- Everything the loop needs in-tree (public flags, localhost `publishConfig`, exact-pin scaffold, lockfile dispatch) is
|
|
170
|
+
the real shipping configuration. Nothing anywhere special-cases "testing" — which is the point.
|
package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Kit images live at a canonical home; a self-host mirrors it
|
|
2
|
+
|
|
3
|
+
[ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md) gave installed mode published-`<kitversion>` Kit image refs and
|
|
4
|
+
deferred the question of where those tags actually live "while nothing is published." The first installed-mode converge
|
|
5
|
+
against a real cluster ends the deferral: `publishedKitRefs()` emits bare `jr2-harness:<kitversion>`, a node resolves
|
|
6
|
+
that against `docker.io/library/`, and nothing is there — no registry a user pushes to can help, because the pod spec
|
|
7
|
+
never names it.
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
|
|
11
|
+
- **Kit images have a canonical public home: `ghcr.io/snapwich`.** `publishedKitRefs()` bakes it:
|
|
12
|
+
`ghcr.io/snapwich/jr2-harness:<kitversion>`, likewise the operator. Public GHCR egress is free to the project, so an
|
|
13
|
+
open-source kit can afford a home every user's nodes pull from — and only a canonical home makes
|
|
14
|
+
`npm i -g @jr2/cli && jr2 init && jr2 up` work with zero image plumbing, which is the product's promise. Pushing there
|
|
15
|
+
is part of the release train ([ADR-0019](0019-one-converging-command-against-the-current-context.md)): a release is
|
|
16
|
+
not done until the images for its npm version are at the home.
|
|
17
|
+
- **A new config key `kitRegistry` re-homes the Kit refs** — `<kitRegistry>/jr2-harness:<kitversion>` — for self-hosted,
|
|
18
|
+
air-gapped, or mirror-only clusters. It re-homes the one Pinned image too, the Custodian's Envoy
|
|
19
|
+
([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)):
|
|
20
|
+
`docker.io/envoyproxy/envoy:<tag>@<digest>` becomes `<kitRegistry>/envoy:<tag>@<digest>`. A mirror-only cluster needs
|
|
21
|
+
it as much as the Harness, and the digest does not move with the registry. It is deliberately **separate from
|
|
22
|
+
`registry`**: the two keys answer different questions — `registry` says where images _this converge builds_ go;
|
|
23
|
+
`kitRegistry` says where _published artifacts_ live. One key for both would force every private-`registry` user to
|
|
24
|
+
mirror Kit images they could have pulled from the home, making the common corporate case pay the rare air-gapped
|
|
25
|
+
case's cost. Like `registry`, it is deployment-varying → env.
|
|
26
|
+
- **Getting Kit images into a self-hosted registry is deliberate and instance-less — never part of `jr2 up`.** Kit
|
|
27
|
+
images are shared by every instance on a cluster (and possibly by every instance in an org); updating them is a
|
|
28
|
+
deliberate act, not a side effect of converging one instance. Two arms, split by what each audience holds:
|
|
29
|
+
- **`jr2 kit push <registry>`** (in the binary, for the installed self-hoster): a **mirror**, nothing more —
|
|
30
|
+
registry-to-registry manifest copy of the Kit refs at the CLI's own `<kitversion>`, canonical home → target — and of
|
|
31
|
+
the Custodian's Pinned image, its upstream home → target, at the same digest — via
|
|
32
|
+
`docker buildx imagetools create`. That copies the full manifest list (every platform, exact digests) without
|
|
33
|
+
landing bytes on the host; a daemon-side `docker pull`/`push` would flatten a multi-arch image to the host's
|
|
34
|
+
platform and ship an amd64-only image to an arm64 cluster. A tag already present in the target is skipped and
|
|
35
|
+
reported: published version tags never move, so present implies current. The binary has **no build arm**: the npm
|
|
36
|
+
packages carry no Harness source, and an installed CLI that could build Kit images is exactly the patched-Harness
|
|
37
|
+
eject hatch [ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)/ADR-0038 welded shut.
|
|
38
|
+
- **`just kit-push <registry>`** (in the checkout, for the kit developer and the release pipeline): build the Kit
|
|
39
|
+
images from source **multi-arch** (`docker buildx build --platform linux/amd64,linux/arm64 --push`) at the published
|
|
40
|
+
names into the given registry. This is how the canonical home gets its images at release — and how a dev-loop
|
|
41
|
+
registry gets images the home does not have yet. Single-arch pushes here would make the mirror faithfully copy the
|
|
42
|
+
deficiency to every self-host. It does not touch the Pinned image: it builds, and jr2 builds no Envoy.
|
|
43
|
+
- **The `@dist` tier pulls Kit images through a real registry**
|
|
44
|
+
([ADR-0043](0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md)'s rule — fake the registry,
|
|
45
|
+
never the mechanism — applied to the second registry the product touches). The tier's kind bring-up adds a local OCI
|
|
46
|
+
registry (the upstream kind local-registry pattern: a `registry:2` container plus containerd `hosts.toml` on the
|
|
47
|
+
nodes); the fixture seeds it with `just kit-push`, scenarios set `kitRegistry`, and the nodes genuinely pull —
|
|
48
|
+
`kind load` at published names had bypassed the pull path entirely, leaving the mode users run covered by no tier,
|
|
49
|
+
which is the failure class ADR-0043 exists to delete.
|
|
50
|
+
|
|
51
|
+
## Considered options
|
|
52
|
+
|
|
53
|
+
- **No canonical home — self-host as the only model.** Rejected: every user's first `jr2 up` fails until they stand up a
|
|
54
|
+
registry and mirror the kit's images into it, inverting "the CLI does all the work." The bandwidth objection to a home
|
|
55
|
+
turned out to be moot: GHCR egress for public images is GitHub's cost, not the project's.
|
|
56
|
+
- **One `registry` key prefixing everything.** Rejected above — the corporate-registry case pays the air-gapped case's
|
|
57
|
+
mirror cost for no benefit.
|
|
58
|
+
- **Mirroring inside `jr2 up`** (absent tag → pull home, push self-host, converge on). Rejected: kit updates on a shared
|
|
59
|
+
cluster should be deliberate; a per-instance converge is the wrong actor to move an org-shared artifact, and the same
|
|
60
|
+
logic wants to run with no instance at all.
|
|
61
|
+
- **One `jr2 kit push` with a checkout build arm** (checkout-is-the-signal, like `jr2 up`). Rejected: the build arm
|
|
62
|
+
needs the whole repo, so it belongs to the repo's own tooling — ADR-0038 already scoped `just` recipes as "shortcuts
|
|
63
|
+
for building images without a converge." The binary keeps only the arm its audience can use.
|
|
64
|
+
- **Digest-pinned Kit refs** (`…@sha256:`), killing tag staleness outright. Rejected: installed mode would need a
|
|
65
|
+
converge-time registry resolve plus digest bookkeeping — a production mechanism purchased to serve a dev-loop
|
|
66
|
+
annoyance (see the accepted remainder).
|
|
67
|
+
- **`imagePullPolicy: Always` on Kit containers.** Rejected: taxes every production pull to serve the same dev-loop
|
|
68
|
+
annoyance, inverting ADR-0038's "IfNotPresent is correct because tags are immutable."
|
|
69
|
+
|
|
70
|
+
## Consequences
|
|
71
|
+
|
|
72
|
+
- ADR-0038's deferred edge ("air-gapped clusters cannot pull published kit images") closes: the answer is the promised
|
|
73
|
+
registry prefix, spelled `kitRegistry`, fed by a deliberate mirror.
|
|
74
|
+
- **Accepted remainder: a re-pushed dev tag is stale on any node that pulled the old one.** The loop pushes
|
|
75
|
+
`jr2-harness:0.0.0` repeatedly; `IfNotPresent` keeps the first bytes a node saw. Version tags are immutable everywhere
|
|
76
|
+
but the loop, and kit-source iteration is checkout mode's job — its content-addressed tags make staleness impossible
|
|
77
|
+
by construction. The installed loop tests distribution mechanics, not Harness code; when its content truly must move
|
|
78
|
+
on a persistent cluster, the escape is manual (`crictl rmi` on the node, or a throwaway cluster). `@dist` is immune —
|
|
79
|
+
its registry and cluster are ephemeral per run.
|
|
80
|
+
- `just kit-push` needs a multi-platform buildx builder (qemu for the foreign arch). A checkout-mode converge against a
|
|
81
|
+
foreign-arch cluster needs no workaround: `jr2 up` derives the platform set from the cluster's nodes and passes
|
|
82
|
+
`--platform` explicitly ([ADR-0045](0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md)), so
|
|
83
|
+
`DOCKER_DEFAULT_PLATFORM` steers nothing.
|
|
84
|
+
- The home-cluster installed-mode loop becomes: `just dist-up` (npm side), `just kit-push zot.cluster.snapwich.net`
|
|
85
|
+
(image side), then an instance whose env sets `kitRegistry`.
|