@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/bash.js +33 -3
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +151 -46
- package/dist/builtin.js.map +1 -1
- package/dist/exec.js +37 -1
- package/dist/exec.js.map +1 -1
- package/dist/index.js +151 -46
- package/dist/index.js.map +1 -1
- package/dist/install.js +16 -0
- package/dist/install.js.map +1 -1
- package/dist/logs.js +9 -0
- package/dist/logs.js.map +1 -1
- package/dist/pack.js +151 -46
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +2 -1
- package/dist/patch.js.map +1 -1
- package/dist/read.js +8 -1
- package/dist/read.js.map +1 -1
- package/dist/scaffold.js +10 -3
- package/dist/scaffold.js.map +1 -1
- package/dist/write.js +4 -4
- package/dist/write.js.map +1 -1
- package/package.json +2 -2
package/dist/install.js
CHANGED
|
@@ -153,6 +153,22 @@ var installTool = {
|
|
|
153
153
|
const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
|
|
154
154
|
(p) => p.trim()
|
|
155
155
|
) : [];
|
|
156
|
+
const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
|
|
157
|
+
for (const pkg of pkgList) {
|
|
158
|
+
if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-")) {
|
|
159
|
+
yield {
|
|
160
|
+
type: "final",
|
|
161
|
+
output: {
|
|
162
|
+
packages: pkgList,
|
|
163
|
+
exit_code: 1,
|
|
164
|
+
output: `Invalid package name "${pkg}". Names must match ${PKG_NAME_RE} and not start with "-".`,
|
|
165
|
+
dry_run: Boolean(input.dry_run),
|
|
166
|
+
truncated: false
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
156
172
|
if (pkgList.length > 0) args.push(...pkgList);
|
|
157
173
|
yield {
|
|
158
174
|
type: "log",
|
package/dist/install.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/_spawn-stream.ts","../src/_util.ts","../src/install.ts"],"names":["resolve"],"mappings":"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","file":"install.js","sourcesContent":["import { spawn } from 'node:child_process';\nimport { buildChildEnv } from '@wrongstack/core';\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 env: buildChildEnv(),\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 * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { spawnStream } from './_spawn-stream.js';\nimport { safeResolve } from './_util.js';\n\ninterface InstallInput {\n packages?: string | string[];\n save?: 'dependency' | 'dev' | 'optional';\n cwd?: string;\n dry_run?: boolean;\n global?: boolean;\n}\n\ninterface InstallOutput {\n packages: string[];\n exit_code: number;\n output: string;\n dry_run: boolean;\n truncated: boolean;\n}\n\nexport const installTool: Tool<InstallInput, InstallOutput> = {\n name: 'install',\n category: 'Package Management',\n description: 'Install npm packages. Detects pnpm/npm/yarn and uses the right package manager.',\n usageHint:\n 'Set `packages` to install. `save` as dependency type. `global` for global install. `dry_run` to preview.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 120_000,\n inputSchema: {\n type: 'object',\n properties: {\n packages: {\n type: 'string',\n description:\n 'Package(s) to install: single name, comma-separated list, or empty for all deps',\n },\n save: {\n type: 'string',\n enum: ['dependency', 'dev', 'optional'],\n description: 'Save as regular, dev, or optional dependency',\n },\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n dry_run: {\n type: 'boolean',\n description: 'Preview install without modifying (default: false)',\n },\n global: { type: 'boolean', description: 'Install globally (default: false)' },\n },\n },\n async execute(input, ctx, opts) {\n let final: InstallOutput | undefined;\n for await (const ev of installTool.executeStream!(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('install: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<InstallOutput>> {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const pkgManager = await detectPackageManager(cwd);\n yield { type: 'log', text: `Resolving with ${pkgManager}…`, data: { phase: 'resolve' } };\n\n const save = input.save === 'dev' ? '-D' : input.save === 'optional' ? '-O' : '';\n const globalFlag = input.global ? ['-g'] : [];\n\n const args: string[] = [];\n if (input.dry_run) args.push('--dry-run');\n if (pkgManager === 'pnpm') {\n if (save) args.push(save);\n args.push('add', ...globalFlag);\n } else if (pkgManager === 'yarn') {\n args.push('add', ...globalFlag);\n } else {\n args.push('install', ...globalFlag);\n }\n\n const pkgList = input.packages\n ? (Array.isArray(input.packages) ? input.packages : input.packages.split(',')).map((p) =>\n p.trim(),\n )\n : [];\n if (pkgList.length > 0) args.push(...pkgList);\n\n yield {\n type: 'log',\n text: `Fetching ${pkgList.length || 'all'} packages…`,\n data: { phase: 'fetch' },\n };\n\n const result = yield* spawnStream({\n cmd: pkgManager,\n args,\n cwd,\n signal: opts.signal,\n maxBytes: 100_000,\n });\n\n yield {\n type: 'final',\n output: {\n packages: pkgList,\n exit_code: result.exitCode,\n output: result.stdout || result.stderr || result.error || '',\n dry_run: args.includes('--dry-run'),\n truncated: result.truncated,\n },\n };\n },\n};\n\nasync function detectPackageManager(cwd: string): Promise<string> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n return 'npm';\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/_spawn-stream.ts","../src/_util.ts","../src/install.ts"],"names":["resolve"],"mappings":"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","file":"install.js","sourcesContent":["import { spawn } from 'node:child_process';\nimport { buildChildEnv } from '@wrongstack/core';\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 env: buildChildEnv(),\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 * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { spawnStream } from './_spawn-stream.js';\r\nimport { safeResolve } from './_util.js';\r\n\r\ninterface InstallInput {\r\n packages?: string | string[];\r\n save?: 'dependency' | 'dev' | 'optional';\r\n cwd?: string;\r\n dry_run?: boolean;\r\n global?: boolean;\r\n}\r\n\r\ninterface InstallOutput {\r\n packages: string[];\r\n exit_code: number;\r\n output: string;\r\n dry_run: boolean;\r\n truncated: boolean;\r\n}\r\n\r\nexport const installTool: Tool<InstallInput, InstallOutput> = {\r\n name: 'install',\r\n category: 'Package Management',\r\n description: 'Install npm packages. Detects pnpm/npm/yarn and uses the right package manager.',\r\n usageHint:\r\n 'Set `packages` to install. `save` as dependency type. `global` for global install. `dry_run` to preview.',\r\n permission: 'confirm',\r\n mutating: true,\r\n timeoutMs: 120_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n packages: {\r\n type: 'string',\r\n description:\r\n 'Package(s) to install: single name, comma-separated list, or empty for all deps',\r\n },\r\n save: {\r\n type: 'string',\r\n enum: ['dependency', 'dev', 'optional'],\r\n description: 'Save as regular, dev, or optional dependency',\r\n },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n dry_run: {\r\n type: 'boolean',\r\n description: 'Preview install without modifying (default: false)',\r\n },\r\n global: { type: 'boolean', description: 'Install globally (default: false)' },\r\n },\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: InstallOutput | undefined;\r\n for await (const ev of installTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('install: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<InstallOutput>> {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const pkgManager = await detectPackageManager(cwd);\r\n yield { type: 'log', text: `Resolving with ${pkgManager}…`, data: { phase: 'resolve' } };\r\n\r\n const save = input.save === 'dev' ? '-D' : input.save === 'optional' ? '-O' : '';\r\n const globalFlag = input.global ? ['-g'] : [];\r\n\r\n const args: string[] = [];\r\n if (input.dry_run) args.push('--dry-run');\r\n if (pkgManager === 'pnpm') {\r\n if (save) args.push(save);\r\n args.push('add', ...globalFlag);\r\n } else if (pkgManager === 'yarn') {\r\n args.push('add', ...globalFlag);\r\n } else {\r\n args.push('install', ...globalFlag);\r\n }\r\n\r\n const pkgList = input.packages\r\n ? (Array.isArray(input.packages) ? input.packages : input.packages.split(',')).map((p) =>\r\n p.trim(),\r\n )\r\n : [];\r\n\r\n // Validate package names to prevent flag injection and path traversal.\r\n // A name like \"--ignore-scripts=false\" would be interpreted as a flag;\r\n // \"file:../../etc/passwd\" as a local path specifier.\r\n const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\\/)?[a-z0-9._-]+$/i;\r\n for (const pkg of pkgList) {\r\n if (!PKG_NAME_RE.test(pkg) || pkg.startsWith('-')) {\r\n yield {\r\n type: 'final',\r\n output: {\r\n packages: pkgList,\r\n exit_code: 1,\r\n output: `Invalid package name \"${pkg}\". Names must match ${PKG_NAME_RE} and not start with \"-\".`,\r\n dry_run: Boolean(input.dry_run),\r\n truncated: false,\r\n },\r\n };\r\n return;\r\n }\r\n }\r\n\r\n if (pkgList.length > 0) args.push(...pkgList);\r\n\r\n yield {\r\n type: 'log',\r\n text: `Fetching ${pkgList.length || 'all'} packages…`,\r\n data: { phase: 'fetch' },\r\n };\r\n\r\n const result = yield* spawnStream({\r\n cmd: pkgManager,\r\n args,\r\n cwd,\r\n signal: opts.signal,\r\n maxBytes: 100_000,\r\n });\r\n\r\n yield {\r\n type: 'final',\r\n output: {\r\n packages: pkgList,\r\n exit_code: result.exitCode,\r\n output: result.stdout || result.stderr || result.error || '',\r\n dry_run: args.includes('--dry-run'),\r\n truncated: result.truncated,\r\n },\r\n };\r\n },\r\n};\r\n\r\nasync function detectPackageManager(cwd: string): Promise<string> {\r\n const { stat } = await import('node:fs/promises');\r\n try {\r\n await stat(`${cwd}/pnpm-lock.yaml`);\r\n return 'pnpm';\r\n } catch {\r\n try {\r\n await stat(`${cwd}/yarn.lock`);\r\n return 'yarn';\r\n } catch {\r\n return 'npm';\r\n }\r\n }\r\n}\r\n"]}
|
package/dist/logs.js
CHANGED
|
@@ -125,6 +125,15 @@ var logsTool = {
|
|
|
125
125
|
async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
126
126
|
const args = ["logs"];
|
|
127
127
|
if (lines > 0) args.push("--tail", String(lines));
|
|
128
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]+$/.test(service)) {
|
|
129
|
+
return {
|
|
130
|
+
source: `docker:${service}`,
|
|
131
|
+
entries: [],
|
|
132
|
+
total: 0,
|
|
133
|
+
truncated: false,
|
|
134
|
+
stream_mode: false
|
|
135
|
+
};
|
|
136
|
+
}
|
|
128
137
|
args.push("--timestamps", service);
|
|
129
138
|
return new Promise((resolve2) => {
|
|
130
139
|
let stdout = "";
|
package/dist/logs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/_regex.ts","../src/_util.ts","../src/logs.ts"],"names":["resolve","path"],"mappings":"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","file":"logs.js","sourcesContent":["/**\n * Compile a user-supplied regex with conservative bounds against ReDoS.\n *\n * Node's regex engine (V8) is backtracking-based and cannot interrupt a\n * synchronous match — a pattern like `(a+)+$` against a sufficiently long\n * line will pin a worker for seconds. The executor's outer `timeoutMs` only\n * fires between async boundaries, so a long regex eval inside a sync loop\n * is uninterruptible.\n *\n * We can't fully prevent ReDoS without an alternative engine (re2-wasm), but\n * we can sharply limit the blast radius:\n *\n * 1. Cap pattern length — practically all legitimate user patterns are\n * under 256 characters. A 4 KB pattern is almost certainly malicious\n * or a copy-paste accident.\n * 2. Reject patterns containing the most obvious super-linear structures.\n * This is a coarse filter (false-positives are likely; we accept that\n * for hostile-input contexts).\n *\n * Callers should additionally bound the *subject* length (e.g. by capping\n * line size before matching).\n */\n\nconst MAX_PATTERN_LEN = 512;\n\n// Heuristics for catastrophic-backtracking constructs. Not exhaustive; bias\n// toward false-positives in tools that accept LLM-generated input.\nconst DANGEROUS_PATTERNS: ReadonlyArray<RegExp> = [\n /(\\([^)]*[+*][^)]*\\))[+*]/, // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier\n /(\\(\\?:[^)]*[+*][^)]*\\))[+*]/, // same, with non-capturing group\n];\n\nexport interface CompileResult {\n ok: true;\n regex: RegExp;\n}\n\nexport interface CompileFail {\n ok: false;\n reason: string;\n}\n\nexport function compileUserRegex(pattern: string, flags: string): CompileResult | CompileFail {\n if (typeof pattern !== 'string') {\n return { ok: false, reason: 'pattern must be a string' };\n }\n if (pattern.length === 0) {\n return { ok: false, reason: 'pattern is empty' };\n }\n if (pattern.length > MAX_PATTERN_LEN) {\n return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };\n }\n for (const rx of DANGEROUS_PATTERNS) {\n if (rx.test(pattern)) {\n return {\n ok: false,\n reason:\n 'pattern looks vulnerable to catastrophic backtracking — rewrite without nested quantifiers',\n };\n }\n }\n try {\n return { ok: true, regex: new RegExp(pattern, flags) };\n } catch (err) {\n return {\n ok: false,\n reason: err instanceof Error ? err.message : 'invalid regex',\n };\n }\n}\n\n/**\n * Truncate a subject line to a safe length for synchronous regex eval.\n * The cap is conservative; tools that need exact-line matching against very\n * long lines should use ripgrep externally rather than the native walker.\n */\nexport const MAX_SUBJECT_LEN = 64 * 1024;\n\nexport function capSubject(line: string): string {\n return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;\n}\n","import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import { spawn } from 'node:child_process';\nimport type { Tool } from '@wrongstack/core';\nimport { compileUserRegex } from './_regex.js';\nimport { safeResolve } from './_util.js';\n\ninterface LogsInput {\n service?: string;\n path?: string;\n lines?: number;\n stream?: boolean;\n filter?: string;\n since?: '1h' | '6h' | '24h' | 'all';\n cwd?: string;\n}\n\ninterface LogEntry {\n timestamp: string;\n level: string;\n message: string;\n source?: string;\n}\n\ninterface LogsOutput {\n source: string;\n entries: LogEntry[];\n total: number;\n truncated: boolean;\n stream_mode: boolean;\n}\n\nexport const logsTool: Tool<LogsInput, LogsOutput> = {\n name: 'logs',\n category: 'Logs',\n description:\n 'Stream or fetch logs from a service or file. Supports Docker, systemd, or plain log files.',\n usageHint:\n 'Set `service` for Docker/systemd, `path` for file. `lines` limits output. `stream` for tail -f behavior. `filter` regex filters lines.',\n permission: 'confirm',\n mutating: false,\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n service: {\n type: 'string',\n description: 'Service name for Docker or systemd journal',\n },\n path: {\n type: 'string',\n description: 'Path to log file (alternative to service)',\n },\n lines: {\n type: 'integer',\n description: 'Number of log lines to fetch (default: 100, 0 for all)',\n minimum: 0,\n maximum: 10000,\n },\n stream: {\n type: 'boolean',\n description: 'Stream logs continuously (like tail -f) (default: false)',\n },\n filter: {\n type: 'string',\n description: 'Regex pattern to filter log lines',\n },\n since: {\n type: 'string',\n enum: ['1h', '6h', '24h', 'all'],\n description: 'Only show logs since duration',\n },\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n },\n },\n async execute(input, ctx, opts) {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const lines = input.lines ?? 100;\n let filterRe: RegExp | null = null;\n if (input.filter) {\n const compiled = compileUserRegex(input.filter, 'i');\n if (!compiled.ok) {\n throw new Error(`logs: ${compiled.reason}`);\n }\n filterRe = compiled.regex;\n }\n\n if (input.service) {\n return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);\n }\n\n if (input.path) {\n return await fileLogs(safeResolve(input.path, ctx), lines, filterRe, input.stream ?? false);\n }\n\n return {\n source: 'none',\n entries: [],\n total: 0,\n truncated: false,\n stream_mode: false,\n };\n },\n};\n\nasync function dockerLogs(\n service: string,\n lines: number,\n filterRe: RegExp | null,\n cwd: string,\n signal: AbortSignal,\n since?: string,\n): Promise<LogsOutput> {\n const args = ['logs'];\n if (lines > 0) args.push('--tail', String(lines));\n if (since) {\n const sinceMap: Record<string, string> = { '1h': '1h', '6h': '6h', '24h': '24h' };\n args.push('--since', sinceMap[since] ?? '1h');\n }\n args.push('--timestamps', service);\n\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n const MAX = 200_000;\n\n const child = spawn('docker', args, { cwd, signal, stdio: ['ignore', 'pipe', 'pipe'] });\n child.stdout?.on('data', (c) => {\n if (stdout.length < MAX) stdout += c.toString();\n });\n child.stderr?.on('data', (c) => {\n if (stderr.length < MAX) stderr += c.toString();\n });\n child.on('close', (code) => {\n const output = stdout + stderr;\n const entries = parseLogLines(output, filterRe);\n resolve({\n source: `docker:${service}`,\n entries,\n total: entries.length,\n truncated: output.length >= MAX,\n stream_mode: false,\n });\n });\n child.on('error', (e) =>\n resolve({\n source: `docker:${service}`,\n entries: [],\n total: 0,\n truncated: false,\n stream_mode: false,\n }),\n );\n });\n}\n\n// Hard cap on tail-window size — `lines: 0` historically meant \"all\" and\n// happily buffered an entire multi-GB log into memory. Cap at 100k lines;\n// callers that need more should narrow with `filter`.\nconst MAX_TAIL_LINES = 100_000;\n\nasync function fileLogs(\n path: string,\n lines: number,\n filterRe: RegExp | null,\n stream: boolean,\n): Promise<LogsOutput> {\n const { createInterface } = await import('node:readline');\n const { createReadStream } = await import('node:fs');\n const entries: LogEntry[] = [];\n\n // Effective tail window: clamp to MAX_TAIL_LINES; treat 0 / negative as\n // \"max window\" rather than \"unlimited\" so a malicious /proc/kcore path\n // cannot OOM the worker.\n const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;\n // Rolling window backed by a fixed-size circular buffer — at most\n // `effLines` strings live in memory regardless of file size.\n const window: string[] = new Array(effLines);\n let writeIdx = 0;\n let totalLines = 0;\n\n const rl = createInterface({\n input: createReadStream(path),\n crlfDelay: Number.POSITIVE_INFINITY,\n });\n\n for await (const line of rl) {\n if (filterRe && !filterRe.test(line)) continue;\n window[writeIdx] = line;\n writeIdx = (writeIdx + 1) % effLines;\n totalLines++;\n }\n\n // Read the window back in arrival order.\n const ordered: string[] = [];\n const start = totalLines >= effLines ? writeIdx : 0;\n const count = Math.min(totalLines, effLines);\n for (let i = 0; i < count; i++) {\n const v = window[(start + i) % effLines];\n if (v !== undefined) ordered.push(v);\n }\n\n for (const line of ordered) {\n const parsed = parseLine(line);\n if (parsed) entries.push(parsed);\n }\n\n return {\n source: path,\n entries,\n total: entries.length,\n truncated: totalLines > effLines,\n stream_mode: stream,\n };\n}\n\nfunction parseLogLines(output: string, filterRe: RegExp | null): LogEntry[] {\n const lines = output.split('\\n').filter(Boolean);\n const entries: LogEntry[] = [];\n\n for (const line of lines) {\n if (filterRe && !filterRe.test(line)) continue;\n const parsed = parseLine(line);\n if (parsed) entries.push(parsed);\n }\n\n return entries;\n}\n\nfunction parseLine(line: string): LogEntry | null {\n const tsRe = /^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z?)\\s+(?:\\[?(\\w+)\\]?)\\s*(.*)/;\n const match = tsRe.exec(line);\n\n if (match) {\n return {\n timestamp: match[1] ?? '',\n level: match[2]?.toLowerCase() ?? 'info',\n message: match[3] ?? '',\n };\n }\n\n const levelRe = /(?:ERROR|WARN|INFO|DEBUG|TRACE)\\s+(.*)/i;\n const levelMatch = levelRe.exec(line);\n\n if (levelMatch) {\n return {\n timestamp: '',\n level: levelMatch[1]?.toLowerCase() ?? 'info',\n message: levelMatch[2] ?? line,\n };\n }\n\n return {\n timestamp: '',\n level: 'info',\n message: line,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/_regex.ts","../src/_util.ts","../src/logs.ts"],"names":["resolve","path"],"mappings":"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","file":"logs.js","sourcesContent":["/**\n * Compile a user-supplied regex with conservative bounds against ReDoS.\n *\n * Node's regex engine (V8) is backtracking-based and cannot interrupt a\n * synchronous match — a pattern like `(a+)+$` against a sufficiently long\n * line will pin a worker for seconds. The executor's outer `timeoutMs` only\n * fires between async boundaries, so a long regex eval inside a sync loop\n * is uninterruptible.\n *\n * We can't fully prevent ReDoS without an alternative engine (re2-wasm), but\n * we can sharply limit the blast radius:\n *\n * 1. Cap pattern length — practically all legitimate user patterns are\n * under 256 characters. A 4 KB pattern is almost certainly malicious\n * or a copy-paste accident.\n * 2. Reject patterns containing the most obvious super-linear structures.\n * This is a coarse filter (false-positives are likely; we accept that\n * for hostile-input contexts).\n *\n * Callers should additionally bound the *subject* length (e.g. by capping\n * line size before matching).\n */\n\nconst MAX_PATTERN_LEN = 512;\n\n// Heuristics for catastrophic-backtracking constructs. Not exhaustive; bias\n// toward false-positives in tools that accept LLM-generated input.\nconst DANGEROUS_PATTERNS: ReadonlyArray<RegExp> = [\n /(\\([^)]*[+*][^)]*\\))[+*]/, // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier\n /(\\(\\?:[^)]*[+*][^)]*\\))[+*]/, // same, with non-capturing group\n];\n\nexport interface CompileResult {\n ok: true;\n regex: RegExp;\n}\n\nexport interface CompileFail {\n ok: false;\n reason: string;\n}\n\nexport function compileUserRegex(pattern: string, flags: string): CompileResult | CompileFail {\n if (typeof pattern !== 'string') {\n return { ok: false, reason: 'pattern must be a string' };\n }\n if (pattern.length === 0) {\n return { ok: false, reason: 'pattern is empty' };\n }\n if (pattern.length > MAX_PATTERN_LEN) {\n return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };\n }\n for (const rx of DANGEROUS_PATTERNS) {\n if (rx.test(pattern)) {\n return {\n ok: false,\n reason:\n 'pattern looks vulnerable to catastrophic backtracking — rewrite without nested quantifiers',\n };\n }\n }\n try {\n return { ok: true, regex: new RegExp(pattern, flags) };\n } catch (err) {\n return {\n ok: false,\n reason: err instanceof Error ? err.message : 'invalid regex',\n };\n }\n}\n\n/**\n * Truncate a subject line to a safe length for synchronous regex eval.\n * The cap is conservative; tools that need exact-line matching against very\n * long lines should use ripgrep externally rather than the native walker.\n */\nexport const MAX_SUBJECT_LEN = 64 * 1024;\n\nexport function capSubject(line: string): string {\n return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;\n}\n","import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import { spawn } from 'node:child_process';\r\nimport type { Tool } from '@wrongstack/core';\r\nimport { compileUserRegex } from './_regex.js';\r\nimport { safeResolve } from './_util.js';\r\n\r\ninterface LogsInput {\r\n service?: string;\r\n path?: string;\r\n lines?: number;\r\n stream?: boolean;\r\n filter?: string;\r\n since?: '1h' | '6h' | '24h' | 'all';\r\n cwd?: string;\r\n}\r\n\r\ninterface LogEntry {\r\n timestamp: string;\r\n level: string;\r\n message: string;\r\n source?: string;\r\n}\r\n\r\ninterface LogsOutput {\r\n source: string;\r\n entries: LogEntry[];\r\n total: number;\r\n truncated: boolean;\r\n stream_mode: boolean;\r\n}\r\n\r\nexport const logsTool: Tool<LogsInput, LogsOutput> = {\r\n name: 'logs',\r\n category: 'Logs',\r\n description:\r\n 'Stream or fetch logs from a service or file. Supports Docker, systemd, or plain log files.',\r\n usageHint:\r\n 'Set `service` for Docker/systemd, `path` for file. `lines` limits output. `stream` for tail -f behavior. `filter` regex filters lines.',\r\n permission: 'confirm',\r\n mutating: false,\r\n timeoutMs: 30_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n service: {\r\n type: 'string',\r\n description: 'Service name for Docker or systemd journal',\r\n },\r\n path: {\r\n type: 'string',\r\n description: 'Path to log file (alternative to service)',\r\n },\r\n lines: {\r\n type: 'integer',\r\n description: 'Number of log lines to fetch (default: 100, 0 for all)',\r\n minimum: 0,\r\n maximum: 10000,\r\n },\r\n stream: {\r\n type: 'boolean',\r\n description: 'Stream logs continuously (like tail -f) (default: false)',\r\n },\r\n filter: {\r\n type: 'string',\r\n description: 'Regex pattern to filter log lines',\r\n },\r\n since: {\r\n type: 'string',\r\n enum: ['1h', '6h', '24h', 'all'],\r\n description: 'Only show logs since duration',\r\n },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n },\r\n },\r\n async execute(input, ctx, opts) {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const lines = input.lines ?? 100;\r\n let filterRe: RegExp | null = null;\r\n if (input.filter) {\r\n const compiled = compileUserRegex(input.filter, 'i');\r\n if (!compiled.ok) {\r\n throw new Error(`logs: ${compiled.reason}`);\r\n }\r\n filterRe = compiled.regex;\r\n }\r\n\r\n if (input.service) {\r\n return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);\r\n }\r\n\r\n if (input.path) {\r\n return await fileLogs(safeResolve(input.path, ctx), lines, filterRe, input.stream ?? false);\r\n }\r\n\r\n return {\r\n source: 'none',\r\n entries: [],\r\n total: 0,\r\n truncated: false,\r\n stream_mode: false,\r\n };\r\n },\r\n};\r\n\r\nasync function dockerLogs(\r\n service: string,\r\n lines: number,\r\n filterRe: RegExp | null,\r\n cwd: string,\r\n signal: AbortSignal,\r\n since?: string,\r\n): Promise<LogsOutput> {\r\n const args = ['logs'];\r\n if (lines > 0) args.push('--tail', String(lines));\r\n if (since) {\r\n const sinceMap: Record<string, string> = { '1h': '1h', '6h': '6h', '24h': '24h' };\r\n args.push('--since', sinceMap[since] ?? '1h');\r\n }\r\n // Validate service name to prevent container name injection.\r\n // Docker container names are limited to [a-zA-Z0-9][a-zA-Z0-9._-]+.\r\n if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]+$/.test(service)) {\r\n return {\r\n source: `docker:${service}`,\r\n entries: [],\r\n total: 0,\r\n truncated: false,\r\n stream_mode: false,\r\n };\r\n }\r\n args.push('--timestamps', service);\r\n\r\n return new Promise((resolve) => {\r\n let stdout = '';\r\n let stderr = '';\r\n const MAX = 200_000;\r\n\r\n const child = spawn('docker', args, { cwd, signal, stdio: ['ignore', 'pipe', 'pipe'] });\r\n child.stdout?.on('data', (c) => {\r\n if (stdout.length < MAX) stdout += c.toString();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n if (stderr.length < MAX) stderr += c.toString();\r\n });\r\n child.on('close', (code) => {\r\n const output = stdout + stderr;\r\n const entries = parseLogLines(output, filterRe);\r\n resolve({\r\n source: `docker:${service}`,\r\n entries,\r\n total: entries.length,\r\n truncated: output.length >= MAX,\r\n stream_mode: false,\r\n });\r\n });\r\n child.on('error', (e) =>\r\n resolve({\r\n source: `docker:${service}`,\r\n entries: [],\r\n total: 0,\r\n truncated: false,\r\n stream_mode: false,\r\n }),\r\n );\r\n });\r\n}\r\n\r\n// Hard cap on tail-window size — `lines: 0` historically meant \"all\" and\r\n// happily buffered an entire multi-GB log into memory. Cap at 100k lines;\r\n// callers that need more should narrow with `filter`.\r\nconst MAX_TAIL_LINES = 100_000;\r\n\r\nasync function fileLogs(\r\n path: string,\r\n lines: number,\r\n filterRe: RegExp | null,\r\n stream: boolean,\r\n): Promise<LogsOutput> {\r\n const { createInterface } = await import('node:readline');\r\n const { createReadStream } = await import('node:fs');\r\n const entries: LogEntry[] = [];\r\n\r\n // Effective tail window: clamp to MAX_TAIL_LINES; treat 0 / negative as\r\n // \"max window\" rather than \"unlimited\" so a malicious /proc/kcore path\r\n // cannot OOM the worker.\r\n const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;\r\n // Rolling window backed by a fixed-size circular buffer — at most\r\n // `effLines` strings live in memory regardless of file size.\r\n const window: string[] = new Array(effLines);\r\n let writeIdx = 0;\r\n let totalLines = 0;\r\n\r\n const rl = createInterface({\r\n input: createReadStream(path),\r\n crlfDelay: Number.POSITIVE_INFINITY,\r\n });\r\n\r\n for await (const line of rl) {\r\n if (filterRe && !filterRe.test(line)) continue;\r\n window[writeIdx] = line;\r\n writeIdx = (writeIdx + 1) % effLines;\r\n totalLines++;\r\n }\r\n\r\n // Read the window back in arrival order.\r\n const ordered: string[] = [];\r\n const start = totalLines >= effLines ? writeIdx : 0;\r\n const count = Math.min(totalLines, effLines);\r\n for (let i = 0; i < count; i++) {\r\n const v = window[(start + i) % effLines];\r\n if (v !== undefined) ordered.push(v);\r\n }\r\n\r\n for (const line of ordered) {\r\n const parsed = parseLine(line);\r\n if (parsed) entries.push(parsed);\r\n }\r\n\r\n return {\r\n source: path,\r\n entries,\r\n total: entries.length,\r\n truncated: totalLines > effLines,\r\n stream_mode: stream,\r\n };\r\n}\r\n\r\nfunction parseLogLines(output: string, filterRe: RegExp | null): LogEntry[] {\r\n const lines = output.split('\\n').filter(Boolean);\r\n const entries: LogEntry[] = [];\r\n\r\n for (const line of lines) {\r\n if (filterRe && !filterRe.test(line)) continue;\r\n const parsed = parseLine(line);\r\n if (parsed) entries.push(parsed);\r\n }\r\n\r\n return entries;\r\n}\r\n\r\nfunction parseLine(line: string): LogEntry | null {\r\n const tsRe = /^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z?)\\s+(?:\\[?(\\w+)\\]?)\\s*(.*)/;\r\n const match = tsRe.exec(line);\r\n\r\n if (match) {\r\n return {\r\n timestamp: match[1] ?? '',\r\n level: match[2]?.toLowerCase() ?? 'info',\r\n message: match[3] ?? '',\r\n };\r\n }\r\n\r\n const levelRe = /(?:ERROR|WARN|INFO|DEBUG|TRACE)\\s+(.*)/i;\r\n const levelMatch = levelRe.exec(line);\r\n\r\n if (levelMatch) {\r\n return {\r\n timestamp: '',\r\n level: levelMatch[1]?.toLowerCase() ?? 'info',\r\n message: levelMatch[2] ?? line,\r\n };\r\n }\r\n\r\n return {\r\n timestamp: '',\r\n level: 'info',\r\n message: line,\r\n };\r\n}\r\n"]}
|
package/dist/pack.js
CHANGED
|
@@ -61,8 +61,8 @@ async function* spawnStream(opts) {
|
|
|
61
61
|
let spawnFailed = false;
|
|
62
62
|
for (; ; ) {
|
|
63
63
|
while (queue.length === 0) {
|
|
64
|
-
await new Promise((
|
|
65
|
-
waiter =
|
|
64
|
+
await new Promise((resolve5) => {
|
|
65
|
+
waiter = resolve5;
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
const chunk = queue.shift();
|
|
@@ -284,12 +284,41 @@ var bashTool = {
|
|
|
284
284
|
signal: opts.signal
|
|
285
285
|
});
|
|
286
286
|
if (input.background) {
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
let buf2 = "";
|
|
288
|
+
let truncated = false;
|
|
289
|
+
const child2 = spawn(shell, args, {
|
|
290
|
+
cwd: ctx.projectRoot,
|
|
291
|
+
env,
|
|
292
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
293
|
+
detached: true,
|
|
294
|
+
signal: opts.signal
|
|
295
|
+
});
|
|
296
|
+
const pid = child2.pid;
|
|
297
|
+
child2.stdout?.on("data", (chunk) => {
|
|
298
|
+
if (!truncated) {
|
|
299
|
+
const remain = MAX_OUTPUT - buf2.length;
|
|
300
|
+
if (remain > 0) {
|
|
301
|
+
buf2 += chunk.toString().slice(0, remain);
|
|
302
|
+
}
|
|
303
|
+
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
child2.stderr?.on("data", (chunk) => {
|
|
307
|
+
if (!truncated) {
|
|
308
|
+
const remain = MAX_OUTPUT - buf2.length;
|
|
309
|
+
if (remain > 0) {
|
|
310
|
+
buf2 += chunk.toString().slice(0, remain);
|
|
311
|
+
}
|
|
312
|
+
if (buf2.length >= MAX_OUTPUT) truncated = true;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
child2.on("close", () => {
|
|
316
|
+
});
|
|
317
|
+
if (typeof pid === "number") child2.unref();
|
|
289
318
|
yield {
|
|
290
319
|
type: "final",
|
|
291
320
|
output: {
|
|
292
|
-
output:
|
|
321
|
+
output: truncated ? buf2.slice(0, MAX_OUTPUT) + "\u2026[truncated]" : buf2,
|
|
293
322
|
exit_code: null,
|
|
294
323
|
timed_out: false,
|
|
295
324
|
pid
|
|
@@ -334,6 +363,7 @@ var bashTool = {
|
|
|
334
363
|
}
|
|
335
364
|
}, 2e3);
|
|
336
365
|
timers.push(killTimer);
|
|
366
|
+
killTimer.unref?.();
|
|
337
367
|
} catch {
|
|
338
368
|
}
|
|
339
369
|
}
|
|
@@ -351,10 +381,10 @@ var bashTool = {
|
|
|
351
381
|
queue.push(c);
|
|
352
382
|
}
|
|
353
383
|
};
|
|
354
|
-
const next = () => new Promise((
|
|
384
|
+
const next = () => new Promise((resolve5) => {
|
|
355
385
|
const c = queue.shift();
|
|
356
|
-
if (c)
|
|
357
|
-
else resolveNext =
|
|
386
|
+
if (c) resolve5(c);
|
|
387
|
+
else resolveNext = resolve5;
|
|
358
388
|
});
|
|
359
389
|
let lastFlush = Date.now();
|
|
360
390
|
const flush = () => {
|
|
@@ -589,7 +619,7 @@ function findGitDir(cwd) {
|
|
|
589
619
|
return null;
|
|
590
620
|
}
|
|
591
621
|
function runGit(args, cwd, signal) {
|
|
592
|
-
return new Promise((
|
|
622
|
+
return new Promise((resolve5) => {
|
|
593
623
|
let stdout = "";
|
|
594
624
|
let stderr = "";
|
|
595
625
|
const child = spawn("git", args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -599,8 +629,8 @@ function runGit(args, cwd, signal) {
|
|
|
599
629
|
child.stderr?.on("data", (c) => {
|
|
600
630
|
stderr += c.toString();
|
|
601
631
|
});
|
|
602
|
-
child.on("close", (code) =>
|
|
603
|
-
child.on("error", (e) =>
|
|
632
|
+
child.on("close", (code) => resolve5({ stdout, stderr, exitCode: code ?? 0 }));
|
|
633
|
+
child.on("error", (e) => resolve5({ stdout: "", stderr: e.message, exitCode: 1 }));
|
|
604
634
|
});
|
|
605
635
|
}
|
|
606
636
|
async function fileDiff(input, ctx, signal) {
|
|
@@ -934,7 +964,7 @@ var ALLOWED_COMMANDS = {
|
|
|
934
964
|
cargo: ["--version", "build", "test", "check"],
|
|
935
965
|
rustc: ["--version"],
|
|
936
966
|
go: ["version", "run", "build", "test"],
|
|
937
|
-
python: ["--version"
|
|
967
|
+
python: ["--version"],
|
|
938
968
|
pip: ["--version", "install", "list"],
|
|
939
969
|
docker: ["--version", "ps", "images", "build"],
|
|
940
970
|
kubectl: ["version", "get", "describe", "logs"]
|
|
@@ -942,6 +972,30 @@ var ALLOWED_COMMANDS = {
|
|
|
942
972
|
var MAX_ARGS = 20;
|
|
943
973
|
var MAX_OUTPUT2 = 2e5;
|
|
944
974
|
var TIMEOUT_MS = 3e4;
|
|
975
|
+
var BLOCKED_ARG_PATTERNS = {
|
|
976
|
+
// python -c/--command executes arbitrary code; python -m runs modules
|
|
977
|
+
python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
|
|
978
|
+
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack
|
|
979
|
+
git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/],
|
|
980
|
+
// node -r/--require preloads arbitrary modules; --eval executes code
|
|
981
|
+
node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
|
|
982
|
+
// go run could execute arbitrary .go files; -ldflags could inject build-time code
|
|
983
|
+
go: [/^-ldflags$/],
|
|
984
|
+
// bun --preload is similar to node --require
|
|
985
|
+
bun: [/^--preload$/]
|
|
986
|
+
};
|
|
987
|
+
function validateArgs(cmd, args) {
|
|
988
|
+
const blocked = BLOCKED_ARG_PATTERNS[cmd];
|
|
989
|
+
if (!blocked) return null;
|
|
990
|
+
for (const arg of args) {
|
|
991
|
+
for (const pattern of blocked) {
|
|
992
|
+
if (pattern.test(arg)) {
|
|
993
|
+
return `Blocked argument "${arg}" for command "${cmd}" (matches security pattern ${pattern})`;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
945
999
|
var execTool = {
|
|
946
1000
|
name: "exec",
|
|
947
1001
|
category: "Shell",
|
|
@@ -985,6 +1039,18 @@ var execTool = {
|
|
|
985
1039
|
}
|
|
986
1040
|
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
987
1041
|
const timeout = Math.max(1, Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS));
|
|
1042
|
+
const argError = validateArgs(cmd, args);
|
|
1043
|
+
if (argError) {
|
|
1044
|
+
return {
|
|
1045
|
+
command: cmd,
|
|
1046
|
+
args,
|
|
1047
|
+
stdout: "",
|
|
1048
|
+
stderr: argError,
|
|
1049
|
+
exitCode: 1,
|
|
1050
|
+
truncated: false,
|
|
1051
|
+
allowed: false
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
988
1054
|
const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
|
|
989
1055
|
const rel = path.relative(ctx.projectRoot, requestedCwd);
|
|
990
1056
|
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
@@ -1004,7 +1070,7 @@ var execTool = {
|
|
|
1004
1070
|
}
|
|
1005
1071
|
};
|
|
1006
1072
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
1007
|
-
return new Promise((
|
|
1073
|
+
return new Promise((resolve5) => {
|
|
1008
1074
|
let stdout = "";
|
|
1009
1075
|
let stderr = "";
|
|
1010
1076
|
let killed = false;
|
|
@@ -1026,7 +1092,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1026
1092
|
});
|
|
1027
1093
|
child.on("close", (code) => {
|
|
1028
1094
|
clearTimeout(timer);
|
|
1029
|
-
|
|
1095
|
+
resolve5({
|
|
1030
1096
|
command: cmd,
|
|
1031
1097
|
args,
|
|
1032
1098
|
stdout: stdout.slice(0, MAX_OUTPUT2),
|
|
@@ -1038,7 +1104,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1038
1104
|
});
|
|
1039
1105
|
child.on("error", (err) => {
|
|
1040
1106
|
clearTimeout(timer);
|
|
1041
|
-
|
|
1107
|
+
resolve5({
|
|
1042
1108
|
command: cmd,
|
|
1043
1109
|
args,
|
|
1044
1110
|
stdout: stdout.slice(0, MAX_OUTPUT2),
|
|
@@ -1565,7 +1631,7 @@ function buildArgs(input) {
|
|
|
1565
1631
|
}
|
|
1566
1632
|
}
|
|
1567
1633
|
function runGit2(args, cwd, signal) {
|
|
1568
|
-
return new Promise((
|
|
1634
|
+
return new Promise((resolve5) => {
|
|
1569
1635
|
let stdout = "";
|
|
1570
1636
|
let stderr = "";
|
|
1571
1637
|
const child = spawn("git", args, {
|
|
@@ -1584,7 +1650,7 @@ function runGit2(args, cwd, signal) {
|
|
|
1584
1650
|
}
|
|
1585
1651
|
});
|
|
1586
1652
|
child.on("error", (err) => {
|
|
1587
|
-
|
|
1653
|
+
resolve5({
|
|
1588
1654
|
command: args[0],
|
|
1589
1655
|
stdout,
|
|
1590
1656
|
stderr: err.message,
|
|
@@ -1593,7 +1659,7 @@ function runGit2(args, cwd, signal) {
|
|
|
1593
1659
|
});
|
|
1594
1660
|
});
|
|
1595
1661
|
child.on("close", (code) => {
|
|
1596
|
-
|
|
1662
|
+
resolve5({
|
|
1597
1663
|
command: args[0],
|
|
1598
1664
|
stdout: stdout.slice(0, MAX_OUTPUT3),
|
|
1599
1665
|
stderr: stderr.slice(0, MAX_OUTPUT3),
|
|
@@ -1774,13 +1840,13 @@ var grepTool = {
|
|
|
1774
1840
|
}
|
|
1775
1841
|
};
|
|
1776
1842
|
async function detectRg(signal) {
|
|
1777
|
-
return new Promise((
|
|
1843
|
+
return new Promise((resolve5) => {
|
|
1778
1844
|
try {
|
|
1779
1845
|
const p = spawn("rg", ["--version"], { stdio: "ignore", signal });
|
|
1780
|
-
p.on("error", () =>
|
|
1781
|
-
p.on("close", (code) =>
|
|
1846
|
+
p.on("error", () => resolve5(false));
|
|
1847
|
+
p.on("close", (code) => resolve5(code === 0));
|
|
1782
1848
|
} catch {
|
|
1783
|
-
|
|
1849
|
+
resolve5(false);
|
|
1784
1850
|
}
|
|
1785
1851
|
});
|
|
1786
1852
|
}
|
|
@@ -2039,6 +2105,22 @@ var installTool = {
|
|
|
2039
2105
|
const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
|
|
2040
2106
|
(p) => p.trim()
|
|
2041
2107
|
) : [];
|
|
2108
|
+
const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
|
|
2109
|
+
for (const pkg of pkgList) {
|
|
2110
|
+
if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-")) {
|
|
2111
|
+
yield {
|
|
2112
|
+
type: "final",
|
|
2113
|
+
output: {
|
|
2114
|
+
packages: pkgList,
|
|
2115
|
+
exit_code: 1,
|
|
2116
|
+
output: `Invalid package name "${pkg}". Names must match ${PKG_NAME_RE} and not start with "-".`,
|
|
2117
|
+
dry_run: Boolean(input.dry_run),
|
|
2118
|
+
truncated: false
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2042
2124
|
if (pkgList.length > 0) args.push(...pkgList);
|
|
2043
2125
|
yield {
|
|
2044
2126
|
type: "log",
|
|
@@ -2361,8 +2443,17 @@ var logsTool = {
|
|
|
2361
2443
|
async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
2362
2444
|
const args = ["logs"];
|
|
2363
2445
|
if (lines > 0) args.push("--tail", String(lines));
|
|
2446
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]+$/.test(service)) {
|
|
2447
|
+
return {
|
|
2448
|
+
source: `docker:${service}`,
|
|
2449
|
+
entries: [],
|
|
2450
|
+
total: 0,
|
|
2451
|
+
truncated: false,
|
|
2452
|
+
stream_mode: false
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2364
2455
|
args.push("--timestamps", service);
|
|
2365
|
-
return new Promise((
|
|
2456
|
+
return new Promise((resolve5) => {
|
|
2366
2457
|
let stdout = "";
|
|
2367
2458
|
let stderr = "";
|
|
2368
2459
|
const MAX = 2e5;
|
|
@@ -2376,7 +2467,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
2376
2467
|
child.on("close", (code) => {
|
|
2377
2468
|
const output = stdout + stderr;
|
|
2378
2469
|
const entries = parseLogLines(output, filterRe);
|
|
2379
|
-
|
|
2470
|
+
resolve5({
|
|
2380
2471
|
source: `docker:${service}`,
|
|
2381
2472
|
entries,
|
|
2382
2473
|
total: entries.length,
|
|
@@ -2386,7 +2477,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
2386
2477
|
});
|
|
2387
2478
|
child.on(
|
|
2388
2479
|
"error",
|
|
2389
|
-
(e) =>
|
|
2480
|
+
(e) => resolve5({
|
|
2390
2481
|
source: `docker:${service}`,
|
|
2391
2482
|
entries: [],
|
|
2392
2483
|
total: 0,
|
|
@@ -2520,7 +2611,7 @@ async function detectManager2(cwd) {
|
|
|
2520
2611
|
return "npm";
|
|
2521
2612
|
}
|
|
2522
2613
|
function runOutdated(manager, args, cwd, signal) {
|
|
2523
|
-
return new Promise((
|
|
2614
|
+
return new Promise((resolve5) => {
|
|
2524
2615
|
let stdout = "";
|
|
2525
2616
|
let stderr = "";
|
|
2526
2617
|
const MAX = 1e5;
|
|
@@ -2533,11 +2624,11 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
2533
2624
|
});
|
|
2534
2625
|
child.on("close", (code) => {
|
|
2535
2626
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
2536
|
-
|
|
2627
|
+
resolve5(result);
|
|
2537
2628
|
});
|
|
2538
2629
|
child.on(
|
|
2539
2630
|
"error",
|
|
2540
|
-
(e) =>
|
|
2631
|
+
(e) => resolve5({
|
|
2541
2632
|
exit_code: 1,
|
|
2542
2633
|
packages: [],
|
|
2543
2634
|
total: 0,
|
|
@@ -2620,7 +2711,7 @@ var patchTool = {
|
|
|
2620
2711
|
};
|
|
2621
2712
|
}
|
|
2622
2713
|
}
|
|
2623
|
-
const tmpDir = await fs9.mkdtemp(path.join(
|
|
2714
|
+
const tmpDir = await fs9.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
|
|
2624
2715
|
try {
|
|
2625
2716
|
await fs9.chmod(tmpDir, 448).catch(() => {
|
|
2626
2717
|
});
|
|
@@ -2667,7 +2758,7 @@ function stripPathComponents(p, strip) {
|
|
|
2667
2758
|
return parts.slice(strip).join("/");
|
|
2668
2759
|
}
|
|
2669
2760
|
function runPatch(args, cwd, signal) {
|
|
2670
|
-
return new Promise((
|
|
2761
|
+
return new Promise((resolve5) => {
|
|
2671
2762
|
let stdout = "";
|
|
2672
2763
|
let stderr = "";
|
|
2673
2764
|
const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
|
|
@@ -2678,8 +2769,8 @@ function runPatch(args, cwd, signal) {
|
|
|
2678
2769
|
child.stderr?.on("data", (c) => {
|
|
2679
2770
|
stderr += c.toString();
|
|
2680
2771
|
});
|
|
2681
|
-
child.on("close", (code) =>
|
|
2682
|
-
child.on("error", (e) =>
|
|
2772
|
+
child.on("close", (code) => resolve5({ exitCode: code ?? 1, stdout, stderr }));
|
|
2773
|
+
child.on("error", (e) => resolve5({ exitCode: 1, stdout: "", stderr: e.message }));
|
|
2683
2774
|
});
|
|
2684
2775
|
}
|
|
2685
2776
|
function extractPatchedFiles(output) {
|
|
@@ -2810,7 +2901,14 @@ var readTool = {
|
|
|
2810
2901
|
async execute(input, ctx) {
|
|
2811
2902
|
if (!input?.path) throw new Error("read: path is required");
|
|
2812
2903
|
const absPath = safeResolve(input.path, ctx);
|
|
2813
|
-
|
|
2904
|
+
let stat9;
|
|
2905
|
+
try {
|
|
2906
|
+
stat9 = await fs9.stat(absPath);
|
|
2907
|
+
} catch (err) {
|
|
2908
|
+
const code = err.code;
|
|
2909
|
+
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
2910
|
+
throw new Error(`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`);
|
|
2911
|
+
}
|
|
2814
2912
|
if (!stat9.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
2815
2913
|
if (stat9.size > MAX_BYTES2) {
|
|
2816
2914
|
throw new Error(`read: file too large (${stat9.size} bytes, limit ${MAX_BYTES2})`);
|
|
@@ -2975,13 +3073,13 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
2975
3073
|
return await globNative(pattern, base, extraGlob);
|
|
2976
3074
|
}
|
|
2977
3075
|
function checkRg() {
|
|
2978
|
-
return new Promise((
|
|
3076
|
+
return new Promise((resolve5) => {
|
|
2979
3077
|
try {
|
|
2980
3078
|
const p = spawn("rg", ["--version"], { stdio: "ignore" });
|
|
2981
|
-
p.on("error", () =>
|
|
2982
|
-
p.on("close", (code) =>
|
|
3079
|
+
p.on("error", () => resolve5(false));
|
|
3080
|
+
p.on("close", (code) => resolve5(code === 0));
|
|
2983
3081
|
} catch {
|
|
2984
|
-
|
|
3082
|
+
resolve5(false);
|
|
2985
3083
|
}
|
|
2986
3084
|
});
|
|
2987
3085
|
}
|
|
@@ -2993,10 +3091,10 @@ function spawnRgFind(pattern, base) {
|
|
|
2993
3091
|
buf += chunk.toString();
|
|
2994
3092
|
});
|
|
2995
3093
|
return {
|
|
2996
|
-
promise: new Promise((
|
|
3094
|
+
promise: new Promise((resolve5, reject) => {
|
|
2997
3095
|
child.on("error", reject);
|
|
2998
3096
|
child.on("close", () => {
|
|
2999
|
-
|
|
3097
|
+
resolve5(buf.split("\n").filter(Boolean));
|
|
3000
3098
|
});
|
|
3001
3099
|
})
|
|
3002
3100
|
};
|
|
@@ -3160,7 +3258,7 @@ var scaffoldTool = {
|
|
|
3160
3258
|
const vars = { name, ...input.vars };
|
|
3161
3259
|
const builtIn = BUILT_IN_TEMPLATES[input.template];
|
|
3162
3260
|
if (builtIn) {
|
|
3163
|
-
return await handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
|
|
3261
|
+
return await handleBuiltIn(name, builtIn.files, cwd, ctx, input.dry_run ?? false, vars);
|
|
3164
3262
|
}
|
|
3165
3263
|
return {
|
|
3166
3264
|
template: input.template,
|
|
@@ -3172,12 +3270,19 @@ var scaffoldTool = {
|
|
|
3172
3270
|
};
|
|
3173
3271
|
}
|
|
3174
3272
|
};
|
|
3175
|
-
async function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
|
|
3273
|
+
async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
3176
3274
|
const files = [];
|
|
3177
3275
|
let filesCreated = 0;
|
|
3178
3276
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
3179
3277
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
3180
|
-
const
|
|
3278
|
+
const joinedPath = path.join(cwd, resolvedPath);
|
|
3279
|
+
const root = path.resolve(ctx.projectRoot);
|
|
3280
|
+
const target = path.resolve(joinedPath);
|
|
3281
|
+
const rel = path.relative(root, target);
|
|
3282
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
3283
|
+
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
3284
|
+
}
|
|
3285
|
+
const fullPath = target;
|
|
3181
3286
|
if (!dryRun) {
|
|
3182
3287
|
await fs9.mkdir(path.dirname(fullPath), { recursive: true });
|
|
3183
3288
|
await fs9.writeFile(fullPath, substituteVars(content, name, vars), "utf8");
|
|
@@ -4147,11 +4252,11 @@ var writeTool = {
|
|
|
4147
4252
|
existed = stat10.isFile();
|
|
4148
4253
|
if (existed) {
|
|
4149
4254
|
if (!ctx.hasRead(absPath)) {
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4255
|
+
prev = await fs9.readFile(absPath, "utf8");
|
|
4256
|
+
ctx.recordRead(absPath, stat10.mtimeMs);
|
|
4257
|
+
} else {
|
|
4258
|
+
prev = await fs9.readFile(absPath, "utf8");
|
|
4153
4259
|
}
|
|
4154
|
-
prev = await fs9.readFile(absPath, "utf8");
|
|
4155
4260
|
}
|
|
4156
4261
|
} catch (err) {
|
|
4157
4262
|
if (err.code !== "ENOENT") {
|