owlrunkit 0.25.1 → 0.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.3
4
+
5
+ - Accepts Bubblewrap on a verified read-only Linux system mount when the
6
+ platform remaps root and the system file owners appear as overflow UIDs.
7
+ RunKit runs the checked executable by descriptor and rechecks its identity
8
+ before starting each worker.
9
+ - Ships the Linux helper build script and checksums with the npm package and
10
+ links to the exact corresponding public source tag from `SOURCE.md`.
11
+
12
+ ## 0.25.2
13
+
14
+ - Supports managed Linux worker recovery with system Bubblewrap when Landlock
15
+ is unavailable. RunKit verifies the writable attempt, read-only sandbox
16
+ root, hidden host files, and seccomp process and network restrictions before
17
+ selecting the backend.
18
+ - Keeps Landlock as the first Linux backend. If neither backend passes its
19
+ runtime probe, managed peer commands refuse to start.
20
+
3
21
  ## 0.25.1
4
22
 
5
23
  - Adds enforced Linux peer isolation for x86-64 and ARM64 with Landlock and
@@ -1,2 +1,2 @@
1
- acbc0a76363cbb81700fdb3e7924c6b1403b30af82dbbda9ad8855a6bb53f988 peer-linux-arm64
2
- c6a4b270df2482f52ef1463b6ced5205f34c9abda8d4af0a55a1ac3273985436 peer-linux-x64
1
+ 39e177cebac50c87514e19df991266970a5814eb468ec716250d6e731a73a40d peer-linux-x64
2
+ ad5ecf24f6c13233edb43c8ce0b8aa91b15134e710252ae6e82de39630d67e0e peer-linux-arm64
package/README.md CHANGED
@@ -134,29 +134,9 @@ automatically promote an authoritative writer. Core successor, release
134
134
  compatibility, publication, Project Successor, and project adoption remain
135
135
  separate authority boundaries.
136
136
 
137
- ## Current release
138
-
139
- `owlrunkit@0.25.1` is the version documented here. Its public surfaces
140
- include:
141
-
142
- - Project Driver and typed delivery lifecycle;
143
- - Quick and Formal verification with source-bound receipts;
144
- - controller/target worktree separation and foreign-candidate verification;
145
- - Team Delivery read-only recommendations and portable transfer packets;
146
- - bounded compact-session Hook Recovery;
147
- - optional native Codex Stop continuation on a bound RunKit file event;
148
- - managed local peer recovery for isolated macOS and Linux workers;
149
- - completed-project drift detection and read-only successor scaffolding;
150
- - fail-closed bootstrap, profile, registry, Core, and shared-Skill diagnostics.
151
-
152
- This repository is the public corresponding-source, documentation, issue,
153
- release, and trust surface for OwlRunKit. Day-to-day development remains in a
154
- private source repository; every GPL-covered npm release must map to an exact
155
- public source tag.
156
-
157
137
  ## Install
158
138
 
159
- This package is the lifecycle-stable, registry-gated `owlrunkit@0.25.1`
139
+ This package is the lifecycle-stable, registry-gated `owlrunkit@0.25.3`
160
140
  artifact. Registry adoption remains fail-closed until its exact official npm
161
141
  version, shasum, integrity, and tarball URL are independently verified. Local
162
142
  tarballs remain candidate-test inputs and are never adoption provenance. Until
@@ -164,7 +144,7 @@ that registry evidence exists, use the package only for isolated candidate
164
144
  verification; do not treat the command below as available release truth.
165
145
 
166
146
  ```bash
167
- npm install --save-exact owlrunkit@0.25.1
147
+ npm install --save-exact owlrunkit@0.25.3
168
148
  ```
169
149
 
170
150
  Use `npx --no-install owlrunkit` for project commands. The package bootstrap
@@ -180,9 +160,9 @@ the project Config must independently bind the same Core.
180
160
  ```bash
181
161
  npx --no-install owlrunkit --version
182
162
  npx --no-install owlrunkit bootstrap --workspace /absolute/path/to/project \
183
- --exact owlrunkit@0.25.1 --dry-run
163
+ --exact owlrunkit@0.25.3 --dry-run
184
164
  npx --no-install owlrunkit bootstrap --workspace /absolute/path/to/project \
185
- --exact owlrunkit@0.25.1 --apply
165
+ --exact owlrunkit@0.25.3 --apply
186
166
  npx --no-install owlrunkit init --workspace /absolute/path/to/project
187
167
  npx --no-install owlrunkit doctor --workspace /absolute/path/to/project
188
168
  npx --no-install owlrunkit inspect --workspace /absolute/path/to/project --json --compact
@@ -1060,10 +1040,16 @@ scratch directory is reported for inspection while it remains on disk.
1060
1040
 
1061
1041
  The macOS and Linux backends confine writes to the attempt and deny network
1062
1042
  connections and new worker processes. Linux supports x86-64 and ARM64 with
1063
- Landlock ABI 3 or later, seccomp filtering, and `close_range`. The packaged
1064
- helper checks the effective policy before a managed task starts. It terminates
1065
- the worker if the supervisor process is killed. The host caller supplies the
1066
- worker command and peer identity. Existing Desktop sessions and writers using
1043
+ seccomp filtering and `close_range`. RunKit tries Landlock ABI 3 or later
1044
+ first. If Landlock is unavailable, it can use a trusted system Bubblewrap
1045
+ executable with user and mount namespaces. The Bubblewrap probe verifies
1046
+ that the attempt is writable, host files are hidden, and new processes and
1047
+ network sockets are denied. If the probe fails, the task stays blocked.
1048
+ On Linux environments that remap root, RunKit can trust `/usr/bin/bwrap` on
1049
+ a dedicated read-only `/usr` system mount after verifying its mount identity
1050
+ and checking for writable aliases. It launches the verified file descriptor.
1051
+ On Linux, the worker is terminated if its supervisor is killed. The host
1052
+ caller supplies the worker command and peer identity. Existing Desktop sessions and writers using
1067
1053
  legacy shared-directory leases follow their own recovery procedure. An
1068
1054
  existing managed task remains pinned to the core that created it.
1069
1055
 
package/SOURCE.md CHANGED
@@ -1,16 +1,15 @@
1
1
  # Corresponding source
2
2
 
3
3
  OwlRunKit is distributed under GPL-3.0-or-later. The corresponding public
4
- source for this release is [OwlRunKit v0.25.1](https://github.com/yeemio/owlrunkit/tree/v0.25.1).
5
- It includes the JavaScript runtime, C source and build script for the Linux
6
- helpers, package metadata, public contracts, and bundled integration resources.
4
+ source is [OwlRunKit v0.25.3](https://github.com/yeemio/owlrunkit/tree/v0.25.3).
5
+ The npm package includes the JavaScript runtime, read-only verifier, C source,
6
+ and [Linux helper build script](packages/runkit/build-linux-helper.sh).
7
+ The distributed helpers statically link musl and include its copyright notice.
7
8
 
8
9
  ## Rebuild the Linux helpers
9
10
 
10
- Check out the public tag, then run these commands from its root. The helper
11
- build uses the Alpine 3.22 image and the compiler and Linux headers from its
12
- package repository. Build on each architecture or use a Docker environment
13
- that supports both build platforms.
11
+ From the public tag or unpacked npm package, run the build on Linux x86-64
12
+ and ARM64 using Alpine 3.22 with `build-base` and `linux-headers`.
14
13
 
15
14
  ```sh
16
15
  mkdir -p .build
@@ -23,6 +22,7 @@ done
23
22
  (cd .build && sha256sum -c ../LINUX-HELPERS.sha256)
24
23
  ```
25
24
 
26
- The npm tarball contains both helper executables alongside their C source and
27
- the musl copyright notice. Its official registry version, shasum, integrity,
28
- and tarball URL identify the published package.
25
+ The authoritative release is identified by the npm registry version, shasum,
26
+ integrity, and tarball URL. Local directories, `file:` dependencies, local
27
+ tarballs, Git URLs, workspace links, and mutable dist-tags are not accepted as
28
+ formal cross-project adoption evidence.
@@ -7,14 +7,14 @@ description: Run receipt-backed, project-owned work with OwlCoda RunKit. Use whe
7
7
 
8
8
  Use the bundled deterministic Core to coordinate work through project artifacts rather than chat memory. Require Node.js 20 or later.
9
9
 
10
- Bundled registry-gated artifact: standalone `owlrunkit@0.25.1`, bundling Contract
11
- `0.2` and Core `0.25.1` with manifest
12
- `sha256:0934a076b147847a4623bc89263b677c88088b493495fc74f11485985b7903b5`.
10
+ Bundled registry-gated artifact: standalone `owlrunkit@0.25.3`, bundling Contract
11
+ `0.2` and Core `0.25.3` with manifest
12
+ `sha256:901f3b5da227555a1a6b65f9deb4efa71f8f3bf9c3f9a34b1a30034da8779d66`.
13
13
  Registry adoption remains fail-closed until exact official npm registry
14
- provenance for `owlrunkit@0.25.1` is independently verified. The
15
- published rollback baseline for this artifact is `owlrunkit@0.25.0`, with SHA-1
16
- `8cfb52126320bfdafc722bcc41db6cada19f91cc` and canonical npm tarball URL
17
- `https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.0.tgz`.
14
+ provenance for `owlrunkit@0.25.3` is independently verified. The
15
+ published rollback baseline for this artifact is `owlrunkit@0.25.2`, with SHA-1
16
+ `0b4bc23778549cdd3537f2e78d55302b34237ded` and canonical npm tarball URL
17
+ `https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.2.tgz`.
18
18
  The root `owlcoda` package has a separate version lifecycle.
19
19
 
20
20
  The session is an execution interface. Project artifacts are the system of record.
@@ -370,9 +370,9 @@ below has no CLI entry.
370
370
 
371
371
  ```bash
372
372
  npx --no-install owlrunkit bootstrap --workspace "$PROJECT_ROOT" \
373
- --exact owlrunkit@0.25.1 --dry-run
373
+ --exact owlrunkit@0.25.3 --dry-run
374
374
  npx --no-install owlrunkit bootstrap --workspace "$PROJECT_ROOT" \
375
- --exact owlrunkit@0.25.1 --apply
375
+ --exact owlrunkit@0.25.3 --apply
376
376
  npx --no-install owlrunkit init --workspace "$PROJECT_ROOT"
377
377
  npx --no-install owlrunkit inspect --json --workspace "$PROJECT_ROOT"
378
378
  npx --no-install owlrunkit inspect --json --compact --workspace "$PROJECT_ROOT"
@@ -2,9 +2,9 @@
2
2
  "schemaVersion": "OwlCodaRunKitConfigV2",
3
3
  "core": {
4
4
  "contractVersion": "0.2",
5
- "coreVersion": "0.25.1",
6
- "coreManifestSha256": "sha256:0934a076b147847a4623bc89263b677c88088b493495fc74f11485985b7903b5",
7
- "coreSourceRef": "artifact:sha256:0934a076b147847a4623bc89263b677c88088b493495fc74f11485985b7903b5"
5
+ "coreVersion": "0.25.3",
6
+ "coreManifestSha256": "sha256:901f3b5da227555a1a6b65f9deb4efa71f8f3bf9c3f9a34b1a30034da8779d66",
7
+ "coreSourceRef": "artifact:sha256:901f3b5da227555a1a6b65f9deb4efa71f8f3bf9c3f9a34b1a30034da8779d66"
8
8
  },
9
9
  "authorizationPolicy": "external_explicit_authority_required"
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "owlrunkit",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "description": "Project Driver for cross-Agent project continuity and progression; execution, evidence, verification, and recovery are the underlying trust substrate.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "author": "yeemio <owlclaw@163.com>",
@@ -42,6 +42,16 @@
42
42
  "sha256": "sha256:3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"
43
43
  },
44
44
  "supportedProducerCoreIdentities": [
45
+ {
46
+ "contractVersion": "0.2",
47
+ "coreVersion": "0.25.3",
48
+ "coreManifestSha256": "sha256:901f3b5da227555a1a6b65f9deb4efa71f8f3bf9c3f9a34b1a30034da8779d66"
49
+ },
50
+ {
51
+ "contractVersion": "0.2",
52
+ "coreVersion": "0.25.2",
53
+ "coreManifestSha256": "sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc"
54
+ },
45
55
  {
46
56
  "contractVersion": "0.2",
47
57
  "coreVersion": "0.25.1",
@@ -23,6 +23,16 @@ import {
23
23
  export { validQuickReceiptShape } from "./quick-receipt-contract.mjs";
24
24
 
25
25
  export const SUPPORTED_QUICK_CORE_IDENTITIES = Object.freeze([
26
+ {
27
+ contractVersion: "0.2",
28
+ coreVersion: "0.25.3",
29
+ coreManifestSha256: "sha256:901f3b5da227555a1a6b65f9deb4efa71f8f3bf9c3f9a34b1a30034da8779d66",
30
+ },
31
+ {
32
+ contractVersion: "0.2",
33
+ coreVersion: "0.25.2",
34
+ coreManifestSha256: "sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc",
35
+ },
26
36
  {
27
37
  contractVersion: "0.2",
28
38
  coreVersion: "0.25.1",
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ case "$(uname -s)/$(uname -m)" in
5
+ Linux/x86_64) arch=x64 ;;
6
+ Linux/aarch64) arch=arm64 ;;
7
+ *) echo "Build the peer helper on Linux x86_64 or aarch64." >&2; exit 1 ;;
8
+ esac
9
+
10
+ if [ "$#" -ne 1 ] || [ ! -d "$1" ]; then
11
+ echo "Usage: sh build-linux-helper.sh <existing output directory>" >&2
12
+ exit 1
13
+ fi
14
+
15
+ source_dir=$(CDPATH= cd -- "$(dirname -- "$0")/../../scripts/runkit-contract" && pwd)
16
+ "${CC:-cc}" -std=c11 -O2 -Wall -Wextra -Werror -static -s \
17
+ "$source_dir/peer-linux.c" -o "$1/peer-linux-$arch"
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": "OwlCodaRunKitRegistryReleasePolicyV2",
3
3
  "status": "release_artifact_registry_gated",
4
4
  "packageName": "owlrunkit",
5
- "artifactVersion": "0.25.1",
5
+ "artifactVersion": "0.25.3",
6
6
  "registry": "https://registry.npmjs.org",
7
7
  "registryRequiredBeforeAdoption": true,
8
8
  "exactVersionRequired": true,
@@ -23,10 +23,10 @@
23
23
  ],
24
24
  "rollbackRelease": {
25
25
  "packageName": "owlrunkit",
26
- "version": "0.25.0",
27
- "shasum": "8cfb52126320bfdafc722bcc41db6cada19f91cc",
28
- "integrity": "sha512-YgO+KFQ+wmiPVzTRJay9RIyIEYk9e4ViuM0otBIjF8RpkjLcALq2LUJRlo8vmymwxm7TtPCuSNc7UD2gNKFOAA==",
29
- "tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.0.tgz"
26
+ "version": "0.25.2",
27
+ "shasum": "0b4bc23778549cdd3537f2e78d55302b34237ded",
28
+ "integrity": "sha512-a7EJwkR/3o0Pjuz2dxEKkbB6n7WowLloKtYuuyjfXGfZM3D6/IVphdRLBv7oh/lJPk511e/i0XGUfFxt+OhXzA==",
29
+ "tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.2.tgz"
30
30
  },
31
31
  "registryEvidenceMustBeExternal": true,
32
32
  "registryEvidenceSchemaVersion": "OwlCodaRunKitRegistryReleaseEvidenceV1",
package/sbom.spdx.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "owlrunkit-0.25.1",
6
- "documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.25.1",
5
+ "name": "owlrunkit-0.25.3",
6
+ "documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.25.3",
7
7
  "creationInfo": {
8
8
  "created": "2026-11-11T00:00:00Z",
9
9
  "creators": [
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "name": "owlrunkit",
16
16
  "SPDXID": "SPDXRef-Package-OwlRunKit",
17
- "versionInfo": "0.25.1",
17
+ "versionInfo": "0.25.3",
18
18
  "downloadLocation": "NOASSERTION",
19
19
  "filesAnalyzed": false,
20
20
  "licenseConcluded": "GPL-3.0-or-later",
@@ -24,7 +24,7 @@
24
24
  {
25
25
  "referenceCategory": "PACKAGE-MANAGER",
26
26
  "referenceType": "purl",
27
- "referenceLocator": "pkg:npm/owlrunkit@0.25.1"
27
+ "referenceLocator": "pkg:npm/owlrunkit@0.25.3"
28
28
  }
29
29
  ]
30
30
  }
@@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url";
14
14
  import { parseProjectControlState } from "./project-control-parser.mjs";
15
15
 
16
16
  export const CONTRACT_VERSION = "0.2";
17
- export const CORE_VERSION = "0.25.1";
17
+ export const CORE_VERSION = "0.25.3";
18
18
  export const RUNTIME_ROOT = ".owlcoda/runkit";
19
19
 
20
20
  const CORE_FILES = [
@@ -44,6 +44,7 @@ const CORE_FILES = [
44
44
  "linux-runtime.mjs",
45
45
  "offline-store.mjs",
46
46
  "outcome-presentation.mjs",
47
+ "peer-linux-bwrap.mjs",
47
48
  "peer-linux.c",
48
49
  "peer-recovery.mjs",
49
50
  "onboarding-doctor.mjs",
@@ -0,0 +1,204 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ accessSync, closeSync, constants, fstatSync, lstatSync, mkdtempSync, openSync,
5
+ readFileSync, realpathSync, rmSync, statfsSync, writeFileSync,
6
+ } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import path from "node:path";
9
+
10
+ export const BUBBLEWRAP_PEER_BACKEND = "linux_bubblewrap_seccomp_v1";
11
+
12
+ const SANDBOX_HELPER = "/owlrunkit-helper";
13
+ const SANDBOX_WORKER = "/owlrunkit-worker";
14
+ const WORK = "/work";
15
+ const SYSTEM_BWRAP = "/usr/bin/bwrap";
16
+ const FD_EXECUTABLE = "/proc/self/fd/3";
17
+ const OVERFLOW_UID = 65534;
18
+ const PROC_SUPER_MAGIC = 0x9fa0;
19
+ const CAP_SYS_ADMIN = 1n << 21n;
20
+ const MAX_EXECUTABLE_BYTES = 1024 * 1024;
21
+
22
+ function rootOwnedPath(file, stat) {
23
+ if (stat.uid !== 0) return false;
24
+ let parent = path.dirname(file);
25
+ for (;;) {
26
+ const entry = lstatSync(parent);
27
+ if (!entry.isDirectory() || entry.uid !== 0 || (entry.mode & 0o022)) return false;
28
+ const next = path.dirname(parent);
29
+ if (parent === next) return true;
30
+ parent = next;
31
+ }
32
+ }
33
+
34
+ export function readOnlySystemBubblewrapIssue({ uid, uidMap, root, systemDirectories, mountId, mountInfo }) {
35
+ if (uid === 0 || uid === OVERFLOW_UID || uidMap.trim().split(/\s+/u).join(" ") !== `${uid} 0 1`) {
36
+ return "the caller is not the single mapped parent root UID";
37
+ }
38
+ if (!root.isDirectory() || root.uid !== uid || (root.mode & 0o022)
39
+ || !systemDirectories.every(entry =>
40
+ entry.isDirectory() && entry.uid === OVERFLOW_UID && !(entry.mode & 0o022))) {
41
+ return "the system directories do not match the remapped ownership boundary";
42
+ }
43
+
44
+ const mounts = mountInfo.trim().split("\n").map(line => {
45
+ const [description, filesystem] = line.split(" - ");
46
+ const [id, , device, , target, flags] = description.split(" ");
47
+ return { id, device, target, flags: flags?.split(",") ?? [], type: filesystem?.split(" ")[0] };
48
+ });
49
+ const system = mounts.find(mount => mount.id === mountId);
50
+ if (system?.target !== "/usr" || system.type !== "overlay"
51
+ || !["ro", "nosuid", "nodev"].every(flag => system.flags.includes(flag))) {
52
+ return "the opened executable is not on the protected read-only /usr mount";
53
+ }
54
+ if (mounts.some(mount => mount.device === system.device && mount.flags.includes("rw"))) {
55
+ return "the /usr mount has a writable alias";
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function readOnlySystemPathIssue(file, stat, fd) {
61
+ if (file !== SYSTEM_BWRAP || stat.uid !== OVERFLOW_UID || process.getuid() !== process.geteuid()) {
62
+ return "the executable is outside the remapped system path";
63
+ }
64
+ if (statfsSync("/proc/self").type !== PROC_SUPER_MAGIC) return "the kernel mount information is unavailable";
65
+ const capabilities = /^CapPrm:\s*([0-9a-f]+)$/imu.exec(readFileSync("/proc/self/status", "utf8"))?.[1];
66
+ if (!capabilities || (BigInt(`0x${capabilities}`) & CAP_SYS_ADMIN)) {
67
+ return "the caller can reconfigure its system mounts or that capability could not be checked";
68
+ }
69
+ const mountId = /^mnt_id:\s*(\d+)$/mu.exec(readFileSync(`/proc/self/fdinfo/${fd}`, "utf8"))?.[1];
70
+ if (!mountId) return "the kernel did not identify the executable mount";
71
+ return readOnlySystemBubblewrapIssue({
72
+ uid: process.geteuid(),
73
+ uidMap: readFileSync("/proc/self/uid_map", "utf8"),
74
+ root: lstatSync("/"),
75
+ systemDirectories: [lstatSync("/usr"), lstatSync("/usr/bin")],
76
+ mountId,
77
+ mountInfo: readFileSync("/proc/self/mountinfo", "utf8"),
78
+ });
79
+ }
80
+
81
+ function openTrustedBubblewrap(expected) {
82
+ const directories = ["/usr/bin", "/bin", "/usr/local/bin", ...(process.env.PATH ?? "").split(":")];
83
+ const candidates = expected ? [expected.executable]
84
+ : [...new Set(directories)].filter(path.isAbsolute).map(dir => path.join(dir, "bwrap"));
85
+ let systemRejection;
86
+ for (const candidate of candidates) {
87
+ let fd;
88
+ let trusted = false;
89
+ try {
90
+ const executable = realpathSync(candidate);
91
+ if (expected && executable !== expected.executable) continue;
92
+ accessSync(executable, constants.X_OK);
93
+ fd = openSync(executable, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
94
+ const stat = fstatSync(fd);
95
+ if (!stat.isFile() || (stat.mode & 0o6022) || stat.size > MAX_EXECUTABLE_BYTES) continue;
96
+ let trust = "root_owned";
97
+ if (!rootOwnedPath(executable, stat)) {
98
+ const issue = readOnlySystemPathIssue(executable, stat, fd);
99
+ if (issue) {
100
+ if (executable === SYSTEM_BWRAP) systemRejection = issue;
101
+ continue;
102
+ }
103
+ trust = "read_only_system_mount";
104
+ }
105
+ const sandboxDigest = `sha256:${createHash("sha256").update(readFileSync(fd)).digest("hex")}`;
106
+ if (expected && (expected.sandboxDigest !== sandboxDigest || expected.trust !== trust)) continue;
107
+ trusted = true;
108
+ return { executable, fd, sandboxDigest, trust };
109
+ } catch (error) {
110
+ if (!["ENOENT", "EACCES", "ENOTDIR", "ELOOP"].includes(error.code)) throw error;
111
+ } finally {
112
+ if (fd !== undefined && !trusted) closeSync(fd);
113
+ }
114
+ }
115
+ return { reason: ["trusted system Bubblewrap not found", systemRejection].filter(Boolean).join(": ") };
116
+ }
117
+
118
+ export function spawnBubblewrapPeer(backend, args, options) {
119
+ const launcher = openTrustedBubblewrap(backend);
120
+ if (launcher.fd === undefined) {
121
+ throw new Error(`Bubblewrap executable or trust changed after its self-test: ${launcher.reason}`);
122
+ }
123
+ try {
124
+ return spawn(FD_EXECUTABLE, args, { ...options, stdio: [...options.stdio, launcher.fd] });
125
+ } finally {
126
+ closeSync(launcher.fd);
127
+ }
128
+ }
129
+
130
+ export function bubblewrapPeerArgs({ helper, directory, worker, args, environment }) {
131
+ return [
132
+ "--unshare-user", "--new-session", "--die-with-parent", "--clearenv", "--cap-drop", "ALL",
133
+ "--ro-bind-try", "/lib", "/lib",
134
+ "--ro-bind-try", "/lib64", "/lib64",
135
+ "--ro-bind-try", "/usr/lib", "/usr/lib",
136
+ "--ro-bind-try", "/usr/lib64", "/usr/lib64",
137
+ "--ro-bind-try", "/etc/ld.so.cache", "/etc/ld.so.cache",
138
+ "--dev-bind", "/dev/null", "/dev/null",
139
+ "--ro-bind", helper, SANDBOX_HELPER,
140
+ "--ro-bind", worker, SANDBOX_WORKER,
141
+ "--bind", directory, WORK, "--remount-ro", "/", "--chdir", WORK,
142
+ ...Object.entries({ ...environment, TMPDIR: WORK, OPENSSL_CONF: "/dev/null" })
143
+ .flatMap(([key, value]) => ["--setenv", key, value]),
144
+ "--", SANDBOX_HELPER, "run-bwrap", SANDBOX_WORKER, ...args,
145
+ ];
146
+ }
147
+
148
+ const probeWorker = `
149
+ const assert = require('node:assert/strict');
150
+ const fs = require('node:fs');
151
+ const net = require('node:net');
152
+ const { spawnSync } = require('node:child_process');
153
+ assert.equal(process.cwd(), '/work');
154
+ assert.equal(fs.readFileSync('probe-input', 'utf8'), 'from host');
155
+ assert.throws(() => fs.readFileSync(process.argv[1]), { code: 'ENOENT' });
156
+ assert.throws(() => fs.writeFileSync('/probe-outside-work', 'no'), { code: 'EROFS' });
157
+ assert.equal(spawnSync(process.execPath, ['--version']).error?.code, 'EPERM');
158
+ const server = net.createServer();
159
+ server.once('error', error => {
160
+ assert.equal(error.code, 'EPERM');
161
+ fs.writeFileSync('probe-result', 'confined');
162
+ });
163
+ server.listen(0, '127.0.0.1', () => {
164
+ server.close();
165
+ throw new Error('socket unexpectedly succeeded');
166
+ });
167
+ `;
168
+
169
+ export function probeBubblewrapPeer(helper) {
170
+ const directory = mkdtempSync(path.join(realpathSync(tmpdir()), "owlrunkit-bwrap-probe-"));
171
+ let launcher;
172
+ try {
173
+ launcher = openTrustedBubblewrap();
174
+ if (launcher.fd === undefined) return { available: false, reason: launcher.reason };
175
+ const outside = path.join(directory, "outside");
176
+ const work = path.join(directory, "work");
177
+ writeFileSync(outside, "outside", { flag: "wx", mode: 0o600 });
178
+ // Keep the sentinel outside the only writable bind.
179
+ const scratch = mkdtempSync(work);
180
+ writeFileSync(path.join(scratch, "probe-input"), "from host", { flag: "wx", mode: 0o600 });
181
+ const result = spawnSync(FD_EXECUTABLE, bubblewrapPeerArgs({
182
+ helper, directory: scratch, worker: realpathSync(process.execPath),
183
+ args: ["-e", probeWorker, outside], environment: {},
184
+ }), {
185
+ cwd: scratch, env: {}, stdio: ["ignore", "ignore", "pipe", launcher.fd],
186
+ encoding: "utf8", timeout: 5000, maxBuffer: 65536,
187
+ });
188
+ if (result.status !== 0) {
189
+ return { available: false, reason: result.stderr?.trim() || result.error?.code
190
+ || `Bubblewrap self-test exited ${result.status}` };
191
+ }
192
+ const confirmation = path.join(scratch, "probe-result");
193
+ if (readFileSync(confirmation, "utf8") !== "confined") {
194
+ return { available: false, reason: "Bubblewrap self-test did not confirm enforcement" };
195
+ }
196
+ const { executable, sandboxDigest, trust } = launcher;
197
+ return { available: true, id: BUBBLEWRAP_PEER_BACKEND, executable, sandboxDigest, trust, helper };
198
+ } catch (error) {
199
+ return { available: false, reason: `Bubblewrap self-test failed: ${error.code ?? error.message}` };
200
+ } finally {
201
+ if (launcher?.fd !== undefined) closeSync(launcher.fd);
202
+ rmSync(directory, { recursive: true, force: true });
203
+ }
204
+ }
@@ -18,6 +18,7 @@
18
18
  #include <sys/prctl.h>
19
19
  #include <sys/socket.h>
20
20
  #include <sys/stat.h>
21
+ #include <sys/statvfs.h>
21
22
  #include <sys/syscall.h>
22
23
  #include <sys/xattr.h>
23
24
  #include <unistd.h>
@@ -61,7 +62,7 @@ static void allow_path(int ruleset, const char *path, uint64_t rights, int optio
61
62
  close(fd);
62
63
  }
63
64
 
64
- static void restrict_files(const char *directory, const char *worker)
65
+ static void restrict_files(const char *directory, const char *worker, int probe)
65
66
  {
66
67
  const uint64_t read_tree = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
67
68
  const uint64_t work = read_tree | LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_TRUNCATE |
@@ -71,9 +72,18 @@ static void restrict_files(const char *directory, const char *worker)
71
72
  LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_BLOCK |
72
73
  LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO | LANDLOCK_ACCESS_FS_MAKE_SYM };
73
74
  long abi = syscall(SYS_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
74
- if (abi < 0)
75
+ if (abi < 0) {
76
+ if (probe && (errno == ENOSYS || errno == EOPNOTSUPP)) {
77
+ perror("landlock_create_ruleset version");
78
+ _exit(77);
79
+ }
75
80
  fail("landlock_create_ruleset version");
81
+ }
76
82
  if (abi < 3) {
83
+ if (probe) {
84
+ fputs("Landlock ABI 3 is required\n", stderr);
85
+ _exit(77);
86
+ }
77
87
  errno = EOPNOTSUPP;
78
88
  fail("Landlock ABI 3 is required");
79
89
  }
@@ -160,7 +170,7 @@ static void restrict_syscalls(void)
160
170
  ALLOW_SYSCALL(faccessat), ALLOW_SYSCALL(faccessat2), ALLOW_SYSCALL(getdents64),
161
171
  ALLOW_SYSCALL(getcwd), ALLOW_SYSCALL(chdir), ALLOW_SYSCALL(fchdir), ALLOW_SYSCALL(readlinkat),
162
172
  ALLOW_SYSCALL(mkdirat), ALLOW_SYSCALL(unlinkat), ALLOW_SYSCALL(renameat),
163
- ALLOW_SYSCALL(renameat2), ALLOW_SYSCALL(linkat), ALLOW_SYSCALL(symlinkat),
173
+ ALLOW_SYSCALL(renameat2), ALLOW_SYSCALL(linkat),
164
174
  ALLOW_SYSCALL(truncate), ALLOW_SYSCALL(ftruncate), ALLOW_SYSCALL(fsync), ALLOW_SYSCALL(fdatasync),
165
175
  ALLOW_SYSCALL(dup), ALLOW_SYSCALL(dup3), ALLOW_SYSCALL(pipe2),
166
176
  ALLOW_SYSCALL(mmap), ALLOW_SYSCALL(munmap), ALLOW_SYSCALL(mprotect), ALLOW_SYSCALL(mremap),
@@ -181,7 +191,7 @@ static void restrict_syscalls(void)
181
191
  ALLOW_SYSCALL(arch_prctl), ALLOW_SYSCALL(access), ALLOW_SYSCALL(open),
182
192
  ALLOW_SYSCALL(stat), ALLOW_SYSCALL(lstat), ALLOW_SYSCALL(readlink),
183
193
  ALLOW_SYSCALL(mkdir), ALLOW_SYSCALL(rmdir), ALLOW_SYSCALL(unlink),
184
- ALLOW_SYSCALL(rename), ALLOW_SYSCALL(link), ALLOW_SYSCALL(symlink),
194
+ ALLOW_SYSCALL(rename), ALLOW_SYSCALL(link),
185
195
  ALLOW_SYSCALL(dup2), ALLOW_SYSCALL(pipe), ALLOW_SYSCALL(epoll_wait), ALLOW_SYSCALL(poll),
186
196
  #endif
187
197
  BPF_STMT(BPF_RET | BPF_K, DENIED),
@@ -193,6 +203,19 @@ static void restrict_syscalls(void)
193
203
  fail("seccomp filter");
194
204
  }
195
205
 
206
+ static void verify_bubblewrap_root(void)
207
+ {
208
+ char directory[sizeof "/work"];
209
+ struct statvfs root, work;
210
+ if (!getcwd(directory, sizeof directory) || strcmp(directory, "/work") != 0 ||
211
+ statvfs("/", &root) < 0 || statvfs("/work", &work) < 0 ||
212
+ !(root.f_flag & ST_RDONLY) || (work.f_flag & ST_RDONLY) ||
213
+ access("/proc/self", F_OK) == 0 || errno != ENOENT) {
214
+ fputs("Unexpected Bubblewrap filesystem boundary\n", stderr);
215
+ _exit(125);
216
+ }
217
+ }
218
+
196
219
  int main(int argc, char **argv)
197
220
  {
198
221
  if (argc == 2 && strcmp(argv[1], "lock") == 0) {
@@ -203,21 +226,30 @@ int main(int argc, char **argv)
203
226
  fail("control flock");
204
227
  }
205
228
  int probe = argc == 2 && strcmp(argv[1], "probe") == 0;
206
- if (!probe && (argc < 5 || strcmp(argv[1], "run") != 0))
229
+ int bubblewrap = argc >= 3 && strcmp(argv[1], "run-bwrap") == 0;
230
+ if (!probe && !bubblewrap && (argc < 5 || strcmp(argv[1], "run") != 0))
207
231
  return 125;
232
+ char **worker_argv = probe ? NULL : &argv[bubblewrap ? 2 : 4];
208
233
  if (!probe) {
209
- char *end;
210
- long parent = strtol(argv[2], &end, 10);
211
- if (*end || parent <= 0 || prctl(PR_SET_PDEATHSIG, SIGKILL) < 0 || getppid() != parent)
234
+ long parent = getppid();
235
+ if (!bubblewrap) {
236
+ char *end;
237
+ parent = strtol(argv[2], &end, 10);
238
+ if (*end)
239
+ return 125;
240
+ }
241
+ if (parent <= 0 || prctl(PR_SET_PDEATHSIG, SIGKILL) < 0 || getppid() != parent)
212
242
  return 125;
243
+ if (bubblewrap)
244
+ verify_bubblewrap_root();
213
245
  struct stat worker;
214
- if (stat(argv[4], &worker) < 0)
246
+ if (stat(worker_argv[0], &worker) < 0)
215
247
  fail("stat worker");
216
248
  if (worker.st_mode & (S_ISUID | S_ISGID)) {
217
249
  errno = EPERM;
218
250
  fail("set-id workers are unsupported");
219
251
  }
220
- if (getxattr(argv[4], "security.capability", NULL, 0) >= 0) {
252
+ if (getxattr(worker_argv[0], "security.capability", NULL, 0) >= 0) {
221
253
  errno = EPERM;
222
254
  fail("file capabilities on workers are unsupported");
223
255
  }
@@ -228,7 +260,8 @@ int main(int argc, char **argv)
228
260
  fail("close inherited descriptors");
229
261
  if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0)
230
262
  fail("no_new_privs");
231
- restrict_files(probe ? NULL : argv[3], probe ? NULL : argv[4]);
263
+ if (!bubblewrap)
264
+ restrict_files(probe ? NULL : argv[3], probe ? NULL : worker_argv[0], probe);
232
265
  restrict_syscalls();
233
266
  if (probe) {
234
267
  if (open("/etc/passwd", O_RDONLY) >= 0 || errno != EACCES ||
@@ -238,6 +271,6 @@ int main(int argc, char **argv)
238
271
  }
239
272
  return 0;
240
273
  }
241
- execve(argv[4], &argv[4], environ);
274
+ execve(worker_argv[0], worker_argv, environ);
242
275
  fail("execve worker");
243
276
  }
@@ -10,6 +10,9 @@ import path from "node:path";
10
10
 
11
11
  import { RUNTIME_ROOT, currentCoreIdentity } from "./core-contract.mjs";
12
12
  import { linuxRuntimeHelper } from "./linux-runtime.mjs";
13
+ import {
14
+ BUBBLEWRAP_PEER_BACKEND, bubblewrapPeerArgs, probeBubblewrapPeer, spawnBubblewrapPeer,
15
+ } from "./peer-linux-bwrap.mjs";
13
16
  import { safeIdentifier } from "./provenance-common.mjs";
14
17
  import { resolveVerificationEnvelopeBackendV1 } from "./verification-envelope.mjs";
15
18
 
@@ -208,7 +211,14 @@ export function resolvePeerRecoveryBackend() {
208
211
  return { available: true, id: "linux_landlock_seccomp_v1", executable,
209
212
  digest: digest(regularBytes(executable, CHECKPOINT_LIMIT)) };
210
213
  }
211
- return { available: false, reason: result.stderr?.trim() || result.error?.code };
214
+ const reason = result.stderr?.trim() || result.error?.code;
215
+ if (result.status !== 77) return { available: false, reason };
216
+ const fallback = probeBubblewrapPeer(executable);
217
+ if (!fallback.available) {
218
+ return { available: false, reason: [reason, fallback.reason].filter(Boolean).join("; ") };
219
+ }
220
+ const helperHash = digest(regularBytes(executable, CHECKPOINT_LIMIT));
221
+ return { ...fallback, digest: digest(JSON.stringify([helperHash, fallback.sandboxDigest])) };
212
222
  } catch (error) { return { available: false, reason: error.code }; }
213
223
  }
214
224
  return resolveVerificationEnvelopeBackendV1();
@@ -409,7 +419,8 @@ export async function runPeerTask({ workspaceRoot, taskId, agent, generation, co
409
419
  const directory = mkdtempSync(path.join(realpathSync(tmpdir()), "owlrunkit-peer-"));
410
420
  materialize(checkpoint(task, state).files, directory);
411
421
  const attempt = { id, directory, backend: backend.id,
412
- ...(backend.digest ? { backendSha256: backend.digest } : {}) };
422
+ ...(backend.digest ? { backendSha256: backend.digest } : {}),
423
+ ...(backend.trust ? { backendTrust: backend.trust } : {}) };
413
424
  const claimed = append(task, state, {
414
425
  attempt, phase: "running", processExit: null, event: "peer_attempt_started",
415
426
  });
@@ -420,17 +431,39 @@ export async function runPeerTask({ workspaceRoot, taskId, agent, generation, co
420
431
  }
421
432
 
422
433
  return new Promise((resolve, reject) => {
423
- const args = backend.id === "linux_landlock_seccomp_v1"
424
- ? ["run", String(process.pid), directory]
425
- : ["-p", sandboxPolicy(executable, directory)];
426
- const child = spawn(backend.executable, [...args, executable, ...command.slice(1)], {
427
- cwd: directory, env: {
428
- PATH: "/usr/bin:/bin", TMPDIR: directory, OWLRUNKIT_PEER_ATTEMPT: id,
429
- OWLRUNKIT_PEER_PATHS: JSON.stringify(state.paths),
430
- // An isolated worker cannot read host OpenSSL configuration.
431
- ...(backend.id === "linux_landlock_seccomp_v1" ? { OPENSSL_CONF: "/dev/null" } : {}),
432
- }, stdio: ["pipe", "pipe", "pipe"],
433
- });
434
+ const environment = {
435
+ PATH: "/usr/bin:/bin", TMPDIR: directory, OWLRUNKIT_PEER_ATTEMPT: id,
436
+ OWLRUNKIT_PEER_PATHS: JSON.stringify(state.paths),
437
+ };
438
+ let args;
439
+ let env = environment;
440
+ if (backend.id === BUBBLEWRAP_PEER_BACKEND) {
441
+ args = bubblewrapPeerArgs({
442
+ helper: backend.helper, directory, worker: executable,
443
+ args: command.slice(1), environment,
444
+ });
445
+ env = {};
446
+ } else if (backend.id === "linux_landlock_seccomp_v1") {
447
+ args = ["run", String(process.pid), directory, executable, ...command.slice(1)];
448
+ // An isolated worker cannot read host OpenSSL configuration.
449
+ env.OPENSSL_CONF = "/dev/null";
450
+ } else {
451
+ args = ["-p", sandboxPolicy(executable, directory), executable, ...command.slice(1)];
452
+ }
453
+ const options = {
454
+ cwd: directory, env, stdio: ["pipe", "pipe", "pipe"],
455
+ };
456
+ let child;
457
+ try {
458
+ child = backend.id === BUBBLEWRAP_PEER_BACKEND
459
+ ? spawnBubblewrapPeer(backend, args, options)
460
+ : spawn(backend.executable, args, options);
461
+ } catch (error) {
462
+ try {
463
+ resolve(reportExit(task, id, { code: null, signal: null, protocolError: error.message, diagnostic: null }));
464
+ } catch (reportError) { reject(reportError); }
465
+ return;
466
+ }
434
467
  let watcher, pending = Buffer.alloc(0), sentWake, protocolError = null;
435
468
  let diagnostic = Buffer.alloc(0);
436
469
  const stop = error => {
@@ -28,10 +28,18 @@ const SELF_NORMALIZED_QUICK_CORE_VERSIONS = new Set([
28
28
  "0.24.1",
29
29
  "0.25.0",
30
30
  "0.25.1",
31
+ "0.25.2",
32
+ "0.25.3",
31
33
  ]);
32
34
  const EXACT_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
33
35
  const SHA256_REF = /^sha256:[a-f0-9]{64}$/;
34
36
  const TRUSTED_PRIOR_CORE_IDENTITIES = new Map([
37
+ ["0.25.2", new Set([
38
+ "sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc",
39
+ ])],
40
+ ["0.25.1", new Set([
41
+ "sha256:0934a076b147847a4623bc89263b677c88088b493495fc74f11485985b7903b5",
42
+ ])],
35
43
  ["0.25.0", new Set([
36
44
  "sha256:20e75c386f1408f11e908a6d3ee1a39316d9728d5719fa06f8d6a5aa45abd83e",
37
45
  ])],