@cassiomc1/forgeloop 0.1.13 → 0.1.15
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/.forgeloop/forgeloop.gitignore +1 -0
- package/LOOP_ENGINEERING.md +61 -5
- package/PROTOCOL_INTEGRATION.md +104 -0
- package/README.md +34 -16
- package/THREAT_MODEL.md +9 -0
- package/package.json +1 -1
- package/schemas/authority.schema.json +34 -0
- package/schemas/check.schema.json +2 -0
- package/schemas/execution.schema.json +63 -0
- package/src/cli.js +64 -12
- package/src/commands/inspect.js +2 -0
- package/src/commands/prepare-completion.js +2 -2
- package/src/commands/run-check.js +83 -0
- package/src/commands/validate-protocol.js +18 -0
- package/src/core/artifacts.js +8 -0
- package/src/core/audit.js +2 -2
- package/src/core/bundles.js +72 -0
- package/src/core/checks.js +28 -5
- package/src/core/completion-artifacts.js +346 -65
- package/src/core/completion-recovery.js +4 -0
- package/src/core/completion-relationships.js +39 -4
- package/src/core/completion.js +48 -8
- package/src/core/coverage.js +15 -2
- package/src/core/evidence-readiness.js +57 -6
- package/src/core/execution.js +185 -0
- package/src/core/inspect.js +3 -1
- package/src/core/next-action.js +87 -9
- package/src/core/phase.js +29 -6
- package/src/core/protocol.js +5 -0
- package/src/core/receipt.js +6 -6
- package/src/core/runtime-context.js +80 -0
- package/src/core/schema-validation.js +1 -0
- package/src/core/templates.js +1 -0
- package/src/core/trusted-authority.js +296 -0
- package/src/core/verification-capability.js +1110 -0
package/src/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { formatPolicyResult, runPolicy } from "./commands/policy.js";
|
|
|
24
24
|
import { formatBundleResult, runBundle } from "./commands/bundle.js";
|
|
25
25
|
import { formatPrepareCompletionResult, runPrepareCompletion } from "./commands/prepare-completion.js";
|
|
26
26
|
import { formatRecordCheckResult, runRecordCheck } from "./commands/record-check.js";
|
|
27
|
+
import { formatRunCheckResult, runCheck } from "./commands/run-check.js";
|
|
27
28
|
import { formatRecordTerminalResult, runRecordTerminalResult } from "./commands/record-terminal-result.js";
|
|
28
29
|
import { formatNextActionResult, runNext } from "./commands/next.js";
|
|
29
30
|
import { resolveTarget } from "./core/filesystem.js";
|
|
@@ -31,7 +32,7 @@ import { getPackageRoot } from "./core/templates.js";
|
|
|
31
32
|
import { ARTIFACT_PATHS } from "./core/artifacts.js";
|
|
32
33
|
|
|
33
34
|
function usage(command = null) {
|
|
34
|
-
const commands = "init|doctor|update|activate|route|preflight|advance|next|prepare-completion|record-check|record-terminal-result|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
|
|
35
|
+
const commands = "init|doctor|update|activate|route|preflight|advance|next|prepare-completion|run-check|record-check|record-terminal-result|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
|
|
35
36
|
const options = [" --path <directory> target project directory (default: current directory)"];
|
|
36
37
|
if (!command || command === "init" || command === "update") {
|
|
37
38
|
options.push(" --dry-run show planned writes without changing files");
|
|
@@ -53,7 +54,7 @@ function usage(command = null) {
|
|
|
53
54
|
if (!command || command === "advance") {
|
|
54
55
|
options.push(" --to <phase> destination workflow phase");
|
|
55
56
|
}
|
|
56
|
-
if (!command || ["activate", "advance", "next", "prepare-completion", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
|
|
57
|
+
if (!command || ["activate", "advance", "next", "prepare-completion", "run-check", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
|
|
57
58
|
options.push(" --json emit structured output as JSON");
|
|
58
59
|
}
|
|
59
60
|
if (!command || ["preflight", "complete", "audit", "report"].includes(command)) {
|
|
@@ -78,16 +79,23 @@ function usage(command = null) {
|
|
|
78
79
|
if (!command || command === "validate-receipt") {
|
|
79
80
|
options.push(" --file <path> receipt file relative to target");
|
|
80
81
|
}
|
|
81
|
-
if (!command || command === "record-check") {
|
|
82
|
+
if (!command || command === "record-check" || command === "run-check") {
|
|
82
83
|
options.push(" --id <id> stable check identifier");
|
|
83
|
-
options.push(" --kind <kind> check kind (default: command)");
|
|
84
84
|
options.push(" --requirement <id> completion requirement covered by the check");
|
|
85
|
+
options.push(" --details <json> additional structured check details");
|
|
86
|
+
}
|
|
87
|
+
if (!command || command === "record-check") {
|
|
88
|
+
options.push(" --kind <kind> check kind (default: command; use manual-review for manual evidence)");
|
|
85
89
|
options.push(" --status <status> passed, failed, blocked, or not-run");
|
|
86
90
|
options.push(" --evidence-kind <kind> OBSERVED, INFERRED, NOT_VERIFIED, or BLOCKED");
|
|
87
|
-
options.push(" --command <text>
|
|
88
|
-
options.push(" --result <text> observed result supplied by the
|
|
91
|
+
options.push(" --command <text> recorded only as metadata; it is never executed");
|
|
92
|
+
options.push(" --result <text> observed result supplied by the actor");
|
|
89
93
|
options.push(" --exit-code <number> observed process exit code");
|
|
90
|
-
options.push(" --
|
|
94
|
+
options.push(" --execution-ref <id> ForgeLoop execution artifact reference");
|
|
95
|
+
options.push(" --provenance <value> FORGELOOP_EXECUTED, ACTOR_REPORTED, or MANUAL_OBSERVATION");
|
|
96
|
+
}
|
|
97
|
+
if (!command || command === "run-check") {
|
|
98
|
+
options.push(" -- <argv> exact command argv to classify, execute, and attest");
|
|
91
99
|
}
|
|
92
100
|
if (!command || command === "record-terminal-result") {
|
|
93
101
|
options.push(" --requirement <id> terminal requirement covered by the result");
|
|
@@ -133,6 +141,9 @@ export function parseArgs(argv) {
|
|
|
133
141
|
checkResult: null,
|
|
134
142
|
checkExitCode: null,
|
|
135
143
|
checkDetails: null,
|
|
144
|
+
checkExecutionRef: null,
|
|
145
|
+
checkProvenance: null,
|
|
146
|
+
commandArgv: [],
|
|
136
147
|
checkType: null,
|
|
137
148
|
checkSource: null,
|
|
138
149
|
policy: null,
|
|
@@ -144,7 +155,7 @@ export function parseArgs(argv) {
|
|
|
144
155
|
|
|
145
156
|
for (let index = 0; index < argv.length; index += 1) {
|
|
146
157
|
const argument = argv[index];
|
|
147
|
-
if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "next", "prepare-completion", "record-check", "record-terminal-result", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
|
|
158
|
+
if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "next", "prepare-completion", "run-check", "record-check", "record-terminal-result", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
|
|
148
159
|
if (command) throw new Error(`Multiple commands are not supported: ${argument}`);
|
|
149
160
|
command = argument;
|
|
150
161
|
} else if (argument === "--help" || argument === "-h") {
|
|
@@ -304,6 +315,19 @@ export function parseArgs(argv) {
|
|
|
304
315
|
throw new Error("--details must be a JSON object");
|
|
305
316
|
}
|
|
306
317
|
index += 1;
|
|
318
|
+
} else if (argument === "--execution-ref") {
|
|
319
|
+
const executionRef = argv[index + 1];
|
|
320
|
+
if (!executionRef || executionRef.startsWith("-")) throw new Error("--execution-ref requires an execution ID");
|
|
321
|
+
options.checkExecutionRef = executionRef;
|
|
322
|
+
index += 1;
|
|
323
|
+
} else if (argument === "--provenance") {
|
|
324
|
+
const provenance = argv[index + 1];
|
|
325
|
+
if (!provenance || provenance.startsWith("-")) throw new Error("--provenance requires a provenance value");
|
|
326
|
+
options.checkProvenance = provenance;
|
|
327
|
+
index += 1;
|
|
328
|
+
} else if (argument === "--" && command === "run-check") {
|
|
329
|
+
options.commandArgv = argv.slice(index + 1);
|
|
330
|
+
break;
|
|
307
331
|
} else if (argument === "--path") {
|
|
308
332
|
options.path = argv[index + 1];
|
|
309
333
|
if (!options.path || options.path.startsWith("-")) throw new Error("--path requires a directory");
|
|
@@ -324,7 +348,7 @@ export function parseArgs(argv) {
|
|
|
324
348
|
|
|
325
349
|
if (!command) return { command: null, options };
|
|
326
350
|
|
|
327
|
-
const jsonCommands = ["doctor", "route", "activate", "advance", "next", "prepare-completion", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
|
|
351
|
+
const jsonCommands = ["doctor", "route", "activate", "advance", "next", "prepare-completion", "run-check", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
|
|
328
352
|
if (!jsonCommands.includes(command) && options.json) {
|
|
329
353
|
throw new Error(`Option --json is not valid for ${command}`);
|
|
330
354
|
}
|
|
@@ -377,8 +401,10 @@ export function parseArgs(argv) {
|
|
|
377
401
|
options.checkResult,
|
|
378
402
|
options.checkExitCode,
|
|
379
403
|
options.checkDetails,
|
|
404
|
+
options.checkExecutionRef,
|
|
405
|
+
options.checkProvenance,
|
|
380
406
|
];
|
|
381
|
-
if (
|
|
407
|
+
if (!["record-check", "run-check"].includes(command) && checkOptions.some((value) => value !== null)) {
|
|
382
408
|
throw new Error(`Check recording options are not valid for ${command}`);
|
|
383
409
|
}
|
|
384
410
|
if (command === "record-check" && !options.help) {
|
|
@@ -388,6 +414,17 @@ export function parseArgs(argv) {
|
|
|
388
414
|
if (!options.checkEvidenceKind) throw new Error("record-check requires --evidence-kind");
|
|
389
415
|
if (!options.checkCommand && !options.checkResult) throw new Error("record-check requires --command or --result");
|
|
390
416
|
}
|
|
417
|
+
if (command === "run-check" && !options.help) {
|
|
418
|
+
if (!options.checkId) throw new Error("run-check requires --id");
|
|
419
|
+
if (!options.checkRequirement) throw new Error("run-check requires --requirement");
|
|
420
|
+
if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
|
|
421
|
+
|| options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
|
|
422
|
+
throw new Error("run-check accepts only --id, --requirement, --details, and -- <argv>");
|
|
423
|
+
}
|
|
424
|
+
if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
|
|
425
|
+
throw new Error("run-check requires -- followed by an exact command argv");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
391
428
|
return { command, options };
|
|
392
429
|
}
|
|
393
430
|
|
|
@@ -490,6 +527,19 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
490
527
|
return 0;
|
|
491
528
|
}
|
|
492
529
|
|
|
530
|
+
if (command === "run-check") {
|
|
531
|
+
const result = await runCheck({
|
|
532
|
+
target,
|
|
533
|
+
packageRoot,
|
|
534
|
+
id: options.checkId,
|
|
535
|
+
requirement: options.checkRequirement,
|
|
536
|
+
argv: options.commandArgv,
|
|
537
|
+
details: options.checkDetails ?? undefined,
|
|
538
|
+
});
|
|
539
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatRunCheckResult(result));
|
|
540
|
+
return result.check.status === "passed" ? 0 : 1;
|
|
541
|
+
}
|
|
542
|
+
|
|
493
543
|
if (command === "record-check") {
|
|
494
544
|
const result = await runRecordCheck({
|
|
495
545
|
target,
|
|
@@ -501,8 +551,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
501
551
|
evidenceKind: options.checkEvidenceKind,
|
|
502
552
|
command: options.checkCommand ?? undefined,
|
|
503
553
|
result: options.checkResult ?? undefined,
|
|
504
|
-
exitCode: options.checkExitCode,
|
|
554
|
+
...(options.checkExitCode === null ? {} : { exitCode: options.checkExitCode }),
|
|
505
555
|
details: options.checkDetails ?? undefined,
|
|
556
|
+
executionRef: options.checkExecutionRef ?? undefined,
|
|
557
|
+
provenance: options.checkProvenance ?? undefined,
|
|
506
558
|
});
|
|
507
559
|
console.log(options.json ? JSON.stringify(result, null, 2) : formatRecordCheckResult(result));
|
|
508
560
|
return 0;
|
|
@@ -606,7 +658,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
606
658
|
}
|
|
607
659
|
return result.conflicts.length === 0 ? 0 : 1;
|
|
608
660
|
} catch (error) {
|
|
609
|
-
console.error(`error: ${error.message}`);
|
|
661
|
+
console.error(`error: ${error.code ? `${error.code}: ` : ""}${error.message}`);
|
|
610
662
|
return 1;
|
|
611
663
|
}
|
|
612
664
|
}
|
package/src/commands/inspect.js
CHANGED
|
@@ -8,6 +8,8 @@ export function formatInspectResult(report) {
|
|
|
8
8
|
`Manifest: ${report.manifest.status}`,
|
|
9
9
|
`Profile: ${report.profile.mode ?? "unknown"}/${report.profile.status ?? "unknown"}`,
|
|
10
10
|
`Protocol: v${report.protocol.version}`,
|
|
11
|
+
`Authority source: ${report.authority.sourceType ?? "none configured"} / ${report.authority.trusted ? "TRUSTED" : report.authority.trustMode === "NONE" ? "UNATTESTED" : "UNTRUSTED"}`,
|
|
12
|
+
`Authority trust: ${report.authority.trustMode}`,
|
|
11
13
|
`State: ${report.state.status}`,
|
|
12
14
|
`Adapters: ${report.adapters.detected.length} detected`,
|
|
13
15
|
`Findings: ${report.findings.length}`,
|
|
@@ -2,8 +2,8 @@ import { prepareCompletion as prepareCompletionArtifacts } from "../core/complet
|
|
|
2
2
|
|
|
3
3
|
export { prepareCompletionArtifacts as prepareCompletion };
|
|
4
4
|
|
|
5
|
-
export async function runPrepareCompletion({ target, packageRoot }) {
|
|
6
|
-
return prepareCompletionArtifacts({ target, packageRoot });
|
|
5
|
+
export async function runPrepareCompletion({ target, packageRoot, authorityContext, runtimeContext }) {
|
|
6
|
+
return prepareCompletionArtifacts({ target, packageRoot, authorityContext, runtimeContext });
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function formatPrepareCompletionResult(result) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertRecordCheckPrerequisites,
|
|
3
|
+
recordCheck as recordCheckArtifact,
|
|
4
|
+
} from "../core/completion-artifacts.js";
|
|
5
|
+
import { runCommandExecution } from "../core/execution.js";
|
|
6
|
+
|
|
7
|
+
export async function runCheck({
|
|
8
|
+
target,
|
|
9
|
+
packageRoot,
|
|
10
|
+
id,
|
|
11
|
+
requirement,
|
|
12
|
+
argv,
|
|
13
|
+
details,
|
|
14
|
+
authorityContext,
|
|
15
|
+
runtimeContext,
|
|
16
|
+
}) {
|
|
17
|
+
if (typeof id !== "string" || id.trim() === "" || typeof requirement !== "string" || requirement.trim() === "") {
|
|
18
|
+
const error = new Error("run-check requires non-empty id and requirement");
|
|
19
|
+
error.code = "E_CHECK_INVALID";
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const ready = await assertRecordCheckPrerequisites({
|
|
23
|
+
target,
|
|
24
|
+
packageRoot,
|
|
25
|
+
requirement,
|
|
26
|
+
status: "passed",
|
|
27
|
+
evidenceKind: "OBSERVED",
|
|
28
|
+
authorityContext,
|
|
29
|
+
runtimeContext,
|
|
30
|
+
});
|
|
31
|
+
const verificationCycle = ready.state.verificationCycle ?? 1;
|
|
32
|
+
const execution = await runCommandExecution({
|
|
33
|
+
target,
|
|
34
|
+
packageRoot,
|
|
35
|
+
taskId: ready.contract.value.taskId,
|
|
36
|
+
checkId: id,
|
|
37
|
+
requirement,
|
|
38
|
+
verificationCycle,
|
|
39
|
+
argv,
|
|
40
|
+
details,
|
|
41
|
+
authorityContext,
|
|
42
|
+
runtimeContext,
|
|
43
|
+
});
|
|
44
|
+
const status = execution.execution.status === "passed" ? "passed" : "failed";
|
|
45
|
+
const recorded = await recordCheckArtifact({
|
|
46
|
+
target,
|
|
47
|
+
packageRoot,
|
|
48
|
+
id,
|
|
49
|
+
kind: "command",
|
|
50
|
+
requirement,
|
|
51
|
+
status,
|
|
52
|
+
evidenceKind: "OBSERVED",
|
|
53
|
+
command: execution.execution.argv.join(" "),
|
|
54
|
+
result: execution.result,
|
|
55
|
+
...(execution.execution.exitCode === null ? {} : { exitCode: execution.execution.exitCode }),
|
|
56
|
+
details,
|
|
57
|
+
executionRef: execution.execution.executionId,
|
|
58
|
+
provenance: "FORGELOOP_EXECUTED",
|
|
59
|
+
authorityContext,
|
|
60
|
+
runtimeContext,
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
...recorded,
|
|
64
|
+
execution: execution.execution,
|
|
65
|
+
executionPath: execution.path,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatRunCheckResult(result) {
|
|
70
|
+
return [
|
|
71
|
+
"FORGELOOP CHECK EXECUTED",
|
|
72
|
+
`id: ${result.check.id}`,
|
|
73
|
+
`requirement: ${result.check.requirement}`,
|
|
74
|
+
`status: ${result.check.status}`,
|
|
75
|
+
`execution: ${result.execution.executionId}`,
|
|
76
|
+
`argv: ${result.execution.argv.join(" ")}`,
|
|
77
|
+
`exit code: ${result.execution.exitCode ?? "not-started"}`,
|
|
78
|
+
`artifact: ${result.executionPath}`,
|
|
79
|
+
`coverage: ${result.coverage.find((item) => item.requirement === result.check.requirement)?.status ?? "NOT_VERIFIED"}`,
|
|
80
|
+
`receipt: ${result.path}`,
|
|
81
|
+
"",
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
@@ -9,6 +9,7 @@ import { validateTaskBrief, validateDelegatedResult } from "../core/delegation.j
|
|
|
9
9
|
import { ARTIFACT_PATHS, readJsonArtifact } from "../core/artifacts.js";
|
|
10
10
|
import { evaluatePreflight, validateReadyProtocolConsistency } from "../core/preflight.js";
|
|
11
11
|
import { validateEventLedger, validateStateLedgerCoherence } from "../core/events.js";
|
|
12
|
+
import { validateChecksExecutionProvenance } from "../core/completion-artifacts.js";
|
|
12
13
|
|
|
13
14
|
async function readArtifact(target, relativePath, label) {
|
|
14
15
|
if (!relativePath) return null;
|
|
@@ -87,6 +88,23 @@ export async function runValidateProtocol({
|
|
|
87
88
|
await validateLoaded(loaded.find((item) => item.label === "route"), "routing-result", async (value) => assertRouteInvariants(value));
|
|
88
89
|
await validateLoaded(loaded.find((item) => item.label === "state"), "work-state", async (value) => assertWorkStateSemantics(value));
|
|
89
90
|
await validateLoaded(loaded.find((item) => item.label === "receipt"), "execution-receipt", async (value) => validateReceipt(value, packageRoot));
|
|
91
|
+
for (const [value, artifactPath] of [
|
|
92
|
+
[state, stateFile],
|
|
93
|
+
[receipt, receiptFile],
|
|
94
|
+
]) {
|
|
95
|
+
if (!value) continue;
|
|
96
|
+
const provenanceErrors = await validateChecksExecutionProvenance(value.checks, {
|
|
97
|
+
target,
|
|
98
|
+
packageRoot,
|
|
99
|
+
taskId: value.taskId,
|
|
100
|
+
artifactPath,
|
|
101
|
+
});
|
|
102
|
+
schemaErrors.push(...provenanceErrors.map((error) => ({
|
|
103
|
+
...error,
|
|
104
|
+
message: `Command provenance validation failed: ${error.message}`,
|
|
105
|
+
artifacts: [artifactPath, ...(error.artifacts ?? [])],
|
|
106
|
+
})));
|
|
107
|
+
}
|
|
90
108
|
for (const item of loaded.filter((candidate) => candidate.label.startsWith("task brief:"))) {
|
|
91
109
|
await validateLoaded(item, "task-brief", async (value) => validateTaskBrief(value, packageRoot));
|
|
92
110
|
}
|
package/src/core/artifacts.js
CHANGED
|
@@ -17,8 +17,16 @@ export const ARTIFACT_PATHS = Object.freeze({
|
|
|
17
17
|
gates: ".forgeloop/gates",
|
|
18
18
|
state: ".forgeloop/work-state.json",
|
|
19
19
|
receipt: ".forgeloop/execution-receipt.json",
|
|
20
|
+
executionDirectory: ".forgeloop/executions",
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
export function executionArtifactPath(executionId) {
|
|
24
|
+
if (typeof executionId !== "string" || !/^exec-[A-Za-z0-9_-]+$/.test(executionId)) {
|
|
25
|
+
throw new ArtifactError("E_EXECUTION_REF_INVALID", "Execution reference must be a simple execution ID");
|
|
26
|
+
}
|
|
27
|
+
return `${ARTIFACT_PATHS.executionDirectory}/${executionId}.json`;
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
export class ArtifactError extends Error {
|
|
23
31
|
constructor(code, message, artifacts = []) {
|
|
24
32
|
super(message);
|
package/src/core/audit.js
CHANGED
|
@@ -34,8 +34,8 @@ async function compareChangedPaths(target, packageRoot) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export async function evaluateAudit({ target, packageRoot, strict = false } = {}) {
|
|
38
|
-
const completion = await evaluateCompletion({ target, packageRoot, strict });
|
|
37
|
+
export async function evaluateAudit({ target, packageRoot, strict = false, authorityContext, runtimeContext } = {}) {
|
|
38
|
+
const completion = await evaluateCompletion({ target, packageRoot, strict, authorityContext, runtimeContext });
|
|
39
39
|
let manifest = null;
|
|
40
40
|
let manifestError = null;
|
|
41
41
|
try {
|
package/src/core/bundles.js
CHANGED
|
@@ -4,6 +4,8 @@ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts
|
|
|
4
4
|
import { readContract, validateContract } from "./contract.js";
|
|
5
5
|
import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic } from "./filesystem.js";
|
|
6
6
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
7
|
+
import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
|
|
8
|
+
import { readExecutionArtifact } from "./execution.js";
|
|
7
9
|
|
|
8
10
|
export const BUNDLE_SCHEMA_VERSION = 1;
|
|
9
11
|
const BUNDLE_ROOT = ".forgeloop/tasks";
|
|
@@ -37,6 +39,34 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
|
|
|
37
39
|
safeTaskId(taskId);
|
|
38
40
|
const directory = bundleDirectory(taskId);
|
|
39
41
|
const artifacts = [];
|
|
42
|
+
const stateSource = await readJsonArtifact(target, ARTIFACT_PATHS.state, "work-state", packageRoot);
|
|
43
|
+
let receiptSource = null;
|
|
44
|
+
try {
|
|
45
|
+
receiptSource = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
48
|
+
}
|
|
49
|
+
const provenanceErrors = [
|
|
50
|
+
...(await validateChecksExecutionProvenance(stateSource.value.checks, {
|
|
51
|
+
target,
|
|
52
|
+
packageRoot,
|
|
53
|
+
taskId,
|
|
54
|
+
artifactPath: ARTIFACT_PATHS.state,
|
|
55
|
+
})),
|
|
56
|
+
...(await validateChecksExecutionProvenance(receiptSource?.value?.checks, {
|
|
57
|
+
target,
|
|
58
|
+
packageRoot,
|
|
59
|
+
taskId,
|
|
60
|
+
artifactPath: ARTIFACT_PATHS.receipt,
|
|
61
|
+
})),
|
|
62
|
+
];
|
|
63
|
+
if (provenanceErrors.length > 0) {
|
|
64
|
+
const first = provenanceErrors[0];
|
|
65
|
+
const error = new Error(first.message);
|
|
66
|
+
error.code = first.code;
|
|
67
|
+
error.artifacts = first.artifacts;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
40
70
|
const required = [
|
|
41
71
|
[ARTIFACT_PATHS.contract, "contract.json", "current-contract"],
|
|
42
72
|
[ARTIFACT_PATHS.route, "route.json", "routing-result"],
|
|
@@ -64,6 +94,16 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
|
|
|
64
94
|
const copied = await copyJson(target, sourcePath, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
|
|
65
95
|
if (copied && !artifacts.includes(destinationName)) artifacts.push(destinationName);
|
|
66
96
|
}
|
|
97
|
+
const executionRefs = [...new Set([
|
|
98
|
+
...(stateSource.value.checks ?? []),
|
|
99
|
+
...(receiptSource?.value?.checks ?? []),
|
|
100
|
+
].map((check) => check?.executionRef).filter(Boolean))].sort();
|
|
101
|
+
for (const executionRef of executionRefs) {
|
|
102
|
+
const execution = await readExecutionArtifact({ target, executionRef, packageRoot });
|
|
103
|
+
const destination = `${directory}/executions/${execution.value.executionId}.json`;
|
|
104
|
+
await writeJsonArtifact(target, destination, execution.value, "execution", packageRoot);
|
|
105
|
+
artifacts.push(`executions/${execution.value.executionId}.json`);
|
|
106
|
+
}
|
|
67
107
|
const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
|
|
68
108
|
if (await fileExists(eventsPath)) {
|
|
69
109
|
await assertSafePath(target, `${directory}/events.ndjson`);
|
|
@@ -107,7 +147,13 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
107
147
|
"sources.json": ["sources", "source-registry"],
|
|
108
148
|
"config.json": ["config", "config"],
|
|
109
149
|
};
|
|
150
|
+
const executions = {};
|
|
110
151
|
for (const artifact of manifest.value.artifacts) {
|
|
152
|
+
if (artifact.startsWith("executions/") && artifact.endsWith(".json")) {
|
|
153
|
+
const execution = await readJsonArtifact(target, `${directory}/${artifact}`, "execution", packageRoot);
|
|
154
|
+
executions[execution.value.executionId] = execution.value;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
111
157
|
const mapping = mappings[artifact];
|
|
112
158
|
if (!mapping) continue;
|
|
113
159
|
const loadedArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, mapping[1], packageRoot);
|
|
@@ -116,5 +162,31 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
116
162
|
}
|
|
117
163
|
loaded[mapping[0]] = loadedArtifact.value;
|
|
118
164
|
}
|
|
165
|
+
if (Object.keys(executions).length > 0) loaded.executions = executions;
|
|
166
|
+
const provenanceErrors = [
|
|
167
|
+
...(await validateChecksExecutionProvenance(loaded.state?.checks, {
|
|
168
|
+
target,
|
|
169
|
+
packageRoot,
|
|
170
|
+
taskId,
|
|
171
|
+
executionArtifacts: executions,
|
|
172
|
+
allowForeignCwd: true,
|
|
173
|
+
artifactPath: "state.json",
|
|
174
|
+
})),
|
|
175
|
+
...(await validateChecksExecutionProvenance(loaded.receipt?.checks, {
|
|
176
|
+
target,
|
|
177
|
+
packageRoot,
|
|
178
|
+
taskId,
|
|
179
|
+
executionArtifacts: executions,
|
|
180
|
+
allowForeignCwd: true,
|
|
181
|
+
artifactPath: "receipt.json",
|
|
182
|
+
})),
|
|
183
|
+
];
|
|
184
|
+
if (provenanceErrors.length > 0) {
|
|
185
|
+
const first = provenanceErrors[0];
|
|
186
|
+
const error = new Error(first.message);
|
|
187
|
+
error.code = first.code;
|
|
188
|
+
error.artifacts = first.artifacts;
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
119
191
|
return { manifest: manifest.value, artifacts: loaded };
|
|
120
192
|
}
|
package/src/core/checks.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
import { validateVerificationAuthority } from "./verification-capability.js";
|
|
2
3
|
|
|
3
4
|
export const CHECK_SCHEMA_VERSION = 1;
|
|
4
5
|
export const CHECK_STATUSES = Object.freeze(["passed", "failed", "blocked", "not-run"]);
|
|
5
6
|
export const CHECK_EVIDENCE_KINDS = Object.freeze(["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED"]);
|
|
7
|
+
export const CHECK_PROVENANCE = Object.freeze(["FORGELOOP_EXECUTED", "ACTOR_REPORTED", "MANUAL_OBSERVATION"]);
|
|
6
8
|
|
|
7
9
|
function checkError(code, message, artifacts = []) {
|
|
8
10
|
const error = new Error(message);
|
|
@@ -45,7 +47,7 @@ function assertCompoundStatus(value, label) {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
export function createCheck(input = {}) {
|
|
50
|
+
export function createCheck(input = {}, options = {}) {
|
|
49
51
|
const check = {
|
|
50
52
|
schemaVersion: CHECK_SCHEMA_VERSION,
|
|
51
53
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -59,12 +61,14 @@ export function createCheck(input = {}) {
|
|
|
59
61
|
...(input.timestamp !== undefined ? { timestamp: input.timestamp } : {}),
|
|
60
62
|
...(input.repositoryFingerprint !== undefined ? { repositoryFingerprint: input.repositoryFingerprint } : {}),
|
|
61
63
|
...(input.viewport !== undefined ? { viewport: structuredClone(input.viewport) } : {}),
|
|
64
|
+
...(input.executionRef !== undefined ? { executionRef: input.executionRef } : {}),
|
|
65
|
+
...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
|
|
62
66
|
...(input.details !== undefined ? { details: structuredClone(input.details) } : {}),
|
|
63
67
|
};
|
|
64
|
-
return assertCheck(check);
|
|
68
|
+
return assertCheck(check, "check", options);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
export function assertCheck(value, label = "check") {
|
|
71
|
+
export function assertCheck(value, label = "check", options = {}) {
|
|
68
72
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
69
73
|
throw checkError("E_CHECK_INVALID", `${label} must be an object`);
|
|
70
74
|
}
|
|
@@ -75,6 +79,19 @@ export function assertCheck(value, label = "check") {
|
|
|
75
79
|
string(value.kind, `${label}.kind`);
|
|
76
80
|
string(value.requirement, `${label}.requirement`);
|
|
77
81
|
string(value.source, `${label}.source`);
|
|
82
|
+
optionalString(value.executionRef, `${label}.executionRef`);
|
|
83
|
+
if (value.provenance !== undefined && !CHECK_PROVENANCE.includes(value.provenance)) {
|
|
84
|
+
throw checkError("E_CHECK_INVALID", `${label}.provenance must be one of ${CHECK_PROVENANCE.join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
if (options.requireCommandProvenance === true
|
|
87
|
+
&& value.kind === "command"
|
|
88
|
+
&& value.evidenceKind === "OBSERVED"
|
|
89
|
+
&& (value.executionRef === undefined || value.provenance !== "FORGELOOP_EXECUTED")) {
|
|
90
|
+
throw checkError(
|
|
91
|
+
"E_COMMAND_PROVENANCE_UNATTESTED",
|
|
92
|
+
`${label} observed command evidence requires ForgeLoop execution provenance`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
78
95
|
if (!CHECK_STATUSES.includes(value.status)) {
|
|
79
96
|
throw checkError("E_CHECK_INVALID", `${label}.status must be one of ${CHECK_STATUSES.join(", ")}`);
|
|
80
97
|
}
|
|
@@ -97,15 +114,21 @@ export function assertCheck(value, label = "check") {
|
|
|
97
114
|
if (value.status === "not-run" && value.evidenceKind !== "NOT_VERIFIED") {
|
|
98
115
|
throw contradiction(`${label} not-run must use NOT_VERIFIED evidence`);
|
|
99
116
|
}
|
|
117
|
+
if (value.status === "passed") {
|
|
118
|
+
const auth = validateVerificationAuthority(value, options);
|
|
119
|
+
if (!auth.valid) {
|
|
120
|
+
throw checkError(auth.error.code, auth.error.message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
100
123
|
assertCompoundStatus(value, label);
|
|
101
124
|
return value;
|
|
102
125
|
}
|
|
103
126
|
|
|
104
|
-
export function assertCheckList(value, label = "checks") {
|
|
127
|
+
export function assertCheckList(value, label = "checks", options = {}) {
|
|
105
128
|
if (!Array.isArray(value)) throw checkError("E_CHECK_INVALID", `${label} must be an array`);
|
|
106
129
|
const ids = new Set();
|
|
107
130
|
value.forEach((item, index) => {
|
|
108
|
-
assertCheck(item, `${label}[${index}]
|
|
131
|
+
assertCheck(item, `${label}[${index}]`, options);
|
|
109
132
|
if (ids.has(item.id)) throw checkError("E_CHECK_INVALID", `${label} contains duplicate id ${item.id}`);
|
|
110
133
|
ids.add(item.id);
|
|
111
134
|
});
|