@cassiomc1/forgeloop 0.1.14 → 0.1.16
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/DOCS_INDEX.md +60 -0
- package/EXECUTION_STATE.md +9 -0
- package/LOOP_ENGINEERING.md +44 -6
- package/LOOP_SYSTEM_DESIGN.md +8 -0
- package/PROTOCOL_INTEGRATION.md +41 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +183 -643
- package/TERMINOLOGY.md +4 -0
- package/THREAT_MODEL.md +14 -0
- package/docs/assets/forgeloop-flow.svg +1 -0
- package/docs/forgeloop-flow.mmd +51 -0
- package/package.json +16 -3
- package/schemas/check.schema.json +2 -0
- package/schemas/continuity.schema.json +57 -0
- package/schemas/execution.schema.json +63 -0
- package/scripts/CI_VALIDATORS.md +32 -0
- package/src/cli.js +351 -196
- package/src/commands/clear-continuity.js +9 -0
- package/src/commands/continuity.js +25 -0
- package/src/commands/doctor.js +17 -2
- package/src/commands/reconcile-continuity.js +23 -0
- package/src/commands/record-continuity.js +63 -0
- package/src/commands/run-check.js +83 -0
- package/src/commands/status.js +14 -1
- package/src/commands/update.js +12 -13
- package/src/commands/validate-protocol.js +37 -0
- package/src/core/artifacts.js +9 -0
- package/src/core/bundles.js +78 -0
- package/src/core/checks.js +16 -0
- package/src/core/command-resolution.js +295 -0
- package/src/core/command-tokenizer.js +122 -0
- package/src/core/completion-artifacts.js +229 -47
- package/src/core/completion.js +19 -1
- package/src/core/conformance.js +8 -2
- package/src/core/continuity-cli-options.js +58 -0
- package/src/core/continuity-conformance.js +46 -0
- package/src/core/continuity-observability.js +20 -0
- package/src/core/continuity-reconciliation.js +224 -0
- package/src/core/continuity.js +245 -0
- package/src/core/evidence-readiness.js +26 -1
- package/src/core/execution.js +185 -0
- package/src/core/inspect.js +9 -1
- package/src/core/installation-authority.js +178 -0
- package/src/core/json-safety.js +17 -13
- package/src/core/next-action-artifacts.js +118 -0
- package/src/core/next-action-continuity.js +65 -0
- package/src/core/next-action-model.js +127 -0
- package/src/core/next-action-phases.js +12 -0
- package/src/core/next-action.js +22 -249
- package/src/core/npm-classifier.js +343 -0
- package/src/core/package-manager-classifiers.js +37 -0
- package/src/core/preflight-consistency.js +221 -0
- package/src/core/preflight-loaders.js +112 -0
- package/src/core/preflight-model.js +83 -0
- package/src/core/preflight.js +34 -444
- package/src/core/protocol.js +7 -0
- package/src/core/schema-validation.js +16 -1
- package/src/core/templates.js +3 -0
- package/src/core/verification-capability.js +31 -495
- package/src/core/verification-constants.js +61 -0
|
@@ -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
|
@@ -9,6 +9,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
11
|
import { trustedAuthorityConfiguration } from "./trusted-authority.js";
|
|
12
|
+
import { reconcileContinuity } from "./continuity-reconciliation.js";
|
|
13
|
+
import { continuityFinding, continuityIsHealthy } from "./continuity-observability.js";
|
|
12
14
|
|
|
13
15
|
function profileMetadata(bytes) {
|
|
14
16
|
const text = bytes.toString("utf8");
|
|
@@ -35,6 +37,7 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
35
37
|
const statePath = ensureWithin(target, WORK_STATE_PATH);
|
|
36
38
|
const statePresent = await fileExists(statePath);
|
|
37
39
|
const state = await readAndClassifyWorkState({ target, packageRoot, contractFile });
|
|
40
|
+
const continuity = await reconcileContinuity({ target, packageRoot });
|
|
38
41
|
const schemaRoot = manifest?.layoutVersion >= 2
|
|
39
42
|
? ensureWithin(target, FORGELOOP_KIT_DIR)
|
|
40
43
|
: target;
|
|
@@ -69,6 +72,9 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
const continuityIssue = continuityFinding(continuity);
|
|
76
|
+
if (continuityIssue) findings.push(continuityIssue);
|
|
77
|
+
|
|
72
78
|
if (state.status === "INVALID") {
|
|
73
79
|
findings.push({
|
|
74
80
|
code: "state-invalid",
|
|
@@ -123,6 +129,7 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
123
129
|
evidence: protocolEvidence,
|
|
124
130
|
},
|
|
125
131
|
state: { ...state, path: WORK_STATE_PATH, present: statePresent },
|
|
132
|
+
continuity,
|
|
126
133
|
compatibility: {
|
|
127
134
|
deprecated: true,
|
|
128
135
|
agents: [],
|
|
@@ -132,6 +139,7 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
132
139
|
ok: doctor.ok
|
|
133
140
|
&& !manifestError
|
|
134
141
|
&& schemaHealth.status === "valid"
|
|
135
|
-
&& !["INVALID", "REVALIDATION_REQUIRED"].includes(state.status)
|
|
142
|
+
&& !["INVALID", "REVALIDATION_REQUIRED"].includes(state.status)
|
|
143
|
+
&& continuityIsHealthy(continuity),
|
|
136
144
|
};
|
|
137
145
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E_AUTHORITY_INVALID,
|
|
3
|
+
E_AUTHORITY_SCOPE_MISMATCH,
|
|
4
|
+
E_INSTALLATION_AUTHORITY_REQUIRED,
|
|
5
|
+
} from "./verification-constants.js";
|
|
6
|
+
import { classifyCommandResolution } from "./command-resolution.js";
|
|
7
|
+
import {
|
|
8
|
+
E_AUTHORITY_UNTRUSTED_SOURCE,
|
|
9
|
+
resolveTrustedAuthority,
|
|
10
|
+
} from "./trusted-authority.js";
|
|
11
|
+
|
|
12
|
+
export { E_AUTHORITY_UNTRUSTED_SOURCE };
|
|
13
|
+
|
|
14
|
+
export function getInstallationAuthorityRef(check) {
|
|
15
|
+
return (
|
|
16
|
+
check?.details?.installationAuthorityRef
|
|
17
|
+
?? check?.details?.authorityRef
|
|
18
|
+
?? check?.installationAuthorityRef
|
|
19
|
+
?? check?.authorityRef
|
|
20
|
+
?? null
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeToolName(toolName) {
|
|
25
|
+
if (typeof toolName !== "string") return "";
|
|
26
|
+
if (toolName.startsWith("@")) {
|
|
27
|
+
const parts = toolName.slice(1).split("@");
|
|
28
|
+
return `@${parts[0]}`;
|
|
29
|
+
}
|
|
30
|
+
return toolName.split("@")[0];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateAuthorityGrant({ authority, taskId, type = "SOFTWARE_INSTALLATION", tool } = {}) {
|
|
34
|
+
if (!authority || typeof authority !== "object") {
|
|
35
|
+
return {
|
|
36
|
+
valid: false,
|
|
37
|
+
error: {
|
|
38
|
+
code: E_AUTHORITY_INVALID,
|
|
39
|
+
message: "Authority grant artifact is missing or invalid",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (authority.schemaVersion !== 1 || authority.protocolVersion !== 1) {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
error: {
|
|
48
|
+
code: E_AUTHORITY_INVALID,
|
|
49
|
+
message: `Authority grant schema version (${authority.schemaVersion}) or protocol version (${authority.protocolVersion}) is invalid`,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (authority.type !== type) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
error: {
|
|
58
|
+
code: E_AUTHORITY_INVALID,
|
|
59
|
+
message: `Authority grant type '${authority.type}' does not match expected '${type}'`,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (authority.status !== "AUTHORIZED") {
|
|
65
|
+
return {
|
|
66
|
+
valid: false,
|
|
67
|
+
error: {
|
|
68
|
+
code: E_AUTHORITY_INVALID,
|
|
69
|
+
message: `Authority grant is not active (status: '${authority.status}')`,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (authority.source === "agent-self") {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
error: {
|
|
78
|
+
code: E_AUTHORITY_INVALID,
|
|
79
|
+
message: "Self-asserted authority grants with source 'agent-self' are not permitted",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!["operator", "host", "project-policy"].includes(authority.source)) {
|
|
85
|
+
return {
|
|
86
|
+
valid: false,
|
|
87
|
+
error: {
|
|
88
|
+
code: E_AUTHORITY_INVALID,
|
|
89
|
+
message: `Authority grant source '${authority.source}' is not recognized`,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (taskId && authority.taskId && authority.taskId !== taskId) {
|
|
95
|
+
return {
|
|
96
|
+
valid: false,
|
|
97
|
+
error: {
|
|
98
|
+
code: E_AUTHORITY_INVALID,
|
|
99
|
+
message: `Authority grant taskId '${authority.taskId}' does not match current task '${taskId}'`,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (tool && authority.scope?.tool && authority.scope.tool !== "*") {
|
|
105
|
+
const requestedNorm = normalizeToolName(tool);
|
|
106
|
+
const scopeNorm = normalizeToolName(authority.scope.tool);
|
|
107
|
+
const exactMatch = authority.scope.tool === tool
|
|
108
|
+
|| tool.startsWith(`${authority.scope.tool}@`)
|
|
109
|
+
|| authority.scope.tool.startsWith(`${tool}@`)
|
|
110
|
+
|| requestedNorm === scopeNorm;
|
|
111
|
+
|
|
112
|
+
if (!exactMatch) {
|
|
113
|
+
return {
|
|
114
|
+
valid: false,
|
|
115
|
+
error: {
|
|
116
|
+
code: E_AUTHORITY_SCOPE_MISMATCH,
|
|
117
|
+
message: `Authority grant scope tool '${authority.scope.tool}' does not match requested verification tool '${tool}'`,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { valid: true, error: null };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function validateVerificationAuthority(check, options = {}) {
|
|
127
|
+
const canonicalResolution = check?.execution?.resolution ?? check?.details?.execution?.resolution;
|
|
128
|
+
const command = check?.details?.command
|
|
129
|
+
?? (check?.kind === "command" && typeof check?.source === "string" && !check.source.startsWith("check:")
|
|
130
|
+
? check.source
|
|
131
|
+
: null);
|
|
132
|
+
|
|
133
|
+
if (!canonicalResolution && (!command || typeof command !== "string")) return { valid: true, error: null };
|
|
134
|
+
|
|
135
|
+
const classification = canonicalResolution ?? classifyCommandResolution(command);
|
|
136
|
+
if (!classification.mayInstall) return { valid: true, error: null };
|
|
137
|
+
|
|
138
|
+
const authorityRef = getInstallationAuthorityRef(check);
|
|
139
|
+
if (!authorityRef) {
|
|
140
|
+
return {
|
|
141
|
+
valid: false,
|
|
142
|
+
error: {
|
|
143
|
+
code: E_INSTALLATION_AUTHORITY_REQUIRED,
|
|
144
|
+
message: `Verification command '${command}' uses installation-capable resolution (${classification.resolutionMode}) without recorded installation authority reference`,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const resolved = resolveTrustedAuthority({
|
|
150
|
+
authorityRef,
|
|
151
|
+
target: options.target,
|
|
152
|
+
trustedAuthorityFile: options.trustedAuthorityFile,
|
|
153
|
+
trustedAuthorityDir: options.trustedAuthorityDir,
|
|
154
|
+
authorities: options.authorities,
|
|
155
|
+
authority: options.authority,
|
|
156
|
+
authorityContext: options.authorityContext,
|
|
157
|
+
runtimeContext: options.runtimeContext,
|
|
158
|
+
});
|
|
159
|
+
if (!resolved.trusted) {
|
|
160
|
+
if (resolved.error?.code === E_AUTHORITY_INVALID && resolved.sourceConfigured === false) {
|
|
161
|
+
return {
|
|
162
|
+
valid: false,
|
|
163
|
+
error: {
|
|
164
|
+
code: E_INSTALLATION_AUTHORITY_REQUIRED,
|
|
165
|
+
message: "Installation-capable verification requires a host-attested authority context",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { valid: false, error: resolved.error };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return validateAuthorityGrant({
|
|
173
|
+
authority: resolved.authority,
|
|
174
|
+
taskId: options.taskId,
|
|
175
|
+
type: "SOFTWARE_INSTALLATION",
|
|
176
|
+
tool: classification.tool,
|
|
177
|
+
});
|
|
178
|
+
}
|
package/src/core/json-safety.js
CHANGED
|
@@ -23,7 +23,7 @@ export function assertJsonBytes(bytes, label = "JSON artifact", limits = JSON_LI
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function assertJsonLimits(value, label = "JSON artifact", limits = JSON_LIMITS) {
|
|
26
|
-
const
|
|
26
|
+
const ancestors = new Set();
|
|
27
27
|
|
|
28
28
|
function visit(current, depth, location) {
|
|
29
29
|
if (typeof current === "string") {
|
|
@@ -33,19 +33,23 @@ export function assertJsonLimits(value, label = "JSON artifact", limits = JSON_L
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
if (!current || typeof current !== "object") return;
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (current
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
if (ancestors.has(current)) throw new JsonLimitError(`${location} contains a circular reference`);
|
|
37
|
+
ancestors.add(current);
|
|
38
|
+
try {
|
|
39
|
+
if (depth > limits.maxDepth) throw new JsonLimitError(`${location} exceeds the JSON depth limit`);
|
|
40
|
+
if (Array.isArray(current)) {
|
|
41
|
+
if (current.length > limits.maxArrayLength) throw new JsonLimitError(`${location} exceeds the array length limit`);
|
|
42
|
+
current.forEach((item, index) => visit(item, depth + 1, `${location}[${index}]`));
|
|
43
|
+
} else {
|
|
44
|
+
const keys = Object.keys(current);
|
|
45
|
+
if (keys.length > limits.maxObjectKeys) throw new JsonLimitError(`${location} exceeds the object key limit`);
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
if (key.length > limits.maxStringLength) throw new JsonLimitError(`${location} contains an oversized key`);
|
|
48
|
+
visit(current[key], depth + 1, `${location}.${key}`);
|
|
49
|
+
}
|
|
48
50
|
}
|
|
51
|
+
} finally {
|
|
52
|
+
ancestors.delete(current);
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ARTIFACT_PATHS } from "./artifacts.js";
|
|
2
|
+
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
3
|
+
import { coverageForRequirements } from "./coverage.js";
|
|
4
|
+
import { assertCheckList } from "./checks.js";
|
|
5
|
+
|
|
6
|
+
export function artifactError(code, message, artifacts = []) {
|
|
7
|
+
return { code, message, artifacts };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function staleReasons(state, contract, route) {
|
|
11
|
+
const reasons = [];
|
|
12
|
+
if (state.contractFingerprint !== contract.fingerprint) {
|
|
13
|
+
reasons.push(artifactError(
|
|
14
|
+
"E_CONTRACT_STALE",
|
|
15
|
+
"Work state references a different current contract",
|
|
16
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract],
|
|
17
|
+
));
|
|
18
|
+
}
|
|
19
|
+
if (route.value.contractFingerprint !== undefined && route.value.contractFingerprint !== contract.fingerprint) {
|
|
20
|
+
reasons.push(artifactError(
|
|
21
|
+
"E_ROUTE_STALE",
|
|
22
|
+
"Routing result references a different current contract",
|
|
23
|
+
[ARTIFACT_PATHS.route, ARTIFACT_PATHS.contract],
|
|
24
|
+
));
|
|
25
|
+
}
|
|
26
|
+
if (state.routeFingerprint !== route.fingerprint) {
|
|
27
|
+
reasons.push(artifactError(
|
|
28
|
+
"E_ROUTE_STALE",
|
|
29
|
+
"Work state references a different routing result",
|
|
30
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
|
|
31
|
+
));
|
|
32
|
+
}
|
|
33
|
+
if (JSON.stringify(state.selectedGuides) !== JSON.stringify(route.value.guides)) {
|
|
34
|
+
reasons.push(artifactError(
|
|
35
|
+
"E_ROUTE_STALE",
|
|
36
|
+
"Work state guides do not match the persisted routing result",
|
|
37
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
|
|
38
|
+
));
|
|
39
|
+
}
|
|
40
|
+
return reasons;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function freshnessReasons(state, classification) {
|
|
44
|
+
const requiredArtifactPaths = state.requiredArtifacts?.map((artifact) => artifact.path) ?? [];
|
|
45
|
+
const reasons = [artifactError(
|
|
46
|
+
"E_STATE_REVALIDATION_REQUIRED",
|
|
47
|
+
`Work-state checkpoint requires revalidation: ${classification.reasons.join(", ")}`,
|
|
48
|
+
[ARTIFACT_PATHS.state],
|
|
49
|
+
)];
|
|
50
|
+
for (const reason of classification.reasons) {
|
|
51
|
+
const contractRelated = ["CONTRACT_CHANGED", "CONTRACT_INVALID", "CONTRACT_NOT_VERIFIED"].includes(reason);
|
|
52
|
+
const artifactRelated = reason.startsWith("REQUIRED_ARTIFACT");
|
|
53
|
+
reasons.push(artifactError(
|
|
54
|
+
contractRelated ? "E_CONTRACT_STALE" : artifactRelated ? "E_REQUIRED_ARTIFACT_STALE" : "E_REPOSITORY_CHANGED",
|
|
55
|
+
`Work-state freshness check failed: ${reason}`,
|
|
56
|
+
uniqueSorted([
|
|
57
|
+
ARTIFACT_PATHS.state,
|
|
58
|
+
...(contractRelated ? [ARTIFACT_PATHS.contract] : []),
|
|
59
|
+
...(artifactRelated ? requiredArtifactPaths : []),
|
|
60
|
+
]),
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
return reasons;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function checkListReasons(state) {
|
|
67
|
+
try {
|
|
68
|
+
assertCheckList(state.checks, "work-state.checks");
|
|
69
|
+
return [];
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return [artifactError(error.code ?? "E_CHECK_INVALID", error.message, [ARTIFACT_PATHS.state])];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadArtifact(loader, fallback) {
|
|
76
|
+
try {
|
|
77
|
+
return { value: await loader(), error: null, missingArtifacts: [] };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return {
|
|
80
|
+
value: null,
|
|
81
|
+
error,
|
|
82
|
+
missingArtifacts: error?.code === "ARTIFACT_MISSING"
|
|
83
|
+
? (error.artifacts?.length ? error.artifacts : [fallback])
|
|
84
|
+
: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function requirementsAndCoverage({
|
|
90
|
+
target,
|
|
91
|
+
packageRoot,
|
|
92
|
+
contract,
|
|
93
|
+
route,
|
|
94
|
+
checks,
|
|
95
|
+
additionalEvidence = [],
|
|
96
|
+
authorityContext,
|
|
97
|
+
runtimeContext,
|
|
98
|
+
}) {
|
|
99
|
+
const requirements = await requiredEvidenceForTarget({
|
|
100
|
+
target,
|
|
101
|
+
contract,
|
|
102
|
+
route,
|
|
103
|
+
packageRoot,
|
|
104
|
+
additionalEvidence,
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
requirements,
|
|
108
|
+
coverage: coverageForRequirements(requirements, checks, {
|
|
109
|
+
target,
|
|
110
|
+
taskId: contract?.value?.taskId,
|
|
111
|
+
options: { authorityContext, runtimeContext },
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function uniqueSorted(values) {
|
|
117
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
118
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ARTIFACT_PATHS } from "./artifacts.js";
|
|
2
|
+
import { reconcileContinuity } from "./continuity-reconciliation.js";
|
|
3
|
+
import { NEXT_ACTIONS, result } from "./next-action-model.js";
|
|
4
|
+
|
|
5
|
+
const REQUIRED_ARTIFACTS = Object.freeze([ARTIFACT_PATHS.state, ARTIFACT_PATHS.continuity]);
|
|
6
|
+
|
|
7
|
+
function reason(code, message, artifacts = REQUIRED_ARTIFACTS) {
|
|
8
|
+
return { code, message, artifacts };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function nextActionForContinuity({ context, continuity } = {}) {
|
|
12
|
+
if (!continuity || ["ABSENT", "NOT_APPLICABLE"].includes(continuity.classification)) return null;
|
|
13
|
+
|
|
14
|
+
if (continuity.classification === "FRESH") {
|
|
15
|
+
const remaining = continuity.continuity?.remainingWork ?? [];
|
|
16
|
+
if (remaining.length === 0) return null;
|
|
17
|
+
return result({
|
|
18
|
+
...context,
|
|
19
|
+
nextAction: NEXT_ACTIONS.CONTINUE_IMPLEMENTATION,
|
|
20
|
+
reasons: [reason(
|
|
21
|
+
"CONTINUITY_REMAINING_WORK",
|
|
22
|
+
`Execution continuity records ${remaining.length} remaining implementation item${remaining.length === 1 ? "" : "s"}.`,
|
|
23
|
+
[ARTIFACT_PATHS.continuity],
|
|
24
|
+
)],
|
|
25
|
+
requiredArtifacts: REQUIRED_ARTIFACTS,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (continuity.classification === "RECONCILIATION_REQUIRED") {
|
|
30
|
+
return result({
|
|
31
|
+
...context,
|
|
32
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
33
|
+
commands: ["forgeloop reconcile-continuity"],
|
|
34
|
+
reasons: [reason(
|
|
35
|
+
"E_CONTINUITY_RECONCILIATION_REQUIRED",
|
|
36
|
+
"Execution continuity no longer matches canonical state or the current checkout; reconcile it before advancing verification.",
|
|
37
|
+
)],
|
|
38
|
+
requiredArtifacts: REQUIRED_ARTIFACTS,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (["INVALID", "INCONSISTENT"].includes(continuity.classification)) {
|
|
43
|
+
const codes = continuity.reasonCodes?.length > 0
|
|
44
|
+
? continuity.reasonCodes
|
|
45
|
+
: [continuity.classification === "INVALID" ? "E_CONTINUITY_INVALID" : "E_CONTINUITY_INCONSISTENT"];
|
|
46
|
+
return result({
|
|
47
|
+
...context,
|
|
48
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
49
|
+
reasons: codes.map((code, index) => reason(
|
|
50
|
+
code,
|
|
51
|
+
continuity.reasons?.[index] ?? `Execution continuity is ${continuity.classification.toLowerCase()}.`,
|
|
52
|
+
)),
|
|
53
|
+
requiredArtifacts: REQUIRED_ARTIFACTS,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function evaluateContinuityNextAction({ target, packageRoot, context } = {}) {
|
|
61
|
+
return nextActionForContinuity({
|
|
62
|
+
context,
|
|
63
|
+
continuity: await reconcileContinuity({ target, packageRoot }),
|
|
64
|
+
});
|
|
65
|
+
}
|