owlrunkit 0.24.1 → 0.25.0
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 +10 -0
- package/README.md +53 -4
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +20 -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 +5 -5
- package/scripts/runkit-contract/core-contract.mjs +2 -1
- package/scripts/runkit-contract/peer-recovery.mjs +442 -0
- package/scripts/runkit-contract/project-cli-resolver.mjs +4 -0
- package/scripts/runkit-contract/runkit-cli.mjs +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.0
|
|
4
|
+
|
|
5
|
+
- Adds opt-in same-host peer recovery for macOS-contained workers. A cooperating
|
|
6
|
+
agent can request a wake, observe a fresh correlated checkpoint, or continue
|
|
7
|
+
from the last checkpoint as the designated successor.
|
|
8
|
+
- Records a single successor generation and rejects writes and checkpoint
|
|
9
|
+
submissions from the superseded attempt. Recovery remains scoped to the
|
|
10
|
+
managed task and preserves the existing manual path for legacy leases.
|
|
11
|
+
- Retains 0.24.1 as the exact published rollback baseline.
|
|
12
|
+
|
|
3
13
|
## 0.24.1
|
|
4
14
|
|
|
5
15
|
- Corrects the optional Stop hook to notify on RunKit data file changes, including
|
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.
|
|
139
|
+
This package is the lifecycle-stable, registry-gated `owlrunkit@0.25.0`
|
|
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.
|
|
147
|
+
npm install --save-exact owlrunkit@0.25.0
|
|
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.
|
|
163
|
+
--exact owlrunkit@0.25.0 --dry-run
|
|
164
164
|
npx --no-install owlrunkit bootstrap --workspace /absolute/path/to/project \
|
|
165
|
-
--exact owlrunkit@0.
|
|
165
|
+
--exact owlrunkit@0.25.0 --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
|
|
@@ -995,6 +995,55 @@ Installing the package enables nothing and changes no global configuration,
|
|
|
995
995
|
shared Skill or other workspace. This is same-session synchronous Stop
|
|
996
996
|
continuation; fully idle Desktop wake requires a separate supported ingress.
|
|
997
997
|
|
|
998
|
+
## Managed local peer recovery
|
|
999
|
+
|
|
1000
|
+
On macOS, opt a new task into peer recovery before starting its writer. The
|
|
1001
|
+
project controller names the current agent, successor, and exact checkpoint
|
|
1002
|
+
paths in `peer.json`, for example:
|
|
1003
|
+
|
|
1004
|
+
```json
|
|
1005
|
+
{"owner":"alpha","successor":"bravo","paths":["result.txt"]}
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
```bash
|
|
1009
|
+
npx --no-install owlrunkit peer create --workspace . --task repair --request peer.json
|
|
1010
|
+
npx --no-install owlrunkit peer run --workspace . --task repair --agent alpha --generation 1 -- /absolute/worker
|
|
1011
|
+
npx --no-install owlrunkit peer inspect --workspace . --task repair
|
|
1012
|
+
npx --no-install owlrunkit peer recover --workspace . --task repair --agent bravo --generation 1 --revision 2
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
Run the last two commands from the cooperating agent while it is investigating
|
|
1016
|
+
an interruption. Use the actual generation and revision returned by `inspect`.
|
|
1017
|
+
Recovery requests a wake from the current worker. A matching, changed checkpoint
|
|
1018
|
+
keeps that worker in charge. If wake remains unconfirmed after the bounded wait,
|
|
1019
|
+
recovery can move ownership to the designated successor because the current
|
|
1020
|
+
attempt already runs in an isolated directory. One durable revision binds the
|
|
1021
|
+
successor generation to the last accepted checkpoint. The successor explicitly
|
|
1022
|
+
runs its own worker using that generation. An ACK or timeout is not a proof of
|
|
1023
|
+
worker death. Retry the same observed revision to read back a race or recovery.
|
|
1024
|
+
|
|
1025
|
+
Use a local worker that speaks newline-delimited JSON. RunKit seeds the attempt
|
|
1026
|
+
directory from the previous checkpoint and sets `OWLRUNKIT_PEER_PATHS` to the
|
|
1027
|
+
ordered path array. The worker reads a wake challenge from stdin as
|
|
1028
|
+
`{"type":"wake","wakeId":"..."}`. Stdout is reserved for protocol messages.
|
|
1029
|
+
Report work as `{"type":"checkpoint","wakeId":"...","files":[{"path":"result.txt","content":"..."}]}`.
|
|
1030
|
+
Use the wake ID only when reporting progress in response to that challenge.
|
|
1031
|
+
The `files` array covers the exact ordered paths, with canonical base64 bytes
|
|
1032
|
+
or `null` for a missing file. The decoded checkpoint limit is 1 MiB. Emit
|
|
1033
|
+
`finish` with the same `files` shape to record the result. Human diagnostics
|
|
1034
|
+
go to stderr. `inspect` returns the checkpoint path and hash for handoff and
|
|
1035
|
+
review. Only accepted checkpoints seed the next attempt, and the previous
|
|
1036
|
+
scratch directory is reported for inspection while it remains on disk.
|
|
1037
|
+
|
|
1038
|
+
The macOS backend confines each worker's writes to its own attempt and denies
|
|
1039
|
+
network and additional processes. The host caller explicitly authorizes the
|
|
1040
|
+
worker command, and supplies the peer identity. This is a supported boundary
|
|
1041
|
+
for contained workers and their checkpoints. An unmanaged file lease, a raw
|
|
1042
|
+
shared-directory writer, or an existing Desktop session needs its own recovery
|
|
1043
|
+
procedure. Legacy lease records and `control.lock` are untouched. The result
|
|
1044
|
+
is task evidence for the existing acceptance process. Canonical source
|
|
1045
|
+
integration remains a separate authorized step.
|
|
1046
|
+
|
|
998
1047
|
## Shared Skill upgrades
|
|
999
1048
|
|
|
1000
1049
|
Process-wide Codex Skill updates automatically use the registered fleet:
|
|
@@ -7,18 +7,29 @@ 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.
|
|
11
|
-
`0.2` and Core `0.
|
|
12
|
-
`sha256:
|
|
10
|
+
Bundled registry-gated artifact: standalone `owlrunkit@0.25.0`, bundling Contract
|
|
11
|
+
`0.2` and Core `0.25.0` with manifest
|
|
12
|
+
`sha256:20e75c386f1408f11e908a6d3ee1a39316d9728d5719fa06f8d6a5aa45abd83e`.
|
|
13
13
|
Registry adoption remains fail-closed until exact official npm registry
|
|
14
|
-
provenance for `owlrunkit@0.
|
|
15
|
-
published rollback baseline for this artifact is `owlrunkit@0.24.
|
|
16
|
-
`
|
|
17
|
-
`https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.24.
|
|
14
|
+
provenance for `owlrunkit@0.25.0` is independently verified. The
|
|
15
|
+
published rollback baseline for this artifact is `owlrunkit@0.24.1`, with SHA-1
|
|
16
|
+
`b70b857c128f67d19d401acaa096a6cf9f1f348e` and canonical npm tarball URL
|
|
17
|
+
`https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.24.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.
|
|
21
21
|
|
|
22
|
+
## Optional managed local peer recovery
|
|
23
|
+
|
|
24
|
+
For a task already enrolled in `owlrunkit peer` with an isolated worker, inspect
|
|
25
|
+
the task when your cooperating agent stops progressing. As the designated
|
|
26
|
+
successor, run `peer recover` with the observed generation and revision. A
|
|
27
|
+
new checkpoint tied to the wake challenge keeps the original agent active.
|
|
28
|
+
When recovery grants a successor generation, explicitly run the authorized
|
|
29
|
+
worker in that generation and resume from its seeded checkpoint. Treat an ACK
|
|
30
|
+
without progress as unconfirmed. Keep legacy shared-workspace leases on the
|
|
31
|
+
manual recovery path. Use the package README and `peer --help` for setup.
|
|
32
|
+
|
|
22
33
|
## Optional file-event continuation
|
|
23
34
|
|
|
24
35
|
RunKit includes `scripts/runkit-contract/file-event-stop-hook.mjs` for an
|
|
@@ -357,9 +368,9 @@ below has no CLI entry.
|
|
|
357
368
|
|
|
358
369
|
```bash
|
|
359
370
|
npx --no-install owlrunkit bootstrap --workspace "$PROJECT_ROOT" \
|
|
360
|
-
--exact owlrunkit@0.
|
|
371
|
+
--exact owlrunkit@0.25.0 --dry-run
|
|
361
372
|
npx --no-install owlrunkit bootstrap --workspace "$PROJECT_ROOT" \
|
|
362
|
-
--exact owlrunkit@0.
|
|
373
|
+
--exact owlrunkit@0.25.0 --apply
|
|
363
374
|
npx --no-install owlrunkit init --workspace "$PROJECT_ROOT"
|
|
364
375
|
npx --no-install owlrunkit inspect --json --workspace "$PROJECT_ROOT"
|
|
365
376
|
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.
|
|
6
|
-
"coreManifestSha256": "sha256:
|
|
7
|
-
"coreSourceRef": "artifact:sha256:
|
|
5
|
+
"coreVersion": "0.25.0",
|
|
6
|
+
"coreManifestSha256": "sha256:20e75c386f1408f11e908a6d3ee1a39316d9728d5719fa06f8d6a5aa45abd83e",
|
|
7
|
+
"coreSourceRef": "artifact:sha256:20e75c386f1408f11e908a6d3ee1a39316d9728d5719fa06f8d6a5aa45abd83e"
|
|
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.
|
|
3
|
+
"version": "0.25.0",
|
|
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.0",
|
|
48
|
+
"coreManifestSha256": "sha256:20e75c386f1408f11e908a6d3ee1a39316d9728d5719fa06f8d6a5aa45abd83e"
|
|
49
|
+
},
|
|
45
50
|
{
|
|
46
51
|
"contractVersion": "0.2",
|
|
47
52
|
"coreVersion": "0.24.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.0",
|
|
29
|
+
coreManifestSha256: "sha256:20e75c386f1408f11e908a6d3ee1a39316d9728d5719fa06f8d6a5aa45abd83e",
|
|
30
|
+
},
|
|
26
31
|
{
|
|
27
32
|
contractVersion: "0.2",
|
|
28
33
|
coreVersion: "0.24.1",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": "OwlCodaRunKitRegistryReleasePolicyV2",
|
|
3
3
|
"status": "release_artifact_registry_gated",
|
|
4
4
|
"packageName": "owlrunkit",
|
|
5
|
-
"artifactVersion": "0.
|
|
5
|
+
"artifactVersion": "0.25.0",
|
|
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.24.
|
|
27
|
-
"shasum": "
|
|
28
|
-
"integrity": "sha512-
|
|
29
|
-
"tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.24.
|
|
26
|
+
"version": "0.24.1",
|
|
27
|
+
"shasum": "b70b857c128f67d19d401acaa096a6cf9f1f348e",
|
|
28
|
+
"integrity": "sha512-8Fr5XTAbtRlxyYboDSB8C257sxSWWiAFqGU6yexF99NXgilJ3pbxtMwq4FcbrlHN5V6yJLME04vTceI2s9F4bA==",
|
|
29
|
+
"tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.24.1.tgz"
|
|
30
30
|
},
|
|
31
31
|
"registryEvidenceMustBeExternal": true,
|
|
32
32
|
"registryEvidenceSchemaVersion": "OwlCodaRunKitRegistryReleaseEvidenceV1",
|
package/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "owlrunkit-0.
|
|
6
|
-
"documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.
|
|
5
|
+
"name": "owlrunkit-0.25.0",
|
|
6
|
+
"documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.25.0",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-
|
|
8
|
+
"created": "2026-11-11T00:00:00Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Organization: OwlCoda"
|
|
11
11
|
]
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"name": "owlrunkit",
|
|
16
16
|
"SPDXID": "SPDXRef-Package-OwlRunKit",
|
|
17
|
-
"versionInfo": "0.
|
|
17
|
+
"versionInfo": "0.25.0",
|
|
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.
|
|
27
|
+
"referenceLocator": "pkg:npm/owlrunkit@0.25.0"
|
|
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.
|
|
17
|
+
export const CORE_VERSION = "0.25.0";
|
|
18
18
|
export const RUNTIME_ROOT = ".owlcoda/runkit";
|
|
19
19
|
|
|
20
20
|
const CORE_FILES = [
|
|
@@ -42,6 +42,7 @@ const CORE_FILES = [
|
|
|
42
42
|
"lifecycle-orchestration.mjs",
|
|
43
43
|
"offline-store.mjs",
|
|
44
44
|
"outcome-presentation.mjs",
|
|
45
|
+
"peer-recovery.mjs",
|
|
45
46
|
"onboarding-doctor.mjs",
|
|
46
47
|
"onboarding-bootstrap.mjs",
|
|
47
48
|
"owner-authority-trust.mjs",
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
closeSync, constants, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync,
|
|
5
|
+
mkdtempSync, openSync, readSync, readdirSync, realpathSync, unlinkSync,
|
|
6
|
+
watch, writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
import { RUNTIME_ROOT, currentCoreIdentity } from "./core-contract.mjs";
|
|
12
|
+
import { safeIdentifier } from "./provenance-common.mjs";
|
|
13
|
+
import { resolveVerificationEnvelopeBackendV1 } from "./verification-envelope.mjs";
|
|
14
|
+
|
|
15
|
+
const SCHEMA = "OwlRunKitPeerRecoveryRevisionV1";
|
|
16
|
+
const CHECKPOINT_LIMIT = 1024 * 1024;
|
|
17
|
+
const RECORD_LIMIT = 2 * CHECKPOINT_LIMIT;
|
|
18
|
+
const MAX_REVISIONS = 4096;
|
|
19
|
+
const REVISION_NAME = /^([0-9]{6})\.json$/u;
|
|
20
|
+
|
|
21
|
+
function digest(value) {
|
|
22
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function realDirectory(ref) {
|
|
26
|
+
if (!lstatSync(ref).isDirectory() || realpathSync(ref) !== ref) {
|
|
27
|
+
throw new Error("peer_recovery_redirected_directory");
|
|
28
|
+
}
|
|
29
|
+
return ref;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function directoryBelow(root, segments, create) {
|
|
33
|
+
let current = root;
|
|
34
|
+
for (const segment of segments) {
|
|
35
|
+
current = path.join(current, segment);
|
|
36
|
+
if (create && !existsSync(current)) {
|
|
37
|
+
try { mkdirSync(current, { mode: 0o700 }); }
|
|
38
|
+
catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
39
|
+
}
|
|
40
|
+
realDirectory(current);
|
|
41
|
+
}
|
|
42
|
+
return current;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function taskDirectory(workspaceRoot, taskId, create = false) {
|
|
46
|
+
safeIdentifier(taskId, "peer recovery task");
|
|
47
|
+
const root = realpathSync(workspaceRoot);
|
|
48
|
+
const runtime = directoryBelow(root, RUNTIME_ROOT.split("/"), false);
|
|
49
|
+
const directory = directoryBelow(runtime, ["peer-recovery", taskId], create);
|
|
50
|
+
return { root, directory, taskId, core: currentCoreIdentity() };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function regularBytes(ref, limit) {
|
|
54
|
+
const stat = lstatSync(ref);
|
|
55
|
+
if (!stat.isFile() || stat.size > limit || realpathSync(ref) !== ref) {
|
|
56
|
+
throw new Error("peer_recovery_invalid_file");
|
|
57
|
+
}
|
|
58
|
+
const fd = openSync(ref, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
59
|
+
try {
|
|
60
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
61
|
+
let length = 0;
|
|
62
|
+
while (length < buffer.length) {
|
|
63
|
+
const bytes = readSync(fd, buffer, length, buffer.length - length, null);
|
|
64
|
+
if (bytes === 0) break;
|
|
65
|
+
length += bytes;
|
|
66
|
+
}
|
|
67
|
+
if (length > limit) throw new Error("peer_recovery_file_too_large");
|
|
68
|
+
return buffer.subarray(0, length);
|
|
69
|
+
} finally { closeSync(fd); }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function revisionPath(task, revision) {
|
|
73
|
+
return path.join(task.directory, `${String(revision).padStart(6, "0")}.json`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readRevision(task, revision) {
|
|
77
|
+
const record = JSON.parse(regularBytes(revisionPath(task, revision), RECORD_LIMIT).toString("utf8"));
|
|
78
|
+
if (record.schemaVersion !== SCHEMA || record.workspaceRoot !== task.root
|
|
79
|
+
|| record.taskId !== task.taskId || record.revision !== revision
|
|
80
|
+
|| record.authorizationGranted !== false || !Number.isSafeInteger(record.generation)
|
|
81
|
+
|| record.generation < 1 || !Number.isSafeInteger(record.checkpointRevision)
|
|
82
|
+
|| record.checkpointRevision < 1 || record.checkpointRevision > revision
|
|
83
|
+
|| !["ready", "running", "exited", "completed"].includes(record.phase)
|
|
84
|
+
|| JSON.stringify(validatePaths(record.paths)) !== JSON.stringify(record.paths)) {
|
|
85
|
+
throw new Error("peer_recovery_revision_invalid");
|
|
86
|
+
}
|
|
87
|
+
if (record.core?.coreManifestSha256 !== task.core.coreManifestSha256
|
|
88
|
+
|| record.core?.coreVersion !== task.core.coreVersion) {
|
|
89
|
+
throw new Error("peer_recovery_core_pin_mismatch");
|
|
90
|
+
}
|
|
91
|
+
safeIdentifier(record.owner, "recorded current peer");
|
|
92
|
+
safeIdentifier(record.successor, "recorded successor peer");
|
|
93
|
+
return record;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function current(task) {
|
|
97
|
+
const revisions = readdirSync(task.directory).map(name => REVISION_NAME.exec(name))
|
|
98
|
+
.filter(Boolean).map(match => Number(match[1])).sort((a, b) => a - b);
|
|
99
|
+
if (revisions.length === 0) throw new Error("peer_recovery_task_uninitialized");
|
|
100
|
+
if (revisions.some((revision, index) => revision !== index + 1)) {
|
|
101
|
+
throw new Error("peer_recovery_revision_gap");
|
|
102
|
+
}
|
|
103
|
+
return readRevision(task, revisions.length);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Exactly one contender can link the next revision. A crash before the link
|
|
107
|
+
// leaves only ignored scratch, and after the link the whole transition is visible.
|
|
108
|
+
function append(task, previous, fields) {
|
|
109
|
+
const revision = (previous?.revision ?? 0) + 1;
|
|
110
|
+
if (revision > MAX_REVISIONS) throw new Error("peer_recovery_revision_limit");
|
|
111
|
+
const record = {
|
|
112
|
+
...previous, files: undefined, ...fields,
|
|
113
|
+
schemaVersion: SCHEMA, workspaceRoot: task.root, taskId: task.taskId, core: task.core,
|
|
114
|
+
revision, recordedAt: new Date().toISOString(), authorizationGranted: false,
|
|
115
|
+
};
|
|
116
|
+
const bytes = Buffer.from(`${JSON.stringify(record)}\n`);
|
|
117
|
+
if (bytes.length > RECORD_LIMIT) throw new Error("peer_recovery_record_too_large");
|
|
118
|
+
const scratch = path.join(task.directory, `.tmp-${randomUUID()}`);
|
|
119
|
+
const fd = openSync(scratch, "wx", 0o600);
|
|
120
|
+
try {
|
|
121
|
+
writeFileSync(fd, bytes);
|
|
122
|
+
fsyncSync(fd);
|
|
123
|
+
try { linkSync(scratch, revisionPath(task, revision)); }
|
|
124
|
+
catch (error) { if (error.code === "EEXIST") return null; throw error; }
|
|
125
|
+
const directoryFd = openSync(task.directory, "r");
|
|
126
|
+
try { fsyncSync(directoryFd); } finally { closeSync(directoryFd); }
|
|
127
|
+
return record;
|
|
128
|
+
} finally { closeSync(fd); unlinkSync(scratch); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validatePaths(paths) {
|
|
132
|
+
if (!Array.isArray(paths) || paths.length < 1 || paths.length > 64) {
|
|
133
|
+
throw new Error("peer_recovery_paths_required");
|
|
134
|
+
}
|
|
135
|
+
const normalized = paths.map(ref => {
|
|
136
|
+
if (typeof ref !== "string" || ref.length > 240 || /[\\:\p{Cc}]/u.test(ref)
|
|
137
|
+
|| ref.split("/").some(part => !part || part === "." || part === ".."
|
|
138
|
+
|| /^(?:\.git|\.owlcoda|node_modules)$/iu.test(part))) {
|
|
139
|
+
throw new Error("peer_recovery_invalid_path");
|
|
140
|
+
}
|
|
141
|
+
return ref;
|
|
142
|
+
}).sort();
|
|
143
|
+
const folded = normalized.map(ref => ref.normalize("NFC").toLowerCase()).sort();
|
|
144
|
+
if (folded.some((ref, index) => index && (ref === folded[index - 1]
|
|
145
|
+
|| folded.some(parent => ref.startsWith(`${parent}/`))))) {
|
|
146
|
+
throw new Error("peer_recovery_overlapping_paths");
|
|
147
|
+
}
|
|
148
|
+
return normalized;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function checkedFiles(paths, input) {
|
|
152
|
+
if (!Array.isArray(input) || input.length !== paths.length) {
|
|
153
|
+
throw new Error("peer_recovery_checkpoint_paths_mismatch");
|
|
154
|
+
}
|
|
155
|
+
let bytes = 0;
|
|
156
|
+
const files = paths.map((ref, index) => {
|
|
157
|
+
const item = input[index];
|
|
158
|
+
if (item?.path !== ref || (item.content !== null && typeof item.content !== "string")) {
|
|
159
|
+
throw new Error("peer_recovery_checkpoint_paths_mismatch");
|
|
160
|
+
}
|
|
161
|
+
if (item.content !== null) {
|
|
162
|
+
const content = Buffer.from(item.content, "base64");
|
|
163
|
+
if (content.toString("base64") !== item.content) throw new Error("peer_recovery_checkpoint_base64_invalid");
|
|
164
|
+
bytes += content.length;
|
|
165
|
+
if (bytes > CHECKPOINT_LIMIT) throw new Error("peer_recovery_checkpoint_too_large");
|
|
166
|
+
}
|
|
167
|
+
return { path: ref, content: item.content };
|
|
168
|
+
});
|
|
169
|
+
return { files, sha256: digest(JSON.stringify(files)) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function initialFile(root, ref) {
|
|
173
|
+
const target = path.join(root, ref);
|
|
174
|
+
try {
|
|
175
|
+
const content = regularBytes(target, CHECKPOINT_LIMIT).toString("base64");
|
|
176
|
+
return { path: ref, content };
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error.code === "ENOENT") return { path: ref, content: null };
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function checkpoint(task, state) {
|
|
184
|
+
const source = readRevision(task, state.checkpointRevision);
|
|
185
|
+
const result = checkedFiles(state.paths, source.files);
|
|
186
|
+
if (result.sha256 !== state.checkpointSha256) throw new Error("peer_recovery_checkpoint_drift");
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function projection(task, state) {
|
|
191
|
+
const { files: _files, ...record } = state;
|
|
192
|
+
return {
|
|
193
|
+
...record, checkpointPath: revisionPath(task, state.checkpointRevision),
|
|
194
|
+
status: state.event, exitCode: 0, authorizationGranted: false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function requireBackend() {
|
|
199
|
+
const backend = resolveVerificationEnvelopeBackendV1();
|
|
200
|
+
if (!backend.available) throw new Error("peer_recovery_enforced_backend_unavailable");
|
|
201
|
+
return backend;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function createPeerRecoveryTask({ workspaceRoot, taskId, owner, successor, paths }) {
|
|
205
|
+
requireBackend();
|
|
206
|
+
safeIdentifier(owner, "initial peer");
|
|
207
|
+
safeIdentifier(successor, "successor peer");
|
|
208
|
+
if (owner === successor) throw new Error("peer_recovery_requires_distinct_peers");
|
|
209
|
+
const ownedPaths = validatePaths(paths);
|
|
210
|
+
const root = realpathSync(workspaceRoot);
|
|
211
|
+
const initial = checkedFiles(ownedPaths, ownedPaths.map(ref => initialFile(root, ref)));
|
|
212
|
+
const task = taskDirectory(root, taskId, true);
|
|
213
|
+
const created = append(task, null, {
|
|
214
|
+
generation: 1, owner, successor, paths: ownedPaths, phase: "ready", attempt: null,
|
|
215
|
+
wake: null, event: "peer_task_ready", checkpointRevision: 1,
|
|
216
|
+
checkpointSha256: initial.sha256, files: initial.files,
|
|
217
|
+
});
|
|
218
|
+
if (!created) throw new Error("peer_recovery_task_already_exists");
|
|
219
|
+
return projection(task, created);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function inspectPeerRecoveryTask({ workspaceRoot, taskId }) {
|
|
223
|
+
const task = taskDirectory(workspaceRoot, taskId);
|
|
224
|
+
return projection(task, current(task));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function observe(task, examine, timeoutMs) {
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
let watcher, timer, settled = false;
|
|
230
|
+
const finish = (error, value) => {
|
|
231
|
+
if (settled) return;
|
|
232
|
+
settled = true;
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
watcher?.close();
|
|
235
|
+
if (error) reject(error);
|
|
236
|
+
else resolve(value);
|
|
237
|
+
};
|
|
238
|
+
const check = () => {
|
|
239
|
+
try {
|
|
240
|
+
const value = examine(current(task));
|
|
241
|
+
if (value) finish(null, value);
|
|
242
|
+
} catch (error) { finish(error); }
|
|
243
|
+
};
|
|
244
|
+
try {
|
|
245
|
+
watcher = watch(task.directory, check);
|
|
246
|
+
watcher.once("error", finish);
|
|
247
|
+
timer = setTimeout(() => { check(); finish(null, null); }, timeoutMs);
|
|
248
|
+
check();
|
|
249
|
+
} catch (error) { finish(error); }
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function recoverPeerTask({ workspaceRoot, taskId, agent, generation, revision, waitMs = 5000 }) {
|
|
254
|
+
requireBackend();
|
|
255
|
+
safeIdentifier(agent, "successor peer");
|
|
256
|
+
if (!Number.isSafeInteger(generation) || generation < 1 || !Number.isSafeInteger(revision)
|
|
257
|
+
|| revision < 1 || !Number.isInteger(waitMs)
|
|
258
|
+
|| waitMs < 100 || waitMs > 60000) throw new Error("peer_recovery_invalid_request");
|
|
259
|
+
const task = taskDirectory(workspaceRoot, taskId);
|
|
260
|
+
let requestedWakeId;
|
|
261
|
+
for (;;) {
|
|
262
|
+
const state = current(task);
|
|
263
|
+
if (state.generation !== generation) {
|
|
264
|
+
if (state.generation === generation + 1 && state.owner === agent) return projection(task, state);
|
|
265
|
+
throw new Error("peer_recovery_generation_changed");
|
|
266
|
+
}
|
|
267
|
+
if (state.successor !== agent) throw new Error("peer_recovery_successor_mismatch");
|
|
268
|
+
if (state.phase === "completed") return projection(task, state);
|
|
269
|
+
if (state.wake?.observed && (state.wake.id === requestedWakeId
|
|
270
|
+
|| state.wake.requestedFromRevision === revision)) return projection(task, state);
|
|
271
|
+
if (state.phase === "running" && (!state.wake || state.wake.observed)) {
|
|
272
|
+
if (state.revision !== revision) throw new Error("peer_recovery_observation_stale");
|
|
273
|
+
const wake = { id: randomUUID(), requestedFromRevision: revision,
|
|
274
|
+
deadline: Date.now() + waitMs, observed: false };
|
|
275
|
+
if (append(task, state, { wake, event: "peer_wake_requested" })) requestedWakeId = wake.id;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (state.wake && !state.wake.observed && state.wake.requestedFromRevision === revision) {
|
|
279
|
+
requestedWakeId = state.wake.id;
|
|
280
|
+
}
|
|
281
|
+
if (state.revision !== revision && (!requestedWakeId || state.wake?.id !== requestedWakeId)) {
|
|
282
|
+
throw new Error("peer_recovery_observation_stale");
|
|
283
|
+
}
|
|
284
|
+
if (state.phase === "running" && Date.now() < state.wake.deadline) {
|
|
285
|
+
requestedWakeId = state.wake.id;
|
|
286
|
+
await observe(task, next => next.generation !== generation || next.phase !== "running"
|
|
287
|
+
|| next.wake?.id !== requestedWakeId || next.wake.observed,
|
|
288
|
+
Math.max(1, state.wake.deadline - Date.now()));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
checkpoint(task, state);
|
|
292
|
+
const replacement = append(task, state, {
|
|
293
|
+
owner: agent, successor: state.owner, generation: generation + 1,
|
|
294
|
+
phase: "ready", attempt: null, wake: null, processExit: null, event: "peer_takeover_ready",
|
|
295
|
+
recovery: {
|
|
296
|
+
fromGeneration: generation, fromAgent: state.owner, previousAttempt: state.attempt,
|
|
297
|
+
wakeId: state.wake?.id ?? null,
|
|
298
|
+
evidence: state.phase === "running" ? "wake_unconfirmed_isolated"
|
|
299
|
+
: state.phase === "exited" ? "managed_process_exited" : "attempt_not_started",
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
if (replacement) return projection(task, replacement);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function materialize(files, directory) {
|
|
307
|
+
for (const file of files) {
|
|
308
|
+
if (file.content === null) continue;
|
|
309
|
+
const ref = path.join(directory, file.path);
|
|
310
|
+
mkdirSync(path.dirname(ref), { recursive: true, mode: 0o700 });
|
|
311
|
+
writeFileSync(ref, Buffer.from(file.content, "base64"), { flag: "wx", mode: 0o600 });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function sandboxPolicy(executable, directory) {
|
|
316
|
+
// Only the host process receives control-store access. The worker inherits
|
|
317
|
+
// pipes, never a writable descriptor into the project or the next attempt.
|
|
318
|
+
return [
|
|
319
|
+
"(version 1)", "(allow default)", "(deny file-write*)", "(deny file-read-data)",
|
|
320
|
+
`(allow file-read-data file-write* (subpath ${JSON.stringify(directory)}))`,
|
|
321
|
+
`(allow file-read-data (literal ${JSON.stringify(executable)}))`,
|
|
322
|
+
'(allow file-read-data (literal "/") (subpath "/System") (subpath "/usr/lib"))',
|
|
323
|
+
"(deny network*)", "(deny mach-lookup)", "(deny signal)", "(deny process-fork)", "(deny process-exec)",
|
|
324
|
+
`(allow process-exec (literal ${JSON.stringify(executable)}))`,
|
|
325
|
+
].join("\n");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function reportCheckpoint(task, attemptId, message) {
|
|
329
|
+
for (;;) {
|
|
330
|
+
const state = current(task);
|
|
331
|
+
if (state.phase !== "running" || state.attempt?.id !== attemptId) return false;
|
|
332
|
+
const value = checkedFiles(state.paths, message.files);
|
|
333
|
+
const finish = message.type === "finish";
|
|
334
|
+
const changed = value.sha256 !== state.checkpointSha256;
|
|
335
|
+
if (!finish && !changed) return true;
|
|
336
|
+
const observed = changed && state.wake?.id === message.wakeId && state.wake !== null;
|
|
337
|
+
if (append(task, state, {
|
|
338
|
+
files: value.files, checkpointRevision: state.revision + 1, checkpointSha256: value.sha256,
|
|
339
|
+
wake: observed ? { ...state.wake, observed: true } : state.wake,
|
|
340
|
+
phase: finish ? "completed" : "running",
|
|
341
|
+
event: finish ? "peer_task_completed" : observed ? "peer_wake_progress_observed" : "peer_checkpoint_recorded",
|
|
342
|
+
})) return true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function reportExit(task, attemptId, processExit) {
|
|
347
|
+
for (;;) {
|
|
348
|
+
const state = current(task);
|
|
349
|
+
if (state.attempt?.id !== attemptId) {
|
|
350
|
+
return { ...projection(task, state), status: "peer_attempt_superseded", exitCode: 2 };
|
|
351
|
+
}
|
|
352
|
+
if (state.phase !== "running") return projection(task, state);
|
|
353
|
+
const next = append(task, state, {
|
|
354
|
+
phase: "exited", event: "peer_attempt_exited", processExit,
|
|
355
|
+
});
|
|
356
|
+
if (next) return { ...projection(task, next), exitCode: 2 };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function runPeerTask({ workspaceRoot, taskId, agent, generation, command }) {
|
|
361
|
+
const backend = requireBackend();
|
|
362
|
+
safeIdentifier(agent, "current peer");
|
|
363
|
+
if (!Array.isArray(command) || !path.isAbsolute(command[0] ?? "")) {
|
|
364
|
+
throw new Error("peer_recovery_absolute_worker_required");
|
|
365
|
+
}
|
|
366
|
+
const executable = realpathSync(command[0]);
|
|
367
|
+
if (!lstatSync(executable).isFile()) throw new Error("peer_recovery_worker_invalid");
|
|
368
|
+
const task = taskDirectory(workspaceRoot, taskId);
|
|
369
|
+
const state = current(task);
|
|
370
|
+
if (state.phase !== "ready" || state.owner !== agent || state.generation !== generation) {
|
|
371
|
+
throw new Error("peer_recovery_attempt_not_available");
|
|
372
|
+
}
|
|
373
|
+
const id = randomUUID();
|
|
374
|
+
const directory = mkdtempSync(path.join(realpathSync(tmpdir()), "owlrunkit-peer-"));
|
|
375
|
+
materialize(checkpoint(task, state).files, directory);
|
|
376
|
+
const attempt = { id, directory, backend: backend.id };
|
|
377
|
+
const claimed = append(task, state, {
|
|
378
|
+
attempt, phase: "running", processExit: null, event: "peer_attempt_started",
|
|
379
|
+
});
|
|
380
|
+
if (!claimed) throw new Error("peer_recovery_attempt_already_claimed");
|
|
381
|
+
|
|
382
|
+
return new Promise((resolve, reject) => {
|
|
383
|
+
const child = spawn(backend.executable, ["-p", sandboxPolicy(executable, directory),
|
|
384
|
+
executable, ...command.slice(1)], {
|
|
385
|
+
cwd: directory, env: {
|
|
386
|
+
PATH: "/usr/bin:/bin", TMPDIR: directory, OWLRUNKIT_PEER_ATTEMPT: id,
|
|
387
|
+
OWLRUNKIT_PEER_PATHS: JSON.stringify(state.paths),
|
|
388
|
+
}, stdio: ["pipe", "pipe", "pipe"],
|
|
389
|
+
});
|
|
390
|
+
let watcher, pending = Buffer.alloc(0), sentWake, protocolError = null;
|
|
391
|
+
let diagnostic = Buffer.alloc(0);
|
|
392
|
+
const stop = error => {
|
|
393
|
+
protocolError ??= error.message;
|
|
394
|
+
child.kill("SIGKILL");
|
|
395
|
+
};
|
|
396
|
+
const wake = () => {
|
|
397
|
+
try {
|
|
398
|
+
const live = current(task);
|
|
399
|
+
if (live.attempt?.id !== id || live.phase !== "running") return;
|
|
400
|
+
if (live.wake && !live.wake.observed && live.wake.id !== sentWake) {
|
|
401
|
+
sentWake = live.wake.id;
|
|
402
|
+
child.stdin.write(`${JSON.stringify({ type: "wake", wakeId: sentWake })}\n`);
|
|
403
|
+
}
|
|
404
|
+
} catch (error) { stop(error); }
|
|
405
|
+
};
|
|
406
|
+
child.stdin.on("error", error => { if (error.code !== "EPIPE") stop(error); });
|
|
407
|
+
child.stderr.on("data", chunk => {
|
|
408
|
+
const remaining = 4096 - diagnostic.length;
|
|
409
|
+
if (remaining > 0) diagnostic = Buffer.concat([diagnostic, chunk.subarray(0, remaining)]);
|
|
410
|
+
});
|
|
411
|
+
child.stdout.on("data", chunk => {
|
|
412
|
+
if (protocolError) return;
|
|
413
|
+
try {
|
|
414
|
+
pending = Buffer.concat([pending, chunk]);
|
|
415
|
+
let end;
|
|
416
|
+
while ((end = pending.indexOf(10)) >= 0) {
|
|
417
|
+
if (end > RECORD_LIMIT) throw new Error("peer_recovery_worker_message_too_large");
|
|
418
|
+
const message = JSON.parse(pending.subarray(0, end).toString("utf8"));
|
|
419
|
+
pending = pending.subarray(end + 1);
|
|
420
|
+
if (message?.type === "ack") continue;
|
|
421
|
+
if (!new Set(["checkpoint", "finish"]).has(message?.type)) {
|
|
422
|
+
throw new Error("peer_recovery_worker_message_invalid");
|
|
423
|
+
}
|
|
424
|
+
reportCheckpoint(task, id, message);
|
|
425
|
+
}
|
|
426
|
+
if (pending.length > RECORD_LIMIT) throw new Error("peer_recovery_worker_message_too_large");
|
|
427
|
+
} catch (error) { stop(error); }
|
|
428
|
+
});
|
|
429
|
+
child.once("error", error => { protocolError ??= error.message; });
|
|
430
|
+
child.once("close", (code, signal) => {
|
|
431
|
+
watcher?.close();
|
|
432
|
+
try { resolve(reportExit(task, id, { code, signal, protocolError,
|
|
433
|
+
diagnostic: diagnostic.toString("utf8") || null })); }
|
|
434
|
+
catch (error) { reject(error); }
|
|
435
|
+
});
|
|
436
|
+
try {
|
|
437
|
+
watcher = watch(task.directory, wake);
|
|
438
|
+
watcher.once("error", stop);
|
|
439
|
+
wake();
|
|
440
|
+
} catch (error) { stop(error); }
|
|
441
|
+
});
|
|
442
|
+
}
|
|
@@ -26,10 +26,14 @@ const SELF_NORMALIZED_QUICK_CORE_VERSIONS = new Set([
|
|
|
26
26
|
"0.23.0",
|
|
27
27
|
"0.24.0",
|
|
28
28
|
"0.24.1",
|
|
29
|
+
"0.25.0",
|
|
29
30
|
]);
|
|
30
31
|
const EXACT_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
|
|
31
32
|
const SHA256_REF = /^sha256:[a-f0-9]{64}$/;
|
|
32
33
|
const TRUSTED_PRIOR_CORE_IDENTITIES = new Map([
|
|
34
|
+
["0.24.1", new Set([
|
|
35
|
+
"sha256:94aaa2e3a6d504374fa78cfcd341e31b33087806c9ddf5f84b3d3b47a6328e08",
|
|
36
|
+
])],
|
|
33
37
|
["0.24.0", new Set([
|
|
34
38
|
"sha256:02e3607701a275014fe95e95e025aeca7cac34ebe4a47e603b8c9db0e45596ff",
|
|
35
39
|
])],
|
|
@@ -92,6 +92,12 @@ import {
|
|
|
92
92
|
formatInspectHuman,
|
|
93
93
|
} from "./inspect-presentation.mjs";
|
|
94
94
|
import { buildHookRecoveryV1 } from "./hook-recovery.mjs";
|
|
95
|
+
import {
|
|
96
|
+
createPeerRecoveryTask,
|
|
97
|
+
inspectPeerRecoveryTask,
|
|
98
|
+
recoverPeerTask,
|
|
99
|
+
runPeerTask,
|
|
100
|
+
} from "./peer-recovery.mjs";
|
|
95
101
|
import { attestQuickReceiptDetails } from "./quick-attest.mjs";
|
|
96
102
|
import { readLocalQuickMetrics } from "./quick-metrics.mjs";
|
|
97
103
|
import { runQuickEvidenceReuse } from "./quick-evidence-reuse.mjs";
|
|
@@ -265,6 +271,7 @@ Agent team projects
|
|
|
265
271
|
npx --no-install owlrunkit project <init|assign|checkpoint|handoff|reject-and-return|reopen-upstream|transfer|work-item revise-scope|work-item require-evidence|reconcile|delivery record|decision|verification|capture|target-snapshot|import-delivery|status|takeover|successor|successor scaffold> [options]
|
|
266
272
|
npx --no-install owlrunkit team <recommend|status|packet validate> [options]
|
|
267
273
|
Durable project truth plus read-only Team recommendation and packet validation. No Agent dispatch.
|
|
274
|
+
Managed local peer recovery: npx --no-install owlrunkit peer --help
|
|
268
275
|
Core successor: npx --no-install owlrunkit core-successor <plan|apply|resume> --workspace . [...]
|
|
269
276
|
Other onboarding commands:
|
|
270
277
|
npx --no-install owlrunkit profiles validate --workspace .; npx --no-install owlrunkit profiles impact --workspace . --changed <path>
|
|
@@ -2443,6 +2450,52 @@ function requestCommand(options, handler) {
|
|
|
2443
2450
|
return handler({ workspaceRoot: root, runId, request: readJson(requestPath) });
|
|
2444
2451
|
}
|
|
2445
2452
|
|
|
2453
|
+
const PEER_HELP = `Usage:
|
|
2454
|
+
owlrunkit peer create --workspace <dir> --task <id> --request <json>
|
|
2455
|
+
owlrunkit peer inspect --workspace <dir> --task <id>
|
|
2456
|
+
owlrunkit peer run --workspace <dir> --task <id> --agent <id> --generation <n> -- /absolute/worker [args]
|
|
2457
|
+
owlrunkit peer recover --workspace <dir> --task <id> --agent <id> --generation <n> --revision <n> [--wait-ms <n>]
|
|
2458
|
+
`;
|
|
2459
|
+
|
|
2460
|
+
function runPeerCommand(rest) {
|
|
2461
|
+
const [action, ...args] = rest;
|
|
2462
|
+
const separator = args.indexOf("--");
|
|
2463
|
+
const optionArgs = separator === -1 ? args : args.slice(0, separator);
|
|
2464
|
+
if (["--help", "-h"].includes(action) || optionArgs.includes("--help") || optionArgs.includes("-h")) {
|
|
2465
|
+
return withHumanOutput({ status: "help", exitCode: 0, authorizationGranted: false }, PEER_HELP);
|
|
2466
|
+
}
|
|
2467
|
+
const flags = {
|
|
2468
|
+
create: ["request"], inspect: [], run: ["agent", "generation"],
|
|
2469
|
+
recover: ["agent", "generation", "revision", "wait-ms"],
|
|
2470
|
+
};
|
|
2471
|
+
if (!Object.hasOwn(flags, action) || (separator !== -1 && action !== "run")) {
|
|
2472
|
+
throw new Error(PEER_HELP);
|
|
2473
|
+
}
|
|
2474
|
+
const options = parseOptions(optionArgs);
|
|
2475
|
+
const allowed = new Set(["workspace", "task", "json", ...flags[action]]);
|
|
2476
|
+
if (Object.keys(options).some(key => !allowed.has(key))) throw new Error(PEER_HELP);
|
|
2477
|
+
const task = { workspaceRoot: workspace(options), taskId: requireOption(options, "task") };
|
|
2478
|
+
if (action === "inspect") return inspectPeerRecoveryTask(task);
|
|
2479
|
+
if (action === "create") {
|
|
2480
|
+
const request = readJson(realpathSync(requireOption(options, "request")));
|
|
2481
|
+
return createPeerRecoveryTask({ ...task, owner: request.owner,
|
|
2482
|
+
successor: request.successor, paths: request.paths });
|
|
2483
|
+
}
|
|
2484
|
+
const peer = {
|
|
2485
|
+
...task, agent: requireOption(options, "agent"),
|
|
2486
|
+
generation: parseCanonicalNonnegativeSafeInteger(requireOption(options, "generation"), "generation"),
|
|
2487
|
+
};
|
|
2488
|
+
if (action === "run") {
|
|
2489
|
+
if (separator === -1) throw new Error(PEER_HELP);
|
|
2490
|
+
return runPeerTask({ ...peer, command: args.slice(separator + 1) });
|
|
2491
|
+
}
|
|
2492
|
+
return recoverPeerTask({ ...peer,
|
|
2493
|
+
revision: parseCanonicalNonnegativeSafeInteger(requireOption(options, "revision"), "revision"),
|
|
2494
|
+
waitMs: options["wait-ms"] === undefined ? undefined
|
|
2495
|
+
: parseCanonicalNonnegativeSafeInteger(options["wait-ms"], "wait-ms"),
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2446
2499
|
export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
|
|
2447
2500
|
try {
|
|
2448
2501
|
const [command, ...rest] = argv;
|
|
@@ -2461,6 +2514,7 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
|
|
|
2461
2514
|
authorizationGranted: false,
|
|
2462
2515
|
}, `owlrunkit ${CORE_VERSION}\n`);
|
|
2463
2516
|
}
|
|
2517
|
+
if (command === "peer") return await runPeerCommand(rest);
|
|
2464
2518
|
const separatorIndex = rest.indexOf("--");
|
|
2465
2519
|
const acceptsExactCommand = command === "verify"
|
|
2466
2520
|
|| command === "quick-verify"
|