@loopingai/core 0.1.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 +674 -0
- package/README.md +279 -0
- package/dist/a2a/agent-stub.d.ts +61 -0
- package/dist/a2a/agent-stub.d.ts.map +1 -0
- package/dist/a2a/agent-stub.js +2 -0
- package/dist/a2a/agent-stub.js.map +1 -0
- package/dist/a2a/card.d.ts +119 -0
- package/dist/a2a/card.d.ts.map +1 -0
- package/dist/a2a/card.js +115 -0
- package/dist/a2a/card.js.map +1 -0
- package/dist/a2a/context.d.ts +14 -0
- package/dist/a2a/context.d.ts.map +1 -0
- package/dist/a2a/context.js +55 -0
- package/dist/a2a/context.js.map +1 -0
- package/dist/a2a/executor.d.ts +85 -0
- package/dist/a2a/executor.d.ts.map +1 -0
- package/dist/a2a/executor.js +107 -0
- package/dist/a2a/executor.js.map +1 -0
- package/dist/a2a/index.d.ts +18 -0
- package/dist/a2a/index.d.ts.map +1 -0
- package/dist/a2a/index.js +17 -0
- package/dist/a2a/index.js.map +1 -0
- package/dist/a2a/notify.d.ts +75 -0
- package/dist/a2a/notify.d.ts.map +1 -0
- package/dist/a2a/notify.js +161 -0
- package/dist/a2a/notify.js.map +1 -0
- package/dist/a2a/parts.d.ts +46 -0
- package/dist/a2a/parts.d.ts.map +1 -0
- package/dist/a2a/parts.js +95 -0
- package/dist/a2a/parts.js.map +1 -0
- package/dist/a2a/task-store.d.ts +45 -0
- package/dist/a2a/task-store.d.ts.map +1 -0
- package/dist/a2a/task-store.js +100 -0
- package/dist/a2a/task-store.js.map +1 -0
- package/dist/a2a/task.d.ts +86 -0
- package/dist/a2a/task.d.ts.map +1 -0
- package/dist/a2a/task.js +16 -0
- package/dist/a2a/task.js.map +1 -0
- package/dist/a2a/verify.d.ts +64 -0
- package/dist/a2a/verify.d.ts.map +1 -0
- package/dist/a2a/verify.js +122 -0
- package/dist/a2a/verify.js.map +1 -0
- package/dist/agent/budget.d.ts +47 -0
- package/dist/agent/budget.d.ts.map +1 -0
- package/dist/agent/budget.js +43 -0
- package/dist/agent/budget.js.map +1 -0
- package/dist/agent/control.d.ts +110 -0
- package/dist/agent/control.d.ts.map +1 -0
- package/dist/agent/control.js +115 -0
- package/dist/agent/control.js.map +1 -0
- package/dist/agent/final-reply.d.ts +49 -0
- package/dist/agent/final-reply.d.ts.map +1 -0
- package/dist/agent/final-reply.js +52 -0
- package/dist/agent/final-reply.js.map +1 -0
- package/dist/agent/history.d.ts +83 -0
- package/dist/agent/history.d.ts.map +1 -0
- package/dist/agent/history.js +117 -0
- package/dist/agent/history.js.map +1 -0
- package/dist/agent/index.d.ts +17 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +17 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/inference.d.ts +52 -0
- package/dist/agent/inference.d.ts.map +1 -0
- package/dist/agent/inference.js +79 -0
- package/dist/agent/inference.js.map +1 -0
- package/dist/agent/model.d.ts +75 -0
- package/dist/agent/model.d.ts.map +1 -0
- package/dist/agent/model.js +62 -0
- package/dist/agent/model.js.map +1 -0
- package/dist/agent/session.d.ts +92 -0
- package/dist/agent/session.d.ts.map +1 -0
- package/dist/agent/session.js +80 -0
- package/dist/agent/session.js.map +1 -0
- package/dist/config.d.ts +159 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +92 -0
- package/dist/config.js.map +1 -0
- package/dist/contract/index.d.ts +10 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +9 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/plugin.d.ts +163 -0
- package/dist/contract/plugin.d.ts.map +1 -0
- package/dist/contract/plugin.js +39 -0
- package/dist/contract/plugin.js.map +1 -0
- package/dist/contract/recipe.d.ts +170 -0
- package/dist/contract/recipe.d.ts.map +1 -0
- package/dist/contract/recipe.js +2 -0
- package/dist/contract/recipe.js.map +1 -0
- package/dist/contract/validation.d.ts +86 -0
- package/dist/contract/validation.d.ts.map +1 -0
- package/dist/contract/validation.js +76 -0
- package/dist/contract/validation.js.map +1 -0
- package/dist/db/db.d.ts +131 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +91 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/index.d.ts +9 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +9 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrations/index.d.ts +21 -0
- package/dist/db/migrations/index.d.ts.map +1 -0
- package/dist/db/migrations/index.js +54 -0
- package/dist/db/migrations/index.js.map +1 -0
- package/dist/db/models/subtasks.d.ts +102 -0
- package/dist/db/models/subtasks.d.ts.map +1 -0
- package/dist/db/models/subtasks.js +285 -0
- package/dist/db/models/subtasks.js.map +1 -0
- package/dist/db/models/tasks.d.ts +92 -0
- package/dist/db/models/tasks.d.ts.map +1 -0
- package/dist/db/models/tasks.js +212 -0
- package/dist/db/models/tasks.js.map +1 -0
- package/dist/db/schema.d.ts +488 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +91 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/env.d.ts +46 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +40 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +47 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +47 -0
- package/dist/platform.js.map +1 -0
- package/dist/runtime/index.d.ts +86 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +123 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/tool-families.d.ts +33 -0
- package/dist/runtime/tool-families.d.ts.map +1 -0
- package/dist/runtime/tool-families.js +62 -0
- package/dist/runtime/tool-families.js.map +1 -0
- package/dist/subagent/fingerprint.d.ts +38 -0
- package/dist/subagent/fingerprint.d.ts.map +1 -0
- package/dist/subagent/fingerprint.js +97 -0
- package/dist/subagent/fingerprint.js.map +1 -0
- package/dist/subagent/index.d.ts +147 -0
- package/dist/subagent/index.d.ts.map +1 -0
- package/dist/subagent/index.js +299 -0
- package/dist/subagent/index.js.map +1 -0
- package/dist/subagent/prompt.d.ts +34 -0
- package/dist/subagent/prompt.d.ts.map +1 -0
- package/dist/subagent/prompt.js +46 -0
- package/dist/subagent/prompt.js.map +1 -0
- package/dist/subagent/run.d.ts +151 -0
- package/dist/subagent/run.d.ts.map +1 -0
- package/dist/subagent/run.js +491 -0
- package/dist/subagent/run.js.map +1 -0
- package/dist/subagent/workspace.d.ts +65 -0
- package/dist/subagent/workspace.d.ts.map +1 -0
- package/dist/subagent/workspace.js +53 -0
- package/dist/subagent/workspace.js.map +1 -0
- package/dist/subtasks/catalog.d.ts +40 -0
- package/dist/subtasks/catalog.d.ts.map +1 -0
- package/dist/subtasks/catalog.js +36 -0
- package/dist/subtasks/catalog.js.map +1 -0
- package/dist/subtasks/decomposition.d.ts +90 -0
- package/dist/subtasks/decomposition.d.ts.map +1 -0
- package/dist/subtasks/decomposition.js +205 -0
- package/dist/subtasks/decomposition.js.map +1 -0
- package/dist/subtasks/delegate.d.ts +77 -0
- package/dist/subtasks/delegate.d.ts.map +1 -0
- package/dist/subtasks/delegate.js +102 -0
- package/dist/subtasks/delegate.js.map +1 -0
- package/dist/subtasks/index.d.ts +11 -0
- package/dist/subtasks/index.d.ts.map +1 -0
- package/dist/subtasks/index.js +11 -0
- package/dist/subtasks/index.js.map +1 -0
- package/dist/subtasks/scheduler.d.ts +49 -0
- package/dist/subtasks/scheduler.d.ts.map +1 -0
- package/dist/subtasks/scheduler.js +48 -0
- package/dist/subtasks/scheduler.js.map +1 -0
- package/dist/subtasks/subtask-types.d.ts +100 -0
- package/dist/subtasks/subtask-types.d.ts.map +1 -0
- package/dist/subtasks/subtask-types.js +111 -0
- package/dist/subtasks/subtask-types.js.map +1 -0
- package/dist/subtasks/types.d.ts +321 -0
- package/dist/subtasks/types.d.ts.map +1 -0
- package/dist/subtasks/types.js +16 -0
- package/dist/subtasks/types.js.map +1 -0
- package/dist/testing/auth.d.ts +12 -0
- package/dist/testing/auth.d.ts.map +1 -0
- package/dist/testing/auth.js +25 -0
- package/dist/testing/auth.js.map +1 -0
- package/dist/testing/do.d.ts +30 -0
- package/dist/testing/do.d.ts.map +1 -0
- package/dist/testing/do.js +26 -0
- package/dist/testing/do.js.map +1 -0
- package/dist/testing/fake-session.d.ts +27 -0
- package/dist/testing/fake-session.d.ts.map +1 -0
- package/dist/testing/fake-session.js +38 -0
- package/dist/testing/fake-session.js.map +1 -0
- package/dist/testing/fixtures.d.ts +27 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +67 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +31 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-model.d.ts +43 -0
- package/dist/testing/mock-model.d.ts.map +1 -0
- package/dist/testing/mock-model.js +62 -0
- package/dist/testing/mock-model.js.map +1 -0
- package/dist/testing/node.d.ts +24 -0
- package/dist/testing/node.d.ts.map +1 -0
- package/dist/testing/node.js +24 -0
- package/dist/testing/node.js.map +1 -0
- package/dist/testing/vcr-global-setup.d.ts +3 -0
- package/dist/testing/vcr-global-setup.d.ts.map +1 -0
- package/dist/testing/vcr-global-setup.js +9 -0
- package/dist/testing/vcr-global-setup.js.map +1 -0
- package/dist/testing/vcr-shared.d.ts +21 -0
- package/dist/testing/vcr-shared.d.ts.map +1 -0
- package/dist/testing/vcr-shared.js +21 -0
- package/dist/testing/vcr-shared.js.map +1 -0
- package/dist/testing/vcr-spec.d.ts +17 -0
- package/dist/testing/vcr-spec.d.ts.map +1 -0
- package/dist/testing/vcr-spec.js +72 -0
- package/dist/testing/vcr-spec.js.map +1 -0
- package/dist/testing/vcr.d.ts +81 -0
- package/dist/testing/vcr.d.ts.map +1 -0
- package/dist/testing/vcr.js +211 -0
- package/dist/testing/vcr.js.map +1 -0
- package/dist/worker/index.d.ts +62 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +194 -0
- package/dist/worker/index.js.map +1 -0
- package/eslint-rules/index.js +31 -0
- package/eslint-rules/no-deprecated-object-properties.js +81 -0
- package/package.json +147 -0
- package/scripts/generate-keys.mjs +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# @loopingai/core
|
|
2
|
+
|
|
3
|
+
**The mandatory foundation for a Looping agent on Cloudflare Workers.**
|
|
4
|
+
|
|
5
|
+
Zero-trust A2A (signed AgentCard, gateway-JWT verification, no shared secrets), the
|
|
6
|
+
durable task lifecycle, the delegation and subagent runtime, and the test harness.
|
|
7
|
+
|
|
8
|
+
You bring the loop and the prompts. Core brings everything you cannot choose not to have.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @loopingai/core
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> Part of a three-package split:
|
|
15
|
+
> **`@loopingai/core`** (this) ·
|
|
16
|
+
> [`looping-plugins`](https://github.com/Looping-AI/looping-plugins) (optional, composable capabilities) ·
|
|
17
|
+
> [`looping-starter`](https://github.com/Looping-AI/looping-starter) (a working agent that composes them).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Why this exists
|
|
22
|
+
|
|
23
|
+
An agent that talks to other agents has to answer one question before anything else:
|
|
24
|
+
_is the caller who they claim to be, and can they prove it without a shared secret?_
|
|
25
|
+
That answer — and the durable machinery for accepting a turn, decomposing it, and
|
|
26
|
+
delivering a result out of band — is identical for every agent. It is also the part
|
|
27
|
+
that is easy to get subtly and silently wrong.
|
|
28
|
+
|
|
29
|
+
So it ships once, here, with the security-critical paths pinned by tests. Anything
|
|
30
|
+
optional is a plugin. Anything opinionated belongs to your app.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
### 1. Generate a signing key
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx looping-keys
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Set the private JWK as `A2A_SIGNING_KEY` (`.dev.vars` locally, `wrangler secret put`
|
|
43
|
+
when deployed) and the origins you accept calls from as `GATEWAY_ORIGINS`:
|
|
44
|
+
|
|
45
|
+
```ini
|
|
46
|
+
# .dev.vars
|
|
47
|
+
A2A_SIGNING_KEY={"crv":"Ed25519","d":"…","x":"…","kty":"OKP","kid":"a2a-2026-08-01"}
|
|
48
|
+
GATEWAY_ORIGINS=["https://gateway.example.com"]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The public half is never configured anywhere — the Worker derives it from the private
|
|
52
|
+
key and serves it at the card's `jku`.
|
|
53
|
+
|
|
54
|
+
### 2. Put the A2A edge in front of your Durable Object
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { createA2AWorker } from "@loopingai/core/worker";
|
|
58
|
+
|
|
59
|
+
const manifest = {
|
|
60
|
+
name: "my-agent",
|
|
61
|
+
description: "Does a useful thing.",
|
|
62
|
+
version: "1.0.0",
|
|
63
|
+
capabilities: { streaming: false, pushNotifications: true, extensions: [] },
|
|
64
|
+
defaultInputModes: ["text/plain"],
|
|
65
|
+
defaultOutputModes: ["text/plain"],
|
|
66
|
+
skills: []
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default {
|
|
70
|
+
fetch: createA2AWorker({
|
|
71
|
+
manifest,
|
|
72
|
+
// One DO instance per verified caller — this is what makes a task
|
|
73
|
+
// unreachable from any other caller by construction.
|
|
74
|
+
resolveAgent: (identity) =>
|
|
75
|
+
env.MY_AGENT.get(env.MY_AGENT.idFromName(identity.key!)),
|
|
76
|
+
// Must be idempotent: the gateway retries dispatch.
|
|
77
|
+
startTurn: async (turn) => {
|
|
78
|
+
await env.TURN_WORKFLOW.create({ id: turn.messageId, params: turn });
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
} satisfies ExportedHandler<Env>;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
That handler serves three routes: the public JWKS, a **signed** AgentCard at
|
|
85
|
+
`/.well-known/agent-card.json`, and gateway-authenticated JSON-RPC. Every POST is
|
|
86
|
+
verified before a Durable Object is ever addressed.
|
|
87
|
+
|
|
88
|
+
### 3. Build the runtime in your Durable Object
|
|
89
|
+
|
|
90
|
+
Everything that would otherwise be a module-level constant is resolved once per DO
|
|
91
|
+
instance, from your config and your installed plugins:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { Agent } from "agents";
|
|
95
|
+
import { createAgentRuntime } from "@loopingai/core";
|
|
96
|
+
import { AgentDB } from "@loopingai/core/db";
|
|
97
|
+
|
|
98
|
+
export class MyAgent extends Agent<Env> {
|
|
99
|
+
runtime!: ReturnType<typeof createAgentRuntime>;
|
|
100
|
+
db!: AgentDB;
|
|
101
|
+
|
|
102
|
+
async onStart() {
|
|
103
|
+
this.runtime = createAgentRuntime({
|
|
104
|
+
config: { model: { chatModelId: "@cf/zai-org/glm-5.2" } },
|
|
105
|
+
plugins: [scraper({ apiKey: this.env.SCRAPER_API_KEY })],
|
|
106
|
+
env: this.env // opt in to verifying every plugin's declared bindings exist
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.db = new AgentDB(this.ctx.storage, {
|
|
110
|
+
maxSubtasks: this.runtime.config.maxSubtasks,
|
|
111
|
+
stores: this.runtime.stores
|
|
112
|
+
});
|
|
113
|
+
await this.db.ensureReady();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Resolving a registry at _import_ time is the one thing this package exists to
|
|
119
|
+
prevent: it freezes the registry before `env` exists (which on Workers is always),
|
|
120
|
+
defeats tree-shaking, and makes runtime plugin selection impossible.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Exports
|
|
125
|
+
|
|
126
|
+
No root barrel. Each area is its own subpath, so importing the delegation layer does
|
|
127
|
+
not drag in the A2A adapter, and the test harness cannot reach a production bundle.
|
|
128
|
+
|
|
129
|
+
| Subpath | What's in it |
|
|
130
|
+
| ------------------------------ | --------------------------------------------------------------------------- |
|
|
131
|
+
| `@loopingai/core` | `createAgentRuntime`, the plugin contract, config shapes, platform facts |
|
|
132
|
+
| `@loopingai/core/a2a` | card signing, JWKS, gateway-JWT verify, push notify, task store, executor |
|
|
133
|
+
| `@loopingai/core/worker` | `createA2AWorker()` — the whole zero-trust edge |
|
|
134
|
+
| `@loopingai/core/agent` | session, history, model runtime, inference, budget, control tools |
|
|
135
|
+
| `@loopingai/core/subtasks` | delegation types, decomposition, the `delegate` tool, wave scheduler |
|
|
136
|
+
| `@loopingai/core/subagent` | `RecipeSubagentBase`, resumable runs, fingerprinting, workspace |
|
|
137
|
+
| `@loopingai/core/db` | `AgentDB`, `notify_tasks` + `subtasks` schema, migrations, `PluginStore` |
|
|
138
|
+
| `@loopingai/core/testing` | VCR, `FakeSession`, `mockModel`, DO helpers, JWK fixtures — _workerd realm_ |
|
|
139
|
+
| `@loopingai/core/testing/node` | the VCR recorder — _Node realm, never import from a spec_ |
|
|
140
|
+
| `@loopingai/core/eslint` | the `no-deprecated-object-properties` rule |
|
|
141
|
+
|
|
142
|
+
`/testing*` and `/eslint` are structurally incapable of entering a runtime graph, and
|
|
143
|
+
`npm run verify:exports` asserts exactly that before every publish.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## The zero-trust model
|
|
148
|
+
|
|
149
|
+
No secret ever crosses the boundary, in either direction.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
Gateway ──── EdDSA JWT, jku → its public JWKS ────▶ Agent "the agent knows the gateway"
|
|
153
|
+
Agent ──── signed AgentCard, jku → its JWKS ────▶ Gateway "the gateway knows the agent"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`verifyGatewayToken` runs four checks, in this order, on every single call:
|
|
157
|
+
|
|
158
|
+
1. **`jku` present** in the protected header (RFC 7515 §4.1.2).
|
|
159
|
+
2. **`jku` origin is allowlisted** — validated _before_ the fetch, so an attacker
|
|
160
|
+
cannot point `jku` at a JWKS they control.
|
|
161
|
+
3. **`iss` origin equals `jku` origin** — one listed gateway cannot impersonate another.
|
|
162
|
+
4. **`jwtVerify` pinned to EdDSA.**
|
|
163
|
+
|
|
164
|
+
All four are load-bearing. Do not make any of them optional, and do not add a
|
|
165
|
+
local-development bypass — run a local gateway instead. `verify.spec.ts` asserts each
|
|
166
|
+
one negatively, including that an unlisted `jku` is rejected _before_ any network
|
|
167
|
+
call happens.
|
|
168
|
+
|
|
169
|
+
The agent's card is signed over its **wire (protobuf-JSON) encoding**, which is what
|
|
170
|
+
makes the served document a fixed point under the repeated decoding a verifier
|
|
171
|
+
performs. A gateway pins the card's `kid` + `jku` on first registration
|
|
172
|
+
(Trust-On-First-Use).
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Plugins
|
|
177
|
+
|
|
178
|
+
A capability is a plugin. Core never imports one — your app registers it, which keeps
|
|
179
|
+
bundle size proportional to what you actually installed.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { definePlugin } from "@loopingai/core";
|
|
183
|
+
|
|
184
|
+
export const scraper = (config: { apiKey: string }) =>
|
|
185
|
+
definePlugin({
|
|
186
|
+
key: "scraper",
|
|
187
|
+
subtaskType: {
|
|
188
|
+
key: "scrape",
|
|
189
|
+
description: "fetch a page and summarize it",
|
|
190
|
+
params: z.object({ url: z.string().describe("page to fetch") }),
|
|
191
|
+
recipe
|
|
192
|
+
},
|
|
193
|
+
toolFamilies: { web: (ctx) => ({ tools: { fetchPage: /* … */ } }) },
|
|
194
|
+
capability: "You can scrape a page and summarize it.",
|
|
195
|
+
requires: { secrets: ["SCRAPER_API_KEY"] },
|
|
196
|
+
store: { plugin: "scraper", version: 1, ensureTables: (sql, from) => { /* … */ } }
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`createAgentRuntime` fails at DO start — never mid-request — on a duplicate plugin
|
|
201
|
+
key, a duplicate tool family, a missing declared binding, or a
|
|
202
|
+
`PLUGIN_CONTRACT_VERSION` mismatch. Because core, plugins, and starter publish from
|
|
203
|
+
separate repos, one of them is always briefly behind; that version assert turns the
|
|
204
|
+
skew into a readable sentence instead of a structural-type error several frames from
|
|
205
|
+
its cause.
|
|
206
|
+
|
|
207
|
+
The contract is **additive-only within a major**: new capabilities arrive as optional
|
|
208
|
+
fields on `AgentPlugin`.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Testing
|
|
213
|
+
|
|
214
|
+
The harness both predecessor agents grew, shipped so you don't grow it a third time.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import {
|
|
218
|
+
FakeSession,
|
|
219
|
+
mockModel,
|
|
220
|
+
makeGatewayToken,
|
|
221
|
+
makeDoHelpers
|
|
222
|
+
} from "@loopingai/core/testing";
|
|
223
|
+
|
|
224
|
+
const { withDb } = makeDoHelpers(env.MY_AGENT);
|
|
225
|
+
|
|
226
|
+
await withDb("accepts a turn once", async (db) => {
|
|
227
|
+
await db.ensureReady();
|
|
228
|
+
db.tasks.begin({ messageId: "m1", taskId: "t1", contextId: "c1" });
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- **VCR** — record/replay real HTTP against on-disk cassettes, split across the Node
|
|
233
|
+
and workerd realms because specs run in workerd, which has no filesystem. Point
|
|
234
|
+
vitest's `globalSetup` at `@loopingai/core/testing/vcr-global-setup`.
|
|
235
|
+
- **Fakes** — `FakeSession` (a `SessionLike` reference implementation) and `mockModel`
|
|
236
|
+
(a scripted `LanguageModel`), so a loop can be driven with no model call at all.
|
|
237
|
+
- **Fixtures** — Ed25519 keypairs and a gateway-JWT signer, so the zero-trust path is
|
|
238
|
+
exercisable end to end without a real gateway.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## What core deliberately does _not_ contain
|
|
243
|
+
|
|
244
|
+
- The turn loop, triage, and the DO / Workflow class bodies. Core ships the argument
|
|
245
|
+
and budget types; you write the loop.
|
|
246
|
+
- The main agent's soul. Core ships no prompt copy.
|
|
247
|
+
- Config _values_ — model ids, budgets, limits. Core ships the shapes and safe
|
|
248
|
+
defaults, and `resolveConfig` validates your overrides.
|
|
249
|
+
- Vectorize recall, browser tools, shell. All optional → plugins.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Requirements
|
|
254
|
+
|
|
255
|
+
- **Node** ≥ 24 (for build and test only — the package itself runs on workerd)
|
|
256
|
+
- **Bindings:** `AI`, one Durable Object, one Workflow
|
|
257
|
+
- **Secrets:** `A2A_SIGNING_KEY`, `GATEWAY_ORIGINS`
|
|
258
|
+
- **Peers, never bundled:** `agents`, `ai`, `workers-ai-provider`
|
|
259
|
+
|
|
260
|
+
That last point is not stylistic: two copies of `agents` in one Worker breaks the
|
|
261
|
+
`Session` / `SessionMessage` types and every `instanceof`. For local development
|
|
262
|
+
across the three repos use `file:` overrides, or `npm pack` plus a tarball install —
|
|
263
|
+
**not `npm link`**, which duplicates peer dependencies.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Contributing
|
|
268
|
+
|
|
269
|
+
[`AGENTS.md`](./AGENTS.md) documents the constraints this package is guardian of.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npm run check # prettier + eslint + tsc (src) + tsc (test) + build
|
|
273
|
+
npm test # vitest, inside real workerd
|
|
274
|
+
npm run verify:exports # the publish gate: subpaths, ESM specifiers, realm isolation
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
[GPL-3.0-only](./LICENSE).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Task, TaskState } from "@a2a-js/sdk";
|
|
2
|
+
import type { GatewayIdentity } from "./verify.js";
|
|
3
|
+
import type { PlainTask } from "./task.js";
|
|
4
|
+
/**
|
|
5
|
+
* The task-lifecycle surface core calls on an agent Durable Object, declared
|
|
6
|
+
* structurally so core never imports a consumer's DO class.
|
|
7
|
+
*
|
|
8
|
+
* A consumer's `DurableObjectStub<MyAgent>` satisfies this by construction:
|
|
9
|
+
* Cloudflare's RPC type mapping wraps each return in `Promise<Serializable<T>>`,
|
|
10
|
+
* and every type below is already `Serializable` — that is precisely why
|
|
11
|
+
* {@link PlainTask} exists (see {@link file://./task.ts}).
|
|
12
|
+
*
|
|
13
|
+
* `listTasks` is optional. `ListTasks` is new in A2A v1.0 and an agent that does
|
|
14
|
+
* not keep a queryable task history simply omits it; {@link DurableTaskStore}
|
|
15
|
+
* then answers the RPC with `UnsupportedOperationError` instead of failing at
|
|
16
|
+
* the binding. Everything else is mandatory — they are the accept-and-notify
|
|
17
|
+
* contract.
|
|
18
|
+
*/
|
|
19
|
+
export interface TaskAgent {
|
|
20
|
+
/**
|
|
21
|
+
* Record (or reuse) the `submitted` task for a turn. **Must be idempotent on
|
|
22
|
+
* `messageId`**: the gateway retries dispatch, and the executor relies on this
|
|
23
|
+
* plus a deterministic workflow id to make a turn run exactly once.
|
|
24
|
+
*/
|
|
25
|
+
beginTask(input: {
|
|
26
|
+
messageId: string;
|
|
27
|
+
taskId: string;
|
|
28
|
+
contextId: string;
|
|
29
|
+
}): Promise<PlainTask>;
|
|
30
|
+
getTask(taskId: string): Promise<PlainTask | null>;
|
|
31
|
+
/** Returns false when the write was refused (e.g. the task is already terminal). */
|
|
32
|
+
saveTask(task: Task): Promise<boolean>;
|
|
33
|
+
/** Marks the task canceled and returns it, or null when there is no such task. */
|
|
34
|
+
cancelTask(taskId: string): Promise<PlainTask | null>;
|
|
35
|
+
listTasks?(query: TaskListQuery): Promise<TaskListPage>;
|
|
36
|
+
}
|
|
37
|
+
export interface TaskListQuery {
|
|
38
|
+
contextId?: string;
|
|
39
|
+
state?: TaskState;
|
|
40
|
+
/** Epoch ms; matches the `updated_at` column stamped on every status write. */
|
|
41
|
+
updatedAfter?: number;
|
|
42
|
+
includeArtifacts: boolean;
|
|
43
|
+
historyLength?: number;
|
|
44
|
+
limit: number;
|
|
45
|
+
offset: number;
|
|
46
|
+
}
|
|
47
|
+
export interface TaskListPage {
|
|
48
|
+
tasks: Task[];
|
|
49
|
+
/** Total matching rows, ignoring `limit`/`offset` — drives `nextPageToken`. */
|
|
50
|
+
totalSize: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the agent DO stub for a verified caller.
|
|
54
|
+
*
|
|
55
|
+
* Supplied by the consumer because addressing is theirs: core does not know the
|
|
56
|
+
* binding name, and the identity→instance mapping is a product decision. The
|
|
57
|
+
* conventional implementation keys one DO instance per `identity.key`, which is
|
|
58
|
+
* what makes a task unreachable from any other caller by construction.
|
|
59
|
+
*/
|
|
60
|
+
export type AgentResolver = (identity: GatewayIdentity) => TaskAgent;
|
|
61
|
+
//# sourceMappingURL=agent-stub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-stub.d.ts","sourceRoot":"","sources":["../../src/a2a/agent-stub.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAEvB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAEnD,oFAAoF;IACpF,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEvC,kFAAkF;IAClF,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAEtD,SAAS,CAAC,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,eAAe,KAAK,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-stub.js","sourceRoot":"","sources":["../../src/a2a/agent-stub.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { JWK } from "jose";
|
|
2
|
+
import { AgentCard } from "@a2a-js/sdk";
|
|
3
|
+
/** The default JSON-RPC path an agent answers on (the card's only interface). */
|
|
4
|
+
export declare const A2A_RPC_PATH = "/a2a";
|
|
5
|
+
/**
|
|
6
|
+
* The transport-independent half of an {@link AgentCard} — everything that does
|
|
7
|
+
* not depend on the request origin. {@link buildBaseCard} adds
|
|
8
|
+
* `supportedInterfaces` and the security fields.
|
|
9
|
+
*
|
|
10
|
+
* Derived from the SDK `AgentCard` rather than hand-declared so a protocol field
|
|
11
|
+
* that gains a requirement fails the build here instead of silently going
|
|
12
|
+
* unadvertised.
|
|
13
|
+
*/
|
|
14
|
+
export type AgentManifest = Pick<AgentCard, "name" | "description" | "version" | "capabilities" | "defaultInputModes" | "defaultOutputModes" | "skills">;
|
|
15
|
+
/**
|
|
16
|
+
* An AgentCard in its **protobuf-JSON** encoding — the form served at the
|
|
17
|
+
* well-known path and the exact document a signature is computed over. Distinct
|
|
18
|
+
* from the in-memory `AgentCard`: the wire form omits proto defaults and renders
|
|
19
|
+
* oneofs (`securitySchemes`, `Part.content`) as a single key rather than a
|
|
20
|
+
* `$case` tag.
|
|
21
|
+
*/
|
|
22
|
+
export type WireAgentCard = Record<string, unknown>;
|
|
23
|
+
export interface CardSigningConfig {
|
|
24
|
+
/** Ed25519 private JWK (with `kid`) that signs the card. */
|
|
25
|
+
privateJwk: JWK & {
|
|
26
|
+
kid: string;
|
|
27
|
+
};
|
|
28
|
+
/** Public URL serving this agent's JWKS — embedded as the JWS `jku`. */
|
|
29
|
+
jku: string;
|
|
30
|
+
}
|
|
31
|
+
export interface BuildCardOptions {
|
|
32
|
+
/** Origin the card is served from; the interface `url` is built under it. */
|
|
33
|
+
origin: string;
|
|
34
|
+
/** Path this agent answers JSON-RPC on. Defaults to {@link A2A_RPC_PATH}. */
|
|
35
|
+
rpcPath?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Advertise the gateway's Bearer-JWT scheme in `securitySchemes`.
|
|
38
|
+
*
|
|
39
|
+
* **Defaults to `false` for historical reasons that no longer hold — see
|
|
40
|
+
* below before relying on the default.**
|
|
41
|
+
*
|
|
42
|
+
* `SecurityScheme` is the card's only protobuf *oneof*, and in earlier SDK
|
|
43
|
+
* releases `SecurityScheme.fromJSON` read only the wire spelling
|
|
44
|
+
* (`{ httpAuthSecurityScheme: … }`) and not the decoded `$case` form. A
|
|
45
|
+
* verifier that decodes the fetched card and then canonicalizes it through
|
|
46
|
+
* `toJSON(fromJSON(card))` decodes twice, and the second pass collapsed
|
|
47
|
+
* `{ gatewayJwt: { httpAuthSecurityScheme: … } }` to `{ gatewayJwt: {} }` — a
|
|
48
|
+
* different document from the one that was signed, so the signature failed.
|
|
49
|
+
* looping-gateway's `canonicalCardPayload` double-decodes exactly this way.
|
|
50
|
+
*
|
|
51
|
+
* **As of the pinned `@a2a-js/sdk` (1.0.1) that is fixed.** `card.spec.ts`
|
|
52
|
+
* asserts it directly: an advertised-schemes card is a fixed point under
|
|
53
|
+
* repeated decoding, and its signature verifies both as served and after a
|
|
54
|
+
* double decode. So the safety argument for the `false` default is gone, and
|
|
55
|
+
* the remaining reason to keep it is deployment ordering — a gateway pinned to
|
|
56
|
+
* an older SDK copy would still collapse the oneof. Flip the default once the
|
|
57
|
+
* gateways in play are known to be on ≥1.0.1; the specs will hold the line if
|
|
58
|
+
* a later SDK regresses.
|
|
59
|
+
*
|
|
60
|
+
* `securityRequirements` is unaffected either way — it is a plain map, not a
|
|
61
|
+
* oneof — so the card always declares that auth is *required*, it just may not
|
|
62
|
+
* describe the scheme.
|
|
63
|
+
*/
|
|
64
|
+
advertiseSecuritySchemes?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build the (unsigned) AgentCard.
|
|
68
|
+
*
|
|
69
|
+
* v1.0 replaced the card's flat `url` / `preferredTransport` / `protocolVersion`
|
|
70
|
+
* fields with an ordered `supportedInterfaces` list, where each entry pins its
|
|
71
|
+
* own protocol binding *and* protocol version. That per-interface
|
|
72
|
+
* `protocolVersion` is what a client's transport factory matches on and what the
|
|
73
|
+
* Worker validates the `A2A-Version` request header against, so it must be the
|
|
74
|
+
* real protocol version rather than the agent's own version string.
|
|
75
|
+
*/
|
|
76
|
+
export declare function buildBaseCard(manifest: AgentManifest, opts: BuildCardOptions): AgentCard;
|
|
77
|
+
/**
|
|
78
|
+
* Sign the card with a detached-payload EdDSA flattened JWS, returning the
|
|
79
|
+
* signed **wire** card to serve. v1.0 standardized this (A2A spec §8.4), so the
|
|
80
|
+
* canonicalization and JWS construction are the SDK's rather than a local
|
|
81
|
+
* scheme: a JCS (RFC 8785) canonicalization of the card with `signatures`
|
|
82
|
+
* removed, under a protected header carrying `alg`, `kid` and `typ` (all three
|
|
83
|
+
* are required — the SDK verifier rejects a signature missing any of them).
|
|
84
|
+
*
|
|
85
|
+
* The card is normalized to its wire form *before* signing because that is what
|
|
86
|
+
* the verifier canonicalizes: `verifyAgentCardSignature` re-encodes whatever
|
|
87
|
+
* document it fetched through `AgentCard.toJSON(AgentCard.fromJSON(card))`.
|
|
88
|
+
* Signing the in-memory proto object instead would canonicalize `$case` tags and
|
|
89
|
+
* proto defaults that never appear on the wire, and every signature would fail.
|
|
90
|
+
*
|
|
91
|
+
* A verifying gateway strips the `signatures` array, recomputes the canonical
|
|
92
|
+
* payload, and verifies — pinning this key's `kid`+`jku` on first registration
|
|
93
|
+
* (Trust-On-First-Use).
|
|
94
|
+
*/
|
|
95
|
+
export declare function signCard(card: AgentCard, cfg: CardSigningConfig): Promise<WireAgentCard>;
|
|
96
|
+
/**
|
|
97
|
+
* The card's protobuf-JSON encoding. Typed back as `AgentCard` for the SDK
|
|
98
|
+
* signer, which is generic over "the document to canonicalize" rather than over
|
|
99
|
+
* the in-memory shape specifically.
|
|
100
|
+
*
|
|
101
|
+
* Encode-only — never `toJSON(fromJSON(card))`. `fromJSON` reads the *wire*
|
|
102
|
+
* spelling of a oneof, so running it over an in-memory card silently drops every
|
|
103
|
+
* `$case`-tagged field. The verifier's round trip is the mirror image and stable:
|
|
104
|
+
* it decodes the document it fetched and re-encodes it to exactly this.
|
|
105
|
+
*/
|
|
106
|
+
export declare function wireCard(card: AgentCard): AgentCard;
|
|
107
|
+
/**
|
|
108
|
+
* Parse and validate the `A2A_SIGNING_KEY` secret into the private JWK used to
|
|
109
|
+
* sign the card. Throws if the JWK is missing its `kid` (required for the JWS
|
|
110
|
+
* protected header and gateway key-pinning).
|
|
111
|
+
*/
|
|
112
|
+
export declare function parsePrivateJwk(raw: string): CardSigningConfig["privateJwk"];
|
|
113
|
+
/** Public card-signing JWKS (served at the `jku`): the private JWK minus `d`. */
|
|
114
|
+
export declare function publicCardJwks(privateJwk: JWK & {
|
|
115
|
+
kid: string;
|
|
116
|
+
}): {
|
|
117
|
+
keys: JWK[];
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=card.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../src/a2a/card.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAChC,OAAO,EAEL,SAAS,EAEV,MAAM,aAAa,CAAC;AAKrB,iFAAiF;AACjF,eAAO,MAAM,YAAY,SAAS,CAAC;AAEnC;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,IAAI,CAC9B,SAAS,EACP,MAAM,GACN,aAAa,GACb,SAAS,GACT,cAAc,GACd,mBAAmB,GACnB,oBAAoB,GACpB,QAAQ,CACX,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,UAAU,EAAE,GAAG,GAAG;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAUD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,aAAa,EACvB,IAAI,EAAE,gBAAgB,GACrB,SAAS,CAiCX;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,SAAS,EACf,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,aAAa,CAAC,CASxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAI5E;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,UAAU,EAAE,GAAG,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IACjE,IAAI,EAAE,GAAG,EAAE,CAAC;CACb,CAIA"}
|
package/dist/a2a/card.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { A2A_PROTOCOL_VERSION, AgentCard, generateAgentCardSignature } from "@a2a-js/sdk";
|
|
2
|
+
/** JWS algorithm for the card signature — must match the gateway (`EdDSA`). */
|
|
3
|
+
const ALG = "EdDSA";
|
|
4
|
+
/** The default JSON-RPC path an agent answers on (the card's only interface). */
|
|
5
|
+
export const A2A_RPC_PATH = "/a2a";
|
|
6
|
+
/**
|
|
7
|
+
* The `httpAuthSecurityScheme` describing the gateway's Bearer JWT, and the
|
|
8
|
+
* requirement that references it. Split out so the requirement can be advertised
|
|
9
|
+
* even when {@link BuildCardOptions.advertiseSecuritySchemes} keeps the scheme
|
|
10
|
+
* itself off the wire.
|
|
11
|
+
*/
|
|
12
|
+
const GATEWAY_SCHEME_ID = "gatewayJwt";
|
|
13
|
+
/**
|
|
14
|
+
* Build the (unsigned) AgentCard.
|
|
15
|
+
*
|
|
16
|
+
* v1.0 replaced the card's flat `url` / `preferredTransport` / `protocolVersion`
|
|
17
|
+
* fields with an ordered `supportedInterfaces` list, where each entry pins its
|
|
18
|
+
* own protocol binding *and* protocol version. That per-interface
|
|
19
|
+
* `protocolVersion` is what a client's transport factory matches on and what the
|
|
20
|
+
* Worker validates the `A2A-Version` request header against, so it must be the
|
|
21
|
+
* real protocol version rather than the agent's own version string.
|
|
22
|
+
*/
|
|
23
|
+
export function buildBaseCard(manifest, opts) {
|
|
24
|
+
const rpcPath = opts.rpcPath ?? A2A_RPC_PATH;
|
|
25
|
+
return {
|
|
26
|
+
...manifest,
|
|
27
|
+
supportedInterfaces: [
|
|
28
|
+
{
|
|
29
|
+
url: `${opts.origin}${rpcPath}`,
|
|
30
|
+
protocolBinding: "JSONRPC",
|
|
31
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
32
|
+
// Single-tenant agent: one DO per verified gateway identity, so the
|
|
33
|
+
// protocol-level tenant slot goes unused.
|
|
34
|
+
tenant: ""
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
provider: undefined,
|
|
38
|
+
documentationUrl: undefined,
|
|
39
|
+
iconUrl: undefined,
|
|
40
|
+
securitySchemes: opts.advertiseSecuritySchemes
|
|
41
|
+
? {
|
|
42
|
+
[GATEWAY_SCHEME_ID]: {
|
|
43
|
+
scheme: {
|
|
44
|
+
$case: "httpAuthSecurityScheme",
|
|
45
|
+
value: { description: "", scheme: "bearer", bearerFormat: "JWT" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
: {},
|
|
50
|
+
// v0.3's `security: [{ gatewayJwt: [] }]`. The empty `list` means the scheme
|
|
51
|
+
// is required but carries no scopes. Safe to advertise unconditionally: this
|
|
52
|
+
// is a plain map, so it survives the round trip that eats `securitySchemes`.
|
|
53
|
+
securityRequirements: [{ schemes: { [GATEWAY_SCHEME_ID]: { list: [] } } }],
|
|
54
|
+
signatures: []
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sign the card with a detached-payload EdDSA flattened JWS, returning the
|
|
59
|
+
* signed **wire** card to serve. v1.0 standardized this (A2A spec §8.4), so the
|
|
60
|
+
* canonicalization and JWS construction are the SDK's rather than a local
|
|
61
|
+
* scheme: a JCS (RFC 8785) canonicalization of the card with `signatures`
|
|
62
|
+
* removed, under a protected header carrying `alg`, `kid` and `typ` (all three
|
|
63
|
+
* are required — the SDK verifier rejects a signature missing any of them).
|
|
64
|
+
*
|
|
65
|
+
* The card is normalized to its wire form *before* signing because that is what
|
|
66
|
+
* the verifier canonicalizes: `verifyAgentCardSignature` re-encodes whatever
|
|
67
|
+
* document it fetched through `AgentCard.toJSON(AgentCard.fromJSON(card))`.
|
|
68
|
+
* Signing the in-memory proto object instead would canonicalize `$case` tags and
|
|
69
|
+
* proto defaults that never appear on the wire, and every signature would fail.
|
|
70
|
+
*
|
|
71
|
+
* A verifying gateway strips the `signatures` array, recomputes the canonical
|
|
72
|
+
* payload, and verifies — pinning this key's `kid`+`jku` on first registration
|
|
73
|
+
* (Trust-On-First-Use).
|
|
74
|
+
*/
|
|
75
|
+
export async function signCard(card, cfg) {
|
|
76
|
+
const sign = generateAgentCardSignature(cfg.privateJwk, {
|
|
77
|
+
alg: ALG,
|
|
78
|
+
kid: cfg.privateJwk.kid,
|
|
79
|
+
typ: "JOSE",
|
|
80
|
+
jku: cfg.jku
|
|
81
|
+
});
|
|
82
|
+
const signed = await sign(wireCard(card));
|
|
83
|
+
return signed;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The card's protobuf-JSON encoding. Typed back as `AgentCard` for the SDK
|
|
87
|
+
* signer, which is generic over "the document to canonicalize" rather than over
|
|
88
|
+
* the in-memory shape specifically.
|
|
89
|
+
*
|
|
90
|
+
* Encode-only — never `toJSON(fromJSON(card))`. `fromJSON` reads the *wire*
|
|
91
|
+
* spelling of a oneof, so running it over an in-memory card silently drops every
|
|
92
|
+
* `$case`-tagged field. The verifier's round trip is the mirror image and stable:
|
|
93
|
+
* it decodes the document it fetched and re-encodes it to exactly this.
|
|
94
|
+
*/
|
|
95
|
+
export function wireCard(card) {
|
|
96
|
+
return AgentCard.toJSON(card);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parse and validate the `A2A_SIGNING_KEY` secret into the private JWK used to
|
|
100
|
+
* sign the card. Throws if the JWK is missing its `kid` (required for the JWS
|
|
101
|
+
* protected header and gateway key-pinning).
|
|
102
|
+
*/
|
|
103
|
+
export function parsePrivateJwk(raw) {
|
|
104
|
+
const jwk = JSON.parse(raw);
|
|
105
|
+
if (!jwk.kid)
|
|
106
|
+
throw new Error("A2A_SIGNING_KEY must include a `kid`");
|
|
107
|
+
return jwk;
|
|
108
|
+
}
|
|
109
|
+
/** Public card-signing JWKS (served at the `jku`): the private JWK minus `d`. */
|
|
110
|
+
export function publicCardJwks(privateJwk) {
|
|
111
|
+
const { d: _d, ...pub } = privateJwk;
|
|
112
|
+
void _d;
|
|
113
|
+
return { keys: [{ ...pub, use: "sig", alg: ALG }] };
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card.js","sourceRoot":"","sources":["../../src/a2a/card.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,SAAS,EACT,0BAA0B,EAC3B,MAAM,aAAa,CAAC;AAErB,+EAA+E;AAC/E,MAAM,GAAG,GAAG,OAAO,CAAC;AAEpB,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AA0EnC;;;;;GAKG;AACH,MAAM,iBAAiB,GAAG,YAAY,CAAC;AAEvC;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAuB,EACvB,IAAsB;IAEtB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,YAAY,CAAC;IAC7C,OAAO;QACL,GAAG,QAAQ;QACX,mBAAmB,EAAE;YACnB;gBACE,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE;gBAC/B,eAAe,EAAE,SAAS;gBAC1B,eAAe,EAAE,oBAAoB;gBACrC,oEAAoE;gBACpE,0CAA0C;gBAC1C,MAAM,EAAE,EAAE;aACX;SACF;QACD,QAAQ,EAAE,SAAS;QACnB,gBAAgB,EAAE,SAAS;QAC3B,OAAO,EAAE,SAAS;QAClB,eAAe,EAAE,IAAI,CAAC,wBAAwB;YAC5C,CAAC,CAAC;gBACE,CAAC,iBAAiB,CAAC,EAAE;oBACnB,MAAM,EAAE;wBACN,KAAK,EAAE,wBAAwB;wBAC/B,KAAK,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE;qBAClE;iBACF;aACF;YACH,CAAC,CAAC,EAAE;QACN,6EAA6E;QAC7E,6EAA6E;QAC7E,6EAA6E;QAC7E,oBAAoB,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;QAC1E,UAAU,EAAE,EAAE;KACf,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,IAAe,EACf,GAAsB;IAEtB,MAAM,IAAI,GAAG,0BAA0B,CAAC,GAAG,CAAC,UAAU,EAAE;QACtD,GAAG,EAAE,GAAG;QACR,GAAG,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG;QACvB,GAAG,EAAE,MAAM;QACX,GAAG,EAAE,GAAG,CAAC,GAAG;KACb,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1C,OAAO,MAAkC,CAAC;AAC5C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAe;IACtC,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAc,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAqB,CAAC;IAChD,IAAI,CAAC,GAAG,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACtE,OAAO,GAAsC,CAAC;AAChD,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,cAAc,CAAC,UAAiC;IAG9D,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,GAAG,EAAE,GAAG,UAAU,CAAC;IACrC,KAAK,EAAE,CAAC;IACR,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ServerCallContext } from "@a2a-js/sdk/server";
|
|
2
|
+
import type { GatewayIdentity } 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` is deliberately unset: one agent DO instance *is* the tenant, keyed
|
|
9
|
+
* by the verified caller, so there is no second axis to scope on.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildCallContext(request: Request, identity: GatewayIdentity): ServerCallContext;
|
|
12
|
+
/** Echo back the extensions the handler actually activated (spec §14.2.2). */
|
|
13
|
+
export declare function extensionHeaders(context: ServerCallContext): HeadersInit;
|
|
14
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/a2a/context.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AA6BnD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,eAAe,GACxB,iBAAiB,CAYnB;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,WAAW,CAIxE"}
|
|
@@ -0,0 +1,55 @@
|
|
|
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 gateway-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 GatewayUser {
|
|
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` is deliberately unset: one agent DO instance *is* the tenant, keyed
|
|
35
|
+
* by the verified caller, so there is no second axis to scope on.
|
|
36
|
+
*/
|
|
37
|
+
export function buildCallContext(request, identity) {
|
|
38
|
+
const headers = {};
|
|
39
|
+
for (const [name, value] of request.headers)
|
|
40
|
+
headers[name] = value;
|
|
41
|
+
return defaultServerCallContextBuilder({
|
|
42
|
+
extensions: Extensions.parseServiceParameter(request.headers.get(HTTP_EXTENSION_HEADER) ?? undefined),
|
|
43
|
+
user: new GatewayUser(identity),
|
|
44
|
+
headers,
|
|
45
|
+
requestedVersion: request.headers.get(A2A_VERSION_HEADER) ?? undefined
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Echo back the extensions the handler actually activated (spec §14.2.2). */
|
|
49
|
+
export function extensionHeaders(context) {
|
|
50
|
+
const activated = context.activatedExtensions;
|
|
51
|
+
if (!activated?.length)
|
|
52
|
+
return {};
|
|
53
|
+
return { [HTTP_EXTENSION_HEADER]: Extensions.toServiceParameter(activated) };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=context.js.map
|