sello 0.1.2 → 0.1.4

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
@@ -15,6 +15,33 @@ agent calls service
15
15
  -> owner fetches, verifies, decrypts
16
16
  ```
17
17
 
18
+ ## Try It
19
+
20
+ Requires Node.js 22.7 or newer.
21
+
22
+ From a new project or temporary folder:
23
+
24
+ ```bash
25
+ # Terminal 1
26
+ npx --yes sello dev
27
+
28
+ # Terminal 2
29
+ npx --yes sello emit-demo
30
+ npx --yes sello actions
31
+ ```
32
+
33
+ Then open:
34
+
35
+ ```text
36
+ http://localhost:8787/actions
37
+ ```
38
+
39
+ To see the tiny wrapped-tool source:
40
+
41
+ ```bash
42
+ npx --yes sello init-demo
43
+ ```
44
+
18
45
  ## Why Sello?
19
46
 
20
47
  Most agent logs are written by the same system whose behavior they describe. If the agent, runtime, or operator is compromised, those logs can be incomplete or false.
@@ -48,13 +75,13 @@ The implementation includes a local end-to-end demo, compact JWS token verificat
48
75
 
49
76
  ## Start Here
50
77
 
51
- Sello currently requires Node.js 22.7 or newer.
52
-
53
78
  | Goal | Read |
54
79
  |------|------|
55
80
  | Add Sello in a few lines | [SDK Quickstart](docs/sdk-quickstart.md) |
81
+ | Emit your first receipt | `npx --yes sello dev`, then `npx --yes sello emit-demo` |
56
82
  | Try a wrapped tool locally | `node --run dev`, then `node --run example:tool` |
57
83
  | Try an MCP-style tool call | `node --run dev`, then `node --run example:mcp` |
84
+ | See a minimal MCP integration | [examples/mcp-minimal-server.ts](examples/mcp-minimal-server.ts) |
58
85
  | Understand the protocol | [SPEC.md](SPEC.md) Quick Start |
59
86
  | Run the local demo | `node --run demo` |
60
87
  | Run the test suite | `node --run test` |
@@ -81,6 +108,14 @@ Then inspect verified actions:
81
108
  npx sello actions --token <agent-token>
82
109
  ```
83
110
 
111
+ In local dev, `sello dev` prints and saves a demo token:
112
+
113
+ ```bash
114
+ SELLO_ACTION_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
115
+ ```
116
+
117
+ You usually do not need to copy it. `npx sello actions` reads the latest dev token from `.sello/dev.json`. Use `--token` when you want to inspect receipts for a specific agent authorization token from your own system.
118
+
84
119
  Sello works with your own log server. `sello.build` is optional convenience, not a protocol requirement.
85
120
 
86
121
  To try the repo's local loop:
@@ -98,6 +133,8 @@ The example wraps a fake calendar tool, emits a service-signed encrypted receipt
98
133
 
99
134
  For an MCP-shaped `tools/call` boundary, run `node --run example:mcp` instead of `node --run example:tool`.
100
135
 
136
+ For a smaller production-shaped MCP example, see [examples/mcp-minimal-server.ts](examples/mcp-minimal-server.ts). It wraps one `tools/call` handler with `sello.service()` and leaves unknown tools unreceipted.
137
+
101
138
  ## The First 10 Minutes
102
139
 
103
140
  If you are implementing Sello, start with one local loop:
@@ -180,18 +217,10 @@ Much of this prior work surfaced after Sello's design had already converged on s
180
217
  - Use verifiable log integrated time for revocation decisions, not the receipt timestamp.
181
218
  - Deduplicate only on the full spec key, including action type and input/output hashes.
182
219
 
183
- ## License
184
-
185
- Apache 2.0. See [LICENSE](LICENSE).
186
-
187
220
  ## Feedback
188
221
 
189
222
  Issues and pull requests are welcome. This is an early draft; adversarial review is the point.
190
223
 
191
- ## Development
192
-
193
- The current dependency-free test command is:
224
+ ## License
194
225
 
195
- ```bash
196
- node --run test
197
- ```
226
+ Apache 2.0. See [LICENSE](LICENSE).
package/dist/cli/sello.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createServer, } from "node:http";
4
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
 
7
7
  import { generateEd25519KeyPair } from "../cose/sign1.js";
8
8
  import { deriveTokenIdentifiers, sha256, toHex } from "../crypto/identifiers.js";
9
9
  import { generateHpkeKeyPair } from "../hpke/receipt.js";
10
10
  import { MockTransparencyLog } from "../log/mock-log.js";
11
+ import { canonicalJsonBytes } from "../mcp/middleware.js";
11
12
  import { verifyReceipts } from "../owner/verify.js";
12
13
  import {
13
14
  loadSignedRegistry,
@@ -25,10 +26,12 @@ import {
25
26
  } from "../sdk/keys.js";
26
27
  import {
27
28
  deserializeEntry,
29
+ http as httpLog,
28
30
  queryHttpLogByTokenRef,
29
31
  serializeEntry,
30
32
  toCanonicalLogUrl,
31
33
  } from "../sdk/logs.js";
34
+ import { createSelloService } from "../sdk/service.js";
32
35
 
33
36
 
34
37
 
@@ -56,6 +59,12 @@ try {
56
59
  case "dev":
57
60
  await devCommand(process.argv.slice(3));
58
61
  break;
62
+ case "emit-demo":
63
+ await emitDemoCommand(process.argv.slice(3));
64
+ break;
65
+ case "init-demo":
66
+ initDemoCommand(process.argv.slice(3));
67
+ break;
59
68
  case "keys":
60
69
  keysCommand(process.argv.slice(3));
61
70
  break;
@@ -156,6 +165,75 @@ async function devCommand(args ) {
156
165
  });
157
166
  }
158
167
 
168
+ async function emitDemoCommand(args ) {
169
+ const state = loadDevStateOrThrow(
170
+ "missing local Sello dev state. Run `sello dev` first, then run `sello emit-demo` in another terminal from the same directory.",
171
+ );
172
+ const title = readFlag(args, "--title") ?? "Test Sello receipt";
173
+ const receipts = createSelloService({
174
+ service: state.serviceId,
175
+ serviceKey: state.serviceKey,
176
+ tokenIssuer: state.tokenIssuerPublicKey,
177
+ log: httpLog(state.logUrl, { endpoint: state.logEndpoint }),
178
+ submit: { mode: "await" },
179
+ });
180
+ const createEvent = receipts.tool (
181
+ "calendar.create_event",
182
+ async (request) => ({
183
+ id: `evt_${slug(request.title)}`,
184
+ calendarId: request.calendarId,
185
+ title: request.title,
186
+ status: "created",
187
+ createdAt: new Date().toISOString(),
188
+ }),
189
+ {
190
+ canonicalizeInput: (request) => canonicalJsonBytes({
191
+ calendarId: request.calendarId,
192
+ title: request.title,
193
+ start: request.start,
194
+ attendees: request.attendees,
195
+ }),
196
+ },
197
+ );
198
+ const response = await createEvent({
199
+ authorizationToken: state.agentToken,
200
+ calendarId: "demo-calendar",
201
+ title,
202
+ start: "2026-06-05T17:00:00Z",
203
+ attendees: ["ada@example.com", "grace@example.com"],
204
+ });
205
+
206
+ await receipts.flush();
207
+
208
+ console.log("Emitted demo Sello receipt.");
209
+ console.log(JSON.stringify(response, null, 2));
210
+ console.log("");
211
+ console.log("View verified actions with:");
212
+ console.log(" sello actions");
213
+ console.log("");
214
+ console.log("Or open:");
215
+ console.log(` ${actionViewerUrl(state)}`);
216
+ }
217
+
218
+ function initDemoCommand(args ) {
219
+ const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
220
+ const force = args.includes("--force");
221
+
222
+ if (existsSync(output) && !force) {
223
+ throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
224
+ }
225
+
226
+ writeFileSync(output, demoEmitterSource(), { mode: 0o644 });
227
+
228
+ console.log(`Created ${output}`);
229
+ console.log("");
230
+ console.log("Run:");
231
+ console.log(" npm install sello");
232
+ console.log(" npx sello dev");
233
+ console.log(` node ${output}`);
234
+ console.log(" npx sello actions");
235
+ }
236
+
159
237
  function keysCommand(args ) {
160
238
  const subcommand = args[0] ?? "service";
161
239
  if (subcommand !== "service") {
@@ -338,6 +416,14 @@ function loadDevStateIfPresent() {
338
416
  }
339
417
  }
340
418
 
419
+ function loadDevStateOrThrow(message ) {
420
+ const state = loadDevStateIfPresent();
421
+ if (!state) {
422
+ throw new TypeError(message);
423
+ }
424
+ return state;
425
+ }
426
+
341
427
  function devStatePath() {
342
428
  return join(process.cwd(), ".sello", "dev.json");
343
429
  }
@@ -477,6 +563,8 @@ function isRecord(value ) {
477
563
  function printHelp() {
478
564
  console.log(`Usage:
479
565
  sello dev [--port 8787] [--service service-id] [--dry-run]
566
+ sello emit-demo [--title title]
567
+ sello init-demo [--output emit-receipt.mjs] [--force]
480
568
  sello actions [--token agent-token]
481
569
  sello keys service
482
570
  sello inspect-env
@@ -513,3 +601,79 @@ function enforceNodeVersion() {
513
601
  );
514
602
  }
515
603
  }
604
+
605
+
606
+
607
+
608
+
609
+
610
+
611
+
612
+
613
+
614
+
615
+
616
+
617
+
618
+
619
+
620
+
621
+ function actionViewerUrl(state ) {
622
+ const endpoint = new URL(state.logEndpoint);
623
+ return `${endpoint.origin}/actions`;
624
+ }
625
+
626
+ function slug(value ) {
627
+ return value
628
+ .toLowerCase()
629
+ .replace(/[^a-z0-9]+/g, "_")
630
+ .replace(/^_+|_+$/g, "")
631
+ .slice(0, 40);
632
+ }
633
+
634
+ function demoEmitterSource() {
635
+ return `import { readFileSync } from "node:fs";
636
+ import { canonicalJsonBytes, sello } from "sello";
637
+
638
+ const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
639
+
640
+ const receipts = sello.service({
641
+ service: state.serviceId,
642
+ serviceKey: state.serviceKey,
643
+ tokenIssuer: state.tokenIssuerPublicKey,
644
+ log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
645
+ submit: { mode: "await" },
646
+ });
647
+
648
+ const createEvent = receipts.tool(
649
+ "calendar.create_event",
650
+ async (request) => ({
651
+ id: "evt_test_sello_receipt",
652
+ calendarId: request.calendarId,
653
+ title: request.title,
654
+ status: "created",
655
+ createdAt: new Date().toISOString(),
656
+ }),
657
+ {
658
+ canonicalizeInput: (request) =>
659
+ canonicalJsonBytes({
660
+ calendarId: request.calendarId,
661
+ title: request.title,
662
+ start: request.start,
663
+ attendees: request.attendees,
664
+ }),
665
+ },
666
+ );
667
+
668
+ const result = await createEvent({
669
+ authorizationToken: state.agentToken,
670
+ calendarId: "demo-calendar",
671
+ title: "Test Sello receipt",
672
+ start: "2026-06-05T17:00:00Z",
673
+ attendees: ["ada@example.com", "grace@example.com"],
674
+ });
675
+
676
+ await receipts.flush();
677
+ console.log(result);
678
+ `;
679
+ }
@@ -18,6 +18,23 @@ The service process emits receipts. It does not need the owner private key.
18
18
 
19
19
  ## Local Development
20
20
 
21
+ From a new project, the shortest loop is:
22
+
23
+ ```bash
24
+ # Terminal 1
25
+ npx --yes sello dev
26
+
27
+ # Terminal 2
28
+ npx --yes sello emit-demo
29
+ npx --yes sello actions
30
+ ```
31
+
32
+ To write the tiny emitter file into your project:
33
+
34
+ ```bash
35
+ npx --yes sello init-demo
36
+ ```
37
+
21
38
  Inside this repo, start the local log and action viewer:
22
39
 
23
40
  ```bash
@@ -46,6 +63,8 @@ http://localhost:8787/actions
46
63
 
47
64
  Both examples read `.sello/dev.json`, wrap a fake calendar handler, submit one encrypted receipt, and let the owner verify it locally.
48
65
 
66
+ For the smallest production-shaped MCP boundary, see [`examples/mcp-minimal-server.ts`](../examples/mcp-minimal-server.ts). It wraps one `tools/call` handler with `sello.service()` and leaves unknown tools unreceipted.
67
+
49
68
  For your own tool server, copy the printed service env:
50
69
 
51
70
  ```bash
@@ -63,6 +82,14 @@ Call your wrapped tool with the printed dev token. Then view actions:
63
82
  npx sello actions
64
83
  ```
65
84
 
85
+ In local dev, `sello dev` prints and saves the token:
86
+
87
+ ```bash
88
+ SELLO_ACTION_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
89
+ ```
90
+
91
+ `npx sello actions` reads that token from `.sello/dev.json`. Pass `--token` only when you want to inspect receipts for a specific agent authorization token.
92
+
66
93
  ## Self-Hosted Production
67
94
 
68
95
  Use your own Sello-compatible log server:
@@ -0,0 +1,95 @@
1
+ import {
2
+ canonicalJsonBytes,
3
+ sello,
4
+ type SelloReceipts,
5
+ } from "../src/index.ts";
6
+
7
+ export type MinimalMcpRequest = {
8
+ headers: {
9
+ authorization?: string;
10
+ Authorization?: string;
11
+ };
12
+ body: {
13
+ jsonrpc: "2.0";
14
+ id: string | number | null;
15
+ method: "tools/call";
16
+ params: {
17
+ name: string;
18
+ arguments: Record<string, unknown>;
19
+ };
20
+ };
21
+ };
22
+
23
+ export type MinimalMcpResponse = {
24
+ jsonrpc: "2.0";
25
+ id: string | number | null;
26
+ result?: {
27
+ content: { type: "text"; text: string }[];
28
+ };
29
+ error?: {
30
+ code: number;
31
+ message: string;
32
+ };
33
+ };
34
+
35
+ export function createCalendarMcpServer(
36
+ receipts: SelloReceipts = sello.service(),
37
+ ) {
38
+ const createEvent = receipts.tool<MinimalMcpRequest, MinimalMcpResponse>(
39
+ "mcp.tools/call.calendar.create_event",
40
+ async (request) => {
41
+ const args = request.body.params.arguments;
42
+ const title = readString(args.title, "title");
43
+
44
+ return {
45
+ jsonrpc: "2.0",
46
+ id: request.body.id,
47
+ result: {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: `created ${title}`,
52
+ },
53
+ ],
54
+ },
55
+ };
56
+ },
57
+ {
58
+ canonicalizeInput: (request) => canonicalJsonBytes({
59
+ method: request.body.method,
60
+ params: request.body.params,
61
+ }),
62
+ canonicalizeOutput: (response) => canonicalJsonBytes(response),
63
+ },
64
+ );
65
+
66
+ return {
67
+ async handle(request: MinimalMcpRequest): Promise<MinimalMcpResponse> {
68
+ if (
69
+ request.body.method !== "tools/call" ||
70
+ request.body.params.name !== "calendar.create_event"
71
+ ) {
72
+ return {
73
+ jsonrpc: "2.0",
74
+ id: request.body.id,
75
+ error: {
76
+ code: -32601,
77
+ message: "tool not found",
78
+ },
79
+ };
80
+ }
81
+
82
+ return await createEvent(request);
83
+ },
84
+
85
+ flush: () => receipts.flush(),
86
+ };
87
+ }
88
+
89
+ function readString(value: unknown, name: string): string {
90
+ if (typeof value !== "string" || value.length === 0) {
91
+ throw new TypeError(`${name} must be a non-empty string`);
92
+ }
93
+
94
+ return value;
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sello",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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,6 +31,7 @@
31
31
  "bench": "node --experimental-strip-types src/cli/bench.ts",
32
32
  "demo": "node --experimental-strip-types src/cli/demo.ts",
33
33
  "dev": "node --experimental-strip-types src/cli/sello.ts dev",
34
+ "example:mcp:minimal": "node --test --experimental-strip-types test/examples/mcp-minimal-server.test.ts",
34
35
  "example:mcp": "node --experimental-strip-types examples/mcp-tool-server.ts",
35
36
  "example:tool": "node --experimental-strip-types examples/quickstart-tool.ts",
36
37
  "build": "node --disable-warning=ExperimentalWarning scripts/build-dist.mjs",
package/src/cli/sello.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env -S node --experimental-strip-types
2
2
 
3
3
  import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
4
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
 
7
7
  import { generateEd25519KeyPair } from "../cose/sign1.ts";
@@ -9,6 +9,7 @@ import { deriveTokenIdentifiers, sha256, toHex } from "../crypto/identifiers.ts"
9
9
  import { generateHpkeKeyPair } from "../hpke/receipt.ts";
10
10
  import { type CanonicalLogUrl } from "../log/canonical-url.ts";
11
11
  import { MockTransparencyLog } from "../log/mock-log.ts";
12
+ import { canonicalJsonBytes } from "../mcp/middleware.ts";
12
13
  import { verifyReceipts } from "../owner/verify.ts";
13
14
  import {
14
15
  loadSignedRegistry,
@@ -26,10 +27,12 @@ import {
26
27
  } from "../sdk/keys.ts";
27
28
  import {
28
29
  deserializeEntry,
30
+ http as httpLog,
29
31
  queryHttpLogByTokenRef,
30
32
  serializeEntry,
31
33
  toCanonicalLogUrl,
32
34
  } from "../sdk/logs.ts";
35
+ import { createSelloService } from "../sdk/service.ts";
33
36
 
34
37
  type DevState = {
35
38
  serviceId: string;
@@ -57,6 +60,12 @@ try {
57
60
  case "dev":
58
61
  await devCommand(process.argv.slice(3));
59
62
  break;
63
+ case "emit-demo":
64
+ await emitDemoCommand(process.argv.slice(3));
65
+ break;
66
+ case "init-demo":
67
+ initDemoCommand(process.argv.slice(3));
68
+ break;
60
69
  case "keys":
61
70
  keysCommand(process.argv.slice(3));
62
71
  break;
@@ -157,6 +166,75 @@ async function devCommand(args: string[]): Promise<void> {
157
166
  });
158
167
  }
159
168
 
169
+ async function emitDemoCommand(args: string[]): Promise<void> {
170
+ const state = loadDevStateOrThrow(
171
+ "missing local Sello dev state. Run `sello dev` first, then run `sello emit-demo` in another terminal from the same directory.",
172
+ );
173
+ const title = readFlag(args, "--title") ?? "Test Sello receipt";
174
+ const receipts = createSelloService({
175
+ service: state.serviceId,
176
+ serviceKey: state.serviceKey,
177
+ tokenIssuer: state.tokenIssuerPublicKey,
178
+ log: httpLog(state.logUrl, { endpoint: state.logEndpoint }),
179
+ submit: { mode: "await" },
180
+ });
181
+ const createEvent = receipts.tool<DemoEventRequest, DemoEventResponse>(
182
+ "calendar.create_event",
183
+ async (request) => ({
184
+ id: `evt_${slug(request.title)}`,
185
+ calendarId: request.calendarId,
186
+ title: request.title,
187
+ status: "created",
188
+ createdAt: new Date().toISOString(),
189
+ }),
190
+ {
191
+ canonicalizeInput: (request) => canonicalJsonBytes({
192
+ calendarId: request.calendarId,
193
+ title: request.title,
194
+ start: request.start,
195
+ attendees: request.attendees,
196
+ }),
197
+ },
198
+ );
199
+ const response = await createEvent({
200
+ authorizationToken: state.agentToken,
201
+ calendarId: "demo-calendar",
202
+ title,
203
+ start: "2026-06-05T17:00:00Z",
204
+ attendees: ["ada@example.com", "grace@example.com"],
205
+ });
206
+
207
+ await receipts.flush();
208
+
209
+ console.log("Emitted demo Sello receipt.");
210
+ console.log(JSON.stringify(response, null, 2));
211
+ console.log("");
212
+ console.log("View verified actions with:");
213
+ console.log(" sello actions");
214
+ console.log("");
215
+ console.log("Or open:");
216
+ console.log(` ${actionViewerUrl(state)}`);
217
+ }
218
+
219
+ function initDemoCommand(args: string[]): void {
220
+ const output = readFlag(args, "--output") ?? "emit-receipt.mjs";
221
+ const force = args.includes("--force");
222
+
223
+ if (existsSync(output) && !force) {
224
+ throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
225
+ }
226
+
227
+ writeFileSync(output, demoEmitterSource(), { mode: 0o644 });
228
+
229
+ console.log(`Created ${output}`);
230
+ console.log("");
231
+ console.log("Run:");
232
+ console.log(" npm install sello");
233
+ console.log(" npx sello dev");
234
+ console.log(` node ${output}`);
235
+ console.log(" npx sello actions");
236
+ }
237
+
160
238
  function keysCommand(args: string[]): void {
161
239
  const subcommand = args[0] ?? "service";
162
240
  if (subcommand !== "service") {
@@ -339,6 +417,14 @@ function loadDevStateIfPresent(): DevState | undefined {
339
417
  }
340
418
  }
341
419
 
420
+ function loadDevStateOrThrow(message: string): DevState {
421
+ const state = loadDevStateIfPresent();
422
+ if (!state) {
423
+ throw new TypeError(message);
424
+ }
425
+ return state;
426
+ }
427
+
342
428
  function devStatePath(): string {
343
429
  return join(process.cwd(), ".sello", "dev.json");
344
430
  }
@@ -478,6 +564,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
478
564
  function printHelp(): void {
479
565
  console.log(`Usage:
480
566
  sello dev [--port 8787] [--service service-id] [--dry-run]
567
+ sello emit-demo [--title title]
568
+ sello init-demo [--output emit-receipt.mjs] [--force]
481
569
  sello actions [--token agent-token]
482
570
  sello keys service
483
571
  sello inspect-env
@@ -514,3 +602,79 @@ function enforceNodeVersion(): void {
514
602
  );
515
603
  }
516
604
  }
605
+
606
+ type DemoEventRequest = {
607
+ authorizationToken: string;
608
+ calendarId: string;
609
+ title: string;
610
+ start: string;
611
+ attendees: string[];
612
+ };
613
+
614
+ type DemoEventResponse = {
615
+ id: string;
616
+ calendarId: string;
617
+ title: string;
618
+ status: "created";
619
+ createdAt: string;
620
+ };
621
+
622
+ function actionViewerUrl(state: DevState): string {
623
+ const endpoint = new URL(state.logEndpoint);
624
+ return `${endpoint.origin}/actions`;
625
+ }
626
+
627
+ function slug(value: string): string {
628
+ return value
629
+ .toLowerCase()
630
+ .replace(/[^a-z0-9]+/g, "_")
631
+ .replace(/^_+|_+$/g, "")
632
+ .slice(0, 40);
633
+ }
634
+
635
+ function demoEmitterSource(): string {
636
+ return `import { readFileSync } from "node:fs";
637
+ import { canonicalJsonBytes, sello } from "sello";
638
+
639
+ const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
640
+
641
+ const receipts = sello.service({
642
+ service: state.serviceId,
643
+ serviceKey: state.serviceKey,
644
+ tokenIssuer: state.tokenIssuerPublicKey,
645
+ log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
646
+ submit: { mode: "await" },
647
+ });
648
+
649
+ const createEvent = receipts.tool(
650
+ "calendar.create_event",
651
+ async (request) => ({
652
+ id: "evt_test_sello_receipt",
653
+ calendarId: request.calendarId,
654
+ title: request.title,
655
+ status: "created",
656
+ createdAt: new Date().toISOString(),
657
+ }),
658
+ {
659
+ canonicalizeInput: (request) =>
660
+ canonicalJsonBytes({
661
+ calendarId: request.calendarId,
662
+ title: request.title,
663
+ start: request.start,
664
+ attendees: request.attendees,
665
+ }),
666
+ },
667
+ );
668
+
669
+ const result = await createEvent({
670
+ authorizationToken: state.agentToken,
671
+ calendarId: "demo-calendar",
672
+ title: "Test Sello receipt",
673
+ start: "2026-06-05T17:00:00Z",
674
+ attendees: ["ada@example.com", "grace@example.com"],
675
+ });
676
+
677
+ await receipts.flush();
678
+ console.log(result);
679
+ `;
680
+ }