@wrongstack/tools 0.3.3 → 0.3.7

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
@@ -67,7 +67,14 @@ var readTool = {
67
67
  async execute(input, ctx) {
68
68
  if (!input?.path) throw new Error("read: path is required");
69
69
  const absPath = safeResolve(input.path, ctx);
70
- const stat9 = await fs4.stat(absPath);
70
+ let stat9;
71
+ try {
72
+ stat9 = await fs4.stat(absPath);
73
+ } catch (err) {
74
+ const code = err.code;
75
+ if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
76
+ throw new Error(`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`);
77
+ }
71
78
  if (!stat9.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
72
79
  if (stat9.size > MAX_BYTES) {
73
80
  throw new Error(`read: file too large (${stat9.size} bytes, limit ${MAX_BYTES})`);
@@ -125,11 +132,11 @@ var writeTool = {
125
132
  existed = stat10.isFile();
126
133
  if (existed) {
127
134
  if (!ctx.hasRead(absPath)) {
128
- throw new Error(
129
- `write: file "${input.path}" exists but was not read in this session. Read it first.`
130
- );
135
+ prev = await fs4.readFile(absPath, "utf8");
136
+ ctx.recordRead(absPath, stat10.mtimeMs);
137
+ } else {
138
+ prev = await fs4.readFile(absPath, "utf8");
131
139
  }
132
- prev = await fs4.readFile(absPath, "utf8");
133
140
  }
134
141
  } catch (err) {
135
142
  if (err.code !== "ENOENT") {
@@ -436,13 +443,13 @@ async function globFiles(pattern, base, extraGlob) {
436
443
  return await globNative(pattern, base, extraGlob);
437
444
  }
438
445
  function checkRg() {
439
- return new Promise((resolve4) => {
446
+ return new Promise((resolve5) => {
440
447
  try {
441
448
  const p = spawn("rg", ["--version"], { stdio: "ignore" });
442
- p.on("error", () => resolve4(false));
443
- p.on("close", (code) => resolve4(code === 0));
449
+ p.on("error", () => resolve5(false));
450
+ p.on("close", (code) => resolve5(code === 0));
444
451
  } catch {
445
- resolve4(false);
452
+ resolve5(false);
446
453
  }
447
454
  });
448
455
  }
@@ -454,10 +461,10 @@ function spawnRgFind(pattern, base) {
454
461
  buf += chunk.toString();
455
462
  });
456
463
  return {
457
- promise: new Promise((resolve4, reject) => {
464
+ promise: new Promise((resolve5, reject) => {
458
465
  child.on("error", reject);
459
466
  child.on("close", () => {
460
- resolve4(buf.split("\n").filter(Boolean));
467
+ resolve5(buf.split("\n").filter(Boolean));
461
468
  });
462
469
  })
463
470
  };
@@ -621,13 +628,13 @@ var grepTool = {
621
628
  }
622
629
  };
623
630
  async function detectRg(signal) {
624
- return new Promise((resolve4) => {
631
+ return new Promise((resolve5) => {
625
632
  try {
626
633
  const p = spawn("rg", ["--version"], { stdio: "ignore", signal });
627
- p.on("error", () => resolve4(false));
628
- p.on("close", (code) => resolve4(code === 0));
634
+ p.on("error", () => resolve5(false));
635
+ p.on("close", (code) => resolve5(code === 0));
629
636
  } catch {
630
- resolve4(false);
637
+ resolve5(false);
631
638
  }
632
639
  });
633
640
  }
@@ -882,12 +889,41 @@ var bashTool = {
882
889
  signal: opts.signal
883
890
  });
884
891
  if (input.background) {
885
- const pid = child.pid;
886
- if (typeof pid === "number") child.unref();
892
+ let buf2 = "";
893
+ let truncated = false;
894
+ const child2 = spawn(shell, args, {
895
+ cwd: ctx.projectRoot,
896
+ env,
897
+ stdio: ["ignore", "pipe", "pipe"],
898
+ detached: true,
899
+ signal: opts.signal
900
+ });
901
+ const pid = child2.pid;
902
+ child2.stdout?.on("data", (chunk) => {
903
+ if (!truncated) {
904
+ const remain = MAX_OUTPUT - buf2.length;
905
+ if (remain > 0) {
906
+ buf2 += chunk.toString().slice(0, remain);
907
+ }
908
+ if (buf2.length >= MAX_OUTPUT) truncated = true;
909
+ }
910
+ });
911
+ child2.stderr?.on("data", (chunk) => {
912
+ if (!truncated) {
913
+ const remain = MAX_OUTPUT - buf2.length;
914
+ if (remain > 0) {
915
+ buf2 += chunk.toString().slice(0, remain);
916
+ }
917
+ if (buf2.length >= MAX_OUTPUT) truncated = true;
918
+ }
919
+ });
920
+ child2.on("close", () => {
921
+ });
922
+ if (typeof pid === "number") child2.unref();
887
923
  yield {
888
924
  type: "final",
889
925
  output: {
890
- output: `[background] pid=${pid ?? "unknown"}`,
926
+ output: truncated ? buf2.slice(0, MAX_OUTPUT) + "\u2026[truncated]" : buf2,
891
927
  exit_code: null,
892
928
  timed_out: false,
893
929
  pid
@@ -932,6 +968,7 @@ var bashTool = {
932
968
  }
933
969
  }, 2e3);
934
970
  timers.push(killTimer);
971
+ killTimer.unref?.();
935
972
  } catch {
936
973
  }
937
974
  }
@@ -949,10 +986,10 @@ var bashTool = {
949
986
  queue.push(c);
950
987
  }
951
988
  };
952
- const next = () => new Promise((resolve4) => {
989
+ const next = () => new Promise((resolve5) => {
953
990
  const c = queue.shift();
954
- if (c) resolve4(c);
955
- else resolveNext = resolve4;
991
+ if (c) resolve5(c);
992
+ else resolveNext = resolve5;
956
993
  });
957
994
  let lastFlush = Date.now();
958
995
  const flush = () => {
@@ -1051,7 +1088,7 @@ var ALLOWED_COMMANDS = {
1051
1088
  cargo: ["--version", "build", "test", "check"],
1052
1089
  rustc: ["--version"],
1053
1090
  go: ["version", "run", "build", "test"],
1054
- python: ["--version", "-c"],
1091
+ python: ["--version"],
1055
1092
  pip: ["--version", "install", "list"],
1056
1093
  docker: ["--version", "ps", "images", "build"],
1057
1094
  kubectl: ["version", "get", "describe", "logs"]
@@ -1059,6 +1096,30 @@ var ALLOWED_COMMANDS = {
1059
1096
  var MAX_ARGS = 20;
1060
1097
  var MAX_OUTPUT2 = 2e5;
1061
1098
  var TIMEOUT_MS = 3e4;
1099
+ var BLOCKED_ARG_PATTERNS = {
1100
+ // python -c/--command executes arbitrary code; python -m runs modules
1101
+ python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
1102
+ // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack
1103
+ git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/],
1104
+ // node -r/--require preloads arbitrary modules; --eval executes code
1105
+ node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
1106
+ // go run could execute arbitrary .go files; -ldflags could inject build-time code
1107
+ go: [/^-ldflags$/],
1108
+ // bun --preload is similar to node --require
1109
+ bun: [/^--preload$/]
1110
+ };
1111
+ function validateArgs(cmd, args) {
1112
+ const blocked = BLOCKED_ARG_PATTERNS[cmd];
1113
+ if (!blocked) return null;
1114
+ for (const arg of args) {
1115
+ for (const pattern of blocked) {
1116
+ if (pattern.test(arg)) {
1117
+ return `Blocked argument "${arg}" for command "${cmd}" (matches security pattern ${pattern})`;
1118
+ }
1119
+ }
1120
+ }
1121
+ return null;
1122
+ }
1062
1123
  var execTool = {
1063
1124
  name: "exec",
1064
1125
  category: "Shell",
@@ -1102,6 +1163,18 @@ var execTool = {
1102
1163
  }
1103
1164
  const args = (input.args ?? []).slice(0, MAX_ARGS);
1104
1165
  const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));
1166
+ const argError = validateArgs(cmd, args);
1167
+ if (argError) {
1168
+ return {
1169
+ command: cmd,
1170
+ args,
1171
+ stdout: "",
1172
+ stderr: argError,
1173
+ exitCode: 1,
1174
+ truncated: false,
1175
+ allowed: false
1176
+ };
1177
+ }
1105
1178
  const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
1106
1179
  const rel = path.relative(ctx.projectRoot, requestedCwd);
1107
1180
  if (rel.startsWith("..") || path.isAbsolute(rel)) {
@@ -1121,7 +1194,7 @@ var execTool = {
1121
1194
  }
1122
1195
  };
1123
1196
  function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1124
- return new Promise((resolve4) => {
1197
+ return new Promise((resolve5) => {
1125
1198
  let stdout = "";
1126
1199
  let stderr = "";
1127
1200
  let killed = false;
@@ -1143,7 +1216,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1143
1216
  });
1144
1217
  child.on("close", (code) => {
1145
1218
  clearTimeout(timer);
1146
- resolve4({
1219
+ resolve5({
1147
1220
  command: cmd,
1148
1221
  args,
1149
1222
  stdout: stdout.slice(0, MAX_OUTPUT2),
@@ -1155,7 +1228,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1155
1228
  });
1156
1229
  child.on("error", (err) => {
1157
1230
  clearTimeout(timer);
1158
- resolve4({
1231
+ resolve5({
1159
1232
  command: cmd,
1160
1233
  args,
1161
1234
  stdout: stdout.slice(0, MAX_OUTPUT2),
@@ -1946,7 +2019,7 @@ function buildArgs(input) {
1946
2019
  }
1947
2020
  }
1948
2021
  function runGit(args, cwd, signal) {
1949
- return new Promise((resolve4) => {
2022
+ return new Promise((resolve5) => {
1950
2023
  let stdout = "";
1951
2024
  let stderr = "";
1952
2025
  const child = spawn("git", args, {
@@ -1965,7 +2038,7 @@ function runGit(args, cwd, signal) {
1965
2038
  }
1966
2039
  });
1967
2040
  child.on("error", (err) => {
1968
- resolve4({
2041
+ resolve5({
1969
2042
  command: args[0],
1970
2043
  stdout,
1971
2044
  stderr: err.message,
@@ -1974,7 +2047,7 @@ function runGit(args, cwd, signal) {
1974
2047
  });
1975
2048
  });
1976
2049
  child.on("close", (code) => {
1977
- resolve4({
2050
+ resolve5({
1978
2051
  command: args[0],
1979
2052
  stdout: stdout.slice(0, MAX_OUTPUT3),
1980
2053
  stderr: stderr.slice(0, MAX_OUTPUT3),
@@ -2023,7 +2096,7 @@ var patchTool = {
2023
2096
  };
2024
2097
  }
2025
2098
  }
2026
- const tmpDir = await fs4.mkdtemp(path.join(dir, ".wstack_patch_"));
2099
+ const tmpDir = await fs4.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
2027
2100
  try {
2028
2101
  await fs4.chmod(tmpDir, 448).catch(() => {
2029
2102
  });
@@ -2070,7 +2143,7 @@ function stripPathComponents(p, strip) {
2070
2143
  return parts.slice(strip).join("/");
2071
2144
  }
2072
2145
  function runPatch(args, cwd, signal) {
2073
- return new Promise((resolve4) => {
2146
+ return new Promise((resolve5) => {
2074
2147
  let stdout = "";
2075
2148
  let stderr = "";
2076
2149
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
@@ -2081,8 +2154,8 @@ function runPatch(args, cwd, signal) {
2081
2154
  child.stderr?.on("data", (c) => {
2082
2155
  stderr += c.toString();
2083
2156
  });
2084
- child.on("close", (code) => resolve4({ exitCode: code ?? 1, stdout, stderr }));
2085
- child.on("error", (e) => resolve4({ exitCode: 1, stdout: "", stderr: e.message }));
2157
+ child.on("close", (code) => resolve5({ exitCode: code ?? 1, stdout, stderr }));
2158
+ child.on("error", (e) => resolve5({ exitCode: 1, stdout: "", stderr: e.message }));
2086
2159
  });
2087
2160
  }
2088
2161
  function extractPatchedFiles(output) {
@@ -2284,7 +2357,7 @@ function findGitDir2(cwd) {
2284
2357
  return null;
2285
2358
  }
2286
2359
  function runGit2(args, cwd, signal) {
2287
- return new Promise((resolve4) => {
2360
+ return new Promise((resolve5) => {
2288
2361
  let stdout = "";
2289
2362
  let stderr = "";
2290
2363
  const child = spawn("git", args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
@@ -2294,8 +2367,8 @@ function runGit2(args, cwd, signal) {
2294
2367
  child.stderr?.on("data", (c) => {
2295
2368
  stderr += c.toString();
2296
2369
  });
2297
- child.on("close", (code) => resolve4({ stdout, stderr, exitCode: code ?? 0 }));
2298
- child.on("error", (e) => resolve4({ stdout: "", stderr: e.message, exitCode: 1 }));
2370
+ child.on("close", (code) => resolve5({ stdout, stderr, exitCode: code ?? 0 }));
2371
+ child.on("error", (e) => resolve5({ stdout: "", stderr: e.message, exitCode: 1 }));
2299
2372
  });
2300
2373
  }
2301
2374
  async function fileDiff(input, ctx, signal) {
@@ -2542,8 +2615,8 @@ async function* spawnStream(opts) {
2542
2615
  let spawnFailed = false;
2543
2616
  for (; ; ) {
2544
2617
  while (queue.length === 0) {
2545
- await new Promise((resolve4) => {
2546
- waiter = resolve4;
2618
+ await new Promise((resolve5) => {
2619
+ waiter = resolve5;
2547
2620
  });
2548
2621
  }
2549
2622
  const chunk = queue.shift();
@@ -3047,6 +3120,22 @@ var installTool = {
3047
3120
  const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
3048
3121
  (p) => p.trim()
3049
3122
  ) : [];
3123
+ const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
3124
+ for (const pkg of pkgList) {
3125
+ if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-")) {
3126
+ yield {
3127
+ type: "final",
3128
+ output: {
3129
+ packages: pkgList,
3130
+ exit_code: 1,
3131
+ output: `Invalid package name "${pkg}". Names must match ${PKG_NAME_RE} and not start with "-".`,
3132
+ dry_run: Boolean(input.dry_run),
3133
+ truncated: false
3134
+ }
3135
+ };
3136
+ return;
3137
+ }
3138
+ }
3050
3139
  if (pkgList.length > 0) args.push(...pkgList);
3051
3140
  yield {
3052
3141
  type: "log",
@@ -3247,7 +3336,7 @@ async function detectManager2(cwd) {
3247
3336
  return "npm";
3248
3337
  }
3249
3338
  function runOutdated(manager, args, cwd, signal) {
3250
- return new Promise((resolve4) => {
3339
+ return new Promise((resolve5) => {
3251
3340
  let stdout = "";
3252
3341
  let stderr = "";
3253
3342
  const MAX = 1e5;
@@ -3260,11 +3349,11 @@ function runOutdated(manager, args, cwd, signal) {
3260
3349
  });
3261
3350
  child.on("close", (code) => {
3262
3351
  const result = parseOutdatedOutput(stdout, code ?? 0);
3263
- resolve4(result);
3352
+ resolve5(result);
3264
3353
  });
3265
3354
  child.on(
3266
3355
  "error",
3267
- (e) => resolve4({
3356
+ (e) => resolve5({
3268
3357
  exit_code: 1,
3269
3358
  packages: [],
3270
3359
  total: 0,
@@ -3378,8 +3467,17 @@ var logsTool = {
3378
3467
  async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
3379
3468
  const args = ["logs"];
3380
3469
  if (lines > 0) args.push("--tail", String(lines));
3470
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]+$/.test(service)) {
3471
+ return {
3472
+ source: `docker:${service}`,
3473
+ entries: [],
3474
+ total: 0,
3475
+ truncated: false,
3476
+ stream_mode: false
3477
+ };
3478
+ }
3381
3479
  args.push("--timestamps", service);
3382
- return new Promise((resolve4) => {
3480
+ return new Promise((resolve5) => {
3383
3481
  let stdout = "";
3384
3482
  let stderr = "";
3385
3483
  const MAX = 2e5;
@@ -3393,7 +3491,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
3393
3491
  child.on("close", (code) => {
3394
3492
  const output = stdout + stderr;
3395
3493
  const entries = parseLogLines(output, filterRe);
3396
- resolve4({
3494
+ resolve5({
3397
3495
  source: `docker:${service}`,
3398
3496
  entries,
3399
3497
  total: entries.length,
@@ -3403,7 +3501,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
3403
3501
  });
3404
3502
  child.on(
3405
3503
  "error",
3406
- (e) => resolve4({
3504
+ (e) => resolve5({
3407
3505
  source: `docker:${service}`,
3408
3506
  entries: [],
3409
3507
  total: 0,
@@ -3764,7 +3862,7 @@ var scaffoldTool = {
3764
3862
  const vars = { name, ...input.vars };
3765
3863
  const builtIn = BUILT_IN_TEMPLATES[input.template];
3766
3864
  if (builtIn) {
3767
- return await handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
3865
+ return await handleBuiltIn(name, builtIn.files, cwd, ctx, input.dry_run ?? false, vars);
3768
3866
  }
3769
3867
  return {
3770
3868
  template: input.template,
@@ -3776,12 +3874,19 @@ var scaffoldTool = {
3776
3874
  };
3777
3875
  }
3778
3876
  };
3779
- async function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
3877
+ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
3780
3878
  const files = [];
3781
3879
  let filesCreated = 0;
3782
3880
  for (const [filePath, content] of Object.entries(templateFiles)) {
3783
3881
  const resolvedPath = substituteVars(filePath, name, vars);
3784
- const fullPath = path.join(cwd, resolvedPath);
3882
+ const joinedPath = path.join(cwd, resolvedPath);
3883
+ const root = path.resolve(ctx.projectRoot);
3884
+ const target = path.resolve(joinedPath);
3885
+ const rel = path.relative(root, target);
3886
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
3887
+ throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
3888
+ }
3889
+ const fullPath = target;
3785
3890
  if (!dryRun) {
3786
3891
  await fs4.mkdir(path.dirname(fullPath), { recursive: true });
3787
3892
  await fs4.writeFile(fullPath, substituteVars(content, name, vars), "utf8");