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).
This commit is contained in:
34
native/landlock-run/docs/architecture.md
Normal file
34
native/landlock-run/docs/architecture.md
Normal file
@@ -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
native/landlock-run/docs/cli-contract.md
Normal file
34
native/landlock-run/docs/cli-contract.md
Normal file
@@ -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
native/landlock-run/docs/naming.md
Normal file
30
native/landlock-run/docs/naming.md
Normal file
@@ -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
native/landlock-run/docs/packaging.md
Normal file
45
native/landlock-run/docs/packaging.md
Normal file
@@ -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
native/landlock-run/docs/release.md
Normal file
57
native/landlock-run/docs/release.md
Normal file
@@ -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
native/landlock-run/docs/support-matrix.md
Normal file
18
native/landlock-run/docs/support-matrix.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user