Files
deepseek-harness/native/landlock-run/docs/architecture.md
kingwl 0a486f09c9 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).
2026-07-14 23:39:58 +08:00

4.1 KiB

Architecture

This repository owns confinement mechanism, not policy: consumers (agent harnesses, sandbox seams) decide which paths a run may read or write; this package family provides the launcher that enforces those grants and the JS seam that resolves and speaks to it. The packaging follows the per-platform-package model of node-addon-require-builtin (and esbuild), adapted from Node addons to standalone static executables.

Two-layer package family

The family is one entry package plus per-platform binary packages:

  • Entry package (node-addon-landlock-run): ESM JavaScript. Owns the tool's CLI contract — path resolution (launcherPath), the functional probe (probe), grant-argv construction (grantArgs), and the contract constants. Ships the C source in its tarball for auditability. Lists every platform package as 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 — 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.