@cassiomc1/forgeloop 0.1.14 → 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.
@@ -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
+ }
@@ -24,6 +24,7 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
24
24
  "activation",
25
25
  "policy",
26
26
  "task-bundle",
27
+ "execution",
27
28
  ]);
28
29
 
29
30
  export class SchemaValidationError extends Error {
@@ -59,6 +59,7 @@ export const TEMPLATE_PATHS = [
59
59
  "schemas/config.schema.json",
60
60
  "schemas/preflight.schema.json",
61
61
  "schemas/check.schema.json",
62
+ "schemas/execution.schema.json",
62
63
  "schemas/evidence-coverage.schema.json",
63
64
  "schemas/event.schema.json",
64
65
  "schemas/activation.schema.json",