@dynamicagents/core 0.9.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/LICENSE +201 -0
- package/README.md +522 -0
- package/dist/a2a/agent-stub.d.ts +60 -0
- package/dist/a2a/agent-stub.js +1 -0
- package/dist/a2a/caller-token.d.ts +44 -0
- package/dist/a2a/caller-token.js +61 -0
- package/dist/a2a/caller.d.ts +23 -0
- package/dist/a2a/caller.js +33 -0
- package/dist/a2a/card.d.ts +158 -0
- package/dist/a2a/card.js +163 -0
- package/dist/a2a/context.d.ts +19 -0
- package/dist/a2a/context.js +61 -0
- package/dist/a2a/deliver.d.ts +159 -0
- package/dist/a2a/deliver.js +185 -0
- package/dist/a2a/executor.d.ts +84 -0
- package/dist/a2a/executor.js +106 -0
- package/dist/a2a/index.d.ts +35 -0
- package/dist/a2a/index.js +34 -0
- package/dist/a2a/notify.d.ts +106 -0
- package/dist/a2a/notify.js +180 -0
- package/dist/a2a/parts.d.ts +45 -0
- package/dist/a2a/parts.js +94 -0
- package/dist/a2a/push.d.ts +70 -0
- package/dist/a2a/push.js +53 -0
- package/dist/a2a/self-origin.d.ts +91 -0
- package/dist/a2a/self-origin.js +114 -0
- package/dist/a2a/task-store.d.ts +44 -0
- package/dist/a2a/task-store.js +99 -0
- package/dist/a2a/task.d.ts +85 -0
- package/dist/a2a/task.js +15 -0
- package/dist/a2a/verify.d.ts +80 -0
- package/dist/a2a/verify.js +143 -0
- package/dist/agent/budget.d.ts +46 -0
- package/dist/agent/budget.js +42 -0
- package/dist/agent/control.d.ts +109 -0
- package/dist/agent/control.js +115 -0
- package/dist/agent/errors.d.ts +85 -0
- package/dist/agent/errors.js +64 -0
- package/dist/agent/final-reply.d.ts +49 -0
- package/dist/agent/final-reply.js +68 -0
- package/dist/agent/history.d.ts +97 -0
- package/dist/agent/history.js +133 -0
- package/dist/agent/index.d.ts +29 -0
- package/dist/agent/index.js +29 -0
- package/dist/agent/inference.d.ts +110 -0
- package/dist/agent/inference.js +120 -0
- package/dist/agent/model.d.ts +90 -0
- package/dist/agent/model.js +1 -0
- package/dist/agent/session.d.ts +100 -0
- package/dist/agent/session.js +82 -0
- package/dist/agent/workers-ai/index.d.ts +23 -0
- package/dist/agent/workers-ai/index.js +23 -0
- package/dist/agent/workers-ai/runtime.d.ts +42 -0
- package/dist/agent/workers-ai/runtime.js +63 -0
- package/dist/alarm/index.d.ts +77 -0
- package/dist/alarm/index.js +116 -0
- package/dist/config.d.ts +202 -0
- package/dist/config.js +135 -0
- package/dist/contract/index.d.ts +9 -0
- package/dist/contract/index.js +8 -0
- package/dist/contract/plugin.d.ts +324 -0
- package/dist/contract/plugin.js +114 -0
- package/dist/contract/recipe.d.ts +180 -0
- package/dist/contract/recipe.js +1 -0
- package/dist/contract/validation.d.ts +91 -0
- package/dist/contract/validation.js +84 -0
- package/dist/db/db.d.ts +147 -0
- package/dist/db/db.js +90 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrations/index.d.ts +20 -0
- package/dist/db/migrations/index.js +60 -0
- package/dist/db/models/subtasks.d.ts +100 -0
- package/dist/db/models/subtasks.js +241 -0
- package/dist/db/models/tasks.d.ts +118 -0
- package/dist/db/models/tasks.js +274 -0
- package/dist/db/schema.d.ts +468 -0
- package/dist/db/schema.js +88 -0
- package/dist/env.d.ts +53 -0
- package/dist/env.js +47 -0
- package/dist/host/agent.d.ts +305 -0
- package/dist/host/agent.js +400 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/index.js +19 -0
- package/dist/host/plugin-host.d.ts +42 -0
- package/dist/host/plugin-host.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/job/index.d.ts +21 -0
- package/dist/job/index.js +21 -0
- package/dist/job/lifecycle.d.ts +176 -0
- package/dist/job/lifecycle.js +230 -0
- package/dist/job/state.d.ts +92 -0
- package/dist/job/state.js +40 -0
- package/dist/platform.d.ts +138 -0
- package/dist/platform.js +140 -0
- package/dist/round/agent.d.ts +271 -0
- package/dist/round/agent.js +678 -0
- package/dist/round/index.d.ts +25 -0
- package/dist/round/index.js +23 -0
- package/dist/round/policy.d.ts +98 -0
- package/dist/round/policy.js +1 -0
- package/dist/round/subagent.d.ts +87 -0
- package/dist/round/subagent.js +108 -0
- package/dist/round/turn.d.ts +249 -0
- package/dist/round/turn.js +564 -0
- package/dist/round/workflow.d.ts +147 -0
- package/dist/round/workflow.js +387 -0
- package/dist/runtime/index.d.ts +127 -0
- package/dist/runtime/index.js +186 -0
- package/dist/runtime/tool-families.d.ts +32 -0
- package/dist/runtime/tool-families.js +61 -0
- package/dist/subagent/fingerprint.d.ts +37 -0
- package/dist/subagent/fingerprint.js +92 -0
- package/dist/subagent/index.d.ts +169 -0
- package/dist/subagent/index.js +330 -0
- package/dist/subagent/prompt.d.ts +32 -0
- package/dist/subagent/prompt.js +37 -0
- package/dist/subagent/run.d.ts +157 -0
- package/dist/subagent/run.js +540 -0
- package/dist/subagent/workspace.d.ts +85 -0
- package/dist/subagent/workspace.js +127 -0
- package/dist/subtasks/catalog.d.ts +40 -0
- package/dist/subtasks/catalog.js +36 -0
- package/dist/subtasks/decomposition.d.ts +85 -0
- package/dist/subtasks/decomposition.js +156 -0
- package/dist/subtasks/delegate.d.ts +120 -0
- package/dist/subtasks/delegate.js +131 -0
- package/dist/subtasks/index.d.ts +9 -0
- package/dist/subtasks/index.js +9 -0
- package/dist/subtasks/subtask-types.d.ts +91 -0
- package/dist/subtasks/subtask-types.js +103 -0
- package/dist/subtasks/types.d.ts +295 -0
- package/dist/subtasks/types.js +15 -0
- package/dist/testing/auth.d.ts +34 -0
- package/dist/testing/auth.js +35 -0
- package/dist/testing/do.d.ts +29 -0
- package/dist/testing/do.js +25 -0
- package/dist/testing/fake-session.d.ts +26 -0
- package/dist/testing/fake-session.js +37 -0
- package/dist/testing/fixtures.d.ts +64 -0
- package/dist/testing/fixtures.js +104 -0
- package/dist/testing/harness.d.ts +97 -0
- package/dist/testing/harness.js +138 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.js +35 -0
- package/dist/testing/mock-model.d.ts +77 -0
- package/dist/testing/mock-model.js +136 -0
- package/dist/testing/node.d.ts +56 -0
- package/dist/testing/node.js +56 -0
- package/dist/testing/vcr-global-setup.d.ts +12 -0
- package/dist/testing/vcr-global-setup.js +15 -0
- package/dist/testing/vcr-shared.d.ts +38 -0
- package/dist/testing/vcr-shared.js +33 -0
- package/dist/testing/vcr-spec.d.ts +25 -0
- package/dist/testing/vcr-spec.js +124 -0
- package/dist/testing/vcr-store.d.ts +86 -0
- package/dist/testing/vcr-store.js +191 -0
- package/dist/testing/vcr.d.ts +117 -0
- package/dist/testing/vcr.js +275 -0
- package/dist/worker/define-agent.d.ts +123 -0
- package/dist/worker/define-agent.js +20 -0
- package/dist/worker/index.d.ts +218 -0
- package/dist/worker/index.js +369 -0
- package/eslint-rules/index.js +31 -0
- package/eslint-rules/no-deprecated-object-properties.js +81 -0
- package/package.json +178 -0
- package/scripts/generate-keys.mjs +48 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type JWK } from "jose";
|
|
2
|
+
import { type Task } from "@a2a-js/sdk";
|
|
3
|
+
import type { PlainTask } from "./task.js";
|
|
4
|
+
/**
|
|
5
|
+
* Outbound push-notification (accept + notify) helpers — the "notify" half of the
|
|
6
|
+
* async A2A contract. The gatekeeper dispatches `SendMessage` with a
|
|
7
|
+
* `taskPushNotificationConfig` (webhook `url` + validation `token`), we accept
|
|
8
|
+
* immediately with a `submitted` Task, and later POST the terminal Task back to
|
|
9
|
+
* that webhook. This module builds the Task shapes and signs + sends that
|
|
10
|
+
* callback.
|
|
11
|
+
*
|
|
12
|
+
* The callback is authenticated exactly like the AgentCard: a short-lived EdDSA
|
|
13
|
+
* JWT signed by `A2A_SIGNING_KEY`, whose protected header `kid`+`jku` must equal
|
|
14
|
+
* the card's signing `kid`+`jku` (see {@link file://./card.ts} `signCard`) — the
|
|
15
|
+
* gatekeeper pinned those at registration (Trust-On-First-Use) and verifies the
|
|
16
|
+
* callback token against that same public JWKS. No shared secret crosses the
|
|
17
|
+
* boundary; only our public key is ever used to verify.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The two shared values on this hop — the signature algorithm and the header
|
|
21
|
+
* carrying the per-task validation `token` — come from
|
|
22
|
+
* `@dynamicagents/g2a-protocol`, the same package the gatekeeper reads them from.
|
|
23
|
+
*
|
|
24
|
+
* The header used to be declared here and again in slack-gatekeeper, each with a
|
|
25
|
+
* comment naming the other file. It is a slightly different case from the claim
|
|
26
|
+
* names: the value is `@a2a-js/sdk`'s own default for `tokenHeaderName`, not
|
|
27
|
+
* Dynamic Agents' choice, but the SDK never *exports* it — it exists only as an
|
|
28
|
+
* inline fallback — so neither side could import it and both wrote it down.
|
|
29
|
+
*
|
|
30
|
+
* Re-exported so `@dynamicagents/core/a2a` keeps being where an agent finds it.
|
|
31
|
+
*/
|
|
32
|
+
export { NOTIFICATION_TOKEN_HEADER } from "@dynamicagents/g2a-protocol";
|
|
33
|
+
/**
|
|
34
|
+
* The `submitted` Task we return synchronously to accept a turn (A2A §7.2). The
|
|
35
|
+
* gatekeeper only requires a non-empty `id`; the actual reply follows later via the
|
|
36
|
+
* callback.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildSubmittedTask(taskId: string, contextId: string): PlainTask;
|
|
39
|
+
/**
|
|
40
|
+
* The terminal `completed` Task for a turn the agent deliberately did not
|
|
41
|
+
* answer — a turn an `AgentPlugin.shouldHandleTurn` gate declined, or whatever
|
|
42
|
+
* else a host treats as "nothing to say". Same shape as
|
|
43
|
+
* {@link buildSubmittedTask}: **no `status.message` at all**.
|
|
44
|
+
*
|
|
45
|
+
* The callback is still POSTed. The gatekeeper's pending row has to resolve — we
|
|
46
|
+
* simply hand it nothing to post to Slack. There is no `messageId` because there
|
|
47
|
+
* is no message, so unlike {@link buildCompletedTask} nothing needs a stable id
|
|
48
|
+
* for the gatekeeper to dedupe on: a `notify`-step retry re-delivers no content and
|
|
49
|
+
* is idempotent by construction.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildNoReplyCompletedTask(taskId: string, contextId: string): PlainTask;
|
|
52
|
+
/**
|
|
53
|
+
* A non-terminal `working` Task snapshot carrying an intermediate content message.
|
|
54
|
+
* Streamed live from the DO as the tool loop emits content before the final reply.
|
|
55
|
+
*
|
|
56
|
+
* `messageId` is derived from `${taskId}:${key}` — stable across re-runs (see
|
|
57
|
+
* {@link agentTextMessage}) so the gatekeeper dedupes correctly on workflow replay.
|
|
58
|
+
*
|
|
59
|
+
* `key` is a **semantic** string, not a step counter, and the distinction is
|
|
60
|
+
* load-bearing: an agent that runs several rounds per task emits progress from
|
|
61
|
+
* each, so a bare index makes round 0's third step and round 1's third step the
|
|
62
|
+
* same message to the gatekeeper — it dedupes the second away and the user watches
|
|
63
|
+
* a task go quiet. A caller that genuinely has one flat sequence can pass
|
|
64
|
+
* `String(i)`; one with rounds should key on both (`r1:step:3`), and milestones
|
|
65
|
+
* on what they are (`ack:1`).
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildWorkingTask(taskId: string, contextId: string, text: string, key: string): PlainTask;
|
|
68
|
+
/**
|
|
69
|
+
* The terminal `completed` Task POSTed to the gatekeeper callback. The `messageId` is
|
|
70
|
+
* deterministic (`${taskId}:final`, not a fresh UUID) because this is built in the
|
|
71
|
+
* workflow body, which re-runs on replay: a random id would change on a notify-step
|
|
72
|
+
* retry and the gatekeeper would dedupe the final message as a new one and double-post.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildCompletedTask(taskId: string, contextId: string, reply: string): PlainTask;
|
|
75
|
+
/**
|
|
76
|
+
* The terminal `failed` Task POSTed to the gatekeeper callback — an unexpected,
|
|
77
|
+
* non-transient failure aborted the turn.
|
|
78
|
+
*
|
|
79
|
+
* A2A v1.0 gives a task no structured error (`TaskStatus` is only
|
|
80
|
+
* `{state, message, timestamp}`), so the state *is* the failure signal and `text`
|
|
81
|
+
* is the only place to explain. Keep that text user-safe: the gatekeeper renders it
|
|
82
|
+
* to a human, under its own "⚠️ *Agent …* (failed):" prefix.
|
|
83
|
+
*
|
|
84
|
+
* Shares the `${taskId}:final` messageId with {@link buildCompletedTask} by
|
|
85
|
+
* design: a Task terminates exactly once and the two states are mutually
|
|
86
|
+
* exclusive, so only one of them is ever built and posted — and a notify retry
|
|
87
|
+
* re-posts that same one under the same dedupe key.
|
|
88
|
+
*/
|
|
89
|
+
export declare function buildFailedTask(taskId: string, contextId: string, text: string): PlainTask;
|
|
90
|
+
/**
|
|
91
|
+
* Sign the callback JWT the gatekeeper verifies against our pinned card key. The
|
|
92
|
+
* protected header mirrors the card signature (`kid`+`jku`); `aud` must equal the
|
|
93
|
+
* exact webhook URL the gatekeeper handed us in the `taskPushNotificationConfig`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function signCallbackJwt(privateJwk: JWK & {
|
|
96
|
+
kid: string;
|
|
97
|
+
}, opts: {
|
|
98
|
+
jku: string;
|
|
99
|
+
aud: string;
|
|
100
|
+
}): Promise<string>;
|
|
101
|
+
/**
|
|
102
|
+
* POST a Task snapshot to the gatekeeper's push-notification webhook, wrapped in the
|
|
103
|
+
* v1.0 `StreamResponse` envelope. Returns the raw `Response` so the caller (the
|
|
104
|
+
* workflow's `notify` step) can decide whether a non-2xx warrants a retry.
|
|
105
|
+
*/
|
|
106
|
+
export declare function postNotification(url: string, token: string, jwt: string, task: Task): Promise<Response>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { SignJWT } from "jose";
|
|
2
|
+
import { TaskState } from "@a2a-js/sdk";
|
|
3
|
+
import { V1PushNotificationSerializer } from "@a2a-js/sdk/server";
|
|
4
|
+
import { A2A_JWS_ALG, NOTIFICATION_TOKEN_HEADER } from "@dynamicagents/g2a-protocol";
|
|
5
|
+
import { agentTextMessage } from "./parts.js";
|
|
6
|
+
/**
|
|
7
|
+
* Outbound push-notification (accept + notify) helpers — the "notify" half of the
|
|
8
|
+
* async A2A contract. The gatekeeper dispatches `SendMessage` with a
|
|
9
|
+
* `taskPushNotificationConfig` (webhook `url` + validation `token`), we accept
|
|
10
|
+
* immediately with a `submitted` Task, and later POST the terminal Task back to
|
|
11
|
+
* that webhook. This module builds the Task shapes and signs + sends that
|
|
12
|
+
* callback.
|
|
13
|
+
*
|
|
14
|
+
* The callback is authenticated exactly like the AgentCard: a short-lived EdDSA
|
|
15
|
+
* JWT signed by `A2A_SIGNING_KEY`, whose protected header `kid`+`jku` must equal
|
|
16
|
+
* the card's signing `kid`+`jku` (see {@link file://./card.ts} `signCard`) — the
|
|
17
|
+
* gatekeeper pinned those at registration (Trust-On-First-Use) and verifies the
|
|
18
|
+
* callback token against that same public JWKS. No shared secret crosses the
|
|
19
|
+
* boundary; only our public key is ever used to verify.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The two shared values on this hop — the signature algorithm and the header
|
|
23
|
+
* carrying the per-task validation `token` — come from
|
|
24
|
+
* `@dynamicagents/g2a-protocol`, the same package the gatekeeper reads them from.
|
|
25
|
+
*
|
|
26
|
+
* The header used to be declared here and again in slack-gatekeeper, each with a
|
|
27
|
+
* comment naming the other file. It is a slightly different case from the claim
|
|
28
|
+
* names: the value is `@a2a-js/sdk`'s own default for `tokenHeaderName`, not
|
|
29
|
+
* Dynamic Agents' choice, but the SDK never *exports* it — it exists only as an
|
|
30
|
+
* inline fallback — so neither side could import it and both wrote it down.
|
|
31
|
+
*
|
|
32
|
+
* Re-exported so `@dynamicagents/core/a2a` keeps being where an agent finds it.
|
|
33
|
+
*/
|
|
34
|
+
export { NOTIFICATION_TOKEN_HEADER } from "@dynamicagents/g2a-protocol";
|
|
35
|
+
/**
|
|
36
|
+
* Callback-JWT lifetime. The gatekeeper enforces `maxTokenAge: 10m` with a 60s clock
|
|
37
|
+
* tolerance, so keep this comfortably under that.
|
|
38
|
+
*/
|
|
39
|
+
const CALLBACK_TOKEN_TTL = "5m";
|
|
40
|
+
/**
|
|
41
|
+
* The SDK's canonical v1.0 push-notification body encoder: the `StreamResponse`
|
|
42
|
+
* envelope as protobuf-JSON, with content type `application/a2a+json`. v1.0
|
|
43
|
+
* moved push notifications onto the same envelope the streaming transports use
|
|
44
|
+
* (v0.3 POSTed a bare `Task`), so the encoding is the SDK's rather than ours —
|
|
45
|
+
* the gatekeeper decodes it with `StreamResponse.fromJSON`.
|
|
46
|
+
*/
|
|
47
|
+
const PUSH_SERIALIZER = new V1PushNotificationSerializer();
|
|
48
|
+
/** A Task snapshot in `state` carrying no message — nothing to say, only a state change. */
|
|
49
|
+
function buildBareTask(taskId, contextId, state) {
|
|
50
|
+
return {
|
|
51
|
+
id: taskId,
|
|
52
|
+
contextId,
|
|
53
|
+
status: {
|
|
54
|
+
state,
|
|
55
|
+
message: undefined,
|
|
56
|
+
timestamp: new Date().toISOString()
|
|
57
|
+
},
|
|
58
|
+
artifacts: [],
|
|
59
|
+
history: [],
|
|
60
|
+
metadata: undefined
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The `submitted` Task we return synchronously to accept a turn (A2A §7.2). The
|
|
65
|
+
* gatekeeper only requires a non-empty `id`; the actual reply follows later via the
|
|
66
|
+
* callback.
|
|
67
|
+
*/
|
|
68
|
+
export function buildSubmittedTask(taskId, contextId) {
|
|
69
|
+
return buildBareTask(taskId, contextId, TaskState.TASK_STATE_SUBMITTED);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The terminal `completed` Task for a turn the agent deliberately did not
|
|
73
|
+
* answer — a turn an `AgentPlugin.shouldHandleTurn` gate declined, or whatever
|
|
74
|
+
* else a host treats as "nothing to say". Same shape as
|
|
75
|
+
* {@link buildSubmittedTask}: **no `status.message` at all**.
|
|
76
|
+
*
|
|
77
|
+
* The callback is still POSTed. The gatekeeper's pending row has to resolve — we
|
|
78
|
+
* simply hand it nothing to post to Slack. There is no `messageId` because there
|
|
79
|
+
* is no message, so unlike {@link buildCompletedTask} nothing needs a stable id
|
|
80
|
+
* for the gatekeeper to dedupe on: a `notify`-step retry re-delivers no content and
|
|
81
|
+
* is idempotent by construction.
|
|
82
|
+
*/
|
|
83
|
+
export function buildNoReplyCompletedTask(taskId, contextId) {
|
|
84
|
+
return buildBareTask(taskId, contextId, TaskState.TASK_STATE_COMPLETED);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A Task snapshot POSTed to the gatekeeper callback in a given `state`, carrying one
|
|
88
|
+
* `agent` message. The gatekeeper reads the reply from `status.message.parts`, so
|
|
89
|
+
* the text lives there.
|
|
90
|
+
*/
|
|
91
|
+
function buildTaskUpdate(taskId, contextId, state, text, messageId) {
|
|
92
|
+
const task = buildBareTask(taskId, contextId, state);
|
|
93
|
+
task.status.message = agentTextMessage({
|
|
94
|
+
messageId,
|
|
95
|
+
text,
|
|
96
|
+
contextId,
|
|
97
|
+
taskId
|
|
98
|
+
});
|
|
99
|
+
return task;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A non-terminal `working` Task snapshot carrying an intermediate content message.
|
|
103
|
+
* Streamed live from the DO as the tool loop emits content before the final reply.
|
|
104
|
+
*
|
|
105
|
+
* `messageId` is derived from `${taskId}:${key}` — stable across re-runs (see
|
|
106
|
+
* {@link agentTextMessage}) so the gatekeeper dedupes correctly on workflow replay.
|
|
107
|
+
*
|
|
108
|
+
* `key` is a **semantic** string, not a step counter, and the distinction is
|
|
109
|
+
* load-bearing: an agent that runs several rounds per task emits progress from
|
|
110
|
+
* each, so a bare index makes round 0's third step and round 1's third step the
|
|
111
|
+
* same message to the gatekeeper — it dedupes the second away and the user watches
|
|
112
|
+
* a task go quiet. A caller that genuinely has one flat sequence can pass
|
|
113
|
+
* `String(i)`; one with rounds should key on both (`r1:step:3`), and milestones
|
|
114
|
+
* on what they are (`ack:1`).
|
|
115
|
+
*/
|
|
116
|
+
export function buildWorkingTask(taskId, contextId, text, key) {
|
|
117
|
+
return buildTaskUpdate(taskId, contextId, TaskState.TASK_STATE_WORKING, text, `${taskId}:${key}`);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The terminal `completed` Task POSTed to the gatekeeper callback. The `messageId` is
|
|
121
|
+
* deterministic (`${taskId}:final`, not a fresh UUID) because this is built in the
|
|
122
|
+
* workflow body, which re-runs on replay: a random id would change on a notify-step
|
|
123
|
+
* retry and the gatekeeper would dedupe the final message as a new one and double-post.
|
|
124
|
+
*/
|
|
125
|
+
export function buildCompletedTask(taskId, contextId, reply) {
|
|
126
|
+
return buildTaskUpdate(taskId, contextId, TaskState.TASK_STATE_COMPLETED, reply, `${taskId}:final`);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The terminal `failed` Task POSTed to the gatekeeper callback — an unexpected,
|
|
130
|
+
* non-transient failure aborted the turn.
|
|
131
|
+
*
|
|
132
|
+
* A2A v1.0 gives a task no structured error (`TaskStatus` is only
|
|
133
|
+
* `{state, message, timestamp}`), so the state *is* the failure signal and `text`
|
|
134
|
+
* is the only place to explain. Keep that text user-safe: the gatekeeper renders it
|
|
135
|
+
* to a human, under its own "⚠️ *Agent …* (failed):" prefix.
|
|
136
|
+
*
|
|
137
|
+
* Shares the `${taskId}:final` messageId with {@link buildCompletedTask} by
|
|
138
|
+
* design: a Task terminates exactly once and the two states are mutually
|
|
139
|
+
* exclusive, so only one of them is ever built and posted — and a notify retry
|
|
140
|
+
* re-posts that same one under the same dedupe key.
|
|
141
|
+
*/
|
|
142
|
+
export function buildFailedTask(taskId, contextId, text) {
|
|
143
|
+
return buildTaskUpdate(taskId, contextId, TaskState.TASK_STATE_FAILED, text, `${taskId}:final`);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Sign the callback JWT the gatekeeper verifies against our pinned card key. The
|
|
147
|
+
* protected header mirrors the card signature (`kid`+`jku`); `aud` must equal the
|
|
148
|
+
* exact webhook URL the gatekeeper handed us in the `taskPushNotificationConfig`.
|
|
149
|
+
*/
|
|
150
|
+
export async function signCallbackJwt(privateJwk, opts) {
|
|
151
|
+
return new SignJWT({})
|
|
152
|
+
.setProtectedHeader({
|
|
153
|
+
alg: A2A_JWS_ALG,
|
|
154
|
+
kid: privateJwk.kid,
|
|
155
|
+
jku: opts.jku
|
|
156
|
+
})
|
|
157
|
+
.setAudience(opts.aud)
|
|
158
|
+
.setIssuedAt()
|
|
159
|
+
.setExpirationTime(CALLBACK_TOKEN_TTL)
|
|
160
|
+
.sign(privateJwk);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* POST a Task snapshot to the gatekeeper's push-notification webhook, wrapped in the
|
|
164
|
+
* v1.0 `StreamResponse` envelope. Returns the raw `Response` so the caller (the
|
|
165
|
+
* workflow's `notify` step) can decide whether a non-2xx warrants a retry.
|
|
166
|
+
*/
|
|
167
|
+
export async function postNotification(url, token, jwt, task) {
|
|
168
|
+
const { body, contentType } = PUSH_SERIALIZER.serialize({
|
|
169
|
+
payload: { $case: "task", value: task }
|
|
170
|
+
});
|
|
171
|
+
return fetch(url, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"content-type": contentType,
|
|
175
|
+
authorization: `Bearer ${jwt}`,
|
|
176
|
+
[NOTIFICATION_TOKEN_HEADER]: token
|
|
177
|
+
},
|
|
178
|
+
body
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Message, type Part } from "@a2a-js/sdk";
|
|
2
|
+
import type { PlainMessage, PlainPart } from "./task.js";
|
|
3
|
+
/**
|
|
4
|
+
* A `text` part carrying `text`. Typed as the narrowed {@link PlainPart} (which
|
|
5
|
+
* widens to `Part` for free) so a message built here can cross the DO RPC
|
|
6
|
+
* boundary — see {@link file://./task.ts}.
|
|
7
|
+
*/
|
|
8
|
+
export declare function textPart(text: string): PlainPart;
|
|
9
|
+
/** Concatenate the text of every `text` part, trimming surrounding whitespace. */
|
|
10
|
+
export declare function partsText(parts: Part[] | undefined): string;
|
|
11
|
+
/** The plain-text content of an inbound A2A message (what the caller said). */
|
|
12
|
+
export declare function textOf(message: Message): string;
|
|
13
|
+
/**
|
|
14
|
+
* Bounds the inbound user text carried in a durable Workflow payload (UTF-8).
|
|
15
|
+
* Workflow params have a platform size limit, and a caller's message is the one
|
|
16
|
+
* unbounded thing that goes into them.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MAX_INBOUND_TEXT_BYTES: number;
|
|
19
|
+
/** Invalid inbound content that must not cross the A2A-to-workflow boundary. */
|
|
20
|
+
export declare class InboundPartError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract and validate the user-turn text. Rejects a message with no usable
|
|
25
|
+
* text, and enforces a single UTF-8 size bound *before* the text enters a
|
|
26
|
+
* workflow payload — past that point the failure is a workflow that will not
|
|
27
|
+
* start, with no request left to answer on.
|
|
28
|
+
*
|
|
29
|
+
* File and data parts are deliberately out of scope: only text crosses into the
|
|
30
|
+
* agent runtime.
|
|
31
|
+
*/
|
|
32
|
+
export declare function inboundText(message: Message): string;
|
|
33
|
+
/**
|
|
34
|
+
* An `agent`-role message carrying one text part, with the proto-required
|
|
35
|
+
* fields this agent never sets filled in. `messageId` is the gatekeeper's dedupe
|
|
36
|
+
* key, so callers pass a **stable** id (never a fresh random per attempt) — a
|
|
37
|
+
* callback the workflow/DO re-runs must reuse the same id or the gatekeeper
|
|
38
|
+
* treats the replay as a new message and double-posts.
|
|
39
|
+
*/
|
|
40
|
+
export declare function agentTextMessage(input: {
|
|
41
|
+
messageId: string;
|
|
42
|
+
text: string;
|
|
43
|
+
contextId: string;
|
|
44
|
+
taskId: string;
|
|
45
|
+
}): PlainMessage;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Role } from "@a2a-js/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Constructors and readers for A2A v1.0 message content — the one place the
|
|
4
|
+
* adapter reaches into the `@a2a-js/sdk` message shape. Everything past
|
|
5
|
+
* {@link file://./executor.ts A2AExecutor} works in plain strings, so the agent
|
|
6
|
+
* runtime never sees an A2A type.
|
|
7
|
+
*
|
|
8
|
+
* The v1.0 data model is generated from the protobuf schema, so the wire types
|
|
9
|
+
* are "all fields present": a `Part` carries a `content` oneof discriminated by
|
|
10
|
+
* `$case` (plus `filename`/`mediaType`) instead of v0.3's `kind`, and a
|
|
11
|
+
* `Message` carries empty strings and empty arrays rather than omitted
|
|
12
|
+
* optionals. Hand-writing those literals at every call site is noisy and easy to
|
|
13
|
+
* get subtly wrong, so every part/message this agent emits is built here.
|
|
14
|
+
*/
|
|
15
|
+
/** Media type stamped on the text parts this agent emits. */
|
|
16
|
+
const TEXT_MEDIA_TYPE = "text/plain";
|
|
17
|
+
/**
|
|
18
|
+
* A `text` part carrying `text`. Typed as the narrowed {@link PlainPart} (which
|
|
19
|
+
* widens to `Part` for free) so a message built here can cross the DO RPC
|
|
20
|
+
* boundary — see {@link file://./task.ts}.
|
|
21
|
+
*/
|
|
22
|
+
export function textPart(text) {
|
|
23
|
+
return {
|
|
24
|
+
content: { $case: "text", value: text },
|
|
25
|
+
metadata: undefined,
|
|
26
|
+
filename: "",
|
|
27
|
+
mediaType: TEXT_MEDIA_TYPE
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Concatenate the text of every `text` part, trimming surrounding whitespace. */
|
|
31
|
+
export function partsText(parts) {
|
|
32
|
+
let out = "";
|
|
33
|
+
for (const part of parts ?? []) {
|
|
34
|
+
if (part.content?.$case === "text")
|
|
35
|
+
out += part.content.value;
|
|
36
|
+
}
|
|
37
|
+
return out.trim();
|
|
38
|
+
}
|
|
39
|
+
/** The plain-text content of an inbound A2A message (what the caller said). */
|
|
40
|
+
export function textOf(message) {
|
|
41
|
+
return partsText(message.parts);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Bounds the inbound user text carried in a durable Workflow payload (UTF-8).
|
|
45
|
+
* Workflow params have a platform size limit, and a caller's message is the one
|
|
46
|
+
* unbounded thing that goes into them.
|
|
47
|
+
*/
|
|
48
|
+
export const MAX_INBOUND_TEXT_BYTES = 256 * 1024;
|
|
49
|
+
const encoder = new TextEncoder();
|
|
50
|
+
/** Invalid inbound content that must not cross the A2A-to-workflow boundary. */
|
|
51
|
+
export class InboundPartError extends Error {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "InboundPartError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract and validate the user-turn text. Rejects a message with no usable
|
|
59
|
+
* text, and enforces a single UTF-8 size bound *before* the text enters a
|
|
60
|
+
* workflow payload — past that point the failure is a workflow that will not
|
|
61
|
+
* start, with no request left to answer on.
|
|
62
|
+
*
|
|
63
|
+
* File and data parts are deliberately out of scope: only text crosses into the
|
|
64
|
+
* agent runtime.
|
|
65
|
+
*/
|
|
66
|
+
export function inboundText(message) {
|
|
67
|
+
const text = textOf(message);
|
|
68
|
+
if (!text) {
|
|
69
|
+
throw new InboundPartError("message has no usable text");
|
|
70
|
+
}
|
|
71
|
+
if (encoder.encode(text).byteLength > MAX_INBOUND_TEXT_BYTES) {
|
|
72
|
+
throw new InboundPartError("message text exceeds the size limit");
|
|
73
|
+
}
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* An `agent`-role message carrying one text part, with the proto-required
|
|
78
|
+
* fields this agent never sets filled in. `messageId` is the gatekeeper's dedupe
|
|
79
|
+
* key, so callers pass a **stable** id (never a fresh random per attempt) — a
|
|
80
|
+
* callback the workflow/DO re-runs must reuse the same id or the gatekeeper
|
|
81
|
+
* treats the replay as a new message and double-posts.
|
|
82
|
+
*/
|
|
83
|
+
export function agentTextMessage(input) {
|
|
84
|
+
return {
|
|
85
|
+
messageId: input.messageId,
|
|
86
|
+
role: Role.ROLE_AGENT,
|
|
87
|
+
parts: [textPart(input.text)],
|
|
88
|
+
contextId: input.contextId,
|
|
89
|
+
taskId: input.taskId,
|
|
90
|
+
metadata: undefined,
|
|
91
|
+
extensions: [],
|
|
92
|
+
referenceTaskIds: []
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Task } from "@a2a-js/sdk";
|
|
2
|
+
import type { OnContent } from "../agent/inference.js";
|
|
3
|
+
/**
|
|
4
|
+
* The gatekeeper callback channel for one accepted turn.
|
|
5
|
+
*
|
|
6
|
+
* Every agent that accepts asynchronously has to do the same four things with
|
|
7
|
+
* the push config the gatekeeper handed it: sign a callback JWT against the
|
|
8
|
+
* deployment's card key, POST `working` snapshots as the model reasons, POST the
|
|
9
|
+
* terminal Task, and never let a failed progress post take down a turn. That was
|
|
10
|
+
* written out longhand in four places across two agents in the starter — twice as
|
|
11
|
+
* a `streamWorking` closure inside a Durable Object, twice as a `notify` step
|
|
12
|
+
* inside a Workflow — with the signing, the header names and the swallow-and-log
|
|
13
|
+
* discipline duplicated each time.
|
|
14
|
+
*
|
|
15
|
+
* It is one object here because none of it is per-agent. What *is* per-agent is
|
|
16
|
+
* the notification **key**, and that is the one thing a caller supplies.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Everything needed to call a gatekeeper back about one task.
|
|
20
|
+
*
|
|
21
|
+
* RPC-serializable by construction: it crosses the Workflow → Durable Object
|
|
22
|
+
* boundary on every turn, so it holds only strings.
|
|
23
|
+
*/
|
|
24
|
+
export interface TurnPushContext {
|
|
25
|
+
/** The accepted task id (echoed on every callback of this turn). */
|
|
26
|
+
taskId: string;
|
|
27
|
+
/** A2A context id, echoed on every callback. */
|
|
28
|
+
contextId: string;
|
|
29
|
+
/** Gatekeeper push-notification webhook (also the callback JWT `aud`). */
|
|
30
|
+
pushUrl: string;
|
|
31
|
+
/** Per-task validation token the gatekeeper set; echoed in the callback header. */
|
|
32
|
+
pushToken: string;
|
|
33
|
+
/** This agent's card-signing JWKS URL — the callback JWT `jku` (pinned key). */
|
|
34
|
+
jku: string;
|
|
35
|
+
}
|
|
36
|
+
export interface PushChannel {
|
|
37
|
+
/**
|
|
38
|
+
* POST one `working` Task snapshot, keyed by a stable semantic `key`.
|
|
39
|
+
*
|
|
40
|
+
* **Best-effort**: every failure is logged and swallowed, so a progress post
|
|
41
|
+
* can never abort generation or fail a durable step. The key is what lets the
|
|
42
|
+
* gatekeeper dedupe a re-posted event on replay, so it must be derived from
|
|
43
|
+
* position (`r2:step:3`), never from content or a clock.
|
|
44
|
+
*/
|
|
45
|
+
working(text: string, key: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* An {@link OnContent} sink that posts each intermediate message as a
|
|
48
|
+
* `working` snapshot, signing the JWT once and reusing it across the turn.
|
|
49
|
+
*
|
|
50
|
+
* `key` maps a 0-based step ordinal to its notification key. An agent running
|
|
51
|
+
* one turn per task can use the bare index; an agent with rounds **must**
|
|
52
|
+
* include the round, or two rounds of one task collide on the gatekeeper.
|
|
53
|
+
*/
|
|
54
|
+
stream(key: (stepIndex: number) => string): OnContent;
|
|
55
|
+
/**
|
|
56
|
+
* POST the terminal Task. **Throws on a non-2xx** so the calling Workflow step
|
|
57
|
+
* retries — the opposite of {@link working}, because this is the delivery the
|
|
58
|
+
* whole turn exists for.
|
|
59
|
+
*/
|
|
60
|
+
deliver(task: Task): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the callback channel for one turn.
|
|
64
|
+
*
|
|
65
|
+
* `signingKey` is the deployment's Ed25519 private JWK as JSON — `A2A_SIGNING_KEY`
|
|
66
|
+
* by default. There is one per origin, not one per agent: the card sits at a
|
|
67
|
+
* well-known URI, which RFC 8615 defines per-authority, so this origin publishes
|
|
68
|
+
* one card and the gatekeeper pins one key for every agent on it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function createPushChannel(signingKey: string, push: TurnPushContext): PushChannel;
|
package/dist/a2a/push.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parsePrivateJwk } from "./card.js";
|
|
2
|
+
import { buildWorkingTask, postNotification, signCallbackJwt } from "./notify.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build the callback channel for one turn.
|
|
5
|
+
*
|
|
6
|
+
* `signingKey` is the deployment's Ed25519 private JWK as JSON — `A2A_SIGNING_KEY`
|
|
7
|
+
* by default. There is one per origin, not one per agent: the card sits at a
|
|
8
|
+
* well-known URI, which RFC 8615 defines per-authority, so this origin publishes
|
|
9
|
+
* one card and the gatekeeper pins one key for every agent on it.
|
|
10
|
+
*/
|
|
11
|
+
export function createPushChannel(signingKey, push) {
|
|
12
|
+
// Signed lazily and reused: one turn can post many progress snapshots, and
|
|
13
|
+
// re-signing per message costs a key import each time for no benefit.
|
|
14
|
+
let jwt;
|
|
15
|
+
const sign = () => (jwt ??= signCallbackJwt(parsePrivateJwk(signingKey), {
|
|
16
|
+
jku: push.jku,
|
|
17
|
+
aud: push.pushUrl
|
|
18
|
+
}));
|
|
19
|
+
const post = async (task) => postNotification(push.pushUrl, push.pushToken, await sign(), task);
|
|
20
|
+
const working = async (text, key) => {
|
|
21
|
+
try {
|
|
22
|
+
const res = await post(buildWorkingTask(push.taskId, push.contextId, text, key));
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
console.warn("[push] working notification non-2xx", {
|
|
25
|
+
taskId: push.taskId,
|
|
26
|
+
key,
|
|
27
|
+
status: res.status
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
// Swallowed on purpose, including a signing failure: a turn that cannot
|
|
33
|
+
// report its progress is still a turn that should deliver its answer.
|
|
34
|
+
console.warn("[push] working notification failed", {
|
|
35
|
+
taskId: push.taskId,
|
|
36
|
+
key,
|
|
37
|
+
err: String(err)
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
working,
|
|
43
|
+
stream(key) {
|
|
44
|
+
return (text, stepIndex) => working(text, key(stepIndex));
|
|
45
|
+
},
|
|
46
|
+
async deliver(task) {
|
|
47
|
+
const res = await post(task);
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
throw new Error(`gatekeeper notification failed: HTTP ${res.status}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This deployment's own public origin — learned from the request path, never
|
|
3
|
+
* configured.
|
|
4
|
+
*
|
|
5
|
+
* ## Why an agent needs it at all
|
|
6
|
+
*
|
|
7
|
+
* A Worker that only *answers* never needs to know its own name.
|
|
8
|
+
* {@link file://../worker/index.ts createA2AWorker} derives its audience, its
|
|
9
|
+
* card and its `jku` from `new URL(request.url).origin`, and none of it outlives
|
|
10
|
+
* the request. An agent that **calls out** mint-signed does need it:
|
|
11
|
+
* {@link file://./caller-token.ts signCallerToken} puts it in `iss` and derives
|
|
12
|
+
* the token's `jku` from it. That call happens inside a Durable Object, where
|
|
13
|
+
* there is no `Request` — which is the whole difficulty.
|
|
14
|
+
*
|
|
15
|
+
* The obvious answer is a `SELF_ORIGIN` secret, and it is the wrong one. It
|
|
16
|
+
* restates a value the request already carries, and it has to be kept
|
|
17
|
+
* byte-identical by hand with the origin allowlist on the far side, in every
|
|
18
|
+
* environment, forever. Every sibling that tried it took it back out —
|
|
19
|
+
* `slack-gatekeeper` discovers its own origin from the first signature-verified
|
|
20
|
+
* request rather than being told, and a verifying Worker that once carried a
|
|
21
|
+
* configured audience replaced it with `url.origin`.
|
|
22
|
+
*
|
|
23
|
+
* ## Where the value comes from
|
|
24
|
+
*
|
|
25
|
+
* Core already sends the origin into the Durable Object on every turn, one field
|
|
26
|
+
* short of this use. `A2AExecutor` computes `jku` as `${origin}${jwksPath}` and
|
|
27
|
+
* it rides {@link file://./push.ts TurnPushContext} through the Workflow into
|
|
28
|
+
* `runTaskTurn` and `executeSubtaskChunk`.
|
|
29
|
+
*
|
|
30
|
+
* That is the same origin `signCallerToken` needs, and not by coincidence: a
|
|
31
|
+
* caller token's `jku` **must** be the JWKS the verifier fetches, and `iss` must
|
|
32
|
+
* agree with it — the third check in {@link file://./verify.ts verify.ts}. An
|
|
33
|
+
* origin derived from anywhere else is exactly what that check exists to catch,
|
|
34
|
+
* so deriving it from the `jku` core already serves makes the agreement
|
|
35
|
+
* structural instead of clerical.
|
|
36
|
+
*
|
|
37
|
+
* ## Pinned on the first turn, and in memory
|
|
38
|
+
*
|
|
39
|
+
* The first origin an isolate is told wins, and later ones are ignored. That is
|
|
40
|
+
* not laziness about staleness — it is what makes the value safe to *read*.
|
|
41
|
+
*
|
|
42
|
+
* A Durable Object's input gate stays open across a non-storage await, and this
|
|
43
|
+
* package runs concurrent RPCs into one object by design (`round/workflow.ts`
|
|
44
|
+
* runs a round's branches under `Promise.all`). Mutable instance state can
|
|
45
|
+
* therefore change while a turn is awaiting a model call, and the credential
|
|
46
|
+
* thunks that read this are lazy — they run several frames below the turn, when
|
|
47
|
+
* the client is built. Pinned, the field is immutable after its first write, so
|
|
48
|
+
* every concurrent reader in the isolate gets the same string and no turn can
|
|
49
|
+
* sign as another turn's origin.
|
|
50
|
+
*
|
|
51
|
+
* The cost of pinning is what an agent does not have: several identities. An
|
|
52
|
+
* agent has one endpoint — the one its card advertises, the one a gatekeeper calls
|
|
53
|
+
* and a verifier allowlists — so there is nothing to follow. Note the asymmetry
|
|
54
|
+
* with a *verifier*, which derives the audience it expects per request and must
|
|
55
|
+
* not cache it: a verifier has to accept every hostname it answers on, while a
|
|
56
|
+
* signer needs one stable identity.
|
|
57
|
+
*
|
|
58
|
+
* Nothing is persisted, which is what keeps a pin from outliving its truth. An
|
|
59
|
+
* isolate is fresh on every `wrangler deploy` and recycles on its own, so a moved
|
|
60
|
+
* deployment re-learns its origin from the next turn it serves.
|
|
61
|
+
*/
|
|
62
|
+
export declare class SelfOrigin {
|
|
63
|
+
private observed?;
|
|
64
|
+
/**
|
|
65
|
+
* Offer the origin of an absolute URL seen on the request path — a `jku`, an
|
|
66
|
+
* endpoint, or a bare origin. Only `.origin` is kept, so a path or a trailing
|
|
67
|
+
* slash cannot reach a token claim.
|
|
68
|
+
*
|
|
69
|
+
* **The first usable value wins**; every later call is a no-op, including one
|
|
70
|
+
* naming a different origin. Called at each RPC entry and again when the push
|
|
71
|
+
* channel is built, so most calls are already no-ops — but the reason for the
|
|
72
|
+
* pin is the read side, not the write side. See the note above the class.
|
|
73
|
+
*
|
|
74
|
+
* Silently ignores anything unusable (absent, relative, or a scheme that has
|
|
75
|
+
* no meaningful origin), and an unusable value never pins: the parse comes
|
|
76
|
+
* first and the field is assigned only on success. This runs at the top of a
|
|
77
|
+
* turn, where a diagnostic value must never be the thing that fails it; the
|
|
78
|
+
* throw belongs at {@link require}, where something actually wanted the value.
|
|
79
|
+
*/
|
|
80
|
+
note(url: string | undefined): void;
|
|
81
|
+
/** The pinned origin, or `undefined` when nothing has carried one yet. */
|
|
82
|
+
peek(): string | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* The pinned origin, for a caller that cannot proceed without it.
|
|
85
|
+
*
|
|
86
|
+
* Throws naming the timing, because that is what the mistake always is: the
|
|
87
|
+
* value arrives with a turn, so `onStart`, a constructor and a scheduled
|
|
88
|
+
* callback all run before any request has said what this deployment is called.
|
|
89
|
+
*/
|
|
90
|
+
require(): string;
|
|
91
|
+
}
|