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 +41 -12
- package/dist/cli/sello.js +165 -1
- package/docs/sdk-quickstart.md +27 -0
- package/examples/mcp-minimal-server.ts +95 -0
- package/package.json +2 -1
- package/src/cli/sello.ts +165 -1
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
|
-
##
|
|
192
|
-
|
|
193
|
-
The current dependency-free test command is:
|
|
224
|
+
## License
|
|
194
225
|
|
|
195
|
-
|
|
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
|
+
}
|
package/docs/sdk-quickstart.md
CHANGED
|
@@ -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.
|
|
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
|
+
}
|