@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
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
import { readContract } from "./contract.js";
|
|
9
9
|
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
10
10
|
import { completionEvidenceForGuides } from "./guide-metadata.js";
|
|
11
|
-
import { createCheck } from "./checks.js";
|
|
11
|
+
import { CHECK_PROVENANCE, createCheck } from "./checks.js";
|
|
12
12
|
import { createEvidence } from "./evidence.js";
|
|
13
13
|
import { coverageForRequirements } from "./coverage.js";
|
|
14
|
-
import { assertCompletionRelationships } from "./completion-relationships.js";
|
|
14
|
+
import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
|
|
15
15
|
import { evaluatePreflight } from "./preflight.js";
|
|
16
16
|
import { currentChangedPaths } from "./repository.js";
|
|
17
17
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
@@ -19,6 +19,8 @@ import { createReceipt, validateReceipt } from "./receipt.js";
|
|
|
19
19
|
import { readWorkState, writeWorkState } from "./work-state.js";
|
|
20
20
|
import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
21
21
|
import { normalizeRequirements, classifyRequirement } from "./evidence-readiness.js";
|
|
22
|
+
import { classifyCommandResolution, validateVerificationAuthority } from "./verification-capability.js";
|
|
23
|
+
import { readExecutionArtifact, validateExecutionBinding } from "./execution.js";
|
|
22
24
|
|
|
23
25
|
function artifactError(code, message, artifacts = []) {
|
|
24
26
|
const error = new Error(message);
|
|
@@ -58,6 +60,102 @@ async function readOptionalConfig(target, packageRoot) {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
function formatArgv(argv) {
|
|
64
|
+
return argv.map((argument) => /[\s"']/u.test(argument)
|
|
65
|
+
? JSON.stringify(argument)
|
|
66
|
+
: argument).join(" ");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function commandProvenanceError(message = "Observed command evidence requires a ForgeLoop execution artifact") {
|
|
70
|
+
return artifactError("E_COMMAND_PROVENANCE_UNATTESTED", message, [ARTIFACT_PATHS.receipt]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Revalidate the ForgeLoop-owned execution artifact behind an observed command
|
|
75
|
+
* check. This is intentionally asynchronous so completion and audit can verify
|
|
76
|
+
* the artifact instead of trusting duplicated check metadata.
|
|
77
|
+
*/
|
|
78
|
+
export async function validateCheckExecutionProvenance(check, {
|
|
79
|
+
target,
|
|
80
|
+
packageRoot,
|
|
81
|
+
taskId,
|
|
82
|
+
executionArtifacts,
|
|
83
|
+
allowForeignCwd = false,
|
|
84
|
+
} = {}) {
|
|
85
|
+
if (check?.kind !== "command" || check.evidenceKind !== "OBSERVED") return null;
|
|
86
|
+
if (check.provenance !== "FORGELOOP_EXECUTED" || !check.executionRef) {
|
|
87
|
+
throw commandProvenanceError();
|
|
88
|
+
}
|
|
89
|
+
const artifact = executionArtifacts
|
|
90
|
+
? { value: executionArtifacts[check.executionRef] }
|
|
91
|
+
: await readExecutionArtifact({
|
|
92
|
+
target,
|
|
93
|
+
executionRef: check.executionRef,
|
|
94
|
+
packageRoot,
|
|
95
|
+
});
|
|
96
|
+
if (!artifact.value) {
|
|
97
|
+
throw artifactError(
|
|
98
|
+
"E_EXECUTION_REF_INVALID",
|
|
99
|
+
"Execution reference does not resolve to an execution artifact in this bundle",
|
|
100
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const execution = validateExecutionBinding({
|
|
104
|
+
execution: artifact.value,
|
|
105
|
+
taskId,
|
|
106
|
+
checkId: check.id,
|
|
107
|
+
requirement: check.requirement,
|
|
108
|
+
verificationCycle: check.details?.verificationCycle ?? 1,
|
|
109
|
+
});
|
|
110
|
+
if (!allowForeignCwd && path.resolve(execution.cwd) !== path.resolve(target)) {
|
|
111
|
+
throw artifactError(
|
|
112
|
+
"E_EXECUTION_REF_INVALID",
|
|
113
|
+
"Execution artifact cwd does not match the current target",
|
|
114
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (check.status === "passed" && (execution.status !== "passed" || execution.exitCode !== 0)) {
|
|
118
|
+
throw artifactError(
|
|
119
|
+
"E_EXECUTION_REF_INVALID",
|
|
120
|
+
"A passed command check must reference a successful execution artifact",
|
|
121
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (check.status === "failed" && execution.status !== "failed") {
|
|
125
|
+
throw artifactError(
|
|
126
|
+
"E_EXECUTION_REF_INVALID",
|
|
127
|
+
"A failed command check must reference a failed execution artifact",
|
|
128
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (check.exitCode !== undefined && check.exitCode !== execution.exitCode) {
|
|
132
|
+
throw artifactError(
|
|
133
|
+
"E_EXECUTION_REF_INVALID",
|
|
134
|
+
"Check exitCode does not match its execution artifact",
|
|
135
|
+
[ARTIFACT_PATHS.executionDirectory],
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return execution;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function validateChecksExecutionProvenance(checks, options = {}) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
for (const check of Array.isArray(checks) ? checks : []) {
|
|
144
|
+
try {
|
|
145
|
+
await validateCheckExecutionProvenance(check, options);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
errors.push({
|
|
148
|
+
code: error.code ?? "E_EXECUTION_REF_INVALID",
|
|
149
|
+
message: error.message,
|
|
150
|
+
artifacts: error.artifacts ?? [options.artifactPath ?? ARTIFACT_PATHS.receipt],
|
|
151
|
+
checkId: check?.id,
|
|
152
|
+
requirementId: check?.requirement,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return errors;
|
|
157
|
+
}
|
|
158
|
+
|
|
61
159
|
export async function requiredEvidenceForTarget({ target, contract, route, packageRoot, additionalEvidence = [] }) {
|
|
62
160
|
const config = await readOptionalConfig(target, packageRoot);
|
|
63
161
|
const guideEvidence = await completionEvidenceForGuides(route.value.guides, packageRoot);
|
|
@@ -69,7 +167,7 @@ export async function requiredEvidenceForTarget({ target, contract, route, packa
|
|
|
69
167
|
])].sort();
|
|
70
168
|
}
|
|
71
169
|
|
|
72
|
-
export async function prepareCompletion({ target, packageRoot }) {
|
|
170
|
+
export async function prepareCompletion({ target, packageRoot, authorityContext, runtimeContext }) {
|
|
73
171
|
const contract = await readContract(target, packageRoot);
|
|
74
172
|
const route = await readPersistedRoute(target, packageRoot);
|
|
75
173
|
const state = await readWorkState(target, packageRoot);
|
|
@@ -83,7 +181,12 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
83
181
|
let existing = null;
|
|
84
182
|
try {
|
|
85
183
|
existing = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
86
|
-
await validateReceipt(existing.value, packageRoot
|
|
184
|
+
await validateReceipt(existing.value, packageRoot, {
|
|
185
|
+
target,
|
|
186
|
+
taskId: contract?.value?.taskId,
|
|
187
|
+
authorityContext,
|
|
188
|
+
runtimeContext,
|
|
189
|
+
});
|
|
87
190
|
} catch (error) {
|
|
88
191
|
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
89
192
|
}
|
|
@@ -97,17 +200,19 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
97
200
|
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
98
201
|
});
|
|
99
202
|
const existingValue = existing?.value ?? {};
|
|
100
|
-
|
|
101
|
-
contract,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
receipt
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const changedPaths =
|
|
109
|
-
? [...
|
|
110
|
-
:
|
|
203
|
+
if (existingValue.taskId && existingValue.taskId !== contract.value.taskId) {
|
|
204
|
+
throw artifactError("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [ARTIFACT_PATHS.receipt]);
|
|
205
|
+
}
|
|
206
|
+
if (existing && existingValue.stateFingerprint === undefined) {
|
|
207
|
+
throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [ARTIFACT_PATHS.receipt]);
|
|
208
|
+
}
|
|
209
|
+
assertStateIdentity({ contract, route, state });
|
|
210
|
+
const observedPaths = await currentChangedPaths(target);
|
|
211
|
+
const changedPaths = observedPaths !== null
|
|
212
|
+
? [...observedPaths]
|
|
213
|
+
: existing
|
|
214
|
+
? [...(existingValue.changedPaths ?? [])]
|
|
215
|
+
: [];
|
|
111
216
|
const checks = existing ? [...existingValue.checks] : [...state.checks];
|
|
112
217
|
const evidence = existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
|
|
113
218
|
const receipt = await createReceipt({
|
|
@@ -126,7 +231,11 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
126
231
|
changedPaths,
|
|
127
232
|
checks,
|
|
128
233
|
evidence,
|
|
129
|
-
evidenceCoverage: coverageForRequirements(requiredEvidence, checks
|
|
234
|
+
evidenceCoverage: coverageForRequirements(requiredEvidence, checks, {
|
|
235
|
+
target,
|
|
236
|
+
taskId: contract.value.taskId,
|
|
237
|
+
options: { authorityContext, runtimeContext },
|
|
238
|
+
}),
|
|
130
239
|
review: existingValue.review ?? { status: "not-run", independent: false },
|
|
131
240
|
limitations: [...(existingValue.limitations ?? [])],
|
|
132
241
|
publication: existingValue.publication ?? {
|
|
@@ -135,7 +244,24 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
135
244
|
pullRequest: null,
|
|
136
245
|
deployed: false,
|
|
137
246
|
},
|
|
138
|
-
}, packageRoot
|
|
247
|
+
}, packageRoot, {
|
|
248
|
+
target,
|
|
249
|
+
taskId: contract.value.taskId,
|
|
250
|
+
authorityContext,
|
|
251
|
+
runtimeContext,
|
|
252
|
+
});
|
|
253
|
+
assertCompletionRelationships({
|
|
254
|
+
contract,
|
|
255
|
+
route,
|
|
256
|
+
state,
|
|
257
|
+
receipt,
|
|
258
|
+
requiredEvidence,
|
|
259
|
+
requireRequiredChecks: false,
|
|
260
|
+
target,
|
|
261
|
+
taskId: contract.value.taskId,
|
|
262
|
+
authorityContext,
|
|
263
|
+
runtimeContext,
|
|
264
|
+
});
|
|
139
265
|
const written = await writeJsonArtifact(
|
|
140
266
|
target,
|
|
141
267
|
ARTIFACT_PATHS.receipt,
|
|
@@ -167,38 +293,20 @@ function appendUniqueEvidence(evidence, nextEvidence) {
|
|
|
167
293
|
return exists ? [...evidence] : [...evidence, nextEvidence];
|
|
168
294
|
}
|
|
169
295
|
|
|
170
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Read-only lifecycle checks shared by run-check and record-check. Keeping
|
|
298
|
+
* these checks before process launch prevents a command from running when the
|
|
299
|
+
* target is not ready to receive verification evidence.
|
|
300
|
+
*/
|
|
301
|
+
export async function assertRecordCheckPrerequisites({
|
|
171
302
|
target,
|
|
172
303
|
packageRoot,
|
|
173
|
-
id,
|
|
174
|
-
kind = "command",
|
|
175
304
|
requirement,
|
|
176
305
|
status,
|
|
177
306
|
evidenceKind,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
details,
|
|
182
|
-
}) {
|
|
183
|
-
requiredString(id, "check id");
|
|
184
|
-
requiredString(kind, "check kind");
|
|
185
|
-
requiredString(requirement, "check requirement");
|
|
186
|
-
requiredString(status, "check status");
|
|
187
|
-
requiredString(evidenceKind, "evidence kind");
|
|
188
|
-
if (command !== undefined && typeof command !== "string") {
|
|
189
|
-
throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
|
|
190
|
-
}
|
|
191
|
-
if (result !== undefined && typeof result !== "string") {
|
|
192
|
-
throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
|
|
193
|
-
}
|
|
194
|
-
if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
|
|
195
|
-
throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
|
|
196
|
-
}
|
|
197
|
-
if ((typeof command !== "string" || command.trim() === "")
|
|
198
|
-
&& (typeof result !== "string" || result.trim() === "")) {
|
|
199
|
-
throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
|
|
200
|
-
}
|
|
201
|
-
|
|
307
|
+
authorityContext,
|
|
308
|
+
runtimeContext,
|
|
309
|
+
} = {}) {
|
|
202
310
|
const state = await readWorkState(target, packageRoot);
|
|
203
311
|
if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a check", [ARTIFACT_PATHS.state]);
|
|
204
312
|
if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
|
|
@@ -235,9 +343,125 @@ export async function recordCheck({
|
|
|
235
343
|
}
|
|
236
344
|
|
|
237
345
|
const existingReceipt = await readCurrentReceipt(target, packageRoot);
|
|
238
|
-
await validateReceipt(existingReceipt.value, packageRoot
|
|
239
|
-
|
|
240
|
-
|
|
346
|
+
await validateReceipt(existingReceipt.value, packageRoot, {
|
|
347
|
+
target,
|
|
348
|
+
taskId: contract.value.taskId,
|
|
349
|
+
authorityContext,
|
|
350
|
+
runtimeContext,
|
|
351
|
+
});
|
|
352
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
353
|
+
if (!ledger.valid) {
|
|
354
|
+
const first = ledger.errors[0];
|
|
355
|
+
throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
|
|
356
|
+
}
|
|
357
|
+
if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
|
|
358
|
+
throw artifactError(
|
|
359
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
360
|
+
"record-check requires VERIFICATION_STARTED in the current task ledger",
|
|
361
|
+
[ARTIFACT_PATHS.events],
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
state,
|
|
366
|
+
contract,
|
|
367
|
+
route,
|
|
368
|
+
preflight,
|
|
369
|
+
requiredEvidence,
|
|
370
|
+
existingReceipt,
|
|
371
|
+
ledger,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function recordCheck({
|
|
376
|
+
target,
|
|
377
|
+
packageRoot,
|
|
378
|
+
id,
|
|
379
|
+
kind = "command",
|
|
380
|
+
requirement,
|
|
381
|
+
status,
|
|
382
|
+
evidenceKind,
|
|
383
|
+
command,
|
|
384
|
+
result,
|
|
385
|
+
exitCode,
|
|
386
|
+
details,
|
|
387
|
+
executionRef,
|
|
388
|
+
provenance,
|
|
389
|
+
authorityContext,
|
|
390
|
+
runtimeContext,
|
|
391
|
+
}) {
|
|
392
|
+
requiredString(id, "check id");
|
|
393
|
+
requiredString(kind, "check kind");
|
|
394
|
+
requiredString(requirement, "check requirement");
|
|
395
|
+
requiredString(status, "check status");
|
|
396
|
+
requiredString(evidenceKind, "evidence kind");
|
|
397
|
+
if (command !== undefined && typeof command !== "string") {
|
|
398
|
+
throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
|
|
399
|
+
}
|
|
400
|
+
if (result !== undefined && typeof result !== "string") {
|
|
401
|
+
throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
|
|
402
|
+
}
|
|
403
|
+
if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
|
|
404
|
+
throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
|
|
405
|
+
}
|
|
406
|
+
if (executionRef !== undefined && (typeof executionRef !== "string" || executionRef.trim() === "")) {
|
|
407
|
+
throw artifactError("E_EXECUTION_REF_INVALID", "executionRef must be a non-empty string when supplied");
|
|
408
|
+
}
|
|
409
|
+
if (provenance !== undefined && !CHECK_PROVENANCE.includes(provenance)) {
|
|
410
|
+
throw artifactError("E_CHECK_INVALID", `provenance must be one of ${CHECK_PROVENANCE.join(", ")}`);
|
|
411
|
+
}
|
|
412
|
+
if ((typeof command !== "string" || command.trim() === "")
|
|
413
|
+
&& (typeof result !== "string" || result.trim() === "")) {
|
|
414
|
+
throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const context = await assertRecordCheckPrerequisites({
|
|
418
|
+
target,
|
|
419
|
+
packageRoot,
|
|
420
|
+
requirement,
|
|
421
|
+
status,
|
|
422
|
+
evidenceKind,
|
|
423
|
+
authorityContext,
|
|
424
|
+
runtimeContext,
|
|
425
|
+
});
|
|
426
|
+
const {
|
|
427
|
+
state,
|
|
428
|
+
contract,
|
|
429
|
+
route,
|
|
430
|
+
requiredEvidence,
|
|
431
|
+
existingReceipt,
|
|
432
|
+
} = context;
|
|
433
|
+
|
|
434
|
+
const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
|
|
435
|
+
const observedCommand = kind === "command" && evidenceKind === "OBSERVED";
|
|
436
|
+
if (observedCommand && (!executionRef || provenance !== "FORGELOOP_EXECUTED")) {
|
|
437
|
+
throw commandProvenanceError();
|
|
438
|
+
}
|
|
439
|
+
const execution = executionRef
|
|
440
|
+
? await validateCheckExecutionProvenance({
|
|
441
|
+
kind,
|
|
442
|
+
evidenceKind,
|
|
443
|
+
provenance,
|
|
444
|
+
executionRef,
|
|
445
|
+
status,
|
|
446
|
+
exitCode,
|
|
447
|
+
id,
|
|
448
|
+
requirement,
|
|
449
|
+
details: { ...(details ?? {}), verificationCycle: state.verificationCycle ?? 1 },
|
|
450
|
+
}, {
|
|
451
|
+
target,
|
|
452
|
+
packageRoot,
|
|
453
|
+
taskId: contract.value.taskId,
|
|
454
|
+
})
|
|
455
|
+
: null;
|
|
456
|
+
const effectiveCommand = execution ? formatArgv(execution.argv) : commandSpec;
|
|
457
|
+
const source = effectiveCommand || `check:${id}`;
|
|
458
|
+
const recordedResult = result?.trim() || `recorded command: ${effectiveCommand || source}`;
|
|
459
|
+
const classification = execution?.resolution ?? (effectiveCommand !== undefined ? classifyCommandResolution(effectiveCommand) : null);
|
|
460
|
+
const installationAuthorized = Boolean(
|
|
461
|
+
details?.installationAuthorized
|
|
462
|
+
|| details?.authority?.softwareInstallation === "AUTHORIZED"
|
|
463
|
+
|| details?.execution?.installationAuthorized
|
|
464
|
+
);
|
|
241
465
|
const check = createCheck({
|
|
242
466
|
id,
|
|
243
467
|
kind,
|
|
@@ -245,14 +469,41 @@ export async function recordCheck({
|
|
|
245
469
|
status,
|
|
246
470
|
evidenceKind,
|
|
247
471
|
source,
|
|
472
|
+
...(executionRef === undefined ? {} : { executionRef }),
|
|
473
|
+
...(provenance === undefined ? {} : { provenance }),
|
|
248
474
|
timestamp: new Date().toISOString(),
|
|
249
|
-
...(exitCode
|
|
475
|
+
...(execution?.exitCode !== null && execution?.exitCode !== undefined
|
|
476
|
+
? { exitCode: execution.exitCode }
|
|
477
|
+
: exitCode === undefined ? {} : { exitCode }),
|
|
250
478
|
details: {
|
|
251
|
-
...(
|
|
479
|
+
...(effectiveCommand === undefined ? {} : { command: effectiveCommand }),
|
|
252
480
|
...(result === undefined ? {} : { result }),
|
|
253
481
|
...(details === undefined ? {} : details),
|
|
254
482
|
verificationCycle: state.verificationCycle ?? 1,
|
|
483
|
+
...(classification ? {
|
|
484
|
+
execution: {
|
|
485
|
+
...(details?.execution ?? {}),
|
|
486
|
+
...(execution ? {
|
|
487
|
+
executionRef: execution.executionId,
|
|
488
|
+
argv: [...execution.argv],
|
|
489
|
+
cwd: execution.cwd,
|
|
490
|
+
resolution: execution.resolution,
|
|
491
|
+
status: execution.status,
|
|
492
|
+
exitCode: execution.exitCode,
|
|
493
|
+
} : {}),
|
|
494
|
+
resolutionMode: classification.resolutionMode,
|
|
495
|
+
mayInstall: classification.mayInstall,
|
|
496
|
+
installationAuthorized,
|
|
497
|
+
},
|
|
498
|
+
} : {}),
|
|
255
499
|
},
|
|
500
|
+
}, {
|
|
501
|
+
target,
|
|
502
|
+
taskId: contract.value.taskId,
|
|
503
|
+
packageRoot,
|
|
504
|
+
authorityContext,
|
|
505
|
+
runtimeContext,
|
|
506
|
+
requireCommandProvenance: observedCommand,
|
|
256
507
|
});
|
|
257
508
|
const evidence = createEvidence({
|
|
258
509
|
kind: evidenceKind,
|
|
@@ -265,6 +516,19 @@ export async function recordCheck({
|
|
|
265
516
|
},
|
|
266
517
|
});
|
|
267
518
|
|
|
519
|
+
if (status === "passed") {
|
|
520
|
+
const auth = validateVerificationAuthority(check, {
|
|
521
|
+
target,
|
|
522
|
+
taskId: contract.value.taskId,
|
|
523
|
+
packageRoot,
|
|
524
|
+
authorityContext,
|
|
525
|
+
runtimeContext,
|
|
526
|
+
});
|
|
527
|
+
if (!auth.valid) {
|
|
528
|
+
throw artifactError(auth.error.code, auth.error.message, [ARTIFACT_PATHS.receipt]);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
268
532
|
const checks = mergeByCheckId(existingReceipt.value.checks ?? [], check);
|
|
269
533
|
const evidenceList = appendUniqueEvidence(existingReceipt.value.evidence ?? [], evidence);
|
|
270
534
|
assertCompletionRelationships({
|
|
@@ -274,20 +538,16 @@ export async function recordCheck({
|
|
|
274
538
|
receipt: existingReceipt.value,
|
|
275
539
|
requiredEvidence,
|
|
276
540
|
requireRequiredChecks: false,
|
|
541
|
+
target,
|
|
542
|
+
taskId: contract.value.taskId,
|
|
543
|
+
authorityContext,
|
|
544
|
+
runtimeContext,
|
|
545
|
+
});
|
|
546
|
+
const coverage = coverageForRequirements(requiredEvidence, checks, {
|
|
547
|
+
target,
|
|
548
|
+
taskId: contract.value.taskId,
|
|
549
|
+
options: { authorityContext, runtimeContext },
|
|
277
550
|
});
|
|
278
|
-
const ledger = await validateEventLedger(target, packageRoot);
|
|
279
|
-
if (!ledger.valid) {
|
|
280
|
-
const first = ledger.errors[0];
|
|
281
|
-
throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
|
|
282
|
-
}
|
|
283
|
-
if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
|
|
284
|
-
throw artifactError(
|
|
285
|
-
"E_PHASE_CHRONOLOGY_INVALID",
|
|
286
|
-
"record-check requires VERIFICATION_STARTED in the current task ledger",
|
|
287
|
-
[ARTIFACT_PATHS.events],
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
const coverage = coverageForRequirements(requiredEvidence, checks);
|
|
291
551
|
const nextState = {
|
|
292
552
|
...state,
|
|
293
553
|
checks,
|
|
@@ -302,7 +562,12 @@ export async function recordCheck({
|
|
|
302
562
|
evidenceCoverage: coverage,
|
|
303
563
|
stateFingerprint: canonicalFingerprint(nextState),
|
|
304
564
|
verificationCycle: state.verificationCycle ?? 1,
|
|
305
|
-
}, packageRoot
|
|
565
|
+
}, packageRoot, {
|
|
566
|
+
target,
|
|
567
|
+
taskId: contract.value.taskId,
|
|
568
|
+
authorityContext,
|
|
569
|
+
runtimeContext,
|
|
570
|
+
});
|
|
306
571
|
|
|
307
572
|
assertCompletionRelationships({
|
|
308
573
|
contract,
|
|
@@ -311,6 +576,10 @@ export async function recordCheck({
|
|
|
311
576
|
receipt: nextReceipt,
|
|
312
577
|
requiredEvidence,
|
|
313
578
|
requireRequiredChecks: false,
|
|
579
|
+
target,
|
|
580
|
+
taskId: contract.value.taskId,
|
|
581
|
+
authorityContext,
|
|
582
|
+
runtimeContext,
|
|
314
583
|
});
|
|
315
584
|
|
|
316
585
|
await writeWorkState(target, nextState, { packageRoot });
|
|
@@ -351,6 +620,8 @@ export async function recordTerminalResult({
|
|
|
351
620
|
source,
|
|
352
621
|
result,
|
|
353
622
|
details = {},
|
|
623
|
+
authorityContext,
|
|
624
|
+
runtimeContext,
|
|
354
625
|
} = {}) {
|
|
355
626
|
if (!target || !requirement || !type || !status || !source || !result) {
|
|
356
627
|
throw artifactError("E_CHECK_INVALID", "record-terminal-result requires target, requirement, type, status, source, and result", [ARTIFACT_PATHS.state]);
|
|
@@ -413,7 +684,12 @@ export async function recordTerminalResult({
|
|
|
413
684
|
}
|
|
414
685
|
|
|
415
686
|
const existingReceipt = await readCurrentReceipt(target, packageRoot);
|
|
416
|
-
await validateReceipt(existingReceipt.value, packageRoot
|
|
687
|
+
await validateReceipt(existingReceipt.value, packageRoot, {
|
|
688
|
+
target,
|
|
689
|
+
taskId: contract?.value?.taskId,
|
|
690
|
+
authorityContext,
|
|
691
|
+
runtimeContext,
|
|
692
|
+
});
|
|
417
693
|
|
|
418
694
|
if (type === "PUBLICATION") {
|
|
419
695
|
const rank = {
|
|
@@ -540,7 +816,12 @@ export async function recordTerminalResult({
|
|
|
540
816
|
const nextReceipt = await createReceipt({
|
|
541
817
|
...receiptUpdates,
|
|
542
818
|
stateFingerprint: canonicalFingerprint(nextState),
|
|
543
|
-
}, packageRoot
|
|
819
|
+
}, packageRoot, {
|
|
820
|
+
target,
|
|
821
|
+
taskId: contract.value.taskId,
|
|
822
|
+
authorityContext,
|
|
823
|
+
runtimeContext,
|
|
824
|
+
});
|
|
544
825
|
|
|
545
826
|
await writeWorkState(target, nextState, { packageRoot });
|
|
546
827
|
await writeJsonArtifact(
|
|
@@ -8,6 +8,10 @@ export const RECOVERABLE_COMPLETION_EVIDENCE_CODES = Object.freeze([
|
|
|
8
8
|
"E_VERIFICATION_CHECK_REQUIRED",
|
|
9
9
|
"E_CHECK_REQUIRED",
|
|
10
10
|
"E_CHECK_INVALID",
|
|
11
|
+
"E_INSTALLATION_AUTHORITY_REQUIRED",
|
|
12
|
+
"E_AUTHORITY_INVALID",
|
|
13
|
+
"E_AUTHORITY_SCOPE_MISMATCH",
|
|
14
|
+
"E_AUTHORITY_UNTRUSTED_SOURCE",
|
|
11
15
|
]);
|
|
12
16
|
|
|
13
17
|
export function isRecoverableCompletionEvidenceCode(code) {
|
|
@@ -3,6 +3,7 @@ import { assertCheckList } from "./checks.js";
|
|
|
3
3
|
import { assertCoverageList, coverageForRequirements } from "./coverage.js";
|
|
4
4
|
import { assertEvidenceList } from "./evidence.js";
|
|
5
5
|
import { evaluateRequiredEvidence } from "./evidence-readiness.js";
|
|
6
|
+
import { validateVerificationAuthority } from "./verification-capability.js";
|
|
6
7
|
|
|
7
8
|
function issue(code, message, artifacts = [], details = {}) {
|
|
8
9
|
return { code, message, artifacts, ...details };
|
|
@@ -65,10 +66,23 @@ export function completionRelationshipErrors({
|
|
|
65
66
|
requiredEvidence = [],
|
|
66
67
|
requireReceiptStateFingerprint = true,
|
|
67
68
|
requireRequiredChecks = true,
|
|
69
|
+
target,
|
|
70
|
+
taskId,
|
|
71
|
+
authorities,
|
|
72
|
+
authorityContext,
|
|
73
|
+
runtimeContext,
|
|
68
74
|
} = {}) {
|
|
69
75
|
const errors = stateIdentityErrors({ contract, route, state });
|
|
70
76
|
const contractValue = contract?.value ?? contract;
|
|
71
77
|
const contractFingerprint = contract?.fingerprint;
|
|
78
|
+
const effectiveTaskId = taskId ?? contractValue?.taskId ?? state?.taskId ?? receipt?.taskId;
|
|
79
|
+
const authOptions = {
|
|
80
|
+
...(target ? { target } : {}),
|
|
81
|
+
...(effectiveTaskId ? { taskId: effectiveTaskId } : {}),
|
|
82
|
+
...(authorities ? { authorities } : {}),
|
|
83
|
+
...(authorityContext ? { authorityContext } : {}),
|
|
84
|
+
...(runtimeContext ? { runtimeContext } : {}),
|
|
85
|
+
};
|
|
72
86
|
if (contractValue && receipt && contractValue.taskId !== receipt.taskId) {
|
|
73
87
|
errors.push(issue("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt]));
|
|
74
88
|
}
|
|
@@ -80,14 +94,30 @@ export function completionRelationshipErrors({
|
|
|
80
94
|
}
|
|
81
95
|
|
|
82
96
|
if (state) {
|
|
83
|
-
addAssertion(errors, () => assertCheckList(state.checks, "work-state.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.state]);
|
|
97
|
+
addAssertion(errors, () => assertCheckList(state.checks, "work-state.checks", authOptions), "E_CHECK_INVALID", [ARTIFACT_PATHS.state]);
|
|
84
98
|
addAssertion(errors, () => assertEvidenceList(state.verificationEvidence, "work-state.verificationEvidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.state]);
|
|
99
|
+
for (const check of state.checks ?? []) {
|
|
100
|
+
if (check.status === "passed") {
|
|
101
|
+
const auth = validateVerificationAuthority(check, authOptions);
|
|
102
|
+
if (!auth.valid) {
|
|
103
|
+
errors.push(issue(auth.error.code, auth.error.message, [ARTIFACT_PATHS.state]));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
85
107
|
}
|
|
86
108
|
if (receipt) {
|
|
87
|
-
addAssertion(errors, () => assertCheckList(receipt.checks, "receipt.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.receipt]);
|
|
109
|
+
addAssertion(errors, () => assertCheckList(receipt.checks, "receipt.checks", authOptions), "E_CHECK_INVALID", [ARTIFACT_PATHS.receipt]);
|
|
88
110
|
addAssertion(errors, () => assertEvidenceList(receipt.evidence ?? [], "receipt.evidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.receipt]);
|
|
111
|
+
for (const check of receipt.checks ?? []) {
|
|
112
|
+
if (check.status === "passed") {
|
|
113
|
+
const auth = validateVerificationAuthority(check, authOptions);
|
|
114
|
+
if (!auth.valid) {
|
|
115
|
+
errors.push(issue(auth.error.code, auth.error.message, [ARTIFACT_PATHS.receipt]));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
89
119
|
if (requireRequiredChecks) {
|
|
90
|
-
const readiness = evaluateRequiredEvidence({ requirements: requiredEvidence, checks: receipt.checks });
|
|
120
|
+
const readiness = evaluateRequiredEvidence({ requirements: requiredEvidence, checks: receipt.checks, options: authOptions });
|
|
91
121
|
for (const requirement of readiness.missing) {
|
|
92
122
|
errors.push(issue("E_EVIDENCE_REQUIRED", `Required check is missing: ${requirement.text}`, [ARTIFACT_PATHS.receipt], { requirementId: requirement.id }));
|
|
93
123
|
}
|
|
@@ -100,7 +130,12 @@ export function completionRelationshipErrors({
|
|
|
100
130
|
}
|
|
101
131
|
}
|
|
102
132
|
|
|
103
|
-
const expectedCoverage = state ? coverageForRequirements(requiredEvidence, state.checks
|
|
133
|
+
const expectedCoverage = state ? coverageForRequirements(requiredEvidence, state.checks, {
|
|
134
|
+
target,
|
|
135
|
+
taskId: effectiveTaskId,
|
|
136
|
+
authorities,
|
|
137
|
+
options: authOptions,
|
|
138
|
+
}) : [];
|
|
104
139
|
if (state?.evidenceCoverage !== undefined) {
|
|
105
140
|
addAssertion(errors, () => assertCoverageList(state.evidenceCoverage, "work-state.evidenceCoverage"), "E_EVIDENCE_COVERAGE_INVALID", [ARTIFACT_PATHS.state]);
|
|
106
141
|
if (!sameValue(state.evidenceCoverage, expectedCoverage)) {
|