Bring the node-addon-landlock-run tree (tag v0.0.1, commit 614f7fd) into native/landlock-run as its source of record: launcher development happens here, next to the harness consumers, and the standalone repository becomes the release mirror the tree is exported to for packing and publishing (procedure in native/README.md). The subtree keeps its own pnpm workspace and lockfile and is NOT added to the harness workspace: harness installs, gates, and CI never touch it. The mirror's .github/ stays out of the subtree; a separate manually-dispatched workflow (.github/workflows/landlock-run.yml) runs the subtree's CI legs — the per-architecture native builds, real-kernel launcher proofs, and pack rehearsal — adapted with working-directory/cache paths. eslint ignores the subtree like vendor/; AGENTS.md gains the native/ layout line (+5 words on its budget ceiling).
4.1 KiB
Architecture
This repository owns confinement mechanism, not policy: consumers (agent harnesses, sandbox seams) decide which paths a run may read or write; this package family provides the launcher that enforces those grants and the JS seam that resolves and speaks to it. The packaging follows the per-platform-package model of node-addon-require-builtin (and esbuild), adapted from Node addons to standalone static executables.
Two-layer package family
The family is one entry package plus per-platform binary packages:
- Entry package (
node-addon-landlock-run): ESM JavaScript. Owns the tool's CLI contract — path resolution (launcherPath), the functional probe (probe), grant-argv construction (grantArgs), and the contract constants. Ships the C source in its tarball for auditability. Lists every platform package as anoptionalDependency. - Platform packages (
node-addon-landlock-run-linux-{x64,arm64}): one prebuilt static binary underbin/, aprebuilds.jsondeclaring it, and no JavaScript at all. npm'sos/cpufields select the matching one at install time; the entry package resolves it to a file path — there is nothing to import.
Because the contract parser and the binary version together in one family, probe-parsing drift against the binary is structurally impossible — the failure mode the split exists to prevent.
There is no shared loader package: platform packages have nothing to load. If a second tool ever needs shared JS, extract it then, not preemptively.
Resolution and availability
launcherPath() resolves node-addon-landlock-run-<platform>-<arch> and returns <package>/bin/landlock-run. When the package is not resolvable it returns a deterministic fallback path inside the entry package's own node_modules that simply never exists. Existence is deliberately unchecked either way: probe() is the single availability signal, and a missing binary probes unusable exactly like an unenforcing kernel. Consumers get one degradation path, not two.
The probe is functional — the launcher builds and enforces a real maximal ruleset in a short-lived child — because version checks would miss a kernel that has the syscalls but refuses enforcement.
Fail-closed everywhere
The launcher exits 125 without exec'ing the command on any launcher-level failure: usage error, unenforcing kernel, unopenable grant root, failed exec. Partial enforcement (an older Landlock ABI governing only a subset of accesses) is accepted, reported on stderr, and surfaced by the probe as partial — the consumer decides what its mode vocabulary promises at each level. Neither the binary nor the entry package reads environment variables: which binary confines a process is never decidable by the ambient environment.
Build and release model
Builds are native-only. scripts/build.ts compiles the running architecture's binaries with the distro musl-gcc (static: no loader or libc expectations on consumers, one binary for glibc and musl distros); CI's per-architecture runners are the builders of record, and no cross toolchain exists in the repo. The audit surface of a tool is its reviewed C source plus CI provenance, enforced by three gates: platform prepack refuses missing/wrong-ELF binaries, entry prepack refuses unbuilt lib/, and the release pipeline byte-pins installed binaries against the workspace builds they were packed from.
The package matrix is checked-in metadata (prebuilds.json + os/cpu fields); scripts/github-matrix.mjs derives the CI and Release matrices from it, so adding a platform extends automation without editing workflows.
Adding a platform
A new platform adds one packages/<platform>/ package (package.json with os/cpu, prebuilds.json, README, LICENSE), a runner entry in scripts/github-matrix.mjs, and a row in support-matrix.md — added only together with a native GitHub runner that builds and proves it (the no-cross-toolchain rule). Sibling launchers for other confinement mechanisms belong in their own repositories on this same template, not as second tools here.