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 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.
@@ -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 emitter or HTTP route:
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(" npx sello dev");
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("Then open http://localhost:8787/actions");
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(" npx sello dev");
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("Then open http://localhost:8787/actions");
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
+ }
@@ -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,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.