capsite/toybox: OCapN vats; createVat takes remote objects
The admin socket now also speaks OCapN over Cap'n Web: a WebSocket there reaches the VatAdmin DO, an OCapN peer whose bootstrap (swissnum vatAdmin) offers createVat(src, params). params may hold remote objects; the vat's root comes back as one. Each OCapN vat is its own peer: SES and @endo/ocapn run in its facet (vat-runtime.js, bundled with esbuild), around the vat's own module, which exports buildRootObject(vatPowers, params). Passing a client's object to a vat, or a vat's root to the client, is an OCapN handoff, so the receiver dials the exporter. Nothing here can dial out, so the VatAdmin is also the router: a client registers its gateway under a designator, a vat's designator is its id, and every peer reaches every other through it. The netlayer is ertp-clerk's, made to work over Workers RPC stubs and plain promises as well as Cap'n Web. Not yet: sturdyrefs that survive a restart, an adminNode, and grants (egress, mount) for OCapN vats. App vats (export class App) still work beside them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8d8900d84d
commit
9356f63c64
13 files changed
+983
-61
No files matched your search
@@ -1,3 +1,4 @@
|
||||
state/
|
||||
admin.sock
|
||||
switchboard-*.sock
|
||||
dist/
|
||||
@@ -0,0 +1,16 @@
|
||||
/** @file Bundle the supervisor and the OCapN vat runtime for workerd (see config.capnp). */
|
||||
import { build } from "esbuild";
|
||||
|
||||
const common = {
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "neutral",
|
||||
mainFields: ["module", "main"],
|
||||
conditions: ["workerd", "import"],
|
||||
external: ["cloudflare:workers", "node:*"],
|
||||
logLevel: "warning",
|
||||
};
|
||||
|
||||
await build({ ...common, entryPoints: ["supervisor.js"], outfile: "dist/supervisor.js", external: [...common.external, "vat-runtime.txt"] });
|
||||
// loaded as text, beside the vat's own module (vat.js)
|
||||
await build({ ...common, entryPoints: ["vat-runtime.js"], outfile: "dist/vat-runtime.js", external: [...common.external, "./vat.js"] });
|
||||
+5
-3
@@ -24,19 +24,21 @@ const config :Workerd.Config = (
|
||||
|
||||
const supervisor :Workerd.Worker = (
|
||||
compatibilityDate = "2026-10-01",
|
||||
# bundled by build.js
|
||||
modules = [
|
||||
(name = "supervisor.js", esModule = embed "supervisor.js"),
|
||||
(name = "labels.js", esModule = embed "labels.js"),
|
||||
(name = "capnweb", esModule = embed "node_modules/capnweb/dist/index-workers.js"),
|
||||
(name = "supervisor.js", esModule = embed "dist/supervisor.js"),
|
||||
(name = "vat-runtime.txt", text = embed "dist/vat-runtime.js"),
|
||||
],
|
||||
durableObjectNamespaces = [
|
||||
(className = "Vat", uniqueKey = "toybox-vat", enableSql = true),
|
||||
(className = "Directory", uniqueKey = "toybox-directory", enableSql = true),
|
||||
(className = "VatAdmin", uniqueKey = "toybox-vat-admin", enableSql = true),
|
||||
],
|
||||
durableObjectStorage = (localDisk = "state"),
|
||||
bindings = [
|
||||
(name = "VATS", durableObjectNamespace = "Vat"),
|
||||
(name = "DIRECTORY", durableObjectNamespace = "Directory"),
|
||||
(name = "VAT_ADMIN", durableObjectNamespace = "VatAdmin"),
|
||||
(name = "LOADER", workerLoader = ()),
|
||||
(name = "SWITCHBOARD", service = "switchboard"),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* @file An OCapN netlayer whose transport is an RPC session: Cap'n Web, or
|
||||
* Workers RPC, or one proxied through the other.
|
||||
*
|
||||
* Each OCapN connection is a pair of {@link ByteInbox}es, one per
|
||||
* direction, exchanged through a gateway; each `deliver` call carries
|
||||
* exactly one OCapN message, so no framing is needed. The RPC session
|
||||
* provides ordering and only-the-peer-can-inject; OCapN does the rest.
|
||||
*
|
||||
* After vattr97's ertp-clerk (`ocapn-capnweb-netlayer.ts`), with the stub
|
||||
* methods only Cap'n Web has (`onRpcBroken`) made optional.
|
||||
*
|
||||
* @see https://github.com/ocapn/ocapn/blob/main/draft-specifications/Netlayers.md
|
||||
*/
|
||||
import { RpcTarget } from "capnweb";
|
||||
|
||||
export const TRANSPORT = "capnweb";
|
||||
|
||||
/**
|
||||
* A peer's location, by designator alone: a router finds the peer.
|
||||
*
|
||||
* @param {string} designator
|
||||
*/
|
||||
export const locationOf = (designator) => ({ type: "ocapn-peer", transport: TRANSPORT, designator, hints: false });
|
||||
|
||||
/** Same as `locationToLocationId` in `@endo/ocapn`, which its exports map doesn't expose. */
|
||||
const locationId = ({ designator, transport }) => `ocapn://${designator}.${transport}`;
|
||||
|
||||
/** Receives one direction of an OCapN connection, one message per call. */
|
||||
export class ByteInbox extends RpcTarget {
|
||||
#onMessage;
|
||||
|
||||
constructor(onMessage) {
|
||||
super();
|
||||
this.#onMessage = onMessage;
|
||||
}
|
||||
|
||||
/** @param {Uint8Array} bytes */
|
||||
deliver(bytes) {
|
||||
// OCapN's syrup reader needs offset 0; a pooled Buffer view may not have it.
|
||||
this.#onMessage(bytes.byteOffset === 0 ? bytes : new Uint8Array(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
const dispose = (stub) => stub?.[Symbol.dispose]?.();
|
||||
|
||||
/** Write to a remote inbox, discarding each result at once so the session retains none. */
|
||||
const makeSocket = (inbox, close) => ({
|
||||
write: (bytes) => dispose(inbox.deliver(bytes)),
|
||||
end: close,
|
||||
});
|
||||
|
||||
/**
|
||||
* A remote inbox reached through a gateway that is only a promise (a
|
||||
* local async call, not a pipelining stub): writes queue in order behind it.
|
||||
*/
|
||||
const deferredInbox = (gatewayP, myInbox) => {
|
||||
const inboxP = Promise.resolve(gatewayP).then((gateway) => gateway.connect(myInbox));
|
||||
return {
|
||||
deliver: (bytes) => void inboxP.then((inbox) => dispose(inbox.deliver(bytes))),
|
||||
onRpcBroken: (cb) => void inboxP.then((inbox) => inbox.onRpcBroken?.(cb), cb),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Make an OCapN netlayer for `client.registerNetlayer`.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.designator this peer's designator
|
||||
* @param {(designator: string) => any} opts.openGateway the gateway of
|
||||
* the peer with this designator: a stub or pipelining promise
|
||||
* (`openGateway(d).connect(...)`), or a plain promise for one
|
||||
* @returns a netlayer maker; the netlayer has a `gateway` that accepts
|
||||
* incoming connections
|
||||
*/
|
||||
export const makeRpcNetLayer = ({ designator, openGateway }) => {
|
||||
const location = locationOf(designator);
|
||||
const connections = new Set();
|
||||
|
||||
return (handlers, logger) => {
|
||||
const track = (connection, inbox) => {
|
||||
connections.add(connection);
|
||||
inbox.onRpcBroken?.((reason) => {
|
||||
if (connections.delete(connection)) logger.log("rpc netlayer: connection broken", reason);
|
||||
handlers.handleConnectionClose(connection, reason);
|
||||
});
|
||||
};
|
||||
|
||||
const netlayer = {
|
||||
location,
|
||||
locationId: locationId(location),
|
||||
connect: (peer) => {
|
||||
const gateway = openGateway(peer.designator);
|
||||
let connection;
|
||||
const myInbox = new ByteInbox((bytes) => handlers.handleMessageData(connection, bytes));
|
||||
// Pipelined: writes before the peer answers queue in order.
|
||||
const peerInbox =
|
||||
typeof gateway.connect === "function" ? gateway.connect(myInbox) : deferredInbox(gateway, myInbox);
|
||||
connection = handlers.makeConnection(
|
||||
netlayer,
|
||||
true,
|
||||
makeSocket(peerInbox, () => {
|
||||
connections.delete(connection);
|
||||
dispose(gateway);
|
||||
}),
|
||||
);
|
||||
track(connection, peerInbox);
|
||||
return connection;
|
||||
},
|
||||
shutdown: () => {
|
||||
for (const connection of [...connections]) connection.end();
|
||||
},
|
||||
};
|
||||
|
||||
class Gateway extends RpcTarget {
|
||||
connect(peerInbox) {
|
||||
const peer = peerInbox.dup?.() ?? peerInbox;
|
||||
const connection = handlers.makeConnection(
|
||||
netlayer,
|
||||
false,
|
||||
makeSocket(peer, () => {
|
||||
connections.delete(connection);
|
||||
dispose(peer);
|
||||
}),
|
||||
);
|
||||
track(connection, peer);
|
||||
return new ByteInbox((bytes) => handlers.handleMessageData(connection, bytes));
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign(netlayer, { gateway: new Gateway() });
|
||||
};
|
||||
};
|
||||
+7
-2
@@ -4,15 +4,20 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "mkdir -p state && rm -f admin.sock && systemd-socket-activate -l 127.0.0.1:8787 -l ./admin.sock -- node_modules/workerd/bin/workerd serve --experimental config.capnp -S http=3 -S admin=4 -d state=state -e switchboard=unix:./switchboard-public.sock",
|
||||
"start": "node build.js && mkdir -p state && rm -f admin.sock && systemd-socket-activate -l 127.0.0.1:8787 -l ./admin.sock -- node_modules/workerd/bin/workerd serve --experimental config.capnp -S http=3 -S admin=4 -d state=state -e switchboard=unix:./switchboard-public.sock",
|
||||
"switchboard": "rm -f switchboard-public.sock switchboard-local.sock && systemd-socket-activate -l ./switchboard-public.sock -l ./switchboard-local.sock -- node_modules/workerd/bin/workerd serve --experimental switchboard.capnp -S public=3 -S local=4",
|
||||
"test": "node --test"
|
||||
"test": "node build.js && node --import ./test/lockdown.js --test test/*.test.js",
|
||||
"build": "node build.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.28.2",
|
||||
"workerd": "1.20261006.1",
|
||||
"ws": "8.22.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@endo/far": "^1.1.14",
|
||||
"@endo/init": "^1.1.13",
|
||||
"@endo/ocapn": "1.1.1",
|
||||
"capnweb": "0.12.0"
|
||||
}
|
||||
}
|
||||
Generated
+477
@@ -8,10 +8,22 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@endo/far':
|
||||
specifier: ^1.1.14
|
||||
version: 1.1.14
|
||||
'@endo/init':
|
||||
specifier: ^1.1.13
|
||||
version: 1.1.13
|
||||
'@endo/ocapn':
|
||||
specifier: 1.1.1
|
||||
version: 1.1.1
|
||||
capnweb:
|
||||
specifier: 0.12.0
|
||||
version: 0.12.0
|
||||
devDependencies:
|
||||
esbuild:
|
||||
specifier: ^0.28.2
|
||||
version: 0.28.2
|
||||
workerd:
|
||||
specifier: 1.20261006.1
|
||||
version: 1.20261006.1
|
||||
@@ -51,9 +63,242 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@endo/base64@1.1.1':
|
||||
resolution: {integrity: sha512-6Aiy5oU/5/LYrHLbEPz0OWQ+CQsnAOC3qQYQ3nlwWi2JsU8fIZWjwfa+/+MSx8LTzITC250ZnMvtR0y/FIOQKw==}
|
||||
|
||||
'@endo/bytes@1.0.2':
|
||||
resolution: {integrity: sha512-gKG8v4c7SD/QzULlFl1OKBuUCCy+9H78xsCL40hZn1q2/hVCNAHrxPJFO6VbA8Nk8UJP6P+NjvZY+xdrfwytMQ==}
|
||||
|
||||
'@endo/cache-map@1.1.0':
|
||||
resolution: {integrity: sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw==}
|
||||
|
||||
'@endo/common@1.4.0':
|
||||
resolution: {integrity: sha512-+d0CCzEYUP4PZDVGAUHMNfCEBn/GOyq/39y33N4FFUb6VPNJosfKFK5bI38mTaqwr6gxLGbzm8aeiElz+NaSDQ==}
|
||||
|
||||
'@endo/env-options@1.1.11':
|
||||
resolution: {integrity: sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA==}
|
||||
|
||||
'@endo/errors@1.3.1':
|
||||
resolution: {integrity: sha512-AmR2DSiahQR66SHlEczNWe0eUotMYE2uDwxPj9fAY2l2lovwHWGzZKXNXdQP/u62c8vetU8mG98Mh5vRZEvFTg==}
|
||||
|
||||
'@endo/eventual-send@1.5.0':
|
||||
resolution: {integrity: sha512-mIRewS15mk+oV8y7kszyE0KWHZAlVoe98bEo/WJFJvBv1Rd83IjnofbuZcyWToWd70vgCxfkutWRJBK2Xtwd5A==}
|
||||
|
||||
'@endo/far@1.1.14':
|
||||
resolution: {integrity: sha512-OlM+yCNWVqhwSZa6MyjkVB0oQSTAQ15KoiDGL1qowtnTnhuGUbOXuaUAsDf4eAJZ4L+h2KsJOUdyYEuhNsDPLg==}
|
||||
|
||||
'@endo/harden@1.1.0':
|
||||
resolution: {integrity: sha512-d0CF7EVqymx9YUbkRI2sj10NrMLhrpmsaq5aBbEEbWDFxNe7F/mM8Gv8zT+iHErKEfeMCId90n5mk665SnxrRA==}
|
||||
|
||||
'@endo/hex@1.1.1':
|
||||
resolution: {integrity: sha512-I2Q7roaHk0fE4JTj2GXfxHPR14qTFK9bOvp3nKf2Fk0r/CjgL4zFcHYolvqY15SRNmZDcywnQ+MBTW82b7BUpQ==}
|
||||
|
||||
'@endo/immutable-arraybuffer@2.0.0':
|
||||
resolution: {integrity: sha512-xvfJvo4CgETh6UjSKwhNsFLZrqrTf2k4ZFpcprEN2UtCQRSOk+CcLP70kFbrOesB2Z/c1z9MaUZDS1JY6IlnUg==}
|
||||
|
||||
'@endo/init@1.1.13':
|
||||
resolution: {integrity: sha512-7I94pHD0ax+WDj5dP1MKgzgSY6/GsdwCc/Ap2zFY4lzhCO944zQ4MChAW2yX28QKJK80VyJzqoW5BQ56ngEHHA==}
|
||||
|
||||
'@endo/lockdown@1.0.19':
|
||||
resolution: {integrity: sha512-pcc6TqTBlx43422gHFzS+FrUfyJlbDoWUrynp78Mv+FrIZv4947FPcfjlXolgwvO00alMlSYxfXrrXUV93ywaA==}
|
||||
|
||||
'@endo/marshal@1.10.0':
|
||||
resolution: {integrity: sha512-1ny6r0+XFpkpJNJEOApZQlFNB8FsDxzU2oCv1hpN1HjpszsKcTiyfXU/n/cJIs3KOOS06Tkt72pgPlasHpsVGA==}
|
||||
|
||||
'@endo/nat@5.2.0':
|
||||
resolution: {integrity: sha512-yTNaysErNbrAWV8H2oC4e83nytG8Vzcu6VrxOkSIJQt+IVS9BUlSa1KkiKrVwu2IcIC67gJIkolPq3SgMGDjfQ==}
|
||||
|
||||
'@endo/ocapn@1.1.1':
|
||||
resolution: {integrity: sha512-3VtHmyMsHwDPwcVusCTgtb8VXr539iZk+r8vzzOU6/cWyMhJuxRGOKR3r5EzDHVG4tY/R6tihoGt7QLnNDuceQ==}
|
||||
|
||||
'@endo/pass-style@1.8.2':
|
||||
resolution: {integrity: sha512-63kbFNT+mropjQ0wzCkjSR5uEOT8FAIk16qHqrE+ciqSjMVd2hsW6vx10dbyo1s2xtmvXduUiTl+LqtnPa2oSw==}
|
||||
|
||||
'@endo/promise-kit@1.2.1':
|
||||
resolution: {integrity: sha512-1nyXBOuUnVo/i47On8RCXPaT/1RIsSpJ1sGBClfZrcTtnsJfj8fElEVlJIE8NFsQiZPLNANYI+i/NYi837+s0Q==}
|
||||
engines: {node: '>=11.0'}
|
||||
|
||||
'@endo/stream@1.3.1':
|
||||
resolution: {integrity: sha512-v21VRzo5wjzw7XMHCWAmDS50hkUQi/m9C0MQTIAAqLm6w9+/Er9AEXl8FGIbz+VjtJWk5SNVbuqA51N7oBaLDQ==}
|
||||
|
||||
'@endo/syrup-frame@0.1.1':
|
||||
resolution: {integrity: sha512-+uha+zI05RHPlSNVI7uaB2KD1EQ9gKAMcOzj1eT3y7Ng/vcW1bo7clCVVo3t+dOFOc6vYSsXgKufi40qxXRgYQ==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.28.2':
|
||||
resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.28.2':
|
||||
resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.28.2':
|
||||
resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.28.2':
|
||||
resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.28.2':
|
||||
resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.28.2':
|
||||
resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.28.2':
|
||||
resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.28.2':
|
||||
resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.28.2':
|
||||
resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.28.2':
|
||||
resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.28.2':
|
||||
resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.28.2':
|
||||
resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.28.2':
|
||||
resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.28.2':
|
||||
resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.28.2':
|
||||
resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.28.2':
|
||||
resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.28.2':
|
||||
resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.28.2':
|
||||
resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.28.2':
|
||||
resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@noble/curves@1.9.7':
|
||||
resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@noble/hashes@1.8.0':
|
||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
capnweb@0.12.0:
|
||||
resolution: {integrity: sha512-jgZ/LMtMVTi+RVlooIslH78Gg23XbDTdvcLghWx3oz17D6u5GuJARt+uhZk/wcTo+f81eeabfnow4d3zLBj+JQ==}
|
||||
|
||||
esbuild@0.28.2:
|
||||
resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
ses@2.3.0:
|
||||
resolution: {integrity: sha512-qd3iWzDqGKllI2FmExKsWLrJwt+6COWb8jUdzaCn8Cq5OTeoXdQ62Gn/xLy0GWLORC3du6yBAVe/V0B0dLVKew==}
|
||||
|
||||
workerd@1.20261006.1:
|
||||
resolution: {integrity: sha512-8MadBuffZ5NdpLwZjUmsqT5QlaKXq4gjgT6wzcKeynZTICNXyePhc+5QVoKiK7+MMhFTLNrRO2KXLlpk8Q6BkA==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -88,8 +333,240 @@ snapshots:
|
||||
'@cloudflare/workerd-windows-64@1.20261006.1':
|
||||
optional: true
|
||||
|
||||
'@endo/base64@1.1.1': {}
|
||||
|
||||
'@endo/bytes@1.0.2':
|
||||
dependencies:
|
||||
'@endo/harden': 1.1.0
|
||||
'@endo/immutable-arraybuffer': 2.0.0
|
||||
|
||||
'@endo/cache-map@1.1.0': {}
|
||||
|
||||
'@endo/common@1.4.0':
|
||||
dependencies:
|
||||
'@endo/errors': 1.3.1
|
||||
'@endo/eventual-send': 1.5.0
|
||||
'@endo/harden': 1.1.0
|
||||
'@endo/promise-kit': 1.2.1
|
||||
|
||||
'@endo/env-options@1.1.11': {}
|
||||
|
||||
'@endo/errors@1.3.1':
|
||||
dependencies:
|
||||
'@endo/harden': 1.1.0
|
||||
ses: 2.3.0
|
||||
|
||||
'@endo/eventual-send@1.5.0':
|
||||
dependencies:
|
||||
'@endo/env-options': 1.1.11
|
||||
'@endo/harden': 1.1.0
|
||||
|
||||
'@endo/far@1.1.14':
|
||||
dependencies:
|
||||
'@endo/errors': 1.3.1
|
||||
'@endo/eventual-send': 1.5.0
|
||||
'@endo/pass-style': 1.8.2
|
||||
|
||||
'@endo/harden@1.1.0': {}
|
||||
|
||||
'@endo/hex@1.1.1':
|
||||
dependencies:
|
||||
'@endo/harden': 1.1.0
|
||||
|
||||
'@endo/immutable-arraybuffer@2.0.0': {}
|
||||
|
||||
'@endo/init@1.1.13':
|
||||
dependencies:
|
||||
'@endo/base64': 1.1.1
|
||||
'@endo/eventual-send': 1.5.0
|
||||
'@endo/harden': 1.1.0
|
||||
'@endo/lockdown': 1.0.19
|
||||
'@endo/promise-kit': 1.2.1
|
||||
|
||||
'@endo/lockdown@1.0.19':
|
||||
dependencies:
|
||||
ses: 2.3.0
|
||||
|
||||
'@endo/marshal@1.10.0':
|
||||
dependencies:
|
||||
'@endo/common': 1.4.0
|
||||
'@endo/env-options': 1.1.11
|
||||
'@endo/errors': 1.3.1
|
||||
'@endo/eventual-send': 1.5.0
|
||||
'@endo/harden': 1.1.0
|
||||
'@endo/nat': 5.2.0
|
||||
'@endo/pass-style': 1.8.2
|
||||
|
||||
'@endo/nat@5.2.0': {}
|
||||
|
||||
'@endo/ocapn@1.1.1':
|
||||
dependencies:
|
||||
'@endo/bytes': 1.0.2
|
||||
'@endo/eventual-send': 1.5.0
|
||||
'@endo/harden': 1.1.0
|
||||
'@endo/hex': 1.1.1
|
||||
'@endo/init': 1.1.13
|
||||
'@endo/marshal': 1.10.0
|
||||
'@endo/nat': 5.2.0
|
||||
'@endo/pass-style': 1.8.2
|
||||
'@endo/promise-kit': 1.2.1
|
||||
'@endo/stream': 1.3.1
|
||||
'@endo/syrup-frame': 0.1.1
|
||||
'@noble/curves': 1.9.7
|
||||
'@noble/hashes': 1.8.0
|
||||
ws: 8.22.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@endo/pass-style@1.8.2':
|
||||
dependencies:
|
||||
'@endo/common': 1.4.0
|
||||
'@endo/env-options': 1.1.11
|
||||
'@endo/errors': 1.3.1
|
||||
'@endo/eventual-send': 1.5.0
|
||||
'@endo/harden': 1.1.0
|
||||
'@endo/promise-kit': 1.2.1
|
||||
|
||||
'@endo/promise-kit@1.2.1':
|
||||
dependencies:
|
||||
'@endo/harden': 1.1.0
|
||||
|
||||
'@endo/stream@1.3.1':
|
||||
dependencies:
|
||||
'@endo/eventual-send': 1.5.0
|
||||
'@endo/harden': 1.1.0
|
||||
'@endo/promise-kit': 1.2.1
|
||||
ses: 2.3.0
|
||||
|
||||
'@endo/syrup-frame@0.1.1':
|
||||
dependencies:
|
||||
'@endo/harden': 1.1.0
|
||||
'@endo/init': 1.1.13
|
||||
'@endo/promise-kit': 1.2.1
|
||||
'@endo/stream': 1.3.1
|
||||
|
||||
'@esbuild/aix-ppc64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.28.2':
|
||||
optional: true
|
||||
|
||||
'@noble/curves@1.9.7':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
||||
'@noble/hashes@1.8.0': {}
|
||||
|
||||
capnweb@0.12.0: {}
|
||||
|
||||
esbuild@0.28.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.28.2
|
||||
'@esbuild/android-arm': 0.28.2
|
||||
'@esbuild/android-arm64': 0.28.2
|
||||
'@esbuild/android-x64': 0.28.2
|
||||
'@esbuild/darwin-arm64': 0.28.2
|
||||
'@esbuild/darwin-x64': 0.28.2
|
||||
'@esbuild/freebsd-arm64': 0.28.2
|
||||
'@esbuild/freebsd-x64': 0.28.2
|
||||
'@esbuild/linux-arm': 0.28.2
|
||||
'@esbuild/linux-arm64': 0.28.2
|
||||
'@esbuild/linux-ia32': 0.28.2
|
||||
'@esbuild/linux-loong64': 0.28.2
|
||||
'@esbuild/linux-mips64el': 0.28.2
|
||||
'@esbuild/linux-ppc64': 0.28.2
|
||||
'@esbuild/linux-riscv64': 0.28.2
|
||||
'@esbuild/linux-s390x': 0.28.2
|
||||
'@esbuild/linux-x64': 0.28.2
|
||||
'@esbuild/netbsd-arm64': 0.28.2
|
||||
'@esbuild/netbsd-x64': 0.28.2
|
||||
'@esbuild/openbsd-arm64': 0.28.2
|
||||
'@esbuild/openbsd-x64': 0.28.2
|
||||
'@esbuild/openharmony-arm64': 0.28.2
|
||||
'@esbuild/sunos-x64': 0.28.2
|
||||
'@esbuild/win32-arm64': 0.28.2
|
||||
'@esbuild/win32-ia32': 0.28.2
|
||||
'@esbuild/win32-x64': 0.28.2
|
||||
|
||||
ses@2.3.0:
|
||||
dependencies:
|
||||
'@endo/cache-map': 1.1.0
|
||||
'@endo/env-options': 1.1.11
|
||||
'@endo/immutable-arraybuffer': 2.0.0
|
||||
|
||||
workerd@1.20261006.1:
|
||||
optionalDependencies:
|
||||
'@cloudflare/workerd-darwin-64': 1.20261006.1
|
||||
|
||||
+157
-10
@@ -1,7 +1,12 @@
|
||||
/** @file toybox supervisor: vats with code installed at runtime, each app a facet; ingress by mount, egress by grant. */
|
||||
import "@endo/init";
|
||||
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
||||
import { newWebSocketRpcSession } from "capnweb";
|
||||
import { RpcTarget, newWebSocketRpcSession, newWorkersRpcResponse } from "capnweb";
|
||||
import { E, Far } from "@endo/far";
|
||||
import { makeClient, swissnumFromBytes } from "@endo/ocapn";
|
||||
import { carries, conflict, mergeLabels, urlLabels } from "./labels.js";
|
||||
import { locationOf, makeRpcNetLayer } from "./netlayer.js";
|
||||
import VAT_RUNTIME from "vat-runtime.txt";
|
||||
|
||||
/**
|
||||
* A stored grant: labels as JSON, or (before labels) a bare `host[:port]`.
|
||||
@@ -35,6 +40,8 @@ export class Vat extends DurableObject {
|
||||
ctx.storage.sql.exec(
|
||||
"CREATE TABLE IF NOT EXISTS incarnation (k INTEGER PRIMARY KEY CHECK (k = 0), n INTEGER, egress TEXT)",
|
||||
);
|
||||
// absent: an app vat (exports class App), from before OCapN vats
|
||||
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS kind (k INTEGER PRIMARY KEY CHECK (k = 0), kind TEXT)");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,42 +49,65 @@ export class Vat extends DurableObject {
|
||||
*
|
||||
* @param {string} src an ES module exporting `class App extends DurableObject`
|
||||
* @param {string | null} egress the app's grant: egress labels as JSON (see labels.js), or none
|
||||
* @param {"app" | "ocapn"} kind `app`: `src` exports `class App`; `ocapn`:
|
||||
* `src` exports `buildRootObject`, run by vat-runtime.js
|
||||
* @returns {Promise<string>} the code's SHA-256, hex
|
||||
*/
|
||||
async start(src, egress) {
|
||||
async start(src, egress, kind = "app") {
|
||||
const hash = hex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(src)));
|
||||
const { sql } = this.ctx.storage;
|
||||
sql.exec("INSERT OR IGNORE INTO bundle VALUES (?, ?)", hash, src);
|
||||
sql.exec("INSERT OR REPLACE INTO current VALUES (0, ?)", hash);
|
||||
const [prev] = sql.exec("SELECT n FROM incarnation").toArray();
|
||||
sql.exec("INSERT OR REPLACE INTO incarnation VALUES (0, ?, ?)", (prev?.n ?? 0) + 1, egress);
|
||||
sql.exec("INSERT OR REPLACE INTO kind VALUES (0, ?)", kind);
|
||||
this.ctx.facets.abort("app", new Error("upgraded"));
|
||||
return hash;
|
||||
}
|
||||
|
||||
#app({ hash, src, n, egress }) {
|
||||
#app({ src, n, egress, kind }) {
|
||||
return this.ctx.facets.get("app", () => {
|
||||
const env = egress ? { egress: this.ctx.exports.Egress({ props: { labels: grantLabels(egress) } }) } : {};
|
||||
const code =
|
||||
kind === "ocapn"
|
||||
? {
|
||||
mainModule: "runtime.js",
|
||||
modules: { "runtime.js": VAT_RUNTIME, "vat.js": src },
|
||||
env: { ...env, vatId: this.ctx.id.toString(), router: this.ctx.exports.Router({ props: {} }) },
|
||||
}
|
||||
: { mainModule: "app.js", modules: { "app.js": src }, env };
|
||||
const worker = this.env.LOADER.get(`${this.ctx.id}:${n}`, () => ({
|
||||
compatibilityDate: "2026-10-01",
|
||||
mainModule: "app.js",
|
||||
modules: { "app.js": src },
|
||||
env: egress ? { egress: this.ctx.exports.Egress({ props: { labels: grantLabels(egress) } }) } : {},
|
||||
...code,
|
||||
globalOutbound: null,
|
||||
}));
|
||||
return { class: worker.getDurableObjectClass("App") };
|
||||
});
|
||||
}
|
||||
|
||||
fetch(request) {
|
||||
#current() {
|
||||
const [row] = this.ctx.storage.sql
|
||||
.exec(
|
||||
`SELECT b.hash, b.src, coalesce(i.n, 0) AS n, i.egress
|
||||
FROM current c JOIN bundle b USING (hash) LEFT JOIN incarnation i ON i.k = 0`,
|
||||
`SELECT b.hash, b.src, coalesce(i.n, 0) AS n, i.egress, coalesce(k.kind, 'app') AS kind
|
||||
FROM current c JOIN bundle b USING (hash)
|
||||
LEFT JOIN incarnation i ON i.k = 0 LEFT JOIN kind k ON k.k = 0`,
|
||||
)
|
||||
.toArray();
|
||||
return row;
|
||||
}
|
||||
|
||||
fetch(request) {
|
||||
const row = this.#current();
|
||||
if (!row) return new Response("no code installed\n", { status: 404 });
|
||||
return this.#app(row).fetch(request);
|
||||
}
|
||||
|
||||
/** An OCapN vat's gateway, for the router. */
|
||||
gateway() {
|
||||
const row = this.#current();
|
||||
if (row?.kind !== "ocapn") throw new Error("not an OCapN vat");
|
||||
return this.#app(row).gateway();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,6 +159,119 @@ export class Egress extends WorkerEntrypoint {
|
||||
}
|
||||
}
|
||||
|
||||
/** The vatAdmin's designator, and the swissnum of its bootstrap object. */
|
||||
const VAT_ADMIN = "vatAdmin";
|
||||
/** The swissnum of an OCapN vat's bootstrap (see vat-runtime.js). */
|
||||
const VAT_SWISSNUM = "vat";
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* The vatAdmin: an OCapN peer, and the router through which every OCapN
|
||||
* peer here reaches every other. One instance, by name.
|
||||
*
|
||||
* Peers find each other by designator: the vatAdmin's is `vatAdmin`, a
|
||||
* vat's is its id, and a client registers its own when it connects to
|
||||
* the admin socket. Nothing here can dial out, so a handoff's receiver
|
||||
* reaches the exporter through this router.
|
||||
*/
|
||||
export class VatAdmin extends DurableObject {
|
||||
#clients = new Map();
|
||||
#ready;
|
||||
|
||||
constructor(ctx, env) {
|
||||
super(ctx, env);
|
||||
this.#ready = this.#boot();
|
||||
}
|
||||
|
||||
async #boot() {
|
||||
const vatAdmin = Far("vatAdmin", {
|
||||
/**
|
||||
* Create an OCapN vat running `src`; `params` (any passable,
|
||||
* remote objects included) reach its `buildRootObject`.
|
||||
*
|
||||
* @param {string} src an ES module exporting `buildRootObject(vatPowers, params)`
|
||||
*/
|
||||
createVat: async (src, params = {}) => {
|
||||
const id = this.env.VATS.newUniqueId();
|
||||
await this.env.VATS.get(id).start(src, null, "ocapn");
|
||||
const vatBootstrap = await this.#enliven(id.toString(), VAT_SWISSNUM);
|
||||
const root = await E(vatBootstrap).start(params);
|
||||
return harden({ id: id.toString(), root });
|
||||
},
|
||||
});
|
||||
this.ocapn = makeClient({ debugLabel: VAT_ADMIN, swissnumTable: new Map([[VAT_ADMIN, vatAdmin]]) });
|
||||
const netlayer = await this.ocapn.registerNetlayer(
|
||||
makeRpcNetLayer({ designator: VAT_ADMIN, openGateway: (designator) => this.gateway(designator) }),
|
||||
);
|
||||
this.own = netlayer.gateway;
|
||||
}
|
||||
|
||||
#enliven(designator, swissnum) {
|
||||
return this.ocapn.enlivenSturdyRef(
|
||||
this.ocapn.makeSturdyRef(locationOf(designator), swissnumFromBytes(encoder.encode(swissnum))),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The gateway of the peer with this designator.
|
||||
*
|
||||
* @param {string} designator
|
||||
*/
|
||||
async gateway(designator) {
|
||||
await this.#ready;
|
||||
if (designator === VAT_ADMIN) return this.own;
|
||||
const client = this.#clients.get(designator);
|
||||
if (client) return client;
|
||||
let id;
|
||||
try {
|
||||
id = this.env.VATS.idFromString(designator);
|
||||
} catch {
|
||||
throw new Error("no such peer");
|
||||
}
|
||||
return this.env.VATS.get(id).gateway();
|
||||
}
|
||||
|
||||
/** 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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What an OCapN client on the admin socket gets: register its own
|
||||
* gateway under a designator, and reach any peer's.
|
||||
*/
|
||||
class AdminSession extends RpcTarget {
|
||||
#vatAdmin;
|
||||
#clients;
|
||||
|
||||
constructor(vatAdmin, clients) {
|
||||
super();
|
||||
this.#vatAdmin = vatAdmin;
|
||||
this.#clients = clients;
|
||||
}
|
||||
|
||||
/** @param {string} designator @param {any} gateway */
|
||||
register(designator, gateway) {
|
||||
if (designator === VAT_ADMIN || /^[0-9a-f]{64}$/.test(designator) || this.#clients.has(designator)) {
|
||||
throw new Error("designator taken");
|
||||
}
|
||||
this.#clients.set(designator, gateway.dup?.() ?? gateway);
|
||||
}
|
||||
|
||||
/** @param {string} designator */
|
||||
gateway(designator) {
|
||||
return this.#vatAdmin.gateway(designator);
|
||||
}
|
||||
}
|
||||
|
||||
/** A vat's way to the router: `env.router` in an OCapN vat. */
|
||||
export class Router extends WorkerEntrypoint {
|
||||
gateway(designator) {
|
||||
return this.env.VAT_ADMIN.getByName(VAT_ADMIN).gateway(designator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounts: which vat serves which label set. One instance, by name.
|
||||
*
|
||||
@@ -231,7 +374,8 @@ const grantParam = (url, name) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* The admin socket: `createVat` and its `adminNode`, over HTTP.
|
||||
* The admin socket: a WebSocket upgrade reaches the {@link VatAdmin} (OCapN
|
||||
* over Cap'n Web); otherwise, app vats over HTTP:
|
||||
*
|
||||
* - `POST /` with an ES module body creates a vat and starts it; the reply
|
||||
* is `{"vat": <id>, "hash": <code hash>}`, and the id addresses it here.
|
||||
@@ -245,6 +389,9 @@ const grantParam = (url, name) => {
|
||||
*/
|
||||
export class Admin extends WorkerEntrypoint {
|
||||
async fetch(request) {
|
||||
if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") {
|
||||
return this.env.VAT_ADMIN.getByName(VAT_ADMIN).fetch(request);
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
const [, first, second] = url.pathname.split("/");
|
||||
let id;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// @endo/init/debug.js, but with `overrideTaming: 'moderate'`: Node 24's
|
||||
// undici assigns `message` to its errors (ClientDestroyedError when a
|
||||
// WebSocket closes), which 'min' makes a TypeError.
|
||||
import '@endo/init/pre-remoting.js';
|
||||
|
||||
lockdown({
|
||||
errorTaming: 'unsafe',
|
||||
overrideTaming: 'moderate',
|
||||
domainTaming: 'unsafe',
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/** @file OCapN vats: a client on the admin socket creates a vat and passes it a remote object. */
|
||||
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { after, before, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { E, Far } from "@endo/far";
|
||||
import { makeClient, swissnumFromBytes } from "@endo/ocapn";
|
||||
import { newWebSocketRpcSession } from "capnweb";
|
||||
import WebSocket from "ws";
|
||||
import { locationOf, makeRpcNetLayer } from "../netlayer.js";
|
||||
import { freePort, here, startWorkerd } from "./workerd.js";
|
||||
|
||||
const vatSrc = (name) => readFile(join(here, "vats", name), "utf8");
|
||||
|
||||
/**
|
||||
* An OCapN client on the admin socket: registers its own gateway with the
|
||||
* router there, and reaches every other peer through it.
|
||||
*
|
||||
* @param {string} adminSock
|
||||
* @param {string} designator
|
||||
*/
|
||||
const connectClient = async (adminSock, designator) => {
|
||||
const ws = new WebSocket(`ws+unix:${adminSock}:/`);
|
||||
await new Promise((resolve, reject) => (ws.once("open", resolve), ws.once("error", reject)));
|
||||
const session = newWebSocketRpcSession(ws);
|
||||
const ocapn = makeClient({ debugLabel: designator, verbose: !!process.env.OCAPN_LOG });
|
||||
const netlayer = await ocapn.registerNetlayer(
|
||||
makeRpcNetLayer({ designator, openGateway: (d) => session.gateway(d) }),
|
||||
);
|
||||
await session.register(designator, netlayer.gateway);
|
||||
const vatAdmin = await ocapn.enlivenSturdyRef(
|
||||
ocapn.makeSturdyRef(locationOf("vatAdmin"), swissnumFromBytes(new TextEncoder().encode("vatAdmin"))),
|
||||
);
|
||||
return { vatAdmin, close: () => (ocapn.shutdown(), ws.close()) };
|
||||
};
|
||||
|
||||
let dir, server, client;
|
||||
|
||||
before(async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), "toybox-ocapn-"));
|
||||
await mkdir(join(dir, "state"));
|
||||
const port = await freePort();
|
||||
server = await startWorkerd(
|
||||
"config.capnp",
|
||||
[
|
||||
"-s", `http=127.0.0.1:${port}`, "-s", `admin=unix:${join(dir, "admin.sock")}`,
|
||||
"-d", `state=${join(dir, "state")}`, "-e", `switchboard=unix:${join(dir, "no-switchboard.sock")}`,
|
||||
],
|
||||
{ port, host: "127.0.0.1" },
|
||||
[join(dir, "admin.sock")],
|
||||
);
|
||||
client = await connectClient(join(dir, "admin.sock"), "client-1");
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
client?.close();
|
||||
await server?.stop();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("createVat passes a remote object in; the vat's root comes back", async () => {
|
||||
const carol = Far("carol", { greet: (name) => `hi ${name}, from carol` });
|
||||
const { id, root } = await E(client.vatAdmin).createVat(await vatSrc("greeter.js"), { carol });
|
||||
assert.match(id, /^[0-9a-f]{64}$/);
|
||||
assert.equal(await E(root).hello(), "hello from the vat");
|
||||
assert.equal(await E(root).greetVia("vat"), "hi vat, from carol");
|
||||
});
|
||||
+1
-46
@@ -1,34 +1,14 @@
|
||||
/** @file toybox end to end: a real workerd, an admin socket, a public port, a switchboard with two handles. */
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { createServer as createHttpServer, request } from "node:http";
|
||||
import { connect, createServer } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { after, before, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { WebSocketServer } from "ws";
|
||||
|
||||
const here = new URL("..", import.meta.url).pathname;
|
||||
// the package binary, not the .bin shim: the shim forks it, so a kill would orphan workerd
|
||||
const workerd = join(here, "node_modules/workerd/bin/workerd");
|
||||
import { freePort, here, startWorkerd } from "./workerd.js";
|
||||
const app = (name) => readFile(join(here, "apps", name), "utf8");
|
||||
|
||||
const freePort = () =>
|
||||
new Promise((resolve) => {
|
||||
const s = createServer().listen(0, "127.0.0.1", () => {
|
||||
const { port } = s.address();
|
||||
s.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
|
||||
/** Resolves once something accepts connections at `opts` (a port or a socket path). */
|
||||
const accepting = (opts) =>
|
||||
new Promise((resolve) => {
|
||||
const s = connect(opts, () => (s.end(), resolve(true)));
|
||||
s.on("error", () => resolve(false));
|
||||
});
|
||||
|
||||
/**
|
||||
* The admin socket: holding it is the authority to create and upgrade vats.
|
||||
*
|
||||
@@ -67,31 +47,6 @@ const makePublic = (base) => ({
|
||||
put: async (pathAndQuery, src) => (await fetch(`${base}${pathAndQuery}`, { method: "PUT", body: src })).status,
|
||||
});
|
||||
|
||||
/**
|
||||
* Run workerd on `config` until `stop()`; resolves once `listening` accepts.
|
||||
*
|
||||
* @param {string} config
|
||||
* @param {string[]} args socket, directory, and external overrides
|
||||
* @param {{ port: number } | { path: string }} listening
|
||||
* @param {string[]} stale socket files a crash leaves behind; bind() would fail on them
|
||||
*/
|
||||
const startWorkerd = async (config, args, listening, stale = []) => {
|
||||
for (const f of stale) await rm(f, { force: true });
|
||||
const child = spawn(workerd, ["serve", "--experimental", join(here, config), ...args], {
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
let log = "";
|
||||
child.stderr.on("data", (c) => (log += c));
|
||||
const exited = new Promise((resolve) => child.on("exit", resolve));
|
||||
for (let i = 0; i < 100; i++) {
|
||||
// SIGKILL: a crash is the harder durability test
|
||||
if (await accepting(listening)) return { stop: () => (child.kill("SIGKILL"), exited) };
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
child.kill("SIGKILL");
|
||||
throw new Error(`workerd did not start: ${log}`);
|
||||
};
|
||||
|
||||
/** An HTTP server that answers GET with "plain ok", echoes WebSocket messages, and counts requests. */
|
||||
const startEcho = async () => {
|
||||
let hits = 0;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/** @file Run real workerd processes for tests. */
|
||||
import { spawn } from "node:child_process";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { connect, createServer } from "node:net";
|
||||
import { join } from "node:path";
|
||||
|
||||
export const here = new URL("..", import.meta.url).pathname;
|
||||
// the package binary, not the .bin shim: the shim forks it, so a kill would orphan workerd
|
||||
const workerd = join(here, "node_modules/workerd/bin/workerd");
|
||||
|
||||
export const freePort = () =>
|
||||
new Promise((resolve) => {
|
||||
const s = createServer().listen(0, "127.0.0.1", () => {
|
||||
const { port } = s.address();
|
||||
s.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
|
||||
/** Resolves once something accepts connections at `opts` (a port or a socket path). */
|
||||
export const accepting = (opts) =>
|
||||
new Promise((resolve) => {
|
||||
const s = connect(opts, () => (s.end(), resolve(true)));
|
||||
s.on("error", () => resolve(false));
|
||||
});
|
||||
|
||||
/**
|
||||
* Run workerd on `config` until `stop()`; resolves once `listening` accepts.
|
||||
*
|
||||
* @param {string} config
|
||||
* @param {string[]} args socket, directory, and external overrides
|
||||
* @param {{ port: number } | { path: string }} listening
|
||||
* @param {string[]} stale socket files a crash leaves behind; bind() would fail on them
|
||||
*/
|
||||
export const startWorkerd = async (config, args, listening, stale = []) => {
|
||||
for (const f of stale) await rm(f, { force: true });
|
||||
const child = spawn(workerd, ["serve", "--experimental", join(here, config), ...args], {
|
||||
stdio: ["ignore", process.env.WORKERD_LOG ? "inherit" : "ignore", "pipe"],
|
||||
});
|
||||
let log = "";
|
||||
// WORKERD_LOG=1 echoes workerd's stderr, for debugging
|
||||
child.stderr.on("data", (c) => (process.env.WORKERD_LOG ? process.stderr.write(c) : (log += c)));
|
||||
const exited = new Promise((resolve) => child.on("exit", resolve));
|
||||
for (let i = 0; i < 100; i++) {
|
||||
// SIGKILL: a crash is the harder durability test
|
||||
if (await accepting(listening)) return { stop: () => (child.kill("SIGKILL"), exited) };
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
child.kill("SIGKILL");
|
||||
throw new Error(`workerd did not start: ${log}`);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @file The runtime of an OCapN vat: SES, an OCapN peer, and the vat's own
|
||||
* module (`vat.js`), which exports `buildRootObject(vatPowers, params)`.
|
||||
*
|
||||
* Bundled (see build.js) and loaded as the facet's main module beside the
|
||||
* vat's code. The vat's designator is its id; it reaches other peers, and
|
||||
* they reach it, through the vatAdmin's router (`env.router`).
|
||||
*/
|
||||
import "@endo/init";
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
import { E, Far } from "@endo/far";
|
||||
import { makeClient } from "@endo/ocapn";
|
||||
import { makeRpcNetLayer } from "./netlayer.js";
|
||||
import * as vat from "./vat.js";
|
||||
|
||||
/**
|
||||
* The swissnum of a vat's bootstrap: the vatAdmin starts the vat through it.
|
||||
* Not exported: workerd takes every export of a main module for an entrypoint.
|
||||
*/
|
||||
const VAT_SWISSNUM = "vat";
|
||||
|
||||
export class App extends DurableObject {
|
||||
#gateway;
|
||||
|
||||
constructor(ctx, env) {
|
||||
super(ctx, env);
|
||||
this.#gateway = this.#boot();
|
||||
}
|
||||
|
||||
async #boot() {
|
||||
const { vatId, router } = this.env;
|
||||
let root;
|
||||
const vatPowers = Object.freeze({ E, Far, storage: this.ctx.storage.kv });
|
||||
const bootstrap = Far("vatBootstrap", {
|
||||
start: (params) => {
|
||||
root ??= vat.buildRootObject(vatPowers, params);
|
||||
return root;
|
||||
},
|
||||
});
|
||||
const ocapn = makeClient({ debugLabel: vatId.slice(0, 8), swissnumTable: new Map([[VAT_SWISSNUM, bootstrap]]) });
|
||||
const netlayer = await ocapn.registerNetlayer(
|
||||
makeRpcNetLayer({ designator: vatId, openGateway: (designator) => router.gateway(designator) }),
|
||||
);
|
||||
return netlayer.gateway;
|
||||
}
|
||||
|
||||
/** This vat's OCapN gateway, for the router. */
|
||||
gateway() {
|
||||
return this.#gateway;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** A vat that greets through the `carol` it was created with. */
|
||||
export const buildRootObject = ({ E, Far }, { carol }) =>
|
||||
Far("greeterRoot", {
|
||||
greetVia: (name) => E(carol).greet(name),
|
||||
hello: () => "hello from the vat",
|
||||
});
|
||||
Reference in new issue
Block a user