@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.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/agent/agent-resolution.errors.js +42 -0
- package/dist/agent/agent-resolution.layer.js +204 -0
- package/dist/agent/agent-resolution.service.js +2259 -0
- package/dist/agent/agent-resolution.types.js +1 -0
- package/dist/agent/controlled-executor.js +704 -0
- package/dist/agent/harness-adapters.js +85 -0
- package/dist/cli/cli.js +618 -0
- package/dist/cli/exit-codes.js +28 -0
- package/dist/cli/follower-commands.js +3 -0
- package/dist/cli/render.js +56 -0
- package/dist/cli/source-commands.js +5 -0
- package/dist/domain/brand.js +29 -0
- package/dist/domain/identity.js +31 -0
- package/dist/domain/npm-package-spec.js +186 -0
- package/dist/domain/profile.js +950 -0
- package/dist/domain/recipe-versions.js +297 -0
- package/dist/domain/resource.js +259 -0
- package/dist/domain/synchronization.js +346 -0
- package/dist/enrollment/enrollment.errors.js +43 -0
- package/dist/enrollment/enrollment.layer.js +724 -0
- package/dist/enrollment/enrollment.service.js +3 -0
- package/dist/enrollment/enrollment.types.js +59 -0
- package/dist/enrollment/follower-client.js +585 -0
- package/dist/enrollment/source-server.js +313 -0
- package/dist/machine/linux.layer.js +1183 -0
- package/dist/machine/machine-state.errors.js +52 -0
- package/dist/machine/machine-state.service.js +3 -0
- package/dist/machine/machine-state.types.js +1 -0
- package/dist/machine/macos.layer.js +470 -0
- package/dist/machine/windows.layer.js +879 -0
- package/dist/profile/discovery.js +740 -0
- package/dist/profile/profile-catalog.errors.js +50 -0
- package/dist/profile/profile-catalog.layer.js +20 -0
- package/dist/profile/profile-catalog.service.js +7 -0
- package/dist/profile/profile-codec.js +153 -0
- package/dist/profile/publication.js +298 -0
- package/dist/profile/tool-catalog.js +384 -0
- package/dist/runtime/doctor.js +306 -0
- package/dist/runtime/layers.js +706 -0
- package/dist/runtime/main.js +38 -0
- package/dist/schedule/linux-schedule.js +24 -0
- package/dist/schedule/macos-schedule.js +25 -0
- package/dist/schedule/schedule-manager.errors.js +17 -0
- package/dist/schedule/schedule-manager.layer.js +205 -0
- package/dist/schedule/schedule-manager.service.js +3 -0
- package/dist/schedule/schedule-manager.types.js +114 -0
- package/dist/schedule/windows-schedule.js +25 -0
- package/dist/state/state-repository.errors.js +55 -0
- package/dist/state/state-repository.layer.js +1507 -0
- package/dist/state/state-repository.service.js +3 -0
- package/dist/state/state-repository.types.js +1 -0
- package/dist/state/state-schema.js +298 -0
- package/dist/synchronization/config-codec.js +97 -0
- package/dist/synchronization/executor.js +700 -0
- package/dist/synchronization/follower-orchestration.js +939 -0
- package/dist/synchronization/follower-sync-config.js +81 -0
- package/dist/synchronization/npm-artifact.js +670 -0
- package/dist/synchronization/planner.js +378 -0
- package/dist/synchronization/recovery.js +397 -0
- package/dist/synchronization/resource-executors.js +1198 -0
- package/dist/synchronization/resource-plans.js +645 -0
- package/dist/synchronization/synchronization.errors.js +102 -0
- package/dist/synchronization/synchronization.layer.js +97 -0
- package/dist/synchronization/synchronization.service.js +11 -0
- package/dist/synchronization/synchronization.types.js +1 -0
- 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
|
+
});
|