@jr2/orchestrator 0.6.0 → 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 +86 -9
- package/docs/concepts/architecture.md +41 -28
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +7 -5
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +4 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +50 -22
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +5 -4
- package/docs/concepts/pools-and-sources.md +187 -11
- package/docs/concepts/secrets-and-trust.md +73 -33
- package/docs/concepts/the-menu.md +11 -0
- package/docs/concepts/workspaces.md +6 -5
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/guides/add-tools-to-the-sandbox.md +55 -11
- 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 +24 -8
- 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/fix-a-failed-converge.md +18 -6
- 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 +9 -6
- package/docs/guides/look-inside-a-sandbox.md +101 -31
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-one-worker-per-item.md +15 -9
- package/docs/guides/set-an-agents-access-and-dials.md +2 -0
- 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 +1 -1
- package/docs/guides/watch-a-run.md +127 -121
- 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 +17 -7
- package/docs/llms.txt +81 -15
- package/docs/reference/authoring-surface/agent.md +1 -1
- package/docs/reference/authoring-surface/answer.md +1 -1
- package/docs/reference/authoring-surface/arg.md +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/file.md +1 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +1 -1
- package/docs/reference/authoring-surface/jr2setup.md +13 -3
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +5 -3
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +1 -1
- package/docs/reference/authoring-surface/pool.md +77 -9
- package/docs/reference/authoring-surface/push.md +1 -1
- package/docs/reference/authoring-surface/readyof.md +1 -1
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +1 -1
- 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 +56 -1
- 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 +14 -1
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
- package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-binding.md +1 -1
- 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-commandoutput.md +1 -1
- package/docs/reference/authoring-surface/type-customize.md +16 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-framepath.md +1 -1
- 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 +1 -1
- package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
- 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-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
- 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 +1 -1
- 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-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-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 +16 -7
- 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 +1 -1
- package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
- package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
- package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
- package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
- package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
- package/docs/reference/authoring-surface/workspace.md +1 -1
- package/docs/reference/authoring-surface/worktree.md +1 -1
- package/docs/reference/authoring-surface.md +88 -167
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +235 -2
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
- package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
- 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 +9 -2
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
- 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 +18 -14
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
- 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 +32 -18
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
- 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 +116 -34
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
- 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 +7 -6
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
- 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 -18
- 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 +4 -3
- 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 +5 -4
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- 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 +7 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
- 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 +51 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
- 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 +17 -13
- 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/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
- 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 +6 -4
- package/docs/reference/glossary.md +96 -42
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +976 -149
- package/docs/why.md +2 -1
- package/package.json +10 -3
- package/src/actor.ts +36 -10
- package/src/agent.ts +50 -0
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +52 -7
- package/src/config.ts +550 -1
- package/src/conversations.ts +453 -0
- package/src/custodian.ts +8 -2
- package/src/customize.ts +143 -4
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +2 -1
- package/src/effect.ts +60 -7
- package/src/effector-wire.ts +272 -27
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +10 -4
- package/src/held-secrets.ts +72 -23
- package/src/holds.ts +2 -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 +76 -38
- package/src/http.ts +889 -94
- package/src/index.ts +16 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +22 -3
- package/src/machine-doc.ts +26 -1
- package/src/mcp-effect.ts +424 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +64 -2
- package/src/notices.ts +138 -10
- package/src/open-body.ts +25 -11
- package/src/open-effect.ts +17 -4
- package/src/open.ts +4 -2
- package/src/parts.ts +87 -9
- package/src/party.ts +2 -1
- package/src/pick.ts +2 -2
- package/src/pool.ts +1142 -163
- package/src/registration.ts +203 -13
- package/src/run-host.ts +1509 -171
- package/src/run-list.ts +191 -0
- package/src/sandbox-kube.ts +53 -5
- package/src/secret-placement.ts +239 -33
- package/src/server.ts +53 -4
- package/src/setup.ts +72 -32
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +263 -22
- package/src/wire.ts +17 -0
- package/src/workspace.ts +50 -6
- package/src/worktree.ts +3 -3
- 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
|
@@ -58,7 +58,7 @@ lifecycle stay implemented once, in the registration table.
|
|
|
58
58
|
|
|
59
59
|
## External side: the same primitive over HTTP (`gate`), and each gate is a resource
|
|
60
60
|
|
|
61
|
-
`gate` is the symmetric actor for **every non-agent caller** — humans (`jr2 send`,
|
|
61
|
+
`gate` is the symmetric actor for **every non-agent caller** — humans (`jr2 send`, the Console), a Hook, CI, other
|
|
62
62
|
systems. "Human" is policy, not mechanism, so the actor is not named for one caller. Each invocation is an addressable
|
|
63
63
|
**gate**: input is `{ gate?, meta? }` — `gate` an optional authored id (the id is derived when absent — below), `meta`
|
|
64
64
|
serializable caller/integration context (PR URL, title); its accepted set derives from the gated state's transitions,
|
|
@@ -72,11 +72,29 @@ scopes event resolution), so `gate: "F-12"` in two concurrent runs cannot collid
|
|
|
72
72
|
Gate-as-resource is forced by concurrency: several children park simultaneously (three features in `humanReview` is the
|
|
73
73
|
_normal_ case), all accepting `approve` — a run-level POST is ambiguous. And every real external surface already thinks
|
|
74
74
|
in gates: a CLI listing shows pending decisions and acts on one; a UI inbox renders one card per gate (schemas drive the
|
|
75
|
-
forms, `meta` drives the copy); a
|
|
76
|
-
why `meta` exists. Generalizing beyond humans also closes a hole hard-coded event types left: a push
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
forms, `meta` drives the copy); a hooked Gate's own handler matches a forge's delivery against its `meta` (e.g. `pr`,
|
|
76
|
+
below), which is why `meta` exists. Generalizing beyond humans also closes a hole hard-coded event types left: a push
|
|
77
|
+
seam like "a Hook brings an item" is a Pool's `source` Gate ([ADR-0017](0017-pool-over-a-source-port.md)). The cross-run
|
|
78
|
+
inbox is `GET /gates`: the open Gates of every live run that the Caller may answer, oldest first, with a `workflow`
|
|
79
|
+
filter, each entry `{ runId, workflow, gate, path, accepts, acceptsNow, meta, openedAt, hook? }` (`hook` names the Hook
|
|
80
|
+
kinds that the Gate hears, when it hears one) and never the run's `context`
|
|
81
|
+
([ADR-0014](0014-observation-is-open-run-state-is-not.md)). A Gate that takes no event from a Caller, as a Pool's
|
|
82
|
+
`source` Gate that hears only Hooks, is not in it: no person can answer it. `jr2 gates [--workflow]` prints it, and the
|
|
83
|
+
Console's Gate cards read it. A stream of it is a later addition. CLI segment-matching over derived gate ids
|
|
84
|
+
(`--gate F-12` resolving an unambiguous segment, git-style like run-id prefixes) is deferred.
|
|
85
|
+
|
|
86
|
+
**The `answer` scope can name single Gates**, in the form of
|
|
87
|
+
[ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md). It names a Gate by its invoke
|
|
88
|
+
id, the one static name a Gate has in the Machine that invokes it, and the id that its events carry as their sender
|
|
89
|
+
(below): the invoke `id` that the author wrote, or else the key path of its gated state below the root of that Machine
|
|
90
|
+
(`body.humanReview`), with `.0`, `.1` after it when that state invokes more than one unnamed Gate. So `body.humanReview`
|
|
91
|
+
names that Gate in every child of a Pool, and two gated states with one key at two paths of one Machine have two names.
|
|
92
|
+
The name is not unique across the Workflow: a Workflow composes Machines by invoke
|
|
93
|
+
([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)), and two composed Machines that each gate a
|
|
94
|
+
state `approve` give that name to two Gates. One per-Gate name covers every Gate of the Workflow that has that invoke
|
|
95
|
+
id, and `jr2 up` refuses a per-Gate name that matches Gates in more than one Machine of the Workflow, so a scope never
|
|
96
|
+
opens a Gate that its author did not see; the author writes an invoke `id` on one of them to tell them apart. It is not
|
|
97
|
+
the caller-facing gate id, which is different in each child and which a workflow may compute.
|
|
80
98
|
|
|
81
99
|
**The gate id is derived; authoring one is the exception.** Absent an authored `gate`, the id is the gate actor's own
|
|
82
100
|
path below the run root, and jr2Setup's menu-derivation walk names an unnamed gate invoke with its state key path so the
|
|
@@ -102,15 +120,202 @@ vocabulary — workflows speak only `defineEvent` / `agentRun` / `gate`.
|
|
|
102
120
|
|
|
103
121
|
**Every delivered event carries its sender.** The table writes `sender: { party: "agent" | "gate", id }` on each event
|
|
104
122
|
it delivers, after the schema parse and from the registration alone: `id` is the Agent's slot, or the Gate's invoke id
|
|
105
|
-
(static in the Machine, the same after a restore — not the caller-facing gate id, which a workflow may compute).
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
(static in the Machine, the same after a restore — not the caller-facing gate id, which a workflow may compute). A
|
|
124
|
+
Gate's event also carries the **Caller** that answered it
|
|
125
|
+
([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)): `via`, `name` and
|
|
126
|
+
`onBehalfOf`, with `checked` naming the fields that jr2 verified. `via` and `name` are always checked, because an
|
|
127
|
+
authenticator made them; `onBehalfOf` is checked only for a Hook, whose signature covers the payload it comes from. The
|
|
128
|
+
run records the same Caller as the answer's `answeredBy`. An event that a Gate's `look` gives (below) has the Gate as
|
|
129
|
+
its Sender and no Caller. The Sender is plain data on the event, so it rides the mailbox, a delayed event the run host
|
|
130
|
+
persists beside the snapshot, and a restore; a restored registration writes it again from the persisted input and the
|
|
131
|
+
invoke id. The Machine's own events (`raise`, `sendTo` itself) carry none. `defineEvent` refuses an input that declares
|
|
132
|
+
`sender`, and both surfaces refuse a body that carries it (ADR-0013): no caller and no def can write it. What an author
|
|
133
|
+
does with it is ADR-0015's.
|
|
110
134
|
|
|
111
135
|
`CANCEL` stays reserved as the run-level infra interrupt. jr's "exit 3 and let the human drive the bash script" becomes
|
|
112
136
|
"park in a durable state that advertises its human surface."
|
|
113
137
|
|
|
138
|
+
## A Gate hears a Hook, and looks before it waits
|
|
139
|
+
|
|
140
|
+
A Hook ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)) is a door for one
|
|
141
|
+
outside service. Its delivery is not an event of any Machine: a Gate makes one of it, or ignores it. So a hooked Gate is
|
|
142
|
+
an extension of the Gate, not a second concept. It has the same derived id, accepted set, Sender, Console card and
|
|
143
|
+
`jr2 send` path as any Gate. The Hook checks the delivery and decides nothing; only a Gate's handler or its look makes
|
|
144
|
+
an event for a run.
|
|
145
|
+
|
|
146
|
+
- **A Gate slot declares what it hears.** `actors: { review: gate({ meta, delivers, hooks, look, reply }) }`, and the
|
|
147
|
+
state invokes it by its key:
|
|
148
|
+
`invoke: { src: "review", input: ({ context }) => ({ meta: { pr: context.pr, sha: context.sha } }) }`. A plain Gate
|
|
149
|
+
stays `src: "gate"`. `meta` is a zod schema of the meta that the invoking state sets. The Gate checks the meta when it
|
|
150
|
+
opens, and the handlers and the look read it typed. It is a schema and not a type argument, because TypeScript does
|
|
151
|
+
not infer `delivers` when an author names one type argument.
|
|
152
|
+
- **`delivers` lists the events that the handlers and the look can give.** The load walk refuses one that is not in the
|
|
153
|
+
Gate's derived accepted set. The Chart door refuses one that is not in the Chart's Vocabulary
|
|
154
|
+
([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)). An event in `delivers` is `audience: "external"` unless
|
|
155
|
+
its def sets an audience, so an Agent below the Gate never has it on its Menu, and the author does not have to know
|
|
156
|
+
this ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)). This default can change what a Menu shows, so the
|
|
157
|
+
change that builds it runs the `@model` tier
|
|
158
|
+
([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)).
|
|
159
|
+
- **A handler for each Hook kind and event key.** `hooks: { github: { "pull_request_review.submitted": handler } }`. The
|
|
160
|
+
key is the kind's id and the event key that the kind reads from the delivery (ADR-0078), never a Hook's name: two
|
|
161
|
+
Hooks of one kind (two organizations, two secrets) reach the same handlers. The table is typed at compile time: each
|
|
162
|
+
kind module that the program imports adds its id and its payload types to `interface HookKinds` by declaration
|
|
163
|
+
merging, and a key that no kind in scope gives does not compile. There is no registry at run time: the door matches a
|
|
164
|
+
delivery by the kind's id and the event key, exactly. A kind's schema key can end in `.*` (`"block_actions.*"`) to
|
|
165
|
+
type each key with that prefix, but a handler names the full key (`"block_actions.approve"`): a handler key that holds
|
|
166
|
+
`*` does not compile, and `gate()` and `source()` refuse it, so the load walk refuses its Workflow. No handler hears a
|
|
167
|
+
handshake. A handler reads `{ payload, meta, caller }`, where `payload` is the decoded body after the kind's schema
|
|
168
|
+
parsed it (below). Its type is the schema's output, so a handler reads only the fields that the schema names. It is
|
|
169
|
+
pure and synchronous, as a guard is: no I/O, no Promise, no clock. It matches the payload against the Gate's `meta`
|
|
170
|
+
(for a review, the PR's url and the pushed commit) and gives one plain event of `delivers`, such as
|
|
171
|
+
`{ type: "approve", by }`, typed by that list, or `undefined` to ignore the delivery. The Orchestrator parses the
|
|
172
|
+
event with its def's schema and asks the run whether it takes it from this Gate now, as it does for `jr2 send`
|
|
173
|
+
(ADR-0013). There are no correlation keys: each delivery calls every open handler of its kind and event key.
|
|
174
|
+
- **A look reads the fact that the Gate waits for.** `look: { read, input, every?, until }`. `read` is an `http` Effect
|
|
175
|
+
marked `idempotent: true`, or a promise actor, which a Machine can invoke anyway; `gate()` refuses another Effect
|
|
176
|
+
([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)). The promise is for
|
|
177
|
+
the default test tier, which has no Instance Effector, and for a fact inside the Instance. A read of a service outside
|
|
178
|
+
the Instance is an `http` read, because the Orchestrator makes no call out of the Instance for a run. The read is a
|
|
179
|
+
child of the Gate in the run's actor system, so an `http` read goes through the run's Instance Effector and its
|
|
180
|
+
ledger, and `jr2 up` places it as it places an Effect slot. The look runs once after the Gate registers: a change
|
|
181
|
+
before the read is in its answer, and a change after the Gate registered reaches the Gate as a delivery. It runs again
|
|
182
|
+
after each restore, and every `every` (10 minutes by default) while the Gate is open. `until` gives an event of
|
|
183
|
+
`delivers`, or `undefined` for "not yet". So a delivery that came before the Gate opened, a delivery lost while the
|
|
184
|
+
Orchestrator was down, and a restart all come to the same read, and the author writes no idiom for them. The run takes
|
|
185
|
+
a looked event only when it takes it from this Gate now, as for `jr2 send`. A read that fails, an `until` that throws,
|
|
186
|
+
and an event that the run does not take now are each a line in the Orchestrator's log, and the look reads again at the
|
|
187
|
+
next `every`: a look never faults the run. A looked event has the Gate as its Sender and no `answeredBy`: the look is
|
|
188
|
+
the Machine's own read, not a Caller. The per-run feed marks it `looked`, behind the token as a Turn marker is, before
|
|
189
|
+
the delivery, because a delivery can end the run in its own send. The look stops after the run takes its event, until
|
|
190
|
+
the Gate opens again, because a look reads a level: a Gate that stays open after its event would get the same event
|
|
191
|
+
every `every`. So a state that goes back to its own Gate must change the `meta` (a new `sha`), or it loops. A restore
|
|
192
|
+
opens the Gate again, so a Gate that stayed open after its event looks one more time after a restore.
|
|
193
|
+
- **`look: "none"` for a Gate that hears a command.** A `/retry` comment is an edge, not a level, so there is no fact to
|
|
194
|
+
read. A lost delivery of a command stays lost.
|
|
195
|
+
- **`jr2 up` warns about a hooked Gate with no look and no `after` on its state**, and does not refuse: a delivery lost
|
|
196
|
+
in an outage then stops the run until a person answers the Gate. The fix is one line, a look or `look: "none"`.
|
|
197
|
+
- **A bad delivery marks the feed and never faults the run.** A handler that throws, an event that is not in `delivers`,
|
|
198
|
+
and an event that the run does not take now each put a `hook-refused` mark on the run's feed and a line in the
|
|
199
|
+
answer's `refused`. The run goes on. A refusal that the author wants the person to read is a throw in the handler,
|
|
200
|
+
with the reason as its message.
|
|
201
|
+
- **`reply` gives the text that the person who acted reads**, for a kind whose answer a person reads (`slack`,
|
|
202
|
+
`discord`; not `github`, and not a Slack event). `reply(outcome, { meta, caller })` is pure and synchronous, as a
|
|
203
|
+
handler is. `outcome` is that Gate's own: `delivered` with the event, or `refused` with the reason. It gives plain
|
|
204
|
+
text or `undefined`, never a service's JSON, and the kind escapes the text and puts it in its envelope (below). It
|
|
205
|
+
runs after the run answers, because only then is `refused` known, so the text is not in the handler's return. A Gate
|
|
206
|
+
whose handler gave `undefined` was not a target, and its `reply` does not run: a click on one message does not make
|
|
207
|
+
the other open Gates of that key speak. A throw gives no text, and the hook log keeps the error. Only the person who
|
|
208
|
+
acted sees the text. A message that everyone sees is a change of shared state, so it is an Effect of the Machine. So
|
|
209
|
+
is the reply after the work: a plain `http` Effect with a bot token, such as Slack's `chat.postMessage`, never the
|
|
210
|
+
delivery's `response_url`, which takes 5 posts in 30 minutes.
|
|
211
|
+
- **A packaged Machine writes its handlers and leaves its look Open.** A forge's payload is public, so a package writes
|
|
212
|
+
the handlers for each forge that it supports, and handlers are never Open. It imports the kinds that it hears, so
|
|
213
|
+
their payload types are in scope where it is compiled. A look needs a url and a token, so a package leaves
|
|
214
|
+
`look: open` ([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)). The composer
|
|
215
|
+
binds it with `customize(m, { gates: { review: { look, hooks } } })`, which also adds or replaces a handler by kind
|
|
216
|
+
and event key; `null` removes one. An Open look that nobody binds gets the warning above, not a refusal, so a person
|
|
217
|
+
can still answer the Gate with `jr2 send`. A forge part gives the handlers and the look for one fact (a review
|
|
218
|
+
verdict, CI on a commit). It takes event defs and secrets, never a Hook's name, and each verdict compares the commit,
|
|
219
|
+
not only the PR. It types its handlers by the kind's schemas, so a field that it reads is a field that the kind
|
|
220
|
+
parses.
|
|
221
|
+
- **A parked run keeps the handlers, the look and the `reply` of the Assembly that it holds**
|
|
222
|
+
([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)): they are code in the
|
|
223
|
+
Assembly, and the Gate's input stays data, so a restore registers the Gate again as before.
|
|
224
|
+
|
|
225
|
+
## The Hook's door
|
|
226
|
+
|
|
227
|
+
- **`POST /hooks/:name`.** Each Hook that `jr2.config.ts` declares has one stable address, which a service is set up
|
|
228
|
+
with once. The Hook's verifier authenticates the route, outside the bands of ADR-0014 (ADR-0078). A delivery that
|
|
229
|
+
fails it, or whose signed timestamp is more than 300 seconds from the door's clock, gets 401, and nothing else
|
|
230
|
+
happens. The window is jr2's, the same for every kind. A name that the current config does not declare gets 404. With
|
|
231
|
+
no current Assembly the door answers 503 before the verifier, because no config says which Hooks exist. A body above 2
|
|
232
|
+
MiB gets 413, after the name and before the verifier. The door reads no more of the body than the limit, because this
|
|
233
|
+
is the one route with no bearer token that takes a body, and a `mac` check sends the body to the Instance Effector as
|
|
234
|
+
base64, which its wire limits to 4 MiB. Without the limit, a large delivery that is correctly signed would get a
|
|
235
|
+
false 503. The hook log keeps a `too-large` line, which claims no delivery id. A `mac` check that the Instance
|
|
236
|
+
Effector cannot answer (none is deployed, or no replica answers) is 503 too, so the service can send again: the
|
|
237
|
+
Orchestrator holds no secret to check with
|
|
238
|
+
([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)).
|
|
239
|
+
- **The kind's handshake comes after the verifier.** A Slack `url_verification` gets `{ "challenge": … }`, and a Discord
|
|
240
|
+
`PING` gets `{ "type": 1 }`. A handshake reaches no handler, and the hook log keeps it as `handshake`. A request that
|
|
241
|
+
the kind cannot read (its `read` throws, or gives no id or no event key) gets 400, and the hook log keeps the error as
|
|
242
|
+
`unreadable`.
|
|
243
|
+
- **The door parses the payload before the fan-out.** A Hook kind gives a zod schema for each event key, and the door
|
|
244
|
+
parses the decoded body with the schema of the delivery's key. Each object in a schema is a loose object, and a schema
|
|
245
|
+
names only the fields that handlers need. So a field that the service adds breaks nothing, and a named field that is
|
|
246
|
+
missing or of the wrong type fails the parse. A failed parse reaches no handler. The hook log keeps a
|
|
247
|
+
`payload-refused` line with zod's issues, and the answer names the kind and the event key in
|
|
248
|
+
`payloadRefused: { kind, key, issues }`. The status is 200, because a second try fails the same way. A key that the
|
|
249
|
+
kind gives no schema for is not parsed, and it reaches nothing, because no handler can hear it.
|
|
250
|
+
- **A repeat is found by the delivery id AND the body's digest.** A delivery whose id or digest the hook log holds gets
|
|
251
|
+
200 with `seen: true`, and nothing is delivered. The check is for each Hook, because two Hooks can get one body (an
|
|
252
|
+
organization's webhook and a repository's). Only a delivery that the door claimed counts: a handshake, a
|
|
253
|
+
`payload-refused` line, an `unreadable` line, a `hold-full` line (a 429, below) and a `no-live-run` line (a 503,
|
|
254
|
+
below) never make a repeat, so a refused payload that comes again is parsed again, and a delivery that every Source
|
|
255
|
+
was too full to hold is heard again. The check and the claim are one synchronous step, so of two copies that come at
|
|
256
|
+
once, one is delivered. A service may or may not send a delivery again; the design depends on neither, because the
|
|
257
|
+
look finds what a lost delivery said.
|
|
258
|
+
- **The fan-in reaches open Gates only.** A delivery goes to every open Gate, in every live run of a Workflow that the
|
|
259
|
+
Hook may answer, whose handlers hear its kind and event key, and to nothing else. A Pool's `source` Gate is one of
|
|
260
|
+
them ([ADR-0017](0017-pool-over-a-source-port.md)). So a Hook answers a Gate of a run, or brings items to a Source. It
|
|
261
|
+
never starts a run, and it never names one.
|
|
262
|
+
- **The answer is the outcome.** 200 with
|
|
263
|
+
`{ delivery, seen, delivered: [{ run, workflow, gate, event }], refused: [{ run, gate, event?, reason }], ignored, sources, payloadRefused? }`:
|
|
264
|
+
`ignored` counts the open Gates whose handler gave nothing, and `sources` holds each Source's verdict (ADR-0017). A
|
|
265
|
+
delivery that reached nothing gets 200 with `delivered: []`. After the verifier, the fan-out always runs first. Then
|
|
266
|
+
the answer is 429 when every Source's verdict is `hold-full`, and 503 only in two cases: a standing Pool that hears
|
|
267
|
+
this kind and event key has no live run and is not paused, or the delivery followed a standing Pool's hand-overs to
|
|
268
|
+
the new run's Source 3 times and the Pool handed its run over again (ADR-0017). In the second case the Pool can have a
|
|
269
|
+
live run: the delivery stops following it, so that it cannot follow without end. Neither 503 claims the delivery's id,
|
|
270
|
+
so the service's send again is heard. The 503s that come before the verifier are in the door's rule above. So a Pool
|
|
271
|
+
in back-off does not stop the review Gates of other runs. This is the answer of a kind whose answer no person reads: a
|
|
272
|
+
forge, or a Slack event, which keeps its 429 and 503 so that Slack sends it again.
|
|
273
|
+
- **A person reads the answer to a chat command or click.** For a Slack command or click and a Discord interaction, the
|
|
274
|
+
kind's envelope takes the place of the outcome, and the hook log keeps the outcome. The text joins the `reply` of
|
|
275
|
+
every target: Sources first (what the act brought), then Gates (what the act answered), and in each by Workflow name,
|
|
276
|
+
then by run id. Empty texts and exact repeats are dropped. A text from a `skipped` verdict shows only when no other
|
|
277
|
+
target gave a text, because a skip says "not for me". Every target speaks, and not one winner, because the outcome is
|
|
278
|
+
already a list and the load walk cannot see which Gates will be open. When no target gives a text, the door writes one
|
|
279
|
+
from the outcome: a repeat, a delivery that reached nothing, a payload that the parse refused, or what the fan-in did
|
|
280
|
+
(a Gate that took an event, a Pool that is paused). For a 429 or a 503 of the fan-in, the door adds a line with the
|
|
281
|
+
cause after the targets' texts, so the text always says why. This text is the same for every service, so the kind
|
|
282
|
+
never sees the outcome. A paused Pool has no run, so no `reply` of it runs: the door's text says that it is paused.
|
|
283
|
+
The 503s and the 404 that come before the verifier stay as they are, because the door cannot know who reads the answer
|
|
284
|
+
before the kind reads a verified request. The door cuts the text to the limit that the kind gives (Discord: 2000
|
|
285
|
+
characters), and the kind puts it in its envelope. The envelope shows the text to the person who acted, and to nobody
|
|
286
|
+
else: Slack `{ "response_type": "ephemeral", "text": … }`, Discord
|
|
287
|
+
`{ "type": 4, "data": { "content": …, "flags": 64 } }`. The text is plain: no Block Kit blocks, no Discord embeds and
|
|
288
|
+
no `in_channel` in v1. A rich reply adds later without a break.
|
|
289
|
+
- **Such an answer is 200.** Where the rules above give 429 or 503, a delivery whose answer a person reads gets 200, and
|
|
290
|
+
its text says why. A non-200 shows the person the service's own error, which says nothing about what jr2 did. The hook
|
|
291
|
+
log keeps the status that the rules give. A failed verifier stays 401.
|
|
292
|
+
- **The reply to a Slack click goes to its `response_url`.** Slack shows no text from the answer to a Block Kit click:
|
|
293
|
+
that answer can only be an empty 200. So the `slack` kind's envelope for a click is an empty 200 and a follow-up, as
|
|
294
|
+
data: a post of the text to the click's `response_url`. The door answers the empty 200, and then makes the post
|
|
295
|
+
through the Instance Effector's `http` call
|
|
296
|
+
([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)): to a host that the kind declares
|
|
297
|
+
(`hooks.slack.com`), with no secret (the url is in the verified payload), one try. The door refuses a url that does
|
|
298
|
+
not start with one of the kind's `posts`, and posts nothing. A failed or refused post is a `follow-up` line in the
|
|
299
|
+
hook log. A `reply` that threw is in its delivery's line, as `replyErrors`. It is the answer by another road, with no
|
|
300
|
+
slot, no ledger row and no run. No Machine can post it, because a click that reaches nothing has no run.
|
|
301
|
+
- **No deferred answer.** The text is a verdict or an outcome, which is ready when the answer leaves. The work never is.
|
|
302
|
+
Discord's deferred answer (type 5) would make the run's result the answer, but its token lives 15 minutes, a run is
|
|
303
|
+
often longer, and a deferred answer that nobody edits stays "thinking". When the door misses the service's 3 seconds,
|
|
304
|
+
the person sees the service's error, the hook log keeps what jr2 did, and a held item still runs.
|
|
305
|
+
- **The hook log keeps each delivery for `runs.retain`**
|
|
306
|
+
([ADR-0065](0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md)): its Hook, id, digest, outcome and
|
|
307
|
+
the status that the rules give, a `payload-refused` delivery with zod's issues, a Source's skip with its reason, a
|
|
308
|
+
`reply` that threw, and a failed follow-up post. `jr2 hooks <name>` prints it, newest first, from `GET /hooks/:name`,
|
|
309
|
+
which takes the Instance token alone ([ADR-0014](0014-observation-is-open-run-state-is-not.md)): a line can hold a
|
|
310
|
+
payload's fields and a handler's words. A run's blob keeps its `(delivery, gate)` pairs as the `delivery` of each
|
|
311
|
+
answer that a Hook gave (the run's last 100 answers, ADR-0078), so a restore never gives one delivery twice to one
|
|
312
|
+
Gate.
|
|
313
|
+
- **Tests.** The default tier drives `POST /hooks/:name` with a `token` and an `ed25519` verifier against hooked Gates
|
|
314
|
+
and a Pool's Source, with the handshakes, the parse, the join of the replies and the envelopes. It has no Instance
|
|
315
|
+
Effector, so the `@kind` tier drives a `mac` verifier through the Instance Effector, and a look whose `http` read runs
|
|
316
|
+
there ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md) lists the facts of
|
|
317
|
+
the services that the build must check).
|
|
318
|
+
|
|
114
319
|
## Static imports; live objects constructed per-invocation from input
|
|
115
320
|
|
|
116
321
|
Everything above is reachable by plain `import`. We rejected two alternatives:
|
|
@@ -126,6 +331,42 @@ The doctrine that makes static import work, sharpening ADR-0007: **actor logic i
|
|
|
126
331
|
per-invocation from serializable input** — the Harness client from an endpoint, the kube client from ambient config.
|
|
127
332
|
Tests mock by `machine.provide()` at the layer under test.
|
|
128
333
|
|
|
334
|
+
## Considered options for a Hook's delivery
|
|
335
|
+
|
|
336
|
+
- **Correlation keys**: a Gate states keys (`{ pr }`, `{ sha }`), a Hook event carries them, and the door routes by
|
|
337
|
+
them. Not now: the fan-out calls every open handler of the kind and event key, and keys can come later as an index,
|
|
338
|
+
without a break, when a measurement shows the need.
|
|
339
|
+
- **A kept buffer**: the door keeps each Hook event for a time and gives it to a Gate that opens later. Rejected: it
|
|
340
|
+
gives edges again in the order that they arrived, so an old verdict can come after a newer one. The look reads the
|
|
341
|
+
level instead.
|
|
342
|
+
- **A Listener**: an actor at a Machine's root, `listen(kind, { delivers, on, hold })`, that turns each delivery into
|
|
343
|
+
one of its own events and holds it until the Machine takes it. Rejected: it is a second concept beside the Gate, and a
|
|
344
|
+
third party of Sender, outside the position rule of ADR-0015.
|
|
345
|
+
- **A Lookout**: a delivery only wakes an Effect slot, which reads the fact with an idempotent read and settles on it,
|
|
346
|
+
so the payload never decides. Rejected as the only form: it is the soundest, and the hardest to write. Each wait needs
|
|
347
|
+
a read and a schema, and a service with nothing to read needs a second form. Its read is the Gate's `look`.
|
|
348
|
+
- **Payloads typed only**: a kind gives a TypeScript type for each event key, and the door parses nothing. Rejected: the
|
|
349
|
+
type then promises what nothing checks, and a fault in it shows in the handler of every Workflow, as a `hook-refused`
|
|
350
|
+
mark far from its cause, instead of at the kind.
|
|
351
|
+
- **Handlers as a list of `on(kind, handlers)` calls**, which needs no `HookKinds`. Rejected: TypeScript lost the Gate's
|
|
352
|
+
`meta` type through the nested generic call, and the handler's `meta` was `never`.
|
|
353
|
+
- **A brand on a handler's return** (`deliver(approve, { by })`). Rejected: a plain event typed by `delivers` gets the
|
|
354
|
+
same checks from the compiler and the walk, with one name less.
|
|
355
|
+
- **The reply text in the handler's return.** Rejected: the handler runs before the run answers, so it cannot know
|
|
356
|
+
`refused`.
|
|
357
|
+
- **`reply` in the Hook's config**, a function of the whole outcome. Rejected: the config is not the Assembly, it knows
|
|
358
|
+
no Workflow, and a publish does not hand it over.
|
|
359
|
+
- **The author's envelope** (`{ response_type, text }`, a Discord type), or a `reply` for each kind. Rejected: every
|
|
360
|
+
author learns two services for one text, and a message that the channel sees is shared state, which is an Effect.
|
|
361
|
+
- **A reply that changes the message** (Discord type 7, Slack `replace_original`). Rejected: a click that counts for
|
|
362
|
+
nothing would remove the buttons for everyone. The Machine's update of the message follows its state.
|
|
363
|
+
- **One winner** among the targets' texts (the first target, or a load-walk refusal of two). Rejected: it hides what
|
|
364
|
+
happened at the other targets, and the walk cannot see which Gates will be open.
|
|
365
|
+
- **No text for a Slack click**, only the Machine's update. Rejected: a click that reaches nothing, or that the run
|
|
366
|
+
refuses, then shows nothing at all.
|
|
367
|
+
- **A deadline fallback** (a deferred answer at 2.5 s, and the text posted later). Not now: the door is fast by design,
|
|
368
|
+
and the fallback adds a timer and a second road for a rare case.
|
|
369
|
+
|
|
129
370
|
## Consequences
|
|
130
371
|
|
|
131
372
|
- Addresses are computed by jr2, never by the workflow — iids always (ADR-0016): fresh per invocation by default (the
|
|
@@ -138,4 +379,7 @@ Tests mock by `machine.provide()` at the layer under test.
|
|
|
138
379
|
only**, which pass `endpoint` directly in run input — e2e drives their Machines by playing the Harness's Menu against
|
|
139
380
|
`/agents/<iid>/*` with no cluster. Workflows that invoke `workspace()` always get real Sandboxes (the data plane is
|
|
140
381
|
never faked — ADR-0009); their e2e tier requires kind.
|
|
141
|
-
-
|
|
382
|
+
- A Caller's free text to an Agent is a Steering message ([ADR-0002](0002-duplex-streaming-actor.md)), never a workflow
|
|
383
|
+
event: it is in no Vocabulary, and no Gate takes it.
|
|
384
|
+
- Not set yet: a time budget for the handlers, which run in the Orchestrator's event loop for each open Gate of their
|
|
385
|
+
kind and event key.
|
|
@@ -88,8 +88,15 @@ the same contract as a temp directory.
|
|
|
88
88
|
|
|
89
89
|
- The body chooses how many Agents work in its Workspace (ADR-0004, ADR-0075). The wrapper has no lock machinery for
|
|
90
90
|
them.
|
|
91
|
-
- The workspace labels the CR with its run (`jr2.dev/run`, `jr2.dev/workflow
|
|
92
|
-
|
|
91
|
+
- The workspace labels the CR with its run, its Workflow and its path (`jr2.dev/run`, `jr2.dev/workflow`,
|
|
92
|
+
`jr2.dev/workspace`), and writes the exact path in the annotation `jr2.dev/workspace-path`. The path is the workspace
|
|
93
|
+
actor's path in the run's Machine: actor ids joined by `/`, and a Pool child's id is its item's id, which can hold any
|
|
94
|
+
character. A label value holds at most 63 characters from `[A-Za-z0-9._-]`, so the label holds a form of the path for
|
|
95
|
+
a selector only: each other character becomes `-`, leading and trailing characters that are not alphanumeric are
|
|
96
|
+
removed (a label value must start and end with one), and a value above 63 characters is cut and ends with a hash, as
|
|
97
|
+
the CR's name does. A person selects by it with `kubectl -l`; no jr2 reader does. The `jr2 workspace` verbs list the
|
|
98
|
+
CRs by `jr2.dev/run` and compare a handle, `<run-prefix>/<workspace path>`, with the annotation, with no Orchestrator
|
|
99
|
+
(ADR-0009). They do not select by a run prefix, because a label selector matches only a whole value.
|
|
93
100
|
- **Lost workspace: the wrapper emits, the body decides.** While the body runs, a lease actor beside it renews the
|
|
94
101
|
Sandbox's keepalive and follows the Sandbox watch for whether the workspace it attached to is still there (ADR-0021,
|
|
95
102
|
ADR-0063) — continuously, and on snapshot restore as the first tick of that same loop rather than a separate
|
|
@@ -32,7 +32,7 @@ is needed either (ADR-0006).
|
|
|
32
32
|
([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). The fetch token's Stand-in
|
|
33
33
|
is public, `jr2-held-JR2_SANDBOX_TOKEN`, and `jr2-upload-pack` holds it as a constant. The Harness reaches the
|
|
34
34
|
Orchestrator only through the Custodian: the agent routes on its control socket, `/run/jr2/custodian/control.sock`,
|
|
35
|
-
which no confined program can replace (ADR-0076), and the fetch on `127.0.0.1:8081`. The Custodian forwards
|
|
35
|
+
which no confined program can replace (ADR-0076), and the fetch on `127.0.0.1:8081`. The Custodian forwards six
|
|
36
36
|
routes, with each route's own token put in place of its Stand-in, and answers every other route itself:
|
|
37
37
|
|
|
38
38
|
```
|
|
@@ -40,6 +40,7 @@ is needed either (ADR-0006).
|
|
|
40
40
|
POST /agents/<iid>/events a pick → the Orchestrator
|
|
41
41
|
GET /agents/<iid>/deliveries/<id> an outstanding pick's answer → the Orchestrator
|
|
42
42
|
POST /agents/<iid>/notices a step boundary's notices heard → the Orchestrator
|
|
43
|
+
POST /agents/<iid>/conversation a batch of the conversation's entries (ADR-0023) → the Orchestrator
|
|
43
44
|
POST /fetch the ask (ADR-0053), for THIS pod's Sandbox → POST /sandboxes/<name>/fetch
|
|
44
45
|
anything else 404
|
|
45
46
|
```
|
|
@@ -47,10 +48,10 @@ is needed either (ADR-0006).
|
|
|
47
48
|
So the Agent's sole control-plane peer is a container it can reach but whose credential it cannot read. That is what
|
|
48
49
|
makes ADR-0006's "the Agent never steers the workflow" **enforced** rather than advertised.
|
|
49
50
|
|
|
50
|
-
- **The Harness presents the Menu to its model itself.**
|
|
51
|
-
accepted event into a Menu item, with the event's JSON Schema as its parameters, and turns a pick
|
|
52
|
-
`POST …/events` — and, for an outstanding pick, the polls of its delivery, inside the same tool call. The
|
|
53
|
-
one model answer's calls in the model's order
|
|
51
|
+
- **The Harness presents the Menu to its model itself.** No MCP server serves the Menu, and no MCP client reads it.
|
|
52
|
+
`menu.ts` turns each accepted event into a Menu item, with the event's JSON Schema as its parameters, and turns a pick
|
|
53
|
+
into one `POST …/events` — and, for an outstanding pick, the polls of its delivery, inside the same tool call. The
|
|
54
|
+
engine runs one model answer's calls in the model's order
|
|
54
55
|
([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)), and only the first pick of a model
|
|
55
56
|
answer runs. It imports nothing from pi: `menu-tools.ts` gives each item to pi as a tool. A harness that only speaks
|
|
56
57
|
MCP would get a second presenter over the same `menu.ts`, not a second Menu. The receipt comes back as prose
|
|
@@ -60,15 +61,16 @@ is needed either (ADR-0006).
|
|
|
60
61
|
keeps two thin HTTP surfaces, neither of them MCP:
|
|
61
62
|
|
|
62
63
|
```
|
|
63
|
-
# human /
|
|
64
|
-
GET /runs/:id → open gates: accepts + schemas + acceptsNow + meta [
|
|
65
|
-
POST /runs/:id/gates/:gate/events → validate + deliver, or 409 [
|
|
64
|
+
# human / Hook / CI — the Gate resource of ADR-0011, a Caller's (ADR-0078)
|
|
65
|
+
GET /runs/:id → open gates: accepts + schemas + acceptsNow + meta [answer]
|
|
66
|
+
POST /runs/:id/gates/:gate/events → validate + deliver, or 409 [answer]
|
|
66
67
|
|
|
67
68
|
# the Agent's, through its pod's Custodian
|
|
68
69
|
GET /agents/:iid/surface → Menu + Allowed picks + schemas + semantics [control token]
|
|
69
70
|
POST /agents/:iid/events → validate + deliver → the receipt, or 409 [control token]
|
|
70
71
|
GET /agents/:iid/deliveries/:id → an outstanding pick's answer, long-polled [control token]
|
|
71
72
|
POST /agents/:iid/notices → the notices a step boundary appended, heard (ADR-0062) [control token]
|
|
73
|
+
POST /agents/:iid/conversation → a batch of the conversation's entries (ADR-0023) [control token]
|
|
72
74
|
```
|
|
73
75
|
|
|
74
76
|
The receipt is `{ delivered, event, moved, turnComplete, allowed, deliveryId }`. A pick the workflow is still running
|
|
@@ -100,7 +102,10 @@ is needed either (ADR-0006).
|
|
|
100
102
|
accepts each token only on its own routes. Neither reaches a Gate: a Gate refuses every principal of an Agent.
|
|
101
103
|
Neither reaches another Sandbox. A Stand-in is refused on every route.
|
|
102
104
|
- **Instance token** — the human/CLI credential for Gates and run control. Minted into an in-cluster Secret at
|
|
103
|
-
`jr2 up`; the CLI reads it over the kube API (RBAC is the gate) while port-forwarding the Service (ADR-0019).
|
|
105
|
+
`jr2 up`; the CLI reads it over the kube API (RBAC is the gate) while port-forwarding the Service (ADR-0019). It is
|
|
106
|
+
one Caller among others: a named Caller token, a JWT and a Hook reach Gates within their scopes
|
|
107
|
+
([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)). No Caller is a
|
|
108
|
+
principal of an Agent.
|
|
104
109
|
- **The Instance-token door.** The Instance token may also read any Agent's surface and deliver to it —
|
|
105
110
|
`GET /agents/:iid/surface`, `POST /agents/:iid/events` — because it is the operator. That is the operator speaking
|
|
106
111
|
AS the Agent: it reads the Agent's Menu and Allowed picks and its picks are judged as the Agent's — they carry the
|
|
@@ -143,7 +148,7 @@ is needed either (ADR-0006).
|
|
|
143
148
|
- **The Orchestrator serves MCP, and the Custodian forwards it.** Rejected: it keeps the MCP transport and its session
|
|
144
149
|
handling in the control plane and on both ends, to carry two plain HTTP calls.
|
|
145
150
|
- **A uniform `/registrations/:address` API** for every caller. Rejected: it dissolves the run-scoped, discoverable Gate
|
|
146
|
-
resource ADR-0011 designed on purpose (a
|
|
151
|
+
resource ADR-0011 designed on purpose (a hooked Gate's handler matches its own `meta.pr`; `GET /runs/:id` lists open
|
|
147
152
|
decisions) in exchange for aesthetic symmetry.
|
|
148
153
|
|
|
149
154
|
## Consequences
|
|
@@ -151,7 +156,9 @@ is needed either (ADR-0006).
|
|
|
151
156
|
- **The operator needs no change.** ADR-0001 made sidecars generic container fragments the operator schedules without
|
|
152
157
|
understanding; the Custodian is exactly that. `kubeSandbox` composes it (plus the token Secret, written right after
|
|
153
158
|
the CR with the CR as its owner from birth, so Kubernetes reaps it with the Sandbox and no path leaks it — ADR-0001).
|
|
154
|
-
- **
|
|
159
|
+
- **The control plane speaks no MCP.** The Orchestrator and the CLI carry no MCP dependency, and no MCP carries the
|
|
160
|
+
Menu. The Harness and the Instance Effector speak MCP only as clients of an author's servers
|
|
161
|
+
([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)).
|
|
155
162
|
- **One container holds the Sandbox's tokens and every model key.** A fault in the Custodian's engine is a fault in the
|
|
156
163
|
one container that holds both. ADR-0059 records the trade.
|
|
157
164
|
- **The `@kind` e2e tier owns the pod → Orchestrator leg** — the pod's Harness actually calls its tool through the
|
|
@@ -170,6 +177,5 @@ is needed either (ADR-0006).
|
|
|
170
177
|
Custodian in every Harness pod must reach it and no policy can tell the Custodian's packets from the Agent's.
|
|
171
178
|
- **The Harness wire is authenticated in the other direction.** The Orchestrator bears a token derived for the placement
|
|
172
179
|
on every Harness call, and the Harness holds only its digest (ADR-0058). Same signed-name idea, reversed caller.
|
|
173
|
-
- **
|
|
174
|
-
|
|
175
|
-
(ADR-0014 inherits the same question).
|
|
180
|
+
- **Callers without kube credentials are ADR-0078's.** The Instance token rides kube RBAC for anyone with cluster creds;
|
|
181
|
+
a Caller without them (`--url` mode, a browser, a Hook) holds a named token, a JWT or a Hook's signature.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Observation is open; run state and control need
|
|
1
|
+
# Observation is open; run state and control need a Caller's scope
|
|
2
2
|
|
|
3
3
|
[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) minted two tokens and scoped them
|
|
4
4
|
for **delivery** — a Sandbox token may deliver to its own agent registrations, an Instance token may deliver to gates.
|
|
@@ -16,20 +16,44 @@ Building `jr2 visualize` (the Machine renderer and its live run highlighting) fo
|
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
open /workflows, /workflows/:name/machine, /viz/* structure
|
|
19
|
+
/console/config (the login's issuers, ADR-0032) public values
|
|
19
20
|
/workflows/:name/runs, …/runs/:runId/events OBSERVATION (new)
|
|
20
21
|
authenticated /agents/:iid/* the Agent's surface [Sandbox token]
|
|
21
|
-
|
|
22
|
-
POST /workflows/:name/runs starting a run (ADR-0058)
|
|
23
|
-
/runs/:id/gates/:gate/events gates (ADR-0011/0013)
|
|
22
|
+
guarded each route below, opened by one scope of a Caller run state + control [a Caller, ADR-0078]
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
- **Run state and run control are
|
|
27
|
-
Sandbox token — the one credential that sits in a pod an Agent shares a network namespace with — could
|
|
28
|
-
run's context (other features' branches, ticket bodies, review verdicts, Harness endpoints) and
|
|
29
|
-
Neither is on the Agent's surface, no more than a gate is: ADR-0013 scopes that token to
|
|
30
|
-
recorded against its _own_ Sandbox. This closes the same class of hole ADR-0013 closed
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
- **Run state and run control are a Caller's, and each route needs one scope.** `authenticated` accepts _any_ token we
|
|
26
|
+
minted, so a Sandbox token — the one credential that sits in a pod an Agent shares a network namespace with — could
|
|
27
|
+
read **every** run's context (other features' branches, ticket bodies, review verdicts, Harness endpoints) and
|
|
28
|
+
**CANCEL any run**. Neither is on the Agent's surface, no more than a gate is: ADR-0013 scopes that token to
|
|
29
|
+
delivering into registrations recorded against its _own_ Sandbox. This closes the same class of hole ADR-0013 closed
|
|
30
|
+
for gates, on the routes it did not name. The guarded band takes only a Caller
|
|
31
|
+
([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)): a Sandbox token is never
|
|
32
|
+
one. Each route of the band needs one scope, checked against the Workflow of the run or the door that it addresses:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
start POST /workflows/:name/runs, POST /workflows/:wrapper/charts/:name/runs
|
|
36
|
+
read GET /runs (summaries of the runs of its Workflows, live and ended), GET /runs/:id (the whole status),
|
|
37
|
+
GET /runs/:id/events, GET /runs/:id/conversations[/:iid[/events]] (the kept conversations, ADR-0023)
|
|
38
|
+
answer POST /runs/:id/gates/:gate/events, GET /gates (the open Gates it may answer), and the `gates` of GET /runs/:id
|
|
39
|
+
cancel POST /runs/:id/events (CANCEL)
|
|
40
|
+
steer POST /runs/:id/steering, GET /runs/:id/agents (the slot list { agent, open, steerable }, ADR-0002)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`answer` alone opens a run's `gates` and their routing, and never its `context`: a person who answers a Gate reads the
|
|
44
|
+
Gate's `meta`, which the author wrote for that person. So `GET /runs/:id` gives `read` the status and `answer` the
|
|
45
|
+
`gates`; with `answer` alone the answer is the run's observation (below) and its `gates`. `GET /runs` leaves out the
|
|
46
|
+
runs of the Workflows that the Caller may not read; it refuses no run, and refuses only a Caller with no `read` scope
|
|
47
|
+
at all (ADR-0078). `answer` can name single Gates of a Workflow (ADR-0078). Then the gate POST refuses another Gate of
|
|
48
|
+
that Workflow, and `GET /gates` (its entries are ADR-0011's) and the `gates` of `GET /runs/:id` list only the Gates
|
|
49
|
+
that the Caller may answer. Neither gives the run's `context`. `GET /runs/resolve` answers ids to a Caller with any
|
|
50
|
+
scope, for the Workflows of its scopes. `GET /whoami` answers any Caller. A route that no scope names stays the
|
|
51
|
+
Instance token's: the runtime Chart routes (`PUT` and `DELETE /workflows/:wrapper/charts/:name`), `GET /repos`, a
|
|
52
|
+
Hook's log (`GET /hooks/:name`, which can hold a payload's fields and a handler's words), and the Instance-token door
|
|
53
|
+
on `/agents/:iid/*` (ADR-0013) except the conversation route, which takes only a control token
|
|
54
|
+
([ADR-0023](0023-the-harness-prints-the-conversation.md)). `POST /hooks/:name` is outside the bands: the Hook's own
|
|
55
|
+
verifier authenticates it ([ADR-0011](0011-workflow-defined-events.md)). The band is named for what it checks, not for
|
|
56
|
+
a role — "operator" in this repo is the Kubernetes operator (ADR-0001).
|
|
33
57
|
|
|
34
58
|
- **The observer gets a PROJECTION, not a credential.** `RunObservation` is
|
|
35
59
|
`{ runId, workflow, status, value, children }` — identity, and where in the Machine the run is. What is absent is the
|
|
@@ -48,16 +72,15 @@ instanceOnly /runs, /runs/:id, /runs/:id/events (GET + POST) run state + c
|
|
|
48
72
|
|
|
49
73
|
## Considered options
|
|
50
74
|
|
|
51
|
-
- **Give the page a token.** It would
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
- **Give the page a token.** It would be a credential — gates, cancel, every run's context — handed to whatever can load
|
|
76
|
+
a URL. Putting it in `jr2.config.ts` (checked in) is worse still, and the token is minted per boot, so the page would
|
|
77
|
+
not have it anyway. Rejected: the visualizer needs to _see_ a run, and seeing is not a subset of the credential that
|
|
78
|
+
steers it. That mismatch is the whole reason a projection exists.
|
|
55
79
|
- **Keep `/runs*` on `authenticated` and filter in the handler.** Rejected: the guard would then depend on every future
|
|
56
80
|
handler remembering to redact. The band is a middleware precisely so the default is refusal.
|
|
57
81
|
- **A fourth "observer" token.** Rejected: it buys nothing today (the observation surface carries no secret) and costs a
|
|
58
82
|
minting, distribution, and revocation story. If observation ever needs to be _private_ — a deployed jr2 Application on
|
|
59
|
-
a shared network —
|
|
60
|
-
callers.
|
|
83
|
+
a shared network — the guarded band and ADR-0078's scopes are already where it goes.
|
|
61
84
|
|
|
62
85
|
## Consequences
|
|
63
86
|
|
|
@@ -66,11 +89,11 @@ instanceOnly /runs, /runs/:id, /runs/:id/events (GET + POST) run state + c
|
|
|
66
89
|
now **403**. The test pinned the hole.
|
|
67
90
|
- **The API is a cluster Service** (ADR-0019), so "open" means open to whoever can reach it — in-cluster peers, a
|
|
68
91
|
port-forward, or an ingress — deliberately, since what is open is structure plus lit states. Anything that would
|
|
69
|
-
embarrass a user if seen belongs
|
|
70
|
-
- **The CLI is unaffected.** It holds the Instance token from the in-cluster Secret (ADR-0019)
|
|
71
|
-
routes, as before.
|
|
72
|
-
- **
|
|
73
|
-
|
|
92
|
+
embarrass a user if seen belongs in the guarded band, and that is where it is.
|
|
93
|
+
- **The CLI is unaffected.** It holds the Instance token from the in-cluster Secret (ADR-0019), which has every scope,
|
|
94
|
+
and reads the guarded routes, as before.
|
|
95
|
+
- **A Caller without kube credentials is ADR-0078's:** a named token, a JWT, or a browser login. The band split holds
|
|
96
|
+
for every Caller; what the guarded band checks is one scope for each route.
|
|
74
97
|
|
|
75
98
|
- **One scoped exception, and it does not live on this surface at all (ADR-0023).** The Harness prints each Agent's
|
|
76
99
|
conversation — prompts, assistant text, thinking, tool calls, and the answer jr2 gave each Menu pick — to container
|
|
@@ -87,9 +110,12 @@ instanceOnly /runs, /runs/:id, /runs/:id/events (GET + POST) run state + c
|
|
|
87
110
|
form; then it prints there as the model read it. The answer also carries an Effect's output as the model read it: a
|
|
88
111
|
`command` Effect's output prints there up to the bound, and a Held value an upstream echoed into that output prints
|
|
89
112
|
with it (ADR-0023, ADR-0059). What remains, deliberately, is that on a shared cluster the population who can read an
|
|
90
|
-
Agent's reasoning, and what its picks answered, is whoever can read pod logs.
|
|
91
|
-
|
|
113
|
+
Agent's reasoning, and what its picks answered, is whoever can read pod logs. The Orchestrator's kept copy of the same
|
|
114
|
+
conversation is not part of the exception: it is on this surface, behind `read`, and `jr2 logs` and the Console read
|
|
115
|
+
it there (ADR-0023). Thus `read` opens the same residual of a pick's answer for as long as the run is kept
|
|
116
|
+
(`runs.retain`, ADR-0065). If the pod-log audience becomes unacceptable, the print is what to turn off, and the record
|
|
117
|
+
stays.
|
|
92
118
|
|
|
93
|
-
Evidence: `packages/orchestrator/src/http.ts` (the
|
|
94
|
-
`RunChild`), `test/auth.test.ts` (the boundary), `test/
|
|
95
|
-
any depth of the child tree).
|
|
119
|
+
Evidence: `packages/orchestrator/src/http.ts` (the middlewares), `run-host.ts` (`observe` / `RunObservation` /
|
|
120
|
+
`RunChild`), `test/auth.test.ts` (the boundary), `test/callers.test.ts` (each scope on its routes),
|
|
121
|
+
`test/http-viz.test.ts` (a planted secret never reaches the feed, at any depth of the child tree).
|