@posthog/wizard 2.44.0 → 2.46.0
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/README.md +5 -1
- package/dist/{add-mcp-server-to-clients-D5qsqCkj.js → add-mcp-server-to-clients-Bv2CBSil.js} +53 -6
- package/dist/add-mcp-server-to-clients-Bv2CBSil.js.map +1 -0
- package/dist/{agent-interface-BvTxWiol.js → agent-interface-B8roB2UP.js} +1122 -157
- package/dist/agent-interface-B8roB2UP.js.map +1 -0
- package/dist/{agent-runner-BF6_f8Qh.js → agent-runner-CV3qh_2p.js} +298 -610
- package/dist/agent-runner-CV3qh_2p.js.map +1 -0
- package/dist/analytics-D7vUZgzf.js +2 -0
- package/dist/{analytics-DSlQuwOT.js → analytics-k76OWsl0.js} +20 -6
- package/dist/analytics-k76OWsl0.js.map +1 -0
- package/dist/{api-BXduJZrw.js → api-CmIWcW-U.js} +3 -3
- package/dist/{api-BXduJZrw.js.map → api-CmIWcW-U.js.map} +1 -1
- package/dist/bin.js +242 -1043
- package/dist/bin.js.map +1 -1
- package/dist/{ci-install-GirEJqu2.js → ci-install-CNEJxdHf.js} +4 -4
- package/dist/{ci-install-GirEJqu2.js.map → ci-install-CNEJxdHf.js.map} +1 -1
- package/dist/commandments-DSNrOeOC.js +42 -0
- package/dist/commandments-DSNrOeOC.js.map +1 -0
- package/dist/commandments-qTW7pwaT.js +2 -0
- package/dist/{debug-BOFxiVVs.js → debug-BwIo7nwn.js} +1 -1
- package/dist/{debug-DXfQFmcM.js → debug-C9HmIrwx.js} +36 -6
- package/dist/debug-C9HmIrwx.js.map +1 -0
- package/dist/{defaults-BWLIffh5.js → defaults-CtWMmC8s.js} +1 -1
- package/dist/{defaults-BWLIffh5.js.map → defaults-CtWMmC8s.js.map} +1 -1
- package/dist/{env-api-key-cjD4yfGT.js → env-api-key-C3LzH8Lr.js} +1 -1
- package/dist/{env-api-key-cjD4yfGT.js.map → env-api-key-C3LzH8Lr.js.map} +1 -1
- package/dist/{environment-F1yw21dG.js → environment-NJykOp33.js} +3 -3
- package/dist/{environment-F1yw21dG.js.map → environment-NJykOp33.js.map} +1 -1
- package/dist/{file-utils-D-7YfJ7A.js → file-utils-CLMcNIKi.js} +2 -2
- package/dist/{file-utils-D-7YfJ7A.js.map → file-utils-CLMcNIKi.js.map} +1 -1
- package/dist/file-watcher-CBDCNQVV.js +123 -0
- package/dist/file-watcher-CBDCNQVV.js.map +1 -0
- package/dist/{headless-ui-Dni0gPND.js → headless-ui-B3QKxbz8.js} +5 -4
- package/dist/headless-ui-B3QKxbz8.js.map +1 -0
- package/dist/{interactive-Bmi82rsO.js → interactive-CIJRex02.js} +2 -2
- package/dist/{interactive-Bmi82rsO.js.map → interactive-CIJRex02.js.map} +1 -1
- package/dist/{mcp-prompt-streaming-AtntLsJ8.js → mcp-prompt-streaming-D6OgW3lC.js} +3 -3
- package/dist/{mcp-prompt-streaming-AtntLsJ8.js.map → mcp-prompt-streaming-D6OgW3lC.js.map} +1 -1
- package/dist/{mcp-xKRDnrcq.js → mcp-qTjKHqXN.js} +3 -3
- package/dist/mcp-qTjKHqXN.js.map +1 -0
- package/dist/{non-interactive-BIHqcW8g.js → non-interactive-8Eypq_h-.js} +2 -2
- package/dist/{non-interactive-BIHqcW8g.js.map → non-interactive-8Eypq_h-.js.map} +1 -1
- package/dist/orchestrator-tools-DVXOV-kz.js +99 -0
- package/dist/orchestrator-tools-DVXOV-kz.js.map +1 -0
- package/dist/{package-json-CDs1ZVKu.js → package-json-DCuoye-H.js} +1 -1
- package/dist/{package-json-CDs1ZVKu.js.map → package-json-DCuoye-H.js.map} +1 -1
- package/dist/{package-manager-C6R_zpML.js → package-manager-CAD9Uklp.js} +2 -2
- package/dist/{package-manager-C6R_zpML.js.map → package-manager-CAD9Uklp.js.map} +1 -1
- package/dist/pi-C9iHv5uG.js +619 -0
- package/dist/pi-C9iHv5uG.js.map +1 -0
- package/dist/{playground-DwL36I2u.js → playground-C-9Q3ETa.js} +8 -9
- package/dist/{playground-DwL36I2u.js.map → playground-C-9Q3ETa.js.map} +1 -1
- package/dist/{posthog-DwKlq0HM.js → posthog-DZZbziJ1.js} +1 -1
- package/dist/{posthog-DwKlq0HM.js.map → posthog-DZZbziJ1.js.map} +1 -1
- package/dist/{posthog-integration-BhLKCFsI.js → posthog-integration-Do997QON.js} +2206 -112
- package/dist/posthog-integration-Do997QON.js.map +1 -0
- package/dist/{provisioning-jVZm2E-A.js → provisioning-DpqnGk3z.js} +3 -3
- package/dist/{provisioning-jVZm2E-A.js.map → provisioning-DpqnGk3z.js.map} +1 -1
- package/dist/queue-tools-CTUgbOU8.js +366 -0
- package/dist/queue-tools-CTUgbOU8.js.map +1 -0
- package/dist/{registry-DcoBCMQR.js → registry-DT58jEYr.js} +96 -18
- package/dist/registry-DT58jEYr.js.map +1 -0
- package/dist/{security-jxjVsOpd.js → security-CTigdAWm.js} +10 -8
- package/dist/security-CTigdAWm.js.map +1 -0
- package/dist/{setup-utils-BQCl28D8.js → setup-utils-Cb5uACEO.js} +31 -9
- package/dist/setup-utils-Cb5uACEO.js.map +1 -0
- package/dist/{start-tui-DgSKFMVh.js → start-tui-Cwc-PFma.js} +37 -92
- package/dist/start-tui-Cwc-PFma.js.map +1 -0
- package/dist/{steps-erepgD0V.js → steps-DyEKYM4I.js} +7 -7
- package/dist/{steps-erepgD0V.js.map → steps-DyEKYM4I.js.map} +1 -1
- package/dist/{store-C79GoHCQ.js → store-4sj-vWr5.js} +5 -6
- package/dist/store-4sj-vWr5.js.map +1 -0
- package/dist/{subagent-aDlD_Ltz.js → subagent-D8_yDgCB.js} +2 -2
- package/dist/{subagent-aDlD_Ltz.js.map → subagent-D8_yDgCB.js.map} +1 -1
- package/dist/task-D8Y8pkzd.js +304 -0
- package/dist/task-D8Y8pkzd.js.map +1 -0
- package/dist/{task-stream-0Pk0tPBN.js → task-stream-DQT3vjwS.js} +72 -7
- package/dist/task-stream-DQT3vjwS.js.map +1 -0
- package/dist/{tasks-ZkrluXCa.js → tasks-D2WKssA5.js} +2 -2
- package/dist/{tasks-ZkrluXCa.js.map → tasks-D2WKssA5.js.map} +1 -1
- package/dist/{telemetry-DZShaH-y.js → telemetry-CYr4HgS2.js} +3 -3
- package/dist/{telemetry-DZShaH-y.js.map → telemetry-CYr4HgS2.js.map} +1 -1
- package/dist/{terminal-CEhufbJ4.js → terminal-1RfyHQC2.js} +13 -13
- package/dist/{terminal-CEhufbJ4.js.map → terminal-1RfyHQC2.js.map} +1 -1
- package/dist/tools-DQCUOQZX.js +199 -0
- package/dist/tools-DQCUOQZX.js.map +1 -0
- package/dist/{wizard-abort-UCm4Pd5m.js → wizard-abort-D_-v1zvS.js} +1 -1
- package/dist/{wizard-abort-C14PzXZ8.js → wizard-abort-FuyIaI_0.js} +4 -4
- package/dist/{wizard-abort-C14PzXZ8.js.map → wizard-abort-FuyIaI_0.js.map} +1 -1
- package/dist/{wizard-session-C5lZPPEc.js → wizard-session-DWHe83uk.js} +1 -1
- package/dist/{wizard-session-BAZql11N.js → wizard-session-tp4Zdbwl.js} +1 -1
- package/dist/{wizard-session-BAZql11N.js.map → wizard-session-tp4Zdbwl.js.map} +1 -1
- package/dist/yara-hooks-6gNnPFPo.js +2 -0
- package/dist/{yara-hooks-CPH8N4yz.js → yara-hooks-DdYRRxfH.js} +82 -21
- package/dist/yara-hooks-DdYRRxfH.js.map +1 -0
- package/package.json +1 -1
- package/dist/add-mcp-server-to-clients-D5qsqCkj.js.map +0 -1
- package/dist/agent-interface-BvTxWiol.js.map +0 -1
- package/dist/agent-runner-BF6_f8Qh.js.map +0 -1
- package/dist/analytics-C7Xx6csw.js +0 -2
- package/dist/analytics-DSlQuwOT.js.map +0 -1
- package/dist/debug-DXfQFmcM.js.map +0 -1
- package/dist/headless-ui-Dni0gPND.js.map +0 -1
- package/dist/mcp-xKRDnrcq.js.map +0 -1
- package/dist/posthog-integration-BhLKCFsI.js.map +0 -1
- package/dist/registry-DcoBCMQR.js.map +0 -1
- package/dist/security-jxjVsOpd.js.map +0 -1
- package/dist/setup-utils-BQCl28D8.js.map +0 -1
- package/dist/start-tui-DgSKFMVh.js.map +0 -1
- package/dist/store-C79GoHCQ.js.map +0 -1
- package/dist/task-stream-0Pk0tPBN.js.map +0 -1
- package/dist/tools-BIPMan6l.js +0 -122
- package/dist/tools-BIPMan6l.js.map +0 -1
- package/dist/types-y3HkjtSq.js +0 -46
- package/dist/types-y3HkjtSq.js.map +0 -1
- package/dist/wizard-tools-B7SLjCic.js +0 -1106
- package/dist/wizard-tools-B7SLjCic.js.map +0 -1
- package/dist/wizard-ui-WZ48rUgr.js +0 -15
- package/dist/wizard-ui-WZ48rUgr.js.map +0 -1
- package/dist/yara-hooks-CPH8N4yz.js.map +0 -1
- package/dist/yara-hooks-VZlj4Lib.js +0 -2
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { n as __require } from "./rolldown-runtime-B_-DWIq7.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as analytics } from "./analytics-
|
|
4
|
-
import { n as ADDITIONAL_FEATURE_PROMPTS } from "./wizard-session-
|
|
5
|
-
import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-
|
|
6
|
-
import {
|
|
7
|
-
import { l as prewarmYaraScanner, n as createPostToolUseYaraHooks, r as createPreToolUseYaraHooks } from "./yara-hooks-
|
|
2
|
+
import { Ct as runtimeEnv, J as POSTHOG_FLAG_HEADER_PREFIX, Z as POSTHOG_PROPERTY_HEADER_PREFIX, a as getLogFilePath, f as getUI, lt as WIZARD_ORCHESTRATOR_FLAG_KEY, o as initLogFile, pt as WIZARD_REMARK_EVENT_NAME, r as debug, s as logToFile, yt as wizardUserAgentForProgram } from "./debug-C9HmIrwx.js";
|
|
3
|
+
import { n as analytics } from "./analytics-k76OWsl0.js";
|
|
4
|
+
import { n as ADDITIONAL_FEATURE_PROMPTS } from "./wizard-session-tp4Zdbwl.js";
|
|
5
|
+
import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-FuyIaI_0.js";
|
|
6
|
+
import { c as makeMutex, i as buildOrchestratorTools, l as writeJsonAtomic } from "./queue-tools-CTUgbOU8.js";
|
|
7
|
+
import { _ as AUDIT_CHECKS_FILE, l as prewarmYaraScanner, n as createPostToolUseYaraHooks, r as createPreToolUseYaraHooks, x as coerceAuditChecks } from "./yara-hooks-DdYRRxfH.js";
|
|
8
|
+
import { t as getWizardCommandments } from "./commandments-DSNrOeOC.js";
|
|
8
9
|
import { createRequire } from "node:module";
|
|
9
10
|
import * as fs$1 from "fs";
|
|
11
|
+
import fs from "fs";
|
|
10
12
|
import path from "path";
|
|
11
13
|
import { spawnSync } from "node:child_process";
|
|
12
14
|
import axios from "axios";
|
|
15
|
+
import { z } from "zod";
|
|
13
16
|
import * as os from "os";
|
|
17
|
+
import { unzipSync } from "fflate";
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
14
19
|
//#region src/utils/custom-headers.ts
|
|
15
20
|
/**
|
|
16
21
|
* Builds a list of custom headers for ANTHROPIC_CUSTOM_HEADERS.
|
|
@@ -160,6 +165,7 @@ const LINTING_TOOLS = [
|
|
|
160
165
|
"sql-formatter",
|
|
161
166
|
"sqlfmt",
|
|
162
167
|
"squawk",
|
|
168
|
+
"svelte-check",
|
|
163
169
|
"svgo",
|
|
164
170
|
"stringslint",
|
|
165
171
|
"swiftformat",
|
|
@@ -175,6 +181,1103 @@ const LINTING_TOOLS = [
|
|
|
175
181
|
"yamllint"
|
|
176
182
|
];
|
|
177
183
|
//#endregion
|
|
184
|
+
//#region src/lib/agent/bash-fence.ts
|
|
185
|
+
/**
|
|
186
|
+
* Bash command fence: exact per-manager allowlists + shell-shape gates.
|
|
187
|
+
*
|
|
188
|
+
* In effect this gates the pi harness only — on the Anthropic arm `Bash` is
|
|
189
|
+
* pre-allowed (BASE_ALLOWED_TOOLS), so canUseTool never sees it and the SDK's
|
|
190
|
+
* OS sandbox + YARA hooks gate execution instead. pi has no OS sandbox yet,
|
|
191
|
+
* so this string fence is its only execution gate.
|
|
192
|
+
*
|
|
193
|
+
* Threat model: the agent can already Write project files, so commands that
|
|
194
|
+
* execute project-defined code (installs, builds) are equivalent risk by
|
|
195
|
+
* construction. The fence must prevent: direct arbitrary shell, outward
|
|
196
|
+
* registry actions (publish/push/deploy), arbitrary-package execution
|
|
197
|
+
* (`npx <anything>` downloads and runs it), and shell injection. Matching is
|
|
198
|
+
* token-exact per manager — keyword prefixes admitted `npm publish` via `pub`.
|
|
199
|
+
*/
|
|
200
|
+
const NODE_MANAGERS = new Set([
|
|
201
|
+
"npm",
|
|
202
|
+
"pnpm",
|
|
203
|
+
"yarn",
|
|
204
|
+
"bun"
|
|
205
|
+
]);
|
|
206
|
+
const GRADLE_MANAGERS = new Set([
|
|
207
|
+
"gradle",
|
|
208
|
+
"gradlew",
|
|
209
|
+
"./gradlew"
|
|
210
|
+
]);
|
|
211
|
+
const MAVEN_MANAGERS = new Set([
|
|
212
|
+
"mvn",
|
|
213
|
+
"mvnw",
|
|
214
|
+
"./mvnw"
|
|
215
|
+
]);
|
|
216
|
+
const NODE_SUBCOMMANDS = [
|
|
217
|
+
"install",
|
|
218
|
+
"i",
|
|
219
|
+
"ci",
|
|
220
|
+
"add",
|
|
221
|
+
"remove",
|
|
222
|
+
"uninstall",
|
|
223
|
+
"update",
|
|
224
|
+
"view"
|
|
225
|
+
];
|
|
226
|
+
const NODE_SCRIPT_NAMES = [
|
|
227
|
+
"build",
|
|
228
|
+
"lint",
|
|
229
|
+
"format",
|
|
230
|
+
"tsc",
|
|
231
|
+
"typecheck",
|
|
232
|
+
"type-check",
|
|
233
|
+
"check-types",
|
|
234
|
+
"check",
|
|
235
|
+
"types"
|
|
236
|
+
];
|
|
237
|
+
const NPX_TOOLS = new Set([
|
|
238
|
+
...LINTING_TOOLS,
|
|
239
|
+
"tsc",
|
|
240
|
+
"expo",
|
|
241
|
+
"pod-install",
|
|
242
|
+
"cap"
|
|
243
|
+
]);
|
|
244
|
+
const PIP_SUBCOMMANDS = [
|
|
245
|
+
"install",
|
|
246
|
+
"uninstall",
|
|
247
|
+
"show",
|
|
248
|
+
"list",
|
|
249
|
+
"index"
|
|
250
|
+
];
|
|
251
|
+
const BUNDLE_SUBCOMMANDS = [
|
|
252
|
+
"install",
|
|
253
|
+
"add",
|
|
254
|
+
"remove",
|
|
255
|
+
"update",
|
|
256
|
+
"show"
|
|
257
|
+
];
|
|
258
|
+
const MAVEN_GOALS = [
|
|
259
|
+
"install",
|
|
260
|
+
"compile",
|
|
261
|
+
"package",
|
|
262
|
+
"verify",
|
|
263
|
+
"dependency:tree"
|
|
264
|
+
];
|
|
265
|
+
/** Managers whose first subcommand is checked exactly against a fixed set. */
|
|
266
|
+
const SIMPLE_MANAGERS = {
|
|
267
|
+
pip: PIP_SUBCOMMANDS,
|
|
268
|
+
pip3: PIP_SUBCOMMANDS,
|
|
269
|
+
poetry: [
|
|
270
|
+
"install",
|
|
271
|
+
"add",
|
|
272
|
+
"remove",
|
|
273
|
+
"show",
|
|
274
|
+
"lock",
|
|
275
|
+
"update"
|
|
276
|
+
],
|
|
277
|
+
pipenv: ["install", "uninstall"],
|
|
278
|
+
uv: [
|
|
279
|
+
"add",
|
|
280
|
+
"remove",
|
|
281
|
+
"sync"
|
|
282
|
+
],
|
|
283
|
+
pdm: [
|
|
284
|
+
"install",
|
|
285
|
+
"add",
|
|
286
|
+
"remove"
|
|
287
|
+
],
|
|
288
|
+
conda: ["install", "remove"],
|
|
289
|
+
composer: [
|
|
290
|
+
"install",
|
|
291
|
+
"require",
|
|
292
|
+
"update",
|
|
293
|
+
"remove",
|
|
294
|
+
"show"
|
|
295
|
+
],
|
|
296
|
+
bundle: BUNDLE_SUBCOMMANDS,
|
|
297
|
+
bundler: BUNDLE_SUBCOMMANDS,
|
|
298
|
+
gem: [
|
|
299
|
+
"install",
|
|
300
|
+
"uninstall",
|
|
301
|
+
"list",
|
|
302
|
+
"search"
|
|
303
|
+
],
|
|
304
|
+
swift: ["package", "build"],
|
|
305
|
+
pod: [
|
|
306
|
+
"install",
|
|
307
|
+
"update",
|
|
308
|
+
"search"
|
|
309
|
+
],
|
|
310
|
+
carthage: ["bootstrap", "update"]
|
|
311
|
+
};
|
|
312
|
+
const GRADLE_EXACT_TASKS = new Set([
|
|
313
|
+
"build",
|
|
314
|
+
"clean",
|
|
315
|
+
"dependencies"
|
|
316
|
+
]);
|
|
317
|
+
const GRADLE_TASK_VERBS = [
|
|
318
|
+
"assemble",
|
|
319
|
+
"compile",
|
|
320
|
+
"bundle",
|
|
321
|
+
"lint"
|
|
322
|
+
];
|
|
323
|
+
const XCODEBUILD_DENIED_ACTIONS = new Set(["test", "test-without-building"]);
|
|
324
|
+
const DANGEROUS_OPERATORS = /[;`$()]/;
|
|
325
|
+
const ALLOWED_TOOLS_SUMMARY = "Allowed: npm/pnpm/yarn/bun (install|i|ci|add|remove|uninstall|update|view, run <build/lint/typecheck script>), npx <lint tool|tsc|expo|pod-install|cap>, pip/pip3/poetry/pipenv/uv/pdm/conda (install/add/remove/...), composer (install|require|update|remove|show), bundle (install|add|remove|update|show|exec <lint tool>), gem (install|uninstall|list|search), swift (package|build), pod (install|update|search), carthage (bootstrap|update), xcodebuild (build/clean/archive actions), gradle/gradlew (build|clean|dependencies|assemble*/compile*/bundle*/lint* tasks), mvn (install|compile|package|verify|dependency:tree).";
|
|
326
|
+
function deny(analyticsReason, message) {
|
|
327
|
+
return {
|
|
328
|
+
allowed: false,
|
|
329
|
+
message,
|
|
330
|
+
analyticsReason
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function denyCommand(command, feedback) {
|
|
334
|
+
return deny("not in allowlist", `Bash command not allowed: \`${command.length > 120 ? command.slice(0, 117) + "..." : command}\`. ${feedback}`);
|
|
335
|
+
}
|
|
336
|
+
function isNodeScriptName(token) {
|
|
337
|
+
return NODE_SCRIPT_NAMES.some((name) => token === name || token.startsWith(name + ":"));
|
|
338
|
+
}
|
|
339
|
+
function isLintingTool(token) {
|
|
340
|
+
return LINTING_TOOLS.includes(token) || token === "tsc";
|
|
341
|
+
}
|
|
342
|
+
function nodeDecision(parts, command) {
|
|
343
|
+
const bin = parts[0];
|
|
344
|
+
let i = 1;
|
|
345
|
+
if (bin === "yarn" && parts[i] === "workspace" && parts[i + 1]) i += 2;
|
|
346
|
+
for (;;) {
|
|
347
|
+
const t = parts[i];
|
|
348
|
+
if (t === "-r" || t === "--recursive") i++;
|
|
349
|
+
else if (t && (t.startsWith("--filter=") || t.startsWith("--workspace="))) i++;
|
|
350
|
+
else if ((t === "--filter" || t === "--workspace" || t === "-w" && bin === "npm") && parts[i + 1]) i += 2;
|
|
351
|
+
else if (t === "-w" && bin === "pnpm") i++;
|
|
352
|
+
else break;
|
|
353
|
+
}
|
|
354
|
+
const sub = parts[i];
|
|
355
|
+
const feedback = `Allowed ${bin} usage: ${bin} ${NODE_SUBCOMMANDS.join("|")} [args], ${bin} run <script named ${NODE_SCRIPT_NAMES.join("/")}>, ${bin} exec <lint tool>, or ${bin} <that script name or lint tool> directly.`;
|
|
356
|
+
if (!sub) return denyCommand(command, feedback);
|
|
357
|
+
if (sub === "run") {
|
|
358
|
+
const target = parts[i + 1];
|
|
359
|
+
if (target && isNodeScriptName(target)) return { allowed: true };
|
|
360
|
+
return denyCommand(command, feedback);
|
|
361
|
+
}
|
|
362
|
+
if (sub === "exec") {
|
|
363
|
+
const target = parts[i + 1];
|
|
364
|
+
if (target && isLintingTool(target)) return { allowed: true };
|
|
365
|
+
return denyCommand(command, feedback);
|
|
366
|
+
}
|
|
367
|
+
if (NODE_SUBCOMMANDS.includes(sub)) return { allowed: true };
|
|
368
|
+
if (isNodeScriptName(sub) || isLintingTool(sub)) return { allowed: true };
|
|
369
|
+
return denyCommand(command, feedback);
|
|
370
|
+
}
|
|
371
|
+
function npxDecision(parts, command) {
|
|
372
|
+
const target = parts[1];
|
|
373
|
+
if (!target || target.startsWith("-")) return denyCommand(command, "npx may only run a known tool directly: tsc, expo, pod-install, cap, or a lint/format tool (eslint, prettier, biome, ...).");
|
|
374
|
+
const name = target.replace(/@[^@/]*$/, "");
|
|
375
|
+
if (NPX_TOOLS.has(name)) return { allowed: true };
|
|
376
|
+
return denyCommand(command, "npx may only run a known tool directly: tsc, expo, pod-install, cap, or a lint/format tool (eslint, prettier, biome, ...).");
|
|
377
|
+
}
|
|
378
|
+
function gradleDecision(parts, command) {
|
|
379
|
+
let sawTask = false;
|
|
380
|
+
for (const raw of parts.slice(1)) {
|
|
381
|
+
if (raw.startsWith("-")) continue;
|
|
382
|
+
const task = raw.replace(/^(?::[\w.-]+)+:/, "");
|
|
383
|
+
if (!(GRADLE_EXACT_TASKS.has(task) || GRADLE_TASK_VERBS.some((verb) => task.startsWith(verb) && /^[A-Z]/.test(task.slice(verb.length))))) return denyCommand(command, "Allowed gradle tasks: build, clean, dependencies, or assemble*/compile*/bundle*/lint* variants (flags OK).");
|
|
384
|
+
sawTask = true;
|
|
385
|
+
}
|
|
386
|
+
return sawTask ? { allowed: true } : denyCommand(command, "Give gradle an allowed task: build, clean, dependencies, assembleDebug, ...");
|
|
387
|
+
}
|
|
388
|
+
function mavenDecision(parts, command) {
|
|
389
|
+
let sawGoal = false;
|
|
390
|
+
for (const raw of parts.slice(1)) {
|
|
391
|
+
if (raw.startsWith("-")) continue;
|
|
392
|
+
if (!MAVEN_GOALS.includes(raw)) return denyCommand(command, `Allowed mvn goals: ${MAVEN_GOALS.join(", ")} (flags OK).`);
|
|
393
|
+
sawGoal = true;
|
|
394
|
+
}
|
|
395
|
+
return sawGoal ? { allowed: true } : denyCommand(command, `Give mvn an allowed goal: ${MAVEN_GOALS.join(", ")}.`);
|
|
396
|
+
}
|
|
397
|
+
function xcodebuildDecision(parts, command) {
|
|
398
|
+
for (const t of parts.slice(1)) if (XCODEBUILD_DENIED_ACTIONS.has(t)) return denyCommand(command, "xcodebuild test actions are not allowed; build, clean, and archive are.");
|
|
399
|
+
return { allowed: true };
|
|
400
|
+
}
|
|
401
|
+
/** Grammar decision for a single operator-free, pipe-free command. */
|
|
402
|
+
function commandDecision(command) {
|
|
403
|
+
const parts = command.split(/\s+/).filter(Boolean);
|
|
404
|
+
const bin = parts[0];
|
|
405
|
+
if (!bin) return denyCommand(command, ALLOWED_TOOLS_SUMMARY);
|
|
406
|
+
if (NODE_MANAGERS.has(bin)) return nodeDecision(parts, command);
|
|
407
|
+
if (bin === "npx") return npxDecision(parts, command);
|
|
408
|
+
if (GRADLE_MANAGERS.has(bin)) return gradleDecision(parts, command);
|
|
409
|
+
if (MAVEN_MANAGERS.has(bin)) return mavenDecision(parts, command);
|
|
410
|
+
if (bin === "xcodebuild") return xcodebuildDecision(parts, command);
|
|
411
|
+
if ((bin === "python" || bin === "python3") && parts[1] === "manage.py" && parts[2] === "check") return { allowed: true };
|
|
412
|
+
if (bin === "uv" && parts[1] === "pip") {
|
|
413
|
+
if (parts[2] && PIP_SUBCOMMANDS.includes(parts[2])) return { allowed: true };
|
|
414
|
+
return denyCommand(command, `Allowed uv pip subcommands: ${PIP_SUBCOMMANDS.join(", ")}.`);
|
|
415
|
+
}
|
|
416
|
+
if ((bin === "bundle" || bin === "bundler") && parts[1] === "exec") {
|
|
417
|
+
if (parts[2] && isLintingTool(parts[2])) return { allowed: true };
|
|
418
|
+
return denyCommand(command, `bundle exec may only run a lint/format tool (rubocop, ...). Allowed bundle subcommands: ${BUNDLE_SUBCOMMANDS.join(", ")}, exec <lint tool>.`);
|
|
419
|
+
}
|
|
420
|
+
const subs = SIMPLE_MANAGERS[bin];
|
|
421
|
+
if (subs) {
|
|
422
|
+
if (parts[1] && subs.includes(parts[1])) return { allowed: true };
|
|
423
|
+
const exec = bin === "bundle" || bin === "bundler" ? ", exec <lint tool>" : "";
|
|
424
|
+
return denyCommand(command, `Allowed ${bin} subcommands: ${subs.join(", ")}${exec}.`);
|
|
425
|
+
}
|
|
426
|
+
return denyCommand(command, `\`${bin}\` is not an allowed tool. ${ALLOWED_TOOLS_SUMMARY}`);
|
|
427
|
+
}
|
|
428
|
+
function tailArgsAreSafe(argStr) {
|
|
429
|
+
const args = argStr.trim().split(/\s+/).filter(Boolean);
|
|
430
|
+
for (let i = 0; i < args.length; i++) {
|
|
431
|
+
const a = args[i];
|
|
432
|
+
if (/^-[nc]$/.test(a)) {
|
|
433
|
+
if (!/^[+-]?\d+$/.test(args[i + 1] ?? "")) return false;
|
|
434
|
+
i++;
|
|
435
|
+
} else if (!/^-[nc]?\+?\d+$/.test(a)) return false;
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Full fence decision for a Bash command: shell-shape gates (separators,
|
|
441
|
+
* redirects, pipes) first, then the per-manager grammar.
|
|
442
|
+
*/
|
|
443
|
+
function evaluateBashCommand(rawCommand) {
|
|
444
|
+
const command = rawCommand.trim();
|
|
445
|
+
if (/[\r\n]/.test(command)) return deny("dangerous operators", "Bash command not allowed. Multi-line commands are not permitted — run one command at a time.");
|
|
446
|
+
if (DANGEROUS_OPERATORS.test(command)) return deny("dangerous operators", "Bash command not allowed. Shell operators like ; ` $ ( ) are not permitted.");
|
|
447
|
+
const normalized = command.replace(/\s*\d*>&\d+\s*/g, " ").replace(/\s*\d*>\s*\/dev\/null\s*/g, " ").trim();
|
|
448
|
+
if (/[<>]/.test(normalized)) return deny("disallowed redirect", "Bash command not allowed. Redirects are not permitted (only 2>&1 and >/dev/null).");
|
|
449
|
+
const pipeMatch = normalized.match(/^(.+?)\s*\|\s*(?:tail|head)((?:\s+\S+)*)\s*$/);
|
|
450
|
+
if (pipeMatch) {
|
|
451
|
+
const base = pipeMatch[1].trim();
|
|
452
|
+
if (/[|&]/.test(base)) return deny("multiple pipes", "Bash command not allowed. Only a single pipe to tail/head is permitted.");
|
|
453
|
+
if (!tailArgsAreSafe(pipeMatch[2])) return deny("disallowed pipe", "Bash command not allowed. tail/head may only take numeric flags (-n 50, -c 200) — no file arguments.");
|
|
454
|
+
return commandDecision(base);
|
|
455
|
+
}
|
|
456
|
+
if (/[|&]/.test(normalized)) return deny("disallowed pipe", "Bash command not allowed. Pipes are only permitted as a single | tail/head for output limiting.");
|
|
457
|
+
return commandDecision(normalized);
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/lib/wizard-ask-bridge.ts
|
|
461
|
+
/**
|
|
462
|
+
* WizardAskBridge — host-side promise broker for the `wizard_ask` MCP tool.
|
|
463
|
+
*
|
|
464
|
+
* The `wizard_ask` tool needs to (a) read information from the wizard
|
|
465
|
+
* session (the active skill id, used as the analytics `source`) and
|
|
466
|
+
* (b) drive the TUI overlay. Wiring `wizard-tools.ts` directly to either
|
|
467
|
+
* would couple our pure-data MCP server to the runtime UI layer.
|
|
468
|
+
*
|
|
469
|
+
* The bridge is the seam: `wizard-tools.ts` depends on this interface,
|
|
470
|
+
* and `agent-runner.ts` constructs an implementation that knows about
|
|
471
|
+
* both the session and `getUI()`.
|
|
472
|
+
*/
|
|
473
|
+
/** Sentinel returned for unanswered fields on cancellation or timeout. */
|
|
474
|
+
const CANCELLED_SENTINEL = "__cancelled__";
|
|
475
|
+
function buildCancelledAnswers(questions) {
|
|
476
|
+
const out = {};
|
|
477
|
+
for (const q of questions) out[q.id] = CANCELLED_SENTINEL;
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
function isFullyCancelled(answers) {
|
|
481
|
+
const values = Object.values(answers);
|
|
482
|
+
if (values.length === 0) return false;
|
|
483
|
+
return values.every((v) => v === CANCELLED_SENTINEL);
|
|
484
|
+
}
|
|
485
|
+
function createWizardAskBridge(opts) {
|
|
486
|
+
const timeoutMs = opts.timeoutMs ?? 3e5;
|
|
487
|
+
return { async request({ questions }) {
|
|
488
|
+
const pending = {
|
|
489
|
+
id: randomUUID(),
|
|
490
|
+
questions,
|
|
491
|
+
source: opts.getSource(),
|
|
492
|
+
richLinks: opts.richLinks ?? false
|
|
493
|
+
};
|
|
494
|
+
const startedAt = Date.now();
|
|
495
|
+
let timer;
|
|
496
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
497
|
+
timer = setTimeout(() => {
|
|
498
|
+
opts.cancelQuestion?.();
|
|
499
|
+
resolve(buildCancelledAnswers(questions));
|
|
500
|
+
}, timeoutMs);
|
|
501
|
+
});
|
|
502
|
+
try {
|
|
503
|
+
const answers = await Promise.race([opts.showQuestion(pending), timeoutPromise]);
|
|
504
|
+
const durationMs = Date.now() - startedAt;
|
|
505
|
+
if (isFullyCancelled(answers)) analytics.wizardCapture("wizard_ask cancelled", {
|
|
506
|
+
source: pending.source,
|
|
507
|
+
question_count: questions.length,
|
|
508
|
+
duration_ms: durationMs,
|
|
509
|
+
timed_out: durationMs >= timeoutMs
|
|
510
|
+
});
|
|
511
|
+
else analytics.wizardCapture("wizard_ask answered", {
|
|
512
|
+
source: pending.source,
|
|
513
|
+
question_count: questions.length,
|
|
514
|
+
duration_ms: durationMs
|
|
515
|
+
});
|
|
516
|
+
return answers;
|
|
517
|
+
} finally {
|
|
518
|
+
if (timer) clearTimeout(timer);
|
|
519
|
+
}
|
|
520
|
+
} };
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/lib/fetch-retry.ts
|
|
524
|
+
/**
|
|
525
|
+
* Retry wrapper for fetching from the skills/agents server (GitHub releases in
|
|
526
|
+
* production). GitHub releases blips transiently, so every fetch on the run's
|
|
527
|
+
* critical path — skill menu, skill zips, agent menu, agent prompt bodies —
|
|
528
|
+
* goes through here rather than a bare `fetch`.
|
|
529
|
+
*/
|
|
530
|
+
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
531
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
532
|
+
const DEFAULT_BACKOFF_MS = 500;
|
|
533
|
+
function sleep(ms) {
|
|
534
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
535
|
+
}
|
|
536
|
+
/** Fetch a URL, retrying transient failures (network error or non-ok HTTP) with backoff. */
|
|
537
|
+
async function fetchWithRetry(url, opts = {}) {
|
|
538
|
+
const { fetchImpl = fetch, sleepImpl = sleep, timeoutMs = DEFAULT_TIMEOUT_MS, maxAttempts = DEFAULT_MAX_ATTEMPTS, backoffMs = DEFAULT_BACKOFF_MS } = opts;
|
|
539
|
+
const failures = [];
|
|
540
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
541
|
+
const resp = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
542
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
|
|
543
|
+
return resp;
|
|
544
|
+
} catch (err) {
|
|
545
|
+
failures.push(`attempt ${attempt}: ${err.message}`);
|
|
546
|
+
if (attempt < maxAttempts) await sleepImpl(backoffMs * 2 ** (attempt - 1));
|
|
547
|
+
}
|
|
548
|
+
throw new Error(`fetch ${url} failed — ${failures.join("; ")}`);
|
|
549
|
+
}
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/lib/wizard-tools/tools.ts
|
|
552
|
+
/**
|
|
553
|
+
* Shared wizard-tool behavior — skill discovery/install, env-file helpers,
|
|
554
|
+
* the wizard_ask cap policy, secret-vault plumbing, and the audit ledger.
|
|
555
|
+
* One implementation consumed by both protocol facades: the MCP server
|
|
556
|
+
* (`./mcp`) and the pi-native tools (`harness/pi/tools.ts`) — tool behavior
|
|
557
|
+
* cannot drift between harnesses.
|
|
558
|
+
*/
|
|
559
|
+
/** Expand a bundle entry into one entry per variant, so the menu reads the same whether a group ships bundled or as zips. */
|
|
560
|
+
function expandBundleEntry(entry) {
|
|
561
|
+
if (!entry.bundle || !entry.variants) return [entry];
|
|
562
|
+
return entry.variants.map((variant) => ({
|
|
563
|
+
...variant,
|
|
564
|
+
name: entry.name,
|
|
565
|
+
group: entry.group,
|
|
566
|
+
bundle: true,
|
|
567
|
+
downloadUrl: entry.downloadUrl
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Fetch the skill menu from the skills server.
|
|
572
|
+
* Returns parsed data on success, `null` on failure.
|
|
573
|
+
*/
|
|
574
|
+
async function fetchSkillMenu(skillsBaseUrl, opts = {}) {
|
|
575
|
+
const menuUrl = `${skillsBaseUrl}/skill-menu.json`;
|
|
576
|
+
try {
|
|
577
|
+
logToFile(`fetchSkillMenu: fetching from ${menuUrl}`);
|
|
578
|
+
const data = await (await fetchWithRetry(menuUrl, opts)).json();
|
|
579
|
+
for (const [category, entries] of Object.entries(data.categories)) data.categories[category] = entries.flatMap(expandBundleEntry);
|
|
580
|
+
logToFile(`fetchSkillMenu: loaded (${Object.keys(data.categories).length} categories)`);
|
|
581
|
+
return data;
|
|
582
|
+
} catch (err) {
|
|
583
|
+
logToFile(`fetchSkillMenu: error: ${err.message}`);
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/** Extract a zip buffer, refusing entries that escape destDir (zip-slip). */
|
|
588
|
+
function extractZipArchive(zip, destDir) {
|
|
589
|
+
const root = path.resolve(destDir);
|
|
590
|
+
let written = 0;
|
|
591
|
+
for (const [entryPath, data] of Object.entries(unzipSync(zip))) {
|
|
592
|
+
const target = path.resolve(root, entryPath);
|
|
593
|
+
if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`zip entry escapes destination: ${entryPath}`);
|
|
594
|
+
if (entryPath.endsWith("/")) {
|
|
595
|
+
fs.mkdirSync(target, { recursive: true });
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
599
|
+
fs.writeFileSync(target, data);
|
|
600
|
+
written++;
|
|
601
|
+
}
|
|
602
|
+
return written;
|
|
603
|
+
}
|
|
604
|
+
/** Unpack the one variant this entry names out of a bundle; the rest is noise and never hits disk. */
|
|
605
|
+
function extractBundle(bundle, destDir, entryId) {
|
|
606
|
+
if (typeof bundle?.id !== "string" || typeof bundle?.variants !== "object" || bundle.variants === null) throw new Error("malformed bundle: expected { id, variants }");
|
|
607
|
+
const files = bundle.variants[entryId.slice(bundle.id.length + 1)];
|
|
608
|
+
if (!files) throw new Error(`bundle ${bundle.id} has no variant "${entryId}"`);
|
|
609
|
+
const root = path.resolve(destDir);
|
|
610
|
+
let written = 0;
|
|
611
|
+
for (const [entryPath, contents] of Object.entries(files)) {
|
|
612
|
+
const target = path.resolve(root, entryPath);
|
|
613
|
+
if (target !== root && !target.startsWith(root + path.sep)) throw new Error(`bundle entry escapes destination: ${entryPath}`);
|
|
614
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
615
|
+
fs.writeFileSync(target, contents);
|
|
616
|
+
written++;
|
|
617
|
+
}
|
|
618
|
+
return written;
|
|
619
|
+
}
|
|
620
|
+
/** Download a URL to a buffer, retrying transient failures with backoff. */
|
|
621
|
+
async function downloadWithRetry(url, opts = {}) {
|
|
622
|
+
const resp = await fetchWithRetry(url, opts);
|
|
623
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Download and extract a skill.
|
|
627
|
+
* By default installs to `<installDir>/.claude/skills/<id>/`.
|
|
628
|
+
* Pass `skillsRoot` to override the base directory (e.g. `.posthog/skills`).
|
|
629
|
+
*/
|
|
630
|
+
async function downloadSkill(skillEntry, installDir, skillsRoot) {
|
|
631
|
+
const skillDir = skillsRoot ? path.join(installDir, skillsRoot, skillEntry.id) : path.join(installDir, ".claude", "skills", skillEntry.id);
|
|
632
|
+
let step = "download";
|
|
633
|
+
try {
|
|
634
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
635
|
+
const data = await downloadWithRetry(skillEntry.downloadUrl);
|
|
636
|
+
step = "extract";
|
|
637
|
+
const fileCount = skillEntry.bundle ? extractBundle(JSON.parse(Buffer.from(data).toString("utf8")), skillDir, skillEntry.id) : extractZipArchive(data, skillDir);
|
|
638
|
+
fs.writeFileSync(path.join(skillDir, ".posthog-wizard"), "");
|
|
639
|
+
logToFile(`downloadSkill: installed ${skillEntry.id} from ${skillEntry.downloadUrl} (${fileCount} files)`);
|
|
640
|
+
return { success: true };
|
|
641
|
+
} catch (err) {
|
|
642
|
+
logToFile(`downloadSkill: error: ${err.message}`);
|
|
643
|
+
analytics.wizardCapture("skill install failed", {
|
|
644
|
+
skill_id: skillEntry.id,
|
|
645
|
+
step,
|
|
646
|
+
platform: process.platform,
|
|
647
|
+
error: String(err.message).slice(0, 500)
|
|
648
|
+
});
|
|
649
|
+
return {
|
|
650
|
+
success: false,
|
|
651
|
+
error: err.message
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* High-level "install a skill by ID" helper. Fetches the skill menu,
|
|
657
|
+
* finds the skill, downloads and extracts it. Programs should use this
|
|
658
|
+
* instead of composing fetchSkillMenu + downloadSkill themselves.
|
|
659
|
+
*/
|
|
660
|
+
async function installSkillById(skillId, installDir, skillsBaseUrl, skillsRoot) {
|
|
661
|
+
const menu = await fetchSkillMenu(skillsBaseUrl);
|
|
662
|
+
if (!menu) return { kind: "menu-fetch-failed" };
|
|
663
|
+
const skill = Object.values(menu.categories).flat().find((s) => s.id === skillId);
|
|
664
|
+
if (!skill) return {
|
|
665
|
+
kind: "skill-not-found",
|
|
666
|
+
skillId
|
|
667
|
+
};
|
|
668
|
+
const result = await downloadSkill(skill, installDir, skillsRoot);
|
|
669
|
+
if (!result.success) return {
|
|
670
|
+
kind: "download-failed",
|
|
671
|
+
message: result.error ?? "unknown"
|
|
672
|
+
};
|
|
673
|
+
return {
|
|
674
|
+
kind: "ok",
|
|
675
|
+
path: skillsRoot ? `${skillsRoot}/${skillId}` : `.claude/skills/${skillId}`
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Pure decision function for the wizard_ask caps. Returns whether the
|
|
680
|
+
* upcoming call should proceed and, if not, the error message to surface
|
|
681
|
+
* to the agent. Extracted so the policy can be unit-tested without
|
|
682
|
+
* spinning up an MCP server.
|
|
683
|
+
*
|
|
684
|
+
* The adjacency nudge fires exactly once per run (the caller records it
|
|
685
|
+
* via `adjacencyNudged`) — flows that legitimately need several
|
|
686
|
+
* sequential, answer-dependent asks then proceed up to `maxQuestions`.
|
|
687
|
+
* Without the flag the rejected call would never advance the counter and
|
|
688
|
+
* every later call would be rejected, making caps above the threshold
|
|
689
|
+
* unreachable.
|
|
690
|
+
*/
|
|
691
|
+
function evaluateAskCap(callCount, maxQuestions, adjacencyNudged = false) {
|
|
692
|
+
if (callCount >= maxQuestions) return {
|
|
693
|
+
kind: "capped",
|
|
694
|
+
reason: "max_questions",
|
|
695
|
+
message: `Error: wizard_ask cap reached (${maxQuestions} calls in this run). Proceed with sensible defaults using the answers you already have, or emit [ABORT] requirements-incomplete.`
|
|
696
|
+
};
|
|
697
|
+
if (!adjacencyNudged && callCount >= 3) return {
|
|
698
|
+
kind: "capped",
|
|
699
|
+
reason: "adjacency",
|
|
700
|
+
message: `Error: too many wizard_ask calls in a row (${callCount} so far). Batch the remaining questions into a single call — the schema accepts up to 8 questions per invocation. If the remaining questions truly depend on earlier answers, ask again and they will go through.`
|
|
701
|
+
};
|
|
702
|
+
return { kind: "ok" };
|
|
703
|
+
}
|
|
704
|
+
const ENV_FILE_PATH_DESCRIPTION = "Path to the .env file, relative to the wizard working directory. Pass \".env\" for a file in that directory, or include the selected subproject path (for example, \"packages/app/.env\") for a nested project. Never prefix it with the wizard working directory's path inside an ancestor repository.";
|
|
705
|
+
/**
|
|
706
|
+
* Resolve filePath relative to workingDirectory, rejecting path traversal.
|
|
707
|
+
*/
|
|
708
|
+
function resolveEnvPath(workingDirectory, filePath) {
|
|
709
|
+
const resolved = path.resolve(workingDirectory, filePath);
|
|
710
|
+
if (!resolved.startsWith(workingDirectory + path.sep) && resolved !== workingDirectory) throw new Error(`Path traversal rejected: "${filePath}" resolves outside working directory`);
|
|
711
|
+
return resolved;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Ensure the given env file basename is covered by .gitignore in the working directory.
|
|
715
|
+
* Creates .gitignore if it doesn't exist; appends the entry if missing.
|
|
716
|
+
*/
|
|
717
|
+
function ensureGitignoreCoverage(workingDirectory, envFileName) {
|
|
718
|
+
const gitignorePath = path.join(workingDirectory, ".gitignore");
|
|
719
|
+
if (fs.existsSync(gitignorePath)) {
|
|
720
|
+
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
721
|
+
if (content.split("\n").some((line) => line.trim() === envFileName)) return;
|
|
722
|
+
const newContent = content.endsWith("\n") ? `${content}${envFileName}\n` : `${content}\n${envFileName}\n`;
|
|
723
|
+
fs.writeFileSync(gitignorePath, newContent, "utf8");
|
|
724
|
+
} else fs.writeFileSync(gitignorePath, `${envFileName}\n`, "utf8");
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Parse a .env file's content and return the set of defined key names.
|
|
728
|
+
*/
|
|
729
|
+
function parseEnvKeys(content) {
|
|
730
|
+
const keys = /* @__PURE__ */ new Set();
|
|
731
|
+
for (const line of content.split("\n")) {
|
|
732
|
+
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
733
|
+
if (match) keys.add(match[1]);
|
|
734
|
+
}
|
|
735
|
+
return keys;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Merge key-value pairs into existing .env content.
|
|
739
|
+
* Updates existing keys in-place, appends new keys at the end.
|
|
740
|
+
*/
|
|
741
|
+
function mergeEnvValues(content, values) {
|
|
742
|
+
let result = content;
|
|
743
|
+
const updatedKeys = /* @__PURE__ */ new Set();
|
|
744
|
+
for (const [key, value] of Object.entries(values)) {
|
|
745
|
+
const regex = new RegExp(`^(\\s*${key}\\s*=).*$`, "m");
|
|
746
|
+
if (regex.test(result)) {
|
|
747
|
+
result = result.replace(regex, `$1${value}`);
|
|
748
|
+
updatedKeys.add(key);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const newKeys = Object.entries(values).filter(([key]) => !updatedKeys.has(key));
|
|
752
|
+
if (newKeys.length > 0) {
|
|
753
|
+
if (result.length > 0 && !result.endsWith("\n")) result += "\n";
|
|
754
|
+
for (const [key, value] of newKeys) result += `${key}=${value}\n`;
|
|
755
|
+
}
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Swap sensitive text answers for opaque vault refs before they return to
|
|
760
|
+
* the agent — the raw value never enters the LLM conversation. Cancelled
|
|
761
|
+
* answers pass through as the sentinel, unvaulted.
|
|
762
|
+
*/
|
|
763
|
+
function vaultSensitiveAnswers(questions, answers, vault) {
|
|
764
|
+
const sensitiveById = new Map(questions.filter((q) => q.sensitive).map((q) => [q.id, q.prompt]));
|
|
765
|
+
const sanitised = {};
|
|
766
|
+
for (const [id, answer] of Object.entries(answers)) {
|
|
767
|
+
const label = sensitiveById.get(id);
|
|
768
|
+
if (label !== void 0 && typeof answer === "string" && answer !== "__cancelled__") {
|
|
769
|
+
const ref = vault.put(answer, {
|
|
770
|
+
label,
|
|
771
|
+
source: "wizard_ask"
|
|
772
|
+
});
|
|
773
|
+
sanitised[id] = { secretRef: ref };
|
|
774
|
+
logToFile(`wizard_ask: vaulted answer for "${id}" as ${ref}`);
|
|
775
|
+
} else sanitised[id] = answer;
|
|
776
|
+
}
|
|
777
|
+
return sanitised;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Resolve `{secretRef}` entries host-side, so the value is written but never
|
|
781
|
+
* returned to the agent. Callers format their own error envelope from the
|
|
782
|
+
* failing key/ref.
|
|
783
|
+
*/
|
|
784
|
+
function resolveEnvSecretRefs(values, vault) {
|
|
785
|
+
const resolved = {};
|
|
786
|
+
const refKeys = [];
|
|
787
|
+
for (const [key, val] of Object.entries(values)) {
|
|
788
|
+
if (typeof val === "string") {
|
|
789
|
+
resolved[key] = val;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const secret = vault.get(val.secretRef);
|
|
793
|
+
if (secret === void 0) return {
|
|
794
|
+
ok: false,
|
|
795
|
+
key,
|
|
796
|
+
secretRef: val.secretRef
|
|
797
|
+
};
|
|
798
|
+
resolved[key] = secret;
|
|
799
|
+
refKeys.push(key);
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
ok: true,
|
|
803
|
+
values: resolved,
|
|
804
|
+
refKeys
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const AUDIT_STATUSES = [
|
|
808
|
+
"pending",
|
|
809
|
+
"pass",
|
|
810
|
+
"error",
|
|
811
|
+
"warning",
|
|
812
|
+
"suggestion"
|
|
813
|
+
];
|
|
814
|
+
/** Atomically write the audit ledger. Thin typed wrapper over writeJsonAtomic. */
|
|
815
|
+
function writeLedgerAtomic(targetPath, checks) {
|
|
816
|
+
writeJsonAtomic(targetPath, checks);
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Apply a batch of patches to the ledger by id. Returns the new array and the
|
|
820
|
+
* list of update ids that didn't match any existing check.
|
|
821
|
+
*/
|
|
822
|
+
function applyAuditUpdates(current, updates) {
|
|
823
|
+
const byId = new Map(current.map((c) => [c.id, c]));
|
|
824
|
+
const unknown = [];
|
|
825
|
+
for (const u of updates) {
|
|
826
|
+
const existing = byId.get(u.id);
|
|
827
|
+
if (!existing) {
|
|
828
|
+
unknown.push(u.id);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
byId.set(u.id, {
|
|
832
|
+
...existing,
|
|
833
|
+
status: u.status,
|
|
834
|
+
...u.file !== void 0 ? { file: u.file } : {},
|
|
835
|
+
...u.details !== void 0 ? { details: u.details } : {}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
next: current.map((c) => byId.get(c.id) ?? c),
|
|
840
|
+
unknown
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Append new checks to a seeded ledger. Duplicate ids are reported without
|
|
845
|
+
* mutating the current ledger, including duplicates inside the additions.
|
|
846
|
+
*/
|
|
847
|
+
function applyAuditAdditions(current, additions) {
|
|
848
|
+
const existingIds = new Set(current.map((c) => c.id));
|
|
849
|
+
const additionIds = /* @__PURE__ */ new Set();
|
|
850
|
+
const duplicates = [];
|
|
851
|
+
for (const check of additions) {
|
|
852
|
+
if (existingIds.has(check.id) || additionIds.has(check.id)) {
|
|
853
|
+
duplicates.push(check.id);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
additionIds.add(check.id);
|
|
857
|
+
}
|
|
858
|
+
if (duplicates.length > 0) return {
|
|
859
|
+
next: current,
|
|
860
|
+
duplicates
|
|
861
|
+
};
|
|
862
|
+
return {
|
|
863
|
+
next: [...current, ...additions],
|
|
864
|
+
duplicates: []
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function readLedger(targetPath) {
|
|
868
|
+
if (!fs.existsSync(targetPath)) return [];
|
|
869
|
+
try {
|
|
870
|
+
return coerceAuditChecks(JSON.parse(fs.readFileSync(targetPath, "utf8")));
|
|
871
|
+
} catch {
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function appendAuditChecksToLedger(targetPath, additions) {
|
|
876
|
+
if (!fs.existsSync(targetPath)) return {
|
|
877
|
+
ok: false,
|
|
878
|
+
reason: "missing-ledger"
|
|
879
|
+
};
|
|
880
|
+
const { next, duplicates } = applyAuditAdditions(readLedger(targetPath), additions);
|
|
881
|
+
if (duplicates.length > 0) return {
|
|
882
|
+
ok: false,
|
|
883
|
+
reason: "duplicate-ids",
|
|
884
|
+
ids: duplicates
|
|
885
|
+
};
|
|
886
|
+
writeLedgerAtomic(targetPath, next);
|
|
887
|
+
return {
|
|
888
|
+
ok: true,
|
|
889
|
+
added: additions.length
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const SERVER_NAME = "wizard-tools";
|
|
893
|
+
/** Tool names exposed by the wizard-tools server, keyed for selective use. */
|
|
894
|
+
const WIZARD_TOOL_NAMES = {
|
|
895
|
+
checkEnvKeys: `mcp__${SERVER_NAME}__check_env_keys`,
|
|
896
|
+
setEnvValues: `mcp__${SERVER_NAME}__set_env_values`,
|
|
897
|
+
detectPackageManager: `mcp__${SERVER_NAME}__detect_package_manager`,
|
|
898
|
+
loadSkillMenu: `mcp__${SERVER_NAME}__load_skill_menu`,
|
|
899
|
+
installSkill: `mcp__${SERVER_NAME}__install_skill`,
|
|
900
|
+
auditSeedChecks: `mcp__${SERVER_NAME}__audit_seed_checks`,
|
|
901
|
+
auditAddChecks: `mcp__${SERVER_NAME}__audit_add_checks`,
|
|
902
|
+
auditResolveChecks: `mcp__${SERVER_NAME}__audit_resolve_checks`,
|
|
903
|
+
wizardAsk: `mcp__${SERVER_NAME}__wizard_ask`,
|
|
904
|
+
enqueueTask: `mcp__${SERVER_NAME}__enqueue_task`,
|
|
905
|
+
completeTask: `mcp__${SERVER_NAME}__complete_task`,
|
|
906
|
+
readHandoffs: `mcp__${SERVER_NAME}__read_handoffs`
|
|
907
|
+
};
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/lib/secret-vault.ts
|
|
910
|
+
/**
|
|
911
|
+
* Session-scoped secret vault.
|
|
912
|
+
*
|
|
913
|
+
* Tools that handle sensitive values (personal API keys pasted by the user
|
|
914
|
+
* or minted by the wizard, OAuth tokens, etc.) store them here and return
|
|
915
|
+
* an opaque `secret:<uuid>` ref to the agent. The agent passes the ref
|
|
916
|
+
* around to subsequent tools — `set_env_values` resolves it host-side
|
|
917
|
+
* before writing — but the raw value never enters the LLM conversation.
|
|
918
|
+
*
|
|
919
|
+
* The vault is created once per `createWizardToolsServer` call and lives
|
|
920
|
+
* for the duration of a single wizard run. There is no persistence and
|
|
921
|
+
* no cross-session sharing; refs minted in one run cannot be resolved in
|
|
922
|
+
* another.
|
|
923
|
+
*/
|
|
924
|
+
const REF_PREFIX = "secret:";
|
|
925
|
+
function createSecretVault() {
|
|
926
|
+
const store = /* @__PURE__ */ new Map();
|
|
927
|
+
return {
|
|
928
|
+
put(value, meta) {
|
|
929
|
+
const ref = `${REF_PREFIX}${randomUUID()}`;
|
|
930
|
+
store.set(ref, {
|
|
931
|
+
value,
|
|
932
|
+
meta: {
|
|
933
|
+
ref,
|
|
934
|
+
label: meta.label,
|
|
935
|
+
source: meta.source,
|
|
936
|
+
createdAt: Date.now()
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
return ref;
|
|
940
|
+
},
|
|
941
|
+
get(ref) {
|
|
942
|
+
return store.get(ref)?.value;
|
|
943
|
+
},
|
|
944
|
+
has(ref) {
|
|
945
|
+
return store.has(ref);
|
|
946
|
+
},
|
|
947
|
+
list() {
|
|
948
|
+
return [...store.values()].map((s) => s.meta);
|
|
949
|
+
},
|
|
950
|
+
clear() {
|
|
951
|
+
store.clear();
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region src/lib/wizard-tools/mcp.ts
|
|
957
|
+
/**
|
|
958
|
+
* MCP facade over `./tools` — the unified in-process server the anthropic
|
|
959
|
+
* harness mounts. Declarations (zod schemas, descriptions) and the MCP result
|
|
960
|
+
* envelope live here; all behavior is imported from the shared core.
|
|
961
|
+
*
|
|
962
|
+
* Provides: check_env_keys, set_env_values, detect_package_manager,
|
|
963
|
+
* load_skill_menu / install_skill, audit_* ledger tools, wizard_ask, and the
|
|
964
|
+
* orchestrator queue tools. Secret values never leave the machine.
|
|
965
|
+
*/
|
|
966
|
+
const auditCheckSchema = z.object({
|
|
967
|
+
id: z.string().min(1),
|
|
968
|
+
area: z.string().min(1),
|
|
969
|
+
label: z.string().min(1),
|
|
970
|
+
status: z.enum(AUDIT_STATUSES),
|
|
971
|
+
file: z.string().optional(),
|
|
972
|
+
details: z.string().optional()
|
|
973
|
+
});
|
|
974
|
+
const auditUpdateSchema = z.object({
|
|
975
|
+
id: z.string().min(1),
|
|
976
|
+
status: z.enum(AUDIT_STATUSES),
|
|
977
|
+
file: z.string().optional(),
|
|
978
|
+
details: z.string().optional()
|
|
979
|
+
});
|
|
980
|
+
let _sdkModule$1 = null;
|
|
981
|
+
async function getSDKModule$1() {
|
|
982
|
+
if (!_sdkModule$1) _sdkModule$1 = await import("@anthropic-ai/claude-agent-sdk");
|
|
983
|
+
return _sdkModule$1;
|
|
984
|
+
}
|
|
985
|
+
/** Default per-run cap on wizard_ask calls when no override is provided. */
|
|
986
|
+
/**
|
|
987
|
+
* Create the unified in-process MCP server with all wizard tools.
|
|
988
|
+
* Must be called asynchronously because the SDK is an ESM module loaded via dynamic import.
|
|
989
|
+
*/
|
|
990
|
+
async function createWizardToolsServer(options) {
|
|
991
|
+
const { workingDirectory, detectPackageManager, skillsBaseUrl, askBridge, askMaxQuestions = 10, secretVault = createSecretVault(), orchestrator } = options;
|
|
992
|
+
const { tool, createSdkMcpServer } = await getSDKModule$1();
|
|
993
|
+
let askCallCount = 0;
|
|
994
|
+
let askAdjacencyNudged = false;
|
|
995
|
+
let cachedSkillMenu = {};
|
|
996
|
+
let categoryNames = ["integration"];
|
|
997
|
+
const menu = await fetchSkillMenu(skillsBaseUrl);
|
|
998
|
+
if (menu) cachedSkillMenu = menu.categories;
|
|
999
|
+
const keys = Object.keys(cachedSkillMenu);
|
|
1000
|
+
if (keys.length > 0) categoryNames = keys;
|
|
1001
|
+
const checkEnvKeys = tool("check_env_keys", "Check which environment variable keys are present or missing in a .env file. Never reveals values.", {
|
|
1002
|
+
filePath: z.string().describe(ENV_FILE_PATH_DESCRIPTION),
|
|
1003
|
+
keys: z.array(z.string()).describe("Environment variable key names to check")
|
|
1004
|
+
}, (args) => {
|
|
1005
|
+
const resolved = resolveEnvPath(workingDirectory, args.filePath);
|
|
1006
|
+
logToFile(`check_env_keys: ${resolved}, keys: ${args.keys.join(", ")}`);
|
|
1007
|
+
const existingKeys = fs.existsSync(resolved) ? parseEnvKeys(fs.readFileSync(resolved, "utf8")) : /* @__PURE__ */ new Set();
|
|
1008
|
+
const results = {};
|
|
1009
|
+
for (const key of args.keys) results[key] = existingKeys.has(key) ? "present" : "missing";
|
|
1010
|
+
return { content: [{
|
|
1011
|
+
type: "text",
|
|
1012
|
+
text: JSON.stringify(results, null, 2)
|
|
1013
|
+
}] };
|
|
1014
|
+
});
|
|
1015
|
+
const setEnvValues = tool("set_env_values", "Create or update environment variable keys in a .env file. Creates the file if it does not exist. Ensures .gitignore coverage. Each value can be either a literal string or a secret reference of the form `{ \"secretRef\": \"secret:...\" }` returned by another tool (e.g. wizard_ask). Secret references are resolved locally — the actual value is written to the file but never returned to the agent.", {
|
|
1016
|
+
filePath: z.string().describe(ENV_FILE_PATH_DESCRIPTION),
|
|
1017
|
+
values: z.record(z.string(), z.union([z.string(), z.object({ secretRef: z.string() })])).describe("Key → (literal string OR { secretRef } pointing to a vaulted secret)")
|
|
1018
|
+
}, (args) => {
|
|
1019
|
+
const forbidden = Object.keys(args.values).find((k) => k.toUpperCase() === "POSTHOG_KEY");
|
|
1020
|
+
if (forbidden) return {
|
|
1021
|
+
content: [{
|
|
1022
|
+
type: "text",
|
|
1023
|
+
text: `Error: "${forbidden}" is not a valid PostHog env var name. Use the project-specific key name from your framework's integration guide (e.g. NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN).`
|
|
1024
|
+
}],
|
|
1025
|
+
isError: true
|
|
1026
|
+
};
|
|
1027
|
+
const resolution = resolveEnvSecretRefs(args.values, secretVault);
|
|
1028
|
+
if (!resolution.ok) return {
|
|
1029
|
+
content: [{
|
|
1030
|
+
type: "text",
|
|
1031
|
+
text: `Error: secret reference "${resolution.secretRef}" for key "${resolution.key}" is not known to the vault. The ref may have expired, been minted in a different run, or been mistyped.`
|
|
1032
|
+
}],
|
|
1033
|
+
isError: true
|
|
1034
|
+
};
|
|
1035
|
+
const { values: resolvedValues, refKeys: resolvedRefKeys } = resolution;
|
|
1036
|
+
const resolved = resolveEnvPath(workingDirectory, args.filePath);
|
|
1037
|
+
logToFile(`set_env_values: ${resolved}, keys: ${Object.keys(resolvedValues).join(", ")}${resolvedRefKeys.length > 0 ? ` (secret refs: ${resolvedRefKeys.join(", ")})` : ""}`);
|
|
1038
|
+
const content = mergeEnvValues(fs.existsSync(resolved) ? fs.readFileSync(resolved, "utf8") : "", resolvedValues);
|
|
1039
|
+
const dir = path.dirname(resolved);
|
|
1040
|
+
if (!fs.existsSync(dir)) {
|
|
1041
|
+
analytics.wizardCapture("set_env_values parent dir missing", { platform: process.platform });
|
|
1042
|
+
return {
|
|
1043
|
+
content: [{
|
|
1044
|
+
type: "text",
|
|
1045
|
+
text: `Error: parent directory does not exist: "${path.dirname(args.filePath)}". filePath is resolved against the wizard working directory — pass ".env" for a file there, or "<subproject>/.env" for an existing nested project.`
|
|
1046
|
+
}],
|
|
1047
|
+
isError: true
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
fs.writeFileSync(resolved, content, "utf8");
|
|
1051
|
+
ensureGitignoreCoverage(workingDirectory, path.basename(resolved));
|
|
1052
|
+
return { content: [{
|
|
1053
|
+
type: "text",
|
|
1054
|
+
text: `Updated ${Object.keys(args.values).length} key(s) in ${args.filePath}`
|
|
1055
|
+
}] };
|
|
1056
|
+
});
|
|
1057
|
+
const detectPM = tool("detect_package_manager", "Detect which package manager(s) the project uses. Returns the name, install command, and run command for each detected package manager. Call this before running any install commands.", {}, async () => {
|
|
1058
|
+
logToFile(`detect_package_manager: scanning ${workingDirectory}`);
|
|
1059
|
+
const result = await detectPackageManager(workingDirectory);
|
|
1060
|
+
logToFile(`detect_package_manager: detected ${result.detected.length} package manager(s)`);
|
|
1061
|
+
return { content: [{
|
|
1062
|
+
type: "text",
|
|
1063
|
+
text: JSON.stringify(result, null, 2)
|
|
1064
|
+
}] };
|
|
1065
|
+
});
|
|
1066
|
+
const loadSkillMenu = tool("load_skill_menu", "Load available PostHog skills for a category. Returns skill IDs and names. Call this first, then use install_skill with the chosen ID.", { category: z.enum(categoryNames).describe("Skill category") }, (args) => {
|
|
1067
|
+
const skills = cachedSkillMenu[args.category];
|
|
1068
|
+
if (!skills || skills.length === 0) return {
|
|
1069
|
+
content: [{
|
|
1070
|
+
type: "text",
|
|
1071
|
+
text: `No skills found for category "${args.category}".`
|
|
1072
|
+
}],
|
|
1073
|
+
isError: true
|
|
1074
|
+
};
|
|
1075
|
+
const menuText = skills.map((s) => `- ${s.id}: ${s.name}`).join("\n");
|
|
1076
|
+
logToFile(`load_skill_menu: returning ${skills.length} skills for "${args.category}"`);
|
|
1077
|
+
return { content: [{
|
|
1078
|
+
type: "text",
|
|
1079
|
+
text: menuText
|
|
1080
|
+
}] };
|
|
1081
|
+
});
|
|
1082
|
+
const installSkill = tool("install_skill", "Download and install a PostHog skill by ID. Call load_skill_menu first to see available skills. Extracts the skill to .claude/skills/<skillId>/.", { skillId: z.string().describe("Skill ID from the skill menu (e.g., \"integration-nextjs-app-router\")") }, async (args) => {
|
|
1083
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(args.skillId)) return {
|
|
1084
|
+
content: [{
|
|
1085
|
+
type: "text",
|
|
1086
|
+
text: "Error: skillId must be lowercase alphanumeric with hyphens."
|
|
1087
|
+
}],
|
|
1088
|
+
isError: true
|
|
1089
|
+
};
|
|
1090
|
+
const skill = Object.values(cachedSkillMenu).flat().find((s) => s.id === args.skillId);
|
|
1091
|
+
if (!skill) return {
|
|
1092
|
+
content: [{
|
|
1093
|
+
type: "text",
|
|
1094
|
+
text: `Error: skill "${args.skillId}" not found. Use load_skill_menu to see available skills.`
|
|
1095
|
+
}],
|
|
1096
|
+
isError: true
|
|
1097
|
+
};
|
|
1098
|
+
const result = await downloadSkill(skill, workingDirectory);
|
|
1099
|
+
if (result.success) return { content: [{
|
|
1100
|
+
type: "text",
|
|
1101
|
+
text: `Skill installed to .claude/skills/${args.skillId}/`
|
|
1102
|
+
}] };
|
|
1103
|
+
else {
|
|
1104
|
+
analytics.captureException(/* @__PURE__ */ new Error("Skill install failed: download-failed"), {
|
|
1105
|
+
source: "install_skill_tool",
|
|
1106
|
+
skill_id: args.skillId,
|
|
1107
|
+
error_detail: String(result.error).slice(0, 500),
|
|
1108
|
+
platform: process.platform
|
|
1109
|
+
});
|
|
1110
|
+
return {
|
|
1111
|
+
content: [{
|
|
1112
|
+
type: "text",
|
|
1113
|
+
text: `Error installing skill: ${result.error}`
|
|
1114
|
+
}],
|
|
1115
|
+
isError: true
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
const auditLedgerPath = path.join(workingDirectory, AUDIT_CHECKS_FILE);
|
|
1120
|
+
const auditMutex = makeMutex();
|
|
1121
|
+
const auditSeedChecks = tool("audit_seed_checks", "Seed the audit ledger at .posthog-audit-checks.json with the full set of pending checks. Call this once at the start of the audit. Atomically replaces any existing ledger.", { checks: z.array(auditCheckSchema).describe("Full pending checklist to write to the ledger") }, async (args) => {
|
|
1122
|
+
return auditMutex(() => {
|
|
1123
|
+
writeLedgerAtomic(auditLedgerPath, args.checks);
|
|
1124
|
+
logToFile(`audit_seed_checks: wrote ${args.checks.length} entries`);
|
|
1125
|
+
return { content: [{
|
|
1126
|
+
type: "text",
|
|
1127
|
+
text: `Seeded ${args.checks.length} audit checks.`
|
|
1128
|
+
}] };
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
const auditAddChecks = tool("audit_add_checks", "Append one or more pending checks to the existing audit ledger at .posthog-audit-checks.json. Call audit_seed_checks first. Atomically rejects duplicate ids without changing the ledger.", { checks: z.array(auditCheckSchema).min(1).describe("Additional checks to append to the existing ledger") }, async (args) => {
|
|
1132
|
+
return auditMutex(() => {
|
|
1133
|
+
const result = appendAuditChecksToLedger(auditLedgerPath, args.checks);
|
|
1134
|
+
if (!result.ok) {
|
|
1135
|
+
if (result.reason === "missing-ledger") return {
|
|
1136
|
+
content: [{
|
|
1137
|
+
type: "text",
|
|
1138
|
+
text: "Error: audit ledger does not exist. Run audit_seed_checks first."
|
|
1139
|
+
}],
|
|
1140
|
+
isError: true
|
|
1141
|
+
};
|
|
1142
|
+
return {
|
|
1143
|
+
content: [{
|
|
1144
|
+
type: "text",
|
|
1145
|
+
text: `Error: duplicate check id(s): ${result.ids.join(", ")}. Check ids must be unique.`
|
|
1146
|
+
}],
|
|
1147
|
+
isError: true
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
logToFile(`audit_add_checks: added ${result.added} entries`);
|
|
1151
|
+
return { content: [{
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: `Added ${result.added} audit check(s).`
|
|
1154
|
+
}] };
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
const auditResolveChecks = tool("audit_resolve_checks", "Resolve one or more audit checks by id. Patches each entry's status (and optional file/details) and writes the ledger back atomically. Concurrent calls serialize.", { updates: z.array(auditUpdateSchema).min(1).describe("Patches to apply, keyed by check id") }, async (args) => {
|
|
1158
|
+
return auditMutex(() => {
|
|
1159
|
+
const { next, unknown } = applyAuditUpdates(readLedger(auditLedgerPath), args.updates);
|
|
1160
|
+
if (unknown.length > 0) return {
|
|
1161
|
+
content: [{
|
|
1162
|
+
type: "text",
|
|
1163
|
+
text: `Error: unknown check id(s): ${unknown.join(", ")}. Run audit_seed_checks first or check the id.`
|
|
1164
|
+
}],
|
|
1165
|
+
isError: true
|
|
1166
|
+
};
|
|
1167
|
+
writeLedgerAtomic(auditLedgerPath, next);
|
|
1168
|
+
logToFile(`audit_resolve_checks: applied ${args.updates.length} update(s)`);
|
|
1169
|
+
return { content: [{
|
|
1170
|
+
type: "text",
|
|
1171
|
+
text: `Resolved ${args.updates.length} check(s).`
|
|
1172
|
+
}] };
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
const askQuestionSchema = z.object({
|
|
1176
|
+
id: z.string().min(1).describe("Stable key for the answer in the response map"),
|
|
1177
|
+
prompt: z.string().min(1).describe("Question text shown to the user"),
|
|
1178
|
+
kind: z.enum([
|
|
1179
|
+
"single",
|
|
1180
|
+
"multi",
|
|
1181
|
+
"text"
|
|
1182
|
+
]).describe("'single' = pick one option, 'multi' = pick any, 'text' = free-form single-line answer"),
|
|
1183
|
+
options: z.array(z.object({
|
|
1184
|
+
label: z.string(),
|
|
1185
|
+
value: z.string(),
|
|
1186
|
+
description: z.string().optional().describe("Optional secondary line shown dimmed and wrapped beneath the label (multi-select only). Use when a choice needs more than a title — e.g. what a custom scout watches and what makes it speak up.")
|
|
1187
|
+
})).optional().describe("Required for kind=single|multi; ignored for kind=text"),
|
|
1188
|
+
required: z.boolean().optional().describe("Defaults to true"),
|
|
1189
|
+
sensitive: z.boolean().optional().describe("Only valid for kind='text'. When true, the user's answer is stored in the wizard's secret vault and returned to you as { secretRef: 'secret:...' } instead of the raw string. Use for API keys, tokens, and any other secret the user types in. The secretRef is only resolved by wizard-tools that accept it (e.g. set_env_values) — it is NOT resolved when passed to other MCP tools (e.g. PostHog data-warehouse tools), which will reject it. For a secret that must reach another tool, write it to the env with set_env_values first, or use that tool's own credential-reference flow.")
|
|
1190
|
+
});
|
|
1191
|
+
return createSdkMcpServer({
|
|
1192
|
+
name: SERVER_NAME,
|
|
1193
|
+
version: "1.0.0",
|
|
1194
|
+
tools: [
|
|
1195
|
+
checkEnvKeys,
|
|
1196
|
+
setEnvValues,
|
|
1197
|
+
detectPM,
|
|
1198
|
+
loadSkillMenu,
|
|
1199
|
+
installSkill,
|
|
1200
|
+
auditSeedChecks,
|
|
1201
|
+
auditAddChecks,
|
|
1202
|
+
auditResolveChecks,
|
|
1203
|
+
tool("wizard_ask", "Ask the user one or more structured questions and wait for their answers. Use this whenever you would otherwise inline a question in your text output. Batch related questions into a single call (up to 8) rather than asking one at a time; sequential calls are fine when later questions genuinely depend on earlier answers. A fully cancelled or timed-out response does NOT count against the per-run cap — treat it as \"the user declined\" and fall back gracefully (e.g. hand over a deep link) without worrying about a wasted call.", { questions: z.array(askQuestionSchema).min(1).max(8) }, async (args) => {
|
|
1204
|
+
if (!askBridge) return {
|
|
1205
|
+
content: [{
|
|
1206
|
+
type: "text",
|
|
1207
|
+
text: "Error: wizard_ask is not available in this environment (CI / non-interactive). Proceed with sensible defaults or emit [ABORT] requirements-incomplete."
|
|
1208
|
+
}],
|
|
1209
|
+
isError: true
|
|
1210
|
+
};
|
|
1211
|
+
const capDecision = evaluateAskCap(askCallCount, askMaxQuestions, askAdjacencyNudged);
|
|
1212
|
+
if (capDecision.kind === "capped") {
|
|
1213
|
+
if (capDecision.reason === "adjacency") askAdjacencyNudged = true;
|
|
1214
|
+
analytics.wizardCapture("wizard_ask capped", {
|
|
1215
|
+
reason: capDecision.reason,
|
|
1216
|
+
call_count: askCallCount,
|
|
1217
|
+
max_questions: askMaxQuestions
|
|
1218
|
+
});
|
|
1219
|
+
return {
|
|
1220
|
+
content: [{
|
|
1221
|
+
type: "text",
|
|
1222
|
+
text: capDecision.message
|
|
1223
|
+
}],
|
|
1224
|
+
isError: true
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
for (const q of args.questions) {
|
|
1228
|
+
if ((q.kind === "single" || q.kind === "multi") && (!q.options || q.options.length === 0)) return {
|
|
1229
|
+
content: [{
|
|
1230
|
+
type: "text",
|
|
1231
|
+
text: `Error: question "${q.id}" has kind="${q.kind}" but no options. Provide at least one { label, value } option, or change kind to "text".`
|
|
1232
|
+
}],
|
|
1233
|
+
isError: true
|
|
1234
|
+
};
|
|
1235
|
+
if (q.sensitive && q.kind !== "text") return {
|
|
1236
|
+
content: [{
|
|
1237
|
+
type: "text",
|
|
1238
|
+
text: `Error: question "${q.id}" sets sensitive=true but kind="${q.kind}". Only kind="text" answers can be vaulted as secrets.`
|
|
1239
|
+
}],
|
|
1240
|
+
isError: true
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1244
|
+
for (const q of args.questions) {
|
|
1245
|
+
if (ids.has(q.id)) return {
|
|
1246
|
+
content: [{
|
|
1247
|
+
type: "text",
|
|
1248
|
+
text: `Error: duplicate question id "${q.id}". Each question must have a unique id.`
|
|
1249
|
+
}],
|
|
1250
|
+
isError: true
|
|
1251
|
+
};
|
|
1252
|
+
ids.add(q.id);
|
|
1253
|
+
}
|
|
1254
|
+
askCallCount += 1;
|
|
1255
|
+
try {
|
|
1256
|
+
const answers = await askBridge.request({ questions: args.questions });
|
|
1257
|
+
if (isFullyCancelled(answers)) askCallCount -= 1;
|
|
1258
|
+
const sanitised = vaultSensitiveAnswers(args.questions, answers, secretVault);
|
|
1259
|
+
logToFile(`wizard_ask: resolved ${Object.keys(answers).length} answer(s) for ${args.questions.length} question(s)`);
|
|
1260
|
+
return { content: [{
|
|
1261
|
+
type: "text",
|
|
1262
|
+
text: JSON.stringify({ answers: sanitised }, null, 2)
|
|
1263
|
+
}] };
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
askCallCount -= 1;
|
|
1266
|
+
logToFile(`wizard_ask: error: ${err?.message ?? err}`);
|
|
1267
|
+
return {
|
|
1268
|
+
content: [{
|
|
1269
|
+
type: "text",
|
|
1270
|
+
text: `Error: wizard_ask failed: ${err?.message ?? String(err)}`
|
|
1271
|
+
}],
|
|
1272
|
+
isError: true
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
}),
|
|
1276
|
+
...orchestrator ? buildOrchestratorTools(tool, orchestrator) : []
|
|
1277
|
+
]
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
//#endregion
|
|
178
1281
|
//#region src/lib/agent/triage-provider.ts
|
|
179
1282
|
/**
|
|
180
1283
|
* LLM provider for warlock security-scan triage.
|
|
@@ -231,44 +1334,6 @@ function createTriageLLMProvider(auth) {
|
|
|
231
1334
|
};
|
|
232
1335
|
}
|
|
233
1336
|
//#endregion
|
|
234
|
-
//#region src/lib/agent/commandments.ts
|
|
235
|
-
/**
|
|
236
|
-
* Wizard-wide commandments that are always appended as a system prompt.
|
|
237
|
-
*
|
|
238
|
-
* Keep this as a simple string so it can be inlined into the compiled bundle
|
|
239
|
-
* without extra files, copying, or runtime I/O.
|
|
240
|
-
*/
|
|
241
|
-
const WIZARD_COMMANDMENTS = [
|
|
242
|
-
"Never hallucinate a PostHog project token, host, or any other secret. Always use the real values that have been configured for this project (for example via environment variables).",
|
|
243
|
-
"Never write API keys, access tokens, or other secrets directly into source code. Always reference environment variables instead, and rely on the wizard-tools MCP server (check_env_keys / set_env_values) to create or update .env files.",
|
|
244
|
-
"Always use the detect_package_manager tool from the wizard-tools MCP server to determine the package manager. Do not guess based on lockfiles or hard-code npm, yarn, pnpm, bun, pip, etc.",
|
|
245
|
-
"Before writing to any file, you MUST read that exact file immediately beforehand using the Read tool, even if you have already read it earlier in the run. This avoids tool failures and stale edits.",
|
|
246
|
-
"Treat feature flags, custom properties, and event names as part of an analytics contract. Prefer reusing existing names and patterns in the project. When you must introduce new ones, make them clear, descriptive, and consistent with existing conventions, and avoid scattering the same flag or property across many unrelated callsites.",
|
|
247
|
-
"Keep PostHog data capture at its defaults unless the user explicitly asks otherwise. Do not disable autocapture, do not disable session recording, and never set opt_out_capturing (or opted_out) to true in the SDK init config — these turn off data the user almost always wants. Note: posthog.opt_out_capturing() called at runtime for GDPR consent flows is legitimate; the rule is about the init configuration.",
|
|
248
|
-
"Never put personally identifiable information — emails, full or partial names, phone numbers, physical or IP addresses, or other user-entered PII — in capture() event properties. PII belongs on the PERSON: send it via identify()/$set (or the SDK's person-property API), and capture events with a stable distinct id derived from the authenticated user or session, never a raw email or name. This holds for every SDK, client and server.",
|
|
249
|
-
"Prefer minimal, targeted edits that achieve the requested behavior while preserving existing structure and style. Avoid large refactors, broad reformatting, or unrelated changes unless explicitly requested.",
|
|
250
|
-
"When you change a file that already exists, edit it in place. If an edit fails because the text you are matching is not unique, add just enough surrounding context to make it unique, or apply the change to each occurrence — never fall back to rewriting the whole file. Write a file in full only when you are creating it, never to modify one that already exists.",
|
|
251
|
-
"Before you overwrite or delete anything that already exists, look at what you would remove. This integration only adds instrumentation, so keep every part of the project unrelated to PostHog exactly as it was — never drop existing code, comments, or markup to make room for a change.",
|
|
252
|
-
"Do not spawn subagents unless explicitly instructed to do so.",
|
|
253
|
-
"Create tasks as soon as you understand the work you are going to carry out. Break the list into distinct stages of work that the user can follow through. Create all tasks in a single tool call, in the order you will be performing them. Drive the work with TaskUpdate: status in_progress when you begin a task, completed when done.",
|
|
254
|
-
"Keep task titles broad and stage-oriented — describe the purpose or area of work, not the specific files, paths, or symbols involved. Do not name individual files, modules, or directories inside task titles, and do not include illustrative examples within a task title.",
|
|
255
|
-
"Do not assume \"PostHog provider\" or \"PostHogProvider\" in task labels, status messages, or code. Only use a provider if the skill workflow explicitly requires one for the specific framework.",
|
|
256
|
-
"If you see a [YARA WARNING] or [YARA VIOLATION] message, handle it silently. Fix the issue (revert the write, avoid the flagged pattern) without mentioning security warnings or YARA to the user. These are internal guardrails, not user-facing alerts. Never show scary security language to the user.",
|
|
257
|
-
[
|
|
258
|
-
"When a skill instructs you to gather information from the user, use the `wizard_ask` MCP tool from the wizard-tools server. Never inline questions in your text output expecting a reply — the user has no way to answer text.",
|
|
259
|
-
"When a skill provides a numbered or bulleted list of questions, translate the entire list into a single `wizard_ask` tool call:",
|
|
260
|
-
" - One tool call per skill step. Batch every question from that step into the `questions` array — never split into multiple calls.",
|
|
261
|
-
" - Infer `kind` from the question phrasing: comma-separated alternatives (\"React, Vue, or vanilla JS?\") → `single`; phrasing like \"all that apply\" or \"any of\" → `multi`; everything else → `text`.",
|
|
262
|
-
" - For `single` and `multi`, extract the alternatives from the prose into `options` as `{ label, value }` pairs. Use the human phrase as `label` and a lowercase-hyphenated form as `value` (e.g., `label: \"Vanilla JS\"`, `value: \"vanilla-js\"`).",
|
|
263
|
-
" - Use a kebab-case slug of the question label as `id` (e.g., \"Tech stack\" → `tech-stack`, \"Show frequency\" → `show-frequency`).",
|
|
264
|
-
" - Do not invent fields the schema does not define (no `source`, `category`, `priority`, etc.) — the tool rejects unknown fields and the wizard already knows which skill is running.",
|
|
265
|
-
"After `wizard_ask` returns, use the answers directly — do not re-ask in text or call `wizard_ask` again for the same fields."
|
|
266
|
-
].join("\n")
|
|
267
|
-
].join("\n");
|
|
268
|
-
function getWizardCommandments() {
|
|
269
|
-
return WIZARD_COMMANDMENTS;
|
|
270
|
-
}
|
|
271
|
-
//#endregion
|
|
272
1337
|
//#region src/lib/agent/agent-phase.ts
|
|
273
1338
|
/** Maps a Claude SDK tool name to the phase that tool implies. Returns null
|
|
274
1339
|
* when the tool doesn't drive the visualizer (Task*, TodoWrite, etc.). */
|
|
@@ -959,62 +2024,9 @@ function buildAgentEnv(wizardMetadata, wizardFlags) {
|
|
|
959
2024
|
return encoded;
|
|
960
2025
|
}
|
|
961
2026
|
/**
|
|
962
|
-
*
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
"npm",
|
|
966
|
-
"pnpm",
|
|
967
|
-
"yarn",
|
|
968
|
-
"bun",
|
|
969
|
-
"npx",
|
|
970
|
-
"pip",
|
|
971
|
-
"pip3",
|
|
972
|
-
"poetry",
|
|
973
|
-
"pipenv",
|
|
974
|
-
"uv"
|
|
975
|
-
];
|
|
976
|
-
/**
|
|
977
|
-
* Safe scripts/commands that can be run with any package manager.
|
|
978
|
-
* Uses startsWith matching, so 'build' matches 'build', 'build:prod', etc.
|
|
979
|
-
* Note: Linting tools are in LINTING_TOOLS and checked separately.
|
|
980
|
-
*/
|
|
981
|
-
const SAFE_SCRIPTS = [
|
|
982
|
-
"install",
|
|
983
|
-
"add",
|
|
984
|
-
"ci",
|
|
985
|
-
"build",
|
|
986
|
-
"tsc",
|
|
987
|
-
"typecheck",
|
|
988
|
-
"type-check",
|
|
989
|
-
"check-types",
|
|
990
|
-
"types",
|
|
991
|
-
"lint",
|
|
992
|
-
"format"
|
|
993
|
-
];
|
|
994
|
-
/**
|
|
995
|
-
* Dangerous shell operators that could allow command injection.
|
|
996
|
-
* Note: We handle `2>&1` and `| tail/head` separately as safe patterns.
|
|
997
|
-
*/
|
|
998
|
-
const DANGEROUS_OPERATORS = /[;`$()]/;
|
|
999
|
-
/**
|
|
1000
|
-
* Check if command is an allowed package manager command.
|
|
1001
|
-
* Matches: <pkg-manager> [run|exec] <safe-script> [args...]
|
|
1002
|
-
*/
|
|
1003
|
-
function matchesAllowedPrefix(command) {
|
|
1004
|
-
const parts = command.split(/\s+/);
|
|
1005
|
-
if (parts.length === 0 || !PACKAGE_MANAGERS.includes(parts[0])) return false;
|
|
1006
|
-
let scriptIndex = 1;
|
|
1007
|
-
if (parts[scriptIndex] === "run" || parts[scriptIndex] === "exec") scriptIndex++;
|
|
1008
|
-
if (parts[scriptIndex] === "i") return true;
|
|
1009
|
-
const scriptPart = parts.slice(scriptIndex).join(" ");
|
|
1010
|
-
return SAFE_SCRIPTS.some((safe) => scriptPart.startsWith(safe)) || LINTING_TOOLS.some((tool) => scriptPart.startsWith(tool));
|
|
1011
|
-
}
|
|
1012
|
-
/**
|
|
1013
|
-
* Permission hook that allows only safe commands.
|
|
1014
|
-
* - Package manager install commands
|
|
1015
|
-
* - Build/typecheck/lint commands for verification
|
|
1016
|
-
* - Piping to tail/head for output limiting is allowed
|
|
1017
|
-
* - Stderr redirection (2>&1) is allowed
|
|
2027
|
+
* Permission hook that allows only safe commands. Bash commands are gated by
|
|
2028
|
+
* the exact per-manager fence in bash-fence.ts (install/build/typecheck/lint
|
|
2029
|
+
* commands, single | tail/head, 2>&1).
|
|
1018
2030
|
*
|
|
1019
2031
|
* `wizardAskPending` is true while a wizard_ask overlay is open — when set,
|
|
1020
2032
|
* Write/Edit calls are denied as a defense-in-depth measure against a
|
|
@@ -1040,7 +2052,8 @@ function wizardCanUseTool(toolName, input, context = {}) {
|
|
|
1040
2052
|
if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
|
|
1041
2053
|
const filePath = typeof input.file_path === "string" ? input.file_path : "";
|
|
1042
2054
|
const basename = path.basename(filePath);
|
|
1043
|
-
|
|
2055
|
+
const isEnvExample = /^\.env\.(example|sample|template|dist)$/.test(basename);
|
|
2056
|
+
if (basename.startsWith(".env") && !isEnvExample) {
|
|
1044
2057
|
logToFile(`Denying ${toolName} on env file: ${filePath}`);
|
|
1045
2058
|
return {
|
|
1046
2059
|
behavior: "deny",
|
|
@@ -1071,56 +2084,8 @@ function wizardCanUseTool(toolName, input, context = {}) {
|
|
|
1071
2084
|
updatedInput: input
|
|
1072
2085
|
};
|
|
1073
2086
|
const command = (typeof input.command === "string" ? input.command : "").trim();
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
debug(`Denying bash command with dangerous operators: ${command}`);
|
|
1077
|
-
analytics.wizardCapture("bash denied", {
|
|
1078
|
-
reason: "dangerous operators",
|
|
1079
|
-
command
|
|
1080
|
-
});
|
|
1081
|
-
return {
|
|
1082
|
-
behavior: "deny",
|
|
1083
|
-
message: `Bash command not allowed. Shell operators like ; \` $ ( ) are not permitted.`
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
const normalized = command.replace(/\s*\d*>&\d+\s*/g, " ").trim();
|
|
1087
|
-
const pipeMatch = normalized.match(/^(.+?)\s*\|\s*(tail|head)(\s+\S+)*\s*$/);
|
|
1088
|
-
if (pipeMatch) {
|
|
1089
|
-
const baseCommand = pipeMatch[1].trim();
|
|
1090
|
-
if (/[|&]/.test(baseCommand)) {
|
|
1091
|
-
logToFile(`Denying bash command with multiple pipes: ${command}`);
|
|
1092
|
-
debug(`Denying bash command with multiple pipes: ${command}`);
|
|
1093
|
-
analytics.wizardCapture("bash denied", {
|
|
1094
|
-
reason: "multiple pipes",
|
|
1095
|
-
command
|
|
1096
|
-
});
|
|
1097
|
-
return {
|
|
1098
|
-
behavior: "deny",
|
|
1099
|
-
message: `Bash command not allowed. Only single pipe to tail/head is permitted.`
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
|
-
if (matchesAllowedPrefix(baseCommand)) {
|
|
1103
|
-
logToFile(`Allowing bash command with output limiter: ${command}`);
|
|
1104
|
-
debug(`Allowing bash command with output limiter: ${command}`);
|
|
1105
|
-
return {
|
|
1106
|
-
behavior: "allow",
|
|
1107
|
-
updatedInput: input
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
if (/[|&]/.test(normalized)) {
|
|
1112
|
-
logToFile(`Denying bash command with pipe/&: ${command}`);
|
|
1113
|
-
debug(`Denying bash command with pipe/&: ${command}`);
|
|
1114
|
-
analytics.wizardCapture("bash denied", {
|
|
1115
|
-
reason: "disallowed pipe",
|
|
1116
|
-
command
|
|
1117
|
-
});
|
|
1118
|
-
return {
|
|
1119
|
-
behavior: "deny",
|
|
1120
|
-
message: `Bash command not allowed. Pipes are only permitted with tail/head for output limiting.`
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
if (matchesAllowedPrefix(normalized)) {
|
|
2087
|
+
const decision = evaluateBashCommand(command);
|
|
2088
|
+
if (decision.allowed) {
|
|
1124
2089
|
logToFile(`Allowing bash command: ${command}`);
|
|
1125
2090
|
debug(`Allowing bash command: ${command}`);
|
|
1126
2091
|
return {
|
|
@@ -1128,15 +2093,15 @@ function wizardCanUseTool(toolName, input, context = {}) {
|
|
|
1128
2093
|
updatedInput: input
|
|
1129
2094
|
};
|
|
1130
2095
|
}
|
|
1131
|
-
logToFile(`Denying bash command: ${command}`);
|
|
1132
|
-
debug(`Denying bash command: ${command}`);
|
|
2096
|
+
logToFile(`Denying bash command (${decision.analyticsReason}): ${command}`);
|
|
2097
|
+
debug(`Denying bash command (${decision.analyticsReason}): ${command}`);
|
|
1133
2098
|
analytics.wizardCapture("bash denied", {
|
|
1134
|
-
reason:
|
|
2099
|
+
reason: decision.analyticsReason,
|
|
1135
2100
|
command
|
|
1136
2101
|
});
|
|
1137
2102
|
return {
|
|
1138
2103
|
behavior: "deny",
|
|
1139
|
-
message:
|
|
2104
|
+
message: decision.message
|
|
1140
2105
|
};
|
|
1141
2106
|
}
|
|
1142
2107
|
/**
|
|
@@ -1807,6 +2772,6 @@ function handleSDKMessage(message, options, spinner, signals, receivedSuccessRes
|
|
|
1807
2772
|
}
|
|
1808
2773
|
}
|
|
1809
2774
|
//#endregion
|
|
1810
|
-
export { wizardCanUseTool as a, classifySettingsConflicts as c, sanitizeAgentSubprocessEnv as d, AgentOutputSignals as f,
|
|
2775
|
+
export { isFullyCancelled as A, mergeEnvValues as C, vaultSensitiveAnswers as D, resolveEnvSecretRefs as E, fetchWithRetry as O, installSkillById as S, resolveEnvPath as T, ENV_FILE_PATH_DESCRIPTION as _, wizardCanUseTool as a, evaluateAskCap as b, classifySettingsConflicts as c, sanitizeAgentSubprocessEnv as d, AgentOutputSignals as f, createSecretVault as g, createTriageLLMProvider as h, runAgent as i, createWizardAskBridge as k, recoverOrphanedSettingsBackups as l, REMARK_INSTRUCTION as m, buildRunTags as n, backupAndFixClaudeSettings as o, AgentSignals as p, initializeAgent as r, checkAllSettingsConflicts as s, buildAgentEnv as t, restoreClaudeSettings as u, WIZARD_TOOL_NAMES as v, parseEnvKeys as w, fetchSkillMenu as x, downloadSkill as y };
|
|
1811
2776
|
|
|
1812
|
-
//# sourceMappingURL=agent-interface-
|
|
2777
|
+
//# sourceMappingURL=agent-interface-B8roB2UP.js.map
|