sello 0.1.14 → 0.1.16
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 +28 -9
- package/dist/cli/sello.js +65 -8
- package/dist/sdk/service.js +129 -3
- package/docs/a2a.md +127 -0
- package/docs/mcp.md +137 -0
- package/docs/release-checklist.md +7 -0
- package/docs/sdk-quickstart.md +24 -0
- 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 +65 -8
- 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.
|
|
@@ -150,6 +144,28 @@ receipts.mcpTool("calendar.create_event", handler, {
|
|
|
150
144
|
});
|
|
151
145
|
```
|
|
152
146
|
|
|
147
|
+
See [docs/mcp.md](docs/mcp.md) for token handling, hash boundaries, unknown-tool behavior, and action viewing.
|
|
148
|
+
|
|
149
|
+
## Add Sello to an A2A Agent
|
|
150
|
+
|
|
151
|
+
If your service receives Agent2Agent messages, wrap the message handler:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { sello } from "sello";
|
|
155
|
+
|
|
156
|
+
const receipts = sello.service();
|
|
157
|
+
|
|
158
|
+
export const sendMessage = receipts.a2aMessage(async (request, context) => {
|
|
159
|
+
return agent.handleMessage(request, context);
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`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`.
|
|
164
|
+
|
|
165
|
+
By default, Sello hashes the A2A JSON-RPC `method` and `params`, excluding request ids, headers, bearer tokens, and runtime context.
|
|
166
|
+
|
|
167
|
+
See [docs/a2a.md](docs/a2a.md) for token handling, hash boundaries, unknown-method behavior, and action viewing.
|
|
168
|
+
|
|
153
169
|
## See Logged Actions
|
|
154
170
|
|
|
155
171
|
```bash
|
|
@@ -184,15 +200,18 @@ Sello does not prove that the agent called every service it should have called,
|
|
|
184
200
|
## Learn More
|
|
185
201
|
|
|
186
202
|
- [SDK Quickstart](docs/sdk-quickstart.md): local dev, HTTP demo, self-hosted config, and hosted config.
|
|
203
|
+
- [MCP Integration](docs/mcp.md): where Sello wraps MCP tool callbacks.
|
|
204
|
+
- [A2A Integration](docs/a2a.md): where Sello wraps Agent2Agent message handlers.
|
|
187
205
|
- [SDKs](sdks/README.md): TypeScript and Python package layout.
|
|
188
206
|
- [Python SDK](sdks/python/README.md): Python package install command, scope, and test command.
|
|
189
207
|
- [Protocol Walkthrough](docs/protocol-walkthrough.md): the primitive receipt loop for implementers.
|
|
190
208
|
- [SPEC.md](SPEC.md): the Sello protocol draft.
|
|
191
209
|
- [Notarized Agents paper](https://arxiv.org/abs/2606.04193): design rationale, threat model, and prior art.
|
|
192
210
|
- [sdks/typescript/examples/mcp-minimal-server.ts](sdks/typescript/examples/mcp-minimal-server.ts): a small MCP integration.
|
|
211
|
+
- [sdks/typescript/examples/a2a-minimal-server.ts](sdks/typescript/examples/a2a-minimal-server.ts): a small A2A integration.
|
|
193
212
|
- [docs/security-review.md](docs/security-review.md) and [docs/sdk-security-audit.md](docs/sdk-security-audit.md): current review notes.
|
|
194
213
|
|
|
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.
|
|
214
|
+
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
215
|
|
|
197
216
|
## Core Terms
|
|
198
217
|
|
package/dist/cli/sello.js
CHANGED
|
@@ -199,6 +199,29 @@ async function devCommand(args ) {
|
|
|
199
199
|
console.log(`Local dev log: ${logPath}`);
|
|
200
200
|
console.log(`Loaded ${devLogEntryCountLabel(loadedEntries)}.`);
|
|
201
201
|
});
|
|
202
|
+
server.on("error", (error) => {
|
|
203
|
+
if (isPortInUse(error)) {
|
|
204
|
+
console.error(`sello: port ${port} is already in use.`);
|
|
205
|
+
console.error("");
|
|
206
|
+
console.error("Try:");
|
|
207
|
+
console.error(` sello dev --port ${port + 1}`);
|
|
208
|
+
console.error("");
|
|
209
|
+
console.error("Or stop the process using that port:");
|
|
210
|
+
console.error(` lsof -nP -iTCP:${port} -sTCP:LISTEN`);
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isListenError(error)) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
console.error(`sello: could not start dev server on port ${port}.`);
|
|
218
|
+
console.error(message);
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw error;
|
|
224
|
+
});
|
|
202
225
|
}
|
|
203
226
|
|
|
204
227
|
async function emitDemoCommand(args ) {
|
|
@@ -257,14 +280,7 @@ async function callHttpDemoCommand(args ) {
|
|
|
257
280
|
);
|
|
258
281
|
const url = readFlag(args, "--url") ?? "http://localhost:8790/calendar/events";
|
|
259
282
|
const title = readFlag(args, "--title") ?? "Ship Sello";
|
|
260
|
-
const response = await
|
|
261
|
-
method: "POST",
|
|
262
|
-
headers: {
|
|
263
|
-
authorization: `Bearer ${state.agentToken}`,
|
|
264
|
-
"content-type": "application/json",
|
|
265
|
-
},
|
|
266
|
-
body: JSON.stringify({ title }),
|
|
267
|
-
});
|
|
283
|
+
const response = await fetchHttpDemo(url, state.agentToken, title);
|
|
268
284
|
const responseText = await response.text();
|
|
269
285
|
|
|
270
286
|
if (!response.ok) {
|
|
@@ -283,6 +299,31 @@ async function callHttpDemoCommand(args ) {
|
|
|
283
299
|
console.log(` ${actionViewerUrl(state)}`);
|
|
284
300
|
}
|
|
285
301
|
|
|
302
|
+
async function fetchHttpDemo(
|
|
303
|
+
url ,
|
|
304
|
+
agentToken ,
|
|
305
|
+
title ,
|
|
306
|
+
) {
|
|
307
|
+
try {
|
|
308
|
+
return await fetch(url, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: {
|
|
311
|
+
authorization: `Bearer ${agentToken}`,
|
|
312
|
+
"content-type": "application/json",
|
|
313
|
+
},
|
|
314
|
+
body: JSON.stringify({ title }),
|
|
315
|
+
});
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (isFetchFailed(error)) {
|
|
318
|
+
throw new TypeError(
|
|
319
|
+
`could not reach the HTTP demo route at ${url}.\n\nStart it in another terminal:\n node sello-http-route.mjs`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
286
327
|
function initDemoCommand(args ) {
|
|
287
328
|
const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
|
|
288
329
|
const force = args.includes("--force");
|
|
@@ -787,6 +828,22 @@ function isRecord(value ) {
|
|
|
787
828
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
788
829
|
}
|
|
789
830
|
|
|
831
|
+
function isPortInUse(error ) {
|
|
832
|
+
return (
|
|
833
|
+
isRecord(error) &&
|
|
834
|
+
error.code === "EADDRINUSE" &&
|
|
835
|
+
error.syscall === "listen"
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function isListenError(error ) {
|
|
840
|
+
return isRecord(error) && error.syscall === "listen";
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function isFetchFailed(error ) {
|
|
844
|
+
return error instanceof TypeError && error.message === "fetch failed";
|
|
845
|
+
}
|
|
846
|
+
|
|
790
847
|
function printHelp() {
|
|
791
848
|
console.log(`Usage:
|
|
792
849
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
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,127 @@
|
|
|
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
|
+
## Example
|
|
126
|
+
|
|
127
|
+
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.
|
package/docs/mcp.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Add Sello to an MCP Server
|
|
2
|
+
|
|
3
|
+
Sello belongs around the tool callback: after your MCP server has identified the tool, before your business logic runs.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { sello } from "sello";
|
|
7
|
+
|
|
8
|
+
const receipts = sello.service();
|
|
9
|
+
|
|
10
|
+
server.registerTool(
|
|
11
|
+
"calendar.create_event",
|
|
12
|
+
{ inputSchema: createEventInputSchema },
|
|
13
|
+
receipts.mcpTool("calendar.create_event", async (args) => {
|
|
14
|
+
const event = await calendar.events.create(args);
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: "text", text: event.id }],
|
|
17
|
+
};
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Some MCP SDK versions call this registration method `tool` instead of `registerTool`. Use the same callback slot either way.
|
|
23
|
+
|
|
24
|
+
## What It Does
|
|
25
|
+
|
|
26
|
+
`receipts.mcpTool(...)` returns a normal MCP tool callback. When the agent calls the tool, Sello:
|
|
27
|
+
|
|
28
|
+
1. Reads the agent authorization token from the MCP context.
|
|
29
|
+
2. Verifies the token before your callback runs.
|
|
30
|
+
3. Runs your callback unchanged.
|
|
31
|
+
4. Emits a `success`, `error`, or `denied` receipt.
|
|
32
|
+
5. Preserves the callback return value and rethrows callback errors.
|
|
33
|
+
|
|
34
|
+
The default receipt action type is:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
mcp.tools/call.<tool-name>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For `calendar.create_event`, the action type is:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
mcp.tools/call.calendar.create_event
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Token Source
|
|
47
|
+
|
|
48
|
+
By default, Sello looks for `Authorization: Bearer ...` in common MCP context shapes, including:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
context.headers.authorization
|
|
52
|
+
context.requestInfo.headers.authorization
|
|
53
|
+
context.request.headers.authorization
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Fetch-style `Headers` objects are supported too.
|
|
57
|
+
|
|
58
|
+
If your transport stores the token somewhere else, pass an extractor:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
receipts.mcpTool("calendar.create_event", handler, {
|
|
62
|
+
authorizationToken: ({ context }) => context.session.token,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The token should be the same authorization token the agent used for the tool call. Sello hashes the exact token bytes into `sello_token_ref`; do not parse and reserialize it first.
|
|
67
|
+
|
|
68
|
+
## What Gets Hashed
|
|
69
|
+
|
|
70
|
+
The default MCP input hash covers only the stable MCP action shape:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"method": "tools/call",
|
|
75
|
+
"params": {
|
|
76
|
+
"name": "calendar.create_event",
|
|
77
|
+
"arguments": {}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Sello does not hash:
|
|
83
|
+
|
|
84
|
+
- The bearer token.
|
|
85
|
+
- Transport headers.
|
|
86
|
+
- Connection/session objects.
|
|
87
|
+
- Other context that may vary across runtimes.
|
|
88
|
+
|
|
89
|
+
That keeps receipts stable without leaking secrets or transport details.
|
|
90
|
+
|
|
91
|
+
If you need a different hash boundary, pass `canonicalizeInput`.
|
|
92
|
+
|
|
93
|
+
## Unknown Tools
|
|
94
|
+
|
|
95
|
+
Only wrap tools you actually execute. If a request names an unknown tool, return your normal MCP `method not found` or `tool not found` error without emitting a Sello receipt.
|
|
96
|
+
|
|
97
|
+
The minimal example does this:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
if (request.body.params.name !== "calendar.create_event") {
|
|
101
|
+
return {
|
|
102
|
+
jsonrpc: "2.0",
|
|
103
|
+
id: request.body.id,
|
|
104
|
+
error: { code: -32601, message: "tool not found" },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## View Actions
|
|
110
|
+
|
|
111
|
+
In local development:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx sello dev
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Run your MCP tool call, then view verified actions:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx sello actions
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Or open:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
http://localhost:8787/actions
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For a specific agent token:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx sello actions --token <agent-token>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Example
|
|
136
|
+
|
|
137
|
+
See [`sdks/typescript/examples/mcp-minimal-server.ts`](../sdks/typescript/examples/mcp-minimal-server.ts) for a dependency-free MCP-shaped example. It wraps one `tools/call` handler and leaves unknown tools unreceipted.
|
|
@@ -16,6 +16,9 @@ Use this checklist before publishing a Sello release.
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
node --run test
|
|
19
|
+
node --run package:test
|
|
20
|
+
node --run package:a2a-test
|
|
21
|
+
node --run package:mcp-test
|
|
19
22
|
npm pack --dry-run
|
|
20
23
|
```
|
|
21
24
|
|
|
@@ -28,6 +31,8 @@ cd "$tmpdir/sello"
|
|
|
28
31
|
node -v # must be v22.7.0 or newer
|
|
29
32
|
node --run test
|
|
30
33
|
node --run package:test
|
|
34
|
+
node --run package:a2a-test
|
|
35
|
+
node --run package:mcp-test
|
|
31
36
|
npm pack --dry-run
|
|
32
37
|
node --experimental-strip-types sdks/typescript/src/cli/sello.ts --help
|
|
33
38
|
node --experimental-strip-types sdks/typescript/src/cli/sello.ts dev --dry-run
|
|
@@ -67,6 +72,8 @@ git push origin main --tags
|
|
|
67
72
|
After publishing:
|
|
68
73
|
|
|
69
74
|
- Confirm `npm view sello version` shows the new version.
|
|
75
|
+
- Confirm `SELLO_PACKAGE_SPEC=sello@$(node -p "require('./package.json').version") node --run package:a2a-test` passes against the published npm package.
|
|
76
|
+
- Confirm `SELLO_PACKAGE_SPEC=sello@$(node -p "require('./package.json').version") node --run package:mcp-test` passes against the published npm package.
|
|
70
77
|
- Publish a GitHub Release for the pushed tag so PyPI trusted publishing runs.
|
|
71
78
|
- Confirm `python -m pip index versions sello` shows the new version after PyPI publishing.
|
|
72
79
|
- Confirm `npx sello --help` works from a clean temp directory.
|
package/docs/sdk-quickstart.md
CHANGED
|
@@ -126,6 +126,30 @@ receipts.mcpTool("calendar.create_event", handler, {
|
|
|
126
126
|
});
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
For the complete MCP placement notes, see [MCP Integration](mcp.md).
|
|
130
|
+
|
|
131
|
+
For an A2A agent, wrap the message handler you already expose:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { sello } from "sello";
|
|
135
|
+
|
|
136
|
+
const receipts = sello.service();
|
|
137
|
+
|
|
138
|
+
export const sendMessage = receipts.a2aMessage(async (request, context) => {
|
|
139
|
+
return agent.handleMessage(request, context);
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`receipts.a2aMessage(...)` uses action type `a2a.<method>`, for example `a2a.message/send`, and hashes only the A2A JSON-RPC method and params. It excludes request ids, headers, bearer tokens, and runtime context. If your transport stores the token somewhere else, pass an extractor:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
receipts.a2aMessage(handler, {
|
|
147
|
+
authorizationToken: ({ context }) => context.session.token,
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For the complete A2A placement notes, see [A2A Integration](a2a.md).
|
|
152
|
+
|
|
129
153
|
Or run the matching Python example:
|
|
130
154
|
|
|
131
155
|
```bash
|
|
@@ -30,6 +30,7 @@ These notes cover the first Stripe-style SDK implementation pass. They are writt
|
|
|
30
30
|
- Success, error, and denied paths emit receipts without including plaintext request or response bodies.
|
|
31
31
|
- The wrapper uses the configured service identity and key for every receipt.
|
|
32
32
|
- `receipts.mcpTool(...)` reuses the same wrapper path. It defaults to hashing the MCP `tools/call` method, tool name, and arguments, while excluding bearer tokens and transport context from the action input hash.
|
|
33
|
+
- `receipts.a2aMessage(...)` reuses the same wrapper path. It defaults to hashing the A2A JSON-RPC method and params, while excluding bearer tokens, request ids, headers, and runtime context from the action input hash.
|
|
33
34
|
|
|
34
35
|
## Phase 4: Logs And Action Viewing
|
|
35
36
|
|
|
@@ -44,6 +45,7 @@ These notes cover the first Stripe-style SDK implementation pass. They are writt
|
|
|
44
45
|
- The example uses `submit: { mode: "await" }` so the command succeeds only after the receipt append completes.
|
|
45
46
|
- The example canonicalizes only tool input fields and excludes the authorization token wrapper from the action input hash.
|
|
46
47
|
- The MCP-style example reads the bearer token from the transport header, but hashes only the `tools/call` method and params.
|
|
48
|
+
- The A2A-style example reads the bearer token from the transport header, but hashes only the JSON-RPC method and params.
|
|
47
49
|
- README and quickstart docs keep self-hosting first-class and describe `sello.build` as optional convenience.
|
|
48
50
|
|
|
49
51
|
## Residual Risks
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sello",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Reference implementation of the Sello protocol for service-signed AI agent receipts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,10 +31,13 @@
|
|
|
31
31
|
"bench": "node --experimental-strip-types sdks/typescript/src/cli/bench.ts",
|
|
32
32
|
"demo": "node --experimental-strip-types sdks/typescript/src/cli/demo.ts",
|
|
33
33
|
"dev": "node --experimental-strip-types sdks/typescript/src/cli/sello.ts dev",
|
|
34
|
+
"example:a2a:minimal": "node --test --experimental-strip-types sdks/typescript/test/examples/a2a-minimal-server.test.ts",
|
|
34
35
|
"example:mcp:minimal": "node --test --experimental-strip-types sdks/typescript/test/examples/mcp-minimal-server.test.ts",
|
|
35
36
|
"example:mcp": "node --experimental-strip-types sdks/typescript/examples/mcp-tool-server.ts",
|
|
36
37
|
"example:tool": "node --experimental-strip-types sdks/typescript/examples/quickstart-tool.ts",
|
|
37
38
|
"build": "node --disable-warning=ExperimentalWarning sdks/typescript/scripts/build-dist.mjs",
|
|
39
|
+
"package:a2a-test": "node sdks/typescript/scripts/package-a2a-smoke.mjs",
|
|
40
|
+
"package:mcp-test": "node sdks/typescript/scripts/package-mcp-smoke.mjs",
|
|
38
41
|
"package:test": "node sdks/typescript/scripts/package-smoke.mjs",
|
|
39
42
|
"pack:check": "node sdks/typescript/scripts/pack-check.mjs",
|
|
40
43
|
"prepack": "node --disable-warning=ExperimentalWarning sdks/typescript/scripts/build-dist.mjs",
|
|
@@ -49,7 +52,8 @@
|
|
|
49
52
|
"transparency-log",
|
|
50
53
|
"cose",
|
|
51
54
|
"hpke",
|
|
52
|
-
"mcp"
|
|
55
|
+
"mcp",
|
|
56
|
+
"a2a"
|
|
53
57
|
],
|
|
54
58
|
"publishConfig": {
|
|
55
59
|
"access": "public"
|
package/sdks/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The protocol, paper, and project docs live at the repository root. Language SDKs live here:
|
|
4
4
|
|
|
5
|
-
- [`typescript/`](typescript/): npm package source, CLI, examples, fixtures, and Node tests.
|
|
5
|
+
- [`typescript/`](typescript/): npm package source, CLI, MCP/A2A helpers, examples, fixtures, and Node tests.
|
|
6
6
|
- [`python/`](python/): Python package source and tests.
|
|
7
7
|
|
|
8
8
|
Root commands still work from the repository top level:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { sello, type SelloReceipts } from "../src/index.ts";
|
|
2
|
+
|
|
3
|
+
export type MinimalA2aRequest = {
|
|
4
|
+
headers: {
|
|
5
|
+
authorization?: string;
|
|
6
|
+
Authorization?: string;
|
|
7
|
+
};
|
|
8
|
+
body: {
|
|
9
|
+
jsonrpc: "2.0";
|
|
10
|
+
id: string | number | null;
|
|
11
|
+
method: string;
|
|
12
|
+
params: {
|
|
13
|
+
message: {
|
|
14
|
+
role: "user" | "agent";
|
|
15
|
+
parts: { kind: "text"; text: string }[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type MinimalA2aResponse = {
|
|
22
|
+
jsonrpc: "2.0";
|
|
23
|
+
id: string | number | null;
|
|
24
|
+
result?: {
|
|
25
|
+
kind: "message";
|
|
26
|
+
messageId: string;
|
|
27
|
+
role: "agent";
|
|
28
|
+
parts: { kind: "text"; text: string }[];
|
|
29
|
+
};
|
|
30
|
+
error?: {
|
|
31
|
+
code: number;
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function createCalendarA2aAgent(
|
|
37
|
+
receipts: SelloReceipts = sello.service(),
|
|
38
|
+
) {
|
|
39
|
+
const sendMessage = receipts.a2aMessage<
|
|
40
|
+
MinimalA2aRequest["body"],
|
|
41
|
+
MinimalA2aResponse,
|
|
42
|
+
MinimalA2aRequest
|
|
43
|
+
>(async (body) => {
|
|
44
|
+
const text = body.params.message.parts
|
|
45
|
+
.filter((part) => part.kind === "text")
|
|
46
|
+
.map((part) => part.text)
|
|
47
|
+
.join(" ");
|
|
48
|
+
const title = text || "untitled";
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
jsonrpc: "2.0",
|
|
52
|
+
id: body.id,
|
|
53
|
+
result: {
|
|
54
|
+
kind: "message",
|
|
55
|
+
messageId: "calendar-reply-1",
|
|
56
|
+
role: "agent",
|
|
57
|
+
parts: [
|
|
58
|
+
{
|
|
59
|
+
kind: "text",
|
|
60
|
+
text: `created ${title}`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
async handle(request: MinimalA2aRequest): Promise<MinimalA2aResponse> {
|
|
69
|
+
if (request.body.method !== "message/send") {
|
|
70
|
+
return {
|
|
71
|
+
jsonrpc: "2.0",
|
|
72
|
+
id: request.body.id,
|
|
73
|
+
error: {
|
|
74
|
+
code: -32601,
|
|
75
|
+
message: "method not found",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return await sendMessage(request.body, request);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
flush: () => receipts.flush(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -200,6 +200,29 @@ async function devCommand(args: string[]): Promise<void> {
|
|
|
200
200
|
console.log(`Local dev log: ${logPath}`);
|
|
201
201
|
console.log(`Loaded ${devLogEntryCountLabel(loadedEntries)}.`);
|
|
202
202
|
});
|
|
203
|
+
server.on("error", (error) => {
|
|
204
|
+
if (isPortInUse(error)) {
|
|
205
|
+
console.error(`sello: port ${port} is already in use.`);
|
|
206
|
+
console.error("");
|
|
207
|
+
console.error("Try:");
|
|
208
|
+
console.error(` sello dev --port ${port + 1}`);
|
|
209
|
+
console.error("");
|
|
210
|
+
console.error("Or stop the process using that port:");
|
|
211
|
+
console.error(` lsof -nP -iTCP:${port} -sTCP:LISTEN`);
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (isListenError(error)) {
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
console.error(`sello: could not start dev server on port ${port}.`);
|
|
219
|
+
console.error(message);
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
throw error;
|
|
225
|
+
});
|
|
203
226
|
}
|
|
204
227
|
|
|
205
228
|
async function emitDemoCommand(args: string[]): Promise<void> {
|
|
@@ -258,14 +281,7 @@ async function callHttpDemoCommand(args: string[]): Promise<void> {
|
|
|
258
281
|
);
|
|
259
282
|
const url = readFlag(args, "--url") ?? "http://localhost:8790/calendar/events";
|
|
260
283
|
const title = readFlag(args, "--title") ?? "Ship Sello";
|
|
261
|
-
const response = await
|
|
262
|
-
method: "POST",
|
|
263
|
-
headers: {
|
|
264
|
-
authorization: `Bearer ${state.agentToken}`,
|
|
265
|
-
"content-type": "application/json",
|
|
266
|
-
},
|
|
267
|
-
body: JSON.stringify({ title }),
|
|
268
|
-
});
|
|
284
|
+
const response = await fetchHttpDemo(url, state.agentToken, title);
|
|
269
285
|
const responseText = await response.text();
|
|
270
286
|
|
|
271
287
|
if (!response.ok) {
|
|
@@ -284,6 +300,31 @@ async function callHttpDemoCommand(args: string[]): Promise<void> {
|
|
|
284
300
|
console.log(` ${actionViewerUrl(state)}`);
|
|
285
301
|
}
|
|
286
302
|
|
|
303
|
+
async function fetchHttpDemo(
|
|
304
|
+
url: string,
|
|
305
|
+
agentToken: string,
|
|
306
|
+
title: string,
|
|
307
|
+
): Promise<Response> {
|
|
308
|
+
try {
|
|
309
|
+
return await fetch(url, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: {
|
|
312
|
+
authorization: `Bearer ${agentToken}`,
|
|
313
|
+
"content-type": "application/json",
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify({ title }),
|
|
316
|
+
});
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (isFetchFailed(error)) {
|
|
319
|
+
throw new TypeError(
|
|
320
|
+
`could not reach the HTTP demo route at ${url}.\n\nStart it in another terminal:\n node sello-http-route.mjs`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
287
328
|
function initDemoCommand(args: string[]): void {
|
|
288
329
|
const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
|
|
289
330
|
const force = args.includes("--force");
|
|
@@ -788,6 +829,22 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
788
829
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
789
830
|
}
|
|
790
831
|
|
|
832
|
+
function isPortInUse(error: unknown): boolean {
|
|
833
|
+
return (
|
|
834
|
+
isRecord(error) &&
|
|
835
|
+
error.code === "EADDRINUSE" &&
|
|
836
|
+
error.syscall === "listen"
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function isListenError(error: unknown): boolean {
|
|
841
|
+
return isRecord(error) && error.syscall === "listen";
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function isFetchFailed(error: unknown): boolean {
|
|
845
|
+
return error instanceof TypeError && error.message === "fetch failed";
|
|
846
|
+
}
|
|
847
|
+
|
|
791
848
|
function printHelp(): void {
|
|
792
849
|
console.log(`Usage:
|
|
793
850
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
|
@@ -26,6 +26,8 @@ export type SelloValueOrGetter<Request, Value> =
|
|
|
26
26
|
| Value
|
|
27
27
|
| ((request: Request) => Value);
|
|
28
28
|
|
|
29
|
+
export type SelloActionType<Request> = string | ((request: Request) => string);
|
|
30
|
+
|
|
29
31
|
export type SelloReceiptEvent<Response = unknown> = {
|
|
30
32
|
resultStatus: ResultStatus;
|
|
31
33
|
receipt: BuiltReceipt;
|
|
@@ -72,6 +74,35 @@ export type SelloMcpToolOptions<Args, Response, Context = unknown> = {
|
|
|
72
74
|
) => Response | Promise<Response>;
|
|
73
75
|
};
|
|
74
76
|
|
|
77
|
+
export type SelloA2aMessageInvocation<Request, Context = unknown> = {
|
|
78
|
+
request: Request;
|
|
79
|
+
context: Context;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type SelloA2aMessageHandler<Request, Response, Context = unknown> = (
|
|
83
|
+
request: Request,
|
|
84
|
+
context: Context,
|
|
85
|
+
) => Response | Promise<Response>;
|
|
86
|
+
|
|
87
|
+
export type SelloA2aMessageOptions<Request, Response, Context = unknown> = {
|
|
88
|
+
actionType?: SelloActionType<SelloA2aMessageInvocation<Request, Context>>;
|
|
89
|
+
authorizationToken?: SelloValueOrGetter<
|
|
90
|
+
SelloA2aMessageInvocation<Request, Context>,
|
|
91
|
+
string | Uint8Array
|
|
92
|
+
>;
|
|
93
|
+
canonicalizeInput?: (
|
|
94
|
+
invocation: SelloA2aMessageInvocation<Request, Context>,
|
|
95
|
+
) => Uint8Array;
|
|
96
|
+
canonicalizeOutput?: (response: Response) => Uint8Array;
|
|
97
|
+
canonicalizeError?: (error: unknown) => Uint8Array;
|
|
98
|
+
isDenied?: (
|
|
99
|
+
invocation: SelloA2aMessageInvocation<Request, Context>,
|
|
100
|
+
) => boolean | Promise<boolean>;
|
|
101
|
+
deniedResponse?: (
|
|
102
|
+
invocation: SelloA2aMessageInvocation<Request, Context>,
|
|
103
|
+
) => Response | Promise<Response>;
|
|
104
|
+
};
|
|
105
|
+
|
|
75
106
|
export type SelloServiceConfig = {
|
|
76
107
|
service?: string;
|
|
77
108
|
serviceKey?: ServiceKeyInput;
|
|
@@ -101,6 +132,10 @@ export type SelloReceipts = {
|
|
|
101
132
|
handler: SelloMcpToolHandler<Args, Response, Context>,
|
|
102
133
|
options?: SelloMcpToolOptions<Args, Response, Context>,
|
|
103
134
|
): SelloMcpToolHandler<Args, Response, Context>;
|
|
135
|
+
a2aMessage<Request, Response, Context = unknown>(
|
|
136
|
+
handler: SelloA2aMessageHandler<Request, Response, Context>,
|
|
137
|
+
options?: SelloA2aMessageOptions<Request, Response, Context>,
|
|
138
|
+
): SelloA2aMessageHandler<Request, Response, Context>;
|
|
104
139
|
flush(): Promise<void>;
|
|
105
140
|
};
|
|
106
141
|
|
|
@@ -151,11 +186,14 @@ export function createSelloService(input?: SelloServiceInput): SelloReceipts {
|
|
|
151
186
|
}
|
|
152
187
|
|
|
153
188
|
function wrapTool<Request, Response>(
|
|
154
|
-
actionType:
|
|
189
|
+
actionType: SelloActionType<Request>,
|
|
155
190
|
handler: SelloToolHandler<Request, Response>,
|
|
156
191
|
options: SelloToolOptions<Request, Response> = {},
|
|
157
192
|
): SelloToolHandler<Request, Response> {
|
|
158
|
-
if (
|
|
193
|
+
if (
|
|
194
|
+
(typeof actionType === "string" && actionType.length === 0) ||
|
|
195
|
+
(typeof actionType !== "string" && typeof actionType !== "function")
|
|
196
|
+
) {
|
|
159
197
|
throw new TypeError("Sello action type must be a non-empty string");
|
|
160
198
|
}
|
|
161
199
|
|
|
@@ -175,6 +213,7 @@ export function createSelloService(input?: SelloServiceInput): SelloReceipts {
|
|
|
175
213
|
resolved.fallbackSelloLogs,
|
|
176
214
|
resolved.log.logUrl,
|
|
177
215
|
);
|
|
216
|
+
const resolvedActionType = resolveActionType(actionType, request);
|
|
178
217
|
const base = {
|
|
179
218
|
authorizationTokenBytes: verifiedToken.authorizationTokenBytes,
|
|
180
219
|
ownerHpkePublicKey: verifiedToken.ownerHpkePublicKey,
|
|
@@ -183,7 +222,7 @@ export function createSelloService(input?: SelloServiceInput): SelloReceipts {
|
|
|
183
222
|
servicePrivateKey: resolved.servicePrivateKey,
|
|
184
223
|
serviceIdentifier: resolved.service,
|
|
185
224
|
logUrl: resolved.log.logUrl,
|
|
186
|
-
actionType,
|
|
225
|
+
actionType: resolvedActionType,
|
|
187
226
|
actionInputBytes: (options.canonicalizeInput ?? canonicalJsonBytes)(request),
|
|
188
227
|
timestamp: resolved.now(),
|
|
189
228
|
};
|
|
@@ -272,6 +311,32 @@ export function createSelloService(input?: SelloServiceInput): SelloReceipts {
|
|
|
272
311
|
return await wrapped({ name, arguments: args, context });
|
|
273
312
|
};
|
|
274
313
|
},
|
|
314
|
+
a2aMessage<Request, Response, Context = unknown>(
|
|
315
|
+
handler: SelloA2aMessageHandler<Request, Response, Context>,
|
|
316
|
+
options: SelloA2aMessageOptions<Request, Response, Context> = {},
|
|
317
|
+
): SelloA2aMessageHandler<Request, Response, Context> {
|
|
318
|
+
const wrapped = wrapTool<
|
|
319
|
+
SelloA2aMessageInvocation<Request, Context>,
|
|
320
|
+
Response
|
|
321
|
+
>(
|
|
322
|
+
options.actionType ?? defaultA2aActionType,
|
|
323
|
+
async (invocation) => handler(invocation.request, invocation.context),
|
|
324
|
+
{
|
|
325
|
+
authorizationToken:
|
|
326
|
+
options.authorizationToken ?? defaultA2aAuthorizationToken,
|
|
327
|
+
canonicalizeInput:
|
|
328
|
+
options.canonicalizeInput ?? defaultA2aCanonicalizeInput,
|
|
329
|
+
canonicalizeOutput: options.canonicalizeOutput,
|
|
330
|
+
canonicalizeError: options.canonicalizeError,
|
|
331
|
+
isDenied: options.isDenied,
|
|
332
|
+
deniedResponse: options.deniedResponse,
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
return async (request: Request, context: Context): Promise<Response> => {
|
|
337
|
+
return await wrapped({ request, context });
|
|
338
|
+
};
|
|
339
|
+
},
|
|
275
340
|
async flush(): Promise<void> {
|
|
276
341
|
await publisher?.flush();
|
|
277
342
|
},
|
|
@@ -588,6 +653,53 @@ function defaultMcpCanonicalizeInput(
|
|
|
588
653
|
});
|
|
589
654
|
}
|
|
590
655
|
|
|
656
|
+
function defaultA2aAuthorizationToken(
|
|
657
|
+
invocation: SelloA2aMessageInvocation<unknown, unknown>,
|
|
658
|
+
): string | Uint8Array {
|
|
659
|
+
const fromContext = authorizationTokenFromUnknown(invocation.context);
|
|
660
|
+
if (fromContext) {
|
|
661
|
+
return fromContext;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const fromRequest = authorizationTokenFromUnknown(invocation.request);
|
|
665
|
+
if (fromRequest) {
|
|
666
|
+
return fromRequest;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
throw new TypeError(
|
|
670
|
+
"Sello A2A authorization token not found. Pass authorizationToken or include an Authorization header in the A2A context.",
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function defaultA2aActionType(
|
|
675
|
+
invocation: SelloA2aMessageInvocation<unknown, unknown>,
|
|
676
|
+
): string {
|
|
677
|
+
const method = methodFromA2aRequest(invocation.request);
|
|
678
|
+
return method ? `a2a.${method}` : "a2a.message/send";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function defaultA2aCanonicalizeInput(
|
|
682
|
+
invocation: SelloA2aMessageInvocation<unknown, unknown>,
|
|
683
|
+
): Uint8Array {
|
|
684
|
+
if (isRecord(invocation.request) && typeof invocation.request.method === "string") {
|
|
685
|
+
return canonicalJsonBytes({
|
|
686
|
+
method: invocation.request.method,
|
|
687
|
+
params: invocation.request.params ?? null,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return canonicalJsonBytes(invocation.request);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function methodFromA2aRequest(request: unknown): string | undefined {
|
|
695
|
+
if (!isRecord(request)) {
|
|
696
|
+
return undefined;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const method = request.method;
|
|
700
|
+
return typeof method === "string" && method.length > 0 ? method : undefined;
|
|
701
|
+
}
|
|
702
|
+
|
|
591
703
|
function authorizationTokenFromUnknown(value: unknown): string | Uint8Array | undefined {
|
|
592
704
|
if (!isRecord(value)) {
|
|
593
705
|
return undefined;
|
|
@@ -693,6 +805,20 @@ function resolveValue<Request, Value>(
|
|
|
693
805
|
return value;
|
|
694
806
|
}
|
|
695
807
|
|
|
808
|
+
function resolveActionType<Request>(
|
|
809
|
+
actionType: SelloActionType<Request>,
|
|
810
|
+
request: Request,
|
|
811
|
+
): string {
|
|
812
|
+
const resolved =
|
|
813
|
+
typeof actionType === "function" ? actionType(request) : actionType;
|
|
814
|
+
|
|
815
|
+
if (typeof resolved !== "string" || resolved.length === 0) {
|
|
816
|
+
throw new TypeError("Sello action type must be a non-empty string");
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return resolved;
|
|
820
|
+
}
|
|
821
|
+
|
|
696
822
|
function envSubmitMode(env: Environment): SubmitMode | SelloSubmitOptions | undefined {
|
|
697
823
|
const mode = env.SELLO_SUBMIT_MODE;
|
|
698
824
|
if (mode === undefined) {
|