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 +9 -0
- package/LINUX-HELPERS.sha256 +2 -0
- package/README.md +7 -4
- package/SOURCE.md +23 -9
- 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/packages/runkit/build-linux-helper.sh +17 -0
- package/registry-release-policy-v2.json +5 -5
- package/sbom.spdx.json +4 -4
- package/scripts/runkit-contract/core-contract.mjs +1 -1
- package/scripts/runkit-contract/peer-linux-bwrap.mjs +118 -23
- package/scripts/runkit-contract/peer-recovery.mjs +19 -6
- package/scripts/runkit-contract/project-cli-resolver.mjs +4 -0
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
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
2
|
-
|
|
3
|
-
OwlRunKit is
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
11
|
-
`0.2` and Core `0.25.
|
|
12
|
-
`sha256:
|
|
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.
|
|
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.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.
|
|
373
|
+
--exact owlrunkit@0.25.3 --dry-run
|
|
374
374
|
npx --no-install owlrunkit bootstrap --workspace "$PROJECT_ROOT" \
|
|
375
|
-
--exact owlrunkit@0.25.
|
|
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.
|
|
6
|
-
"coreManifestSha256": "sha256:
|
|
7
|
-
"coreSourceRef": "artifact:sha256:
|
|
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.
|
|
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.
|
|
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.
|
|
27
|
-
"shasum": "
|
|
28
|
-
"integrity": "sha512
|
|
29
|
-
"tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.25.
|
|
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.
|
|
6
|
-
"documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.25.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
])],
|