@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/core/completion.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
|
-
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
2
|
+
import { requiredEvidenceForTarget, validateChecksExecutionProvenance } from "./completion-artifacts.js";
|
|
3
3
|
import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger, validateStateLedgerCoherence } from "./events.js";
|
|
4
4
|
import { evaluatePreflight } from "./preflight.js";
|
|
5
5
|
import { readContract } from "./contract.js";
|
|
@@ -42,6 +42,9 @@ function repairNext(error) {
|
|
|
42
42
|
case "E_CHECK_INVALID":
|
|
43
43
|
case "E_CHECK_STATUS_CONTRADICTION":
|
|
44
44
|
return "Run forgeloop record-check with compatible observed evidence for the named requirement.";
|
|
45
|
+
case "E_COMMAND_PROVENANCE_UNATTESTED":
|
|
46
|
+
case "E_EXECUTION_REF_INVALID":
|
|
47
|
+
return "Run forgeloop run-check with the exact argv, or record the result as manual/NOT_VERIFIED evidence without claiming command execution.";
|
|
45
48
|
case "E_EVIDENCE_PARTIAL":
|
|
46
49
|
return "Run or finish the missing component checks and record observed evidence.";
|
|
47
50
|
case "E_EVIDENCE_INVALID":
|
|
@@ -77,6 +80,13 @@ function repairNext(error) {
|
|
|
77
80
|
return "Satisfy or refresh the named gate, then rerun forgeloop preflight.";
|
|
78
81
|
case "E_PROFILE_UNVERIFIED":
|
|
79
82
|
return "Use Standard mode for a fresh target, or verify PROJECT_PROFILE.md before Strict completion.";
|
|
83
|
+
case "E_INSTALLATION_AUTHORITY_REQUIRED":
|
|
84
|
+
case "E_AUTHORITY_INVALID":
|
|
85
|
+
case "E_AUTHORITY_SCOPE_MISMATCH":
|
|
86
|
+
case "E_AUTHORITY_UNTRUSTED_SOURCE":
|
|
87
|
+
return "Do not execute installation-capable verification commands without explicit scoped installation authority; use local equivalents or record NOT_VERIFIED.";
|
|
88
|
+
case "E_VERIFICATION_TOOL_UNAVAILABLE":
|
|
89
|
+
return "Use an available local verifier, an existing equivalent, or record NOT_VERIFIED if installation was not authorized.";
|
|
80
90
|
default:
|
|
81
91
|
return "Resolve this validator finding in the named artifact before retrying completion.";
|
|
82
92
|
}
|
|
@@ -151,7 +161,7 @@ async function validateLedger(target, taskId, state, errors, packageRoot) {
|
|
|
151
161
|
return ledger;
|
|
152
162
|
}
|
|
153
163
|
|
|
154
|
-
export async function evaluateCompletion({ target, packageRoot, strict = false } = {}) {
|
|
164
|
+
export async function evaluateCompletion({ target, packageRoot, strict = false, authorityContext, runtimeContext } = {}) {
|
|
155
165
|
const errors = [];
|
|
156
166
|
const preflight = await evaluatePreflight({ target, packageRoot, strict });
|
|
157
167
|
errors.push(...preflight.errors);
|
|
@@ -198,12 +208,32 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
198
208
|
|
|
199
209
|
if (receipt) {
|
|
200
210
|
try {
|
|
201
|
-
await validateReceipt(receipt.value, packageRoot
|
|
211
|
+
await validateReceipt(receipt.value, packageRoot, {
|
|
212
|
+
target,
|
|
213
|
+
taskId: contract?.value?.taskId,
|
|
214
|
+
authorityContext,
|
|
215
|
+
runtimeContext,
|
|
216
|
+
});
|
|
202
217
|
} catch (error) {
|
|
203
218
|
errors.push(issue(error.code ?? "E_RECEIPT_INVALID", `Execution receipt is invalid: ${error.message}`, [ARTIFACT_PATHS.receipt]));
|
|
204
219
|
}
|
|
205
220
|
}
|
|
206
221
|
|
|
222
|
+
if (contract) {
|
|
223
|
+
errors.push(...await validateChecksExecutionProvenance(state?.checks, {
|
|
224
|
+
target,
|
|
225
|
+
packageRoot,
|
|
226
|
+
taskId: contract.value.taskId,
|
|
227
|
+
artifactPath: ARTIFACT_PATHS.state,
|
|
228
|
+
}));
|
|
229
|
+
errors.push(...await validateChecksExecutionProvenance(receipt?.value?.checks, {
|
|
230
|
+
target,
|
|
231
|
+
packageRoot,
|
|
232
|
+
taskId: contract.value.taskId,
|
|
233
|
+
artifactPath: ARTIFACT_PATHS.receipt,
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
207
237
|
if (state && !["REVIEWING", "COMPLETE"].includes(state.phase)) {
|
|
208
238
|
errors.push(issue("E_PHASE_PREREQUISITE_MISSING", `Completion requires REVIEWING or COMPLETE state, found ${state.phase}`, [ARTIFACT_PATHS.state]));
|
|
209
239
|
}
|
|
@@ -223,6 +253,10 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
223
253
|
state,
|
|
224
254
|
receipt: receipt?.value,
|
|
225
255
|
requiredEvidence,
|
|
256
|
+
target,
|
|
257
|
+
taskId: contract?.value?.taskId,
|
|
258
|
+
authorityContext,
|
|
259
|
+
runtimeContext,
|
|
226
260
|
});
|
|
227
261
|
errors.push(...relationshipErrors);
|
|
228
262
|
coverage = receipt?.value?.evidenceCoverage ?? [];
|
|
@@ -290,12 +324,18 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
290
324
|
};
|
|
291
325
|
}
|
|
292
326
|
|
|
293
|
-
export async function runComplete({ target, packageRoot, strict = false, persist = true } = {}) {
|
|
294
|
-
const result = await evaluateCompletion({ target, packageRoot, strict });
|
|
327
|
+
export async function runComplete({ target, packageRoot, strict = false, persist = true, authorityContext, runtimeContext } = {}) {
|
|
328
|
+
const result = await evaluateCompletion({ target, packageRoot, strict, authorityContext, runtimeContext });
|
|
295
329
|
const rejectionCodes = [...new Set(result.errors.map((error) => error.code))].sort();
|
|
296
330
|
const evidenceOnlyRejection = rejectionCodes.length > 0
|
|
297
331
|
&& rejectionCodes.every(isRecoverableCompletionEvidenceCode);
|
|
298
|
-
|
|
332
|
+
const authorityRejection = rejectionCodes.some((code) => [
|
|
333
|
+
"E_INSTALLATION_AUTHORITY_REQUIRED",
|
|
334
|
+
"E_AUTHORITY_INVALID",
|
|
335
|
+
"E_AUTHORITY_SCOPE_MISMATCH",
|
|
336
|
+
"E_AUTHORITY_UNTRUSTED_SOURCE",
|
|
337
|
+
].includes(code));
|
|
338
|
+
if (persist && result.status === "REJECTED" && evidenceOnlyRejection && !authorityRejection) {
|
|
299
339
|
const state = await readWorkState(target, packageRoot);
|
|
300
340
|
if (state?.phase === "REVIEWING") {
|
|
301
341
|
const reasonCodes = rejectionCodes;
|
|
@@ -335,7 +375,7 @@ export async function runComplete({ target, packageRoot, strict = false, persist
|
|
|
335
375
|
...receipt.value,
|
|
336
376
|
stateFingerprint: canonicalFingerprint(next),
|
|
337
377
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
338
|
-
}, packageRoot);
|
|
378
|
+
}, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
|
|
339
379
|
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
|
340
380
|
}
|
|
341
381
|
const ledger = await validateEventLedger(target, packageRoot);
|
|
@@ -380,7 +420,7 @@ export async function runComplete({ target, packageRoot, strict = false, persist
|
|
|
380
420
|
...receipt.value,
|
|
381
421
|
stateFingerprint: canonicalFingerprint(next),
|
|
382
422
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
383
|
-
}, packageRoot);
|
|
423
|
+
}, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
|
|
384
424
|
await writeWorkState(target, next, { packageRoot });
|
|
385
425
|
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
|
386
426
|
}
|
package/src/core/coverage.js
CHANGED
|
@@ -73,10 +73,23 @@ export function assertCoverageList(value, label = "evidenceCoverage") {
|
|
|
73
73
|
return value;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function coverageForRequirements(requirements, checks, {
|
|
76
|
+
export function coverageForRequirements(requirements, checks, {
|
|
77
|
+
blockedIds = [],
|
|
78
|
+
target,
|
|
79
|
+
taskId,
|
|
80
|
+
authorities,
|
|
81
|
+
options = {},
|
|
82
|
+
} = {}) {
|
|
77
83
|
const normalizedRequirements = normalizeRequirements(requirements ?? []);
|
|
78
84
|
const normalizedChecks = Array.isArray(checks) ? checks : [];
|
|
79
|
-
const readiness = evaluateRequiredEvidence({
|
|
85
|
+
const readiness = evaluateRequiredEvidence({
|
|
86
|
+
requirements: normalizedRequirements,
|
|
87
|
+
checks: normalizedChecks,
|
|
88
|
+
target,
|
|
89
|
+
taskId,
|
|
90
|
+
authorities,
|
|
91
|
+
options,
|
|
92
|
+
});
|
|
80
93
|
const covered = new Set(readiness.covered.map((item) => item.id));
|
|
81
94
|
const partial = new Set(readiness.partial.map((item) => item.id));
|
|
82
95
|
const invalid = new Set(readiness.invalid.map((item) => item.id));
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
import { sha256 } from "./manifest.js";
|
|
2
|
+
import { validateVerificationAuthority } from "./verification-capability.js";
|
|
3
|
+
|
|
4
|
+
export const E_COMMAND_PROVENANCE_UNATTESTED = "E_COMMAND_PROVENANCE_UNATTESTED";
|
|
5
|
+
|
|
6
|
+
export function validateCommandProvenance(check) {
|
|
7
|
+
if (check?.kind !== "command" || check.evidenceKind !== "OBSERVED") {
|
|
8
|
+
return { valid: true, error: null };
|
|
9
|
+
}
|
|
10
|
+
if (check.provenance !== "FORGELOOP_EXECUTED" || typeof check.executionRef !== "string" || check.executionRef.trim() === "") {
|
|
11
|
+
return {
|
|
12
|
+
valid: false,
|
|
13
|
+
error: {
|
|
14
|
+
code: E_COMMAND_PROVENANCE_UNATTESTED,
|
|
15
|
+
message: "Observed command evidence requires a ForgeLoop execution artifact",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { valid: true, error: null };
|
|
20
|
+
}
|
|
2
21
|
|
|
3
22
|
export const REQUIREMENT_TYPES = Object.freeze([
|
|
4
23
|
"PRODUCT",
|
|
@@ -246,7 +265,7 @@ export function authoritativeChecksForRequirements({ requirements = [], checks =
|
|
|
246
265
|
});
|
|
247
266
|
}
|
|
248
267
|
|
|
249
|
-
function componentStatus(check, requirement, allChecks = []) {
|
|
268
|
+
function componentStatus(check, requirement, allChecks = [], options = {}) {
|
|
250
269
|
if (requirement.operator !== "ALL" || !requirement.requirements?.length) return null;
|
|
251
270
|
const components = check?.details?.components;
|
|
252
271
|
const statuses = requirement.requirements.map((child) => {
|
|
@@ -254,10 +273,23 @@ function componentStatus(check, requirement, allChecks = []) {
|
|
|
254
273
|
const matchingComp = components.filter((item) => (
|
|
255
274
|
item?.requirementId === child.id || item?.requirement === child.text
|
|
256
275
|
)).at(-1);
|
|
257
|
-
if (matchingComp)
|
|
276
|
+
if (matchingComp) {
|
|
277
|
+
const provenance = validateCommandProvenance(matchingComp);
|
|
278
|
+
if (!provenance.valid) return { ...matchingComp, status: "failed", reasonCode: provenance.error.code };
|
|
279
|
+
const auth = validateVerificationAuthority(matchingComp, options);
|
|
280
|
+
if (!auth.valid) return { ...matchingComp, status: "failed", reasonCode: auth.error.code };
|
|
281
|
+
return matchingComp;
|
|
282
|
+
}
|
|
258
283
|
}
|
|
259
284
|
const childCandidates = allChecks.filter((candidate) => matchesRequirement(candidate, child));
|
|
260
|
-
|
|
285
|
+
const childCheck = latestAuthoritativeCheck(childCandidates);
|
|
286
|
+
if (childCheck) {
|
|
287
|
+
const provenance = validateCommandProvenance(childCheck);
|
|
288
|
+
if (!provenance.valid) return { ...childCheck, status: "failed", reasonCode: provenance.error.code };
|
|
289
|
+
const auth = validateVerificationAuthority(childCheck, options);
|
|
290
|
+
if (!auth.valid) return { ...childCheck, status: "failed", reasonCode: auth.error.code };
|
|
291
|
+
}
|
|
292
|
+
return childCheck;
|
|
261
293
|
});
|
|
262
294
|
if (statuses.some((item) => !item)) return "MISSING";
|
|
263
295
|
if (statuses.some((item) => item.status === "failed")) return "INVALID";
|
|
@@ -265,7 +297,20 @@ function componentStatus(check, requirement, allChecks = []) {
|
|
|
265
297
|
return "COVERED";
|
|
266
298
|
}
|
|
267
299
|
|
|
268
|
-
export function evaluateRequiredEvidence({
|
|
300
|
+
export function evaluateRequiredEvidence({
|
|
301
|
+
requirements = [],
|
|
302
|
+
checks = [],
|
|
303
|
+
target,
|
|
304
|
+
taskId,
|
|
305
|
+
authorities,
|
|
306
|
+
options = {},
|
|
307
|
+
} = {}) {
|
|
308
|
+
const authOptions = {
|
|
309
|
+
...(target ? { target } : {}),
|
|
310
|
+
...(taskId ? { taskId } : {}),
|
|
311
|
+
...(authorities ? { authorities } : {}),
|
|
312
|
+
...options,
|
|
313
|
+
};
|
|
269
314
|
const normalized = normalizeRequirements(requirements);
|
|
270
315
|
const result = {
|
|
271
316
|
ready: true,
|
|
@@ -291,8 +336,14 @@ export function evaluateRequiredEvidence({ requirements = [], checks = [] } = {}
|
|
|
291
336
|
}
|
|
292
337
|
const candidates = checks.filter((check) => matchesRequirement(check, requirement));
|
|
293
338
|
const check = latestAuthoritativeCheck(candidates);
|
|
294
|
-
const
|
|
295
|
-
|
|
339
|
+
const provenance = check ? validateCommandProvenance(check) : { valid: true };
|
|
340
|
+
const auth = provenance.valid
|
|
341
|
+
? (check ? validateVerificationAuthority(check, authOptions) : { valid: true })
|
|
342
|
+
: provenance;
|
|
343
|
+
const compound = componentStatus(check, requirement, checks, authOptions);
|
|
344
|
+
if (!auth.valid) {
|
|
345
|
+
result.invalid.push({ ...requirement, reasonCode: auth.error.code });
|
|
346
|
+
} else if (compound === "INVALID" || check?.status === "failed") {
|
|
296
347
|
result.invalid.push(requirement);
|
|
297
348
|
} else if (compound === "PARTIAL") {
|
|
298
349
|
result.partial.push(requirement);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ARTIFACT_PATHS,
|
|
6
|
+
executionArtifactPath,
|
|
7
|
+
readJsonArtifact,
|
|
8
|
+
writeJsonArtifact,
|
|
9
|
+
} from "./artifacts.js";
|
|
10
|
+
import {
|
|
11
|
+
classifyCommandResolution,
|
|
12
|
+
resolveExecutionResolution,
|
|
13
|
+
validateVerificationAuthority,
|
|
14
|
+
E_COMMAND_RESOLUTION_AMBIGUOUS,
|
|
15
|
+
} from "./verification-capability.js";
|
|
16
|
+
|
|
17
|
+
export { E_COMMAND_RESOLUTION_AMBIGUOUS };
|
|
18
|
+
export const EXECUTION_KIND = "COMMAND_EXECUTION";
|
|
19
|
+
|
|
20
|
+
function executionError(code, message, artifacts = []) {
|
|
21
|
+
const error = new Error(message);
|
|
22
|
+
error.code = code;
|
|
23
|
+
error.artifacts = artifacts;
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeArgv(argv) {
|
|
28
|
+
if (!Array.isArray(argv) || argv.length === 0 || argv.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
29
|
+
throw executionError("E_EXECUTION_INVALID", "Execution argv must contain at least one non-empty string");
|
|
30
|
+
}
|
|
31
|
+
return [...argv];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateAuthorityBeforeLaunch({ target, taskId, argv, resolution, details, authorityContext, runtimeContext }) {
|
|
35
|
+
if (!resolution.mayInstall) return;
|
|
36
|
+
const check = {
|
|
37
|
+
kind: "command",
|
|
38
|
+
source: argv[0],
|
|
39
|
+
details: {
|
|
40
|
+
...(details ?? {}),
|
|
41
|
+
execution: { resolution },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const authority = validateVerificationAuthority(check, {
|
|
45
|
+
target,
|
|
46
|
+
taskId,
|
|
47
|
+
authorityContext,
|
|
48
|
+
runtimeContext,
|
|
49
|
+
});
|
|
50
|
+
if (!authority.valid) {
|
|
51
|
+
throw executionError(authority.error.code ?? "E_INSTALLATION_AUTHORITY_REQUIRED", authority.error.message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function executeProcess(argv, cwd) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
let spawnError = null;
|
|
58
|
+
try {
|
|
59
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
60
|
+
cwd,
|
|
61
|
+
shell: false,
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
63
|
+
});
|
|
64
|
+
child.stdout?.resume();
|
|
65
|
+
child.stderr?.resume();
|
|
66
|
+
child.once("error", (error) => {
|
|
67
|
+
spawnError = error;
|
|
68
|
+
});
|
|
69
|
+
child.once("close", (exitCode) => {
|
|
70
|
+
resolve({ exitCode, spawnError });
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
resolve({ exitCode: null, spawnError: error });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function runCommandExecution({
|
|
79
|
+
target,
|
|
80
|
+
packageRoot,
|
|
81
|
+
taskId,
|
|
82
|
+
checkId,
|
|
83
|
+
requirement,
|
|
84
|
+
verificationCycle = 1,
|
|
85
|
+
argv,
|
|
86
|
+
details,
|
|
87
|
+
authorityContext,
|
|
88
|
+
runtimeContext,
|
|
89
|
+
} = {}) {
|
|
90
|
+
const commandArgv = normalizeArgv(argv);
|
|
91
|
+
const resolution = await resolveExecutionResolution({
|
|
92
|
+
argv: commandArgv,
|
|
93
|
+
cwd: target,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
resolution.resolutionMode === "UNKNOWN"
|
|
98
|
+
&& resolution.mayInstall === true
|
|
99
|
+
&& (
|
|
100
|
+
resolution.reason === "NPM_WORKSPACE_SCRIPT_UNRESOLVED"
|
|
101
|
+
|| resolution.reason === "NPM_SUBCOMMAND_AMBIGUOUS"
|
|
102
|
+
|| resolution.reason === "NPM_COMMAND_UNCLASSIFIED"
|
|
103
|
+
|| resolution.reason === "NPM_OPTION_VALUE_AMBIGUOUS"
|
|
104
|
+
)
|
|
105
|
+
) {
|
|
106
|
+
const error = new Error(
|
|
107
|
+
resolution.reason === "NPM_WORKSPACE_SCRIPT_UNRESOLVED"
|
|
108
|
+
? "npm workspace script execution cannot be proven from the current target. Run ForgeLoop against the selected workspace directory."
|
|
109
|
+
: "Command execution context could not be proven safe before launch."
|
|
110
|
+
);
|
|
111
|
+
error.code = E_COMMAND_RESOLUTION_AMBIGUOUS;
|
|
112
|
+
error.resolution = resolution;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
validateAuthorityBeforeLaunch({
|
|
117
|
+
target,
|
|
118
|
+
taskId,
|
|
119
|
+
argv: commandArgv,
|
|
120
|
+
resolution,
|
|
121
|
+
details,
|
|
122
|
+
authorityContext,
|
|
123
|
+
runtimeContext,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const executionId = `exec-${randomUUID()}`;
|
|
127
|
+
const startedAt = new Date().toISOString();
|
|
128
|
+
const processResult = await executeProcess(commandArgv, target);
|
|
129
|
+
const finishedAt = new Date().toISOString();
|
|
130
|
+
const execution = {
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
protocolVersion: 1,
|
|
133
|
+
executionId,
|
|
134
|
+
taskId,
|
|
135
|
+
checkId,
|
|
136
|
+
requirement,
|
|
137
|
+
verificationCycle,
|
|
138
|
+
kind: EXECUTION_KIND,
|
|
139
|
+
argv: commandArgv,
|
|
140
|
+
cwd: target,
|
|
141
|
+
resolution: {
|
|
142
|
+
resolutionMode: resolution.resolutionMode,
|
|
143
|
+
mayInstall: resolution.mayInstall,
|
|
144
|
+
installer: resolution.installer,
|
|
145
|
+
tool: resolution.tool,
|
|
146
|
+
},
|
|
147
|
+
...(resolution.dispatch ? { dispatch: resolution.dispatch } : {}),
|
|
148
|
+
startedAt,
|
|
149
|
+
finishedAt,
|
|
150
|
+
status: processResult.exitCode === 0 && !processResult.spawnError ? "passed" : "failed",
|
|
151
|
+
exitCode: processResult.exitCode,
|
|
152
|
+
};
|
|
153
|
+
const path = executionArtifactPath(executionId);
|
|
154
|
+
const written = await writeJsonArtifact(target, path, execution, "execution", packageRoot);
|
|
155
|
+
return {
|
|
156
|
+
path: written.path,
|
|
157
|
+
execution: written.value,
|
|
158
|
+
result: processResult.spawnError
|
|
159
|
+
? "process failed to start"
|
|
160
|
+
: `process exited with code ${processResult.exitCode}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function readExecutionArtifact({ target, executionRef, packageRoot } = {}) {
|
|
165
|
+
let relativePath;
|
|
166
|
+
try {
|
|
167
|
+
relativePath = executionArtifactPath(executionRef);
|
|
168
|
+
const artifact = await readJsonArtifact(target, relativePath, "execution", packageRoot);
|
|
169
|
+
return artifact;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error.code === "E_EXECUTION_REF_INVALID") throw error;
|
|
172
|
+
throw executionError("E_EXECUTION_REF_INVALID", "Execution reference does not resolve to a valid ForgeLoop artifact", [relativePath ?? ARTIFACT_PATHS.executionDirectory]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function validateExecutionBinding({ execution, taskId, checkId, requirement, verificationCycle = 1 } = {}) {
|
|
177
|
+
if (!execution || execution.kind !== EXECUTION_KIND
|
|
178
|
+
|| execution.taskId !== taskId
|
|
179
|
+
|| execution.checkId !== checkId
|
|
180
|
+
|| execution.requirement !== requirement
|
|
181
|
+
|| execution.verificationCycle !== undefined && execution.verificationCycle !== verificationCycle) {
|
|
182
|
+
throw executionError("E_EXECUTION_REF_INVALID", "Execution artifact does not match the current check binding", [ARTIFACT_PATHS.executionDirectory]);
|
|
183
|
+
}
|
|
184
|
+
return execution;
|
|
185
|
+
}
|
package/src/core/inspect.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createEvidence } from "./evidence.js";
|
|
|
8
8
|
import { runDoctor } from "../commands/doctor.js";
|
|
9
9
|
import { findProfilePath } from "./profile.js";
|
|
10
10
|
import { FORGELOOP_KIT_DIR } from "./target-layout.js";
|
|
11
|
+
import { trustedAuthorityConfiguration } from "./trusted-authority.js";
|
|
11
12
|
|
|
12
13
|
function profileMetadata(bytes) {
|
|
13
14
|
const text = bytes.toString("utf8");
|
|
@@ -17,7 +18,7 @@ function profileMetadata(bytes) {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export async function inspectTarget({ target, packageRoot, contractFile = null }) {
|
|
21
|
+
export async function inspectTarget({ target, packageRoot, contractFile = null, authorityContext, runtimeContext }) {
|
|
21
22
|
let manifest = null;
|
|
22
23
|
let manifestError = null;
|
|
23
24
|
try {
|
|
@@ -91,6 +92,7 @@ export async function inspectTarget({ target, packageRoot, contractFile = null }
|
|
|
91
92
|
];
|
|
92
93
|
return {
|
|
93
94
|
target: { path: target },
|
|
95
|
+
authority: trustedAuthorityConfiguration({ target, authorityContext, runtimeContext }),
|
|
94
96
|
manifest: {
|
|
95
97
|
present: manifest !== null,
|
|
96
98
|
status: manifestError ? "invalid" : manifest ? "ready" : "missing",
|
package/src/core/next-action.js
CHANGED
|
@@ -236,7 +236,16 @@ async function loadArtifact(loader, fallback) {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
async function requirementsAndCoverage({
|
|
239
|
+
async function requirementsAndCoverage({
|
|
240
|
+
target,
|
|
241
|
+
packageRoot,
|
|
242
|
+
contract,
|
|
243
|
+
route,
|
|
244
|
+
checks,
|
|
245
|
+
additionalEvidence = [],
|
|
246
|
+
authorityContext,
|
|
247
|
+
runtimeContext,
|
|
248
|
+
}) {
|
|
240
249
|
const requirements = await requiredEvidenceForTarget({
|
|
241
250
|
target,
|
|
242
251
|
contract,
|
|
@@ -244,14 +253,21 @@ async function requirementsAndCoverage({ target, packageRoot, contract, route, c
|
|
|
244
253
|
packageRoot,
|
|
245
254
|
additionalEvidence,
|
|
246
255
|
});
|
|
247
|
-
return {
|
|
256
|
+
return {
|
|
257
|
+
requirements,
|
|
258
|
+
coverage: coverageForRequirements(requirements, checks, {
|
|
259
|
+
target,
|
|
260
|
+
taskId: contract?.value?.taskId,
|
|
261
|
+
options: { authorityContext, runtimeContext },
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
248
264
|
}
|
|
249
265
|
|
|
250
266
|
export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
251
267
|
const normalized = typeof targetOrOptions === "string"
|
|
252
268
|
? { target: targetOrOptions, packageRoot: packageRootOption }
|
|
253
269
|
: targetOrOptions;
|
|
254
|
-
const { target, packageRoot } = normalized ?? {};
|
|
270
|
+
const { target, packageRoot, authorityContext, runtimeContext } = normalized ?? {};
|
|
255
271
|
const workState = await loadArtifact(
|
|
256
272
|
() => readWorkState(target, packageRoot),
|
|
257
273
|
ARTIFACT_PATHS.state,
|
|
@@ -544,6 +560,8 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
544
560
|
route,
|
|
545
561
|
checks: state.checks,
|
|
546
562
|
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
563
|
+
authorityContext,
|
|
564
|
+
runtimeContext,
|
|
547
565
|
});
|
|
548
566
|
const authoritative = authoritativeChecksForRequirements({
|
|
549
567
|
requirements: ordinaryLeafRequirements(evidence.requirements),
|
|
@@ -587,7 +605,12 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
587
605
|
);
|
|
588
606
|
}
|
|
589
607
|
try {
|
|
590
|
-
await validateReceipt(receipt.value.value, packageRoot
|
|
608
|
+
await validateReceipt(receipt.value.value, packageRoot, {
|
|
609
|
+
target,
|
|
610
|
+
taskId: contract?.value?.taskId,
|
|
611
|
+
authorityContext,
|
|
612
|
+
runtimeContext,
|
|
613
|
+
});
|
|
591
614
|
} catch (error) {
|
|
592
615
|
return decision(
|
|
593
616
|
context,
|
|
@@ -596,7 +619,13 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
596
619
|
[...requiredArtifacts, ARTIFACT_PATHS.receipt],
|
|
597
620
|
);
|
|
598
621
|
}
|
|
599
|
-
const readiness = evaluateRequiredEvidence({
|
|
622
|
+
const readiness = evaluateRequiredEvidence({
|
|
623
|
+
requirements: evidence.requirements,
|
|
624
|
+
checks: state.checks,
|
|
625
|
+
target,
|
|
626
|
+
taskId: contract?.value?.taskId,
|
|
627
|
+
options: { authorityContext, runtimeContext },
|
|
628
|
+
});
|
|
600
629
|
const receiptRelationships = completionRelationshipErrors({
|
|
601
630
|
contract,
|
|
602
631
|
route,
|
|
@@ -604,8 +633,26 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
604
633
|
receipt: receipt.value.value,
|
|
605
634
|
requiredEvidence: evidence.requirements,
|
|
606
635
|
requireRequiredChecks: false,
|
|
636
|
+
target,
|
|
637
|
+
taskId: contract?.value?.taskId,
|
|
638
|
+
authorityContext,
|
|
639
|
+
runtimeContext,
|
|
607
640
|
});
|
|
608
641
|
if (receiptRelationships.length > 0) {
|
|
642
|
+
if (receiptRelationships.some((err) => (
|
|
643
|
+
err.code === "E_RECEIPT_STATE_MISMATCH"
|
|
644
|
+
|| err.code === "E_RECEIPT_CYCLE_MISMATCH"
|
|
645
|
+
|| err.code === "E_RECEIPT_CONTRACT_MISMATCH"
|
|
646
|
+
|| err.code === "E_ROUTE_GUIDE_MISMATCH"
|
|
647
|
+
|| err.code === "E_EVIDENCE_COVERAGE_INVALID"
|
|
648
|
+
))) {
|
|
649
|
+
return decision(
|
|
650
|
+
context,
|
|
651
|
+
NEXT_ACTIONS.PREPARE_COMPLETION,
|
|
652
|
+
artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [ARTIFACT_PATHS.receipt]),
|
|
653
|
+
[...requiredArtifacts, ARTIFACT_PATHS.receipt],
|
|
654
|
+
);
|
|
655
|
+
}
|
|
609
656
|
return result({
|
|
610
657
|
...context,
|
|
611
658
|
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
@@ -670,8 +717,16 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
670
717
|
route,
|
|
671
718
|
checks: state.checks,
|
|
672
719
|
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
720
|
+
authorityContext,
|
|
721
|
+
runtimeContext,
|
|
722
|
+
});
|
|
723
|
+
const readiness = evaluateRequiredEvidence({
|
|
724
|
+
requirements: evidence.requirements,
|
|
725
|
+
checks: state.checks,
|
|
726
|
+
target,
|
|
727
|
+
taskId: contract?.value?.taskId,
|
|
728
|
+
options: { authorityContext, runtimeContext },
|
|
673
729
|
});
|
|
674
|
-
const readiness = evaluateRequiredEvidence({ requirements: evidence.requirements, checks: state.checks });
|
|
675
730
|
if (!readiness.ready) {
|
|
676
731
|
let recoveryAuthorized = false;
|
|
677
732
|
if (state.lastCompletionAttempt?.status === "REJECTED") {
|
|
@@ -737,7 +792,12 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
737
792
|
);
|
|
738
793
|
}
|
|
739
794
|
try {
|
|
740
|
-
await validateReceipt(receipt.value.value, packageRoot
|
|
795
|
+
await validateReceipt(receipt.value.value, packageRoot, {
|
|
796
|
+
target,
|
|
797
|
+
taskId: contract?.value?.taskId,
|
|
798
|
+
authorityContext,
|
|
799
|
+
runtimeContext,
|
|
800
|
+
});
|
|
741
801
|
} catch (error) {
|
|
742
802
|
return decision(
|
|
743
803
|
context,
|
|
@@ -752,8 +812,26 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
752
812
|
state,
|
|
753
813
|
receipt: receipt.value.value,
|
|
754
814
|
requiredEvidence: evidence.requirements,
|
|
815
|
+
target,
|
|
816
|
+
taskId: contract?.value?.taskId,
|
|
817
|
+
authorityContext,
|
|
818
|
+
runtimeContext,
|
|
755
819
|
});
|
|
756
820
|
if (receiptRelationships.length > 0) {
|
|
821
|
+
if (receiptRelationships.some((err) => (
|
|
822
|
+
err.code === "E_RECEIPT_STATE_MISMATCH"
|
|
823
|
+
|| err.code === "E_RECEIPT_CYCLE_MISMATCH"
|
|
824
|
+
|| err.code === "E_RECEIPT_CONTRACT_MISMATCH"
|
|
825
|
+
|| err.code === "E_ROUTE_GUIDE_MISMATCH"
|
|
826
|
+
|| err.code === "E_EVIDENCE_COVERAGE_INVALID"
|
|
827
|
+
))) {
|
|
828
|
+
return decision(
|
|
829
|
+
context,
|
|
830
|
+
NEXT_ACTIONS.PREPARE_COMPLETION,
|
|
831
|
+
artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [ARTIFACT_PATHS.receipt]),
|
|
832
|
+
[...requiredArtifacts, ARTIFACT_PATHS.receipt],
|
|
833
|
+
);
|
|
834
|
+
}
|
|
757
835
|
return result({
|
|
758
836
|
...context,
|
|
759
837
|
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
@@ -761,7 +839,7 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
761
839
|
requiredArtifacts: [...requiredArtifacts, ARTIFACT_PATHS.receipt],
|
|
762
840
|
});
|
|
763
841
|
}
|
|
764
|
-
const completion = await evaluateCompletion({ target, packageRoot });
|
|
842
|
+
const completion = await evaluateCompletion({ target, packageRoot, authorityContext, runtimeContext });
|
|
765
843
|
if (completion.status !== "VALID") {
|
|
766
844
|
const terminalPendingErrors = completion.errors.filter((err) => (
|
|
767
845
|
err.code === "E_PUBLICATION_REQUIREMENT_PENDING" || err.code === "E_PRODUCTION_REQUIREMENT_PENDING"
|
|
@@ -793,7 +871,7 @@ export async function getNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
793
871
|
return decision(context, NEXT_ACTIONS.RUN_COMPLETE, artifactError("COMPLETION_READY", "Completion artifacts and cross-artifact validation are valid"));
|
|
794
872
|
}
|
|
795
873
|
if (state.phase === "COMPLETE") {
|
|
796
|
-
const completion = await evaluateCompletion({ target, packageRoot });
|
|
874
|
+
const completion = await evaluateCompletion({ target, packageRoot, authorityContext, runtimeContext });
|
|
797
875
|
if (completion.status === "VALID") {
|
|
798
876
|
return decision(context, NEXT_ACTIONS.NONE, artifactError("PHASE_COMPLETE", "Completion is validator-backed and terminal"));
|
|
799
877
|
}
|