@wrongstack/tools 0.1.4 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +127 -0
  2. package/dist/audit.js.map +1 -1
  3. package/dist/bash.js +103 -5
  4. package/dist/bash.js.map +1 -1
  5. package/dist/builtin.js +550 -258
  6. package/dist/builtin.js.map +1 -1
  7. package/dist/diff.js +5 -9
  8. package/dist/diff.js.map +1 -1
  9. package/dist/document.js +0 -1
  10. package/dist/document.js.map +1 -1
  11. package/dist/edit.js +2 -2
  12. package/dist/edit.js.map +1 -1
  13. package/dist/exec.d.ts +0 -1
  14. package/dist/exec.js +105 -44
  15. package/dist/exec.js.map +1 -1
  16. package/dist/fetch.js +110 -25
  17. package/dist/fetch.js.map +1 -1
  18. package/dist/format.js.map +1 -1
  19. package/dist/git.d.ts +0 -1
  20. package/dist/git.js +9 -9
  21. package/dist/git.js.map +1 -1
  22. package/dist/glob.js +0 -1
  23. package/dist/glob.js.map +1 -1
  24. package/dist/grep.js +58 -3
  25. package/dist/grep.js.map +1 -1
  26. package/dist/index.js +549 -257
  27. package/dist/index.js.map +1 -1
  28. package/dist/install.js.map +1 -1
  29. package/dist/lint.js.map +1 -1
  30. package/dist/logs.js +61 -6
  31. package/dist/logs.js.map +1 -1
  32. package/dist/outdated.js.map +1 -1
  33. package/dist/patch.js +68 -29
  34. package/dist/patch.js.map +1 -1
  35. package/dist/read.js +0 -1
  36. package/dist/read.js.map +1 -1
  37. package/dist/replace.js +59 -9
  38. package/dist/replace.js.map +1 -1
  39. package/dist/scaffold.js +5 -6
  40. package/dist/scaffold.js.map +1 -1
  41. package/dist/test.js.map +1 -1
  42. package/dist/todo.js +1 -1
  43. package/dist/todo.js.map +1 -1
  44. package/dist/tool-use.js +0 -8
  45. package/dist/tool-use.js.map +1 -1
  46. package/dist/tree.js +9 -5
  47. package/dist/tree.js.map +1 -1
  48. package/dist/typecheck.js.map +1 -1
  49. package/dist/write.js +0 -1
  50. package/dist/write.js.map +1 -1
  51. package/package.json +7 -4
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # @wrongstack/tools
2
+
3
+ Built-in tools the WrongStack agent uses to read, edit, and act on the user's project.
4
+
5
+ Each tool implements the `Tool` interface from [`@wrongstack/core`](../core), with a JSON schema, an `execute` function, a permission level (`auto` / `confirm` / `deny`), and security gates appropriate to its blast radius.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @wrongstack/tools @wrongstack/core
11
+ ```
12
+
13
+ ## Catalog
14
+
15
+ ### Filesystem
16
+
17
+ | Tool | Permission | Mutating | Notes |
18
+ |------|------------|----------|-------|
19
+ | `read` | `auto` | no | Records mtime for stale-read detection |
20
+ | `write` | `confirm` | yes | Creates new files |
21
+ | `edit` | `confirm` | yes | str_replace; requires prior `read`; FAT-aware mtime tolerance |
22
+ | `replace` | `confirm` | yes | Regex replace across files; symlink-skipped + realpath-revalidated |
23
+ | `glob` | `auto` | no | Glob pattern matching |
24
+ | `grep` | `auto` | no | rg-backed; user-regex compiled through ReDoS guard |
25
+ | `tree` | `auto` | no | Project tree; clears its polling timer (no leaks) |
26
+ | `patch` | `confirm` | yes | GNU-patch diff applier; targets pre-validated against `projectRoot` |
27
+ | `diff` | `auto` | no | Git diff against HEAD |
28
+ | `json` | `auto` | no | jq-style JSON query |
29
+
30
+ ### Execution
31
+
32
+ | Tool | Permission | Mutating | Notes |
33
+ |------|------------|----------|-------|
34
+ | `bash` | `confirm` | yes | Sanitized child env; POSIX process-group kill on timeout |
35
+ | `exec` | `confirm` | yes | Allowlist-only; validated `cwd` inside `projectRoot` |
36
+ | `git` | `confirm` | yes (commits) | Typed subcommands only — no raw `args` (drops RCE via `-c …`) |
37
+
38
+ ### Network
39
+
40
+ | Tool | Permission | Mutating | Notes |
41
+ |------|------------|----------|-------|
42
+ | `fetch` | `auto` | no | SSRF-hardened (IPv4 + IPv6 private CIDR, redirect re-validation, http://-downgrade refused) |
43
+ | `search` | `auto` | no | Web search via configured provider |
44
+
45
+ ### Project lifecycle
46
+
47
+ | Tool | Permission | Notes |
48
+ |------|------------|-------|
49
+ | `lint` | `auto` | Project-aware lint runner (eslint / biome / ruff / golangci-lint / …) |
50
+ | `format` | `confirm` | Project-aware formatter |
51
+ | `typecheck` | `auto` | Project-aware typechecker |
52
+ | `test` | `confirm` | Project-aware test runner |
53
+ | `install` | `confirm` | Package manager install |
54
+ | `audit` | `auto` | Dependency vuln audit |
55
+ | `outdated` | `auto` | List outdated dependencies |
56
+ | `scaffold` | `confirm` | Template-based scaffolding |
57
+ | `document` | `confirm` | Inline JSDoc generation |
58
+ | `logs` | `auto` | Tail logs with rolling 100k-line window |
59
+
60
+ ### Agent control
61
+
62
+ | Tool | Notes |
63
+ |------|-------|
64
+ | `todo` | TodoWrite / TodoRead for session task tracking |
65
+ | `tool_search` | Lazy-load deferred tool schemas |
66
+ | `tool_use` | Generic single-tool call |
67
+ | `batch_tool_use` | Parallel multi-tool dispatch |
68
+ | `tool_help` | Show tool usage hint |
69
+ | `remember` / `forget` | Memory-store mutations |
70
+ | `create_mode` | Author a new agent mode |
71
+
72
+ ## Quick example
73
+
74
+ ```ts
75
+ import { ToolRegistry } from '@wrongstack/core';
76
+ import { readTool, editTool, bashTool, builtinTools } from '@wrongstack/tools';
77
+
78
+ // Cherry-pick:
79
+ const tools = new ToolRegistry([readTool, editTool, bashTool]);
80
+
81
+ // Or take the whole built-in set:
82
+ const all = new ToolRegistry(builtinTools);
83
+ ```
84
+
85
+ ## Security properties (0.1.6 hardening)
86
+
87
+ - **`bash` / `exec` child env sanitized** to a fixed allowlist + secret-substring strip (`TOKEN` / `SECRET` / `PASSWORD` / `AUTH` / `BEARER` / `COOKIE` / `PRIVATE` / `KEY`). Opt-out via `WRONGSTACK_BASH_ENV_PASSTHROUGH=1`.
88
+ - **`bash` POSIX process-group kill** with `SIGTERM → 800 ms → SIGKILL`. Runaway grandchildren can't survive the timeout.
89
+ - **`fetch` SSRF defenses**: numeric CIDR checks for IPv4 (`10/8`, `127/8`, `169.254/16`, `100.64/10`, `224/4`, `240/4`, …) and 8-group-expanded IPv6 (including Node's compressed `::ffff:7f00:1` form for v4-mapped addresses). Redirect target re-validated on every hop. `http://` downgrade refused.
90
+ - **`patch` diff-target validation**: every `+++` target post-strip is resolved against `projectRoot` before GNU patch sees the diff. `strip` clamped to `≥1`. Temp diff written to a `0700 mkdtemp` directory. Run with `LANG=C` / `LC_ALL=C`.
91
+ - **`replace` / `grep` symlink-safe**: lstat + realpath + projectRoot revalidation; symlinks skipped, not followed.
92
+ - **User-regex ReDoS guard** (`_regex.ts`): 512-char cap, nested-quantifier rejection (`(a+)+`, `(?:x+)*`), 64 KB subject-line cap. Applied to `grep`, `replace`, `logs`.
93
+ - **`git` `args` raw string removed** — the `-c core.sshCommand=…` RCE bypass is no longer reachable.
94
+ - **`grep` stdout buffer capped at 1 MB** — pathological producers can't pin memory.
95
+ - **`logs` rolling window** of 100k lines max; `lines: 0` no longer means "buffer the whole file".
96
+
97
+ Full threat model: [SECURITY.md](../../SECURITY.md) at the repo root.
98
+
99
+ ## Writing a custom tool
100
+
101
+ ```ts
102
+ import type { Tool } from '@wrongstack/core';
103
+
104
+ export const echoTool: Tool<{ text: string }, string> = {
105
+ name: 'echo',
106
+ description: 'Echo a string back.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: { text: { type: 'string' } },
110
+ required: ['text'],
111
+ },
112
+ permission: 'auto',
113
+ mutating: false,
114
+ // Tells permission policy which input field is the trust subject
115
+ subjectKey: 'text',
116
+ async execute(input, _ctx, { signal }) {
117
+ if (signal.aborted) throw new Error('aborted');
118
+ return input.text;
119
+ },
120
+ };
121
+ ```
122
+
123
+ See [docs/tool-author-guide.md](../../docs/tool-author-guide.md) for the full contract.
124
+
125
+ ## License
126
+
127
+ MIT
package/dist/audit.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/audit.ts"],"names":["resolve"],"mappings":"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","file":"audit.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { safeResolve, spawnStream } from './_util.js';\r\n\r\ninterface AuditInput {\r\n cwd?: string;\r\n level?: 'low' | 'moderate' | 'high' | 'critical';\r\n fix?: boolean;\r\n packages?: string | string[];\r\n}\r\n\r\ninterface AuditVulnerability {\r\n severity: string;\r\n package: string;\r\n title: string;\r\n url: string;\r\n}\r\n\r\ninterface AuditOutput {\r\n exit_code: number;\r\n vulnerabilities: AuditVulnerability[];\r\n total: number;\r\n summary: string;\r\n output: string;\r\n truncated: boolean;\r\n}\r\n\r\nexport const auditTool: Tool<AuditInput, AuditOutput> = {\r\n name: 'audit',\r\n description:\r\n 'Run npm/pnpm security audit. Returns vulnerabilities sorted by severity.',\r\n usageHint:\r\n 'Set `level` to filter minimum severity. `fix` attempts auto-fix. `packages` checks specific packages.',\r\n permission: 'confirm',\r\n mutating: false,\r\n timeoutMs: 60_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n level: {\r\n type: 'string',\r\n enum: ['low', 'moderate', 'high', 'critical'],\r\n description: 'Minimum severity level to report',\r\n },\r\n fix: { type: 'boolean', description: 'Attempt to fix vulnerabilities (default: false)' },\r\n packages: { type: 'string', description: 'Specific package(s) to audit (comma-separated)' },\r\n },\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: AuditOutput | undefined;\r\n for await (const ev of auditTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('audit: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<AuditOutput>> {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const manager = await detectManager(cwd);\r\n yield { type: 'log', text: `Auditing with ${manager}…`, data: { manager } };\r\n\r\n const args = ['audit', '--json'];\r\n if (input.fix) args.push('--fix');\r\n if (input.packages) {\r\n const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(',');\r\n args.push(...pkgs.map((p: string) => p.trim()));\r\n }\r\n\r\n const result = yield* spawnStream({\r\n cmd: manager,\r\n args,\r\n cwd,\r\n signal: opts.signal,\r\n maxBytes: 100_000,\r\n });\r\n\r\n yield { type: 'final', output: parseAuditOutput(result.stdout, result.exitCode) };\r\n },\r\n};\r\n\r\nasync function detectManager(cwd: string): Promise<string> {\r\n const { stat } = await import('node:fs/promises');\r\n try { await stat(`${cwd}/pnpm-lock.yaml`); return 'pnpm'; } catch { /* */ }\r\n try { await stat(`${cwd}/yarn.lock`); return 'yarn'; } catch { /* */ }\r\n return 'npm';\r\n}\r\n\r\nfunction parseAuditOutput(json: string, exitCode: number): AuditOutput {\r\n if (!json) {\r\n return {\r\n exit_code: exitCode,\r\n vulnerabilities: [],\r\n total: 0,\r\n summary: exitCode === 0 ? 'No vulnerabilities found' : 'Audit failed',\r\n output: '',\r\n truncated: false,\r\n };\r\n }\r\n\r\n try {\r\n const data = JSON.parse(json);\r\n const advisories: AuditVulnerability[] = [];\r\n const ads = data.advisories ?? {};\r\n for (const id of Object.keys(ads)) {\r\n const adv = ads[id];\r\n advisories.push({\r\n severity: adv.severity ?? 'unknown',\r\n package: adv.module_name ?? id,\r\n title: adv.title ?? 'Unknown vulnerability',\r\n url: adv.url ?? '',\r\n });\r\n }\r\n\r\n const total = advisories.length;\r\n const summary = total === 0\r\n ? 'No vulnerabilities found'\r\n : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === 'critical').length} critical, ${advisories.filter((a) => a.severity === 'high').length} high`;\r\n\r\n return {\r\n exit_code: exitCode,\r\n vulnerabilities: advisories,\r\n total,\r\n summary,\r\n output: json,\r\n truncated: json.length >= 100_000,\r\n };\r\n } catch {\r\n return {\r\n exit_code: exitCode,\r\n vulnerabilities: [],\r\n total: 0,\r\n summary: 'Could not parse audit output',\r\n output: json,\r\n truncated: false,\r\n };\r\n }\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/_spawn-stream.ts","../src/audit.ts"],"names":["resolve"],"mappings":"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","file":"audit.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\n","import { spawn } from 'node:child_process';\nimport type { ToolProgressEvent } from '@wrongstack/core';\n\nexport interface SpawnStreamResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n error?: string;\n}\n\nexport interface SpawnStreamOptions {\n cmd: string;\n args: string[];\n cwd: string;\n signal: AbortSignal;\n maxBytes?: number;\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\n flushBytes?: number;\n}\n\n/**\n * Spawn a child process and yield `partial_output` progress events as\n * stdout/stderr arrive (batched by byte threshold), then return the full\n * buffered result. Shared between install/lint/format/typecheck/test/audit\n * so the TUI live tail sees consistent progress regardless of which tool\n * is running.\n */\nexport async function* spawnStream(\n opts: SpawnStreamOptions,\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\n const max = opts.maxBytes ?? 200_000;\n const flushAt = opts.flushBytes ?? 4 * 1024;\n let stdout = '';\n let stderr = '';\n let pending = '';\n let error: string | undefined;\n\n const child = spawn(opts.cmd, opts.args, {\n cwd: opts.cwd,\n signal: opts.signal,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\n const queue: Chunk[] = [];\n let waiter: (() => void) | undefined;\n const wake = () => {\n if (waiter) {\n const w = waiter;\n waiter = undefined;\n w();\n }\n };\n\n child.stdout?.on('data', (c) => {\n const s = c.toString();\n if (stdout.length < max) stdout += s;\n queue.push({ kind: 'out', data: s });\n wake();\n });\n child.stderr?.on('data', (c) => {\n const s = c.toString();\n if (stderr.length < max) stderr += s;\n queue.push({ kind: 'err', data: s });\n wake();\n });\n child.on('error', (e) => {\n error = e.message;\n queue.push({ kind: 'error', data: e.message });\n wake();\n });\n child.on('close', (code) => {\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\n wake();\n });\n\n let exitCode = 0;\n let spawnFailed = false;\n for (;;) {\n while (queue.length === 0) {\n await new Promise<void>((resolve) => {\n waiter = resolve;\n });\n }\n const chunk = queue.shift()!;\n if (chunk.kind === 'close') {\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\n // rather than the negative platform code Node fabricates.\n if (!spawnFailed) exitCode = chunk.code ?? 0;\n break;\n }\n if (chunk.kind === 'error') {\n spawnFailed = true;\n exitCode = 1;\n // close usually follows\n continue;\n }\n pending += chunk.data;\n if (pending.length >= flushAt) {\n yield { type: 'partial_output', text: pending };\n pending = '';\n }\n }\n if (pending.length > 0) {\n yield { type: 'partial_output', text: pending };\n }\n\n return {\n stdout,\n stderr,\n exitCode,\n truncated: stdout.length >= max || stderr.length >= max,\n error,\n };\n}\n","import type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { safeResolve } from './_util.js';\nimport { spawnStream } from './_spawn-stream.js';\n\r\ninterface AuditInput {\r\n cwd?: string;\r\n level?: 'low' | 'moderate' | 'high' | 'critical';\r\n fix?: boolean;\r\n packages?: string | string[];\r\n}\r\n\r\ninterface AuditVulnerability {\r\n severity: string;\r\n package: string;\r\n title: string;\r\n url: string;\r\n}\r\n\r\ninterface AuditOutput {\r\n exit_code: number;\r\n vulnerabilities: AuditVulnerability[];\r\n total: number;\r\n summary: string;\r\n output: string;\r\n truncated: boolean;\r\n}\r\n\r\nexport const auditTool: Tool<AuditInput, AuditOutput> = {\r\n name: 'audit',\r\n description:\r\n 'Run npm/pnpm security audit. Returns vulnerabilities sorted by severity.',\r\n usageHint:\r\n 'Set `level` to filter minimum severity. `fix` attempts auto-fix. `packages` checks specific packages.',\r\n permission: 'confirm',\r\n mutating: false,\r\n timeoutMs: 60_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n level: {\r\n type: 'string',\r\n enum: ['low', 'moderate', 'high', 'critical'],\r\n description: 'Minimum severity level to report',\r\n },\r\n fix: { type: 'boolean', description: 'Attempt to fix vulnerabilities (default: false)' },\r\n packages: { type: 'string', description: 'Specific package(s) to audit (comma-separated)' },\r\n },\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: AuditOutput | undefined;\r\n for await (const ev of auditTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('audit: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<AuditOutput>> {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const manager = await detectManager(cwd);\r\n yield { type: 'log', text: `Auditing with ${manager}…`, data: { manager } };\r\n\r\n const args = ['audit', '--json'];\r\n if (input.fix) args.push('--fix');\r\n if (input.packages) {\r\n const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(',');\r\n args.push(...pkgs.map((p: string) => p.trim()));\r\n }\r\n\r\n const result = yield* spawnStream({\r\n cmd: manager,\r\n args,\r\n cwd,\r\n signal: opts.signal,\r\n maxBytes: 100_000,\r\n });\r\n\r\n yield { type: 'final', output: parseAuditOutput(result.stdout, result.exitCode) };\r\n },\r\n};\r\n\r\nasync function detectManager(cwd: string): Promise<string> {\r\n const { stat } = await import('node:fs/promises');\r\n try { await stat(`${cwd}/pnpm-lock.yaml`); return 'pnpm'; } catch { /* */ }\r\n try { await stat(`${cwd}/yarn.lock`); return 'yarn'; } catch { /* */ }\r\n return 'npm';\r\n}\r\n\r\nfunction parseAuditOutput(json: string, exitCode: number): AuditOutput {\r\n if (!json) {\r\n return {\r\n exit_code: exitCode,\r\n vulnerabilities: [],\r\n total: 0,\r\n summary: exitCode === 0 ? 'No vulnerabilities found' : 'Audit failed',\r\n output: '',\r\n truncated: false,\r\n };\r\n }\r\n\r\n try {\r\n const data = JSON.parse(json);\r\n const advisories: AuditVulnerability[] = [];\r\n const ads = data.advisories ?? {};\r\n for (const id of Object.keys(ads)) {\r\n const adv = ads[id];\r\n advisories.push({\r\n severity: adv.severity ?? 'unknown',\r\n package: adv.module_name ?? id,\r\n title: adv.title ?? 'Unknown vulnerability',\r\n url: adv.url ?? '',\r\n });\r\n }\r\n\r\n const total = advisories.length;\r\n const summary = total === 0\r\n ? 'No vulnerabilities found'\r\n : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === 'critical').length} critical, ${advisories.filter((a) => a.severity === 'high').length} high`;\r\n\r\n return {\r\n exit_code: exitCode,\r\n vulnerabilities: advisories,\r\n total,\r\n summary,\r\n output: json,\r\n truncated: json.length >= 100_000,\r\n };\r\n } catch {\r\n return {\r\n exit_code: exitCode,\r\n vulnerabilities: [],\r\n total: 0,\r\n summary: 'Could not parse audit output',\r\n output: json,\r\n truncated: false,\r\n };\r\n }\r\n}\r\n"]}
package/dist/bash.js CHANGED
@@ -12,6 +12,84 @@ function truncateMiddle(s, max) {
12
12
  ` + s.slice(-half);
13
13
  }
14
14
 
15
+ // src/_env.ts
16
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set([
17
+ "PATH",
18
+ "HOME",
19
+ "USER",
20
+ "USERNAME",
21
+ "LOGNAME",
22
+ "SHELL",
23
+ "LANG",
24
+ "LC_ALL",
25
+ "LC_CTYPE",
26
+ "TERM",
27
+ "TZ",
28
+ "TMPDIR",
29
+ "TEMP",
30
+ "TMP",
31
+ "PWD",
32
+ "OLDPWD",
33
+ "COMSPEC",
34
+ "SYSTEMROOT",
35
+ "SYSTEMDRIVE",
36
+ "WINDIR",
37
+ "PROGRAMFILES",
38
+ "PROGRAMFILES(X86)",
39
+ "PROGRAMDATA",
40
+ "APPDATA",
41
+ "LOCALAPPDATA",
42
+ "USERPROFILE",
43
+ "PUBLIC",
44
+ "PATHEXT"
45
+ ]);
46
+ var SECRET_NAME_PARTS = [
47
+ "TOKEN",
48
+ "SECRET",
49
+ "PASSWORD",
50
+ "PASSWD",
51
+ "AUTH",
52
+ "CRED",
53
+ "BEARER",
54
+ "COOKIE",
55
+ "PRIVATE"
56
+ ];
57
+ function looksSecret(name) {
58
+ const upper = name.toUpperCase();
59
+ for (const p of SECRET_NAME_PARTS) {
60
+ if (upper.includes(p)) return true;
61
+ }
62
+ if (/(?:^|_)KEY(?:$|_|S$)/i.test(upper)) return true;
63
+ if (/API[_-]?KEY/i.test(upper)) return true;
64
+ if (/ACCESS[_-]?KEY/i.test(upper)) return true;
65
+ if (/SESSION[_-]?ID/i.test(upper) === false && /SESSION/i.test(upper)) {
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ function buildChildEnv(sessionId) {
71
+ const passthrough = process.env["WRONGSTACK_BASH_ENV_PASSTHROUGH"] === "1";
72
+ const out = {};
73
+ for (const [k, v] of Object.entries(process.env)) {
74
+ if (v === void 0) continue;
75
+ if (passthrough) {
76
+ out[k] = v;
77
+ continue;
78
+ }
79
+ const upper = k.toUpperCase();
80
+ if (ALLOWED_KEYS.has(upper)) {
81
+ out[k] = v;
82
+ continue;
83
+ }
84
+ if (looksSecret(upper)) continue;
85
+ if (upper.startsWith("NODE_") || upper.startsWith("NPM_") || upper.startsWith("PNPM_") || upper.startsWith("YARN_") || upper.startsWith("GIT_") || upper.startsWith("CI") || upper.startsWith("XDG_") || upper === "EDITOR" || upper === "VISUAL" || upper === "PAGER") {
86
+ out[k] = v;
87
+ }
88
+ }
89
+ if (sessionId) out["WRONGSTACK_SESSION_ID"] = sessionId;
90
+ return out;
91
+ }
92
+
15
93
  // src/bash.ts
16
94
  var MAX_OUTPUT = 32768;
17
95
  var DEFAULT_TIMEOUT = 3e4;
@@ -23,6 +101,10 @@ var bashTool = {
23
101
  usageHint: "Runs via `bash -c` (or `cmd /c` on Windows). Cwd is the project root. Default timeout 30s. Output truncated from the middle if oversized. Use for git, npm, builds, tests.",
24
102
  permission: "confirm",
25
103
  mutating: true,
104
+ // Trust rules match on the literal `command` string. Without subjectKey
105
+ // the policy heuristic would have done the same here, but declaring it
106
+ // explicitly removes the implicit cross-tool aliasing.
107
+ subjectKey: "command",
26
108
  timeoutMs: 3e4,
27
109
  maxOutputBytes: MAX_OUTPUT,
28
110
  estimatedDurationMs: 3e3,
@@ -49,13 +131,13 @@ var bashTool = {
49
131
  const isWin = os.platform() === "win32";
50
132
  const shell = isWin ? process.env["COMSPEC"] ?? "cmd.exe" : process.env["SHELL"] ?? "/bin/bash";
51
133
  const args = isWin ? ["/c", input.command] : ["-c", input.command];
52
- const env = { ...process.env };
53
- env["WRONGSTACK_SESSION_ID"] = ctx.session.id;
134
+ const env = buildChildEnv(ctx.session?.id);
135
+ const detached = isWin ? !!input.background : true;
54
136
  const child = spawn(shell, args, {
55
137
  cwd: ctx.projectRoot,
56
138
  env,
57
139
  stdio: input.background ? "ignore" : ["ignore", "pipe", "pipe"],
58
- detached: input.background,
140
+ detached,
59
141
  signal: opts.signal
60
142
  });
61
143
  if (input.background) {
@@ -85,10 +167,26 @@ var bashTool = {
85
167
  }
86
168
  } else {
87
169
  try {
88
- child.kill("SIGTERM");
170
+ if (typeof child.pid === "number") {
171
+ try {
172
+ process.kill(-child.pid, "SIGTERM");
173
+ } catch {
174
+ child.kill("SIGTERM");
175
+ }
176
+ } else {
177
+ child.kill("SIGTERM");
178
+ }
89
179
  const killTimer = setTimeout(() => {
90
180
  try {
91
- child.kill("SIGKILL");
181
+ if (typeof child.pid === "number") {
182
+ try {
183
+ process.kill(-child.pid, "SIGKILL");
184
+ } catch {
185
+ child.kill("SIGKILL");
186
+ }
187
+ } else {
188
+ child.kill("SIGKILL");
189
+ }
92
190
  } catch {
93
191
  }
94
192
  }, 2e3);
package/dist/bash.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/bash.ts"],"names":["spawn","resolve"],"mappings":"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","file":"bash.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import { spawn } from 'node:child_process';\r\nimport * as os from 'node:os';\r\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { stripAnsi } from '@wrongstack/core';\r\nimport { truncateMiddle } from './_util.js';\r\n\r\ninterface BashInput {\r\n command: string;\r\n timeout_ms?: number;\r\n background?: boolean;\r\n}\r\n\r\ninterface BashOutput {\r\n output: string;\r\n exit_code: number | null;\r\n timed_out: boolean;\r\n pid?: number;\r\n}\r\n\r\nconst MAX_OUTPUT = 32_768;\r\nconst DEFAULT_TIMEOUT = 30_000;\r\n// Flush partial_output every 200ms or when 4 KiB accumulates — whichever\r\n// comes first. Smaller batches make the TUI feel responsive; larger ones\r\n// keep EventBus traffic reasonable on chatty processes.\r\nconst STREAM_FLUSH_INTERVAL_MS = 200;\r\nconst STREAM_FLUSH_BYTES = 4 * 1024;\r\n\r\nexport const bashTool: Tool<BashInput, BashOutput> = {\r\n name: 'bash',\r\n description: 'Run a shell command. stdout and stderr are merged.',\r\n usageHint:\r\n 'Runs via `bash -c` (or `cmd /c` on Windows). Cwd is the project root. Default timeout 30s. Output truncated from the middle if oversized. Use for git, npm, builds, tests.',\r\n permission: 'confirm',\r\n mutating: true,\r\n timeoutMs: 30_000,\r\n maxOutputBytes: MAX_OUTPUT,\r\n estimatedDurationMs: 3_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n command: { type: 'string' },\r\n timeout_ms: { type: 'integer' },\r\n background: { type: 'boolean' },\r\n },\r\n required: ['command'],\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: BashOutput | undefined;\r\n for await (const ev of bashTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('bash: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<BashOutput>> {\r\n if (!input?.command) throw new Error('bash: command is required');\r\n const timeoutMs = Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT, 600_000);\r\n\r\n const isWin = os.platform() === 'win32';\r\n const shell = isWin\r\n ? process.env['COMSPEC'] ?? 'cmd.exe'\r\n : process.env['SHELL'] ?? '/bin/bash';\r\n const args = isWin ? ['/c', input.command] : ['-c', input.command];\r\n\r\n const env: NodeJS.ProcessEnv = { ...process.env };\r\n env['WRONGSTACK_SESSION_ID'] = ctx.session.id;\r\n\r\n const child = spawn(shell, args, {\r\n cwd: ctx.projectRoot,\r\n env,\r\n stdio: input.background ? 'ignore' : ['ignore', 'pipe', 'pipe'],\r\n detached: input.background,\r\n signal: opts.signal,\r\n });\r\n\r\n if (input.background) {\r\n const pid = child.pid;\r\n if (typeof pid === 'number') child.unref();\r\n yield {\r\n type: 'final',\r\n output: {\r\n output: `[background] pid=${pid ?? 'unknown'}`,\r\n exit_code: null,\r\n timed_out: false,\r\n pid,\r\n },\r\n };\r\n return;\r\n }\r\n\r\n let buf = '';\r\n let pending = '';\r\n let timedOut = false;\r\n const timers: NodeJS.Timeout[] = [];\r\n const timer = setTimeout(() => {\r\n timedOut = true;\r\n if (isWin) {\r\n try { child.kill(); } catch { /* ignore */ }\r\n } else {\r\n try {\r\n child.kill('SIGTERM');\r\n const killTimer = setTimeout(() => {\r\n try { child.kill('SIGKILL'); } catch { /* ignore */ }\r\n }, 2000);\r\n timers.push(killTimer);\r\n } catch { /* ignore */ }\r\n }\r\n }, timeoutMs);\r\n timers.push(timer);\r\n timer.unref?.();\r\n\r\n // Bridge the EventEmitter-style child to an async iterator. We push\r\n // chunks into a queue and let the generator pull them; this lets us\r\n // yield 'partial_output' events to the executor at flush boundaries.\r\n type Chunk = { kind: 'data'; text: string } | { kind: 'end'; code: number | null } | { kind: 'error'; err: Error };\r\n const queue: Chunk[] = [];\r\n let resolveNext: ((c: Chunk) => void) | null = null;\r\n const push = (c: Chunk) => {\r\n if (resolveNext) {\r\n const r = resolveNext;\r\n resolveNext = null;\r\n r(c);\r\n } else {\r\n queue.push(c);\r\n }\r\n };\r\n const next = (): Promise<Chunk> => new Promise((resolve) => {\r\n const c = queue.shift();\r\n if (c) resolve(c);\r\n else resolveNext = resolve;\r\n });\r\n\r\n let lastFlush = Date.now();\r\n const flush = () => {\r\n if (pending.length === 0) return null;\r\n const text = pending;\r\n pending = '';\r\n lastFlush = Date.now();\r\n return text;\r\n };\r\n\r\n child.stdout?.on('data', (chunk) => {\r\n const text = chunk.toString();\r\n buf += text;\r\n pending += text;\r\n push({ kind: 'data', text });\r\n });\r\n child.stderr?.on('data', (chunk) => {\r\n const text = chunk.toString();\r\n buf += text;\r\n pending += text;\r\n push({ kind: 'data', text });\r\n });\r\n child.on('error', (err) => {\r\n for (const t of timers) clearTimeout(t);\r\n push({ kind: 'error', err });\r\n });\r\n child.on('close', (code) => {\r\n for (const t of timers) clearTimeout(t);\r\n push({ kind: 'end', code });\r\n });\r\n\r\n try {\r\n while (true) {\r\n const c = await next();\r\n if (c.kind === 'error') throw c.err;\r\n if (c.kind === 'end') {\r\n const remainder = flush();\r\n if (remainder !== null) {\r\n yield { type: 'partial_output', text: remainder };\r\n }\r\n const cleaned = stripAnsi(buf).replace(/\\r\\n?/g, '\\n');\r\n yield {\r\n type: 'final',\r\n output: {\r\n output: truncateMiddle(cleaned, MAX_OUTPUT),\r\n exit_code: c.code,\r\n timed_out: timedOut,\r\n },\r\n };\r\n return;\r\n }\r\n // Decide whether to flush. Time-based OR size-based to keep latency\r\n // low for slow-emitting commands without overwhelming the TUI for\r\n // chatty ones.\r\n const now = Date.now();\r\n if (pending.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {\r\n const text = flush();\r\n if (text) yield { type: 'partial_output', text };\r\n }\r\n }\r\n } finally {\r\n for (const t of timers) clearTimeout(t);\r\n }\r\n },\r\n};\r\n\r\n// Re-export types so consumers can narrow on stream events.\r\nexport type { BashInput, BashOutput };\r\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/_env.ts","../src/bash.ts"],"names":["resolve"],"mappings":"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","file":"bash.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\n","/**\n * Build a sanitized child-process environment.\n *\n * The bash/exec tools execute LLM-generated commands. The parent process\n * carries provider API keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, ...), VCS\n * tokens (GITHUB_TOKEN), and cloud credentials. Forwarding those to a child\n * is an exfiltration vector even with `permission: 'confirm'` — a model can\n * compose a command whose secret-leaking effect is not obvious from a quick\n * read of the shell pipeline.\n *\n * Strategy: copy a small, explicit allowlist of variables that real builds\n * need, then copy anything else that does NOT look secret-bearing. This\n * preserves user-friendly behavior (locale, terminal, npm config) while\n * blocking the obvious leak channels.\n *\n * Override with `WRONGSTACK_BASH_ENV_PASSTHROUGH=1` to forward the full\n * parent environment unchanged (opt-in for advanced users who understand\n * the risk).\n */\n\nconst ALLOWED_KEYS = new Set<string>([\n 'PATH',\n 'HOME',\n 'USER',\n 'USERNAME',\n 'LOGNAME',\n 'SHELL',\n 'LANG',\n 'LC_ALL',\n 'LC_CTYPE',\n 'TERM',\n 'TZ',\n 'TMPDIR',\n 'TEMP',\n 'TMP',\n 'PWD',\n 'OLDPWD',\n 'COMSPEC',\n 'SYSTEMROOT',\n 'SYSTEMDRIVE',\n 'WINDIR',\n 'PROGRAMFILES',\n 'PROGRAMFILES(X86)',\n 'PROGRAMDATA',\n 'APPDATA',\n 'LOCALAPPDATA',\n 'USERPROFILE',\n 'PUBLIC',\n 'PATHEXT',\n]);\n\n// Substring match against env-var names (case-insensitive). Bias toward\n// false-positives — a missing var is recoverable, an exfiltrated key is not.\n// Only consulted for vars NOT on the curated allowlist; PWD/PASSWD-style\n// false positives there are avoided by checking allowlist first.\nconst SECRET_NAME_PARTS = [\n 'TOKEN',\n 'SECRET',\n 'PASSWORD',\n 'PASSWD',\n 'AUTH',\n 'CRED',\n 'BEARER',\n 'COOKIE',\n 'PRIVATE',\n];\n\nfunction looksSecret(name: string): boolean {\n const upper = name.toUpperCase();\n for (const p of SECRET_NAME_PARTS) {\n if (upper.includes(p)) return true;\n }\n // KEY is tricky — PUBLIC_KEY is fine to forward but most _KEY vars are\n // secrets. Require word boundary so KEYBOARD_LAYOUT etc. are not flagged.\n if (/(?:^|_)KEY(?:$|_|S$)/i.test(upper)) return true;\n if (/API[_-]?KEY/i.test(upper)) return true;\n if (/ACCESS[_-]?KEY/i.test(upper)) return true;\n if (/SESSION[_-]?ID/i.test(upper) === false && /SESSION/i.test(upper)) {\n // SESSION_ID is metadata (we set our own); other SESSION_* often holds\n // session cookies. Be conservative.\n return true;\n }\n return false;\n}\n\nexport function buildChildEnv(sessionId?: string): NodeJS.ProcessEnv {\n const passthrough = process.env['WRONGSTACK_BASH_ENV_PASSTHROUGH'] === '1';\n const out: NodeJS.ProcessEnv = {};\n\n for (const [k, v] of Object.entries(process.env)) {\n if (v === undefined) continue;\n if (passthrough) {\n out[k] = v;\n continue;\n }\n const upper = k.toUpperCase();\n // 1. Forward names on the explicit allowlist — these are well-known\n // non-secret system variables (PATH, HOME, LANG, ...).\n if (ALLOWED_KEYS.has(upper)) {\n out[k] = v;\n continue;\n }\n // 2. Strip anything that looks like a secret.\n if (looksSecret(upper)) continue;\n // 3. Forward tooling-prefixed vars that builds commonly need, unless\n // they already failed the secret check above.\n if (\n upper.startsWith('NODE_') ||\n upper.startsWith('NPM_') ||\n upper.startsWith('PNPM_') ||\n upper.startsWith('YARN_') ||\n upper.startsWith('GIT_') ||\n upper.startsWith('CI') ||\n upper.startsWith('XDG_') ||\n upper === 'EDITOR' ||\n upper === 'VISUAL' ||\n upper === 'PAGER'\n ) {\n out[k] = v;\n }\n }\n\n if (sessionId) out['WRONGSTACK_SESSION_ID'] = sessionId;\n return out;\n}\n","import { spawn } from 'node:child_process';\r\nimport * as os from 'node:os';\r\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { stripAnsi } from '@wrongstack/core';\r\nimport { truncateMiddle } from './_util.js';\r\nimport { buildChildEnv } from './_env.js';\r\n\r\ninterface BashInput {\r\n command: string;\r\n timeout_ms?: number;\r\n background?: boolean;\r\n}\r\n\r\ninterface BashOutput {\r\n output: string;\r\n exit_code: number | null;\r\n timed_out: boolean;\r\n pid?: number;\r\n}\r\n\r\nconst MAX_OUTPUT = 32_768;\r\nconst DEFAULT_TIMEOUT = 30_000;\r\n// Flush partial_output every 200ms or when 4 KiB accumulates — whichever\r\n// comes first. Smaller batches make the TUI feel responsive; larger ones\r\n// keep EventBus traffic reasonable on chatty processes.\r\nconst STREAM_FLUSH_INTERVAL_MS = 200;\r\nconst STREAM_FLUSH_BYTES = 4 * 1024;\r\n\r\nexport const bashTool: Tool<BashInput, BashOutput> = {\r\n name: 'bash',\r\n description: 'Run a shell command. stdout and stderr are merged.',\r\n usageHint:\r\n 'Runs via `bash -c` (or `cmd /c` on Windows). Cwd is the project root. Default timeout 30s. Output truncated from the middle if oversized. Use for git, npm, builds, tests.',\r\n permission: 'confirm',\r\n mutating: true,\r\n // Trust rules match on the literal `command` string. Without subjectKey\r\n // the policy heuristic would have done the same here, but declaring it\r\n // explicitly removes the implicit cross-tool aliasing.\r\n subjectKey: 'command',\r\n timeoutMs: 30_000,\r\n maxOutputBytes: MAX_OUTPUT,\r\n estimatedDurationMs: 3_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n command: { type: 'string' },\r\n timeout_ms: { type: 'integer' },\r\n background: { type: 'boolean' },\r\n },\r\n required: ['command'],\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: BashOutput | undefined;\r\n for await (const ev of bashTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('bash: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<BashOutput>> {\r\n if (!input?.command) throw new Error('bash: command is required');\r\n const timeoutMs = Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT, 600_000);\r\n\r\n const isWin = os.platform() === 'win32';\r\n const shell = isWin\r\n ? process.env['COMSPEC'] ?? 'cmd.exe'\r\n : process.env['SHELL'] ?? '/bin/bash';\r\n const args = isWin ? ['/c', input.command] : ['-c', input.command];\r\n\r\n const env = buildChildEnv(ctx.session?.id);\r\n\r\n // On POSIX we put the shell in its own process group so that timeout /\r\n // abort can kill the entire group with `process.kill(-pid)`. Otherwise\r\n // `bash -c \"sleep 9999 & disown\"` would leave the grandchild running.\r\n // `detached: true` is also reused for the user-facing background mode;\r\n // we always want detached on POSIX, only on Windows is it tied to the\r\n // explicit background flag.\r\n const detached = isWin ? !!input.background : true;\r\n const child = spawn(shell, args, {\r\n cwd: ctx.projectRoot,\r\n env,\r\n stdio: input.background ? 'ignore' : ['ignore', 'pipe', 'pipe'],\r\n detached,\r\n signal: opts.signal,\r\n });\r\n\r\n if (input.background) {\r\n const pid = child.pid;\r\n if (typeof pid === 'number') child.unref();\r\n yield {\r\n type: 'final',\r\n output: {\r\n output: `[background] pid=${pid ?? 'unknown'}`,\r\n exit_code: null,\r\n timed_out: false,\r\n pid,\r\n },\r\n };\r\n return;\r\n }\r\n\r\n let buf = '';\r\n let pending = '';\r\n let timedOut = false;\r\n const timers: NodeJS.Timeout[] = [];\r\n const timer = setTimeout(() => {\r\n timedOut = true;\r\n if (isWin) {\r\n try { child.kill(); } catch { /* ignore */ }\r\n } else {\r\n try {\r\n // Kill the process group, not just the shell — pid is positive,\r\n // group id is the negated pid. Without this a runaway grandchild\r\n // ('sleep 9999 & disown') survives bash termination.\r\n if (typeof child.pid === 'number') {\r\n try { process.kill(-child.pid, 'SIGTERM'); } catch { child.kill('SIGTERM'); }\r\n } else {\r\n child.kill('SIGTERM');\r\n }\r\n const killTimer = setTimeout(() => {\r\n try {\r\n if (typeof child.pid === 'number') {\r\n try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); }\r\n } else {\r\n child.kill('SIGKILL');\r\n }\r\n } catch { /* ignore */ }\r\n }, 2000);\r\n timers.push(killTimer);\r\n } catch { /* ignore */ }\r\n }\r\n }, timeoutMs);\r\n timers.push(timer);\r\n timer.unref?.();\r\n\r\n // Bridge the EventEmitter-style child to an async iterator. We push\r\n // chunks into a queue and let the generator pull them; this lets us\r\n // yield 'partial_output' events to the executor at flush boundaries.\r\n type Chunk = { kind: 'data'; text: string } | { kind: 'end'; code: number | null } | { kind: 'error'; err: Error };\r\n const queue: Chunk[] = [];\r\n let resolveNext: ((c: Chunk) => void) | null = null;\r\n const push = (c: Chunk) => {\r\n if (resolveNext) {\r\n const r = resolveNext;\r\n resolveNext = null;\r\n r(c);\r\n } else {\r\n queue.push(c);\r\n }\r\n };\r\n const next = (): Promise<Chunk> => new Promise((resolve) => {\r\n const c = queue.shift();\r\n if (c) resolve(c);\r\n else resolveNext = resolve;\r\n });\r\n\r\n let lastFlush = Date.now();\r\n const flush = () => {\r\n if (pending.length === 0) return null;\r\n const text = pending;\r\n pending = '';\r\n lastFlush = Date.now();\r\n return text;\r\n };\r\n\r\n child.stdout?.on('data', (chunk) => {\r\n const text = chunk.toString();\r\n buf += text;\r\n pending += text;\r\n push({ kind: 'data', text });\r\n });\r\n child.stderr?.on('data', (chunk) => {\r\n const text = chunk.toString();\r\n buf += text;\r\n pending += text;\r\n push({ kind: 'data', text });\r\n });\r\n child.on('error', (err) => {\r\n for (const t of timers) clearTimeout(t);\r\n push({ kind: 'error', err });\r\n });\r\n child.on('close', (code) => {\r\n for (const t of timers) clearTimeout(t);\r\n push({ kind: 'end', code });\r\n });\r\n\r\n try {\r\n while (true) {\r\n const c = await next();\r\n if (c.kind === 'error') throw c.err;\r\n if (c.kind === 'end') {\r\n const remainder = flush();\r\n if (remainder !== null) {\r\n yield { type: 'partial_output', text: remainder };\r\n }\r\n const cleaned = stripAnsi(buf).replace(/\\r\\n?/g, '\\n');\r\n yield {\r\n type: 'final',\r\n output: {\r\n output: truncateMiddle(cleaned, MAX_OUTPUT),\r\n exit_code: c.code,\r\n timed_out: timedOut,\r\n },\r\n };\r\n return;\r\n }\r\n // Decide whether to flush. Time-based OR size-based to keep latency\r\n // low for slow-emitting commands without overwhelming the TUI for\r\n // chatty ones.\r\n const now = Date.now();\r\n if (pending.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {\r\n const text = flush();\r\n if (text) yield { type: 'partial_output', text };\r\n }\r\n }\r\n } finally {\r\n for (const t of timers) clearTimeout(t);\r\n }\r\n },\r\n};\r\n\r\n// Re-export types so consumers can narrow on stream events.\r\nexport type { BashInput, BashOutput };\r\n"]}