@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,369 @@
1
+ import { AGENT_CARD_PATH, SendMessageRequest } from "@a2a-js/sdk";
2
+ import { DefaultRequestHandler, JsonRpcTransportHandler, validateVersion } from "@a2a-js/sdk/server";
3
+ import { RequestMalformedError, toJsonRpcError } from "@a2a-js/sdk/errors";
4
+ import { JWKS_PATH, endpointUrl } from "@dynamicagents/g2a-protocol";
5
+ import { A2A_RPC_PATH, buildBaseCard, parsePrivateJwk, publicCardJwks, signCard, signCardInPlace } from "../a2a/card.js";
6
+ import { buildCallContext, extensionHeaders } from "../a2a/context.js";
7
+ import { GatekeeperAuthError, bearerToken, verifyGatekeeperToken } from "../a2a/verify.js";
8
+ import { A2AExecutor } from "../a2a/executor.js";
9
+ import { DurableTaskStore } from "../a2a/task-store.js";
10
+ import { parseGatekeeperOrigins } from "../env.js";
11
+ /**
12
+ * The A2A Worker: the zero-trust, no-shared-secrets edge every Dynamic Agents agent
13
+ * puts in front of its Durable Object.
14
+ *
15
+ * Trust flows entirely on domains and asymmetric (Ed25519) signatures over
16
+ * public JWKS: a gatekeeper verifies and pins this agent's identity from its signed
17
+ * card at registration ("G knows R"), and this Worker verifies the gatekeeper's
18
+ * identity JWT on every JSON-RPC call ("R knows G"). No secret is ever shared in
19
+ * either direction.
20
+ *
21
+ * **Several agents share one Worker, addressed by `tenant`.** They share one
22
+ * origin, one endpoint, one signing key and one card at the well-known path;
23
+ * what differs is the tenant id on every request. That is the A2A mechanism for
24
+ * exactly this — `AgentInterface.tenant` is "an opaque string used for routing
25
+ * requests to a specific agent or tenant when multiple agents are served behind
26
+ * a single A2A endpoint", and §8.3.2 requires a client to send the value the
27
+ * selected interface declared.
28
+ *
29
+ * A tenant is **required on every request**. There is no default and no
30
+ * implicit routing: an absent tenant is an error, because guessing one would
31
+ * mean picking an agent the caller never named.
32
+ *
33
+ * This replaces mounting a handler per path prefix. That could not work: the
34
+ * card is a **well-known URI**, which RFC 8615 defines per-authority, so only
35
+ * one card per origin is discoverable at the registered path — a gatekeeper
36
+ * resolving `…/.well-known/agent-card.json` against the origin found whichever
37
+ * agent owned the bare path and pinned *its* key for all of them.
38
+ *
39
+ * Three routes:
40
+ *
41
+ * 1. `GET jwksPath` — the card-signing public JWKS, resolving every card's
42
+ * `jku`.
43
+ * 2. `GET …/.well-known/agent-card.json` — the signed **stub** card for the
44
+ * origin. Matched by suffix, since that path is fixed by the spec.
45
+ * 3. `POST rpcPath` — gatekeeper-authenticated JSON-RPC, routed to the named
46
+ * tenant. `GetExtendedAgentCard` returns that tenant's own signed card;
47
+ * everything else runs a turn against its Durable Object.
48
+ *
49
+ * Two independent checks keep one tenant's traffic out of another's:
50
+ * `audience` proves the token was minted for this deployment, and
51
+ * {@link file://../a2a/verify.ts TENANT_CLAIM} proves it was minted for *this
52
+ * agent on it*. The second is what makes `tenant` more than an unauthenticated
53
+ * routing hint in the request body.
54
+ */
55
+ export { defineAgent } from "./define-agent.js";
56
+ /**
57
+ * Default path serving the card-signing public JWKS (the card's `jku`).
58
+ *
59
+ * From `@dynamicagents/g2a-protocol`, because the gatekeeper serves its own JWKS at
60
+ * the same path and fetches ours from whatever `jku` says — see
61
+ * {@link file://../a2a/verify.ts} for why the two sides share a package rather
62
+ * than a comment. Re-exported so this stays importable from
63
+ * `@dynamicagents/core/worker`, where it has always lived.
64
+ */
65
+ export { JWKS_PATH } from "@dynamicagents/g2a-protocol";
66
+ /** The JSON-RPC method carrying a turn (v1.0 renamed v0.3's `message/send`). */
67
+ const SEND_MESSAGE_METHOD = "SendMessage";
68
+ function unauthorized(reason) {
69
+ return new Response(`unauthorized: ${reason}`, {
70
+ status: 401,
71
+ headers: { "www-authenticate": 'Bearer error="invalid_token"' }
72
+ });
73
+ }
74
+ /**
75
+ * A JSON-RPC error envelope for a failure raised before the SDK handler ran,
76
+ * echoing the request's `id` so a conformant client can correlate it. Errors are
77
+ * transported at HTTP 200 per JSON-RPC 2.0, matching what the SDK handler
78
+ * returns for the failures it maps itself.
79
+ *
80
+ * Takes an already-mapped `error` because the mapper has to match the error's
81
+ * origin: `@a2a-js/sdk/errors` and `@a2a-js/sdk/server` are separately bundled
82
+ * entry points that each carry their own copy of the error hierarchy, so a
83
+ * mapper only recognizes errors its own bundle constructed — the other one fails
84
+ * its `instanceof` check and flattens every semantic error to a generic internal
85
+ * error. Each call site below picks accordingly.
86
+ */
87
+ function jsonRpcErrorResponse(body, error) {
88
+ const id = body?.id ?? null;
89
+ return Response.json({ jsonrpc: "2.0", id, error });
90
+ }
91
+ /**
92
+ * Why a `SendMessage` fails the async-only contract, or `undefined` when it
93
+ * holds. (Other methods carry no config and are always fine.)
94
+ *
95
+ * Checked here, before the SDK handler, rather than in the executor: an executor
96
+ * throw is turned into a `failed` task, which a client reads as an accepted turn
97
+ * that never calls back. A rejected send has to surface as a JSON-RPC error.
98
+ */
99
+ function pushConfigError(rpcBody) {
100
+ if (rpcBody.method !== SEND_MESSAGE_METHOD)
101
+ return undefined;
102
+ // Decode through the generated codec rather than reading the raw body: the
103
+ // wire form is protobuf-JSON, so this is what normalizes field naming and
104
+ // oneof shapes into the typed request the SDK handler will also see.
105
+ let params;
106
+ try {
107
+ params = SendMessageRequest.fromJSON(rpcBody.params);
108
+ }
109
+ catch (err) {
110
+ return `params are not a valid SendMessageRequest: ${err.message}`;
111
+ }
112
+ const pushConfig = params.configuration?.taskPushNotificationConfig;
113
+ if (!pushConfig?.url) {
114
+ return ("configuration.taskPushNotificationConfig.url is required: this agent " +
115
+ "replies asynchronously via push notification (A2A §13.2)");
116
+ }
117
+ if (!pushConfig.token) {
118
+ return ("configuration.taskPushNotificationConfig.token is required: the gatekeeper " +
119
+ "uses it to correlate the callback to the pending task (A2A §13.2)");
120
+ }
121
+ try {
122
+ new URL(pushConfig.url);
123
+ }
124
+ catch {
125
+ return `configuration.taskPushNotificationConfig.url is not a valid URL: ${pushConfig.url}`;
126
+ }
127
+ return undefined;
128
+ }
129
+ export function createA2AWorker(options) {
130
+ // Normalized once, here, because each of these is used for two things that
131
+ // must agree: routing compares it against `url.pathname`, and `endpointUrl`
132
+ // composes it into the card's interface URL and the expected audience. A
133
+ // configured path written without its leading slash used to satisfy neither —
134
+ // `url.pathname` always has one, so the route simply never matched, and the
135
+ // audience came out as `https://hostpath`. Normalizing at the split keeps the
136
+ // two derivations from disagreeing whichever way it is written.
137
+ const withSlash = (path) => path.startsWith("/") ? path : `/${path}`;
138
+ const jwksPath = withSlash(options.jwksPath ?? JWKS_PATH);
139
+ const rpcPath = withSlash(options.rpcPath ?? A2A_RPC_PATH);
140
+ const requirePushConfig = options.requirePushConfig ?? true;
141
+ const declared = options.tenants ?? {};
142
+ const defined = options.agents ?? [];
143
+ // A tenant declared both ways is ambiguous, and silently letting one win is how
144
+ // a deployment serves an agent nobody meant to mount. Caught at startup.
145
+ const duplicate = defined.find((a) => Object.hasOwn(declared, a.tenant));
146
+ if (duplicate) {
147
+ throw new Error(`createA2AWorker got tenant '${duplicate.tenant}' from both \`agents\` and ` +
148
+ "`tenants` — declare each agent once");
149
+ }
150
+ // …and the same ambiguity within `agents` alone, which the check above cannot
151
+ // see. `definedByTenant` below is a `Map`, so a repeated id silently keeps the
152
+ // *last* entry: two `defineAgent` calls sharing a tenant would mount one and
153
+ // discard the other with no signal anywhere. That is the identical failure the
154
+ // cross-check exists to prevent, reached by copy-pasting a declaration rather
155
+ // than by mixing the two forms — which is the likelier mistake of the two.
156
+ const seen = new Set();
157
+ for (const agent of defined) {
158
+ if (seen.has(agent.tenant)) {
159
+ throw new Error(`createA2AWorker got tenant '${agent.tenant}' twice in \`agents\` — ` +
160
+ "declare each agent once");
161
+ }
162
+ seen.add(agent.tenant);
163
+ }
164
+ // Nothing can be served without at least one agent, and a deployment that
165
+ // configured none is a startup mistake rather than a per-request one.
166
+ const tenantIds = [...Object.keys(declared), ...defined.map((a) => a.tenant)];
167
+ if (tenantIds.length === 0) {
168
+ throw new Error("createA2AWorker requires at least one agent — pass `agents` or `tenants`");
169
+ }
170
+ // …and one registered under `""` is the same mistake wearing a disguise. A
171
+ // request must name a tenant, and the empty string is how "named none" is
172
+ // spelled, so that agent is registered and permanently unreachable — every
173
+ // call to it is refused as a missing tenant before the lookup ever runs. The
174
+ // check above would otherwise report a deployment with no routable agent as
175
+ // satisfying "at least one tenant".
176
+ if (tenantIds.some((id) => !id)) {
177
+ throw new Error("createA2AWorker requires every tenant id to be non-empty: a request " +
178
+ "names its tenant, so an agent registered under '' can never be reached");
179
+ }
180
+ const definedByTenant = new Map(defined.map((a) => [a.tenant, a]));
181
+ // The tenant id is caller-controlled, so the lookup is by *own* property.
182
+ // Plain indexing reaches `Object.prototype`, and every inherited name —
183
+ // `toString`, `constructor`, `__proto__` — comes back truthy, so a tenant
184
+ // called one of those slips past an `if (!agent)` guard and only fails later
185
+ // when a function is read off it. That is a 500 where the whole point of the
186
+ // guard is a 400 naming the unknown tenant. (A `Map` has no such inheritance,
187
+ // so the `agents` half needs no equivalent guard.)
188
+ //
189
+ // Takes `env` because a `defineAgent` entry resolves its bindings from it —
190
+ // there is no `env` at module scope on Workers, so the closure is built per
191
+ // request rather than at construction.
192
+ const tenantAgent = (env, id) => {
193
+ if (Object.hasOwn(declared, id))
194
+ return declared[id];
195
+ const agent = definedByTenant.get(id);
196
+ if (!agent)
197
+ return undefined;
198
+ return {
199
+ manifest: agent.manifest,
200
+ resolveAgent: (identity) => agent.resolveAgent(env, identity),
201
+ startTurn: (turn) => agent.startTurn(env, turn)
202
+ };
203
+ };
204
+ // Only reachable through the first overload, which has already established
205
+ // that `TEnv` carries the two documented names.
206
+ const readSecrets = options.secrets ??
207
+ ((env) => ({
208
+ signingKey: env.A2A_SIGNING_KEY,
209
+ gatekeeperOrigins: env.GATEKEEPER_ORIGINS
210
+ }));
211
+ return async function fetch(request, env) {
212
+ const url = new URL(request.url);
213
+ const origin = url.origin;
214
+ const secrets = readSecrets(env);
215
+ // This agent's own endpoint, which is also exactly what its card advertises
216
+ // as its interface — so the value the gatekeeper was registered with and the
217
+ // value checked here are the same string by construction.
218
+ const audience = typeof options.audience === "function"
219
+ ? options.audience(url)
220
+ : (options.audience ?? endpointUrl(origin, rpcPath));
221
+ const privateJwk = parsePrivateJwk(secrets.signingKey);
222
+ const signing = { privateJwk, jku: endpointUrl(origin, jwksPath) };
223
+ const cardFor = (manifest, tenant) => buildBaseCard(manifest, {
224
+ origin,
225
+ rpcPath: options.rpcPath,
226
+ advertiseSecuritySchemes: options.advertiseSecuritySchemes,
227
+ tenant
228
+ });
229
+ // (1) Card-signing public JWKS — resolves every card's `jku` for the
230
+ // gatekeeper. One key per origin, so one JWKS for every tenant.
231
+ if (request.method === "GET" && url.pathname === jwksPath) {
232
+ return Response.json(publicCardJwks(privateJwk), {
233
+ headers: { "cache-control": "public, max-age=3600" }
234
+ });
235
+ }
236
+ // (2) The stub card, at the well-known path. `signCard` returns the
237
+ // protobuf-JSON encoding — the exact document the signature is computed
238
+ // over — so it is serialized here without re-encoding.
239
+ if (request.method === "GET" && url.pathname.endsWith(AGENT_CARD_PATH)) {
240
+ return Response.json(await signCard(cardFor(options.manifest), signing));
241
+ }
242
+ // (3) A2A JSON-RPC — gatekeeper-authenticated, dispatched into the caller's DO.
243
+ //
244
+ // Matched on `rpcPath`, not on the method alone. Accepting every POST made
245
+ // the path this agent advertises purely decorative: a call to any URL on the
246
+ // origin was served as JSON-RPC, so a mounted agent's isolation rested
247
+ // entirely on an outer router matching first, and a typo'd endpoint quietly
248
+ // worked instead of 404ing.
249
+ if (request.method === "POST" && url.pathname === rpcPath) {
250
+ const token = bearerToken(request);
251
+ if (!token)
252
+ return unauthorized("missing gatekeeper bearer token");
253
+ let identity;
254
+ let authorizedTenant;
255
+ try {
256
+ ({ identity, tenant: authorizedTenant } = await verifyGatekeeperToken(token, {
257
+ allowedOrigins: parseGatekeeperOrigins(secrets.gatekeeperOrigins),
258
+ audience,
259
+ identityClaim: options.identityClaim,
260
+ tenantClaim: options.tenantClaim
261
+ }));
262
+ }
263
+ catch (err) {
264
+ const message = err instanceof GatekeeperAuthError
265
+ ? err.message
266
+ : "verification failed";
267
+ return unauthorized(message);
268
+ }
269
+ // The DO instance is keyed by the verified `identity.key`; without it the
270
+ // executor cannot route the call — refuse rather than fall back to a
271
+ // shared instance. Guaranteed non-null past this point.
272
+ if (!identity.key) {
273
+ return new Response("bad request: gatekeeper identity missing key", {
274
+ status: 400
275
+ });
276
+ }
277
+ const body = await request.json();
278
+ // `typeof null === "object"`, so the null check is load-bearing: a body of
279
+ // literal `null` is valid JSON and would otherwise make `rpcBody` null,
280
+ // throwing on the property reads below instead of reaching the SDK
281
+ // handler, which maps it to the same -32602 that `[]` or a string gets.
282
+ const rpcBody = (typeof body === "object" && body !== null ? body : {});
283
+ // Which agent this call is for. Read straight off the body rather than
284
+ // through a codec: every request message carries `tenant` in the same
285
+ // place, so decoding as one specific request type would mean knowing the
286
+ // method first, and the tenant is needed to pick the handler that would
287
+ // decide that.
288
+ const requestedTenant = rpcBody.params?.tenant ?? "";
289
+ if (typeof requestedTenant !== "string" || !requestedTenant) {
290
+ return jsonRpcErrorResponse(body, toJsonRpcError(new RequestMalformedError("params.tenant is required: this endpoint serves several agents " +
291
+ "and none is the default. Use the tenant declared on the " +
292
+ "interface of the agent's card (A2A §8.3.2), or call " +
293
+ "GetExtendedAgentCard with it to fetch that card.")));
294
+ }
295
+ // The token names the tenant the gatekeeper authorized; the body names the
296
+ // one the call addressed. They must agree.
297
+ //
298
+ // This is the check that makes `tenant` more than a routing hint. Every
299
+ // tenant here shares one endpoint and so one `aud`, which means the
300
+ // audience cannot tell them apart — without this, a token legitimately
301
+ // minted for one agent could be replayed against any of its siblings by
302
+ // editing a field in the request body. An absent claim is a rejection
303
+ // rather than a wildcard for the same reason.
304
+ if (authorizedTenant !== requestedTenant) {
305
+ return unauthorized(`gatekeeper token authorizes tenant '${authorizedTenant || "<none>"}', ` +
306
+ `but the request addressed '${requestedTenant}'`);
307
+ }
308
+ const agent = tenantAgent(env, requestedTenant);
309
+ if (!agent) {
310
+ return jsonRpcErrorResponse(body, toJsonRpcError(new RequestMalformedError(`unknown tenant '${requestedTenant}'`)));
311
+ }
312
+ const card = cardFor(agent.manifest, requestedTenant);
313
+ const context = buildCallContext(request, identity, requestedTenant);
314
+ // v1.0 negotiates the protocol version per request via the `A2A-Version`
315
+ // header, and the SDK leaves enforcement to the transport binding (its
316
+ // Express handlers do it; this Worker is the equivalent seam). An absent
317
+ // header is treated as `0.3` by the SDK, which a v1.0-only card does not
318
+ // advertise — so a legacy caller is rejected here rather than silently
319
+ // mis-served.
320
+ try {
321
+ validateVersion(context.requestedVersion, card, "JSONRPC");
322
+ }
323
+ catch (err) {
324
+ // Thrown by the server bundle — map it with the server bundle's mapper.
325
+ return jsonRpcErrorResponse(body, JsonRpcTransportHandler.mapToJSONRPCError(err));
326
+ }
327
+ if (requirePushConfig) {
328
+ const contractError = pushConfigError(rpcBody);
329
+ if (contractError) {
330
+ // Constructed from the errors bundle — map it with that bundle's.
331
+ return jsonRpcErrorResponse(body, toJsonRpcError(new RequestMalformedError(contractError)));
332
+ }
333
+ }
334
+ const handler = new DefaultRequestHandler(card, new DurableTaskStore(identity, agent.resolveAgent), new A2AExecutor({
335
+ identity,
336
+ jku: `${origin}${jwksPath}`,
337
+ resolveAgent: agent.resolveAgent,
338
+ startTurn: agent.startTurn
339
+ }), undefined, undefined, undefined,
340
+ // `GetExtendedAgentCard` — how a caller gets a *tenant's* card, since
341
+ // only the stub is discoverable at the well-known path.
342
+ //
343
+ // Returns the in-memory card and lets the SDK encode it: its JSON-RPC
344
+ // transport runs `AgentCard.toJSON()` over whatever comes back, so
345
+ // handing it `signCard`'s already-encoded wire card would encode twice
346
+ // and silently break the signature. See {@link signCardInPlace}.
347
+ //
348
+ // The tenant is read off the context rather than the request params
349
+ // because that is all the SDK passes; it is the value this Worker
350
+ // already authorized and routed on.
351
+ async (ctx) => {
352
+ const requested = ctx.tenant ?? "";
353
+ const target = tenantAgent(env, requested);
354
+ if (!target) {
355
+ throw new RequestMalformedError(`unknown tenant '${requested}'`);
356
+ }
357
+ return signCardInPlace(cardFor(target.manifest, requested), signing);
358
+ });
359
+ const rpc = new JsonRpcTransportHandler(handler);
360
+ const result = await rpc.handle(body, context);
361
+ // Streaming is not advertised; reject async generators outright.
362
+ if (Symbol.asyncIterator in result) {
363
+ return new Response("streaming not supported", { status: 501 });
364
+ }
365
+ return Response.json(result, { headers: extensionHeaders(context) });
366
+ }
367
+ return new Response("not found", { status: 404 });
368
+ };
369
+ }
@@ -0,0 +1,31 @@
1
+ import { noDeprecatedObjectProperties } from "./no-deprecated-object-properties.js";
2
+
3
+ /**
4
+ * `@dynamicagents/core/eslint` — the lint rules this stack needs that no upstream
5
+ * plugin provides.
6
+ *
7
+ * Consume as a flat-config plugin:
8
+ *
9
+ * ```js
10
+ * import da from "@dynamicagents/core/eslint";
11
+ *
12
+ * export default [
13
+ * {
14
+ * files: ["src/**\/*.ts"],
15
+ * plugins: { da },
16
+ * languageOptions: {
17
+ * parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname }
18
+ * },
19
+ * rules: { "da/no-deprecated-object-properties": "error" }
20
+ * }
21
+ * ];
22
+ * ```
23
+ *
24
+ * `no-deprecated-object-properties` is type-aware, so the consuming config must
25
+ * enable `projectService` (or `project`) for it to see anything.
26
+ */
27
+ export const rules = {
28
+ "no-deprecated-object-properties": noDeprecatedObjectProperties
29
+ };
30
+
31
+ export default { rules };
@@ -0,0 +1,81 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+
3
+ /**
4
+ * Flags `@deprecated` properties used as keys in object literals.
5
+ *
6
+ * `@typescript-eslint/no-deprecated` cannot see these. For an object literal it
7
+ * calls `getTypeAtLocation(objectExpression)`, which yields the literal's own
8
+ * *inferred* type — an anonymous type whose properties carry no JSDoc — rather
9
+ * than the contextual type that actually declares the deprecation. (Its JSX
10
+ * branch uses `getContextualType` and does work.) So member access like
11
+ * `opts.system` is reported, but `generateText({ system })` is silently allowed,
12
+ * even when the object is written to an explicitly annotated variable.
13
+ *
14
+ * That blind spot covers essentially every options-bag API — the AI SDK's
15
+ * `generateText`/`streamText` among them — so this rule closes it by resolving
16
+ * the contextual type itself and checking each key against it.
17
+ *
18
+ * Upstream issue: typescript-eslint#10883.
19
+ */
20
+ export const noDeprecatedObjectProperties = ESLintUtils.RuleCreator.withoutDocs(
21
+ {
22
+ meta: {
23
+ type: "problem",
24
+ docs: {
25
+ description:
26
+ "Disallow `@deprecated` properties as keys in object literals"
27
+ },
28
+ messages: {
29
+ deprecated: "`{{name}}` is deprecated.{{reason}}"
30
+ },
31
+ schema: []
32
+ },
33
+ defaultOptions: [],
34
+ create(context) {
35
+ const services = ESLintUtils.getParserServices(context);
36
+ const checker = services.program.getTypeChecker();
37
+
38
+ /** Property name for a non-computed key, or undefined if not statically known. */
39
+ const staticName = (property) => {
40
+ if (property.computed) return undefined;
41
+ const { key } = property;
42
+ if (key.type === "Identifier") return key.name;
43
+ if (key.type === "Literal" && typeof key.value === "string")
44
+ return key.value;
45
+ return undefined;
46
+ };
47
+
48
+ return {
49
+ ObjectExpression(node) {
50
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node);
51
+ // Undefined whenever the literal is not contextually typed (e.g. an
52
+ // unannotated `const`), in which case there is no declared property to
53
+ // consult and nothing to report.
54
+ const contextualType = checker.getContextualType(tsNode);
55
+ if (contextualType === undefined) return;
56
+
57
+ for (const property of node.properties) {
58
+ if (property.type !== "Property") continue; // skip SpreadElement
59
+ const name = staticName(property);
60
+ if (name === undefined) continue;
61
+
62
+ const symbol = contextualType.getProperty(name);
63
+ if (symbol === undefined) continue;
64
+
65
+ const tag = symbol
66
+ .getJsDocTags(checker)
67
+ .find((t) => t.name === "deprecated");
68
+ if (tag === undefined) continue;
69
+
70
+ const text = (tag.text ?? []).map((part) => part.text).join("");
71
+ context.report({
72
+ node: property.key,
73
+ messageId: "deprecated",
74
+ data: { name, reason: text ? ` ${text}` : "" }
75
+ });
76
+ }
77
+ }
78
+ };
79
+ }
80
+ }
81
+ );
package/package.json ADDED
@@ -0,0 +1,178 @@
1
+ {
2
+ "name": "@dynamicagents/core",
3
+ "version": "0.9.0",
4
+ "description": "Shared, mandatory foundation for Dynamic Agents on Cloudflare Workers: zero-trust A2A, durable task lifecycle, delegation and subagent runtime, test harness.",
5
+ "keywords": [
6
+ "a2a",
7
+ "agent2agent",
8
+ "ai-agents",
9
+ "cloudflare-workers",
10
+ "durable-objects",
11
+ "agents-sdk",
12
+ "zero-trust",
13
+ "jwks",
14
+ "eddsa",
15
+ "workflows"
16
+ ],
17
+ "license": "Apache-2.0",
18
+ "author": "Dynamic Agents",
19
+ "homepage": "https://github.com/dynamicagents/core#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/dynamicagents/core.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/dynamicagents/core/issues"
26
+ },
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "bin": {
30
+ "da-keys": "scripts/generate-keys.mjs"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "eslint-rules",
35
+ "scripts/generate-keys.mjs",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "exports": {
40
+ ".": {
41
+ "types": "./dist/index.d.ts",
42
+ "import": "./dist/index.js"
43
+ },
44
+ "./a2a": {
45
+ "types": "./dist/a2a/index.d.ts",
46
+ "import": "./dist/a2a/index.js"
47
+ },
48
+ "./worker": {
49
+ "types": "./dist/worker/index.d.ts",
50
+ "import": "./dist/worker/index.js"
51
+ },
52
+ "./agent": {
53
+ "types": "./dist/agent/index.d.ts",
54
+ "import": "./dist/agent/index.js"
55
+ },
56
+ "./host": {
57
+ "types": "./dist/host/index.d.ts",
58
+ "import": "./dist/host/index.js"
59
+ },
60
+ "./alarm": {
61
+ "types": "./dist/alarm/index.d.ts",
62
+ "import": "./dist/alarm/index.js"
63
+ },
64
+ "./job": {
65
+ "types": "./dist/job/index.d.ts",
66
+ "import": "./dist/job/index.js"
67
+ },
68
+ "./round": {
69
+ "types": "./dist/round/index.d.ts",
70
+ "import": "./dist/round/index.js"
71
+ },
72
+ "./subtasks": {
73
+ "types": "./dist/subtasks/index.d.ts",
74
+ "import": "./dist/subtasks/index.js"
75
+ },
76
+ "./subagent": {
77
+ "types": "./dist/subagent/index.d.ts",
78
+ "import": "./dist/subagent/index.js"
79
+ },
80
+ "./db": {
81
+ "types": "./dist/db/index.d.ts",
82
+ "import": "./dist/db/index.js"
83
+ },
84
+ "./testing": {
85
+ "types": "./dist/testing/index.d.ts",
86
+ "import": "./dist/testing/index.js"
87
+ },
88
+ "./testing/fixtures": {
89
+ "types": "./dist/testing/fixtures.d.ts",
90
+ "import": "./dist/testing/fixtures.js"
91
+ },
92
+ "./testing/node": {
93
+ "types": "./dist/testing/node.d.ts",
94
+ "import": "./dist/testing/node.js"
95
+ },
96
+ "./testing/vcr-global-setup": {
97
+ "types": "./dist/testing/vcr-global-setup.d.ts",
98
+ "import": "./dist/testing/vcr-global-setup.js"
99
+ },
100
+ "./eslint": "./eslint-rules/index.js",
101
+ "./package.json": "./package.json"
102
+ },
103
+ "scripts": {
104
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
105
+ "db:generate": "drizzle-kit generate && node scripts/build-migrations.mjs",
106
+ "keys": "node scripts/generate-keys.mjs",
107
+ "types": "wrangler types --include-env=false",
108
+ "types:check": "wrangler types --include-env=false --check && node scripts/verify-runtime-types.mjs",
109
+ "format": "prettier --write .",
110
+ "lint": "eslint .",
111
+ "check": "npm run types:check && prettier --check . && eslint . && tsc && tsc -p test/tsconfig.json && npm run build",
112
+ "test": "vitest run",
113
+ "test:watch": "vitest",
114
+ "verify:exports": "node scripts/verify-exports.mjs",
115
+ "prepack": "npm run build && npm run verify:exports",
116
+ "prepublishOnly": "npm run check && npm test && npm run verify:exports",
117
+ "prepare": "husky"
118
+ },
119
+ "dependencies": {
120
+ "@a2a-js/sdk": "^1.0.1",
121
+ "@dynamicagents/g2a-protocol": "^0.3.0",
122
+ "drizzle-orm": "^0.45.2",
123
+ "jose": "^6.2.10",
124
+ "zod": "^4.4.3"
125
+ },
126
+ "peerDependencies": {
127
+ "@cloudflare/vitest-pool-workers": ">=0.18",
128
+ "@typescript-eslint/utils": ">=8",
129
+ "agents": "^0.21.0",
130
+ "ai": "^7.0.77",
131
+ "vitest": ">=4",
132
+ "workers-ai-provider": "^4.0.0"
133
+ },
134
+ "peerDependenciesMeta": {
135
+ "@cloudflare/vitest-pool-workers": {
136
+ "optional": true
137
+ },
138
+ "@typescript-eslint/utils": {
139
+ "optional": true
140
+ },
141
+ "vitest": {
142
+ "optional": true
143
+ }
144
+ },
145
+ "devDependencies": {
146
+ "@cloudflare/vitest-pool-workers": "^0.22.0",
147
+ "@types/node": "^26.2.0",
148
+ "agents": "^0.21.0",
149
+ "ai": "^7.0.77",
150
+ "drizzle-kit": "^0.31.10",
151
+ "eslint": "^10.9.0",
152
+ "husky": "^9.1.7",
153
+ "prettier": "^3.9.6",
154
+ "typescript": "^6.0.3",
155
+ "typescript-eslint": "^8.67.0",
156
+ "vitest": "^4.1.11",
157
+ "workers-ai-provider": "^4.0.0",
158
+ "wrangler": "^4.125.0"
159
+ },
160
+ "engines": {
161
+ "node": ">=24"
162
+ },
163
+ "publishConfig": {
164
+ "access": "public"
165
+ },
166
+ "overrides": {
167
+ "@esbuild-kit/core-utils": {
168
+ "esbuild": "^0.25.0"
169
+ }
170
+ },
171
+ "allowScripts": {
172
+ "esbuild": false,
173
+ "fsevents": false,
174
+ "sharp": false,
175
+ "workerd": false,
176
+ "core-js-pure": false
177
+ }
178
+ }