@jr2/orchestrator 0.5.1 → 0.7.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/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +142 -29
- package/docs/concepts/architecture.md +60 -34
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +13 -8
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +7 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +133 -50
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +14 -4
- package/docs/concepts/pools-and-sources.md +193 -17
- package/docs/concepts/secrets-and-trust.md +186 -62
- package/docs/concepts/the-menu.md +14 -0
- package/docs/concepts/workspaces.md +135 -56
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +72 -15
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +29 -12
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +19 -7
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +14 -7
- package/docs/guides/look-inside-a-sandbox.md +115 -42
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +34 -27
- package/docs/guides/set-an-agents-access-and-dials.md +69 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +7 -1
- package/docs/guides/watch-a-run.md +146 -117
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +18 -8
- package/docs/llms.txt +110 -23
- package/docs/reference/authoring-surface/agent.md +35 -12
- package/docs/reference/authoring-surface/answer.md +20 -4
- package/docs/reference/authoring-surface/arg.md +28 -6
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +37 -22
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +18 -5
- package/docs/reference/authoring-surface/file.md +37 -17
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +109 -9
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +14 -4
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +41 -8
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +23 -5
- package/docs/reference/authoring-surface/pool.md +79 -11
- package/docs/reference/authoring-surface/push.md +31 -13
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +26 -5
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +59 -4
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- 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-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +6 -7
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
- package/docs/reference/authoring-surface/type-customize.md +20 -3
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +6 -6
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +30 -9
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- 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 +66 -38
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +88 -147
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +248 -6
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
- package/docs/reference/decisions.md +9 -4
- package/docs/reference/glossary.md +138 -45
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +988 -123
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +7 -5
- package/package.json +10 -3
- package/src/actor.ts +194 -42
- package/src/agent.ts +56 -6
- package/src/ambient.ts +3 -5
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +238 -26
- package/src/charts.ts +9 -2
- package/src/config.ts +566 -7
- package/src/conversations.ts +453 -0
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +257 -77
- package/src/customize.ts +154 -7
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +52 -4
- package/src/effect.ts +401 -132
- package/src/effector-wire.ts +278 -30
- package/src/fail.ts +17 -4
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +84 -35
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +197 -39
- package/src/holds.ts +5 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +164 -70
- package/src/http.ts +933 -99
- package/src/index.ts +30 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +23 -3
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +29 -5
- package/src/mcp-effect.ts +424 -0
- package/src/model-keys.ts +24 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +82 -7
- package/src/notices.ts +576 -59
- package/src/open-body.ts +110 -30
- package/src/open-effect.ts +17 -4
- package/src/open.ts +40 -7
- package/src/parts.ts +125 -35
- package/src/party.ts +35 -1
- package/src/pick.ts +43 -9
- package/src/pool.ts +1172 -172
- package/src/registration.ts +249 -17
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +1614 -190
- package/src/run-list.ts +191 -0
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +116 -21
- package/src/secret-placement.ts +248 -35
- package/src/server.ts +53 -4
- package/src/setup.ts +158 -38
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +362 -46
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +27 -10
- package/src/workspace.ts +314 -259
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# An Agent's control credential lives only where no Workspace code runs
|
|
2
|
+
|
|
3
|
+
[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) scoped an Agent's control
|
|
4
|
+
credential to its Sandbox. That stops a coder in one Sandbox from delivering a pick to a reviewer in another Sandbox.
|
|
5
|
+
[ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md) made several Agents in one Sandbox normal:
|
|
6
|
+
concurrent coders in one clone, or a coder beside an independent reviewer. In one Sandbox the Sandbox scope is not
|
|
7
|
+
sufficient. Every Agent's `bash` is a child of the one Harness process, at the one uid, and it inherits the env of that
|
|
8
|
+
process. So a coder's `bash`, driven by a prompt injection, can read the Stand-in of the control credential, send
|
|
9
|
+
`POST /agents/<reviewer-iid>/events` through the Custodian, and approve its own review. The reviewer did nothing.
|
|
10
|
+
|
|
11
|
+
The attack does not need the coder's own `bash`. The Agents of one Workspace share a writable `/work`. Code that one
|
|
12
|
+
Agent writes there — a test file, a git hook, `core.fsmonitor`, a smudge filter — runs when another process runs on
|
|
13
|
+
`/work`: the reviewer's `bash`, its `node --test` or `git diff`, or a `command` Effect. That code then has whatever
|
|
14
|
+
credential its process can read. So the question is not which container holds the credential. It is which processes run
|
|
15
|
+
code from `/work`.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
- **The boundary.** A credential that lets a request act for an Agent lives only where no Workspace code runs.
|
|
20
|
+
**Workspace code** is each process whose program or input comes from `/work`: the Working tools, tests, builds, git
|
|
21
|
+
(its hooks, its filters, fsmonitor, textconv and each repository-local config), `command` Effects and stdio MCP
|
|
22
|
+
servers. The boundary needs no separate container for each Agent.
|
|
23
|
+
- **One Harness container for each Sandbox.** It serves every workspace-ful Agent below the `workspace()`
|
|
24
|
+
([ADR-0005](0005-sandbox-pod-composition.md)). A Menu-only Agent has no Sandbox, and it runs on the Instance Harness,
|
|
25
|
+
as before ([ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md)).
|
|
26
|
+
- **The Harness process is trusted, and every program that a model drives runs confined.** The Harness process runs
|
|
27
|
+
jr2's code only. It runs the engine loop for each conversation, and it holds the control credential. Each other
|
|
28
|
+
process in the Harness container runs in one Landlock domain, which the Harness is not in:
|
|
29
|
+
- each `bash` call, and so each test, build, git hook and planted program that it runs;
|
|
30
|
+
- each process of a search tool (`rg`, `grep`);
|
|
31
|
+
- the I/O of the file tools (read, write, edit);
|
|
32
|
+
- jr2's own git for the attach, the cut and the sync
|
|
33
|
+
([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md),
|
|
34
|
+
[ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)), because a smudge filter or a textconv
|
|
35
|
+
in the shared `.git/config` runs in git's process;
|
|
36
|
+
- each `command` Effect that is placed in the Harness, which is each `command` that names no secret;
|
|
37
|
+
- each stdio MCP server of an Agent
|
|
38
|
+
([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)),
|
|
39
|
+
which the Harness starts through the spawner, never with its MCP client's own spawn.
|
|
40
|
+
|
|
41
|
+
So the trust boundary is inside the Harness container: the Harness process on one side, the domain on the other.
|
|
42
|
+
[ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md) makes Landlock a requirement of jr2, so no path runs a
|
|
43
|
+
program unconfined.
|
|
44
|
+
|
|
45
|
+
- **One spawner makes the domain.** At its start, the Harness starts the **spawner**, `effector spawner` on the runtime
|
|
46
|
+
volume ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), confined once. The spawner
|
|
47
|
+
starts each process that the Harness asks for, and it does each file tool's I/O and each read, list and remove of a
|
|
48
|
+
path that the Harness needs in the Agent's files (the sync's git folders and its bundle). It sends back the output,
|
|
49
|
+
the exit status, the file's bytes and the names in a folder. A process inherits its parent's domain, so every program
|
|
50
|
+
of every Agent of the Sandbox is in the one domain. They see, signal and trace each other, as one user's processes do
|
|
51
|
+
on a machine: a later `bash` call can `kill` or `lsof` a server that an earlier call started. The tools that the model
|
|
52
|
+
calls do not change: `bash` takes a command, and `read`, `write` and `edit` take a path. If the spawner stops, the
|
|
53
|
+
Harness starts it again, with the same rules. The new spawner makes a new domain, a sibling of the old one, and the
|
|
54
|
+
processes that the old spawner started run on in the old domain. A process of the new domain cannot trace one of the
|
|
55
|
+
old domain, or read its `/proc/<pid>/environ` or its open files. Below ABI 6 it can still signal it, so a later `bash`
|
|
56
|
+
call can end a server from before the restart; at ABI 6 or later it cannot. The confiner never follows a symbolic link
|
|
57
|
+
to find what a rule allows: a link gets no rule (what it names gets its own rule where it is allowed, and the kernel
|
|
58
|
+
checks the path that a link resolves to), and the confiner refuses to start when a rule's path has a link on it. A
|
|
59
|
+
rule on a link would allow what the link names at each restart, and a confined process can change that.
|
|
60
|
+
- **A person's shell is a process of the domain.** `jr2 workspace shell`
|
|
61
|
+
([ADR-0009](0009-cli-and-instance-interface.md)) is `kubectl exec -c harness` of a small client in `/opt/jr2`, which
|
|
62
|
+
starts as the Harness starts (`effector nocore --harness-env`). The client starts nothing itself. It asks the Harness,
|
|
63
|
+
on a Unix socket in `/run/jr2/shell`, for the shell or for the one command after `--`, and the Harness asks the
|
|
64
|
+
spawner. So the shell is in the one domain, with the domain's env (the Stand-ins that the Workspace grants), its umask
|
|
65
|
+
and git identity, and the Workspace's cwd. It sees, signals and traces the Agents' processes, as a later `bash` call
|
|
66
|
+
does: a person can stop a dev server that an Agent left. With a terminal, the spawner opens a pseudo-terminal for the
|
|
67
|
+
shell, as its controlling terminal in a session of its own. The client sends the keys, the terminal's size and the
|
|
68
|
+
hang-up, and the spawner closes the terminal when the shell exits. The output has flow control: while the socket to
|
|
69
|
+
the client is full, the Harness pauses the spawn, and the spawner reads none of its output until the resume. So the
|
|
70
|
+
terminal fills and the shell's writes block, as on a slow terminal, and a fast shell does not fill the memory of the
|
|
71
|
+
Harness that serves the Agents' Turns. `/run/jr2/shell` is an emptyDir of the Harness container alone, and the domain
|
|
72
|
+
can make no file under `/run/jr2`, so no process of the domain listens in the Harness's place. A process of the domain
|
|
73
|
+
can connect to the socket, because Landlock does not control a connect. So the client proves that it is not one: its
|
|
74
|
+
first frame carries a digest of the control Stand-in, which it reads from the file and which the domain cannot read.
|
|
75
|
+
The Stand-in itself never goes on the socket. Without the proof, the Harness starts nothing.
|
|
76
|
+
- **The domain can do what the container's uid can do, except reach jr2's secrets.** The rules give every right on every
|
|
77
|
+
path, with one exception: `/run/jr2`. There the domain can list the names in each folder, and do nothing else.
|
|
78
|
+
Landlock only narrows the Unix permissions, so a confined program can do everything that the uid can do without
|
|
79
|
+
Landlock, except:
|
|
80
|
+
- reach the contents of `/run/jr2`: read, write, truncate, make, remove, move or link a file or a folder there.
|
|
81
|
+
`/run/jr2` holds the control Stand-in, each held secret's Stand-in, the folder of the Custodian's control socket,
|
|
82
|
+
the folder of the shell's socket, and the Harness's conversations. Every path that jr2 holds for the Harness alone
|
|
83
|
+
is under `/run/jr2`. Landlock allows a path only by its rules, so the confiner allows every sibling of `/run/jr2`
|
|
84
|
+
and of each folder above it, one level at a time. It also gives the root the right to list names on its whole tree,
|
|
85
|
+
so `ls /`, `ls /run` and `find /` work as they do outside jr2. That right lists the names under `/run/jr2` too: a
|
|
86
|
+
held secret's name, a conversation's file name. These names are not secrets.
|
|
87
|
+
- trace the Harness process, or read its `/proc/<pid>/environ` or `/proc/<pid>/mem`. A confined process cannot ptrace
|
|
88
|
+
a process outside its domain.
|
|
89
|
+
- at Landlock ABI 6 or later, connect to an abstract Unix socket that a process outside the domain made. The
|
|
90
|
+
containers of the pod share one network namespace, so this keeps the domain off the abstract sockets of the Harness
|
|
91
|
+
and of the Custodian.
|
|
92
|
+
|
|
93
|
+
Landlock does not limit a connect to a named Unix socket. So a confined program can connect to the Custodian's control
|
|
94
|
+
socket. There the Custodian is the guard: it refuses a request that does not carry the secret control Stand-in, and
|
|
95
|
+
the fetch Stand-in, which every confined program holds, is valid only on the fetch route. Each listener answers 404 to
|
|
96
|
+
the other's routes, so the fetch Stand-in gets nothing on the control socket.
|
|
97
|
+
|
|
98
|
+
The kernel, not the rules, keeps what other pods share. The Repo caches under `/repos`, the runtime under `/opt/jr2`
|
|
99
|
+
and the CA bundles are mounted read-only, and the pod mounts no ServiceAccount token. The domain does not limit the
|
|
100
|
+
network. A request reaches a held credential only through the Custodian
|
|
101
|
+
([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)).
|
|
102
|
+
|
|
103
|
+
- **No Harness pod reaches a cloud metadata address.** Where the metadata service of a node answers pods, it gives the
|
|
104
|
+
node's cloud credential, which no `workspace()` grants. The Custodian's dial guard (ADR-0059) applies only to a
|
|
105
|
+
request that goes through the Custodian, and a program of the Agent can connect directly. So `jr2 up` converges an
|
|
106
|
+
egress NetworkPolicy for each Harness pod: `jr2-sandbox-egress` selects each Sandbox pod,
|
|
107
|
+
`jr2-instance-harness-egress` selects the Instance Harness, and each check pod of the provider preflight gets one of
|
|
108
|
+
its own, before the pod. Each policy allows all egress except `169.254.169.254/32`, `fd00:ec2::254/128`,
|
|
109
|
+
`169.254.170.2/32`, `169.254.170.23/32` and `100.100.100.200/32`: the metadata service of AWS, GCP and Azure, the
|
|
110
|
+
credential agents of ECS and EKS Pod Identity, and the metadata service of Alibaba Cloud.
|
|
111
|
+
- NetworkPolicy has no deny rule. So the rule allows `0.0.0.0/0` and `::/0`, each with its addresses as `except`, and
|
|
112
|
+
it also allows every pod of the cluster (`namespaceSelector: {}`).
|
|
113
|
+
- The Instance Harness and the check pod run no Workspace code. They get the policy because the check pod stands for
|
|
114
|
+
every Harness pod ([ADR-0019](0019-one-converging-command-against-the-current-context.md)): a model that only the
|
|
115
|
+
node's credential reaches must fail the check, not a Turn.
|
|
116
|
+
- The block has two limits. The list is not complete for every cloud provider, and only a CNI that enforces
|
|
117
|
+
NetworkPolicy applies it. So the shared-cluster guide tells the admin to check the metadata address of their
|
|
118
|
+
provider, and to give pods no node credential: Workload Identity, or IMDSv2 with a hop limit of 1.
|
|
119
|
+
|
|
120
|
+
- **The env of the domain is the container's own env, with the granted Stand-ins.** A program in the domain gets the env
|
|
121
|
+
that the container started with: the image's `ENV`, `harness.env` and `envFrom`, the proxy and CA variables. jr2's own
|
|
122
|
+
`JR2_*` names are removed, except `JR2_CUSTODIAN_URL`. It also gets `HOME`, `TMPDIR`, and the Stand-ins of the secrets
|
|
123
|
+
that the `workspace()` grants (its `secrets` list), each under its own name. It never gets what the Harness process
|
|
124
|
+
added to its own env after the start, and so it never gets the control Stand-in, or the Stand-in of a model key that
|
|
125
|
+
the `secrets` list does not name. A toolchain and a bring-your-own image work as they work outside jr2.
|
|
126
|
+
- **The Harness process runs with jr2's env alone.** node, glibc and OpenSSL read names from the env that load code or
|
|
127
|
+
configuration from a path: `NODE_OPTIONS=--require /app/x.js`, `LD_PRELOAD`, `OPENSSL_CONF`. The image's `ENV` can
|
|
128
|
+
name a file that the domain writes. So the Harness starts as `effector nocore --harness-env -- node …`. The effector
|
|
129
|
+
and tini are static binaries, so no `LD_*` name changes them. Before it starts node, the effector:
|
|
130
|
+
- gives node only the names that jr2 composed for the container: each name of its `env`, which the Orchestrator lists
|
|
131
|
+
in `JR2_HARNESS_ENV_NAMES`, and each key of its `envFrom`, to which the Orchestrator gives the prefix
|
|
132
|
+
`JR2_ENVFROM_`. The image's `ENV` can also hold a name with that prefix, and node then gets it. The marks keep out a
|
|
133
|
+
name that an image sets by accident, not one that its author sets to attack: the image's author already controls the
|
|
134
|
+
Harness process through the image's glibc
|
|
135
|
+
([ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md)).
|
|
136
|
+
- stops, and starts nothing, on an `envFrom` key that starts with `JR2_`. jr2's own names, such as `JR2_MENU_ONLY`,
|
|
137
|
+
which turns the domain off, come from the container's `env` alone, so a shared Secret cannot set one. `jr2 up`
|
|
138
|
+
refuses a `JR2_` name in `harness.env`, and a `JR2_` key of a Secret or a ConfigMap that `harness.envFrom` loads.
|
|
139
|
+
- removes from them each name that loads code or configuration from a path, or writes to one: every `LD_*` and
|
|
140
|
+
`MALLOC_*` name, `NODE_OPTIONS`, `NODE_PATH`, `NODE_COMPILE_CACHE`, `OPENSSL_CONF`, `GCONV_PATH` and the others of
|
|
141
|
+
its list, and a `TZ` that names a file.
|
|
142
|
+
- gives node `HOME=/run/jr2/home`, whatever the start env holds. node reads `HOME` once, at its start, into the
|
|
143
|
+
folders where a bare `require()` looks last: `$HOME/.node_modules` and `$HOME/.node_libraries`. A provider module
|
|
144
|
+
that pi-ai loads on its first Turn can miss in `node_modules` and fall through to them. So a Harness process with a
|
|
145
|
+
`HOME` that the domain writes runs a module that the domain put there.
|
|
146
|
+
- puts the container's whole start env, each `envFrom` key under its own name, in `JR2_START_ENV`, as base64. The
|
|
147
|
+
domain's env is made from it, so the image's `ENV` and the container's `HOME` reach each program of the domain. No
|
|
148
|
+
program reads that name. A `JR2_START_ENV` that the start env already holds is dropped. Without
|
|
149
|
+
`JR2_HARNESS_ENV_NAMES` the effector starts nothing.
|
|
150
|
+
|
|
151
|
+
At its start, before it loads any other module, the Harness refuses to run when a folder where node looks for a module
|
|
152
|
+
is outside `/opt/jr2` and `/run/jr2`, and it checks that its `HOME` is `/run/jr2/home`. A change of `HOME` after node
|
|
153
|
+
started is too late, so the Harness never makes one.
|
|
154
|
+
|
|
155
|
+
A name of jr2's env that names a credential file which the Harness process reads (`AWS_CONFIG_FILE`,
|
|
156
|
+
`GOOGLE_APPLICATION_CREDENTIALS`, and the others) is the admin's choice. `jr2 up` refuses such a path in a folder that
|
|
157
|
+
the domain writes in every pod (`/work`, `/tmp`, `/dev/shm`, `/home/jr2`), and warns on any other path: whether the
|
|
158
|
+
Sandbox's uid can write a folder of the image is the image's, and jr2 cannot read it.
|
|
159
|
+
|
|
160
|
+
- **The file tools reach what `bash` reaches.** Their I/O runs in the spawner, so the kernel applies the domain's rules
|
|
161
|
+
to them. A path under `/run/jr2`, or a symbolic link to one, fails with the error that an unreadable file gives. `~`
|
|
162
|
+
in a path is the domain's `HOME`, as it is for `bash`.
|
|
163
|
+
- **The Harness process touches no path that the domain can write.** Each path in the Agent's files that the Harness
|
|
164
|
+
reads, lists or removes goes through the spawner. The one exception is the file that holds the full output of a `bash`
|
|
165
|
+
call. The Harness makes it in the domain's `TMPDIR`, with a name that must be new and must not be a link, and it
|
|
166
|
+
writes through that open file alone. If the file did not land in `TMPDIR`, the Harness removes it and refuses. The
|
|
167
|
+
Harness process has its own `HOME`, `/run/jr2/home`, from its start (above), which does not exist and which no process
|
|
168
|
+
can make, because code in the Harness process reads files under `HOME`: node looks for modules in `~/.node_modules`,
|
|
169
|
+
the AWS SDK reads `~/.aws/config`, which can name a `credential_process` to run, and Google's library reads
|
|
170
|
+
`~/.config/gcloud`. The domain keeps the Workspace's `HOME`.
|
|
171
|
+
- **One secret control Stand-in for each Sandbox.** Only the Harness process holds it, as a file under `/run/jr2`. It is
|
|
172
|
+
in no env of the domain. The Custodian puts the Sandbox's control token in its place on the control routes (ADR-0059),
|
|
173
|
+
and the real token never leaves the Custodian.
|
|
174
|
+
- **The control routes are a Unix socket under `/run/jr2`.** The Harness sends each control request to the Custodian's
|
|
175
|
+
control socket, not to a loopback port. A confined program cannot make a socket under `/run/jr2`, so it cannot take
|
|
176
|
+
the control listener's place while the Custodian restarts, and it cannot receive the control Stand-in. It can connect
|
|
177
|
+
to the socket, and the Custodian refuses what it sends (above). The fetch route stays on loopback, because its
|
|
178
|
+
Stand-in is public.
|
|
179
|
+
- **The Harness states the slot.** On each control request, the Harness states the Agent that the conversation belongs
|
|
180
|
+
to: its slot. It takes the slot from its own record of the conversation, never from the URL. The slot is the Agent's
|
|
181
|
+
full identity below the run: the actor path of its Machine and its agent name, which is a continued conversation's
|
|
182
|
+
Instance ID without the run id ([ADR-0057](0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md)). So two
|
|
183
|
+
Agents with one key in two child Machines are two slots. The Orchestrator checks that the stated slot is the slot of
|
|
184
|
+
the target registration, on the surface read, the pick, the delivery poll and the acknowledgment of a notice
|
|
185
|
+
([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)). This check finds a routing fault. It is not
|
|
186
|
+
the security boundary: the boundary is that only the Harness holds the credential.
|
|
187
|
+
- **Two tokens, each checked on its own routes.** This is defence in depth:
|
|
188
|
+
- The **control token** is what the Custodian puts in place of the secret control Stand-in. It is valid only on the
|
|
189
|
+
agent routes (`/agents/<iid>/surface`, `/agents/<iid>/events`, `/agents/<iid>/deliveries/<id>`, the route on which
|
|
190
|
+
the Harness acknowledges a notice, and `/agents/<iid>/conversation`, on which it sends the entries of the
|
|
191
|
+
conversation, [ADR-0023](0023-the-harness-prints-the-conversation.md)), and it carries the stated slot.
|
|
192
|
+
- The **fetch token** is what the Custodian puts in place of the public Sandbox Stand-in that `jr2-upload-pack` holds
|
|
193
|
+
as a constant. It is valid only on `POST /sandboxes/<sandbox>/fetch`
|
|
194
|
+
([ADR-0053](0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md)). The Custodian
|
|
195
|
+
accepts the public Stand-in on the fetch route only.
|
|
196
|
+
- The Orchestrator accepts each token only on its own routes. A Gate accepts neither, and no other principal of an
|
|
197
|
+
Agent: an Agent never reaches a Gate (ADR-0013).
|
|
198
|
+
- **An engine runs its loop in the Harness process.** pi does: its loop is in the Harness process, and only its tools
|
|
199
|
+
run in the domain. Each engine adapter states where its loop runs, and `engine.ts` refuses an adapter whose engine
|
|
200
|
+
runs in the domain. Every process of the one domain can see, trace and signal every other one, so another Agent's code
|
|
201
|
+
could write such an engine's stdin, read its environ and forge its Menu calls.
|
|
202
|
+
- **An engine with its own process, such as Claude Code, needs a domain for each Agent first.** The root domain then
|
|
203
|
+
holds only the spawner, which no process can trace or read (`PR_SET_DUMPABLE 0`). For each conversation, the spawner
|
|
204
|
+
starts a sub-spawner one Landlock layer deeper, with a socket pair as its stdio and as the subreaper
|
|
205
|
+
(`PR_SET_CHILD_SUBREAPER`) of what it starts. Every process of that Agent comes from its sub-spawner: the engine, its
|
|
206
|
+
`bash` and its git. Landlock denies a process of one nested domain the `/proc/<pid>/fd` and the environ of a process
|
|
207
|
+
in a sibling domain, so an Agent's code cannot drive another Agent's engine. They still share `/work`. Such an engine
|
|
208
|
+
also needs these, as mechanisms:
|
|
209
|
+
- **Authority stays in the Harness.** The engine holds no token in its env, its argv or a file. The Harness knows the
|
|
210
|
+
engine by the socket that it gave it. The Harness accepts a Menu call only when it matches a tool call of a model
|
|
211
|
+
answer that the Harness relayed, and only the first pick of that answer
|
|
212
|
+
([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). The Turn's safety hooks become decisions
|
|
213
|
+
of the relay and of the Menu server.
|
|
214
|
+
- **The model endpoint is not a proxy.** It sets the path, the model and the Dials, and refuses every other API of the
|
|
215
|
+
provider (files, batches, fine-tuning). It counts the usage into the Settlement, and it refuses a request outside a
|
|
216
|
+
Turn. The context of each request comes from jr2's record: the engine's own session file is a cache that jr2 does
|
|
217
|
+
not trust. It is never on the Harness's wire port.
|
|
218
|
+
- **Spawns go through the spawner.** An adapter starts its engine through the spawner, with the domain's env, never
|
|
219
|
+
with the Harness process's env.
|
|
220
|
+
- **No such engine on the Instance Harness.** The Instance Harness has no domain, and its control Stand-in is valid
|
|
221
|
+
for each Menu-only Agent of the Instance. A Menu-only Agent's engine runs its loop in the Harness process
|
|
222
|
+
([ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md)).
|
|
223
|
+
- **Chart Agents and Pool workers need no provisioning.** The Agents that a Chart makes
|
|
224
|
+
([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)) and each Turn of a Pool worker run in the one Harness, and
|
|
225
|
+
the Harness states their slot. The provision knows nothing of the Agents of the Sandbox.
|
|
226
|
+
|
|
227
|
+
## Considered options
|
|
228
|
+
|
|
229
|
+
- **A raw `kubectl exec -c harness` shell (the old `jr2 ssh`).** Rejected: it runs outside the Landlock domain and can
|
|
230
|
+
read `/run/jr2`, so a person's shell, and any code that the person runs in it, reaches the control Stand-in.
|
|
231
|
+
`jr2 workspace shell` is a process of the spawner's domain ([ADR-0009](0009-cli-and-instance-interface.md)).
|
|
232
|
+
- **A person's shell in a sibling domain with the same rules**, which the client makes again with the confiner, so the
|
|
233
|
+
spawner needs no pseudo-terminal and the Harness needs no socket. Rejected: the shell reaches what an Agent's `bash`
|
|
234
|
+
reaches and nothing more, but it is not in the Agents' domain. It cannot trace their processes, and at ABI 6 or later
|
|
235
|
+
it cannot signal them, so a person cannot stop a runaway dev server, which is a usual reason to open a shell.
|
|
236
|
+
- **One Harness container for each agent slot, each with its own secret Stand-in.** Rejected. Kubernetes isolates one
|
|
237
|
+
container from another, but all the slots share the writable `/work`. Code that one Agent plants there runs in a
|
|
238
|
+
sibling's container when the sibling's `bash`, git or tests run, or when a `command` Effect runs there, and that code
|
|
239
|
+
reads the credential of the container that runs it. So container isolation between Agents gives nothing against an
|
|
240
|
+
attack that goes through `/work`. It also costs a container, a port, a Secret and a share of the Size for each slot,
|
|
241
|
+
and it has no container for an Agent that a Chart makes at run time.
|
|
242
|
+
- **A trusted control container and a tools container with no credential.** Rejected. It gives the same protection as
|
|
243
|
+
confinement: no Workspace code runs where the credential is. But it adds a container, it sends each tool call across a
|
|
244
|
+
container edge, and it changes [ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md) (the
|
|
245
|
+
Harness is one server). jr2 already confines the code that an Agent wrote in the Effector (ADR-0068). One mechanism
|
|
246
|
+
serves one problem.
|
|
247
|
+
- **A second uid inside the Harness container**, so that `bash` runs as a uid that cannot read the credential. Rejected.
|
|
248
|
+
Under `runAsNonRoot`, `allowPrivilegeEscalation: false` and `capabilities: drop ALL`, no process can `setuid` without
|
|
249
|
+
`CAP_SETUID`, and under `no_new_privs` that likely also needs `allowPrivilegeEscalation: true`. It adds privilege to
|
|
250
|
+
the container that runs code a model controls.
|
|
251
|
+
- **A user namespace**, so that the Harness maps a second uid with no host privilege. Rejected: the default seccomp
|
|
252
|
+
profile (`RuntimeDefault`) blocks `unshare(CLONE_NEWUSER)` from a process with no privilege. It needs a custom seccomp
|
|
253
|
+
profile and a kernel that permits it, so it is not portable to a self-hosted cluster.
|
|
254
|
+
- **No right to list `/` and the folders above `/run/jr2`.** Rejected. `ls /`, `ls /run`, `find /` and a glob such as
|
|
255
|
+
`/*` then fail, which stops normal work. Landlock can only add rights, and a right on a folder holds on its whole
|
|
256
|
+
tree, so a right to list `/` also lists `/run/jr2`. The names there are not secrets.
|
|
257
|
+
- **The Harness process inherits the image's env.** Rejected: the image's `ENV` can hold
|
|
258
|
+
`NODE_OPTIONS=--require /app/x.js`, and `/app` can be a folder that the Sandbox's uid owns. Workspace code writes
|
|
259
|
+
`/app/x.js`, ends the Harness (below ABI 6 a signal can), and the restarted Harness runs that file with the control
|
|
260
|
+
Stand-in in its memory. A deny-list alone on the image's env is not sufficient: the next name that loads code is not
|
|
261
|
+
on it.
|
|
262
|
+
- **The Harness process keeps the domain's `HOME`, or takes its own after node starts.** Rejected: the domain writes
|
|
263
|
+
that folder, and code in the Harness process reads files there. A planted `~/.aws/config` with a `credential_process`
|
|
264
|
+
would run a program outside the domain. node reads `HOME` once, at its start: a module planted in
|
|
265
|
+
`$HOME/.node_modules` or `$HOME/.node_libraries` would run in the Harness process, as one named by `NODE_OPTIONS` or
|
|
266
|
+
`NODE_PATH` would. So the Harness gets its `HOME` when it is started.
|
|
267
|
+
- **Rules that allow only `/work`, `HOME`, `TMPDIR` and the system paths.** Rejected. It stops what an Agent does on a
|
|
268
|
+
machine: a write to a folder of the image that the uid owns (`/app`, a virtualenv, `/usr/local`), a read of `/var` or
|
|
269
|
+
`/srv`, a write to `/proc/self/oom_score_adj`. A bring-your-own image breaks in ways that its author cannot see. The
|
|
270
|
+
secrets are under one path, so a rule that denies that path is sufficient.
|
|
271
|
+
- **A new domain for each process that the Harness starts.** Rejected. Each `bash` call is then its own domain, so a
|
|
272
|
+
later call cannot signal, trace or `lsof` the server that an earlier call started. That is normal behaviour for an
|
|
273
|
+
Agent.
|
|
274
|
+
- **One spawner for each conversation, today.** Rejected while every engine runs its loop in the Harness process. Then
|
|
275
|
+
an Agent's processes hold nothing that another Agent's code can use to act as that Agent: the credential and the loop
|
|
276
|
+
are in the Harness, and each process gets the one env of the Sandbox. Through `/work`, one Agent can only try to
|
|
277
|
+
persuade another Agent's model. The separation costs a process and a layer for each conversation. It is the path for
|
|
278
|
+
an engine with its own process (above): there, a sibling in the one domain forges the engine's Menu calls at once.
|
|
279
|
+
- **The file tools do their I/O in the Harness, with a check of the path.** Rejected. It puts the rule in two places,
|
|
280
|
+
the kernel and jr2's code, and the check in code must handle each symbolic link and each race.
|
|
281
|
+
- **The control routes on a loopback port.** Rejected. A confined program can bind `127.0.0.1:<port>` while the
|
|
282
|
+
Custodian restarts, and it then receives the secret control Stand-in on the Harness's next request.
|
|
283
|
+
- **The Custodian authenticates the caller.** Rejected alone: every process reaches the Custodian over the one pod
|
|
284
|
+
loopback. The Custodian can tell the Harness from a child only by a secret that the child cannot read, which is this
|
|
285
|
+
decision.
|
|
286
|
+
- **Keep the Sandbox-scoped credential, and call the Agents of one Workspace mutually trusting.** Rejected: a coder
|
|
287
|
+
beside an independent reviewer in one Workspace is a motivating case.
|
|
288
|
+
- **Refuse all of `169.254.0.0/16` at the egress.** Rejected: NodeLocal DNSCache answers on `169.254.20.10`, so the pods
|
|
289
|
+
of a cluster that runs it lose DNS.
|
|
290
|
+
- **An egress policy with `ipBlock` peers only.** Rejected: a CNI that applies an `ipBlock` to the address of a pod
|
|
291
|
+
(Cilium) then cuts DNS, the Orchestrator and each Service. So the rule also allows every pod of the cluster.
|
|
292
|
+
- **The egress policy on the Sandbox pods only.** Rejected: the check pod of the provider preflight stands for every
|
|
293
|
+
Harness pod. Without the policy there, a model that only the node's credential reaches passes the check and fails in a
|
|
294
|
+
Turn.
|
|
295
|
+
|
|
296
|
+
## Consequences
|
|
297
|
+
|
|
298
|
+
- **No Workspace code can act as an Agent.** Planted code finds no control credential: not in its env, not in a file
|
|
299
|
+
that it can read, not through the Harness's `/proc`. It cannot deliver a pick for any Agent, its own included. Only
|
|
300
|
+
the Harness process delivers a pick, for the conversation whose model called the pick.
|
|
301
|
+
- **What remains.**
|
|
302
|
+
- An Agent can still try to persuade another Agent through what that Agent reads: a file, a commit message, the output
|
|
303
|
+
of a test. That is an attack on content, not a forged pick. The other Agent's model chooses each pick, and the
|
|
304
|
+
Orchestrator judges it against that Turn's Allowed picks
|
|
305
|
+
([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)).
|
|
306
|
+
- A confined child can spend only a held secret that its `workspace()` grants. Every Stand-in but the fetch Stand-in
|
|
307
|
+
is secret, and the child's env holds only the granted ones (ADR-0059). So code in `/work` reaches a model key only
|
|
308
|
+
when the author grants that key.
|
|
309
|
+
- Below Landlock ABI 6, a confined process can still send a signal to the Harness process, which has the same uid. So
|
|
310
|
+
the Harness starts with no core file (`effector nocore`, a core limit of zero), and with Node's `--disable-sigusr1`.
|
|
311
|
+
Without them, a dumping signal writes the Harness's memory, with its Stand-ins, to a file in its working folder, and
|
|
312
|
+
SIGUSR1 opens Node's inspector on the pod loopback, which runs the child's code in the Harness. What a signal can
|
|
313
|
+
still do is stop or end the Harness. That is a denial of service in the Agent's own Sandbox, not an impersonation.
|
|
314
|
+
The Harness names its process `jr2-harness`, so an Agent's `pkill node` does not end it by accident. ABI 6 scopes
|
|
315
|
+
signals and abstract Unix sockets to the domain, and the confiner uses both scopes when the kernel offers them
|
|
316
|
+
(ADR-0077): the Harness is then outside each signal from the domain.
|
|
317
|
+
- The domain can connect to the Custodian's control socket. The Custodian refuses each request that does not carry the
|
|
318
|
+
control Stand-in, which no process of the domain holds.
|
|
319
|
+
- The domain can see that a path under `/run/jr2` exists, by its name or by `stat`, and its size. It cannot read what
|
|
320
|
+
is in it.
|
|
321
|
+
- The node's cloud credential is closed only at the metadata addresses that jr2 knows, and only on a CNI that enforces
|
|
322
|
+
NetworkPolicy (the decision above).
|
|
323
|
+
- **The pod is ADR-0005's pod**: one Harness, a Custodian, and an optional User Container and Sandbox Effector. The
|
|
324
|
+
count of Agents does not change it, and the Size splits as before
|
|
325
|
+
([ADR-0060](0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md)).
|
|
326
|
+
- **The Instance Harness runs no Workspace code.** A Menu-only Agent has no Working tools (ADR-0031), so the credential
|
|
327
|
+
of the Instance Harness is already where no Workspace code runs.
|
|
328
|
+
- **Tests.** The `@kind` tier proves the boundary from the model's own `bash`: it finds no Stand-in of the pod in its
|
|
329
|
+
env, in a file or through the Harness's `/proc`; a test that the coder plants and the reviewer's own `node --test`
|
|
330
|
+
runs cannot deliver a pick as the reviewer, and the run does not settle; a granted held secret works, and no model key
|
|
331
|
+
is in the env; a second Agent runs git on its Worktree, and the step checks the output. It also proves that the domain
|
|
332
|
+
does not stop normal work: a server that one `bash` call starts is found and ended by a later call; a write to a
|
|
333
|
+
folder of the image that the uid owns, outside `/work`, succeeds; a move of a file into another folder succeeds;
|
|
334
|
+
`ls /` and `ls /run` succeed; and `read` reads the full output of a `bash` call. It proves the metadata block: kind
|
|
335
|
+
has no metadata service, so the step gives `169.254.169.254` to the node, and a pod that the policy does not select
|
|
336
|
+
gets the node's refusal there. The Agent's `bash` does not, and it still connects to the model's host and to the
|
|
337
|
+
Orchestrator's Service. A unit test pins the policy that `jr2 up` composes. The unit and conformance tests refuse the
|
|
338
|
+
control token on the fetch route, the fetch token on an agent route, a stated slot that is not the registration's, and
|
|
339
|
+
both tokens at a Gate, and they check the slot on the delivery poll. A test reads the Harness container's mounts from
|
|
340
|
+
the pod spec. It fails on a mount that is not read-only, not under `/run/jr2`, and not the Workspace's own (`/work`,
|
|
341
|
+
`HOME`), so a new mount is classified before it ships. Unit tests start node through `effector nocore --harness-env`
|
|
342
|
+
with a `HOME` that holds a planted `.node_modules`, and check that node looks for no module there; they also retarget
|
|
343
|
+
a link onto `/run/jr2`, restart the spawner, and check that the new domain reads nothing there. The `@kind` tier reads
|
|
344
|
+
the Harness process's environ from outside the pod and checks its `HOME`.
|
|
345
|
+
- **The `@model` tier runs after the build.** The change alters what the Working tools can do and where a notice is
|
|
346
|
+
delivered ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)), so it is a `@model`-tier change
|
|
347
|
+
([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# jr2 requires Landlock on every Sandbox node
|
|
2
|
+
|
|
3
|
+
Two boundaries of jr2 are Landlock domains. [ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)
|
|
4
|
+
confines the child of a credentialed `command` in the Effector, so that the Agent's code cannot read the Effector's key
|
|
5
|
+
or a secret that the command does not name.
|
|
6
|
+
[ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md) confines each process that the
|
|
7
|
+
Harness starts in `/work`, so that no Workspace code can read the control credential and act as an Agent. On a node with
|
|
8
|
+
no Landlock, neither boundary exists. A path that runs there with less confinement is a second jr2, with a weaker
|
|
9
|
+
boundary, to support and to test.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
- **Landlock is a requirement of jr2 on every Sandbox node.** It holds where Workspace code runs, which is also where
|
|
14
|
+
the check reaches. No component has an unconfined branch, anywhere.
|
|
15
|
+
- **The Repo cache agent detects it, on each Sandbox node.** The cache agent's DaemonSet has one pod on each Sandbox
|
|
16
|
+
node, by the same `nodeSelector` and `tolerations` as the Sandboxes
|
|
17
|
+
([ADR-0052](0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md)). The agent probes Landlock
|
|
18
|
+
on its node. On a node without it, the agent is NotReady, and its status gives the reason.
|
|
19
|
+
- **`jr2 up` refuses to converge while a Sandbox node lacks Landlock.** It reads the DaemonSet's pods (one list call),
|
|
20
|
+
and it names each Sandbox node whose cache agent reports no Landlock
|
|
21
|
+
([ADR-0019](0019-one-converging-command-against-the-current-context.md)). The admin keeps a bad node out of the set
|
|
22
|
+
with `sandbox.nodeSelector` or a taint, as ADR-0052 says. There is still no jr2 node label.
|
|
23
|
+
- **The refusal is an ordinary converge failure** (ADR-0019). It comes within the cache agent's rollout wait, which
|
|
24
|
+
`jr2 up` already makes. It names each Sandbox node without Landlock, the probe's reason, and the fix: keep the node
|
|
25
|
+
out with `sandbox.nodeSelector` or a taint (ADR-0052), or enable Landlock in its kernel. A run of `jr2 up` after the
|
|
26
|
+
fix converges.
|
|
27
|
+
- **Each confining component probes at its start, and refuses to serve without it.** These are the Harness of a Sandbox
|
|
28
|
+
and the Sandbox Effector. The probe is `landlock_create_ruleset` with `LANDLOCK_CREATE_RULESET_VERSION`. This is the
|
|
29
|
+
backstop for a node that joins after the converge.
|
|
30
|
+
- **No component keeps a path for a node that cannot confine a child.** ADR-0068's Effector refuses to start on such a
|
|
31
|
+
node, whatever its Workspace holds.
|
|
32
|
+
- **The minimum ABI is 3.** Under ABI 1, a confined process can never move or link a file into another folder, whatever
|
|
33
|
+
the rules allow, and package managers, git and `mv` do that. ABI 2 adds the `REFER` right, so the domain can do it
|
|
34
|
+
(ADR-0076). Under ABI 2, Landlock does not handle `truncate(2)`, so a confined process can truncate a file that the
|
|
35
|
+
rules let it do nothing else to: a conversation under `/run/jr2`, which the Harness rebuilds an Agent's record from.
|
|
36
|
+
ABI 3 adds the `TRUNCATE` right, and the confiner always handles it. The confiner uses the scopes of abstract Unix
|
|
37
|
+
sockets and of signals (ABI 6) when the kernel offers them. Each probe refuses a kernel below ABI 3, with the ABI that
|
|
38
|
+
it found.
|
|
39
|
+
- **The components that probe.** The cache agent probes as the detector, though it confines nothing. The Harness of a
|
|
40
|
+
Sandbox and the Sandbox Effector probe because they confine. The Instance Harness and the Instance Effector confine
|
|
41
|
+
nothing: a Menu-only Agent has no Working tools ([ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md)),
|
|
42
|
+
and the Instance Effector runs no `command`. They do not probe.
|
|
43
|
+
|
|
44
|
+
## Considered options
|
|
45
|
+
|
|
46
|
+
- **A minimum of ABI 1.** Rejected: under it no confined process can move a file into another folder, which stops normal
|
|
47
|
+
work.
|
|
48
|
+
- **A minimum of ABI 2.** Rejected: under it the domain can truncate a file under `/run/jr2`. One Agent's code can then
|
|
49
|
+
erase or roll back another Agent's conversation, and below ABI 6 it can end the Harness so that the Harness rebuilds
|
|
50
|
+
from the cut file. It admits Linux 5.19 to 6.1 (Debian 12's stock kernel among them), for a boundary with a hole.
|
|
51
|
+
- **Refuse only a Sandbox with more than one Agent on a node without Landlock.** Rejected: it keeps a less secure
|
|
52
|
+
branch, which jr2 must then support and test.
|
|
53
|
+
- **Run unconfined, with a warning.** Rejected: Workspace code can then read the control credential, and a warning does
|
|
54
|
+
not stop it.
|
|
55
|
+
- **A check pod on each node at each `jr2 up`.** Rejected: it starts one pod for each node on every converge, and it
|
|
56
|
+
misses a node that joins later.
|
|
57
|
+
- **One check pod, with the start-up probe as the backstop.** Rejected: a bad node shows only when a run lands there and
|
|
58
|
+
fails.
|
|
59
|
+
- **jr2 labels the nodes that have Landlock, and a Sandbox requires the label.** Rejected: it reverses ADR-0052, and it
|
|
60
|
+
needs a cluster-wide permission to patch Nodes.
|
|
61
|
+
|
|
62
|
+
## Consequences
|
|
63
|
+
|
|
64
|
+
- **The kernels that this excludes.** Landlock ABI 3 is in Linux 6.2 and later. A node with an older kernel cannot run
|
|
65
|
+
jr2: for example RHEL 8 (4.18), Debian 11 (5.10), Debian 12 on its stock kernel (6.1), Ubuntu 20.04 and 22.04 on their
|
|
66
|
+
first kernels (5.4, 5.15), and Amazon Linux 2 (4.14 or 5.10). A distribution kernel can carry a backport: the probe,
|
|
67
|
+
not the version, decides. A newer kernel must also enable Landlock: its list of security modules (`CONFIG_LSM`, or the
|
|
68
|
+
`lsm=` boot parameter) must name `landlock`. On a node, `/sys/kernel/security/lsm` lists the modules that run.
|
|
69
|
+
Measured from a hardened pod with no capabilities: kind on WSL2 (6.6) offers ABI 3; the home cluster's arm64 nodes
|
|
70
|
+
(6.8), ABI 4; spark (6.11, arm64), ABI 5.
|
|
71
|
+
- **kind on a host inherits the host's kernel.** A kind node is a container, so the kernel of the host decides. On macOS
|
|
72
|
+
or Windows, Docker runs the nodes in a virtual machine, and the kernel of that machine decides.
|
|
73
|
+
- **A mixed cluster.** `jr2 up` names each Sandbox node without Landlock and refuses until the admin keeps it out of the
|
|
74
|
+
set. A node that joins after the converge shows as a NotReady cache agent. A Sandbox placed on it fails at its start,
|
|
75
|
+
because its Harness refuses to serve.
|
|
76
|
+
- **The check runs when a Machine uses a Sandbox.** `jr2 up` converges the cache agent then
|
|
77
|
+
([ADR-0051](0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md)). With no Sandbox, no node needs
|
|
78
|
+
Landlock, and there is nothing to check.
|
|
79
|
+
- **The docs list the requirement** where they list what jr2 needs from a cluster and from a host.
|