@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
package/src/callers.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// The JWT issuers of the Caller seam, and the record of a Caller on a run (ADR-0078).
|
|
2
|
+
//
|
|
3
|
+
// The seam itself (its order, the Instance token, the named `jr2c_` tokens) is in tokens.ts. This
|
|
4
|
+
// file adds its third authenticator: a JWT from an identity provider that the config declares, which
|
|
5
|
+
// the Orchestrator checks against the provider's published keys. That fetch is the Orchestrator's
|
|
6
|
+
// one call out of the Instance, and it spends no secret (ADR-0068).
|
|
7
|
+
//
|
|
8
|
+
// It also holds what a run records of a Caller: the Caller without its scopes, because a run
|
|
9
|
+
// records who acted, not what that credential could do then.
|
|
10
|
+
|
|
11
|
+
import { createRemoteJWKSet, customFetch, decodeJwt, jwtVerify, type JWTPayload } from "jose";
|
|
12
|
+
import { SCOPES, type AnswerGrant, type GateGrant, type JwtIssuer, type Scopes } from "./config.ts";
|
|
13
|
+
import type { Caller, CallerAnswer, CallerAuthenticator } from "./tokens.ts";
|
|
14
|
+
|
|
15
|
+
// ---- What a run records ------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** A Caller as a run records it (ADR-0078): who acted, and the party it says it acted for. Never its
|
|
18
|
+
* scopes. */
|
|
19
|
+
export type CallerRecord = {
|
|
20
|
+
via: Caller["via"];
|
|
21
|
+
name: string;
|
|
22
|
+
/** The name that jr2 shows, when it is not `name` (a JWT's `email`). */
|
|
23
|
+
shown?: string;
|
|
24
|
+
/** The Caller's claim about the party it acts for. jr2 records it and never checks it, except for
|
|
25
|
+
* a Hook, whose signature covers it. */
|
|
26
|
+
onBehalfOf?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Who started a run (ADR-0078): a Caller on a guarded route; a run, through a local `dispatch`
|
|
31
|
+
* (ADR-0071); or jr2 itself, for a standing run (ADR-0017), at `boot`, at a `publish`, at a
|
|
32
|
+
* `hand-over`, or at a `resume`. A run that a foreign `dispatch` started records the Caller whose
|
|
33
|
+
* credential the other Instance sent.
|
|
34
|
+
*/
|
|
35
|
+
export type RunStarter =
|
|
36
|
+
| ({ kind: "caller" } & CallerRecord)
|
|
37
|
+
| { kind: "run"; run: string; slot: string }
|
|
38
|
+
| { kind: "jr2"; why: "boot" | "publish" | "hand-over" | "resume" };
|
|
39
|
+
|
|
40
|
+
/** One answer to a Gate of a run, and the Caller that gave it (ADR-0011, ADR-0078). `delivery` is
|
|
41
|
+
* the Hook's delivery id when a Hook gave it: the run's `(delivery, gate)` pairs, so a restore never
|
|
42
|
+
* gives one delivery twice to one Gate. */
|
|
43
|
+
export type GateAnswer = { gate: string; type: string; at: string; answeredBy: CallerRecord; delivery?: string };
|
|
44
|
+
|
|
45
|
+
/** How many Gate answers a run keeps, the last ones: a standing run's Gate can take an answer for
|
|
46
|
+
* each delivery, and the run's blob is saved on each transition. */
|
|
47
|
+
export const GATE_ANSWERS_KEPT = 100;
|
|
48
|
+
|
|
49
|
+
/** The record of a Caller: the Caller without its scopes. */
|
|
50
|
+
export function callerRecord(caller: Caller): CallerRecord {
|
|
51
|
+
return {
|
|
52
|
+
via: caller.via,
|
|
53
|
+
name: caller.name,
|
|
54
|
+
...(caller.shown !== undefined ? { shown: caller.shown } : {}),
|
|
55
|
+
...(caller.onBehalfOf !== undefined ? { onBehalfOf: caller.onBehalfOf } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A Caller as a line names it: `token/ci`, then the name that jr2 shows and the party it acts for. */
|
|
60
|
+
export function callerText(caller: CallerRecord): string {
|
|
61
|
+
return (
|
|
62
|
+
`${caller.via}/${caller.name}` +
|
|
63
|
+
(caller.shown !== undefined ? ` (${caller.shown})` : "") +
|
|
64
|
+
(caller.onBehalfOf !== undefined ? ` on behalf of ${caller.onBehalfOf}` : "")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---- The JWT authenticator ---------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/** The algorithms a JWT may be signed with: the asymmetric ones alone, because a published key
|
|
71
|
+
* must not be able to sign. */
|
|
72
|
+
const ALGORITHMS = ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
73
|
+
|
|
74
|
+
/** How far the issuer's clock and ours may differ, for `exp` and `nbf`. */
|
|
75
|
+
const CLOCK_TOLERANCE_S = 30;
|
|
76
|
+
|
|
77
|
+
/** How long the keys are kept before a JWT makes a fetch again. */
|
|
78
|
+
const KEYS_MAX_AGE_MS = 10 * 60_000;
|
|
79
|
+
|
|
80
|
+
/** The shortest time between two fetches of the keys, for a JWT whose key is not known; also how
|
|
81
|
+
* long a failed fetch of the discovery document is kept before a JWT makes a fetch again. */
|
|
82
|
+
const KEYS_COOLDOWN_MS = 30_000;
|
|
83
|
+
|
|
84
|
+
/** How long one fetch of the discovery document or the keys may take. */
|
|
85
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
86
|
+
|
|
87
|
+
/** The claims a {@link JwtIssuer} reads when its `claims` names no other. */
|
|
88
|
+
const DEFAULT_CLAIMS = { name: "sub", shown: "email", groups: "groups" } as const;
|
|
89
|
+
|
|
90
|
+
/** The fetch that the JWT authenticator uses: the global one, or a test's. */
|
|
91
|
+
export type JwksFetch = (url: string, init: { signal?: AbortSignal }) => Promise<Response>;
|
|
92
|
+
|
|
93
|
+
/** The options of {@link jwtAuthenticator}, for tests. */
|
|
94
|
+
export type JwtAuthenticatorOptions = {
|
|
95
|
+
/** The fetch for the discovery document and the keys. Default: the global `fetch`. */
|
|
96
|
+
fetch?: JwksFetch;
|
|
97
|
+
/** The shortest time between two fetches of the keys, and of a discovery that failed. Default: 30 s. */
|
|
98
|
+
cooldownMs?: number;
|
|
99
|
+
/** The clock, in milliseconds, that measures the cooldown of a failed discovery. Default: `Date.now`. */
|
|
100
|
+
now?: () => number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type KeySet = ReturnType<typeof createRemoteJWKSet>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The JWT issuers' authenticator (ADR-0078). It claims a bearer that is a JWT whose `iss` claim is
|
|
107
|
+
* the `issuer` of an entry in `issuers()`, which is read on each request, so a publish applies with
|
|
108
|
+
* no restart. A claimed JWT that fails a check is "mine, and bad" (401). A JWT of an issuer that no
|
|
109
|
+
* entry names is not claimed.
|
|
110
|
+
*/
|
|
111
|
+
export function jwtAuthenticator(
|
|
112
|
+
issuers: () => readonly JwtIssuer[],
|
|
113
|
+
opts: JwtAuthenticatorOptions = {},
|
|
114
|
+
): CallerAuthenticator {
|
|
115
|
+
const doFetch: JwksFetch = opts.fetch ?? ((url, init) => fetch(url, init));
|
|
116
|
+
const cooldownMs = opts.cooldownMs ?? KEYS_COOLDOWN_MS;
|
|
117
|
+
const now = opts.now ?? Date.now;
|
|
118
|
+
// The key sets, by the issuer and its keys' URL: an entry that a publish changes gets a new one.
|
|
119
|
+
// `failedAt`: when its discovery failed. The failure is kept for the cooldown, so a flood of JWTs
|
|
120
|
+
// that name an issuer whose discovery fails makes at most one fetch in each cooldown.
|
|
121
|
+
const keySets = new Map<string, { keySet: Promise<KeySet>; failedAt?: number }>();
|
|
122
|
+
const idOf = (entry: JwtIssuer): string => JSON.stringify([entry.issuer, entry.jwks ?? null]);
|
|
123
|
+
|
|
124
|
+
const keySetOf = (entry: JwtIssuer): Promise<KeySet> => {
|
|
125
|
+
const id = idOf(entry);
|
|
126
|
+
const found = keySets.get(id);
|
|
127
|
+
if (found && (found.failedAt === undefined || now() - found.failedAt < cooldownMs)) return found.keySet;
|
|
128
|
+
const slot: { keySet: Promise<KeySet>; failedAt?: number } = {
|
|
129
|
+
keySet: (async () => {
|
|
130
|
+
const url = entry.jwks ?? (await discoverJwks(entry.issuer, doFetch));
|
|
131
|
+
return createRemoteJWKSet(new URL(url), {
|
|
132
|
+
cacheMaxAge: KEYS_MAX_AGE_MS,
|
|
133
|
+
cooldownDuration: cooldownMs,
|
|
134
|
+
timeoutDuration: FETCH_TIMEOUT_MS,
|
|
135
|
+
[customFetch]: (u, init) => doFetch(u, { signal: init.signal }),
|
|
136
|
+
});
|
|
137
|
+
})(),
|
|
138
|
+
};
|
|
139
|
+
slot.keySet.catch(() => {
|
|
140
|
+
slot.failedAt = now();
|
|
141
|
+
});
|
|
142
|
+
keySets.set(id, slot);
|
|
143
|
+
return slot.keySet;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Drop the key sets of each issuer and keys' URL that the config no longer names: the cache
|
|
147
|
+
* holds at most one entry for each entry of `issuers()`. */
|
|
148
|
+
const prune = (current: readonly JwtIssuer[]): void => {
|
|
149
|
+
if (keySets.size === 0) return;
|
|
150
|
+
const named = new Set(current.map(idOf));
|
|
151
|
+
for (const id of keySets.keys()) if (!named.has(id)) keySets.delete(id);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return async (bearer): Promise<CallerAnswer> => {
|
|
155
|
+
if (bearer.split(".").length !== 3) return undefined;
|
|
156
|
+
let unverified: JWTPayload;
|
|
157
|
+
try {
|
|
158
|
+
unverified = decodeJwt(bearer);
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const current = issuers();
|
|
163
|
+
prune(current);
|
|
164
|
+
const entry = current.find((i) => i.issuer === unverified.iss);
|
|
165
|
+
if (!entry) return undefined;
|
|
166
|
+
|
|
167
|
+
let payload: JWTPayload;
|
|
168
|
+
try {
|
|
169
|
+
({ payload } = await jwtVerify(bearer, await keySetOf(entry), {
|
|
170
|
+
issuer: entry.issuer,
|
|
171
|
+
audience: entry.audience,
|
|
172
|
+
algorithms: ALGORITHMS,
|
|
173
|
+
clockTolerance: CLOCK_TOLERANCE_S,
|
|
174
|
+
requiredClaims: ["exp"],
|
|
175
|
+
}));
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return { refused: `a JWT of ${entry.issuer} did not verify: ${err instanceof Error ? err.message : err}` };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const claims = { ...DEFAULT_CLAIMS, ...entry.claims };
|
|
181
|
+
const name = payload[claims.name];
|
|
182
|
+
if (typeof name !== "string" || name === "") {
|
|
183
|
+
return { refused: `a JWT of ${entry.issuer} has no "${claims.name}" claim, which names its Caller` };
|
|
184
|
+
}
|
|
185
|
+
const shown = payload[claims.shown];
|
|
186
|
+
return {
|
|
187
|
+
via: "jwt",
|
|
188
|
+
name,
|
|
189
|
+
...(typeof shown === "string" && shown !== "" && shown !== name ? { shown } : {}),
|
|
190
|
+
may: joinScopes(
|
|
191
|
+
valuesOf(payload[claims.groups]).flatMap((v) => (Object.hasOwn(entry.groups, v) ? [entry.groups[v]!] : [])),
|
|
192
|
+
),
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The `jwks_uri` of an issuer's OIDC discovery document. */
|
|
198
|
+
async function discoverJwks(issuer: string, doFetch: JwksFetch): Promise<string> {
|
|
199
|
+
const url = `${issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`;
|
|
200
|
+
const res = await doFetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
201
|
+
if (!res.ok) throw new Error(`${url} answered ${res.status}`);
|
|
202
|
+
const doc = (await res.json()) as { jwks_uri?: unknown };
|
|
203
|
+
if (typeof doc.jwks_uri !== "string") throw new Error(`${url} names no jwks_uri`);
|
|
204
|
+
return doc.jwks_uri;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The values of a groups claim: a list of strings, or one string. */
|
|
208
|
+
function valuesOf(claim: unknown): string[] {
|
|
209
|
+
if (typeof claim === "string") return [claim];
|
|
210
|
+
if (Array.isArray(claim)) return claim.filter((v): v is string => typeof v === "string");
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The scopes of each grant, joined: a scope that one gives for every Workflow is for every
|
|
215
|
+
* Workflow, and lists join. A JWT whose groups give one team's Gate and another team's Gate of one
|
|
216
|
+
* Workflow may answer both. */
|
|
217
|
+
export function joinScopes(grants: readonly Scopes[]): Scopes {
|
|
218
|
+
const out: Record<string, AnswerGrant> = {};
|
|
219
|
+
for (const scope of SCOPES) {
|
|
220
|
+
let joined: AnswerGrant | undefined;
|
|
221
|
+
for (const may of grants) {
|
|
222
|
+
const grant: AnswerGrant | undefined = may[scope];
|
|
223
|
+
if (grant === undefined) continue;
|
|
224
|
+
if (grant === "all" || joined === "all") joined = "all";
|
|
225
|
+
else joined = joinItems(joined ?? [], grant);
|
|
226
|
+
}
|
|
227
|
+
if (joined !== undefined) out[scope] = joined;
|
|
228
|
+
}
|
|
229
|
+
return out as Scopes;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Two lists of one scope, joined: each Workflow name once, and each `answer` item that names Gates
|
|
233
|
+
* once (ADR-0078). An item that names Gates stays beside a bare name of its Workflow, which opens
|
|
234
|
+
* them anyway. */
|
|
235
|
+
function joinItems(a: readonly (string | GateGrant)[], b: readonly (string | GateGrant)[]): (string | GateGrant)[] {
|
|
236
|
+
const seen = new Set<string>();
|
|
237
|
+
const out: (string | GateGrant)[] = [];
|
|
238
|
+
for (const item of [...a, ...b]) {
|
|
239
|
+
const key = typeof item === "string" ? item : JSON.stringify([item.workflow, [...item.gates].sort()]);
|
|
240
|
+
if (seen.has(key)) continue;
|
|
241
|
+
seen.add(key);
|
|
242
|
+
out.push(item);
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
package/src/chart.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { defineEvent, type EventAudience, type EventDef } from "@jr2/agent-proto
|
|
|
25
25
|
import type { AgentDefinition, ThinkingLevel, WorkspaceAccess } from "./agent.ts";
|
|
26
26
|
import { inputKeysOf, inputShapeOf, isEffect, type InputShape } from "./effect.ts";
|
|
27
27
|
import { FAILURE_GUARD } from "./fail.ts";
|
|
28
|
+
import { isGateSlot } from "./gate.ts";
|
|
28
29
|
import { compileChart, durationMs, type LentParts, type OpenBodyDeclaration } from "./open-body.ts";
|
|
29
30
|
import { SENT_BY } from "./party.ts";
|
|
30
31
|
import { loadWarningsOf } from "./vocabulary.ts";
|
|
@@ -673,6 +674,7 @@ function lendsOf(decl: OpenBodyDeclaration) {
|
|
|
673
674
|
agents: decl.lends.agents ?? [],
|
|
674
675
|
effects: decl.lends.effects ?? [],
|
|
675
676
|
machines: decl.lends.machines ?? [],
|
|
677
|
+
gates: decl.lends.gates ?? [],
|
|
676
678
|
guards: decl.lends.guards ?? [],
|
|
677
679
|
actions: decl.lends.actions ?? [],
|
|
678
680
|
delays: decl.lends.delays ?? [],
|
|
@@ -695,7 +697,14 @@ export function narrowedChartSchema(decl: OpenBodyDeclaration): Record<string, u
|
|
|
695
697
|
function narrowOf(decl: OpenBodyDeclaration): Narrow {
|
|
696
698
|
const l = lendsOf(decl);
|
|
697
699
|
return {
|
|
698
|
-
srcs: [
|
|
700
|
+
srcs: [
|
|
701
|
+
...l.agents,
|
|
702
|
+
...l.effects,
|
|
703
|
+
...l.machines,
|
|
704
|
+
...l.gates,
|
|
705
|
+
...(l.gate ? ["gate"] : []),
|
|
706
|
+
...(l.fail ? ["fail"] : []),
|
|
707
|
+
],
|
|
699
708
|
lentAgents: l.agents,
|
|
700
709
|
lendsEffects: l.effects.length > 0,
|
|
701
710
|
gate: l.gate,
|
|
@@ -853,6 +862,9 @@ export type AdmittedChart = {
|
|
|
853
862
|
agents: Record<string, AgentDefinition>;
|
|
854
863
|
/** The bytes of the text's canonical JSON. */
|
|
855
864
|
size: number;
|
|
865
|
+
/** The invoke ids of the Chart's Gates (ADR-0011), as `jr2Setup` names them: the names that the
|
|
866
|
+
* `answer` scope can name them by (ADR-0078). */
|
|
867
|
+
gates: string[];
|
|
856
868
|
};
|
|
857
869
|
|
|
858
870
|
/**
|
|
@@ -1064,6 +1076,8 @@ class Door {
|
|
|
1064
1076
|
private readonly invokeKinds = new Map<ChartInvoke, NonNullable<ReturnType<Door["srcKind"]>>>();
|
|
1065
1077
|
/** Every `jr2.sentBy` id, for the party check. */
|
|
1066
1078
|
private readonly sentBys: Array<{ id: string; path: string }> = [];
|
|
1079
|
+
/** The Gates' invoke ids, which the party check finds. */
|
|
1080
|
+
private readonly gateIds = new Set<string>();
|
|
1067
1081
|
|
|
1068
1082
|
private readonly chart: ChartText;
|
|
1069
1083
|
private readonly decl: OpenBodyDeclaration;
|
|
@@ -1126,7 +1140,14 @@ class Door {
|
|
|
1126
1140
|
const events = Object.entries(this.chart.events ?? {}).map(([name, ev]) => this.eventDef(name, ev)!);
|
|
1127
1141
|
const agents: Record<string, AgentDefinition> = {};
|
|
1128
1142
|
for (const [slot, a] of Object.entries(this.chart.agents ?? {})) agents[slot] = { ...a };
|
|
1129
|
-
const admitted: AdmittedChart = {
|
|
1143
|
+
const admitted: AdmittedChart = {
|
|
1144
|
+
text: this.chart,
|
|
1145
|
+
hash: chartHash(this.chart),
|
|
1146
|
+
events,
|
|
1147
|
+
agents,
|
|
1148
|
+
size,
|
|
1149
|
+
gates: [...this.gateIds],
|
|
1150
|
+
};
|
|
1130
1151
|
this.trialCompile(admitted);
|
|
1131
1152
|
return admitted;
|
|
1132
1153
|
}
|
|
@@ -1152,11 +1173,12 @@ class Door {
|
|
|
1152
1173
|
// --- rule 3: names --------------------------------------------------------------------------------
|
|
1153
1174
|
|
|
1154
1175
|
/** The lent actors by kind, as the lent set has them now. */
|
|
1155
|
-
private lentActors(): Array<{ name: string; kind: "Agent" | "Effect" | "Machine" }> {
|
|
1156
|
-
const out: Array<{ name: string; kind: "Agent" | "Effect" | "Machine" }> = [];
|
|
1176
|
+
private lentActors(): Array<{ name: string; kind: "Agent" | "Effect" | "Machine" | "Gate slot" }> {
|
|
1177
|
+
const out: Array<{ name: string; kind: "Agent" | "Effect" | "Machine" | "Gate slot" }> = [];
|
|
1157
1178
|
for (const name of this.lends.agents) if (name in this.lent.actors) out.push({ name, kind: "Agent" });
|
|
1158
1179
|
for (const name of this.lends.effects) if (name in this.lent.actors) out.push({ name, kind: "Effect" });
|
|
1159
1180
|
for (const name of this.lends.machines) if (name in this.lent.actors) out.push({ name, kind: "Machine" });
|
|
1181
|
+
for (const name of this.lends.gates) if (name in this.lent.actors) out.push({ name, kind: "Gate slot" });
|
|
1160
1182
|
return out;
|
|
1161
1183
|
}
|
|
1162
1184
|
|
|
@@ -1170,7 +1192,7 @@ class Door {
|
|
|
1170
1192
|
/** What an invoke's `src` names, or undefined after a refusal line. */
|
|
1171
1193
|
private srcKind(src: string, path: string): InvokeKind | undefined {
|
|
1172
1194
|
const lentNow = (name: string) => name in this.lent.actors;
|
|
1173
|
-
const declared = [...this.lends.agents, ...this.lends.effects, ...this.lends.machines];
|
|
1195
|
+
const declared = [...this.lends.agents, ...this.lends.effects, ...this.lends.machines, ...this.lends.gates];
|
|
1174
1196
|
if (declared.includes(src) && !lentNow(src)) {
|
|
1175
1197
|
this.fail(
|
|
1176
1198
|
3,
|
|
@@ -1181,6 +1203,22 @@ class Door {
|
|
|
1181
1203
|
if (this.lends.agents.includes(src)) return "agent";
|
|
1182
1204
|
if (this.lends.effects.includes(src)) return "effect";
|
|
1183
1205
|
if (this.lends.machines.includes(src)) return "machine";
|
|
1206
|
+
if (this.lends.gates.includes(src)) {
|
|
1207
|
+
// A lent Gate slot (ADR-0011, ADR-0070): its handlers and its look give its `delivers`, which
|
|
1208
|
+
// the Chart's Machine must take. One that is not in the Chart's Vocabulary could never arrive.
|
|
1209
|
+
const slot = this.lent.actors[src];
|
|
1210
|
+
const outside = isGateSlot(slot)
|
|
1211
|
+
? slot.gateSlot.delivers.map((d) => d.name).filter((name) => !this.eventSchemas.has(name))
|
|
1212
|
+
: [];
|
|
1213
|
+
if (outside.length) {
|
|
1214
|
+
this.fail(
|
|
1215
|
+
3,
|
|
1216
|
+
`${path}: the Gate slot "${src}" delivers ${outside.map((n) => `"${n}"`).join(", ")}, which ${outside.length > 1 ? "are" : "is"} not in the Chart's \`events\` (events: ${names([...this.eventSchemas.keys()])}); declare ${outside.length > 1 ? "them" : "it"} in \`events\` and handle ${outside.length > 1 ? "them" : "it"} on this state`,
|
|
1217
|
+
);
|
|
1218
|
+
return undefined;
|
|
1219
|
+
}
|
|
1220
|
+
return "gate";
|
|
1221
|
+
}
|
|
1184
1222
|
if (src === "gate") {
|
|
1185
1223
|
if (this.lends.gate) return "gate";
|
|
1186
1224
|
this.fail(
|
|
@@ -1375,7 +1413,14 @@ class Door {
|
|
|
1375
1413
|
// --- rule 5: minted Agents ------------------------------------------------------------------------
|
|
1376
1414
|
|
|
1377
1415
|
private checkAgents(): void {
|
|
1378
|
-
const lentNames = new Set([
|
|
1416
|
+
const lentNames = new Set([
|
|
1417
|
+
...this.lends.agents,
|
|
1418
|
+
...this.lends.effects,
|
|
1419
|
+
...this.lends.machines,
|
|
1420
|
+
...this.lends.gates,
|
|
1421
|
+
"gate",
|
|
1422
|
+
"fail",
|
|
1423
|
+
]);
|
|
1379
1424
|
for (const [slot, a] of Object.entries(this.chart.agents ?? {})) {
|
|
1380
1425
|
const path = `agents.${slot}`;
|
|
1381
1426
|
if (lentNames.has(slot)) {
|
|
@@ -1719,7 +1764,7 @@ class Door {
|
|
|
1719
1764
|
/** The parties' rules, as `jr2Setup` applies them (ADR-0015), each with its path in the Chart. */
|
|
1720
1765
|
private checkParties(): void {
|
|
1721
1766
|
const agents = new Set<string>();
|
|
1722
|
-
const gates =
|
|
1767
|
+
const gates = this.gateIds;
|
|
1723
1768
|
const slotStates = new Map<string, Node[]>();
|
|
1724
1769
|
for (const node of this.nodes) {
|
|
1725
1770
|
const invokes = listAt(node.state.invoke, `${node.doc}.invoke`);
|