@vela-science/canopus 0.4.0-rc.1
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 +249 -0
- package/LICENSE-APACHE +176 -0
- package/LICENSE-MIT +21 -0
- package/README.md +124 -0
- package/SECURITY.md +35 -0
- package/capsules/erdos1056-k15/README.md +35 -0
- package/capsules/erdos1056-k15/bin/linux-arm64/10428401-10428600/verifier +0 -0
- package/capsules/erdos1056-k15/bin/linux-x86_64/10428401-10428600/verifier +0 -0
- package/capsules/formal-erdos-505-test-dim-one/README.md +20 -0
- package/capsules/formal-erdos-505-test-dim-one/verifier +105 -0
- package/dist/src/activity/events.d.ts +15 -0
- package/dist/src/activity/events.js +24 -0
- package/dist/src/activity/store.d.ts +10 -0
- package/dist/src/activity/store.js +165 -0
- package/dist/src/artifact/freeze.d.ts +21 -0
- package/dist/src/artifact/freeze.js +177 -0
- package/dist/src/artifact/materialize.d.ts +6 -0
- package/dist/src/artifact/materialize.js +29 -0
- package/dist/src/budget/enforce.d.ts +30 -0
- package/dist/src/budget/enforce.js +98 -0
- package/dist/src/candidate/validate.d.ts +3 -0
- package/dist/src/candidate/validate.js +32 -0
- package/dist/src/capability/withdrawal.d.ts +47 -0
- package/dist/src/capability/withdrawal.js +487 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +503 -0
- package/dist/src/contracts/candidate.d.ts +50 -0
- package/dist/src/contracts/candidate.js +93 -0
- package/dist/src/contracts/mission.d.ts +139 -0
- package/dist/src/contracts/mission.js +401 -0
- package/dist/src/contracts/validation.d.ts +25 -0
- package/dist/src/contracts/validation.js +95 -0
- package/dist/src/engines/codex-events.d.ts +25 -0
- package/dist/src/engines/codex-events.js +219 -0
- package/dist/src/engines/codex-exec.d.ts +17 -0
- package/dist/src/engines/codex-exec.js +136 -0
- package/dist/src/engines/codex-home.d.ts +7 -0
- package/dist/src/engines/codex-home.js +50 -0
- package/dist/src/engines/codex-tool-free.d.ts +26 -0
- package/dist/src/engines/codex-tool-free.js +213 -0
- package/dist/src/engines/codex-tools-native.d.ts +30 -0
- package/dist/src/engines/codex-tools-native.js +405 -0
- package/dist/src/engines/engine.d.ts +55 -0
- package/dist/src/engines/engine.js +4 -0
- package/dist/src/engines/fake.d.ts +7 -0
- package/dist/src/engines/fake.js +37 -0
- package/dist/src/evidence/manifests.d.ts +9 -0
- package/dist/src/evidence/manifests.js +56 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.js +32 -0
- package/dist/src/mission/prepare.d.ts +33 -0
- package/dist/src/mission/prepare.js +375 -0
- package/dist/src/product/custody.d.ts +27 -0
- package/dist/src/product/custody.js +131 -0
- package/dist/src/product/doctor.d.ts +61 -0
- package/dist/src/product/doctor.js +278 -0
- package/dist/src/product/profile-bundle.d.ts +21 -0
- package/dist/src/product/profile-bundle.js +78 -0
- package/dist/src/product/profile.d.ts +67 -0
- package/dist/src/product/profile.js +251 -0
- package/dist/src/product/replay.d.ts +10 -0
- package/dist/src/product/replay.js +71 -0
- package/dist/src/product/run.d.ts +31 -0
- package/dist/src/product/run.js +264 -0
- package/dist/src/product/runtime.d.ts +18 -0
- package/dist/src/product/runtime.js +64 -0
- package/dist/src/product/version.d.ts +1 -0
- package/dist/src/product/version.js +6 -0
- package/dist/src/product/withdraw.d.ts +8 -0
- package/dist/src/product/withdraw.js +255 -0
- package/dist/src/projection/diagnostic.d.ts +12 -0
- package/dist/src/projection/diagnostic.js +61 -0
- package/dist/src/projection/failure.d.ts +26 -0
- package/dist/src/projection/failure.js +63 -0
- package/dist/src/projection/run.d.ts +89 -0
- package/dist/src/projection/run.js +159 -0
- package/dist/src/receipt/map.d.ts +25 -0
- package/dist/src/receipt/map.js +162 -0
- package/dist/src/repair/plan.d.ts +2 -0
- package/dist/src/repair/plan.js +22 -0
- package/dist/src/roles.d.ts +2 -0
- package/dist/src/roles.js +9 -0
- package/dist/src/run.d.ts +94 -0
- package/dist/src/run.js +654 -0
- package/dist/src/util/canonical.d.ts +5 -0
- package/dist/src/util/canonical.js +53 -0
- package/dist/src/util/command.d.ts +26 -0
- package/dist/src/util/command.js +161 -0
- package/dist/src/util/files.d.ts +3 -0
- package/dist/src/util/files.js +65 -0
- package/dist/src/vela/cli.d.ts +71 -0
- package/dist/src/vela/cli.js +718 -0
- package/dist/src/vela/types.d.ts +27 -0
- package/dist/src/vela/types.js +1 -0
- package/dist/src/verifier/run.d.ts +24 -0
- package/dist/src/verifier/run.js +315 -0
- package/dist/src/workspace/prepare.d.ts +30 -0
- package/dist/src/workspace/prepare.js +183 -0
- package/docs/MISSIONS.md +78 -0
- package/docs/RELEASES.md +1036 -0
- package/docs/RUN_RECORD.md +70 -0
- package/missions/erdos1056-k15-next/mission.draft.json +40 -0
- package/missions/formal-erdos-505-test-dim-one/mission.draft.json +40 -0
- package/package.json +89 -0
- package/profiles/erdos1056-k15-10428401-10428600.json +33 -0
- package/profiles/formal-erdos-505-test-dim-one.json +35 -0
- package/runtime/native-worker/config-linux.toml +20 -0
- package/runtime/native-worker/config.toml +17 -0
- package/schemas/candidate.v0.json +125 -0
- package/schemas/engine-manifest.v0.json +45 -0
- package/schemas/engine-output.v0.json +41 -0
- package/schemas/mission.v0.json +175 -0
- package/schemas/mission.v1.json +224 -0
- package/schemas/verifier-manifest.v0.json +48 -0
- package/schemas/verifier-manifest.v1.json +59 -0
- package/scripts/run-hostile-native-custody-fixture.mjs +382 -0
- package/scripts/run-hostile-verifier-fixture.mjs +63 -0
- package/tests/fixtures/hostile-verifier/probe.py +35 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isolatedEnvironment, runCommand } from "../util/command.js";
|
|
5
|
+
import { MAX_EXECUTABLE_BYTES, sha256RegularFile } from "../util/files.js";
|
|
6
|
+
export function executableNames(name, platform, pathExt) {
|
|
7
|
+
if (platform !== "win32" || path.win32.extname(name) !== "")
|
|
8
|
+
return [name];
|
|
9
|
+
const extensions = pathExt
|
|
10
|
+
.split(";")
|
|
11
|
+
.map((extension) => extension.trim())
|
|
12
|
+
.filter((extension) => extension !== "")
|
|
13
|
+
.map((extension) => extension.startsWith(".") ? extension : `.${extension}`);
|
|
14
|
+
return [name, ...extensions.map((extension) => `${name}${extension.toLowerCase()}`)];
|
|
15
|
+
}
|
|
16
|
+
export async function findExecutable(name, searchPath = process.env.PATH ?? "", options = {}) {
|
|
17
|
+
const platform = options.platform ?? process.platform;
|
|
18
|
+
const pathExt = options.pathExt ?? process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
19
|
+
const paths = platform === "win32" ? path.win32 : path;
|
|
20
|
+
const names = executableNames(name, platform, pathExt);
|
|
21
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
22
|
+
const binary = await realpath(path.resolve(name));
|
|
23
|
+
await access(binary, constants.X_OK);
|
|
24
|
+
return binary;
|
|
25
|
+
}
|
|
26
|
+
for (const directory of searchPath.split(platform === "win32" ? ";" : path.delimiter)) {
|
|
27
|
+
if (directory === "")
|
|
28
|
+
continue;
|
|
29
|
+
for (const executable of names) {
|
|
30
|
+
const candidate = paths.join(directory, executable);
|
|
31
|
+
try {
|
|
32
|
+
await access(candidate, constants.X_OK);
|
|
33
|
+
return await realpath(candidate);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Continue through the explicit PATH and PATHEXT only.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`${name} was not found on PATH`);
|
|
41
|
+
}
|
|
42
|
+
export async function runtimeIdentity(options) {
|
|
43
|
+
const runner = options.runner ?? runCommand;
|
|
44
|
+
const binary = await findExecutable(options.name);
|
|
45
|
+
const result = await runner({
|
|
46
|
+
argv: [binary, ...(options.versionArgs ?? ["--version"])],
|
|
47
|
+
cwd: options.cwd,
|
|
48
|
+
env: isolatedEnvironment(options.home),
|
|
49
|
+
timeoutMs: 30_000,
|
|
50
|
+
maxOutputBytes: 64 * 1024,
|
|
51
|
+
});
|
|
52
|
+
if (result.exitCode !== 0)
|
|
53
|
+
throw new Error(`${options.name} version probe failed`);
|
|
54
|
+
const stdout = result.stdout.toString("utf8").trim();
|
|
55
|
+
const version = stdout === "" ? result.stderr.toString("utf8").trim() : stdout;
|
|
56
|
+
if (version === "" || version.length > 4096) {
|
|
57
|
+
throw new Error(`${options.name} returned an invalid version`);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
binary,
|
|
61
|
+
version,
|
|
62
|
+
sha256: await sha256RegularFile(binary, MAX_EXECUTABLE_BYTES),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CANOPUS_VERSION: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
const packageJson = JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8"));
|
|
3
|
+
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
|
4
|
+
throw new Error("Canopus package version is missing");
|
|
5
|
+
}
|
|
6
|
+
export const CANOPUS_VERSION = packageJson.version;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdir, mkdtemp, readdir, realpath, rm } from "node:fs/promises";
|
|
4
|
+
import { consumeWithdrawalCapability, installWithdrawalCapabilitySecret, loadWithdrawalCapability, } from "../capability/withdrawal.js";
|
|
5
|
+
import { parseRunRecord } from "../projection/run.js";
|
|
6
|
+
import { canonicalJcs, contentDigest, protocolDigest, sha256Bytes, } from "../util/canonical.js";
|
|
7
|
+
import { isolatedEnvironment, runCommand } from "../util/command.js";
|
|
8
|
+
import { readBoundedRegularFile } from "../util/files.js";
|
|
9
|
+
import { VelaClient } from "../vela/cli.js";
|
|
10
|
+
async function gitText(runner, cwd, home, argv) {
|
|
11
|
+
const result = await runner({
|
|
12
|
+
argv: ["git", ...argv],
|
|
13
|
+
cwd,
|
|
14
|
+
env: isolatedEnvironment(home),
|
|
15
|
+
timeoutMs: 120_000,
|
|
16
|
+
maxOutputBytes: 8 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.exitCode !== 0 || result.stderr.length !== 0) {
|
|
19
|
+
throw new Error(`git ${argv.join(" ")} failed: stdout=${sha256Bytes(result.stdout)} stderr=${sha256Bytes(result.stderr)}`);
|
|
20
|
+
}
|
|
21
|
+
return result.stdout.toString("utf8").trim();
|
|
22
|
+
}
|
|
23
|
+
function safeActor(actor) {
|
|
24
|
+
return [...actor].map((character) => /[A-Za-z0-9_-]/u.test(character) ? character : "-").join("");
|
|
25
|
+
}
|
|
26
|
+
async function scientificProjectionRoot(frontierRoot) {
|
|
27
|
+
const project = JSON.parse((await readBoundedRegularFile(path.join(frontierRoot, "frontier.json"), 128 * 1024 * 1024))
|
|
28
|
+
.toString("utf8"));
|
|
29
|
+
const fields = [
|
|
30
|
+
"actors",
|
|
31
|
+
"anchor_links",
|
|
32
|
+
"artifacts",
|
|
33
|
+
"condition_records",
|
|
34
|
+
"evidence_atoms",
|
|
35
|
+
"findings",
|
|
36
|
+
"frontier",
|
|
37
|
+
"frontier_id",
|
|
38
|
+
"released_diff_packs",
|
|
39
|
+
"sources",
|
|
40
|
+
"statement_attestations",
|
|
41
|
+
"verifier_attachments",
|
|
42
|
+
];
|
|
43
|
+
return contentDigest(Object.fromEntries(fields.map((field) => [field, project[field] ?? null])));
|
|
44
|
+
}
|
|
45
|
+
function jsonObject(value, at) {
|
|
46
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
47
|
+
throw new Error(`${at} must be an object`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function proposalStanding(value) {
|
|
52
|
+
if (value !== "pending_review" &&
|
|
53
|
+
value !== "accepted" &&
|
|
54
|
+
value !== "applied" &&
|
|
55
|
+
value !== "rejected" &&
|
|
56
|
+
value !== "revision_requested" &&
|
|
57
|
+
value !== "needs_revision" &&
|
|
58
|
+
value !== "withdrawn") {
|
|
59
|
+
throw new Error(`proposal has unrecognized standing ${JSON.stringify(value)}`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
async function frontierWithin(repo, frontier) {
|
|
64
|
+
const resolved = await realpath(path.resolve(repo, frontier));
|
|
65
|
+
if (resolved !== repo && !resolved.startsWith(`${repo}${path.sep}`)) {
|
|
66
|
+
throw new Error("capability frontier resolves outside the selected source repository");
|
|
67
|
+
}
|
|
68
|
+
return resolved;
|
|
69
|
+
}
|
|
70
|
+
async function terminalProposalEvent(frontierRoot, proposalId) {
|
|
71
|
+
const directory = path.join(frontierRoot, ".vela", "events");
|
|
72
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
73
|
+
if (entries.length > 1_000_000)
|
|
74
|
+
throw new Error("frontier event directory exceeds the supported audit bound");
|
|
75
|
+
const matches = [];
|
|
76
|
+
const decisionByKind = new Map([
|
|
77
|
+
["review.accepted", "accepted"],
|
|
78
|
+
["review.rejected", "rejected"],
|
|
79
|
+
["review.revision_requested", "revision_requested"],
|
|
80
|
+
["proposal.withdrawn", "withdrawn"],
|
|
81
|
+
]);
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (!entry.isFile() || !/^vev_[0-9a-f]{16}\.json$/u.test(entry.name))
|
|
84
|
+
continue;
|
|
85
|
+
const event = jsonObject(JSON.parse((await readBoundedRegularFile(path.join(directory, entry.name), 8 * 1024 * 1024)).toString("utf8")), `event ${entry.name}`);
|
|
86
|
+
const decision = typeof event.kind === "string" ? decisionByKind.get(event.kind) : undefined;
|
|
87
|
+
if (decision === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
const target = jsonObject(event.target, `event ${entry.name}.target`);
|
|
90
|
+
if (target.type !== "proposal" || target.id !== proposalId)
|
|
91
|
+
continue;
|
|
92
|
+
const eventId = entry.name.slice(0, -".json".length);
|
|
93
|
+
if (event.id !== eventId || typeof event.signature !== "string" || event.signature.length === 0) {
|
|
94
|
+
throw new Error(`terminal proposal event ${entry.name} is not a signed canonical event file`);
|
|
95
|
+
}
|
|
96
|
+
const payload = jsonObject(event.payload, `event ${entry.name}.payload`);
|
|
97
|
+
if (payload.proposal_id !== proposalId) {
|
|
98
|
+
throw new Error(`terminal proposal event ${entry.name} payload targets another proposal`);
|
|
99
|
+
}
|
|
100
|
+
if (decision === "withdrawn") {
|
|
101
|
+
if (payload.schema !== "vela.proposal-withdrawal.v1") {
|
|
102
|
+
throw new Error(`withdrawal event ${entry.name} has the wrong payload schema`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (payload.verdict !== decision) {
|
|
106
|
+
throw new Error(`review event ${entry.name} verdict disagrees with its kind`);
|
|
107
|
+
}
|
|
108
|
+
matches.push({ event_id: eventId, decision });
|
|
109
|
+
}
|
|
110
|
+
if (matches.length > 1)
|
|
111
|
+
throw new Error("proposal has conflicting terminal events");
|
|
112
|
+
return matches[0] ?? null;
|
|
113
|
+
}
|
|
114
|
+
function terminalDecisionMatchesStanding(decision, standing) {
|
|
115
|
+
return decision === "accepted"
|
|
116
|
+
? standing === "accepted" || standing === "applied"
|
|
117
|
+
: decision === "revision_requested"
|
|
118
|
+
? standing === "revision_requested" || standing === "needs_revision"
|
|
119
|
+
: decision === standing;
|
|
120
|
+
}
|
|
121
|
+
export async function withdrawProduct(options) {
|
|
122
|
+
if (options.reason.trim().length === 0)
|
|
123
|
+
throw new Error("withdrawal reason must not be empty");
|
|
124
|
+
const runner = options.runner ?? runCommand;
|
|
125
|
+
const source = await realpath(options.frontier);
|
|
126
|
+
const runFile = path.resolve(options.runFile);
|
|
127
|
+
const record = parseRunRecord(JSON.parse((await readBoundedRegularFile(runFile, 8 * 1024 * 1024)).toString("utf8")));
|
|
128
|
+
const capability = await loadWithdrawalCapability(record.landing.proposal_id, options.storeRoot);
|
|
129
|
+
if (capability.manifest.state !== "available" || !capability.secret_available) {
|
|
130
|
+
throw new Error("withdrawal capability is not available");
|
|
131
|
+
}
|
|
132
|
+
if (capability.manifest.receipt_root !== record.landing.receipt_root ||
|
|
133
|
+
canonicalJcs(capability.manifest.final_roots) !== canonicalJcs(record.final_roots)) {
|
|
134
|
+
throw new Error("withdrawal capability does not match the selected run");
|
|
135
|
+
}
|
|
136
|
+
const controlRoot = await mkdtemp(path.join(os.tmpdir(), "canopus-withdraw-"));
|
|
137
|
+
const controlHome = path.join(controlRoot, "home");
|
|
138
|
+
const clone = path.join(controlRoot, "candidate");
|
|
139
|
+
const replay = path.join(controlRoot, "replay");
|
|
140
|
+
await mkdir(path.join(controlHome, ".vela", "agents", safeActor(capability.manifest.actor)), {
|
|
141
|
+
recursive: true,
|
|
142
|
+
mode: 0o700,
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
const sourceHead = await gitText(runner, source, controlHome, ["rev-parse", "--verify", "HEAD^{commit}"]);
|
|
146
|
+
const sourceStatus = await gitText(runner, source, controlHome, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
147
|
+
if (sourceStatus !== "")
|
|
148
|
+
throw new Error("source frontier must be clean before withdrawal");
|
|
149
|
+
const client = new VelaClient({
|
|
150
|
+
binary: capability.manifest.vela.binary,
|
|
151
|
+
expectedVersion: capability.manifest.vela.version,
|
|
152
|
+
expectedSha256: capability.manifest.vela.sha256,
|
|
153
|
+
home: controlHome,
|
|
154
|
+
runner,
|
|
155
|
+
});
|
|
156
|
+
const strictBaseline = capability.manifest.strict_baseline;
|
|
157
|
+
const sourceInspection = await client.inspect(source, capability.manifest.frontier, strictBaseline);
|
|
158
|
+
const frontierRoot = await frontierWithin(source, capability.manifest.frontier);
|
|
159
|
+
const proposalPath = path.join(frontierRoot, ".vela", "proposals", `${record.landing.proposal_id}.json`);
|
|
160
|
+
const proposal = jsonObject(JSON.parse((await readBoundedRegularFile(proposalPath, 8 * 1024 * 1024)).toString("utf8")), "proposal");
|
|
161
|
+
if (proposal.id !== record.landing.proposal_id)
|
|
162
|
+
throw new Error("proposal file id disagrees with the run");
|
|
163
|
+
const standing = proposalStanding(proposal.status);
|
|
164
|
+
const terminalEvent = await terminalProposalEvent(frontierRoot, record.landing.proposal_id);
|
|
165
|
+
const stableHead = await gitText(runner, source, controlHome, ["rev-parse", "--verify", "HEAD^{commit}"]);
|
|
166
|
+
const stableStatus = await gitText(runner, source, controlHome, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
167
|
+
if (stableHead !== sourceHead || stableStatus !== "") {
|
|
168
|
+
throw new Error("source frontier changed during proposal verification");
|
|
169
|
+
}
|
|
170
|
+
if (standing !== "pending_review") {
|
|
171
|
+
if (terminalEvent === null || !terminalDecisionMatchesStanding(terminalEvent.decision, standing)) {
|
|
172
|
+
throw new Error("terminal proposal standing is not backed by exactly one matching signed event");
|
|
173
|
+
}
|
|
174
|
+
await consumeWithdrawalCapability(record.landing.proposal_id, terminalEvent.decision === "withdrawn" ? "withdrawn" : "human_decision_observed", options.storeRoot);
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
command: "withdraw",
|
|
178
|
+
proposal_id: record.landing.proposal_id,
|
|
179
|
+
terminal_event_id: terminalEvent.event_id,
|
|
180
|
+
state: terminalEvent.decision === "withdrawn" ? "already_withdrawn" : "human_decision_observed",
|
|
181
|
+
frontier_mutated: false,
|
|
182
|
+
capability_consumed: true,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (terminalEvent !== null)
|
|
186
|
+
throw new Error("pending proposal already has a terminal event");
|
|
187
|
+
if (protocolDigest(proposal) !== capability.manifest.proposal_root) {
|
|
188
|
+
throw new Error("pending proposal bytes drifted from the retained withdrawal capability");
|
|
189
|
+
}
|
|
190
|
+
await gitText(runner, controlRoot, controlHome, ["clone", "--quiet", "--no-local", source, clone]);
|
|
191
|
+
const before = await client.inspect(clone, capability.manifest.frontier, strictBaseline);
|
|
192
|
+
if (canonicalJcs(before.roots) !== canonicalJcs(sourceInspection.roots)) {
|
|
193
|
+
throw new Error("disposable withdrawal clone does not match the selected source head");
|
|
194
|
+
}
|
|
195
|
+
await installWithdrawalCapabilitySecret(record.landing.proposal_id, path.join(controlHome, ".vela", "agents", safeActor(capability.manifest.actor), "private.key"), options.storeRoot);
|
|
196
|
+
const scientificRootBefore = await scientificProjectionRoot(path.resolve(clone, capability.manifest.frontier));
|
|
197
|
+
const withdrawal = await client.withdraw(clone, capability.manifest.frontier, record.landing.proposal_id, capability.manifest.actor, options.reason);
|
|
198
|
+
const after = await client.inspect(clone, capability.manifest.frontier, strictBaseline);
|
|
199
|
+
const scientificRootAfter = await scientificProjectionRoot(path.resolve(clone, capability.manifest.frontier));
|
|
200
|
+
if (after.roots.git_commit === before.roots.git_commit || after.roots.vela_event_log === before.roots.vela_event_log) {
|
|
201
|
+
throw new Error("withdrawal did not append and publish one event");
|
|
202
|
+
}
|
|
203
|
+
if (withdrawal.publication_commit !== after.roots.git_commit ||
|
|
204
|
+
withdrawal.state_root_before !== before.roots.vela_event_log ||
|
|
205
|
+
withdrawal.state_root_after !== after.roots.vela_event_log) {
|
|
206
|
+
throw new Error("withdrawal response does not bind the verified Git and event-log roots");
|
|
207
|
+
}
|
|
208
|
+
if (scientificRootAfter !== scientificRootBefore) {
|
|
209
|
+
throw new Error("withdrawal changed the accepted scientific projection");
|
|
210
|
+
}
|
|
211
|
+
const withdrawnFrontier = await frontierWithin(await realpath(clone), capability.manifest.frontier);
|
|
212
|
+
const withdrawnProposal = jsonObject(JSON.parse((await readBoundedRegularFile(path.join(withdrawnFrontier, ".vela", "proposals", `${record.landing.proposal_id}.json`), 8 * 1024 * 1024)).toString("utf8")), "withdrawn proposal");
|
|
213
|
+
const withdrawnEvent = await terminalProposalEvent(withdrawnFrontier, record.landing.proposal_id);
|
|
214
|
+
if (withdrawnProposal.status !== "withdrawn" ||
|
|
215
|
+
withdrawnEvent?.decision !== "withdrawn" ||
|
|
216
|
+
withdrawnEvent.event_id !== withdrawal.withdrawal_event_id) {
|
|
217
|
+
throw new Error("withdrawal output is not backed by the exact materialized terminal event");
|
|
218
|
+
}
|
|
219
|
+
await gitText(runner, controlRoot, controlHome, ["clone", "--quiet", "--no-local", clone, replay]);
|
|
220
|
+
const reproduced = await client.inspect(replay, capability.manifest.frontier, strictBaseline);
|
|
221
|
+
if (canonicalJcs(reproduced.roots) !== canonicalJcs(after.roots)) {
|
|
222
|
+
throw new Error("clean-clone withdrawal replay drifted");
|
|
223
|
+
}
|
|
224
|
+
const unchangedHead = await gitText(runner, source, controlHome, ["rev-parse", "--verify", "HEAD^{commit}"]);
|
|
225
|
+
const unchangedStatus = await gitText(runner, source, controlHome, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
226
|
+
if (unchangedHead !== sourceHead || unchangedStatus !== "") {
|
|
227
|
+
throw new Error("source frontier changed during withdrawal");
|
|
228
|
+
}
|
|
229
|
+
await gitText(runner, source, controlHome, ["fetch", "--quiet", "--no-tags", clone, "HEAD"]);
|
|
230
|
+
await gitText(runner, source, controlHome, ["merge", "--ff-only", "--no-edit", "FETCH_HEAD"]);
|
|
231
|
+
const installed = await client.inspect(source, capability.manifest.frontier, strictBaseline);
|
|
232
|
+
if (canonicalJcs(installed.roots) !== canonicalJcs(after.roots)) {
|
|
233
|
+
throw new Error("fast-forwarded source does not match the verified withdrawal clone");
|
|
234
|
+
}
|
|
235
|
+
const installedStatus = await gitText(runner, source, controlHome, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
236
|
+
if (installedStatus !== "")
|
|
237
|
+
throw new Error("fast-forwarded source is not clean");
|
|
238
|
+
await consumeWithdrawalCapability(record.landing.proposal_id, "withdrawn", options.storeRoot);
|
|
239
|
+
return {
|
|
240
|
+
ok: true,
|
|
241
|
+
command: "withdraw",
|
|
242
|
+
proposal_id: record.landing.proposal_id,
|
|
243
|
+
withdrawal_event_id: withdrawal.withdrawal_event_id,
|
|
244
|
+
roots: after.roots,
|
|
245
|
+
scientific_state_root_before: scientificRootBefore,
|
|
246
|
+
scientific_state_root_after: scientificRootAfter,
|
|
247
|
+
accepted_state_changed: false,
|
|
248
|
+
clean_clone_reproduced: true,
|
|
249
|
+
capability_consumed: true,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
await rm(controlRoot, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DiagnosticRunRecord } from "../run.js";
|
|
2
|
+
export declare function parseDiagnosticRunRecord(value: unknown): DiagnosticRunRecord;
|
|
3
|
+
export declare function projectDiagnosticRun(record: DiagnosticRunRecord): {
|
|
4
|
+
schema: "canopus.diagnostic-projection.v1";
|
|
5
|
+
authority: "read_only_projection";
|
|
6
|
+
run_id: string;
|
|
7
|
+
target: string;
|
|
8
|
+
candidate_digest: string;
|
|
9
|
+
verifier_status: "passed" | "failed" | "error";
|
|
10
|
+
landed: false;
|
|
11
|
+
clean_clone_reproduced: true;
|
|
12
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { arrayAt, enumAt, exactKeys, objectAt, relativePathAt, sha256At, stringAt, } from "../contracts/validation.js";
|
|
2
|
+
function literal(value, expected, at) {
|
|
3
|
+
if (value !== expected)
|
|
4
|
+
throw new Error(`${at} must be ${String(expected)}`);
|
|
5
|
+
return expected;
|
|
6
|
+
}
|
|
7
|
+
export function parseDiagnosticRunRecord(value) {
|
|
8
|
+
const record = objectAt(value, "diagnostic run");
|
|
9
|
+
exactKeys(record, [
|
|
10
|
+
"schema", "run_id", "status", "mode", "authority", "external_gate_credit",
|
|
11
|
+
"mission", "candidate", "verifier", "landing", "reproduction", "budget",
|
|
12
|
+
], [], "diagnostic run");
|
|
13
|
+
const mission = objectAt(record.mission, "diagnostic run.mission");
|
|
14
|
+
exactKeys(mission, ["id", "target", "digest", "starting_roots"], [], "diagnostic run.mission");
|
|
15
|
+
const candidate = objectAt(record.candidate, "diagnostic run.candidate");
|
|
16
|
+
exactKeys(candidate, ["digest", "status", "claim", "artifacts", "caveats"], [], "diagnostic run.candidate");
|
|
17
|
+
const verifier = objectAt(record.verifier, "diagnostic run.verifier");
|
|
18
|
+
exactKeys(verifier, ["status", "sandbox", "record"], [], "diagnostic run.verifier");
|
|
19
|
+
const reproduction = objectAt(record.reproduction, "diagnostic run.reproduction");
|
|
20
|
+
exactKeys(reproduction, ["matched", "roots", "verifier_status", "stdout_digest", "stderr_digest"], [], "diagnostic run.reproduction");
|
|
21
|
+
objectAt(mission.starting_roots, "diagnostic run.mission.starting_roots");
|
|
22
|
+
objectAt(reproduction.roots, "diagnostic run.reproduction.roots");
|
|
23
|
+
objectAt(record.budget, "diagnostic run.budget");
|
|
24
|
+
literal(record.schema, "canopus.diagnostic-run.v1", "diagnostic run.schema");
|
|
25
|
+
literal(record.status, "completed", "diagnostic run.status");
|
|
26
|
+
literal(record.mode, "no_land", "diagnostic run.mode");
|
|
27
|
+
literal(record.authority, "non_authoritative", "diagnostic run.authority");
|
|
28
|
+
literal(record.external_gate_credit, false, "diagnostic run.external_gate_credit");
|
|
29
|
+
literal(record.landing, null, "diagnostic run.landing");
|
|
30
|
+
stringAt(record.run_id, "diagnostic run.run_id", { min: 5, max: 128 });
|
|
31
|
+
stringAt(mission.id, "diagnostic run.mission.id", { min: 1, max: 134 });
|
|
32
|
+
stringAt(mission.target, "diagnostic run.mission.target", { min: 1, max: 256 });
|
|
33
|
+
sha256At(mission.digest, "diagnostic run.mission.digest");
|
|
34
|
+
sha256At(candidate.digest, "diagnostic run.candidate.digest");
|
|
35
|
+
enumAt(candidate.status, "diagnostic run.candidate.status", ["success", "null", "failed"]);
|
|
36
|
+
stringAt(candidate.claim, "diagnostic run.candidate.claim", { min: 1, max: 8192 });
|
|
37
|
+
arrayAt(candidate.artifacts, "diagnostic run.candidate.artifacts", { max: 10 }, (item, at) => {
|
|
38
|
+
const artifact = objectAt(item, at);
|
|
39
|
+
exactKeys(artifact, ["path", "kind", "digest", "bytes"], [], at);
|
|
40
|
+
relativePathAt(artifact.path, `${at}.path`);
|
|
41
|
+
stringAt(artifact.kind, `${at}.kind`, { min: 1, max: 128 });
|
|
42
|
+
sha256At(artifact.digest, `${at}.digest`);
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
arrayAt(candidate.caveats, "diagnostic run.candidate.caveats", { max: 10 }, (item, at) => stringAt(item, at, { min: 1, max: 4096 }));
|
|
46
|
+
enumAt(verifier.status, "diagnostic run.verifier.status", ["passed", "failed", "error"]);
|
|
47
|
+
literal(reproduction.matched, true, "diagnostic run.reproduction.matched");
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
export function projectDiagnosticRun(record) {
|
|
51
|
+
return {
|
|
52
|
+
schema: "canopus.diagnostic-projection.v1",
|
|
53
|
+
authority: "read_only_projection",
|
|
54
|
+
run_id: record.run_id,
|
|
55
|
+
target: record.mission.target,
|
|
56
|
+
candidate_digest: record.candidate.digest,
|
|
57
|
+
verifier_status: record.verifier.status,
|
|
58
|
+
landed: false,
|
|
59
|
+
clean_clone_reproduced: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const FAILURE_RECORD_SCHEMA: "canopus.failure.v0";
|
|
2
|
+
export declare const FAILURE_PROJECTION_SCHEMA: "canopus.failure-projection.v0";
|
|
3
|
+
export interface FailureRecord {
|
|
4
|
+
schema: typeof FAILURE_RECORD_SCHEMA;
|
|
5
|
+
run_id: string;
|
|
6
|
+
error: string;
|
|
7
|
+
phase: string;
|
|
8
|
+
landing_observed: boolean;
|
|
9
|
+
landing_recovery: string | null;
|
|
10
|
+
activity_tip: string;
|
|
11
|
+
authority: "non_authoritative";
|
|
12
|
+
}
|
|
13
|
+
export interface FailureProjection {
|
|
14
|
+
schema: typeof FAILURE_PROJECTION_SCHEMA;
|
|
15
|
+
authority: "read_only_projection";
|
|
16
|
+
run_id: string;
|
|
17
|
+
status: "failed";
|
|
18
|
+
phase: string;
|
|
19
|
+
error: string;
|
|
20
|
+
landing_status: "not_attempted" | "observed_requires_recovery";
|
|
21
|
+
landing_recovery: string | null;
|
|
22
|
+
activity_tip: string;
|
|
23
|
+
deletion_test: "Vela state does not depend on this projection; inspect retained recovery evidence for any observed landing effect.";
|
|
24
|
+
}
|
|
25
|
+
export declare function parseFailureRecord(value: unknown): FailureRecord;
|
|
26
|
+
export declare function projectFailure(record: FailureRecord): FailureProjection;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { exactKeys, objectAt, relativePathAt, sha256At, stringAt, } from "../contracts/validation.js";
|
|
2
|
+
export const FAILURE_RECORD_SCHEMA = "canopus.failure.v0";
|
|
3
|
+
export const FAILURE_PROJECTION_SCHEMA = "canopus.failure-projection.v0";
|
|
4
|
+
function literal(value, expected, at) {
|
|
5
|
+
if (value !== expected)
|
|
6
|
+
throw new Error(`${at} must be ${String(expected)}`);
|
|
7
|
+
return expected;
|
|
8
|
+
}
|
|
9
|
+
function booleanAt(value, at) {
|
|
10
|
+
if (typeof value !== "boolean")
|
|
11
|
+
throw new Error(`${at} must be a boolean`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
export function parseFailureRecord(value) {
|
|
15
|
+
const record = objectAt(value, "failure");
|
|
16
|
+
exactKeys(record, [
|
|
17
|
+
"schema",
|
|
18
|
+
"run_id",
|
|
19
|
+
"error",
|
|
20
|
+
"phase",
|
|
21
|
+
"landing_observed",
|
|
22
|
+
"landing_recovery",
|
|
23
|
+
"activity_tip",
|
|
24
|
+
"authority",
|
|
25
|
+
], [], "failure");
|
|
26
|
+
return {
|
|
27
|
+
schema: literal(record.schema, FAILURE_RECORD_SCHEMA, "failure.schema"),
|
|
28
|
+
run_id: stringAt(record.run_id, "failure.run_id", {
|
|
29
|
+
min: 5,
|
|
30
|
+
max: 128,
|
|
31
|
+
pattern: /^run_[A-Za-z0-9._-]+$/u,
|
|
32
|
+
}),
|
|
33
|
+
error: stringAt(record.error, "failure.error", { min: 1, max: 16_384 }),
|
|
34
|
+
phase: stringAt(record.phase, "failure.phase", {
|
|
35
|
+
min: 1,
|
|
36
|
+
max: 128,
|
|
37
|
+
pattern: /^[a-z][a-z0-9_]*$/u,
|
|
38
|
+
}),
|
|
39
|
+
landing_observed: booleanAt(record.landing_observed, "failure.landing_observed"),
|
|
40
|
+
landing_recovery: record.landing_recovery === null
|
|
41
|
+
? null
|
|
42
|
+
: relativePathAt(record.landing_recovery, "failure.landing_recovery"),
|
|
43
|
+
activity_tip: sha256At(record.activity_tip, "failure.activity_tip"),
|
|
44
|
+
authority: literal(record.authority, "non_authoritative", "failure.authority"),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function projectFailure(record) {
|
|
48
|
+
if (record.landing_observed !== (record.landing_recovery !== null)) {
|
|
49
|
+
throw new Error("failure landing observation and recovery evidence disagree");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
schema: FAILURE_PROJECTION_SCHEMA,
|
|
53
|
+
authority: "read_only_projection",
|
|
54
|
+
run_id: record.run_id,
|
|
55
|
+
status: "failed",
|
|
56
|
+
phase: record.phase,
|
|
57
|
+
error: record.error,
|
|
58
|
+
landing_status: record.landing_observed ? "observed_requires_recovery" : "not_attempted",
|
|
59
|
+
landing_recovery: record.landing_recovery,
|
|
60
|
+
activity_tip: record.activity_tip,
|
|
61
|
+
deletion_test: "Vela state does not depend on this projection; inspect retained recovery evidence for any observed landing effect.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { MissionRoots } from "../contracts/mission.js";
|
|
2
|
+
export declare const RUN_RECORD_SCHEMA: "canopus.run.v0";
|
|
3
|
+
export declare const RUN_PROJECTION_SCHEMA: "canopus.run-projection.v0";
|
|
4
|
+
export interface RunRecord {
|
|
5
|
+
schema: typeof RUN_RECORD_SCHEMA;
|
|
6
|
+
run_id: string;
|
|
7
|
+
status: "completed";
|
|
8
|
+
authority: "non_authoritative";
|
|
9
|
+
external_gate_credit: false;
|
|
10
|
+
mission: {
|
|
11
|
+
id: string;
|
|
12
|
+
target: string;
|
|
13
|
+
digest: string;
|
|
14
|
+
starting_roots: MissionRoots;
|
|
15
|
+
};
|
|
16
|
+
candidate: {
|
|
17
|
+
digest: string;
|
|
18
|
+
status: "success" | "null" | "failed";
|
|
19
|
+
claim: string;
|
|
20
|
+
artifacts: Array<{
|
|
21
|
+
path: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
digest: string;
|
|
24
|
+
bytes: number;
|
|
25
|
+
}>;
|
|
26
|
+
caveats: string[];
|
|
27
|
+
};
|
|
28
|
+
verifier: {
|
|
29
|
+
status: "passed" | "failed" | "error";
|
|
30
|
+
sandbox: "macos_sandbox" | "container_network_denied";
|
|
31
|
+
record: {
|
|
32
|
+
argv: string[];
|
|
33
|
+
executable_digest: string;
|
|
34
|
+
exit_code: number;
|
|
35
|
+
stdout_digest: string;
|
|
36
|
+
stderr_digest: string;
|
|
37
|
+
duration_ms: number;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
landing: {
|
|
41
|
+
operation_id: string;
|
|
42
|
+
receipt_root: string;
|
|
43
|
+
proposal_id: string;
|
|
44
|
+
route: "permit" | "defer" | "exact_retry";
|
|
45
|
+
original_route: "permit" | "defer" | null;
|
|
46
|
+
accepted_event_delta: number | null;
|
|
47
|
+
publication_state: string;
|
|
48
|
+
};
|
|
49
|
+
final_roots: MissionRoots;
|
|
50
|
+
reproduction: {
|
|
51
|
+
matched: boolean;
|
|
52
|
+
roots: MissionRoots;
|
|
53
|
+
verifier_status: "passed" | "failed" | "error";
|
|
54
|
+
stdout_digest: string;
|
|
55
|
+
stderr_digest: string;
|
|
56
|
+
};
|
|
57
|
+
budget: {
|
|
58
|
+
research_elapsed_ms: number;
|
|
59
|
+
research_processes: number;
|
|
60
|
+
research_output_bytes: number;
|
|
61
|
+
prompt_bytes: number;
|
|
62
|
+
artifact_bytes: number;
|
|
63
|
+
attempts: number;
|
|
64
|
+
observed_tokens: number;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export interface RunProjection {
|
|
68
|
+
schema: typeof RUN_PROJECTION_SCHEMA;
|
|
69
|
+
authority: "read_only_projection";
|
|
70
|
+
run_id: string;
|
|
71
|
+
mission_id: string;
|
|
72
|
+
target: string;
|
|
73
|
+
starting_roots: MissionRoots;
|
|
74
|
+
final_roots: MissionRoots;
|
|
75
|
+
candidate_digest: string;
|
|
76
|
+
candidate_status: "success" | "null" | "failed";
|
|
77
|
+
artifact_digests: string[];
|
|
78
|
+
verifier_status: "passed" | "failed" | "error";
|
|
79
|
+
receipt_root: string;
|
|
80
|
+
proposal_id: string;
|
|
81
|
+
route: "permit" | "defer" | "exact_retry";
|
|
82
|
+
original_route: "permit" | "defer" | null;
|
|
83
|
+
accepted_state_effect: "policy_admitted" | "unchanged_pending" | "exact_retry";
|
|
84
|
+
publication_state: string;
|
|
85
|
+
clean_clone_reproduced: boolean;
|
|
86
|
+
deletion_test: "Vela replay and accepted state do not depend on this projection.";
|
|
87
|
+
}
|
|
88
|
+
export declare function parseRunRecord(value: unknown): RunRecord;
|
|
89
|
+
export declare function projectRun(record: RunRecord): RunProjection;
|