Compare commits

...
Author SHA1 Message Date
Quixos Subtree Publisher c2454054da Publish quixos-nix-helpers from 99248f90aba2ef64f089244a909159d432efb8af 2026-10-29 12:30:56 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 a962586381 Retire staged Yarn dependencies during Nix installation (#473) 2026-10-29 12:30:56 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 43fcf6b27a Bound ordinary Restricted Execution engine socket directories (#503) 2026-10-29 12:30:56 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 3bb4324aa0 Budget OpenSSH temporary sockets in the shared address policy (#503) 2026-10-29 12:30:56 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 df9f50ea31 Reuse the canonical language bridge in deep Agent tmux fixtures (#503) 2026-10-29 12:30:56 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 4a87fc2c7d Validate complete engine addresses and retain shared socket inputs (#503) 2026-10-29 12:30:55 +00:00
Quixos Development Agent 01f26965-84fa-4a79-8069-725682c13065 97ce77d3de Share the canonical socket implementation with public Nix helpers (#503) 2026-10-29 12:30:55 +00:00
Quixos Subtree Publisher 5657043450 Publish quixos-nix-helpers from e4393946ba63cfef75580f6aa6ba840035e7d43c 2026-10-13 10:19:49 +00:00
Timothy J. Aveni 9797799e83 Compose independent native Central and System development lifecycle; qualify checked Graph and VM providers 2026-10-12 02:19:34 -07:00
Timothy J. Aveni f67294f26d Qualify hosted development and compose service ownership 2026-10-11 13:53:36 -07:00
Quixos Subtree Publisher eb38d91c1b Publish quixos-nix-helpers from 358e649a7d70828db2ef96d5fed18931e48bc498 2026-10-10 15:37:44 +00:00
Timothy J. Aveni 6e591d5588 Make Node dependency outputs reproducible without build caches 2026-10-10 08:37:44 -07:00
Quixos Subtree Publisher 4a0eec33d8 Publish quixos-nix-helpers from 286ada5419c4fc2184b387cea4715fae1a74bde5 2026-10-10 00:10:14 +00:00
Timothy J. Aveni d6434efc28 Reuse checked bootstrap catalogs and coordinate execution deadlines 2026-10-09 17:10:14 -07:00
Timothy J. Aveni ccb48afc04 Document package architecture and organize current guidance and project backlog 2026-10-09 10:33:39 -07:00
Quixos Subtree Publisher e4e5c1f175 Publish quixos-nix-helpers from 98cd7bc237027368fd2c77f294f59281ef3df160 2026-10-08 12:33:28 +00:00
Timothy J. Aveni 1b91e21d49 Keep restricted engines reserved through asynchronous retirement 2026-10-08 05:21:10 -07:00
Quixos Subtree Publisher fcfce6259e Publish quixos-nix-helpers from 806ea0e9f41340acf119b7374f5150ef5b378074 2026-10-08 11:31:16 +00:00
Timothy J. Aveni dec3aa475a Expose workspace-scoped restricted artifact placement and live executor activity
Show per-export placement, static eligibility blockers, bounded metadata and live pool/idle/timing diagnostics. Attach inspector updates through one private shard stream and the existing authenticated SSE hub without waking graph views or VMs. Fix packed agent preparation to use OpenSSH and the worker’s own private systemd credential.
2026-10-08 04:31:16 -07:00
Quixos Subtree Publisher 50b6442a41 Publish quixos-nix-helpers from 57684c7548883dc4fc25dc8f943c7a43b3489fcf 2026-10-08 08:03:14 +00:00
Timothy J. Aveni a9f42e14c3 Include canonical hashing in restricted package execution 2026-10-08 01:03:14 -07:00
Quixos Subtree Publisher eba5d732f2 Publish quixos-nix-helpers from 932fe7714761fb8b5db4bd6401357cdff762a428 2026-10-08 05:08:28 +00:00
Timothy J. Aveni b1397465ee Admit checked restricted artifacts without individual code approvals 2026-10-07 21:43:44 -07:00
Timothy J. Aveni b5046ad680 Confine restricted engines before launch and preserve the reader recovery baseline 2026-10-07 20:47:53 -07:00
Quixos Subtree Publisher 9a211ee5d7 Publish quixos-nix-helpers from abc91ab1cbbd61ffd7f0a49dbc5768598ac8745c 2026-10-08 00:03:01 +00:00
Timothy J. Aveni dbd159be70 Keep checked browser runtime versions independent of authority upgrades 2026-10-07 17:03:01 -07:00
Timothy J. Aveni d6e66b537f Pool host-approved restricted code with scoped and fair graph execution 2026-10-07 16:47:19 -07:00
Timothy J. Aveni 929fdb4a28 Reuse restricted engines with fresh invocation state and revocable ports 2026-10-07 16:14:15 -07:00
Timothy J. Aveni f934347041 Bound checked candidate metadata and restricted invocation resources 2026-10-07 15:51:22 -07:00
Quixos Subtree Publisher ea923c40c4 Publish quixos-nix-helpers from 039f055a7dcff2a29d59a8c37d7433b269b50f7a 2026-10-07 11:04:32 +00:00
Timothy J. Aveni 47fcb47d5c Implement VM-scoped restricted registry transport 2026-10-07 04:04:32 -07:00
Quixos Subtree Publisher 91679e8cc8 Publish quixos-nix-helpers from e5aec2bb26110ed474929f353ee51ff46d4f20d6 2026-10-01 11:03:19 +00:00
Timothy J. Aveni 1f7aa80130 Instrument and improve incremental workspace checks
Profile source capture, Nix evaluation/realization, resource/query checking,
and implementation builds without putting timings into immutable outputs.
Keep modular repositories; bound independent capture work, snapshot jj once
per observation, and retain final concurrent-edit and Git completeness checks.

Compose exact resource/closure/frontend/toolchain checks as memoized Nix
derivations. Preserve whole-workspace validation and the full compiler path
when no checked dependency is supplied. Share canonical Nixpkgs by content,
index workspace build contexts once, and narrow lock/driver/compiler inputs
with conservative complete-source fallback. Batch React witness checks only
when distinct source closures cannot share ambient declarations; otherwise
run the original independent TypeScript checks.

Seed local authoring toolchains with exact canonical fetch identities when
they match the selected inputs; retain store-path inputs for overrides.
Add generic correctness checks rather than permanent Reader timing/content
fixtures. Verify actual Reader edits and rejection/recovery in a private
workbench, preserving the original user's workbench and runtime.

Query checking currently caches at resource/closure boundaries, not individual
query semantic dependency boundaries. Template helper pins and reproducible
fixture snapshots follow in a separate publication change.
2026-10-01 04:03:19 -07:00
Timothy J. Aveni 6227d64b08 Build Camino packages from explicit contract inputs
Project source-free capability contracts and keep exact source provenance in package assembly. Reuse compiled groups across unrelated graph and dependency implementation edits. Keep React public inputs separate from workspace query lowering and retain explicit type-companion dependencies.

Allow independent execution-package checks, scope resource compilation to declared dependencies, and memoize source closure traversal. Document generic-specialization limits and measured Reader rebuild behavior.

Verified protocol, artifact, package, React, and managed activation checks; retained-Git independent/workspace artifact equality and incompatible type dependency rejection; nix fmt -- --check and lint. One-off Reader measurements recompiled 1 of 13 groups for a child edit and 0 for a workspace QX default edit.
2026-09-30 22:55:07 -07:00
Timothy J. Aveni 988e10263a Add graph-aware React controls with stable asynchronous selection
Add typed command/live bindings, Choice and Toggle, CRDT text wrappers, and explicit browser-local controls. Migrate Reader and current example inputs. Enforce editable-control authoring checks in the React artifact backend.

Command controls display query observations, permit one local execution at a time, and delay pending feedback for 200 ms. Include keyboard, focus, shadow-root, top-layer popup, rejection/no-op, native-field eligibility, and IME coverage.

Verified SDK builds and tests, synchronous-editing browser qualification, Reader/Tasks qualification, presentation-mount qualification, and the review-dashboard browser test. Monorepo formatting and lint pass.

The full composition qualification stops at the unchanged combined-to-review workspace fixture transition: STORAGE_MODEL_CHANGED from changed/removed stored conformance owners. Its dashboard browser test passes when run directly.
2026-09-30 00:20:00 -07:00
Timothy J. Aveni 8e55a11c4a Correct Reader producer completion and prepare only the active source
Route durable completion to the initiating PresentationMount, guarded against stale navigation and unrelated fan-out. Publish Gutenberg passages directly, preserve unfinished manual drafts, and open completed documents in the main reader pane. Prepare the selected provider and explicitly opened inspector only. Consolidate Studio factory contracts so optional execution placement is determined by implementations. Support typed stateful presentation props and local-module role selection in the React build layer. Update integration, browser, portal, and authoring documentation.
2026-09-28 21:54:18 -07:00
Timothy J. Aveni b0d9453a51 Make local graph execution optional across composed packages 2026-09-28 21:05:42 -07:00
Timothy J. Aveni 009c5fcb4d Complete prepared presentation build and runtime integration 2026-09-27 18:51:05 -07:00
Timothy J. Aveni d6efe5622d Add shared Quixos React controls and delayed component fallback 2026-09-24 12:11:15 -07:00
Timothy J. Aveni 99b6b5110f Make Camino examples and scaffolds complete Yarn and Nix packages 2026-09-22 15:31:42 -07:00
Quixos Subtree Publisher db5e4a4aa8 Publish quixos-nix-helpers from 54e443af5dd963f1fa6f83f062f4943b46ad2535 2026-09-22 22:27:14 +00:00
Timothy J. Aveni f34ce87f55 Make Camino examples and scaffolds complete Yarn and Nix packages 2026-09-22 15:27:14 -07:00
Quixos Subtree Publisher 2a7034fdd2 Publish quixos-nix-helpers from d74b619314718cce6da10800d278d6bcc04b71fa 2026-09-22 22:04:28 +00:00
Timothy J. Aveni 785c54992f Make Camino examples and scaffolds complete Yarn and Nix packages 2026-09-22 15:04:28 -07:00
Timothy J. Aveni 0da022e216 Build composable Camino packages with explicit type and source dependencies 2026-09-22 14:42:54 -07:00
Timothy J. Aveni 85734e3003 Implement typed module contracts and conformance bindings 2026-09-22 13:51:32 -07:00
Timothy J. Aveni 0d7d5daea6 Load checked installations independently of the running compiler (WI-16)
Keep compiler-dependent checks in Nix builds; load stored plans and programs with original provenance and runtime artifact validation. Allow an identical executable world to receive a new activation epoch after a toolchain rebuild.

Compiler-update/altered-program regression, coordinator and full sandboxed integration checks pass; lint and formatting pass. Default platform/source wiring remains the next task.
2026-09-21 22:01:47 -07:00
Timothy J. Aveni daa20ca79f Build checked component artifacts and shared React session SDK (WI-09) 2026-09-21 16:01:48 -07:00
Timothy J. Aveni 95c9c3db7a Add independently owned reconnectable execution units (WI-03) 2026-09-21 12:44:36 -07:00
Timothy J. Aveni 66512e84bb Compile and dispatch authoritative acceptance hooks with target-specific package roots 2026-09-20 22:05:59 -07:00
Timothy J. Aveni ca0efe7252 Build checked asynchronous service registries with Nix 2026-09-20 21:06:45 -07:00
Timothy J. Aveni 9257198eec Add cooperative cancellation for asynchronous package invocations 2026-09-20 20:07:57 -07:00
Timothy J. Aveni f352ac0c3b Build checked portable package artifacts and shared runtime assets 2026-09-20 00:42:19 -07:00
Quixos Subtree Publisher e2747066ca Publish quixos-nix-helpers from 1de28b236de076d239752b77553f833dfbdb5b43 2026-09-16 23:49:09 +00:00
Timothy J. Aveni 0c3c9c6ad8 Revert generated-file renaming; retain formatter exclusions 2026-09-15 20:13:10 -07:00
Timothy J. Aveni b3d31b37b3 Mark first-party generated files with .gen; preserve third-party conventions 2026-09-15 17:57:46 -07:00
Timothy J. Aveni 6f3814587f Format authored monorepo code with pinned language formatters 2026-09-15 15:41:24 -07:00
Timothy J. Aveni 1c09bf43c2 Simplify workspace authoring and isolate managed component styles
Use imperative scaffolds without a registry/refresh lifecycle; preserve authored runtime wiring and generate checked descriptors. Consolidate binding options, embed checked TSX/CSS source, queue mutable convergence while allowing parallel immutable checks, and preload real CLI observations. Add Shadow DOM boundaries with scoped overlays and composed-event handling. Update template/toolchain pins and document VM-free authoring, browser, and stateful acceptance; no deployment or default-template selection.
2026-09-14 20:55:32 -07:00
Quixos Subtree Publisher 6657af7ca0 Publish quixos-nix-helpers from 8484d99fcf78a49341a17597bc62211e5f1807dd 2026-09-15 03:49:38 +00:00
Timothy J. Aveni e3080467c9 Simplify workspace authoring and isolate managed component styles 2026-09-14 20:49:38 -07:00
Quixos Subtree Publisher e84b62f4d6 Publish quixos-nix-helpers from fc13e9b8411c2210b12db5e558e2b4fa7785505e 2026-09-15 03:42:37 +00:00
Timothy J. Aveni 06c668b587 Simplify workspace authoring and isolate managed component styles 2026-09-14 20:42:37 -07:00
Timothy J. Aveni 56fa26acc8 Unify workspace authoring, verification and scaffolding workflows
Use exact jj snapshots and one candidate-bound Nix builder for incremental checks, template validation and activation. Keep provenance internal and separate recovery checkpoint failures from local command success.

Provision workspace-scoped managed package/interface repositories with recoverable Central effects. Add TypeScript/React presets, function and dependency commands, and scaffold enrollment for all TODO packages. Install authoring guides and controlled Codex sandbox rules.

Invalidate module resolutions across cutover, including in-flight races, and content-address host platform entries. Strengthen domain-model and verification instructions.

Validated real jj/Nix authoring, React/Slate dependency installation, bottom-up local Git publication, packaged CLI tests, PostgreSQL recovery/auth tests, Web Studio tests and host configuration. Public protocol/helpers and the validated 19-resource TODO template are published. Retained the approved exact private baseline and updated the installation's default template pin to 68d54f0d52be433ebf60bdc1faf7646c57f90307. Master and live deployments remain unchanged. See docs/WORKSPACE_AUTHORING_PROGRESS.md.
2026-09-13 23:06:38 -07:00
Quixos Subtree Publisher cf7945abb6 Publish quixos-nix-helpers from 7fd5e15105cef21d2b1c3da860c53c5033f66256 2026-09-14 05:46:08 +00:00
Timothy J. Aveni 74b6ff2ace Unify workspace authoring, verification and scaffolding workflows 2026-09-13 22:46:08 -07:00
Timothy J. Aveni 5861e91298 Migrate TODO implementations to generated candidate bindings 2026-09-13 20:28:50 -07:00
Quixos Subtree Publisher d109410617 Publish quixos-nix-helpers from 22bb3d02264980d74de65c34bbbcb81764dc65c0 2026-09-14 03:28:50 +00:00
Quixos Subtree Publisher 8b2a224698 Publish quixos-nix-helpers from e25eee6ce4f13702b2454a9354bc79a29eb2e4a1 2026-09-14 03:01:53 +00:00
Timothy J. Aveni 80b65b6f0c Migrate TODO implementations to generated candidate bindings 2026-09-13 20:01:53 -07:00
Timothy J. Aveni 32652d5279 Implement workspace evolution, migrations, and runtime continuity
Enable evolution by default for source-backed workspaces. Add stable
conformance ownership, semantic-major review, candidate typechecking,
and durable fenced cutover with explicit migrations and forward recovery.

Independently supervise package runtimes so unchanged resource owners keep
their processes and connections across cutover. Add scoped invocation
authority, resource sessions, and typed callback rebinding.

Wire opaque object references through generated bindings and RPCs. Add
canonical relationship sets, keyed maps, and ordered lists with scoped
transactional mutations, revision checks, and inverse consistency. Support
planned cascade deletion, protection, tombstones, and lifecycle foundations.

Add journaled structural edits, package/function/migration scaffolding,
managed repository creation, and resumable bottom-up dependency pin
publication. Document lifetime boundaries, revision pinning, prototype
compatibility policy, commands, and deferred work.

Validate with 210 tests, user-systemd process/connection continuity,
generated-package TypeScript checks, and Nix host/protocol checks.
TTL handoff, physical reclamation, general multi-step migrations, and
root-systemd migration isolation acceptance remain deferred.
2026-09-10 18:29:25 -07:00
Timothy J. Aveni 7e4fb60155 Generate typed QX bindings and add source editing tools 2026-09-08 15:42:20 -07:00
Quixos Subtree Publisher b6cd2f3cd7 Publish quixos-nix-helpers from b384206b9c01a9ac50030a583d028b71291bc8c5 2026-09-06 07:07:06 +00:00
Timothy J. Aveni 2f66f25153 Build workspace agent, capability graph, and versioned cutovers 2026-09-06 00:07:06 -07:00
Quixos Subtree Publisher 2ee30c177c Publish quixos-nix-helpers from fa363fa2f4e83d0996c1229ab1ecf9e0541d5d4f 2026-09-05 04:28:56 +00:00
Timothy J. Aveni 7177130c03 Introduce repository-backed capability workspaces
- define and validate the capability and resource-lock languages
- provision workspace source repositories through Central Gitea
- build package runtimes from pinned standalone sources
- replace legacy schema compilation with workspace persistence plans
- add stable optimistic registers and Automerge CRDT documents
- modernize TypeScript/Nix package builds and runtime activation readiness
2026-09-04 19:08:10 -07:00
Quixos Subtree Publisher 1965741625 Publish quixos-nix-helpers from ce0dd80f56df35bf3db1be01236a949a704cb4c2 2026-09-04 17:41:38 +00:00
Timothy J. Aveni 220aaaa08c Host workspace repositories on Central Gitea 2026-09-04 10:41:38 -07:00
47 changed files with 5033 additions and 335 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
{
"version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git",
"sourceCommit": "b5db4f65b59842ba912cb89d11237d0ce428ea95",
"sourceRepo": "https://gitea.internal.quixos.org/quixos/quixos.git",
"sourceCommit": "99248f90aba2ef64f089244a909159d432efb8af",
"sourcePath": "quixos-instance/quixos-nix-helpers",
"exportName": "quixos-nix-helpers",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
"mirrorRemote": "https://repos.quixos.org/quixos-toolchain/quixos-nix-helpers.git"
}
File diff suppressed because one or more lines are too long
+193
View File
@@ -0,0 +1,193 @@
# Checked capability and component artifacts
This is the **current helper/output format**, not the core package abstraction.
See [the architecture](../../docs/ARCHITECTURE.md) and
[current gaps](../../docs/ARCHITECTURE_STATUS.md). In particular, the universal
artifact inventory and component-only omission of a server are implementation
gaps to resolve, not requirements for arbitrary package servers.
`mkCaminoArtifacts` is the fresh package builder. It regenerates candidate
bindings, typechecks the entry's dependency closure, bundles declared targets,
generates a process service when requested, and emits content manifests and
receipts. It has no migration step or package-defined shell hooks.
```nix
# Inside a per-system flake output:
quixosPackages.checkedArtifacts =
{ buildContract, sourceDigest, generator, core, sharedRuntime, typescript, packageRevisionId, ... }:
helpers.mkCaminoArtifacts {
inherit pkgs buildContract sourceDigest core sharedRuntime typescript packageRevisionId;
protocol = generator;
src = ./.;
portable = {
entry = "src/portable.ts";
bindingOutput = "src/gen/qx.ts";
registryExport = "registry";
targets = [ "browser" "server" ];
};
};
```
`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
omits `portable` and supplies `service.entry`, an ordinary JavaScript process entry
using the versioned stdin/stdout protocol. Custom services own their lifecycle and
may hold external resources. This first helper supports JavaScript custom service
entries; other languages can emit the same checked artifact contract independently.
Additional portable source dependencies can be provided as a locked `nodeModules` directory.
The workspace supplies the exact SDK and compiler; packages cannot replace those
with their own versions. Authored source symlinks and non-bundled portable imports
other than the SDK and server Node builtins are rejected. This is build closure
checking, not a purity proof or security sandbox.
`mkCaminoYarnDependencies` installs the locked dependency recipe and exports its
installed Node dependency tree, including native package results and executable
links. Generated Yarn install/linker state stays outside runtime outputs. Yarn
describes its install-state file as an optimization cache in its
[configuration reference](https://yarnpkg.com/configuration/yarnrc#installStatePath).
This helper output is a dependency directory for a build, not a standalone
Package or a runnable Yarn project. Its `devShell` supplies freestanding Yarn for
editable projects; do not run Yarn against the readonly dependency output.
Installation discards its disposable archive cache and streams dependencies into
the output while retiring staged files, including across separate build/store
mounts. It preserves package layout, native results, hidden data and executable
links without holding two complete dependency trees. The maintained
`camino-yarn-dependencies` check covers transfer and runtime layout; consuming
Studio and Package-builder checks retain real locked-installation coverage.
Use the shared `installDependencies` assembly for consumers: authored libraries
are copied into the consumer's `node_modules`, where Node can find their
transitive dependencies, while selected SDK links retain their canonical type
identity. Reserved SDK substitutions fail checks. Web Studio uses this same
assembly rather than retaining a full Yarn project just to resolve UI libraries.
Its dependency inputs contain manifests, lock and Yarn configuration/plugin
recipe; ordinary Studio source edits do not enter that dependency derivation.
Recipes with local/workspace dependencies must still include their needed source.
This is language-specific build machinery; it adds no core Package file contract.
The core/Automerge bundle and Replicache bundle are built once. Browser modules
import the core through a content-addressed gateway URL, independent of package
identity. Each manifest covers the shared bytes it uses. The gateway retains its
workspace authentication and explicit file registry; it never interprets an HTTP
path as a Nix store path. Applications cannot import Replicache through the SDK.
`checked-artifacts.nix` evaluates a package's `checkedArtifacts` output without
activation. During the checked Nix build, `loadCheckedArtifactWorld` verifies manifest bytes, the selected
generator identity, and exact regenerated binding sources. Receipts are consistency
evidence from a trusted Nix build, not cryptographic attestations of remote builds.
The build stores the checked plan, kernel/query programs, their digests and original
compiler provenance in its immutable candidate output. Runtime loading validates
that output's identities, artifact contents and supported runtime contracts. It
does not regenerate bindings or programs with the currently running compiler.
Nix owns rebuilding and cache reuse when a toolchain input changes.
The root `camino-artifact-builds` check exercises actual reader/service outputs,
module and process execution, browser shared imports, rejection recovery, and
altered receipt rejection at the build boundary. The managed workspace host loads
the resulting checked candidate before activation. No adapter pretends this ABI
works in the old server dispatcher.
Component packages add `components = { inherit (react) sdk runtime; entries; }`.
The ordinary execution candidate checker supplies the selected React build inputs
when the package declares components. Each entry maps a component ID to
`{ entry = "src/card.tsx"; export = "Card"; }`. See the
[React SDK](../camino-react/README.md) for bindings and hooks. CSS is emitted as a
checked artifact; local image/font imports are inlined. The helper rejects
undeclared external assets and substituted React/client dependencies.
Component-only packages produce no backend service.
`camino-component-artifacts` checks bad subject/SDK/dependency/asset inputs and
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 = 10000;
maxBytes = 1048576;
maxPortCalls = 1000;
maxQueued = 8;
};
};
```
Each invocation gets a fresh Worker in a reusable engine process. An engine runs
one active frame; the adapter has bounded concurrency and queueing. 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.
The default root/Package Invocation wall deadline is 10 seconds. The separate
root CPU allowance remains at most 4 CPU seconds; extra wall time accommodates
queueing and graph-port waits. Ordinary graph database transactions and idle
transactions allow 30 seconds, and ordinary client HTTP requests allow 45 seconds
so server failures can be returned. Nested calls inherit the remaining root
budget; explicit smaller limits still apply. Restore/activation jobs retain their
separate budgets. These defaults are starting limits, not latency targets.
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.
The packaged runtime uses the platform's Bubblewrap/seccomp launcher. Only the
engine's read-only runtime closure and private broker directory are mounted;
native filesystem/process/network escape probes qualify that boundary. Systemd
limits the whole package in a workspace VM. Shared authority additionally charges
each engine to its own cgroup before launch. No per-isolate memory protection or
Cloudflare hosted side-channel defenses are claimed. See the
[runtime decision](../../docs/RESTRICTED_RUNTIME_DECISION.md#os-process-confinement-implementation)
for the exact boundary and residual risks. 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.
+131
View File
@@ -0,0 +1,131 @@
# Fresh artifact builder. Candidate plans and runtime dependencies are supplied by the workspace compiler.
{
pkgs,
src,
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,
typescript ? core.dependencies,
packageRevisionId,
portable ? null,
remote ? null,
service ? { },
nodeModules ? null,
components ? null,
group ? false,
}:
let
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
buildContract
compiler
packageRevisionId
core
sharedRuntime
nodeModules
service
;
group = true;
components =
if components == null then
null
else
{
inherit (components) sdk runtime entries;
dependencies = components.sdk.dependencies;
bindingOutput = components.bindingOutput or "src/gen/client.ts";
contracts = components.contracts or [ ];
};
source = import ./implementation-source.nix {
inherit (pkgs) lib;
inherit src;
};
portable =
if portable == null then
null
else
{
inherit (portable) entry;
entries = portable.entries or { };
registryExport = portable.registryExport or "registry";
bindingOutput = portable.bindingOutput or "src/gen/qx.ts";
targets =
portable.targets or [
"browser"
"server"
];
};
remote =
if remote == null then
null
else
{
inherit (remote) entry;
registryExport = remote.registryExport or "registry";
bindingOutput = remote.bindingOutput or "src/gen/remote.ts";
};
node = "${pkgs.nodejs_24}/bin/node";
tsc = "${typescript}/node_modules/typescript/bin/tsc";
esbuild = "${pkgs.esbuild}/bin/esbuild";
shell = "${pkgs.runtimeShell}";
serviceAdapter = ./portable-service.mjs;
restrictedSDK = "${(import ./restricted-execution { inherit pkgs core; }).sdk}/core.mjs";
}
);
compiled =
pkgs.runCommand "camino-compiled-artifacts"
{
passthru.buildConfig = config;
QUIXOS_TRACE_PROFILE = "1";
}
''
${pkgs.nodejs_24}/bin/node ${buildTools}/build-artifacts.mjs ${config}
'';
in
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;
}
+97
View File
@@ -0,0 +1,97 @@
import fs from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
import { pathToFileURL } from "node:url";
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 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");
const generatorDigest = digest(await fs.readFile(config.compiler));
const implementations = [],
components = [],
files = new Map(),
ids = new Set();
const recordFile = async (name, mediaType) => {
const bytes = await fs.readFile(path.join(output, compiler.artifactPath(name)));
return { path: name, bytes: bytes.length, digest: digest(bytes), mediaType };
};
await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true });
for (const root of config.groups) {
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 !== 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.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 });
for (const artifact of [...manifest.implementations, ...(manifest.components ?? [])]) {
const key = `${artifact.compatibility === "org.quixos.react.esm/1" ? "module" : "implementation"}:${artifact.id}`;
if (ids.has(key)) throw Error("Duplicate artifact group export " + key);
ids.add(key);
for (const file of artifact.files) {
const bytes = await read(pkg.revision, file.path, file.bytes);
if (bytes.length !== file.bytes || digest(bytes) !== file.digest)
throw Error("Modified group asset " + file.path);
const previous = files.get(file.path);
if (previous && JSON.stringify(previous) !== JSON.stringify(file))
throw Error("Conflicting group asset " + file.path);
if (!previous) {
await fs.mkdir(path.dirname(path.join(output, file.path)), { recursive: true });
await fs.writeFile(path.join(output, file.path), bytes, { mode: file.path.startsWith("bin/") ? 0o755 : 0o644 });
files.set(file.path, file);
}
}
}
for (const [name, bytes] of expected.sources) {
const actual = await fs.readFile(path.join(root, name));
if (!Buffer.from(bytes).equals(actual)) throw Error("Modified group type witness " + name);
await fs.writeFile(path.join(output, name), bytes);
}
implementations.push(...manifest.implementations);
components.push(...(manifest.components ?? []));
}
if (implementations.filter((entry) => entry.compatibility === "quixos-service-v1").length > 1)
throw Error("A package can have only one process service owner");
if (!implementations.some((entry) => entry.compatibility === "quixos-service-v1"))
await buildService({
config: { ...config, service: {} },
compiler,
pkg,
output,
coreServer: path.join(config.sharedRuntime, "core.mjs"),
sharedFiles: [...files.values()].filter((file) => file.path.startsWith("share/quixos/shared/")),
implementations,
recordFile,
});
const manifest = compiler.parsePackageArtifacts({
schemaVersion: 1,
packageRevisionId: pkg.revision,
bindingDigest: contract.digest,
sourceDigest: config.sourceDigest,
versions: contract.definition.versions,
implementations,
...(components.length ? { components } : {}),
});
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));
// Source selection is attested here; compilation groups retain their own receipt.
compiler.checkPackageCoverage({ ...pkg, bindingDigest: contract.digest, sourceDigest: config.sourceDigest }, manifest);
+39
View File
@@ -0,0 +1,39 @@
{
pkgs,
context,
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)
buildContract
sourceDigest
packageRevisionId
sharedRuntime
;
inherit compiler;
node = "${pkgs.nodejs_24}/bin/node";
esbuild = "${pkgs.esbuild}/bin/esbuild";
shell = "${pkgs.runtimeShell}";
serviceAdapter = ./portable-service.mjs;
}
);
in
pkgs.runCommand "camino-package"
{
passthru = {
caminoGroups = groups;
assemblyConfig = config;
};
}
''
${pkgs.nodejs_24}/bin/node ${buildTools}/assemble-package.mjs ${config}
''
+69
View File
@@ -0,0 +1,69 @@
import { createRequire } from "node:module";
import path from "node:path";
// Resolve build dependencies from the package's locked node_modules, not from
// the helper's own Nix-store location. This program runs only during the build.
const require = createRequire(path.join(process.cwd(), "package.json"));
const { build } = require("esbuild");
const options = JSON.parse(process.argv[2]);
const platform = new Map([
["react", "/__quixos/platform/react/v18.mjs"],
["react/jsx-runtime", "/__quixos/platform/react-jsx-runtime/v18.mjs"],
["react/jsx-dev-runtime", "/__quixos/platform/react-jsx-dev-runtime/v18.mjs"],
["@quixos/web-studio-react-runtime", "/__quixos/platform/web-studio-react-runtime/v1.mjs"],
]);
await build({
...options,
plugins: [
{
name: "quixos-browser-source",
setup(api) {
api.onResolve({ filter: /\?browser-source$/ }, async ({ path: specifier, resolveDir }) => {
if (!specifier.startsWith("./") && !specifier.startsWith("../"))
throw new Error("Browser source imports must name a relative compiled module");
const resolved = await api.resolve(specifier.slice(0, -"?browser-source".length), {
resolveDir,
kind: "import-statement",
});
if (resolved.errors.length) return { errors: resolved.errors };
return { path: resolved.path, namespace: "browser-source" };
});
api.onLoad({ filter: /.*/, namespace: "browser-source" }, async ({ path: entry }) => {
const browser = await build({
entryPoints: [entry],
bundle: true,
write: false,
outfile: "component.mjs",
format: "esm",
platform: "browser",
target: "es2022",
metafile: true,
plugins: [
{
name: "quixos-platform",
setup(browserApi) {
browserApi.onResolve({ filter: /.*/ }, ({ path: name }) =>
platform.has(name) ? { path: platform.get(name), external: true } : undefined,
);
},
},
],
});
const js = browser.outputFiles.find((file) => file.path.endsWith(".mjs"));
if (!js) throw new Error("Browser source build produced no JavaScript");
const css = browser.outputFiles.find((file) => file.path.endsWith(".css"));
if (css && Object.values(browser.metafile.outputs).some((output) => output.exports?.includes("styles")))
throw new Error("Use either imported CSS or an explicit styles export, not both");
if (browser.outputFiles.some((file) => !file.path.endsWith(".mjs") && !file.path.endsWith(".css")))
throw new Error("Browser assets must be embedded, not emitted as unserved files");
const source = js.text + (css ? `\nexport const styles = ${JSON.stringify(css.text)};\n` : "");
return {
contents: `export default ${JSON.stringify(source)};`,
loader: "js",
watchFiles: Object.keys(browser.metafile.inputs),
};
});
},
},
],
});
+245
View File
@@ -0,0 +1,245 @@
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. */
import fs from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
import { builtinModules } from "node:module";
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 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");
const generatorDigest = digest(await fs.readFile(config.compiler));
const runtime =
config.portable || config.remote
? JSON.parse(await fs.readFile(path.join(config.sharedRuntime, "runtime.json"), "utf8"))
: { files: [] };
const relative = (p) => compiler.artifactPath(p);
const implementations = [];
const generated = new Map();
const recordFile = async (p, mediaType) => {
const bytes = await fs.readFile(path.join(output, relative(p)));
return { path: p, digest: digest(bytes), bytes: bytes.length, mediaType };
};
const copyRuntime = async () => {
await fs.mkdir(path.join(output, "share/quixos/shared"), { recursive: true });
for (const file of runtime.files) {
relative(file.path);
const bytes = await fs.readFile(path.join(config.sharedRuntime, file.path));
if (bytes.length !== file.bytes || digest(bytes) !== file.digest) throw Error("Shared runtime mismatch");
await fs.writeFile(path.join(output, "share/quixos/shared", file.path), bytes);
}
};
await copyRuntime();
const sharedFiles = await Promise.all(
runtime.files.map((f) => recordFile(`share/quixos/shared/${f.path}`, f.mediaType)),
);
const coreFile = sharedFiles.find((f) => f.path.endsWith("/core.mjs"));
const coreURL = coreFile ? compiler.contentArtifactUrl(coreFile) : null;
const coreServer = path.join(config.sharedRuntime, "core.mjs");
await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true });
const portable = config.portable;
for (const target of Object.keys(portable?.entries ?? {}))
if (!["browser", "server"].includes(target)) throw Error(`Unsupported portable entry target ${target}`);
for (const settings of [portable, config.remote].filter(Boolean))
if (!/^[$A-Z_a-z][$\w]*$/.test(settings.registryExport))
throw Error("Registry export must be a JavaScript identifier");
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 });
const writable = async (directory) => {
await fs.chmod(directory, 0o755);
for (const item of await fs.readdir(directory, { withFileTypes: true })) {
const file = path.join(directory, item.name);
if (item.isSymbolicLink()) throw Error("Authored package source cannot contain symlinks");
if (item.isDirectory()) await writable(file);
else await fs.chmod(file, 0o644);
}
};
await writable(work);
const modules = path.join(work, "node_modules");
await fs.mkdir(path.join(modules, "@quixos"), { recursive: true });
await installDependencies({
destination: modules,
application: config.nodeModules,
selected: { "@quixos/camino-replica-core": config.core },
reserved: ["@quixos/camino-replica-core", "@quixos/camino-replica-client", "@quixos/camino-replica-engine"],
});
const packageFile = path.join(work, "package.json");
const packageMetadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
if (error.code === "ENOENT") return {};
throw error;
});
await fs.writeFile(packageFile, JSON.stringify({ ...packageMetadata, type: "module" }));
const bindingPath = relative(settings.bindingOutput);
const source =
mode === "remote"
? 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,
mode === "remote" ? "share/quixos/generated/remote.ts" : compiler.generatedBindingPath(executionTarget),
),
source,
);
const entry = relative(settings.entries?.[target] ?? settings.entry);
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 metadata = path.join(work, "bundle-meta.json");
measureBuild("portable-bundle", { package: pkg.revision, target, mode }, () =>
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" },
),
);
const meta = JSON.parse(await fs.readFile(metadata, "utf8"));
const built = Object.values(meta.outputs).find((o) => o.entryPoint);
if (!built?.exports.includes(settings.registryExport)) throw Error("Bundle lacks the declared registry export");
// Portable modules must contain their ordinary source dependencies and share only the checked SDK.
if (
Object.values(meta.outputs).some((o) =>
o.imports.some(
(i) =>
i.external &&
i.path !== coreImport &&
!(target === "server" && builtinModules.includes(i.path.replace(/^node:/, ""))),
),
)
)
throw Error("Unexpected external import in portable module");
const exports = pkg.exports
.filter((e) =>
mode === "remote"
? e.execution.kind === "remote"
: e.execution.kind !== "remote" && e.execution.targets.includes(executionTarget),
)
.map((e) => e.id);
const body = {
id: `${mode}-module`,
target: executionTarget,
compatibility: "quixos-js-module-v1",
path: bundlePath,
ports: "quixos-transaction-ports-v1",
registryExport: settings.registryExport,
exports,
files: [await recordFile(bundlePath, "text/javascript"), ...sharedFiles],
};
implementations.push({ ...body, contentDigest: compiler.implementationDigest(body) });
if (target === "server" && mode !== "remote" && exports.length) {
// A closed candidate is not an eligibility claim. The host checks these
// bytes and its exact qualified SDK before admitting it to the sandbox.
// Keep a normal service implementation for unsupported dependencies.
const restrictedPath = "share/quixos/restricted/module.mjs";
const sdkPath = "share/quixos/restricted/core.mjs";
const sdkName = "quixos:restricted-sdk/v1";
await fs.mkdir(path.dirname(path.join(output, restrictedPath)), { recursive: true });
await fs.copyFile(config.restrictedSDK, path.join(output, sdkPath));
measureBuild("restricted-bundle", { package: pkg.revision }, () =>
execFileSync(
config.esbuild,
[
entry,
"--bundle",
"--platform=node",
"--format=esm",
"--minify-whitespace",
"--target=es2024",
`--outfile=${path.join(output, restrictedPath)}`,
`--alias:@quixos/camino-replica-core=${sdkName}`,
`--external:${sdkName}`,
],
{ cwd: work, stdio: "inherit" },
),
);
const restricted = {
id: "restricted-module",
target: "server",
compatibility: "quixos-restricted-module-v1",
path: restrictedPath,
ports: "quixos-transaction-ports-v1",
registryExport: settings.registryExport,
exports,
files: [await recordFile(restrictedPath, "text/javascript"), await recordFile(sdkPath, "text/javascript")],
};
implementations.push({ ...restricted, contentDigest: compiler.implementationDigest(restricted) });
}
}
const components =
config.group && !config.components
? []
: 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.parseArtifactGroup({
schemaVersion: 1,
packageRevisionId: pkg.revision,
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.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");
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));
+264
View File
@@ -0,0 +1,264 @@
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, contract, pkg, output, recordFile, generated }) {
const settings = config.components;
if (!settings) {
if (pkg.components?.length) throw Error("Declared components require checked component build settings");
return [];
}
const declarations = config.group
? (pkg.components ?? []).filter((entry) => Object.hasOwn(settings.entries, entry.id))
: (pkg.components ?? []);
const entries = Object.entries(settings.entries);
if (
entries.length !== declarations.length ||
entries.some(([id]) => !declarations.some((component) => component.id === id))
)
throw Error("Component build entries differ from package declarations");
const runtime = JSON.parse(await fs.readFile(path.join(settings.runtime, "runtime.json"), "utf8"));
if (
runtime.sdkCodeDigest !== digest(await fs.readFile(path.join(settings.sdk, "dist/index.js"))) ||
runtime.sdkTypeDigest !== digest(await fs.readFile(path.join(settings.sdk, "dist/index.d.ts")))
)
throw Error("Component SDK differs from selected shared runtime");
const sharedFiles = [];
for (const file of runtime.files) {
compiler.artifactPath(file.path);
const bytes = await fs.readFile(path.join(settings.runtime, file.path));
if (bytes.length !== file.bytes || digest(bytes) !== file.digest) throw Error("Modified component runtime");
const target = "share/quixos/component-shared/" + file.path;
await fs.mkdir(path.dirname(path.join(output, target)), { recursive: true });
await fs.writeFile(path.join(output, target), bytes);
sharedFiles.push(await recordFile(target, file.mediaType));
}
const sharedModules = Object.fromEntries(
compiler.componentSharedModules.map((name) => {
const file = sharedFiles.find((file) => file.path === "share/quixos/component-shared/" + runtime.modules[name]);
if (!file) throw Error("Missing shared component module " + name);
return [name, file.path];
}),
);
const imports = Object.fromEntries(
Object.entries(sharedModules).map(([name, file]) => [
name,
compiler.contentArtifactUrl(sharedFiles.find((value) => value.path === file)),
]),
);
const core = runtime.files.find((file) => file.path === "core.mjs");
const selected = JSON.parse(await fs.readFile(path.join(config.sharedRuntime, "runtime.json"), "utf8")).files.find(
(file) => file.path === "core.mjs",
);
if (core?.digest !== selected?.digest) throw Error("Component runtime core differs from portable runtime core");
const work = path.resolve("work-components");
await fs.cp(config.source, work, { recursive: true, dereference: false });
const writable = async (directory) => {
await fs.chmod(directory, 0o755);
for (const item of await fs.readdir(directory, { withFileTypes: true })) {
const file = path.join(directory, item.name);
if (item.isSymbolicLink()) throw Error("Authored component source cannot contain symlinks");
if (item.isDirectory()) await writable(file);
else await fs.chmod(file, 0o644);
}
};
await writable(work);
const modules = path.join(work, "node_modules");
await fs.mkdir(path.join(modules, "@quixos"), { recursive: true });
await installDependencies({
destination: modules,
application: config.nodeModules,
defaults: [path.join(settings.dependencies, "node_modules")],
selected: { "@quixos/camino-react": settings.sdk },
reserved: [
"react",
"react-dom",
"scheduler",
"csstype",
"@types/react",
"@types/react-dom",
"@types/prop-types",
"@quixos/camino-react",
"@quixos/camino-replica-core",
"@quixos/camino-replica-client",
"@quixos/camino-replica-engine",
],
});
const packageFile = path.join(work, "package.json");
const packageMetadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
if (error.code === "ENOENT") return {};
throw error;
});
await fs.writeFile(packageFile, JSON.stringify({ ...packageMetadata, type: "module" }));
// CommonJS dependencies may require React. An ESM bridge lets esbuild lower
// that require while retaining the one host-owned runtime instance.
const sharedAliases = [];
for (const [name, url] of Object.entries(imports)) {
const shim = path.join(work, "shared-" + digest(Buffer.from(name)).slice(0, 16) + ".mjs");
await fs.writeFile(
shim,
`export * 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 = 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);
const assetPath = "component-assets.d.ts";
await fs.writeFile(path.join(work, assetPath), assets);
await fs.writeFile(path.join(output, "share/quixos/generated/client.ts"), bindings);
await fs.writeFile(path.join(output, "share/quixos/generated/component-assets.d.ts"), assets);
generated.set("component-client", { file: path.join(work, bindingPath), source: bindings });
generated.set("component-assets", { file: path.join(work, assetPath), source: assets });
// Public props are type-only inputs owned by imported interface resources.
// Augment the generated nominal registry without importing an implementation.
const propsImports = [],
propsMembers = [],
seenProps = new Set();
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 = contract.definition.moduleInterfaces.filter(
(iface) =>
iface.revisionId === companion.interfaceRevisionId || iface.definitionId === companion.interfaceRevisionId,
);
if (!contracts.length) continue;
for (const [memberId, entry] of Object.entries(companion.members)) {
if (!/^[$A-Z_a-z][$\w]*$/.test(entry.export)) throw Error("Invalid props type export");
const alias = `Props${propsImports.length}`;
propsImports.push(
`import type {${entry.export} as ${alias}} from ${JSON.stringify(path.join(root, "types", compiler.artifactPath(entry.path)))};`,
);
for (const iface of contracts) {
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);
propsMembers.push(`${JSON.stringify(key)}: ${alias};`);
}
}
}
const propsPath = "component-props.d.ts";
await fs.writeFile(
path.join(work, propsPath),
propsImports.join("\n") +
`\nexport {};\ndeclare module ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))} { interface ModuleProps {${propsMembers.join("\n")}} }\n`,
);
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);
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 "react";declare const props:Pick<ComponentProps<Modules[${JSON.stringify(declaration.displayName)}]>,"subject"|"input">;const mount = <Component {...props}/>;\n`
: ""),
);
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(
path.join(work, facade),
`export {${entry.export}} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};\n`,
);
await fs.mkdir(path.join(output, directory), { recursive: true });
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 (
Object.keys(meta.inputs).some((file) =>
/camino-replica-engine|camino-replica-client|replicache|node_modules\/(?:react|react-dom|scheduler)(?:\/|$)/.test(
file,
),
)
)
throw Error("Component bundled an internal engine/client or duplicate React");
if (
outputs.some(([, value]) =>
value.imports.some((imported) => imported.external && !Object.values(imports).includes(imported.path)),
)
)
throw Error("Component has an undeclared external module or asset");
const files = [...sharedFiles];
for (const [file] of outputs) {
const absolute = path.resolve(work, file),
relative = path.relative(output, absolute);
compiler.artifactPath(relative);
if (!relative.startsWith(directory + "/")) throw Error("Component output escaped declared artifact closure");
files.push(await recordFile(relative, relative.endsWith(".css") ? "text/css" : "text/javascript"));
}
const body = {
id,
subject: declaration.subject,
compatibility: "org.quixos.react.esm/1",
path: directory + "/module.mjs",
exportName: entry.export,
sharedModules,
files,
};
components.push({ ...body, contentDigest: compiler.componentArtifactDigest(body) });
}
return components;
}
+12
View File
@@ -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 })}`,
);
}
}
+107
View File
@@ -0,0 +1,107 @@
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");
const iface = resource.revision;
const output = process.env.out;
if (!output) throw Error("Missing Nix output");
const work = path.resolve("contracts-source");
const declarations = [];
await fs.cp(config.source, work, { recursive: true });
const writable = async (directory) => {
await fs.chmod(directory, 0o755);
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
const file = path.join(directory, entry.name);
if (entry.isSymbolicLink()) throw Error("Contract source cannot contain symlinks");
if (entry.isDirectory()) await writable(file);
else {
await fs.chmod(file, 0o644);
if (/\.d\.[cm]?ts$/.test(file)) declarations.push(path.relative(work, file));
}
}
};
await writable(work);
const packageFile = path.join(work, "package.json");
const metadata = await fs.readFile(packageFile, "utf8").then(JSON.parse, (error) => {
if (error.code === "ENOENT") return {};
throw error;
});
await fs.writeFile(packageFile, JSON.stringify({ ...metadata, type: "module" }));
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.component/2", "org.quixos.react.subject-only/1"].includes(
member.contract,
)
)
throw Error("Props entry is not a React module member: " + id);
if (
!/^[$A-Z_a-z][$\w]*$/.test(entry.export) ||
path.isAbsolute(entry.entry) ||
entry.entry.split("/").some((part) => part === ".." || !part)
)
throw Error("Invalid props type entry");
const filename = `props-${index++}.ts`;
const typeImport = JSON.stringify("./" + entry.entry.replace(/\.[cm]?tsx?$/, ".js"));
await fs.writeFile(
path.join(work, filename),
`import type {${entry.export} as Props} from ${typeImport};
type Assert<T extends true> = T;
type ObjectProps = Assert<Props extends object ? true : false>;
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`,
);
members[id] = { path: filename.replace(/\.ts$/, ".d.ts"), export: "Props" };
}
await fs.mkdir(path.join(output, "types"), { recursive: true });
if (!Object.keys(members).length) throw Error("React type companion declares no module members");
execFileSync(
config.node,
[
config.tsc,
"--declaration",
"--emitDeclarationOnly",
"--strict",
"--target",
"es2024",
"--lib",
"es2024,dom,dom.iterable,esnext.disposable",
"--module",
"nodenext",
"--moduleResolution",
"nodenext",
"--rootDir",
work,
"--outDir",
path.join(output, "types"),
...Object.values(members).map((entry) => path.join(work, entry.path.replace(/\.d\.ts$/, ".ts"))),
],
{ cwd: work, stdio: "inherit" },
);
// TypeScript emits declarations for implementation sources, but does not copy
// authored declarations which those emitted files may import.
for (const file of declarations) {
await fs.mkdir(path.dirname(path.join(output, "types", file)), { recursive: true });
await fs.copyFile(path.join(work, file), path.join(output, "types", file));
}
await fs.writeFile(path.join(output, "package.json"), JSON.stringify({ type: "module" }));
await fs.writeFile(
path.join(output, "react-contracts.json"),
JSON.stringify({ schemaVersion: 1, interfaceRevisionId: iface.revisionId, members }, null, 2),
);
+91
View File
@@ -0,0 +1,91 @@
import fs from "node:fs/promises";
import path from "node:path";
import { execFileSync } from "node:child_process";
export async function buildService({
config,
compiler,
pkg,
output,
coreServer,
sharedFiles,
implementations,
recordFile,
}) {
const relative = compiler.artifactPath;
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 });
let serviceEntry;
if (config.service.entry) {
if (!/\.m?js$/.test(config.service.entry))
throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging");
serviceEntry = path.join(config.source, relative(config.service.entry));
} else {
const modules = implementations.filter(
(implementation) =>
implementation.target === "server" && implementation.compatibility === "quixos-js-module-v1",
);
if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id))))
throw Error("Generated service does not cover every package export; provide a custom service");
if (!modules.length) throw Error("Generated service requires a checked registry");
serviceEntry = path.resolve("service-entry.mjs");
const imports = modules
.map(
(module, index) =>
`import { ${module.registryExport} as registry${index} } from ${JSON.stringify(path.join(output, module.path))};`,
)
.join("\n");
await fs.writeFile(
serviceEntry,
imports +
`\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)};
const registries = [${modules.map((_, index) => "registry" + index).join(",")}];
const owners = new Map();
for (const registry of registries) for (const id of registry.exports) {
if (owners.has(id)) throw Error("Duplicate registry export " + id);
owners.set(id, registry);
}
servePortableRegistry({exports: [...owners.keys()], invoke(request, channel, context) {
const registry = owners.get(request.exportId);
if (!registry) throw Error("Unknown export");
return registry.invoke(request, channel, context);
}});\n`,
);
}
const serviceModule = "share/quixos/service/main.mjs";
execFileSync(
config.esbuild,
[
serviceEntry,
"--bundle",
"--platform=node",
"--format=esm",
"--minify-whitespace",
"--target=es2024",
`--external:${coreServer}`,
`--outfile=${path.join(output, serviceModule)}`,
],
{ stdio: "inherit" },
);
await fs.writeFile(
path.join(output, "bin/service"),
`#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.node} "$service_root/${serviceModule}" "$@"\n`,
{ mode: 0o755 },
);
const serviceBody = {
id: "service",
target: "server",
compatibility: "quixos-service-v1",
path: "bin/service",
ports: "quixos-transaction-ports-v1",
exports: pkg.exports.map((e) => e.id),
files: [
await recordFile("bin/service", "text/x-shellscript"),
await recordFile(serviceModule, "text/javascript"),
...sharedFiles,
],
};
implementations.unshift({ ...serviceBody, contentDigest: compiler.implementationDigest(serviceBody) });
}
}
+17
View File
@@ -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
);
}
+40
View File
@@ -0,0 +1,40 @@
import fs from "node:fs";
import crypto from "node:crypto";
const [schemaPath, packageRevisionId, bindingOutput, generatorPath, output] = process.argv.slice(2);
if (!schemaPath || !packageRevisionId || !bindingOutput || !generatorPath || !output)
throw new Error("Missing candidate check receipt inputs");
const canonical = (value) =>
Array.isArray(value)
? value.map(canonical)
: value && typeof value === "object"
? Object.fromEntries(
Object.entries(value)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([key, entry]) => [key, canonical(entry)]),
)
: value;
const hash = (value) =>
`sha256:${crypto
.createHash("sha256")
.update(JSON.stringify(canonical(value)))
.digest("hex")}`;
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
if (!schema.packages.some((entry) => entry.revisionId === packageRevisionId))
throw new Error("Checked binding schema lacks the package");
const generated = fs.readFileSync(bindingOutput, "utf8");
if (generated !== fs.readFileSync(".qx-checked-bindings", "utf8"))
throw new Error("Build replaced candidate-generated bindings; its check is not evidence for this candidate");
const receipt = {
schemaVersion: 1,
packageRevisionId,
success: true,
bindingSchema: schema,
bindingSchemaDigest: hash(schema),
generatedDigest: hash(generated),
checkerDigest: hash({
generatorPath,
compiler: JSON.parse(fs.readFileSync("node_modules/typescript/package.json", "utf8")),
lock: fs.readFileSync("yarn.lock", "utf8"),
}),
};
fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" });
+49
View File
@@ -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);
}
+51
View File
@@ -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]);
}
}
}
});
}
+59
View File
@@ -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));
});
+30
View File
@@ -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);
}
+61
View File
@@ -0,0 +1,61 @@
import fs from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
async function packages(directory) {
const result = new Map();
for (const entry of await fs.readdir(directory)) {
if (entry.startsWith(".")) continue;
if (entry.startsWith("@")) {
for (const name of await fs.readdir(path.join(directory, entry)))
result.set(`${entry}/${name}`, path.join(directory, entry, name));
} else result.set(entry, path.join(directory, entry));
}
return result;
}
async function packageDigest(directory) {
const digest = createHash("sha256");
async function visit(relative) {
for (const entry of (await fs.readdir(path.join(directory, relative), { withFileTypes: true })).sort((a, b) =>
a.name.localeCompare(b.name),
)) {
if (entry.name === "node_modules") continue;
const name = path.join(relative, entry.name),
file = path.join(directory, name);
if (entry.isDirectory()) await visit(name);
else {
const bytes = await fs.readFile(file);
digest.update(JSON.stringify([name, bytes.length]));
digest.update(bytes);
}
}
}
await visit("");
return digest.digest("hex");
}
/** Merge at package granularity: a scope such as @types is not a reserved package. */
export async function installDependencies({ destination, application, defaults = [], selected = {}, reserved = [] }) {
const entries = new Map();
const authored = new Set();
for (const root of defaults) for (const [name, file] of await packages(root)) entries.set(name, file);
for (const [name, file] of Object.entries(selected)) entries.set(name, file);
if (application)
for (const [name, file] of await packages(application)) {
if (reserved.includes(name)) {
const canonical = entries.get(name);
if (!canonical || (await packageDigest(file)) !== (await packageDigest(canonical)))
throw Error("Application dependencies cannot substitute selected React/client types or checked SDK: " + name);
} else {
entries.set(name, file);
authored.add(name);
}
}
for (const [name, file] of entries) {
await fs.mkdir(path.dirname(path.join(destination, name)), { recursive: true });
// Application packages resolve siblings from this assembled node_modules.
// Keep SDK links canonical: copying those would duplicate branded types and
// React declarations from their own retained dependency closures.
if (authored.has(name)) await fs.cp(file, path.join(destination, name), { recursive: true, dereference: true });
else await fs.symlink(file, path.join(destination, name));
}
}
+2
View File
@@ -0,0 +1,2 @@
# The selected protocol owns semantic projection; builders receive its result.
args@{ protocol, ... }: import "${protocol}/share/package-context.nix" args
+57
View File
@@ -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"
''
+171
View File
@@ -0,0 +1,171 @@
/** Generated-service transport. The supervisor owns process isolation, deadlines and termination.
* stdout is reserved for this protocol; application diagnostics belong on stderr. */
export function servePortableRegistry(
registry,
{ maxBytes = 4 * 1024 * 1024, maxInvocations = 64, maxPortCalls = 10000 } = {},
) {
const abi = "quixos-transaction-ports-v1";
const active = new Map();
const pending = new Map();
let buffer = Buffer.alloc(0),
stopped = false;
const send = (message) => {
const encoded = JSON.stringify(message);
if (Buffer.byteLength(encoded) > maxBytes) throw Error("SERVICE_MESSAGE_LIMIT");
// A slow or absent coordinator must not cause an unbounded stdout queue.
if (process.stdout.writableLength + Buffer.byteLength(encoded) > maxBytes * 2) throw Error("SERVICE_BACKPRESSURE");
process.stdout.write(encoded + "\n");
};
const errorBody = (error) => ({
code: typeof error?.code === "string" ? error.code.slice(0, 1024) : "PACKAGE_ERROR",
message: String(error?.message ?? error).slice(0, 4096),
});
const rejectPorts = (id, reason) => {
for (const [key, call] of pending)
if (call.invocation === id) {
pending.delete(key);
call.reject(reason);
}
};
const cancel = (id, reason) => {
const token = active.get(id);
if (token) {
token.cancelled = true;
token.controller.abort(reason);
}
rejectPorts(id, reason);
};
const fatal = (error) => {
if (stopped) return;
stopped = true;
for (const id of active.keys()) cancel(id, error);
process.stderr.write(String(error?.message ?? error) + "\n");
process.exitCode = 1;
process.stdin.destroy();
};
const receive = (message) => {
if (!message || message.abi !== abi) throw Error("SERVICE_ABI_MISMATCH");
if (message.kind === "shutdown") {
for (const id of active.keys()) cancel(id, Error("SERVICE_SHUTDOWN"));
stopped = true;
process.stdin.destroy();
return;
}
if (message.kind === "cancel") {
const token = active.get(message.invocation);
if (token && token.frame !== message.frame) throw Error("INVALID_CANCELLATION_FRAME");
cancel(message.invocation, Error("INVOCATION_CANCELLED"));
// A completed result may already be in flight. Acknowledge without resurrecting its context.
if (!token) send({ abi, kind: "cancelled", invocation: message.invocation, frame: message.frame });
return;
}
if (message.kind === "port-result") {
const key = JSON.stringify([message.invocation, message.sequence]);
const call = pending.get(key);
if (!call && (!active.has(message.invocation) || active.get(message.invocation).cancelled)) return;
if (!call || message.frame !== call.frame) throw Error("UNEXPECTED_PORT_RESULT");
pending.delete(key);
if (message.status === "returned") call.resolve(message.output);
else if (message.status === "failed")
call.reject(
Object.assign(Error(String(message.error?.message ?? "Port failed")), {
code: String(message.error?.code ?? "PORT_FAILED"),
}),
);
else throw Error("INVALID_PORT_RESULT");
return;
}
if (
message.kind !== "invoke" ||
typeof message.invocation !== "string" ||
typeof message.frame !== "string" ||
!message.invocation ||
!message.frame ||
message.invocation.length > 4096 ||
message.frame.length > 4096
)
throw Error("INVALID_INVOCATION");
if (active.has(message.invocation) || active.size >= maxInvocations) throw Error("INVOCATION_LIMIT_OR_DUPLICATE");
const token = { frame: message.frame, controller: new AbortController(), cancelled: false };
active.set(message.invocation, token);
let sequence = 0;
const channel = (request) => {
if (active.get(message.invocation) !== token || token.cancelled)
return Promise.reject(Error("EXPIRED_INVOCATION"));
if (++sequence > maxPortCalls || pending.size >= maxPortCalls) return Promise.reject(Error("PORT_CALL_LIMIT"));
const key = JSON.stringify([message.invocation, sequence]);
return new Promise((resolve, reject) => {
pending.set(key, { invocation: message.invocation, frame: message.frame, resolve, reject });
try {
send({
abi,
kind: "port",
frame: message.frame,
invocation: message.invocation,
sequence,
portId: request.portId,
action: { ...request.action, ...(request.key === undefined ? {} : { key: request.key }) },
input: request.input,
});
} catch (error) {
pending.delete(key);
reject(error);
}
});
};
void registry
.invoke(
{ exportId: message.exportId, workspace: message.workspace, receiver: message.receiver, input: message.input },
channel,
Object.freeze({ signal: token.controller.signal, effect: message.effect }),
)
.then(
(output) => {
if (active.get(message.invocation) === token && !token.cancelled)
send({ abi, invocation: message.invocation, status: "returned", output });
},
(error) => {
if (active.get(message.invocation) === token && !token.cancelled)
send({ abi, invocation: message.invocation, status: "failed", error: errorBody(error) });
},
)
.catch(fatal)
.finally(() => {
if (active.get(message.invocation) === token) {
active.delete(message.invocation);
token.controller.abort(Error("INVOCATION_ENDED"));
rejectPorts(message.invocation, Error("INVOCATION_ENDED"));
if (token.cancelled && !stopped)
send({ abi, kind: "cancelled", invocation: message.invocation, frame: token.frame });
}
})
.catch(fatal);
};
process.stdin.on("data", (bytes) => {
if (stopped) return;
try {
// Consume each line before retaining the remainder; combined transport chunks may contain many frames.
let offset = 0;
while (offset < bytes.length) {
const newline = bytes.indexOf(10, offset);
const end = newline < 0 ? bytes.length : newline;
if (buffer.length + end - offset > maxBytes) throw Error("SERVICE_MESSAGE_LIMIT");
buffer = Buffer.concat([buffer, bytes.subarray(offset, end)]);
if (newline < 0) break;
receive(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer)));
buffer = Buffer.alloc(0);
offset = newline + 1;
if (stopped) break;
}
} catch (error) {
fatal(error);
}
});
process.stdin.on("end", () => {
if (buffer.length) fatal(Error("TRUNCATED_SERVICE_MESSAGE"));
for (const id of active.keys()) cancel(id, Error("COORDINATOR_DISCONNECTED"));
});
process.stdin.on("error", fatal);
process.stdout.on("error", fatal);
send({ abi, kind: "ready", exports: registry.exports });
}
+543 -332
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
pkgs,
context,
src,
members,
dependencies ? null,
}:
let
buildTools = import ./build-tools.nix;
config = pkgs.writeText "camino-react-contracts.json" (
builtins.toJSON {
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 ${buildTools}/build-react-contracts.mjs ${config}
''
+34
View File
@@ -0,0 +1,34 @@
{
pkgs,
context,
src,
modules,
bindingOutput ? "src/gen/client.ts",
dependencies ? null,
}:
let
group = import ./artifacts.nix {
inherit pkgs src;
inherit (context)
buildContract
core
sharedRuntime
typescript
packageRevisionId
;
protocol = context.generator;
group = true;
nodeModules = dependencies;
components = {
inherit bindingOutput;
inherit (context.react) sdk runtime;
entries = modules;
contracts = context.react.contracts or [ ];
};
};
package = import ./assemble-package.nix {
inherit pkgs context;
artifacts = [ group ];
};
in
package // { caminoGroup = group; }
+84
View File
@@ -0,0 +1,84 @@
{ pkgs, core }:
let
workerd = import ./workerd.nix { inherit pkgs; };
sandbox = import ./sandbox.nix {
inherit pkgs;
runtime = workerd;
executable = "${workerd}/bin/workerd";
};
nativeProbe =
pkgs.runCommand "quixos-restricted-native-probe"
{
nativeBuildInputs = [ pkgs.stdenv.cc ];
}
''
mkdir -p "$out/bin"
$CC -O2 -Wall -Wextra -Werror ${./sandbox-probe.c} -o "$out/bin/probe"
'';
nativeProbeSandbox = import ./sandbox.nix {
inherit pkgs;
runtime = nativeProbe;
executable = "${nativeProbe}/bin/probe";
};
mkIdentityLauncher =
settings: import ./identity-launcher.nix ({ inherit pkgs sandbox; } // settings);
identityLauncher = mkIdentityLauncher { };
# 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,hashJson} 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 ${../socket-address.mjs} "$out/lib/socket-address.mjs"
substituteInPlace "$out/lib/service.mjs" --replace-fail '../socket-address.mjs' './socket-address.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_SANDBOX ${sandbox}/bin/quixos-restricted-sandbox \
--set QUIXOS_RESTRICTED_WORKER "$out/lib/worker.mjs"
'';
qualificationSource = pkgs.lib.fileset.toSource {
root = ../.;
fileset = pkgs.lib.fileset.unions [
(pkgs.lib.fileset.fileFilter (file: file.hasExt "mjs") ./.)
../socket-address.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 ${qualificationSource}/restricted-execution/service.test.mjs
touch "$out"
'';
in
{
inherit
workerd
sandbox
identityLauncher
mkIdentityLauncher
nativeProbeSandbox
sdk
runtime
qualificationSource
qualification
;
}
+88
View File
@@ -0,0 +1,88 @@
#define _GNU_SOURCE
#include <dirent.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <unistd.h>
/* Installed only as a narrowly executable NixOS wrapper. This trusted helper
* accepts no executable, environment, filesystem root or arbitrary UID. The
* caller is platform authority, never a workspace guest or authored Worker. */
static void fail(const char *message) {
fprintf(stderr, "RESTRICTED_IDENTITY: %s\n", message);
exit(125);
}
int main(int argc, char **argv) {
const char *root = ENGINE_DIRECTORY "/";
uid_t caller = getuid();
gid_t group = getgid();
struct passwd *owner = getpwnam(AUTHORITY_USER);
if (geteuid() != 0) fail("privileged wrapper unavailable");
if (!owner) fail("authority identity unavailable");
if (caller != owner->pw_uid) fail("caller identity mismatch");
if (argc != 3) fail("invalid arguments");
char *end;
unsigned long number = strtoul(argv[2], &end, 10);
if (!*argv[2] || *end || number < ENGINE_FIRST_UID || number >= ENGINE_FIRST_UID + ENGINE_IDENTITY_COUNT)
fail("invalid engine identity");
uid_t identity = (uid_t)number;
if (strlen(argv[1]) != strlen(root) + 16 || strncmp(argv[1], root, strlen(root)))
fail("invalid engine directory");
const char *name = argv[1] + strlen(root);
if (strncmp(name, "qx-engine-", 10) ||
strlen(name) != 16 || strspn(name + 10, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") != 6)
fail("invalid engine directory");
int parent = open(ENGINE_DIRECTORY, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
struct stat info;
if (parent < 0 || fstat(parent, &info) || info.st_uid != caller || (info.st_mode & 066))
fail("invalid engine root");
int directory = openat(parent, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
if (directory < 0 || fstat(directory, &info) || info.st_uid != caller || (info.st_mode & 077))
fail("invalid engine owner");
DIR *entries = fdopendir(dup(directory));
if (!entries) fail("engine entries");
struct dirent *entry;
unsigned int files = 0;
while ((entry = readdir(entries))) {
const char *file = entry->d_name;
if (!strcmp(file, ".") || !strcmp(file, "..")) continue;
int socket = !strcmp(file, "broker.sock");
if ((!socket && strcmp(file, "config.capnp") && strcmp(file, "parent.mjs")) ||
fstatat(directory, file, &info, AT_SYMLINK_NOFOLLOW) || info.st_uid != caller ||
info.st_nlink != 1 || (socket ? !S_ISSOCK(info.st_mode) : !S_ISREG(info.st_mode)) ||
info.st_size > 4 * 1024 * 1024)
fail("unexpected engine file");
files++;
}
closedir(entries);
if (files != 3) fail("incomplete engine configuration");
const char *files_to_transfer[] = {"config.capnp", "parent.mjs", "broker.sock"};
for (size_t i = 0; i < 3; i++) {
if (fchownat(directory, files_to_transfer[i], identity, group, AT_SYMLINK_NOFOLLOW) ||
fchmodat(directory, files_to_transfer[i], 0660, 0))
fail("engine file ownership");
}
if (fchown(directory, identity, group) || fchmod(directory, 0770))
fail("engine directory ownership");
close(directory);
close(parent);
struct rlimit core = {0, 0};
if (setrlimit(RLIMIT_CORE, &core) || setgroups(0, NULL) ||
setresgid(identity, identity, identity) || setresuid(identity, identity, identity) ||
prctl(PR_SET_DUMPABLE, 0) || prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
fail("identity confinement");
/* The containing directory grants only its owner and authority access. A
* socket created by workerd must also admit authority's different UID/GID. */
umask(0000);
char *args[] = {SANDBOX_EXECUTABLE, argv[1], NULL};
clearenv();
execv(SANDBOX_EXECUTABLE, args);
fail("sandbox launch");
}
@@ -0,0 +1,42 @@
# Trusted host construction only. Authored Workers cannot choose the caller,
# reserved identities, engine directory or sandbox executable.
{
pkgs,
sandbox,
authorityUser ? "quixos-graph",
engineDirectory ? "/run/quixos-restricted-engines",
firstUid ? 610000,
identityCount ? 64,
}:
assert builtins.match "[a-z][a-z0-9_-]{0,63}" authorityUser != null;
assert builtins.match "/run/[a-zA-Z0-9_/-]+" engineDirectory != null;
assert !(pkgs.lib.hasInfix ".." engineDirectory);
assert !(pkgs.lib.hasSuffix "/" engineDirectory);
assert builtins.isInt firstUid && builtins.isInt identityCount;
assert firstUid >= 610000 && firstUid + identityCount <= 2147483647;
assert identityCount >= 1 && identityCount <= 64;
pkgs.runCommand "quixos-restricted-identity-launcher"
{
nativeBuildInputs = [
pkgs.stdenv.cc
pkgs.nodejs_24
];
QUIXOS_RESTRICTED_ENGINE_DIRECTORY = engineDirectory;
}
''
# Validate the complete generated filename with the shared UTF-8 policy,
# before compiling or installing a launcher for this configured /run root.
node --input-type=module <<'JS'
import { checkedSocketPath } from '${../socket-address.mjs}';
for (const filename of ["broker.sock", "engine.sock"])
checkedSocketPath(process.env.QUIXOS_RESTRICTED_ENGINE_DIRECTORY + "/qx-engine-XXXXXX/" + filename);
JS
mkdir -p "$out/bin"
$CC -O2 -Wall -Wextra -Werror ${./identity-launcher.c} \
-DSANDBOX_EXECUTABLE='"${sandbox}/bin/quixos-restricted-sandbox"' \
-DAUTHORITY_USER='"${authorityUser}"' \
-DENGINE_DIRECTORY='"${engineDirectory}"' \
-DENGINE_FIRST_UID=${toString firstUid} \
-DENGINE_IDENTITY_COUNT=${toString identityCount} \
-o "$out/bin/quixos-restricted-engine"
''
+50
View File
@@ -0,0 +1,50 @@
{
executor,
enable,
controlGroup,
authorityUser ? "quixos-graph",
engineDirectory ? "/run/quixos-restricted-engines",
firstUid ? 610000,
identityCount ? 64,
wrapperName ? "quixos-restricted-engine",
}:
{ config, lib, ... }:
{
config = lib.mkIf enable {
assertions = [
{
assertion = lib.all (
user: user.uid == null || user.uid < firstUid || user.uid >= firstUid + identityCount
) (builtins.attrValues config.users.users);
message = "UIDs ${toString firstUid}–${
toString (firstUid + identityCount - 1)
} are reserved for restricted engine identities.";
}
{
assertion = lib.all (
group: group.gid == null || group.gid < firstUid || group.gid >= firstUid + identityCount
) (builtins.attrValues config.users.groups);
message = "Restricted engine GIDs must not overlap host groups.";
}
];
systemd.tmpfiles.rules = [
"d ${engineDirectory} 0711 ${authorityUser} ${controlGroup} -"
];
security.wrappers.${wrapperName} = {
source = "${
executor.mkIdentityLauncher {
inherit
authorityUser
engineDirectory
firstUid
identityCount
;
}
}/bin/quixos-restricted-engine";
owner = "root";
group = controlGroup;
permissions = "u+rx,g+rx,o-rwx";
setuid = true;
};
};
}
+64
View File
@@ -0,0 +1,64 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>
static void check(int condition, const char *label) {
if (!condition) {
fprintf(stderr, "ESCAPE: %s (%s)\n", label, strerror(errno));
exit(1);
}
}
int main(void) {
for (int fd = 3; fd < 256; fd++)
check(fcntl(fd, F_GETFD) < 0 && errno == EBADF, "inherited descriptor");
check(!getenv("QUIXOS_VM_SECRET"), "parent environment");
check(open("/etc/test-secret", O_RDONLY) < 0, "authority filesystem");
check(open("/sys/fs/cgroup/cgroup.procs", O_WRONLY) < 0, "resource controls");
check(open("/proc/1/root/etc/test-secret", O_RDONLY) < 0, "proc root escape");
check(socket(AF_INET, SOCK_STREAM, 0) < 0 && errno == EPERM, "IPv4 socket");
check(socket(AF_INET6, SOCK_STREAM, 0) < 0 && errno == EPERM, "IPv6 socket");
check(socket(AF_NETLINK, SOCK_RAW, 0) < 0 && errno == EPERM, "netlink socket");
check(ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0 && errno == EPERM, "ptrace");
check(unshare(CLONE_NEWUSER) < 0, "nested user namespace");
pid_t child = fork();
if (child == 0) _exit(2);
check(child < 0 && errno == EPERM, "subprocess");
check(access("/bin/sh", X_OK) < 0, "ambient executable");
check(getppid() <= 1, "host process visibility");
struct sockaddr_un address = {.sun_family = AF_UNIX};
int outside = socket(AF_UNIX, SOCK_STREAM, 0);
check(outside >= 0, "Unix socket support");
strcpy(address.sun_path, "/run/quixos-native-authority/test.sock");
check(connect(outside, (struct sockaddr *)&address, sizeof(address)) < 0,
"authority Unix socket");
close(outside);
outside = socket(AF_UNIX, SOCK_STREAM, 0);
memset(address.sun_path, 0, sizeof(address.sun_path));
strcpy(address.sun_path + 1, "quixos-native-authority");
check(connect(outside, (struct sockaddr *)&address,
offsetof(struct sockaddr_un, sun_path) + 1 + strlen(address.sun_path + 1)) < 0,
"authority abstract socket");
close(outside);
char directory[4096];
check(getcwd(directory, sizeof(directory)) != NULL, "engine directory");
check(snprintf(address.sun_path, sizeof(address.sun_path), "%s/broker.sock", directory) <
(int)sizeof(address.sun_path), "broker path length");
int broker = socket(AF_UNIX, SOCK_STREAM, 0);
check(broker >= 0 && connect(broker, (struct sockaddr *)&address, sizeof(address)) == 0,
"authorized broker");
check(write(broker, "sandbox-probe", 13) == 13, "broker write");
close(broker);
puts("RESTRICTED_NATIVE_SANDBOX_PASSED");
return 0;
}
+77
View File
@@ -0,0 +1,77 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* Trusted, single-process admission gate. The supervisor charges this PID to
* its engine cgroup before releasing stdin. All subsequently created namespace
* helpers and threads inherit that charge. No authored code runs here. */
static void fail(const char *message) {
fprintf(stderr, "RESTRICTED_SANDBOX: %s\n", message);
exit(125);
}
int main(int argc, char **argv) {
struct stat info;
if (argc != 2 || argv[1][0] != '/' || strstr(argv[1], "/../") ||
lstat(argv[1], &info) || !S_ISDIR(info.st_mode) ||
info.st_uid != getuid() || (info.st_mode & 007))
fail("invalid engine directory");
char gate;
if (read(STDIN_FILENO, &gate, 1) != 1 || gate != 1)
fail("engine admission was not released");
int input = open("/dev/null", O_RDONLY);
if (input < 0 || dup2(input, STDIN_FILENO) < 0)
fail("stdin");
close(input);
int filter = open(SANDBOX_FILTER, O_RDONLY);
if (filter < 0)
fail("seccomp filter");
char filter_fd[32];
snprintf(filter_fd, sizeof(filter_fd), "%d", filter);
char *args[4096];
size_t count = 0;
#define ARG(value) args[count++] = (value)
ARG(SANDBOX_BWRAP);
ARG("--unshare-all");
ARG("--unshare-user");
ARG("--die-with-parent");
ARG("--disable-userns");
ARG("--assert-userns-disabled");
ARG("--cap-drop"); ARG("ALL");
ARG("--hostname"); ARG("quixos-restricted");
ARG("--clearenv");
ARG("--setenv"); ARG("TZ"); ARG("UTC");
ARG("--setenv"); ARG("LANG"); ARG("C.UTF-8");
FILE *closure = fopen(SANDBOX_CLOSURE, "r");
if (!closure)
fail("runtime closure");
char *line = NULL;
size_t capacity = 0;
while (getline(&line, &capacity, closure) >= 0) {
line[strcspn(line, "\n")] = 0;
if (strncmp(line, "/nix/store/", 11) || strchr(line, ' ') || count > 4000)
fail("runtime closure entry");
char *path = strdup(line);
if (!path)
fail("runtime closure allocation");
ARG("--ro-bind"); ARG(path); ARG(path);
}
fclose(closure);
free(line);
ARG("--proc"); ARG("/proc");
ARG("--dev"); ARG("/dev");
ARG("--tmpfs"); ARG("/tmp");
ARG("--bind"); ARG(argv[1]); ARG(argv[1]);
ARG("--chdir"); ARG(argv[1]);
ARG("--seccomp"); ARG(filter_fd);
ARG("--"); ARG(SANDBOX_EXECUTABLE);
ARG("serve"); ARG("--experimental"); ARG("config.capnp");
ARG(NULL);
execv(SANDBOX_BWRAP, args);
fail("bubblewrap failed");
}
+30
View File
@@ -0,0 +1,30 @@
{
pkgs,
runtime,
executable,
}:
let
closure = pkgs.closureInfo { rootPaths = [ runtime ]; };
filter =
pkgs.runCommand "quixos-restricted-seccomp"
{
nativeBuildInputs = [
pkgs.stdenv.cc
pkgs.pkg-config
];
buildInputs = [ pkgs.libseccomp ];
}
''
$CC -O2 -Wall -Wextra -Werror ${./seccomp.c} $(pkg-config --cflags --libs libseccomp) -o filter
./filter > "$out"
'';
in
pkgs.runCommand "quixos-restricted-sandbox" { nativeBuildInputs = [ pkgs.stdenv.cc ]; } ''
mkdir -p "$out/bin"
$CC -O2 -Wall -Wextra -Werror ${./sandbox.c} \
-DSANDBOX_BWRAP='"${pkgs.bubblewrap}/bin/bwrap"' \
-DSANDBOX_EXECUTABLE='"${executable}"' \
-DSANDBOX_FILTER='"${filter}"' \
-DSANDBOX_CLOSURE='"${closure}/store-paths"' \
-o "$out/bin/quixos-restricted-sandbox"
''
+54
View File
@@ -0,0 +1,54 @@
#define _GNU_SOURCE
#include <errno.h>
#include <linux/sched.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
/* Layered on an empty mount tree, fresh PID/user/network namespaces, no caps,
* no inherited descriptors and an immutable runtime closure. Deny kernel and
* process escape surfaces; retain V8's ordinary memory/thread syscalls. */
static void checked(int result) {
if (result < 0) {
fprintf(stderr, "seccomp rule: %d\n", result);
exit(1);
}
}
int main(void) {
scmp_filter_ctx filter = seccomp_init(SCMP_ACT_ALLOW);
if (!filter) return 1;
const char *denied[] = {
"fork", "vfork", "execveat", "ptrace", "process_vm_readv", "process_vm_writev",
"pidfd_getfd", "kcmp", "bpf", "perf_event_open", "userfaultfd", "io_uring_setup",
"mount", "umount2", "pivot_root", "chroot", "move_mount", "open_tree", "fsopen",
"fsconfig", "fsmount", "fspick", "mount_setattr", "open_by_handle_at",
"name_to_handle_at", "setns", "unshare", "keyctl", "add_key", "request_key",
"reboot", "kexec_load", "kexec_file_load", "init_module", "finit_module",
"delete_module", "swapon", "swapoff", "syslog", "acct", "sethostname",
"setdomainname", "iopl", "ioperm", "lookup_dcookie", NULL
};
for (size_t i = 0; denied[i]; i++) {
int syscall = seccomp_syscall_resolve_name(denied[i]);
if (syscall != __NR_SCMP_ERROR)
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), syscall, 0));
}
/* glibc falls back to clone when clone3 is unavailable. Only thread creation
* is admitted; subprocesses and new namespaces cannot be manufactured. */
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(ENOSYS), SCMP_SYS(clone3), 0));
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(clone), 1,
SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_THREAD, 0)));
const unsigned long namespaces[] = {CLONE_NEWUSER, CLONE_NEWNS, CLONE_NEWNET,
CLONE_NEWPID, CLONE_NEWIPC, CLONE_NEWUTS, CLONE_NEWCGROUP};
for (size_t i = 0; i < sizeof(namespaces) / sizeof(namespaces[0]); i++)
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(clone), 1,
SCMP_A0(SCMP_CMP_MASKED_EQ, namespaces[i], namespaces[i])));
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_NE, AF_UNIX)));
checked(seccomp_rule_add(filter, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(ioctl), 1,
SCMP_A1(SCMP_CMP_EQ, 0x5412))); /* TIOCSTI */
checked(seccomp_export_bpf(filter, STDOUT_FILENO));
seccomp_release(filter);
return 0;
}
+816
View File
@@ -0,0 +1,816 @@
import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { createServer, request as httpRequest } from "node:http";
import { mkdtemp, readFile, writeFile, rm, open, lstat, readdir } from "node:fs/promises";
import { constants } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import {
checkedSocketPath,
socketDirectory,
prepareSocketDirectory,
removeEmptySocketDirectory,
} from "../socket-address.mjs";
const abi = "quixos-transaction-ports-v1";
/** Reserved supervisor faults are not authored domain rejections. A graph
* transaction must roll back without recording a permanent rejected outcome
* when its executor dies, exceeds its limits or cannot start. */
export const restrictedExecutionFailureCodes = Object.freeze([
"RESTRICTED_ABI_MISMATCH",
"RESTRICTED_BACKPRESSURE",
"RESTRICTED_CPU_ACCOUNTING",
"RESTRICTED_DEADLINE",
"RESTRICTED_DIAGNOSTIC_LIMIT",
"RESTRICTED_DUPLICATE_INVOCATION",
"RESTRICTED_ENGINE_EXITED",
"RESTRICTED_ENGINE_LIMIT",
"RESTRICTED_ENTRY_LIMIT",
"RESTRICTED_IDENTITY_UNAVAILABLE",
"RESTRICTED_MESSAGE_LIMIT",
"RESTRICTED_PACKAGE_FAILED",
"RESTRICTED_PORT_FAILED",
"RESTRICTED_PORT_LIMIT",
"RESTRICTED_PROCESS_LIMIT",
"RESTRICTED_QUEUE_FULL",
"RESTRICTED_SDK_LIMIT",
"RESTRICTED_SOURCE_LIMIT",
"RESTRICTED_START_FAILED",
"RESTRICTED_UNKNOWN_INVOCATION",
"RESTRICTED_UNKNOWN_PORT",
]);
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("..");
/** Host-wide engine reservations. Only trusted adapters hold these callbacks;
* workers cannot alter admission or evict another invocation's engine. */
export class RestrictedEngineBudget {
#leases = new Set();
constructor(limit = 8, reserve = async () => ({})) {
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 64) throw fixedError("INVALID_ENGINE_BUDGET");
this.limit = limit;
this.reserve = reserve;
}
async acquire(idle, evict) {
while (this.#leases.size >= this.limit) {
const victim = [...this.#leases].find((entry) => entry.idle());
if (!victim) throw fixedError("RESTRICTED_ENGINE_LIMIT");
await victim.evict();
}
const lease = { idle, evict };
this.#leases.add(lease);
try {
const resource = await this.reserve();
let releasing;
const release = () =>
(releasing ??= (async () => {
await resource.release?.();
this.#leases.delete(lease);
})());
release.attach = resource.attach;
release.usage = resource.usage;
release.kill = resource.kill;
release.uid = resource.uid;
return release;
} catch (error) {
this.#leases.delete(lease);
throw error;
}
}
stats() {
return { engines: this.#leases.size, limit: this.limit };
}
}
async function boundedSource(path, maximum, code) {
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const stat = await handle.stat();
if (!stat.isFile() || stat.size > maximum) throw fixedError(code);
const bytes = Buffer.alloc(stat.size + 1);
let offset = 0;
while (offset < bytes.length) {
const read = await handle.read(bytes, offset, bytes.length - offset, offset);
if (!read.bytesRead) break;
offset += read.bytesRead;
}
if (offset !== stat.size) throw fixedError(code);
return new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, offset));
} finally {
await handle.close();
}
}
/** Trusted stdio adapter inside one workspace VM. Engine processes never own
* coordinator credentials. The supervising service must impose VM/OS limits.
* Each engine runs one fresh Worker per invocation. Host tokens expire before
* reuse; cancellation and watchdogs destroy the affected engine, not neighbors. */
export async function serveRestrictedRegistry(config, io = process) {
if (
!object(config) ||
!absolute(config.workerd) ||
!absolute(config.worker) ||
!absolute(config.module) ||
(config.sandbox !== undefined && !absolute(config.sandbox)) ||
(config.sandboxIdentity !== undefined && typeof config.sandboxIdentity !== "boolean") ||
!/^[$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 ?? 10000,
maxQueued: config.maxQueued ?? 8,
maxActive: config.maxActive ?? 2,
maxNested: config.maxNested ?? 4,
engineIdleMs: config.engineIdleMs ?? 30000,
};
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 ||
!Number.isSafeInteger(limits.maxActive) ||
limits.maxActive < 1 ||
limits.maxActive > 16 ||
!Number.isSafeInteger(limits.maxNested) ||
limits.maxNested < 1 ||
limits.maxNested > 16 ||
!Number.isSafeInteger(limits.engineIdleMs) ||
limits.engineIdleMs < 1 ||
limits.engineIdleMs > 60000
)
throw fixedError("INVALID_RESTRICTED_LIMITS");
const packageSource = await boundedSource(config.module, 1024 * 1024, "RESTRICTED_SOURCE_LIMIT");
const workerSource = await boundedSource(config.worker, 65536, "RESTRICTED_ENTRY_LIMIT");
const sdk = [];
for (const m of config.sdk)
sdk.push({ ...m, source: await boundedSource(m.path, 2 * 1024 * 1024, "RESTRICTED_SDK_LIMIT") });
const engineBudget = config.engineBudget ?? new RestrictedEngineBudget(limits.maxActive + limits.maxNested);
const active = new Map(),
engines = new Set(),
idleEngines = [];
const metrics = { starts: 0, reused: 0, completed: 0, peakRSSBytes: 0 };
const changed = () => {
try {
config.onChange?.();
} catch {}
};
let buffered = Buffer.alloc(0),
stopped = false,
failure;
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);
};
// A sandbox has namespace helpers as well as workerd. Charge the complete
// delegated cgroup when available; the qualification fallback follows the
// bounded process tree, never trusting a worker's self-reported counters.
const engineUsage = async (engine) => {
if (engine.release?.usage) return engine.release.usage();
engine.processTicks ??= new Map();
const pending = [engine.child.pid];
let memoryBytes = 0,
visited = 0;
while (pending.length) {
if (++visited > 64) throw fixedError("RESTRICTED_PROCESS_LIMIT");
const pid = pending.pop();
try {
const stat = await readFile(`/proc/${pid}/stat`, "utf8");
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
engine.processTicks.set(`${pid}:${fields[19]}`, Number(fields[11]) + Number(fields[12]));
const status = await readFile(`/proc/${pid}/status`, "utf8");
memoryBytes += Number(status.match(/^VmRSS:\s+(\d+)/m)?.[1] ?? 0) * 1024;
const children = await readFile(`/proc/${pid}/task/${pid}/children`, "utf8");
pending.push(...children.trim().split(/\s+/).filter(Boolean).map(Number));
} catch (error) {
if (!["ENOENT", "ESRCH"].includes(error.code)) throw error;
}
}
return {
cpuMicroseconds: [...engine.processTicks.values()].reduce((sum, ticks) => sum + ticks * 10000, 0),
memoryBytes,
};
};
const sampleCPU = async (call) => {
if (!call.child?.pid) return;
const { cpuMicroseconds: total } = await engineUsage(call.engine);
const cpuMicroseconds = total - (call.cpuBaseline ?? 0);
if (call.engine) call.engine.cpuTotal = total;
if (!Number.isSafeInteger(cpuMicroseconds) || cpuMicroseconds < (call.cpuMicroseconds ?? 0))
throw fixedError("RESTRICTED_CPU_ACCOUNTING");
if (cpuMicroseconds !== (call.cpuMicroseconds ?? 0)) {
call.cpuMicroseconds = cpuMicroseconds;
send({ kind: "usage", invocation: call.message.invocation, frame: call.message.frame, cpuMicroseconds });
}
};
const watchCPU = (call) => {
call.cpuTimer = setTimeout(() => {
call.cpuRead = sampleCPU(call).then(() => {
if (call.open) watchCPU(call);
});
void call.cpuRead.catch(fail);
}, 20);
};
const retire = async (call, reason) => {
if (call.retiring) return call.retiring;
call.open = false;
call.retiring = (async () => {
clearTimeout(call.timer);
clearTimeout(call.cpuTimer);
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(() => {});
clearTimeout(call.cpuTimer);
await call.cpuRead?.catch(() => {});
let usageFailure;
try {
await sampleCPU(call);
} catch (error) {
usageFailure = error;
}
const engine = call.engine;
if (engine) {
if (!stopped && reason.code === "INVOCATION_ENDED" && !usageFailure && !engine.destroying) {
const rss = (await engineUsage(engine)).memoryBytes;
metrics.peakRSSBytes = Math.max(metrics.peakRSSBytes, rss);
// Bound retained engine heaps as well as lifetime. Hard aggregate
// physical memory remains the supervising process/cgroup's contract.
// Keep the lease busy through every await. Another artifact can evict
// an idle lease immediately; never publish an engine still retiring
// or one whose process died while its final usage read was pending.
if (rss <= 128 * 1024 * 1024 && !stopped && !engine.destroying && engine.current === call) {
// Revoke the frame token atomically with publishing idle capacity.
engine.current = null;
idleEngines.push(engine);
engine.idleUntil = Date.now() + limits.engineIdleMs;
engine.idleTimer = setTimeout(() => void destroyEngine(engine).catch(fail), limits.engineIdleMs);
} else await destroyEngine(engine);
} else await destroyEngine(engine);
}
active.delete(call.message.invocation);
if (usageFailure) throw usageFailure;
})();
return call.retiring;
};
const fail = (error) => {
if (failure) return failure;
stopped = true;
return (failure = (async () => {
io.stdin.destroy();
for (const call of [...queued]) await retire(call, error);
await Promise.all([...active.values()].map((call) => retire(call, error)));
await Promise.all([...engines].map(destroyEngine));
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 destroyEngine = async (engine) => {
if (engine.destroying) return engine.destroying;
engine.destroying = (async () => {
clearTimeout(engine.idleTimer);
const index = idleEngines.indexOf(engine);
if (index >= 0) idleEngines.splice(index, 1);
engine.current = null;
if (engine.child?.pid) {
if (engine.release.kill) await engine.release.kill();
else {
try {
process.kill(-engine.child.pid, "SIGKILL");
} catch (error) {
if (error.code !== "ESRCH") throw error;
}
}
await engine.exited;
}
const sameDirectory = async (directory, owner) => {
const current = await lstat(directory);
if (
!owner ||
!current.isDirectory() ||
current.dev !== owner.dev ||
current.ino !== owner.ino ||
current.uid !== owner.uid ||
current.mode & 0o077
)
throw fixedError("RESTRICTED_START_FAILED");
};
// A bounded ordinary-engine parent is private to this admitted lifetime.
// Never close/unlink through a replacement directory or release its lease
// while the original namespace's ownership remains uncertain.
if (engine.socketParent) {
await sameDirectory(engine.socketParent, engine.socketParentOwner);
if (engine.directory) await sameDirectory(engine.directory, engine.directoryOwner);
if (engine.broker) {
const current = await lstat(engine.broker).catch((error) => {
if (error.code !== "ENOENT") throw error;
return null;
});
if (
current &&
(!engine.brokerOwner ||
!current.isSocket() ||
current.dev !== engine.brokerOwner.dev ||
current.ino !== engine.brokerOwner.ino)
)
throw fixedError("RESTRICTED_START_FAILED");
}
}
engine.server?.closeAllConnections();
if (engine.server) await new Promise((resolve) => engine.server.close(resolve));
if (engine.socketParent) {
await sameDirectory(engine.socketParent, engine.socketParentOwner);
if (engine.directory) await sameDirectory(engine.directory, engine.directoryOwner);
}
if (engine.directory) await rm(engine.directory, { recursive: true, force: true });
if (engine.socketParent) {
await removeEmptySocketDirectory(engine.socketIdentity);
const remaining = await lstat(engine.socketParent).catch((error) => {
if (error.code !== "ENOENT") throw error;
return null;
});
if (remaining) throw fixedError("RESTRICTED_START_FAILED");
}
engines.delete(engine);
await engine.release?.();
changed();
})();
return engine.destroying;
};
const prepareEngine = async (call) => {
const cached = idleEngines.pop();
if (cached) {
clearTimeout(cached.idleTimer);
cached.current = call;
call.engine = cached;
call.child = cached.child;
call.cpuBaseline = cached.cpuTotal ?? 0;
metrics.reused++;
watchCPU(call);
return;
}
const engine = { current: call, cpuTotal: 0 };
engines.add(engine);
call.engine = engine;
engine.release = await engineBudget.acquire(
() => !engine.current && !engine.destroying,
() => destroyEngine(engine),
);
if (!call.open) return;
let directoryRoot;
if (config.sandboxIdentity) {
// The privileged helper owns a fixed explicit /run root and requires
// this exact basename. Keep its cross-UID ancestry and identity rules.
directoryRoot = tmpdir();
} else {
engine.socketIdentity = { kind: "restricted-engine", process: process.pid, lifetime: randomUUID() };
directoryRoot = socketDirectory(engine.socketIdentity);
}
for (const filename of ["broker.sock", "engine.sock"])
checkedSocketPath(directoryRoot + "/qx-engine-XXXXXX/" + filename);
if (!config.sandboxIdentity) {
engine.socketParent = await prepareSocketDirectory(engine.socketIdentity);
engine.socketParentOwner = await lstat(engine.socketParent);
if ((await readdir(engine.socketParent)).length) throw fixedError("RESTRICTED_START_FAILED");
}
engine.directory = await mkdtemp(join(directoryRoot, "qx-engine-"));
if (engine.socketParent) engine.directoryOwner = await lstat(engine.directory);
if (!call.open) return;
const broker = checkedSocketPath(join(engine.directory, "broker.sock"));
engine.socket = checkedSocketPath(join(engine.directory, "engine.sock"));
engine.server = createServer(async (request, response) => {
const call = engine.current;
const message = call?.message;
try {
if (!call?.open || request.method !== "POST" || request.url !== "/port/" + call.token)
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 > Math.min(limits.maxPortCalls, message.budget?.calls ?? 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"}}');
}
});
if (engine.socketParent) engine.broker = broker;
await new Promise((resolve, reject) => {
engine.server.once("error", reject);
engine.server.listen(broker, resolve);
});
if (engine.socketParent) {
engine.brokerOwner = await lstat(broker);
}
const modules = { "entry.mjs": { js: workerSource }, "package.mjs": { js: packageSource } };
for (const module of sdk) modules[module.name] = { js: module.source };
const setting = { registryExport: config.registryExport, exports: config.exports, maxBytes: limits.maxBytes };
// The parent contains immutable checked source only. It never imports the
// authored module or keeps a child stub, request, result or frame handle.
const parent = `const modules=${JSON.stringify(modules)}, setting=${JSON.stringify(setting)};
export default {async fetch(request,env) {
const input=await request.json();
const child=env.LOADER.load({compatibilityDate:"2026-10-05",mainModule:"entry.mjs",modules,
globalOutbound:null,env:{BROKER:env.BROKER,CONFIG:{...setting,frameToken:input.frameToken}}});
const response=await child.getEntrypoint().fetch(new Request("http://invocation/",{
method:"POST",body:JSON.stringify(input.message)}));
const body=await response.arrayBuffer();
return new Response(body,{status:response.status,headers:response.headers});
}};`;
await writeFile(join(engine.directory, "parent.mjs"), parent);
const configuration = `using Workerd = import "/workerd/workerd.capnp"; const config :Workerd.Config = (
services=[(name="main",worker=(modules=[(name="parent.mjs",esModule=embed "parent.mjs")],
compatibilityDate="2026-10-05",compatibilityFlags=["experimental"],globalOutbound="deny",
bindings=[(name="LOADER",workerLoader=()),(name="BROKER",service="broker")])),
(name="broker",external=(address=${JSON.stringify("unix:" + broker)},http=())),
(name="deny",network=(allow=[]))],
sockets=[(name="frame",address=${JSON.stringify("unix:" + engine.socket)},http=(),service="main")]);`;
const file = join(engine.directory, "config.capnp");
await writeFile(file, configuration);
if (!call.open) return;
if (config.sandboxIdentity && !Number.isSafeInteger(engine.release.uid))
throw fixedError("RESTRICTED_IDENTITY_UNAVAILABLE");
engine.child = spawn(
config.sandbox ?? config.workerd,
config.sandbox
? [engine.directory, ...(config.sandboxIdentity ? [String(engine.release.uid)] : [])]
: ["serve", "--experimental", file],
{
detached: true,
env: { TZ: "UTC", LANG: "C.UTF-8" },
cwd: engine.directory,
stdio: [config.sandbox ? "pipe" : "ignore", "ignore", "pipe"],
},
);
metrics.starts++;
changed();
engine.exited = new Promise((resolve) => {
engine.child.once("exit", resolve);
engine.child.once("error", resolve);
});
engine.child.stdin?.on("error", () => engine.current?.terminate("RESTRICTED_START_FAILED"));
call.child = engine.child;
let diagnostics = 0;
engine.child.stderr.on("data", (chunk) => {
if ((diagnostics += chunk.length) > 65536) {
if (engine.current?.open) engine.current.terminate("RESTRICTED_DIAGNOSTIC_LIMIT");
else void destroyEngine(engine).catch(fail);
} else config.diagnostics?.(chunk);
});
await engine.release.attach?.(engine.child.pid);
if (config.sandbox && call.open) engine.child.stdin.end(Buffer.from([1]));
watchCPU(call);
engine.child.once("error", () => engine.current?.terminate("RESTRICTED_START_FAILED"));
void engine.exited.then(() => {
if (engine.current?.open) engine.current.terminate("RESTRICTED_ENGINE_EXITED");
else void destroyEngine(engine).catch(fail);
});
};
const available = (call) =>
[...active.values()].filter((entry) => entry.nested === call.nested).length <
(call.nested ? limits.maxNested : limits.maxActive);
const pump = () => {
while (!stopped) {
const index = queued.findIndex(available);
if (index < 0) return;
void invoke(queued.splice(index, 1)[0]).catch(fail);
}
};
const finish = async (call, reason, reply) => {
if (reply.kind === "cancelled") call.cancellationReply = reply;
return (call.finished ??= (async () => {
await retire(call, reason);
metrics.lastInvocation = {
queueMs: (call.startedAt ?? performance.now()) - call.receivedAt,
preparationMs: call.preparationMs ?? 0,
roundtripMs: call.roundtripMs ?? 0,
initializationMs: call.initializationMs ?? null,
executionMs: call.executionMs ?? null,
totalMs: performance.now() - call.receivedAt,
};
metrics.lastFailure =
reply.status === "failed" && /^[A-Z0-9_]{1,128}$/.test(reply.error?.code) ? reply.error.code : null;
changed();
send(call.cancellationReply ?? reply);
pump();
})());
};
const invoke = async (call) => {
const { message } = call;
call.startedAt = performance.now();
active.set(message.invocation, call);
changed();
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 {
call.token = randomUUID();
call.preparing = prepareEngine(call);
await call.preparing;
call.preparationMs = performance.now() - call.startedAt;
if (!call.open) return;
const roundtripAt = performance.now();
const result = await new Promise((resolve, reject) => {
const attempt = () => {
if (!call.open) {
reject(fixedError("EXPIRED_CONTEXT"));
return;
}
const request = httpRequest(
{ socketPath: call.engine.socket, path: "/", method: "POST", agent: false },
async (response) => {
try {
const result = await body(response);
for (const [header, key] of [
["x-quixos-initialization-ms", "initializationMs"],
["x-quixos-execution-ms", "executionMs"],
]) {
const value = Number(response.headers[header]);
if (Number.isFinite(value) && value >= 0 && value <= limits.deadlineMs) call[key] = value;
}
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, frameToken: call.token }));
};
attempt();
});
call.roundtripMs = performance.now() - roundtripAt;
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);
metrics.completed++;
} catch (error) {
config.diagnostics?.(Buffer.from(`RESTRICTED_HOST_ERROR:${error.code ?? error.name}\n`));
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 (
message.budget !== undefined &&
(!object(message.budget) ||
Object.keys(message.budget).sort().join(",") !== "calls,deadlineMs,depth,root" ||
typeof message.budget.root !== "string" ||
!message.budget.root ||
message.budget.root.length > 4096 ||
!Number.isSafeInteger(message.budget.depth) ||
message.budget.depth < 0 ||
message.budget.depth > 32 ||
!Number.isSafeInteger(message.budget.calls) ||
message.budget.calls < 1 ||
message.budget.calls > 1000000 ||
!Number.isSafeInteger(message.budget.deadlineMs) ||
message.budget.deadlineMs < 1 ||
message.budget.deadlineMs > 60000)
)
throw fixedError("INVALID_RESTRICTED_BUDGET");
if (active.has(message.invocation) || queued.some((call) => call.message.invocation === message.invocation))
throw fixedError("RESTRICTED_DUPLICATE_INVOCATION");
const call = {
message,
receivedAt: performance.now(),
open: true,
pending: new Map(),
sequence: 0,
nested: (message.budget?.depth ?? 0) > 1,
};
// Descendants cannot wait behind ancestors that hold all nested capacity.
// Reject excess fan-out/depth rather than deadlock an admitted call tree.
if (!available(call) && (call.nested || queued.length >= limits.maxQueued)) {
send({
invocation: message.invocation,
status: "failed",
error: { code: "RESTRICTED_QUEUE_FULL", message: "RESTRICTED_QUEUE_FULL" },
});
return;
}
// 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);
},
Math.min(limits.deadlineMs, message.budget?.deadlineMs ?? 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.get(message.invocation);
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")),
stats: () => ({
...metrics,
active: active.size,
queued: queued.length,
engines: engines.size,
idle: idleEngines.length,
idleDeadline: idleEngines.length
? new Date(Math.min(...idleEngines.map((engine) => engine.idleUntil))).toISOString()
: null,
}),
};
}
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,
sandbox: process.env.QUIXOS_RESTRICTED_SANDBOX,
module: join(dirname(process.argv[2]), config.module),
});
}
+769
View File
@@ -0,0 +1,769 @@
import test from "node:test";
import assert from "node:assert/strict";
import { PassThrough } from "node:stream";
import { mkdtemp, writeFile, rm, mkdir, readdir, lstat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { serveRestrictedRegistry, RestrictedEngineBudget } from "./service.mjs";
import { socketDirectory, checkedSocketPath } from "../socket-address.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 = [];
const usage = [];
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 (value.kind === "usage") {
usage.push(value);
continue;
}
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,
sandbox: process.env.QUIXOS_TEST_SANDBOX,
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, budget) => {
const invocation = "invoke-" + ++next,
frame = "frame-" + next;
send({
kind: "invoke",
invocation,
frame,
exportId: "test",
workspace: "workspace:restricted",
receiver: null,
input,
effect: "snapshot",
...(budget ? { budget } : {}),
});
return { invocation, frame };
};
return {
io,
receive,
send,
start,
usage,
stats: service.stats,
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 sockets and retirement are independent of deep Unicode TMPDIR", async () => {
const root = await mkdtemp(join(tmpdir(), "restricted-deep-"));
const deep = join(root, "保留された状態".repeat(12));
await mkdir(deep);
const oldTmp = process.env.TMPDIR;
const socketRoot = dirname(socketDirectory({ kind: "qualification-probe" }));
const names = async () =>
readdir(socketRoot).catch((error) => {
if (error.code !== "ENOENT") throw error;
return [];
});
const before = new Set(await names());
let f, namespace;
process.env.TMPDIR = deep;
try {
assert.ok(Buffer.byteLength(join(deep, "qx-engine-XXXXXX", "engine.sock")) > 100);
f = await fixture(registry("return request.input;"));
f.start("deep runtime");
const result = await f.receive();
assert.equal(result.status, "returned");
assert.equal(result.output, "deep runtime");
assert.equal(f.stats().starts, 1);
const created = (await names()).filter((name) => !before.has(name));
assert.equal(created.length, 1);
namespace = join(socketRoot, created[0]);
const engines = await readdir(namespace);
assert.equal(engines.length, 1);
for (const name of ["broker.sock", "engine.sock"]) {
const endpoint = checkedSocketPath(join(namespace, engines[0], name));
assert.ok((await lstat(endpoint)).isSocket());
}
} finally {
try {
await f?.close();
} finally {
if (oldTmp === undefined) delete process.env.TMPDIR;
else process.env.TMPDIR = oldTmp;
}
}
await assert.rejects(lstat(namespace), { code: "ENOENT" });
assert.ok((await lstat(deep)).isDirectory());
await rm(root, { recursive: true, force: true });
});
test("uncertain OS cleanup cannot release engine capacity", async () => {
const budget = new RestrictedEngineBudget(1, async () => ({
async release() {
throw Error("kernel cleanup not confirmed");
},
}));
const release = await budget.acquire(
() => false,
async () => {},
);
await assert.rejects(release(), /cleanup not confirmed/);
assert.equal(budget.stats().engines, 1);
await assert.rejects(
budget.acquire(
() => false,
async () => {},
),
{ code: "RESTRICTED_ENGINE_LIMIT" },
);
});
test("parallel frames route independently and cancellation cannot retire a neighbor", async () => {
const f = await fixture(
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:request.input});"),
);
try {
const first = f.start("a"),
second = f.start("b");
const ports = new Map([await f.receive(), await f.receive()].map((port) => [port.invocation, port]));
assert.equal(ports.get(first.invocation).input, "a");
assert.equal(ports.get(second.invocation).input, "b");
f.send({ kind: "cancel", ...first });
assert.equal((await f.receive()).kind, "cancelled");
f.send({
kind: "port-result",
...first,
sequence: ports.get(first.invocation).sequence,
status: "returned",
output: "late",
});
f.send({
kind: "port-result",
...second,
sequence: ports.get(second.invocation).sequence,
status: "returned",
output: "survived",
});
const result = await f.receive();
assert.equal(result.invocation, second.invocation);
assert.equal(result.output, "survived");
assert.equal(f.io.exitCode, 0);
} finally {
await f.close();
}
});
test("A to B to A uses reserved nested capacity while both root lanes are saturated", async () => {
const source = registry(
"if(request.input==='leaf')return 'done'; return await channel({portId:'next',action:{kind:'state',primitive:'read'},input:request.input});",
);
const a = await fixture(source, { maxActive: 1, maxNested: 1, maxQueued: 1 });
const b = await fixture(source, { maxActive: 1, maxNested: 1, maxQueued: 1 });
const budget = (depth) => ({ root: "root-chain", depth, deadlineMs: 2000, calls: 20 });
try {
const parent = a.start("B", budget(1)),
aPort = await a.receive();
const neighbor = b.start("held", { ...budget(1), root: "other-root" });
await b.receive();
const child = b.start("A", budget(2)),
bPort = await b.receive();
const leaf = a.start("leaf", budget(3));
const leafResult = await a.receive();
assert.equal(leafResult.invocation, leaf.invocation);
assert.equal(leafResult.output, "done");
b.send({ kind: "port-result", ...child, sequence: bPort.sequence, status: "returned", output: leafResult.output });
const childResult = await b.receive();
a.send({
kind: "port-result",
...parent,
sequence: aPort.sequence,
status: "returned",
output: childResult.output,
});
assert.equal((await a.receive()).output, "done");
b.send({ kind: "cancel", ...neighbor });
assert.equal((await b.receive()).kind, "cancelled");
} finally {
await a.close();
await b.close();
}
});
test("the remaining root wall budget overrides a longer adapter deadline", async () => {
const f = await fixture(
registry("return await channel({portId:'held',action:{kind:'state',primitive:'read'},input:null});"),
{ deadlineMs: 2000 },
);
try {
const started = performance.now();
f.start(null, { root: "short-root", depth: 2, deadlineMs: 150, calls: 10 });
await f.receive();
const result = await f.receive();
assert.equal(result.error.code, "RESTRICTED_DEADLINE");
assert.ok(performance.now() - started < 1000);
} finally {
await f.close();
}
});
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("checked handlers hash canonical values without ambient capabilities", async () => {
const sdk = process.env.QUIXOS_TEST_SDK;
const C = await import(sdk);
const workspace = C.workspaceId("workspace:restricted");
const text = { kind: "scalar", name: "string" };
const value = {
kind: "record",
fields: {
text,
options: {
kind: "record",
fields: {
level: { kind: "scalar", name: "int32" },
enabled: { kind: "scalar", name: "bool" },
},
},
},
};
const declarations = [
{ id: "test", name: "test", effect: "snapshot", receiver: null, input: value, output: text, methods: [] },
];
const source = `import {definePortableRegistry,hashJson} from 'qx:core'; export const registry=definePortableRegistry(${JSON.stringify(declarations)},{test:ctx=>hashJson('test-content',ctx.input)});`;
const f = await fixture(source, { sdk: [{ name: "qx:core", path: sdk }] });
try {
const input = { text: "雨后的书店", options: { level: 3, enabled: true } };
let expected;
for (const candidate of [input, { options: input.options, text: input.text }, { ...input, text: "別の文" }]) {
f.start(C.encodeArgument(value, candidate, { workspace }));
const result = await f.receive();
assert.equal(result.status, "returned");
const actual = C.decodeArgument(text, result.output, { workspace });
assert.equal(actual, await C.hashJson("test-content", candidate));
if (candidate.text === input.text) {
expected ??= actual;
assert.equal(actual, expected, "Object key order must not invalidate content caches");
} else assert.notEqual(actual, expected, "Content changes must invalidate the key");
}
} 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");
assert.ok(
f.usage.some((value) => value.cpuMicroseconds >= 50000),
"External watchdog did not account spinning engine CPU",
);
}
} 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, maxActive: 1 },
);
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: 4000, maxActive: 1 },
);
try {
const first = f.start();
assert.equal((await f.receive()).kind, "port");
// Expire while the first call is deliberately blocked. Two equal relative
// deadlines let fast startup legitimately begin the queued call first.
const queued = f.start(null, { root: "queued-deadline", deadlineMs: 50, depth: 1, calls: 10 });
const expired = await f.receive();
assert.equal(expired.invocation, queued.invocation);
assert.equal(expired.error.code, "RESTRICTED_DEADLINE");
f.send({ kind: "cancel", ...first });
const cancelled = await f.receive();
assert.equal(cancelled.invocation, first.invocation);
assert.equal(cancelled.kind, "cancelled");
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();
}
});
test("warm engines create fresh transitive module state, closures and built-ins", async () => {
const directory = await mkdtemp(join(tmpdir(), "qx-transitive-"));
const helper = join(directory, "helper.mjs");
await writeFile(
helper,
"let value=0, retained; export function touch(channel){const old=retained; retained=channel; return {count:++value, captured:!!old};}",
);
const f = await fixture(
`import {touch} from 'helper.mjs'; let count=0;
export const registry={exports:['test'],async invoke(request,channel){
const before={object:Object.prototype.retained??null,array:Array.prototype.retained??null,
map:Map.prototype.retained??null, global:globalThis.retained??null};
Object.prototype.retained=request.input; Array.prototype.retained=request.input;
Map.prototype.retained=request.input; globalThis.retained=request.input;
return {count:++count, helper:touch(channel), before};
}};`,
{ sdk: [{ name: "helper.mjs", path: helper }] },
);
try {
for (const value of ["private graph A", "private graph B", "private graph C"]) {
f.start(value);
assert.deepEqual((await f.receive()).output, {
count: 1,
helper: { count: 1, captured: false },
before: { object: null, array: null, map: null, global: null },
});
}
assert.equal(f.stats().starts, 1, "Qualification must exercise actual engine reuse");
assert.equal(f.stats().reused, 2);
assert.equal(f.stats().idle, 1);
} finally {
await f.close();
await rm(directory, { recursive: true, force: true });
}
});
test("an expired frame token cannot borrow a later invocation's broker", async () => {
const directory = await mkdtemp(join(tmpdir(), "qx-token-"));
const worker = join(directory, "token-test.mjs");
// Trusted test entrypoint deliberately replays the old token. Authored
// registries never receive CONFIG or BROKER directly.
await writeFile(
worker,
`export default {async fetch(request,env) {
const message=await request.json();
const output=message.input ? (await env.BROKER.fetch('http://frame/port/'+message.input,{
method:'POST',body:JSON.stringify({portId:'read',action:{kind:'state',primitive:'read'},input:null})})).status
: env.CONFIG.frameToken;
return Response.json({status:'returned',output});
}};`,
);
const f = await fixture(registry("return null;"), { worker });
try {
f.start();
const token = (await f.receive()).output;
assert.match(token, /^[a-f0-9-]{36}$/);
f.start(token);
assert.equal((await f.receive()).output, 409);
assert.equal(f.stats().starts, 1);
assert.equal(f.stats().reused, 1);
} finally {
await f.close();
await rm(directory, { recursive: true, force: true });
}
});
test("idle engines evict without a graph poll and restart with fresh state", async () => {
const updates = [];
const f = await fixture(registry("return request.input;"), {
engineIdleMs: 30,
onChange: () => updates.push(f.stats()),
});
try {
f.start("first");
assert.equal((await f.receive()).output, "first");
await new Promise((resolve) => setTimeout(resolve, 100));
assert.equal(f.stats().engines, 0);
assert.ok(
updates.some((s) => s.active > 0),
"Inspectors observe active invocation admission",
);
assert.ok(
updates.some((s) => s.idle > 0 && s.idleDeadline),
"Idle eviction deadline is visible",
);
assert.equal(updates.at(-1).engines, 0, "Eviction pushes an update without a graph poll");
f.start("second");
assert.equal((await f.receive()).output, "second");
assert.equal(f.stats().starts, 2);
} finally {
await f.close();
}
});
for (const loop of [
"while(true) await Promise.resolve();",
"const memory=[]; for(let n=0;n<128;n++) memory.push(new Uint8Array(256*1024)); while(true) await Promise.resolve(memory);",
])
test("await/allocation pressure is fenced without delaying a neighboring engine: " + loop.slice(0, 28), async () => {
const f = await fixture(registry(`if(request.input==='spin'){${loop}} return request.input;`), {
deadlineMs: 1500,
});
try {
const stuck = f.start("spin", { root: "stuck", depth: 1, deadlineMs: 350, calls: 10 });
const before = performance.now();
const healthy = f.start("healthy");
const replies = [await f.receive(), await f.receive()];
assert.equal(replies.find((r) => r.invocation === healthy.invocation).output, "healthy");
assert.equal(replies.find((r) => r.invocation === stuck.invocation).error.code, "RESTRICTED_DEADLINE");
assert.equal(replies[0].invocation, healthy.invocation, "Healthy work must finish before the stuck deadline");
assert.ok(performance.now() - before < 1200);
f.start("after fault");
assert.equal((await f.receive()).output, "after fault");
assert.equal(f.stats().starts, 2, "The healthy engine survives its neighbor's termination");
} finally {
await f.close();
}
});
test("detached microtask loops cannot escape the invocation wall deadline", async () => {
const f = await fixture(
registry("void(async()=>{while(true)await Promise.resolve();})(); return 'nominal return';"),
{ deadlineMs: 200 },
);
try {
const before = performance.now();
f.start();
assert.equal((await f.receive()).error.code, "RESTRICTED_DEADLINE");
assert.ok(performance.now() - before < 1000);
assert.equal(f.stats().engines, 0);
} finally {
await f.close();
}
});
test("detached digest callbacks cannot use a completed frame or contaminate reuse", async () => {
const f = await fixture(
registry(`
void(async()=>{
for(let n=0;n<100;n++)await crypto.subtle.digest('SHA-256',new Uint8Array(32768));
await channel({portId:'late',action:{kind:'state',primitive:'read'},input:request.input});
})().catch(()=>{});
return request.input;
`),
);
try {
f.start("private A");
assert.equal((await f.receive()).output, "private A");
await new Promise((resolve) => setTimeout(resolve, 75));
f.start("private B");
const reply = await f.receive();
assert.equal(reply.status, "returned", "No late graph port may appear after completion");
assert.equal(reply.output, "private B");
assert.equal(f.stats().starts, 1);
} finally {
await f.close();
}
});
test("the shared engine budget evicts idle code families and rejects busy oversubscription", async () => {
const engineBudget = new RestrictedEngineBudget(1);
const a = await fixture(registry("return request.input;"), { engineBudget });
const b = await fixture(
registry("return await channel({portId:'read',action:{kind:'state',primitive:'read'},input:null});"),
{ engineBudget },
);
try {
a.start("first family");
assert.equal((await a.receive()).output, "first family");
const busy = b.start();
const port = await b.receive();
assert.equal(a.stats().engines, 0, "Idle foreign code is evicted before allocating another engine");
assert.equal(engineBudget.stats().engines, 1);
a.start("overload");
assert.equal((await a.receive()).error.code, "RESTRICTED_ENGINE_LIMIT");
assert.equal(engineBudget.stats().engines, 1);
b.send({ kind: "port-result", ...busy, sequence: port.sequence, status: "returned", output: "survived" });
assert.equal((await b.receive()).output, "survived");
} finally {
await Promise.all([a.close(), b.close()]);
}
assert.equal(engineBudget.stats().engines, 0);
});
test("engine retirement accounting keeps its admission lease busy", async () => {
const observations = [];
let idle;
const engineBudget = new RestrictedEngineBudget(1, async () => ({
usage: async () => {
observations.push(idle());
await new Promise((done) => setImmediate(done));
return { cpuMicroseconds: 0, memoryBytes: 1024 };
},
release: async () => {},
}));
const acquire = engineBudget.acquire.bind(engineBudget);
engineBudget.acquire = (isIdle, evict) => {
idle = isIdle;
return acquire(isIdle, evict);
};
const f = await fixture(registry("return request.input;"), { engineBudget });
try {
f.start("first");
assert.equal((await f.receive()).output, "first");
assert.ok(observations.length > 0);
assert.ok(
observations.every((value) => !value),
"An engine must not become evictable during asynchronous retirement accounting",
);
assert.equal(f.stats().idle, 1);
assert.equal(idle(), true);
f.start("second");
assert.equal((await f.receive()).output, "second");
assert.equal(f.stats().starts, 1);
} finally {
await f.close();
}
assert.equal(engineBudget.stats().engines, 0);
});
+105
View File
@@ -0,0 +1,105 @@
// Platform entrypoint, evaluated before loading the authored registry. A fresh
// Worker owns exactly one frame. The parent engine is reusable; BROKER's host
// token grants only this frame and expires before the next invocation.
const monotonic = performance.now.bind(performance);
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 loading = monotonic();
const source = await import("./package.mjs");
const initializationMs = monotonic() - loading;
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/" + env.CONFIG.frameToken, {
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;
};
const running = monotonic();
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",
"x-quixos-initialization-ms": String(initializationMs),
"x-quixos-execution-ms": String(monotonic() - running),
},
});
} 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;
}
},
};
+46
View File
@@ -0,0 +1,46 @@
# Pinned upstream engine. This binary supplies an import/capability boundary;
# production execution additionally requires the qualified OS identity,
# namespace, syscall, cgroup and invocation-broker boundaries.
{ 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 Quixos restricted execution";
homepage = "https://github.com/cloudflare/workerd";
license = pkgs.lib.licenses.asl20;
platforms = builtins.attrNames platforms;
};
}
+44
View File
@@ -0,0 +1,44 @@
// Language bridge to the shared socket policy; identities contain no credentials.
import {
checkedSocketPath,
prepareSocketDirectory,
removeEmptySocketDirectory,
socketAddress,
} from "./socket-address.mjs";
const [operation, role = "bootstrap"] = process.argv.slice(2);
if (
!["check-path", "prepare", "prepare-directory", "address", "cleanup"].includes(operation) ||
process.argv.length > 4 ||
(process.argv.length === 4 && !["prepare", "address"].includes(operation))
)
throw Error("Select a socket operation and an optional address role");
const chunks = [];
let length = 0;
for await (const chunk of process.stdin) {
length += chunk.length;
if (length > 1024 * 1024) throw Error("Socket command input exceeds its bound");
chunks.push(chunk);
}
const identity = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
switch (operation) {
case "check-path":
console.log(checkedSocketPath(identity));
break;
case "prepare": {
// Validate the role before creating any directories.
const address = socketAddress(identity, role);
await prepareSocketDirectory(identity);
console.log(address);
break;
}
case "prepare-directory":
console.log(await prepareSocketDirectory(identity));
break;
case "address":
console.log(socketAddress(identity, role));
break;
case "cleanup":
await removeEmptySocketDirectory(identity);
break;
}
+14
View File
@@ -0,0 +1,14 @@
{ pkgs }:
let
code = pkgs.runCommand "quixos-socket-address-code" { } ''
mkdir "$out"
cp ${./socket-address-cli.mjs} "$out/socket-address-cli.mjs"
cp ${./socket-address.mjs} "$out/socket-address.mjs"
'';
in
pkgs.writeShellApplication {
name = "quixos-socket-address";
text = ''
exec ${pkgs.nodejs_24}/bin/node --max-old-space-size=64 ${code}/socket-address-cli.mjs "$@"
'';
}
+8
View File
@@ -0,0 +1,8 @@
export type SocketOptions = { uid?: number; root?: string };
export const socketPathBytes: number;
export function checkedSocketPath(value: unknown): string;
export function socketDirectory(identity: unknown, options?: SocketOptions): string;
export function socketAddress(identity: unknown, role: string, options?: SocketOptions): string;
export function prepareSocketDirectory(identity: unknown, options?: SocketOptions): Promise<string>;
export function prepareSocketParent(socket: string): Promise<string>;
export function removeEmptySocketDirectory(identity: unknown, options?: SocketOptions): Promise<void>;
+150
View File
@@ -0,0 +1,150 @@
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
export const socketPathBytes = 100;
function canonical(value, ancestors = new Set()) {
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
if (!value || typeof value !== "object" || ancestors.has(value) || ancestors.size >= 16)
throw Error("Socket identity must be finite, acyclic JSON");
if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
throw Error("Socket identity must use plain JSON objects");
ancestors.add(value);
try {
if (Array.isArray(value)) {
if (
Object.keys(value).length !== value.length ||
!Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)).every(Boolean)
)
throw Error("Socket identity cannot contain sparse arrays");
return `[${value.map((entry) => canonical(entry, ancestors)).join(",")}]`;
}
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonical(value[key], ancestors)}`)
.join(",")}}`;
} finally {
ancestors.delete(value);
}
}
export function checkedSocketPath(value) {
if (
typeof value !== "string" ||
!path.isAbsolute(value) ||
value.includes("\0") ||
path.normalize(value) !== value ||
Buffer.byteLength(value) > socketPathBytes
)
throw Error(`Unix socket address must be absolute and at most ${socketPathBytes} bytes`);
return value;
}
function settings(options = {}) {
const uid = options.uid ?? process.getuid?.();
if (!Number.isInteger(uid) || uid < 0 || uid > 4294967295)
throw Error("Socket namespace requires an explicit Unix UID");
const root = options.root ?? `/tmp/qx-${uid}`;
if (
typeof root !== "string" ||
!path.isAbsolute(root) ||
path.normalize(root) !== root ||
root.includes("\0") ||
Buffer.byteLength(root) > 18
)
throw Error("Socket runtime root must be an explicit absolute path of at most18 bytes");
return { uid, root };
}
/** Public deterministic locator, never a capability or ownership claim. The
* caller supplies its admitted owner/lifetime; guest input grants no authority. */
export function socketDirectory(identity, options) {
const { uid, root } = settings(options);
const input = canonical(["quixos-unix-socket-v1", uid, identity]);
if (Buffer.byteLength(input) > 1024 * 1024) throw Error("Socket identity exceeds its bound");
// Preserve all 256 bits while leaving room for protocol-added socket suffixes.
return path.join(root, createHash("sha256").update(input).digest("base64url"));
}
export function socketAddress(identity, role, options) {
if (typeof role !== "string" || role !== role.trim() || !/^[a-z][a-z0-9-]{0,10}$/.test(role))
throw Error("Invalid bounded socket role");
return checkedSocketPath(path.join(socketDirectory(identity, options), role + ".sock"));
}
async function privateDirectory(directory, uid) {
await fs.mkdir(directory, { mode: 0o700 }).catch((error) => {
if (error.code !== "EEXIST") throw error;
});
const stat = await fs.lstat(directory);
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== uid || stat.mode & 0o077)
throw Error("Socket runtime directory must already be owned and private");
}
/** Validate ancestors before creating the private endpoint directory. A
* predictable name is safe only inside a filesystem namespace we actually own. */
export async function prepareSocketParent(socket) {
const directory = path.dirname(checkedSocketPath(socket)),
uid = process.getuid?.();
if (!Number.isInteger(uid)) throw Error("Socket namespace requires a Unix principal");
let current = "/";
const components = directory.split("/").filter(Boolean);
for (let index = 0; index < components.length; index++) {
current = path.join(current, components[index]);
await fs.mkdir(current, { mode: 0o700 }).catch((error) => {
if (error.code !== "EEXIST") throw error;
});
let stat = await fs.lstat(current);
if (current === "/tmp" && stat.isSymbolicLink() && stat.uid === 0) stat = await fs.stat(current); // macOS's OS-owned /tmp alias.
const final = index === components.length - 1;
// The OS-provided sticky /tmp, like / itself, is a bootstrap trust anchor.
// Nix user namespaces can report its owner as an unmapped UID. This does
// not admit arbitrary foreign sticky directories or relax child ownership.
const systemTemporary = current === "/tmp" && Boolean(stat.mode & 0o1000);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
(!systemTemporary && ![0, uid].includes(stat.uid)) ||
(stat.mode & 0o022 && !(stat.mode & 0o1000)) ||
(final && (stat.uid !== uid || stat.mode & 0o077))
)
throw Error("Socket directory ancestry must be owned and protected");
}
return directory;
}
/** Materialize ordinary IPC directories only. No registry, claim, lock or
* socket unlink occurs here; existing transport ownership still controls bind. */
export async function prepareSocketDirectory(identity, options) {
const { uid, root } = settings(options);
if (uid !== process.getuid?.()) throw Error("Cannot prepare another Unix principal's socket namespace");
const directory = socketDirectory(identity, options);
await prepareSocketParent(path.join(root, "probe.sock"));
await privateDirectory(directory, uid);
return directory;
}
export async function removeEmptySocketDirectory(identity, options) {
const { uid, root } = settings(options),
directory = socketDirectory(identity, options);
if (uid !== process.getuid?.()) throw Error("Cannot clean another Unix principal's socket namespace");
const parent = await fs.lstat(root).catch((error) => {
if (error.code !== "ENOENT") throw error;
return null;
});
if (!parent) return;
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || parent.mode & 0o077)
throw Error("Socket cleanup parent ownership differs");
const stat = await fs.lstat(directory).catch((error) => {
if (error.code !== "ENOENT") throw error;
return null;
});
if (!stat) return;
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== uid || stat.mode & 0o077)
throw Error("Socket cleanup directory ownership differs");
await fs.rmdir(directory).catch((error) => {
if (!["ENOTEMPTY", "ENOENT"].includes(error.code)) throw error;
});
}
+31
View File
@@ -0,0 +1,31 @@
# Tiny generated-project interface fixture. Real offline Yarn installation
# remains covered by the consuming Studio and Package-builder checks.
{ stdenvNoCC, nodejs }:
{ src, overrideAttrs ? null }:
let
project = stdenvNoCC.mkDerivation {
name = "yarn-dependencies-transfer-fixture";
inherit src;
configurePhase = ''
mkdir -p node_modules/fixture node_modules/.bin .yarn/cache
printf '%s\n' '{"main":"index.cjs"}' > node_modules/fixture/package.json
printf '%s\n' 'module.exports = {value: "retained dependency"};' > node_modules/fixture/index.cjs
ln node_modules/fixture/index.cjs node_modules/fixture/twin.cjs
ln -s fixture node_modules/alias
printf '%s\n' '#!${nodejs}/bin/node' 'console.log("linked executable");' > node_modules/fixture/cli.cjs
chmod +x node_modules/fixture/cli.cjs
ln -s ../fixture/cli.cjs node_modules/.bin/fixture
printf '%s\n' 'hidden dependency data' > node_modules/.fixture-data
printf '%s\n' 'disposable linker state' > node_modules/.yarn-state.yml
printf '%s\n' 'disposable archive' > .yarn/cache/fixture.zip
'';
postInstall = ''
test ! -e node_modules
test ! -e .yarn/cache
test ! -e "$out/.yarn-state.yml"
test ! -e "$out/node_modules"
test "$(stat -c %i "$out/fixture/index.cjs")" = "$(stat -c %i "$out/fixture/twin.cjs")"
'';
};
in
if overrideAttrs == null then project else project.overrideAttrs overrideAttrs
+41
View File
@@ -0,0 +1,41 @@
{
pkgs,
context,
src,
entry,
targets ? [
"browser"
"server"
],
entries ? { },
bindingOutput ? "src/gen/qx.ts",
dependencies ? null,
}:
let
group = import ./artifacts.nix {
inherit pkgs src;
inherit (context)
buildContract
core
sharedRuntime
typescript
packageRevisionId
;
protocol = context.generator;
group = true;
nodeModules = dependencies;
portable = {
inherit
entry
entries
targets
bindingOutput
;
};
};
package = import ./assemble-package.nix {
inherit pkgs context;
artifacts = [ group ];
};
in
package // { caminoGroup = group; }
+30
View File
@@ -0,0 +1,30 @@
{
pkgs,
context,
src,
entry,
bindingOutput ? "src/gen/remote.ts",
dependencies ? null,
service ? { },
}:
let
group = import ./artifacts.nix {
inherit pkgs src service;
inherit (context)
buildContract
core
sharedRuntime
typescript
packageRevisionId
;
protocol = context.generator;
group = true;
nodeModules = dependencies;
remote = { inherit entry bindingOutput; };
};
package = import ./assemble-package.nix {
inherit pkgs context;
artifacts = [ group ];
};
in
package // { caminoGroup = group; }
+40
View File
@@ -0,0 +1,40 @@
{
pkgs,
src,
nodejs ? pkgs.nodejs_24,
}:
let
project = (pkgs.callPackage (src + "/yarn-project.nix") { inherit nodejs; }) {
inherit src;
overrideAttrs = _: {
buildPhase = "runHook preBuild; runHook postBuild";
installPhase = ''
set -o pipefail
runHook preInstall
test -d node_modules
test ! -L node_modules
mkdir -p "$out"
# Installation is finished: retire its disposable archive cache, then
# stream dependencies across the build/store mount boundary without
# retaining a second complete tree. This is the same installation
# policy used by Central's dependency derivations.
rm -rf .yarn/cache
${pkgs.gnutar}/bin/tar --create --file=- --remove-files node_modules \
| ${pkgs.gnutar}/bin/tar --extract --file=- --strip-components=1 --directory="$out"
# Node resolves installed packages directly. This generated index is for
# Yarn's linker, not a runtime input; do not export build-session state.
rm -f "$out/.yarn-state.yml"
runHook postInstall
'';
};
};
in
project
// {
devShell = pkgs.mkShell {
packages = [
nodejs
project.yarn-freestanding
];
};
}
+24
View File
@@ -0,0 +1,24 @@
{ pkgs }:
let
dependencies = import ./yarn-dependencies.nix {
inherit pkgs;
src = ./test/yarn-dependencies;
};
in
pkgs.runCommand "quixos-yarn-dependencies-check" { nativeBuildInputs = [ pkgs.nodejs_24 ]; } ''
node - ${dependencies} <<'JS'
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const {execFileSync} = require('node:child_process');
const root = process.argv[2];
assert.deepEqual(require(path.join(root, 'fixture')), {value: 'retained dependency'});
assert.deepEqual(require(path.join(root, 'alias')), {value: 'retained dependency'});
assert.equal(fs.readlinkSync(path.join(root, 'alias')), 'fixture');
assert.equal(fs.readlinkSync(path.join(root, '.bin/fixture')), '../fixture/cli.cjs');
assert.equal(execFileSync(path.join(root, '.bin/fixture'), {encoding: 'utf8'}), 'linked executable\n');
assert.equal(fs.readFileSync(path.join(root, '.fixture-data'), 'utf8'), 'hidden dependency data\n');
assert.equal(fs.existsSync(path.join(root, '.yarn-state.yml')), false);
JS
touch "$out"
''
+22
View File
@@ -0,0 +1,22 @@
{ pkgs }:
let
yarnCli = pkgs.fetchurl {
url = "https://repo.yarnpkg.com/4.18.0/packages/yarnpkg-cli/bin/yarn.js";
hash = "sha512-/Lhxb+fNDuzhQf/Bi5IZOp35IEwbqDGJwoiDUiP8C75kr0c7qw1emSen2utcryuwfrJ4fMkzjKBA6hJfKh8vfg==";
};
yarn = pkgs.writeShellScriptBin "yarn" ''
exec ${pkgs.nodejs_24}/bin/node ${yarnCli} "$@"
'';
in
pkgs.mkShell {
packages = [
pkgs.nodejs_24
yarn
];
shellHook = ''
if [ ! -f .yarn/plugins/yarn-plugin-nixify.cjs ]; then
mkdir -p .yarn/plugins
cp ${./.yarn/plugins/yarn-plugin-nixify.cjs} .yarn/plugins/yarn-plugin-nixify.cjs
fi
'';
}