sello 0.1.15 → 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 CHANGED
@@ -16,16 +16,10 @@
16
16
  <p align="center">
17
17
  <a href="#try-it">Quickstart</a> &middot;
18
18
  <a href="#add-sello-to-a-tool">Add Sello</a> &middot;
19
- <a href="#add-sello-to-an-mcp-server">MCP</a> &middot;
19
+ <a href="docs/mcp.md">MCP</a> &middot;
20
20
  <a href="sdks/README.md">SDKs</a> &middot;
21
- <a href="#see-logged-actions">Actions</a> &middot;
22
- <a href="#how-it-works">How It Works</a> &middot;
23
- <a href="#learn-more">Learn More</a> &middot;
24
21
  <a href="SPEC.md">Protocol</a> &middot;
25
- <a href="https://arxiv.org/abs/2606.04193">Paper</a> &middot;
26
- <a href="SECURITY.md">Security</a> &middot;
27
- <a href="CONTRIBUTING.md">Contributing</a> &middot;
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
 
@@ -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 (typeof actionType !== "string" || actionType.length === 0) {
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.
@@ -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.15",
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
+ }
@@ -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: string,
189
+ actionType: SelloActionType<Request>,
155
190
  handler: SelloToolHandler<Request, Response>,
156
191
  options: SelloToolOptions<Request, Response> = {},
157
192
  ): SelloToolHandler<Request, Response> {
158
- if (typeof actionType !== "string" || actionType.length === 0) {
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) {