@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,61 @@
|
|
|
1
|
+
import { importJWK, SignJWT } from "jose";
|
|
2
|
+
import { A2A_JWS_ALG, IDENTITY_CLAIM, TENANT_CLAIM, jwksUrl } from "@dynamicagents/g2a-protocol";
|
|
3
|
+
import { parsePrivateJwk } from "./card.js";
|
|
4
|
+
/** Default lifetime: long enough for clock skew, short enough to be worthless from a log. */
|
|
5
|
+
const DEFAULT_TTL_SECONDS = 120;
|
|
6
|
+
/**
|
|
7
|
+
* `importJWK` does real work and this is on the per-request path.
|
|
8
|
+
*
|
|
9
|
+
* Keyed by the raw secret so a rotated key invalidates the entry rather than
|
|
10
|
+
* being ignored for the life of the isolate.
|
|
11
|
+
*/
|
|
12
|
+
let cached;
|
|
13
|
+
async function signingKeyFor(raw) {
|
|
14
|
+
if (cached?.raw === raw)
|
|
15
|
+
return cached;
|
|
16
|
+
const jwk = parsePrivateJwk(raw);
|
|
17
|
+
// Not cast to `CryptoKey`: `importJWK` returns a union, and asserting the
|
|
18
|
+
// branch would be a lie the day this key is anything but Ed25519.
|
|
19
|
+
const key = await importJWK(jwk, A2A_JWS_ALG);
|
|
20
|
+
cached = { raw, key, kid: jwk.kid };
|
|
21
|
+
return cached;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Sign a short-lived token identifying **this agent as a caller** to another
|
|
25
|
+
* service that trusts its card key.
|
|
26
|
+
*
|
|
27
|
+
* The production sibling of `makeGatekeeperToken`, which core previously shipped
|
|
28
|
+
* only from `/testing` — so an agent that had to call out mint-signed had to
|
|
29
|
+
* hand-write this shape, and every deployment that did so wrote its own subtly
|
|
30
|
+
* different version of the `iss`/`jku` agreement above.
|
|
31
|
+
*
|
|
32
|
+
* Distinct from {@link signCallbackJwt}, which carries **no** claims: that one
|
|
33
|
+
* proves "the agent you called is calling you back about this task", where this
|
|
34
|
+
* proves "this is who I am and which tenant I speak for".
|
|
35
|
+
*/
|
|
36
|
+
export async function signCallerToken(options) {
|
|
37
|
+
const { key, kid } = await signingKeyFor(options.signingKey);
|
|
38
|
+
// Normalized for the same reason `audience` is, and it has to happen before
|
|
39
|
+
// both uses: `jku` names only the origin, so an `issuer` carrying a trailing
|
|
40
|
+
// slash or a path would sign an `iss` that disagrees with it — and a verifier
|
|
41
|
+
// comparing `iss` byte-for-byte against a normalized origin allowlist rejects
|
|
42
|
+
// the token even though the two URLs share an origin.
|
|
43
|
+
const issuer = new URL(options.issuer).origin;
|
|
44
|
+
return new SignJWT({
|
|
45
|
+
[IDENTITY_CLAIM]: options.identity,
|
|
46
|
+
[TENANT_CLAIM]: options.tenant
|
|
47
|
+
})
|
|
48
|
+
.setProtectedHeader({
|
|
49
|
+
alg: A2A_JWS_ALG,
|
|
50
|
+
kid,
|
|
51
|
+
// Where the far side fetches the public half. A verifier must check this
|
|
52
|
+
// origin against its own allowlist *before* fetching, which is what stops
|
|
53
|
+
// a forged token nominating an attacker-controlled JWKS.
|
|
54
|
+
jku: jwksUrl(issuer)
|
|
55
|
+
})
|
|
56
|
+
.setIssuer(issuer)
|
|
57
|
+
.setAudience(new URL(options.audience).origin)
|
|
58
|
+
.setIssuedAt()
|
|
59
|
+
.setExpirationTime(`${options.ttlSeconds ?? DEFAULT_TTL_SECONDS}s`)
|
|
60
|
+
.sign(key);
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { GatekeeperIdentity } from "./verify.js";
|
|
2
|
+
/**
|
|
3
|
+
* Per-request system-prompt suffix describing the verified calling gatekeeper-agent
|
|
4
|
+
* instance, from the gatekeeper identity JWT.
|
|
5
|
+
*
|
|
6
|
+
* Core ships this and not a soul, and the line between them is the point: a soul
|
|
7
|
+
* is prompt copy expressing who an agent *is*, which nobody else can write for
|
|
8
|
+
* you. This is a **rendering of a protocol fact** — who the gatekeeper proved it was
|
|
9
|
+
* — and that fact has one correct rendering. Two agents disagreeing about how to
|
|
10
|
+
* describe their caller would be a bug, not a personality. It is the same
|
|
11
|
+
* category as `renderSubagentPrompt`'s budget section, which core also owns.
|
|
12
|
+
*
|
|
13
|
+
* **Advisory context only.** This is the calling *agent instance*, not the human
|
|
14
|
+
* on the other end of it, so an agent's soul must never let the model read it as
|
|
15
|
+
* "who you're talking to" — point the model at whatever speaker wrapper the
|
|
16
|
+
* gatekeeper applies instead.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately neutral about what a workspace is: `GatekeeperIdentity.workspaceId`
|
|
19
|
+
* is whatever the calling gatekeeper partitions by, and core does not know that it
|
|
20
|
+
* is a Slack team. Override `DynamicAgent.callerContext` for a deployment that
|
|
21
|
+
* wants to name it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function callerContext(identity: GatekeeperIdentity): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request system-prompt suffix describing the verified calling gatekeeper-agent
|
|
3
|
+
* instance, from the gatekeeper identity JWT.
|
|
4
|
+
*
|
|
5
|
+
* Core ships this and not a soul, and the line between them is the point: a soul
|
|
6
|
+
* is prompt copy expressing who an agent *is*, which nobody else can write for
|
|
7
|
+
* you. This is a **rendering of a protocol fact** — who the gatekeeper proved it was
|
|
8
|
+
* — and that fact has one correct rendering. Two agents disagreeing about how to
|
|
9
|
+
* describe their caller would be a bug, not a personality. It is the same
|
|
10
|
+
* category as `renderSubagentPrompt`'s budget section, which core also owns.
|
|
11
|
+
*
|
|
12
|
+
* **Advisory context only.** This is the calling *agent instance*, not the human
|
|
13
|
+
* on the other end of it, so an agent's soul must never let the model read it as
|
|
14
|
+
* "who you're talking to" — point the model at whatever speaker wrapper the
|
|
15
|
+
* gatekeeper applies instead.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately neutral about what a workspace is: `GatekeeperIdentity.workspaceId`
|
|
18
|
+
* is whatever the calling gatekeeper partitions by, and core does not know that it
|
|
19
|
+
* is a Slack team. Override `DynamicAgent.callerContext` for a deployment that
|
|
20
|
+
* wants to name it.
|
|
21
|
+
*/
|
|
22
|
+
export function callerContext(identity) {
|
|
23
|
+
const label = identity.name ?? identity.key;
|
|
24
|
+
if (!label) {
|
|
25
|
+
return "\n\nCalling agent instance: unknown (the gatekeeper did not include an agent identity).";
|
|
26
|
+
}
|
|
27
|
+
const withKind = identity.kind ? `${label} (${identity.kind})` : label;
|
|
28
|
+
const lines = ["", "", `Calling agent instance: ${withKind}.`];
|
|
29
|
+
if (identity.workspaceId != null) {
|
|
30
|
+
lines.push(`Calling workspace: ${identity.workspaceId}.`);
|
|
31
|
+
}
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { JWK } from "jose";
|
|
2
|
+
import { AgentCard } from "@a2a-js/sdk";
|
|
3
|
+
/**
|
|
4
|
+
* The card signature uses the one algorithm every Dynamic Agents A2A signature uses,
|
|
5
|
+
* and the default RPC path is the one the gatekeeper also knows. Both come from
|
|
6
|
+
* `@dynamicagents/g2a-protocol` — see {@link file://./verify.ts} for why that is a
|
|
7
|
+
* separate package rather than a constant declared on each side.
|
|
8
|
+
*/
|
|
9
|
+
export { A2A_RPC_PATH } from "@dynamicagents/g2a-protocol";
|
|
10
|
+
/**
|
|
11
|
+
* The transport-independent half of an {@link AgentCard} — everything that does
|
|
12
|
+
* not depend on the request origin. {@link buildBaseCard} adds
|
|
13
|
+
* `supportedInterfaces` and the security fields.
|
|
14
|
+
*
|
|
15
|
+
* Derived from the SDK `AgentCard` rather than hand-declared so a protocol field
|
|
16
|
+
* that gains a requirement fails the build here instead of silently going
|
|
17
|
+
* unadvertised.
|
|
18
|
+
*/
|
|
19
|
+
export type AgentManifest = Pick<AgentCard, "name" | "description" | "version" | "capabilities" | "defaultInputModes" | "defaultOutputModes" | "skills">;
|
|
20
|
+
/**
|
|
21
|
+
* An AgentCard in its **protobuf-JSON** encoding — the form served at the
|
|
22
|
+
* well-known path and the exact document a signature is computed over. Distinct
|
|
23
|
+
* from the in-memory `AgentCard`: the wire form omits proto defaults and renders
|
|
24
|
+
* oneofs (`securitySchemes`, `Part.content`) as a single key rather than a
|
|
25
|
+
* `$case` tag.
|
|
26
|
+
*/
|
|
27
|
+
export type WireAgentCard = Record<string, unknown>;
|
|
28
|
+
export interface CardSigningConfig {
|
|
29
|
+
/** Ed25519 private JWK (with `kid`) that signs the card. */
|
|
30
|
+
privateJwk: JWK & {
|
|
31
|
+
kid: string;
|
|
32
|
+
};
|
|
33
|
+
/** Public URL serving this agent's JWKS — embedded as the JWS `jku`. */
|
|
34
|
+
jku: string;
|
|
35
|
+
}
|
|
36
|
+
export interface BuildCardOptions {
|
|
37
|
+
/** Origin the card is served from; the interface `url` is built under it. */
|
|
38
|
+
origin: string;
|
|
39
|
+
/** Path this agent answers JSON-RPC on. Defaults to {@link A2A_RPC_PATH}. */
|
|
40
|
+
rpcPath?: string;
|
|
41
|
+
/**
|
|
42
|
+
* The tenant this card describes, advertised on its interface entry.
|
|
43
|
+
*
|
|
44
|
+
* Several agents share one endpoint here, so `tenant` is what says *which*
|
|
45
|
+
* one — A2A spec §8.3.2 requires a client to echo the declared value on every
|
|
46
|
+
* request, and this Worker refuses a request that does not.
|
|
47
|
+
*
|
|
48
|
+
* Omitted for the **root stub card**, which describes the origin rather than
|
|
49
|
+
* any agent: it advertises the endpoint and `extendedAgentCard`, and a caller
|
|
50
|
+
* reaches a real agent by asking for its tenant.
|
|
51
|
+
*/
|
|
52
|
+
tenant?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Advertise the gatekeeper's Bearer-JWT scheme in `securitySchemes`.
|
|
55
|
+
*
|
|
56
|
+
* **Defaults to `false` for historical reasons that no longer hold — see
|
|
57
|
+
* below before relying on the default.**
|
|
58
|
+
*
|
|
59
|
+
* `SecurityScheme` is the card's only protobuf *oneof*, and in earlier SDK
|
|
60
|
+
* releases `SecurityScheme.fromJSON` read only the wire spelling
|
|
61
|
+
* (`{ httpAuthSecurityScheme: … }`) and not the decoded `$case` form. A
|
|
62
|
+
* verifier that decodes the fetched card and then canonicalizes it through
|
|
63
|
+
* `toJSON(fromJSON(card))` decodes twice, and the second pass collapsed
|
|
64
|
+
* `{ gatekeeperJwt: { httpAuthSecurityScheme: … } }` to `{ gatekeeperJwt: {} }` — a
|
|
65
|
+
* different document from the one that was signed, so the signature failed.
|
|
66
|
+
* slack-gatekeeper's `canonicalCardPayload` double-decodes exactly this way.
|
|
67
|
+
*
|
|
68
|
+
* **As of the pinned `@a2a-js/sdk` (1.0.1) that is fixed.** `card.spec.ts`
|
|
69
|
+
* asserts it directly: an advertised-schemes card is a fixed point under
|
|
70
|
+
* repeated decoding, and its signature verifies both as served and after a
|
|
71
|
+
* double decode. So the safety argument for the `false` default is gone, and
|
|
72
|
+
* the remaining reason to keep it is deployment ordering — a gatekeeper pinned to
|
|
73
|
+
* an older SDK copy would still collapse the oneof. Flip the default once the
|
|
74
|
+
* gatekeepers in play are known to be on ≥1.0.1; the specs will hold the line if
|
|
75
|
+
* a later SDK regresses.
|
|
76
|
+
*
|
|
77
|
+
* `securityRequirements` is unaffected either way — it is a plain map, not a
|
|
78
|
+
* oneof — so the card always declares that auth is *required*, it just may not
|
|
79
|
+
* describe the scheme.
|
|
80
|
+
*/
|
|
81
|
+
advertiseSecuritySchemes?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the (unsigned) AgentCard.
|
|
85
|
+
*
|
|
86
|
+
* v1.0 replaced the card's flat `url` / `preferredTransport` / `protocolVersion`
|
|
87
|
+
* fields with an ordered `supportedInterfaces` list, where each entry pins its
|
|
88
|
+
* own protocol binding *and* protocol version. That per-interface
|
|
89
|
+
* `protocolVersion` is what a client's transport factory matches on and what the
|
|
90
|
+
* Worker validates the `A2A-Version` request header against, so it must be the
|
|
91
|
+
* real protocol version rather than the agent's own version string.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildBaseCard(manifest: AgentManifest, opts: BuildCardOptions): AgentCard;
|
|
94
|
+
/**
|
|
95
|
+
* Sign the card with a detached-payload EdDSA flattened JWS, returning the
|
|
96
|
+
* signed **wire** card to serve. v1.0 standardized this (A2A spec §8.4), so the
|
|
97
|
+
* canonicalization and JWS construction are the SDK's rather than a local
|
|
98
|
+
* scheme: a JCS (RFC 8785) canonicalization of the card with `signatures`
|
|
99
|
+
* removed, under a protected header carrying `alg`, `kid` and `typ` (all three
|
|
100
|
+
* are required — the SDK verifier rejects a signature missing any of them).
|
|
101
|
+
*
|
|
102
|
+
* The card is normalized to its wire form *before* signing because that is what
|
|
103
|
+
* the verifier canonicalizes: `verifyAgentCardSignature` re-encodes whatever
|
|
104
|
+
* document it fetched through `AgentCard.toJSON(AgentCard.fromJSON(card))`.
|
|
105
|
+
* Signing the in-memory proto object instead would canonicalize `$case` tags and
|
|
106
|
+
* proto defaults that never appear on the wire, and every signature would fail.
|
|
107
|
+
*
|
|
108
|
+
* A verifying gatekeeper strips the `signatures` array, recomputes the canonical
|
|
109
|
+
* payload, and verifies — pinning this key's `kid`+`jku` on first registration
|
|
110
|
+
* (Trust-On-First-Use).
|
|
111
|
+
*/
|
|
112
|
+
export declare function signCard(card: AgentCard, cfg: CardSigningConfig): Promise<WireAgentCard>;
|
|
113
|
+
/**
|
|
114
|
+
* The same signature, attached to the **in-memory** card instead of the wire
|
|
115
|
+
* one — for a caller that hands the card to something which will encode it
|
|
116
|
+
* itself.
|
|
117
|
+
*
|
|
118
|
+
* That caller is `GetExtendedAgentCard`. The SDK's JSON-RPC transport runs
|
|
119
|
+
* `AgentCard.toJSON()` over whatever the extended-card provider returns, so
|
|
120
|
+
* returning {@link signCard}'s wire card means `toJSON` runs over a document
|
|
121
|
+
* that has already been encoded. For most cards that is a no-op and looks fine
|
|
122
|
+
* — which is the trap. `securitySchemes` is a protobuf *oneof*: encoding it
|
|
123
|
+
* twice collapses `{ gatekeeperJwt: { httpAuthSecurityScheme: … } }` to
|
|
124
|
+
* `{ gatekeeperJwt: {} }`, the served document stops matching the one that was
|
|
125
|
+
* signed, and every signature verification fails.
|
|
126
|
+
*
|
|
127
|
+
* So the double encode is latent today only because
|
|
128
|
+
* {@link BuildCardOptions.advertiseSecuritySchemes} defaults to `false`; it
|
|
129
|
+
* would surface the day that flag is flipped, as a signature failure with
|
|
130
|
+
* nothing pointing back here. Returning the in-memory card lets the SDK do the
|
|
131
|
+
* single encode the signature was computed over, and is correct either way.
|
|
132
|
+
*
|
|
133
|
+
* `card.spec.ts` pins both halves.
|
|
134
|
+
*/
|
|
135
|
+
export declare function signCardInPlace(card: AgentCard, cfg: CardSigningConfig): Promise<AgentCard>;
|
|
136
|
+
/**
|
|
137
|
+
* The card's protobuf-JSON encoding. Typed back as `AgentCard` for the SDK
|
|
138
|
+
* signer, which is generic over "the document to canonicalize" rather than over
|
|
139
|
+
* the in-memory shape specifically.
|
|
140
|
+
*
|
|
141
|
+
* Encode-only — never `toJSON(fromJSON(card))`. `fromJSON` reads the *wire*
|
|
142
|
+
* spelling of a oneof, so running it over an in-memory card silently drops every
|
|
143
|
+
* `$case`-tagged field. The verifier's round trip is the mirror image and stable:
|
|
144
|
+
* it decodes the document it fetched and re-encodes it to exactly this.
|
|
145
|
+
*/
|
|
146
|
+
export declare function wireCard(card: AgentCard): AgentCard;
|
|
147
|
+
/**
|
|
148
|
+
* Parse and validate the `A2A_SIGNING_KEY` secret into the private JWK used to
|
|
149
|
+
* sign the card. Throws if the JWK is missing its `kid` (required for the JWS
|
|
150
|
+
* protected header and gatekeeper key-pinning).
|
|
151
|
+
*/
|
|
152
|
+
export declare function parsePrivateJwk(raw: string): CardSigningConfig["privateJwk"];
|
|
153
|
+
/** Public card-signing JWKS (served at the `jku`): the private JWK minus `d`. */
|
|
154
|
+
export declare function publicCardJwks(privateJwk: JWK & {
|
|
155
|
+
kid: string;
|
|
156
|
+
}): {
|
|
157
|
+
keys: JWK[];
|
|
158
|
+
};
|
package/dist/a2a/card.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { A2A_PROTOCOL_VERSION, AgentCard, generateAgentCardSignature } from "@a2a-js/sdk";
|
|
2
|
+
import { A2A_JWS_ALG, A2A_RPC_PATH, endpointUrl } from "@dynamicagents/g2a-protocol";
|
|
3
|
+
/**
|
|
4
|
+
* The card signature uses the one algorithm every Dynamic Agents A2A signature uses,
|
|
5
|
+
* and the default RPC path is the one the gatekeeper also knows. Both come from
|
|
6
|
+
* `@dynamicagents/g2a-protocol` — see {@link file://./verify.ts} for why that is a
|
|
7
|
+
* separate package rather than a constant declared on each side.
|
|
8
|
+
*/
|
|
9
|
+
export { A2A_RPC_PATH } from "@dynamicagents/g2a-protocol";
|
|
10
|
+
/**
|
|
11
|
+
* The `httpAuthSecurityScheme` describing the gatekeeper's Bearer JWT, and the
|
|
12
|
+
* requirement that references it. Split out so the requirement can be advertised
|
|
13
|
+
* even when {@link BuildCardOptions.advertiseSecuritySchemes} keeps the scheme
|
|
14
|
+
* itself off the wire.
|
|
15
|
+
*/
|
|
16
|
+
const GATEKEEPER_SCHEME_ID = "gatekeeperJwt";
|
|
17
|
+
/**
|
|
18
|
+
* Build the (unsigned) AgentCard.
|
|
19
|
+
*
|
|
20
|
+
* v1.0 replaced the card's flat `url` / `preferredTransport` / `protocolVersion`
|
|
21
|
+
* fields with an ordered `supportedInterfaces` list, where each entry pins its
|
|
22
|
+
* own protocol binding *and* protocol version. That per-interface
|
|
23
|
+
* `protocolVersion` is what a client's transport factory matches on and what the
|
|
24
|
+
* Worker validates the `A2A-Version` request header against, so it must be the
|
|
25
|
+
* real protocol version rather than the agent's own version string.
|
|
26
|
+
*/
|
|
27
|
+
export function buildBaseCard(manifest, opts) {
|
|
28
|
+
const rpcPath = opts.rpcPath ?? A2A_RPC_PATH;
|
|
29
|
+
return {
|
|
30
|
+
...manifest,
|
|
31
|
+
capabilities: {
|
|
32
|
+
// `extensions` is required on the proto type but absent from most
|
|
33
|
+
// hand-written manifests, so it is defaulted before the spread rather
|
|
34
|
+
// than after — a manifest that declares extensions keeps them.
|
|
35
|
+
extensions: [],
|
|
36
|
+
...manifest.capabilities,
|
|
37
|
+
// Forced on, not inherited from the manifest: an agent here is *only*
|
|
38
|
+
// reachable as a tenant, and its card *only* through
|
|
39
|
+
// `GetExtendedAgentCard`. The SDK gates that method on this flag, reading
|
|
40
|
+
// it off whichever card the request handler was built with (spec §3.3.4 —
|
|
41
|
+
// it MUST return `UnsupportedOperationError` when unset), so every card
|
|
42
|
+
// this Worker serves has to carry it, the stub and the tenants alike.
|
|
43
|
+
extendedAgentCard: true
|
|
44
|
+
},
|
|
45
|
+
supportedInterfaces: [
|
|
46
|
+
{
|
|
47
|
+
// The gatekeeper reads this URL off the card and mints `aud` from it with
|
|
48
|
+
// `audienceFor`; this Worker derives its expected audience the same
|
|
49
|
+
// way. `endpointUrl` is what makes those provably equal — the protocol
|
|
50
|
+
// package pins `audienceFor(endpointUrl(o, p)) === endpointUrl(o, p)`
|
|
51
|
+
// for every path, rather than leaving both sides to concatenate.
|
|
52
|
+
url: endpointUrl(opts.origin, rpcPath),
|
|
53
|
+
protocolBinding: "JSONRPC",
|
|
54
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
55
|
+
tenant: opts.tenant ?? ""
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
provider: undefined,
|
|
59
|
+
documentationUrl: undefined,
|
|
60
|
+
iconUrl: undefined,
|
|
61
|
+
securitySchemes: opts.advertiseSecuritySchemes
|
|
62
|
+
? {
|
|
63
|
+
[GATEKEEPER_SCHEME_ID]: {
|
|
64
|
+
scheme: {
|
|
65
|
+
$case: "httpAuthSecurityScheme",
|
|
66
|
+
value: { description: "", scheme: "bearer", bearerFormat: "JWT" }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
: {},
|
|
71
|
+
// v0.3's `security: [{ gatekeeperJwt: [] }]`. The empty `list` means the scheme
|
|
72
|
+
// is required but carries no scopes. Safe to advertise unconditionally: this
|
|
73
|
+
// is a plain map, so it survives the round trip that eats `securitySchemes`.
|
|
74
|
+
securityRequirements: [
|
|
75
|
+
{ schemes: { [GATEKEEPER_SCHEME_ID]: { list: [] } } }
|
|
76
|
+
],
|
|
77
|
+
signatures: []
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Sign the card with a detached-payload EdDSA flattened JWS, returning the
|
|
82
|
+
* signed **wire** card to serve. v1.0 standardized this (A2A spec §8.4), so the
|
|
83
|
+
* canonicalization and JWS construction are the SDK's rather than a local
|
|
84
|
+
* scheme: a JCS (RFC 8785) canonicalization of the card with `signatures`
|
|
85
|
+
* removed, under a protected header carrying `alg`, `kid` and `typ` (all three
|
|
86
|
+
* are required — the SDK verifier rejects a signature missing any of them).
|
|
87
|
+
*
|
|
88
|
+
* The card is normalized to its wire form *before* signing because that is what
|
|
89
|
+
* the verifier canonicalizes: `verifyAgentCardSignature` re-encodes whatever
|
|
90
|
+
* document it fetched through `AgentCard.toJSON(AgentCard.fromJSON(card))`.
|
|
91
|
+
* Signing the in-memory proto object instead would canonicalize `$case` tags and
|
|
92
|
+
* proto defaults that never appear on the wire, and every signature would fail.
|
|
93
|
+
*
|
|
94
|
+
* A verifying gatekeeper strips the `signatures` array, recomputes the canonical
|
|
95
|
+
* payload, and verifies — pinning this key's `kid`+`jku` on first registration
|
|
96
|
+
* (Trust-On-First-Use).
|
|
97
|
+
*/
|
|
98
|
+
export async function signCard(card, cfg) {
|
|
99
|
+
const sign = generateAgentCardSignature(cfg.privateJwk, {
|
|
100
|
+
alg: A2A_JWS_ALG,
|
|
101
|
+
kid: cfg.privateJwk.kid,
|
|
102
|
+
typ: "JOSE",
|
|
103
|
+
jku: cfg.jku
|
|
104
|
+
});
|
|
105
|
+
const signed = await sign(wireCard(card));
|
|
106
|
+
return signed;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The same signature, attached to the **in-memory** card instead of the wire
|
|
110
|
+
* one — for a caller that hands the card to something which will encode it
|
|
111
|
+
* itself.
|
|
112
|
+
*
|
|
113
|
+
* That caller is `GetExtendedAgentCard`. The SDK's JSON-RPC transport runs
|
|
114
|
+
* `AgentCard.toJSON()` over whatever the extended-card provider returns, so
|
|
115
|
+
* returning {@link signCard}'s wire card means `toJSON` runs over a document
|
|
116
|
+
* that has already been encoded. For most cards that is a no-op and looks fine
|
|
117
|
+
* — which is the trap. `securitySchemes` is a protobuf *oneof*: encoding it
|
|
118
|
+
* twice collapses `{ gatekeeperJwt: { httpAuthSecurityScheme: … } }` to
|
|
119
|
+
* `{ gatekeeperJwt: {} }`, the served document stops matching the one that was
|
|
120
|
+
* signed, and every signature verification fails.
|
|
121
|
+
*
|
|
122
|
+
* So the double encode is latent today only because
|
|
123
|
+
* {@link BuildCardOptions.advertiseSecuritySchemes} defaults to `false`; it
|
|
124
|
+
* would surface the day that flag is flipped, as a signature failure with
|
|
125
|
+
* nothing pointing back here. Returning the in-memory card lets the SDK do the
|
|
126
|
+
* single encode the signature was computed over, and is correct either way.
|
|
127
|
+
*
|
|
128
|
+
* `card.spec.ts` pins both halves.
|
|
129
|
+
*/
|
|
130
|
+
export async function signCardInPlace(card, cfg) {
|
|
131
|
+
const signed = await signCard(card, cfg);
|
|
132
|
+
return { ...card, signatures: signed.signatures };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The card's protobuf-JSON encoding. Typed back as `AgentCard` for the SDK
|
|
136
|
+
* signer, which is generic over "the document to canonicalize" rather than over
|
|
137
|
+
* the in-memory shape specifically.
|
|
138
|
+
*
|
|
139
|
+
* Encode-only — never `toJSON(fromJSON(card))`. `fromJSON` reads the *wire*
|
|
140
|
+
* spelling of a oneof, so running it over an in-memory card silently drops every
|
|
141
|
+
* `$case`-tagged field. The verifier's round trip is the mirror image and stable:
|
|
142
|
+
* it decodes the document it fetched and re-encodes it to exactly this.
|
|
143
|
+
*/
|
|
144
|
+
export function wireCard(card) {
|
|
145
|
+
return AgentCard.toJSON(card);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse and validate the `A2A_SIGNING_KEY` secret into the private JWK used to
|
|
149
|
+
* sign the card. Throws if the JWK is missing its `kid` (required for the JWS
|
|
150
|
+
* protected header and gatekeeper key-pinning).
|
|
151
|
+
*/
|
|
152
|
+
export function parsePrivateJwk(raw) {
|
|
153
|
+
const jwk = JSON.parse(raw);
|
|
154
|
+
if (!jwk.kid)
|
|
155
|
+
throw new Error("A2A_SIGNING_KEY must include a `kid`");
|
|
156
|
+
return jwk;
|
|
157
|
+
}
|
|
158
|
+
/** Public card-signing JWKS (served at the `jku`): the private JWK minus `d`. */
|
|
159
|
+
export function publicCardJwks(privateJwk) {
|
|
160
|
+
const { d: _d, ...pub } = privateJwk;
|
|
161
|
+
void _d;
|
|
162
|
+
return { keys: [{ ...pub, use: "sig", alg: A2A_JWS_ALG }] };
|
|
163
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ServerCallContext } from "@a2a-js/sdk/server";
|
|
2
|
+
import type { GatekeeperIdentity } from "./verify.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build the call context for one verified JSON-RPC request. The default builder
|
|
5
|
+
* stashes the raw headers in the context's state bag, so an executor can reach
|
|
6
|
+
* them the same way it would under the Express binding.
|
|
7
|
+
*
|
|
8
|
+
* `tenant` names which agent on this origin the call is for. The SDK's
|
|
9
|
+
* `JsonRpcTransportHandler` would also lift it off `params.tenant` on its own,
|
|
10
|
+
* but only when the context arrives without one — so passing it here is not
|
|
11
|
+
* redundant: this Worker has already parsed, authorized and routed on the
|
|
12
|
+
* tenant before a handler exists to receive the context, and setting it keeps
|
|
13
|
+
* the value the executor and task store see identical to the one that chose
|
|
14
|
+
* them. It is also what reaches the extended-card provider, which the SDK calls
|
|
15
|
+
* with the context alone.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildCallContext(request: Request, identity: GatekeeperIdentity, tenant: string): ServerCallContext;
|
|
18
|
+
/** Echo back the extensions the handler actually activated (spec §14.2.2). */
|
|
19
|
+
export declare function extensionHeaders(context: ServerCallContext): HeadersInit;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { A2A_VERSION_HEADER, Extensions, HTTP_EXTENSION_HEADER } from "@a2a-js/sdk";
|
|
2
|
+
import { defaultServerCallContextBuilder } from "@a2a-js/sdk/server";
|
|
3
|
+
/**
|
|
4
|
+
* The per-call {@link ServerCallContext} bridge for a Workers `fetch` handler.
|
|
5
|
+
*
|
|
6
|
+
* v1.0 made the call context mandatory on every request-handler and task-store
|
|
7
|
+
* method, and the SDK only ships Express and gRPC bindings that build one; this
|
|
8
|
+
* is the equivalent seam for `fetch`.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The verified calling gatekeeper-agent, as the SDK's {@link User}. `userName` is
|
|
12
|
+
* the canonical instance key (e.g. `custom:7:analytics`) — the same value the
|
|
13
|
+
* agent Durable Object is keyed by, so the SDK's owner-scoped bookkeeping lines
|
|
14
|
+
* up with the isolation this Worker already enforces by routing.
|
|
15
|
+
*/
|
|
16
|
+
class GatekeeperUser {
|
|
17
|
+
identity;
|
|
18
|
+
constructor(identity) {
|
|
19
|
+
this.identity = identity;
|
|
20
|
+
}
|
|
21
|
+
get isAuthenticated() {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
get userName() {
|
|
25
|
+
// The Worker rejects a keyless identity (400) before a context is built.
|
|
26
|
+
return this.identity.key ?? "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the call context for one verified JSON-RPC request. The default builder
|
|
31
|
+
* stashes the raw headers in the context's state bag, so an executor can reach
|
|
32
|
+
* them the same way it would under the Express binding.
|
|
33
|
+
*
|
|
34
|
+
* `tenant` names which agent on this origin the call is for. The SDK's
|
|
35
|
+
* `JsonRpcTransportHandler` would also lift it off `params.tenant` on its own,
|
|
36
|
+
* but only when the context arrives without one — so passing it here is not
|
|
37
|
+
* redundant: this Worker has already parsed, authorized and routed on the
|
|
38
|
+
* tenant before a handler exists to receive the context, and setting it keeps
|
|
39
|
+
* the value the executor and task store see identical to the one that chose
|
|
40
|
+
* them. It is also what reaches the extended-card provider, which the SDK calls
|
|
41
|
+
* with the context alone.
|
|
42
|
+
*/
|
|
43
|
+
export function buildCallContext(request, identity, tenant) {
|
|
44
|
+
const headers = {};
|
|
45
|
+
for (const [name, value] of request.headers)
|
|
46
|
+
headers[name] = value;
|
|
47
|
+
return defaultServerCallContextBuilder({
|
|
48
|
+
extensions: Extensions.parseServiceParameter(request.headers.get(HTTP_EXTENSION_HEADER) ?? undefined),
|
|
49
|
+
user: new GatekeeperUser(identity),
|
|
50
|
+
headers,
|
|
51
|
+
requestedVersion: request.headers.get(A2A_VERSION_HEADER) ?? undefined,
|
|
52
|
+
tenant
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Echo back the extensions the handler actually activated (spec §14.2.2). */
|
|
56
|
+
export function extensionHeaders(context) {
|
|
57
|
+
const activated = context.activatedExtensions;
|
|
58
|
+
if (!activated?.length)
|
|
59
|
+
return {};
|
|
60
|
+
return { [HTTP_EXTENSION_HEADER]: Extensions.toServiceParameter(activated) };
|
|
61
|
+
}
|