@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
|
@@ -1,5 +1,31 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
E_AUTHORITY_UNTRUSTED_SOURCE,
|
|
4
|
+
resolveTrustedAuthority,
|
|
5
|
+
} from "./trusted-authority.js";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
export {
|
|
8
|
+
AUTHORITY_TRUST_MODES,
|
|
9
|
+
createAuthorityContext,
|
|
10
|
+
createForgeLoopContext,
|
|
11
|
+
} from "./runtime-context.js";
|
|
12
|
+
|
|
1
13
|
export const E_VERIFICATION_TOOL_UNAVAILABLE = "E_VERIFICATION_TOOL_UNAVAILABLE";
|
|
2
14
|
export const E_INSTALLATION_AUTHORITY_REQUIRED = "E_INSTALLATION_AUTHORITY_REQUIRED";
|
|
15
|
+
export const E_COMMAND_RESOLUTION_AMBIGUOUS = "E_COMMAND_RESOLUTION_AMBIGUOUS";
|
|
16
|
+
export const E_AUTHORITY_INVALID = "E_AUTHORITY_INVALID";
|
|
17
|
+
export const E_AUTHORITY_SCOPE_MISMATCH = "E_AUTHORITY_SCOPE_MISMATCH";
|
|
18
|
+
|
|
19
|
+
export { E_AUTHORITY_UNTRUSTED_SOURCE };
|
|
20
|
+
|
|
21
|
+
export const RESOLUTION_MODES = Object.freeze([
|
|
22
|
+
"LOCAL_EXECUTABLE",
|
|
23
|
+
"LOCAL_PACKAGE_BINARY",
|
|
24
|
+
"NON_INSTALLING_RESOLUTION",
|
|
25
|
+
"INSTALL_CAPABLE_RESOLUTION",
|
|
26
|
+
"EXPLICIT_INSTALLATION",
|
|
27
|
+
"UNKNOWN",
|
|
28
|
+
]);
|
|
3
29
|
|
|
4
30
|
export function classifyVerificationCapability({
|
|
5
31
|
available = false,
|
|
@@ -45,3 +71,1087 @@ export function classifyVerificationCapability({
|
|
|
45
71
|
message: "Verification tool is absent and installation was not authorized.",
|
|
46
72
|
};
|
|
47
73
|
}
|
|
74
|
+
|
|
75
|
+
function tokenizeCommand(commandString) {
|
|
76
|
+
const tokens = [];
|
|
77
|
+
let current = "";
|
|
78
|
+
let inSingleQuote = false;
|
|
79
|
+
let inDoubleQuote = false;
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < commandString.length; i++) {
|
|
82
|
+
const char = commandString[i];
|
|
83
|
+
|
|
84
|
+
if (char === "'" && !inDoubleQuote) {
|
|
85
|
+
inSingleQuote = !inSingleQuote;
|
|
86
|
+
} else if (char === '"' && !inSingleQuote) {
|
|
87
|
+
inDoubleQuote = !inDoubleQuote;
|
|
88
|
+
} else if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) {
|
|
89
|
+
if (current.length > 0) {
|
|
90
|
+
tokens.push(current);
|
|
91
|
+
current = "";
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
current += char;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (current.length > 0) {
|
|
99
|
+
tokens.push(current);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return tokens;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function splitCommandPipeline(commandString) {
|
|
106
|
+
const parts = [];
|
|
107
|
+
let current = "";
|
|
108
|
+
let inSingleQuote = false;
|
|
109
|
+
let inDoubleQuote = false;
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < commandString.length; i++) {
|
|
112
|
+
const char = commandString[i];
|
|
113
|
+
const next = commandString[i + 1];
|
|
114
|
+
|
|
115
|
+
if (char === "'" && !inDoubleQuote) {
|
|
116
|
+
inSingleQuote = !inSingleQuote;
|
|
117
|
+
current += char;
|
|
118
|
+
} else if (char === '"' && !inSingleQuote) {
|
|
119
|
+
inDoubleQuote = !inDoubleQuote;
|
|
120
|
+
current += char;
|
|
121
|
+
} else if (!inSingleQuote && !inDoubleQuote) {
|
|
122
|
+
if ((char === "&" && next === "&") || (char === "|" && next === "|")) {
|
|
123
|
+
if (current.trim().length > 0) parts.push(current.trim());
|
|
124
|
+
current = "";
|
|
125
|
+
i++; // skip next char
|
|
126
|
+
} else if (char === ";" || char === "|") {
|
|
127
|
+
if (current.trim().length > 0) parts.push(current.trim());
|
|
128
|
+
current = "";
|
|
129
|
+
} else {
|
|
130
|
+
current += char;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
current += char;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (current.trim().length > 0) parts.push(current.trim());
|
|
138
|
+
return parts.length > 0 ? parts : [commandString];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractToolFromArgs(args) {
|
|
142
|
+
if (!Array.isArray(args) || args.length === 0) return null;
|
|
143
|
+
for (let idx = 0; idx < args.length; idx++) {
|
|
144
|
+
const arg = args[idx];
|
|
145
|
+
if (arg === "-p" || arg === "--package") {
|
|
146
|
+
if (args[idx + 1] && !args[idx + 1].startsWith("-")) {
|
|
147
|
+
return args[idx + 1];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (arg.startsWith("--package=")) {
|
|
151
|
+
return arg.split("=")[1];
|
|
152
|
+
}
|
|
153
|
+
if (!arg.startsWith("-")) {
|
|
154
|
+
return arg;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extractNpmExecTool(args) {
|
|
161
|
+
if (!Array.isArray(args) || args.length === 0) return null;
|
|
162
|
+
for (let idx = 0; idx < args.length; idx++) {
|
|
163
|
+
const arg = args[idx];
|
|
164
|
+
if (arg === "-p" || arg === "--package") {
|
|
165
|
+
if (args[idx + 1] && !args[idx + 1].startsWith("-")) {
|
|
166
|
+
return args[idx + 1];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (arg.startsWith("--package=")) {
|
|
170
|
+
return arg.slice("--package=".length);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let afterDoubleDash = false;
|
|
174
|
+
for (let idx = 0; idx < args.length; idx++) {
|
|
175
|
+
const arg = args[idx];
|
|
176
|
+
if (arg === "--") {
|
|
177
|
+
afterDoubleDash = true;
|
|
178
|
+
if (args[idx + 1] && !args[idx + 1].startsWith("-")) {
|
|
179
|
+
return args[idx + 1];
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!afterDoubleDash && !arg.startsWith("-")) {
|
|
184
|
+
return arg;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeExecutableName(binaryToken) {
|
|
191
|
+
const base = binaryToken.split(/[\\/]/u).pop() ?? binaryToken;
|
|
192
|
+
return base.toLowerCase().replace(/\.(?:cmd|bat|exe)$/u, "");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const NPM_INSTALL_COMMANDS = new Set([
|
|
196
|
+
"install", "add", "i", "in", "ins", "inst", "insta", "instal", "isnt", "isnta", "isntal", "isntall",
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
const NPM_CI_COMMANDS = new Set([
|
|
200
|
+
"ci", "clean-install", "ic", "install-clean", "isntall-clean",
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
const NPM_INSTALL_TEST_COMMANDS = new Set([
|
|
204
|
+
"install-test", "it",
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
const NPM_INSTALL_CI_TEST_COMMANDS = new Set([
|
|
208
|
+
"install-ci-test", "cit", "clean-install-test", "sit",
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
const NPM_EXEC_COMMANDS = new Set([
|
|
212
|
+
"exec", "x",
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
const NPM_INIT_COMMANDS = new Set([
|
|
216
|
+
"init", "create", "innit",
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
const NPM_SCRIPT_COMMANDS = new Set([
|
|
220
|
+
"test", "t", "tst", "start", "stop", "restart", "run", "run-script", "rum", "urn",
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
const NPM_KNOWN_NON_INSTALLING_COMMANDS = new Set([
|
|
224
|
+
"v", "view", "info", "show", "list", "ls", "outdated", "config", "help", "help-search", "doctor", "ping", "root", "prefix", "bin", "whoami",
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const NPM_KNOWN_BOOLEAN_OPTIONS = new Set([
|
|
228
|
+
"--silent", "--json", "--long", "--parseable", "--global", "-g", "--force", "--yes", "-y", "--no",
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const NPM_OPTIONS_WITH_VALUE = new Set([
|
|
232
|
+
"--workspace",
|
|
233
|
+
"-w",
|
|
234
|
+
"--loglevel",
|
|
235
|
+
"--prefix",
|
|
236
|
+
"-C",
|
|
237
|
+
"--userconfig",
|
|
238
|
+
"--registry",
|
|
239
|
+
"--cache",
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
export function parseNpmInvocationArgs(rest) {
|
|
243
|
+
if (!Array.isArray(rest)) {
|
|
244
|
+
return {
|
|
245
|
+
subcommand: null,
|
|
246
|
+
subcommandIndex: -1,
|
|
247
|
+
args: [],
|
|
248
|
+
leadingOptions: [],
|
|
249
|
+
workspace: null,
|
|
250
|
+
workspaces: false,
|
|
251
|
+
ambiguous: true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const leadingOptions = [];
|
|
256
|
+
let workspace = null;
|
|
257
|
+
let workspaces = false;
|
|
258
|
+
|
|
259
|
+
let i = 0;
|
|
260
|
+
while (i < rest.length) {
|
|
261
|
+
const arg = rest[i];
|
|
262
|
+
|
|
263
|
+
if (arg === "--") {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!arg.startsWith("-")) {
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (arg === "--workspaces" || arg === "--ws") {
|
|
272
|
+
workspaces = true;
|
|
273
|
+
leadingOptions.push(arg);
|
|
274
|
+
i += 1;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (arg.startsWith("--workspace=")) {
|
|
279
|
+
workspace = arg.slice("--workspace=".length) || null;
|
|
280
|
+
leadingOptions.push(arg);
|
|
281
|
+
i += 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (arg.startsWith("-w=")) {
|
|
286
|
+
workspace = arg.slice(3) || null;
|
|
287
|
+
leadingOptions.push(arg);
|
|
288
|
+
i += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (arg === "--workspace" || arg === "-w") {
|
|
293
|
+
leadingOptions.push(arg);
|
|
294
|
+
const value = rest[i + 1] ?? null;
|
|
295
|
+
if (value !== null && !value.startsWith("-")) {
|
|
296
|
+
workspace = value;
|
|
297
|
+
leadingOptions.push(value);
|
|
298
|
+
i += 2;
|
|
299
|
+
} else {
|
|
300
|
+
workspace = value;
|
|
301
|
+
i += 1;
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (NPM_OPTIONS_WITH_VALUE.has(arg)) {
|
|
307
|
+
leadingOptions.push(arg);
|
|
308
|
+
const value = rest[i + 1] ?? null;
|
|
309
|
+
if (value !== null && !value.startsWith("-")) {
|
|
310
|
+
leadingOptions.push(value);
|
|
311
|
+
i += 2;
|
|
312
|
+
} else {
|
|
313
|
+
i += 1;
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (/^--[^=]+=/.test(arg)) {
|
|
319
|
+
leadingOptions.push(arg);
|
|
320
|
+
i += 1;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (NPM_KNOWN_BOOLEAN_OPTIONS.has(arg)) {
|
|
325
|
+
leadingOptions.push(arg);
|
|
326
|
+
i += 1;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (/^--/.test(arg)) {
|
|
331
|
+
const next = rest[i + 1];
|
|
332
|
+
if (next && !next.startsWith("-")) {
|
|
333
|
+
return {
|
|
334
|
+
subcommand: null,
|
|
335
|
+
subcommandIndex: -1,
|
|
336
|
+
args: [],
|
|
337
|
+
leadingOptions,
|
|
338
|
+
workspace,
|
|
339
|
+
workspaces,
|
|
340
|
+
ambiguous: true,
|
|
341
|
+
reason: "NPM_OPTION_VALUE_AMBIGUOUS",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
leadingOptions.push(arg);
|
|
345
|
+
i += 1;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (arg.startsWith("-")) {
|
|
350
|
+
leadingOptions.push(arg);
|
|
351
|
+
i += 1;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const subcommand = rest[i] ? rest[i].toLowerCase() : null;
|
|
357
|
+
const trailingArgs = subcommand ? rest.slice(i + 1) : [];
|
|
358
|
+
|
|
359
|
+
for (let j = 0; j < trailingArgs.length; j++) {
|
|
360
|
+
const tArg = trailingArgs[j];
|
|
361
|
+
if (tArg === "--") break;
|
|
362
|
+
if (tArg === "--workspaces" || tArg === "--ws") {
|
|
363
|
+
workspaces = true;
|
|
364
|
+
} else if (tArg.startsWith("--workspace=")) {
|
|
365
|
+
workspace = tArg.slice("--workspace=".length) || null;
|
|
366
|
+
} else if (tArg.startsWith("-w=")) {
|
|
367
|
+
workspace = tArg.slice(3) || null;
|
|
368
|
+
} else if (tArg === "--workspace" || tArg === "-w") {
|
|
369
|
+
const val = trailingArgs[j + 1];
|
|
370
|
+
if (val && !val.startsWith("-")) {
|
|
371
|
+
workspace = val;
|
|
372
|
+
j += 1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const ambiguous = subcommand === null;
|
|
378
|
+
return {
|
|
379
|
+
subcommand,
|
|
380
|
+
subcommandIndex: subcommand ? i : -1,
|
|
381
|
+
args: trailingArgs,
|
|
382
|
+
leadingOptions,
|
|
383
|
+
workspace,
|
|
384
|
+
workspaces,
|
|
385
|
+
ambiguous,
|
|
386
|
+
...(ambiguous ? { reason: "NPM_SUBCOMMAND_AMBIGUOUS" } : {}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function parseNpmInvocation(argv) {
|
|
391
|
+
const tokens = unwrapCommandArgv(Array.isArray(argv) ? argv : tokenizeCommand(argv));
|
|
392
|
+
if (!tokens || tokens.length === 0) {
|
|
393
|
+
return {
|
|
394
|
+
subcommand: null,
|
|
395
|
+
subcommandIndex: -1,
|
|
396
|
+
args: [],
|
|
397
|
+
leadingOptions: [],
|
|
398
|
+
workspace: null,
|
|
399
|
+
workspaces: false,
|
|
400
|
+
ambiguous: true,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const binary = normalizeExecutableName(tokens[0]);
|
|
404
|
+
if (binary !== "npm") {
|
|
405
|
+
return {
|
|
406
|
+
subcommand: null,
|
|
407
|
+
subcommandIndex: -1,
|
|
408
|
+
args: [],
|
|
409
|
+
leadingOptions: [],
|
|
410
|
+
workspace: null,
|
|
411
|
+
workspaces: false,
|
|
412
|
+
ambiguous: true,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return parseNpmInvocationArgs(tokens.slice(1));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function npmWorkspaceSelection(npmInvocation) {
|
|
419
|
+
return {
|
|
420
|
+
scoped: Boolean(npmInvocation?.workspace || npmInvocation?.workspaces),
|
|
421
|
+
workspace: npmInvocation?.workspace ?? null,
|
|
422
|
+
allWorkspaces: npmInvocation?.workspaces === true,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function classifyNpmInvocation(npm) {
|
|
427
|
+
const sub = npm.subcommand;
|
|
428
|
+
|
|
429
|
+
if (npm.ambiguous) {
|
|
430
|
+
return {
|
|
431
|
+
resolutionMode: "UNKNOWN",
|
|
432
|
+
mayInstall: true,
|
|
433
|
+
installer: "npm",
|
|
434
|
+
tool: null,
|
|
435
|
+
reason: npm.reason || "NPM_SUBCOMMAND_AMBIGUOUS",
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (NPM_EXEC_COMMANDS.has(sub)) {
|
|
440
|
+
return {
|
|
441
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
442
|
+
mayInstall: true,
|
|
443
|
+
installer: `npm ${sub}`,
|
|
444
|
+
tool: extractNpmExecTool(npm.args),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (NPM_INSTALL_COMMANDS.has(sub) || NPM_CI_COMMANDS.has(sub) || NPM_INSTALL_TEST_COMMANDS.has(sub) || NPM_INSTALL_CI_TEST_COMMANDS.has(sub)) {
|
|
449
|
+
let tool = null;
|
|
450
|
+
let installer = `npm ${sub}`;
|
|
451
|
+
if (NPM_INSTALL_COMMANDS.has(sub)) {
|
|
452
|
+
tool = extractToolFromArgs(npm.args);
|
|
453
|
+
installer = "npm";
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
457
|
+
mayInstall: true,
|
|
458
|
+
installer,
|
|
459
|
+
tool,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (NPM_INIT_COMMANDS.has(sub)) {
|
|
464
|
+
let firstMeaningful = null;
|
|
465
|
+
for (let i = 0; i < npm.args.length; i++) {
|
|
466
|
+
const arg = npm.args[i];
|
|
467
|
+
if (arg === "--") {
|
|
468
|
+
const next = npm.args[i + 1];
|
|
469
|
+
firstMeaningful = next && !next.startsWith("-") ? next : null;
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
if (!arg.startsWith("-")) {
|
|
473
|
+
firstMeaningful = arg;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (firstMeaningful) {
|
|
478
|
+
return {
|
|
479
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
480
|
+
mayInstall: true,
|
|
481
|
+
installer: `npm ${sub}`,
|
|
482
|
+
tool: firstMeaningful,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
resolutionMode: "LOCAL_PACKAGE_BINARY",
|
|
487
|
+
mayInstall: false,
|
|
488
|
+
installer: null,
|
|
489
|
+
tool: null,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (sub === "update" || sub === "up" || sub === "upgrade") {
|
|
494
|
+
return {
|
|
495
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
496
|
+
mayInstall: true,
|
|
497
|
+
installer: `npm ${sub}`,
|
|
498
|
+
tool: extractToolFromArgs(npm.args),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (sub === "audit") {
|
|
503
|
+
if (npm.args.includes("fix")) {
|
|
504
|
+
return {
|
|
505
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
506
|
+
mayInstall: true,
|
|
507
|
+
installer: "npm audit fix",
|
|
508
|
+
tool: null,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
resolutionMode: "LOCAL_PACKAGE_BINARY",
|
|
513
|
+
mayInstall: false,
|
|
514
|
+
installer: null,
|
|
515
|
+
tool: null,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (NPM_SCRIPT_COMMANDS.has(sub)) {
|
|
520
|
+
return {
|
|
521
|
+
resolutionMode: "LOCAL_PACKAGE_BINARY",
|
|
522
|
+
mayInstall: false,
|
|
523
|
+
installer: null,
|
|
524
|
+
tool: null,
|
|
525
|
+
dispatch: {
|
|
526
|
+
kind: "npm-script-command",
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (NPM_KNOWN_NON_INSTALLING_COMMANDS.has(sub)) {
|
|
532
|
+
return {
|
|
533
|
+
resolutionMode: "LOCAL_PACKAGE_BINARY",
|
|
534
|
+
mayInstall: false,
|
|
535
|
+
installer: null,
|
|
536
|
+
tool: null,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
resolutionMode: "UNKNOWN",
|
|
542
|
+
mayInstall: true,
|
|
543
|
+
installer: "npm",
|
|
544
|
+
tool: null,
|
|
545
|
+
reason: "NPM_COMMAND_UNCLASSIFIED",
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function classifySingleCommand(commandInput) {
|
|
550
|
+
const tokens = Array.isArray(commandInput) ? [...commandInput] : tokenizeCommand(commandInput);
|
|
551
|
+
if (tokens.length === 0) {
|
|
552
|
+
return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let i = 0;
|
|
556
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) {
|
|
557
|
+
i++;
|
|
558
|
+
}
|
|
559
|
+
if (i >= tokens.length) {
|
|
560
|
+
return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const binaryToken = tokens[i];
|
|
564
|
+
const binary = normalizeExecutableName(binaryToken);
|
|
565
|
+
const rest = tokens.slice(i + 1);
|
|
566
|
+
|
|
567
|
+
if (binary === "call") {
|
|
568
|
+
return classifyCommandResolution(rest);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (binaryToken.includes("node_modules/.bin/") || binaryToken.startsWith("./node_modules/")) {
|
|
572
|
+
return { resolutionMode: "LOCAL_PACKAGE_BINARY", mayInstall: false, installer: null, tool: binary };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Check npx
|
|
576
|
+
if (binary === "npx") {
|
|
577
|
+
if (rest.some((arg) => arg === "--no-install" || arg === "--no")) {
|
|
578
|
+
const nonInstallArgs = rest.filter((arg) => arg !== "--no-install" && arg !== "--no");
|
|
579
|
+
return {
|
|
580
|
+
resolutionMode: "NON_INSTALLING_RESOLUTION",
|
|
581
|
+
mayInstall: false,
|
|
582
|
+
installer: "npx",
|
|
583
|
+
tool: extractToolFromArgs(nonInstallArgs),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
588
|
+
mayInstall: true,
|
|
589
|
+
installer: "npx",
|
|
590
|
+
tool: extractToolFromArgs(rest),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Check pnpx, bunx, uvx
|
|
595
|
+
if (binary === "pnpx" || binary === "bunx" || binary === "uvx") {
|
|
596
|
+
return {
|
|
597
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
598
|
+
mayInstall: true,
|
|
599
|
+
installer: binary,
|
|
600
|
+
tool: extractToolFromArgs(rest),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Check pnpm and yarn
|
|
605
|
+
if (binary === "pnpm" || binary === "yarn") {
|
|
606
|
+
if (rest[0] === "dlx") {
|
|
607
|
+
return {
|
|
608
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
609
|
+
mayInstall: true,
|
|
610
|
+
installer: `${binary} dlx`,
|
|
611
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
if (["add", "install", "i"].includes(rest[0])) {
|
|
615
|
+
return {
|
|
616
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
617
|
+
mayInstall: true,
|
|
618
|
+
installer: binary,
|
|
619
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
return { resolutionMode: "LOCAL_PACKAGE_BINARY", mayInstall: false, installer: null, tool: null };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Check bun
|
|
626
|
+
if (binary === "bun") {
|
|
627
|
+
if (rest[0] === "x") {
|
|
628
|
+
return {
|
|
629
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
630
|
+
mayInstall: true,
|
|
631
|
+
installer: "bun x",
|
|
632
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (["add", "install", "i"].includes(rest[0])) {
|
|
636
|
+
return {
|
|
637
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
638
|
+
mayInstall: true,
|
|
639
|
+
installer: "bun",
|
|
640
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
return { resolutionMode: "LOCAL_PACKAGE_BINARY", mayInstall: false, installer: null, tool: null };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Check uv
|
|
647
|
+
if (binary === "uv") {
|
|
648
|
+
if (rest[0] === "tool" && rest[1] === "run") {
|
|
649
|
+
return {
|
|
650
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
651
|
+
mayInstall: true,
|
|
652
|
+
installer: "uv tool run",
|
|
653
|
+
tool: extractToolFromArgs(rest.slice(2)),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
if (rest[0] === "pip" && rest[1] === "install") {
|
|
657
|
+
return {
|
|
658
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
659
|
+
mayInstall: true,
|
|
660
|
+
installer: "uv pip install",
|
|
661
|
+
tool: extractToolFromArgs(rest.slice(2)),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
if (rest[0] === "add") {
|
|
665
|
+
return {
|
|
666
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
667
|
+
mayInstall: true,
|
|
668
|
+
installer: "uv add",
|
|
669
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
return { resolutionMode: "LOCAL_EXECUTABLE", mayInstall: false, installer: null, tool: null };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Check pipx
|
|
676
|
+
if (binary === "pipx") {
|
|
677
|
+
if (rest[0] === "run") {
|
|
678
|
+
return {
|
|
679
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
680
|
+
mayInstall: true,
|
|
681
|
+
installer: "pipx run",
|
|
682
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (rest[0] === "install") {
|
|
686
|
+
return {
|
|
687
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
688
|
+
mayInstall: true,
|
|
689
|
+
installer: "pipx install",
|
|
690
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return { resolutionMode: "LOCAL_EXECUTABLE", mayInstall: false, installer: null, tool: null };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Check npm
|
|
697
|
+
if (binary === "npm") {
|
|
698
|
+
const npm = parseNpmInvocationArgs(rest);
|
|
699
|
+
return classifyNpmInvocation(npm);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Check python/pip
|
|
703
|
+
if (binary === "pip" || binary === "pip3") {
|
|
704
|
+
if (rest[0] === "install") {
|
|
705
|
+
return {
|
|
706
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
707
|
+
mayInstall: true,
|
|
708
|
+
installer: binary,
|
|
709
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (binary === "python" || binary === "python3") {
|
|
714
|
+
if (rest[0] === "-m" && rest[1] === "pip" && rest[2] === "install") {
|
|
715
|
+
return {
|
|
716
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
717
|
+
mayInstall: true,
|
|
718
|
+
installer: `${binary} -m pip install`,
|
|
719
|
+
tool: extractToolFromArgs(rest.slice(3)),
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Check cargo
|
|
725
|
+
if (binary === "cargo") {
|
|
726
|
+
if (rest[0] === "install" || rest[0] === "binstall") {
|
|
727
|
+
return {
|
|
728
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
729
|
+
mayInstall: true,
|
|
730
|
+
installer: binary,
|
|
731
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// System package managers
|
|
737
|
+
if (["brew", "apt", "apt-get", "apk", "dnf", "pacman"].includes(binary)) {
|
|
738
|
+
if (["install", "add", "-S"].includes(rest[0])) {
|
|
739
|
+
return {
|
|
740
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
741
|
+
mayInstall: true,
|
|
742
|
+
installer: binary,
|
|
743
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return { resolutionMode: "LOCAL_EXECUTABLE", mayInstall: false, installer: null, tool: null };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export function classifyCommandResolution(commandInput) {
|
|
752
|
+
if (Array.isArray(commandInput)) {
|
|
753
|
+
if (commandInput.length === 0 || commandInput.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
754
|
+
return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
|
|
755
|
+
}
|
|
756
|
+
const binary = normalizeExecutableName(commandInput[0]);
|
|
757
|
+
if (["sh", "bash", "zsh", "dash", "ksh"].includes(binary)) {
|
|
758
|
+
const shellFlagIndex = commandInput.findIndex((item, index) => index > 0 && /^-.*c/.test(item));
|
|
759
|
+
const shellCommand = shellFlagIndex >= 0 ? commandInput[shellFlagIndex + 1] : null;
|
|
760
|
+
if (shellCommand) return classifyCommandResolution(shellCommand);
|
|
761
|
+
}
|
|
762
|
+
if (binary === "cmd") {
|
|
763
|
+
const shellFlagIndex = commandInput.findIndex((item, index) => index > 0 && /^\/c$/iu.test(item));
|
|
764
|
+
const shellCommand = shellFlagIndex >= 0 ? commandInput.slice(shellFlagIndex + 1).join(" ") : null;
|
|
765
|
+
if (shellCommand) return classifyCommandResolution(shellCommand);
|
|
766
|
+
}
|
|
767
|
+
return classifySingleCommand(commandInput);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (typeof commandInput !== "string" || commandInput.trim() === "") {
|
|
771
|
+
return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const subcommands = splitCommandPipeline(commandInput);
|
|
775
|
+
for (const subcommand of subcommands) {
|
|
776
|
+
const res = classifySingleCommand(subcommand);
|
|
777
|
+
if (res.mayInstall) {
|
|
778
|
+
return res;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return classifySingleCommand(subcommands[0] || commandInput);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function unwrapCommandArgv(argv) {
|
|
786
|
+
if (!Array.isArray(argv) || argv.length === 0) return null;
|
|
787
|
+
let i = 0;
|
|
788
|
+
while (i < argv.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(argv[i])) {
|
|
789
|
+
i++;
|
|
790
|
+
}
|
|
791
|
+
if (i >= argv.length) return null;
|
|
792
|
+
const binary = normalizeExecutableName(argv[i]);
|
|
793
|
+
if (["sh", "bash", "zsh", "dash", "ksh"].includes(binary)) {
|
|
794
|
+
const shellFlagIndex = argv.findIndex((item, index) => index > i && /^-.*c/.test(item));
|
|
795
|
+
const shellCommand = shellFlagIndex >= 0 ? argv[shellFlagIndex + 1] : null;
|
|
796
|
+
if (shellCommand) return tokenizeCommand(shellCommand);
|
|
797
|
+
}
|
|
798
|
+
if (binary === "cmd") {
|
|
799
|
+
const shellFlagIndex = argv.findIndex((item, index) => index > i && /^\/c$/iu.test(item));
|
|
800
|
+
const shellCommand = shellFlagIndex >= 0 ? argv.slice(shellFlagIndex + 1).join(" ") : null;
|
|
801
|
+
if (shellCommand) return tokenizeCommand(shellCommand);
|
|
802
|
+
}
|
|
803
|
+
if (binary === "call") {
|
|
804
|
+
return argv.slice(i + 1);
|
|
805
|
+
}
|
|
806
|
+
return argv.slice(i);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export function getNpmScriptName(argv) {
|
|
810
|
+
const npm = parseNpmInvocation(argv);
|
|
811
|
+
if (npm.ambiguous || !npm.subcommand) {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const sub = npm.subcommand;
|
|
816
|
+
const args = npm.args;
|
|
817
|
+
|
|
818
|
+
if (["test", "t", "tst"].includes(sub)) return "test";
|
|
819
|
+
if (["start", "stop", "restart"].includes(sub)) return sub;
|
|
820
|
+
if (["run", "run-script", "rum", "urn"].includes(sub)) {
|
|
821
|
+
let afterDoubleDash = false;
|
|
822
|
+
for (let idx = 0; idx < args.length; idx++) {
|
|
823
|
+
const arg = args[idx];
|
|
824
|
+
if (arg === "--") {
|
|
825
|
+
afterDoubleDash = true;
|
|
826
|
+
if (args[idx + 1] && !args[idx + 1].startsWith("-")) {
|
|
827
|
+
return args[idx + 1];
|
|
828
|
+
}
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (!afterDoubleDash && !arg.startsWith("-")) {
|
|
832
|
+
if (idx > 0 && (args[idx - 1] === "-w" || args[idx - 1] === "--workspace" || NPM_OPTIONS_WITH_VALUE.has(args[idx - 1]))) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
return arg;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function getNpmLifecycleCandidates({ scriptName, scripts = {} } = {}) {
|
|
843
|
+
if (scriptName === "restart") {
|
|
844
|
+
if (typeof scripts?.restart === "string" && scripts.restart.trim() !== "") {
|
|
845
|
+
return ["prerestart", "restart", "postrestart"];
|
|
846
|
+
}
|
|
847
|
+
return [
|
|
848
|
+
"prerestart",
|
|
849
|
+
"prestop",
|
|
850
|
+
"stop",
|
|
851
|
+
"poststop",
|
|
852
|
+
"prestart",
|
|
853
|
+
"start",
|
|
854
|
+
"poststart",
|
|
855
|
+
"postrestart",
|
|
856
|
+
];
|
|
857
|
+
}
|
|
858
|
+
return [`pre${scriptName}`, scriptName, `post${scriptName}`];
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export const MAX_NPM_SCRIPT_DEPTH = 16;
|
|
862
|
+
|
|
863
|
+
async function resolveNpmScriptRisk({
|
|
864
|
+
scriptName,
|
|
865
|
+
packageJson,
|
|
866
|
+
visited = new Set(),
|
|
867
|
+
depth = 0,
|
|
868
|
+
} = {}) {
|
|
869
|
+
if (depth > MAX_NPM_SCRIPT_DEPTH) {
|
|
870
|
+
return {
|
|
871
|
+
resolutionMode: "UNKNOWN",
|
|
872
|
+
mayInstall: true,
|
|
873
|
+
installer: "npm-script",
|
|
874
|
+
tool: null,
|
|
875
|
+
reason: "MAX_SCRIPT_DEPTH",
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (visited.has(scriptName)) {
|
|
880
|
+
return {
|
|
881
|
+
resolutionMode: "UNKNOWN",
|
|
882
|
+
mayInstall: true,
|
|
883
|
+
installer: "npm-script",
|
|
884
|
+
tool: null,
|
|
885
|
+
reason: "SCRIPT_CYCLE",
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const nextVisited = new Set(visited);
|
|
890
|
+
nextVisited.add(scriptName);
|
|
891
|
+
|
|
892
|
+
const candidates = getNpmLifecycleCandidates({
|
|
893
|
+
scriptName,
|
|
894
|
+
scripts: packageJson?.scripts ?? {},
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
for (const candidate of candidates) {
|
|
898
|
+
const scriptBody = packageJson?.scripts?.[candidate];
|
|
899
|
+
if (typeof scriptBody !== "string" || scriptBody.trim() === "") {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const direct = classifyCommandResolution(scriptBody);
|
|
904
|
+
if (direct.mayInstall) {
|
|
905
|
+
return {
|
|
906
|
+
resolutionMode: direct.resolutionMode,
|
|
907
|
+
mayInstall: true,
|
|
908
|
+
installer: direct.installer ?? "npm-script",
|
|
909
|
+
tool: direct.tool,
|
|
910
|
+
dispatch: {
|
|
911
|
+
kind: "npm-script",
|
|
912
|
+
scriptName: candidate,
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const subcommands = splitCommandPipeline(scriptBody);
|
|
918
|
+
for (const subcommand of subcommands) {
|
|
919
|
+
const nestedScriptName = getNpmScriptName(subcommand);
|
|
920
|
+
if (nestedScriptName) {
|
|
921
|
+
const nested = await resolveNpmScriptRisk({
|
|
922
|
+
scriptName: nestedScriptName,
|
|
923
|
+
packageJson,
|
|
924
|
+
visited: nextVisited,
|
|
925
|
+
depth: depth + 1,
|
|
926
|
+
});
|
|
927
|
+
if (nested?.mayInstall) {
|
|
928
|
+
return nested;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async function readPackageJsonIfPresent(cwd) {
|
|
938
|
+
if (!cwd || typeof cwd !== "string") return null;
|
|
939
|
+
try {
|
|
940
|
+
const pkgPath = path.resolve(cwd, "package.json");
|
|
941
|
+
const raw = await fs.readFile(pkgPath, "utf8");
|
|
942
|
+
return JSON.parse(raw);
|
|
943
|
+
} catch {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
export async function resolveExecutionResolution({ argv, cwd } = {}) {
|
|
949
|
+
const direct = classifyCommandResolution(argv);
|
|
950
|
+
if (direct.mayInstall && direct.resolutionMode !== "UNKNOWN") return direct;
|
|
951
|
+
|
|
952
|
+
const npmInvocation = parseNpmInvocation(argv);
|
|
953
|
+
const scriptName = getNpmScriptName(argv);
|
|
954
|
+
|
|
955
|
+
if (scriptName && (npmInvocation.workspace || npmInvocation.workspaces)) {
|
|
956
|
+
return {
|
|
957
|
+
resolutionMode: "UNKNOWN",
|
|
958
|
+
mayInstall: true,
|
|
959
|
+
installer: "npm-workspace",
|
|
960
|
+
tool: null,
|
|
961
|
+
reason: "NPM_WORKSPACE_SCRIPT_UNRESOLVED",
|
|
962
|
+
dispatch: {
|
|
963
|
+
kind: "npm-workspace-script",
|
|
964
|
+
scriptName,
|
|
965
|
+
},
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (direct.mayInstall) return direct;
|
|
970
|
+
if (!scriptName) return direct;
|
|
971
|
+
|
|
972
|
+
const packageJson = await readPackageJsonIfPresent(cwd);
|
|
973
|
+
if (!packageJson || typeof packageJson.scripts !== "object" || packageJson.scripts === null) {
|
|
974
|
+
return direct;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const nestedRisk = await resolveNpmScriptRisk({
|
|
978
|
+
scriptName,
|
|
979
|
+
packageJson,
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
if (nestedRisk?.mayInstall) {
|
|
983
|
+
return nestedRisk;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return direct;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export function getInstallationAuthorityRef(check) {
|
|
990
|
+
return (
|
|
991
|
+
check?.details?.installationAuthorityRef
|
|
992
|
+
?? check?.details?.authorityRef
|
|
993
|
+
?? check?.installationAuthorityRef
|
|
994
|
+
?? check?.authorityRef
|
|
995
|
+
?? null
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function normalizeToolName(toolName) {
|
|
1000
|
+
if (typeof toolName !== "string") return "";
|
|
1001
|
+
if (toolName.startsWith("@")) {
|
|
1002
|
+
const parts = toolName.slice(1).split("@");
|
|
1003
|
+
return `@${parts[0]}`;
|
|
1004
|
+
}
|
|
1005
|
+
return toolName.split("@")[0];
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
export function validateAuthorityGrant({ authority, taskId, type = "SOFTWARE_INSTALLATION", tool } = {}) {
|
|
1009
|
+
if (!authority || typeof authority !== "object") {
|
|
1010
|
+
return {
|
|
1011
|
+
valid: false,
|
|
1012
|
+
error: {
|
|
1013
|
+
code: E_AUTHORITY_INVALID,
|
|
1014
|
+
message: "Authority grant artifact is missing or invalid",
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (authority.schemaVersion !== 1 || authority.protocolVersion !== 1) {
|
|
1020
|
+
return {
|
|
1021
|
+
valid: false,
|
|
1022
|
+
error: {
|
|
1023
|
+
code: E_AUTHORITY_INVALID,
|
|
1024
|
+
message: `Authority grant schema version (${authority.schemaVersion}) or protocol version (${authority.protocolVersion}) is invalid`,
|
|
1025
|
+
},
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (authority.type !== type) {
|
|
1030
|
+
return {
|
|
1031
|
+
valid: false,
|
|
1032
|
+
error: {
|
|
1033
|
+
code: E_AUTHORITY_INVALID,
|
|
1034
|
+
message: `Authority grant type '${authority.type}' does not match expected '${type}'`,
|
|
1035
|
+
},
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (authority.status !== "AUTHORIZED") {
|
|
1040
|
+
return {
|
|
1041
|
+
valid: false,
|
|
1042
|
+
error: {
|
|
1043
|
+
code: E_AUTHORITY_INVALID,
|
|
1044
|
+
message: `Authority grant is not active (status: '${authority.status}')`,
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (authority.source === "agent-self") {
|
|
1050
|
+
return {
|
|
1051
|
+
valid: false,
|
|
1052
|
+
error: {
|
|
1053
|
+
code: E_AUTHORITY_INVALID,
|
|
1054
|
+
message: "Self-asserted authority grants with source 'agent-self' are not permitted",
|
|
1055
|
+
},
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (!["operator", "host", "project-policy"].includes(authority.source)) {
|
|
1060
|
+
return {
|
|
1061
|
+
valid: false,
|
|
1062
|
+
error: {
|
|
1063
|
+
code: E_AUTHORITY_INVALID,
|
|
1064
|
+
message: `Authority grant source '${authority.source}' is not recognized`,
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (taskId && authority.taskId && authority.taskId !== taskId) {
|
|
1070
|
+
return {
|
|
1071
|
+
valid: false,
|
|
1072
|
+
error: {
|
|
1073
|
+
code: E_AUTHORITY_INVALID,
|
|
1074
|
+
message: `Authority grant taskId '${authority.taskId}' does not match current task '${taskId}'`,
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (tool && authority.scope?.tool && authority.scope.tool !== "*") {
|
|
1080
|
+
const requestedNorm = normalizeToolName(tool);
|
|
1081
|
+
const scopeNorm = normalizeToolName(authority.scope.tool);
|
|
1082
|
+
const exactMatch = authority.scope.tool === tool
|
|
1083
|
+
|| tool.startsWith(`${authority.scope.tool}@`)
|
|
1084
|
+
|| authority.scope.tool.startsWith(`${tool}@`)
|
|
1085
|
+
|| requestedNorm === scopeNorm;
|
|
1086
|
+
|
|
1087
|
+
if (!exactMatch) {
|
|
1088
|
+
return {
|
|
1089
|
+
valid: false,
|
|
1090
|
+
error: {
|
|
1091
|
+
code: E_AUTHORITY_SCOPE_MISMATCH,
|
|
1092
|
+
message: `Authority grant scope tool '${authority.scope.tool}' does not match requested verification tool '${tool}'`,
|
|
1093
|
+
},
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
return { valid: true, error: null };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export function validateVerificationAuthority(check, options = {}) {
|
|
1102
|
+
const canonicalResolution = check?.execution?.resolution ?? check?.details?.execution?.resolution;
|
|
1103
|
+
const command = check?.details?.command
|
|
1104
|
+
?? (check?.kind === "command" && typeof check?.source === "string" && !check.source.startsWith("check:")
|
|
1105
|
+
? check.source
|
|
1106
|
+
: null);
|
|
1107
|
+
|
|
1108
|
+
if (!canonicalResolution && (!command || typeof command !== "string")) {
|
|
1109
|
+
return { valid: true, error: null };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const classification = canonicalResolution ?? classifyCommandResolution(command);
|
|
1113
|
+
if (!classification.mayInstall) {
|
|
1114
|
+
return { valid: true, error: null };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const authorityRef = getInstallationAuthorityRef(check);
|
|
1118
|
+
if (!authorityRef) {
|
|
1119
|
+
return {
|
|
1120
|
+
valid: false,
|
|
1121
|
+
error: {
|
|
1122
|
+
code: E_INSTALLATION_AUTHORITY_REQUIRED,
|
|
1123
|
+
message: `Verification command '${command}' uses installation-capable resolution (${classification.resolutionMode}) without recorded installation authority reference`,
|
|
1124
|
+
},
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const resolved = resolveTrustedAuthority({
|
|
1129
|
+
authorityRef,
|
|
1130
|
+
target: options.target,
|
|
1131
|
+
trustedAuthorityFile: options.trustedAuthorityFile,
|
|
1132
|
+
trustedAuthorityDir: options.trustedAuthorityDir,
|
|
1133
|
+
authorities: options.authorities,
|
|
1134
|
+
authority: options.authority,
|
|
1135
|
+
authorityContext: options.authorityContext,
|
|
1136
|
+
runtimeContext: options.runtimeContext,
|
|
1137
|
+
});
|
|
1138
|
+
if (!resolved.trusted) {
|
|
1139
|
+
if (resolved.error?.code === E_AUTHORITY_INVALID && resolved.sourceConfigured === false) {
|
|
1140
|
+
return {
|
|
1141
|
+
valid: false,
|
|
1142
|
+
error: {
|
|
1143
|
+
code: E_INSTALLATION_AUTHORITY_REQUIRED,
|
|
1144
|
+
message: "Installation-capable verification requires a host-attested authority context",
|
|
1145
|
+
},
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
return { valid: false, error: resolved.error };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return validateAuthorityGrant({
|
|
1152
|
+
authority: resolved.authority,
|
|
1153
|
+
taskId: options.taskId,
|
|
1154
|
+
type: "SOFTWARE_INSTALLATION",
|
|
1155
|
+
tool: classification.tool,
|
|
1156
|
+
});
|
|
1157
|
+
}
|