Selaa lähdekoodia

chore: adopt node-addon-landlock-run source as native/ subtree

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).
kingwl 1 kuukausi sitten
vanhempi
sitoutus
0a486f09c9
46 muutettua tiedostoa jossa 2582 lisäystä ja 1 poistoa
  1. 127 0
      .github/workflows/landlock-run.yml
  2. 1 0
      AGENTS.md
  3. 1 0
      eslint.config.mjs
  4. 20 0
      native/README.md
  5. 13 0
      native/landlock-run/.gitignore
  6. 50 0
      native/landlock-run/AGENTS.md
  7. 28 0
      native/landlock-run/LICENSE
  8. 58 0
      native/landlock-run/README.md
  9. 34 0
      native/landlock-run/docs/architecture.md
  10. 34 0
      native/landlock-run/docs/cli-contract.md
  11. 30 0
      native/landlock-run/docs/naming.md
  12. 45 0
      native/landlock-run/docs/packaging.md
  13. 57 0
      native/landlock-run/docs/release.md
  14. 18 0
      native/landlock-run/docs/support-matrix.md
  15. 30 0
      native/landlock-run/package.json
  16. 16 0
      native/landlock-run/packages/entry/README.md
  17. 36 0
      native/landlock-run/packages/entry/package.json
  18. 126 0
      native/landlock-run/packages/entry/src/index.ts
  19. 302 0
      native/landlock-run/packages/entry/src/main.c
  20. 11 0
      native/landlock-run/packages/entry/tsconfig.json
  21. 28 0
      native/landlock-run/packages/linux-arm64/LICENSE
  22. 7 0
      native/landlock-run/packages/linux-arm64/README.md
  23. 26 0
      native/landlock-run/packages/linux-arm64/package.json
  24. 10 0
      native/landlock-run/packages/linux-arm64/prebuilds.json
  25. 28 0
      native/landlock-run/packages/linux-x64/LICENSE
  26. 7 0
      native/landlock-run/packages/linux-x64/README.md
  27. 26 0
      native/landlock-run/packages/linux-x64/package.json
  28. 10 0
      native/landlock-run/packages/linux-x64/prebuilds.json
  29. 345 0
      native/landlock-run/pnpm-lock.yaml
  30. 8 0
      native/landlock-run/pnpm-workspace.yaml
  31. 51 0
      native/landlock-run/scripts/assemble-prebuilds.mjs
  32. 86 0
      native/landlock-run/scripts/build.ts
  33. 90 0
      native/landlock-run/scripts/bump-release.mjs
  34. 42 0
      native/landlock-run/scripts/commit-release.mjs
  35. 66 0
      native/landlock-run/scripts/github-matrix.mjs
  36. 76 0
      native/landlock-run/scripts/pack-release.mjs
  37. 88 0
      native/landlock-run/scripts/repo.mjs
  38. 25 0
      native/landlock-run/scripts/verify-entry-lib.mjs
  39. 31 0
      native/landlock-run/scripts/verify-launcher-binary.mjs
  40. 223 0
      native/landlock-run/scripts/verify-packed-install.mjs
  41. 52 0
      native/landlock-run/scripts/verify-release.mjs
  42. 76 0
      native/landlock-run/test/entry.test.js
  43. 121 0
      native/landlock-run/test/launcher.test.js
  44. 11 0
      native/landlock-run/tsconfig.base.json
  45. 11 0
      native/landlock-run/tsconfig.json
  46. 1 1
      scripts/doc-budgets.manifest.json

+ 127 - 0
.github/workflows/landlock-run.yml

@@ -0,0 +1,127 @@
+# Manually-dispatched CI for the landlock-run source of record
+# (native/landlock-run). A separate workflow from ci.yml on purpose: the
+# subtree is a self-contained pnpm workspace with its own gates, exercised on
+# demand — per-architecture native legs (build + behavioral tests + pack
+# rehearsal on real kernels) plus one darwin leg proving the documented
+# degradation on hosts without a platform package. Legs derive from the
+# subtree's checked-in package matrix (scripts/github-matrix.mjs). Packing
+# for npm happens in the release mirror (node-addon-landlock-run) after an
+# export — see native/README.md; this workflow never packs for release.
+name: Landlock Run
+
+on:
+  workflow_dispatch:
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+
+defaults:
+  run:
+    working-directory: native/landlock-run
+
+jobs:
+  matrix:
+    name: Matrix
+    runs-on: ubuntu-24.04
+    outputs:
+      ci: ${{ steps.matrix.outputs.ci }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - id: matrix
+        run: echo "ci=$(node ./scripts/github-matrix.mjs ci)" >> "$GITHUB_OUTPUT"
+
+  native:
+    name: ${{ matrix.platform }}
+    needs: matrix
+    runs-on: ${{ matrix.runner }}
+    strategy:
+      fail-fast: false
+      matrix: ${{ fromJson(needs.matrix.outputs.ci) }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: pnpm/action-setup@v4
+        with:
+          package_json_file: native/landlock-run/package.json
+
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 24
+          cache: pnpm
+          cache-dependency-path: native/landlock-run/pnpm-lock.yaml
+
+      - name: Install dependencies
+        run: pnpm install --frozen-lockfile
+
+      - name: Install musl toolchain
+        run: |
+          sudo apt-get update -q
+          sudo apt-get install -yq musl-tools
+
+      - name: Build TypeScript
+        run: pnpm build:ts
+
+      - name: Typecheck
+        run: pnpm typecheck
+
+      - name: Build native binaries (this architecture is the builder of record)
+        run: pnpm build:native
+
+      - name: Entry tests (keyless)
+        run: node ./test/entry.test.js
+
+      # NALR_REQUIRE_LANDLOCK: a self-skip on the very platform that exists to
+      # prove enforcement would be a false green, so an unenforcing kernel
+      # fails the leg instead of skipping.
+      - name: Launcher tests (real kernel enforcement)
+        run: node ./test/launcher.test.js
+        env:
+          NALR_REQUIRE_LANDLOCK: 1
+
+      - name: Pack rehearsal (pack → install → confine, this platform only)
+        run: |
+          node ./scripts/pack-release.mjs .release/npm --current-platform-only
+          node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only
+        env:
+          NALR_REQUIRE_LANDLOCK: 1
+
+  darwin:
+    name: darwin (no platform package — degradation proof)
+    runs-on: macos-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: pnpm/action-setup@v4
+        with:
+          package_json_file: native/landlock-run/package.json
+
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 24
+          cache: pnpm
+          cache-dependency-path: native/landlock-run/pnpm-lock.yaml
+
+      - name: Install dependencies
+        run: pnpm install --frozen-lockfile
+
+      - name: Build TypeScript
+        run: pnpm build:ts
+
+      - name: Typecheck
+        run: pnpm typecheck
+
+      - name: Entry tests (keyless)
+        run: node ./test/entry.test.js
+
+      - name: Launcher tests (must self-skip cleanly)
+        run: node ./test/launcher.test.js
+
+      - name: Pack rehearsal (entry only — fallback resolution + unusable probe)
+        run: |
+          node ./scripts/pack-release.mjs .release/npm --current-platform-only
+          node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only

+ 1 - 0
AGENTS.md

@@ -30,6 +30,7 @@ packages/    Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai
   support/     dev/test infrastructure packages
   util/        zero-dependency utilities
 python/      Python SDK and bundled runtime (see python/README.md)
+native/      node-addon-landlock-run source of record (see native/README.md)
 examples/    Runnable demos: thin cordis.yml leaves over the app packages (see examples/AGENTS.md)
 docs/        architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)
 scripts/     repo gates and generators

+ 1 - 0
eslint.config.mjs

@@ -13,6 +13,7 @@ export default tseslint.config(
       '.claude/**', // harness-local state (worktrees, skills) — other checkouts, not this one's sources
       '**/.doc-typecheck-*/**',
       'vendor/**', // vendored source keeps upstream style and idioms
+      'native/**', // imported landlock-run subtree: self-contained workspace with its own gates (native/README.md)
       '**/*.js',
       '**/*.mjs',
       '*.config.ts', // root tool configs (vitest, tsdown) — no project service

+ 20 - 0
native/README.md

@@ -0,0 +1,20 @@
+# native/
+
+Source of record for `node-addon-landlock-run`, the Landlock self-restrict-then-exec launcher the harness consumes from npm (`packages/sandbox/sandbox-local`, `packages/bash/bash-sandbox`). Launcher development happens HERE, next to the consumers; the standalone repository is the release mirror that packs and publishes the npm package family.
+
+## Release mirror
+
+| Directory | Mirror repo | Last exported release | Commit |
+|---|---|---|---|
+| `landlock-run/` | https://github.com/deepseek-harness/node-addon-landlock-run | `v0.0.1` | `614f7fd7dc11e6eaceefba9e7ff1fbe28b51ba22` |
+
+The subtree is a self-contained pnpm workspace with its own `AGENTS.md`, docs, gates, and lockfile; it is NOT part of the harness workspace (`pnpm-workspace.yaml` does not include it), so harness installs, builds, and CI gates never touch it. The mirror's `.github/` stays out of the subtree — [.github/workflows/landlock-run.yml](../.github/workflows/landlock-run.yml) (manual dispatch) runs the subtree's CI legs here, and a change to those legs is mirrored into the mirror's `ci.yml` at the next export.
+
+## Export procedure (cutting a release)
+
+1. Land the launcher change here through a normal harness PR; dispatch the `Landlock Run` workflow and get its legs green.
+2. In the mirror checkout, replace everything except `.github/`: `git -C <mirror> rm -rq -- . ':!.github'`, then `git -C <harness> archive HEAD:native/landlock-run | tar -x -C <mirror>`, then `git -C <mirror> add -A` and commit.
+3. In the mirror, follow its release checklist (`docs/release.md`): `pnpm release:commit <version>` → merge → tag `vX.Y.Z` → two-phase `Release` workflow (`publish=false` rehearsal, then `publish=true` from the tag).
+4. Update the manifest table above with the released tag/commit, and bump the harness consumers' dependency range in the same change.
+
+The mirror must not diverge: a change committed there directly (hotfix during a release) is ported back here before the next export.

+ 13 - 0
native/landlock-run/.gitignore

@@ -0,0 +1,13 @@
+# Built native binaries ride npm tarballs via each package's `files` list,
+# never git. Root-level rules on purpose: a package-nested ignore file would
+# also steer `pnpm pack` and has silently dropped payload from tarballs before.
+packages/*/bin/
+packages/*/lib/
+
+/.claude/
+/.release/
+dist/
+node_modules/
+/package-lock.json
+*.log
+*.tsbuildinfo

+ 50 - 0
native/landlock-run/AGENTS.md

@@ -0,0 +1,50 @@
+# AGENTS.md
+
+This workspace builds `landlock-run`, a Landlock self-restrict-then-exec launcher: a small, auditable confinement binary distributed as prebuilt per-platform npm packages, plus the thin JS entry package that resolves it and speaks its CLI contract. The source of record is the `deepseek-harness` repository's `native/landlock-run/`; the `node-addon-landlock-run` repository is the release mirror this tree is exported to for packing and publishing (procedure: `native/README.md` in the harness repo). Make changes in the source of record, never only in the mirror.
+
+## Pre-release stance
+
+The project is pre-1.0. Prefer the correct public shape over compatibility shims: if a package name, exported field, layout, or contract detail is wrong, rename it and update all references in the same change. Do not add deprecated aliases unless a stable release already needs them.
+
+## Runtime safety rules
+
+- Every tool must fail closed. If a ruleset cannot be created or the kernel does not enforce it, exit non-zero WITHOUT exec'ing the wrapped command. Never run unconfined as a fallback.
+- Runtime binaries and the entry packages take NO environment-variable overrides: which binary confines a process must never be decidable by the ambient environment. Test injection is by function parameter; the `NALR_*` prefix is for build/test orchestration only.
+- Kernel UAPI is self-defined in the C source (verbatim from the kernel headers), keeping builds independent of toolchain header vintage and making the definitions part of the audit record.
+- No libraries beyond libc, linked statically against musl. The audit surface of a tool is its C source plus the kernel's stable syscall contract.
+- The CLI contract of each tool ([docs/cli-contract.md](docs/cli-contract.md)) is the cross-repo compatibility surface: argv grammar, exit codes, and report lines change only with a version bump and a changelog entry, and consumers parse them only through the entry package.
+- There is deliberately NO install-time build fallback: a host without a matching platform package gets a nonexistent launcher path, the consumer's probe fails, and the consumer falls closed — that degradation is part of the design, not a gap to fill with node-gyp.
+
+## Repository layout
+
+```text
+packages/entry/     Published entry package: JS seam (resolve/probe/grants) + the C source.
+packages/linux-*/   Published per-platform packages: one prebuilt static binary, no JavaScript.
+scripts/            Build, matrix derivation, prepack gates, and release orchestration.
+test/               Plain-node behavioral tests (entry seam + real-kernel launcher proofs).
+docs/               Architecture, packaging, CLI contract, release, support matrix, naming.
+```
+
+## Commands
+
+```sh
+pnpm install
+pnpm build:ts        # entry packages → lib/
+pnpm build:native    # this Linux architecture's binaries (needs musl-tools); fails fast elsewhere
+pnpm typecheck
+pnpm test            # entry tests everywhere; launcher tests need linux + built binary
+```
+
+## Packaging invariants
+
+- The package matrix is explicit, checked-in metadata: `packages/<name>/package.json` (`os`, `cpu`), `packages/<name>/prebuilds.json` (the binaries that may exist there), and [docs/support-matrix.md](docs/support-matrix.md) stay synchronized when the matrix changes. `scripts/github-matrix.mjs` derives CI and release matrices from it; nothing else enumerates platforms.
+- Platform package names contain platform only (`-linux-x64`), never tool variants — those stay inside `prebuilds.json`. Static musl linking is why there is no libc suffix: one binary serves glibc and musl distros.
+- Platform packages ship no JavaScript; the entry package resolves them to file paths. Backends prove themselves at runtime through the functional probe, never through metadata trust.
+- Builds are native-only: each architecture compiles its own binary on its own runner (CI is the builder of record); no cross toolchain enters the repo.
+- Every tarball is gated at pack time: platform packages refuse to pack without their declared binaries present, executable, and in the right ELF architecture (`verify-launcher-binary.mjs`), entry packages without built `lib/` (`verify-entry-lib.mjs`), and the release pipeline byte-pins installed binaries against the workspace builds (`verify-packed-install.mjs`).
+- Platform tarballs are packed with `npm pack`, never `pnpm pack`: pnpm's pack path strips the executable bit (observed on 11.7.0), shipping a launcher no consumer can spawn. `pack-release.mjs` encodes the split; the rehearsal asserts executability of the installed copy so a regression fails loudly instead of masquerading as a non-enforcing kernel.
+- Generated artifacts stay out of git: `packages/*/bin/`, `packages/*/lib/`, `dist/`, `.release/`, `*.tsbuildinfo`. Ignore rules live in the ROOT `.gitignore` only — a package-nested ignore file can silently drop payload from tarballs.
+
+## Documentation
+
+User-facing docs are English. Keep the README focused on install, usage, and support status; durable design decisions belong in docs/ alongside the code, and the current implemented shape belongs in [docs/architecture.md](docs/architecture.md).

+ 28 - 0
native/landlock-run/LICENSE

@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, node-addon-landlock-run contributors
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 58 - 0
native/landlock-run/README.md

@@ -0,0 +1,58 @@
+# node-addon-landlock-run
+
+A [Landlock](https://landlock.io/) self-restrict-then-exec launcher for confining subprocesses on Linux, distributed as prebuilt per-platform npm packages plus a thin JS entry package that resolves the binary and speaks its CLI contract. Built for agent harnesses and other hosts that need to run untrusted commands under a filesystem allow-list without confining themselves.
+
+The first tool is **`landlock-run`** — a self-restrict-then-exec [Landlock](https://landlock.io/) launcher (~300 lines of C11 over the raw kernel UAPI, statically linked against musl). It installs a Landlock ruleset on itself and `exec`s the wrapped command; the ruleset is inherited across `execve`, so the command and every process it spawns run confined while the invoking process stays unrestricted. Fail-closed: if the kernel cannot enforce, it exits without running the command.
+
+## Install
+
+```sh
+npm install node-addon-landlock-run
+```
+
+Published packages use an entry package plus platform optional packages:
+
+```text
+node-addon-landlock-run
+node-addon-landlock-run-linux-x64
+node-addon-landlock-run-linux-arm64
+```
+
+npm's `os`/`cpu` fields make installers fetch only the matching platform package. There is no install-time build fallback on purpose: on a host without a platform package the resolved path never exists, the probe reports `unusable`, and the consumer falls closed.
+
+## Usage
+
+```js
+import { grantArgs, launcherPath, probe } from 'node-addon-landlock-run';
+
+const launcher = launcherPath();
+if (probe(launcher) !== 'unusable') {
+  const argv = [launcher, ...grantArgs({ readOnly: ['/'], readWrite: ['/tmp/work'] }), '--', 'bash', '-c', command];
+  // spawn argv with your process runner of choice
+}
+```
+
+The public API is intentionally small:
+
+- `launcherPath()`: absolute path of this host's launcher (existence deliberately unchecked — the probe is the availability signal).
+- `probe(launcher?, { timeoutMs? })`: functional enforcement probe — `'full' | 'partial' | 'unusable'`.
+- `grantArgs({ readOnly?, readWrite? })`: the launcher's grant argv; everything not granted is denied.
+- `LAUNCHER_BIN`, `LAUNCHER_FAILURE_EXIT` (125): contract constants.
+
+The full binary contract (argv grammar, exit codes, report lines) is pinned in [docs/cli-contract.md](docs/cli-contract.md).
+
+## Support
+
+linux-x64 and linux-arm64, kernel with Landlock enabled (5.13+; ABI level determines `full` vs `partial` enforcement — see [docs/support-matrix.md](docs/support-matrix.md)). Other platforms deliberately have no package: consumers run different confinement backends there.
+
+## Development
+
+```sh
+corepack enable
+pnpm install
+pnpm build:ts        # entry packages → lib/
+pnpm build:native    # this Linux architecture's binaries (apt-get install musl-tools)
+pnpm test
+```
+
+Binaries are git-ignored and built natively per architecture — locally for your own machine, by CI's per-arch runners as the builders of record. Release flow: [docs/release.md](docs/release.md).

+ 34 - 0
native/landlock-run/docs/architecture.md

@@ -0,0 +1,34 @@
+# 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`](https://www.npmjs.com/package/@esplus/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 an `optionalDependency`.
+- **Platform packages** (`node-addon-landlock-run-linux-{x64,arm64}`): one prebuilt static binary under `bin/`, a `prebuilds.json` declaring it, and no JavaScript at all. npm's `os`/`cpu` fields 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](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.

+ 34 - 0
native/landlock-run/docs/cli-contract.md

@@ -0,0 +1,34 @@
+# CLI contract: landlock-run
+
+This file pins the launcher's externally observable behavior — the cross-repo compatibility surface between the binaries and every consumer. Consumers interact with it only through the entry package (`launcherPath`/`probe`/`grantArgs`); changing anything below requires a version bump for the whole package family and a note in the release notes.
+
+## Invocation grammar
+
+```text
+landlock-run [--ro <path>]... [--rw <path>]... -- <argv>...
+landlock-run --probe
+```
+
+- `--ro <path>`: grant read + execute beneath `<path>`.
+- `--rw <path>`: grant full filesystem access beneath `<path>` (every access the negotiated kernel ABI can govern).
+- Everything not granted is denied — Landlock rulesets are allow-lists.
+- A grant on a non-directory keeps only its file-compatible access bits (this is how a `--rw /dev/null` grant works).
+- `--`: mandatory separator; everything after it is the command argv, exec'd via `execvp` with the launcher's environment unchanged.
+- `--probe`: mutually exclusive with grants and a command.
+- No other flags, no environment-variable inputs.
+
+## Exit codes
+
+- `125` (`LAUNCHER_FAILURE_EXIT`): every launcher-level failure — usage error, kernel that cannot enforce Landlock, unopenable grant root, failed `exec`. The wrapped command was NOT run (fail-closed; the one exception is `exec` itself failing after restriction, which by definition never ran the command either).
+- Any other status: the wrapped command's own exit status, passed through unchanged.
+- `--probe`: `0` when the kernel enforces (fully or partially), `125` otherwise.
+
+## Report lines
+
+- Probe success prints exactly one stdout line: `landlock: fully enforced` or `landlock: partially enforced (older ABI)`. The entry package's `probe()` maps these to `full`/`partial`; a non-zero probe exit maps to `unusable`.
+- A confined run under a partial-ABI kernel prints one stderr line `landlock-run: partial enforcement (older Landlock ABI)` and proceeds — still confined for everything the kernel supports.
+- Every fatal error prints one stderr line prefixed `landlock-run: ` before exiting `125`.
+
+## Confinement semantics
+
+The launcher sets `no_new_privs`, installs the ruleset on itself, and `exec`s the command; the ruleset is inherited across `execve`, so every descendant process is equally confined. The ruleset governs the filesystem accesses of the kernel's negotiated Landlock ABI (up to ABI 5); accesses newer than the running ABI are not governed and are the difference between `full` and `partial`.

+ 30 - 0
native/landlock-run/docs/naming.md

@@ -0,0 +1,30 @@
+# Naming
+
+## npm packages
+
+The public package family is unscoped, using the `node-addon-landlock-run` package prefix; platform packages append platform information only:
+
+```text
+node-addon-landlock-run
+node-addon-landlock-run-<platform>
+```
+
+Platform suffixes carry no libc component (binaries are static musl) and no variant component — variants stay inside `prebuilds.json` and binary filenames.
+
+## Binaries
+
+The launcher executable is `landlock-run`, shipped at `bin/landlock-run` inside each platform package.
+
+## Environment variables
+
+The `NALR_` prefix (Node Addon Landlock Run) is reserved for build/test orchestration:
+
+```text
+NALR_REQUIRE_LANDLOCK   test-only: an unenforcing kernel fails instead of skipping
+```
+
+Runtime binaries and entry packages read NO environment variables — a runtime safety rule ([AGENTS.md](../AGENTS.md)), not a naming convention. Do not include the npm scope in environment variable names.
+
+## C symbols
+
+The launcher is a single C file with static linkage; there is no exported symbol namespace. Kernel UAPI constants keep their kernel names prefixed `LL_` where locally defined.

+ 45 - 0
native/landlock-run/docs/packaging.md

@@ -0,0 +1,45 @@
+# Packaging
+
+The package family uses the same broad shape as native packages such as esbuild: one JS entry package plus platform optional packages. Unlike Node addons there is no ABI or backend dimension — each platform package carries exactly the static executables its `prebuilds.json` declares.
+
+## Published packages
+
+```text
+node-addon-landlock-run
+node-addon-landlock-run-linux-x64
+node-addon-landlock-run-linux-arm64
+```
+
+Unsupported platforms are intentionally absent from `optionalDependencies` — see [support-matrix.md](support-matrix.md).
+
+## Package matrix
+
+The matrix is explicit in checked-in metadata:
+
+- `packages/entry/package.json` lists the platform packages as `optionalDependencies`.
+- `packages/<name>/package.json` declares `os` and `cpu`. There is no `libc` field on purpose: the binaries are statically linked against musl and run on glibc and musl distros alike.
+- `packages/<name>/prebuilds.json` declares the binaries that may exist in that package (`tool`, `kind`, `path`).
+- [support-matrix.md](support-matrix.md) explains why unsupported platform packages are not published.
+
+`scripts/github-matrix.mjs` derives the CI and Release matrices from these files. `scripts/build.ts` builds only the current host's targets, into `packages/<name>/bin/`; it is not a matrix generator. When changing the matrix, update package metadata, `prebuilds.json`, the lockfile, and the support/release docs in the same change.
+
+## Runtime selection
+
+1. npm's `os`/`cpu` fields make installers fetch only the matching platform package.
+2. The entry package's `launcherPath()` resolves it to `<package>/bin/landlock-run`; unresolvable packages yield a deterministic, never-existing fallback path.
+3. `probe()` is the single availability signal: missing binary and unenforcing kernel are deliberately indistinguishable (`unusable`), so consumers have one fail-closed path.
+
+## No install fallback
+
+The entry package has NO install script and never compiles on the consumer host. A compile fallback would require a musl toolchain everywhere and turn a clean fail-closed degradation into an environment-dependent maybe. The packed-manifest check in `verify-packed-install.mjs` enforces the absence of install lifecycle scripts.
+
+## Pack gates
+
+Platform tarballs are produced by `npm pack`, entry tarballs by `pnpm pack` — deliberately split: `pnpm pack` (observed on 11.7.0) normalizes file modes and strips the executable bit, which would ship a launcher no consumer can spawn, while platform packages have no dependencies and so need none of pnpm's workspace-protocol conversion; entry packages need that conversion and carry no executables. `scripts/pack-release.mjs` encodes the split — never hand-pack a platform package with pnpm.
+
+Both pack paths produce the exact publish bytes behind a `prepack` gate:
+
+- Platform packages: `scripts/verify-launcher-binary.mjs` — every declared binary present, executable, ELF `e_machine` matching the declared `cpu`, nothing undeclared in `bin/`.
+- Entry packages: `scripts/verify-entry-lib.mjs` — built `lib/` present.
+
+`scripts/verify-packed-install.mjs` then rehearses the consumer path from the packed tarballs: payload checks, a throwaway install, a byte-pin of the installed binary against the workspace build, an executability check on the installed copy, and a real confinement world-proof through the installed launcher. A non-executable or missing binary fails loudly here instead of masquerading as a non-enforcing kernel.

+ 57 - 0
native/landlock-run/docs/release.md

@@ -0,0 +1,57 @@
+# Release
+
+Pre-1.0: treat this as a release checklist, not a stability policy.
+
+## Versioning
+
+One version across every package in the repo. Use the bump helper:
+
+```sh
+pnpm release:bump patch          # or minor / major / x.y.z
+```
+
+It updates the root and every `packages/*` manifest, refreshes the lockfile (`--ignore-scripts --lockfile-only`), and runs `release:verify`. Explicit versions accept full semver including prereleases (`pnpm release:bump 0.0.0-test.0`); the publish workflow puts prerelease versions under the `next` dist-tag, so `latest` never points at a test build. Keep `workspace:*` dependencies in source; pnpm converts them to concrete versions during pack.
+
+Version bumps are normal source changes: open a release PR (or commit) with the manifests and lockfile, merge it, then create the matching `vX.Y.Z` tag from that commit. The publish workflow validates that the tag matches every package version.
+
+```sh
+pnpm release:commit patch        # bump + stage + commit in one command
+git tag v0.0.2
+```
+
+## Preflight
+
+```sh
+pnpm install --frozen-lockfile
+pnpm build:ts
+pnpm typecheck
+pnpm test                        # launcher half needs a Linux host with the binary built
+```
+
+On a Linux host, also rehearse the pack path locally:
+
+```sh
+pnpm build:native
+node ./scripts/pack-release.mjs .release/npm --current-platform-only
+node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only
+```
+
+## Publish
+
+Use the `Release` workflow so every binary is built on its matching native runner:
+
+1. Run it with `publish=false` (from the release commit) to build all platform binaries, assemble and verify the payloads, pack the tarballs in publish order, rehearse the packed install, and upload the `npm-tarballs` artifact for inspection.
+2. Create and push the `vX.Y.Z` tag matching the package versions.
+3. Run the same workflow from that tag with `publish=true`.
+
+The workflow publishes only from the final packed tarballs, in `publish-order.txt` order (platform packages before the entry that optionally depends on them). It supports npm trusted publishing through GitHub OIDC; without it, provide an `NPM_TOKEN` secret in the `npm-publish` environment. Packages publish with `--access public`.
+
+Manual local fallback (current platform's packages only) — always through `pack-release.mjs`, never `pnpm publish` directly (pnpm's pack path strips the launcher's executable bit; see [packaging.md](packaging.md)):
+
+```sh
+node ./scripts/pack-release.mjs dist/npm --current-platform-only
+node ./scripts/verify-packed-install.mjs dist/npm --current-platform-only
+while IFS= read -r tarball; do npm publish "dist/npm/${tarball}" --access public; done < dist/npm/publish-order.txt
+```
+
+Do not commit `.npmrc` files with tokens or registry overrides.

+ 18 - 0
native/landlock-run/docs/support-matrix.md

@@ -0,0 +1,18 @@
+# Support matrix
+
+## Supported
+
+| Platform package | GitHub runner (builder of record) | Notes |
+|---|---|---|
+| `node-addon-landlock-run-linux-x64` | `ubuntu-24.04` | static musl — glibc and musl distros alike |
+| `node-addon-landlock-run-linux-arm64` | `ubuntu-24.04-arm` | static musl — glibc and musl distros alike |
+
+Enforcement additionally requires a kernel with Landlock enabled (5.13+). The negotiated ABI level decides the probe verdict: every access this build knows governed → `full`; an older ABI governing a subset → `partial` (still confined for everything it supports); Landlock absent or disabled → `unusable`, and the launcher refuses to run commands at all. The probe — not the kernel version — is the authority: a kernel built without Landlock, or with the LSM disabled, probes `unusable` regardless of its version.
+
+## Deliberately unsupported
+
+- **darwin**: macOS consumers typically confine through `sandbox-exec`/Seatbelt, which ships with the OS — there is no binary to distribute.
+- **win32**: a Windows confinement launcher would be a different mechanism in its own repository, not a port of this one.
+- **Other Linux architectures** (riscv64, s390x, …): no native CI builder of record yet. The no-cross-toolchain rule means a platform package is added only together with a native runner that builds and proves it.
+
+A consumer on an unsupported platform resolves a nonexistent launcher path, probes `unusable`, and falls closed — the documented degradation, exercised by CI's darwin leg.

+ 30 - 0
native/landlock-run/package.json

@@ -0,0 +1,30 @@
+{
+  "name": "node-addon-landlock-run-workspace",
+  "version": "0.0.1",
+  "private": true,
+  "type": "module",
+  "license": "BSD-3-Clause",
+  "packageManager": "pnpm@11.7.0",
+  "scripts": {
+    "build": "pnpm build:ts",
+    "build:ts": "tsc -b",
+    "build:native": "tsx ./scripts/build.ts",
+    "typecheck": "tsc --noEmit && tsc -b --dry",
+    "test": "node ./test/entry.test.js && node ./test/launcher.test.js",
+    "test:entry": "node ./test/entry.test.js",
+    "test:launcher": "node ./test/launcher.test.js",
+    "gha:matrix": "node ./scripts/github-matrix.mjs",
+    "release:bump": "node ./scripts/bump-release.mjs",
+    "release:commit": "node ./scripts/commit-release.mjs",
+    "release:assemble-prebuilds": "node ./scripts/assemble-prebuilds.mjs",
+    "release:verify": "node ./scripts/verify-release.mjs",
+    "release:pack": "node ./scripts/pack-release.mjs",
+    "release:verify-packed-install": "node ./scripts/verify-packed-install.mjs"
+  },
+  "devDependencies": {
+    "node-addon-landlock-run": "workspace:*",
+    "@types/node": "^24.10.0",
+    "tsx": "^4.20.6",
+    "typescript": "^5.9.3"
+  }
+}

+ 16 - 0
native/landlock-run/packages/entry/README.md

@@ -0,0 +1,16 @@
+# node-addon-landlock-run
+
+Landlock self-restrict-then-exec launcher for confining subprocesses on Linux: this entry package resolves the per-platform prebuilt binary, runs its functional enforcement probe, and builds its grant argv — consumers never spell launcher flags or parse launcher output themselves.
+
+```js
+import { grantArgs, launcherPath, probe } from 'node-addon-landlock-run';
+
+const launcher = launcherPath();
+if (probe(launcher) !== 'unusable') {
+  const argv = [launcher, ...grantArgs({ readOnly: ['/'], readWrite: ['/tmp/work'] }), '--', 'bash', '-c', command];
+}
+```
+
+The launcher installs a Landlock ruleset on itself and `exec`s the wrapped command; the ruleset is inherited across `execve`, so the whole process tree runs confined. Everything not granted is denied, and launcher failures exit `125` without running the command — fail-closed, never fail-open. The binary contract is pinned in the repo's `docs/cli-contract.md`; the C source rides this tarball (`src/main.c`) for audit.
+
+Platform packages (`os`/`cpu`-selected optional dependencies, no JavaScript inside): `node-addon-landlock-run-linux-x64`, `node-addon-landlock-run-linux-arm64`. On hosts without one, `launcherPath()` returns a deterministic nonexistent path and `probe()` reports `'unusable'` — there is deliberately no install-time compile fallback.

+ 36 - 0
native/landlock-run/packages/entry/package.json

@@ -0,0 +1,36 @@
+{
+  "name": "node-addon-landlock-run",
+  "version": "0.0.1",
+  "type": "module",
+  "description": "Landlock self-restrict-then-exec launcher for sandboxing subprocesses on Linux: per-platform prebuilt static binaries plus the JS seam that resolves, probes, and speaks their CLI contract",
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./lib/index.d.ts",
+      "default": "./lib/index.js"
+    },
+    "./package.json": "./package.json"
+  },
+  "files": [
+    "README.md",
+    "lib/",
+    "!lib/*.tsbuildinfo",
+    "src/main.c"
+  ],
+  "scripts": {
+    "build:js": "tsc -b",
+    "prepack": "node ../../scripts/verify-entry-lib.mjs"
+  },
+  "engines": {
+    "node": ">=20"
+  },
+  "license": "BSD-3-Clause",
+  "publishConfig": {
+    "access": "public"
+  },
+  "optionalDependencies": {
+    "node-addon-landlock-run-linux-arm64": "workspace:*",
+    "node-addon-landlock-run-linux-x64": "workspace:*"
+  }
+}

+ 126 - 0
native/landlock-run/packages/entry/src/index.ts

@@ -0,0 +1,126 @@
+/**
+ * The JS seam over the prebuilt `landlock-run` launcher: resolve the
+ * binary for this host, build its grant argv, and run its functional probe.
+ *
+ * This module owns the launcher's CLI contract (`docs/cli-contract.md`) so
+ * consumers never parse launcher output or spell launcher flags themselves —
+ * the contract and the binaries version together in one package family,
+ * which makes probe-parsing drift against the binary structurally
+ * impossible. Policy stays with the consumer: this package does not know
+ * what a "sandbox mode" is, only which paths are granted read or write.
+ *
+ * Deliberately no environment-variable overrides anywhere in this module:
+ * which binary confines a process must never be decidable by the ambient
+ * environment. Test injection is by function parameter.
+ */
+import { spawnSync } from 'node:child_process'
+import { createRequire } from 'node:module'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+/** The launcher binary's file name inside each platform package's `bin/`. */
+export const LAUNCHER_BIN = 'landlock-run'
+
+/**
+ * The exit code for every launcher-level failure (usage error, unenforcing
+ * kernel, unopenable grant root, failed exec) — chosen because the wrapped
+ * command itself is unlikely to use it, so a consumer can tell launcher
+ * failures from command failures. Part of the CLI contract.
+ */
+export const LAUNCHER_FAILURE_EXIT = 125
+
+/**
+ * The probe's verdict on this host: `full` when the running kernel enforces
+ * every access the launcher can govern, `partial` when an older Landlock ABI
+ * governs only a subset (still confined for everything it supports), and
+ * `unusable` when nothing can be enforced — a kernel without Landlock, a
+ * disabled LSM, or a missing binary, all indistinguishable on purpose
+ * because the consumer's answer is the same: do not trust this launcher.
+ */
+export type LandlockEnforcement = 'full' | 'partial' | 'unusable'
+
+/**
+ * Filesystem grants for one confined run. Everything not granted is denied —
+ * Landlock rulesets are allow-lists.
+ */
+export interface LauncherGrants {
+  /** Roots granted read + execute beneath (the launcher's `--ro`). */
+  readonly readOnly?: readonly string[]
+  /** Roots granted full filesystem access beneath (the launcher's `--rw`). */
+  readonly readWrite?: readonly string[]
+}
+
+/**
+ * Path of the launcher binary for this host: resolved from the per-platform
+ * npm package `node-addon-landlock-run-<platform>-<arch>` (npm's
+ * `os`/`cpu` fields make installers fetch only the matching one). When the
+ * package is not resolvable — a platform without one, or an install that
+ * skipped the optional dependency — the returned fallback path points inside
+ * this package's own `node_modules` and simply never exists. Existence is
+ * deliberately not checked either way: {@link probe} is the single
+ * availability signal (a missing binary probes `unusable` the same way an
+ * unenforcing kernel does).
+ * @param resolvePackageJson - test seam over `require.resolve` (the default
+ *   covers real installs); receives the platform package's `package.json`
+ *   specifier and returns its absolute path, throwing when unresolvable.
+ * @returns the absolute launcher path to probe and exec.
+ */
+export function launcherPath(
+  resolvePackageJson: (specifier: string) => string = createRequire(import.meta.url).resolve,
+): string {
+  const platformPackage = `node-addon-landlock-run-${process.platform}-${process.arch}`
+  try {
+    return join(dirname(resolvePackageJson(`${platformPackage}/package.json`)), 'bin', LAUNCHER_BIN)
+  } catch {
+    // Unresolvable platform package: no such package exists for this host, or
+    // it was not installed. Fall back to the path pnpm's layout WOULD use —
+    // absolute, inside this package's boundary (never cwd-relative: a
+    // spawnable relative path here would hand cwd control over which binary
+    // confines), and nonexistent exactly when the package is absent.
+    return fileURLToPath(new URL(`../node_modules/${platformPackage}/bin/${LAUNCHER_BIN}`, import.meta.url))
+  }
+}
+
+/**
+ * The launcher grant arguments for one set of filesystem grants — everything
+ * before the `--` argv separator. A caller spawns
+ * `[launcherPath(), ...grantArgs(grants), '--', ...command]`; the flag
+ * spellings stay private to this package.
+ * @param grants - the read-only and read-write roots to allow.
+ * @returns the `--ro <path>` / `--rw <path>` argument list, read-only roots
+ *   first, in the caller's order.
+ */
+export function grantArgs(grants: LauncherGrants): string[] {
+  return [
+    ...(grants.readOnly ?? []).flatMap(root => ['--ro', root]),
+    ...(grants.readWrite ?? []).flatMap(root => ['--rw', root]),
+  ]
+}
+
+/**
+ * Functional probe: `landlock-run --probe` builds and enforces a maximal
+ * ruleset in a short-lived child and exits 0 only when the running kernel
+ * actually enforces it — `--version`-style checks would miss a kernel that
+ * has the syscalls but refuses enforcement. The probe's one report line is
+ * part of the CLI contract and distinguishes complete from per-ABI-subset
+ * enforcement; a zero exit without the partial marker reads as `full`. A
+ * failed or timed-out spawn (missing binary, wrong architecture, unenforcing
+ * kernel) probes `unusable`. Synchronous by design: consumers run it once
+ * and cache the verdict.
+ * @param launcher - the launcher path to probe; defaults to
+ *   {@link launcherPath}'s resolution for this host.
+ * @param options - `timeoutMs` bounds the probe child (default 2000).
+ * @returns the enforcement verdict for this host.
+ */
+export function probe(
+  launcher: string = launcherPath(),
+  options: { timeoutMs?: number } = {},
+): LandlockEnforcement {
+  const result = spawnSync(launcher, ['--probe'], {
+    timeout: options.timeoutMs ?? 2000,
+    encoding: 'utf8',
+    stdio: ['ignore', 'pipe', 'ignore'],
+  })
+  if (result.status !== 0) return 'unusable'
+  return /partially enforced/.test(result.stdout) ? 'partial' : 'full'
+}

+ 302 - 0
native/landlock-run/packages/entry/src/main.c

@@ -0,0 +1,302 @@
+/*
+ * landlock-run: self-restrict-then-exec Landlock launcher.
+ *
+ * The Landlock rung of a consuming sandbox seam, for Linux hosts where
+ * `bwrap` is
+ * unusable (not installed, unprivileged user namespaces disabled, or an LSM
+ * profile that denies mount — Landlock is an independent syscall family and
+ * needs none of those). The launcher installs a Landlock
+ * ruleset on itself and `exec`s the wrapped command; the ruleset is inherited
+ * across `execve`, so the command (and every process it spawns) runs confined
+ * while the invoking process stays unrestricted.
+ *
+ * CLI contract (mirrors the `bwrap` runner argv shape the executor wraps):
+ *
+ *   landlock-run [--ro <path>]... [--rw <path>]... -- <argv>...
+ *   landlock-run --probe
+ *
+ * `--ro` grants read+execute beneath the path; `--rw` grants full filesystem
+ * access beneath the path. Everything else is denied (Landlock is an
+ * allow-list). `--probe` builds a maximal ruleset and reports whether the
+ * running kernel actually enforces it — the executor's functional probe.
+ *
+ * Fail-closed: if the ruleset cannot be created or is NOT enforced by the
+ * kernel, the launcher exits non-zero WITHOUT exec'ing the command. A partial
+ * (best-effort) enforcement on an older ABI is accepted and reported on
+ * stderr; the consumer's mode vocabulary keeps its file-effect promises
+ * honest per ABI level (surfaced as `full` vs `partial` by the entry
+ * package's probe).
+ *
+ * Plain C11 over the raw Landlock UAPI — no libraries beyond libc (musl,
+ * linked statically), so the whole audit surface is this file plus the
+ * kernel's stable syscall contract. Built natively per architecture by
+ * `scripts/build.ts` into the per-platform npm packages
+ * (`node-addon-landlock-run-linux-{x64,arm64}`); the argv grammar,
+ * exit codes, and report lines are pinned in `docs/cli-contract.md`.
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+/*
+ * The Landlock UAPI, defined locally instead of via <linux/landlock.h>: the
+ * kernel's user-space ABI is stable by contract, self-defining it keeps the
+ * build independent of the toolchain's header vintage, and the definitions
+ * double as the audit record of exactly which kernel surface this launcher
+ * touches. Layouts and values are verbatim from the kernel header (the
+ * path-beneath struct is packed there, so it must be packed here).
+ */
+struct landlock_ruleset_attr {
+  uint64_t handled_access_fs;
+};
+
+struct landlock_path_beneath_attr {
+  uint64_t allowed_access;
+  int32_t parent_fd;
+} __attribute__((packed));
+
+#define LANDLOCK_CREATE_RULESET_VERSION (1U << 0)
+#define LANDLOCK_RULE_PATH_BENEATH 1
+
+/* Filesystem access bits, grouped by the Landlock ABI that introduced them. */
+#define LL_FS_EXECUTE     (UINT64_C(1) << 0)  /* ABI 1 */
+#define LL_FS_WRITE_FILE  (UINT64_C(1) << 1)
+#define LL_FS_READ_FILE   (UINT64_C(1) << 2)
+#define LL_FS_READ_DIR    (UINT64_C(1) << 3)
+#define LL_FS_REMOVE_DIR  (UINT64_C(1) << 4)
+#define LL_FS_REMOVE_FILE (UINT64_C(1) << 5)
+#define LL_FS_MAKE_CHAR   (UINT64_C(1) << 6)
+#define LL_FS_MAKE_DIR    (UINT64_C(1) << 7)
+#define LL_FS_MAKE_REG    (UINT64_C(1) << 8)
+#define LL_FS_MAKE_SOCK   (UINT64_C(1) << 9)
+#define LL_FS_MAKE_FIFO   (UINT64_C(1) << 10)
+#define LL_FS_MAKE_BLOCK  (UINT64_C(1) << 11)
+#define LL_FS_MAKE_SYM    (UINT64_C(1) << 12)
+#define LL_FS_REFER       (UINT64_C(1) << 13) /* ABI 2 */
+#define LL_FS_TRUNCATE    (UINT64_C(1) << 14) /* ABI 3 (ABI 4 added TCP bits only) */
+#define LL_FS_IOCTL_DEV   (UINT64_C(1) << 15) /* ABI 5 */
+
+#define LL_ABI1_MASK (LL_FS_REFER - 1) /* bits 0..12: every ABI-1 access, nothing newer */
+
+/*
+ * Newest ABI this build knows; the negotiation below scales the actual
+ * ruleset down to what the running kernel supports (the best-effort compat
+ * stance of the previous Rust launcher, made explicit).
+ */
+#define MAX_ABI 5L
+
+/*
+ * Landlock has no libc wrappers; these are the raw syscalls. The numbers are
+ * identical on every architecture (the post-2011 unified table) — the
+ * fallbacks only matter to a libc older than the feature.
+ */
+#ifndef __NR_landlock_create_ruleset
+#define __NR_landlock_create_ruleset 444
+#define __NR_landlock_add_rule 445
+#define __NR_landlock_restrict_self 446
+#endif
+
+/*
+ * Every fatal launcher error prints `landlock-run: <message>` to stderr
+ * and exits 125 — a code the wrapped command itself is unlikely to use, so
+ * the executor can tell launcher failures from command failures.
+ */
+#define EXIT_LAUNCHER_FAILURE 125
+
+static const char NOT_ENFORCED_MESSAGE[] =
+  "landlock is not enforced by this kernel (ABI unsupported or disabled)";
+
+/* Print one fatal `landlock-run: ...` line; returns the fatal exit code. */
+static int fail(const char *prefix, const char *detail) {
+  if (detail == NULL) {
+    fprintf(stderr, "landlock-run: %s\n", prefix);
+  } else {
+    fprintf(stderr, "landlock-run: %s: %s\n", prefix, detail);
+  }
+  return EXIT_LAUNCHER_FAILURE;
+}
+
+static int fail_usage(const char *message, const char *detail) {
+  fprintf(stderr, "landlock-run: usage error: %s%s\n", message, detail == NULL ? "" : detail);
+  return EXIT_LAUNCHER_FAILURE;
+}
+
+/* Parsed CLI: either a probe, or grants plus the command argv after `--`. */
+struct cli {
+  int probe;
+  const char **ro;
+  size_t ro_count;
+  const char **rw;
+  size_t rw_count;
+  char **command; /* NULL-terminated tail of main's argv */
+};
+
+/*
+ * Hand-rolled argv parsing — four flags do not justify a parsing library,
+ * and the previous Rust launcher made the same call for the same reason.
+ * Returns 0 on success, else the process exit code (message already printed).
+ */
+static int parse(int argc, char **argv, struct cli *cli) {
+  /* argc bounds each grant list; the launcher execs or exits, so no free. */
+  cli->ro = calloc(argc > 0 ? (size_t)argc : 1, sizeof *cli->ro);
+  cli->rw = calloc(argc > 0 ? (size_t)argc : 1, sizeof *cli->rw);
+  if (cli->ro == NULL || cli->rw == NULL) return fail("out of memory", NULL);
+
+  int index = 1;
+  while (index < argc) {
+    const char *arg = argv[index];
+    if (strcmp(arg, "--probe") == 0) {
+      cli->probe = 1;
+      index += 1;
+    } else if (strcmp(arg, "--ro") == 0 || strcmp(arg, "--rw") == 0) {
+      if (index + 1 >= argc) {
+        return fail_usage(arg, " requires a path");
+      }
+      if (strcmp(arg, "--ro") == 0) {
+        cli->ro[cli->ro_count++] = argv[index + 1];
+      } else {
+        cli->rw[cli->rw_count++] = argv[index + 1];
+      }
+      index += 2;
+    } else if (strcmp(arg, "--") == 0) {
+      cli->command = &argv[index + 1];
+      break;
+    } else {
+      return fail_usage("unknown argument: ", arg);
+    }
+  }
+  if (cli->probe) {
+    if (cli->ro_count > 0 || cli->rw_count > 0 || (cli->command != NULL && cli->command[0] != NULL)) {
+      return fail_usage("--probe takes no other arguments", NULL);
+    }
+  } else if (cli->command == NULL || cli->command[0] == NULL) {
+    return fail_usage("missing `-- <argv>...` command", NULL);
+  }
+  return 0;
+}
+
+/* The filesystem accesses the running kernel's ABI can govern. */
+static uint64_t fs_mask_for_abi(long abi) {
+  uint64_t mask = LL_ABI1_MASK;
+  if (abi >= 2) mask |= LL_FS_REFER;
+  if (abi >= 3) mask |= LL_FS_TRUNCATE;
+  if (abi >= 5) mask |= LL_FS_IOCTL_DEV;
+  return mask;
+}
+
+/* Add one path-beneath rule; 0 on success, else the exit code. */
+static int add_rule(int ruleset_fd, const char *path, uint64_t access) {
+  int path_fd = open(path, O_PATH | O_CLOEXEC);
+  if (path_fd < 0) {
+    /* Fail closed on an unopenable grant root: silently narrowing the
+     * granted set would be safe, but running with a profile the caller did
+     * not get is not worth the ambiguity. */
+    fprintf(stderr, "landlock-run: cannot open rule path: %s: %s\n", path, strerror(errno));
+    return EXIT_LAUNCHER_FAILURE;
+  }
+  /* The kernel rejects directory-only accesses on a non-directory rule
+   * (EINVAL), so a file grant keeps only the file-compatible bits — how the
+   * `--rw /dev/null` grant works. Same clamp the Rust crate's
+   * path_beneath_rules helper applied. */
+  struct stat st;
+  if (fstat(path_fd, &st) == 0 && !S_ISDIR(st.st_mode)) {
+    access &= LL_FS_EXECUTE | LL_FS_WRITE_FILE | LL_FS_READ_FILE | LL_FS_TRUNCATE | LL_FS_IOCTL_DEV;
+  }
+  struct landlock_path_beneath_attr attr = { .allowed_access = access, .parent_fd = path_fd };
+  if (syscall(__NR_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &attr, 0) != 0) {
+    int saved = errno;
+    close(path_fd);
+    return fail("landlock ruleset error", strerror(saved));
+  }
+  close(path_fd);
+  return 0;
+}
+
+/*
+ * Install the ruleset on the current thread, negotiating the kernel's ABI
+ * down from MAX_ABI. `--ro` paths get the read side of the vocabulary (read
+ * file/dir + execute — the wrapped `bash` and everything it spawns must
+ * remain executable); `--rw` paths get every filesystem access the
+ * negotiated ABI can grant. Sets `no_new_privs` first (mandatory for an
+ * unprivileged restrict, and it neutralizes setuid/setgid escalation inside
+ * the sandbox). On success `*partial` reports whether the kernel governs
+ * only a subset of MAX_ABI's accesses. Returns 0, else the exit code.
+ */
+static int restrict_self(const struct cli *cli, int *partial) {
+  long abi = syscall(__NR_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
+  if (abi < 0) {
+    /* ENOSYS: kernel built without Landlock; EOPNOTSUPP: built but disabled.
+     * Either way: not enforceable — fail CLOSED, never exec unconfined. */
+    return fail(NOT_ENFORCED_MESSAGE, NULL);
+  }
+  *partial = abi < MAX_ABI;
+  uint64_t handled = fs_mask_for_abi(abi < MAX_ABI ? abi : MAX_ABI);
+
+  struct landlock_ruleset_attr attr = { .handled_access_fs = handled };
+  int ruleset_fd = (int)syscall(__NR_landlock_create_ruleset, &attr, sizeof attr, 0);
+  if (ruleset_fd < 0) return fail("landlock ruleset error", strerror(errno));
+
+  const uint64_t read_side = LL_FS_EXECUTE | LL_FS_READ_FILE | LL_FS_READ_DIR;
+  for (size_t i = 0; i < cli->ro_count; i++) {
+    int code = add_rule(ruleset_fd, cli->ro[i], read_side & handled);
+    if (code != 0) return code;
+  }
+  for (size_t i = 0; i < cli->rw_count; i++) {
+    int code = add_rule(ruleset_fd, cli->rw[i], handled);
+    if (code != 0) return code;
+  }
+
+  if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
+    return fail("landlock ruleset error", strerror(errno));
+  }
+  if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) != 0) {
+    return fail("landlock ruleset error", strerror(errno));
+  }
+  close(ruleset_fd);
+  return 0;
+}
+
+int main(int argc, char **argv) {
+  struct cli cli = { 0 };
+  int code = parse(argc, argv, &cli);
+  if (code != 0) return code;
+
+  if (cli.probe) {
+    /* The functional probe: build and enforce a maximal ruleset in THIS
+     * short-lived process (the probe run exits right after). `--version`
+     * style checks would miss a kernel that has the syscalls but refuses
+     * enforcement; actually restricting is the only honest signal. The one
+     * report line is part of the launcher CLI contract — the executor reads
+     * enforcement completeness from it. */
+    static const char *probe_root = "/";
+    struct cli probe = { .ro = &probe_root, .ro_count = 1 };
+    int partial = 0;
+    code = restrict_self(&probe, &partial);
+    if (code != 0) return code;
+    printf("landlock: %s\n", partial ? "partially enforced (older ABI)" : "fully enforced");
+    return 0;
+  }
+
+  int partial = 0;
+  code = restrict_self(&cli, &partial);
+  if (code != 0) return code;
+  if (partial) {
+    /* Older ABI: some handled accesses are not governed (e.g. truncate
+     * before ABI 3). Still confined for everything the kernel supports —
+     * report, do not refuse. */
+    fprintf(stderr, "landlock-run: partial enforcement (older Landlock ABI)\n");
+  }
+
+  execvp(cli.command[0], cli.command);
+  /* exec only returns on failure. */
+  return fail("exec failed", strerror(errno));
+}

+ 11 - 0
native/landlock-run/packages/entry/tsconfig.json

@@ -0,0 +1,11 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "composite": true,
+    "declaration": true,
+    "outDir": "lib",
+    "rootDir": "src",
+    "tsBuildInfoFile": "lib/.tsbuildinfo"
+  },
+  "include": ["src/**/*.ts"]
+}

+ 28 - 0
native/landlock-run/packages/linux-arm64/LICENSE

@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, node-addon-landlock-run contributors
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 7 - 0
native/landlock-run/packages/linux-arm64/README.md

@@ -0,0 +1,7 @@
+# node-addon-landlock-run-linux-arm64
+
+Prebuilt `bin/landlock-run` Landlock launcher for linux-arm64 — a static musl binary compiled natively (no cross toolchain) from the C source shipped in [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run). npm's `os`/`cpu` fields select this package at install time; the entry package resolves it to a file path — it ships no JavaScript and is never imported.
+
+The binary is git-ignored and rides the npm tarball via the `files` list; the `prepack` gate refuses to pack when it is missing or has the wrong ELF architecture, and the release pipeline byte-pins the packed binary against the CI build it came from. Static musl linking means one binary for glibc and musl distros alike — hence no libc suffix in the name.
+
+Sibling: `node-addon-landlock-run-linux-x64`.

+ 26 - 0
native/landlock-run/packages/linux-arm64/package.json

@@ -0,0 +1,26 @@
+{
+  "name": "node-addon-landlock-run-linux-arm64",
+  "version": "0.0.1",
+  "description": "Prebuilt landlock-run Landlock launcher binary for linux-arm64 (static musl) — resolved as a file path by node-addon-landlock-run, never imported",
+  "os": [
+    "linux"
+  ],
+  "cpu": [
+    "arm64"
+  ],
+  "files": [
+    "README.md",
+    "bin/",
+    "prebuilds.json"
+  ],
+  "scripts": {
+    "prepack": "node ../../scripts/verify-launcher-binary.mjs"
+  },
+  "engines": {
+    "node": ">=20"
+  },
+  "license": "BSD-3-Clause",
+  "publishConfig": {
+    "access": "public"
+  }
+}

+ 10 - 0
native/landlock-run/packages/linux-arm64/prebuilds.json

@@ -0,0 +1,10 @@
+{
+  "platform": "linux-arm64",
+  "binaries": [
+    {
+      "tool": "landlock-run",
+      "kind": "static-musl",
+      "path": "bin/landlock-run"
+    }
+  ]
+}

+ 28 - 0
native/landlock-run/packages/linux-x64/LICENSE

@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, node-addon-landlock-run contributors
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 7 - 0
native/landlock-run/packages/linux-x64/README.md

@@ -0,0 +1,7 @@
+# node-addon-landlock-run-linux-x64
+
+Prebuilt `bin/landlock-run` Landlock launcher for linux-x64 — a static musl binary compiled natively (no cross toolchain) from the C source shipped in [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run). npm's `os`/`cpu` fields select this package at install time; the entry package resolves it to a file path — it ships no JavaScript and is never imported.
+
+The binary is git-ignored and rides the npm tarball via the `files` list; the `prepack` gate refuses to pack when it is missing or has the wrong ELF architecture, and the release pipeline byte-pins the packed binary against the CI build it came from. Static musl linking means one binary for glibc and musl distros alike — hence no libc suffix in the name.
+
+Sibling: `node-addon-landlock-run-linux-arm64`.

+ 26 - 0
native/landlock-run/packages/linux-x64/package.json

@@ -0,0 +1,26 @@
+{
+  "name": "node-addon-landlock-run-linux-x64",
+  "version": "0.0.1",
+  "description": "Prebuilt landlock-run Landlock launcher binary for linux-x64 (static musl) — resolved as a file path by node-addon-landlock-run, never imported",
+  "os": [
+    "linux"
+  ],
+  "cpu": [
+    "x64"
+  ],
+  "files": [
+    "README.md",
+    "bin/",
+    "prebuilds.json"
+  ],
+  "scripts": {
+    "prepack": "node ../../scripts/verify-launcher-binary.mjs"
+  },
+  "engines": {
+    "node": ">=20"
+  },
+  "license": "BSD-3-Clause",
+  "publishConfig": {
+    "access": "public"
+  }
+}

+ 10 - 0
native/landlock-run/packages/linux-x64/prebuilds.json

@@ -0,0 +1,10 @@
+{
+  "platform": "linux-x64",
+  "binaries": [
+    {
+      "tool": "landlock-run",
+      "kind": "static-musl",
+      "path": "bin/landlock-run"
+    }
+  ]
+}

+ 345 - 0
native/landlock-run/pnpm-lock.yaml

@@ -0,0 +1,345 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    devDependencies:
+      '@types/node':
+        specifier: ^24.10.0
+        version: 24.13.2
+      node-addon-landlock-run:
+        specifier: workspace:*
+        version: link:packages/entry
+      tsx:
+        specifier: ^4.20.6
+        version: 4.23.0
+      typescript:
+        specifier: ^5.9.3
+        version: 5.9.3
+
+  packages/entry:
+    optionalDependencies:
+      node-addon-landlock-run-linux-arm64:
+        specifier: workspace:*
+        version: link:../linux-arm64
+      node-addon-landlock-run-linux-x64:
+        specifier: workspace:*
+        version: link:../linux-x64
+
+  packages/linux-arm64: {}
+
+  packages/linux-x64: {}
+
+packages:
+
+  '@esbuild/aix-ppc64@0.28.1':
+    resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/android-arm64@0.28.1':
+    resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm@0.28.1':
+    resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-x64@0.28.1':
+    resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/darwin-arm64@0.28.1':
+    resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.28.1':
+    resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/freebsd-arm64@0.28.1':
+    resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.28.1':
+    resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/linux-arm64@0.28.1':
+    resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.28.1':
+    resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.28.1':
+    resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.28.1':
+    resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.28.1':
+    resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.28.1':
+    resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.28.1':
+    resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.28.1':
+    resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.28.1':
+    resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/netbsd-arm64@0.28.1':
+    resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.28.1':
+    resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-arm64@0.28.1':
+    resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.28.1':
+    resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openharmony-arm64@0.28.1':
+    resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@esbuild/sunos-x64@0.28.1':
+    resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/win32-arm64@0.28.1':
+    resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.28.1':
+    resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.28.1':
+    resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
+  '@types/node@24.13.2':
+    resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}
+
+  esbuild@0.28.1:
+    resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  fsevents@2.3.3:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
+  tsx@4.23.0:
+    resolution: {integrity: sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==}
+    engines: {node: '>=18.0.0'}
+    hasBin: true
+
+  typescript@5.9.3:
+    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  undici-types@7.18.2:
+    resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+
+snapshots:
+
+  '@esbuild/aix-ppc64@0.28.1':
+    optional: true
+
+  '@esbuild/android-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/android-arm@0.28.1':
+    optional: true
+
+  '@esbuild/android-x64@0.28.1':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/darwin-x64@0.28.1':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-arm@0.28.1':
+    optional: true
+
+  '@esbuild/linux-ia32@0.28.1':
+    optional: true
+
+  '@esbuild/linux-loong64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.28.1':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-s390x@0.28.1':
+    optional: true
+
+  '@esbuild/linux-x64@0.28.1':
+    optional: true
+
+  '@esbuild/netbsd-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.28.1':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.28.1':
+    optional: true
+
+  '@esbuild/openharmony-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/sunos-x64@0.28.1':
+    optional: true
+
+  '@esbuild/win32-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/win32-ia32@0.28.1':
+    optional: true
+
+  '@esbuild/win32-x64@0.28.1':
+    optional: true
+
+  '@types/node@24.13.2':
+    dependencies:
+      undici-types: 7.18.2
+
+  esbuild@0.28.1:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.28.1
+      '@esbuild/android-arm': 0.28.1
+      '@esbuild/android-arm64': 0.28.1
+      '@esbuild/android-x64': 0.28.1
+      '@esbuild/darwin-arm64': 0.28.1
+      '@esbuild/darwin-x64': 0.28.1
+      '@esbuild/freebsd-arm64': 0.28.1
+      '@esbuild/freebsd-x64': 0.28.1
+      '@esbuild/linux-arm': 0.28.1
+      '@esbuild/linux-arm64': 0.28.1
+      '@esbuild/linux-ia32': 0.28.1
+      '@esbuild/linux-loong64': 0.28.1
+      '@esbuild/linux-mips64el': 0.28.1
+      '@esbuild/linux-ppc64': 0.28.1
+      '@esbuild/linux-riscv64': 0.28.1
+      '@esbuild/linux-s390x': 0.28.1
+      '@esbuild/linux-x64': 0.28.1
+      '@esbuild/netbsd-arm64': 0.28.1
+      '@esbuild/netbsd-x64': 0.28.1
+      '@esbuild/openbsd-arm64': 0.28.1
+      '@esbuild/openbsd-x64': 0.28.1
+      '@esbuild/openharmony-arm64': 0.28.1
+      '@esbuild/sunos-x64': 0.28.1
+      '@esbuild/win32-arm64': 0.28.1
+      '@esbuild/win32-ia32': 0.28.1
+      '@esbuild/win32-x64': 0.28.1
+
+  fsevents@2.3.3:
+    optional: true
+
+  tsx@4.23.0:
+    dependencies:
+      esbuild: 0.28.1
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  typescript@5.9.3: {}
+
+  undici-types@7.18.2: {}

+ 8 - 0
native/landlock-run/pnpm-workspace.yaml

@@ -0,0 +1,8 @@
+packages:
+  - packages/*
+
+# pnpm 10+ blocks any dependency shipping an install/build script until it is
+# explicitly reviewed here. Deny by default; esbuild (tsx's bundled native
+# binary) genuinely needs its script.
+allowBuilds:
+  esbuild: true

+ 51 - 0
native/landlock-run/scripts/assemble-prebuilds.mjs

@@ -0,0 +1,51 @@
+#!/usr/bin/env node
+/**
+ * Assemble downloaded release artifacts into the platform packages and
+ * verify the result. The Release workflow's build legs upload one
+ * `prebuild-<package>` artifact per platform package (its `bin/` payload);
+ * this script copies each into `packages/<package>/bin/` and then checks
+ * every declared binary for presence and ELF architecture.
+ *
+ * Usage: `node scripts/assemble-prebuilds.mjs <artifact-root>`.
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { platformDirs, root, verifyPlatformBinaries } from './repo.mjs';
+
+const artifactRoot = path.resolve(process.argv[2] || '.release/prebuild-artifacts');
+
+if (!fs.existsSync(artifactRoot)) {
+  throw new Error(`prebuild artifact directory does not exist: ${artifactRoot}`);
+}
+
+const platforms = platformDirs().map((dir) => path.basename(dir));
+
+for (const name of platforms) {
+  const binDir = path.join(root, 'packages', name, 'bin');
+  fs.rmSync(binDir, { recursive: true, force: true });
+  fs.mkdirSync(binDir, { recursive: true });
+}
+
+for (const artifactName of fs.readdirSync(artifactRoot)) {
+  const artifactDir = path.join(artifactRoot, artifactName);
+  if (!fs.statSync(artifactDir).isDirectory()) continue;
+
+  const name = platforms.find((candidate) => artifactName === `prebuild-${candidate}`);
+  if (!name) {
+    throw new Error(`cannot map artifact to a platform package: ${artifactName}`);
+  }
+
+  for (const file of fs.readdirSync(artifactDir)) {
+    const source = path.join(artifactDir, file);
+    const destination = path.join(root, 'packages', name, 'bin', file);
+    fs.copyFileSync(source, destination);
+    fs.chmodSync(destination, 0o755);
+    console.log(`Copied ${path.relative(root, source)} -> ${path.relative(root, destination)}`);
+  }
+}
+
+for (const dir of platformDirs()) {
+  const { name, count } = verifyPlatformBinaries(path.join(root, dir));
+  console.log(`Verified ${name}: ${count} binaries`);
+}

+ 86 - 0
native/landlock-run/scripts/build.ts

@@ -0,0 +1,86 @@
+/**
+ * Build every native tool this host can build, into its per-platform
+ * package.
+ *
+ * Targets are derived from the checked-in matrix: each
+ * `packages/<name>/prebuilds.json` whose `platform` matches this host names
+ * the binaries to produce; the TOOLS table below maps each `tool` to its C
+ * source. Builds are NATIVE-ONLY — each Linux architecture compiles its own
+ * binary with the distro's `musl-gcc` (static musl: runs on glibc and musl
+ * distros alike, no loader or libc expectations on the consumer host), and
+ * CI's per-arch runners are the builders of record. No cross toolchain
+ * exists here on purpose: native runners replace it, and the audit surface
+ * is the reviewed C source plus CI provenance.
+ *
+ * Binaries land in `packages/<name>/bin/` — git-ignored (root
+ * `.gitignore`), packed into the platform package's npm tarball behind its
+ * `prepack` gate (`scripts/verify-launcher-binary.mjs`).
+ *
+ * Run: `pnpm run build:native` (Linux with musl-gcc on PATH:
+ * `apt-get install musl-tools`). Non-Linux hosts fail fast — no platform
+ * package exists for them to build.
+ */
+import { spawnSync } from 'node:child_process'
+import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs'
+import { basename, dirname, join, resolve } from 'node:path'
+
+/** Each native tool's C source, keyed by the `tool` field in prebuilds.json. */
+const TOOLS: Record<string, { source: string }> = {
+  'landlock-run': { source: 'packages/entry/src/main.c' },
+}
+
+const repoRoot = resolve(import.meta.dirname, '..')
+
+if (process.platform !== 'linux') {
+  console.error(`build: native tools are built natively per Linux architecture (no cross toolchain) — nothing to build on ${process.platform}. CI's per-arch runners build and rehearse every platform package.`)
+  process.exit(1)
+}
+const hostPlatform = `linux-${process.arch}`
+
+/** This host's platform packages, from the checked-in matrix. */
+const targets: { packageDir: string; tool: string; binaryPath: string; kind: string }[] = []
+const packagesRoot = join(repoRoot, 'packages')
+for (const name of readdirSync(packagesRoot).sort()) {
+  const prebuildsFile = join(packagesRoot, name, 'prebuilds.json')
+  if (!existsSync(prebuildsFile)) continue
+  const prebuilds = JSON.parse(readFileSync(prebuildsFile, 'utf8')) as {
+    platform: string
+    binaries: { tool: string; kind: string; path: string }[]
+  }
+  if (prebuilds.platform !== hostPlatform) continue
+  for (const binary of prebuilds.binaries) {
+    targets.push({ packageDir: join(packagesRoot, name), tool: binary.tool, binaryPath: binary.path, kind: binary.kind })
+  }
+}
+if (targets.length === 0) {
+  console.error(`build: no platform package declares binaries for ${hostPlatform} — supported platforms are the packages/*/prebuilds.json "platform" values.`)
+  process.exit(1)
+}
+
+for (const target of targets) {
+  const tool = TOOLS[target.tool]
+  if (tool === undefined) {
+    console.error(`build: prebuilds.json names unknown tool "${target.tool}" — add it to the TOOLS table in scripts/build.ts.`)
+    process.exit(1)
+  }
+  if (target.kind !== 'static-musl') {
+    console.error(`build: unknown binary kind "${target.kind}" — the only toolchain here is static musl.`)
+    process.exit(1)
+  }
+  const binary = join(target.packageDir, target.binaryPath)
+  mkdirSync(dirname(binary), { recursive: true })
+
+  // -static against musl: self-contained, no loader/libc expectations on the
+  // consumer host. -Werror is safe to keep hard: CI pins the builder images,
+  // and a new warning on a toolchain bump deserves a look, not a pass.
+  const result = spawnSync('musl-gcc', [
+    '-std=c11', '-Os', '-Wall', '-Wextra', '-Werror', '-static', '-s',
+    '-o', binary, join(repoRoot, tool.source),
+  ], { stdio: ['ignore', 'inherit', 'inherit'] })
+  if (result.error !== undefined || result.status !== 0) {
+    console.error('build: musl-gcc failed' +
+      (result.error ? ` (${result.error.message} — is musl-tools installed?)` : ''))
+    process.exit(1)
+  }
+  console.log(`build: built ${basename(target.packageDir)}/${target.binaryPath}`)
+}

+ 90 - 0
native/landlock-run/scripts/bump-release.mjs

@@ -0,0 +1,90 @@
+#!/usr/bin/env node
+/**
+ * Bump every package (workspace root + packages/*) to one version, refresh
+ * the lockfile, and verify. Usage: `pnpm release:bump <major|minor|patch|x.y.z>`.
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { spawnSync } from 'node:child_process';
+import { packageDirs, readJson, root } from './repo.mjs';
+
+const bump = process.argv[2];
+const releaseTypes = new Set(['major', 'minor', 'patch']);
+
+function writeJson(file, value) {
+  fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
+}
+
+function run(command, args) {
+  const result = spawnSync(command, args, {
+    cwd: root,
+    stdio: 'inherit',
+    env: { ...process.env, CI: 'true' },
+  });
+  if (result.error) throw result.error;
+  if (result.status !== 0) {
+    process.exit(result.status ?? 1);
+  }
+}
+
+function packageFiles() {
+  return ['package.json', ...packageDirs().map((dir) => path.join(dir, 'package.json'))];
+}
+
+function parseVersion(version) {
+  const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
+  if (!match) {
+    throw new Error(`increment types need a plain x.y.z current version (current: ${version}) — pass an explicit target version instead`);
+  }
+  return match.slice(1).map((part) => Number(part));
+}
+
+/** Explicit target versions accept full semver, prereleases included (test publishes). */
+const EXPLICIT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
+
+function nextVersion(current, release) {
+  if (EXPLICIT_VERSION.test(release)) return release;
+
+  if (!releaseTypes.has(release)) {
+    throw new Error('Usage: pnpm release:bump <major|minor|patch|x.y.z>');
+  }
+
+  const [major, minor, patch] = parseVersion(current);
+  if (release === 'major') return `${major + 1}.0.0`;
+  if (release === 'minor') return `${major}.${minor + 1}.0`;
+  return `${major}.${minor}.${patch + 1}`;
+}
+
+function currentPublishedVersion(files) {
+  const versions = new Set(
+    files
+      .filter((file) => file.startsWith('packages/'))
+      .map((file) => readJson(path.join(root, file)).version),
+  );
+  if (versions.size !== 1) {
+    throw new Error(`published package versions differ: ${[...versions].join(', ')}`);
+  }
+  return [...versions][0];
+}
+
+if (!bump) {
+  console.error('Usage: pnpm release:bump <major|minor|patch|x.y.z>');
+  process.exit(1);
+}
+
+const files = packageFiles();
+const targetVersion = nextVersion(currentPublishedVersion(files), bump);
+
+for (const file of files) {
+  const fullPath = path.join(root, file);
+  const json = readJson(fullPath);
+  json.version = targetVersion;
+  writeJson(fullPath, json);
+  console.log(`${file}: ${targetVersion}`);
+}
+
+run('pnpm', ['install', '--ignore-scripts', '--lockfile-only']);
+run('node', ['./scripts/verify-release.mjs']);
+
+console.log(`Release version bumped to ${targetVersion}`);

+ 42 - 0
native/landlock-run/scripts/commit-release.mjs

@@ -0,0 +1,42 @@
+#!/usr/bin/env node
+/**
+ * Bump, stage, and commit a release in one command:
+ * `pnpm release:commit <major|minor|patch|x.y.z>`. The tag stays manual —
+ * create it from the merged release commit.
+ */
+
+import path from 'node:path';
+import { spawnSync } from 'node:child_process';
+import { packageDirs, readJson, root } from './repo.mjs';
+
+const bump = process.argv[2];
+
+function run(command, args) {
+  const result = spawnSync(command, args, {
+    cwd: root,
+    stdio: 'inherit',
+    env: { ...process.env, CI: 'true' },
+  });
+  if (result.error) throw result.error;
+  if (result.status !== 0) {
+    process.exit(result.status ?? 1);
+  }
+}
+
+if (!bump) {
+  console.error('Usage: pnpm release:commit <major|minor|patch|x.y.z>');
+  process.exit(1);
+}
+
+run('node', ['./scripts/bump-release.mjs', bump]);
+
+const version = readJson(path.join(root, packageDirs()[0], 'package.json')).version;
+run('git', [
+  'add',
+  'package.json',
+  'packages/*/package.json',
+  'pnpm-lock.yaml',
+]);
+run('git', ['commit', '-m', `release: ${version}`]);
+
+console.log(`Committed release ${version}. Create the tag manually: git tag v${version}`);

+ 66 - 0
native/landlock-run/scripts/github-matrix.mjs

@@ -0,0 +1,66 @@
+#!/usr/bin/env node
+/**
+ * Derive the GitHub Actions matrices from the checked-in package matrix
+ * (`packages/<name>/prebuilds.json`). Single source: adding a platform
+ * package extends CI and Release without editing a workflow.
+ *
+ *   node scripts/github-matrix.mjs ci                → one leg per distinct platform
+ *   node scripts/github-matrix.mjs release-prebuild  → one leg per platform package
+ */
+
+import path from 'node:path';
+import { platformDirs, readJson, root } from './repo.mjs';
+
+/** GitHub runner per prebuilds.json `platform` value — native builders only, no cross toolchain. */
+const RUNNERS = {
+  'linux-x64': 'ubuntu-24.04',
+  'linux-arm64': 'ubuntu-24.04-arm',
+};
+
+function runnerFor(platform) {
+  const runner = RUNNERS[platform];
+  if (!runner) {
+    throw new Error(`missing GitHub runner for platform: ${platform}`);
+  }
+  return runner;
+}
+
+function platformManifests() {
+  return platformDirs().map((dir) => ({
+    dir,
+    name: path.basename(dir),
+    prebuilds: readJson(path.join(root, dir, 'prebuilds.json')),
+  }));
+}
+
+function ciMatrix() {
+  const platforms = [...new Set(platformManifests().map(({ prebuilds }) => prebuilds.platform))].sort();
+  return {
+    include: platforms.map((platform) => ({ platform, runner: runnerFor(platform) })),
+  };
+}
+
+function releasePrebuildMatrix() {
+  return {
+    include: platformManifests().map(({ dir, name, prebuilds }) => ({
+      platform: prebuilds.platform,
+      package: name,
+      dir,
+      runner: runnerFor(prebuilds.platform),
+      artifact: `prebuild-${name}`,
+    })),
+  };
+}
+
+const target = process.argv[2];
+const matrices = {
+  ci: ciMatrix,
+  'release-prebuild': releasePrebuildMatrix,
+};
+
+if (!target || !matrices[target]) {
+  console.error(`Usage: node scripts/github-matrix.mjs <${Object.keys(matrices).join('|')}>`);
+  process.exit(1);
+}
+
+process.stdout.write(JSON.stringify(matrices[target]()));

+ 76 - 0
native/landlock-run/scripts/pack-release.mjs

@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+/**
+ * Pack every published package into release tarballs, in publish order
+ * (platform packages first, then the entries that optionally depend on
+ * them), and write `publish-order.txt` next to them. `pnpm pack` produces
+ * the EXACT bytes `pnpm publish` would upload and runs each package's
+ * `prepack` gate, so a missing binary or unbuilt `lib/` refuses here.
+ *
+ * Usage: `node scripts/pack-release.mjs [dest] [--current-platform-only]`.
+ * The flag packs only THIS host's platform package plus the entries — for
+ * per-architecture CI legs, where the other architecture's binary does not
+ * exist (the exact refusal its prepack gate exists for).
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { spawnSync } from 'node:child_process';
+import { entryDirs, platformDirs, readJson, root } from './repo.mjs';
+
+const args = process.argv.slice(2);
+const currentPlatformOnly = args.includes('--current-platform-only');
+const destination = path.resolve(args.find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm'));
+
+function hostPlatformDirs() {
+  const hostPlatform = `${process.platform}-${process.arch}`;
+  return platformDirs().filter((dir) => readJson(path.join(root, dir, 'prebuilds.json')).platform === hostPlatform);
+}
+
+function run(command, args) {
+  const result = spawnSync(command, args, {
+    cwd: root,
+    stdio: 'inherit',
+  });
+  if (result.error) throw result.error;
+  if (result.status !== 0) {
+    process.exit(result.status ?? 1);
+  }
+}
+
+function tarballName(manifest) {
+  if (manifest.name.startsWith('@')) {
+    return `${manifest.name.slice(1).replace('/', '-')}-${manifest.version}.tgz`;
+  }
+  return `${manifest.name}-${manifest.version}.tgz`;
+}
+
+fs.rmSync(destination, { recursive: true, force: true });
+fs.mkdirSync(destination, { recursive: true });
+
+const dirs = [...(currentPlatformOnly ? hostPlatformDirs() : platformDirs()), ...entryDirs()];
+const platformSet = new Set(platformDirs());
+const publishOrder = [];
+for (const dir of dirs) {
+  const manifest = readJson(path.join(root, dir, 'package.json'));
+  // Platform packages are packed with npm: pnpm pack (observed on 11.7.0)
+  // normalizes file modes and STRIPS the executable bit, which ships a
+  // launcher no consumer can spawn; npm pack preserves it. Platform packages
+  // have no dependencies by construction, so they need none of pnpm's
+  // workspace-protocol conversion — the entry packages do, and carry no
+  // executables, so they keep pnpm pack.
+  if (platformSet.has(dir)) {
+    run('npm', ['pack', `./${dir}`, '--pack-destination', destination]);
+  } else {
+    run('pnpm', ['--dir', dir, 'pack', '--pack-destination', destination]);
+  }
+
+  const tarball = tarballName(manifest);
+  const tarballPath = path.join(destination, tarball);
+  if (!fs.existsSync(tarballPath)) {
+    throw new Error(`expected pack output not found: ${tarballPath}`);
+  }
+  publishOrder.push(tarball);
+}
+
+fs.writeFileSync(path.join(destination, 'publish-order.txt'), `${publishOrder.join('\n')}\n`);
+console.log(`Packed ${publishOrder.length} packages into ${path.relative(root, destination)}`);

+ 88 - 0
native/landlock-run/scripts/repo.mjs

@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+/**
+ * Shared helpers for the repo scripts: package discovery, the checked-in
+ * prebuild matrix, and binary verification. The package matrix is explicit
+ * metadata — `packages/<name>/prebuilds.json` marks a platform package and
+ * declares its binaries; everything else under `packages/` is an entry
+ * package. Scripts derive from these files and never guess.
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+export const root = fileURLToPath(new URL('..', import.meta.url));
+export const packagesRoot = path.join(root, 'packages');
+
+/** ELF `e_machine` (offset 18, little-endian) per platform-package `cpu` value. */
+export const E_MACHINE = { x64: 62, arm64: 183 };
+
+export function readJson(file) {
+  return JSON.parse(fs.readFileSync(file, 'utf8'));
+}
+
+/** Platform packages: every `packages/<name>` carrying a `prebuilds.json`. */
+export function platformDirs() {
+  return fs.readdirSync(packagesRoot)
+    .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json')))
+    .sort()
+    .map((name) => path.join('packages', name));
+}
+
+/** Entry packages: every other `packages/<name>` with a `package.json`. */
+export function entryDirs() {
+  return fs.readdirSync(packagesRoot)
+    .filter((name) => !fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json')))
+    .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'package.json')))
+    .sort()
+    .map((name) => path.join('packages', name));
+}
+
+/** All published packages in publish order: platform packages before the entries that optionally depend on them. */
+export function packageDirs() {
+  return [...platformDirs(), ...entryDirs()];
+}
+
+/**
+ * Verify one platform package's binaries against its `prebuilds.json`:
+ * every declared binary exists, nothing undeclared sits in `bin/`, and each
+ * file's ELF `e_machine` matches the package's declared `cpu`. Throws with
+ * a remediation message on the first mismatch.
+ */
+export function verifyPlatformBinaries(packageDir) {
+  const manifest = readJson(path.join(packageDir, 'package.json'));
+  const prebuilds = readJson(path.join(packageDir, 'prebuilds.json'));
+  const cpu = manifest.cpu?.[0];
+  if (cpu === undefined || !(cpu in E_MACHINE)) {
+    throw new Error(`${manifest.name}: unsupported or missing "cpu" in package.json (expected one of: ${Object.keys(E_MACHINE).join(', ')})`);
+  }
+
+  for (const binary of prebuilds.binaries) {
+    const file = path.join(packageDir, binary.path);
+    if (!fs.existsSync(file)) {
+      throw new Error(`${manifest.name}: missing ${binary.path} — run \`pnpm build:native\` on a ${prebuilds.platform} host (or assemble release artifacts) before packing.`);
+    }
+    try {
+      fs.accessSync(file, fs.constants.X_OK);
+    } catch {
+      // Only reachable when the mode was mangled somewhere between build and
+      // here (e.g. an archive step that normalized permissions) — the build
+      // itself always produces 755.
+      throw new Error(`${manifest.name}: ${binary.path} is not executable — a pack/extract step stripped the mode bit.`);
+    }
+    const machine = fs.readFileSync(file).readUInt16LE(18);
+    if (machine !== E_MACHINE[cpu]) {
+      throw new Error(`${manifest.name}: ${binary.path} has ELF e_machine ${machine}, expected ${E_MACHINE[cpu]} for ${cpu} — the binary was built for a different architecture.`);
+    }
+  }
+
+  const declared = prebuilds.binaries.map((binary) => path.basename(binary.path)).sort();
+  const binDir = path.join(packageDir, 'bin');
+  const actual = fs.existsSync(binDir) ? fs.readdirSync(binDir).sort() : [];
+  const extra = actual.filter((name) => !declared.includes(name));
+  if (extra.length) {
+    throw new Error(`${manifest.name}: bin/ contains files not declared in prebuilds.json: ${extra.join(', ')}`);
+  }
+
+  return { name: manifest.name, count: prebuilds.binaries.length };
+}

+ 25 - 0
native/landlock-run/scripts/verify-entry-lib.mjs

@@ -0,0 +1,25 @@
+#!/usr/bin/env node
+/**
+ * Prepack gate for entry packages: refuse to pack a tarball whose built
+ * `lib/` is missing. Entry `files` lists use globs, and a glob matching
+ * nothing packs a silently JS-less tarball instead of failing — this gate
+ * turns that into a loud refusal on a checkout that never ran
+ * `pnpm build:ts`.
+ *
+ * Runs from each entry package's `prepack` hook (pnpm sets the script cwd
+ * to the package directory).
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+
+const packageDir = process.cwd();
+const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
+
+for (const file of ['lib/index.js', 'lib/index.d.ts']) {
+  if (!fs.existsSync(path.join(packageDir, file))) {
+    console.error(`verify-entry-lib: ${manifest.name} has no ${file} — run \`pnpm build:ts\` before packing.`);
+    process.exit(1);
+  }
+}
+console.log(`verify-entry-lib: ${manifest.name} built lib/ present.`);

+ 31 - 0
native/landlock-run/scripts/verify-launcher-binary.mjs

@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+/**
+ * Prepack gate for platform packages: refuse to pack a tarball whose
+ * declared binaries are missing or built for the wrong architecture.
+ *
+ * Without it, `pnpm pack` on a checkout that never ran
+ * `pnpm run build:native` would ship an EMPTY platform package — the
+ * binary's absence surfacing only at runtime as a failed probe on every
+ * consumer — and a binary copied across packages would advertise an
+ * architecture it cannot execute. The check is presence + ELF `e_machine`
+ * against the package's declared `cpu`; byte provenance is
+ * `verify-packed-install.mjs`'s concern (it pins the installed tarball
+ * against the workspace build).
+ *
+ * Runs from each platform package's `prepack` hook (pnpm sets the script
+ * cwd to the package directory). Also callable directly with an explicit
+ * package directory: `node scripts/verify-launcher-binary.mjs packages/<name>`.
+ */
+
+import path from 'node:path';
+import { root, verifyPlatformBinaries } from './repo.mjs';
+
+const packageDir = process.argv[2] ? path.resolve(root, process.argv[2]) : process.cwd();
+
+try {
+  const { name, count } = verifyPlatformBinaries(packageDir);
+  console.log(`verify-launcher-binary: ${name} — ${count} binaries present with the right ELF architecture.`);
+} catch (error) {
+  console.error(`verify-launcher-binary: ${error instanceof Error ? error.message : error}`);
+  process.exit(1);
+}

+ 223 - 0
native/landlock-run/scripts/verify-packed-install.mjs

@@ -0,0 +1,223 @@
+#!/usr/bin/env node
+/**
+ * Publish-path rehearsal without publishing: verify the packed tarballs are
+ * exactly what a consumer install needs. `pnpm pack` already produced the
+ * bytes `pnpm publish` would upload; this script checks the payload
+ * (coverage, concrete dependency versions, NO lifecycle install scripts —
+ * this family has no install fallback on purpose), unpacks the entry plus
+ * THIS host's platform tarball into a throwaway consumer OUTSIDE the repo,
+ * byte-pins the installed binary against the workspace build it was packed
+ * from, and drives the INSTALLED entry under plain `node` — resolution,
+ * probe, and a real confinement world-proof through the installed launcher.
+ *
+ * On non-Linux hosts (no platform package exists) it instead proves the
+ * documented degradation: resolution falls back to a nonexistent path and
+ * the probe reports `unusable`.
+ *
+ * Usage: `node scripts/verify-packed-install.mjs [tarball-dir] [--current-platform-only]`.
+ * The flag skips the all-platforms tarball-presence check for
+ * per-architecture CI legs. `NALR_REQUIRE_LANDLOCK=1` makes an unenforcing
+ * kernel a failure instead of a skipped world-proof (set on CI, where the
+ * kernel is known).
+ */
+
+import crypto from 'node:crypto';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { spawnSync } from 'node:child_process';
+import { entryDirs, packageDirs, platformDirs, readJson, root } from './repo.mjs';
+
+const args = process.argv.slice(2);
+const currentPlatformOnly = args.includes('--current-platform-only');
+const tarballDir = path.resolve(args.find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm'));
+const entryPackageName = 'node-addon-landlock-run';
+
+function tarballName(manifest) {
+  if (manifest.name.startsWith('@')) {
+    return `${manifest.name.slice(1).replace('/', '-')}-${manifest.version}.tgz`;
+  }
+  return `${manifest.name}-${manifest.version}.tgz`;
+}
+
+function tarballPath(manifest) {
+  const tarball = path.join(tarballDir, tarballName(manifest));
+  if (!fs.existsSync(tarball)) {
+    throw new Error(`missing packed tarball: ${tarball}`);
+  }
+  return tarball;
+}
+
+function run(command, commandArgs, options = {}) {
+  const result = spawnSync(command, commandArgs, {
+    cwd: options.cwd || root,
+    stdio: 'inherit',
+    env: { ...process.env, ...options.env },
+  });
+  if (result.error) throw result.error;
+  if (result.status !== 0) {
+    process.exit(result.status ?? 1);
+  }
+}
+
+function runCapture(command, commandArgs) {
+  const result = spawnSync(command, commandArgs, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
+  if (result.error) throw result.error;
+  if (result.status !== 0) {
+    process.stderr.write(result.stderr);
+    process.exit(result.status ?? 1);
+  }
+  return result.stdout;
+}
+
+function readPackedManifest(manifest) {
+  return JSON.parse(runCapture('tar', ['-xOf', tarballPath(manifest), 'package/package.json']));
+}
+
+function verifyPackedManifest(packed) {
+  const lifecycle = ['preinstall', 'install', 'postinstall', 'prepare'];
+  for (const script of lifecycle) {
+    if (packed.scripts?.[script]) {
+      throw new Error(`${packed.name}: packed manifest carries a "${script}" lifecycle script — this family has no install fallback`);
+    }
+  }
+  for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
+    for (const [name, version] of Object.entries(packed[field] ?? {})) {
+      if (version.includes('workspace:')) {
+        throw new Error(`${packed.name}: packed ${field} still uses the workspace protocol: ${name}@${version}`);
+      }
+    }
+  }
+}
+
+function sha256(file) {
+  return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
+}
+
+function packageInstallDir(packageName) {
+  return path.join(tempRoot, 'node_modules', ...packageName.split('/'));
+}
+
+function unpackTarball(manifest) {
+  const extractRoot = fs.mkdtempSync(path.join(tempRoot, 'extract-'));
+  run('tar', ['-xzf', tarballPath(manifest), '-C', extractRoot]);
+
+  const source = path.join(extractRoot, 'package');
+  const destination = packageInstallDir(manifest.name);
+  fs.rmSync(destination, { recursive: true, force: true });
+  fs.mkdirSync(path.dirname(destination), { recursive: true });
+  fs.renameSync(source, destination);
+  fs.rmSync(extractRoot, { recursive: true, force: true });
+  console.log(`Unpacked ${manifest.name} -> ${path.relative(tempRoot, destination)}`);
+}
+
+const manifests = packageDirs().map((dir) => ({ dir, manifest: readJson(path.join(root, dir, 'package.json')) }));
+const entryManifest = manifests.find(({ manifest }) => manifest.name === entryPackageName)?.manifest;
+if (!entryManifest) throw new Error(`missing source manifest for ${entryPackageName}`);
+
+const hostPlatform = `${process.platform}-${process.arch}`;
+const currentPlatformEntry = manifests.find(
+  ({ dir, manifest }) => platformDirs().includes(dir) && manifest.name === `${entryPackageName}-${hostPlatform}`,
+);
+
+// Payload checks: every expected tarball exists (full mode), the packed
+// entry's optional-dependency set names exactly the platform packages, and
+// no packed manifest carries workspace versions or install lifecycle.
+const expectedTarballs = currentPlatformOnly
+  ? manifests.filter(({ dir }) => entryDirs().includes(dir) || dir === currentPlatformEntry?.dir)
+  : manifests;
+for (const { manifest } of expectedTarballs) {
+  tarballPath(manifest);
+}
+
+const packedEntry = readPackedManifest(entryManifest);
+const platformPackageNames = manifests
+  .filter(({ dir }) => platformDirs().includes(dir))
+  .map(({ manifest }) => manifest.name)
+  .sort();
+const optionalNames = Object.keys(packedEntry.optionalDependencies || {}).sort();
+if (optionalNames.join('\n') !== platformPackageNames.join('\n')) {
+  throw new Error(`packed entry optionalDependencies mismatch\nactual:\n${optionalNames.join('\n')}\nexpected:\n${platformPackageNames.join('\n')}`);
+}
+for (const { manifest } of expectedTarballs) {
+  verifyPackedManifest(readPackedManifest(manifest));
+}
+
+// Throwaway ESM consumer, built from local tarballs only — no registry.
+const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-packed-install-'));
+fs.writeFileSync(
+  path.join(tempRoot, 'package.json'),
+  `${JSON.stringify({ name: 'nalr-packed-install-check', version: '0.0.0', private: true, type: 'module' }, null, 2)}\n`,
+);
+console.log(`Verifying packed install in ${tempRoot}`);
+
+unpackTarball(entryManifest);
+if (currentPlatformEntry) {
+  unpackTarball(currentPlatformEntry.manifest);
+
+  // Byte-pin: the installed binary must be the workspace build it was packed
+  // from — any divergence means the tarball did not carry the built bytes.
+  const prebuilds = readJson(path.join(root, currentPlatformEntry.dir, 'prebuilds.json'));
+  for (const binary of prebuilds.binaries) {
+    const workspaceFile = path.join(root, currentPlatformEntry.dir, binary.path);
+    const installedFile = path.join(packageInstallDir(currentPlatformEntry.manifest.name), binary.path);
+    if (sha256(workspaceFile) !== sha256(installedFile)) {
+      throw new Error(`installed ${binary.path} differs from the workspace build it was packed from`);
+    }
+    console.log(`Byte-pinned ${binary.path} against the workspace build`);
+  }
+} else if (process.platform === 'linux') {
+  throw new Error(`linux host without a platform package in the matrix: ${hostPlatform}`);
+}
+
+// Drive the INSTALLED entry under plain node: resolution, probe, and (on an
+// enforcing kernel) a real confinement world-proof through the installed
+// launcher.
+const driver = path.join(tempRoot, 'driver.mjs');
+fs.writeFileSync(driver, `
+import assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { grantArgs, launcherPath, probe } from 'node-addon-landlock-run';
+
+const requireLandlock = process.env.NALR_REQUIRE_LANDLOCK === '1';
+const platformPackage = 'node-addon-landlock-run-' + process.platform + '-' + process.arch;
+const resolved = launcherPath();
+assert.ok(path.isAbsolute(resolved), 'launcherPath must be absolute');
+assert.ok(resolved.includes(path.join(...platformPackage.split('/'))), 'launcherPath must point into the platform package: ' + resolved);
+
+if (process.platform === 'linux') {
+  assert.ok(fs.existsSync(resolved), 'installed launcher missing at ' + resolved);
+  try {
+    fs.accessSync(resolved, fs.constants.X_OK);
+  } catch {
+    throw new Error('installed launcher is not executable — the pack path stripped the mode bit: ' + resolved);
+  }
+  const enforcement = probe(resolved);
+  console.log('probe through the installed launcher: ' + enforcement);
+  if (enforcement === 'unusable') {
+    if (requireLandlock) throw new Error('NALR_REQUIRE_LANDLOCK=1 but the probe reports unusable');
+    console.log('kernel does not enforce Landlock — skipping the confinement world-proof');
+  } else {
+    const work = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-confine-'));
+    const denied = path.join(work, 'denied.txt');
+    const deniedRun = spawnSync(resolved, [...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'echo x > ' + denied], { encoding: 'utf8' });
+    assert.notEqual(deniedRun.status, 0, 'write outside the grants must fail');
+    assert.ok(!fs.existsSync(denied), 'denied write must not land on disk');
+    const granted = path.join(work, 'granted.txt');
+    const grantedRun = spawnSync(resolved, [...grantArgs({ readOnly: ['/'], readWrite: [work] }), '--', '/bin/sh', '-c', 'echo ok > ' + granted], { encoding: 'utf8' });
+    assert.equal(grantedRun.status, 0, 'granted write must succeed: ' + grantedRun.stderr);
+    assert.equal(fs.readFileSync(granted, 'utf8').trim(), 'ok');
+    console.log('confinement world-proof passed through the installed launcher');
+  }
+} else {
+  assert.ok(!fs.existsSync(resolved), 'no platform package exists for this host — the fallback path must not exist');
+  assert.equal(probe(resolved), 'unusable');
+  console.log('non-linux host: fallback resolution and unusable probe verified');
+}
+`);
+run(process.execPath, [driver], { cwd: tempRoot });
+
+console.log('Packed install verification passed.');

+ 52 - 0
native/landlock-run/scripts/verify-release.mjs

@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+/**
+ * Release verification. Always: every published package carries one shared
+ * version, and — when running from a tag or publishing — the `vX.Y.Z` tag
+ * matches it. With `--prebuilds`: every platform package's declared
+ * binaries exist with the right ELF architecture (run after
+ * `assemble-prebuilds.mjs` or a local `build:native`).
+ */
+
+import path from 'node:path';
+import { packageDirs, platformDirs, readJson, root, verifyPlatformBinaries } from './repo.mjs';
+
+function verifyVersions() {
+  const packages = packageDirs().map((dir) => ({
+    dir,
+    manifest: readJson(path.join(root, dir, 'package.json')),
+  }));
+  const versions = new Set(packages.map((pkg) => pkg.manifest.version));
+  if (versions.size !== 1) {
+    throw new Error([
+      'published package versions must match:',
+      ...packages.map((pkg) => `${pkg.dir}: ${pkg.manifest.version}`),
+    ].join('\n'));
+  }
+
+  const version = packages[0].manifest.version;
+  const ref = process.env.GITHUB_REF || '';
+  const publish = process.env.RELEASE_PUBLISH === 'true';
+  if (publish && !ref.startsWith('refs/tags/v')) {
+    throw new Error('publishing requires running the workflow from a v* tag');
+  }
+  if (ref.startsWith('refs/tags/v')) {
+    const tagVersion = ref.slice('refs/tags/v'.length);
+    if (tagVersion !== version) {
+      throw new Error(`tag/version mismatch: tag v${tagVersion}, packages ${version}`);
+    }
+  }
+
+  console.log(`Verified release version ${version}`);
+}
+
+function verifyPrebuilds() {
+  for (const dir of platformDirs()) {
+    const { name, count } = verifyPlatformBinaries(path.join(root, dir));
+    console.log(`Verified ${name}: ${count} binaries`);
+  }
+}
+
+verifyVersions();
+if (process.argv.includes('--prebuilds')) {
+  verifyPrebuilds();
+}

+ 76 - 0
native/landlock-run/test/entry.test.js

@@ -0,0 +1,76 @@
+/**
+ * Keyless entry-package tests — run on every host, no kernel or binary
+ * required. Cover the JS seam's pure surface: grant-argv construction, the
+ * resolution contract (platform package → fallback), and probe verdicts over
+ * fake launchers. Requires built `lib/` (`pnpm build:ts`).
+ */
+
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import {
+  LAUNCHER_BIN,
+  LAUNCHER_FAILURE_EXIT,
+  grantArgs,
+  launcherPath,
+  probe,
+} from 'node-addon-landlock-run';
+
+// --- constants are part of the CLI contract ---
+assert.equal(LAUNCHER_BIN, 'landlock-run');
+assert.equal(LAUNCHER_FAILURE_EXIT, 125);
+
+// --- grantArgs: flag spelling, ordering, and empty grants ---
+assert.deepEqual(grantArgs({}), []);
+assert.deepEqual(grantArgs({ readOnly: ['/'] }), ['--ro', '/']);
+assert.deepEqual(
+  grantArgs({ readOnly: ['/', '/opt'], readWrite: ['/tmp/work'] }),
+  ['--ro', '/', '--ro', '/opt', '--rw', '/tmp/work'],
+);
+assert.deepEqual(grantArgs({ readWrite: ['/a'], readOnly: ['/b'] }), ['--ro', '/b', '--rw', '/a']);
+
+// --- launcherPath: resolves the platform package next to its package.json ---
+const platformPackage = `node-addon-landlock-run-${process.platform}-${process.arch}`;
+const resolvedViaSeam = launcherPath((specifier) => {
+  assert.equal(specifier, `${platformPackage}/package.json`);
+  return path.join('/fake-install', specifier);
+});
+assert.equal(resolvedViaSeam, path.join('/fake-install', platformPackage, 'bin', LAUNCHER_BIN));
+
+// --- launcherPath: unresolvable package falls back to an absolute, package-boundary path ---
+const fallback = launcherPath(() => {
+  throw new Error('not installed');
+});
+assert.ok(path.isAbsolute(fallback), 'fallback path must be absolute');
+assert.ok(
+  fallback.includes(path.join('node_modules', ...platformPackage.split('/'), 'bin', LAUNCHER_BIN)),
+  `fallback must point at the platform package layout: ${fallback}`,
+);
+
+// --- launcherPath: default resolution agrees with this workspace's layout ---
+const defaultPath = launcherPath();
+assert.ok(path.isAbsolute(defaultPath));
+assert.ok(defaultPath.endsWith(path.join('bin', LAUNCHER_BIN)), defaultPath);
+
+// --- probe: a missing launcher is unusable, indistinguishable from an unenforcing kernel ---
+assert.equal(probe(path.join(os.tmpdir(), 'nalr-no-such-launcher')), 'unusable');
+
+// --- probe: verdict parsing over fake launchers (POSIX shells only) ---
+if (process.platform !== 'win32') {
+  const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-fake-launcher-'));
+  const fake = (name, script) => {
+    const file = path.join(fakeDir, name);
+    fs.writeFileSync(file, `#!/bin/sh\n${script}\n`, { mode: 0o755 });
+    return file;
+  };
+
+  assert.equal(probe(fake('full', 'echo "landlock: fully enforced"; exit 0')), 'full');
+  assert.equal(probe(fake('partial', 'echo "landlock: partially enforced (older ABI)"; exit 0')), 'partial');
+  assert.equal(probe(fake('failing', `exit ${LAUNCHER_FAILURE_EXIT}`)), 'unusable');
+  assert.equal(probe(fake('hanging', 'sleep 10'), { timeoutMs: 200 }), 'unusable');
+
+  fs.rmSync(fakeDir, { recursive: true, force: true });
+}
+
+console.log('entry.test: ok');

+ 121 - 0
native/landlock-run/test/launcher.test.js

@@ -0,0 +1,121 @@
+/**
+ * Behavioral tests against the REAL launcher binary on a real kernel: the
+ * CLI contract (usage errors, exit codes, argv passthrough) and the
+ * confinement world-proofs (denied writes stay off disk, grants land).
+ *
+ * Preconditions and their skip semantics:
+ * - Non-Linux host: skips entirely (exit 0) — there is nothing to build here.
+ * - Linux without the built binary: FAILS — run `pnpm build:native` first.
+ * - Linux whose kernel does not enforce Landlock: skips the enforcement
+ *   half, unless `NALR_REQUIRE_LANDLOCK=1` (set on CI, where a silent skip on
+ *   the very platform that exists to prove enforcement would be a false
+ *   green).
+ */
+
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { spawnSync } from 'node:child_process';
+import {
+  LAUNCHER_FAILURE_EXIT,
+  grantArgs,
+  launcherPath,
+  probe,
+} from 'node-addon-landlock-run';
+
+const requireLandlock = process.env.NALR_REQUIRE_LANDLOCK === '1';
+
+if (process.platform !== 'linux') {
+  console.log(`launcher.test: SKIP — the launcher only exists on linux (host: ${process.platform})`);
+  process.exit(0);
+}
+
+const launcher = launcherPath();
+assert.ok(
+  fs.existsSync(launcher),
+  `launcher.test: no built launcher at ${launcher} — run \`pnpm build:native\` (apt-get install musl-tools) first`,
+);
+
+const run = (args, options = {}) => spawnSync(launcher, args, { encoding: 'utf8', ...options });
+
+// --- usage errors: parse failures exit LAUNCHER_FAILURE_EXIT before any restriction ---
+{
+  const noCommand = run([]);
+  assert.equal(noCommand.status, LAUNCHER_FAILURE_EXIT);
+  assert.match(noCommand.stderr, /usage error: missing `-- <argv>\.\.\.` command/);
+
+  const unknownFlag = run(['--bogus', '--', 'true']);
+  assert.equal(unknownFlag.status, LAUNCHER_FAILURE_EXIT);
+  assert.match(unknownFlag.stderr, /usage error: unknown argument: --bogus/);
+
+  const danglingPath = run(['--ro']);
+  assert.equal(danglingPath.status, LAUNCHER_FAILURE_EXIT);
+  assert.match(danglingPath.stderr, /--ro requires a path/);
+
+  const probeWithExtras = run(['--probe', '--ro', '/']);
+  assert.equal(probeWithExtras.status, LAUNCHER_FAILURE_EXIT);
+  assert.match(probeWithExtras.stderr, /--probe takes no other arguments/);
+}
+
+// --- probe: the functional availability signal ---
+const enforcement = probe(launcher);
+console.log(`launcher.test: probe → ${enforcement}`);
+if (enforcement === 'unusable') {
+  if (requireLandlock) {
+    console.error('launcher.test: NALR_REQUIRE_LANDLOCK=1 but the probe reports unusable — this kernel cannot prove enforcement');
+    process.exit(1);
+  }
+  console.log('launcher.test: SKIP enforcement half — kernel does not enforce Landlock');
+  process.exit(0);
+}
+{
+  const probeRun = run(['--probe']);
+  assert.equal(probeRun.status, 0);
+  assert.match(probeRun.stdout, /^landlock: (fully enforced|partially enforced \(older ABI\))\n$/);
+}
+
+// --- confined exec: the command runs, its exit code passes through ---
+{
+  const echo = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'echo confined-ok']);
+  assert.equal(echo.status, 0, echo.stderr);
+  assert.equal(echo.stdout, 'confined-ok\n');
+
+  const exitCode = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'exit 7']);
+  assert.equal(exitCode.status, 7, 'the wrapped command exit code must pass through unchanged');
+}
+
+// --- world-proofs: denied writes stay off disk, grants land, inheritance crosses exec ---
+{
+  const work = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-launcher-test-'));
+
+  const denied = path.join(work, 'denied.txt');
+  const deniedRun = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', `echo x > ${denied}`]);
+  assert.notEqual(deniedRun.status, 0, 'a write outside the grants must fail');
+  assert.ok(!fs.existsSync(denied), 'the denied write must not land on disk');
+
+  const granted = path.join(work, 'granted.txt');
+  const grantedRun = run([...grantArgs({ readOnly: ['/'], readWrite: [work] }), '--', '/bin/sh', '-c', `echo ok > ${granted}`]);
+  assert.equal(grantedRun.status, 0, grantedRun.stderr);
+  assert.equal(fs.readFileSync(granted, 'utf8'), 'ok\n');
+
+  // The ruleset is inherited across execve: a CHILD of the wrapped command
+  // is confined too, not just the direct exec target.
+  const nested = path.join(work, 'nested.txt');
+  const nestedRun = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', `/bin/sh -c 'echo x > ${nested}'; true`]);
+  assert.equal(nestedRun.status, 0, nestedRun.stderr);
+  assert.ok(!fs.existsSync(nested), 'a denied write from a nested child must not land either');
+
+  fs.rmSync(work, { recursive: true, force: true });
+}
+
+// --- fail closed: an unopenable grant root refuses to exec at all ---
+{
+  const marker = path.join(os.tmpdir(), `nalr-should-not-exist-${process.pid}`);
+  const badGrant = run(['--ro', '/no/such/grant/root', '--', '/bin/sh', '-c', `echo x > ${marker}`]);
+  assert.equal(badGrant.status, LAUNCHER_FAILURE_EXIT);
+  assert.match(badGrant.stderr, /cannot open rule path/);
+  assert.ok(!fs.existsSync(marker), 'the command must never run when the launcher fails');
+}
+
+console.log('launcher.test: ok');

+ 11 - 0
native/landlock-run/tsconfig.base.json

@@ -0,0 +1,11 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "types": ["node"]
+  }
+}

+ 11 - 0
native/landlock-run/tsconfig.json

@@ -0,0 +1,11 @@
+{
+  "extends": "./tsconfig.base.json",
+  "compilerOptions": {
+    "noEmit": true
+  },
+  "files": [],
+  "include": ["scripts/**/*.ts"],
+  "references": [
+    { "path": "./packages/entry" }
+  ]
+}

+ 1 - 1
scripts/doc-budgets.manifest.json

@@ -1,5 +1,5 @@
 {
-  "AGENTS.md": 1370,
+  "AGENTS.md": 1375,
   "docs/AGENTS.md": 1100,
   "docs/architecture.md": 1790,
   "docs/cordis-primer.md": 550,