@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.
Files changed (168) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +522 -0
  3. package/dist/a2a/agent-stub.d.ts +60 -0
  4. package/dist/a2a/agent-stub.js +1 -0
  5. package/dist/a2a/caller-token.d.ts +44 -0
  6. package/dist/a2a/caller-token.js +61 -0
  7. package/dist/a2a/caller.d.ts +23 -0
  8. package/dist/a2a/caller.js +33 -0
  9. package/dist/a2a/card.d.ts +158 -0
  10. package/dist/a2a/card.js +163 -0
  11. package/dist/a2a/context.d.ts +19 -0
  12. package/dist/a2a/context.js +61 -0
  13. package/dist/a2a/deliver.d.ts +159 -0
  14. package/dist/a2a/deliver.js +185 -0
  15. package/dist/a2a/executor.d.ts +84 -0
  16. package/dist/a2a/executor.js +106 -0
  17. package/dist/a2a/index.d.ts +35 -0
  18. package/dist/a2a/index.js +34 -0
  19. package/dist/a2a/notify.d.ts +106 -0
  20. package/dist/a2a/notify.js +180 -0
  21. package/dist/a2a/parts.d.ts +45 -0
  22. package/dist/a2a/parts.js +94 -0
  23. package/dist/a2a/push.d.ts +70 -0
  24. package/dist/a2a/push.js +53 -0
  25. package/dist/a2a/self-origin.d.ts +91 -0
  26. package/dist/a2a/self-origin.js +114 -0
  27. package/dist/a2a/task-store.d.ts +44 -0
  28. package/dist/a2a/task-store.js +99 -0
  29. package/dist/a2a/task.d.ts +85 -0
  30. package/dist/a2a/task.js +15 -0
  31. package/dist/a2a/verify.d.ts +80 -0
  32. package/dist/a2a/verify.js +143 -0
  33. package/dist/agent/budget.d.ts +46 -0
  34. package/dist/agent/budget.js +42 -0
  35. package/dist/agent/control.d.ts +109 -0
  36. package/dist/agent/control.js +115 -0
  37. package/dist/agent/errors.d.ts +85 -0
  38. package/dist/agent/errors.js +64 -0
  39. package/dist/agent/final-reply.d.ts +49 -0
  40. package/dist/agent/final-reply.js +68 -0
  41. package/dist/agent/history.d.ts +97 -0
  42. package/dist/agent/history.js +133 -0
  43. package/dist/agent/index.d.ts +29 -0
  44. package/dist/agent/index.js +29 -0
  45. package/dist/agent/inference.d.ts +110 -0
  46. package/dist/agent/inference.js +120 -0
  47. package/dist/agent/model.d.ts +90 -0
  48. package/dist/agent/model.js +1 -0
  49. package/dist/agent/session.d.ts +100 -0
  50. package/dist/agent/session.js +82 -0
  51. package/dist/agent/workers-ai/index.d.ts +23 -0
  52. package/dist/agent/workers-ai/index.js +23 -0
  53. package/dist/agent/workers-ai/runtime.d.ts +42 -0
  54. package/dist/agent/workers-ai/runtime.js +63 -0
  55. package/dist/alarm/index.d.ts +77 -0
  56. package/dist/alarm/index.js +116 -0
  57. package/dist/config.d.ts +202 -0
  58. package/dist/config.js +135 -0
  59. package/dist/contract/index.d.ts +9 -0
  60. package/dist/contract/index.js +8 -0
  61. package/dist/contract/plugin.d.ts +324 -0
  62. package/dist/contract/plugin.js +114 -0
  63. package/dist/contract/recipe.d.ts +180 -0
  64. package/dist/contract/recipe.js +1 -0
  65. package/dist/contract/validation.d.ts +91 -0
  66. package/dist/contract/validation.js +84 -0
  67. package/dist/db/db.d.ts +147 -0
  68. package/dist/db/db.js +90 -0
  69. package/dist/db/index.d.ts +8 -0
  70. package/dist/db/index.js +8 -0
  71. package/dist/db/migrations/index.d.ts +20 -0
  72. package/dist/db/migrations/index.js +60 -0
  73. package/dist/db/models/subtasks.d.ts +100 -0
  74. package/dist/db/models/subtasks.js +241 -0
  75. package/dist/db/models/tasks.d.ts +118 -0
  76. package/dist/db/models/tasks.js +274 -0
  77. package/dist/db/schema.d.ts +468 -0
  78. package/dist/db/schema.js +88 -0
  79. package/dist/env.d.ts +53 -0
  80. package/dist/env.js +47 -0
  81. package/dist/host/agent.d.ts +305 -0
  82. package/dist/host/agent.js +400 -0
  83. package/dist/host/index.d.ts +20 -0
  84. package/dist/host/index.js +19 -0
  85. package/dist/host/plugin-host.d.ts +42 -0
  86. package/dist/host/plugin-host.js +1 -0
  87. package/dist/index.d.ts +19 -0
  88. package/dist/index.js +17 -0
  89. package/dist/job/index.d.ts +21 -0
  90. package/dist/job/index.js +21 -0
  91. package/dist/job/lifecycle.d.ts +176 -0
  92. package/dist/job/lifecycle.js +230 -0
  93. package/dist/job/state.d.ts +92 -0
  94. package/dist/job/state.js +40 -0
  95. package/dist/platform.d.ts +138 -0
  96. package/dist/platform.js +140 -0
  97. package/dist/round/agent.d.ts +271 -0
  98. package/dist/round/agent.js +678 -0
  99. package/dist/round/index.d.ts +25 -0
  100. package/dist/round/index.js +23 -0
  101. package/dist/round/policy.d.ts +98 -0
  102. package/dist/round/policy.js +1 -0
  103. package/dist/round/subagent.d.ts +87 -0
  104. package/dist/round/subagent.js +108 -0
  105. package/dist/round/turn.d.ts +249 -0
  106. package/dist/round/turn.js +564 -0
  107. package/dist/round/workflow.d.ts +147 -0
  108. package/dist/round/workflow.js +387 -0
  109. package/dist/runtime/index.d.ts +127 -0
  110. package/dist/runtime/index.js +186 -0
  111. package/dist/runtime/tool-families.d.ts +32 -0
  112. package/dist/runtime/tool-families.js +61 -0
  113. package/dist/subagent/fingerprint.d.ts +37 -0
  114. package/dist/subagent/fingerprint.js +92 -0
  115. package/dist/subagent/index.d.ts +169 -0
  116. package/dist/subagent/index.js +330 -0
  117. package/dist/subagent/prompt.d.ts +32 -0
  118. package/dist/subagent/prompt.js +37 -0
  119. package/dist/subagent/run.d.ts +157 -0
  120. package/dist/subagent/run.js +540 -0
  121. package/dist/subagent/workspace.d.ts +85 -0
  122. package/dist/subagent/workspace.js +127 -0
  123. package/dist/subtasks/catalog.d.ts +40 -0
  124. package/dist/subtasks/catalog.js +36 -0
  125. package/dist/subtasks/decomposition.d.ts +85 -0
  126. package/dist/subtasks/decomposition.js +156 -0
  127. package/dist/subtasks/delegate.d.ts +120 -0
  128. package/dist/subtasks/delegate.js +131 -0
  129. package/dist/subtasks/index.d.ts +9 -0
  130. package/dist/subtasks/index.js +9 -0
  131. package/dist/subtasks/subtask-types.d.ts +91 -0
  132. package/dist/subtasks/subtask-types.js +103 -0
  133. package/dist/subtasks/types.d.ts +295 -0
  134. package/dist/subtasks/types.js +15 -0
  135. package/dist/testing/auth.d.ts +34 -0
  136. package/dist/testing/auth.js +35 -0
  137. package/dist/testing/do.d.ts +29 -0
  138. package/dist/testing/do.js +25 -0
  139. package/dist/testing/fake-session.d.ts +26 -0
  140. package/dist/testing/fake-session.js +37 -0
  141. package/dist/testing/fixtures.d.ts +64 -0
  142. package/dist/testing/fixtures.js +104 -0
  143. package/dist/testing/harness.d.ts +97 -0
  144. package/dist/testing/harness.js +138 -0
  145. package/dist/testing/index.d.ts +31 -0
  146. package/dist/testing/index.js +35 -0
  147. package/dist/testing/mock-model.d.ts +77 -0
  148. package/dist/testing/mock-model.js +136 -0
  149. package/dist/testing/node.d.ts +56 -0
  150. package/dist/testing/node.js +56 -0
  151. package/dist/testing/vcr-global-setup.d.ts +12 -0
  152. package/dist/testing/vcr-global-setup.js +15 -0
  153. package/dist/testing/vcr-shared.d.ts +38 -0
  154. package/dist/testing/vcr-shared.js +33 -0
  155. package/dist/testing/vcr-spec.d.ts +25 -0
  156. package/dist/testing/vcr-spec.js +124 -0
  157. package/dist/testing/vcr-store.d.ts +86 -0
  158. package/dist/testing/vcr-store.js +191 -0
  159. package/dist/testing/vcr.d.ts +117 -0
  160. package/dist/testing/vcr.js +275 -0
  161. package/dist/worker/define-agent.d.ts +123 -0
  162. package/dist/worker/define-agent.js +20 -0
  163. package/dist/worker/index.d.ts +218 -0
  164. package/dist/worker/index.js +369 -0
  165. package/eslint-rules/index.js +31 -0
  166. package/eslint-rules/no-deprecated-object-properties.js +81 -0
  167. package/package.json +178 -0
  168. package/scripts/generate-keys.mjs +48 -0
@@ -0,0 +1,114 @@
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 class SelfOrigin {
63
+ 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) {
81
+ if (this.observed || !url)
82
+ return;
83
+ let parsed;
84
+ try {
85
+ parsed = new URL(url);
86
+ }
87
+ catch {
88
+ return;
89
+ }
90
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
91
+ return;
92
+ this.observed = parsed.origin;
93
+ }
94
+ /** The pinned origin, or `undefined` when nothing has carried one yet. */
95
+ peek() {
96
+ return this.observed;
97
+ }
98
+ /**
99
+ * The pinned origin, for a caller that cannot proceed without it.
100
+ *
101
+ * Throws naming the timing, because that is what the mistake always is: the
102
+ * value arrives with a turn, so `onStart`, a constructor and a scheduled
103
+ * callback all run before any request has said what this deployment is called.
104
+ */
105
+ require() {
106
+ if (!this.observed) {
107
+ throw new Error("this deployment's own origin is not known on this instance yet: it is " +
108
+ "learned from the `jku` that arrives with every turn, so it is " +
109
+ "available inside a turn or a subtask chunk — not from onStart, a " +
110
+ "constructor or a scheduled callback");
111
+ }
112
+ return this.observed;
113
+ }
114
+ }
@@ -0,0 +1,44 @@
1
+ import type { TaskStore } from "@a2a-js/sdk/server";
2
+ import { type ListTasksRequest, type ListTasksResponse, type Task } from "@a2a-js/sdk";
3
+ import type { GatekeeperIdentity } from "./verify.js";
4
+ import type { AgentResolver } from "./agent-stub.js";
5
+ /**
6
+ * A durable {@link TaskStore} for the a2a-js `DefaultRequestHandler`, backed by
7
+ * the caller's agent Durable Object (via native RPC) instead of the SDK's
8
+ * per-request `InMemoryTaskStore`.
9
+ *
10
+ * Task state must survive the accept → async callback gap (and answer
11
+ * `GetTask`/`CancelTask`/`ListTasks` across requests), so it lives in the same
12
+ * per-caller DO that owns the Session — keyed by the verified `identity.key`,
13
+ * exactly like {@link file://./executor.ts A2AExecutor}. A workflow updates the
14
+ * same rows through its own DO RPC calls, so this store and the workflow share
15
+ * one source of truth.
16
+ *
17
+ * v1.0 passes a `ServerCallContext` to every method so a shared store can scope
18
+ * rows by tenant/owner. This store needs none of that: the DO instance it routes
19
+ * to *is* the scope, resolved from the caller identity the Worker verified, so a
20
+ * task is unreachable from any other caller by construction.
21
+ */
22
+ export declare class DurableTaskStore implements TaskStore {
23
+ private readonly identity;
24
+ private readonly resolveAgent;
25
+ constructor(identity: GatekeeperIdentity, resolveAgent: AgentResolver);
26
+ load(taskId: string): Promise<Task | undefined>;
27
+ save(task: Task): Promise<void>;
28
+ /**
29
+ * `ListTasks` (new in v1.0): this caller's tasks, newest first, with the
30
+ * spec's optional `contextId` / `status` / `statusTimestampAfter` filters
31
+ * applied in SQL.
32
+ *
33
+ * The page token is the row offset as a decimal string. Offset paging can skip
34
+ * or repeat a row if tasks are written mid-scan, which is acceptable here: the
35
+ * listing is a diagnostic view over an append-mostly table, and the alternative
36
+ * (a keyset cursor) buys stability this caller has no use for.
37
+ *
38
+ * An agent whose DO does not implement `listTasks` answers the RPC with the
39
+ * protocol's own "unsupported" error rather than a binding-level failure — the
40
+ * capability is genuinely optional, and one of the two predecessor agents
41
+ * shipped without it for its whole life.
42
+ */
43
+ list(params: ListTasksRequest): Promise<ListTasksResponse>;
44
+ }
@@ -0,0 +1,99 @@
1
+ import { UnsupportedOperationError } from "@a2a-js/sdk/errors";
2
+ import { TaskState } from "@a2a-js/sdk";
3
+ /**
4
+ * A durable {@link TaskStore} for the a2a-js `DefaultRequestHandler`, backed by
5
+ * the caller's agent Durable Object (via native RPC) instead of the SDK's
6
+ * per-request `InMemoryTaskStore`.
7
+ *
8
+ * Task state must survive the accept → async callback gap (and answer
9
+ * `GetTask`/`CancelTask`/`ListTasks` across requests), so it lives in the same
10
+ * per-caller DO that owns the Session — keyed by the verified `identity.key`,
11
+ * exactly like {@link file://./executor.ts A2AExecutor}. A workflow updates the
12
+ * same rows through its own DO RPC calls, so this store and the workflow share
13
+ * one source of truth.
14
+ *
15
+ * v1.0 passes a `ServerCallContext` to every method so a shared store can scope
16
+ * rows by tenant/owner. This store needs none of that: the DO instance it routes
17
+ * to *is* the scope, resolved from the caller identity the Worker verified, so a
18
+ * task is unreachable from any other caller by construction.
19
+ */
20
+ export class DurableTaskStore {
21
+ identity;
22
+ resolveAgent;
23
+ constructor(identity, resolveAgent) {
24
+ this.identity = identity;
25
+ this.resolveAgent = resolveAgent;
26
+ }
27
+ async load(taskId) {
28
+ return ((await this.resolveAgent(this.identity).getTask(taskId)) ?? undefined);
29
+ }
30
+ async save(task) {
31
+ await this.resolveAgent(this.identity).saveTask(task);
32
+ }
33
+ /**
34
+ * `ListTasks` (new in v1.0): this caller's tasks, newest first, with the
35
+ * spec's optional `contextId` / `status` / `statusTimestampAfter` filters
36
+ * applied in SQL.
37
+ *
38
+ * The page token is the row offset as a decimal string. Offset paging can skip
39
+ * or repeat a row if tasks are written mid-scan, which is acceptable here: the
40
+ * listing is a diagnostic view over an append-mostly table, and the alternative
41
+ * (a keyset cursor) buys stability this caller has no use for.
42
+ *
43
+ * An agent whose DO does not implement `listTasks` answers the RPC with the
44
+ * protocol's own "unsupported" error rather than a binding-level failure — the
45
+ * capability is genuinely optional, and one of the two predecessor agents
46
+ * shipped without it for its whole life.
47
+ */
48
+ async list(params) {
49
+ const agent = this.resolveAgent(this.identity);
50
+ if (!agent.listTasks) {
51
+ throw new UnsupportedOperationError({
52
+ message: "this agent does not support ListTasks"
53
+ });
54
+ }
55
+ const pageSize = clampPageSize(params.pageSize);
56
+ const offset = parseOffset(params.pageToken);
57
+ const { tasks, totalSize } = await agent.listTasks({
58
+ contextId: params.contextId,
59
+ // The proto default (`UNSPECIFIED`) is the spec's "no status filter".
60
+ state: params.status === TaskState.TASK_STATE_UNSPECIFIED
61
+ ? undefined
62
+ : params.status,
63
+ updatedAfter: parseTimestamp(params.statusTimestampAfter),
64
+ includeArtifacts: params.includeArtifacts === true,
65
+ historyLength: params.historyLength,
66
+ limit: pageSize,
67
+ offset
68
+ });
69
+ const nextOffset = offset + tasks.length;
70
+ return {
71
+ tasks,
72
+ nextPageToken: nextOffset < totalSize ? String(nextOffset) : "",
73
+ pageSize,
74
+ totalSize
75
+ };
76
+ }
77
+ }
78
+ /** Page size bounds from the `ListTasks` spec: default 50, min 1, max 100. */
79
+ function clampPageSize(pageSize) {
80
+ if (pageSize === undefined)
81
+ return 50;
82
+ return Math.min(100, Math.max(1, Math.trunc(pageSize)));
83
+ }
84
+ /** Row offset from a page token; an unparseable token restarts at the first page. */
85
+ function parseOffset(pageToken) {
86
+ const offset = Number.parseInt(pageToken, 10);
87
+ return Number.isSafeInteger(offset) && offset > 0 ? offset : 0;
88
+ }
89
+ /**
90
+ * `statusTimestampAfter` (ISO 8601) as epoch milliseconds, matching the
91
+ * `updated_at` column the store filters on — that column is stamped on every
92
+ * status write, so it tracks `status.timestamp`.
93
+ */
94
+ function parseTimestamp(iso) {
95
+ if (!iso)
96
+ return undefined;
97
+ const ms = Date.parse(iso);
98
+ return Number.isNaN(ms) ? undefined : ms;
99
+ }
@@ -0,0 +1,85 @@
1
+ import { type Role, type TaskState } from "@a2a-js/sdk";
2
+ /**
3
+ * The A2A `Task` (and its nested types) as they cross the DO RPC boundary.
4
+ *
5
+ * Two constraints shape these, and the SDK types satisfy neither directly:
6
+ *
7
+ * 1. **Serializability.** A v1.0 `Part` carries a `content` oneof whose `raw`
8
+ * arm is a `Buffer`; its methods fail Cloudflare's `Rpc.Serializable`
9
+ * constraint and collapse the generated DO-stub return types to `never`.
10
+ * Pinning `content` to its `text` arm removes that arm — and this agent only
11
+ * ever produces, and only ever receives, text (see {@link file://./parts.ts}).
12
+ * 2. **Instantiation depth.** These are written out as plain interfaces rather
13
+ * than derived with `Omit<Task, …> & { … }`. The v1.0 model is deep enough
14
+ * that layering mapped types under Cloudflare's own RPC type mapping exceeds
15
+ * TypeScript's instantiation limit at the stub call sites.
16
+ *
17
+ * Each of these widens to its SDK counterpart for free, so no cast is needed at
18
+ * the a2a-js edges (`AgentEvent.task`, `TaskStore.load`/`list`); the
19
+ * {@link _Widens} guard below fails the build if that ever stops holding.
20
+ * Runtime is unaffected — tasks are JSON-round-tripped in storage, so any field
21
+ * the SDK adds survives even though these types don't name it.
22
+ */
23
+ /**
24
+ * The A2A extension `metadata` bag as this agent handles it: never set. Its SDK
25
+ * type is an open `{ [key: string]: any }` map, which is both what collapses the
26
+ * DO-stub types (`unknown` members fail `Rpc.Serializable`) and what pushes the
27
+ * stub-mapped `Task` past TypeScript's instantiation limit. Pinning it to
28
+ * `undefined` keeps both in check and still widens to the SDK type. Metadata a
29
+ * caller sent is not erased — stored tasks are JSON blobs, so it round-trips
30
+ * untouched; it is simply invisible to this agent, which never reads it.
31
+ */
32
+ type A2AMetadata = undefined;
33
+ /** A `Part` restricted to the text arm of its `content` oneof. */
34
+ export interface PlainPart {
35
+ content: {
36
+ $case: "text";
37
+ value: string;
38
+ };
39
+ metadata: A2AMetadata;
40
+ filename: string;
41
+ mediaType: string;
42
+ }
43
+ /** A `Message` whose parts are all {@link PlainPart}. */
44
+ export interface PlainMessage {
45
+ messageId: string;
46
+ contextId: string;
47
+ taskId: string;
48
+ role: Role;
49
+ parts: PlainPart[];
50
+ metadata: A2AMetadata;
51
+ extensions: string[];
52
+ referenceTaskIds: string[];
53
+ }
54
+ /** An `Artifact` whose parts are all {@link PlainPart}. */
55
+ export interface PlainArtifact {
56
+ artifactId: string;
57
+ name: string;
58
+ description: string;
59
+ parts: PlainPart[];
60
+ metadata: A2AMetadata;
61
+ extensions: string[];
62
+ }
63
+ /** A `TaskStatus` whose message is a {@link PlainMessage}. */
64
+ export interface PlainStatus {
65
+ state: TaskState;
66
+ message: PlainMessage | undefined;
67
+ timestamp: string | undefined;
68
+ }
69
+ /** A `Task` built entirely from the plain types above. */
70
+ export interface PlainTask {
71
+ id: string;
72
+ contextId: string;
73
+ status: PlainStatus;
74
+ artifacts: PlainArtifact[];
75
+ history: PlainMessage[];
76
+ metadata: A2AMetadata;
77
+ }
78
+ /**
79
+ * A task state rendered for humans and for the `notify_tasks.state` column
80
+ * (`TASK_STATE_INPUT_REQUIRED` → `input-required`). v1.0 states are numeric enum
81
+ * members, so anything stored or displayed goes through the protobuf JSON name
82
+ * first. Matches slack-gatekeeper's `taskStateLabel`.
83
+ */
84
+ export declare function taskStateLabel(state: TaskState): string;
85
+ export {};
@@ -0,0 +1,15 @@
1
+ import { taskStateToJSON } from "@a2a-js/sdk";
2
+ const _assertWidens = true;
3
+ void _assertWidens;
4
+ /**
5
+ * A task state rendered for humans and for the `notify_tasks.state` column
6
+ * (`TASK_STATE_INPUT_REQUIRED` → `input-required`). v1.0 states are numeric enum
7
+ * members, so anything stored or displayed goes through the protobuf JSON name
8
+ * first. Matches slack-gatekeeper's `taskStateLabel`.
9
+ */
10
+ export function taskStateLabel(state) {
11
+ return taskStateToJSON(state)
12
+ .replace(/^TASK_STATE_/, "")
13
+ .toLowerCase()
14
+ .replace(/_/g, "-");
15
+ }
@@ -0,0 +1,80 @@
1
+ import { type JWTPayload } from "jose";
2
+ import { type GatekeeperIdentity } from "@dynamicagents/g2a-protocol";
3
+ /**
4
+ * Verify the gatekeeper identity JWT (the "B authenticates A" half of zero-trust).
5
+ *
6
+ * The gatekeeper signs a short-lived EdDSA JWT and sends it as a Bearer token on
7
+ * every A2A call. Per RFC 7515 §4.1.2 it embeds a `jku` header pointing at its
8
+ * public JWKS, so remote agents don't need a separately configured JWKS URL —
9
+ * they read `jku` straight from the token and verify the key from there.
10
+ *
11
+ * Security: the `jku` origin is validated against the allowed origins before
12
+ * fetching, preventing key-injection attacks (an attacker cannot point `jku` at
13
+ * their own JWKS and have it accepted).
14
+ *
15
+ * **This file is a guardian.** The four checks below — `jku` present → origin
16
+ * allowlist → `iss` origin equals `jku` origin → `jwtVerify` pinned to EdDSA —
17
+ * are the whole zero-trust contract, and they arrived here byte-identical from
18
+ * two independently-evolved agents. Do not weaken any of them, do not make any
19
+ * of them optional, and do not add a bypass for local development: run a local
20
+ * gatekeeper instead.
21
+ */
22
+ /**
23
+ * The wire contract — claim names, algorithm, identity shape — comes from
24
+ * `@dynamicagents/g2a-protocol`, which the gatekeeper also depends on directly.
25
+ *
26
+ * It used to be declared here and again in the gatekeeper, each with a comment
27
+ * saying it must match the other, because the gatekeeper is not an agent and must
28
+ * not import this package. That failed exactly as it always does: the two sides
29
+ * drifted onto different claim namespaces, verification read an empty tenant, and
30
+ * every request 401'd with both builds green.
31
+ *
32
+ * The namespace moved again in g2a-protocol 0.3.0, to `dynamicagents.dev` — the
33
+ * same class of change, made deliberately this time. It is survivable only
34
+ * because both sides now read it from one package: bump the protocol dependency
35
+ * and ship core and `slack-gatekeeper` in the same deploy.
36
+ *
37
+ * Re-exported below so nothing downstream of `@dynamicagents/core/a2a` has to know
38
+ * the split happened — an agent still imports these from here.
39
+ */
40
+ export { IDENTITY_CLAIM, TENANT_CLAIM, type GatekeeperIdentity } from "@dynamicagents/g2a-protocol";
41
+ /** Thrown when a gatekeeper token is missing or fails verification. */
42
+ export declare class GatekeeperAuthError extends Error {
43
+ constructor(message: string);
44
+ }
45
+ /** Extract the Bearer token from an `Authorization` header, or null. */
46
+ export declare function bearerToken(request: Request): string | null;
47
+ export interface VerifyOptions {
48
+ /** Allowed gatekeeper origins — validates both the `jku` domain and `iss` claim. */
49
+ allowedOrigins: string[];
50
+ audience: string;
51
+ /** Claim carrying the caller identity. Defaults to {@link IDENTITY_CLAIM}. */
52
+ identityClaim?: string;
53
+ /** Claim carrying the authorized tenant. Defaults to {@link TENANT_CLAIM}. */
54
+ tenantClaim?: string;
55
+ }
56
+ /**
57
+ * Normalize configured gatekeeper origins to the HTTPS origins emitted in gatekeeper
58
+ * JWT `jku` and `iss` claims. This accepts a hostname, `http://` URL, or URL
59
+ * with a trailing slash while keeping the verification allowlist exact.
60
+ */
61
+ export declare function normalizeGatekeeperOrigins(origins: string[]): string[];
62
+ /**
63
+ * Verify a gatekeeper JWT and return its payload, parsed identity and authorized
64
+ * tenant.
65
+ *
66
+ * The `jku` JWK Set URL is read directly from the token's protected header
67
+ * (RFC 7515 §4.1.2) and validated against the allowlist before fetching, so no
68
+ * separate JWKS URL configuration is needed on the remote side.
69
+ *
70
+ * `tenant` is returned rather than enforced: this function stays a pure
71
+ * signature/origin question, and only the caller knows which tenant the request
72
+ * actually addressed. {@link createA2AWorker} compares the two.
73
+ *
74
+ * Throws {@link GatekeeperAuthError} on any failure.
75
+ */
76
+ export declare function verifyGatekeeperToken(token: string, opts: VerifyOptions): Promise<{
77
+ payload: JWTPayload;
78
+ identity: GatekeeperIdentity;
79
+ tenant: string;
80
+ }>;
@@ -0,0 +1,143 @@
1
+ import { createRemoteJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
2
+ import { A2A_JWS_ALG, readIdentityClaim, readTenantClaim } from "@dynamicagents/g2a-protocol";
3
+ /**
4
+ * Verify the gatekeeper identity JWT (the "B authenticates A" half of zero-trust).
5
+ *
6
+ * The gatekeeper signs a short-lived EdDSA JWT and sends it as a Bearer token on
7
+ * every A2A call. Per RFC 7515 §4.1.2 it embeds a `jku` header pointing at its
8
+ * public JWKS, so remote agents don't need a separately configured JWKS URL —
9
+ * they read `jku` straight from the token and verify the key from there.
10
+ *
11
+ * Security: the `jku` origin is validated against the allowed origins before
12
+ * fetching, preventing key-injection attacks (an attacker cannot point `jku` at
13
+ * their own JWKS and have it accepted).
14
+ *
15
+ * **This file is a guardian.** The four checks below — `jku` present → origin
16
+ * allowlist → `iss` origin equals `jku` origin → `jwtVerify` pinned to EdDSA —
17
+ * are the whole zero-trust contract, and they arrived here byte-identical from
18
+ * two independently-evolved agents. Do not weaken any of them, do not make any
19
+ * of them optional, and do not add a bypass for local development: run a local
20
+ * gatekeeper instead.
21
+ */
22
+ /**
23
+ * The wire contract — claim names, algorithm, identity shape — comes from
24
+ * `@dynamicagents/g2a-protocol`, which the gatekeeper also depends on directly.
25
+ *
26
+ * It used to be declared here and again in the gatekeeper, each with a comment
27
+ * saying it must match the other, because the gatekeeper is not an agent and must
28
+ * not import this package. That failed exactly as it always does: the two sides
29
+ * drifted onto different claim namespaces, verification read an empty tenant, and
30
+ * every request 401'd with both builds green.
31
+ *
32
+ * The namespace moved again in g2a-protocol 0.3.0, to `dynamicagents.dev` — the
33
+ * same class of change, made deliberately this time. It is survivable only
34
+ * because both sides now read it from one package: bump the protocol dependency
35
+ * and ship core and `slack-gatekeeper` in the same deploy.
36
+ *
37
+ * Re-exported below so nothing downstream of `@dynamicagents/core/a2a` has to know
38
+ * the split happened — an agent still imports these from here.
39
+ */
40
+ export { IDENTITY_CLAIM, TENANT_CLAIM } from "@dynamicagents/g2a-protocol";
41
+ /** Thrown when a gatekeeper token is missing or fails verification. */
42
+ export class GatekeeperAuthError extends Error {
43
+ constructor(message) {
44
+ super(message);
45
+ this.name = "GatekeeperAuthError";
46
+ }
47
+ }
48
+ // jose's remote JWKS helper caches keys + handles rotation; build one per URL
49
+ // and reuse it across requests in the same isolate.
50
+ const jwksByUrl = new Map();
51
+ function jwksFor(url) {
52
+ let set = jwksByUrl.get(url);
53
+ if (!set) {
54
+ set = createRemoteJWKSet(new URL(url));
55
+ jwksByUrl.set(url, set);
56
+ }
57
+ return set;
58
+ }
59
+ /** Extract the Bearer token from an `Authorization` header, or null. */
60
+ export function bearerToken(request) {
61
+ const header = request.headers.get("authorization") ?? "";
62
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
63
+ return match ? match[1] : null;
64
+ }
65
+ /**
66
+ * Normalize configured gatekeeper origins to the HTTPS origins emitted in gatekeeper
67
+ * JWT `jku` and `iss` claims. This accepts a hostname, `http://` URL, or URL
68
+ * with a trailing slash while keeping the verification allowlist exact.
69
+ */
70
+ export function normalizeGatekeeperOrigins(origins) {
71
+ return origins.map((origin) => {
72
+ const trimmed = origin.trim();
73
+ if (!trimmed) {
74
+ throw new GatekeeperAuthError("allowed gatekeeper origin cannot be empty");
75
+ }
76
+ try {
77
+ return new URL(`https://${trimmed.replace(/^https?:\/\//i, "")}`).origin;
78
+ }
79
+ catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ throw new GatekeeperAuthError(`invalid allowed gatekeeper origin '${origin}': ${message}`);
82
+ }
83
+ });
84
+ }
85
+ /**
86
+ * Verify a gatekeeper JWT and return its payload, parsed identity and authorized
87
+ * tenant.
88
+ *
89
+ * The `jku` JWK Set URL is read directly from the token's protected header
90
+ * (RFC 7515 §4.1.2) and validated against the allowlist before fetching, so no
91
+ * separate JWKS URL configuration is needed on the remote side.
92
+ *
93
+ * `tenant` is returned rather than enforced: this function stays a pure
94
+ * signature/origin question, and only the caller knows which tenant the request
95
+ * actually addressed. {@link createA2AWorker} compares the two.
96
+ *
97
+ * Throws {@link GatekeeperAuthError} on any failure.
98
+ */
99
+ export async function verifyGatekeeperToken(token, opts) {
100
+ try {
101
+ const allowedOrigins = normalizeGatekeeperOrigins(opts.allowedOrigins);
102
+ // Extract jku from the protected header — this is the standard way the
103
+ // gatekeeper advertises where to fetch its public key (RFC 7515 §4.1.2).
104
+ const header = decodeProtectedHeader(token);
105
+ const jku = header.jku;
106
+ if (!jku) {
107
+ throw new GatekeeperAuthError("gatekeeper JWT missing jku header (RFC 7515 §4.1.2)");
108
+ }
109
+ // Security: validate the jku origin before fetching. Without this an
110
+ // attacker could forge a token with jku pointing at their own JWKS.
111
+ const jkuOrigin = new URL(jku).origin;
112
+ if (!allowedOrigins.includes(jkuOrigin)) {
113
+ throw new GatekeeperAuthError(`jku origin '${jkuOrigin}' is not in the allowed gatekeeper origins`);
114
+ }
115
+ // Prevent one listed gatekeeper from impersonating another: the origin where
116
+ // keys are fetched must match the origin that issued the token.
117
+ const rawIss = decodeJwt(token).iss ?? "";
118
+ const issOrigin = new URL(rawIss).origin;
119
+ if (issOrigin !== jkuOrigin) {
120
+ throw new GatekeeperAuthError(`jku origin '${jkuOrigin}' does not match iss origin '${issOrigin}'`);
121
+ }
122
+ const { payload } = await jwtVerify(token, jwksFor(jku), {
123
+ issuer: allowedOrigins,
124
+ audience: opts.audience,
125
+ algorithms: [A2A_JWS_ALG]
126
+ });
127
+ // Read through the protocol package's readers rather than indexing the
128
+ // payload here: they are where "an absent or malformed claim yields an
129
+ // empty value, never a default" has its single implementation, and the
130
+ // emptiness is load-bearing — `createA2AWorker` compares the tenant against
131
+ // the one the request body addressed, and `""` matches none of them.
132
+ return {
133
+ payload,
134
+ identity: readIdentityClaim(payload, opts.identityClaim),
135
+ tenant: readTenantClaim(payload, opts.tenantClaim)
136
+ };
137
+ }
138
+ catch (err) {
139
+ if (err instanceof GatekeeperAuthError)
140
+ throw err;
141
+ throw new GatekeeperAuthError(err.message);
142
+ }
143
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * One round's turn allowance, carried as a single mutable object rather than as a
3
+ * number threaded down and a count threaded back up.
4
+ *
5
+ * The distinction matters because a round is not one model call. Each slot may
6
+ * repair a rejected decomposition several times before the round moves on to the
7
+ * fallback, and every one of those attempts *restarts* the round from the same
8
+ * messages — so a failed attempt is work redone, with whatever tool side effects
9
+ * it already had. They all spend the same allowance. When that allowance was a
10
+ * plain number, keeping later attempts honest meant remembering to decrement it
11
+ * between each pair, and forgetting let one round cost a multiple of its budget.
12
+ * Sharing one object removes the thing there was to forget.
13
+ *
14
+ * `spent` is mutated where the turn is actually spent (the model loop's
15
+ * `onStepEnd`) and read where it actually matters (the RPC return that the
16
+ * Workflow meters against the Task). Nothing in between carries it.
17
+ */
18
+ /** One round's allowance and what it has spent, across every attempt in the round. */
19
+ export interface TurnBudget {
20
+ /** What this round was allowed when it started: the Task's unspent turns. */
21
+ readonly allowance: number;
22
+ /** Turns spent so far. Mutated by the model loop, one per completed step. */
23
+ spent: number;
24
+ }
25
+ /** A fresh budget for a round that may spend `allowance` turns. */
26
+ export declare const newTurnBudget: (allowance: number) => TurnBudget;
27
+ /**
28
+ * Steps one `generateText` attempt may spend, given what the run has already used.
29
+ *
30
+ * Must be evaluated **per attempt**: `isStepCount` counts steps within a single
31
+ * call, so an allowance computed once and shared with later attempts would hand
32
+ * each of them the turns its predecessors already burned.
33
+ *
34
+ * The `Math.max(1, …)` floor is load-bearing, not defensive. A primary that
35
+ * consumed the entire remainder still leaves the fallback one step, because an
36
+ * attempt with nothing to spend cannot reach an ending — which fails the round and
37
+ * costs the caller its answer, a far worse trade than one turn. So a round may
38
+ * exceed its allowance by exactly one, and never by more.
39
+ *
40
+ * The bound survives repair attempts because only an attempt that *starts* already
41
+ * at the allowance can overrun, and only by the one step of this floor: an attempt
42
+ * that starts under it is capped at the exact remainder, and `runTurn` refuses to
43
+ * open a repair once the allowance is gone. That leaves the fallback's first
44
+ * attempt as the only one that can ever begin overdrawn.
45
+ */
46
+ export declare const stepAllowance: (maxTurns: number, spent: number) => number;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * One round's turn allowance, carried as a single mutable object rather than as a
3
+ * number threaded down and a count threaded back up.
4
+ *
5
+ * The distinction matters because a round is not one model call. Each slot may
6
+ * repair a rejected decomposition several times before the round moves on to the
7
+ * fallback, and every one of those attempts *restarts* the round from the same
8
+ * messages — so a failed attempt is work redone, with whatever tool side effects
9
+ * it already had. They all spend the same allowance. When that allowance was a
10
+ * plain number, keeping later attempts honest meant remembering to decrement it
11
+ * between each pair, and forgetting let one round cost a multiple of its budget.
12
+ * Sharing one object removes the thing there was to forget.
13
+ *
14
+ * `spent` is mutated where the turn is actually spent (the model loop's
15
+ * `onStepEnd`) and read where it actually matters (the RPC return that the
16
+ * Workflow meters against the Task). Nothing in between carries it.
17
+ */
18
+ /** A fresh budget for a round that may spend `allowance` turns. */
19
+ export const newTurnBudget = (allowance) => ({
20
+ allowance,
21
+ spent: 0
22
+ });
23
+ /**
24
+ * Steps one `generateText` attempt may spend, given what the run has already used.
25
+ *
26
+ * Must be evaluated **per attempt**: `isStepCount` counts steps within a single
27
+ * call, so an allowance computed once and shared with later attempts would hand
28
+ * each of them the turns its predecessors already burned.
29
+ *
30
+ * The `Math.max(1, …)` floor is load-bearing, not defensive. A primary that
31
+ * consumed the entire remainder still leaves the fallback one step, because an
32
+ * attempt with nothing to spend cannot reach an ending — which fails the round and
33
+ * costs the caller its answer, a far worse trade than one turn. So a round may
34
+ * exceed its allowance by exactly one, and never by more.
35
+ *
36
+ * The bound survives repair attempts because only an attempt that *starts* already
37
+ * at the allowance can overrun, and only by the one step of this floor: an attempt
38
+ * that starts under it is capped at the exact remainder, and `runTurn` refuses to
39
+ * open a repair once the allowance is gone. That leaves the fallback's first
40
+ * attempt as the only one that can ever begin overdrawn.
41
+ */
42
+ export const stepAllowance = (maxTurns, spent) => Math.max(1, maxTurns - spent);