owlrunkit 0.25.2 → 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,14 @@
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
+
3
12
  ## 0.25.2
4
13
 
5
14
  - Supports managed Linux worker recovery with system Bubblewrap when Landlock
@@ -0,0 +1,2 @@
1
+ 39e177cebac50c87514e19df991266970a5814eb468ec716250d6e731a73a40d peer-linux-x64
2
+ ad5ecf24f6c13233edb43c8ce0b8aa91b15134e710252ae6e82de39630d67e0e peer-linux-arm64
package/README.md CHANGED
@@ -136,7 +136,7 @@ separate authority boundaries.
136
136
 
137
137
  ## Install
138
138
 
139
- This package is the lifecycle-stable, registry-gated `owlrunkit@0.25.2`
139
+ This package is the lifecycle-stable, registry-gated `owlrunkit@0.25.3`
140
140
  artifact. Registry adoption remains fail-closed until its exact official npm
141
141
  version, shasum, integrity, and tarball URL are independently verified. Local
142
142
  tarballs remain candidate-test inputs and are never adoption provenance. Until
@@ -144,7 +144,7 @@ that registry evidence exists, use the package only for isolated candidate
144
144
  verification; do not treat the command below as available release truth.
145
145
 
146
146
  ```bash
147
- npm install --save-exact owlrunkit@0.25.2
147
+ npm install --save-exact owlrunkit@0.25.3
148
148
  ```
149
149
 
150
150
  Use `npx --no-install owlrunkit` for project commands. The package bootstrap
@@ -160,9 +160,9 @@ the project Config must independently bind the same Core.
160
160
  ```bash
161
161
  npx --no-install owlrunkit --version
162
162
  npx --no-install owlrunkit bootstrap --workspace /absolute/path/to/project \
163
- --exact owlrunkit@0.25.2 --dry-run
163
+ --exact owlrunkit@0.25.3 --dry-run
164
164
  npx --no-install owlrunkit bootstrap --workspace /absolute/path/to/project \
165
- --exact owlrunkit@0.25.2 --apply
165
+ --exact owlrunkit@0.25.3 --apply
166
166
  npx --no-install owlrunkit init --workspace /absolute/path/to/project
167
167
  npx --no-install owlrunkit doctor --workspace /absolute/path/to/project
168
168
  npx --no-install owlrunkit inspect --workspace /absolute/path/to/project --json --compact
@@ -1045,6 +1045,9 @@ first. If Landlock is unavailable, it can use a trusted system Bubblewrap
1045
1045
  executable with user and mount namespaces. The Bubblewrap probe verifies
1046
1046
  that the attempt is writable, host files are hidden, and new processes and
1047
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.
1048
1051
  On Linux, the worker is terminated if its supervisor is killed. The host
1049
1052
  caller supplies the worker command and peer identity. Existing Desktop sessions and writers using
1050
1053
  legacy shared-directory leases follow their own recovery procedure. An
package/SOURCE.md CHANGED
@@ -1,12 +1,26 @@
1
- # Source
2
-
3
- OwlRunKit is maintained in the private OwlCoda source repository. The npm
4
- tarball contains the JavaScript runtime, the read-only verifier and the C
5
- source for the two Linux helper binaries. The distributed binaries link musl
6
- statically and include its copyright notice. Build the helper on each supported
7
- Linux architecture with `sh packages/runkit/build-linux-helper.sh <output>`
8
- in Alpine Linux with `build-base` and `linux-headers`, then pass their output directory to
9
- `packages/runkit/build-package.mjs --linux-helpers`.
1
+ # Corresponding source
2
+
3
+ OwlRunKit is distributed under GPL-3.0-or-later. The corresponding public
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.
8
+
9
+ ## Rebuild the Linux helpers
10
+
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`.
13
+
14
+ ```sh
15
+ mkdir -p .build
16
+ for platform in linux/arm64 linux/amd64; do
17
+ docker run --rm --platform "$platform" \
18
+ -v "$PWD:/source:ro" -v "$PWD/.build:/out" \
19
+ alpine:3.22@sha256:49340b346798875b5592e5d420241f2f9cfba5f47e62ebb1446a1b34734e898f \
20
+ sh -ec 'apk add --no-cache build-base linux-headers; sh /source/packages/runkit/build-linux-helper.sh /out'
21
+ done
22
+ (cd .build && sha256sum -c ../LINUX-HELPERS.sha256)
23
+ ```
10
24
 
11
25
  The authoritative release is identified by the npm registry version, shasum,
12
26
  integrity, and tarball URL. Local directories, `file:` dependencies, local
@@ -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.2`, bundling Contract
11
- `0.2` and Core `0.25.2` with manifest
12
- `sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc`.
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.2` is independently verified. The
15
- published rollback baseline for this artifact is `owlrunkit@0.25.1`, with SHA-1
16
- `755bdcef5f6df55977ee64d2d4391d1098c00168` and canonical npm tarball URL
17
- `https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.1.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.2 --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.2 --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.2",
6
- "coreManifestSha256": "sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc",
7
- "coreSourceRef": "artifact:sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc"
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.2",
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,11 @@
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
+ },
45
50
  {
46
51
  "contractVersion": "0.2",
47
52
  "coreVersion": "0.25.2",
@@ -23,6 +23,11 @@ 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
+ },
26
31
  {
27
32
  contractVersion: "0.2",
28
33
  coreVersion: "0.25.2",
@@ -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.2",
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.1",
27
- "shasum": "755bdcef5f6df55977ee64d2d4391d1098c00168",
28
- "integrity": "sha512-+s+PHZXemJIgBo8ScT65+OngqniojRFMxpoUOTjp/Xkq/szB8FqEUOH1BFlokMz6dx64FSOKb30rDPZWC2lS4w==",
29
- "tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.1.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.2",
6
- "documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.25.2",
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.2",
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.2"
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.2";
17
+ export const CORE_VERSION = "0.25.3";
18
18
  export const RUNTIME_ROOT = ".owlcoda/runkit";
19
19
 
20
20
  const CORE_FILES = [
@@ -1,6 +1,8 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import {
3
- accessSync, constants, lstatSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync,
4
+ accessSync, closeSync, constants, fstatSync, lstatSync, mkdtempSync, openSync,
5
+ readFileSync, realpathSync, rmSync, statfsSync, writeFileSync,
4
6
  } from "node:fs";
5
7
  import { tmpdir } from "node:os";
6
8
  import path from "node:path";
@@ -10,29 +12,119 @@ export const BUBBLEWRAP_PEER_BACKEND = "linux_bubblewrap_seccomp_v1";
10
12
  const SANDBOX_HELPER = "/owlrunkit-helper";
11
13
  const SANDBOX_WORKER = "/owlrunkit-worker";
12
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;
13
21
 
14
- function trustedBubblewrap() {
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) {
15
82
  const directories = ["/usr/bin", "/bin", "/usr/local/bin", ...(process.env.PATH ?? "").split(":")];
16
- for (const directory of new Set(directories)) {
17
- if (!path.isAbsolute(directory)) continue;
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;
18
89
  try {
19
- const file = realpathSync(path.join(directory, "bwrap"));
20
- const stat = lstatSync(file);
21
- if (!stat.isFile() || stat.uid !== 0 || (stat.mode & 0o6022)) continue;
22
- accessSync(file, constants.X_OK);
23
- let parent = path.dirname(file);
24
- for (;;) {
25
- const entry = lstatSync(parent);
26
- if (!entry.isDirectory() || entry.uid !== 0 || (entry.mode & 0o022)) break;
27
- const next = path.dirname(parent);
28
- if (parent === next) return file;
29
- parent = next;
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";
30
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 };
31
109
  } catch (error) {
32
- if (!["ENOENT", "EACCES", "ENOTDIR"].includes(error.code)) throw error;
110
+ if (!["ENOENT", "EACCES", "ENOTDIR", "ELOOP"].includes(error.code)) throw error;
111
+ } finally {
112
+ if (fd !== undefined && !trusted) closeSync(fd);
33
113
  }
34
114
  }
35
- return null;
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
+ }
36
128
  }
37
129
 
38
130
  export function bubblewrapPeerArgs({ helper, directory, worker, args, environment }) {
@@ -75,21 +167,22 @@ const probeWorker = `
75
167
  `;
76
168
 
77
169
  export function probeBubblewrapPeer(helper) {
78
- const executable = trustedBubblewrap();
79
- if (!executable) return { available: false, reason: "trusted system Bubblewrap not found" };
80
170
  const directory = mkdtempSync(path.join(realpathSync(tmpdir()), "owlrunkit-bwrap-probe-"));
171
+ let launcher;
81
172
  try {
173
+ launcher = openTrustedBubblewrap();
174
+ if (launcher.fd === undefined) return { available: false, reason: launcher.reason };
82
175
  const outside = path.join(directory, "outside");
83
176
  const work = path.join(directory, "work");
84
177
  writeFileSync(outside, "outside", { flag: "wx", mode: 0o600 });
85
178
  // Keep the sentinel outside the only writable bind.
86
179
  const scratch = mkdtempSync(work);
87
180
  writeFileSync(path.join(scratch, "probe-input"), "from host", { flag: "wx", mode: 0o600 });
88
- const result = spawnSync(executable, bubblewrapPeerArgs({
181
+ const result = spawnSync(FD_EXECUTABLE, bubblewrapPeerArgs({
89
182
  helper, directory: scratch, worker: realpathSync(process.execPath),
90
183
  args: ["-e", probeWorker, outside], environment: {},
91
184
  }), {
92
- cwd: scratch, env: {}, stdio: ["ignore", "ignore", "pipe"],
185
+ cwd: scratch, env: {}, stdio: ["ignore", "ignore", "pipe", launcher.fd],
93
186
  encoding: "utf8", timeout: 5000, maxBuffer: 65536,
94
187
  });
95
188
  if (result.status !== 0) {
@@ -100,10 +193,12 @@ export function probeBubblewrapPeer(helper) {
100
193
  if (readFileSync(confirmation, "utf8") !== "confined") {
101
194
  return { available: false, reason: "Bubblewrap self-test did not confirm enforcement" };
102
195
  }
103
- return { available: true, id: BUBBLEWRAP_PEER_BACKEND, executable, helper };
196
+ const { executable, sandboxDigest, trust } = launcher;
197
+ return { available: true, id: BUBBLEWRAP_PEER_BACKEND, executable, sandboxDigest, trust, helper };
104
198
  } catch (error) {
105
199
  return { available: false, reason: `Bubblewrap self-test failed: ${error.code ?? error.message}` };
106
200
  } finally {
201
+ if (launcher?.fd !== undefined) closeSync(launcher.fd);
107
202
  rmSync(directory, { recursive: true, force: true });
108
203
  }
109
204
  }
@@ -10,7 +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 { BUBBLEWRAP_PEER_BACKEND, bubblewrapPeerArgs, probeBubblewrapPeer } from "./peer-linux-bwrap.mjs";
13
+ import {
14
+ BUBBLEWRAP_PEER_BACKEND, bubblewrapPeerArgs, probeBubblewrapPeer, spawnBubblewrapPeer,
15
+ } from "./peer-linux-bwrap.mjs";
14
16
  import { safeIdentifier } from "./provenance-common.mjs";
15
17
  import { resolveVerificationEnvelopeBackendV1 } from "./verification-envelope.mjs";
16
18
 
@@ -216,8 +218,7 @@ export function resolvePeerRecoveryBackend() {
216
218
  return { available: false, reason: [reason, fallback.reason].filter(Boolean).join("; ") };
217
219
  }
218
220
  const helperHash = digest(regularBytes(executable, CHECKPOINT_LIMIT));
219
- const sandboxHash = digest(regularBytes(fallback.executable, CHECKPOINT_LIMIT));
220
- return { ...fallback, digest: digest(JSON.stringify([helperHash, sandboxHash])) };
221
+ return { ...fallback, digest: digest(JSON.stringify([helperHash, fallback.sandboxDigest])) };
221
222
  } catch (error) { return { available: false, reason: error.code }; }
222
223
  }
223
224
  return resolveVerificationEnvelopeBackendV1();
@@ -418,7 +419,8 @@ export async function runPeerTask({ workspaceRoot, taskId, agent, generation, co
418
419
  const directory = mkdtempSync(path.join(realpathSync(tmpdir()), "owlrunkit-peer-"));
419
420
  materialize(checkpoint(task, state).files, directory);
420
421
  const attempt = { id, directory, backend: backend.id,
421
- ...(backend.digest ? { backendSha256: backend.digest } : {}) };
422
+ ...(backend.digest ? { backendSha256: backend.digest } : {}),
423
+ ...(backend.trust ? { backendTrust: backend.trust } : {}) };
422
424
  const claimed = append(task, state, {
423
425
  attempt, phase: "running", processExit: null, event: "peer_attempt_started",
424
426
  });
@@ -448,9 +450,20 @@ export async function runPeerTask({ workspaceRoot, taskId, agent, generation, co
448
450
  } else {
449
451
  args = ["-p", sandboxPolicy(executable, directory), executable, ...command.slice(1)];
450
452
  }
451
- const child = spawn(backend.executable, args, {
453
+ const options = {
452
454
  cwd: directory, env, stdio: ["pipe", "pipe", "pipe"],
453
- });
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
+ }
454
467
  let watcher, pending = Buffer.alloc(0), sentWake, protocolError = null;
455
468
  let diagnostic = Buffer.alloc(0);
456
469
  const stop = error => {
@@ -29,10 +29,14 @@ const SELF_NORMALIZED_QUICK_CORE_VERSIONS = new Set([
29
29
  "0.25.0",
30
30
  "0.25.1",
31
31
  "0.25.2",
32
+ "0.25.3",
32
33
  ]);
33
34
  const EXACT_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
34
35
  const SHA256_REF = /^sha256:[a-f0-9]{64}$/;
35
36
  const TRUSTED_PRIOR_CORE_IDENTITIES = new Map([
37
+ ["0.25.2", new Set([
38
+ "sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc",
39
+ ])],
36
40
  ["0.25.1", new Set([
37
41
  "sha256:0934a076b147847a4623bc89263b677c88088b493495fc74f11485985b7903b5",
38
42
  ])],