WIP: toybox: OCapN vats on workerd, with ingress and egress as capabilities #4

Draft
dckc wants to merge 13 commits from toybox into main
pull from: toybox
13 changed files with 983 additions and 61 deletions
Showing only changes of commit 9356f63c64 - Show all commits

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
state/
admin.sock
switchboard-*.sock
dist/
+16
View File
@@ -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
View File
@@ -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"),
],
+133
View File
@@ -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
View File
@@ -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"
}
}
+477
View File
@@ -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
View File
@@ -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;
+10
View File
@@ -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',
});
+68
View File
@@ -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
View File
@@ -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;
+51
View File
@@ -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}`);
};
+51
View File
@@ -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;
}
}
+6
View File
@@ -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",
});