sello 0.1.15 → 0.1.17
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/README.md +39 -11
- package/dist/cli/sello.js +196 -4
- package/dist/sdk/service.js +129 -3
- package/docs/a2a.md +139 -0
- package/docs/mcp.md +149 -0
- package/docs/release-checklist.md +7 -0
- package/docs/sdk-quickstart.md +50 -1
- package/docs/sdk-security-audit.md +2 -0
- package/package.json +6 -2
- package/sdks/README.md +1 -1
- package/sdks/typescript/examples/a2a-minimal-server.ts +85 -0
- package/sdks/typescript/src/cli/sello.ts +196 -4
- package/sdks/typescript/src/sdk/service.ts +129 -3
package/README.md
CHANGED
|
@@ -16,16 +16,10 @@
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<a href="#try-it">Quickstart</a> ·
|
|
18
18
|
<a href="#add-sello-to-a-tool">Add Sello</a> ·
|
|
19
|
-
<a href="
|
|
19
|
+
<a href="docs/mcp.md">MCP</a> ·
|
|
20
20
|
<a href="sdks/README.md">SDKs</a> ·
|
|
21
|
-
<a href="#see-logged-actions">Actions</a> ·
|
|
22
|
-
<a href="#how-it-works">How It Works</a> ·
|
|
23
|
-
<a href="#learn-more">Learn More</a> ·
|
|
24
21
|
<a href="SPEC.md">Protocol</a> ·
|
|
25
|
-
<a href="https://arxiv.org/abs/2606.04193">Paper</a>
|
|
26
|
-
<a href="SECURITY.md">Security</a> ·
|
|
27
|
-
<a href="CONTRIBUTING.md">Contributing</a> ·
|
|
28
|
-
<a href="#license">License</a>
|
|
22
|
+
<a href="https://arxiv.org/abs/2606.04193">Paper</a>
|
|
29
23
|
</p>
|
|
30
24
|
|
|
31
25
|
Sello is a protocol for independently-verifiable records of AI agent actions.
|
|
@@ -110,11 +104,13 @@ SELLO_SUBMIT_MODE=background
|
|
|
110
104
|
|
|
111
105
|
Sello works with your own log server. Using `sello.build` is an optional convenience, not a protocol requirement.
|
|
112
106
|
|
|
113
|
-
To scaffold a tiny
|
|
107
|
+
To scaffold a tiny demo for each boundary:
|
|
114
108
|
|
|
115
109
|
```bash
|
|
116
110
|
npx --yes sello init-demo
|
|
117
111
|
npx --yes sello init-http-demo
|
|
112
|
+
npx --yes sello init-mcp-demo
|
|
113
|
+
npx --yes sello init-a2a-demo
|
|
118
114
|
```
|
|
119
115
|
|
|
120
116
|
## Add Sello to an MCP Server
|
|
@@ -150,6 +146,37 @@ receipts.mcpTool("calendar.create_event", handler, {
|
|
|
150
146
|
});
|
|
151
147
|
```
|
|
152
148
|
|
|
149
|
+
See [docs/mcp.md](docs/mcp.md) for token handling, hash boundaries, unknown-tool behavior, and action viewing.
|
|
150
|
+
|
|
151
|
+
## Add Sello to an A2A Agent
|
|
152
|
+
|
|
153
|
+
If your service receives Agent2Agent messages, wrap the message handler:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { sello } from "sello";
|
|
157
|
+
|
|
158
|
+
const receipts = sello.service();
|
|
159
|
+
|
|
160
|
+
export const sendMessage = receipts.a2aMessage(async (request, context) => {
|
|
161
|
+
return agent.handleMessage(request, context);
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`a2aMessage` verifies the agent token before your handler runs, preserves the handler's return value, rethrows handler errors, and emits a receipt with action type `a2a.<method>`, such as `a2a.message/send`.
|
|
166
|
+
|
|
167
|
+
By default, Sello hashes the A2A JSON-RPC `method` and `params`, excluding request ids, headers, bearer tokens, and runtime context.
|
|
168
|
+
|
|
169
|
+
See [docs/a2a.md](docs/a2a.md) for token handling, hash boundaries, unknown-method behavior, and action viewing.
|
|
170
|
+
|
|
171
|
+
## Examples
|
|
172
|
+
|
|
173
|
+
| What you want to wire | Start here |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| A normal tool handler | [SDK Quickstart](docs/sdk-quickstart.md) and [`quickstart-tool.ts`](sdks/typescript/examples/quickstart-tool.ts) |
|
|
176
|
+
| An MCP tool | `npx --yes sello init-mcp-demo`, [MCP Integration](docs/mcp.md), and [`mcp-minimal-server.ts`](sdks/typescript/examples/mcp-minimal-server.ts) |
|
|
177
|
+
| An A2A message handler | `npx --yes sello init-a2a-demo`, [A2A Integration](docs/a2a.md), and [`a2a-minimal-server.ts`](sdks/typescript/examples/a2a-minimal-server.ts) |
|
|
178
|
+
| Verified action viewing | `npx sello actions` or `http://localhost:8787/actions` |
|
|
179
|
+
|
|
153
180
|
## See Logged Actions
|
|
154
181
|
|
|
155
182
|
```bash
|
|
@@ -184,15 +211,16 @@ Sello does not prove that the agent called every service it should have called,
|
|
|
184
211
|
## Learn More
|
|
185
212
|
|
|
186
213
|
- [SDK Quickstart](docs/sdk-quickstart.md): local dev, HTTP demo, self-hosted config, and hosted config.
|
|
214
|
+
- [MCP Integration](docs/mcp.md): where Sello wraps MCP tool callbacks.
|
|
215
|
+
- [A2A Integration](docs/a2a.md): where Sello wraps Agent2Agent message handlers.
|
|
187
216
|
- [SDKs](sdks/README.md): TypeScript and Python package layout.
|
|
188
217
|
- [Python SDK](sdks/python/README.md): Python package install command, scope, and test command.
|
|
189
218
|
- [Protocol Walkthrough](docs/protocol-walkthrough.md): the primitive receipt loop for implementers.
|
|
190
219
|
- [SPEC.md](SPEC.md): the Sello protocol draft.
|
|
191
220
|
- [Notarized Agents paper](https://arxiv.org/abs/2606.04193): design rationale, threat model, and prior art.
|
|
192
|
-
- [sdks/typescript/examples/mcp-minimal-server.ts](sdks/typescript/examples/mcp-minimal-server.ts): a small MCP integration.
|
|
193
221
|
- [docs/security-review.md](docs/security-review.md) and [docs/sdk-security-audit.md](docs/sdk-security-audit.md): current review notes.
|
|
194
222
|
|
|
195
|
-
The TypeScript SDK is published on [npm](https://www.npmjs.com/package/sello) and lives in [`sdks/typescript/`](sdks/typescript/). The Python SDK is published on [PyPI](https://pypi.org/project/sello/) and lives in [`sdks/python/`](sdks/python/). Both SDKs support the same service-side `sello.service()` flow; Python uses the `@receipts.tool(...)` decorator. Live Rekor proof verification and production identity operations are still future work.
|
|
223
|
+
The TypeScript SDK is published on [npm](https://www.npmjs.com/package/sello) and lives in [`sdks/typescript/`](sdks/typescript/). The Python SDK is published on [PyPI](https://pypi.org/project/sello/) and lives in [`sdks/python/`](sdks/python/). Both SDKs support the same service-side `sello.service()` flow; Python uses the `@receipts.tool(...)` decorator. MCP and A2A helpers are currently TypeScript-first. Live Rekor proof verification and production identity operations are still future work.
|
|
196
224
|
|
|
197
225
|
## Core Terms
|
|
198
226
|
|
package/dist/cli/sello.js
CHANGED
|
@@ -96,6 +96,12 @@ try {
|
|
|
96
96
|
case "init-http-demo":
|
|
97
97
|
initHttpDemoCommand(process.argv.slice(3));
|
|
98
98
|
break;
|
|
99
|
+
case "init-mcp-demo":
|
|
100
|
+
initMcpDemoCommand(process.argv.slice(3));
|
|
101
|
+
break;
|
|
102
|
+
case "init-a2a-demo":
|
|
103
|
+
initA2aDemoCommand(process.argv.slice(3));
|
|
104
|
+
break;
|
|
99
105
|
case "keys":
|
|
100
106
|
keysCommand(process.argv.slice(3));
|
|
101
107
|
break;
|
|
@@ -337,13 +343,13 @@ function initDemoCommand(args ) {
|
|
|
337
343
|
console.log(`Created ${output}`);
|
|
338
344
|
console.log("");
|
|
339
345
|
console.log("Terminal 1: keep the local dev log running");
|
|
340
|
-
console.log(
|
|
346
|
+
console.log(` ${devCommandHint()}`);
|
|
341
347
|
console.log("");
|
|
342
348
|
console.log("Terminal 2: emit and view a receipt");
|
|
343
349
|
console.log(` node ${output}`);
|
|
344
350
|
console.log(" npx sello actions");
|
|
345
351
|
console.log("");
|
|
346
|
-
console.log(
|
|
352
|
+
console.log(`Then open ${actionViewerUrlHint()}`);
|
|
347
353
|
}
|
|
348
354
|
|
|
349
355
|
function initHttpDemoCommand(args ) {
|
|
@@ -359,7 +365,7 @@ function initHttpDemoCommand(args ) {
|
|
|
359
365
|
console.log(`Created ${output}`);
|
|
360
366
|
console.log("");
|
|
361
367
|
console.log("Terminal 1: keep the local dev log running");
|
|
362
|
-
console.log(
|
|
368
|
+
console.log(` ${devCommandHint()}`);
|
|
363
369
|
console.log("");
|
|
364
370
|
console.log("Terminal 2: start the route");
|
|
365
371
|
console.log(` node ${output}`);
|
|
@@ -368,7 +374,51 @@ function initHttpDemoCommand(args ) {
|
|
|
368
374
|
console.log(" npx sello call-http-demo");
|
|
369
375
|
console.log(" npx sello actions");
|
|
370
376
|
console.log("");
|
|
371
|
-
console.log(
|
|
377
|
+
console.log(`Then open ${actionViewerUrlHint()}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function initMcpDemoCommand(args ) {
|
|
381
|
+
const output = readFlag(args, "--output") ?? "sello-mcp-demo.mjs";
|
|
382
|
+
const force = args.includes("--force");
|
|
383
|
+
|
|
384
|
+
if (existsSync(output) && !force) {
|
|
385
|
+
throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
writeFileSync(output, mcpDemoSource(), { mode: 0o644 });
|
|
389
|
+
|
|
390
|
+
console.log(`Created ${output}`);
|
|
391
|
+
console.log("");
|
|
392
|
+
console.log("Terminal 1: keep the local dev log running");
|
|
393
|
+
console.log(` ${devCommandHint()}`);
|
|
394
|
+
console.log("");
|
|
395
|
+
console.log("Terminal 2: run the MCP-shaped tool call and view the receipt");
|
|
396
|
+
console.log(` node ${output}`);
|
|
397
|
+
console.log(" npx sello actions");
|
|
398
|
+
console.log("");
|
|
399
|
+
console.log(`Then open ${actionViewerUrlHint()}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function initA2aDemoCommand(args ) {
|
|
403
|
+
const output = readFlag(args, "--output") ?? "sello-a2a-demo.mjs";
|
|
404
|
+
const force = args.includes("--force");
|
|
405
|
+
|
|
406
|
+
if (existsSync(output) && !force) {
|
|
407
|
+
throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
writeFileSync(output, a2aDemoSource(), { mode: 0o644 });
|
|
411
|
+
|
|
412
|
+
console.log(`Created ${output}`);
|
|
413
|
+
console.log("");
|
|
414
|
+
console.log("Terminal 1: keep the local dev log running");
|
|
415
|
+
console.log(` ${devCommandHint()}`);
|
|
416
|
+
console.log("");
|
|
417
|
+
console.log("Terminal 2: run the A2A-shaped message and view the receipt");
|
|
418
|
+
console.log(` node ${output}`);
|
|
419
|
+
console.log(" npx sello actions");
|
|
420
|
+
console.log("");
|
|
421
|
+
console.log(`Then open ${actionViewerUrlHint()}`);
|
|
372
422
|
}
|
|
373
423
|
|
|
374
424
|
function keysCommand(args ) {
|
|
@@ -851,6 +901,8 @@ function printHelp() {
|
|
|
851
901
|
sello call-http-demo [--url http://localhost:8790/calendar/events] [--title title]
|
|
852
902
|
sello init-demo [--output emit-receipt.mjs] [--force]
|
|
853
903
|
sello init-http-demo [--output sello-http-route.mjs] [--force]
|
|
904
|
+
sello init-mcp-demo [--output sello-mcp-demo.mjs] [--force]
|
|
905
|
+
sello init-a2a-demo [--output sello-a2a-demo.mjs] [--force]
|
|
854
906
|
sello actions [--token agent-token]
|
|
855
907
|
sello keys service
|
|
856
908
|
sello inspect-env
|
|
@@ -917,6 +969,25 @@ function formatHttpDemoResponse(responseText ) {
|
|
|
917
969
|
}
|
|
918
970
|
}
|
|
919
971
|
|
|
972
|
+
function devCommandHint() {
|
|
973
|
+
const state = loadDevStateIfPresent();
|
|
974
|
+
const port = state ? portFromDevState(state) : undefined;
|
|
975
|
+
return port && port !== "8787" ? `npx sello dev --port ${port}` : "npx sello dev";
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function actionViewerUrlHint() {
|
|
979
|
+
const state = loadDevStateIfPresent();
|
|
980
|
+
return state ? actionViewerUrl(state) : "http://localhost:8787/actions";
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function portFromDevState(state ) {
|
|
984
|
+
try {
|
|
985
|
+
return new URL(state.logEndpoint).port;
|
|
986
|
+
} catch {
|
|
987
|
+
return undefined;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
920
991
|
function slug(value ) {
|
|
921
992
|
return value
|
|
922
993
|
.toLowerCase()
|
|
@@ -1080,3 +1151,124 @@ function slug(value) {
|
|
|
1080
1151
|
}
|
|
1081
1152
|
`;
|
|
1082
1153
|
}
|
|
1154
|
+
|
|
1155
|
+
function mcpDemoSource() {
|
|
1156
|
+
return `import { readFileSync } from "node:fs";
|
|
1157
|
+
import { sello } from "sello";
|
|
1158
|
+
|
|
1159
|
+
const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
|
|
1160
|
+
|
|
1161
|
+
const receipts = sello.service({
|
|
1162
|
+
service: state.serviceId,
|
|
1163
|
+
serviceKey: state.serviceKey,
|
|
1164
|
+
tokenIssuer: state.tokenIssuerPublicKey,
|
|
1165
|
+
log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
|
|
1166
|
+
submit: { mode: "await" },
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
const createEvent = receipts.mcpTool("calendar.create_event", async (args) => {
|
|
1170
|
+
const title = readString(args.title, "title");
|
|
1171
|
+
return {
|
|
1172
|
+
content: [
|
|
1173
|
+
{
|
|
1174
|
+
type: "text",
|
|
1175
|
+
text: "created " + title,
|
|
1176
|
+
},
|
|
1177
|
+
],
|
|
1178
|
+
};
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
const response = await createEvent(
|
|
1182
|
+
{
|
|
1183
|
+
title: "MCP launch checklist",
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
requestInfo: {
|
|
1187
|
+
headers: new Headers({
|
|
1188
|
+
authorization: "Bearer " + state.agentToken,
|
|
1189
|
+
}),
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
await receipts.flush();
|
|
1195
|
+
|
|
1196
|
+
console.log("MCP tool response:");
|
|
1197
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1198
|
+
|
|
1199
|
+
function readString(value, name) {
|
|
1200
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1201
|
+
throw new TypeError(name + " must be a non-empty string");
|
|
1202
|
+
}
|
|
1203
|
+
return value;
|
|
1204
|
+
}
|
|
1205
|
+
`;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function a2aDemoSource() {
|
|
1209
|
+
return `import { readFileSync } from "node:fs";
|
|
1210
|
+
import { sello } from "sello";
|
|
1211
|
+
|
|
1212
|
+
const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
|
|
1213
|
+
|
|
1214
|
+
const receipts = sello.service({
|
|
1215
|
+
service: state.serviceId,
|
|
1216
|
+
serviceKey: state.serviceKey,
|
|
1217
|
+
tokenIssuer: state.tokenIssuerPublicKey,
|
|
1218
|
+
log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
|
|
1219
|
+
submit: { mode: "await" },
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
const sendMessage = receipts.a2aMessage(async (request) => ({
|
|
1223
|
+
jsonrpc: "2.0",
|
|
1224
|
+
id: request.id,
|
|
1225
|
+
result: {
|
|
1226
|
+
kind: "message",
|
|
1227
|
+
messageId: "calendar-reply-1",
|
|
1228
|
+
role: "agent",
|
|
1229
|
+
parts: [
|
|
1230
|
+
{
|
|
1231
|
+
kind: "text",
|
|
1232
|
+
text: "created " + readMessageText(request),
|
|
1233
|
+
},
|
|
1234
|
+
],
|
|
1235
|
+
},
|
|
1236
|
+
}));
|
|
1237
|
+
|
|
1238
|
+
const response = await sendMessage(
|
|
1239
|
+
{
|
|
1240
|
+
jsonrpc: "2.0",
|
|
1241
|
+
id: "a2a-demo-1",
|
|
1242
|
+
method: "message/send",
|
|
1243
|
+
params: {
|
|
1244
|
+
message: {
|
|
1245
|
+
role: "user",
|
|
1246
|
+
parts: [
|
|
1247
|
+
{
|
|
1248
|
+
kind: "text",
|
|
1249
|
+
text: "A2A launch checklist",
|
|
1250
|
+
},
|
|
1251
|
+
],
|
|
1252
|
+
},
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
headers: new Headers({
|
|
1257
|
+
authorization: "Bearer " + state.agentToken,
|
|
1258
|
+
}),
|
|
1259
|
+
},
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1262
|
+
await receipts.flush();
|
|
1263
|
+
|
|
1264
|
+
console.log("A2A message response:");
|
|
1265
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1266
|
+
|
|
1267
|
+
function readMessageText(request) {
|
|
1268
|
+
return request.params.message.parts
|
|
1269
|
+
.filter((part) => part.kind === "text")
|
|
1270
|
+
.map((part) => part.text)
|
|
1271
|
+
.join(" ") || "untitled";
|
|
1272
|
+
}
|
|
1273
|
+
`;
|
|
1274
|
+
}
|
package/dist/sdk/service.js
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
|
|
@@ -71,6 +73,35 @@ import {
|
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
74
105
|
|
|
75
106
|
|
|
76
107
|
|
|
@@ -100,6 +131,10 @@ import {
|
|
|
100
131
|
|
|
101
132
|
|
|
102
133
|
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
103
138
|
|
|
104
139
|
|
|
105
140
|
|
|
@@ -150,11 +185,14 @@ export function createSelloService(input ) {
|
|
|
150
185
|
}
|
|
151
186
|
|
|
152
187
|
function wrapTool (
|
|
153
|
-
actionType
|
|
188
|
+
actionType ,
|
|
154
189
|
handler ,
|
|
155
190
|
options = {},
|
|
156
191
|
) {
|
|
157
|
-
if (
|
|
192
|
+
if (
|
|
193
|
+
(typeof actionType === "string" && actionType.length === 0) ||
|
|
194
|
+
(typeof actionType !== "string" && typeof actionType !== "function")
|
|
195
|
+
) {
|
|
158
196
|
throw new TypeError("Sello action type must be a non-empty string");
|
|
159
197
|
}
|
|
160
198
|
|
|
@@ -174,6 +212,7 @@ export function createSelloService(input ) {
|
|
|
174
212
|
resolved.fallbackSelloLogs,
|
|
175
213
|
resolved.log.logUrl,
|
|
176
214
|
);
|
|
215
|
+
const resolvedActionType = resolveActionType(actionType, request);
|
|
177
216
|
const base = {
|
|
178
217
|
authorizationTokenBytes: verifiedToken.authorizationTokenBytes,
|
|
179
218
|
ownerHpkePublicKey: verifiedToken.ownerHpkePublicKey,
|
|
@@ -182,7 +221,7 @@ export function createSelloService(input ) {
|
|
|
182
221
|
servicePrivateKey: resolved.servicePrivateKey,
|
|
183
222
|
serviceIdentifier: resolved.service,
|
|
184
223
|
logUrl: resolved.log.logUrl,
|
|
185
|
-
actionType,
|
|
224
|
+
actionType: resolvedActionType,
|
|
186
225
|
actionInputBytes: (options.canonicalizeInput ?? canonicalJsonBytes)(request),
|
|
187
226
|
timestamp: resolved.now(),
|
|
188
227
|
};
|
|
@@ -271,6 +310,32 @@ export function createSelloService(input ) {
|
|
|
271
310
|
return await wrapped({ name, arguments: args, context });
|
|
272
311
|
};
|
|
273
312
|
},
|
|
313
|
+
a2aMessage (
|
|
314
|
+
handler ,
|
|
315
|
+
options = {},
|
|
316
|
+
) {
|
|
317
|
+
const wrapped = wrapTool
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
(
|
|
321
|
+
options.actionType ?? defaultA2aActionType,
|
|
322
|
+
async (invocation) => handler(invocation.request, invocation.context),
|
|
323
|
+
{
|
|
324
|
+
authorizationToken:
|
|
325
|
+
options.authorizationToken ?? defaultA2aAuthorizationToken,
|
|
326
|
+
canonicalizeInput:
|
|
327
|
+
options.canonicalizeInput ?? defaultA2aCanonicalizeInput,
|
|
328
|
+
canonicalizeOutput: options.canonicalizeOutput,
|
|
329
|
+
canonicalizeError: options.canonicalizeError,
|
|
330
|
+
isDenied: options.isDenied,
|
|
331
|
+
deniedResponse: options.deniedResponse,
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
return async (request , context ) => {
|
|
336
|
+
return await wrapped({ request, context });
|
|
337
|
+
};
|
|
338
|
+
},
|
|
274
339
|
async flush() {
|
|
275
340
|
await publisher?.flush();
|
|
276
341
|
},
|
|
@@ -587,6 +652,53 @@ function defaultMcpCanonicalizeInput(
|
|
|
587
652
|
});
|
|
588
653
|
}
|
|
589
654
|
|
|
655
|
+
function defaultA2aAuthorizationToken(
|
|
656
|
+
invocation ,
|
|
657
|
+
) {
|
|
658
|
+
const fromContext = authorizationTokenFromUnknown(invocation.context);
|
|
659
|
+
if (fromContext) {
|
|
660
|
+
return fromContext;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const fromRequest = authorizationTokenFromUnknown(invocation.request);
|
|
664
|
+
if (fromRequest) {
|
|
665
|
+
return fromRequest;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
throw new TypeError(
|
|
669
|
+
"Sello A2A authorization token not found. Pass authorizationToken or include an Authorization header in the A2A context.",
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function defaultA2aActionType(
|
|
674
|
+
invocation ,
|
|
675
|
+
) {
|
|
676
|
+
const method = methodFromA2aRequest(invocation.request);
|
|
677
|
+
return method ? `a2a.${method}` : "a2a.message/send";
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function defaultA2aCanonicalizeInput(
|
|
681
|
+
invocation ,
|
|
682
|
+
) {
|
|
683
|
+
if (isRecord(invocation.request) && typeof invocation.request.method === "string") {
|
|
684
|
+
return canonicalJsonBytes({
|
|
685
|
+
method: invocation.request.method,
|
|
686
|
+
params: invocation.request.params ?? null,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return canonicalJsonBytes(invocation.request);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function methodFromA2aRequest(request ) {
|
|
694
|
+
if (!isRecord(request)) {
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const method = request.method;
|
|
699
|
+
return typeof method === "string" && method.length > 0 ? method : undefined;
|
|
700
|
+
}
|
|
701
|
+
|
|
590
702
|
function authorizationTokenFromUnknown(value ) {
|
|
591
703
|
if (!isRecord(value)) {
|
|
592
704
|
return undefined;
|
|
@@ -692,6 +804,20 @@ function resolveValue (
|
|
|
692
804
|
return value;
|
|
693
805
|
}
|
|
694
806
|
|
|
807
|
+
function resolveActionType (
|
|
808
|
+
actionType ,
|
|
809
|
+
request ,
|
|
810
|
+
) {
|
|
811
|
+
const resolved =
|
|
812
|
+
typeof actionType === "function" ? actionType(request) : actionType;
|
|
813
|
+
|
|
814
|
+
if (typeof resolved !== "string" || resolved.length === 0) {
|
|
815
|
+
throw new TypeError("Sello action type must be a non-empty string");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return resolved;
|
|
819
|
+
}
|
|
820
|
+
|
|
695
821
|
function envSubmitMode(env ) {
|
|
696
822
|
const mode = env.SELLO_SUBMIT_MODE;
|
|
697
823
|
if (mode === undefined) {
|
package/docs/a2a.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Add Sello to an A2A Agent
|
|
2
|
+
|
|
3
|
+
Sello belongs around the handler that receives an A2A message: after your server has accepted the request and before your agent logic runs.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { sello } from "sello";
|
|
7
|
+
|
|
8
|
+
const receipts = sello.service();
|
|
9
|
+
|
|
10
|
+
export const sendMessage = receipts.a2aMessage(async (request, context) => {
|
|
11
|
+
return agent.handleMessage(request, context);
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What It Does
|
|
16
|
+
|
|
17
|
+
`receipts.a2aMessage(...)` returns a normal A2A message handler. When another agent sends a message, Sello:
|
|
18
|
+
|
|
19
|
+
1. Reads the agent authorization token from the A2A context.
|
|
20
|
+
2. Verifies the token before your handler runs.
|
|
21
|
+
3. Runs your handler unchanged.
|
|
22
|
+
4. Emits a `success`, `error`, or `denied` receipt.
|
|
23
|
+
5. Preserves the handler return value and rethrows handler errors.
|
|
24
|
+
|
|
25
|
+
For JSON-RPC requests, the default receipt action type is:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
a2a.<method>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For `message/send`, the action type is:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
a2a.message/send
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Token Source
|
|
38
|
+
|
|
39
|
+
By default, Sello looks for `Authorization: Bearer ...` in common context or request shapes, including:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
context.headers.authorization
|
|
43
|
+
context.requestInfo.headers.authorization
|
|
44
|
+
context.request.headers.authorization
|
|
45
|
+
request.headers.authorization
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Fetch-style `Headers` objects are supported too.
|
|
49
|
+
|
|
50
|
+
If your A2A runtime stores the token somewhere else, pass an extractor:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
receipts.a2aMessage(handler, {
|
|
54
|
+
authorizationToken: ({ context }) => context.session.token,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The token should be the same authorization token the calling agent used for the A2A request. Sello hashes the exact token bytes into `sello_token_ref`; do not parse and reserialize it first.
|
|
59
|
+
|
|
60
|
+
## What Gets Hashed
|
|
61
|
+
|
|
62
|
+
For JSON-RPC-shaped A2A requests, the default input hash covers only the stable method boundary:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"method": "message/send",
|
|
67
|
+
"params": {}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Sello does not hash:
|
|
72
|
+
|
|
73
|
+
- The bearer token.
|
|
74
|
+
- Transport headers.
|
|
75
|
+
- JSON-RPC request ids.
|
|
76
|
+
- Connection/session objects.
|
|
77
|
+
- Other context that may vary across runtimes.
|
|
78
|
+
|
|
79
|
+
That keeps receipts stable without leaking secrets or transport details.
|
|
80
|
+
|
|
81
|
+
If you need a different hash boundary, pass `canonicalizeInput`.
|
|
82
|
+
|
|
83
|
+
## Unknown Methods
|
|
84
|
+
|
|
85
|
+
Only wrap A2A methods you actually execute. If a request names an unknown method, return your normal JSON-RPC method-not-found error without emitting a Sello receipt.
|
|
86
|
+
|
|
87
|
+
The minimal example does this:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
if (request.body.method !== "message/send") {
|
|
91
|
+
return {
|
|
92
|
+
jsonrpc: "2.0",
|
|
93
|
+
id: request.body.id,
|
|
94
|
+
error: { code: -32601, message: "method not found" },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## View Actions
|
|
100
|
+
|
|
101
|
+
In local development:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx sello dev
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run your A2A message call, then view verified actions:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx sello actions
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Or open:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
http://localhost:8787/actions
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For a specific agent token:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx sello actions --token <agent-token>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Runnable Demo
|
|
126
|
+
|
|
127
|
+
To generate a tiny A2A-shaped demo in your project:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx --yes sello init-a2a-demo
|
|
131
|
+
node sello-a2a-demo.mjs
|
|
132
|
+
npx --yes sello actions
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Keep `npx sello dev` running in another terminal while you run the generated file.
|
|
136
|
+
|
|
137
|
+
## Example
|
|
138
|
+
|
|
139
|
+
See [`sdks/typescript/examples/a2a-minimal-server.ts`](../sdks/typescript/examples/a2a-minimal-server.ts) for a dependency-free A2A-shaped example. It wraps one `message/send` handler and leaves unknown methods unreceipted.
|