@microck/canonfig 2.0.0

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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/agent/agent-resolution.errors.js +42 -0
  4. package/dist/agent/agent-resolution.layer.js +204 -0
  5. package/dist/agent/agent-resolution.service.js +2259 -0
  6. package/dist/agent/agent-resolution.types.js +1 -0
  7. package/dist/agent/controlled-executor.js +704 -0
  8. package/dist/agent/harness-adapters.js +85 -0
  9. package/dist/cli/cli.js +618 -0
  10. package/dist/cli/exit-codes.js +28 -0
  11. package/dist/cli/follower-commands.js +3 -0
  12. package/dist/cli/render.js +56 -0
  13. package/dist/cli/source-commands.js +5 -0
  14. package/dist/domain/brand.js +29 -0
  15. package/dist/domain/identity.js +31 -0
  16. package/dist/domain/npm-package-spec.js +186 -0
  17. package/dist/domain/profile.js +950 -0
  18. package/dist/domain/recipe-versions.js +297 -0
  19. package/dist/domain/resource.js +259 -0
  20. package/dist/domain/synchronization.js +346 -0
  21. package/dist/enrollment/enrollment.errors.js +43 -0
  22. package/dist/enrollment/enrollment.layer.js +724 -0
  23. package/dist/enrollment/enrollment.service.js +3 -0
  24. package/dist/enrollment/enrollment.types.js +59 -0
  25. package/dist/enrollment/follower-client.js +585 -0
  26. package/dist/enrollment/source-server.js +313 -0
  27. package/dist/machine/linux.layer.js +1183 -0
  28. package/dist/machine/machine-state.errors.js +52 -0
  29. package/dist/machine/machine-state.service.js +3 -0
  30. package/dist/machine/machine-state.types.js +1 -0
  31. package/dist/machine/macos.layer.js +470 -0
  32. package/dist/machine/windows.layer.js +879 -0
  33. package/dist/profile/discovery.js +740 -0
  34. package/dist/profile/profile-catalog.errors.js +50 -0
  35. package/dist/profile/profile-catalog.layer.js +20 -0
  36. package/dist/profile/profile-catalog.service.js +7 -0
  37. package/dist/profile/profile-codec.js +153 -0
  38. package/dist/profile/publication.js +298 -0
  39. package/dist/profile/tool-catalog.js +384 -0
  40. package/dist/runtime/doctor.js +306 -0
  41. package/dist/runtime/layers.js +706 -0
  42. package/dist/runtime/main.js +38 -0
  43. package/dist/schedule/linux-schedule.js +24 -0
  44. package/dist/schedule/macos-schedule.js +25 -0
  45. package/dist/schedule/schedule-manager.errors.js +17 -0
  46. package/dist/schedule/schedule-manager.layer.js +205 -0
  47. package/dist/schedule/schedule-manager.service.js +3 -0
  48. package/dist/schedule/schedule-manager.types.js +114 -0
  49. package/dist/schedule/windows-schedule.js +25 -0
  50. package/dist/state/state-repository.errors.js +55 -0
  51. package/dist/state/state-repository.layer.js +1507 -0
  52. package/dist/state/state-repository.service.js +3 -0
  53. package/dist/state/state-repository.types.js +1 -0
  54. package/dist/state/state-schema.js +298 -0
  55. package/dist/synchronization/config-codec.js +97 -0
  56. package/dist/synchronization/executor.js +700 -0
  57. package/dist/synchronization/follower-orchestration.js +939 -0
  58. package/dist/synchronization/follower-sync-config.js +81 -0
  59. package/dist/synchronization/npm-artifact.js +670 -0
  60. package/dist/synchronization/planner.js +378 -0
  61. package/dist/synchronization/recovery.js +397 -0
  62. package/dist/synchronization/resource-executors.js +1198 -0
  63. package/dist/synchronization/resource-plans.js +645 -0
  64. package/dist/synchronization/synchronization.errors.js +102 -0
  65. package/dist/synchronization/synchronization.layer.js +97 -0
  66. package/dist/synchronization/synchronization.service.js +11 -0
  67. package/dist/synchronization/synchronization.types.js +1 -0
  68. package/package.json +66 -0
@@ -0,0 +1,2259 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants, } from "node:fs";
3
+ import { access, lstat, open, realpath, } from "node:fs/promises";
4
+ import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolve, win32, } from "node:path";
5
+ import { Context, Effect, Schema } from "effect";
6
+ import { parseNpmPackageSpecification, } from "../domain/npm-package-spec.js";
7
+ import { AgentProcessError, AgentVerificationError, DeniedAgentCapabilityError, InvalidAgentResponseError, InvalidAgentTaskError, } from "./agent-resolution.errors.js";
8
+ import { controlledEnvironment, redactText, } from "./controlled-executor.js";
9
+ export class AgentResolution extends Context.Service()("canonfig/agent/AgentResolution") {
10
+ }
11
+ const ProcessActionSchema = Schema.Struct({
12
+ kind: Schema.Literal("process"),
13
+ executable: Schema.NonEmptyString,
14
+ arguments: Schema.Array(Schema.String),
15
+ workingDirectory: Schema.optional(Schema.NonEmptyString),
16
+ paths: Schema.Array(Schema.NonEmptyString),
17
+ origins: Schema.Array(Schema.NonEmptyString),
18
+ capabilities: Schema.Array(Schema.Literals([
19
+ "elevation",
20
+ "login",
21
+ "restart",
22
+ "reboot",
23
+ ])),
24
+ });
25
+ const AgentActionProposalSchema = Schema.Struct({
26
+ summary: Schema.NonEmptyString,
27
+ actions: Schema.Array(ProcessActionSchema),
28
+ });
29
+ export const decodeAgentProposal = (text) => Effect.try({
30
+ try: () => JSON.parse(text),
31
+ catch: (cause) => new InvalidAgentResponseError({
32
+ message: `harness response is not JSON: ${String(cause)}`,
33
+ }),
34
+ }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(AgentActionProposalSchema)), Effect.mapError((cause) => cause instanceof InvalidAgentResponseError
35
+ ? cause
36
+ : new InvalidAgentResponseError({ message: String(cause) })));
37
+ const safePositiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
38
+ export const validateAgentTask = (task) => {
39
+ if (!safePositiveInteger(task.timeLimitSeconds)) {
40
+ return Effect.fail(new InvalidAgentTaskError({
41
+ task: task.id,
42
+ message: "timeLimitSeconds must be a positive safe integer",
43
+ }));
44
+ }
45
+ if (!safePositiveInteger(task.outputLimitBytes)) {
46
+ return Effect.fail(new InvalidAgentTaskError({
47
+ task: task.id,
48
+ message: "outputLimitBytes must be a positive safe integer",
49
+ }));
50
+ }
51
+ if (task.allowedExecutables.length === 0) {
52
+ return Effect.fail(new InvalidAgentTaskError({
53
+ task: task.id,
54
+ message: "at least one executable must be allowlisted",
55
+ }));
56
+ }
57
+ const unclassifiable = task.executableAuthorizations?.find((authorization) => isNestedCommandLauncher(authorization.executable));
58
+ if (unclassifiable !== undefined) {
59
+ return Effect.fail(new InvalidAgentTaskError({
60
+ task: task.id,
61
+ message: `${unclassifiable.executable} launches nested commands that cannot be bounded by an execution model`,
62
+ }));
63
+ }
64
+ if (task.verification.command.length === 0) {
65
+ return Effect.fail(new InvalidAgentTaskError({
66
+ task: task.id,
67
+ message: "verification command must not be empty",
68
+ }));
69
+ }
70
+ return Effect.void;
71
+ };
72
+ export const redactAgentTask = (task, secrets) => ({
73
+ ...task,
74
+ summary: redactText(task.summary, secrets),
75
+ desiredOutcome: redactText(task.desiredOutcome, secrets),
76
+ observedEvidence: task.observedEvidence.map((value) => redactText(value, secrets)),
77
+ allowedPaths: task.allowedPaths.map((value) => redactText(value, secrets)),
78
+ allowedExecutables: task.allowedExecutables.map((value) => redactText(value, secrets)),
79
+ executableAuthorizations: task.executableAuthorizations?.map((authorization) => ({
80
+ ...authorization,
81
+ executable: redactText(authorization.executable, secrets),
82
+ })),
83
+ allowedOrigins: task.allowedOrigins.map((value) => redactText(value, secrets)),
84
+ verification: {
85
+ command: task.verification.command.map((value) => redactText(value, secrets)),
86
+ expectContains: task.verification.expectContains === undefined
87
+ ? undefined
88
+ : redactText(task.verification.expectContains, secrets),
89
+ },
90
+ });
91
+ const hasPathSeparator = (value) => value.includes("/") || value.includes("\\");
92
+ const environmentValue = (environment, name) => {
93
+ if (process.platform !== "win32")
94
+ return environment[name];
95
+ const key = Object.keys(environment).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
96
+ return key === undefined ? undefined : environment[key];
97
+ };
98
+ const executableCandidates = (value, environmentEntries, workingDirectory) => {
99
+ if (isAbsolute(value) || win32.isAbsolute(value) || hasPathSeparator(value)) {
100
+ return [resolve(workingDirectory, value)];
101
+ }
102
+ const environment = controlledEnvironment(environmentEntries);
103
+ const extensions = process.platform === "win32"
104
+ ? (environmentValue(environment, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";")
105
+ : [""];
106
+ const path = environmentValue(environment, "PATH") ?? "";
107
+ return path.split(delimiter)
108
+ .filter((directory) => directory.length > 0
109
+ && (isAbsolute(directory) || win32.isAbsolute(directory)))
110
+ .flatMap((directory) => extensions.map((extension) => resolve(directory, `${value}${extension}`)));
111
+ };
112
+ export const resolvedExecutableIdentity = async (value, environment = [], workingDirectory = process.cwd()) => {
113
+ for (const candidate of executableCandidates(value, environment, workingDirectory)) {
114
+ try {
115
+ await access(candidate, constants.X_OK);
116
+ return await realpath(candidate);
117
+ }
118
+ catch {
119
+ // Continue through PATH candidates. A missing or non-executable path is
120
+ // never treated as equivalent to an allowlisted executable.
121
+ }
122
+ }
123
+ return undefined;
124
+ };
125
+ export const executableAllowed = (executable, allowed, environment = [], workingDirectory = process.cwd()) => Effect.promise(async () => {
126
+ const executableIdentity = await resolvedExecutableIdentity(executable, environment, workingDirectory);
127
+ if (executableIdentity === undefined)
128
+ return false;
129
+ for (const entry of allowed) {
130
+ const allowedIdentity = await resolvedExecutableIdentity(entry, environment, workingDirectory);
131
+ if (allowedIdentity === executableIdentity)
132
+ return true;
133
+ }
134
+ return false;
135
+ });
136
+ const portableBasename = (value) => value.includes("\\") ? win32.basename(value) : basename(value);
137
+ const isWindowsPath = (value) => /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("\\\\");
138
+ const pathWithin = (path, root) => {
139
+ const windows = isWindowsPath(path) || isWindowsPath(root);
140
+ const difference = windows
141
+ ? win32.relative(win32.resolve(root), win32.resolve(path))
142
+ : relative(resolve(root), resolve(path));
143
+ return difference === "" || (!difference.startsWith("..")
144
+ && !(windows ? win32.isAbsolute(difference) : isAbsolute(difference)));
145
+ };
146
+ const pathApi = (value, base) => isWindowsPath(value) || isWindowsPath(base) || base.includes("\\")
147
+ ? win32
148
+ : posix;
149
+ const lexicalPath = (value, base) => {
150
+ const api = pathApi(value, base);
151
+ return api.resolve(base, value);
152
+ };
153
+ const canonicalPath = async (value, base) => {
154
+ const absolute = lexicalPath(value, base);
155
+ if (isWindowsPath(absolute) && process.platform !== "win32") {
156
+ return win32.normalize(absolute).toLowerCase();
157
+ }
158
+ const suffix = [];
159
+ let candidate = absolute;
160
+ while (true) {
161
+ try {
162
+ const canonical = await realpath(candidate);
163
+ return suffix.reduceRight((parent, part) => join(parent, part), canonical);
164
+ }
165
+ catch {
166
+ const parent = dirname(candidate);
167
+ if (parent === candidate)
168
+ return absolute;
169
+ suffix.push(basename(candidate));
170
+ candidate = parent;
171
+ }
172
+ }
173
+ };
174
+ const optionValue = (argument) => {
175
+ if (!argument.startsWith("-"))
176
+ return undefined;
177
+ const separator = argument.indexOf("=");
178
+ return separator > 0 ? argument.slice(separator + 1) : undefined;
179
+ };
180
+ const argumentPath = (argument, npmPackageArgument = false) => {
181
+ const value = optionValue(argument) ?? argument;
182
+ if (value.length === 0 || value.startsWith("-"))
183
+ return undefined;
184
+ if (normalizeOrigin(value) !== undefined)
185
+ return undefined;
186
+ if (npmPackageArgument) {
187
+ const specification = parseNpmPackageSpecification(value);
188
+ if (specification.kind === "remote" || specification.kind === "ambiguous") {
189
+ return undefined;
190
+ }
191
+ if (specification.kind === "local")
192
+ return specification.path;
193
+ return undefined;
194
+ }
195
+ const optionName = argument.includes("=")
196
+ ? argument.slice(0, argument.indexOf("=")).replace(/^-+/u, "")
197
+ : undefined;
198
+ const pathOption = optionName !== undefined
199
+ && /(?:^|[-_])(?:output|path|file|dir|directory|destination|dest|target|prefix|root|cwd|config|cache|store|write)(?:$|[-_])/iu
200
+ .test(optionName);
201
+ return pathOption
202
+ || posix.isAbsolute(value)
203
+ || win32.isAbsolute(value)
204
+ || value.startsWith(".")
205
+ || value.startsWith("~")
206
+ || value.includes("/")
207
+ || value.includes("\\")
208
+ ? value
209
+ : undefined;
210
+ };
211
+ const argumentOrigins = (argument, npmPackageArgument = false) => {
212
+ const origins = [...argument.matchAll(/https?:\/\/[^\s"'<>]+/giu)].map((match) => match[0]);
213
+ if (!npmPackageArgument)
214
+ return origins;
215
+ if (argument.startsWith("-") && !argument.includes("="))
216
+ return origins;
217
+ const value = optionValue(argument) ?? argument;
218
+ const specification = parseNpmPackageSpecification(value);
219
+ if (specification.kind === "remote")
220
+ return [...origins, specification.origin];
221
+ if (specification.kind === "ambiguous" && /(?:\/|:|@|\+)/u.test(value)) {
222
+ return [...origins, value];
223
+ }
224
+ return origins;
225
+ };
226
+ const ensureAllowedPath = (path, workingDirectory, taskRoots, harnessRoots) => Effect.promise(async () => {
227
+ const canonical = await canonicalPath(path, workingDirectory);
228
+ const taskBounds = await Promise.all(taskRoots.map((root) => canonicalPath(root, workingDirectory)));
229
+ const harnessBounds = await Promise.all(harnessRoots.map((root) => canonicalPath(root, workingDirectory)));
230
+ return taskBounds.some((root) => pathWithin(canonical, root))
231
+ && harnessBounds.some((root) => pathWithin(canonical, root));
232
+ }).pipe(Effect.flatMap((allowed) => allowed
233
+ ? Effect.void
234
+ : Effect.fail(new DeniedAgentCapabilityError({
235
+ capability: "path",
236
+ value: lexicalPath(path, workingDirectory),
237
+ }))));
238
+ const hasUnsafeUrlCharacter = (value) => [...value].some((character) => {
239
+ const code = character.codePointAt(0) ?? 0;
240
+ return code <= 0x20
241
+ || (code >= 0x7f && code <= 0x9f)
242
+ || "\"'<>\\ ".includes(character);
243
+ });
244
+ const hasExplicitUrlCredentialOrFragment = (value) => {
245
+ if (value.includes("#"))
246
+ return true;
247
+ const authority = /^https?:\/\/([^/?#]*)/iu.exec(value)?.[1];
248
+ return authority?.includes("@") ?? false;
249
+ };
250
+ const normalizeOrigin = (value) => {
251
+ if (value.trim() !== value
252
+ || hasUnsafeUrlCharacter(value)
253
+ || hasExplicitUrlCredentialOrFragment(value))
254
+ return undefined;
255
+ try {
256
+ const url = new URL(value);
257
+ if ((url.protocol !== "http:" && url.protocol !== "https:")
258
+ || url.username.length > 0
259
+ || url.password.length > 0
260
+ || url.hash.length > 0)
261
+ return undefined;
262
+ return url.origin;
263
+ }
264
+ catch {
265
+ return undefined;
266
+ }
267
+ };
268
+ const ensureAllowedOrigin = (value, taskOrigins, harnessOrigins) => {
269
+ const origin = normalizeOrigin(value);
270
+ const taskAllowed = taskOrigins
271
+ .map(normalizeOrigin)
272
+ .filter((candidate) => candidate !== undefined);
273
+ const harnessAllowed = harnessOrigins
274
+ .map(normalizeOrigin)
275
+ .filter((candidate) => candidate !== undefined);
276
+ return origin !== undefined
277
+ && taskAllowed.includes(origin)
278
+ && harnessAllowed.includes(origin)
279
+ ? Effect.void
280
+ : Effect.fail(new DeniedAgentCapabilityError({
281
+ capability: "network-origin",
282
+ value,
283
+ }));
284
+ };
285
+ const elevationExecutables = new Set(["sudo", "doas", "pkexec", "runas"]);
286
+ const loginExecutables = new Set(["login", "logon", "su"]);
287
+ const rebootExecutables = new Set(["halt", "poweroff", "reboot", "shutdown"]);
288
+ const restartExecutables = new Set(["restart", "restart-service"]);
289
+ const rebootCommandExecutables = new Set([
290
+ "init",
291
+ "launchctl",
292
+ "rc-service",
293
+ "service",
294
+ "systemctl",
295
+ "telinit",
296
+ ]);
297
+ const systemctlPowerStateVerbs = new Set([
298
+ "emergency",
299
+ "halt",
300
+ "kexec",
301
+ "poweroff",
302
+ "reboot",
303
+ "rescue",
304
+ "soft-reboot",
305
+ ]);
306
+ const systemctlPowerStateTargets = new Set([
307
+ "ctrl-alt-del.target",
308
+ "emergency.target",
309
+ "halt.target",
310
+ "kexec.target",
311
+ "poweroff.target",
312
+ "reboot.target",
313
+ "rescue.target",
314
+ "soft-reboot.target",
315
+ // systemd's SysV runlevel aliases for the power-state targets above.
316
+ "runlevel0.target",
317
+ "runlevel1.target",
318
+ "runlevel6.target",
319
+ "shutdown.target",
320
+ "sigpwr.target",
321
+ ]);
322
+ const systemctlUnitOperations = new Set([
323
+ "isolate",
324
+ "reload-or-restart",
325
+ "restart",
326
+ "start",
327
+ "try-reload-or-restart",
328
+ "try-restart",
329
+ ]);
330
+ const systemctlEnableOperations = new Set(["enable", "preset", "reenable"]);
331
+ const commandWrappers = new Set([
332
+ "cmd",
333
+ "doas",
334
+ "pkexec",
335
+ "runas",
336
+ "sudo",
337
+ ]);
338
+ const posixShellExecutables = new Set([
339
+ "ash",
340
+ "bash",
341
+ "dash",
342
+ "fish",
343
+ "ksh",
344
+ "nu",
345
+ "sh",
346
+ "zsh",
347
+ ]);
348
+ const interpreterKind = (value) => {
349
+ const command = portableBasename(value)
350
+ .toLowerCase()
351
+ .replace(/\.(?:cmd|exe)$/u, "");
352
+ if (command === "node" || command === "nodejs")
353
+ return "node";
354
+ if (/^(?:python|pypy)(?:\d+(?:\.\d+)*)?$/u.test(command) || command === "py") {
355
+ return "python";
356
+ }
357
+ if (posixShellExecutables.has(command)) {
358
+ return "posix-shell";
359
+ }
360
+ if (command === "powershell" || command === "pwsh")
361
+ return "powershell";
362
+ return undefined;
363
+ };
364
+ /**
365
+ * Executables that run a nested command not derivable from their argv: the
366
+ * descendant is selected by an argument, read from a Makefile or project
367
+ * manifest, or embedded in program text. Neither a leaf nor a bounded
368
+ * script-file classification can bound such a descendant, so these are denied
369
+ * before any allowlist comparison and no configuration can authorize them.
370
+ * This closes the nested-command bypass reported through `xargs`, `find`,
371
+ * `awk`, `perl`, `make`, `npx`, and related wrappers and launchers.
372
+ *
373
+ * Package-manager operations are classified separately below. A registry
374
+ * origin does not bound lifecycle scripts, project configuration, plugins, or
375
+ * installers, so a package manager is a leaf only for structurally recognized
376
+ * non-executing operations or when its canonical script-disable option is
377
+ * present. Runner forms that select a command to execute (`npx`, `uvx`,
378
+ * `make`, `go run`, ...) do not qualify.
379
+ * Recognized script-file interpreters (`interpreterKind`) keep the bounded
380
+ * script-file model. Every other executable still requires an explicit
381
+ * matching classification from both the task and the harness.
382
+ */
383
+ const nestedCommandLaunchers = new Set([
384
+ // argument dispatch: a later argument names the command to run
385
+ "at",
386
+ "batch",
387
+ "cmd",
388
+ "command",
389
+ "env",
390
+ "exec",
391
+ "flock",
392
+ "ltrace",
393
+ "nice",
394
+ "nohup",
395
+ "open",
396
+ "osascript",
397
+ "parallel",
398
+ "perf",
399
+ "screen",
400
+ "script",
401
+ "ssh",
402
+ "stdbuf",
403
+ "strace",
404
+ "systemd-run",
405
+ "time",
406
+ "timeout",
407
+ "tmux",
408
+ "valgrind",
409
+ "watch",
410
+ "wmic",
411
+ "xargs",
412
+ // runners: an argument selects a package, target, file, or task to execute
413
+ "bazel",
414
+ "bazelisk",
415
+ "buck",
416
+ "bunx",
417
+ "bundle",
418
+ "cargo",
419
+ "compose",
420
+ "deno",
421
+ "docker",
422
+ "docker-compose",
423
+ "dotnet",
424
+ "go",
425
+ "gmake",
426
+ "gradle",
427
+ "helm",
428
+ "java",
429
+ "javaw",
430
+ "jshell",
431
+ "just",
432
+ "kubectl",
433
+ "mage",
434
+ "make",
435
+ "mix",
436
+ "mvn",
437
+ "nerdctl",
438
+ "ninja",
439
+ "npx",
440
+ "pipx",
441
+ "pnpx",
442
+ "podman",
443
+ "qjs",
444
+ "rake",
445
+ "task",
446
+ "tsx",
447
+ "uvx",
448
+ // program-text interpreters outside the bounded script-file model
449
+ "awk",
450
+ "ccl",
451
+ "cscript",
452
+ "clisp",
453
+ "erl",
454
+ "escript",
455
+ "expect",
456
+ "gawk",
457
+ "groovy",
458
+ "guile",
459
+ "julia",
460
+ "lua",
461
+ "luajit",
462
+ "mawk",
463
+ "mshta",
464
+ "perl",
465
+ "php",
466
+ "racket",
467
+ "regsvr32",
468
+ "rscript",
469
+ "ruby",
470
+ "rundll32",
471
+ "sbcl",
472
+ "swipl",
473
+ "tclsh",
474
+ "wish",
475
+ "wscript",
476
+ // tools with exec predicates, hooks, filters, or command escapes
477
+ "fd",
478
+ "find",
479
+ "git",
480
+ "hg",
481
+ "sqlite3",
482
+ "svn",
483
+ "tar",
484
+ // elevation and session wrappers; capability derivation gates these too
485
+ "doas",
486
+ "login",
487
+ "logon",
488
+ "pkexec",
489
+ "runas",
490
+ "su",
491
+ "sudo",
492
+ ]);
493
+ /** True when the executable runs nested commands that argv cannot bound. */
494
+ export const isNestedCommandLauncher = (value) => nestedCommandLaunchers.has(portableBasename(value).toLowerCase().replace(/\.(?:cmd|exe|bat|com|ps1)$/u, ""));
495
+ const packageManagerName = (value) => {
496
+ const name = portableBasename(value)
497
+ .toLowerCase()
498
+ .replace(/\.(?:cmd|exe|bat|com|ps1)$/u, "")
499
+ .replace(/^(npm)-cli\.js$/u, "$1")
500
+ .replace(/^(pnpm)\.(?:cjs|js)$/u, "$1")
501
+ .replace(/^(yarn)\.js$/u, "$1");
502
+ return /^pip(?:3(?:\.\d+(?:\.\d+)*)?|-3(?:\.\d+(?:\.\d+)*)?)?$/u.test(name)
503
+ ? "pip"
504
+ : name;
505
+ };
506
+ const argumentsBeforeSeparator = (arguments_) => {
507
+ const separator = arguments_.indexOf("--");
508
+ return separator === -1 ? arguments_ : undefined;
509
+ };
510
+ const firstCommandIndex = (arguments_, optionsWithValues) => {
511
+ for (let index = 0; index < arguments_.length; index += 1) {
512
+ const argument = arguments_[index];
513
+ if (!argument.startsWith("-") || argument === "-")
514
+ return index;
515
+ const option = argument.split("=", 1)[0].toLowerCase();
516
+ if (!argument.includes("=")
517
+ && (optionsWithValues.has(option)
518
+ || /^--@[^:]+:registry$/iu.test(option))) {
519
+ index += 1;
520
+ if (index >= arguments_.length)
521
+ return undefined;
522
+ }
523
+ }
524
+ return undefined;
525
+ };
526
+ const hasEnabledOption = (arguments_, option) => arguments_.some((argument) => argument.split("=", 1)[0].toLowerCase() === option.toLowerCase()
527
+ && (!argument.includes("=")
528
+ || argument.slice(argument.indexOf("=") + 1).toLowerCase() === "true"));
529
+ const hasDisabledOption = (arguments_, option) => arguments_.some((argument) => {
530
+ const separator = argument.indexOf("=");
531
+ return separator > 0
532
+ && argument.slice(0, separator).toLowerCase() === option.toLowerCase()
533
+ && argument.slice(separator + 1).toLowerCase() !== "true";
534
+ });
535
+ const hasSeparateOptionValue = (arguments_, option) => arguments_.some((argument, index) => argument.toLowerCase() === option.toLowerCase()
536
+ && arguments_[index + 1] !== undefined);
537
+ const hasOnlyBinaryAll = (arguments_) => arguments_.some((argument, index) => {
538
+ const separator = argument.indexOf("=");
539
+ const name = (separator > 0 ? argument.slice(0, separator) : argument).toLowerCase();
540
+ if (name !== "--only-binary")
541
+ return false;
542
+ const value = separator > 0 ? argument.slice(separator + 1) : arguments_[index + 1];
543
+ return value?.toLowerCase() === ":all:";
544
+ });
545
+ const hasNonCanonicalBinaryOption = (arguments_) => arguments_.some((argument, index) => {
546
+ const separator = argument.indexOf("=");
547
+ const name = (separator > 0 ? argument.slice(0, separator) : argument).toLowerCase();
548
+ if (name === "--no-binary")
549
+ return true;
550
+ if (name !== "--only-binary")
551
+ return false;
552
+ const value = separator > 0 ? argument.slice(separator + 1) : arguments_[index + 1];
553
+ return value?.toLowerCase() !== ":all:";
554
+ });
555
+ const isUvFindLinksOption = (argument) => {
556
+ const lower = argument.toLowerCase();
557
+ return lower === "--find-links"
558
+ || lower.startsWith("--find-links=")
559
+ || (lower.startsWith("-")
560
+ && !lower.startsWith("--")
561
+ && lower.slice(1).includes("f"));
562
+ };
563
+ const uvRequirementFileOptions = new Set([
564
+ "--build-constraints",
565
+ "--constraint",
566
+ "--constraints",
567
+ "--excludes",
568
+ "--overrides",
569
+ "--requirement",
570
+ "--requirements",
571
+ "--with-requirements",
572
+ ]);
573
+ const uvCommandOptionsWithValues = new Set([
574
+ "--cache-dir",
575
+ "--color",
576
+ "--config-file",
577
+ "--default-index",
578
+ "--directory",
579
+ "--extra-index-url",
580
+ "--find-links",
581
+ "--index",
582
+ "--index-url",
583
+ "--project",
584
+ "-f",
585
+ ]);
586
+ const uvInstallCommand = (arguments_) => {
587
+ const commandIndex = firstCommandIndex(arguments_, uvCommandOptionsWithValues);
588
+ const command = commandIndex === undefined
589
+ ? undefined
590
+ : arguments_[commandIndex]?.toLowerCase();
591
+ return command === "pip" || command === "tool" ? command : undefined;
592
+ };
593
+ const hasUvRequirementFileShortOption = (argument, command) => {
594
+ if (!argument.startsWith("-") || argument.startsWith("--"))
595
+ return false;
596
+ const optionsWithoutValues = new Set([
597
+ "h",
598
+ "n",
599
+ "q",
600
+ "U",
601
+ "v",
602
+ ...(command === "tool" ? ["e"] : []),
603
+ ]);
604
+ const requirementOptions = new Set([
605
+ "b",
606
+ "c",
607
+ ...(command === "pip" ? ["r"] : []),
608
+ ]);
609
+ for (const option of argument.slice(1)) {
610
+ if (requirementOptions.has(option))
611
+ return true;
612
+ if (!optionsWithoutValues.has(option))
613
+ return false;
614
+ }
615
+ return false;
616
+ };
617
+ const isUvRequirementFileOption = (argument, command) => {
618
+ const name = argument.split("=", 1)[0].toLowerCase();
619
+ return uvRequirementFileOptions.has(name)
620
+ || hasUvRequirementFileShortOption(argument, command);
621
+ };
622
+ const hasUvRequirementFileOption = (arguments_) => {
623
+ const command = uvInstallCommand(arguments_);
624
+ return arguments_.some((argument) => isUvRequirementFileOption(argument, command));
625
+ };
626
+ const uvInsecureHostOptions = new Set([
627
+ "--allow-insecure-host",
628
+ "--trusted-host",
629
+ ]);
630
+ const isUvInsecureHostOption = (argument) => uvInsecureHostOptions.has(argument.split("=", 1)[0].toLowerCase());
631
+ const registryOptions = (manager, arguments_) => {
632
+ const options = new Set(manager === "uv"
633
+ ? [
634
+ "--default-index",
635
+ "--index-url",
636
+ "--extra-index-url",
637
+ "-f",
638
+ "--find-links",
639
+ "--index",
640
+ ]
641
+ : manager === "pip"
642
+ ? [
643
+ "-i",
644
+ "--index-url",
645
+ "--extra-index-url",
646
+ "-f",
647
+ "--find-links",
648
+ ]
649
+ : ["--registry"]);
650
+ const result = [];
651
+ for (let index = 0; index < arguments_.length; index += 1) {
652
+ const argument = arguments_[index];
653
+ const separator = argument.indexOf("=");
654
+ const name = (separator > 0 ? argument.slice(0, separator) : argument)
655
+ .toLowerCase();
656
+ const scoped = manager !== "uv"
657
+ && /^--@[^:]+:registry$/u.test(name);
658
+ if (!options.has(name) && !scoped)
659
+ continue;
660
+ const inline = separator > 0 ? argument.slice(separator + 1) : undefined;
661
+ const separate = inline === undefined && arguments_[index + 1] !== undefined;
662
+ result.push({
663
+ index,
664
+ value: inline ?? (separate ? arguments_[index + 1] : undefined),
665
+ consumesNext: separate,
666
+ });
667
+ if (separate)
668
+ index += 1;
669
+ }
670
+ return result;
671
+ };
672
+ const untrustedPackageConfigOption = (manager, argument) => {
673
+ const name = argument.split("=", 1)[0].toLowerCase();
674
+ if (manager === "npm" || manager === "pnpm") {
675
+ return new Set([
676
+ "--config",
677
+ "--config-dir",
678
+ "--globalconfig",
679
+ "--global-config",
680
+ "--userconfig",
681
+ "--user-config",
682
+ ]).has(name);
683
+ }
684
+ if (manager === "bun")
685
+ return name === "--config";
686
+ if (manager === "uv")
687
+ return name === "--config-file";
688
+ if (manager === "pip") {
689
+ return new Set([
690
+ "--cert",
691
+ "--client-cert",
692
+ "--config-settings",
693
+ "--config-setting",
694
+ "--proxy",
695
+ "--trusted-host",
696
+ ]).has(name);
697
+ }
698
+ if (manager === "yarn") {
699
+ return name === "--use-yarnrc" || name === "--rc-file";
700
+ }
701
+ return false;
702
+ };
703
+ const registryOperation = (manager, arguments_) => {
704
+ const unambiguous = argumentsBeforeSeparator(arguments_);
705
+ if (unambiguous === undefined)
706
+ return false;
707
+ const commandOptions = manager === "uv"
708
+ ? uvCommandOptionsWithValues
709
+ : manager === "pip"
710
+ ? new Set([
711
+ "--cache-dir",
712
+ "--cert",
713
+ "--client-cert",
714
+ "--config-settings",
715
+ "--config-setting",
716
+ "--constraint",
717
+ "-c",
718
+ "--extra-index-url",
719
+ "--find-links",
720
+ "-f",
721
+ "-i",
722
+ "--index-url",
723
+ "--isolated",
724
+ "--no-binary",
725
+ "--only-binary",
726
+ "--proxy",
727
+ "--requirement",
728
+ "-r",
729
+ "--trusted-host",
730
+ ])
731
+ : manager === "bun"
732
+ ? new Set(["--config", "--cwd", "--filter", "--registry"])
733
+ : new Set([
734
+ "-C",
735
+ "--cache",
736
+ "--config-dir",
737
+ "--dir",
738
+ "--global-bin-dir",
739
+ "--global-dir",
740
+ "--prefix",
741
+ "--registry",
742
+ "--store-dir",
743
+ "--userconfig",
744
+ "--virtual-store-dir",
745
+ "--workspace-dir",
746
+ ]);
747
+ const commandIndex = firstCommandIndex(unambiguous, commandOptions);
748
+ const command = commandIndex === undefined
749
+ ? undefined
750
+ : unambiguous[commandIndex]?.toLowerCase();
751
+ if (manager === "npm" || manager === "pnpm" || manager === "yarn") {
752
+ return command !== undefined && new Set([
753
+ "add",
754
+ "i",
755
+ "in",
756
+ "ins",
757
+ "inst",
758
+ "insta",
759
+ "instal",
760
+ "install",
761
+ "ci",
762
+ "info",
763
+ "list",
764
+ "ls",
765
+ "outdated",
766
+ "prefix",
767
+ "root",
768
+ "search",
769
+ "view",
770
+ "why",
771
+ ]).has(command);
772
+ }
773
+ if (manager === "bun") {
774
+ return command !== undefined && new Set(["add", "i", "install", "update"]).has(command);
775
+ }
776
+ if (manager === "pip") {
777
+ return command !== undefined && command === "install";
778
+ }
779
+ if (manager === "uv") {
780
+ return command === "tool" && unambiguous[commandIndex + 1]?.toLowerCase() === "install"
781
+ || command === "pip" && unambiguous[commandIndex + 1]?.toLowerCase() === "install";
782
+ }
783
+ return false;
784
+ };
785
+ const packageManagerOptionValues = (manager) => manager === "uv"
786
+ ? uvCommandOptionsWithValues
787
+ : manager === "pip"
788
+ ? new Set([
789
+ "--cache-dir",
790
+ "--cert",
791
+ "--client-cert",
792
+ "--config-setting",
793
+ "--config-settings",
794
+ "--constraint",
795
+ "-c",
796
+ "--extra-index-url",
797
+ "--find-links",
798
+ "--index-url",
799
+ "--proxy",
800
+ "--requirement",
801
+ "-r",
802
+ "--trusted-host",
803
+ "-f",
804
+ "-i",
805
+ ])
806
+ : new Set();
807
+ const nonOptionPackageManagerArguments = (manager, arguments_) => {
808
+ const values = packageManagerOptionValues(manager);
809
+ const result = [];
810
+ for (let index = 0; index < arguments_.length; index += 1) {
811
+ const argument = arguments_[index];
812
+ if (!argument.startsWith("-")) {
813
+ result.push(argument);
814
+ continue;
815
+ }
816
+ const name = argument.split("=", 1)[0].toLowerCase();
817
+ if (!argument.includes("=") && values.has(name))
818
+ index += 1;
819
+ }
820
+ return result;
821
+ };
822
+ const packageScopes = (value) => [...new Set([...value.matchAll(/@[A-Za-z0-9._~-]+\//gu)]
823
+ .map((match) => match[0].slice(0, -1)))].sort();
824
+ export const registryScopesForInvocation = (executable, arguments_) => {
825
+ const manager = packageManagerName(executable);
826
+ if (manager !== "npm" && manager !== "pnpm")
827
+ return [];
828
+ const indexes = npmDependencyArgumentIndexes(manager, arguments_);
829
+ return [...new Set([...indexes]
830
+ .flatMap((index) => packageScopes(arguments_[index] ?? "")))].sort();
831
+ };
832
+ const canonicalRegistryOrigin = (value) => {
833
+ return canonicalRegistryUrl(value)?.origin;
834
+ };
835
+ const canonicalRegistryUrl = (value) => {
836
+ if (value.trim() !== value
837
+ || hasUnsafeUrlCharacter(value)
838
+ || hasExplicitUrlCredentialOrFragment(value))
839
+ return undefined;
840
+ try {
841
+ const url = new URL(value);
842
+ if (url.protocol !== "https:"
843
+ || url.username.length > 0
844
+ || url.password.length > 0
845
+ || url.hash.length > 0
846
+ || url.hostname.length === 0)
847
+ return undefined;
848
+ const urlValue = url.pathname === "/" && url.search.length === 0
849
+ ? url.origin
850
+ : url.href;
851
+ return {
852
+ url: urlValue,
853
+ origin: url.origin,
854
+ };
855
+ }
856
+ catch {
857
+ return undefined;
858
+ }
859
+ };
860
+ export const registryOriginForInvocation = (executable, arguments_) => {
861
+ const manager = packageManagerName(executable);
862
+ const operation = registryOperation(manager, arguments_);
863
+ if (!operation)
864
+ return undefined;
865
+ const option = registryOptions(manager, arguments_)[0];
866
+ return option?.value === undefined
867
+ ? undefined
868
+ : canonicalRegistryUrl(option.value)?.url;
869
+ };
870
+ const safeRegistryValue = (value) => {
871
+ try {
872
+ const url = new URL(value);
873
+ return url.username.length > 0 || url.password.length > 0
874
+ ? "[REDACTED]"
875
+ : value;
876
+ }
877
+ catch {
878
+ return value.includes("@") ? "[REDACTED]" : value;
879
+ }
880
+ };
881
+ const canonicalAllowedRegistry = (taskOrigins, harnessOrigins, actionOrigins) => {
882
+ const task = taskOrigins
883
+ .map(canonicalRegistryUrl)
884
+ .filter((registry) => registry !== undefined);
885
+ const harness = harnessOrigins
886
+ .map(canonicalRegistryUrl)
887
+ .filter((registry) => registry !== undefined);
888
+ const taskOriginsSet = new Set(task
889
+ .map((registry) => registry.origin));
890
+ const harnessOriginsSet = new Set(harness
891
+ .map((registry) => registry.origin));
892
+ const sharedOrigins = [...taskOriginsSet].filter((origin) => harnessOriginsSet.has(origin));
893
+ const reviewed = actionOrigins
894
+ .map(canonicalRegistryUrl)
895
+ .filter((registry) => registry !== undefined)
896
+ .filter((registry) => sharedOrigins.includes(registry.origin));
897
+ const taskCandidates = task.filter((registry) => sharedOrigins.includes(registry.origin));
898
+ const harnessCandidates = harness.filter((registry) => sharedOrigins.includes(registry.origin));
899
+ const candidates = reviewed.length > 0
900
+ ? [...new Set(reviewed.map((registry) => registry.url))]
901
+ : taskCandidates.length > 0
902
+ ? [...new Set(taskCandidates.map((registry) => registry.url))]
903
+ : [...new Set(harnessCandidates.map((registry) => registry.url))];
904
+ return candidates.length === 1 ? candidates[0] : undefined;
905
+ };
906
+ const pipRequirementOption = (argument) => {
907
+ const separator = argument.indexOf("=");
908
+ const name = (separator > 0 ? argument.slice(0, separator) : argument).toLowerCase();
909
+ const inline = separator > 0 ? argument.slice(separator + 1) : undefined;
910
+ if (name === "-r" || name === "--requirement") {
911
+ return { kind: "include", value: inline };
912
+ }
913
+ if (name === "-c" || name === "--constraint") {
914
+ return { kind: "include", value: inline };
915
+ }
916
+ if (name === "-i" || name === "--index-url" || name === "--extra-index-url"
917
+ || name === "-f" || name === "--find-links") {
918
+ return { kind: "index", value: inline };
919
+ }
920
+ if (argument.length > 2 && (argument.startsWith("-r") || argument.startsWith("-c"))) {
921
+ return { kind: "include", value: argument.slice(2) };
922
+ }
923
+ if (argument.length > 2 && (argument.startsWith("-i") || argument.startsWith("-f"))) {
924
+ return { kind: "index", value: argument.slice(2) };
925
+ }
926
+ return undefined;
927
+ };
928
+ const pipRequirementOptionName = (argument) => argument.startsWith("-");
929
+ const classifyPipRequirementFileReference = (value) => {
930
+ if (value.length === 0
931
+ || value === "-"
932
+ || value.trim() !== value
933
+ || value.includes("\u0000")
934
+ || value.startsWith("-")
935
+ || value.startsWith("~")
936
+ || value.startsWith("//")
937
+ || value.startsWith("\\")
938
+ || value.includes("@")
939
+ || /%[0-9A-Fa-f]{2}/u.test(value))
940
+ return "remote";
941
+ // A drive-rooted Windows path is local syntax; every other scheme-like
942
+ // colon, including drive-relative paths, is remote/ambiguous syntax.
943
+ if (/^[A-Za-z]:[\\/](?![\\/])/u.test(value)) {
944
+ return value.slice(2).includes(":") ? "remote" : "local";
945
+ }
946
+ if (value.includes(":"))
947
+ return "remote";
948
+ return "local";
949
+ };
950
+ const pipRequirementTokens = (line) => {
951
+ const tokens = [];
952
+ let token = "";
953
+ let quote;
954
+ let escaped = false;
955
+ for (const character of line) {
956
+ if (escaped) {
957
+ token += character === "\\" || character === "'" || character === '"'
958
+ || /\s/u.test(character)
959
+ ? character
960
+ : `\\${character}`;
961
+ escaped = false;
962
+ continue;
963
+ }
964
+ if (character === "\\" && quote !== "'") {
965
+ escaped = true;
966
+ continue;
967
+ }
968
+ if (quote !== undefined) {
969
+ if (character === quote)
970
+ quote = undefined;
971
+ else
972
+ token += character;
973
+ continue;
974
+ }
975
+ if (character === "'" || character === '"') {
976
+ quote = character;
977
+ continue;
978
+ }
979
+ if (/\s/u.test(character)) {
980
+ if (token.length > 0) {
981
+ tokens.push(token);
982
+ token = "";
983
+ }
984
+ continue;
985
+ }
986
+ token += character;
987
+ }
988
+ if (escaped || quote !== undefined)
989
+ return undefined;
990
+ if (token.length > 0)
991
+ tokens.push(token);
992
+ return tokens;
993
+ };
994
+ const pipRequirementLogicalLines = (text) => {
995
+ if (text.includes("\u0000"))
996
+ return undefined;
997
+ const physical = text.replace(/\r\n?/gu, "\n").split("\n");
998
+ const logical = [];
999
+ let current = "";
1000
+ let quote;
1001
+ for (const line of physical) {
1002
+ let content = "";
1003
+ let escaped = false;
1004
+ for (const character of line) {
1005
+ if (escaped) {
1006
+ content += character;
1007
+ escaped = false;
1008
+ continue;
1009
+ }
1010
+ if (character === "\\" && quote !== "'") {
1011
+ escaped = true;
1012
+ content += character;
1013
+ continue;
1014
+ }
1015
+ if (quote !== undefined) {
1016
+ if (character === quote)
1017
+ quote = undefined;
1018
+ content += character;
1019
+ continue;
1020
+ }
1021
+ if (character === "'" || character === '"') {
1022
+ quote = character;
1023
+ content += character;
1024
+ continue;
1025
+ }
1026
+ if (character === "#"
1027
+ && (content.length === 0 || /\s/u.test(content.at(-1) ?? "")))
1028
+ break;
1029
+ content += character;
1030
+ }
1031
+ if (quote !== undefined) {
1032
+ if (!line.endsWith("\\"))
1033
+ return undefined;
1034
+ }
1035
+ const continuation = /(?<!\\)(?:\\\\)*\\$/u.test(content);
1036
+ if (continuation) {
1037
+ current += `${content.slice(0, -1)} `;
1038
+ continue;
1039
+ }
1040
+ current += content;
1041
+ if (current.trim().length > 0)
1042
+ logical.push(current.trim());
1043
+ current = "";
1044
+ quote = undefined;
1045
+ }
1046
+ return current.trim().length === 0 && quote === undefined ? logical : undefined;
1047
+ };
1048
+ const pipRequirementPackage = (line) => {
1049
+ if (line.length === 0
1050
+ || line.startsWith("-")
1051
+ || /[\\/@:]/u.test(line)
1052
+ || /(?:^|\s)https?:/iu.test(line)
1053
+ || /(?:^|\s)(?:git\+|git:\/\/|github:|gitlab:|bitbucket:|file:|link:|editable)/iu.test(line))
1054
+ return false;
1055
+ const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[([A-Za-z0-9._,-]+)\])?(.*)$/u.exec(line);
1056
+ if (match === null)
1057
+ return false;
1058
+ const remainder = match[3] ?? "";
1059
+ const markerSeparator = remainder.indexOf(";");
1060
+ const specifier = markerSeparator === -1
1061
+ ? remainder
1062
+ : remainder.slice(0, markerSeparator);
1063
+ const marker = markerSeparator === -1
1064
+ ? undefined
1065
+ : remainder.slice(markerSeparator + 1);
1066
+ if (!/^\s*(?:(?:===|==|~=|!=|<=|>=|<|>)\s*[A-Za-z0-9.*+!_-]+(?:\s*,\s*(?:===|==|~=|!=|<=|>=|<|>)\s*[A-Za-z0-9.*+!_-]+)*)?\s*$/u.test(specifier)) {
1067
+ return false;
1068
+ }
1069
+ if (marker !== undefined
1070
+ && !/^\s*[A-Za-z0-9_.-]+\s*(?:===|==|!=|<=|>=|<|>|in|not\s+in)\s*["'A-Za-z0-9_.!*+<>=(), -]+\s*$/u.test(marker)) {
1071
+ return false;
1072
+ }
1073
+ return !/--/u.test(remainder);
1074
+ };
1075
+ const pipRequirementSafeOption = (tokens) => {
1076
+ const name = tokens[0]?.toLowerCase();
1077
+ if (name === "--isolated")
1078
+ return tokens.length === 1;
1079
+ if (name !== "--only-binary" && !name.startsWith("--only-binary="))
1080
+ return false;
1081
+ const value = name === "--only-binary"
1082
+ ? tokens[1]?.toLowerCase()
1083
+ : name.slice("--only-binary=".length);
1084
+ return tokens.length === (name === "--only-binary" ? 2 : 1) && value === ":all:";
1085
+ };
1086
+ const requirementFileIdentityEqual = (left, right) => left.dev === right.dev && left.ino === right.ino && left.size === right.size;
1087
+ const readPipRequirementFile = async (path) => {
1088
+ const maximumBytes = 128 * 1024;
1089
+ const before = await lstat(path);
1090
+ if (!before.isFile() || before.size > maximumBytes) {
1091
+ throw new Error("requirement file is not a bounded regular file");
1092
+ }
1093
+ const handle = await open(path, "r");
1094
+ try {
1095
+ const opened = await handle.stat();
1096
+ if (!requirementFileIdentityEqual(before, opened)) {
1097
+ throw new Error("requirement file identity changed before reading");
1098
+ }
1099
+ const buffer = Buffer.alloc(maximumBytes + 1);
1100
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, 0);
1101
+ if (bytesRead > maximumBytes)
1102
+ throw new Error("requirement file exceeds size limit");
1103
+ const after = await lstat(path);
1104
+ if (!requirementFileIdentityEqual(before, after)) {
1105
+ throw new Error("requirement file identity changed while reading");
1106
+ }
1107
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytesRead));
1108
+ const content = decoded.startsWith("\uFEFF") ? decoded.slice(1) : decoded;
1109
+ return {
1110
+ content,
1111
+ authorization: {
1112
+ path,
1113
+ canonicalPath: await realpath(path),
1114
+ identity: {
1115
+ dev: before.dev,
1116
+ ino: before.ino,
1117
+ size: before.size,
1118
+ },
1119
+ digest: createHash("sha256").update(buffer.subarray(0, bytesRead)).digest("hex"),
1120
+ },
1121
+ };
1122
+ }
1123
+ finally {
1124
+ await handle.close();
1125
+ }
1126
+ };
1127
+ const validatePipRequirementInputs = (executable, arguments_, workingDirectory, task, harness) => Effect.gen(function* () {
1128
+ if (packageManagerName(executable) !== "pip") {
1129
+ return { arguments: arguments_, files: [] };
1130
+ }
1131
+ const normalizedArguments = [...arguments_];
1132
+ const fileOptions = [];
1133
+ for (let index = 0; index < arguments_.length; index += 1) {
1134
+ const optionIndex = index;
1135
+ const option = pipRequirementOption(arguments_[optionIndex]);
1136
+ if (option?.kind !== "include")
1137
+ continue;
1138
+ const value = option.value ?? arguments_[optionIndex + 1];
1139
+ if (option.value === undefined)
1140
+ index += 1;
1141
+ if (value === undefined
1142
+ || classifyPipRequirementFileReference(value) !== "local"
1143
+ || normalizeOrigin(value) !== undefined) {
1144
+ return yield* new DeniedAgentCapabilityError({
1145
+ capability: "package-manager-requirements",
1146
+ value: executable,
1147
+ });
1148
+ }
1149
+ const normalized = lexicalPath(value, workingDirectory);
1150
+ if (option.value !== undefined) {
1151
+ const separator = arguments_[optionIndex].indexOf("=");
1152
+ const prefix = separator > 0
1153
+ ? arguments_[optionIndex].slice(0, separator + 1)
1154
+ : arguments_[optionIndex].slice(0, 2);
1155
+ normalizedArguments[optionIndex] = `${prefix}${normalized}`;
1156
+ }
1157
+ else {
1158
+ normalizedArguments[optionIndex + 1] = normalized;
1159
+ }
1160
+ fileOptions.push({
1161
+ path: normalized,
1162
+ });
1163
+ }
1164
+ const visited = [];
1165
+ const authorizations = [];
1166
+ let fileCount = 0;
1167
+ let totalBytes = 0;
1168
+ const visit = (file, depth) => Effect.gen(function* () {
1169
+ if (depth > 8 || fileCount >= 64) {
1170
+ return yield* new DeniedAgentCapabilityError({
1171
+ capability: "package-manager-requirements",
1172
+ value: file,
1173
+ });
1174
+ }
1175
+ yield* ensureAllowedPath(file, workingDirectory, task.allowedPaths, harness.allowedPaths);
1176
+ const canonical = yield* Effect.promise(() => canonicalPath(file, workingDirectory));
1177
+ if (visited.includes(canonical)) {
1178
+ return yield* new DeniedAgentCapabilityError({
1179
+ capability: "package-manager-requirements",
1180
+ value: file,
1181
+ });
1182
+ }
1183
+ visited.push(canonical);
1184
+ fileCount += 1;
1185
+ const read = yield* Effect.tryPromise({
1186
+ try: () => readPipRequirementFile(file),
1187
+ catch: () => new DeniedAgentCapabilityError({
1188
+ capability: "package-manager-requirements",
1189
+ value: file,
1190
+ }),
1191
+ });
1192
+ const content = read.content;
1193
+ if (read.authorization.canonicalPath !== canonical) {
1194
+ return yield* new DeniedAgentCapabilityError({
1195
+ capability: "package-manager-requirements",
1196
+ value: file,
1197
+ });
1198
+ }
1199
+ authorizations.push(read.authorization);
1200
+ totalBytes += Buffer.byteLength(content, "utf8");
1201
+ if (totalBytes > 1024 * 1024) {
1202
+ return yield* new DeniedAgentCapabilityError({
1203
+ capability: "package-manager-requirements",
1204
+ value: file,
1205
+ });
1206
+ }
1207
+ const lines = pipRequirementLogicalLines(content);
1208
+ if (lines === undefined) {
1209
+ return yield* new DeniedAgentCapabilityError({
1210
+ capability: "package-manager-requirements",
1211
+ value: file,
1212
+ });
1213
+ }
1214
+ for (const line of lines) {
1215
+ const tokens = pipRequirementTokens(line);
1216
+ if (tokens === undefined || tokens.length === 0) {
1217
+ return yield* new DeniedAgentCapabilityError({
1218
+ capability: "package-manager-requirements",
1219
+ value: file,
1220
+ });
1221
+ }
1222
+ const option = pipRequirementOption(tokens[0]);
1223
+ if (option?.value !== undefined && tokens.length !== 1) {
1224
+ return yield* new DeniedAgentCapabilityError({
1225
+ capability: "package-manager-requirements",
1226
+ value: file,
1227
+ });
1228
+ }
1229
+ if (option?.kind === "include") {
1230
+ const include = option.value ?? (tokens.length === 2 ? tokens[1] : undefined);
1231
+ if (include === undefined || (option.value === undefined && tokens.length !== 2)) {
1232
+ return yield* new DeniedAgentCapabilityError({
1233
+ capability: "package-manager-requirements",
1234
+ value: file,
1235
+ });
1236
+ }
1237
+ if (classifyPipRequirementFileReference(include) !== "local") {
1238
+ return yield* new DeniedAgentCapabilityError({
1239
+ capability: "package-manager-requirements",
1240
+ value: file,
1241
+ });
1242
+ }
1243
+ const nested = lexicalPath(include, dirname(file));
1244
+ yield* visit(nested, depth + 1);
1245
+ continue;
1246
+ }
1247
+ if (option?.kind === "index") {
1248
+ const value = option.value ?? (tokens.length === 2 ? tokens[1] : undefined);
1249
+ if (value === undefined || (option.value === undefined && tokens.length !== 2)) {
1250
+ return yield* new DeniedAgentCapabilityError({
1251
+ capability: "network-origin",
1252
+ value: executable,
1253
+ });
1254
+ }
1255
+ if (canonicalRegistryUrl(value) === undefined) {
1256
+ return yield* new DeniedAgentCapabilityError({
1257
+ capability: "network-origin",
1258
+ value: safeRegistryValue(value),
1259
+ });
1260
+ }
1261
+ yield* ensureAllowedOrigin(value, task.allowedOrigins, harness.allowedOrigins);
1262
+ continue;
1263
+ }
1264
+ if (pipRequirementOptionName(tokens[0])
1265
+ ? !pipRequirementSafeOption(tokens)
1266
+ : !pipRequirementPackage(line)) {
1267
+ return yield* new DeniedAgentCapabilityError({
1268
+ capability: "package-manager-requirements",
1269
+ value: file,
1270
+ });
1271
+ }
1272
+ }
1273
+ visited.pop();
1274
+ });
1275
+ for (const entry of fileOptions)
1276
+ yield* visit(entry.path, 0);
1277
+ return { arguments: normalizedArguments, files: authorizations };
1278
+ });
1279
+ const samePipRequirementAuthorization = (left, right) => left.path === right.path
1280
+ && left.canonicalPath === right.canonicalPath
1281
+ && left.identity.dev === right.identity.dev
1282
+ && left.identity.ino === right.identity.ino
1283
+ && left.identity.size === right.identity.size
1284
+ && left.digest === right.digest;
1285
+ export const revalidatePipRequirementFiles = (executable, arguments_, workingDirectory, task, harness, expected) => validatePipRequirementInputs(executable, arguments_, workingDirectory, task, harness).pipe(Effect.flatMap((actual) => actual.files.length === expected.length
1286
+ && actual.files.every((entry, index) => samePipRequirementAuthorization(entry, expected[index]))
1287
+ ? Effect.void
1288
+ : Effect.fail(new DeniedAgentCapabilityError({
1289
+ capability: "package-manager-requirements",
1290
+ value: executable,
1291
+ }))));
1292
+ const registryArguments = (manager, arguments_, registry) => {
1293
+ const options = registryOptions(manager, arguments_);
1294
+ const removed = new Set();
1295
+ for (const option of options) {
1296
+ removed.add(option.index);
1297
+ if (option.consumesNext)
1298
+ removed.add(option.index + 1);
1299
+ }
1300
+ const withoutOptions = arguments_.filter((_argument, index) => !removed.has(index));
1301
+ if (manager === "uv") {
1302
+ const command = withoutOptions.findIndex((argument) => argument.toLowerCase() === "tool" || argument.toLowerCase() === "pip");
1303
+ const flag = command >= 0 && withoutOptions[command].toLowerCase() === "pip"
1304
+ ? "--index-url"
1305
+ : "--default-index";
1306
+ return [...withoutOptions, `${flag}=${registry}`];
1307
+ }
1308
+ if (manager === "pip") {
1309
+ return [...withoutOptions, `--index-url=${registry}`];
1310
+ }
1311
+ const scopes = manager === "npm" || manager === "pnpm"
1312
+ ? registryScopesForInvocation(manager, withoutOptions)
1313
+ : [];
1314
+ return [
1315
+ ...withoutOptions,
1316
+ `--registry=${registry}`,
1317
+ ...scopes.map((scope) => `--${scope}:registry=${registry}`),
1318
+ ];
1319
+ };
1320
+ const authorizeRegistryInvocation = (executable, arguments_, taskOrigins, harnessOrigins, actionOrigins = []) => {
1321
+ const manager = packageManagerName(executable);
1322
+ if (manager === "uv"
1323
+ && hasUvRequirementFileOption(arguments_)) {
1324
+ return Effect.fail(new DeniedAgentCapabilityError({
1325
+ capability: "package-manager-requirements",
1326
+ value: executable,
1327
+ }));
1328
+ }
1329
+ if (manager === "uv"
1330
+ && arguments_.some(isUvInsecureHostOption)) {
1331
+ return Effect.fail(new DeniedAgentCapabilityError({
1332
+ capability: "network-origin",
1333
+ value: executable,
1334
+ }));
1335
+ }
1336
+ if (!registryOperation(manager, arguments_))
1337
+ return Effect.succeed(arguments_);
1338
+ if (arguments_.some((argument) => untrustedPackageConfigOption(manager, argument))) {
1339
+ return Effect.fail(new DeniedAgentCapabilityError({
1340
+ capability: "package-manager-config",
1341
+ value: executable,
1342
+ }));
1343
+ }
1344
+ if (manager === "uv"
1345
+ && arguments_.some((argument) => isUvFindLinksOption(argument)
1346
+ || ["--extra-index-url", "--index"].includes(argument.split("=", 1)[0].toLowerCase()))) {
1347
+ return Effect.fail(new DeniedAgentCapabilityError({
1348
+ capability: "network-origin",
1349
+ value: executable,
1350
+ }));
1351
+ }
1352
+ const registry = canonicalAllowedRegistry(taskOrigins, harnessOrigins, actionOrigins);
1353
+ if (registry === undefined) {
1354
+ return Effect.fail(new DeniedAgentCapabilityError({
1355
+ capability: "network-origin",
1356
+ value: executable,
1357
+ }));
1358
+ }
1359
+ const options = registryOptions(manager, arguments_);
1360
+ const registryOrigin = canonicalRegistryOrigin(registry);
1361
+ for (const option of options) {
1362
+ if (option.value === undefined
1363
+ || registryOrigin === undefined
1364
+ || canonicalRegistryOrigin(option.value) !== registryOrigin) {
1365
+ return Effect.fail(new DeniedAgentCapabilityError({
1366
+ capability: "network-origin",
1367
+ value: option.value === undefined
1368
+ ? executable
1369
+ : safeRegistryValue(option.value),
1370
+ }));
1371
+ }
1372
+ }
1373
+ return Effect.succeed(registryArguments(manager, arguments_, registry));
1374
+ };
1375
+ const isUnboundedSourceDependency = (argument) => {
1376
+ const value = optionValue(argument) ?? argument;
1377
+ const specification = parseNpmPackageSpecification(value);
1378
+ return specification.kind === "ambiguous";
1379
+ };
1380
+ const isExplicitSourceDependency = (argument) => {
1381
+ const value = optionValue(argument) ?? argument;
1382
+ return /^(?:git\+|git:\/\/|github:|gitlab:|bitbucket:|git@|file:|link:|workspace:|https?:\/\/)/iu
1383
+ .test(value)
1384
+ || /(?:^|@)(?:npm:|git\+|git:\/\/|github:|gitlab:|bitbucket:|git@|file:|link:|workspace:|https?:\/\/)/iu
1385
+ .test(value)
1386
+ || /\s+@\s*(?:git\+|git:\/\/|github:|gitlab:|bitbucket:|git@|file:|https?:\/\/)/iu
1387
+ .test(value);
1388
+ };
1389
+ const isPipSourceDependency = (argument) => {
1390
+ const value = optionValue(argument) ?? argument;
1391
+ return isExplicitSourceDependency(value)
1392
+ || /^(?:[.~]{0,2}[\\/]|[A-Za-z]:[\\/]|[\\/])/u.test(value)
1393
+ || /^(?:-e|--editable)(?:=|$)/iu.test(argument);
1394
+ };
1395
+ const npmDependencyArgumentIndexes = (executable, arguments_) => {
1396
+ const manager = packageManagerName(executable);
1397
+ if (manager !== "npm" && manager !== "pnpm")
1398
+ return new Set();
1399
+ const commandIndex = firstCommandIndex(arguments_, new Set([
1400
+ "-C",
1401
+ "--cache",
1402
+ "--config-dir",
1403
+ "--dir",
1404
+ "--global-bin-dir",
1405
+ "--global-dir",
1406
+ "--prefix",
1407
+ "--registry",
1408
+ "--store-dir",
1409
+ "--userconfig",
1410
+ "--virtual-store-dir",
1411
+ "--workspace-dir",
1412
+ ]));
1413
+ if (commandIndex === undefined)
1414
+ return new Set();
1415
+ const command = arguments_[commandIndex]?.toLowerCase();
1416
+ if (command === undefined
1417
+ || !new Set([
1418
+ "add",
1419
+ "ci",
1420
+ "i",
1421
+ "in",
1422
+ "ins",
1423
+ "inst",
1424
+ "insta",
1425
+ "instal",
1426
+ "install",
1427
+ "info",
1428
+ "list",
1429
+ "ls",
1430
+ "outdated",
1431
+ "prefix",
1432
+ "root",
1433
+ "search",
1434
+ "view",
1435
+ "why",
1436
+ ]).has(command))
1437
+ return new Set();
1438
+ const optionValues = new Set([
1439
+ "-C",
1440
+ "--cache",
1441
+ "--config-dir",
1442
+ "--dir",
1443
+ "--global-bin-dir",
1444
+ "--global-dir",
1445
+ "--prefix",
1446
+ "--registry",
1447
+ "--store-dir",
1448
+ "--userconfig",
1449
+ "--virtual-store-dir",
1450
+ "--workspace-dir",
1451
+ ]);
1452
+ const indexes = new Set();
1453
+ for (let index = commandIndex + 1; index < arguments_.length; index += 1) {
1454
+ const argument = arguments_[index];
1455
+ if (argument.startsWith("-")) {
1456
+ if (!argument.includes("=")
1457
+ && (optionValues.has(argument)
1458
+ || /^--@[^:]+:registry$/iu.test(argument)))
1459
+ index += 1;
1460
+ continue;
1461
+ }
1462
+ indexes.add(index);
1463
+ }
1464
+ return indexes;
1465
+ };
1466
+ /**
1467
+ * Classify package-manager argv without consulting package/project metadata.
1468
+ * Unknown commands and ambiguous option layouts are denied: a package manager
1469
+ * may execute lifecycle hooks, project configuration, plugins, or downloaded
1470
+ * installers even though its top-level executable itself is allowlisted.
1471
+ */
1472
+ const packageManagerPolicy = (executable, arguments_) => {
1473
+ const resolvedName = packageManagerName(executable);
1474
+ const manager = /^(?:pip|pip3)(?:\d+(?:\.\d+)*)?$/u.test(resolvedName)
1475
+ ? "pip"
1476
+ : resolvedName;
1477
+ const knownManagers = new Set([
1478
+ "apt",
1479
+ "apt-get",
1480
+ "brew",
1481
+ "bun",
1482
+ "composer",
1483
+ "dnf",
1484
+ "gem",
1485
+ "npm",
1486
+ "pip",
1487
+ "pipenv",
1488
+ "pnpm",
1489
+ "poetry",
1490
+ "uv",
1491
+ "winget",
1492
+ "yarn",
1493
+ "yum",
1494
+ "zypper",
1495
+ ]);
1496
+ if (!knownManagers.has(manager))
1497
+ return undefined;
1498
+ const unambiguous = argumentsBeforeSeparator(arguments_);
1499
+ if (unambiguous === undefined)
1500
+ return { kind: "denied" };
1501
+ const dependencyIndexes = npmDependencyArgumentIndexes(manager, unambiguous);
1502
+ const sourceArguments = manager === "uv" || manager === "pip"
1503
+ ? nonOptionPackageManagerArguments(manager, unambiguous)
1504
+ : unambiguous;
1505
+ if (manager === "npm" || manager === "pnpm"
1506
+ ? [...dependencyIndexes].some((index) => isUnboundedSourceDependency(unambiguous[index] ?? ""))
1507
+ : manager === "pip"
1508
+ ? sourceArguments.some(isPipSourceDependency)
1509
+ : sourceArguments.some(isExplicitSourceDependency)) {
1510
+ return { kind: "denied" };
1511
+ }
1512
+ if (manager === "npm" || manager === "pnpm") {
1513
+ const commandIndex = firstCommandIndex(unambiguous, new Set([
1514
+ "-C",
1515
+ "--cache",
1516
+ "--config-dir",
1517
+ "--dir",
1518
+ "--global-bin-dir",
1519
+ "--global-dir",
1520
+ "--prefix",
1521
+ "--registry",
1522
+ "--store-dir",
1523
+ "--userconfig",
1524
+ "--virtual-store-dir",
1525
+ "--workspace-dir",
1526
+ ]));
1527
+ const command = commandIndex === undefined
1528
+ ? undefined
1529
+ : unambiguous[commandIndex]?.toLowerCase();
1530
+ const nonExecuting = new Set([
1531
+ "diff",
1532
+ "info",
1533
+ "list",
1534
+ "ls",
1535
+ "outdated",
1536
+ "ping",
1537
+ "prefix",
1538
+ "root",
1539
+ "search",
1540
+ "show",
1541
+ "view",
1542
+ "why",
1543
+ ]);
1544
+ if (command !== undefined && nonExecuting.has(command)) {
1545
+ if (manager === "npm")
1546
+ return { kind: "non-executing" };
1547
+ const canonical = "--ignore-pnpmfile";
1548
+ if (hasDisabledOption(unambiguous, canonical)
1549
+ || unambiguous.includes("--use-pnpmfile"))
1550
+ return { kind: "denied" };
1551
+ return {
1552
+ kind: "scripts-disabled",
1553
+ arguments: hasEnabledOption(unambiguous, canonical)
1554
+ ? arguments_
1555
+ : [...arguments_, canonical],
1556
+ };
1557
+ }
1558
+ if (command !== undefined
1559
+ && new Set([
1560
+ "add",
1561
+ "ci",
1562
+ "i",
1563
+ "in",
1564
+ "ins",
1565
+ "inst",
1566
+ "insta",
1567
+ "instal",
1568
+ "install",
1569
+ "isnt",
1570
+ "isnta",
1571
+ "isntal",
1572
+ "r",
1573
+ "remove",
1574
+ "rm",
1575
+ "un",
1576
+ "uninstall",
1577
+ "unlink",
1578
+ ])
1579
+ .has(command)) {
1580
+ const canonical = "--ignore-scripts";
1581
+ if (hasDisabledOption(unambiguous, canonical)
1582
+ || unambiguous.includes("--no-ignore-scripts")
1583
+ || hasSeparateOptionValue(unambiguous, canonical))
1584
+ return { kind: "denied" };
1585
+ if (manager === "pnpm") {
1586
+ const projectHookGate = "--ignore-pnpmfile";
1587
+ if (hasDisabledOption(unambiguous, projectHookGate)
1588
+ || unambiguous.includes("--use-pnpmfile"))
1589
+ return { kind: "denied" };
1590
+ return {
1591
+ kind: "scripts-disabled",
1592
+ arguments: [
1593
+ ...arguments_,
1594
+ ...(hasEnabledOption(unambiguous, canonical) ? [] : [canonical]),
1595
+ ...(hasEnabledOption(unambiguous, projectHookGate)
1596
+ ? []
1597
+ : [projectHookGate]),
1598
+ ],
1599
+ };
1600
+ }
1601
+ return {
1602
+ kind: "scripts-disabled",
1603
+ arguments: hasEnabledOption(unambiguous, canonical)
1604
+ ? arguments_
1605
+ : [...arguments_, canonical],
1606
+ };
1607
+ }
1608
+ return { kind: "denied" };
1609
+ }
1610
+ if (manager === "bun") {
1611
+ const commandIndex = firstCommandIndex(unambiguous, new Set(["--cwd", "--config", "--filter", "--registry"]));
1612
+ const command = commandIndex === undefined
1613
+ ? undefined
1614
+ : unambiguous[commandIndex]?.toLowerCase();
1615
+ if (command === "help") {
1616
+ return { kind: "non-executing" };
1617
+ }
1618
+ if (command !== undefined
1619
+ && new Set(["add", "install", "i", "remove", "rm", "update"]).has(command)) {
1620
+ const canonical = "--ignore-scripts";
1621
+ if (hasDisabledOption(unambiguous, canonical)
1622
+ || hasSeparateOptionValue(unambiguous, canonical))
1623
+ return { kind: "denied" };
1624
+ return {
1625
+ kind: "scripts-disabled",
1626
+ arguments: hasEnabledOption(unambiguous, canonical)
1627
+ ? arguments_
1628
+ : [...arguments_, canonical],
1629
+ };
1630
+ }
1631
+ return { kind: "denied" };
1632
+ }
1633
+ if (manager === "yarn") {
1634
+ // Yarn aliases and plugins can redefine commands, and Yarn has no stable
1635
+ // cross-version argv switch that independently disables all script paths.
1636
+ return { kind: "denied" };
1637
+ }
1638
+ if (manager === "pip") {
1639
+ const commandIndex = firstCommandIndex(unambiguous, new Set([
1640
+ "--cache-dir",
1641
+ "--config-settings",
1642
+ "--constraint",
1643
+ "--extra-index-url",
1644
+ "--find-links",
1645
+ "--index-url",
1646
+ "--isolated",
1647
+ "--proxy",
1648
+ "--requirement",
1649
+ "-r",
1650
+ "-c",
1651
+ "--trusted-host",
1652
+ "-f",
1653
+ "-i",
1654
+ ]));
1655
+ const command = commandIndex === undefined
1656
+ ? undefined
1657
+ : unambiguous[commandIndex]?.toLowerCase();
1658
+ if (command !== undefined
1659
+ && new Set(["check", "freeze", "index", "inspect", "list", "show"]).has(command)) {
1660
+ return { kind: "non-executing" };
1661
+ }
1662
+ if (command === "install") {
1663
+ if (hasNonCanonicalBinaryOption(unambiguous)) {
1664
+ return { kind: "denied" };
1665
+ }
1666
+ const canonical = "--only-binary=:all:";
1667
+ return {
1668
+ kind: "scripts-disabled",
1669
+ arguments: [
1670
+ ...(hasOnlyBinaryAll(unambiguous) ? arguments_ : [...arguments_, canonical]),
1671
+ ...(hasEnabledOption(unambiguous, "--isolated") ? [] : ["--isolated"]),
1672
+ ],
1673
+ };
1674
+ }
1675
+ return { kind: "denied" };
1676
+ }
1677
+ if (manager === "apt" || manager === "apt-get") {
1678
+ const commandIndex = firstCommandIndex(unambiguous, new Set(["-c", "-o", "--config-file", "--option"]));
1679
+ return commandIndex !== undefined
1680
+ && new Set(["check", "download", "indextargets"]).has(unambiguous[commandIndex].toLowerCase())
1681
+ ? { kind: "non-executing" }
1682
+ : { kind: "denied" };
1683
+ }
1684
+ if (manager === "winget") {
1685
+ const commandIndex = firstCommandIndex(unambiguous, new Set(["--accept-source-agreements", "--disable-interactivity"]));
1686
+ return commandIndex !== undefined
1687
+ && new Set(["download", "export", "list", "search", "show"]).has(unambiguous[commandIndex].toLowerCase())
1688
+ ? { kind: "non-executing" }
1689
+ : { kind: "denied" };
1690
+ }
1691
+ if (manager !== "uv") {
1692
+ // These managers have lifecycle hooks, plugins, build extensions, or
1693
+ // installer execution with no complete cross-version script-disable mode.
1694
+ return { kind: "denied" };
1695
+ }
1696
+ // uv can execute arbitrary build backends and project configuration during
1697
+ // install/sync. Only binary-only installs avoid that execution surface.
1698
+ const commandIndex = firstCommandIndex(unambiguous, uvCommandOptionsWithValues);
1699
+ const command = commandIndex === undefined
1700
+ ? undefined
1701
+ : unambiguous[commandIndex]?.toLowerCase();
1702
+ if (command !== undefined && new Set(["help", "version"]).has(command)) {
1703
+ return { kind: "non-executing" };
1704
+ }
1705
+ if (command === "tool"
1706
+ && unambiguous[commandIndex + 1]?.toLowerCase() === "install") {
1707
+ if (hasNonCanonicalBinaryOption(unambiguous)) {
1708
+ return { kind: "denied" };
1709
+ }
1710
+ const canonical = "--only-binary=:all:";
1711
+ if (hasDisabledOption(unambiguous, "--no-config")
1712
+ || hasSeparateOptionValue(unambiguous, "--no-config")) {
1713
+ return { kind: "denied" };
1714
+ }
1715
+ return {
1716
+ kind: "scripts-disabled",
1717
+ arguments: [
1718
+ ...(hasOnlyBinaryAll(unambiguous)
1719
+ ? arguments_
1720
+ : [...arguments_, canonical]),
1721
+ ...(hasEnabledOption(unambiguous, "--no-config") ? [] : ["--no-config"]),
1722
+ ],
1723
+ };
1724
+ }
1725
+ if (command === "pip") {
1726
+ const subcommand = unambiguous[commandIndex + 1]?.toLowerCase();
1727
+ if (subcommand !== undefined && new Set(["list", "show", "tree", "freeze"]).has(subcommand)) {
1728
+ return { kind: "non-executing" };
1729
+ }
1730
+ if (subcommand === "install") {
1731
+ if (hasNonCanonicalBinaryOption(unambiguous)) {
1732
+ return { kind: "denied" };
1733
+ }
1734
+ const canonical = "--only-binary=:all:";
1735
+ if (hasDisabledOption(unambiguous, "--no-config")
1736
+ || hasSeparateOptionValue(unambiguous, "--no-config")) {
1737
+ return { kind: "denied" };
1738
+ }
1739
+ return {
1740
+ kind: "scripts-disabled",
1741
+ arguments: [
1742
+ ...(hasOnlyBinaryAll(unambiguous)
1743
+ ? arguments_
1744
+ : [...arguments_, canonical]),
1745
+ ...(hasEnabledOption(unambiguous, "--no-config") ? [] : ["--no-config"]),
1746
+ ],
1747
+ };
1748
+ }
1749
+ }
1750
+ return { kind: "denied" };
1751
+ };
1752
+ const authorizePackageManagerInvocation = (executable, arguments_) => {
1753
+ const policy = packageManagerPolicy(executable, arguments_);
1754
+ if (policy === undefined || policy.kind === "non-executing") {
1755
+ return Effect.succeed(arguments_);
1756
+ }
1757
+ if (policy.kind === "scripts-disabled") {
1758
+ return Effect.succeed(policy.arguments);
1759
+ }
1760
+ return Effect.fail(new DeniedAgentCapabilityError({
1761
+ capability: "package-manager-scripts",
1762
+ value: executable,
1763
+ }));
1764
+ };
1765
+ const interpreterInvocation = (executable) => {
1766
+ const direct = interpreterKind(executable);
1767
+ return direct === undefined
1768
+ ? undefined
1769
+ : { executable };
1770
+ };
1771
+ const normalizedCommand = (value) => portableBasename(value)
1772
+ .toLowerCase()
1773
+ .replace(/\.(?:bat|cmd|com|exe|ps1)$/u, "");
1774
+ const systemctlGlobalOptionArity = new Map([
1775
+ ["--all", 0],
1776
+ ["--boot-loader-entry", 1],
1777
+ ["--boot-loader-menu", 1],
1778
+ ["--check-inhibitors", 1],
1779
+ ["--dry-run", 0],
1780
+ ["--failed", 0],
1781
+ ["--firmware-setup", 0],
1782
+ ["--force", 0],
1783
+ ["--full", 0],
1784
+ ["--global", 0],
1785
+ ["--help", 0],
1786
+ ["--host", 1],
1787
+ ["--job-mode", 1],
1788
+ ["--kill-value", 1],
1789
+ ["--kill-whom", 1],
1790
+ ["--legend", 1],
1791
+ ["--machine", 1],
1792
+ ["--marked", 0],
1793
+ ["--mkdir", 0],
1794
+ ["--no-ask-password", 0],
1795
+ ["--no-block", 0],
1796
+ ["--no-legend", 0],
1797
+ ["--no-pager", 0],
1798
+ ["--no-reload", 0],
1799
+ ["--no-warn", 0],
1800
+ ["--no-wall", 0],
1801
+ ["--now", 0],
1802
+ ["--output", 1],
1803
+ ["--plain", 0],
1804
+ ["--preset-mode", 1],
1805
+ ["--property", 1],
1806
+ ["--quiet", 0],
1807
+ ["--read-only", 0],
1808
+ ["--recursive", 0],
1809
+ ["--reverse", 0],
1810
+ ["--root", 1],
1811
+ ["--runtime", 0],
1812
+ ["--show-transaction", 0],
1813
+ ["--show-types", 0],
1814
+ ["--signal", 1],
1815
+ ["--state", 1],
1816
+ ["--system", 0],
1817
+ ["--timestamp", 1],
1818
+ ["--type", 1],
1819
+ ["--user", 0],
1820
+ ["--value", 0],
1821
+ ["--version", 0],
1822
+ ["--wait", 0],
1823
+ ["--what", 1],
1824
+ ["--with-dependencies", 0],
1825
+ ["-H", 1],
1826
+ ["-M", 1],
1827
+ ["-P", 1],
1828
+ ["-T", 0],
1829
+ ["-a", 0],
1830
+ ["-f", 0],
1831
+ ["-h", 0],
1832
+ ["-i", 0],
1833
+ ["-l", 0],
1834
+ ["-n", 1],
1835
+ ["-o", 1],
1836
+ ["-p", 1],
1837
+ ["-q", 0],
1838
+ ["-r", 0],
1839
+ ["-s", 1],
1840
+ ["-t", 1],
1841
+ ]);
1842
+ // systemctl accepts the same options before and after a command. Keeping the
1843
+ // option grammar explicit means an option value such as "reboot" can never
1844
+ // become a guessed command or unit.
1845
+ const systemctlOptionArity = systemctlGlobalOptionArity;
1846
+ const normalizedSystemctlTarget = (value) => {
1847
+ return normalizedCommand(value);
1848
+ };
1849
+ const isSystemctlPowerStateTarget = (value) => systemctlPowerStateTargets.has(normalizedSystemctlTarget(value));
1850
+ const wrapperOptionTakesValue = (argument) => new Set([
1851
+ "-a",
1852
+ "-c",
1853
+ "-d",
1854
+ "-g",
1855
+ "-r",
1856
+ "-t",
1857
+ "-u",
1858
+ "--close-from",
1859
+ "--command-timeout",
1860
+ "--group",
1861
+ "--other-user",
1862
+ "--role",
1863
+ "--type",
1864
+ "--user",
1865
+ ]).has(argument.split(/[=:]/u, 1)[0].toLowerCase());
1866
+ const genericOptionTakesValue = (argument) => new Set([
1867
+ "-d",
1868
+ "-g",
1869
+ "-h",
1870
+ "-m",
1871
+ "-p",
1872
+ "-t",
1873
+ "-u",
1874
+ "--chdir",
1875
+ "--directory",
1876
+ "--group",
1877
+ "--host",
1878
+ "--machine",
1879
+ "--property",
1880
+ "--root",
1881
+ "--setenv",
1882
+ "--state",
1883
+ "--type",
1884
+ "--job-mode",
1885
+ ]).has(argument.split(/[=:]/u, 1)[0].toLowerCase());
1886
+ const invalidSystemctlArguments = () => ({
1887
+ valid: false,
1888
+ positionals: [],
1889
+ options: new Set(),
1890
+ });
1891
+ const parseSystemctlArguments = (arguments_) => {
1892
+ const positionals = [];
1893
+ const options = new Set();
1894
+ let endOfOptions = false;
1895
+ for (let index = 0; index < arguments_.length; index += 1) {
1896
+ const argument = arguments_[index];
1897
+ if (endOfOptions) {
1898
+ positionals.push({ command: normalizedCommand(argument), index });
1899
+ continue;
1900
+ }
1901
+ if (argument === "--") {
1902
+ endOfOptions = true;
1903
+ continue;
1904
+ }
1905
+ if (!argument.startsWith("-") || argument === "-") {
1906
+ positionals.push({ command: normalizedCommand(argument), index });
1907
+ continue;
1908
+ }
1909
+ if (argument.startsWith("--")) {
1910
+ const match = /^([^=]+)(?:=(.*))?$/u.exec(argument);
1911
+ const option = match?.[1]?.toLowerCase();
1912
+ const arity = option === undefined
1913
+ ? undefined
1914
+ : systemctlOptionArity.get(option);
1915
+ if (option === undefined || arity === undefined) {
1916
+ return invalidSystemctlArguments();
1917
+ }
1918
+ const inlineValue = match?.[2];
1919
+ if (arity === 1
1920
+ && (inlineValue === undefined || inlineValue.length === 0)) {
1921
+ const separateValue = arguments_[index + 1];
1922
+ if (separateValue === undefined) {
1923
+ return invalidSystemctlArguments();
1924
+ }
1925
+ if (inlineValue !== undefined)
1926
+ return invalidSystemctlArguments();
1927
+ index += 1;
1928
+ }
1929
+ else if (arity === 0 && inlineValue !== undefined) {
1930
+ return invalidSystemctlArguments();
1931
+ }
1932
+ options.add(option);
1933
+ continue;
1934
+ }
1935
+ const shortOptions = argument.slice(1);
1936
+ for (let shortIndex = 0; shortIndex < shortOptions.length; shortIndex += 1) {
1937
+ const option = `-${shortOptions[shortIndex]}`;
1938
+ const arity = systemctlOptionArity.get(option);
1939
+ if (arity === undefined)
1940
+ return invalidSystemctlArguments();
1941
+ if (arity === 1) {
1942
+ const remainder = shortOptions.slice(shortIndex + 1);
1943
+ if (remainder.length === 0) {
1944
+ if (arguments_[index + 1] === undefined)
1945
+ return invalidSystemctlArguments();
1946
+ index += 1;
1947
+ }
1948
+ shortIndex = shortOptions.length;
1949
+ }
1950
+ options.add(option);
1951
+ }
1952
+ }
1953
+ return { valid: true, positionals, options };
1954
+ };
1955
+ const systemctlArgumentsValid = (executable, arguments_) => normalizedCommand(executable) !== "systemctl"
1956
+ || parseSystemctlArguments(arguments_).valid;
1957
+ const commandArguments = (arguments_, takesValue = genericOptionTakesValue) => {
1958
+ const positional = [];
1959
+ let endOfOptions = false;
1960
+ for (let index = 0; index < arguments_.length; index += 1) {
1961
+ const argument = arguments_[index];
1962
+ if (!endOfOptions && argument === "--") {
1963
+ endOfOptions = true;
1964
+ continue;
1965
+ }
1966
+ if (!endOfOptions
1967
+ && (argument.startsWith("-") || /^\/[^/\\]+$/u.test(argument))) {
1968
+ const separateValue = !argument.includes("=")
1969
+ && (!argument.startsWith("-") || argument.startsWith("--") || argument.length === 2);
1970
+ if (separateValue && takesValue(argument))
1971
+ index += 1;
1972
+ continue;
1973
+ }
1974
+ positional.push({ command: normalizedCommand(argument), index });
1975
+ }
1976
+ return positional;
1977
+ };
1978
+ const commandArgument = (arguments_) => {
1979
+ return commandArguments(arguments_, wrapperOptionTakesValue)[0];
1980
+ };
1981
+ const structuralRestartCapabilities = (executable, arguments_, depth = 0) => {
1982
+ if (depth > 8)
1983
+ return new Set();
1984
+ const command = normalizedCommand(executable);
1985
+ if (commandWrappers.has(command)) {
1986
+ if (command === "cmd") {
1987
+ const switchIndex = arguments_.findIndex((argument) => /^\/(?:c|k|r)$/iu.test(argument));
1988
+ if (switchIndex >= 0) {
1989
+ const nested = commandArgument(arguments_.slice(switchIndex + 1));
1990
+ return nested === undefined
1991
+ ? new Set()
1992
+ : structuralRestartCapabilities(nested.command, arguments_.slice(switchIndex + 2), depth + 1);
1993
+ }
1994
+ }
1995
+ const nested = commandArgument(arguments_);
1996
+ return nested === undefined
1997
+ ? new Set()
1998
+ : structuralRestartCapabilities(nested.command, arguments_.slice(nested.index + 1), depth + 1);
1999
+ }
2000
+ if (rebootExecutables.has(command))
2001
+ return new Set(["reboot"]);
2002
+ if (command === "restart-computer" || command === "stop-computer") {
2003
+ return new Set(["reboot"]);
2004
+ }
2005
+ if (restartExecutables.has(command))
2006
+ return new Set(["restart"]);
2007
+ if (!rebootCommandExecutables.has(command))
2008
+ return new Set();
2009
+ if (command === "systemctl") {
2010
+ const parsed = parseSystemctlArguments(arguments_);
2011
+ if (!parsed.valid || parsed.positionals.length === 0) {
2012
+ // An unrecognized option makes the command grammar ambiguous. Treat it
2013
+ // as unsafe rather than allowing an option value to hide a power action.
2014
+ return parsed.valid ? new Set() : new Set(["restart", "reboot"]);
2015
+ }
2016
+ const subcommand = parsed.positionals[0].command;
2017
+ if (systemctlPowerStateVerbs.has(subcommand)) {
2018
+ return new Set(["reboot"]);
2019
+ }
2020
+ if (systemctlUnitOperations.has(subcommand)
2021
+ && parsed.positionals.slice(1).some((unit) => isSystemctlPowerStateTarget(arguments_[unit.index]))) {
2022
+ return new Set(["reboot"]);
2023
+ }
2024
+ if (systemctlEnableOperations.has(subcommand)
2025
+ && parsed.options.has("--now")
2026
+ && parsed.positionals.slice(1).some((unit) => isSystemctlPowerStateTarget(arguments_[unit.index]))) {
2027
+ return new Set(["reboot"]);
2028
+ }
2029
+ return subcommand === "restart" ? new Set(["restart"]) : new Set();
2030
+ }
2031
+ const positional = commandArguments(arguments_);
2032
+ const subcommand = command === "service" || command === "rc-service"
2033
+ ? positional.at(-1)?.command
2034
+ : positional[0]?.command;
2035
+ if (subcommand === undefined)
2036
+ return new Set();
2037
+ if (subcommand === "reboot" || subcommand === "shutdown"
2038
+ || subcommand === "halt" || subcommand === "poweroff"
2039
+ || (command === "init" || command === "telinit")
2040
+ && ["0", "6"].includes(subcommand)) {
2041
+ return new Set(["reboot"]);
2042
+ }
2043
+ if (subcommand === "restart")
2044
+ return new Set(["restart"]);
2045
+ return new Set();
2046
+ };
2047
+ export const derivedCapabilities = (executable, arguments_) => {
2048
+ const capabilities = new Set();
2049
+ const command = normalizedCommand(executable);
2050
+ const tokens = arguments_.map((argument) => argument.toLowerCase());
2051
+ const commandTokens = [
2052
+ command,
2053
+ ...tokens
2054
+ .filter((argument) => !argument.startsWith("-")
2055
+ && !(/^\/[^/\\]+$/u.test(argument)))
2056
+ .map((argument) => portableBasename(argument).replace(/\.(?:cmd|exe)$/u, "")),
2057
+ ];
2058
+ if (elevationExecutables.has(command)
2059
+ || tokens.some((argument) => argument === "--sudo" || argument === "--elevated" || argument.startsWith("/runas"))) {
2060
+ capabilities.add("elevation");
2061
+ }
2062
+ if (commandTokens.some((token) => loginExecutables.has(token))
2063
+ || tokens.some((argument) => argument === "login"
2064
+ || argument === "logout"
2065
+ || argument === "logon"
2066
+ || argument === "session")) {
2067
+ capabilities.add("login");
2068
+ }
2069
+ for (const capability of structuralRestartCapabilities(executable, arguments_)) {
2070
+ capabilities.add(capability);
2071
+ }
2072
+ return capabilities;
2073
+ };
2074
+ const taskBounds = (task) => ({
2075
+ allowedPaths: task.allowedPaths,
2076
+ allowedExecutables: task.allowedExecutables,
2077
+ executableAuthorizations: task.executableAuthorizations,
2078
+ allowedOrigins: task.allowedOrigins,
2079
+ allowedCapabilities: ["elevation", "login", "restart", "reboot"]
2080
+ .filter((capability) => !task.forbidden.includes(capability)),
2081
+ });
2082
+ const behaviorAuthorized = (executable, workingDirectory, environment, authorizations, behavior) => executableAllowed(executable, (authorizations ?? [])
2083
+ .filter((authorization) => authorization.behavior === behavior)
2084
+ .map((authorization) => authorization.executable), environment, workingDirectory);
2085
+ const authorizeExecutableBehavior = (executable, arguments_, workingDirectory, task, harness, deniedCapability, actionOrigins = []) => Effect.gen(function* () {
2086
+ const pipRequirementAuthorization = yield* validatePipRequirementInputs(executable, arguments_, workingDirectory, task, harness);
2087
+ const scriptAuthorizedArguments = yield* authorizePackageManagerInvocation(executable, pipRequirementAuthorization.arguments);
2088
+ const authorizedArguments = yield* authorizeRegistryInvocation(executable, scriptAuthorizedArguments, task.allowedOrigins, harness.allowedOrigins, actionOrigins);
2089
+ if (isNestedCommandLauncher(executable)) {
2090
+ // The descendant command of a launcher is not derivable from argv, so
2091
+ // neither a leaf nor a script-file classification can bound it. Deny
2092
+ // before any allowlist comparison regardless of what it is named.
2093
+ return yield* new DeniedAgentCapabilityError({
2094
+ capability: "nested-command-launcher",
2095
+ value: executable,
2096
+ });
2097
+ }
2098
+ const environment = harness.environment ?? [];
2099
+ const invocation = interpreterInvocation(executable);
2100
+ if (invocation !== undefined) {
2101
+ return yield* new DeniedAgentCapabilityError({
2102
+ capability: "script-interpreter",
2103
+ value: executable,
2104
+ });
2105
+ }
2106
+ const taskAuthorized = yield* behaviorAuthorized(executable, workingDirectory, environment, task.executableAuthorizations, "leaf");
2107
+ const harnessAuthorized = yield* behaviorAuthorized(executable, workingDirectory, environment, harness.executableAuthorizations, "leaf");
2108
+ if (!taskAuthorized || !harnessAuthorized) {
2109
+ return yield* new DeniedAgentCapabilityError({
2110
+ capability: deniedCapability,
2111
+ value: executable,
2112
+ });
2113
+ }
2114
+ return {
2115
+ arguments: authorizedArguments,
2116
+ pipRequirementFiles: pipRequirementAuthorization.files,
2117
+ };
2118
+ });
2119
+ const resolveAuthorizedAction = (action, task, harness = taskBounds(task)) => Effect.gen(function* () {
2120
+ const workingDirectory = action.workingDirectory
2121
+ ?? task.allowedPaths[0]
2122
+ ?? process.cwd();
2123
+ const environment = harness.environment ?? [];
2124
+ const executable = yield* Effect.promise(() => resolvedExecutableIdentity(action.executable, environment, workingDirectory));
2125
+ if (executable === undefined
2126
+ || !(yield* executableAllowed(executable, task.allowedExecutables, environment, workingDirectory))
2127
+ || !(yield* executableAllowed(executable, harness.allowedExecutables, environment, workingDirectory))) {
2128
+ return yield* new DeniedAgentCapabilityError({
2129
+ capability: "executable",
2130
+ value: action.executable,
2131
+ });
2132
+ }
2133
+ if (!systemctlArgumentsValid(executable, action.arguments)) {
2134
+ return yield* new DeniedAgentCapabilityError({
2135
+ capability: "systemctl-grammar",
2136
+ value: executable,
2137
+ });
2138
+ }
2139
+ const capabilities = new Set([
2140
+ ...action.capabilities,
2141
+ ...derivedCapabilities(executable, action.arguments),
2142
+ ]);
2143
+ for (const capability of capabilities) {
2144
+ if (task.forbidden.includes(capability)
2145
+ || !harness.allowedCapabilities.includes(capability)) {
2146
+ return yield* new DeniedAgentCapabilityError({
2147
+ capability,
2148
+ value: action.executable,
2149
+ });
2150
+ }
2151
+ }
2152
+ const authorizedInvocation = yield* authorizeExecutableBehavior(executable, action.arguments, workingDirectory, task, harness, "executable-behavior", action.origins);
2153
+ const authorizedArguments = authorizedInvocation.arguments;
2154
+ const npmArguments = npmDependencyArgumentIndexes(executable, authorizedArguments);
2155
+ const authorizedWorkingDirectory = action.workingDirectory
2156
+ ?? task.allowedPaths[0];
2157
+ if (authorizedWorkingDirectory === undefined) {
2158
+ return yield* new DeniedAgentCapabilityError({
2159
+ capability: "path",
2160
+ value: action.workingDirectory ?? "",
2161
+ });
2162
+ }
2163
+ yield* ensureAllowedPath(authorizedWorkingDirectory, authorizedWorkingDirectory, task.allowedPaths, harness.allowedPaths);
2164
+ for (const path of action.paths) {
2165
+ yield* ensureAllowedPath(path, authorizedWorkingDirectory, task.allowedPaths, harness.allowedPaths);
2166
+ }
2167
+ for (const [index, argument] of authorizedArguments.entries()) {
2168
+ const isNpmDependency = npmArguments.has(index);
2169
+ const path = argumentPath(argument, isNpmDependency);
2170
+ if (path !== undefined) {
2171
+ yield* ensureAllowedPath(path, authorizedWorkingDirectory, task.allowedPaths, harness.allowedPaths);
2172
+ }
2173
+ for (const origin of argumentOrigins(argument, isNpmDependency)) {
2174
+ yield* ensureAllowedOrigin(origin, task.allowedOrigins, harness.allowedOrigins);
2175
+ }
2176
+ }
2177
+ for (const origin of action.origins) {
2178
+ yield* ensureAllowedOrigin(origin, task.allowedOrigins, harness.allowedOrigins);
2179
+ }
2180
+ const resolvedAction = {
2181
+ ...action,
2182
+ executable,
2183
+ arguments: authorizedArguments,
2184
+ };
2185
+ return authorizedInvocation.pipRequirementFiles.length === 0
2186
+ ? resolvedAction
2187
+ : {
2188
+ ...resolvedAction,
2189
+ pipRequirementFiles: authorizedInvocation.pipRequirementFiles,
2190
+ };
2191
+ });
2192
+ export const authorizeAction = (action, task, harness = taskBounds(task)) => resolveAuthorizedAction(action, task, harness).pipe(Effect.asVoid);
2193
+ const resolveAuthorizedVerification = (task, harness) => Effect.gen(function* () {
2194
+ const requestedExecutable = task.verification.command[0] ?? "";
2195
+ const workingDirectory = task.allowedPaths[0] ?? process.cwd();
2196
+ const environment = harness.environment ?? [];
2197
+ const executable = yield* Effect.promise(() => resolvedExecutableIdentity(requestedExecutable, environment, workingDirectory));
2198
+ if (executable === undefined
2199
+ || !(yield* executableAllowed(executable, task.allowedExecutables, environment, workingDirectory))
2200
+ || !(yield* executableAllowed(executable, harness.allowedExecutables, environment, workingDirectory))
2201
+ || derivedCapabilities(executable, task.verification.command.slice(1)).size > 0) {
2202
+ return yield* new DeniedAgentCapabilityError({
2203
+ capability: "verification-executable",
2204
+ value: requestedExecutable,
2205
+ });
2206
+ }
2207
+ const authorizedInvocation = yield* authorizeExecutableBehavior(executable, task.verification.command.slice(1), workingDirectory, task, harness, "verification-executable-behavior");
2208
+ const authorizedArguments = authorizedInvocation.arguments;
2209
+ const npmArguments = npmDependencyArgumentIndexes(executable, authorizedArguments);
2210
+ yield* Effect.forEach(authorizedArguments.entries(), ([index, argument]) => {
2211
+ const isNpmDependency = npmArguments.has(index);
2212
+ const path = argumentPath(argument, isNpmDependency);
2213
+ if (path !== undefined) {
2214
+ return ensureAllowedPath(path, workingDirectory, task.allowedPaths, harness.allowedPaths);
2215
+ }
2216
+ return Effect.forEach(argumentOrigins(argument, isNpmDependency), (origin) => ensureAllowedOrigin(origin, task.allowedOrigins, harness.allowedOrigins), { discard: true });
2217
+ }, { discard: true });
2218
+ return {
2219
+ command: [executable, ...authorizedArguments],
2220
+ pipRequirementFiles: authorizedInvocation.pipRequirementFiles,
2221
+ };
2222
+ });
2223
+ export const resolveAuthorizedProposal = (proposal, task, harness = taskBounds(task)) => Effect.gen(function* () {
2224
+ const actions = yield* Effect.forEach(proposal.actions, (action) => resolveAuthorizedAction(action, task, harness));
2225
+ const verification = yield* resolveAuthorizedVerification(task, harness);
2226
+ return {
2227
+ proposal: { ...proposal, actions },
2228
+ verificationCommand: verification.command,
2229
+ verificationPipRequirementFiles: verification.pipRequirementFiles,
2230
+ };
2231
+ });
2232
+ export const validateProposal = (proposal, task, harness = taskBounds(task)) => resolveAuthorizedProposal(proposal, task, harness).pipe(Effect.asVoid);
2233
+ export const profileChangeProposalFromResolution = (resolution, createdAt) => {
2234
+ if (!Number.isFinite(Date.parse(createdAt))) {
2235
+ return Effect.fail(new InvalidAgentTaskError({
2236
+ task: "source-discovery-resolution",
2237
+ message: "createdAt must be an ISO-compatible timestamp",
2238
+ }));
2239
+ }
2240
+ return Effect.succeed({
2241
+ reviewStatus: "pending",
2242
+ proposal: {
2243
+ createdAt,
2244
+ reason: resolution.reason,
2245
+ additions: resolution.additions,
2246
+ modifications: resolution.modifications,
2247
+ removals: resolution.removals,
2248
+ evidence: resolution.evidence,
2249
+ },
2250
+ });
2251
+ };
2252
+ export const nonzeroProcessError = (executable, exitCode, stderr) => new AgentProcessError({
2253
+ executable,
2254
+ message: `process exited with ${String(exitCode)}: ${stderr}`,
2255
+ });
2256
+ export const failedVerification = (task, message) => new AgentVerificationError({
2257
+ command: task.verification.command,
2258
+ message,
2259
+ });