@wrongstack/tools 0.3.2 → 0.3.4

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/dist/exec.js CHANGED
@@ -43,7 +43,7 @@ var ALLOWED_COMMANDS = {
43
43
  cargo: ["--version", "build", "test", "check"],
44
44
  rustc: ["--version"],
45
45
  go: ["version", "run", "build", "test"],
46
- python: ["--version", "-c"],
46
+ python: ["--version"],
47
47
  pip: ["--version", "install", "list"],
48
48
  docker: ["--version", "ps", "images", "build"],
49
49
  kubectl: ["version", "get", "describe", "logs"]
@@ -51,6 +51,30 @@ var ALLOWED_COMMANDS = {
51
51
  var MAX_ARGS = 20;
52
52
  var MAX_OUTPUT = 2e5;
53
53
  var TIMEOUT_MS = 3e4;
54
+ var BLOCKED_ARG_PATTERNS = {
55
+ // python -c/--command executes arbitrary code; python -m runs modules
56
+ python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
57
+ // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack
58
+ git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/],
59
+ // node -r/--require preloads arbitrary modules; --eval executes code
60
+ node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
61
+ // go run could execute arbitrary .go files; -ldflags could inject build-time code
62
+ go: [/^-ldflags$/],
63
+ // bun --preload is similar to node --require
64
+ bun: [/^--preload$/]
65
+ };
66
+ function validateArgs(cmd, args) {
67
+ const blocked = BLOCKED_ARG_PATTERNS[cmd];
68
+ if (!blocked) return null;
69
+ for (const arg of args) {
70
+ for (const pattern of blocked) {
71
+ if (pattern.test(arg)) {
72
+ return `Blocked argument "${arg}" for command "${cmd}" (matches security pattern ${pattern})`;
73
+ }
74
+ }
75
+ }
76
+ return null;
77
+ }
54
78
  var execTool = {
55
79
  name: "exec",
56
80
  category: "Shell",
@@ -94,6 +118,18 @@ var execTool = {
94
118
  }
95
119
  const args = (input.args ?? []).slice(0, MAX_ARGS);
96
120
  const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));
121
+ const argError = validateArgs(cmd, args);
122
+ if (argError) {
123
+ return {
124
+ command: cmd,
125
+ args,
126
+ stdout: "",
127
+ stderr: argError,
128
+ exitCode: 1,
129
+ truncated: false,
130
+ allowed: false
131
+ };
132
+ }
97
133
  const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
98
134
  const rel = path.relative(ctx.projectRoot, requestedCwd);
99
135
  if (rel.startsWith("..") || path.isAbsolute(rel)) {
package/dist/exec.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/exec.ts"],"names":["resolve"],"mappings":"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","file":"exec.js","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\nimport type { Tool } from '@wrongstack/core';\nimport { buildChildEnv } from './_env.js';\n\nconst ALLOWED_COMMANDS: Record<string, string[]> = {\n node: ['--version', '-r', '--input-type=module'],\n npm: ['--version', 'init', 'install', 'test', 'list', 'pkg', 'doctor'],\n pnpm: ['--version', 'init', 'install', 'add', 'remove', 'list'],\n npx: ['--version'],\n git: [\n '--version',\n 'status',\n 'log',\n 'diff',\n 'branch',\n 'checkout',\n 'stash',\n 'add',\n 'commit',\n 'push',\n 'pull',\n ],\n ls: ['-la', '-l', '-a'],\n cat: [],\n head: ['-n'],\n tail: ['-n'],\n wc: ['-l', '-w', '-c'],\n grep: [],\n find: [],\n echo: [],\n mkdir: ['-p'],\n cp: ['-r'],\n mv: [],\n rm: ['-rf'],\n touch: [],\n bun: ['--version', 'add', 'init'],\n tsc: ['--version', '--noEmit', '--project'],\n vitest: ['--version', 'run', '--coverage'],\n biome: ['--version', 'lint', 'format', 'check'],\n cargo: ['--version', 'build', 'test', 'check'],\n rustc: ['--version'],\n go: ['version', 'run', 'build', 'test'],\n python: ['--version', '-c'],\n pip: ['--version', 'install', 'list'],\n docker: ['--version', 'ps', 'images', 'build'],\n kubectl: ['version', 'get', 'describe', 'logs'],\n};\n\nconst MAX_ARGS = 20;\nconst MAX_OUTPUT = 200_000;\nconst TIMEOUT_MS = 30_000;\n\ninterface ExecInput {\n command: string;\n args?: string[];\n cwd?: string;\n timeout?: number;\n}\n\ninterface ExecOutput {\n command: string;\n args: string[];\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n allowed: boolean;\n}\n\nexport const execTool: Tool<ExecInput, ExecOutput> = {\n name: 'exec',\n category: 'Shell',\n description:\n 'Restricted shell that only runs pre-approved commands with constrained arguments. Safer alternative to `bash`.',\n usageHint:\n 'Set `command` (must be in allowlist). `args` passed through. For arbitrary shell access use the `bash` tool instead.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n command: { type: 'string', description: 'Command to run (must be in allowlist)' },\n args: { type: 'array', items: { type: 'string' }, description: 'Arguments' },\n cwd: { type: 'string', description: 'Working directory (must resolve inside project root)' },\n timeout: { type: 'integer', description: 'Timeout in ms (default: 30000)' },\n },\n required: ['command'],\n },\n async execute(input, ctx, opts) {\n const cmd = input.command.trim();\n if (!cmd)\n return {\n command: cmd,\n args: [],\n stdout: '',\n stderr: 'Empty command',\n exitCode: 1,\n truncated: false,\n allowed: false,\n };\n\n if (!(cmd in ALLOWED_COMMANDS)) {\n return {\n command: cmd,\n args: input.args ?? [],\n stdout: '',\n stderr: `Command \"${cmd}\" not in allowlist. Use the bash tool for arbitrary commands.`,\n exitCode: 1,\n truncated: false,\n allowed: false,\n };\n }\n\n const args = (input.args ?? []).slice(0, MAX_ARGS);\n const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));\n\n // Resolve cwd inside the project root. Model-supplied paths like '/etc'\n // would otherwise let allowlisted commands operate anywhere on disk.\n const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;\n const rel = path.relative(ctx.projectRoot, requestedCwd);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n return {\n command: cmd,\n args,\n stdout: '',\n stderr: `cwd \"${input.cwd}\" resolves outside project root`,\n exitCode: 1,\n truncated: false,\n allowed: false,\n };\n }\n const cwd = requestedCwd;\n const signal = opts.signal;\n\n return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);\n },\n};\n\nfunction runCommand(\n cmd: string,\n args: string[],\n cwd: string,\n timeout: number,\n signal: AbortSignal,\n sessionId: string | undefined,\n): Promise<ExecOutput> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n let killed = false;\n\n const child = spawn(cmd, args, {\n cwd,\n signal,\n env: buildChildEnv(sessionId),\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const timer = setTimeout(() => {\n killed = true;\n child.kill('SIGTERM');\n }, timeout);\n\n child.stdout?.on('data', (chunk: Buffer) => {\n if (stdout.length < MAX_OUTPUT) stdout += chunk.toString();\n });\n\n child.stderr?.on('data', (chunk: Buffer) => {\n if (stderr.length < MAX_OUTPUT) stderr += chunk.toString();\n });\n\n child.on('close', (code) => {\n clearTimeout(timer);\n resolve({\n command: cmd,\n args,\n stdout: stdout.slice(0, MAX_OUTPUT),\n stderr: stderr.slice(0, MAX_OUTPUT),\n exitCode: killed ? 124 : (code ?? 1),\n truncated: stdout.length >= MAX_OUTPUT || stderr.length >= MAX_OUTPUT,\n allowed: true,\n });\n });\n\n child.on('error', (err) => {\n clearTimeout(timer);\n resolve({\n command: cmd,\n args,\n stdout: stdout.slice(0, MAX_OUTPUT),\n stderr: err.message,\n exitCode: 1,\n truncated: false,\n allowed: true,\n });\n });\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/exec.ts"],"names":["resolve"],"mappings":"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","file":"exec.js","sourcesContent":["import { spawn } from 'node:child_process';\r\nimport * as path from 'node:path';\r\nimport type { Tool } from '@wrongstack/core';\r\nimport { buildChildEnv } from './_env.js';\r\n\r\nconst ALLOWED_COMMANDS: Record<string, string[]> = {\r\n node: ['--version', '-r', '--input-type=module'],\r\n npm: ['--version', 'init', 'install', 'test', 'list', 'pkg', 'doctor'],\r\n pnpm: ['--version', 'init', 'install', 'add', 'remove', 'list'],\r\n npx: ['--version'],\r\n git: [\r\n '--version',\r\n 'status',\r\n 'log',\r\n 'diff',\r\n 'branch',\r\n 'checkout',\r\n 'stash',\r\n 'add',\r\n 'commit',\r\n 'push',\r\n 'pull',\r\n ],\r\n ls: ['-la', '-l', '-a'],\r\n cat: [],\r\n head: ['-n'],\r\n tail: ['-n'],\r\n wc: ['-l', '-w', '-c'],\r\n grep: [],\r\n find: [],\r\n echo: [],\r\n mkdir: ['-p'],\r\n cp: ['-r'],\r\n mv: [],\r\n rm: ['-rf'],\r\n touch: [],\r\n bun: ['--version', 'add', 'init'],\r\n tsc: ['--version', '--noEmit', '--project'],\r\n vitest: ['--version', 'run', '--coverage'],\r\n biome: ['--version', 'lint', 'format', 'check'],\r\n cargo: ['--version', 'build', 'test', 'check'],\r\n rustc: ['--version'],\r\n go: ['version', 'run', 'build', 'test'],\r\n python: ['--version'],\r\n pip: ['--version', 'install', 'list'],\r\n docker: ['--version', 'ps', 'images', 'build'],\r\n kubectl: ['version', 'get', 'describe', 'logs'],\r\n};\r\n\r\nconst MAX_ARGS = 20;\r\nconst MAX_OUTPUT = 200_000;\r\nconst TIMEOUT_MS = 30_000;\r\n\r\n// Per-command argument validation. Each entry is a list of regex patterns\r\n// that, if matched against any argument, will reject the invocation.\r\n// This blocks common injection vectors through allowlisted commands.\r\nconst BLOCKED_ARG_PATTERNS: Record<string, RegExp[]> = {\r\n // python -c/--command executes arbitrary code; python -m runs modules\r\n python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],\r\n // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack\r\n git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/],\r\n // node -r/--require preloads arbitrary modules; --eval executes code\r\n node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],\r\n // go run could execute arbitrary .go files; -ldflags could inject build-time code\r\n go: [/^-ldflags$/],\r\n // bun --preload is similar to node --require\r\n bun: [/^--preload$/],\r\n};\r\n\r\nfunction validateArgs(cmd: string, args: string[]): string | null {\r\n const blocked = BLOCKED_ARG_PATTERNS[cmd];\r\n if (!blocked) return null;\r\n\r\n for (const arg of args) {\r\n for (const pattern of blocked) {\r\n if (pattern.test(arg)) {\r\n return `Blocked argument \"${arg}\" for command \"${cmd}\" (matches security pattern ${pattern})`;\r\n }\r\n }\r\n }\r\n return null;\r\n}\r\n\r\ninterface ExecInput {\r\n command: string;\r\n args?: string[];\r\n cwd?: string;\r\n timeout?: number;\r\n}\r\n\r\ninterface ExecOutput {\r\n command: string;\r\n args: string[];\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n allowed: boolean;\r\n}\r\n\r\nexport const execTool: Tool<ExecInput, ExecOutput> = {\r\n name: 'exec',\r\n category: 'Shell',\r\n description:\r\n 'Restricted shell that only runs pre-approved commands with constrained arguments. Safer alternative to `bash`.',\r\n usageHint:\r\n 'Set `command` (must be in allowlist). `args` passed through. For arbitrary shell access use the `bash` tool instead.',\r\n permission: 'confirm',\r\n mutating: true,\r\n timeoutMs: TIMEOUT_MS,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n command: { type: 'string', description: 'Command to run (must be in allowlist)' },\r\n args: { type: 'array', items: { type: 'string' }, description: 'Arguments' },\r\n cwd: { type: 'string', description: 'Working directory (must resolve inside project root)' },\r\n timeout: { type: 'integer', description: 'Timeout in ms (default: 30000)' },\r\n },\r\n required: ['command'],\r\n },\r\n async execute(input, ctx, opts) {\r\n const cmd = input.command.trim();\r\n if (!cmd)\r\n return {\r\n command: cmd,\r\n args: [],\r\n stdout: '',\r\n stderr: 'Empty command',\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n\r\n if (!(cmd in ALLOWED_COMMANDS)) {\r\n return {\r\n command: cmd,\r\n args: input.args ?? [],\r\n stdout: '',\r\n stderr: `Command \"${cmd}\" not in allowlist. Use the bash tool for arbitrary commands.`,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n }\r\n\r\n const args = (input.args ?? []).slice(0, MAX_ARGS);\r\n const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));\r\n\r\n // Validate args against per-command security patterns\r\n const argError = validateArgs(cmd, args);\r\n if (argError) {\r\n return {\r\n command: cmd,\r\n args,\r\n stdout: '',\r\n stderr: argError,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n }\r\n\r\n // Resolve cwd inside the project root. Model-supplied paths like '/etc'\r\n // would otherwise let allowlisted commands operate anywhere on disk.\r\n const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;\r\n const rel = path.relative(ctx.projectRoot, requestedCwd);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n return {\r\n command: cmd,\r\n args,\r\n stdout: '',\r\n stderr: `cwd \"${input.cwd}\" resolves outside project root`,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: false,\r\n };\r\n }\r\n const cwd = requestedCwd;\r\n const signal = opts.signal;\r\n\r\n return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);\r\n },\r\n};\r\n\r\nfunction runCommand(\r\n cmd: string,\r\n args: string[],\r\n cwd: string,\r\n timeout: number,\r\n signal: AbortSignal,\r\n sessionId: string | undefined,\r\n): Promise<ExecOutput> {\r\n return new Promise((resolve) => {\r\n let stdout = '';\r\n let stderr = '';\r\n let killed = false;\r\n\r\n const child = spawn(cmd, args, {\r\n cwd,\r\n signal,\r\n env: buildChildEnv(sessionId),\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n const timer = setTimeout(() => {\r\n killed = true;\r\n child.kill('SIGTERM');\r\n }, timeout);\r\n\r\n child.stdout?.on('data', (chunk: Buffer) => {\r\n if (stdout.length < MAX_OUTPUT) stdout += chunk.toString();\r\n });\r\n\r\n child.stderr?.on('data', (chunk: Buffer) => {\r\n if (stderr.length < MAX_OUTPUT) stderr += chunk.toString();\r\n });\r\n\r\n child.on('close', (code) => {\r\n clearTimeout(timer);\r\n resolve({\r\n command: cmd,\r\n args,\r\n stdout: stdout.slice(0, MAX_OUTPUT),\r\n stderr: stderr.slice(0, MAX_OUTPUT),\r\n exitCode: killed ? 124 : (code ?? 1),\r\n truncated: stdout.length >= MAX_OUTPUT || stderr.length >= MAX_OUTPUT,\r\n allowed: true,\r\n });\r\n });\r\n\r\n child.on('error', (err) => {\r\n clearTimeout(timer);\r\n resolve({\r\n command: cmd,\r\n args,\r\n stdout: stdout.slice(0, MAX_OUTPUT),\r\n stderr: err.message,\r\n exitCode: 1,\r\n truncated: false,\r\n allowed: true,\r\n });\r\n });\r\n });\r\n}\r\n"]}
package/dist/index.js CHANGED
@@ -436,13 +436,13 @@ async function globFiles(pattern, base, extraGlob) {
436
436
  return await globNative(pattern, base, extraGlob);
437
437
  }
438
438
  function checkRg() {
439
- return new Promise((resolve4) => {
439
+ return new Promise((resolve5) => {
440
440
  try {
441
441
  const p = spawn("rg", ["--version"], { stdio: "ignore" });
442
- p.on("error", () => resolve4(false));
443
- p.on("close", (code) => resolve4(code === 0));
442
+ p.on("error", () => resolve5(false));
443
+ p.on("close", (code) => resolve5(code === 0));
444
444
  } catch {
445
- resolve4(false);
445
+ resolve5(false);
446
446
  }
447
447
  });
448
448
  }
@@ -454,10 +454,10 @@ function spawnRgFind(pattern, base) {
454
454
  buf += chunk.toString();
455
455
  });
456
456
  return {
457
- promise: new Promise((resolve4, reject) => {
457
+ promise: new Promise((resolve5, reject) => {
458
458
  child.on("error", reject);
459
459
  child.on("close", () => {
460
- resolve4(buf.split("\n").filter(Boolean));
460
+ resolve5(buf.split("\n").filter(Boolean));
461
461
  });
462
462
  })
463
463
  };
@@ -621,13 +621,13 @@ var grepTool = {
621
621
  }
622
622
  };
623
623
  async function detectRg(signal) {
624
- return new Promise((resolve4) => {
624
+ return new Promise((resolve5) => {
625
625
  try {
626
626
  const p = spawn("rg", ["--version"], { stdio: "ignore", signal });
627
- p.on("error", () => resolve4(false));
628
- p.on("close", (code) => resolve4(code === 0));
627
+ p.on("error", () => resolve5(false));
628
+ p.on("close", (code) => resolve5(code === 0));
629
629
  } catch {
630
- resolve4(false);
630
+ resolve5(false);
631
631
  }
632
632
  });
633
633
  }
@@ -949,10 +949,10 @@ var bashTool = {
949
949
  queue.push(c);
950
950
  }
951
951
  };
952
- const next = () => new Promise((resolve4) => {
952
+ const next = () => new Promise((resolve5) => {
953
953
  const c = queue.shift();
954
- if (c) resolve4(c);
955
- else resolveNext = resolve4;
954
+ if (c) resolve5(c);
955
+ else resolveNext = resolve5;
956
956
  });
957
957
  let lastFlush = Date.now();
958
958
  const flush = () => {
@@ -1051,7 +1051,7 @@ var ALLOWED_COMMANDS = {
1051
1051
  cargo: ["--version", "build", "test", "check"],
1052
1052
  rustc: ["--version"],
1053
1053
  go: ["version", "run", "build", "test"],
1054
- python: ["--version", "-c"],
1054
+ python: ["--version"],
1055
1055
  pip: ["--version", "install", "list"],
1056
1056
  docker: ["--version", "ps", "images", "build"],
1057
1057
  kubectl: ["version", "get", "describe", "logs"]
@@ -1059,6 +1059,30 @@ var ALLOWED_COMMANDS = {
1059
1059
  var MAX_ARGS = 20;
1060
1060
  var MAX_OUTPUT2 = 2e5;
1061
1061
  var TIMEOUT_MS = 3e4;
1062
+ var BLOCKED_ARG_PATTERNS = {
1063
+ // python -c/--command executes arbitrary code; python -m runs modules
1064
+ python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
1065
+ // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack
1066
+ git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/],
1067
+ // node -r/--require preloads arbitrary modules; --eval executes code
1068
+ node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
1069
+ // go run could execute arbitrary .go files; -ldflags could inject build-time code
1070
+ go: [/^-ldflags$/],
1071
+ // bun --preload is similar to node --require
1072
+ bun: [/^--preload$/]
1073
+ };
1074
+ function validateArgs(cmd, args) {
1075
+ const blocked = BLOCKED_ARG_PATTERNS[cmd];
1076
+ if (!blocked) return null;
1077
+ for (const arg of args) {
1078
+ for (const pattern of blocked) {
1079
+ if (pattern.test(arg)) {
1080
+ return `Blocked argument "${arg}" for command "${cmd}" (matches security pattern ${pattern})`;
1081
+ }
1082
+ }
1083
+ }
1084
+ return null;
1085
+ }
1062
1086
  var execTool = {
1063
1087
  name: "exec",
1064
1088
  category: "Shell",
@@ -1102,6 +1126,18 @@ var execTool = {
1102
1126
  }
1103
1127
  const args = (input.args ?? []).slice(0, MAX_ARGS);
1104
1128
  const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));
1129
+ const argError = validateArgs(cmd, args);
1130
+ if (argError) {
1131
+ return {
1132
+ command: cmd,
1133
+ args,
1134
+ stdout: "",
1135
+ stderr: argError,
1136
+ exitCode: 1,
1137
+ truncated: false,
1138
+ allowed: false
1139
+ };
1140
+ }
1105
1141
  const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
1106
1142
  const rel = path.relative(ctx.projectRoot, requestedCwd);
1107
1143
  if (rel.startsWith("..") || path.isAbsolute(rel)) {
@@ -1121,7 +1157,7 @@ var execTool = {
1121
1157
  }
1122
1158
  };
1123
1159
  function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1124
- return new Promise((resolve4) => {
1160
+ return new Promise((resolve5) => {
1125
1161
  let stdout = "";
1126
1162
  let stderr = "";
1127
1163
  let killed = false;
@@ -1143,7 +1179,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1143
1179
  });
1144
1180
  child.on("close", (code) => {
1145
1181
  clearTimeout(timer);
1146
- resolve4({
1182
+ resolve5({
1147
1183
  command: cmd,
1148
1184
  args,
1149
1185
  stdout: stdout.slice(0, MAX_OUTPUT2),
@@ -1155,7 +1191,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1155
1191
  });
1156
1192
  child.on("error", (err) => {
1157
1193
  clearTimeout(timer);
1158
- resolve4({
1194
+ resolve5({
1159
1195
  command: cmd,
1160
1196
  args,
1161
1197
  stdout: stdout.slice(0, MAX_OUTPUT2),
@@ -1946,7 +1982,7 @@ function buildArgs(input) {
1946
1982
  }
1947
1983
  }
1948
1984
  function runGit(args, cwd, signal) {
1949
- return new Promise((resolve4) => {
1985
+ return new Promise((resolve5) => {
1950
1986
  let stdout = "";
1951
1987
  let stderr = "";
1952
1988
  const child = spawn("git", args, {
@@ -1965,7 +2001,7 @@ function runGit(args, cwd, signal) {
1965
2001
  }
1966
2002
  });
1967
2003
  child.on("error", (err) => {
1968
- resolve4({
2004
+ resolve5({
1969
2005
  command: args[0],
1970
2006
  stdout,
1971
2007
  stderr: err.message,
@@ -1974,7 +2010,7 @@ function runGit(args, cwd, signal) {
1974
2010
  });
1975
2011
  });
1976
2012
  child.on("close", (code) => {
1977
- resolve4({
2013
+ resolve5({
1978
2014
  command: args[0],
1979
2015
  stdout: stdout.slice(0, MAX_OUTPUT3),
1980
2016
  stderr: stderr.slice(0, MAX_OUTPUT3),
@@ -2070,7 +2106,7 @@ function stripPathComponents(p, strip) {
2070
2106
  return parts.slice(strip).join("/");
2071
2107
  }
2072
2108
  function runPatch(args, cwd, signal) {
2073
- return new Promise((resolve4) => {
2109
+ return new Promise((resolve5) => {
2074
2110
  let stdout = "";
2075
2111
  let stderr = "";
2076
2112
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
@@ -2081,8 +2117,8 @@ function runPatch(args, cwd, signal) {
2081
2117
  child.stderr?.on("data", (c) => {
2082
2118
  stderr += c.toString();
2083
2119
  });
2084
- child.on("close", (code) => resolve4({ exitCode: code ?? 1, stdout, stderr }));
2085
- child.on("error", (e) => resolve4({ exitCode: 1, stdout: "", stderr: e.message }));
2120
+ child.on("close", (code) => resolve5({ exitCode: code ?? 1, stdout, stderr }));
2121
+ child.on("error", (e) => resolve5({ exitCode: 1, stdout: "", stderr: e.message }));
2086
2122
  });
2087
2123
  }
2088
2124
  function extractPatchedFiles(output) {
@@ -2284,7 +2320,7 @@ function findGitDir2(cwd) {
2284
2320
  return null;
2285
2321
  }
2286
2322
  function runGit2(args, cwd, signal) {
2287
- return new Promise((resolve4) => {
2323
+ return new Promise((resolve5) => {
2288
2324
  let stdout = "";
2289
2325
  let stderr = "";
2290
2326
  const child = spawn("git", args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
@@ -2294,8 +2330,8 @@ function runGit2(args, cwd, signal) {
2294
2330
  child.stderr?.on("data", (c) => {
2295
2331
  stderr += c.toString();
2296
2332
  });
2297
- child.on("close", (code) => resolve4({ stdout, stderr, exitCode: code ?? 0 }));
2298
- child.on("error", (e) => resolve4({ stdout: "", stderr: e.message, exitCode: 1 }));
2333
+ child.on("close", (code) => resolve5({ stdout, stderr, exitCode: code ?? 0 }));
2334
+ child.on("error", (e) => resolve5({ stdout: "", stderr: e.message, exitCode: 1 }));
2299
2335
  });
2300
2336
  }
2301
2337
  async function fileDiff(input, ctx, signal) {
@@ -2542,8 +2578,8 @@ async function* spawnStream(opts) {
2542
2578
  let spawnFailed = false;
2543
2579
  for (; ; ) {
2544
2580
  while (queue.length === 0) {
2545
- await new Promise((resolve4) => {
2546
- waiter = resolve4;
2581
+ await new Promise((resolve5) => {
2582
+ waiter = resolve5;
2547
2583
  });
2548
2584
  }
2549
2585
  const chunk = queue.shift();
@@ -3047,6 +3083,22 @@ var installTool = {
3047
3083
  const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
3048
3084
  (p) => p.trim()
3049
3085
  ) : [];
3086
+ const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
3087
+ for (const pkg of pkgList) {
3088
+ if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-")) {
3089
+ yield {
3090
+ type: "final",
3091
+ output: {
3092
+ packages: pkgList,
3093
+ exit_code: 1,
3094
+ output: `Invalid package name "${pkg}". Names must match ${PKG_NAME_RE} and not start with "-".`,
3095
+ dry_run: Boolean(input.dry_run),
3096
+ truncated: false
3097
+ }
3098
+ };
3099
+ return;
3100
+ }
3101
+ }
3050
3102
  if (pkgList.length > 0) args.push(...pkgList);
3051
3103
  yield {
3052
3104
  type: "log",
@@ -3247,7 +3299,7 @@ async function detectManager2(cwd) {
3247
3299
  return "npm";
3248
3300
  }
3249
3301
  function runOutdated(manager, args, cwd, signal) {
3250
- return new Promise((resolve4) => {
3302
+ return new Promise((resolve5) => {
3251
3303
  let stdout = "";
3252
3304
  let stderr = "";
3253
3305
  const MAX = 1e5;
@@ -3260,11 +3312,11 @@ function runOutdated(manager, args, cwd, signal) {
3260
3312
  });
3261
3313
  child.on("close", (code) => {
3262
3314
  const result = parseOutdatedOutput(stdout, code ?? 0);
3263
- resolve4(result);
3315
+ resolve5(result);
3264
3316
  });
3265
3317
  child.on(
3266
3318
  "error",
3267
- (e) => resolve4({
3319
+ (e) => resolve5({
3268
3320
  exit_code: 1,
3269
3321
  packages: [],
3270
3322
  total: 0,
@@ -3378,8 +3430,17 @@ var logsTool = {
3378
3430
  async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
3379
3431
  const args = ["logs"];
3380
3432
  if (lines > 0) args.push("--tail", String(lines));
3433
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]+$/.test(service)) {
3434
+ return {
3435
+ source: `docker:${service}`,
3436
+ entries: [],
3437
+ total: 0,
3438
+ truncated: false,
3439
+ stream_mode: false
3440
+ };
3441
+ }
3381
3442
  args.push("--timestamps", service);
3382
- return new Promise((resolve4) => {
3443
+ return new Promise((resolve5) => {
3383
3444
  let stdout = "";
3384
3445
  let stderr = "";
3385
3446
  const MAX = 2e5;
@@ -3393,7 +3454,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
3393
3454
  child.on("close", (code) => {
3394
3455
  const output = stdout + stderr;
3395
3456
  const entries = parseLogLines(output, filterRe);
3396
- resolve4({
3457
+ resolve5({
3397
3458
  source: `docker:${service}`,
3398
3459
  entries,
3399
3460
  total: entries.length,
@@ -3403,7 +3464,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
3403
3464
  });
3404
3465
  child.on(
3405
3466
  "error",
3406
- (e) => resolve4({
3467
+ (e) => resolve5({
3407
3468
  source: `docker:${service}`,
3408
3469
  entries: [],
3409
3470
  total: 0,
@@ -3764,7 +3825,7 @@ var scaffoldTool = {
3764
3825
  const vars = { name, ...input.vars };
3765
3826
  const builtIn = BUILT_IN_TEMPLATES[input.template];
3766
3827
  if (builtIn) {
3767
- return await handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
3828
+ return await handleBuiltIn(name, builtIn.files, cwd, ctx, input.dry_run ?? false, vars);
3768
3829
  }
3769
3830
  return {
3770
3831
  template: input.template,
@@ -3776,12 +3837,19 @@ var scaffoldTool = {
3776
3837
  };
3777
3838
  }
3778
3839
  };
3779
- async function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
3840
+ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
3780
3841
  const files = [];
3781
3842
  let filesCreated = 0;
3782
3843
  for (const [filePath, content] of Object.entries(templateFiles)) {
3783
3844
  const resolvedPath = substituteVars(filePath, name, vars);
3784
- const fullPath = path.join(cwd, resolvedPath);
3845
+ const joinedPath = path.join(cwd, resolvedPath);
3846
+ const root = path.resolve(ctx.projectRoot);
3847
+ const target = path.resolve(joinedPath);
3848
+ const rel = path.relative(root, target);
3849
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
3850
+ throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
3851
+ }
3852
+ const fullPath = target;
3785
3853
  if (!dryRun) {
3786
3854
  await fs4.mkdir(path.dirname(fullPath), { recursive: true });
3787
3855
  await fs4.writeFile(fullPath, substituteVars(content, name, vars), "utf8");