Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea923c40c4 | ||
|
|
47fcb47d5c | ||
|
|
91679e8cc8 | ||
|
|
1f7aa80130 | ||
|
|
6227d64b08 | ||
|
|
988e10263a | ||
|
|
8e55a11c4a | ||
|
|
b0d9453a51 | ||
|
|
009c5fcb4d | ||
|
|
d6efe5622d | ||
|
|
99b6b5110f |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
|
||||
"sourceCommit": "54e443af5dd963f1fa6f83f062f4943b46ad2535",
|
||||
"sourceCommit": "039f055a7dcff2a29d59a8c37d7433b269b50f7a",
|
||||
"sourcePath": "quixos-instance/quixos-nix-helpers",
|
||||
"exportName": "quixos-nix-helpers",
|
||||
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
|
||||
|
||||
+77
-2
@@ -8,9 +8,9 @@ receipts. It has no migration step or package-defined shell hooks.
|
||||
```nix
|
||||
# Inside a per-system flake output:
|
||||
quixosPackages.checkedArtifacts =
|
||||
{ plan, generator, core, sharedRuntime, typescript, packageRevisionId }:
|
||||
{ buildContract, sourceDigest, generator, core, sharedRuntime, typescript, packageRevisionId, ... }:
|
||||
helpers.mkCaminoArtifacts {
|
||||
inherit pkgs plan core sharedRuntime typescript packageRevisionId;
|
||||
inherit pkgs buildContract sourceDigest core sharedRuntime typescript packageRevisionId;
|
||||
protocol = generator;
|
||||
src = ./.;
|
||||
portable = {
|
||||
@@ -22,6 +22,25 @@ quixosPackages.checkedArtifacts =
|
||||
};
|
||||
```
|
||||
|
||||
`buildContract` is a canonical, source-free capability slice. `sourceDigest`
|
||||
records the exact selected package source in the final installation manifest;
|
||||
it does not enter the reusable compilation group. Ordinary source dependencies,
|
||||
SDKs and React props companions remain explicit Nix inputs. Raw QX and resource
|
||||
lock files enter through the checked contract and evaluated recipe, and are
|
||||
excluded from the implementation source tree. The `plan` argument on the low-level
|
||||
helper is a composition convenience for fixtures; it is projected before building.
|
||||
Package recipes receive the contract directly and can be checked without a workspace.
|
||||
|
||||
Inside `checkedArtifacts = context: ...`, the package helpers expose
|
||||
`camino.packageSet { inherit nixpkgs system context; }` as the usual package set
|
||||
selection. It shares the workspace composition's evaluated Nixpkgs when the
|
||||
source content, system, and default import arguments match. Pass ordinary Nixpkgs
|
||||
arguments such as `config` or `overlays` to use a separate import. A direct
|
||||
`import nixpkgs { inherit system; }` also works, with additional evaluation cost.
|
||||
The implementation groups depend on the complete execution compiler bundle and
|
||||
JavaScript build drivers by content; unrelated checker CLI and publication
|
||||
metadata changes do not enter those inputs.
|
||||
|
||||
The handler entry exports `registry = createRegistry(handlers)` from the generated
|
||||
bindings. One registry contains many exports. The builder always supplies a server
|
||||
implementation for the generated process service. A service-only package instead
|
||||
@@ -72,3 +91,59 @@ proves that a component-only edit preserves a mixed package's service digest.
|
||||
Run `nix run .#camino-component-source-qualification` from the monorepo to
|
||||
materialize separate retained Git resources and compile an ordinary Nix candidate.
|
||||
This is a source/build qualification harness; it does not deploy a workspace.
|
||||
|
||||
## Restricted execution in a workspace VM
|
||||
|
||||
Add `service.restricted = true` to a checked portable package to select the pinned
|
||||
`workerd` executor. Generated bindings and the ordinary transaction-port protocol
|
||||
stay the same. All server exports must belong to one checked portable registry;
|
||||
remote exports and custom process services retain their ordinary VM execution
|
||||
path. The restricted build rejects Node imports and unresolved dependencies,
|
||||
and supplies an explicit SDK for value codecs, identities and declared graph ports.
|
||||
|
||||
```nix
|
||||
service = {
|
||||
restricted = true;
|
||||
limits = {
|
||||
deadlineMs = 4000;
|
||||
maxBytes = 1048576;
|
||||
maxPortCalls = 1000;
|
||||
maxQueued = 8;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Each invocation gets a fresh engine process. The adapter admits one active frame
|
||||
and a bounded FIFO; queue wait consumes the deadline. Overflow is an explicit
|
||||
`RESTRICTED_QUEUE_FULL` result. Cancellation kills the engine process group and
|
||||
removes its sockets before acknowledgement. All graph calls inherit the trusted
|
||||
invocation identity; authored code cannot choose another frame or workspace.
|
||||
Native graph grants and transaction commit remain enforced by Camino outside
|
||||
the guest. Initialization and synchronous loops are subject to the same deadline.
|
||||
|
||||
Direct network access, native filesystem imports, ambient clock and entropy,
|
||||
parent environment credentials, and unlisted modules are denied. Explicit dates,
|
||||
`Intl.Segmenter`, deterministic hashes and normal JavaScript computation remain
|
||||
available. Host-managed HTTP bridges expose only the current frame's declared
|
||||
graph ports. Authored serialization is untrusted protocol input.
|
||||
|
||||
This is an inner execution restriction, **not a hardened shared-process sandbox**.
|
||||
One workspace owns the surrounding VM. Systemd limits the entire package cgroup,
|
||||
including adapter and child engines; memory exhaustion terminates that group.
|
||||
No per-isolate memory protection is claimed. A guest user can control its VM,
|
||||
so authoritative graph grants and credentials must remain outside it.
|
||||
|
||||
Qualification commands:
|
||||
|
||||
```sh
|
||||
nix build .#checks.x86_64-linux.camino-restricted-execution
|
||||
nix build .#checks.x86_64-linux.camino-restricted-package-runners
|
||||
nix build .#checks.x86_64-linux.camino-restricted-execution-vm
|
||||
```
|
||||
|
||||
The first uses the actual pinned engine for frame isolation, graph ports,
|
||||
SDK codecs, denied ambient access, cancellation and overload. The second runs
|
||||
ordinary package dispatch, rollback, PostgreSQL and browser replica tests with
|
||||
the restricted reader fixture. The VM check adds actual cgroup allocation
|
||||
exhaustion, neighboring-service survival, native socket egress rejection and
|
||||
fresh-frame timing. VM qualification is independent of hosted rollout.
|
||||
|
||||
+73
-8
@@ -2,7 +2,25 @@
|
||||
{
|
||||
pkgs,
|
||||
src,
|
||||
plan,
|
||||
plan ? null,
|
||||
buildContract ?
|
||||
(import ./package-context.nix {
|
||||
inherit
|
||||
pkgs
|
||||
protocol
|
||||
plan
|
||||
packageRevisionId
|
||||
;
|
||||
}).buildContract,
|
||||
sourceDigest ?
|
||||
(import ./package-context.nix {
|
||||
inherit
|
||||
pkgs
|
||||
protocol
|
||||
plan
|
||||
packageRevisionId
|
||||
;
|
||||
}).sourceDigest,
|
||||
protocol,
|
||||
core,
|
||||
sharedRuntime,
|
||||
@@ -16,19 +34,25 @@
|
||||
group ? false,
|
||||
}:
|
||||
let
|
||||
compiler = "${protocol}/libexec/quixos-protocol/execution-world.mjs";
|
||||
buildTools = import ./build-tools.nix;
|
||||
# The standalone bundle is the complete code dependency of this driver.
|
||||
# Changes to unrelated checker CLIs must not invalidate every package group.
|
||||
compiler = builtins.path {
|
||||
path = "${protocol}/libexec/quixos-protocol/execution-world.mjs";
|
||||
name = "execution-world.mjs";
|
||||
};
|
||||
config = pkgs.writeText "camino-artifact-build.json" (
|
||||
builtins.toJSON {
|
||||
inherit
|
||||
plan
|
||||
buildContract
|
||||
compiler
|
||||
packageRevisionId
|
||||
core
|
||||
sharedRuntime
|
||||
nodeModules
|
||||
service
|
||||
group
|
||||
;
|
||||
group = true;
|
||||
components =
|
||||
if components == null then
|
||||
null
|
||||
@@ -39,7 +63,10 @@ let
|
||||
bindingOutput = components.bindingOutput or "src/gen/client.ts";
|
||||
contracts = components.contracts or [ ];
|
||||
};
|
||||
source = src;
|
||||
source = import ./implementation-source.nix {
|
||||
inherit (pkgs) lib;
|
||||
inherit src;
|
||||
};
|
||||
portable =
|
||||
if portable == null then
|
||||
null
|
||||
@@ -69,9 +96,47 @@ let
|
||||
esbuild = "${pkgs.esbuild}/bin/esbuild";
|
||||
shell = "${pkgs.runtimeShell}";
|
||||
serviceAdapter = ./portable-service.mjs;
|
||||
restricted =
|
||||
if service.restricted or false then
|
||||
let
|
||||
executor = import ./restricted-execution { inherit pkgs core; };
|
||||
in
|
||||
{
|
||||
runtime = "${executor.runtime}/bin/quixos-restricted-registry";
|
||||
sdk = "${executor.sdk}/core.mjs";
|
||||
limits = service.limits or { };
|
||||
}
|
||||
else
|
||||
null;
|
||||
}
|
||||
);
|
||||
compiled =
|
||||
pkgs.runCommand "camino-compiled-artifacts"
|
||||
{
|
||||
passthru.buildConfig = config;
|
||||
QUIXOS_TRACE_PROFILE = "1";
|
||||
}
|
||||
''
|
||||
${pkgs.nodejs_24}/bin/node ${buildTools}/build-artifacts.mjs ${config}
|
||||
'';
|
||||
in
|
||||
pkgs.runCommand "camino-checked-artifacts" { passthru.buildConfig = config; } ''
|
||||
${pkgs.nodejs_24}/bin/node ${./.}/build-artifacts.mjs ${config}
|
||||
''
|
||||
if group then
|
||||
compiled
|
||||
else
|
||||
(import ./assemble-package.nix {
|
||||
inherit pkgs;
|
||||
artifacts = [ compiled ];
|
||||
context = {
|
||||
inherit
|
||||
buildContract
|
||||
sourceDigest
|
||||
packageRevisionId
|
||||
sharedRuntime
|
||||
;
|
||||
generator = protocol;
|
||||
};
|
||||
})
|
||||
// {
|
||||
caminoGroup = compiled;
|
||||
buildConfig = config;
|
||||
}
|
||||
|
||||
+14
-20
@@ -6,9 +6,10 @@ import { buildService } from "./build-service.mjs";
|
||||
|
||||
const config = JSON.parse(await fs.readFile(process.argv[2], "utf8"));
|
||||
const compiler = await import(pathToFileURL(config.compiler));
|
||||
const plan = JSON.parse(await fs.readFile(config.plan, "utf8"));
|
||||
const pkg = plan.definition.packages.find((pkg) => pkg.revision === config.packageRevisionId);
|
||||
if (!pkg || compiler.hexDigest(plan.definition) !== plan.digest) throw Error("Invalid package assembly context");
|
||||
const contract = JSON.parse(await fs.readFile(config.buildContract, "utf8"));
|
||||
compiler.checkPackageBuildContract(contract);
|
||||
const pkg = contract.definition.package;
|
||||
if (pkg.revision !== config.packageRevisionId) throw Error("Invalid package assembly context");
|
||||
const output = process.env.out;
|
||||
if (!output) throw Error("Package assembly requires an output directory");
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
@@ -23,18 +24,17 @@ const recordFile = async (name, mediaType) => {
|
||||
};
|
||||
await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true });
|
||||
for (const root of config.groups) {
|
||||
const manifest = compiler.parsePackageArtifacts(
|
||||
const manifest = compiler.parseArtifactGroup(
|
||||
JSON.parse(await fs.readFile(path.join(root, "share/quixos/package-artifacts.json"), "utf8")),
|
||||
);
|
||||
if (
|
||||
manifest.packageRevisionId !== pkg.revision ||
|
||||
manifest.bindingDigest !== pkg.bindingDigest ||
|
||||
manifest.sourceDigest !== pkg.sourceDigest ||
|
||||
JSON.stringify(manifest.versions) !== JSON.stringify(plan.definition.versions)
|
||||
manifest.bindingDigest !== contract.digest ||
|
||||
JSON.stringify(manifest.versions) !== JSON.stringify(contract.definition.versions)
|
||||
)
|
||||
throw Error("Artifact group belongs to a different checked package");
|
||||
const receipt = JSON.parse(await fs.readFile(path.join(root, "share/quixos/build-receipt.json"), "utf8"));
|
||||
const expected = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest);
|
||||
const expected = compiler.expectedGroupReceipt(contract, manifest, generatorDigest);
|
||||
if (JSON.stringify(receipt) !== JSON.stringify(expected.receipt))
|
||||
throw Error("Artifact group has an invalid build receipt");
|
||||
const read = await compiler.artifactFileReader({ [pkg.revision]: root });
|
||||
@@ -80,24 +80,18 @@ if (!implementations.some((entry) => entry.compatibility === "quixos-service-v1"
|
||||
const manifest = compiler.parsePackageArtifacts({
|
||||
schemaVersion: 1,
|
||||
packageRevisionId: pkg.revision,
|
||||
bindingDigest: pkg.bindingDigest,
|
||||
sourceDigest: pkg.sourceDigest,
|
||||
versions: plan.definition.versions,
|
||||
bindingDigest: contract.digest,
|
||||
sourceDigest: config.sourceDigest,
|
||||
versions: contract.definition.versions,
|
||||
implementations,
|
||||
...(components.length ? { components } : {}),
|
||||
});
|
||||
const { receipt, sources } = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest);
|
||||
const { receipt, sources } = compiler.expectedPackageReceipt(contract, manifest, config.sourceDigest, generatorDigest);
|
||||
for (const [name, bytes] of sources) {
|
||||
const actual = await fs.readFile(path.join(output, name));
|
||||
if (!Buffer.from(bytes).equals(actual)) throw Error("Assembly lacks checked generated source " + name);
|
||||
}
|
||||
await fs.writeFile(path.join(output, "share/quixos/package-artifacts.json"), JSON.stringify(manifest, null, 2));
|
||||
await fs.writeFile(path.join(output, "share/quixos/build-receipt.json"), JSON.stringify(receipt, null, 2));
|
||||
// Check package coverage with the same public validator used at installation.
|
||||
const packagePlan = { definition: { ...plan.definition, packages: [pkg] }, digest: "" };
|
||||
packagePlan.digest = compiler.hexDigest(packagePlan.definition);
|
||||
await compiler.checkExecutionWorld(
|
||||
packagePlan,
|
||||
[manifest],
|
||||
await compiler.artifactFileReader({ [pkg.revision]: output }),
|
||||
);
|
||||
// Source selection is attested here; compilation groups retain their own receipt.
|
||||
compiler.checkPackageCoverage({ ...pkg, bindingDigest: contract.digest, sourceDigest: config.sourceDigest }, manifest);
|
||||
|
||||
+13
-3
@@ -4,12 +4,22 @@
|
||||
artifacts,
|
||||
}:
|
||||
let
|
||||
buildTools = import ./build-tools.nix;
|
||||
groups = map (artifact: artifact.caminoGroup or artifact) artifacts;
|
||||
compiler = builtins.path {
|
||||
path = "${context.generator}/libexec/quixos-protocol/execution-world.mjs";
|
||||
name = "execution-world.mjs";
|
||||
};
|
||||
config = pkgs.writeText "camino-package-assembly.json" (
|
||||
builtins.toJSON {
|
||||
inherit groups;
|
||||
inherit (context) plan packageRevisionId sharedRuntime;
|
||||
compiler = "${context.generator}/libexec/quixos-protocol/execution-world.mjs";
|
||||
inherit (context)
|
||||
buildContract
|
||||
sourceDigest
|
||||
packageRevisionId
|
||||
sharedRuntime
|
||||
;
|
||||
inherit compiler;
|
||||
node = "${pkgs.nodejs_24}/bin/node";
|
||||
esbuild = "${pkgs.esbuild}/bin/esbuild";
|
||||
shell = "${pkgs.runtimeShell}";
|
||||
@@ -25,5 +35,5 @@ pkgs.runCommand "camino-package"
|
||||
};
|
||||
}
|
||||
''
|
||||
${pkgs.nodejs_24}/bin/node ${./.}/assemble-package.mjs ${config}
|
||||
${pkgs.nodejs_24}/bin/node ${buildTools}/assemble-package.mjs ${config}
|
||||
''
|
||||
|
||||
+65
-50
@@ -1,4 +1,5 @@
|
||||
import { installDependencies } from "./install-dependencies.mjs";
|
||||
import { measureBuild } from "./build-profile.mjs";
|
||||
import { buildService } from "./build-service.mjs";
|
||||
import { buildComponents } from "./build-components.mjs";
|
||||
/** Trusted Nix build driver. No package-defined build hooks run between generation and witnessing. */
|
||||
@@ -10,9 +11,10 @@ import { execFileSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
const config = JSON.parse(await fs.readFile(process.argv[2], "utf8"));
|
||||
const compiler = await import(pathToFileURL(config.compiler));
|
||||
const plan = JSON.parse(await fs.readFile(config.plan, "utf8"));
|
||||
const pkg = plan.definition.packages.find((p) => p.revision === config.packageRevisionId);
|
||||
if (!pkg || compiler.hexDigest(plan.definition) !== plan.digest) throw Error("Invalid checked execution plan/package");
|
||||
const contract = JSON.parse(await fs.readFile(config.buildContract, "utf8"));
|
||||
compiler.checkPackageBuildContract(contract);
|
||||
const pkg = contract.definition.package;
|
||||
if (pkg.revision !== config.packageRevisionId) throw Error("Invalid build contract package");
|
||||
const output = process.env.out;
|
||||
if (!output) throw Error("Missing Nix output");
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
@@ -55,6 +57,7 @@ const targets = portable ? [...new Set([...portable.targets, "server"])] : [];
|
||||
const builds = targets.map((target) => ({ target, mode: target, settings: portable }));
|
||||
if (config.remote) builds.push({ target: "server", mode: "remote", settings: config.remote });
|
||||
for (const { target, mode, settings } of builds) {
|
||||
const executionTarget = target === "browser" ? "target:org.quixos.execution.javascript@1" : "server";
|
||||
if (!["browser", "server"].includes(target)) throw Error(`Unsupported JS build target ${target}`);
|
||||
const work = path.resolve(`work-${mode}`);
|
||||
await fs.cp(config.source, work, { recursive: true, dereference: false });
|
||||
@@ -85,53 +88,66 @@ for (const { target, mode, settings } of builds) {
|
||||
const bindingPath = relative(settings.bindingOutput);
|
||||
const source =
|
||||
mode === "remote"
|
||||
? compiler.generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision)
|
||||
: compiler.generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target });
|
||||
? compiler.generateRemoteTypeScriptBindings(contract.definition.bindings, pkg.revision)
|
||||
: compiler.generatePortableTypeScriptBindings(contract.definition.bindings, pkg.revision, {
|
||||
target: executionTarget,
|
||||
});
|
||||
await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true });
|
||||
await fs.writeFile(path.join(work, bindingPath), source);
|
||||
generated.set(mode, { file: path.join(work, bindingPath), source });
|
||||
await fs.writeFile(path.join(output, "share/quixos/generated", `${mode}.ts`), source);
|
||||
await fs.writeFile(
|
||||
path.join(
|
||||
output,
|
||||
mode === "remote" ? "share/quixos/generated/remote.ts" : compiler.generatedBindingPath(executionTarget),
|
||||
),
|
||||
source,
|
||||
);
|
||||
const entry = relative(settings.entries?.[target] ?? settings.entry);
|
||||
execFileSync(
|
||||
config.node,
|
||||
[
|
||||
config.tsc,
|
||||
"--noEmit",
|
||||
"--strict",
|
||||
"--skipLibCheck",
|
||||
"false",
|
||||
"--target",
|
||||
"es2024",
|
||||
"--lib",
|
||||
"es2024,dom,esnext.disposable",
|
||||
"--module",
|
||||
"nodenext",
|
||||
"--moduleResolution",
|
||||
"nodenext",
|
||||
entry,
|
||||
],
|
||||
{ cwd: work, stdio: "inherit" },
|
||||
measureBuild("portable-typecheck", { package: pkg.revision, target, mode }, () =>
|
||||
execFileSync(
|
||||
config.node,
|
||||
[
|
||||
config.tsc,
|
||||
"--noEmit",
|
||||
"--strict",
|
||||
"--skipLibCheck",
|
||||
"false",
|
||||
"--target",
|
||||
"es2024",
|
||||
"--lib",
|
||||
"es2024,dom,esnext.disposable",
|
||||
"--module",
|
||||
"nodenext",
|
||||
"--moduleResolution",
|
||||
"nodenext",
|
||||
entry,
|
||||
],
|
||||
{ cwd: work, stdio: "inherit" },
|
||||
),
|
||||
);
|
||||
const bundlePath =
|
||||
mode === "remote" ? "share/quixos/remote/server/module.mjs" : `share/quixos/portable/${target}/module.mjs`;
|
||||
await fs.mkdir(path.dirname(path.join(output, bundlePath)), { recursive: true });
|
||||
const coreImport = target === "browser" ? coreURL : coreServer;
|
||||
const restricted = !!config.restricted && target === "server";
|
||||
const coreImport = target === "browser" ? coreURL : restricted ? config.restricted.sdk : coreServer;
|
||||
const metadata = path.join(work, "bundle-meta.json");
|
||||
execFileSync(
|
||||
config.esbuild,
|
||||
[
|
||||
entry,
|
||||
"--bundle",
|
||||
`--platform=${target === "browser" ? "browser" : "node"}`,
|
||||
"--format=esm",
|
||||
"--minify-whitespace",
|
||||
"--target=es2024",
|
||||
`--outfile=${path.join(output, bundlePath)}`,
|
||||
`--metafile=${metadata}`,
|
||||
`--alias:@quixos/camino-replica-core=${coreImport}`,
|
||||
`--external:${coreImport}`,
|
||||
],
|
||||
{ cwd: work, stdio: "inherit" },
|
||||
measureBuild("portable-bundle", { package: pkg.revision, target, mode }, () =>
|
||||
execFileSync(
|
||||
config.esbuild,
|
||||
[
|
||||
entry,
|
||||
"--bundle",
|
||||
`--platform=${target === "browser" || restricted ? "browser" : "node"}`,
|
||||
"--format=esm",
|
||||
"--minify-whitespace",
|
||||
"--target=es2024",
|
||||
`--outfile=${path.join(output, bundlePath)}`,
|
||||
`--metafile=${metadata}`,
|
||||
`--alias:@quixos/camino-replica-core=${coreImport}`,
|
||||
`--external:${coreImport}`,
|
||||
],
|
||||
{ cwd: work, stdio: "inherit" },
|
||||
),
|
||||
);
|
||||
const meta = JSON.parse(await fs.readFile(metadata, "utf8"));
|
||||
const built = Object.values(meta.outputs).find((o) => o.entryPoint);
|
||||
@@ -143,7 +159,7 @@ for (const { target, mode, settings } of builds) {
|
||||
(i) =>
|
||||
i.external &&
|
||||
i.path !== coreImport &&
|
||||
!(target === "server" && builtinModules.includes(i.path.replace(/^node:/, ""))),
|
||||
!(target === "server" && !restricted && builtinModules.includes(i.path.replace(/^node:/, ""))),
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -152,12 +168,12 @@ for (const { target, mode, settings } of builds) {
|
||||
.filter((e) =>
|
||||
mode === "remote"
|
||||
? e.execution.kind === "remote"
|
||||
: e.execution.kind !== "remote" && e.execution.targets.includes(target),
|
||||
: e.execution.kind !== "remote" && e.execution.targets.includes(executionTarget),
|
||||
)
|
||||
.map((e) => e.id);
|
||||
const body = {
|
||||
id: `${mode}-module`,
|
||||
target,
|
||||
target: executionTarget,
|
||||
compatibility: "quixos-js-module-v1",
|
||||
path: bundlePath,
|
||||
ports: "quixos-transaction-ports-v1",
|
||||
@@ -170,22 +186,21 @@ for (const { target, mode, settings } of builds) {
|
||||
const components =
|
||||
config.group && !config.components
|
||||
? []
|
||||
: await buildComponents({ config, compiler, plan, pkg, output, recordFile, generated });
|
||||
: await buildComponents({ config, compiler, contract, pkg, output, recordFile, generated });
|
||||
if (!config.group || config.service.entry)
|
||||
await buildService({ config, compiler, pkg, output, coreServer, sharedFiles, implementations, recordFile });
|
||||
|
||||
const manifest = compiler.parsePackageArtifacts({
|
||||
const manifest = compiler.parseArtifactGroup({
|
||||
schemaVersion: 1,
|
||||
packageRevisionId: pkg.revision,
|
||||
bindingDigest: pkg.bindingDigest,
|
||||
sourceDigest: pkg.sourceDigest,
|
||||
versions: plan.definition.versions,
|
||||
bindingDigest: contract.digest,
|
||||
versions: contract.definition.versions,
|
||||
implementations,
|
||||
...(components.length ? { components } : {}),
|
||||
});
|
||||
for (const { file, source } of generated.values())
|
||||
if ((await fs.readFile(file, "utf8")) !== source) throw Error("Generated bindings changed during build");
|
||||
const { receipt, sources } = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest);
|
||||
const { receipt, sources } = compiler.expectedGroupReceipt(contract, manifest, generatorDigest);
|
||||
for (const [file, bytes] of sources)
|
||||
if (!Buffer.from(bytes).equals(await fs.readFile(path.join(output, file))))
|
||||
throw Error("Receipt generation differs from compiled bindings");
|
||||
|
||||
+60
-58
@@ -1,10 +1,13 @@
|
||||
import { installDependencies } from "./install-dependencies.mjs";
|
||||
import { checkComponentTypes } from "./component-types.mjs";
|
||||
import { measureBuild } from "./build-profile.mjs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
export async function buildComponents({ config, compiler, plan, pkg, output, recordFile, generated }) {
|
||||
export async function buildComponents({ config, compiler, contract, pkg, output, recordFile, generated }) {
|
||||
const settings = config.components;
|
||||
if (!settings) {
|
||||
if (pkg.components?.length) throw Error("Declared components require checked component build settings");
|
||||
@@ -100,12 +103,14 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
|
||||
await fs.writeFile(
|
||||
shim,
|
||||
`export * from ${JSON.stringify(url)};` +
|
||||
(name === "@quixos/camino-react" ? "" : `export {default} from ${JSON.stringify(url)};`),
|
||||
(["@quixos/camino-react", "@quixos/camino-react/ui"].includes(name)
|
||||
? ""
|
||||
: `export {default} from ${JSON.stringify(url)};`),
|
||||
);
|
||||
sharedAliases.push("--alias:" + name + "=" + shim, "--external:" + url);
|
||||
}
|
||||
const bindingPath = compiler.artifactPath(settings.bindingOutput),
|
||||
bindings = compiler.generateComponentClientBindings(plan, pkg.revision),
|
||||
bindings = contract.definition.component,
|
||||
assets = compiler.componentAssetDeclarations();
|
||||
await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true });
|
||||
await fs.writeFile(path.join(work, bindingPath), bindings);
|
||||
@@ -123,10 +128,9 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
|
||||
for (const root of settings.contracts ?? []) {
|
||||
const companion = JSON.parse(await fs.readFile(path.join(root, "react-contracts.json"), "utf8"));
|
||||
if (companion.schemaVersion !== 1) throw Error("Unsupported React props companion");
|
||||
const contracts = plan.definition.workspace.interfaceImports.filter(
|
||||
const contracts = contract.definition.moduleInterfaces.filter(
|
||||
(iface) =>
|
||||
iface.revisionId === companion.interfaceRevisionId ||
|
||||
iface.application?.definitionId === companion.interfaceRevisionId,
|
||||
iface.revisionId === companion.interfaceRevisionId || iface.definitionId === companion.interfaceRevisionId,
|
||||
);
|
||||
if (!contracts.length) continue;
|
||||
for (const [memberId, entry] of Object.entries(companion.members)) {
|
||||
@@ -136,12 +140,7 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
|
||||
`import type {${entry.export} as ${alias}} from ${JSON.stringify(path.join(root, "types", compiler.artifactPath(entry.path)))};`,
|
||||
);
|
||||
for (const iface of contracts) {
|
||||
if (
|
||||
!(iface.template?.members ?? iface.members).some(
|
||||
(member) => member.kind === "module" && member.id === memberId,
|
||||
)
|
||||
)
|
||||
throw Error("Props companion names an unknown module member");
|
||||
if (!iface.members.includes(memberId)) throw Error("Props companion names an unknown module member");
|
||||
const key = JSON.stringify([iface.revisionId, memberId]);
|
||||
if (seenProps.has(key)) throw Error("Duplicate React props companion for " + key);
|
||||
seenProps.add(key);
|
||||
@@ -155,46 +154,41 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
|
||||
propsImports.join("\n") +
|
||||
`\nexport {};\ndeclare module ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))} { interface ModuleProps {${propsMembers.join("\n")}} }\n`,
|
||||
);
|
||||
const components = [];
|
||||
const witnesses = [];
|
||||
for (const [id, entry] of entries) {
|
||||
const declaration = declarations.find((component) => component.id === id),
|
||||
source = compiler.artifactPath(entry.entry);
|
||||
if (!/^[$A-Z_a-z][$\w]*$/.test(entry.export)) throw Error("Invalid component export name");
|
||||
const key = digest(Buffer.from(id)).slice(0, 24),
|
||||
directory = "share/quixos/components/" + key;
|
||||
const key = digest(Buffer.from(id)).slice(0, 24);
|
||||
const witness = path.join(work, "component-check-" + key + ".tsx");
|
||||
const module = pkg.modules.find((module) => module.id === id);
|
||||
await fs.writeFile(
|
||||
witness,
|
||||
`import type {Modules} from ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))};import {${entry.export} as Component} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};const checked:Modules[${JSON.stringify(declaration.displayName)}]=Component;export default checked;\n` +
|
||||
(module.contract === "org.quixos.react.subject-only/1"
|
||||
? `import type {ComponentProps} from "@quixos/camino-react";declare const subject:ComponentProps<${JSON.stringify(declaration.subject)}>['subject'];const mount = <Component subject={subject}/>;\n`
|
||||
? `import type {ComponentProps} from "react";declare const props:Pick<ComponentProps<Modules[${JSON.stringify(declaration.displayName)}]>,"subject"|"input">;const mount = <Component {...props}/>;\n`
|
||||
: ""),
|
||||
);
|
||||
execFileSync(
|
||||
config.node,
|
||||
[
|
||||
config.tsc,
|
||||
"--noEmit",
|
||||
"--strict",
|
||||
"--skipLibCheck",
|
||||
"false",
|
||||
"--target",
|
||||
"es2024",
|
||||
"--lib",
|
||||
"es2024,dom,dom.iterable,esnext.disposable",
|
||||
"--module",
|
||||
"nodenext",
|
||||
"--moduleResolution",
|
||||
"nodenext",
|
||||
"--jsx",
|
||||
"react-jsx",
|
||||
path.basename(witness),
|
||||
assetPath,
|
||||
propsPath,
|
||||
],
|
||||
{ cwd: work, stdio: "inherit" },
|
||||
);
|
||||
witnesses.push(path.basename(witness));
|
||||
}
|
||||
checkComponentTypes({
|
||||
node: config.node,
|
||||
tsc: config.tsc,
|
||||
work,
|
||||
witnesses,
|
||||
assets: assetPath,
|
||||
props: propsPath,
|
||||
packageRevision: pkg.revision,
|
||||
});
|
||||
const components = [];
|
||||
const { checkControls } = await import(pathToFileURL(path.join(settings.sdk, "lint-controls.mjs")));
|
||||
const { default: ts } = await import(pathToFileURL(path.resolve(path.dirname(config.tsc), "../lib/typescript.js")));
|
||||
const checkedControls = new Set();
|
||||
for (const [id, entry] of entries) {
|
||||
const declaration = declarations.find((component) => component.id === id),
|
||||
source = compiler.artifactPath(entry.entry),
|
||||
key = digest(Buffer.from(id)).slice(0, 24),
|
||||
directory = "share/quixos/components/" + key;
|
||||
const metadata = path.join(work, "component-meta-" + key + ".json");
|
||||
const facade = "component-entry-" + key + ".ts";
|
||||
await fs.writeFile(
|
||||
@@ -202,27 +196,35 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
|
||||
`export {${entry.export}} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};\n`,
|
||||
);
|
||||
await fs.mkdir(path.join(output, directory), { recursive: true });
|
||||
execFileSync(
|
||||
config.esbuild,
|
||||
[
|
||||
facade,
|
||||
"--bundle",
|
||||
"--platform=browser",
|
||||
"--format=esm",
|
||||
"--target=es2024",
|
||||
"--jsx=automatic",
|
||||
'--define:process.env.NODE_ENV="production"',
|
||||
"--metafile=" + metadata,
|
||||
"--outfile=" + path.join(output, directory, "module.mjs"),
|
||||
...sharedAliases,
|
||||
...["svg", "png", "jpg", "jpeg", "gif", "webp", "avif", "woff", "woff2", "ttf", "ico"].map(
|
||||
(extension) => "--loader:." + extension + "=dataurl",
|
||||
),
|
||||
],
|
||||
{ cwd: work, stdio: "inherit" },
|
||||
measureBuild("component-bundle", { package: pkg.revision, component: id }, () =>
|
||||
execFileSync(
|
||||
config.esbuild,
|
||||
[
|
||||
facade,
|
||||
"--bundle",
|
||||
"--platform=browser",
|
||||
"--format=esm",
|
||||
"--target=es2024",
|
||||
"--jsx=automatic",
|
||||
'--define:process.env.NODE_ENV="production"',
|
||||
"--metafile=" + metadata,
|
||||
"--outfile=" + path.join(output, directory, "module.mjs"),
|
||||
...sharedAliases,
|
||||
...["svg", "png", "jpg", "jpeg", "gif", "webp", "avif", "woff", "woff2", "ttf", "ico"].map(
|
||||
(extension) => "--loader:." + extension + "=dataurl",
|
||||
),
|
||||
],
|
||||
{ cwd: work, stdio: "inherit" },
|
||||
),
|
||||
);
|
||||
const meta = JSON.parse(await fs.readFile(metadata, "utf8")),
|
||||
outputs = Object.entries(meta.outputs);
|
||||
for (const file of Object.keys(meta.inputs)) {
|
||||
if (!/\.[cm]?[jt]sx?$/.test(file) || file.includes("node_modules/") || checkedControls.has(file)) continue;
|
||||
checkedControls.add(file);
|
||||
const errors = checkControls(ts, await fs.readFile(path.resolve(work, file), "utf8"), file);
|
||||
if (errors.length) throw Error(errors.join("\n"));
|
||||
}
|
||||
const main = outputs.find(([file]) => file.endsWith("/module.mjs"))?.[1];
|
||||
if (!main?.exports.includes(entry.export)) throw Error("Component bundle lacks declared export");
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/** Timing is log data, never part of an artifact or its witness. */
|
||||
export function measureBuild(phase, details, action) {
|
||||
const started = performance.now();
|
||||
try {
|
||||
return action();
|
||||
} finally {
|
||||
if (process.env.QUIXOS_TRACE_PROFILE === "1")
|
||||
console.error(
|
||||
`[qx-build-profile] ${JSON.stringify({ phase, ms: Math.round(performance.now() - started), ...details })}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { installDependencies } from "./install-dependencies.mjs";
|
||||
const config = JSON.parse(await fs.readFile(process.argv[2], "utf8"));
|
||||
const resource = JSON.parse(await fs.readFile(config.interface, "utf8"));
|
||||
if (resource.kind !== "interface") throw Error("React props belong to an interface resource");
|
||||
@@ -29,14 +30,24 @@ const metadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error)
|
||||
throw error;
|
||||
});
|
||||
await fs.writeFile(packageFile, JSON.stringify({ ...metadata, type: "module" }));
|
||||
if (config.dependencies) await fs.symlink(config.dependencies, path.join(work, "node_modules"));
|
||||
const modules = path.join(output, "node_modules");
|
||||
await fs.mkdir(modules, { recursive: true });
|
||||
await installDependencies({
|
||||
destination: modules,
|
||||
application: config.dependencies,
|
||||
selected: config.reactSdk ? { "@quixos/camino-react": config.reactSdk } : {},
|
||||
reserved: ["@quixos/camino-react"],
|
||||
});
|
||||
await fs.symlink(modules, path.join(work, "node_modules"));
|
||||
const members = Object.create(null);
|
||||
let index = 0;
|
||||
for (const [id, entry] of Object.entries(config.members)) {
|
||||
const member = (iface.template?.members ?? iface.members).find((member) => member.id === id);
|
||||
if (
|
||||
member?.kind !== "module" ||
|
||||
!["org.quixos.react.component/1", "org.quixos.react.subject-only/1"].includes(member.contract)
|
||||
!["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(
|
||||
member.contract,
|
||||
)
|
||||
)
|
||||
throw Error("Props entry is not a React module member: " + id);
|
||||
if (
|
||||
@@ -52,7 +63,7 @@ for (const [id, entry] of Object.entries(config.members)) {
|
||||
`import type {${entry.export} as Props} from ${typeImport};
|
||||
type Assert<T extends true> = T;
|
||||
type ObjectProps = Assert<Props extends object ? true : false>;
|
||||
type ReservedSubject = Assert<"subject" extends keyof Props ? false : true>;
|
||||
type ReservedRendererProps = Assert<Extract<keyof Props, "subject" | "input" | "navigation" | "usingComponent" | "provider" | "coverage" | "componentCodeFallback"> extends never ? true : false>;
|
||||
${member.contract === "org.quixos.react.subject-only/1" ? "type SubjectOnly = Assert<{} extends Props ? true : false>;" : ""}
|
||||
export type {Props};\n`,
|
||||
);
|
||||
@@ -69,6 +80,8 @@ execFileSync(
|
||||
"--strict",
|
||||
"--target",
|
||||
"es2024",
|
||||
"--lib",
|
||||
"es2024,dom,dom.iterable,esnext.disposable",
|
||||
"--module",
|
||||
"nodenext",
|
||||
"--moduleResolution",
|
||||
@@ -88,7 +101,6 @@ for (const file of declarations) {
|
||||
await fs.copyFile(path.join(work, file), path.join(output, "types", file));
|
||||
}
|
||||
await fs.writeFile(path.join(output, "package.json"), JSON.stringify({ type: "module" }));
|
||||
if (config.dependencies) await fs.symlink(config.dependencies, path.join(output, "node_modules"));
|
||||
await fs.writeFile(
|
||||
path.join(output, "react-contracts.json"),
|
||||
JSON.stringify({ schemaVersion: 1, interfaceRevisionId: iface.revisionId, members }, null, 2),
|
||||
|
||||
@@ -13,6 +13,62 @@ export async function buildService({
|
||||
recordFile,
|
||||
}) {
|
||||
const relative = compiler.artifactPath;
|
||||
if (config.restricted) {
|
||||
if (config.service.entry || pkg.exports.some((e) => e.execution.kind === "remote"))
|
||||
throw Error(
|
||||
"Restricted services require portable snapshot/transaction exports; remote/custom services keep their VM path",
|
||||
);
|
||||
const modules = implementations.filter((i) => i.target === "server");
|
||||
if (modules.length !== 1 || pkg.exports.some((e) => !modules[0].exports.includes(e.id)))
|
||||
throw Error("Restricted service requires exactly one checked portable registry covering all exports");
|
||||
const limits = config.restricted.limits;
|
||||
if (!limits || typeof limits !== "object" || Array.isArray(limits))
|
||||
throw Error("Restricted limits must be an object");
|
||||
if (Object.keys(limits).some((key) => !["deadlineMs", "maxBytes", "maxPortCalls", "maxQueued"].includes(key)))
|
||||
throw Error("Unknown restricted service limit");
|
||||
for (const [key, min, max] of [
|
||||
["deadlineMs", 1, 60000],
|
||||
["maxBytes", 1024, 4194304],
|
||||
["maxPortCalls", 1, 10000],
|
||||
["maxQueued", 0, 64],
|
||||
])
|
||||
if (limits[key] !== undefined && (!Number.isSafeInteger(limits[key]) || limits[key] < min || limits[key] > max))
|
||||
throw Error("Invalid restricted service limit " + key);
|
||||
await fs.mkdir(path.join(output, "bin"), { recursive: true });
|
||||
await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true });
|
||||
const configFile = "share/quixos/service/restricted.json";
|
||||
const module = modules[0];
|
||||
await fs.writeFile(
|
||||
path.join(output, configFile),
|
||||
JSON.stringify({
|
||||
module: path.relative("share/quixos/service", module.path),
|
||||
registryExport: module.registryExport,
|
||||
exports: module.exports,
|
||||
sdk: [{ name: config.restricted.sdk, path: config.restricted.sdk }],
|
||||
...limits,
|
||||
}),
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(output, "bin/service"),
|
||||
`#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.restricted.runtime} "$service_root/${configFile}"\n`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
const body = {
|
||||
id: "service",
|
||||
target: "server",
|
||||
compatibility: "quixos-service-v1",
|
||||
path: "bin/service",
|
||||
ports: "quixos-transaction-ports-v1",
|
||||
exports: module.exports,
|
||||
files: [
|
||||
await recordFile("bin/service", "text/x-shellscript"),
|
||||
await recordFile(configFile, "application/json"),
|
||||
...module.files,
|
||||
],
|
||||
};
|
||||
implementations.unshift({ ...body, contentDigest: compiler.implementationDigest(body) });
|
||||
return;
|
||||
}
|
||||
if (pkg.exports.length || config.service.entry) {
|
||||
await fs.mkdir(path.join(output, "bin"), { recursive: true });
|
||||
await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true });
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Standalone build drivers import sibling JavaScript modules. Their code is an
|
||||
# input; Nix helper APIs and test sources do not belong to that dependency.
|
||||
builtins.path {
|
||||
path = ./.;
|
||||
name = "camino-build-tools";
|
||||
filter =
|
||||
filename: type:
|
||||
let
|
||||
name = builtins.baseNameOf filename;
|
||||
in
|
||||
(type == "directory" && filename == toString ./.)
|
||||
|| (
|
||||
builtins.dirOf filename == toString ./.
|
||||
&& builtins.match ".*\\.mjs" name != null
|
||||
&& builtins.match ".*\\.test\\.mjs" name == null
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const [tsc, count, ...args] = process.argv.slice(2);
|
||||
const ts = createRequire(import.meta.url)(path.resolve(path.dirname(tsc), "../lib/typescript.js"));
|
||||
const parsed = ts.parseCommandLine(args);
|
||||
const witnesses = parsed.fileNames.slice(0, Number(count));
|
||||
const shared = parsed.fileNames.slice(Number(count));
|
||||
const host = ts.createCompilerHost(parsed.options, true);
|
||||
// These programs only discover source closures; they do not bind or check.
|
||||
// Sharing immutable parsed source files avoids reading/parsing the same libs
|
||||
// for every witness. Only the final combined program performs typechecking.
|
||||
const sources = new Map();
|
||||
const getSourceFile = host.getSourceFile;
|
||||
host.getSourceFile = (filename, ...options) => {
|
||||
if (!sources.has(filename)) sources.set(filename, getSourceFile(filename, ...options));
|
||||
return sources.get(filename);
|
||||
};
|
||||
for (const method of ["fileExists", "directoryExists", "getDirectories", "readFile", "realpath"]) {
|
||||
if (!host[method]) continue;
|
||||
const original = host[method],
|
||||
cache = new Map();
|
||||
host[method] = (filename) => {
|
||||
if (!cache.has(filename)) cache.set(filename, original(filename));
|
||||
return cache.get(filename);
|
||||
};
|
||||
}
|
||||
const closures = witnesses.map(
|
||||
(witness) => new Set(ts.createProgram([witness, ...shared], parsed.options, host).getSourceFiles()),
|
||||
);
|
||||
const common = new Set([...closures[0]].filter((source) => closures.every((closure) => closure.has(source))));
|
||||
const ambient = (node) =>
|
||||
ts.isModuleDeclaration(node) || ts.isNamespaceExportDeclaration(node) || ts.forEachChild(node, ambient);
|
||||
// A file that is visible to just some witnesses must not contribute globals
|
||||
// or ambient module declarations to the others. Otherwise a combined success
|
||||
// could hide an independent failure. Script files are conservatively unsafe.
|
||||
if (
|
||||
closures.some((closure) =>
|
||||
[...closure].some((source) => !common.has(source) && (!ts.isExternalModule(source) || ambient(source))),
|
||||
)
|
||||
)
|
||||
process.exit(2);
|
||||
|
||||
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
|
||||
const diagnostics = [...parsed.errors, ...ts.getPreEmitDiagnostics(program)];
|
||||
if (diagnostics.length) {
|
||||
console.error(ts.formatDiagnosticsWithColorAndContext(diagnostics, host));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { measureBuild } from "./build-profile.mjs";
|
||||
|
||||
// Component witnesses share bindings and usually the same source graph.
|
||||
// Check a bounded batch once when distinct source closures cannot supply
|
||||
// ambient declarations to one another. Otherwise use the original checks.
|
||||
export function checkComponentTypes({ node, tsc, work, witnesses, assets, props, packageRevision }) {
|
||||
const checkTypes = (files, combined = false) =>
|
||||
execFileSync(
|
||||
node,
|
||||
[
|
||||
...(combined ? [fileURLToPath(new URL("./component-type-batch.mjs", import.meta.url))] : []),
|
||||
tsc,
|
||||
...(combined ? [String(files.length)] : []),
|
||||
"--noEmit",
|
||||
"--strict",
|
||||
"--skipLibCheck",
|
||||
"false",
|
||||
"--target",
|
||||
"es2024",
|
||||
"--lib",
|
||||
"es2024,dom,dom.iterable,esnext.disposable",
|
||||
"--module",
|
||||
"nodenext",
|
||||
"--moduleResolution",
|
||||
"nodenext",
|
||||
"--jsx",
|
||||
"react-jsx",
|
||||
...files,
|
||||
assets,
|
||||
props,
|
||||
],
|
||||
{ cwd: work, stdio: combined ? "pipe" : "inherit" },
|
||||
);
|
||||
measureBuild("component-typecheck", { package: packageRevision, components: witnesses.length }, () => {
|
||||
for (let start = 0; start < witnesses.length; start += 1000) {
|
||||
const batch = witnesses.slice(start, start + 1000);
|
||||
if (batch.length === 1) checkTypes(batch);
|
||||
else {
|
||||
try {
|
||||
checkTypes(batch, true);
|
||||
} catch (error) {
|
||||
if (!(error.status > 0) && error.code !== "ENOBUFS") throw error;
|
||||
console.error("Component batch requires independent witness checks");
|
||||
for (const witness of batch) checkTypes([witness]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { checkComponentTypes } from "./component-types.mjs";
|
||||
|
||||
const tsc = process.env.QUIXOS_TYPESCRIPT;
|
||||
if (!tsc) throw Error("Pass the selected TypeScript compiler path");
|
||||
async function fixture(context, left, right) {
|
||||
const work = await mkdtemp(path.join(os.tmpdir(), "component-types-"));
|
||||
context.after(() => rm(work, { recursive: true, force: true }));
|
||||
await Promise.all(
|
||||
Object.entries({ "left.ts": left, "right.ts": right, "assets.d.ts": "", "props.d.ts": "" }).map(([file, source]) =>
|
||||
writeFile(path.join(work, file), source),
|
||||
),
|
||||
);
|
||||
return {
|
||||
node: process.execPath,
|
||||
tsc,
|
||||
work,
|
||||
witnesses: ["left.ts", "right.ts"],
|
||||
assets: "assets.d.ts",
|
||||
props: "props.d.ts",
|
||||
packageRevision: "test",
|
||||
};
|
||||
}
|
||||
test("shared component typechecking accepts all valid witnesses", async (context) => {
|
||||
checkComponentTypes(await fixture(context, "export const left: string = 'ok';", "export const right: number = 1;"));
|
||||
});
|
||||
test("independently valid ambient declarations use the full individual check fallback", async (context) => {
|
||||
checkComponentTypes(
|
||||
await fixture(
|
||||
context,
|
||||
"export {}; declare global { interface Application { value: string; } }",
|
||||
"export {}; declare global { interface Application { value: number; } }",
|
||||
),
|
||||
);
|
||||
});
|
||||
test("a type error in any witness still rejects the artifact", async (context) => {
|
||||
const input = await fixture(context, "export const left: string = 'ok';", "export const right: string = 1;");
|
||||
assert.throws(() => checkComponentTypes(input));
|
||||
});
|
||||
test("one witness's globals cannot make an independently invalid witness pass", async (context) => {
|
||||
const input = await fixture(
|
||||
context,
|
||||
"export {}; declare global { interface Application { value: string; } }",
|
||||
"export const right: Application = {value: 'ok'};",
|
||||
);
|
||||
assert.throws(() => checkComponentTypes(input));
|
||||
});
|
||||
test("an ambient module from another source closure cannot resolve a missing import", async (context) => {
|
||||
const input = await fixture(
|
||||
context,
|
||||
"declare module 'missing-component-package' { export const value: string; }",
|
||||
"import {value} from 'missing-component-package'; export const right = value;",
|
||||
);
|
||||
assert.throws(() => checkComponentTypes(input));
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
# QX and recursive pins enter through the checked contract and build recipe.
|
||||
# The compiler receives authored implementation files, not resource provenance.
|
||||
{ lib, src }:
|
||||
builtins.path {
|
||||
path = src;
|
||||
name = "camino-implementation-source";
|
||||
filter =
|
||||
path: type:
|
||||
let
|
||||
name = builtins.baseNameOf path;
|
||||
in
|
||||
if type == "directory" then
|
||||
!(builtins.elem name [
|
||||
".git"
|
||||
".jj"
|
||||
"node_modules"
|
||||
])
|
||||
else
|
||||
!(builtins.elem name [
|
||||
"flake.nix"
|
||||
"flake.lock"
|
||||
"quixos.lock"
|
||||
"quixos.check.json"
|
||||
"quixos.react.json"
|
||||
"yarn-project.nix"
|
||||
"components.nix"
|
||||
"contracts.nix"
|
||||
])
|
||||
&& !(lib.hasSuffix ".qx" name);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# The selected protocol owns semantic projection; builders receive its result.
|
||||
args@{ protocol, ... }: import "${protocol}/share/package-context.nix" args
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
pkgs,
|
||||
nixpkgs,
|
||||
system,
|
||||
}:
|
||||
let
|
||||
packageSet = (import ./quixos-package-helpers.nix).packageSet;
|
||||
shared = {
|
||||
marker = "shared";
|
||||
};
|
||||
environment = {
|
||||
inherit system;
|
||||
nixpkgsSource = toString nixpkgs;
|
||||
pkgs = shared;
|
||||
narHash = nixpkgs.narHash;
|
||||
};
|
||||
standard = {
|
||||
inherit nixpkgs system;
|
||||
context.buildEnvironment = environment;
|
||||
};
|
||||
fallback = packageSet { inherit nixpkgs system; };
|
||||
in
|
||||
assert packageSet standard == shared;
|
||||
assert
|
||||
packageSet (
|
||||
standard
|
||||
// {
|
||||
context.buildEnvironment = environment // {
|
||||
nixpkgsSource = "same-content-under-another-name";
|
||||
};
|
||||
}
|
||||
) == shared;
|
||||
assert
|
||||
(packageSet (
|
||||
standard
|
||||
// {
|
||||
context.buildEnvironment = environment // {
|
||||
system = "another-system";
|
||||
};
|
||||
}
|
||||
)).path == fallback.path;
|
||||
assert
|
||||
(packageSet (
|
||||
standard
|
||||
// {
|
||||
context.buildEnvironment = environment // {
|
||||
nixpkgsSource = "another-source";
|
||||
narHash = "another-hash";
|
||||
};
|
||||
}
|
||||
)).path == fallback.path;
|
||||
assert
|
||||
(packageSet (standard // { overlays = [ (_: _: { marker = "overlay"; }) ]; })).marker == "overlay";
|
||||
assert (packageSet (standard // { config.allowBroken = true; })).config.allowBroken;
|
||||
pkgs.runCommand "camino-package-set-check" { } ''
|
||||
touch "$out"
|
||||
''
|
||||
@@ -847,6 +847,40 @@ let
|
||||
in
|
||||
{
|
||||
mkCaminoReactPackage = import ./react-package.nix;
|
||||
# Share the composition's package set only for the same exact Nixpkgs source,
|
||||
# system and default import arguments. Config/overlay/cross-build choices use
|
||||
# ordinary Nixpkgs evaluation. No package implementation is substituted.
|
||||
packageSet =
|
||||
args@{
|
||||
nixpkgs,
|
||||
system,
|
||||
context ? { },
|
||||
...
|
||||
}:
|
||||
let
|
||||
authoredArgs = builtins.removeAttrs args [
|
||||
"nixpkgs"
|
||||
"context"
|
||||
];
|
||||
importArgs = {
|
||||
config = { };
|
||||
overlays = [ ];
|
||||
}
|
||||
// authoredArgs;
|
||||
environment = context.buildEnvironment or null;
|
||||
in
|
||||
if
|
||||
builtins.attrNames authoredArgs == [ "system" ]
|
||||
&& environment != null
|
||||
&& environment.system == system
|
||||
&& (
|
||||
environment.nixpkgsSource == toString nixpkgs
|
||||
|| ((nixpkgs.narHash or null) != null && nixpkgs.narHash == (environment.narHash or null))
|
||||
)
|
||||
then
|
||||
environment.pkgs
|
||||
else
|
||||
import nixpkgs importArgs;
|
||||
mkCaminoReactContracts = import ./react-contracts.nix;
|
||||
mkCaminoYarnDependencies = import ./yarn-dependencies.nix;
|
||||
mkCaminoYarnShell = import ./yarn-shell.nix;
|
||||
|
||||
+20
-3
@@ -6,16 +6,33 @@
|
||||
dependencies ? null,
|
||||
}:
|
||||
let
|
||||
buildTools = import ./build-tools.nix;
|
||||
config = pkgs.writeText "camino-react-contracts.json" (
|
||||
builtins.toJSON {
|
||||
source = src;
|
||||
interface = context.interface;
|
||||
source = import ./implementation-source.nix {
|
||||
inherit (pkgs) lib;
|
||||
inherit src;
|
||||
};
|
||||
interface = builtins.toFile "camino-interface-contract.json" (
|
||||
builtins.unsafeDiscardStringContext (
|
||||
builtins.toJSON (
|
||||
let
|
||||
resource = builtins.fromJSON (builtins.readFile context.interface);
|
||||
in
|
||||
{
|
||||
kind = resource.kind;
|
||||
revision = builtins.removeAttrs resource.revision [ "source" ];
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
inherit members dependencies;
|
||||
reactSdk = context.reactSdk or null;
|
||||
node = "${pkgs.nodejs_24}/bin/node";
|
||||
tsc = "${context.typescript}/node_modules/typescript/bin/tsc";
|
||||
}
|
||||
);
|
||||
in
|
||||
pkgs.runCommand "camino-react-contracts" { } ''
|
||||
${pkgs.nodejs_24}/bin/node ${./.}/build-react-contracts.mjs ${config}
|
||||
${pkgs.nodejs_24}/bin/node ${buildTools}/build-react-contracts.mjs ${config}
|
||||
''
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ let
|
||||
group = import ./artifacts.nix {
|
||||
inherit pkgs src;
|
||||
inherit (context)
|
||||
plan
|
||||
buildContract
|
||||
core
|
||||
sharedRuntime
|
||||
typescript
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{ pkgs, core }:
|
||||
let
|
||||
workerd = import ./workerd.nix { inherit pkgs; };
|
||||
# Transaction-port/value adapters do not need the browser's CRDT/Wasm engine.
|
||||
# Keep this surface explicit; it is bundled into checked restricted handlers.
|
||||
sdk = pkgs.runCommand "quixos-restricted-sdk" { nativeBuildInputs = [ pkgs.esbuild ]; } ''
|
||||
mkdir -p "$out"
|
||||
cat > entry.mjs <<'JS'
|
||||
export {definePortableRegistry,QxReject} from '${core}/dist/portable.js';
|
||||
export {encodeArgument,decodeArgument,canonicalJson,parseJson} from '${core}/dist/value.js';
|
||||
export {workspaceId,operationId,objectId,entryId,hashIdentity} from '${core}/dist/identity.js';
|
||||
export {reference} from '${core}/dist/references.js';
|
||||
export {CodecError,ExecutionError} from '${core}/dist/limits.js';
|
||||
JS
|
||||
esbuild entry.mjs --bundle --platform=neutral --format=esm --target=es2024 --outfile="$out/core.mjs"
|
||||
'';
|
||||
runtime =
|
||||
pkgs.runCommand "quixos-restricted-execution" { nativeBuildInputs = [ pkgs.makeWrapper ]; }
|
||||
''
|
||||
mkdir -p "$out/lib" "$out/bin"
|
||||
cp ${./service.mjs} "$out/lib/service.mjs"
|
||||
cp ${./worker.mjs} "$out/lib/worker.mjs"
|
||||
makeWrapper ${pkgs.nodejs_24}/bin/node "$out/bin/quixos-restricted-registry" \
|
||||
--add-flags "$out/lib/service.mjs" \
|
||||
--set QUIXOS_RESTRICTED_WORKERD ${workerd}/bin/workerd \
|
||||
--set QUIXOS_RESTRICTED_WORKER "$out/lib/worker.mjs"
|
||||
'';
|
||||
qualification =
|
||||
pkgs.runCommand "quixos-restricted-execution-qualification"
|
||||
{
|
||||
nativeBuildInputs = [ pkgs.nodejs_24 ];
|
||||
QUIXOS_TEST_WORKERD = "${workerd}/bin/workerd";
|
||||
QUIXOS_TEST_SDK = "${sdk}/core.mjs";
|
||||
}
|
||||
''
|
||||
node --test ${./.}/service.test.mjs
|
||||
touch "$out"
|
||||
'';
|
||||
in
|
||||
{
|
||||
inherit
|
||||
workerd
|
||||
sdk
|
||||
runtime
|
||||
qualification
|
||||
;
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { createServer, request as httpRequest } from "node:http";
|
||||
import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, dirname } from "node:path";
|
||||
|
||||
const abi = "quixos-transaction-ports-v1";
|
||||
const fixedError = (code) => Object.assign(Error(code), { code });
|
||||
const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
||||
const absolute = (value) =>
|
||||
typeof value === "string" &&
|
||||
value.startsWith("/") &&
|
||||
!/[\s\u0000-\u001f]/.test(value) &&
|
||||
!value.split("/").includes("..");
|
||||
|
||||
/** Trusted stdio adapter inside one workspace VM. Engine processes never own
|
||||
* coordinator credentials. The supervising service must impose VM/OS limits.
|
||||
* Per-frame process replacement prevents retained globals/handles crossing calls. */
|
||||
export async function serveRestrictedRegistry(config, io = process) {
|
||||
if (
|
||||
!object(config) ||
|
||||
!absolute(config.workerd) ||
|
||||
!absolute(config.worker) ||
|
||||
!absolute(config.module) ||
|
||||
!/^[$A-Z_a-z][$\w]*$/.test(config.registryExport) ||
|
||||
!Array.isArray(config.exports) ||
|
||||
!config.exports.length ||
|
||||
config.exports.length > 10000 ||
|
||||
config.exports.some((x) => typeof x !== "string" || !x || x.length > 4096) ||
|
||||
new Set(config.exports).size !== config.exports.length ||
|
||||
!Array.isArray(config.sdk) ||
|
||||
config.sdk.length > 8 ||
|
||||
config.sdk.some(
|
||||
(m) =>
|
||||
!object(m) ||
|
||||
!absolute(m.path) ||
|
||||
typeof m.name !== "string" ||
|
||||
!m.name ||
|
||||
/[\u0000-\u001f]/.test(m.name) ||
|
||||
["entry.mjs", "package.mjs"].includes(m.name),
|
||||
) ||
|
||||
new Set(config.sdk.map((m) => m.name)).size !== config.sdk.length
|
||||
)
|
||||
throw fixedError("INVALID_RESTRICTED_CONFIGURATION");
|
||||
const limits = {
|
||||
maxBytes: config.maxBytes ?? 1024 * 1024,
|
||||
maxPortCalls: config.maxPortCalls ?? 1000,
|
||||
deadlineMs: config.deadlineMs ?? 4000,
|
||||
maxQueued: config.maxQueued ?? 8,
|
||||
};
|
||||
if (
|
||||
!Number.isSafeInteger(limits.maxBytes) ||
|
||||
limits.maxBytes < 1024 ||
|
||||
limits.maxBytes > 4 * 1024 * 1024 ||
|
||||
!Number.isSafeInteger(limits.maxPortCalls) ||
|
||||
limits.maxPortCalls < 1 ||
|
||||
limits.maxPortCalls > 10000 ||
|
||||
!Number.isSafeInteger(limits.deadlineMs) ||
|
||||
limits.deadlineMs < 1 ||
|
||||
limits.deadlineMs > 60000 ||
|
||||
!Number.isSafeInteger(limits.maxQueued) ||
|
||||
limits.maxQueued < 0 ||
|
||||
limits.maxQueued > 64
|
||||
)
|
||||
throw fixedError("INVALID_RESTRICTED_LIMITS");
|
||||
const packageSource = await readFile(config.module, "utf8");
|
||||
if (Buffer.byteLength(packageSource) > 1024 * 1024) throw fixedError("RESTRICTED_SOURCE_LIMIT");
|
||||
const workerSource = await readFile(config.worker, "utf8");
|
||||
if (Buffer.byteLength(workerSource) > 65536) throw fixedError("RESTRICTED_ENTRY_LIMIT");
|
||||
const sdk = await Promise.all(config.sdk.map(async (m) => ({ ...m, source: await readFile(m.path, "utf8") })));
|
||||
if (sdk.some((m) => Buffer.byteLength(m.source) > 2 * 1024 * 1024)) throw fixedError("RESTRICTED_SDK_LIMIT");
|
||||
let active,
|
||||
buffered = Buffer.alloc(0),
|
||||
stopped = false;
|
||||
const queued = [];
|
||||
const send = (value) => {
|
||||
if (stopped) return;
|
||||
const line = JSON.stringify({ abi, ...value }) + "\n";
|
||||
if (
|
||||
Buffer.byteLength(line) > limits.maxBytes ||
|
||||
io.stdout.writableLength + Buffer.byteLength(line) > limits.maxBytes * 2
|
||||
)
|
||||
throw fixedError("RESTRICTED_BACKPRESSURE");
|
||||
io.stdout.write(line);
|
||||
};
|
||||
const retire = async (call, reason) => {
|
||||
if (call.retiring) return call.retiring;
|
||||
call.open = false;
|
||||
call.retiring = (async () => {
|
||||
clearTimeout(call.timer);
|
||||
const index = queued.indexOf(call);
|
||||
if (index >= 0) queued.splice(index, 1);
|
||||
for (const pending of call.pending.values()) pending.reject(reason);
|
||||
call.pending.clear();
|
||||
// Cancellation can arrive during async directory/socket preparation.
|
||||
// Wait for it to settle before claiming every process/handle is gone.
|
||||
await call.preparing?.catch(() => {});
|
||||
if (call.child?.pid) {
|
||||
try {
|
||||
process.kill(-call.child.pid, "SIGKILL");
|
||||
} catch (error) {
|
||||
if (error.code !== "ESRCH") throw error;
|
||||
}
|
||||
await call.exited;
|
||||
}
|
||||
call.server?.closeAllConnections();
|
||||
if (call.server) await new Promise((resolve) => call.server.close(resolve));
|
||||
if (call.directory) await rm(call.directory, { recursive: true, force: true });
|
||||
if (active === call) active = undefined;
|
||||
})();
|
||||
return call.retiring;
|
||||
};
|
||||
const fail = async (error) => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
io.stdin.destroy();
|
||||
for (const call of [...queued]) await retire(call, error);
|
||||
if (active) await retire(active, error);
|
||||
io.stdout.end();
|
||||
io.exitCode = 1;
|
||||
};
|
||||
const body = async (stream) => {
|
||||
const chunks = [];
|
||||
let bytes = 0;
|
||||
for await (const chunk of stream) {
|
||||
bytes += chunk.length;
|
||||
if (bytes > limits.maxBytes) throw fixedError("RESTRICTED_MESSAGE_LIMIT");
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
|
||||
};
|
||||
const pump = () => {
|
||||
if (!stopped && !active && queued.length) void invoke(queued.shift()).catch(fail);
|
||||
};
|
||||
const finish = async (call, reason, reply) => {
|
||||
await retire(call, reason);
|
||||
send(reply);
|
||||
pump();
|
||||
};
|
||||
const invoke = async (call) => {
|
||||
const { message } = call;
|
||||
active = call;
|
||||
const terminate = (code) => {
|
||||
if (!call.open) return;
|
||||
void finish(call, fixedError(code), {
|
||||
invocation: message.invocation,
|
||||
status: "failed",
|
||||
error: { code, message: code },
|
||||
}).catch(fail);
|
||||
};
|
||||
call.terminate = terminate;
|
||||
try {
|
||||
let engine;
|
||||
call.preparing = (async () => {
|
||||
call.directory = await mkdtemp(join(tmpdir(), "qx-frame-"));
|
||||
if (!call.open) return;
|
||||
const broker = join(call.directory, "broker.sock");
|
||||
engine = join(call.directory, "engine.sock");
|
||||
call.server = createServer(async (request, response) => {
|
||||
try {
|
||||
if (!call.open || request.method !== "POST" || request.url !== "/port") throw fixedError("EXPIRED_CONTEXT");
|
||||
const port = await body(request);
|
||||
if (
|
||||
!call.open ||
|
||||
!object(port) ||
|
||||
Object.keys(port).some((key) => !["portId", "action", "input", "key"].includes(key)) ||
|
||||
typeof port.portId !== "string" ||
|
||||
!object(port.action) ||
|
||||
!Object.hasOwn(port, "input")
|
||||
)
|
||||
throw fixedError("INVALID_RESTRICTED_PORT");
|
||||
if (++call.sequence > limits.maxPortCalls || call.pending.size >= limits.maxPortCalls)
|
||||
throw fixedError("RESTRICTED_PORT_LIMIT");
|
||||
const sequence = call.sequence;
|
||||
const result = new Promise((resolve, reject) => call.pending.set(sequence, { resolve, reject }));
|
||||
void result.catch(() => {});
|
||||
send({
|
||||
kind: "port",
|
||||
invocation: message.invocation,
|
||||
frame: message.frame,
|
||||
sequence,
|
||||
portId: port.portId,
|
||||
action: { ...port.action, ...(port.key === undefined ? {} : { key: port.key }) },
|
||||
input: port.input,
|
||||
});
|
||||
const value = await result;
|
||||
if (!call.open) throw fixedError("EXPIRED_CONTEXT");
|
||||
const encoded = JSON.stringify(value);
|
||||
if (Buffer.byteLength(encoded) > limits.maxBytes) throw fixedError("RESTRICTED_MESSAGE_LIMIT");
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(encoded);
|
||||
} catch {
|
||||
response.writeHead(409);
|
||||
response.end('{"status":"failed","error":{"code":"RESTRICTED_PORT_FAILED"}}');
|
||||
}
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
call.server.once("error", reject);
|
||||
call.server.listen(broker, resolve);
|
||||
});
|
||||
await writeFile(join(call.directory, "package.mjs"), packageSource);
|
||||
await writeFile(join(call.directory, "entry.mjs"), workerSource);
|
||||
const modules = [
|
||||
{ name: "entry.mjs", esModule: 'embed "entry.mjs"' },
|
||||
{ name: "package.mjs", esModule: 'embed "package.mjs"' },
|
||||
];
|
||||
for (const [index, module] of sdk.entries()) {
|
||||
const file = `sdk-${index}.mjs`;
|
||||
await writeFile(join(call.directory, file), module.source);
|
||||
modules.push({ name: module.name, esModule: "embed " + JSON.stringify(file) });
|
||||
}
|
||||
const setting = { registryExport: config.registryExport, exports: config.exports, maxBytes: limits.maxBytes };
|
||||
const configuration = `using Workerd = import "/workerd/workerd.capnp"; const config :Workerd.Config = (
|
||||
services = [(name="main", worker=(modules=[${modules.map((m) => `(name=${JSON.stringify(m.name)}, esModule=${m.esModule})`).join(",")}],
|
||||
compatibilityDate="2026-10-05", globalOutbound="deny",
|
||||
bindings=[(name="BROKER", service="broker"),(name="CONFIG",json=${JSON.stringify(JSON.stringify(setting))})])),
|
||||
(name="broker",external=(address=${JSON.stringify("unix:" + broker)},http=())), (name="deny",network=(allow=[]))],
|
||||
sockets=[(name="frame",address=${JSON.stringify("unix:" + engine)},http=(),service="main")]);`;
|
||||
const file = join(call.directory, "config.capnp");
|
||||
await writeFile(file, configuration);
|
||||
if (!call.open) return;
|
||||
call.child = spawn(config.workerd, ["serve", file], {
|
||||
detached: true,
|
||||
env: { TZ: "UTC", LANG: "C.UTF-8" },
|
||||
cwd: call.directory,
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
call.exited = new Promise((resolve) => {
|
||||
call.child.once("exit", resolve);
|
||||
call.child.once("error", resolve);
|
||||
});
|
||||
let diagnostics = 0;
|
||||
call.child.stderr.on("data", (chunk) => {
|
||||
if ((diagnostics += chunk.length) > 65536) terminate("RESTRICTED_DIAGNOSTIC_LIMIT");
|
||||
});
|
||||
call.child.once("error", () => terminate("RESTRICTED_START_FAILED"));
|
||||
void call.exited.then(() => {
|
||||
if (call.open) terminate("RESTRICTED_ENGINE_EXITED");
|
||||
});
|
||||
})();
|
||||
await call.preparing;
|
||||
if (!call.open) return;
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const attempt = () => {
|
||||
if (!call.open) {
|
||||
reject(fixedError("EXPIRED_CONTEXT"));
|
||||
return;
|
||||
}
|
||||
const request = httpRequest({ socketPath: engine, path: "/", method: "POST" }, async (response) => {
|
||||
try {
|
||||
const result = await body(response);
|
||||
if (response.statusCode !== 200) throw fixedError("RESTRICTED_PACKAGE_FAILED");
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
request.once("error", (error) => {
|
||||
if (call.open && ["ENOENT", "ECONNREFUSED"].includes(error.code)) setTimeout(attempt, 5);
|
||||
else reject(error);
|
||||
});
|
||||
request.end(JSON.stringify(message));
|
||||
};
|
||||
attempt();
|
||||
});
|
||||
if (!call.open) return;
|
||||
if (
|
||||
!object(result) ||
|
||||
!["returned", "failed"].includes(result.status) ||
|
||||
Object.keys(result).some(
|
||||
(key) => !(result.status === "returned" ? ["status", "output"] : ["status", "error"]).includes(key),
|
||||
) ||
|
||||
(result.status === "returned"
|
||||
? !Object.hasOwn(result, "output")
|
||||
: !object(result.error) || !/^[A-Z0-9_]{1,128}$/.test(result.error.code))
|
||||
)
|
||||
throw fixedError("RESTRICTED_PACKAGE_FAILED");
|
||||
// Authored code can alter serialization prototypes. Treat even the
|
||||
// platform worker's JSON as untrusted; never accept routing/ABI fields.
|
||||
const reply =
|
||||
result.status === "returned"
|
||||
? { invocation: message.invocation, status: "returned", output: result.output }
|
||||
: {
|
||||
invocation: message.invocation,
|
||||
status: "failed",
|
||||
error: { code: result.error.code, message: result.error.code },
|
||||
};
|
||||
await finish(call, fixedError("INVOCATION_ENDED"), reply);
|
||||
} catch (error) {
|
||||
if (call.open) {
|
||||
const code = /^RESTRICTED_[A-Z_]+$/.test(error.code ?? "") ? error.code : "RESTRICTED_PACKAGE_FAILED";
|
||||
await finish(call, error, { invocation: message.invocation, status: "failed", error: { code, message: code } });
|
||||
}
|
||||
}
|
||||
};
|
||||
const receive = (message) => {
|
||||
if (!object(message) || message.abi !== abi) throw fixedError("RESTRICTED_ABI_MISMATCH");
|
||||
if (message.kind === "shutdown") {
|
||||
void fail(fixedError("SERVICE_SHUTDOWN"));
|
||||
return;
|
||||
}
|
||||
if (message.kind === "invoke") {
|
||||
if (
|
||||
!config.exports.includes(message.exportId) ||
|
||||
!["snapshot", "transaction"].includes(message.effect) ||
|
||||
typeof message.invocation !== "string" ||
|
||||
!message.invocation ||
|
||||
message.invocation.length > 4096 ||
|
||||
typeof message.frame !== "string" ||
|
||||
!message.frame ||
|
||||
message.frame.length > 4096
|
||||
)
|
||||
throw fixedError("INVALID_RESTRICTED_INVOCATION");
|
||||
if ([active, ...queued].some((call) => call?.message.invocation === message.invocation))
|
||||
throw fixedError("RESTRICTED_DUPLICATE_INVOCATION");
|
||||
if (active && queued.length >= limits.maxQueued) {
|
||||
send({
|
||||
invocation: message.invocation,
|
||||
status: "failed",
|
||||
error: { code: "RESTRICTED_QUEUE_FULL", message: "RESTRICTED_QUEUE_FULL" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
const call = { message, open: true, pending: new Map(), sequence: 0 };
|
||||
// Deadline includes queue wait: overload cannot extend a transaction frame.
|
||||
call.timer = setTimeout(() => {
|
||||
if (!call.open) return;
|
||||
void finish(call, fixedError("RESTRICTED_DEADLINE"), {
|
||||
invocation: message.invocation,
|
||||
status: "failed",
|
||||
error: { code: "RESTRICTED_DEADLINE", message: "RESTRICTED_DEADLINE" },
|
||||
}).catch(fail);
|
||||
}, limits.deadlineMs);
|
||||
queued.push(call);
|
||||
pump();
|
||||
return;
|
||||
}
|
||||
const waiting = queued.find(
|
||||
(call) => call.message.invocation === message.invocation && call.message.frame === message.frame,
|
||||
);
|
||||
if (waiting && message.kind === "cancel") {
|
||||
void finish(waiting, fixedError("INVOCATION_CANCELLED"), {
|
||||
kind: "cancelled",
|
||||
invocation: message.invocation,
|
||||
frame: message.frame,
|
||||
}).catch(fail);
|
||||
return;
|
||||
}
|
||||
const call = active;
|
||||
if (!call || message.invocation !== call.message.invocation || message.frame !== call.message.frame) {
|
||||
if (message.kind === "cancel") send({ kind: "cancelled", invocation: message.invocation, frame: message.frame });
|
||||
else if (message.kind !== "port-result") throw fixedError("RESTRICTED_UNKNOWN_INVOCATION");
|
||||
return;
|
||||
}
|
||||
if (message.kind === "cancel") {
|
||||
void finish(call, fixedError("INVOCATION_CANCELLED"), {
|
||||
kind: "cancelled",
|
||||
invocation: message.invocation,
|
||||
frame: message.frame,
|
||||
}).catch(fail);
|
||||
return;
|
||||
}
|
||||
if (message.kind !== "port-result" || !["returned", "failed"].includes(message.status))
|
||||
throw fixedError("INVALID_RESTRICTED_RESULT");
|
||||
const pending = call.pending.get(message.sequence);
|
||||
if (!pending) throw fixedError("RESTRICTED_UNKNOWN_PORT");
|
||||
call.pending.delete(message.sequence);
|
||||
pending.resolve(message);
|
||||
};
|
||||
io.stdin.on("data", (chunk) => {
|
||||
if (stopped) return;
|
||||
try {
|
||||
let offset = 0;
|
||||
while (offset < chunk.length) {
|
||||
const newline = chunk.indexOf(10, offset),
|
||||
end = newline < 0 ? chunk.length : newline;
|
||||
if (buffered.length + end - offset > limits.maxBytes) throw fixedError("RESTRICTED_MESSAGE_LIMIT");
|
||||
buffered = Buffer.concat([buffered, chunk.subarray(offset, end)]);
|
||||
if (newline < 0) break;
|
||||
receive(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffered)));
|
||||
buffered = Buffer.alloc(0);
|
||||
offset = newline + 1;
|
||||
if (stopped) break;
|
||||
}
|
||||
} catch (error) {
|
||||
void fail(error);
|
||||
}
|
||||
});
|
||||
io.stdin.on("end", () => void fail(fixedError("COORDINATOR_DISCONNECTED")));
|
||||
io.stdin.on("error", (error) => void fail(error));
|
||||
io.stdout.on("error", (error) => void fail(error));
|
||||
send({ kind: "ready", exports: config.exports });
|
||||
return { close: () => fail(fixedError("SERVICE_SHUTDOWN")) };
|
||||
}
|
||||
|
||||
if (process.argv[1] === new URL(import.meta.url).pathname) {
|
||||
const config = JSON.parse(await readFile(process.argv[2], "utf8"));
|
||||
await serveRestrictedRegistry({
|
||||
...config,
|
||||
workerd: process.env.QUIXOS_RESTRICTED_WORKERD,
|
||||
worker: process.env.QUIXOS_RESTRICTED_WORKER,
|
||||
module: join(dirname(process.argv[2]), config.module),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { serveRestrictedRegistry } from "./service.mjs";
|
||||
|
||||
const workerd = process.env.QUIXOS_TEST_WORKERD;
|
||||
if (!workerd) throw Error("Set QUIXOS_TEST_WORKERD to the pinned engine");
|
||||
const worker = fileURLToPath(new URL("./worker.mjs", import.meta.url));
|
||||
const abi = "quixos-transaction-ports-v1";
|
||||
let next = 0;
|
||||
async function fixture(source, options = {}) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "qx-restricted-test-"));
|
||||
const module = join(directory, "module.mjs");
|
||||
await writeFile(module, source);
|
||||
const io = { stdin: new PassThrough(), stdout: new PassThrough(), exitCode: 0 };
|
||||
const waiting = [],
|
||||
queued = [];
|
||||
let buffer = "";
|
||||
io.stdout.on("data", (chunk) => {
|
||||
buffer += chunk;
|
||||
let index;
|
||||
while ((index = buffer.indexOf("\n")) >= 0) {
|
||||
const value = JSON.parse(buffer.slice(0, index));
|
||||
buffer = buffer.slice(index + 1);
|
||||
if (waiting.length) waiting.shift()(value);
|
||||
else queued.push(value);
|
||||
}
|
||||
});
|
||||
const receive = async () => {
|
||||
if (queued.length) return queued.shift();
|
||||
let timer;
|
||||
try {
|
||||
return await Promise.race([
|
||||
new Promise((resolve) => waiting.push(resolve)),
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(Error("TEST_MESSAGE_TIMEOUT")), 5000);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
const service = await serveRestrictedRegistry(
|
||||
{ workerd, worker, module, registryExport: "registry", exports: ["test"], sdk: [], ...options },
|
||||
io,
|
||||
);
|
||||
assert.deepEqual(await receive(), { abi, kind: "ready", exports: ["test"] });
|
||||
const send = (value) => io.stdin.write(JSON.stringify({ abi, ...value }) + "\n");
|
||||
const start = (input = null) => {
|
||||
const invocation = "invoke-" + ++next,
|
||||
frame = "frame-" + next;
|
||||
send({
|
||||
kind: "invoke",
|
||||
invocation,
|
||||
frame,
|
||||
exportId: "test",
|
||||
workspace: "workspace:restricted",
|
||||
receiver: null,
|
||||
input,
|
||||
effect: "snapshot",
|
||||
});
|
||||
return { invocation, frame };
|
||||
};
|
||||
return {
|
||||
io,
|
||||
receive,
|
||||
send,
|
||||
start,
|
||||
async close() {
|
||||
await service.close();
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
const registry = (code) => `export const registry={exports:['test'],async invoke(request,channel){${code}}};`;
|
||||
|
||||
test("real engine returns results and resets all globals between frames", async () => {
|
||||
const f = await fixture(
|
||||
registry(
|
||||
"globalThis.retained=(globalThis.retained??0)+1; return {count:globalThis.retained,text:new Intl.Segmenter('zh',{granularity:'word'}).segment('雨后的书店').containing(3).segment};",
|
||||
),
|
||||
);
|
||||
try {
|
||||
for (let n = 0; n < 3; n++) {
|
||||
const call = f.start();
|
||||
const result = await f.receive();
|
||||
assert.equal(result.invocation, call.invocation);
|
||||
assert.equal(result.status, "returned");
|
||||
assert.equal(result.output.count, 1);
|
||||
assert.equal(typeof result.output.text, "string");
|
||||
}
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
test("captured frame is the sole source of nested port identities", async () => {
|
||||
const f = await fixture(
|
||||
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:null});"),
|
||||
);
|
||||
try {
|
||||
const call = f.start();
|
||||
const port = await f.receive();
|
||||
assert.equal(port.kind, "port");
|
||||
assert.equal(port.frame, call.frame);
|
||||
assert.equal(port.invocation, call.invocation);
|
||||
f.send({ kind: "port-result", ...call, sequence: port.sequence, status: "returned", output: 42 });
|
||||
const result = await f.receive();
|
||||
assert.equal(result.status, "returned");
|
||||
assert.equal(result.output, 42);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
test("authored serialization cannot forge result routing or ABI fields", async () => {
|
||||
const f = await fixture(
|
||||
registry(
|
||||
"Object.prototype.toJSON=function(){return this.status==='returned'?{status:'returned',output:42,invocation:'forged',abi:'forged'}:this;};return 42;",
|
||||
),
|
||||
);
|
||||
try {
|
||||
const call = f.start();
|
||||
const result = await f.receive();
|
||||
assert.equal(result.invocation, call.invocation);
|
||||
assert.equal(result.abi, abi);
|
||||
assert.equal(result.status, "failed");
|
||||
assert.equal(result.error.code, "RESTRICTED_PACKAGE_FAILED");
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
test("the checked portable SDK keeps codecs and nested graph ports inside the frame", async () => {
|
||||
const sdk = process.env.QUIXOS_TEST_SDK;
|
||||
assert.ok(sdk, "Set QUIXOS_TEST_SDK to the checked portable SDK");
|
||||
const C = await import(sdk),
|
||||
workspace = C.workspaceId("workspace:restricted");
|
||||
const integer = { kind: "scalar", name: "int64" },
|
||||
unit = { kind: "builtin", name: "unit" };
|
||||
const declarations = [
|
||||
{
|
||||
id: "test",
|
||||
name: "test",
|
||||
effect: "snapshot",
|
||||
receiver: null,
|
||||
input: integer,
|
||||
output: integer,
|
||||
methods: [
|
||||
{
|
||||
name: "get",
|
||||
portName: "count",
|
||||
portId: "count-read",
|
||||
action: { kind: "state", primitive: "read" },
|
||||
input: unit,
|
||||
output: integer,
|
||||
keyed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const source = `import {definePortableRegistry} from 'qx:core'; export const registry=definePortableRegistry(${JSON.stringify(declarations)},{test:async ctx=>ctx.input+await ctx.ports.count.get()});`;
|
||||
const f = await fixture(source, { sdk: [{ name: "qx:core", path: sdk }] });
|
||||
try {
|
||||
const call = f.start(C.encodeArgument(integer, 2n, { workspace }));
|
||||
const port = await f.receive();
|
||||
assert.equal(port.kind, "port");
|
||||
assert.equal(C.decodeArgument(unit, port.input, { workspace }), null);
|
||||
f.send({
|
||||
kind: "port-result",
|
||||
...call,
|
||||
sequence: port.sequence,
|
||||
status: "returned",
|
||||
output: C.encodeArgument(integer, 4n, { workspace }),
|
||||
});
|
||||
const result = await f.receive();
|
||||
assert.equal(result.status, "returned");
|
||||
assert.equal(C.decodeArgument(integer, result.output, { workspace }), 6n);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
test("ambient clock, entropy, network and native imports are denied", async () => {
|
||||
for (const code of [
|
||||
"Date.now()",
|
||||
"new Date()",
|
||||
"new Intl.DateTimeFormat().format()",
|
||||
"new Intl.DateTimeFormat().formatToParts()",
|
||||
"Math.random()",
|
||||
"crypto.randomUUID()",
|
||||
"fetch('https://example.com')",
|
||||
"(await import('node:fs')).readFileSync('/etc/passwd')",
|
||||
"(await import('./unlisted.mjs')).secret",
|
||||
]) {
|
||||
const f = await fixture(registry(`return ${code};`));
|
||||
try {
|
||||
f.start();
|
||||
assert.equal((await f.receive()).status, "failed", code);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
test("explicit dates remain useful without an implicit clock", async () => {
|
||||
const f = await fixture(registry("return new Date('2026-01-01T00:00:00Z').toISOString();"));
|
||||
try {
|
||||
f.start();
|
||||
assert.equal((await f.receive()).output, "2026-01-01T00:00:00.000Z");
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
test("initialization and regex loops are killed, then a fresh frame can start", async () => {
|
||||
for (const source of [
|
||||
"while(true){};" + registry("return 1;"),
|
||||
registry("return /(a+)+$/.test('a'.repeat(100000)+'!');"),
|
||||
]) {
|
||||
const f = await fixture(source, { deadlineMs: 350 });
|
||||
try {
|
||||
for (let n = 0; n < 2; n++) {
|
||||
f.start();
|
||||
const result = await f.receive();
|
||||
assert.equal(result.error.code, "RESTRICTED_DEADLINE");
|
||||
}
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
test("cancellation kills the entire frame before acknowledgment and rejects late port replies", async () => {
|
||||
const f = await fixture(
|
||||
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:null});"),
|
||||
);
|
||||
try {
|
||||
const call = f.start();
|
||||
const port = await f.receive();
|
||||
f.send({ kind: "cancel", ...call });
|
||||
const cancelled = await f.receive();
|
||||
assert.equal(cancelled.kind, "cancelled");
|
||||
f.send({ kind: "port-result", ...call, sequence: port.sequence, status: "returned", output: "late" });
|
||||
const next = f.start();
|
||||
const nextPort = await f.receive();
|
||||
assert.equal(nextPort.invocation, next.invocation);
|
||||
f.send({ kind: "port-result", ...next, sequence: nextPort.sequence, status: "returned", output: "fresh" });
|
||||
const result = await f.receive();
|
||||
assert.equal(result.output, "fresh");
|
||||
assert.equal(result.invocation, next.invocation);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
test("cancellation during asynchronous preparation leaves no running frame", async () => {
|
||||
const f = await fixture(registry("return 1;"));
|
||||
try {
|
||||
for (let n = 0; n < 5; n++) {
|
||||
const call = f.start();
|
||||
f.send({ kind: "cancel", ...call });
|
||||
assert.equal((await f.receive()).kind, "cancelled");
|
||||
}
|
||||
f.start();
|
||||
assert.equal((await f.receive()).output, 1);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("bounded FIFO rejects overload without losing active or queued frames", async () => {
|
||||
const f = await fixture(
|
||||
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:request.input});"),
|
||||
{ maxQueued: 2 },
|
||||
);
|
||||
try {
|
||||
const first = f.start(1),
|
||||
port = await f.receive();
|
||||
const second = f.start(2),
|
||||
third = f.start(3),
|
||||
overflow = f.start(4);
|
||||
const refused = await f.receive();
|
||||
assert.equal(refused.invocation, overflow.invocation);
|
||||
assert.equal(refused.error.code, "RESTRICTED_QUEUE_FULL");
|
||||
f.send({ kind: "cancel", ...second });
|
||||
assert.equal((await f.receive()).invocation, second.invocation);
|
||||
f.send({ kind: "port-result", ...first, sequence: port.sequence, status: "returned", output: 1 });
|
||||
assert.equal((await f.receive()).output, 1);
|
||||
const nextPort = await f.receive();
|
||||
assert.equal(nextPort.invocation, third.invocation);
|
||||
assert.equal(nextPort.input, 3);
|
||||
f.send({ kind: "port-result", ...third, sequence: nextPort.sequence, status: "returned", output: 3 });
|
||||
assert.equal((await f.receive()).output, 3);
|
||||
f.start(5);
|
||||
const fresh = await f.receive();
|
||||
f.send({
|
||||
kind: "port-result",
|
||||
invocation: fresh.invocation,
|
||||
frame: fresh.frame,
|
||||
sequence: fresh.sequence,
|
||||
status: "returned",
|
||||
output: 5,
|
||||
});
|
||||
assert.equal((await f.receive()).output, 5);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("queue wait consumes the deadline and cancelled work never reaches its ports", async () => {
|
||||
const f = await fixture(
|
||||
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:null});"),
|
||||
{ deadlineMs: 350 },
|
||||
);
|
||||
try {
|
||||
const first = f.start();
|
||||
await f.receive();
|
||||
const queued = f.start();
|
||||
const replies = [await f.receive(), await f.receive()];
|
||||
assert.deepEqual(new Set(replies.map((x) => x.invocation)), new Set([first.invocation, queued.invocation]));
|
||||
assert.ok(replies.every((x) => x.error.code === "RESTRICTED_DEADLINE"));
|
||||
const fresh = f.start();
|
||||
const port = await f.receive();
|
||||
assert.equal(port.invocation, fresh.invocation);
|
||||
f.send({ kind: "port-result", ...fresh, sequence: port.sequence, status: "returned", output: "fresh" });
|
||||
assert.equal((await f.receive()).output, "fresh");
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
// Platform entrypoint, evaluated before loading the authored registry. A fresh
|
||||
// engine process owns exactly one frame. BROKER grants only that frame's ports.
|
||||
const deny = () => {
|
||||
throw Error("RESTRICTED_AMBIENT_ACCESS");
|
||||
};
|
||||
const replace = (object, key, value) =>
|
||||
Object.defineProperty(object, key, { value, configurable: false, writable: false });
|
||||
const NativeDate = Date;
|
||||
const FixedDate = new Proxy(NativeDate, {
|
||||
construct(target, args) {
|
||||
if (!args.length) deny();
|
||||
return Reflect.construct(target, args, target);
|
||||
},
|
||||
apply() {
|
||||
deny();
|
||||
},
|
||||
});
|
||||
replace(NativeDate, "now", deny);
|
||||
replace(NativeDate.prototype, "constructor", FixedDate);
|
||||
replace(globalThis, "Date", FixedDate);
|
||||
replace(Math, "random", deny);
|
||||
// Identity/value codecs need deterministic SHA-256, never entropy or key creation.
|
||||
const digest = crypto.subtle.digest.bind(crypto.subtle);
|
||||
replace(globalThis, "crypto", Object.freeze({ subtle: Object.freeze({ digest }) }));
|
||||
for (const name of ["performance", "Temporal", "scheduler", "WebSocket", "EventSource", "SharedArrayBuffer", "Atomics"])
|
||||
replace(globalThis, name, undefined);
|
||||
for (const name of ["setTimeout", "setInterval", "fetch"]) replace(globalThis, name, deny);
|
||||
// Intl's no-argument format defaults to the clock without consulting Date.now.
|
||||
const format = Object.getOwnPropertyDescriptor(Intl.DateTimeFormat.prototype, "format").get;
|
||||
Object.defineProperty(Intl.DateTimeFormat.prototype, "format", {
|
||||
configurable: false,
|
||||
get() {
|
||||
const bound = format.call(this);
|
||||
return (value) => {
|
||||
if (value === undefined) deny();
|
||||
return bound(value);
|
||||
};
|
||||
},
|
||||
});
|
||||
const formatToParts = Intl.DateTimeFormat.prototype.formatToParts;
|
||||
replace(Intl.DateTimeFormat.prototype, "formatToParts", function (value) {
|
||||
if (value === undefined) deny();
|
||||
return formatToParts.call(this, value);
|
||||
});
|
||||
let used = false;
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
if (used) return new Response("FRAME_EXPIRED", { status: 409 });
|
||||
used = true;
|
||||
const input = await request.json();
|
||||
const source = await import("./package.mjs");
|
||||
const registry = source[env.CONFIG.registryExport];
|
||||
if (
|
||||
!registry ||
|
||||
typeof registry.invoke !== "function" ||
|
||||
JSON.stringify(registry.exports) !== JSON.stringify(env.CONFIG.exports)
|
||||
)
|
||||
throw Error("RESTRICTED_REGISTRY_MISMATCH");
|
||||
let open = true;
|
||||
const channel = async (value) => {
|
||||
if (!open) throw Error("EXPIRED_CONTEXT");
|
||||
const response = await env.BROKER.fetch("http://frame/port", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(value),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!open) throw Error("EXPIRED_CONTEXT");
|
||||
if (result.status !== "returned")
|
||||
throw Object.assign(Error(result.error?.message ?? "Port failed"), {
|
||||
code: result.error?.code ?? "PORT_FAILED",
|
||||
});
|
||||
return result.output;
|
||||
};
|
||||
try {
|
||||
const output = await registry.invoke(
|
||||
{ exportId: input.exportId, workspace: input.workspace, receiver: input.receiver, input: input.input },
|
||||
channel,
|
||||
);
|
||||
const body = JSON.stringify({ status: "returned", output });
|
||||
if (new TextEncoder().encode(body).length > env.CONFIG.maxBytes) throw Error("RESTRICTED_OUTPUT_LIMIT");
|
||||
return new Response(body, { headers: { "content-type": "application/json" } });
|
||||
} catch (error) {
|
||||
// Only authored/codec rejection codes cross the engine boundary. Engine
|
||||
// diagnostics stay private to the supervisor, never become protocol data.
|
||||
const code =
|
||||
typeof error?.code === "string" && /^[A-Z0-9_]{1,128}$/.test(error.code)
|
||||
? error.code
|
||||
: "RESTRICTED_PACKAGE_FAILED";
|
||||
return Response.json({ status: "failed", error: { code, message: code } });
|
||||
} finally {
|
||||
open = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
# Pinned upstream engine. This binary supplies an import/capability boundary;
|
||||
# production execution additionally requires a workspace VM and OS limits.
|
||||
{ pkgs }:
|
||||
let
|
||||
version = "1.20261005.1";
|
||||
platforms = {
|
||||
x86_64-linux = {
|
||||
name = "linux-64";
|
||||
hash = "sha256-zm0T3YLo49ORPwR4iwP+WkQBMkiZQYftc+ArBQEoNGY=";
|
||||
};
|
||||
aarch64-linux = {
|
||||
name = "linux-arm64";
|
||||
hash = "sha256-jkvHyexr8Sn2wyqmLJJEOOGfAkfaVPLI7llHOX+FbxU=";
|
||||
};
|
||||
};
|
||||
platform =
|
||||
platforms.${pkgs.stdenv.hostPlatform.system}
|
||||
or (throw "Restricted execution requires Linux x86_64 or aarch64");
|
||||
in
|
||||
pkgs.stdenvNoCC.mkDerivation {
|
||||
pname = "quixos-workerd";
|
||||
inherit version;
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://registry.npmjs.org/@cloudflare/workerd-${platform.name}/-/workerd-${platform.name}-${version}.tgz";
|
||||
inherit (platform) hash;
|
||||
};
|
||||
nativeBuildInputs = [ pkgs.autoPatchelfHook ];
|
||||
buildInputs = [ pkgs.stdenv.cc.cc.lib ];
|
||||
sourceRoot = "package";
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
install -Dm755 bin/workerd "$out/bin/workerd"
|
||||
runHook postInstall
|
||||
'';
|
||||
doInstallCheck = true;
|
||||
installCheckPhase = ''
|
||||
"$out/bin/workerd" --version | grep -Fx 'workerd 2026-10-05'
|
||||
'';
|
||||
meta = {
|
||||
description = "Pinned workerd engine for VM-scoped Quixos restricted execution";
|
||||
homepage = "https://github.com/cloudflare/workerd";
|
||||
license = pkgs.lib.licenses.asl20;
|
||||
platforms = builtins.attrNames platforms;
|
||||
};
|
||||
}
|
||||
@@ -15,7 +15,7 @@ let
|
||||
group = import ./artifacts.nix {
|
||||
inherit pkgs src;
|
||||
inherit (context)
|
||||
plan
|
||||
buildContract
|
||||
core
|
||||
sharedRuntime
|
||||
typescript
|
||||
|
||||
@@ -11,7 +11,7 @@ let
|
||||
group = import ./artifacts.nix {
|
||||
inherit pkgs src service;
|
||||
inherit (context)
|
||||
plan
|
||||
buildContract
|
||||
core
|
||||
sharedRuntime
|
||||
typescript
|
||||
|
||||
Reference in New Issue
Block a user