sello 0.1.7 → 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 +140 -0
- package/docs/sdk-quickstart.md +8 -0
- package/package.json +1 -1
- package/src/cli/sello.ts +140 -0
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;
|
|
@@ -256,6 +259,32 @@ function initDemoCommand(args ) {
|
|
|
256
259
|
console.log("Then open http://localhost:8787/actions");
|
|
257
260
|
}
|
|
258
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");
|
|
275
|
+
console.log(" npx sello dev");
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log("Terminal 2: start the route");
|
|
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\"}'");
|
|
283
|
+
console.log(" npx sello actions");
|
|
284
|
+
console.log("");
|
|
285
|
+
console.log("Then open http://localhost:8787/actions");
|
|
286
|
+
}
|
|
287
|
+
|
|
259
288
|
function keysCommand(args ) {
|
|
260
289
|
const subcommand = args[0] ?? "service";
|
|
261
290
|
if (subcommand !== "service") {
|
|
@@ -650,6 +679,7 @@ function printHelp() {
|
|
|
650
679
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
|
651
680
|
sello emit-demo [--title title]
|
|
652
681
|
sello init-demo [--output emit-receipt.mjs] [--force]
|
|
682
|
+
sello init-http-demo [--output sello-http-route.mjs] [--force]
|
|
653
683
|
sello actions [--token agent-token]
|
|
654
684
|
sello keys service
|
|
655
685
|
sello inspect-env
|
|
@@ -762,3 +792,113 @@ await receipts.flush();
|
|
|
762
792
|
console.log(result);
|
|
763
793
|
`;
|
|
764
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;
|
|
@@ -257,6 +260,32 @@ function initDemoCommand(args: string[]): void {
|
|
|
257
260
|
console.log("Then open http://localhost:8787/actions");
|
|
258
261
|
}
|
|
259
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");
|
|
276
|
+
console.log(" npx sello dev");
|
|
277
|
+
console.log("");
|
|
278
|
+
console.log("Terminal 2: start the route");
|
|
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\"}'");
|
|
284
|
+
console.log(" npx sello actions");
|
|
285
|
+
console.log("");
|
|
286
|
+
console.log("Then open http://localhost:8787/actions");
|
|
287
|
+
}
|
|
288
|
+
|
|
260
289
|
function keysCommand(args: string[]): void {
|
|
261
290
|
const subcommand = args[0] ?? "service";
|
|
262
291
|
if (subcommand !== "service") {
|
|
@@ -651,6 +680,7 @@ function printHelp(): void {
|
|
|
651
680
|
sello dev [--port 8787] [--service service-id] [--dry-run]
|
|
652
681
|
sello emit-demo [--title title]
|
|
653
682
|
sello init-demo [--output emit-receipt.mjs] [--force]
|
|
683
|
+
sello init-http-demo [--output sello-http-route.mjs] [--force]
|
|
654
684
|
sello actions [--token agent-token]
|
|
655
685
|
sello keys service
|
|
656
686
|
sello inspect-env
|
|
@@ -763,3 +793,113 @@ await receipts.flush();
|
|
|
763
793
|
console.log(result);
|
|
764
794
|
`;
|
|
765
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
|
+
}
|