@wrongstack/tools 0.313.0 → 0.313.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.js +10 -4055
- package/dist/bash.js +9 -2308
- package/dist/batch-tool-use.js +2 -187
- package/dist/browser/index.js +25 -1339
- package/dist/builtin.js +69 -33949
- package/dist/chunk-2CTB3UK5.js +197 -0
- package/dist/chunk-2ME7ZWDD.js +1272 -0
- package/dist/chunk-2TFQDEPU.js +113 -0
- package/dist/chunk-2ZCJNKFE.js +315 -0
- package/dist/chunk-3PVYQFRQ.js +472 -0
- package/dist/chunk-3QMQBO5Z.js +89 -0
- package/dist/chunk-42AKKX7O.js +385 -0
- package/dist/chunk-42SKEETB.js +113 -0
- package/dist/chunk-4BHUSMNL.js +104 -0
- package/dist/chunk-4I4LWFOO.js +431 -0
- package/dist/chunk-553IQUMO.js +478 -0
- package/dist/chunk-55KF77OK.js +1944 -0
- package/dist/chunk-6HLZK37X.js +359 -0
- package/dist/chunk-6RNIFLG4.js +305 -0
- package/dist/chunk-A6BRWIXN.js +317 -0
- package/dist/chunk-AHHDWNXC.js +200 -0
- package/dist/chunk-B2CIOKWP.js +899 -0
- package/dist/chunk-BTSFTGIL.js +7 -0
- package/dist/chunk-D33S4RQG.js +222 -0
- package/dist/chunk-DIJKHSUO.js +134 -0
- package/dist/chunk-DPL47QGS.js +3515 -0
- package/dist/chunk-EMRGN4KM.js +170 -0
- package/dist/chunk-FGOABE4Z.js +955 -0
- package/dist/chunk-FW65TP4G.js +520 -0
- package/dist/chunk-GOIZNNOA.js +221 -0
- package/dist/chunk-H5MJUAA6.js +295 -0
- package/dist/chunk-H5OHH6F4.js +223 -0
- package/dist/chunk-HC5RM4WY.js +199 -0
- package/dist/chunk-HEQFMLZ7.js +190 -0
- package/dist/chunk-HFXSNKCV.js +196 -0
- package/dist/chunk-HXSBQ2SE.js +1723 -0
- package/dist/chunk-ILESNGNM.js +163 -0
- package/dist/chunk-INF5CGAO.js +137 -0
- package/dist/chunk-IWDFZ6XR.js +337 -0
- package/dist/chunk-IXMNTNW5.js +238 -0
- package/dist/chunk-J4SRJKBW.js +428 -0
- package/dist/chunk-J5SKD6LN.js +235 -0
- package/dist/chunk-JRJJX3L5.js +424 -0
- package/dist/chunk-KXNDHMUB.js +366 -0
- package/dist/chunk-LIJQMYYX.js +59 -0
- package/dist/chunk-MAN26PZ5.js +260 -0
- package/dist/chunk-MKRQMLZA.js +474 -0
- package/dist/chunk-NBDZMMG2.js +169 -0
- package/dist/chunk-NHA6BEIH.js +101 -0
- package/dist/chunk-OA6YJIWL.js +4412 -0
- package/dist/chunk-OZC255IC.js +2830 -0
- package/dist/chunk-PUR6WU2B.js +154 -0
- package/dist/chunk-Q5JPPCZC.js +12 -0
- package/dist/chunk-QBV725YG.js +128 -0
- package/dist/chunk-QHVW4AAM.js +892 -0
- package/dist/chunk-QNJ7AQ4B.js +1000 -0
- package/dist/chunk-RGISBZBD.js +231 -0
- package/dist/chunk-RJZQWEMS.js +84 -0
- package/dist/chunk-RP2ZA47V.js +15 -0
- package/dist/chunk-RXISBFR5.js +315 -0
- package/dist/chunk-S5CCLZL3.js +99 -0
- package/dist/chunk-TVTCSBJF.js +122 -0
- package/dist/chunk-U4D5S3VQ.js +21 -0
- package/dist/chunk-U4VAATKP.js +193 -0
- package/dist/chunk-UKU2ZX4C.js +203 -0
- package/dist/chunk-UNSKG6DC.js +159 -0
- package/dist/chunk-URFKEIHM.js +1091 -0
- package/dist/chunk-UTV7YAW7.js +178 -0
- package/dist/chunk-V43EQ6VR.js +68 -0
- package/dist/chunk-VB3MJGCV.js +610 -0
- package/dist/chunk-WHIIIREL.js +223 -0
- package/dist/chunk-XMIFYKUT.js +1371 -0
- package/dist/chunk-YF2RSV34.js +54 -0
- package/dist/chunk-Z6DAKQ3U.js +1 -0
- package/dist/chunk-ZIGAZFWE.js +522 -0
- package/dist/circuit-breaker.js +3 -191
- package/dist/codebase-index/index.js +84 -13688
- package/dist/codebase-index/parser-worker-script.js +8 -3995
- package/dist/codebase-index/project-server.js +60 -8548
- package/dist/codebase-index/worker.js +20 -8416
- package/dist/design.js +2 -299
- package/dist/diff.js +4 -256
- package/dist/document.js +4 -222
- package/dist/e2e.js +5 -476
- package/dist/edit.js +14 -10200
- package/dist/exec.js +16 -3131
- package/dist/fetch.js +8 -398
- package/dist/format.js +10 -3750
- package/dist/generic-parser-VUXDIAOZ.js +13 -0
- package/dist/git.js +4 -380
- package/dist/glob.js +6 -280
- package/dist/go-parser-6CA77WZ4.js +469 -0
- package/dist/grep.js +7 -604
- package/dist/index.js +318 -35387
- package/dist/install.js +10 -4091
- package/dist/json-parser-7YMDJSBJ.js +213 -0
- package/dist/json.js +4 -665
- package/dist/kanban.js +3 -2096
- package/dist/languages/index.js +23 -4664
- package/dist/legacy-bridge-7UKZT46G.js +18 -0
- package/dist/lint.js +10 -3706
- package/dist/logs.js +4 -287
- package/dist/memory.js +6 -215
- package/dist/mode.js +3 -99
- package/dist/outdated.js +4 -5170
- package/dist/pack.js +65 -33897
- package/dist/parser-dispatch-VPOFDO2U.js +14 -0
- package/dist/patch.js +13 -9935
- package/dist/plan.js +5 -3286
- package/dist/process-registry.js +8 -651
- package/dist/ps-slash.js +12 -1439
- package/dist/py-parser-GPM5VYRG.js +16 -0
- package/dist/read.js +13 -9816
- package/dist/replace.js +15 -10014
- package/dist/rs-parser-WVQ4KYCX.js +81 -0
- package/dist/scaffold.js +4 -253
- package/dist/search.js +5 -585
- package/dist/session-kanban.js +30 -972
- package/dist/skill.js +2 -215
- package/dist/task.js +5 -3410
- package/dist/test.js +10 -3773
- package/dist/todo.js +4 -2891
- package/dist/tool-help.js +3 -123
- package/dist/tool-search.js +3 -79
- package/dist/tool-tier.js +68 -33984
- package/dist/tool-use.js +2 -93
- package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
- package/dist/tree.js +4 -288
- package/dist/ts-parser-YH4ZMWMH.js +232 -0
- package/dist/typecheck.js +10 -3753
- package/dist/win32.js +6 -60
- package/dist/write.js +14 -9882
- package/dist/yaml-parser-GFAOKV53.js +157 -0
- package/package.json +5 -5
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
import {
|
|
2
|
+
e2ePlanTool
|
|
3
|
+
} from "./chunk-JRJJX3L5.js";
|
|
4
|
+
import {
|
|
5
|
+
codebaseAstReplaceTool,
|
|
6
|
+
codebaseImpactAnalysisTool,
|
|
7
|
+
codebaseIncomingCallsTool,
|
|
8
|
+
codebaseIndexTool,
|
|
9
|
+
codebaseInvariantCheckTool,
|
|
10
|
+
codebaseOutgoingCallsTool,
|
|
11
|
+
codebaseRepoMapTool,
|
|
12
|
+
codebaseSearchTool,
|
|
13
|
+
codebaseSkeletonTool,
|
|
14
|
+
codebaseStatsTool,
|
|
15
|
+
codebaseTargetedTestTool,
|
|
16
|
+
deadCodeScanTool
|
|
17
|
+
} from "./chunk-OZC255IC.js";
|
|
18
|
+
import {
|
|
19
|
+
planTool
|
|
20
|
+
} from "./chunk-KXNDHMUB.js";
|
|
21
|
+
import {
|
|
22
|
+
taskTool
|
|
23
|
+
} from "./chunk-FW65TP4G.js";
|
|
24
|
+
import {
|
|
25
|
+
toolSearchTool
|
|
26
|
+
} from "./chunk-RJZQWEMS.js";
|
|
27
|
+
import {
|
|
28
|
+
toolUseTool
|
|
29
|
+
} from "./chunk-S5CCLZL3.js";
|
|
30
|
+
import {
|
|
31
|
+
batchToolUseTool
|
|
32
|
+
} from "./chunk-U4VAATKP.js";
|
|
33
|
+
import {
|
|
34
|
+
toolHelpTool
|
|
35
|
+
} from "./chunk-QBV725YG.js";
|
|
36
|
+
import {
|
|
37
|
+
designTool
|
|
38
|
+
} from "./chunk-6RNIFLG4.js";
|
|
39
|
+
import {
|
|
40
|
+
testTool
|
|
41
|
+
} from "./chunk-HC5RM4WY.js";
|
|
42
|
+
import {
|
|
43
|
+
installTool
|
|
44
|
+
} from "./chunk-H5OHH6F4.js";
|
|
45
|
+
import {
|
|
46
|
+
auditTool
|
|
47
|
+
} from "./chunk-HEQFMLZ7.js";
|
|
48
|
+
import {
|
|
49
|
+
outdatedTool
|
|
50
|
+
} from "./chunk-2CTB3UK5.js";
|
|
51
|
+
import {
|
|
52
|
+
logsTool
|
|
53
|
+
} from "./chunk-RGISBZBD.js";
|
|
54
|
+
import {
|
|
55
|
+
documentTool
|
|
56
|
+
} from "./chunk-EMRGN4KM.js";
|
|
57
|
+
import {
|
|
58
|
+
scaffoldTool
|
|
59
|
+
} from "./chunk-AHHDWNXC.js";
|
|
60
|
+
import {
|
|
61
|
+
gitTool
|
|
62
|
+
} from "./chunk-RXISBFR5.js";
|
|
63
|
+
import {
|
|
64
|
+
patchTool
|
|
65
|
+
} from "./chunk-2ZCJNKFE.js";
|
|
66
|
+
import {
|
|
67
|
+
jsonTool
|
|
68
|
+
} from "./chunk-VB3MJGCV.js";
|
|
69
|
+
import {
|
|
70
|
+
diffTool
|
|
71
|
+
} from "./chunk-UKU2ZX4C.js";
|
|
72
|
+
import {
|
|
73
|
+
treeTool
|
|
74
|
+
} from "./chunk-J5SKD6LN.js";
|
|
75
|
+
import {
|
|
76
|
+
lintTool
|
|
77
|
+
} from "./chunk-DIJKHSUO.js";
|
|
78
|
+
import {
|
|
79
|
+
formatTool
|
|
80
|
+
} from "./chunk-UTV7YAW7.js";
|
|
81
|
+
import {
|
|
82
|
+
typecheckTool
|
|
83
|
+
} from "./chunk-INF5CGAO.js";
|
|
84
|
+
import {
|
|
85
|
+
languageInfoTool,
|
|
86
|
+
languagePackageTool,
|
|
87
|
+
languageTool
|
|
88
|
+
} from "./chunk-DPL47QGS.js";
|
|
89
|
+
import {
|
|
90
|
+
replaceTool
|
|
91
|
+
} from "./chunk-6HLZK37X.js";
|
|
92
|
+
import {
|
|
93
|
+
globTool
|
|
94
|
+
} from "./chunk-UNSKG6DC.js";
|
|
95
|
+
import {
|
|
96
|
+
grepTool
|
|
97
|
+
} from "./chunk-553IQUMO.js";
|
|
98
|
+
import {
|
|
99
|
+
bashTool,
|
|
100
|
+
checkAndBlockKillCommand,
|
|
101
|
+
diagnoseBashism,
|
|
102
|
+
shellArgs
|
|
103
|
+
} from "./chunk-FGOABE4Z.js";
|
|
104
|
+
import {
|
|
105
|
+
detectDanger,
|
|
106
|
+
execTool
|
|
107
|
+
} from "./chunk-HXSBQ2SE.js";
|
|
108
|
+
import {
|
|
109
|
+
buildChildEnv
|
|
110
|
+
} from "./chunk-BTSFTGIL.js";
|
|
111
|
+
import {
|
|
112
|
+
createOutputSpool,
|
|
113
|
+
spoolNote
|
|
114
|
+
} from "./chunk-2TFQDEPU.js";
|
|
115
|
+
import {
|
|
116
|
+
getProcessRegistry,
|
|
117
|
+
redactCommand
|
|
118
|
+
} from "./chunk-3PVYQFRQ.js";
|
|
119
|
+
import {
|
|
120
|
+
fetchTool
|
|
121
|
+
} from "./chunk-IXMNTNW5.js";
|
|
122
|
+
import {
|
|
123
|
+
searchTool
|
|
124
|
+
} from "./chunk-4I4LWFOO.js";
|
|
125
|
+
import {
|
|
126
|
+
todoTool
|
|
127
|
+
} from "./chunk-J4SRJKBW.js";
|
|
128
|
+
import {
|
|
129
|
+
kanbanTool
|
|
130
|
+
} from "./chunk-55KF77OK.js";
|
|
131
|
+
import {
|
|
132
|
+
browserTools
|
|
133
|
+
} from "./chunk-XMIFYKUT.js";
|
|
134
|
+
import {
|
|
135
|
+
readTool
|
|
136
|
+
} from "./chunk-H5MJUAA6.js";
|
|
137
|
+
import {
|
|
138
|
+
writeTool
|
|
139
|
+
} from "./chunk-PUR6WU2B.js";
|
|
140
|
+
import {
|
|
141
|
+
editTool
|
|
142
|
+
} from "./chunk-MKRQMLZA.js";
|
|
143
|
+
import {
|
|
144
|
+
resolvePowerShell
|
|
145
|
+
} from "./chunk-V43EQ6VR.js";
|
|
146
|
+
import {
|
|
147
|
+
normalizeCommandOutput,
|
|
148
|
+
safeResolveReal
|
|
149
|
+
} from "./chunk-D33S4RQG.js";
|
|
150
|
+
|
|
151
|
+
// src/clarify.ts
|
|
152
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
153
|
+
var clarifyTool = {
|
|
154
|
+
name: "clarify",
|
|
155
|
+
category: "Meta",
|
|
156
|
+
icon: "meta",
|
|
157
|
+
permission: "auto",
|
|
158
|
+
mutating: false,
|
|
159
|
+
description: "Ask the user for proactive clarification with structured multiple-choice options ONLY when an architectural, database schema, or API decision is truly irreversible and has mutually exclusive trade-offs.",
|
|
160
|
+
usageHint: "AUTONOMOUS EXECUTION PRINCIPLE (BIAS FOR ACTION):\n\n- DO NOT overuse or interrupt the user for routine implementation choices. For 95%+ of tasks, autonomously pick the standard best practice and continue executing.\n- Use ONLY when facing an irreversible fork (e.g. data destruction, fundamentally conflicting domain rules) where guessing would be catastrophic.\n- If a solid industry standard exists (e.g. E.164 phone format, indexed foreign keys, ISO dates), adopt it autonomously, execute the next steps, and document the decision in the final reply.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
question: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "The specific architectural, schema, or contract decision to clarify."
|
|
167
|
+
},
|
|
168
|
+
context: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Brief explanation of tradeoffs or why this decision is needed."
|
|
171
|
+
},
|
|
172
|
+
options: {
|
|
173
|
+
type: "array",
|
|
174
|
+
items: { type: "string" },
|
|
175
|
+
description: "2 to 4 distinct options formatted as clear user decisions.",
|
|
176
|
+
minItems: 2,
|
|
177
|
+
maxItems: 5
|
|
178
|
+
},
|
|
179
|
+
recommendedOption: {
|
|
180
|
+
type: "string",
|
|
181
|
+
description: "The assistant-recommended default choice."
|
|
182
|
+
},
|
|
183
|
+
isMultiSelect: {
|
|
184
|
+
type: "boolean",
|
|
185
|
+
description: "Whether multiple options can be chosen simultaneously."
|
|
186
|
+
},
|
|
187
|
+
allowCustomResponse: {
|
|
188
|
+
type: "boolean",
|
|
189
|
+
description: "Whether to allow the user to type a custom response (default: true)."
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
required: ["question", "options"],
|
|
193
|
+
additionalProperties: false
|
|
194
|
+
},
|
|
195
|
+
async execute(input, ctx) {
|
|
196
|
+
try {
|
|
197
|
+
const options = input.options;
|
|
198
|
+
if (!options || options.length < 2) {
|
|
199
|
+
throw new Error("clarify requires at least 2 distinct options.");
|
|
200
|
+
}
|
|
201
|
+
const hostAsk = ctx.askUserChoices;
|
|
202
|
+
if (typeof hostAsk === "function") {
|
|
203
|
+
const res = await hostAsk(input.question, options, input.isMultiSelect);
|
|
204
|
+
return {
|
|
205
|
+
status: "answered",
|
|
206
|
+
question: input.question,
|
|
207
|
+
selectedOptions: res.selected,
|
|
208
|
+
customResponse: res.custom,
|
|
209
|
+
decisionSummary: `User selected: ${res.selected.join(", ")}${res.custom ? ` (Custom: ${res.custom})` : ""}`
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const defaultChoice = input.recommendedOption ?? options[0] ?? "Default";
|
|
213
|
+
return {
|
|
214
|
+
status: "auto_decided",
|
|
215
|
+
question: input.question,
|
|
216
|
+
selectedOptions: [defaultChoice],
|
|
217
|
+
decisionSummary: `Auto-selected recommended option: "${defaultChoice}" (Non-interactive mode)`
|
|
218
|
+
};
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return {
|
|
221
|
+
status: "skipped",
|
|
222
|
+
question: input.question ?? "",
|
|
223
|
+
selectedOptions: [],
|
|
224
|
+
decisionSummary: "",
|
|
225
|
+
error: toErrorMessage(err)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/pwsh.ts
|
|
232
|
+
import { spawn } from "node:child_process";
|
|
233
|
+
import * as os from "node:os";
|
|
234
|
+
import { StringDecoder } from "node:string_decoder";
|
|
235
|
+
import {
|
|
236
|
+
emitProcessCompleted,
|
|
237
|
+
emitProcessOutput,
|
|
238
|
+
emitProcessStarted
|
|
239
|
+
} from "@wrongstack/core/observability";
|
|
240
|
+
var MAX_OUTPUT = 32768;
|
|
241
|
+
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
242
|
+
var STREAM_FLUSH_INTERVAL_MS = 200;
|
|
243
|
+
var STREAM_FLUSH_BYTES = 4 * 1024;
|
|
244
|
+
var MAX_QUEUE_CHUNKS = 500;
|
|
245
|
+
var PWSH_TOOL_DESCRIPTION = "Execute a PowerShell command (`pwsh -Command`) in a fresh process on Windows and return its stdout/stderr. Stateless per call: pass `workdir` instead of using `cd`.";
|
|
246
|
+
var PWSH_TOOL_USAGE_HINT = "Best practices & sandbox protocol for pwsh:\n- **Stateless**: No cwd, variables, or functions persist between calls. Use `workdir` to set the directory.\n- **Paths & Environs**: Use native Windows paths (`C:\\...`) and read env vars via `$env:NAME` (and `$env:DSH_*`).\n- **Exit Codes**: Non-zero exits are reported as `[exit code: N]`. On Windows, a force-killed command settles as exit code 1 (interruption).\n- **Background**: Set `run_in_background: true` for long-running processes (returns job id; manage via job_output/job_kill).\n- **Sandboxing**: Under read-only sandbox, pwsh runs in `ConstrainedLanguage` mode (prefer cmdlets, basic types; .NET reflection and COM fail). Workspace-write runs in `FullLanguage`.\n- **Escalation**: If denied by policy (`[sandbox: file access denied]`), retry once with `sandbox_permissions` plus a one-sentence `justification`.";
|
|
247
|
+
function wrapPwshCommand(command) {
|
|
248
|
+
const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ProgressPreference = 'SilentlyContinue';$ConfirmPreference = 'None';$WhatIfPreference = $false";
|
|
249
|
+
return `${bootstrap}
|
|
250
|
+
$ErrorActionPreference = 'Stop'
|
|
251
|
+
${command}
|
|
252
|
+
if ($LASTEXITCODE -is [int]) { exit $LASTEXITCODE }`;
|
|
253
|
+
}
|
|
254
|
+
var pwshTool = {
|
|
255
|
+
name: "pwsh",
|
|
256
|
+
category: "Shell",
|
|
257
|
+
description: PWSH_TOOL_DESCRIPTION,
|
|
258
|
+
usageHint: PWSH_TOOL_USAGE_HINT,
|
|
259
|
+
selection: {
|
|
260
|
+
doNotUseWhen: "the command is an allowlisted single binary (node, git, pnpm, tsc) needing no shell expansion or pipelines.",
|
|
261
|
+
useInstead: ["exec"]
|
|
262
|
+
},
|
|
263
|
+
permission: "confirm",
|
|
264
|
+
mutating: true,
|
|
265
|
+
riskTier: "destructive",
|
|
266
|
+
icon: "terminal",
|
|
267
|
+
subjectKey: "command",
|
|
268
|
+
capabilities: ["shell.arbitrary"],
|
|
269
|
+
timeoutMs: 61e4,
|
|
270
|
+
maxOutputBytes: MAX_OUTPUT,
|
|
271
|
+
estimatedDurationMs: 3e4,
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
command: {
|
|
276
|
+
type: "string",
|
|
277
|
+
description: "The exact PowerShell command or script block to run."
|
|
278
|
+
},
|
|
279
|
+
workdir: {
|
|
280
|
+
type: "string",
|
|
281
|
+
description: "Absolute or project-relative path to the working directory for this command. Defaults to session working directory."
|
|
282
|
+
},
|
|
283
|
+
timeout_ms: {
|
|
284
|
+
type: "integer",
|
|
285
|
+
description: "Optional timeout for this specific command in milliseconds (default 300000, max 600000)."
|
|
286
|
+
},
|
|
287
|
+
run_in_background: {
|
|
288
|
+
type: "boolean",
|
|
289
|
+
description: "If true, launch the process in the background and return the job ID / PID immediately."
|
|
290
|
+
},
|
|
291
|
+
background: {
|
|
292
|
+
type: "boolean",
|
|
293
|
+
description: "Alias for run_in_background."
|
|
294
|
+
},
|
|
295
|
+
sandbox_permissions: {
|
|
296
|
+
type: "string",
|
|
297
|
+
description: "Escalation mode if retrying a sandbox-denied command (e.g., workspace-write)."
|
|
298
|
+
},
|
|
299
|
+
justification: {
|
|
300
|
+
type: "string",
|
|
301
|
+
description: "One-sentence justification when retrying a denied command with sandbox_permissions."
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
required: ["command"]
|
|
305
|
+
},
|
|
306
|
+
async execute(input, ctx, opts) {
|
|
307
|
+
let final;
|
|
308
|
+
const executeStream = pwshTool.executeStream;
|
|
309
|
+
if (!executeStream) throw new Error("pwshTool: stream execution unavailable");
|
|
310
|
+
for await (const ev of executeStream(input, ctx, opts)) {
|
|
311
|
+
if (ev.type === "final") final = ev.output;
|
|
312
|
+
}
|
|
313
|
+
if (!final) throw new Error("pwsh: stream ended without final event");
|
|
314
|
+
return final;
|
|
315
|
+
},
|
|
316
|
+
async *executeStream(input, ctx, opts) {
|
|
317
|
+
if (!input?.command) throw new Error("pwsh: command is required");
|
|
318
|
+
const isBackground = !!(input.run_in_background || input.background);
|
|
319
|
+
const registry = getProcessRegistry();
|
|
320
|
+
if (!registry.beforeCall(isBackground)) {
|
|
321
|
+
yield {
|
|
322
|
+
type: "final",
|
|
323
|
+
output: {
|
|
324
|
+
output: "",
|
|
325
|
+
exit_code: 1,
|
|
326
|
+
timed_out: false,
|
|
327
|
+
pid: null,
|
|
328
|
+
error: "pwsh: circuit breaker open \u2014 too many consecutive failures or slow calls. Use /kill to inspect or /kill reset to recover."
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const killCheck = await checkAndBlockKillCommand(input.command);
|
|
334
|
+
if (killCheck.blocked) {
|
|
335
|
+
yield {
|
|
336
|
+
type: "final",
|
|
337
|
+
output: {
|
|
338
|
+
output: "",
|
|
339
|
+
exit_code: 1,
|
|
340
|
+
timed_out: false,
|
|
341
|
+
pid: null,
|
|
342
|
+
error: `pwsh: ${killCheck.reason}`
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const isWin = os.platform() === "win32";
|
|
348
|
+
const bin = isWin ? resolvePowerShell("pwsh.exe") : "pwsh";
|
|
349
|
+
const encodedCommand = Buffer.from(wrapPwshCommand(input.command), "utf16le").toString(
|
|
350
|
+
"base64"
|
|
351
|
+
);
|
|
352
|
+
const args = [...shellArgs("pwsh"), encodedCommand];
|
|
353
|
+
const env = buildChildEnv(ctx.session?.id);
|
|
354
|
+
let targetCwd;
|
|
355
|
+
try {
|
|
356
|
+
targetCwd = await safeResolveReal(input.workdir ?? ctx.workingDir ?? ctx.projectRoot, ctx);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
yield {
|
|
359
|
+
type: "final",
|
|
360
|
+
output: {
|
|
361
|
+
output: "",
|
|
362
|
+
exit_code: 1,
|
|
363
|
+
timed_out: false,
|
|
364
|
+
pid: null,
|
|
365
|
+
error: `pwsh: ${err.message}`
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const startedAt = Date.now();
|
|
371
|
+
const detached = !isWin;
|
|
372
|
+
if (isBackground) {
|
|
373
|
+
const child2 = spawn(bin, args, {
|
|
374
|
+
cwd: targetCwd,
|
|
375
|
+
env,
|
|
376
|
+
detached,
|
|
377
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
378
|
+
windowsHide: true
|
|
379
|
+
});
|
|
380
|
+
const pid2 = child2.pid;
|
|
381
|
+
if (typeof pid2 === "number") {
|
|
382
|
+
registry.register({
|
|
383
|
+
pid: pid2,
|
|
384
|
+
name: "pwsh",
|
|
385
|
+
command: redactCommand(input.command),
|
|
386
|
+
startedAt: Date.now(),
|
|
387
|
+
sessionId: ctx.session?.id,
|
|
388
|
+
child: child2,
|
|
389
|
+
processGroupLeader: detached && child2.pid === pid2,
|
|
390
|
+
background: true
|
|
391
|
+
});
|
|
392
|
+
child2.on("close", () => registry.unregister(pid2));
|
|
393
|
+
child2.on("error", () => {
|
|
394
|
+
registry.unregister(pid2);
|
|
395
|
+
registry.afterCall(Date.now() - startedAt, true, isBackground);
|
|
396
|
+
});
|
|
397
|
+
child2.unref();
|
|
398
|
+
const jobId = `job_${pid2}`;
|
|
399
|
+
ctx.recordSideEffect?.({
|
|
400
|
+
toolUseId: `pwsh-bg-${Date.now()}`,
|
|
401
|
+
toolName: "pwsh",
|
|
402
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
403
|
+
input: { command: redactCommand(input.command), background: true },
|
|
404
|
+
outcome: `started background job ${jobId} (PID: ${pid2})`,
|
|
405
|
+
risk: "shell"
|
|
406
|
+
});
|
|
407
|
+
yield {
|
|
408
|
+
type: "final",
|
|
409
|
+
output: {
|
|
410
|
+
output: `Background job started: ${jobId} (PID: ${pid2})
|
|
411
|
+
Working directory: ${targetCwd}`,
|
|
412
|
+
exit_code: null,
|
|
413
|
+
timed_out: false,
|
|
414
|
+
pid: pid2,
|
|
415
|
+
job_id: jobId
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
yield {
|
|
421
|
+
type: "final",
|
|
422
|
+
output: {
|
|
423
|
+
output: "",
|
|
424
|
+
exit_code: 1,
|
|
425
|
+
timed_out: false,
|
|
426
|
+
pid: null,
|
|
427
|
+
error: "pwsh: failed to obtain PID for background process"
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const spool = createOutputSpool({ tool: "pwsh", thresholdBytes: MAX_OUTPUT });
|
|
433
|
+
const child = spawn(bin, args, {
|
|
434
|
+
cwd: targetCwd,
|
|
435
|
+
env,
|
|
436
|
+
detached,
|
|
437
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
438
|
+
windowsHide: true
|
|
439
|
+
});
|
|
440
|
+
const pid = child.pid;
|
|
441
|
+
let stdoutBytes = 0;
|
|
442
|
+
let stderrBytes = 0;
|
|
443
|
+
let telemetryCompleted = false;
|
|
444
|
+
emitProcessStarted({
|
|
445
|
+
...pid !== void 0 ? { pid } : {},
|
|
446
|
+
parentPid: process.pid,
|
|
447
|
+
command: redactCommand(`${bin} ${args.join(" ")}`),
|
|
448
|
+
args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
|
|
449
|
+
cwd: targetCwd,
|
|
450
|
+
background: false,
|
|
451
|
+
startedAt: new Date(startedAt).toISOString()
|
|
452
|
+
});
|
|
453
|
+
const completeForeground = (exitCode, sig) => {
|
|
454
|
+
if (telemetryCompleted) return;
|
|
455
|
+
telemetryCompleted = true;
|
|
456
|
+
emitProcessCompleted({
|
|
457
|
+
...pid !== void 0 ? { pid } : {},
|
|
458
|
+
exitCode,
|
|
459
|
+
...sig ? { signal: sig } : {},
|
|
460
|
+
durationMs: Date.now() - startedAt,
|
|
461
|
+
stdoutBytes,
|
|
462
|
+
stderrBytes,
|
|
463
|
+
timedOut,
|
|
464
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
if (typeof pid === "number") {
|
|
468
|
+
registry.register({
|
|
469
|
+
pid,
|
|
470
|
+
name: "pwsh",
|
|
471
|
+
command: redactCommand(input.command),
|
|
472
|
+
startedAt: Date.now(),
|
|
473
|
+
sessionId: ctx.session?.id,
|
|
474
|
+
child,
|
|
475
|
+
processGroupLeader: detached && child.pid === pid
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
const timeoutMs = Math.min(Math.max(1e3, input.timeout_ms ?? DEFAULT_TIMEOUT_MS), 6e5);
|
|
479
|
+
let timedOut = false;
|
|
480
|
+
const timers = [];
|
|
481
|
+
const killWithTimeout = (timeout) => {
|
|
482
|
+
if (isWin) {
|
|
483
|
+
if (typeof child.pid === "number" && child.exitCode === null) {
|
|
484
|
+
const attempted = registry.kill(child.pid, { force: true, graceMs: timeout });
|
|
485
|
+
if (!attempted) {
|
|
486
|
+
try {
|
|
487
|
+
child.kill();
|
|
488
|
+
} catch (err) {
|
|
489
|
+
console.log(
|
|
490
|
+
JSON.stringify({
|
|
491
|
+
level: "warn",
|
|
492
|
+
event: "pwsh.kill_fallback_failed",
|
|
493
|
+
pid: child.pid,
|
|
494
|
+
platform: process.platform,
|
|
495
|
+
error: err instanceof Error ? err.message : String(err),
|
|
496
|
+
message: "pwsh child.kill() raised after the registry kill failed; process may be leaked"
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (typeof child.pid === "number") {
|
|
505
|
+
registry.kill(child.pid, { graceMs: timeout });
|
|
506
|
+
} else {
|
|
507
|
+
try {
|
|
508
|
+
child.kill("SIGTERM");
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.log(
|
|
511
|
+
JSON.stringify({
|
|
512
|
+
level: "warn",
|
|
513
|
+
event: "pwsh.sigterm_failed",
|
|
514
|
+
platform: process.platform,
|
|
515
|
+
error: err instanceof Error ? err.message : String(err),
|
|
516
|
+
message: 'pwsh child.kill("SIGTERM") raised on POSIX fallback; process may be leaked'
|
|
517
|
+
})
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const timer = setTimeout(() => {
|
|
523
|
+
timedOut = true;
|
|
524
|
+
killWithTimeout(2e3);
|
|
525
|
+
}, timeoutMs);
|
|
526
|
+
timers.push(timer);
|
|
527
|
+
const onAbort = () => killWithTimeout(2e3);
|
|
528
|
+
if (opts.signal.aborted) onAbort();
|
|
529
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
530
|
+
const queue = [];
|
|
531
|
+
let resolveNext = null;
|
|
532
|
+
const push = (c) => {
|
|
533
|
+
if (resolveNext) {
|
|
534
|
+
const r = resolveNext;
|
|
535
|
+
resolveNext = null;
|
|
536
|
+
r(c);
|
|
537
|
+
} else {
|
|
538
|
+
queue.push(c);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
const next = () => new Promise((resolve2) => {
|
|
542
|
+
const c = queue.shift();
|
|
543
|
+
if (c) resolve2(c);
|
|
544
|
+
else resolveNext = resolve2;
|
|
545
|
+
});
|
|
546
|
+
let buf = "";
|
|
547
|
+
let pending = "";
|
|
548
|
+
let lastFlush = Date.now();
|
|
549
|
+
const flush = () => {
|
|
550
|
+
if (pending.length === 0) return null;
|
|
551
|
+
const text = pending;
|
|
552
|
+
pending = "";
|
|
553
|
+
lastFlush = Date.now();
|
|
554
|
+
return text;
|
|
555
|
+
};
|
|
556
|
+
let paused = false;
|
|
557
|
+
const pauseIfFlooded = () => {
|
|
558
|
+
if (!paused && queue.length >= MAX_QUEUE_CHUNKS) {
|
|
559
|
+
paused = true;
|
|
560
|
+
child.stdout?.pause();
|
|
561
|
+
child.stderr?.pause();
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
const resumeIfDrained = () => {
|
|
565
|
+
if (paused && queue.length < MAX_QUEUE_CHUNKS) {
|
|
566
|
+
paused = false;
|
|
567
|
+
child.stdout?.resume();
|
|
568
|
+
child.stderr?.resume();
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
572
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
573
|
+
const onData = (chunk, stream) => {
|
|
574
|
+
const text = (stream === "stdout" ? stdoutDecoder : stderrDecoder).write(chunk);
|
|
575
|
+
if (stream === "stdout") stdoutBytes += chunk.byteLength;
|
|
576
|
+
else stderrBytes += chunk.byteLength;
|
|
577
|
+
emitProcessOutput({ pid: pid ?? 0, stream, chunk });
|
|
578
|
+
if (buf.length < MAX_OUTPUT) {
|
|
579
|
+
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
580
|
+
}
|
|
581
|
+
spool.write(text);
|
|
582
|
+
pending += text;
|
|
583
|
+
push({ kind: "data", text });
|
|
584
|
+
pauseIfFlooded();
|
|
585
|
+
};
|
|
586
|
+
child.stdout?.on("data", (chunk) => onData(chunk, "stdout"));
|
|
587
|
+
child.stderr?.on("data", (chunk) => onData(chunk, "stderr"));
|
|
588
|
+
child.on("error", (err) => {
|
|
589
|
+
for (const t of timers) clearTimeout(t);
|
|
590
|
+
registry.afterCall(Date.now() - startedAt, true);
|
|
591
|
+
completeForeground(1);
|
|
592
|
+
push({ kind: "error", err });
|
|
593
|
+
});
|
|
594
|
+
child.on("close", (code, signal) => {
|
|
595
|
+
for (const t of timers) clearTimeout(t);
|
|
596
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
597
|
+
registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
|
|
598
|
+
completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
|
|
599
|
+
const tail = stdoutDecoder.end() + stderrDecoder.end();
|
|
600
|
+
if (tail) {
|
|
601
|
+
if (buf.length < MAX_OUTPUT) buf += tail.slice(0, MAX_OUTPUT - buf.length);
|
|
602
|
+
spool.write(tail);
|
|
603
|
+
pending += tail;
|
|
604
|
+
}
|
|
605
|
+
push({ kind: "end", code });
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
while (true) {
|
|
609
|
+
const c = await next();
|
|
610
|
+
resumeIfDrained();
|
|
611
|
+
if (c.kind === "error") throw c.err;
|
|
612
|
+
if (c.kind === "end") {
|
|
613
|
+
const remainder = flush();
|
|
614
|
+
if (remainder !== null) {
|
|
615
|
+
yield { type: "partial_output", text: remainder };
|
|
616
|
+
}
|
|
617
|
+
const spooled = spool.finalize();
|
|
618
|
+
let formattedOutput = normalizeCommandOutput(buf);
|
|
619
|
+
if (c.code !== null && c.code !== 0 && !timedOut) {
|
|
620
|
+
if (!formattedOutput.includes(`[exit code: ${c.code}]`)) {
|
|
621
|
+
formattedOutput = formattedOutput ? `${formattedOutput}
|
|
622
|
+
[exit code: ${c.code}]` : `[exit code: ${c.code}]`;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const hint = !timedOut && typeof c.code === "number" && c.code !== 0 ? diagnoseBashism(input.command, "pwsh") : void 0;
|
|
626
|
+
const danger = detectDanger("pwsh", [input.command]);
|
|
627
|
+
const cautionText = danger.level === "caution" && danger.reasons.length > 0 ? `
|
|
628
|
+
[caution: ${danger.reasons.join("; ")}]` : "";
|
|
629
|
+
const finalResultText = formattedOutput + (spooled ? spoolNote(spooled) : "") + (hint ? `
|
|
630
|
+
|
|
631
|
+
${hint}` : "") + cautionText;
|
|
632
|
+
ctx.recordSideEffect?.({
|
|
633
|
+
toolUseId: `pwsh-${Date.now()}`,
|
|
634
|
+
toolName: "pwsh",
|
|
635
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
636
|
+
input: { command: redactCommand(input.command) },
|
|
637
|
+
outcome: timedOut ? `timed out (exit ${c.code})` : `exit ${c.code}`,
|
|
638
|
+
risk: "shell"
|
|
639
|
+
});
|
|
640
|
+
yield {
|
|
641
|
+
type: "final",
|
|
642
|
+
output: {
|
|
643
|
+
output: finalResultText,
|
|
644
|
+
exit_code: timedOut ? 1 : c.code,
|
|
645
|
+
timed_out: timedOut,
|
|
646
|
+
pid
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const now = Date.now();
|
|
652
|
+
if (pending.length >= STREAM_FLUSH_BYTES || now - lastFlush >= STREAM_FLUSH_INTERVAL_MS) {
|
|
653
|
+
const text = flush();
|
|
654
|
+
if (text) yield { type: "partial_output", text };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} finally {
|
|
658
|
+
for (const t of timers) clearTimeout(t);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// src/security-ast-scan-tool.ts
|
|
664
|
+
import * as fs from "node:fs/promises";
|
|
665
|
+
import * as path from "node:path";
|
|
666
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
667
|
+
var SECRET_PATTERNS = [
|
|
668
|
+
{ name: "AWS Access Key", regex: /\b(AKIA[0-9A-Z]{16})\b/ },
|
|
669
|
+
{ name: "GitHub Token", regex: /\b(gh[pousr]_[A-Za-z0-9_]{36,255})\b/ },
|
|
670
|
+
{ name: "Stripe Secret Key", regex: /\b(sk_live_[0-9a-zA-Z]{24,})\b/ },
|
|
671
|
+
{ name: "Slack Token", regex: /\b(xox[baprs]-[0-9a-zA-Z]{10,48})\b/ },
|
|
672
|
+
{ name: "Private Key Header", regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/ }
|
|
673
|
+
];
|
|
674
|
+
var REDOS_PATTERNS = [
|
|
675
|
+
/\([^)]+[+*]\)[+*]/,
|
|
676
|
+
/\([a-zA-Z0-9_.\\-]+\+?\)\+/i,
|
|
677
|
+
/\([a-zA-Z0-9_.\\-]+\*?\)\*/i
|
|
678
|
+
];
|
|
679
|
+
var DB_QUERY_CALL_REGEX = /(?:db|prisma|sequelize|knex|typeorm|connection|pool|repo|repository|client)\.(?:query|execute|raw|find|findAll|findOne|findMany|create|update|delete|save|insert|select)\s*\(/i;
|
|
680
|
+
var SQL_INTERPOLATION_REGEX = /(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN).*\$\{[^}]+\}/i;
|
|
681
|
+
var RAW_SQL_CONCAT_REGEX = /(?:SELECT|INSERT|UPDATE|DELETE).+['"]\s*\+\s*[a-zA-Z0-9_]/i;
|
|
682
|
+
function analyzeSecurityAndPerformance(filePath, content) {
|
|
683
|
+
const findings = [];
|
|
684
|
+
const lines = content.split("\n");
|
|
685
|
+
const relPath = filePath.replace(/\\/g, "/");
|
|
686
|
+
const isTestFile = /(?:test|spec|mock|fixture)[s]?[/\\._]/i.test(relPath);
|
|
687
|
+
if (!isTestFile) {
|
|
688
|
+
for (let i = 0; i < lines.length; i++) {
|
|
689
|
+
const line = lines[i] ?? "";
|
|
690
|
+
for (const secretRule of SECRET_PATTERNS) {
|
|
691
|
+
if (secretRule.regex.test(line)) {
|
|
692
|
+
findings.push({
|
|
693
|
+
ruleId: "sec/hardcoded-secret",
|
|
694
|
+
category: "security",
|
|
695
|
+
severity: "critical",
|
|
696
|
+
file: relPath,
|
|
697
|
+
line: i + 1,
|
|
698
|
+
message: `Hardcoded ${secretRule.name} detected.`,
|
|
699
|
+
snippet: line.trim(),
|
|
700
|
+
recommendation: "Store sensitive keys and credentials in environment variables or a secret vault."
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
for (let i = 0; i < lines.length; i++) {
|
|
707
|
+
const line = lines[i] ?? "";
|
|
708
|
+
if (line.includes("__proto__") || line.includes("constructor.prototype") || /\bObject\.assign\s*\(\s*\{\}\s*,\s*req\.body\b/.test(line)) {
|
|
709
|
+
findings.push({
|
|
710
|
+
ruleId: "sec/prototype-pollution",
|
|
711
|
+
category: "security",
|
|
712
|
+
severity: "warning",
|
|
713
|
+
file: relPath,
|
|
714
|
+
line: i + 1,
|
|
715
|
+
message: "Potential prototype pollution vulnerability on object mutation.",
|
|
716
|
+
snippet: line.trim(),
|
|
717
|
+
recommendation: "Validate input keys, use `Object.create(null)` for map dictionaries, or use secure deep-merge utilities."
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
for (let i = 0; i < lines.length; i++) {
|
|
722
|
+
const line = lines[i] ?? "";
|
|
723
|
+
if (/\beval\s*\(/.test(line) || /\bnew\s+Function\s*\(/.test(line) || /(?:exec|execSync)\s*\(\s*`[^`]*\$\{/.test(line)) {
|
|
724
|
+
findings.push({
|
|
725
|
+
ruleId: "sec/unsafe-dynamic-exec",
|
|
726
|
+
category: "security",
|
|
727
|
+
severity: "critical",
|
|
728
|
+
file: relPath,
|
|
729
|
+
line: i + 1,
|
|
730
|
+
message: "Unsafe dynamic code execution with variable template interpolation.",
|
|
731
|
+
snippet: line.trim(),
|
|
732
|
+
recommendation: "Use static function dispatch or `spawnFile`/`execFile` with an explicit argument array rather than passing raw shell strings."
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
for (let i = 0; i < lines.length; i++) {
|
|
737
|
+
const line = lines[i] ?? "";
|
|
738
|
+
if (SQL_INTERPOLATION_REGEX.test(line) || RAW_SQL_CONCAT_REGEX.test(line)) {
|
|
739
|
+
findings.push({
|
|
740
|
+
ruleId: "sec/sql-injection",
|
|
741
|
+
category: "security",
|
|
742
|
+
severity: "critical",
|
|
743
|
+
file: relPath,
|
|
744
|
+
line: i + 1,
|
|
745
|
+
message: "Raw SQL query constructed via string concatenation or template literal interpolation.",
|
|
746
|
+
snippet: line.trim(),
|
|
747
|
+
recommendation: "Use parameterized queries ($1, ?), prepared statements, or ORM query builder methods."
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
for (let i = 0; i < lines.length; i++) {
|
|
752
|
+
const line = lines[i] ?? "";
|
|
753
|
+
if (line.includes("RegExp") || line.includes("/")) {
|
|
754
|
+
for (const redosPat of REDOS_PATTERNS) {
|
|
755
|
+
if (redosPat.test(line)) {
|
|
756
|
+
findings.push({
|
|
757
|
+
ruleId: "sec/redos-vulnerable-regex",
|
|
758
|
+
category: "security",
|
|
759
|
+
severity: "warning",
|
|
760
|
+
file: relPath,
|
|
761
|
+
line: i + 1,
|
|
762
|
+
message: "Regular expression contains nested quantifiers prone to catastrophic backtracking (ReDoS).",
|
|
763
|
+
snippet: line.trim(),
|
|
764
|
+
recommendation: "Refactor regular expression to avoid nested repetitions like `(a+)+` or `(x*)*`."
|
|
765
|
+
});
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
let inLoopDepth = 0;
|
|
772
|
+
for (let i = 0; i < lines.length; i++) {
|
|
773
|
+
const line = lines[i] ?? "";
|
|
774
|
+
const trimmed = line.trim();
|
|
775
|
+
if (/\b(?:for\s*\(|while\s*\(|for\s+await|\.forEach\s*\(|\.map\s*\(\s*(?:async\s*)?\()/.test(
|
|
776
|
+
trimmed
|
|
777
|
+
)) {
|
|
778
|
+
inLoopDepth++;
|
|
779
|
+
}
|
|
780
|
+
if (inLoopDepth > 0 && DB_QUERY_CALL_REGEX.test(trimmed)) {
|
|
781
|
+
findings.push({
|
|
782
|
+
ruleId: "perf/n-plus-one-query",
|
|
783
|
+
category: "performance",
|
|
784
|
+
severity: "critical",
|
|
785
|
+
file: relPath,
|
|
786
|
+
line: i + 1,
|
|
787
|
+
message: "Database query executed inside a loop (N+1 query anti-pattern).",
|
|
788
|
+
snippet: trimmed,
|
|
789
|
+
recommendation: "Batch query data using `IN (...)` or fetch relationships eagerly before the loop."
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
if (trimmed.includes("}") || trimmed.endsWith(");") && inLoopDepth > 0) {
|
|
793
|
+
inLoopDepth = Math.max(0, inLoopDepth - 1);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return findings;
|
|
797
|
+
}
|
|
798
|
+
var securityAstScanTool = {
|
|
799
|
+
name: "security-ast-scan",
|
|
800
|
+
category: "Code Quality",
|
|
801
|
+
icon: "code",
|
|
802
|
+
permission: "auto",
|
|
803
|
+
mutating: false,
|
|
804
|
+
capabilities: ["fs.read"],
|
|
805
|
+
description: "Scan source code for contract-based security vulnerabilities and performance bottlenecks (N+1 SQL queries, SQL injection, hardcoded secrets, prototype pollution, ReDoS, and unsafe eval).",
|
|
806
|
+
usageHint: 'RUN AFTER WRITING OR REFACTORING CODE TO CATCH HIDDEN DEFECTS:\n\n- Pass `file: "src/user-service.ts"` or direct `content: "..."`.\n- Catches N+1 loops, hardcoded secrets, SQL injection, and prototype pollution before committing.\n- Returns severity-ranked findings with line numbers and exact remediation recommendations.',
|
|
807
|
+
inputSchema: {
|
|
808
|
+
type: "object",
|
|
809
|
+
properties: {
|
|
810
|
+
file: {
|
|
811
|
+
type: "string",
|
|
812
|
+
description: "File path to scan (relative to project root)."
|
|
813
|
+
},
|
|
814
|
+
content: {
|
|
815
|
+
type: "string",
|
|
816
|
+
description: "Direct code string to scan without writing to disk."
|
|
817
|
+
},
|
|
818
|
+
rules: {
|
|
819
|
+
type: "array",
|
|
820
|
+
items: { type: "string" },
|
|
821
|
+
description: "Optional list of rule IDs to filter by."
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
additionalProperties: false
|
|
825
|
+
},
|
|
826
|
+
async execute(input, ctx) {
|
|
827
|
+
try {
|
|
828
|
+
const projectRoot = ctx.projectRoot;
|
|
829
|
+
let targetFile = input.file ?? "inline-code.ts";
|
|
830
|
+
let content = input.content;
|
|
831
|
+
if (!content && input.file) {
|
|
832
|
+
const absPath = path.isAbsolute(input.file) ? input.file : path.resolve(projectRoot, input.file);
|
|
833
|
+
targetFile = path.relative(projectRoot, absPath).replace(/\\/g, "/");
|
|
834
|
+
content = await fs.readFile(absPath, "utf8");
|
|
835
|
+
}
|
|
836
|
+
if (!content) {
|
|
837
|
+
return {
|
|
838
|
+
status: "clean",
|
|
839
|
+
filesScanned: 0,
|
|
840
|
+
totalFindings: 0,
|
|
841
|
+
criticalCount: 0,
|
|
842
|
+
warningCount: 0,
|
|
843
|
+
findings: [],
|
|
844
|
+
summary: "No content or file provided to scan."
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
let findings = analyzeSecurityAndPerformance(targetFile, content);
|
|
848
|
+
if (input.rules && input.rules.length > 0) {
|
|
849
|
+
const ruleSet = new Set(input.rules);
|
|
850
|
+
findings = findings.filter((f) => ruleSet.has(f.ruleId));
|
|
851
|
+
}
|
|
852
|
+
const criticalCount = findings.filter((f) => f.severity === "critical").length;
|
|
853
|
+
const warningCount = findings.filter((f) => f.severity === "warning").length;
|
|
854
|
+
const totalFindings = findings.length;
|
|
855
|
+
const status = totalFindings === 0 ? "clean" : "findings_detected";
|
|
856
|
+
const summary = totalFindings === 0 ? `Security & Performance AST Scan: CLEAN (${targetFile})` : `Security & Performance AST Scan: ${totalFindings} issue(s) detected (${criticalCount} critical, ${warningCount} warning).`;
|
|
857
|
+
return {
|
|
858
|
+
status,
|
|
859
|
+
filesScanned: 1,
|
|
860
|
+
totalFindings,
|
|
861
|
+
criticalCount,
|
|
862
|
+
warningCount,
|
|
863
|
+
findings,
|
|
864
|
+
summary
|
|
865
|
+
};
|
|
866
|
+
} catch (err) {
|
|
867
|
+
return {
|
|
868
|
+
status: "error",
|
|
869
|
+
filesScanned: 0,
|
|
870
|
+
totalFindings: 0,
|
|
871
|
+
criticalCount: 0,
|
|
872
|
+
warningCount: 0,
|
|
873
|
+
findings: [],
|
|
874
|
+
summary: "",
|
|
875
|
+
error: toErrorMessage2(err)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
// src/set-working-dir.ts
|
|
882
|
+
import * as fs2 from "node:fs/promises";
|
|
883
|
+
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
|
|
884
|
+
var setWorkingDirTool = {
|
|
885
|
+
name: "set_working_dir",
|
|
886
|
+
category: "Context",
|
|
887
|
+
description: "Change the current working directory for subsequent file operations and shell tools (`bash`/`exec` spawn in this directory unless given an explicit cwd). The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
|
|
888
|
+
usageHint: "Change the working directory so relative paths in subsequent tool calls resolve from a different directory. Pass `path` to set a new directory, or omit to query the current one. The directory must exist and be inside the project root.",
|
|
889
|
+
permission: "confirm",
|
|
890
|
+
mutating: true,
|
|
891
|
+
capabilities: ["fs.read"],
|
|
892
|
+
icon: "settings",
|
|
893
|
+
timeoutMs: 5e3,
|
|
894
|
+
inputSchema: {
|
|
895
|
+
type: "object",
|
|
896
|
+
properties: {
|
|
897
|
+
path: {
|
|
898
|
+
type: "string",
|
|
899
|
+
description: "Directory to navigate to. Can be relative (to projectRoot) or absolute. If omitted, returns the current working directory without changing it."
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
async execute(input, ctx, _opts) {
|
|
904
|
+
if (!input.path) {
|
|
905
|
+
return {
|
|
906
|
+
current: ctx.workingDir,
|
|
907
|
+
message: `Current working directory is ${ctx.workingDir}`
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
const previous = ctx.workingDir;
|
|
911
|
+
let resolved;
|
|
912
|
+
try {
|
|
913
|
+
resolved = ctx.setWorkingDir(input.path);
|
|
914
|
+
} catch (err) {
|
|
915
|
+
return {
|
|
916
|
+
current: ctx.workingDir,
|
|
917
|
+
error: toErrorMessage3(err)
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
let isDirectory = false;
|
|
921
|
+
try {
|
|
922
|
+
isDirectory = (await fs2.stat(resolved)).isDirectory();
|
|
923
|
+
} catch {
|
|
924
|
+
isDirectory = false;
|
|
925
|
+
}
|
|
926
|
+
if (!isDirectory) {
|
|
927
|
+
try {
|
|
928
|
+
ctx.setWorkingDir(previous);
|
|
929
|
+
} catch {
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
current: ctx.workingDir,
|
|
933
|
+
error: `Directory does not exist (or is not a directory): ${resolved}`
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
current: resolved,
|
|
938
|
+
previous,
|
|
939
|
+
message: `Working directory changed to ${resolved}`
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// src/builtin.ts
|
|
945
|
+
var OPTIONAL_TOOLS = [
|
|
946
|
+
...browserTools,
|
|
947
|
+
e2ePlanTool,
|
|
948
|
+
installTool,
|
|
949
|
+
auditTool,
|
|
950
|
+
outdatedTool,
|
|
951
|
+
logsTool,
|
|
952
|
+
documentTool,
|
|
953
|
+
scaffoldTool,
|
|
954
|
+
toolSearchTool,
|
|
955
|
+
toolUseTool,
|
|
956
|
+
batchToolUseTool,
|
|
957
|
+
toolHelpTool,
|
|
958
|
+
setWorkingDirTool
|
|
959
|
+
];
|
|
960
|
+
var OFF_ONLY_TOOLS = [...browserTools, e2ePlanTool];
|
|
961
|
+
var TIER1_TOOLS = [
|
|
962
|
+
readTool,
|
|
963
|
+
writeTool,
|
|
964
|
+
editTool,
|
|
965
|
+
clarifyTool,
|
|
966
|
+
codebaseAstReplaceTool,
|
|
967
|
+
codebaseInvariantCheckTool,
|
|
968
|
+
codebaseStatsTool,
|
|
969
|
+
codebaseSearchTool,
|
|
970
|
+
codebaseSkeletonTool,
|
|
971
|
+
codebaseRepoMapTool,
|
|
972
|
+
codebaseImpactAnalysisTool,
|
|
973
|
+
codebaseTargetedTestTool,
|
|
974
|
+
securityAstScanTool,
|
|
975
|
+
codebaseIncomingCallsTool,
|
|
976
|
+
codebaseOutgoingCallsTool,
|
|
977
|
+
codebaseIndexTool,
|
|
978
|
+
bashTool,
|
|
979
|
+
grepTool,
|
|
980
|
+
globTool,
|
|
981
|
+
diffTool,
|
|
982
|
+
patchTool,
|
|
983
|
+
jsonTool,
|
|
984
|
+
searchTool
|
|
985
|
+
];
|
|
986
|
+
var TIER2_TOOLS = [
|
|
987
|
+
replaceTool,
|
|
988
|
+
execTool,
|
|
989
|
+
pwshTool,
|
|
990
|
+
fetchTool,
|
|
991
|
+
gitTool,
|
|
992
|
+
treeTool,
|
|
993
|
+
lintTool,
|
|
994
|
+
formatTool,
|
|
995
|
+
typecheckTool,
|
|
996
|
+
testTool,
|
|
997
|
+
languageInfoTool,
|
|
998
|
+
languageTool,
|
|
999
|
+
languagePackageTool,
|
|
1000
|
+
todoTool,
|
|
1001
|
+
planTool,
|
|
1002
|
+
kanbanTool,
|
|
1003
|
+
taskTool,
|
|
1004
|
+
installTool,
|
|
1005
|
+
auditTool,
|
|
1006
|
+
designTool
|
|
1007
|
+
];
|
|
1008
|
+
var TIER3_TOOLS = [
|
|
1009
|
+
outdatedTool,
|
|
1010
|
+
logsTool,
|
|
1011
|
+
documentTool,
|
|
1012
|
+
scaffoldTool,
|
|
1013
|
+
deadCodeScanTool,
|
|
1014
|
+
toolSearchTool,
|
|
1015
|
+
toolUseTool,
|
|
1016
|
+
batchToolUseTool,
|
|
1017
|
+
toolHelpTool,
|
|
1018
|
+
setWorkingDirTool
|
|
1019
|
+
];
|
|
1020
|
+
var builtinTools = [
|
|
1021
|
+
...browserTools,
|
|
1022
|
+
e2ePlanTool,
|
|
1023
|
+
readTool,
|
|
1024
|
+
writeTool,
|
|
1025
|
+
editTool,
|
|
1026
|
+
clarifyTool,
|
|
1027
|
+
codebaseAstReplaceTool,
|
|
1028
|
+
codebaseInvariantCheckTool,
|
|
1029
|
+
codebaseStatsTool,
|
|
1030
|
+
codebaseSearchTool,
|
|
1031
|
+
codebaseSkeletonTool,
|
|
1032
|
+
codebaseRepoMapTool,
|
|
1033
|
+
codebaseImpactAnalysisTool,
|
|
1034
|
+
codebaseTargetedTestTool,
|
|
1035
|
+
securityAstScanTool,
|
|
1036
|
+
codebaseIncomingCallsTool,
|
|
1037
|
+
codebaseOutgoingCallsTool,
|
|
1038
|
+
codebaseIndexTool,
|
|
1039
|
+
deadCodeScanTool,
|
|
1040
|
+
replaceTool,
|
|
1041
|
+
globTool,
|
|
1042
|
+
grepTool,
|
|
1043
|
+
bashTool,
|
|
1044
|
+
execTool,
|
|
1045
|
+
pwshTool,
|
|
1046
|
+
fetchTool,
|
|
1047
|
+
searchTool,
|
|
1048
|
+
todoTool,
|
|
1049
|
+
planTool,
|
|
1050
|
+
kanbanTool,
|
|
1051
|
+
taskTool,
|
|
1052
|
+
gitTool,
|
|
1053
|
+
patchTool,
|
|
1054
|
+
jsonTool,
|
|
1055
|
+
diffTool,
|
|
1056
|
+
treeTool,
|
|
1057
|
+
lintTool,
|
|
1058
|
+
formatTool,
|
|
1059
|
+
typecheckTool,
|
|
1060
|
+
testTool,
|
|
1061
|
+
languageInfoTool,
|
|
1062
|
+
languageTool,
|
|
1063
|
+
languagePackageTool,
|
|
1064
|
+
installTool,
|
|
1065
|
+
auditTool,
|
|
1066
|
+
outdatedTool,
|
|
1067
|
+
logsTool,
|
|
1068
|
+
documentTool,
|
|
1069
|
+
scaffoldTool,
|
|
1070
|
+
designTool,
|
|
1071
|
+
toolSearchTool,
|
|
1072
|
+
toolUseTool,
|
|
1073
|
+
batchToolUseTool,
|
|
1074
|
+
toolHelpTool,
|
|
1075
|
+
setWorkingDirTool
|
|
1076
|
+
];
|
|
1077
|
+
|
|
1078
|
+
export {
|
|
1079
|
+
clarifyTool,
|
|
1080
|
+
PWSH_TOOL_DESCRIPTION,
|
|
1081
|
+
pwshTool,
|
|
1082
|
+
analyzeSecurityAndPerformance,
|
|
1083
|
+
securityAstScanTool,
|
|
1084
|
+
OPTIONAL_TOOLS,
|
|
1085
|
+
OFF_ONLY_TOOLS,
|
|
1086
|
+
TIER1_TOOLS,
|
|
1087
|
+
TIER2_TOOLS,
|
|
1088
|
+
TIER3_TOOLS,
|
|
1089
|
+
builtinTools
|
|
1090
|
+
};
|
|
1091
|
+
//# sourceMappingURL=chunk-URFKEIHM.js.map
|