capsite/toybox: ingress is a capability; a hello-world web vat
E(vatAdmin).makeIngress(labels) reserves a mount and returns an ingress; whoever holds it serves requests carrying those labels by calling E(ingress).serve(handler). A vat gets one in its params, like any other remote object; nothing in a vat's module is an entrypoint. A request reaches the handler as passable data (method, path, labels, body), and the response comes back as data (status, headers, body). The handler is a live reference for now: after a restart its mount answers 503. vats/hello.js serves hello, world; pnpm hello creates it on a running toybox (client.js is the Node OCapN client, shared with the tests). pnpm start and pnpm switchboard never ran: systemd-socket-activate wants absolute paths for unix sockets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8025ec4b20
commit
a984461cee
6 files changed
+129
-32
No files matched your search
@@ -178,6 +178,8 @@ const encodeUtf8 = harden(TextEncoder.prototype.encode.bind(new TextEncoder()));
|
||||
*/
|
||||
export class VatAdmin extends DurableObject {
|
||||
#clients = new Map();
|
||||
/** mount key → the handler serving it (a live reference, for now) */
|
||||
#handlers = new Map();
|
||||
#ready;
|
||||
|
||||
constructor(ctx, env) {
|
||||
@@ -200,6 +202,21 @@ export class VatAdmin extends DurableObject {
|
||||
const root = await E(vatBootstrap).start(params);
|
||||
return harden({ id: id.toString(), root });
|
||||
},
|
||||
/**
|
||||
* Reserve a mount at `labels` and return the ingress for it: whoever
|
||||
* holds the ingress (a vat it is passed to, say) serves requests
|
||||
* that carry those labels.
|
||||
*
|
||||
* @param {Record<string, string>} labels
|
||||
*/
|
||||
makeIngress: async (labels) => {
|
||||
const key = `ingress:${crypto.randomUUID()}`;
|
||||
await this.env.DIRECTORY.getByName("directory").mount(key, harden({ ...labels }));
|
||||
return Far("ingress", {
|
||||
/** @param {{ handle: (request: object) => object }} handler */
|
||||
serve: (handler) => void this.#handlers.set(key, handler),
|
||||
});
|
||||
},
|
||||
});
|
||||
// workerd runs FinalizationRegistry callbacks outside any request, where
|
||||
// sending the resulting GC message is forbidden I/O. So imports are
|
||||
@@ -240,6 +257,19 @@ export class VatAdmin extends DurableObject {
|
||||
return this.env.VATS.get(id).gateway();
|
||||
}
|
||||
|
||||
/**
|
||||
* One HTTP request, as passable data, for the handler serving `key`;
|
||||
* `null` when nothing serves it.
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {object} request
|
||||
*/
|
||||
async serveHttp(key, request) {
|
||||
await this.#ready;
|
||||
const handler = this.#handlers.get(key);
|
||||
return handler ? E(handler).handle(harden(request)) : null;
|
||||
}
|
||||
|
||||
/** A WebSocket here starts a Cap'n Web session whose main object is an {@link AdminSession}. */
|
||||
fetch(request) {
|
||||
return newWorkersRpcResponse(request, new AdminSession(this, this.#clients));
|
||||
@@ -361,6 +391,7 @@ export class Public extends WorkerEntrypoint {
|
||||
}
|
||||
const mount = await this.env.DIRECTORY.getByName("directory").lookup(labels);
|
||||
if (!mount) return new Response("nothing mounted here\n", { status: 404 });
|
||||
if (mount.vat.startsWith("ingress:")) return this.#serveOcapn(mount, labels, request);
|
||||
const forwarded = new Request(request);
|
||||
forwarded.headers.set("Toybox-Connection-Labels", JSON.stringify(mergeLabels(mount.labels, labels)));
|
||||
const resp = await this.env.VATS.get(this.env.VATS.idFromString(mount.vat)).fetch(forwarded);
|
||||
@@ -369,6 +400,18 @@ export class Public extends WorkerEntrypoint {
|
||||
sealed.headers.set("Referrer-Policy", "no-referrer");
|
||||
return sealed;
|
||||
}
|
||||
|
||||
/** A mount made by `makeIngress`: the request goes, as data, to whoever serves it. */
|
||||
async #serveOcapn(mount, labels, request) {
|
||||
const { pathname } = new URL(request.url);
|
||||
const body = ["GET", "HEAD"].includes(request.method) ? "" : await request.text();
|
||||
const req = { method: request.method, path: pathname, labels: mergeLabels(mount.labels, labels), body };
|
||||
const res = await this.env.VAT_ADMIN.getByName(VAT_ADMIN).serveHttp(mount.vat, req);
|
||||
if (!res) return new Response("mounted, but nothing serves it now\n", { status: 503 });
|
||||
const resp = new Response(res.body ?? "", { status: res.status ?? 200, headers: res.headers ?? {} });
|
||||
resp.headers.set("Referrer-Policy", "no-referrer");
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user