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 +9 -0
- package/README.md +11 -28
- package/SOURCE.md +14 -28
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +9 -9
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/package.json +1 -1
- package/packages/attest/distribution-manifest-v1.json +5 -0
- package/packages/attest/src/quick.mjs +5 -0
- package/registry-release-policy-v2.json +5 -5
- package/sbom.spdx.json +4 -4
- package/scripts/runkit-contract/core-contract.mjs +2 -1
- package/scripts/runkit-contract/peer-linux-arm64 +0 -0
- package/scripts/runkit-contract/peer-linux-bwrap.mjs +109 -0
- package/scripts/runkit-contract/peer-linux-x64 +0 -0
- package/scripts/runkit-contract/peer-linux.c +45 -12
- package/scripts/runkit-contract/peer-recovery.mjs +31 -11
- package/scripts/runkit-contract/project-cli-resolver.mjs +4 -0
- package/LINUX-HELPERS.sha256 +0 -2
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
#
|
|
2
|
-
|
|
3
|
-
OwlRunKit is
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
11
|
-
`0.2` and Core `0.25.
|
|
12
|
-
`sha256:
|
|
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.
|
|
15
|
-
published rollback baseline for this artifact is `owlrunkit@0.25.
|
|
16
|
-
`
|
|
17
|
-
`https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.
|
|
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.
|
|
373
|
+
--exact owlrunkit@0.25.2 --dry-run
|
|
374
374
|
npx --no-install owlrunkit bootstrap --workspace "$PROJECT_ROOT" \
|
|
375
|
-
--exact owlrunkit@0.25.
|
|
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.
|
|
6
|
-
"coreManifestSha256": "sha256:
|
|
7
|
-
"coreSourceRef": "artifact:sha256:
|
|
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.
|
|
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.
|
|
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.
|
|
27
|
-
"shasum": "
|
|
28
|
-
"integrity": "sha512
|
|
29
|
-
"tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.
|
|
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.
|
|
6
|
-
"documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.25.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
Binary file
|
|
@@ -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
|
+
}
|
|
Binary file
|
|
@@ -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),
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
424
|
-
|
|
425
|
-
:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
])],
|
package/LINUX-HELPERS.sha256
DELETED