@wrongstack/tools 0.277.2 → 0.280.1

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/pack.js CHANGED
@@ -1753,7 +1753,7 @@ function looksLikePowerShell(command) {
1753
1753
  if (/&\s+\$/.test(trimmed)) return true;
1754
1754
  if (/(^|\s)@\s*\(/.test(trimmed)) return true;
1755
1755
  if (/(^|\s)@\{/.test(trimmed)) return true;
1756
- if (/(?:^|[\s\[\(\{,;])(?:-eq|-ne|-lt|-gt|-le|-ge|-like|-notlike|-match|-notmatch|-contains|-notcontains|-in|-notin|-and|-or|-not|-band|-bor|-bxor|-replace|-isplit|-csplit|-osplit|-join|-is|-as|-f)(?:$|[\s\]\)\},;])/i.test(trimmed)) {
1756
+ if (/(?:^|[\s[({,;])(?:-eq|-ne|-lt|-gt|-le|-ge|-like|-notlike|-match|-notmatch|-contains|-notcontains|-in|-notin|-and|-or|-not|-band|-bor|-bxor|-replace|-isplit|-csplit|-osplit|-join|-is|-as|-f)(?:$|[\s\])},;])/i.test(trimmed)) {
1757
1757
  return true;
1758
1758
  }
1759
1759
  if (PS_VERB_RE.test(trimmed)) return true;
@@ -1780,7 +1780,7 @@ function looksLikePowerShellExtended(command) {
1780
1780
  return true;
1781
1781
  }
1782
1782
  if (/^\s*<#|#>\s*$/m.test(trimmed)) return true;
1783
- if (/(?:^|\s)[-/\/](?:AsPlainText|PipelineVariable|pv|FilterHashtable|OutVariable|ov)(?:\s|=|$)/i.test(trimmed)) {
1783
+ if (/(?:^|\s)[-//](?:AsPlainText|PipelineVariable|pv|FilterHashtable|OutVariable|ov)(?:\s|=|$)/i.test(trimmed)) {
1784
1784
  return true;
1785
1785
  }
1786
1786
  return false;
@@ -3713,9 +3713,8 @@ func formatType(t ast.Expr) string {
3713
3713
  }
3714
3714
  `;
3715
3715
  async function syncGoParse(filePath, content, lang) {
3716
- const tmpDir = path3.join(os2.tmpdir(), "ws-go-parse");
3716
+ const tmpDir = await fs2.mkdtemp(path3.join(os2.tmpdir(), "ws-go-parse-"));
3717
3717
  try {
3718
- await fs2.mkdir(tmpDir, { recursive: true });
3719
3718
  const scriptPath = path3.join(tmpDir, "parse.go");
3720
3719
  await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
3721
3720
  const proc = spawn("go", ["run", scriptPath], {
@@ -3759,6 +3758,8 @@ async function syncGoParse(filePath, content, lang) {
3759
3758
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
3760
3759
  } catch {
3761
3760
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
3761
+ } finally {
3762
+ await fs2.rm(tmpDir, { recursive: true, force: true });
3762
3763
  }
3763
3764
  }
3764
3765
  async function parseSymbols3(opts) {
@@ -6091,6 +6092,294 @@ function findSimilarity(haystack, needle) {
6091
6092
  }
6092
6093
  return line;
6093
6094
  }
6095
+
6096
+ // src/_danger-detect.ts
6097
+ var argHas = (args, value) => args.includes(value);
6098
+ var argMatches = (args, re) => args.some((a) => re.test(a));
6099
+ var hasShortFlags = (args, letters) => {
6100
+ const seen = /* @__PURE__ */ new Set();
6101
+ for (const a of args) {
6102
+ if (!a.startsWith("-") || a.startsWith("--")) continue;
6103
+ for (const ch of a.replace(/^-+/, "")) seen.add(ch);
6104
+ }
6105
+ return letters.split("").every((l) => seen.has(l));
6106
+ };
6107
+ var RULES = [
6108
+ // ----- rm / rmdir: recursive force delete (any path) -----
6109
+ // Note: BLOCKED_ARG_PATTERNS already hard-denies root/home/glob paths,
6110
+ // but `rm -rf ./build` is a normal dev workflow that the user might
6111
+ // want to do intentionally. We downgrade it to 'destructive' so the
6112
+ // confirm prompt can approve.
6113
+ {
6114
+ id: "rm-recursive",
6115
+ level: "destructive",
6116
+ test: (cmd, args) => (cmd === "rm" || cmd === "rmdir") && hasShortFlags(args, "rf"),
6117
+ reason: "recursive force-delete"
6118
+ },
6119
+ // ----- Windows PowerShell Remove-Item: -Recurse -Force -----
6120
+ {
6121
+ id: "powershell-remove-item-recursive-force",
6122
+ level: "destructive",
6123
+ test: (cmd, args) => {
6124
+ if (cmd !== "powershell" && cmd !== "pwsh") return false;
6125
+ const hasRecurse = argMatches(args, /^-(?:R|Recurse|Recurse\s)/);
6126
+ const hasForce = argHas(args, "-Force") || argHas(args, "-F");
6127
+ if (argHas(args, "-WhatIf")) return false;
6128
+ return hasRecurse && hasForce;
6129
+ },
6130
+ reason: "Remove-Item with -Recurse -Force"
6131
+ },
6132
+ // ----- find -exec / -ok / -execdir -----
6133
+ {
6134
+ id: "find-exec",
6135
+ level: "destructive",
6136
+ test: (cmd, args) => {
6137
+ if (cmd !== "find") return false;
6138
+ return args.some(
6139
+ (a) => a === "-exec" || a === "-exec;" || a === "-ok" || a === "-ok;" || a === "-execdir" || a === "-execdir;" || a.startsWith("-exec=") || a.startsWith("-ok=") || a.startsWith("-execdir=")
6140
+ );
6141
+ },
6142
+ reason: "find with -exec/-ok (executes arbitrary command on matches)"
6143
+ },
6144
+ // ----- git --exec= / --upload-pack= / --receive-pack= -----
6145
+ // These run arbitrary commands via the git transport layer.
6146
+ {
6147
+ id: "git-exec",
6148
+ level: "destructive",
6149
+ test: (cmd, args) => cmd === "git" && args.some(
6150
+ (a) => a.startsWith("--exec=") || a.startsWith("--upload-pack=") || a.startsWith("--receive-pack=") || a === "--exec" || a === "--upload-pack" || a === "--receive-pack"
6151
+ ),
6152
+ reason: "git with --exec/--upload-pack/--receive-pack (runs arbitrary code)"
6153
+ },
6154
+ // ----- Windows: format / diskpart / bcdedit -----
6155
+ {
6156
+ id: "win32-format",
6157
+ level: "destructive",
6158
+ test: (cmd) => cmd === "format" || cmd === "format.exe",
6159
+ reason: "format (Windows disk format)"
6160
+ },
6161
+ {
6162
+ id: "win32-diskpart",
6163
+ level: "destructive",
6164
+ test: (cmd) => cmd === "diskpart" || cmd === "diskpart.exe",
6165
+ reason: "diskpart (Windows partition editor)"
6166
+ },
6167
+ {
6168
+ id: "win32-bcdedit",
6169
+ level: "destructive",
6170
+ test: (cmd) => cmd === "bcdedit" || cmd === "bcdedit.exe",
6171
+ reason: "bcdedit (Windows boot config editor)"
6172
+ },
6173
+ // ----- mkfs family -----
6174
+ {
6175
+ id: "mkfs",
6176
+ level: "destructive",
6177
+ test: (cmd) => /^mkfs(\.[a-z0-9]+)?$/.test(cmd) || cmd === "mkswap",
6178
+ reason: "mkfs (filesystem creation \u2014 destroys existing data)"
6179
+ },
6180
+ // ----- dd writing to a block device -----
6181
+ {
6182
+ id: "dd-to-block-device",
6183
+ level: "destructive",
6184
+ test: (cmd, args) => {
6185
+ if (cmd !== "dd") return false;
6186
+ return args.some((a) => /of=\/dev\/(sd|hd|nvme|vd|mmcblk|xvd|loop|disk)/.test(a));
6187
+ },
6188
+ reason: "dd writing to a block device"
6189
+ },
6190
+ // ----- Secure-erase tools -----
6191
+ {
6192
+ id: "shred",
6193
+ level: "destructive",
6194
+ test: (cmd) => cmd === "shred" || cmd === "shred.exe",
6195
+ reason: "shred (secure file delete)"
6196
+ },
6197
+ {
6198
+ id: "wipefs",
6199
+ level: "destructive",
6200
+ test: (cmd) => cmd === "wipefs" || cmd === "wipefs.exe",
6201
+ reason: "wipefs (signature wipe \u2014 destroys filesystem headers)"
6202
+ },
6203
+ {
6204
+ id: "sdelete",
6205
+ level: "destructive",
6206
+ test: (cmd) => cmd === "sdelete" || cmd === "sdelete.exe",
6207
+ reason: "sdelete (Sysinternals secure delete)"
6208
+ },
6209
+ // ----- VCS history rewrite (destructive) -----
6210
+ // `git push --force` / `-f` rewrites remote history. `--force-with-lease`
6211
+ // is the safer variant (checks remote hasn't moved) but still rewrites.
6212
+ {
6213
+ id: "git-push-force",
6214
+ level: "destructive",
6215
+ test: (cmd, args) => {
6216
+ if (cmd !== "git") return false;
6217
+ const pushIdx = args.indexOf("push");
6218
+ if (pushIdx < 0) return false;
6219
+ for (let i = pushIdx + 1; i < args.length; i++) {
6220
+ const a = args[i];
6221
+ if (a === "--force" || a === "-f" || a === "--force-with-lease") return true;
6222
+ if (!a.startsWith("-") && !a.includes("=")) continue;
6223
+ if (a.startsWith("--force")) return true;
6224
+ }
6225
+ return false;
6226
+ },
6227
+ reason: "git push with --force / -f (rewrites remote history)"
6228
+ },
6229
+ // ----- git reset --hard (destructive) -----
6230
+ {
6231
+ id: "git-reset-hard",
6232
+ level: "destructive",
6233
+ test: (cmd, args) => cmd === "git" && args.some((a) => a === "--hard" || a.startsWith("--hard=")),
6234
+ reason: "git reset --hard (discards working tree + index)"
6235
+ },
6236
+ // ----- git clean -f / -fd (destructive) -----
6237
+ {
6238
+ id: "git-clean-force",
6239
+ level: "destructive",
6240
+ test: (cmd, args) => {
6241
+ if (cmd !== "git") return false;
6242
+ const cleanIdx = args.indexOf("clean");
6243
+ if (cleanIdx < 0) return false;
6244
+ return args.slice(cleanIdx + 1).some(
6245
+ (a) => a === "-f" || a === "--force" || a.startsWith("-f") || a.startsWith("--force=")
6246
+ );
6247
+ },
6248
+ reason: "git clean -f (deletes untracked files)"
6249
+ },
6250
+ // ----- package publish (destructive — public, irreversible) -----
6251
+ {
6252
+ id: "npm-publish",
6253
+ level: "destructive",
6254
+ test: (cmd, args) => {
6255
+ if (!["npm", "pnpm", "yarn", "bun", "cargo"].includes(cmd)) return false;
6256
+ return args.includes("publish") || cmd === "cargo" && args.includes("yank");
6257
+ },
6258
+ reason: "publishing to a public package registry (hard to reverse)"
6259
+ },
6260
+ // ----- k8s cluster-wide destructive ops (destructive) -----
6261
+ {
6262
+ id: "kubectl-delete-namespace",
6263
+ level: "destructive",
6264
+ test: (cmd, args) => {
6265
+ if (cmd !== "kubectl") return false;
6266
+ const delIdx = args.indexOf("delete");
6267
+ if (delIdx < 0) return false;
6268
+ const after = args.slice(delIdx + 1);
6269
+ return after[0] === "namespace" || after[0] === "ns";
6270
+ },
6271
+ reason: "kubectl delete namespace (deletes all resources in the namespace)"
6272
+ },
6273
+ {
6274
+ id: "kubectl-drain",
6275
+ level: "destructive",
6276
+ test: (cmd, args) => cmd === "kubectl" && args.includes("drain"),
6277
+ reason: "kubectl drain (evicts pods, marks node unschedulable)"
6278
+ },
6279
+ // ----- inline code evaluation (caution — high false-positive) -----
6280
+ // Common in scripts: `python -c "..."`, `node -e "..."`, `bash -c "..."`.
6281
+ // We tag 'caution' rather than 'destructive' because these are used in
6282
+ // many legitimate one-liners (e.g. `python -c "print(1)"`).
6283
+ {
6284
+ id: "inline-eval",
6285
+ level: "caution",
6286
+ test: (cmd, args) => {
6287
+ if (![
6288
+ "python",
6289
+ "python3",
6290
+ "python2",
6291
+ "node",
6292
+ "bash",
6293
+ "sh",
6294
+ "zsh",
6295
+ "ruby",
6296
+ "perl",
6297
+ "lua"
6298
+ ].includes(cmd)) {
6299
+ return false;
6300
+ }
6301
+ return args.some(
6302
+ (a) => a === "-c" || a === "-e" || a === "--eval" || a === "-eval" || a === "-E"
6303
+ );
6304
+ },
6305
+ reason: "inline script evaluation (-c / -e / --eval)"
6306
+ },
6307
+ // ----- pipe-to-shell (caution — well-known exfil pattern) -----
6308
+ // The classic `curl https://... | sh` download-and-run vector. Detected by
6309
+ // looking for a known fetcher followed by a shell sink. We use a simple
6310
+ // substring scan; false positives are limited because both tokens must
6311
+ // appear in the same argv.
6312
+ {
6313
+ id: "pipe-to-shell",
6314
+ level: "caution",
6315
+ test: (_cmd, args) => {
6316
+ const hasFetcher = args.some(
6317
+ (a) => /^(curl|wget|fetch|httpie|http)$/i.test(a) || a.startsWith("curl") || a.startsWith("wget")
6318
+ );
6319
+ const hasShellSink = args.some(
6320
+ (a) => a === "sh" || a === "bash" || a === "zsh" || a === "fish" || a === "pwsh" || a === "powershell" || a.endsWith("/sh") || a.endsWith("/bash") || a.endsWith("/zsh") || a.endsWith("/pwsh")
6321
+ );
6322
+ return hasFetcher && hasShellSink;
6323
+ },
6324
+ reason: "network fetch piped to a shell (download-and-run pattern)"
6325
+ },
6326
+ // ----- privilege escalation (caution) -----
6327
+ {
6328
+ id: "sudo",
6329
+ level: "caution",
6330
+ test: (cmd) => cmd === "sudo" || cmd === "doas",
6331
+ reason: "privilege escalation (sudo / doas)"
6332
+ },
6333
+ {
6334
+ id: "runas",
6335
+ level: "caution",
6336
+ test: (cmd) => cmd === "runas" || cmd === "runas.exe",
6337
+ reason: "Windows runas (run as different user)"
6338
+ },
6339
+ // ----- world-writable permissions (caution) -----
6340
+ // `chmod 777` is rarely correct. `chmod -R 777` is almost always wrong.
6341
+ // We only flag octal modes; symbolic modes like `chmod o+w` are
6342
+ // left to the operator's discretion.
6343
+ {
6344
+ id: "chmod-world-writable",
6345
+ level: "caution",
6346
+ test: (cmd, args) => {
6347
+ if (cmd !== "chmod") return false;
6348
+ return args.some((a) => /^[0-7]{3,4}$/.test(a) && /7/.test(a));
6349
+ },
6350
+ reason: "chmod with world-writable octal mode (e.g. 777)"
6351
+ }
6352
+ ];
6353
+ function detectDanger(cmd, args, bypass) {
6354
+ const reasons = [];
6355
+ let level = "safe";
6356
+ let matchedRule;
6357
+ for (const rule of RULES) {
6358
+ if (bypass?.has(rule.id)) continue;
6359
+ if (!rule.test(cmd, args)) continue;
6360
+ reasons.push(rule.reason);
6361
+ matchedRule = rule.id;
6362
+ if (levelRank(rule.level) > levelRank(level)) {
6363
+ level = rule.level;
6364
+ }
6365
+ }
6366
+ if (level === "safe") return { level: "safe", reasons: [] };
6367
+ const result = { level, reasons };
6368
+ if (matchedRule !== void 0) result.matchedRule = matchedRule;
6369
+ return result;
6370
+ }
6371
+ function levelRank(level) {
6372
+ switch (level) {
6373
+ case "safe":
6374
+ return 0;
6375
+ case "caution":
6376
+ return 1;
6377
+ case "destructive":
6378
+ return 2;
6379
+ }
6380
+ }
6381
+
6382
+ // src/exec.ts
6094
6383
  var isWin3 = process.platform === "win32";
6095
6384
  var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
6096
6385
  // JS / TS toolchain
@@ -6698,6 +6987,7 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
6698
6987
  ]);
6699
6988
  var allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
6700
6989
  var normalizeCmd = (c) => c.trim();
6990
+ var dangerBypass = /* @__PURE__ */ new Set();
6701
6991
  function isExecCommandAllowed(cmd) {
6702
6992
  return allowedCommands.has(normalizeCmd(cmd));
6703
6993
  }
@@ -6786,6 +7076,7 @@ function validateArgs(cmd, args) {
6786
7076
  }
6787
7077
  return null;
6788
7078
  }
7079
+ var SAFE_DANGER = { level: "safe", reasons: [] };
6789
7080
  var execTool = {
6790
7081
  name: "exec",
6791
7082
  category: "Shell",
@@ -6830,7 +7121,8 @@ var execTool = {
6830
7121
  stderr: "Circuit breaker is open \u2014 too many consecutive failures. Use /kill reset to recover.",
6831
7122
  exitCode: 1,
6832
7123
  truncated: false,
6833
- allowed: false
7124
+ allowed: false,
7125
+ danger: SAFE_DANGER
6834
7126
  };
6835
7127
  }
6836
7128
  const cmd = input.command.trim();
@@ -6842,7 +7134,8 @@ var execTool = {
6842
7134
  stderr: "Empty command",
6843
7135
  exitCode: 1,
6844
7136
  truncated: false,
6845
- allowed: false
7137
+ allowed: false,
7138
+ danger: SAFE_DANGER
6846
7139
  };
6847
7140
  if (!isExecCommandAllowed(cmd)) {
6848
7141
  return {
@@ -6852,11 +7145,13 @@ var execTool = {
6852
7145
  stderr: `Command "${cmd}" not in allowlist. Add it to your ~/.wrongstack/config.json under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
6853
7146
  exitCode: 1,
6854
7147
  truncated: false,
6855
- allowed: false
7148
+ allowed: false,
7149
+ danger: SAFE_DANGER
6856
7150
  };
6857
7151
  }
6858
7152
  const args = (input.args ?? []).slice(0, MAX_ARGS);
6859
7153
  const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS2, DEFAULT_TIMEOUT_MS2));
7154
+ const danger = detectDanger(cmd, args, dangerBypass);
6860
7155
  const argError = validateArgs(cmd, args);
6861
7156
  if (argError) {
6862
7157
  return {
@@ -6866,7 +7161,8 @@ var execTool = {
6866
7161
  stderr: argError,
6867
7162
  exitCode: 1,
6868
7163
  truncated: false,
6869
- allowed: false
7164
+ allowed: false,
7165
+ danger
6870
7166
  };
6871
7167
  }
6872
7168
  let cwd;
@@ -6880,14 +7176,15 @@ var execTool = {
6880
7176
  stderr: `cwd "${input.cwd ?? ctx.cwd}" resolves outside project root`,
6881
7177
  exitCode: 1,
6882
7178
  truncated: false,
6883
- allowed: false
7179
+ allowed: false,
7180
+ danger
6884
7181
  };
6885
7182
  }
6886
7183
  const signal = opts.signal;
6887
- return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);
7184
+ return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id, danger);
6888
7185
  }
6889
7186
  };
6890
- function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
7187
+ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
6891
7188
  return new Promise((resolve7) => {
6892
7189
  let stdout = "";
6893
7190
  let stderr = "";
@@ -6924,7 +7221,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
6924
7221
  stderr: `spawn failed: ${toErrorMessage$1(err)}`,
6925
7222
  exitCode: 1,
6926
7223
  truncated: false,
6927
- allowed: true
7224
+ allowed: true,
7225
+ danger
6928
7226
  });
6929
7227
  return;
6930
7228
  }
@@ -6943,7 +7241,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
6943
7241
  stderr: stderrText,
6944
7242
  exitCode: isAbort ? 124 : 1,
6945
7243
  truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
6946
- allowed: true
7244
+ allowed: true,
7245
+ danger
6947
7246
  });
6948
7247
  });
6949
7248
  const registry = getProcessRegistry();
@@ -6991,7 +7290,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
6991
7290
  stderr: normalizeCommandOutput(stderr),
6992
7291
  exitCode,
6993
7292
  truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
6994
- allowed: true
7293
+ allowed: true,
7294
+ danger
6995
7295
  });
6996
7296
  });
6997
7297
  });
@@ -8494,7 +8794,6 @@ var jsonTool = {
8494
8794
  return executeTransform(input, ctx);
8495
8795
  case "merge":
8496
8796
  return executeMerge(input);
8497
- case "parse":
8498
8797
  default:
8499
8798
  return executeParse(input, ctx);
8500
8799
  }