@cassiomc1/forgeloop 0.1.12 → 0.1.14
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/.cursor/rules/project-loop.mdc +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/LOOP_ENGINEERING.md +73 -0
- package/LOOP_SYSTEM_DESIGN.md +9 -7
- package/PROTOCOL_INTEGRATION.md +99 -0
- package/README.md +41 -41
- package/THREAT_MODEL.md +6 -0
- package/package.json +1 -1
- package/schemas/authority.schema.json +34 -0
- package/src/commands/inspect.js +2 -0
- package/src/commands/prepare-completion.js +2 -2
- package/src/commands/validate-protocol.js +11 -8
- package/src/core/audit.js +2 -2
- package/src/core/checks.js +12 -5
- package/src/core/completion-artifacts.js +122 -23
- package/src/core/completion-recovery.js +4 -0
- package/src/core/completion-relationships.js +39 -4
- package/src/core/completion.js +29 -7
- package/src/core/conformance.js +47 -5
- package/src/core/coverage.js +15 -2
- package/src/core/evidence-readiness.js +32 -6
- package/src/core/inspect.js +3 -1
- package/src/core/native-adapters.js +4 -0
- 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/trusted-authority.js +296 -0
- package/src/core/verification-capability.js +546 -0
|
@@ -11,7 +11,7 @@ import { completionEvidenceForGuides } from "./guide-metadata.js";
|
|
|
11
11
|
import { 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,7 @@ 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";
|
|
22
23
|
|
|
23
24
|
function artifactError(code, message, artifacts = []) {
|
|
24
25
|
const error = new Error(message);
|
|
@@ -69,7 +70,7 @@ export async function requiredEvidenceForTarget({ target, contract, route, packa
|
|
|
69
70
|
])].sort();
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
export async function prepareCompletion({ target, packageRoot }) {
|
|
73
|
+
export async function prepareCompletion({ target, packageRoot, authorityContext, runtimeContext }) {
|
|
73
74
|
const contract = await readContract(target, packageRoot);
|
|
74
75
|
const route = await readPersistedRoute(target, packageRoot);
|
|
75
76
|
const state = await readWorkState(target, packageRoot);
|
|
@@ -83,7 +84,12 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
83
84
|
let existing = null;
|
|
84
85
|
try {
|
|
85
86
|
existing = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
|
|
86
|
-
await validateReceipt(existing.value, packageRoot
|
|
87
|
+
await validateReceipt(existing.value, packageRoot, {
|
|
88
|
+
target,
|
|
89
|
+
taskId: contract?.value?.taskId,
|
|
90
|
+
authorityContext,
|
|
91
|
+
runtimeContext,
|
|
92
|
+
});
|
|
87
93
|
} catch (error) {
|
|
88
94
|
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
89
95
|
}
|
|
@@ -97,17 +103,19 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
97
103
|
additionalEvidence: preflight.policy?.requiredEvidence ?? [],
|
|
98
104
|
});
|
|
99
105
|
const existingValue = existing?.value ?? {};
|
|
100
|
-
|
|
101
|
-
contract,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
receipt
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const changedPaths =
|
|
109
|
-
? [...
|
|
110
|
-
:
|
|
106
|
+
if (existingValue.taskId && existingValue.taskId !== contract.value.taskId) {
|
|
107
|
+
throw artifactError("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [ARTIFACT_PATHS.receipt]);
|
|
108
|
+
}
|
|
109
|
+
if (existing && existingValue.stateFingerprint === undefined) {
|
|
110
|
+
throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [ARTIFACT_PATHS.receipt]);
|
|
111
|
+
}
|
|
112
|
+
assertStateIdentity({ contract, route, state });
|
|
113
|
+
const observedPaths = await currentChangedPaths(target);
|
|
114
|
+
const changedPaths = observedPaths !== null
|
|
115
|
+
? [...observedPaths]
|
|
116
|
+
: existing
|
|
117
|
+
? [...(existingValue.changedPaths ?? [])]
|
|
118
|
+
: [];
|
|
111
119
|
const checks = existing ? [...existingValue.checks] : [...state.checks];
|
|
112
120
|
const evidence = existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
|
|
113
121
|
const receipt = await createReceipt({
|
|
@@ -126,7 +134,11 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
126
134
|
changedPaths,
|
|
127
135
|
checks,
|
|
128
136
|
evidence,
|
|
129
|
-
evidenceCoverage: coverageForRequirements(requiredEvidence, checks
|
|
137
|
+
evidenceCoverage: coverageForRequirements(requiredEvidence, checks, {
|
|
138
|
+
target,
|
|
139
|
+
taskId: contract.value.taskId,
|
|
140
|
+
options: { authorityContext, runtimeContext },
|
|
141
|
+
}),
|
|
130
142
|
review: existingValue.review ?? { status: "not-run", independent: false },
|
|
131
143
|
limitations: [...(existingValue.limitations ?? [])],
|
|
132
144
|
publication: existingValue.publication ?? {
|
|
@@ -135,7 +147,24 @@ export async function prepareCompletion({ target, packageRoot }) {
|
|
|
135
147
|
pullRequest: null,
|
|
136
148
|
deployed: false,
|
|
137
149
|
},
|
|
138
|
-
}, packageRoot
|
|
150
|
+
}, packageRoot, {
|
|
151
|
+
target,
|
|
152
|
+
taskId: contract.value.taskId,
|
|
153
|
+
authorityContext,
|
|
154
|
+
runtimeContext,
|
|
155
|
+
});
|
|
156
|
+
assertCompletionRelationships({
|
|
157
|
+
contract,
|
|
158
|
+
route,
|
|
159
|
+
state,
|
|
160
|
+
receipt,
|
|
161
|
+
requiredEvidence,
|
|
162
|
+
requireRequiredChecks: false,
|
|
163
|
+
target,
|
|
164
|
+
taskId: contract.value.taskId,
|
|
165
|
+
authorityContext,
|
|
166
|
+
runtimeContext,
|
|
167
|
+
});
|
|
139
168
|
const written = await writeJsonArtifact(
|
|
140
169
|
target,
|
|
141
170
|
ARTIFACT_PATHS.receipt,
|
|
@@ -179,6 +208,8 @@ export async function recordCheck({
|
|
|
179
208
|
result,
|
|
180
209
|
exitCode,
|
|
181
210
|
details,
|
|
211
|
+
authorityContext,
|
|
212
|
+
runtimeContext,
|
|
182
213
|
}) {
|
|
183
214
|
requiredString(id, "check id");
|
|
184
215
|
requiredString(kind, "check kind");
|
|
@@ -199,6 +230,8 @@ export async function recordCheck({
|
|
|
199
230
|
throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
|
|
200
231
|
}
|
|
201
232
|
|
|
233
|
+
const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
|
|
234
|
+
|
|
202
235
|
const state = await readWorkState(target, packageRoot);
|
|
203
236
|
if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a check", [ARTIFACT_PATHS.state]);
|
|
204
237
|
if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
|
|
@@ -235,9 +268,20 @@ export async function recordCheck({
|
|
|
235
268
|
}
|
|
236
269
|
|
|
237
270
|
const existingReceipt = await readCurrentReceipt(target, packageRoot);
|
|
238
|
-
await validateReceipt(existingReceipt.value, packageRoot
|
|
239
|
-
|
|
240
|
-
|
|
271
|
+
await validateReceipt(existingReceipt.value, packageRoot, {
|
|
272
|
+
target,
|
|
273
|
+
taskId: contract.value.taskId,
|
|
274
|
+
authorityContext,
|
|
275
|
+
runtimeContext,
|
|
276
|
+
});
|
|
277
|
+
const source = commandSpec || `check:${id}`;
|
|
278
|
+
const recordedResult = result?.trim() || `recorded command: ${commandSpec || source}`;
|
|
279
|
+
const classification = commandSpec !== undefined ? classifyCommandResolution(commandSpec) : null;
|
|
280
|
+
const installationAuthorized = Boolean(
|
|
281
|
+
details?.installationAuthorized
|
|
282
|
+
|| details?.authority?.softwareInstallation === "AUTHORIZED"
|
|
283
|
+
|| details?.execution?.installationAuthorized
|
|
284
|
+
);
|
|
241
285
|
const check = createCheck({
|
|
242
286
|
id,
|
|
243
287
|
kind,
|
|
@@ -252,7 +296,20 @@ export async function recordCheck({
|
|
|
252
296
|
...(result === undefined ? {} : { result }),
|
|
253
297
|
...(details === undefined ? {} : details),
|
|
254
298
|
verificationCycle: state.verificationCycle ?? 1,
|
|
299
|
+
...(classification ? {
|
|
300
|
+
execution: {
|
|
301
|
+
resolutionMode: classification.resolutionMode,
|
|
302
|
+
mayInstall: classification.mayInstall,
|
|
303
|
+
installationAuthorized,
|
|
304
|
+
},
|
|
305
|
+
} : {}),
|
|
255
306
|
},
|
|
307
|
+
}, {
|
|
308
|
+
target,
|
|
309
|
+
taskId: contract.value.taskId,
|
|
310
|
+
packageRoot,
|
|
311
|
+
authorityContext,
|
|
312
|
+
runtimeContext,
|
|
256
313
|
});
|
|
257
314
|
const evidence = createEvidence({
|
|
258
315
|
kind: evidenceKind,
|
|
@@ -265,6 +322,19 @@ export async function recordCheck({
|
|
|
265
322
|
},
|
|
266
323
|
});
|
|
267
324
|
|
|
325
|
+
if (status === "passed") {
|
|
326
|
+
const auth = validateVerificationAuthority(check, {
|
|
327
|
+
target,
|
|
328
|
+
taskId: contract.value.taskId,
|
|
329
|
+
packageRoot,
|
|
330
|
+
authorityContext,
|
|
331
|
+
runtimeContext,
|
|
332
|
+
});
|
|
333
|
+
if (!auth.valid) {
|
|
334
|
+
throw artifactError(auth.error.code, auth.error.message, [ARTIFACT_PATHS.receipt]);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
268
338
|
const checks = mergeByCheckId(existingReceipt.value.checks ?? [], check);
|
|
269
339
|
const evidenceList = appendUniqueEvidence(existingReceipt.value.evidence ?? [], evidence);
|
|
270
340
|
assertCompletionRelationships({
|
|
@@ -274,6 +344,10 @@ export async function recordCheck({
|
|
|
274
344
|
receipt: existingReceipt.value,
|
|
275
345
|
requiredEvidence,
|
|
276
346
|
requireRequiredChecks: false,
|
|
347
|
+
target,
|
|
348
|
+
taskId: contract.value.taskId,
|
|
349
|
+
authorityContext,
|
|
350
|
+
runtimeContext,
|
|
277
351
|
});
|
|
278
352
|
const ledger = await validateEventLedger(target, packageRoot);
|
|
279
353
|
if (!ledger.valid) {
|
|
@@ -287,7 +361,11 @@ export async function recordCheck({
|
|
|
287
361
|
[ARTIFACT_PATHS.events],
|
|
288
362
|
);
|
|
289
363
|
}
|
|
290
|
-
const coverage = coverageForRequirements(requiredEvidence, checks
|
|
364
|
+
const coverage = coverageForRequirements(requiredEvidence, checks, {
|
|
365
|
+
target,
|
|
366
|
+
taskId: contract.value.taskId,
|
|
367
|
+
options: { authorityContext, runtimeContext },
|
|
368
|
+
});
|
|
291
369
|
const nextState = {
|
|
292
370
|
...state,
|
|
293
371
|
checks,
|
|
@@ -302,7 +380,12 @@ export async function recordCheck({
|
|
|
302
380
|
evidenceCoverage: coverage,
|
|
303
381
|
stateFingerprint: canonicalFingerprint(nextState),
|
|
304
382
|
verificationCycle: state.verificationCycle ?? 1,
|
|
305
|
-
}, packageRoot
|
|
383
|
+
}, packageRoot, {
|
|
384
|
+
target,
|
|
385
|
+
taskId: contract.value.taskId,
|
|
386
|
+
authorityContext,
|
|
387
|
+
runtimeContext,
|
|
388
|
+
});
|
|
306
389
|
|
|
307
390
|
assertCompletionRelationships({
|
|
308
391
|
contract,
|
|
@@ -311,6 +394,10 @@ export async function recordCheck({
|
|
|
311
394
|
receipt: nextReceipt,
|
|
312
395
|
requiredEvidence,
|
|
313
396
|
requireRequiredChecks: false,
|
|
397
|
+
target,
|
|
398
|
+
taskId: contract.value.taskId,
|
|
399
|
+
authorityContext,
|
|
400
|
+
runtimeContext,
|
|
314
401
|
});
|
|
315
402
|
|
|
316
403
|
await writeWorkState(target, nextState, { packageRoot });
|
|
@@ -351,6 +438,8 @@ export async function recordTerminalResult({
|
|
|
351
438
|
source,
|
|
352
439
|
result,
|
|
353
440
|
details = {},
|
|
441
|
+
authorityContext,
|
|
442
|
+
runtimeContext,
|
|
354
443
|
} = {}) {
|
|
355
444
|
if (!target || !requirement || !type || !status || !source || !result) {
|
|
356
445
|
throw artifactError("E_CHECK_INVALID", "record-terminal-result requires target, requirement, type, status, source, and result", [ARTIFACT_PATHS.state]);
|
|
@@ -413,7 +502,12 @@ export async function recordTerminalResult({
|
|
|
413
502
|
}
|
|
414
503
|
|
|
415
504
|
const existingReceipt = await readCurrentReceipt(target, packageRoot);
|
|
416
|
-
await validateReceipt(existingReceipt.value, packageRoot
|
|
505
|
+
await validateReceipt(existingReceipt.value, packageRoot, {
|
|
506
|
+
target,
|
|
507
|
+
taskId: contract?.value?.taskId,
|
|
508
|
+
authorityContext,
|
|
509
|
+
runtimeContext,
|
|
510
|
+
});
|
|
417
511
|
|
|
418
512
|
if (type === "PUBLICATION") {
|
|
419
513
|
const rank = {
|
|
@@ -540,7 +634,12 @@ export async function recordTerminalResult({
|
|
|
540
634
|
const nextReceipt = await createReceipt({
|
|
541
635
|
...receiptUpdates,
|
|
542
636
|
stateFingerprint: canonicalFingerprint(nextState),
|
|
543
|
-
}, packageRoot
|
|
637
|
+
}, packageRoot, {
|
|
638
|
+
target,
|
|
639
|
+
taskId: contract.value.taskId,
|
|
640
|
+
authorityContext,
|
|
641
|
+
runtimeContext,
|
|
642
|
+
});
|
|
544
643
|
|
|
545
644
|
await writeWorkState(target, nextState, { packageRoot });
|
|
546
645
|
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)) {
|
package/src/core/completion.js
CHANGED
|
@@ -77,6 +77,13 @@ function repairNext(error) {
|
|
|
77
77
|
return "Satisfy or refresh the named gate, then rerun forgeloop preflight.";
|
|
78
78
|
case "E_PROFILE_UNVERIFIED":
|
|
79
79
|
return "Use Standard mode for a fresh target, or verify PROJECT_PROFILE.md before Strict completion.";
|
|
80
|
+
case "E_INSTALLATION_AUTHORITY_REQUIRED":
|
|
81
|
+
case "E_AUTHORITY_INVALID":
|
|
82
|
+
case "E_AUTHORITY_SCOPE_MISMATCH":
|
|
83
|
+
case "E_AUTHORITY_UNTRUSTED_SOURCE":
|
|
84
|
+
return "Do not execute installation-capable verification commands without explicit scoped installation authority; use local equivalents or record NOT_VERIFIED.";
|
|
85
|
+
case "E_VERIFICATION_TOOL_UNAVAILABLE":
|
|
86
|
+
return "Use an available local verifier, an existing equivalent, or record NOT_VERIFIED if installation was not authorized.";
|
|
80
87
|
default:
|
|
81
88
|
return "Resolve this validator finding in the named artifact before retrying completion.";
|
|
82
89
|
}
|
|
@@ -151,7 +158,7 @@ async function validateLedger(target, taskId, state, errors, packageRoot) {
|
|
|
151
158
|
return ledger;
|
|
152
159
|
}
|
|
153
160
|
|
|
154
|
-
export async function evaluateCompletion({ target, packageRoot, strict = false } = {}) {
|
|
161
|
+
export async function evaluateCompletion({ target, packageRoot, strict = false, authorityContext, runtimeContext } = {}) {
|
|
155
162
|
const errors = [];
|
|
156
163
|
const preflight = await evaluatePreflight({ target, packageRoot, strict });
|
|
157
164
|
errors.push(...preflight.errors);
|
|
@@ -198,7 +205,12 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
198
205
|
|
|
199
206
|
if (receipt) {
|
|
200
207
|
try {
|
|
201
|
-
await validateReceipt(receipt.value, packageRoot
|
|
208
|
+
await validateReceipt(receipt.value, packageRoot, {
|
|
209
|
+
target,
|
|
210
|
+
taskId: contract?.value?.taskId,
|
|
211
|
+
authorityContext,
|
|
212
|
+
runtimeContext,
|
|
213
|
+
});
|
|
202
214
|
} catch (error) {
|
|
203
215
|
errors.push(issue(error.code ?? "E_RECEIPT_INVALID", `Execution receipt is invalid: ${error.message}`, [ARTIFACT_PATHS.receipt]));
|
|
204
216
|
}
|
|
@@ -223,6 +235,10 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
223
235
|
state,
|
|
224
236
|
receipt: receipt?.value,
|
|
225
237
|
requiredEvidence,
|
|
238
|
+
target,
|
|
239
|
+
taskId: contract?.value?.taskId,
|
|
240
|
+
authorityContext,
|
|
241
|
+
runtimeContext,
|
|
226
242
|
});
|
|
227
243
|
errors.push(...relationshipErrors);
|
|
228
244
|
coverage = receipt?.value?.evidenceCoverage ?? [];
|
|
@@ -290,12 +306,18 @@ export async function evaluateCompletion({ target, packageRoot, strict = false }
|
|
|
290
306
|
};
|
|
291
307
|
}
|
|
292
308
|
|
|
293
|
-
export async function runComplete({ target, packageRoot, strict = false, persist = true } = {}) {
|
|
294
|
-
const result = await evaluateCompletion({ target, packageRoot, strict });
|
|
309
|
+
export async function runComplete({ target, packageRoot, strict = false, persist = true, authorityContext, runtimeContext } = {}) {
|
|
310
|
+
const result = await evaluateCompletion({ target, packageRoot, strict, authorityContext, runtimeContext });
|
|
295
311
|
const rejectionCodes = [...new Set(result.errors.map((error) => error.code))].sort();
|
|
296
312
|
const evidenceOnlyRejection = rejectionCodes.length > 0
|
|
297
313
|
&& rejectionCodes.every(isRecoverableCompletionEvidenceCode);
|
|
298
|
-
|
|
314
|
+
const authorityRejection = rejectionCodes.some((code) => [
|
|
315
|
+
"E_INSTALLATION_AUTHORITY_REQUIRED",
|
|
316
|
+
"E_AUTHORITY_INVALID",
|
|
317
|
+
"E_AUTHORITY_SCOPE_MISMATCH",
|
|
318
|
+
"E_AUTHORITY_UNTRUSTED_SOURCE",
|
|
319
|
+
].includes(code));
|
|
320
|
+
if (persist && result.status === "REJECTED" && evidenceOnlyRejection && !authorityRejection) {
|
|
299
321
|
const state = await readWorkState(target, packageRoot);
|
|
300
322
|
if (state?.phase === "REVIEWING") {
|
|
301
323
|
const reasonCodes = rejectionCodes;
|
|
@@ -335,7 +357,7 @@ export async function runComplete({ target, packageRoot, strict = false, persist
|
|
|
335
357
|
...receipt.value,
|
|
336
358
|
stateFingerprint: canonicalFingerprint(next),
|
|
337
359
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
338
|
-
}, packageRoot);
|
|
360
|
+
}, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
|
|
339
361
|
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
|
340
362
|
}
|
|
341
363
|
const ledger = await validateEventLedger(target, packageRoot);
|
|
@@ -380,7 +402,7 @@ export async function runComplete({ target, packageRoot, strict = false, persist
|
|
|
380
402
|
...receipt.value,
|
|
381
403
|
stateFingerprint: canonicalFingerprint(next),
|
|
382
404
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
383
|
-
}, packageRoot);
|
|
405
|
+
}, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
|
|
384
406
|
await writeWorkState(target, next, { packageRoot });
|
|
385
407
|
await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
|
|
386
408
|
}
|
package/src/core/conformance.js
CHANGED
|
@@ -23,6 +23,24 @@ function sortErrors(errors) {
|
|
|
23
23
|
|| left.message.localeCompare(right.message));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function delegationIsInScope({
|
|
27
|
+
state = null,
|
|
28
|
+
receipt = null,
|
|
29
|
+
events = [],
|
|
30
|
+
taskBriefs = [],
|
|
31
|
+
delegatedResults = [],
|
|
32
|
+
} = {}) {
|
|
33
|
+
if (taskBriefs && taskBriefs.length > 0) return true;
|
|
34
|
+
if (delegatedResults && delegatedResults.length > 0) return true;
|
|
35
|
+
if (state?.delegatedTasks && state.delegatedTasks.length > 0) return true;
|
|
36
|
+
if (state?.delegatedTaskIds && state.delegatedTaskIds.length > 0) return true;
|
|
37
|
+
if (receipt?.delegatedTasks && receipt.delegatedTasks.length > 0) return true;
|
|
38
|
+
if (Array.isArray(events) && events.some((event) => typeof event?.type === "string" && event.type.toLowerCase().includes("delegat"))) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
export function validateTaskArtifactSet({
|
|
27
45
|
route = null,
|
|
28
46
|
state = null,
|
|
@@ -30,6 +48,7 @@ export function validateTaskArtifactSet({
|
|
|
30
48
|
receipt = null,
|
|
31
49
|
taskBriefs = [],
|
|
32
50
|
delegatedResults = [],
|
|
51
|
+
events = [],
|
|
33
52
|
} = {}) {
|
|
34
53
|
const errors = [];
|
|
35
54
|
const incomplete = [];
|
|
@@ -62,6 +81,7 @@ export function validateTaskArtifactSet({
|
|
|
62
81
|
errors.push(error("STATE_RECEIPT_GUIDES_MISMATCH", "execution-receipt.selectedGuides must equal work-state.selectedGuides", ["state", "receipt"]));
|
|
63
82
|
}
|
|
64
83
|
|
|
84
|
+
const delegationActive = delegationIsInScope({ state, receipt, events, taskBriefs, delegatedResults });
|
|
65
85
|
const briefIds = new Set();
|
|
66
86
|
for (const brief of taskBriefs) {
|
|
67
87
|
if (!brief?.taskId) continue;
|
|
@@ -86,13 +106,18 @@ export function validateTaskArtifactSet({
|
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
if (delegationActive) {
|
|
110
|
+
if (taskBriefs.length > 0) {
|
|
111
|
+
for (const taskId of [...briefIds].sort()) {
|
|
112
|
+
if (!delegatedIds.has(taskId)) incomplete.push(`missing delegated result: ${taskId}`);
|
|
113
|
+
}
|
|
114
|
+
} else if (delegatedResults.length > 0) {
|
|
115
|
+
incomplete.push("task briefs are required when delegated results are supplied");
|
|
116
|
+
} else {
|
|
117
|
+
incomplete.push("task briefs and delegated results were not supplied for delegated task");
|
|
92
118
|
}
|
|
93
|
-
} else if (delegatedResults.length === 0) {
|
|
94
|
-
incomplete.push("task briefs and delegated results were not supplied");
|
|
95
119
|
}
|
|
120
|
+
|
|
96
121
|
if (!route || !state || !receipt) incomplete.push("route, state, and receipt are all required for a complete artifact set");
|
|
97
122
|
|
|
98
123
|
const sortedErrors = sortErrors(errors);
|
|
@@ -117,6 +142,22 @@ export function validateTaskArtifactSet({
|
|
|
117
142
|
}
|
|
118
143
|
: null;
|
|
119
144
|
|
|
145
|
+
const delegation = delegationActive
|
|
146
|
+
? {
|
|
147
|
+
status: sortedErrors.some((e) => e.code.includes("DELEGAT") || e.code.includes("TASK"))
|
|
148
|
+
? "INCONSISTENT"
|
|
149
|
+
: incomplete.some((i) => i.includes("delegat") || i.includes("brief"))
|
|
150
|
+
? "INCOMPLETE"
|
|
151
|
+
: "VALID",
|
|
152
|
+
required: true,
|
|
153
|
+
errors: sortedErrors.filter((e) => e.code.includes("DELEGAT") || e.code.includes("TASK")),
|
|
154
|
+
}
|
|
155
|
+
: {
|
|
156
|
+
status: "NOT_APPLICABLE",
|
|
157
|
+
required: false,
|
|
158
|
+
errors: [],
|
|
159
|
+
};
|
|
160
|
+
|
|
120
161
|
const evidenceKind = status === "VALID"
|
|
121
162
|
? "OBSERVED"
|
|
122
163
|
: status === "INCOMPLETE"
|
|
@@ -131,6 +172,7 @@ export function validateTaskArtifactSet({
|
|
|
131
172
|
errors: sortedErrors,
|
|
132
173
|
incomplete: [...new Set(incomplete)].sort(),
|
|
133
174
|
stale,
|
|
175
|
+
delegation,
|
|
134
176
|
evidence: [createEvidence({
|
|
135
177
|
kind: evidenceKind,
|
|
136
178
|
source: "ForgeLoop protocol conformance",
|
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,5 @@
|
|
|
1
1
|
import { sha256 } from "./manifest.js";
|
|
2
|
+
import { validateVerificationAuthority } from "./verification-capability.js";
|
|
2
3
|
|
|
3
4
|
export const REQUIREMENT_TYPES = Object.freeze([
|
|
4
5
|
"PRODUCT",
|
|
@@ -246,7 +247,7 @@ export function authoritativeChecksForRequirements({ requirements = [], checks =
|
|
|
246
247
|
});
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
function componentStatus(check, requirement, allChecks = []) {
|
|
250
|
+
function componentStatus(check, requirement, allChecks = [], options = {}) {
|
|
250
251
|
if (requirement.operator !== "ALL" || !requirement.requirements?.length) return null;
|
|
251
252
|
const components = check?.details?.components;
|
|
252
253
|
const statuses = requirement.requirements.map((child) => {
|
|
@@ -254,10 +255,19 @@ function componentStatus(check, requirement, allChecks = []) {
|
|
|
254
255
|
const matchingComp = components.filter((item) => (
|
|
255
256
|
item?.requirementId === child.id || item?.requirement === child.text
|
|
256
257
|
)).at(-1);
|
|
257
|
-
if (matchingComp)
|
|
258
|
+
if (matchingComp) {
|
|
259
|
+
const auth = validateVerificationAuthority(matchingComp, options);
|
|
260
|
+
if (!auth.valid) return { ...matchingComp, status: "failed", reasonCode: auth.error.code };
|
|
261
|
+
return matchingComp;
|
|
262
|
+
}
|
|
258
263
|
}
|
|
259
264
|
const childCandidates = allChecks.filter((candidate) => matchesRequirement(candidate, child));
|
|
260
|
-
|
|
265
|
+
const childCheck = latestAuthoritativeCheck(childCandidates);
|
|
266
|
+
if (childCheck) {
|
|
267
|
+
const auth = validateVerificationAuthority(childCheck, options);
|
|
268
|
+
if (!auth.valid) return { ...childCheck, status: "failed", reasonCode: auth.error.code };
|
|
269
|
+
}
|
|
270
|
+
return childCheck;
|
|
261
271
|
});
|
|
262
272
|
if (statuses.some((item) => !item)) return "MISSING";
|
|
263
273
|
if (statuses.some((item) => item.status === "failed")) return "INVALID";
|
|
@@ -265,7 +275,20 @@ function componentStatus(check, requirement, allChecks = []) {
|
|
|
265
275
|
return "COVERED";
|
|
266
276
|
}
|
|
267
277
|
|
|
268
|
-
export function evaluateRequiredEvidence({
|
|
278
|
+
export function evaluateRequiredEvidence({
|
|
279
|
+
requirements = [],
|
|
280
|
+
checks = [],
|
|
281
|
+
target,
|
|
282
|
+
taskId,
|
|
283
|
+
authorities,
|
|
284
|
+
options = {},
|
|
285
|
+
} = {}) {
|
|
286
|
+
const authOptions = {
|
|
287
|
+
...(target ? { target } : {}),
|
|
288
|
+
...(taskId ? { taskId } : {}),
|
|
289
|
+
...(authorities ? { authorities } : {}),
|
|
290
|
+
...options,
|
|
291
|
+
};
|
|
269
292
|
const normalized = normalizeRequirements(requirements);
|
|
270
293
|
const result = {
|
|
271
294
|
ready: true,
|
|
@@ -291,8 +314,11 @@ export function evaluateRequiredEvidence({ requirements = [], checks = [] } = {}
|
|
|
291
314
|
}
|
|
292
315
|
const candidates = checks.filter((check) => matchesRequirement(check, requirement));
|
|
293
316
|
const check = latestAuthoritativeCheck(candidates);
|
|
294
|
-
const
|
|
295
|
-
|
|
317
|
+
const auth = check ? validateVerificationAuthority(check, authOptions) : { valid: true };
|
|
318
|
+
const compound = componentStatus(check, requirement, checks, authOptions);
|
|
319
|
+
if (!auth.valid) {
|
|
320
|
+
result.invalid.push({ ...requirement, reasonCode: auth.error.code });
|
|
321
|
+
} else if (compound === "INVALID" || check?.status === "failed") {
|
|
296
322
|
result.invalid.push(requirement);
|
|
297
323
|
} else if (compound === "PARTIAL") {
|
|
298
324
|
result.partial.push(requirement);
|
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",
|