sello 0.1.6 → 0.1.8
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 +9 -0
- package/dist/cli/sello.js +145 -2
- package/docs/sdk-quickstart.md +8 -0
- package/package.json +1 -1
- package/src/cli/sello.ts +145 -2
package/README.md
CHANGED
|
@@ -42,6 +42,12 @@ To see the tiny wrapped-tool source:
|
|
|
42
42
|
npx --yes sello init-demo
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
To scaffold a small HTTP route that emits receipts:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx --yes sello init-http-demo
|
|
49
|
+
```
|
|
50
|
+
|
|
45
51
|
## Why Sello?
|
|
46
52
|
|
|
47
53
|
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.
|
|
@@ -79,6 +85,7 @@ The implementation includes a local end-to-end demo, compact JWS token verificat
|
|
|
79
85
|
|------|------|
|
|
80
86
|
| Add Sello in a few lines | [SDK Quickstart](docs/sdk-quickstart.md) |
|
|
81
87
|
| Emit your first receipt | `npx --yes sello dev`, then `npx --yes sello emit-demo` |
|
|
88
|
+
| Wrap one HTTP route | `npx --yes sello init-http-demo` |
|
|
82
89
|
| Try a wrapped tool locally | `node --run dev`, then `node --run example:tool` |
|
|
83
90
|
| Try an MCP-style tool call | `node --run dev`, then `node --run example:mcp` |
|
|
84
91
|
| See a minimal MCP integration | [examples/mcp-minimal-server.ts](examples/mcp-minimal-server.ts) |
|
|
@@ -135,6 +142,8 @@ For an MCP-shaped `tools/call` boundary, run `node --run example:mcp` instead of
|
|
|
135
142
|
|
|
136
143
|
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
144
|
|
|
145
|
+
For an installed-project bridge from demo to app, run `npx sello init-http-demo`. It writes a small dependency-free HTTP route that imports `sello`, reads the local dev config, verifies a bearer token, runs a handler, and emits a receipt.
|
|
146
|
+
|
|
138
147
|
## The First 10 Minutes
|
|
139
148
|
|
|
140
149
|
If you are implementing Sello, start with one local loop:
|
package/dist/cli/sello.js
CHANGED
|
@@ -84,6 +84,9 @@ try {
|
|
|
84
84
|
case "init-demo":
|
|
85
85
|
initDemoCommand(process.argv.slice(3));
|
|
86
86
|
break;
|
|
87
|
+
case "init-http-demo":
|
|
88
|
+
initHttpDemoCommand(process.argv.slice(3));
|
|
89
|
+
break;
|
|
87
90
|
case "keys":
|
|
88
91
|
keysCommand(process.argv.slice(3));
|
|
89
92
|
break;
|
|
@@ -246,11 +249,40 @@ function initDemoCommand(args ) {
|
|
|
246
249
|
|
|
247
250
|
console.log(`Created ${output}`);
|
|
248
251
|
console.log("");
|
|
249
|
-
console.log("
|
|
250
|
-
console.log("
|
|
252
|
+
console.log("Terminal 1: keep the local dev log running");
|
|
253
|
+
console.log(" npx sello dev");
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log("Terminal 2: emit and view a receipt");
|
|
256
|
+
console.log(` node ${output}`);
|
|
257
|
+
console.log(" npx sello actions");
|
|
258
|
+
console.log("");
|
|
259
|
+
console.log("Then open http://localhost:8787/actions");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function initHttpDemoCommand(args ) {
|
|
263
|
+
const output = readFlag(args, "--output") ?? "sello-http-route.mjs";
|
|
264
|
+
const force = args.includes("--force");
|
|
265
|
+
|
|
266
|
+
if (existsSync(output) && !force) {
|
|
267
|
+
throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
writeFileSync(output, httpRouteDemoSource(), { mode: 0o644 });
|
|
271
|
+
|
|
272
|
+
console.log(`Created ${output}`);
|
|
273
|
+
console.log("");
|
|
274
|
+
console.log("Terminal 1: keep the local dev log running");
|
|
251
275
|
console.log(" npx sello dev");
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log("Terminal 2: start the route");
|
|
252
278
|
console.log(` node ${output}`);
|
|
279
|
+
console.log("");
|
|
280
|
+
console.log("Terminal 3: call the route and view the receipt");
|
|
281
|
+
console.log(" TOKEN=$(node -e 'console.log(JSON.parse(require(\"fs\").readFileSync(\".sello/dev.json\", \"utf8\")).agentToken)')");
|
|
282
|
+
console.log(" curl -sS -X POST http://localhost:8790/calendar/events -H \"authorization: Bearer $TOKEN\" -H \"content-type: application/json\" -d '{\"title\":\"Ship Sello\"}'");
|
|
253
283
|
console.log(" npx sello actions");
|
|
284
|
+
console.log("");
|
|
285
|
+
console.log("Then open http://localhost:8787/actions");
|
|
254
286
|
}
|
|
255
287
|
|
|
256
288
|
function keysCommand(args ) {
|
|
@@ -647,6 +679,7 @@ function printHelp() {
|
|
|
647
679
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
|
648
680
|
sello emit-demo [--title title]
|
|
649
681
|
sello init-demo [--output emit-receipt.mjs] [--force]
|
|
682
|
+
sello init-http-demo [--output sello-http-route.mjs] [--force]
|
|
650
683
|
sello actions [--token agent-token]
|
|
651
684
|
sello keys service
|
|
652
685
|
sello inspect-env
|
|
@@ -759,3 +792,113 @@ await receipts.flush();
|
|
|
759
792
|
console.log(result);
|
|
760
793
|
`;
|
|
761
794
|
}
|
|
795
|
+
|
|
796
|
+
function httpRouteDemoSource() {
|
|
797
|
+
return `import { createServer } from "node:http";
|
|
798
|
+
import { readFileSync } from "node:fs";
|
|
799
|
+
import { canonicalJsonBytes, sello } from "sello";
|
|
800
|
+
|
|
801
|
+
const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
|
|
802
|
+
const port = Number(process.env.PORT ?? "8790");
|
|
803
|
+
|
|
804
|
+
const receipts = sello.service({
|
|
805
|
+
service: state.serviceId,
|
|
806
|
+
serviceKey: state.serviceKey,
|
|
807
|
+
tokenIssuer: state.tokenIssuerPublicKey,
|
|
808
|
+
log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
|
|
809
|
+
submit: { mode: "await" },
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
const createEvent = receipts.tool(
|
|
813
|
+
"http.POST /calendar/events",
|
|
814
|
+
async (request) => {
|
|
815
|
+
const title = readString(request.body.title, "title");
|
|
816
|
+
return {
|
|
817
|
+
id: "evt_" + slug(title),
|
|
818
|
+
title,
|
|
819
|
+
status: "created",
|
|
820
|
+
createdAt: new Date().toISOString(),
|
|
821
|
+
};
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
authorizationToken: (request) => request.authorizationToken,
|
|
825
|
+
canonicalizeInput: (request) =>
|
|
826
|
+
canonicalJsonBytes({
|
|
827
|
+
method: "POST",
|
|
828
|
+
path: "/calendar/events",
|
|
829
|
+
body: request.body,
|
|
830
|
+
}),
|
|
831
|
+
canonicalizeOutput: (response) => canonicalJsonBytes(response),
|
|
832
|
+
},
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
const server = createServer(async (request, response) => {
|
|
836
|
+
try {
|
|
837
|
+
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
838
|
+
if (request.method !== "POST" || path !== "/calendar/events") {
|
|
839
|
+
sendJson(response, 404, { error: "not found" });
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const body = await readJson(request);
|
|
844
|
+
const result = await createEvent({
|
|
845
|
+
authorizationToken: bearerToken(request.headers.authorization),
|
|
846
|
+
body,
|
|
847
|
+
});
|
|
848
|
+
await receipts.flush();
|
|
849
|
+
sendJson(response, 200, result);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
sendJson(response, 400, {
|
|
852
|
+
error: error instanceof Error ? error.message : String(error),
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
server.listen(port, () => {
|
|
858
|
+
console.log("Sello HTTP route demo running at http://localhost:" + port + "/calendar/events");
|
|
859
|
+
console.log("");
|
|
860
|
+
console.log("Call it with:");
|
|
861
|
+
console.log(" TOKEN=$(node -e 'console.log(JSON.parse(require(\\\"fs\\\").readFileSync(\\\".sello/dev.json\\\", \\\"utf8\\\")).agentToken)')");
|
|
862
|
+
console.log(" curl -sS -X POST http://localhost:" + port + "/calendar/events -H \\\"authorization: Bearer $TOKEN\\\" -H \\\"content-type: application/json\\\" -d '{\\\"title\\\":\\\"Ship Sello\\\"}'");
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
async function readJson(request) {
|
|
866
|
+
const chunks = [];
|
|
867
|
+
for await (const chunk of request) {
|
|
868
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
869
|
+
}
|
|
870
|
+
if (chunks.length === 0) {
|
|
871
|
+
return {};
|
|
872
|
+
}
|
|
873
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function bearerToken(header) {
|
|
877
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
878
|
+
if (typeof value !== "string" || !value.startsWith("Bearer ")) {
|
|
879
|
+
throw new TypeError("missing Authorization: Bearer <token> header");
|
|
880
|
+
}
|
|
881
|
+
return value.slice("Bearer ".length);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function sendJson(response, statusCode, body) {
|
|
885
|
+
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
886
|
+
response.end(JSON.stringify(body, null, 2) + "\\n");
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function readString(value, name) {
|
|
890
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
891
|
+
throw new TypeError(name + " must be a non-empty string");
|
|
892
|
+
}
|
|
893
|
+
return value;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function slug(value) {
|
|
897
|
+
return value
|
|
898
|
+
.toLowerCase()
|
|
899
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
900
|
+
.replace(/^_+|_+$/g, "")
|
|
901
|
+
.slice(0, 40);
|
|
902
|
+
}
|
|
903
|
+
`;
|
|
904
|
+
}
|
package/docs/sdk-quickstart.md
CHANGED
|
@@ -35,6 +35,14 @@ To write the tiny emitter file into your project:
|
|
|
35
35
|
npx --yes sello init-demo
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
To write a dependency-free HTTP route example into your project:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx --yes sello init-http-demo
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The route example imports `sello`, reads the local dev config, verifies a bearer token, runs one `POST /calendar/events` handler, and emits a receipt.
|
|
45
|
+
|
|
38
46
|
Inside this repo, start the local log and action viewer:
|
|
39
47
|
|
|
40
48
|
```bash
|
package/package.json
CHANGED
package/src/cli/sello.ts
CHANGED
|
@@ -85,6 +85,9 @@ try {
|
|
|
85
85
|
case "init-demo":
|
|
86
86
|
initDemoCommand(process.argv.slice(3));
|
|
87
87
|
break;
|
|
88
|
+
case "init-http-demo":
|
|
89
|
+
initHttpDemoCommand(process.argv.slice(3));
|
|
90
|
+
break;
|
|
88
91
|
case "keys":
|
|
89
92
|
keysCommand(process.argv.slice(3));
|
|
90
93
|
break;
|
|
@@ -247,11 +250,40 @@ function initDemoCommand(args: string[]): void {
|
|
|
247
250
|
|
|
248
251
|
console.log(`Created ${output}`);
|
|
249
252
|
console.log("");
|
|
250
|
-
console.log("
|
|
251
|
-
console.log("
|
|
253
|
+
console.log("Terminal 1: keep the local dev log running");
|
|
254
|
+
console.log(" npx sello dev");
|
|
255
|
+
console.log("");
|
|
256
|
+
console.log("Terminal 2: emit and view a receipt");
|
|
257
|
+
console.log(` node ${output}`);
|
|
258
|
+
console.log(" npx sello actions");
|
|
259
|
+
console.log("");
|
|
260
|
+
console.log("Then open http://localhost:8787/actions");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function initHttpDemoCommand(args: string[]): void {
|
|
264
|
+
const output = readFlag(args, "--output") ?? "sello-http-route.mjs";
|
|
265
|
+
const force = args.includes("--force");
|
|
266
|
+
|
|
267
|
+
if (existsSync(output) && !force) {
|
|
268
|
+
throw new TypeError(`${output} already exists. Pass --force to overwrite it.`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
writeFileSync(output, httpRouteDemoSource(), { mode: 0o644 });
|
|
272
|
+
|
|
273
|
+
console.log(`Created ${output}`);
|
|
274
|
+
console.log("");
|
|
275
|
+
console.log("Terminal 1: keep the local dev log running");
|
|
252
276
|
console.log(" npx sello dev");
|
|
277
|
+
console.log("");
|
|
278
|
+
console.log("Terminal 2: start the route");
|
|
253
279
|
console.log(` node ${output}`);
|
|
280
|
+
console.log("");
|
|
281
|
+
console.log("Terminal 3: call the route and view the receipt");
|
|
282
|
+
console.log(" TOKEN=$(node -e 'console.log(JSON.parse(require(\"fs\").readFileSync(\".sello/dev.json\", \"utf8\")).agentToken)')");
|
|
283
|
+
console.log(" curl -sS -X POST http://localhost:8790/calendar/events -H \"authorization: Bearer $TOKEN\" -H \"content-type: application/json\" -d '{\"title\":\"Ship Sello\"}'");
|
|
254
284
|
console.log(" npx sello actions");
|
|
285
|
+
console.log("");
|
|
286
|
+
console.log("Then open http://localhost:8787/actions");
|
|
255
287
|
}
|
|
256
288
|
|
|
257
289
|
function keysCommand(args: string[]): void {
|
|
@@ -648,6 +680,7 @@ function printHelp(): void {
|
|
|
648
680
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
|
649
681
|
sello emit-demo [--title title]
|
|
650
682
|
sello init-demo [--output emit-receipt.mjs] [--force]
|
|
683
|
+
sello init-http-demo [--output sello-http-route.mjs] [--force]
|
|
651
684
|
sello actions [--token agent-token]
|
|
652
685
|
sello keys service
|
|
653
686
|
sello inspect-env
|
|
@@ -760,3 +793,113 @@ await receipts.flush();
|
|
|
760
793
|
console.log(result);
|
|
761
794
|
`;
|
|
762
795
|
}
|
|
796
|
+
|
|
797
|
+
function httpRouteDemoSource(): string {
|
|
798
|
+
return `import { createServer } from "node:http";
|
|
799
|
+
import { readFileSync } from "node:fs";
|
|
800
|
+
import { canonicalJsonBytes, sello } from "sello";
|
|
801
|
+
|
|
802
|
+
const state = JSON.parse(readFileSync(".sello/dev.json", "utf8"));
|
|
803
|
+
const port = Number(process.env.PORT ?? "8790");
|
|
804
|
+
|
|
805
|
+
const receipts = sello.service({
|
|
806
|
+
service: state.serviceId,
|
|
807
|
+
serviceKey: state.serviceKey,
|
|
808
|
+
tokenIssuer: state.tokenIssuerPublicKey,
|
|
809
|
+
log: sello.logs.http(state.logUrl, { endpoint: state.logEndpoint }),
|
|
810
|
+
submit: { mode: "await" },
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const createEvent = receipts.tool(
|
|
814
|
+
"http.POST /calendar/events",
|
|
815
|
+
async (request) => {
|
|
816
|
+
const title = readString(request.body.title, "title");
|
|
817
|
+
return {
|
|
818
|
+
id: "evt_" + slug(title),
|
|
819
|
+
title,
|
|
820
|
+
status: "created",
|
|
821
|
+
createdAt: new Date().toISOString(),
|
|
822
|
+
};
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
authorizationToken: (request) => request.authorizationToken,
|
|
826
|
+
canonicalizeInput: (request) =>
|
|
827
|
+
canonicalJsonBytes({
|
|
828
|
+
method: "POST",
|
|
829
|
+
path: "/calendar/events",
|
|
830
|
+
body: request.body,
|
|
831
|
+
}),
|
|
832
|
+
canonicalizeOutput: (response) => canonicalJsonBytes(response),
|
|
833
|
+
},
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
const server = createServer(async (request, response) => {
|
|
837
|
+
try {
|
|
838
|
+
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
839
|
+
if (request.method !== "POST" || path !== "/calendar/events") {
|
|
840
|
+
sendJson(response, 404, { error: "not found" });
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const body = await readJson(request);
|
|
845
|
+
const result = await createEvent({
|
|
846
|
+
authorizationToken: bearerToken(request.headers.authorization),
|
|
847
|
+
body,
|
|
848
|
+
});
|
|
849
|
+
await receipts.flush();
|
|
850
|
+
sendJson(response, 200, result);
|
|
851
|
+
} catch (error) {
|
|
852
|
+
sendJson(response, 400, {
|
|
853
|
+
error: error instanceof Error ? error.message : String(error),
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
server.listen(port, () => {
|
|
859
|
+
console.log("Sello HTTP route demo running at http://localhost:" + port + "/calendar/events");
|
|
860
|
+
console.log("");
|
|
861
|
+
console.log("Call it with:");
|
|
862
|
+
console.log(" TOKEN=$(node -e 'console.log(JSON.parse(require(\\\"fs\\\").readFileSync(\\\".sello/dev.json\\\", \\\"utf8\\\")).agentToken)')");
|
|
863
|
+
console.log(" curl -sS -X POST http://localhost:" + port + "/calendar/events -H \\\"authorization: Bearer $TOKEN\\\" -H \\\"content-type: application/json\\\" -d '{\\\"title\\\":\\\"Ship Sello\\\"}'");
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
async function readJson(request) {
|
|
867
|
+
const chunks = [];
|
|
868
|
+
for await (const chunk of request) {
|
|
869
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
870
|
+
}
|
|
871
|
+
if (chunks.length === 0) {
|
|
872
|
+
return {};
|
|
873
|
+
}
|
|
874
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function bearerToken(header) {
|
|
878
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
879
|
+
if (typeof value !== "string" || !value.startsWith("Bearer ")) {
|
|
880
|
+
throw new TypeError("missing Authorization: Bearer <token> header");
|
|
881
|
+
}
|
|
882
|
+
return value.slice("Bearer ".length);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function sendJson(response, statusCode, body) {
|
|
886
|
+
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
887
|
+
response.end(JSON.stringify(body, null, 2) + "\\n");
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function readString(value, name) {
|
|
891
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
892
|
+
throw new TypeError(name + " must be a non-empty string");
|
|
893
|
+
}
|
|
894
|
+
return value;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function slug(value) {
|
|
898
|
+
return value
|
|
899
|
+
.toLowerCase()
|
|
900
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
901
|
+
.replace(/^_+|_+$/g, "")
|
|
902
|
+
.slice(0, 40);
|
|
903
|
+
}
|
|
904
|
+
`;
|
|
905
|
+
}
|