owlrunkit 0.25.1 → 0.25.2

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.2
4
+
5
+ - Supports managed Linux worker recovery with system Bubblewrap when Landlock
6
+ is unavailable. RunKit verifies the writable attempt, read-only sandbox
7
+ root, hidden host files, and seccomp process and network restrictions before
8
+ selecting the backend.
9
+ - Keeps Landlock as the first Linux backend. If neither backend passes its
10
+ runtime probe, managed peer commands refuse to start.
11
+
3
12
  ## 0.25.1
4
13
 
5
14
  - Adds enforced Linux peer isolation for x86-64 and ARM64 with Landlock and
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.2`
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.2
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.2 --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.2 --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,13 @@ 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, the worker is terminated if its supervisor is killed. The host
1049
+ caller supplies the worker command and peer identity. Existing Desktop sessions and writers using
1067
1050
  legacy shared-directory leases follow their own recovery procedure. An
1068
1051
  existing managed task remains pinned to the core that created it.
1069
1052
 
package/SOURCE.md CHANGED
@@ -1,28 +1,14 @@
1
- # Corresponding source
2
-
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.
7
-
8
- ## Rebuild the Linux helpers
9
-
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.
14
-
15
- ```sh
16
- mkdir -p .build
17
- for platform in linux/arm64 linux/amd64; do
18
- docker run --rm --platform "$platform" \
19
- -v "$PWD:/source:ro" -v "$PWD/.build:/out" \
20
- alpine:3.22@sha256:49340b346798875b5592e5d420241f2f9cfba5f47e62ebb1446a1b34734e898f \
21
- sh -ec 'apk add --no-cache build-base linux-headers; sh /source/packages/runkit/build-linux-helper.sh /out'
22
- done
23
- (cd .build && sha256sum -c ../LINUX-HELPERS.sha256)
24
- ```
25
-
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.
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`.
10
+
11
+ The authoritative release is identified by the npm registry version, shasum,
12
+ integrity, and tarball URL. Local directories, `file:` dependencies, local
13
+ tarballs, Git URLs, workspace links, and mutable dist-tags are not accepted as
14
+ 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.2`, bundling Contract
11
+ `0.2` and Core `0.25.2` with manifest
12
+ `sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc`.
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.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`.
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.2 --dry-run
374
374
  npx --no-install owlrunkit bootstrap --workspace "$PROJECT_ROOT" \
375
- --exact owlrunkit@0.25.1 --apply
375
+ --exact owlrunkit@0.25.2 --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.2",
6
+ "coreManifestSha256": "sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc",
7
+ "coreSourceRef": "artifact:sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc"
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.2",
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.2",
48
+ "coreManifestSha256": "sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc"
49
+ },
45
50
  {
46
51
  "contractVersion": "0.2",
47
52
  "coreVersion": "0.25.1",
@@ -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.2",
29
+ coreManifestSha256: "sha256:da816d343597b91c30e4f96c0a3d765bf5c00d6013d6bee430f58bc701399acc",
30
+ },
26
31
  {
27
32
  contractVersion: "0.2",
28
33
  coreVersion: "0.25.1",
@@ -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.2",
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.1",
27
+ "shasum": "755bdcef5f6df55977ee64d2d4391d1098c00168",
28
+ "integrity": "sha512-+s+PHZXemJIgBo8ScT65+OngqniojRFMxpoUOTjp/Xkq/szB8FqEUOH1BFlokMz6dx64FSOKb30rDPZWC2lS4w==",
29
+ "tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.1.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.2",
6
+ "documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.25.2",
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.2",
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.2"
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.2";
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,109 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import {
3
+ accessSync, constants, lstatSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync,
4
+ } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import path from "node:path";
7
+
8
+ export const BUBBLEWRAP_PEER_BACKEND = "linux_bubblewrap_seccomp_v1";
9
+
10
+ const SANDBOX_HELPER = "/owlrunkit-helper";
11
+ const SANDBOX_WORKER = "/owlrunkit-worker";
12
+ const WORK = "/work";
13
+
14
+ function trustedBubblewrap() {
15
+ 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;
18
+ 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;
30
+ }
31
+ } catch (error) {
32
+ if (!["ENOENT", "EACCES", "ENOTDIR"].includes(error.code)) throw error;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+
38
+ export function bubblewrapPeerArgs({ helper, directory, worker, args, environment }) {
39
+ return [
40
+ "--unshare-user", "--new-session", "--die-with-parent", "--clearenv", "--cap-drop", "ALL",
41
+ "--ro-bind-try", "/lib", "/lib",
42
+ "--ro-bind-try", "/lib64", "/lib64",
43
+ "--ro-bind-try", "/usr/lib", "/usr/lib",
44
+ "--ro-bind-try", "/usr/lib64", "/usr/lib64",
45
+ "--ro-bind-try", "/etc/ld.so.cache", "/etc/ld.so.cache",
46
+ "--dev-bind", "/dev/null", "/dev/null",
47
+ "--ro-bind", helper, SANDBOX_HELPER,
48
+ "--ro-bind", worker, SANDBOX_WORKER,
49
+ "--bind", directory, WORK, "--remount-ro", "/", "--chdir", WORK,
50
+ ...Object.entries({ ...environment, TMPDIR: WORK, OPENSSL_CONF: "/dev/null" })
51
+ .flatMap(([key, value]) => ["--setenv", key, value]),
52
+ "--", SANDBOX_HELPER, "run-bwrap", SANDBOX_WORKER, ...args,
53
+ ];
54
+ }
55
+
56
+ const probeWorker = `
57
+ const assert = require('node:assert/strict');
58
+ const fs = require('node:fs');
59
+ const net = require('node:net');
60
+ const { spawnSync } = require('node:child_process');
61
+ assert.equal(process.cwd(), '/work');
62
+ assert.equal(fs.readFileSync('probe-input', 'utf8'), 'from host');
63
+ assert.throws(() => fs.readFileSync(process.argv[1]), { code: 'ENOENT' });
64
+ assert.throws(() => fs.writeFileSync('/probe-outside-work', 'no'), { code: 'EROFS' });
65
+ assert.equal(spawnSync(process.execPath, ['--version']).error?.code, 'EPERM');
66
+ const server = net.createServer();
67
+ server.once('error', error => {
68
+ assert.equal(error.code, 'EPERM');
69
+ fs.writeFileSync('probe-result', 'confined');
70
+ });
71
+ server.listen(0, '127.0.0.1', () => {
72
+ server.close();
73
+ throw new Error('socket unexpectedly succeeded');
74
+ });
75
+ `;
76
+
77
+ export function probeBubblewrapPeer(helper) {
78
+ const executable = trustedBubblewrap();
79
+ if (!executable) return { available: false, reason: "trusted system Bubblewrap not found" };
80
+ const directory = mkdtempSync(path.join(realpathSync(tmpdir()), "owlrunkit-bwrap-probe-"));
81
+ try {
82
+ const outside = path.join(directory, "outside");
83
+ const work = path.join(directory, "work");
84
+ writeFileSync(outside, "outside", { flag: "wx", mode: 0o600 });
85
+ // Keep the sentinel outside the only writable bind.
86
+ const scratch = mkdtempSync(work);
87
+ writeFileSync(path.join(scratch, "probe-input"), "from host", { flag: "wx", mode: 0o600 });
88
+ const result = spawnSync(executable, bubblewrapPeerArgs({
89
+ helper, directory: scratch, worker: realpathSync(process.execPath),
90
+ args: ["-e", probeWorker, outside], environment: {},
91
+ }), {
92
+ cwd: scratch, env: {}, stdio: ["ignore", "ignore", "pipe"],
93
+ encoding: "utf8", timeout: 5000, maxBuffer: 65536,
94
+ });
95
+ if (result.status !== 0) {
96
+ return { available: false, reason: result.stderr?.trim() || result.error?.code
97
+ || `Bubblewrap self-test exited ${result.status}` };
98
+ }
99
+ const confirmation = path.join(scratch, "probe-result");
100
+ if (readFileSync(confirmation, "utf8") !== "confined") {
101
+ return { available: false, reason: "Bubblewrap self-test did not confirm enforcement" };
102
+ }
103
+ return { available: true, id: BUBBLEWRAP_PEER_BACKEND, executable, helper };
104
+ } catch (error) {
105
+ return { available: false, reason: `Bubblewrap self-test failed: ${error.code ?? error.message}` };
106
+ } finally {
107
+ rmSync(directory, { recursive: true, force: true });
108
+ }
109
+ }
@@ -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,7 @@ 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
14
  import { safeIdentifier } from "./provenance-common.mjs";
14
15
  import { resolveVerificationEnvelopeBackendV1 } from "./verification-envelope.mjs";
15
16
 
@@ -208,7 +209,15 @@ export function resolvePeerRecoveryBackend() {
208
209
  return { available: true, id: "linux_landlock_seccomp_v1", executable,
209
210
  digest: digest(regularBytes(executable, CHECKPOINT_LIMIT)) };
210
211
  }
211
- return { available: false, reason: result.stderr?.trim() || result.error?.code };
212
+ const reason = result.stderr?.trim() || result.error?.code;
213
+ if (result.status !== 77) return { available: false, reason };
214
+ const fallback = probeBubblewrapPeer(executable);
215
+ if (!fallback.available) {
216
+ return { available: false, reason: [reason, fallback.reason].filter(Boolean).join("; ") };
217
+ }
218
+ 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])) };
212
221
  } catch (error) { return { available: false, reason: error.code }; }
213
222
  }
214
223
  return resolveVerificationEnvelopeBackendV1();
@@ -420,16 +429,27 @@ export async function runPeerTask({ workspaceRoot, taskId, agent, generation, co
420
429
  }
421
430
 
422
431
  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"],
432
+ const environment = {
433
+ PATH: "/usr/bin:/bin", TMPDIR: directory, OWLRUNKIT_PEER_ATTEMPT: id,
434
+ OWLRUNKIT_PEER_PATHS: JSON.stringify(state.paths),
435
+ };
436
+ let args;
437
+ let env = environment;
438
+ if (backend.id === BUBBLEWRAP_PEER_BACKEND) {
439
+ args = bubblewrapPeerArgs({
440
+ helper: backend.helper, directory, worker: executable,
441
+ args: command.slice(1), environment,
442
+ });
443
+ env = {};
444
+ } else if (backend.id === "linux_landlock_seccomp_v1") {
445
+ args = ["run", String(process.pid), directory, executable, ...command.slice(1)];
446
+ // An isolated worker cannot read host OpenSSL configuration.
447
+ env.OPENSSL_CONF = "/dev/null";
448
+ } else {
449
+ args = ["-p", sandboxPolicy(executable, directory), executable, ...command.slice(1)];
450
+ }
451
+ const child = spawn(backend.executable, args, {
452
+ cwd: directory, env, stdio: ["pipe", "pipe", "pipe"],
433
453
  });
434
454
  let watcher, pending = Buffer.alloc(0), sentWake, protocolError = null;
435
455
  let diagnostic = Buffer.alloc(0);
@@ -28,10 +28,14 @@ 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",
31
32
  ]);
32
33
  const EXACT_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
33
34
  const SHA256_REF = /^sha256:[a-f0-9]{64}$/;
34
35
  const TRUSTED_PRIOR_CORE_IDENTITIES = new Map([
36
+ ["0.25.1", new Set([
37
+ "sha256:0934a076b147847a4623bc89263b677c88088b493495fc74f11485985b7903b5",
38
+ ])],
35
39
  ["0.25.0", new Set([
36
40
  "sha256:20e75c386f1408f11e908a6d3ee1a39316d9728d5719fa06f8d6a5aa45abd83e",
37
41
  ])],
@@ -1,2 +0,0 @@
1
- acbc0a76363cbb81700fdb3e7924c6b1403b30af82dbbda9ad8855a6bb53f988 peer-linux-arm64
2
- c6a4b270df2482f52ef1463b6ced5205f34c9abda8d4af0a55a1ac3273985436 peer-linux-x64