sello 0.1.9 → 0.1.10

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
@@ -49,6 +49,12 @@ npx --yes sello init-http-demo
49
49
  npx --yes sello call-http-demo
50
50
  ```
51
51
 
52
+ ## What Just Happened?
53
+
54
+ `sello dev` created a local owner key, service key, token, registry, and transparency log. The demo tool or route verified the token before running the handler. After the handler ran, the service signed an encrypted receipt for the action it observed. The local log stored the encrypted receipt, not plaintext action details. `sello actions` fetched the receipt, verified the log entry and service signature, decrypted it with the owner key, and printed the owner's view.
55
+
56
+ Local dev state lives under `.sello/`. The dev log is stored as encrypted receipt entries in `.sello/dev-log.jsonl`, so receipts survive restarting `sello dev` while staying out of git.
57
+
52
58
  ## Why Sello?
53
59
 
54
60
  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.
package/dist/cli/sello.js CHANGED
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createServer, } from "node:http";
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import {
5
+ appendFileSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ writeFileSync,
10
+ } from "node:fs";
5
11
  import { dirname, join } from "node:path";
6
12
 
7
13
  import { generateEd25519KeyPair } from "../cose/sign1.js";
@@ -163,7 +169,7 @@ async function devCommand(args ) {
163
169
  "calendar.example.com/mcp/v1";
164
170
  const logEndpoint = `http://localhost:${port}/api`;
165
171
  const logUrl = toCanonicalLogUrl(`http://localhost:${port}/api`);
166
- const state = createDevState({ serviceId, logUrl, logEndpoint });
172
+ const state = loadOrCreateDevState({ serviceId, logUrl, logEndpoint });
167
173
  saveDevState(state);
168
174
 
169
175
  if (dryRun) {
@@ -174,10 +180,12 @@ async function devCommand(args ) {
174
180
  }
175
181
 
176
182
  const log = new MockTransparencyLog(logUrl);
183
+ const logPath = devLogPath();
184
+ const loadedEntries = loadDevLogEntries(log, logPath);
177
185
  const registry = parseRegistry(textEncoder.encode(state.registryJson));
178
186
  const server = createServer(async (request, response) => {
179
187
  try {
180
- await handleDevRequest({ request, response, log, state, registry });
188
+ await handleDevRequest({ request, response, log, logPath, state, registry });
181
189
  } catch (error) {
182
190
  sendJson(response, 500, {
183
191
  error: error instanceof Error ? error.message : String(error),
@@ -187,6 +195,9 @@ async function devCommand(args ) {
187
195
 
188
196
  server.listen(port, () => {
189
197
  printDevConfig(port, state);
198
+ console.log("");
199
+ console.log(`Local dev log: ${logPath}`);
200
+ console.log(`Loaded ${devLogEntryCountLabel(loadedEntries)}.`);
190
201
  });
191
202
  }
192
203
 
@@ -364,9 +375,10 @@ async function handleDevRequest(input
364
375
 
365
376
 
366
377
 
378
+
367
379
 
368
380
  ) {
369
- const { request, response, log, state, registry } = input;
381
+ const { request, response, log, logPath, state, registry } = input;
370
382
  const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
371
383
 
372
384
  if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/actions")) {
@@ -391,6 +403,7 @@ async function handleDevRequest(input
391
403
  decodeBase64url(body.envelope, "envelope"),
392
404
  typeof body.integratedTime === "string" ? body.integratedTime : undefined,
393
405
  );
406
+ appendDevLogEntry(logPath, entry);
394
407
  sendJson(response, 200, serializeEntry(entry));
395
408
  return;
396
409
  }
@@ -412,6 +425,24 @@ async function handleDevRequest(input
412
425
  sendJson(response, 404, { error: "not found" });
413
426
  }
414
427
 
428
+ function loadOrCreateDevState(input
429
+
430
+
431
+
432
+ ) {
433
+ const existing = loadDevStateIfPresent();
434
+ if (
435
+ existing &&
436
+ existing.serviceId === input.serviceId &&
437
+ existing.logUrl === input.logUrl &&
438
+ existing.logEndpoint === input.logEndpoint
439
+ ) {
440
+ return existing;
441
+ }
442
+
443
+ return createDevState(input);
444
+ }
445
+
415
446
  function verifyDevActions(input
416
447
 
417
448
 
@@ -527,6 +558,54 @@ function devStatePath() {
527
558
  return join(process.cwd(), ".sello", "dev.json");
528
559
  }
529
560
 
561
+ function devLogPath() {
562
+ return join(process.cwd(), ".sello", "dev-log.jsonl");
563
+ }
564
+
565
+ function loadDevLogEntries(log , path ) {
566
+ if (!existsSync(path)) {
567
+ return 0;
568
+ }
569
+
570
+ const lines = textDecoder
571
+ .decode(readFileBytes(path))
572
+ .split(/\r?\n/)
573
+ .filter((line) => line.trim().length > 0);
574
+
575
+ let loaded = 0;
576
+ for (const [index, line] of lines.entries()) {
577
+ let entry;
578
+ try {
579
+ entry = deserializeEntry(JSON.parse(line));
580
+ if (entry.logUrl !== log.logUrl) {
581
+ continue;
582
+ }
583
+ log.append(entry.envelope, entry.integratedTime);
584
+ loaded += 1;
585
+ } catch (error) {
586
+ throw new TypeError(
587
+ `invalid local dev log entry ${index + 1} in ${path}: ${
588
+ error instanceof Error ? error.message : String(error)
589
+ }`,
590
+ );
591
+ }
592
+ }
593
+
594
+ return loaded;
595
+ }
596
+
597
+ function appendDevLogEntry(
598
+ path ,
599
+ entry ,
600
+ ) {
601
+ mkdirSync(dirname(path), { recursive: true });
602
+ appendFileSync(path, `${JSON.stringify(serializeEntry(entry))}\n`, { mode: 0o600 });
603
+ }
604
+
605
+ function devLogEntryCountLabel(count ) {
606
+ return count === 1 ? "1 encrypted receipt" : `${count} encrypted receipts`;
607
+ }
608
+
530
609
  function readFileBytes(path ) {
531
610
  return new Uint8Array(readFileSync(path));
532
611
  }
@@ -48,6 +48,12 @@ npx --yes sello call-http-demo
48
48
  npx --yes sello actions
49
49
  ```
50
50
 
51
+ ## What Just Happened?
52
+
53
+ `sello dev` created local development keys, a demo authorization token, a service registry, and a local transparency log. The wrapped tool or route verified the token before running your handler. After the handler returned, Sello signed an encrypted receipt for the observed action and submitted it to the local log. The log stored encrypted receipt data, not plaintext action details. `sello actions` used the owner key from local dev state to fetch, verify, decrypt, and print the action.
54
+
55
+ Local dev state lives under `.sello/`. The encrypted dev log is stored in `.sello/dev-log.jsonl`, so receipts survive restarting `sello dev` without being committed to git.
56
+
51
57
  Inside this repo, start the local log and action viewer:
52
58
 
53
59
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sello",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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",
package/src/cli/sello.ts CHANGED
@@ -1,7 +1,13 @@
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 { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import {
5
+ appendFileSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ writeFileSync,
10
+ } from "node:fs";
5
11
  import { dirname, join } from "node:path";
6
12
 
7
13
  import { generateEd25519KeyPair } from "../cose/sign1.ts";
@@ -164,7 +170,7 @@ async function devCommand(args: string[]): Promise<void> {
164
170
  "calendar.example.com/mcp/v1";
165
171
  const logEndpoint = `http://localhost:${port}/api`;
166
172
  const logUrl = toCanonicalLogUrl(`http://localhost:${port}/api`);
167
- const state = createDevState({ serviceId, logUrl, logEndpoint });
173
+ const state = loadOrCreateDevState({ serviceId, logUrl, logEndpoint });
168
174
  saveDevState(state);
169
175
 
170
176
  if (dryRun) {
@@ -175,10 +181,12 @@ async function devCommand(args: string[]): Promise<void> {
175
181
  }
176
182
 
177
183
  const log = new MockTransparencyLog(logUrl);
184
+ const logPath = devLogPath();
185
+ const loadedEntries = loadDevLogEntries(log, logPath);
178
186
  const registry = parseRegistry(textEncoder.encode(state.registryJson));
179
187
  const server = createServer(async (request, response) => {
180
188
  try {
181
- await handleDevRequest({ request, response, log, state, registry });
189
+ await handleDevRequest({ request, response, log, logPath, state, registry });
182
190
  } catch (error) {
183
191
  sendJson(response, 500, {
184
192
  error: error instanceof Error ? error.message : String(error),
@@ -188,6 +196,9 @@ async function devCommand(args: string[]): Promise<void> {
188
196
 
189
197
  server.listen(port, () => {
190
198
  printDevConfig(port, state);
199
+ console.log("");
200
+ console.log(`Local dev log: ${logPath}`);
201
+ console.log(`Loaded ${devLogEntryCountLabel(loadedEntries)}.`);
191
202
  });
192
203
  }
193
204
 
@@ -364,10 +375,11 @@ async function handleDevRequest(input: {
364
375
  request: IncomingMessage;
365
376
  response: ServerResponse;
366
377
  log: MockTransparencyLog;
378
+ logPath: string;
367
379
  state: DevState;
368
380
  registry: ReturnType<typeof parseRegistry>;
369
381
  }): Promise<void> {
370
- const { request, response, log, state, registry } = input;
382
+ const { request, response, log, logPath, state, registry } = input;
371
383
  const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
372
384
 
373
385
  if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/actions")) {
@@ -392,6 +404,7 @@ async function handleDevRequest(input: {
392
404
  decodeBase64url(body.envelope, "envelope"),
393
405
  typeof body.integratedTime === "string" ? body.integratedTime : undefined,
394
406
  );
407
+ appendDevLogEntry(logPath, entry);
395
408
  sendJson(response, 200, serializeEntry(entry));
396
409
  return;
397
410
  }
@@ -413,6 +426,24 @@ async function handleDevRequest(input: {
413
426
  sendJson(response, 404, { error: "not found" });
414
427
  }
415
428
 
429
+ function loadOrCreateDevState(input: {
430
+ serviceId: string;
431
+ logUrl: CanonicalLogUrl;
432
+ logEndpoint: string;
433
+ }): DevState {
434
+ const existing = loadDevStateIfPresent();
435
+ if (
436
+ existing &&
437
+ existing.serviceId === input.serviceId &&
438
+ existing.logUrl === input.logUrl &&
439
+ existing.logEndpoint === input.logEndpoint
440
+ ) {
441
+ return existing;
442
+ }
443
+
444
+ return createDevState(input);
445
+ }
446
+
416
447
  function verifyDevActions(input: {
417
448
  log: MockTransparencyLog;
418
449
  state: DevState;
@@ -528,6 +559,54 @@ function devStatePath(): string {
528
559
  return join(process.cwd(), ".sello", "dev.json");
529
560
  }
530
561
 
562
+ function devLogPath(): string {
563
+ return join(process.cwd(), ".sello", "dev-log.jsonl");
564
+ }
565
+
566
+ function loadDevLogEntries(log: MockTransparencyLog, path: string): number {
567
+ if (!existsSync(path)) {
568
+ return 0;
569
+ }
570
+
571
+ const lines = textDecoder
572
+ .decode(readFileBytes(path))
573
+ .split(/\r?\n/)
574
+ .filter((line) => line.trim().length > 0);
575
+
576
+ let loaded = 0;
577
+ for (const [index, line] of lines.entries()) {
578
+ let entry;
579
+ try {
580
+ entry = deserializeEntry(JSON.parse(line));
581
+ if (entry.logUrl !== log.logUrl) {
582
+ continue;
583
+ }
584
+ log.append(entry.envelope, entry.integratedTime);
585
+ loaded += 1;
586
+ } catch (error) {
587
+ throw new TypeError(
588
+ `invalid local dev log entry ${index + 1} in ${path}: ${
589
+ error instanceof Error ? error.message : String(error)
590
+ }`,
591
+ );
592
+ }
593
+ }
594
+
595
+ return loaded;
596
+ }
597
+
598
+ function appendDevLogEntry(
599
+ path: string,
600
+ entry: ReturnType<MockTransparencyLog["append"]>,
601
+ ): void {
602
+ mkdirSync(dirname(path), { recursive: true });
603
+ appendFileSync(path, `${JSON.stringify(serializeEntry(entry))}\n`, { mode: 0o600 });
604
+ }
605
+
606
+ function devLogEntryCountLabel(count: number): string {
607
+ return count === 1 ? "1 encrypted receipt" : `${count} encrypted receipts`;
608
+ }
609
+
531
610
  function readFileBytes(path: string): Uint8Array {
532
611
  return new Uint8Array(readFileSync(path));
533
612
  }