@posthog/wizard 2.29.0 → 2.31.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 +35 -1
- package/dist/README.md +33 -0
- package/dist/{add-mcp-server-to-clients-B6Pj4IKt.js → add-mcp-server-to-clients-BPBRx_Nz.js} +4 -4
- package/dist/{add-mcp-server-to-clients-B6Pj4IKt.js.map → add-mcp-server-to-clients-BPBRx_Nz.js.map} +1 -1
- package/dist/{agent-interface-CVW8H9eF.js → agent-interface-DT_uyR45.js} +504 -425
- package/dist/agent-interface-DT_uyR45.js.map +1 -0
- package/dist/{agent-runner-VzTpPaVT.js → agent-runner-DfRka7f7.js} +41 -24
- package/dist/agent-runner-DfRka7f7.js.map +1 -0
- package/dist/{analytics-DvDjbNmK.js → analytics-3GR9OyE9.js} +14 -6
- package/dist/analytics-3GR9OyE9.js.map +1 -0
- package/dist/{api-DfpSG5xU.js → api-Dmc76exl.js} +3 -3
- package/dist/{api-DfpSG5xU.js.map → api-Dmc76exl.js.map} +1 -1
- package/dist/bin.js +33 -33
- package/dist/bin.js.map +1 -1
- package/dist/{ci-install-Bzfo3nON.js → ci-install-QWrT_cW8.js} +4 -4
- package/dist/{ci-install-Bzfo3nON.js.map → ci-install-QWrT_cW8.js.map} +1 -1
- package/dist/{debug-HQ0NrBA2.js → debug-aqoKImO6.js} +1 -1
- package/dist/{debug-CMZ7kqW1.js → debug-n42RObru.js} +9 -10
- package/dist/debug-n42RObru.js.map +1 -0
- package/dist/{environment-cVP7bGnh.js → environment-BKPsjOXk.js} +3 -3
- package/dist/{environment-cVP7bGnh.js.map → environment-BKPsjOXk.js.map} +1 -1
- package/dist/{file-utils-D1632P4x.js → file-utils-ALRqLr0x.js} +2 -2
- package/dist/{file-utils-D1632P4x.js.map → file-utils-ALRqLr0x.js.map} +1 -1
- package/dist/{interactive-DCIL3NcQ.js → interactive-ChaxKwhe.js} +2 -2
- package/dist/{interactive-DCIL3NcQ.js.map → interactive-ChaxKwhe.js.map} +1 -1
- package/dist/{mcp-prompt-streaming-BRoVSf3N.js → mcp-prompt-streaming-CNmYvvmk.js} +4 -4
- package/dist/{mcp-prompt-streaming-BRoVSf3N.js.map → mcp-prompt-streaming-CNmYvvmk.js.map} +1 -1
- package/dist/{non-interactive-u4VG76Vi.js → non-interactive-BM4hUmlI.js} +2 -2
- package/dist/{non-interactive-u4VG76Vi.js.map → non-interactive-BM4hUmlI.js.map} +1 -1
- package/dist/{package-manager-0M_uIOP0.js → package-manager-l8N6VCPX.js} +2 -2
- package/dist/{package-manager-0M_uIOP0.js.map → package-manager-l8N6VCPX.js.map} +1 -1
- package/dist/{playground-B6wgUvH-.js → playground-C7SbDVI4.js} +4 -4
- package/dist/{playground-B6wgUvH-.js.map → playground-C7SbDVI4.js.map} +1 -1
- package/dist/{posthog-integration-C_9G_kTS.js → posthog-integration-YDzQBfhq.js} +13 -14
- package/dist/{posthog-integration-C_9G_kTS.js.map → posthog-integration-YDzQBfhq.js.map} +1 -1
- package/dist/{provisioning-CgCxuoe6.js → provisioning-ql6mjOVq.js} +3 -3
- package/dist/{provisioning-CgCxuoe6.js.map → provisioning-ql6mjOVq.js.map} +1 -1
- package/dist/{registry-BgsYtCkS.js → registry-5SphnyxS.js} +4 -4
- package/dist/{registry-BgsYtCkS.js.map → registry-5SphnyxS.js.map} +1 -1
- package/dist/{setup-utils-DF6EKEeA.js → setup-utils-CoblNeRY.js} +8 -8
- package/dist/{setup-utils-DF6EKEeA.js.map → setup-utils-CoblNeRY.js.map} +1 -1
- package/dist/{start-tui-Deaj99It.js → start-tui-D_woOYMc.js} +13 -13
- package/dist/{start-tui-Deaj99It.js.map → start-tui-D_woOYMc.js.map} +1 -1
- package/dist/{steps-AF3ulYYe.js → steps-2gR__rtG.js} +7 -7
- package/dist/{steps-AF3ulYYe.js.map → steps-2gR__rtG.js.map} +1 -1
- package/dist/{telemetry-DPVvKu5X.js → telemetry-CqysQT5U.js} +3 -3
- package/dist/{telemetry-DPVvKu5X.js.map → telemetry-CqysQT5U.js.map} +1 -1
- package/dist/{terminal-BVKeWPb3.js → terminal-CeokeMGP.js} +9 -9
- package/dist/{terminal-BVKeWPb3.js.map → terminal-CeokeMGP.js.map} +1 -1
- package/dist/{urls-Bur7Zb7A.js → urls-BOcViDhS.js} +2 -2
- package/dist/{urls-Bur7Zb7A.js.map → urls-BOcViDhS.js.map} +1 -1
- package/dist/{wizard-abort-D3vY7K9a.js → wizard-abort-C0siBgn5.js} +1 -1
- package/dist/{wizard-abort-D0UMhCP5.js → wizard-abort-DFL5Um-M.js} +3 -3
- package/dist/{wizard-abort-D0UMhCP5.js.map → wizard-abort-DFL5Um-M.js.map} +1 -1
- package/package.json +16 -52
- package/dist/agent-interface-CVW8H9eF.js.map +0 -1
- package/dist/agent-runner-VzTpPaVT.js.map +0 -1
- package/dist/analytics-DvDjbNmK.js.map +0 -1
- package/dist/debug-CMZ7kqW1.js.map +0 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { n as __require } from "./rolldown-runtime-B_-DWIq7.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as analytics } from "./analytics-
|
|
4
|
-
import { i as getLlmGatewayUrlFromHost } from "./urls-
|
|
2
|
+
import { J as WIZARD_ORCHESTRATOR_FLAG_KEY, L as POSTHOG_FLAG_HEADER_PREFIX, Q as WIZARD_USER_AGENT, V as POSTHOG_PROPERTY_HEADER_PREFIX, X as WIZARD_REMARK_EVENT_NAME, a as getLogFilePath, f as skillTmpPath, o as initLogFile, p as getUI, r as debug, rt as runtimeEnv, s as logToFile, u as WIZARD_YARA_REPORT_FILE } from "./debug-n42RObru.js";
|
|
3
|
+
import { t as analytics } from "./analytics-3GR9OyE9.js";
|
|
4
|
+
import { i as getLlmGatewayUrlFromHost } from "./urls-BOcViDhS.js";
|
|
5
5
|
import { n as ADDITIONAL_FEATURE_PROMPTS } from "./wizard-session-G3VWD6hv.js";
|
|
6
|
-
import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-
|
|
6
|
+
import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-DFL5Um-M.js";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import * as fs$1 from "fs";
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import * as path$1 from "path";
|
|
11
11
|
import path from "path";
|
|
12
|
+
import axios from "axios";
|
|
12
13
|
import { z } from "zod";
|
|
13
14
|
import fg from "fast-glob";
|
|
14
15
|
import { execFileSync } from "child_process";
|
|
@@ -1210,253 +1211,6 @@ const LINTING_TOOLS = [
|
|
|
1210
1211
|
"yamllint"
|
|
1211
1212
|
];
|
|
1212
1213
|
//#endregion
|
|
1213
|
-
//#region src/lib/yara-scanner.ts
|
|
1214
|
-
const POST_WRITE_EDIT = [{
|
|
1215
|
-
phase: "PostToolUse",
|
|
1216
|
-
tool: "Write"
|
|
1217
|
-
}, {
|
|
1218
|
-
phase: "PostToolUse",
|
|
1219
|
-
tool: "Edit"
|
|
1220
|
-
}];
|
|
1221
|
-
const POST_READ_GREP = [{
|
|
1222
|
-
phase: "PostToolUse",
|
|
1223
|
-
tool: "Read"
|
|
1224
|
-
}, {
|
|
1225
|
-
phase: "PostToolUse",
|
|
1226
|
-
tool: "Grep"
|
|
1227
|
-
}];
|
|
1228
|
-
const PRE_BASH = [{
|
|
1229
|
-
phase: "PreToolUse",
|
|
1230
|
-
tool: "Bash"
|
|
1231
|
-
}];
|
|
1232
|
-
const RULES = [
|
|
1233
|
-
{
|
|
1234
|
-
name: "pii_in_capture_call",
|
|
1235
|
-
description: "Detects PII fields passed to posthog.capture() — violates 'NEVER send PII in capture()' commandment",
|
|
1236
|
-
severity: "high",
|
|
1237
|
-
category: "posthog_pii",
|
|
1238
|
-
appliesTo: POST_WRITE_EDIT,
|
|
1239
|
-
patterns: [
|
|
1240
|
-
/\.capture\s*\([^)]{0,200}email/i,
|
|
1241
|
-
/\.capture\s*\([^)]{0,200}phone/i,
|
|
1242
|
-
/\.capture\s*\([^)]{0,200}full[_\s]?name/i,
|
|
1243
|
-
/\.capture\s*\([^)]{0,200}first[_\s]?name/i,
|
|
1244
|
-
/\.capture\s*\([^)]{0,200}last[_\s]?name/i,
|
|
1245
|
-
/\.capture\s*\([^)]{0,200}(street|mailing|home|billing)[_\s]?address/i,
|
|
1246
|
-
/\.capture\s*\([^)]{0,200}(ssn|social[_\s]?security)/i,
|
|
1247
|
-
/\.capture\s*\([^)]{0,200}(date[_\s]?of[_\s]?birth|dob|birthday)/i,
|
|
1248
|
-
/\.capture\s*\([^)]{0,200}\$ip/,
|
|
1249
|
-
/\.identify\s*\([^)]{0,200}(ssn|social[_\s]?security)/i,
|
|
1250
|
-
/\.identify\s*\([^)]{0,200}(card[_\s]?number|cvv|credit[_\s]?card)/i,
|
|
1251
|
-
/\.identify\s*\([^)]{0,200}(date[_\s]?of[_\s]?birth|dob|birthday)/i,
|
|
1252
|
-
/\.identify\s*\([^)]{0,200}(street|mailing|home|billing)[_\s]?address/i,
|
|
1253
|
-
/\$set[^}]{0,200}email/i,
|
|
1254
|
-
/\$set[^}]{0,200}phone/i
|
|
1255
|
-
]
|
|
1256
|
-
},
|
|
1257
|
-
{
|
|
1258
|
-
name: "hardcoded_posthog_key",
|
|
1259
|
-
description: "Detects hardcoded PostHog API keys in source — violates 'use environment variables' commandment",
|
|
1260
|
-
severity: "high",
|
|
1261
|
-
category: "posthog_hardcoded_key",
|
|
1262
|
-
appliesTo: POST_WRITE_EDIT,
|
|
1263
|
-
patterns: [
|
|
1264
|
-
/phc_[a-zA-Z0-9]{20,}/,
|
|
1265
|
-
/phx_[a-zA-Z0-9]{20,}/,
|
|
1266
|
-
/apiKey\s*[:=]\s*['"][a-zA-Z0-9_]{20,}['"]/,
|
|
1267
|
-
/api_key\s*[:=]\s*['"][a-zA-Z0-9_]{20,}['"]/,
|
|
1268
|
-
/POSTHOG_PROJECT_TOKEN\s*[:=]\s*['"][a-zA-Z0-9_]{20,}['"]/
|
|
1269
|
-
]
|
|
1270
|
-
},
|
|
1271
|
-
{
|
|
1272
|
-
name: "autocapture_disabled",
|
|
1273
|
-
description: "Detects agent disabling autocapture — violates 'don't disable autocapture' commandment",
|
|
1274
|
-
severity: "medium",
|
|
1275
|
-
category: "posthog_autocapture",
|
|
1276
|
-
appliesTo: POST_WRITE_EDIT,
|
|
1277
|
-
patterns: [
|
|
1278
|
-
/autocapture\s*:\s*false/,
|
|
1279
|
-
/autocapture\s*:\s*'false'/,
|
|
1280
|
-
/autocapture\s*:\s*"false"/,
|
|
1281
|
-
/autocapture\s*=\s*False/,
|
|
1282
|
-
/disable_autocapture\s*[:=]\s*(true|True|1)/
|
|
1283
|
-
]
|
|
1284
|
-
},
|
|
1285
|
-
{
|
|
1286
|
-
name: "hardcoded_posthog_host",
|
|
1287
|
-
description: "Detects hardcoded PostHog host URLs in source — should use environment variables",
|
|
1288
|
-
severity: "high",
|
|
1289
|
-
category: "posthog_hardcoded_key",
|
|
1290
|
-
appliesTo: POST_WRITE_EDIT,
|
|
1291
|
-
patterns: [/['"]https:\/\/(us|eu)\.i\.posthog\.com['"]/]
|
|
1292
|
-
},
|
|
1293
|
-
{
|
|
1294
|
-
name: "session_recording_disabled",
|
|
1295
|
-
description: "Detects agent disabling session recording",
|
|
1296
|
-
severity: "medium",
|
|
1297
|
-
category: "posthog_config",
|
|
1298
|
-
appliesTo: POST_WRITE_EDIT,
|
|
1299
|
-
patterns: [/disable_session_recording\s*:\s*true/i, /disable_session_recording\s*=\s*True/]
|
|
1300
|
-
},
|
|
1301
|
-
{
|
|
1302
|
-
name: "opt_out_capturing",
|
|
1303
|
-
description: "Detects agent opting out of PostHog capturing entirely",
|
|
1304
|
-
severity: "medium",
|
|
1305
|
-
category: "posthog_config",
|
|
1306
|
-
appliesTo: POST_WRITE_EDIT,
|
|
1307
|
-
patterns: [/opt_out_capturing\s*[:=]\s*(true|True|1)/i, /opted_out\s*[:=]\s*(true|True|1)/i]
|
|
1308
|
-
},
|
|
1309
|
-
{
|
|
1310
|
-
name: "prompt_injection_wizard_override",
|
|
1311
|
-
description: "Detects classic prompt injection attempting to override agent instructions",
|
|
1312
|
-
severity: "critical",
|
|
1313
|
-
category: "prompt_injection",
|
|
1314
|
-
appliesTo: POST_READ_GREP,
|
|
1315
|
-
patterns: [
|
|
1316
|
-
/ignore previous instructions/i,
|
|
1317
|
-
/disregard all/i,
|
|
1318
|
-
/forget your instructions/i,
|
|
1319
|
-
/override your rules/i,
|
|
1320
|
-
/act as a different/i,
|
|
1321
|
-
/new instructions:/i
|
|
1322
|
-
]
|
|
1323
|
-
},
|
|
1324
|
-
{
|
|
1325
|
-
name: "prompt_injection_wizard_specific",
|
|
1326
|
-
description: "Detects wizard-specific manipulation or tool abuse attempts in project files",
|
|
1327
|
-
severity: "medium",
|
|
1328
|
-
category: "prompt_injection",
|
|
1329
|
-
appliesTo: POST_READ_GREP,
|
|
1330
|
-
patterns: [
|
|
1331
|
-
/skip posthog/i,
|
|
1332
|
-
/do not install posthog/i,
|
|
1333
|
-
/remove posthog/i,
|
|
1334
|
-
/uninstall posthog/i,
|
|
1335
|
-
/delete the posthog/i,
|
|
1336
|
-
/run the following command/i,
|
|
1337
|
-
/execute this shell command/i,
|
|
1338
|
-
/you are now a\s/i
|
|
1339
|
-
]
|
|
1340
|
-
},
|
|
1341
|
-
{
|
|
1342
|
-
name: "prompt_injection_base64",
|
|
1343
|
-
description: "Detects suspicious base64-encoded blocks in file content that may contain obfuscated prompt injection",
|
|
1344
|
-
severity: "critical",
|
|
1345
|
-
category: "prompt_injection",
|
|
1346
|
-
appliesTo: POST_READ_GREP,
|
|
1347
|
-
patterns: [/(?:\/\/|#|\/\*)\s*[A-Za-z0-9+/]{100,}={0,2}/]
|
|
1348
|
-
},
|
|
1349
|
-
{
|
|
1350
|
-
name: "secret_exfiltration_via_command",
|
|
1351
|
-
description: "Detects shell commands attempting to exfiltrate secrets or credentials",
|
|
1352
|
-
severity: "critical",
|
|
1353
|
-
category: "exfiltration",
|
|
1354
|
-
appliesTo: PRE_BASH,
|
|
1355
|
-
patterns: [
|
|
1356
|
-
/curl\s+.*\$\{?[A-Z_]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/i,
|
|
1357
|
-
/wget\s+.*\$\{?[A-Z_]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/i,
|
|
1358
|
-
/(\$\{?[A-Z_]*(KEY|TOKEN|SECRET|PASSWORD)|\.env|credentials)\S*.*\|\s*curl/i,
|
|
1359
|
-
/(\$\{?[A-Z_]*(KEY|TOKEN|SECRET|PASSWORD)|\.env|credentials)\S*.*\|\s*wget/i,
|
|
1360
|
-
/\|\s*nc\s/,
|
|
1361
|
-
/\|\s*netcat\s/,
|
|
1362
|
-
/base64.*\|\s*(curl|wget|nc\s)/i,
|
|
1363
|
-
/cat\s+.*\.env.*\|\s*(curl|wget)/,
|
|
1364
|
-
/curl.*phc_[a-zA-Z0-9]/,
|
|
1365
|
-
/wget.*phc_[a-zA-Z0-9]/
|
|
1366
|
-
]
|
|
1367
|
-
},
|
|
1368
|
-
{
|
|
1369
|
-
name: "destructive_rm",
|
|
1370
|
-
description: "Detects rm -rf or rm -r commands that could mass-delete files",
|
|
1371
|
-
severity: "critical",
|
|
1372
|
-
category: "filesystem_safety",
|
|
1373
|
-
appliesTo: PRE_BASH,
|
|
1374
|
-
patterns: [
|
|
1375
|
-
/\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)\b/,
|
|
1376
|
-
/\brm\s+(-[a-zA-Z]*\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+(-[a-zA-Z]*\s+)*-[a-zA-Z]*f\b/,
|
|
1377
|
-
/\brm\s+(-[a-zA-Z]*\s+)*-[a-zA-Z]*f[a-zA-Z]*\s+(-[a-zA-Z]*\s+)*-[a-zA-Z]*r\b/
|
|
1378
|
-
]
|
|
1379
|
-
},
|
|
1380
|
-
{
|
|
1381
|
-
name: "git_force_push",
|
|
1382
|
-
description: "Detects git push --force which can overwrite remote history",
|
|
1383
|
-
severity: "critical",
|
|
1384
|
-
category: "filesystem_safety",
|
|
1385
|
-
appliesTo: PRE_BASH,
|
|
1386
|
-
patterns: [/git\s+push\s+.*--force/, /git\s+push\s+.*-f\b/]
|
|
1387
|
-
},
|
|
1388
|
-
{
|
|
1389
|
-
name: "git_reset_hard",
|
|
1390
|
-
description: "Detects git reset --hard which discards all uncommitted changes",
|
|
1391
|
-
severity: "critical",
|
|
1392
|
-
category: "filesystem_safety",
|
|
1393
|
-
appliesTo: PRE_BASH,
|
|
1394
|
-
patterns: [/git\s+reset\s+--hard/]
|
|
1395
|
-
},
|
|
1396
|
-
{
|
|
1397
|
-
name: "wrong_posthog_package",
|
|
1398
|
-
description: "Detects installing the wrong PostHog npm package — should be posthog-js or posthog-node",
|
|
1399
|
-
severity: "high",
|
|
1400
|
-
category: "supply_chain",
|
|
1401
|
-
appliesTo: PRE_BASH,
|
|
1402
|
-
patterns: [
|
|
1403
|
-
/npm\s+install\s+(?:--save\s+|--save-dev\s+|-[SD]\s+)*posthog(?!\s*-)/,
|
|
1404
|
-
/pnpm\s+(?:add|install)\s+(?:--save\s+|--save-dev\s+|-[SD]\s+)*posthog(?!\s*-)/,
|
|
1405
|
-
/yarn\s+add\s+(?:--dev\s+|-D\s+)*posthog(?!\s*-)/,
|
|
1406
|
-
/bun\s+(?:add|install)\s+(?:--dev\s+|-[dD]\s+)*posthog(?!\s*-)/
|
|
1407
|
-
]
|
|
1408
|
-
},
|
|
1409
|
-
{
|
|
1410
|
-
name: "npm_install_global",
|
|
1411
|
-
description: "Detects global npm installs — should never install packages globally",
|
|
1412
|
-
severity: "high",
|
|
1413
|
-
category: "supply_chain",
|
|
1414
|
-
appliesTo: PRE_BASH,
|
|
1415
|
-
patterns: [/npm\s+install\s+-g\b/, /npm\s+install\s+--global\b/]
|
|
1416
|
-
}
|
|
1417
|
-
];
|
|
1418
|
-
/** Maximum content length to scan (100 KB). Inputs beyond this are truncated. */
|
|
1419
|
-
const MAX_SCAN_LENGTH = 1e5;
|
|
1420
|
-
/**
|
|
1421
|
-
* Scan content against rules applicable to a given hook phase and tool.
|
|
1422
|
-
* Returns all matching rules (one match per rule, first pattern wins).
|
|
1423
|
-
*/
|
|
1424
|
-
function scan(content, phase, tool) {
|
|
1425
|
-
const scanContent = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
1426
|
-
const applicableRules = RULES.filter((r) => r.appliesTo.some((a) => a.phase === phase && a.tool === tool));
|
|
1427
|
-
const matches = [];
|
|
1428
|
-
for (const rule of applicableRules) for (const pattern of rule.patterns) {
|
|
1429
|
-
const match = pattern.exec(scanContent);
|
|
1430
|
-
if (match) {
|
|
1431
|
-
matches.push({
|
|
1432
|
-
rule,
|
|
1433
|
-
matchedText: match[0],
|
|
1434
|
-
offset: match.index
|
|
1435
|
-
});
|
|
1436
|
-
break;
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
return matches.length > 0 ? {
|
|
1440
|
-
matched: true,
|
|
1441
|
-
matches
|
|
1442
|
-
} : { matched: false };
|
|
1443
|
-
}
|
|
1444
|
-
/**
|
|
1445
|
-
* Scan all files in a skill directory for prompt injection.
|
|
1446
|
-
* Used for context-mill scanning after skill installation.
|
|
1447
|
-
*/
|
|
1448
|
-
function scanSkillDirectory(files) {
|
|
1449
|
-
const allMatches = [];
|
|
1450
|
-
for (const file of files) {
|
|
1451
|
-
const result = scan(file.content, "PostToolUse", "Read");
|
|
1452
|
-
if (result.matched) allMatches.push(...result.matches);
|
|
1453
|
-
}
|
|
1454
|
-
return allMatches.length > 0 ? {
|
|
1455
|
-
matched: true,
|
|
1456
|
-
matches: allMatches
|
|
1457
|
-
} : { matched: false };
|
|
1458
|
-
}
|
|
1459
|
-
//#endregion
|
|
1460
1214
|
//#region src/lib/skill-install.ts
|
|
1461
1215
|
/**
|
|
1462
1216
|
* Check if command is a PostHog skill installation from MCP.
|
|
@@ -1475,26 +1229,98 @@ function isSkillInstallCommand(command) {
|
|
|
1475
1229
|
return url.startsWith("https://github.com/PostHog/context-mill/releases/") || /^http:\/\/localhost:\d+\//.test(url);
|
|
1476
1230
|
}
|
|
1477
1231
|
//#endregion
|
|
1232
|
+
//#region src/lib/programs/events-audit/constants.ts
|
|
1233
|
+
/**
|
|
1234
|
+
* Leaf-level constants for the events-audit program.
|
|
1235
|
+
*
|
|
1236
|
+
* Kept separate from `index.ts` so files like `yara-hooks.ts` can import
|
|
1237
|
+
* the filename constants without dragging in `index.ts`'s heavier imports
|
|
1238
|
+
* (agent-runner, audit/seed, etc.) — which can create import cycles.
|
|
1239
|
+
*/
|
|
1240
|
+
const SETUP_REPORT_FILE = "posthog-events-audit-report.md";
|
|
1241
|
+
const EVENT_INVENTORY_FILE = ".posthog-events-inventory.json";
|
|
1242
|
+
/** Per-part filename pattern emitted by events-audit subagents (e.g. `.posthog-events-inventory.part-3.json`). */
|
|
1243
|
+
const EVENT_INVENTORY_PART_PATTERN = /^\.posthog-events-inventory\.part-\d+\.json$/;
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region src/lib/programs/posthog-integration/constants.ts
|
|
1246
|
+
/**
|
|
1247
|
+
* Leaf-level constants for the posthog-integration program.
|
|
1248
|
+
*
|
|
1249
|
+
* Kept separate from `index.ts` so files like `yara-hooks.ts` can import
|
|
1250
|
+
* the filename constants without dragging in `index.ts`'s heavier imports
|
|
1251
|
+
* (agent-interface, framework-config, etc.) — which would create an import
|
|
1252
|
+
* cycle through agent-interface → yara-hooks.
|
|
1253
|
+
*/
|
|
1254
|
+
const EVENT_PLAN_FILE = ".posthog-events.json";
|
|
1255
|
+
//#endregion
|
|
1478
1256
|
//#region src/lib/yara-hooks.ts
|
|
1479
1257
|
/**
|
|
1480
1258
|
* YARA hook wiring for the Claude Agent SDK.
|
|
1481
1259
|
*
|
|
1482
|
-
* Creates PreToolUse and PostToolUse hook callback arrays that
|
|
1483
|
-
*
|
|
1484
|
-
* hooks are registered in the SDK's query() options alongside
|
|
1485
|
-
* existing Stop hook.
|
|
1260
|
+
* Creates PreToolUse and PostToolUse hook callback arrays that integrate the
|
|
1261
|
+
* real @posthog/warlock security scanner (YARA-X via WASM) into the wizard's
|
|
1262
|
+
* agent loop. These hooks are registered in the SDK's query() options alongside
|
|
1263
|
+
* the existing Stop hook.
|
|
1264
|
+
*
|
|
1265
|
+
* PreToolUse hooks block dangerous commands before execution. PostToolUse hooks
|
|
1266
|
+
* detect violations in written code and prompt injection in read content, and
|
|
1267
|
+
* scan context-mill skill downloads.
|
|
1268
|
+
*
|
|
1269
|
+
* Warlock owns the rules; this file owns the *policy* — how a match maps to a
|
|
1270
|
+
* block / revert / terminate response, plus the optional LLM triage pass that
|
|
1271
|
+
* filters false positives before we act.
|
|
1486
1272
|
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1273
|
+
* Naming: "yara" is the technique — scanning content against YARA rules — so the
|
|
1274
|
+
* wizard-side wiring keeps that name. "warlock" is the engine package that runs
|
|
1275
|
+
* the rules. Both names showing up here is intentional.
|
|
1276
|
+
*
|
|
1277
|
+
* This is Layer 2 (L2) in the wizard's defense-in-depth model, complementing the
|
|
1278
|
+
* prompt-based commandments (L0) and the canUseTool() allowlist (L1).
|
|
1279
|
+
*/
|
|
1280
|
+
let warlockModulePromise = null;
|
|
1281
|
+
function getWarlock() {
|
|
1282
|
+
if (!warlockModulePromise) warlockModulePromise = import("@posthog/warlock").catch((err) => {
|
|
1283
|
+
warlockModulePromise = null;
|
|
1284
|
+
throw err;
|
|
1285
|
+
});
|
|
1286
|
+
return warlockModulePromise;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Pay the WASM-init + rule-compile cost up front, off the hook path, so the
|
|
1290
|
+
* first real tool-call scan doesn't eat cold-start under a hook timeout.
|
|
1291
|
+
* Best-effort: a failure here is non-fatal — hooks still fail closed per scan.
|
|
1490
1292
|
*/
|
|
1293
|
+
async function prewarmYaraScanner() {
|
|
1294
|
+
try {
|
|
1295
|
+
await (await getWarlock()).scan("");
|
|
1296
|
+
logToFile("[YARA] warlock pre-warmed");
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
logToFile("[YARA] warlock pre-warm failed:", err);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1491
1301
|
let scanCount = 0;
|
|
1492
1302
|
const scanViolations = [];
|
|
1493
1303
|
function recordScan() {
|
|
1494
1304
|
scanCount++;
|
|
1495
1305
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1306
|
+
/**
|
|
1307
|
+
* Log a match to the run log + PostHog, and record it in the run's scan report.
|
|
1308
|
+
* Single entry point so every violation is reported consistently.
|
|
1309
|
+
*/
|
|
1310
|
+
function recordMatch(phase, tool, match, action) {
|
|
1311
|
+
const triage = match.triage;
|
|
1312
|
+
logYaraMatch(phase, tool, match, action);
|
|
1313
|
+
scanViolations.push({
|
|
1314
|
+
rule: match.rule,
|
|
1315
|
+
severity: match.metadata.severity ?? "unknown",
|
|
1316
|
+
category: match.metadata.category ?? "unknown",
|
|
1317
|
+
action,
|
|
1318
|
+
phase,
|
|
1319
|
+
tool,
|
|
1320
|
+
description: match.metadata.description ?? "",
|
|
1321
|
+
triageVerdict: triage?.verdict,
|
|
1322
|
+
triageReason: triage?.reason
|
|
1323
|
+
});
|
|
1498
1324
|
}
|
|
1499
1325
|
/** Format the scan report summary. Returns null if no scans occurred */
|
|
1500
1326
|
function formatScanReport() {
|
|
@@ -1536,28 +1362,96 @@ function writeScanReport() {
|
|
|
1536
1362
|
}
|
|
1537
1363
|
return WIZARD_YARA_REPORT_FILE;
|
|
1538
1364
|
}
|
|
1539
|
-
/** Timeout for
|
|
1540
|
-
const HOOK_TIMEOUT_MS =
|
|
1541
|
-
/** Timeout for skill install hook (
|
|
1542
|
-
const SKILL_SCAN_HOOK_TIMEOUT_MS =
|
|
1365
|
+
/** Timeout for scan hooks (PreToolUse, PostToolUse Write/Edit/Read/Grep) */
|
|
1366
|
+
const HOOK_TIMEOUT_MS = 3e4;
|
|
1367
|
+
/** Timeout for the skill install hook (filesystem I/O + multiple scans) */
|
|
1368
|
+
const SKILL_SCAN_HOOK_TIMEOUT_MS = 3e4;
|
|
1369
|
+
/**
|
|
1370
|
+
* Chunk size for scanning (100 KB). Oversized content is scanned in
|
|
1371
|
+
* overlapping chunks so coverage is complete — nothing is silently truncated.
|
|
1372
|
+
* The size also bounds what a single triage call pastes into the LLM prompt
|
|
1373
|
+
* (~25K tokens), so triage always sees the chunk its matches came from.
|
|
1374
|
+
*/
|
|
1375
|
+
const SCAN_CHUNK_SIZE = 1e5;
|
|
1376
|
+
/**
|
|
1377
|
+
* Overlap between adjacent chunks so a pattern straddling a chunk boundary
|
|
1378
|
+
* still lands whole inside at least one chunk. YARA rule strings are at most
|
|
1379
|
+
* a few hundred bytes; 4 KB is generous.
|
|
1380
|
+
*/
|
|
1381
|
+
const SCAN_CHUNK_OVERLAP = 4096;
|
|
1543
1382
|
function logYaraMatch(phase, tool, match, action) {
|
|
1544
|
-
|
|
1383
|
+
const severity = match.metadata.severity ?? "unknown";
|
|
1384
|
+
const category = match.metadata.category ?? "unknown";
|
|
1385
|
+
const ruleAction = match.metadata.action ?? "unknown";
|
|
1386
|
+
const description = match.metadata.description ?? "";
|
|
1387
|
+
logToFile(`[YARA] ${phase}:${tool} [${action.toUpperCase()}] rule "${match.rule}" (severity: ${severity}, category: ${category}, action: ${ruleAction})\n Description: ${description}`);
|
|
1545
1388
|
analytics.wizardCapture("yara rule matched", {
|
|
1546
|
-
rule: match.rule
|
|
1547
|
-
severity: match.
|
|
1548
|
-
category: match.
|
|
1389
|
+
rule: match.rule,
|
|
1390
|
+
severity: match.metadata.severity,
|
|
1391
|
+
category: match.metadata.category,
|
|
1392
|
+
rule_action: match.metadata.action,
|
|
1549
1393
|
action,
|
|
1550
1394
|
phase,
|
|
1551
|
-
tool
|
|
1395
|
+
tool,
|
|
1396
|
+
description: match.metadata.description,
|
|
1397
|
+
triage_verdict: match.triage?.verdict
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Send the run's full scan report to PostHog so maintainers can see why a run
|
|
1402
|
+
* was flagged, warned, or aborted. This is the maintainer-facing report — it is
|
|
1403
|
+
* NEVER shown to the user. No-op if nothing was scanned. The free-text triage
|
|
1404
|
+
* reason is omitted to avoid sending scanned content off the user's machine.
|
|
1405
|
+
*/
|
|
1406
|
+
function captureScanReport() {
|
|
1407
|
+
if (scanCount === 0) return;
|
|
1408
|
+
analytics.wizardCapture("yara scan report", {
|
|
1409
|
+
total_scans: scanCount,
|
|
1410
|
+
violation_count: scanViolations.length,
|
|
1411
|
+
clean_count: scanCount - scanViolations.length,
|
|
1412
|
+
violations: scanViolations.map((v) => ({
|
|
1413
|
+
rule: v.rule,
|
|
1414
|
+
severity: v.severity,
|
|
1415
|
+
category: v.category,
|
|
1416
|
+
action: v.action,
|
|
1417
|
+
phase: v.phase,
|
|
1418
|
+
tool: v.tool,
|
|
1419
|
+
description: v.description,
|
|
1420
|
+
triage_verdict: v.triageVerdict
|
|
1421
|
+
}))
|
|
1552
1422
|
});
|
|
1423
|
+
scanCount = 0;
|
|
1424
|
+
scanViolations.length = 0;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* End-of-run flush for the scan report. Wired once at the runner seam so every
|
|
1428
|
+
* harness (linear, orchestrator, and any future shape) reports without having
|
|
1429
|
+
* to know warlock exists — scanning is already harness-agnostic (it runs from
|
|
1430
|
+
* SDK Pre/PostToolUse hooks), and this keeps reporting at a single shared seam.
|
|
1431
|
+
*
|
|
1432
|
+
* Order matters: write the local --yara-report file FIRST (it reads scanCount /
|
|
1433
|
+
* scanViolations), THEN send telemetry. captureScanReport() zeroes scan state,
|
|
1434
|
+
* which is what makes this whole function idempotent — a second call from
|
|
1435
|
+
* another termination path (e.g. finally after an abort already flushed) finds
|
|
1436
|
+
* scanCount === 0 and every step no-ops.
|
|
1437
|
+
*/
|
|
1438
|
+
function flushScanReport(session) {
|
|
1439
|
+
if (session.yaraReport) {
|
|
1440
|
+
const reportPath = writeScanReport();
|
|
1441
|
+
if (reportPath) {
|
|
1442
|
+
const summary = formatScanReport();
|
|
1443
|
+
getUI().log.info(`YARA scan report: ${reportPath}${summary ?? ""}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
captureScanReport();
|
|
1553
1447
|
}
|
|
1554
1448
|
const WIZARD_DOC_BASENAMES = new Set([
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1449
|
+
EVENT_INVENTORY_FILE,
|
|
1450
|
+
SETUP_REPORT_FILE,
|
|
1451
|
+
AUDIT_REPORT_FILE,
|
|
1452
|
+
EVENT_PLAN_FILE
|
|
1559
1453
|
]);
|
|
1560
|
-
const WIZARD_DOC_PATTERNS = [
|
|
1454
|
+
const WIZARD_DOC_PATTERNS = [EVENT_INVENTORY_PART_PATTERN];
|
|
1561
1455
|
function isWizardDocumentationPath(filePath) {
|
|
1562
1456
|
if (!filePath) return false;
|
|
1563
1457
|
const basename = path.basename(filePath);
|
|
@@ -1572,43 +1466,150 @@ const SEVERITY_RANK = {
|
|
|
1572
1466
|
};
|
|
1573
1467
|
/** Return the highest-severity match from a list of matches. */
|
|
1574
1468
|
function highestSeverityMatch(matches) {
|
|
1575
|
-
return matches.reduce((worst, m) => (SEVERITY_RANK[m.
|
|
1469
|
+
return matches.reduce((worst, m) => (SEVERITY_RANK[m.metadata.severity ?? ""] ?? 0) > (SEVERITY_RANK[worst.metadata.severity ?? ""] ?? 0) ? m : worst);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Keep only matches whose rule targets this content surface. Rules carry a
|
|
1473
|
+
* `scan_context` of 'command' | 'input' | 'output'. An undefined context is
|
|
1474
|
+
* treated as "applies everywhere" (fail-safe — a rule that forgets the tag
|
|
1475
|
+
* still gets enforced rather than silently skipped).
|
|
1476
|
+
*/
|
|
1477
|
+
function matchesForContext(matches, ctx) {
|
|
1478
|
+
return matches.filter((m) => {
|
|
1479
|
+
const c = m.metadata.scan_context;
|
|
1480
|
+
return c === ctx || c === void 0;
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Drop false positives via warlock's LLM triage. Fail-closed: if no provider is
|
|
1485
|
+
* available, or the triage call throws, every match is treated as real — we
|
|
1486
|
+
* never silently suppress a flagged match.
|
|
1487
|
+
*
|
|
1488
|
+
* Every overruled match is reported to PostHog (rule metadata only, never the
|
|
1489
|
+
* free-text reason) so maintainers can alert on triage-overrule patterns — the
|
|
1490
|
+
* signal that someone is either tripping a noisy rule or trying to talk the
|
|
1491
|
+
* triage model out of a real finding.
|
|
1492
|
+
*/
|
|
1493
|
+
async function triageFilter(content, matches, ctx, llmProvider) {
|
|
1494
|
+
if (matches.length === 0) return [];
|
|
1495
|
+
if (!llmProvider) {
|
|
1496
|
+
logToFile(`[YARA] triage skipped (no provider) — treating ${matches.length} match(es) as real`);
|
|
1497
|
+
return matches;
|
|
1498
|
+
}
|
|
1499
|
+
try {
|
|
1500
|
+
const triaged = await (await getWarlock()).triageMatches(content, matches, llmProvider);
|
|
1501
|
+
const kept = triaged.filter((m) => m.triage.verdict === "true_positive");
|
|
1502
|
+
for (const m of triaged) {
|
|
1503
|
+
if (m.triage.verdict === "true_positive") continue;
|
|
1504
|
+
logToFile(`[YARA] triage overruled rule "${m.rule}" (${m.metadata.severity ?? "unknown"}) — not acting on it`);
|
|
1505
|
+
analytics.wizardCapture("yara triage overruled", {
|
|
1506
|
+
rule: m.rule,
|
|
1507
|
+
severity: m.metadata.severity,
|
|
1508
|
+
category: m.metadata.category,
|
|
1509
|
+
scan_context: ctx
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
logToFile(`[YARA] triage: ${matches.length} flagged → ${kept.length} kept as real`);
|
|
1513
|
+
return kept;
|
|
1514
|
+
} catch (err) {
|
|
1515
|
+
logToFile("[YARA] triage failed — treating all matches as real:", err);
|
|
1516
|
+
return matches;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Split oversized content into overlapping chunks so the scanner covers all
|
|
1521
|
+
* of it. Content that fits in one chunk is returned as-is.
|
|
1522
|
+
*/
|
|
1523
|
+
function chunkContent(content) {
|
|
1524
|
+
if (content.length <= SCAN_CHUNK_SIZE) return [content];
|
|
1525
|
+
const chunks = [];
|
|
1526
|
+
const step = SCAN_CHUNK_SIZE - SCAN_CHUNK_OVERLAP;
|
|
1527
|
+
for (let start = 0; start < content.length; start += step) {
|
|
1528
|
+
chunks.push(content.slice(start, start + SCAN_CHUNK_SIZE));
|
|
1529
|
+
if (start + SCAN_CHUNK_SIZE >= content.length) break;
|
|
1530
|
+
}
|
|
1531
|
+
return chunks;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Scan content against warlock rules — chunked when oversized, so nothing is
|
|
1535
|
+
* skipped — and keep only matches for this content surface. Chunks scan
|
|
1536
|
+
* sequentially (the WASM engine is single-threaded; parallelism buys nothing).
|
|
1537
|
+
* Returns each flagged chunk with its matches so triage can later judge every
|
|
1538
|
+
* match against the exact content it came from.
|
|
1539
|
+
*/
|
|
1540
|
+
async function scanForContext(content, ctx) {
|
|
1541
|
+
const warlock = await getWarlock();
|
|
1542
|
+
const chunks = chunkContent(content);
|
|
1543
|
+
if (chunks.length > 1) {
|
|
1544
|
+
logToFile(`[YARA] content is ${content.length} chars — scanning ${chunks.length} overlapping chunks`);
|
|
1545
|
+
analytics.wizardCapture("yara scan chunked", {
|
|
1546
|
+
content_length: content.length,
|
|
1547
|
+
chunk_count: chunks.length,
|
|
1548
|
+
scan_context: ctx
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
const flagged = [];
|
|
1552
|
+
for (const chunk of chunks) {
|
|
1553
|
+
const result = await warlock.scan(chunk);
|
|
1554
|
+
if (!result.matched) continue;
|
|
1555
|
+
const matches = matchesForContext(result.matches, ctx);
|
|
1556
|
+
if (matches.length === 0) continue;
|
|
1557
|
+
flagged.push({
|
|
1558
|
+
chunk,
|
|
1559
|
+
matches
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
return flagged;
|
|
1563
|
+
}
|
|
1564
|
+
/** The overlap between chunks can surface the same rule twice; count it once. */
|
|
1565
|
+
function dedupeByRule(matches) {
|
|
1566
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1567
|
+
return matches.filter((m) => {
|
|
1568
|
+
if (seen.has(m.rule)) return false;
|
|
1569
|
+
seen.add(m.rule);
|
|
1570
|
+
return true;
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Triage each flagged chunk against its own content — the evidence is always
|
|
1575
|
+
* inside the window the LLM sees. Triage calls run in parallel (each is just
|
|
1576
|
+
* an HTTP round trip) so N flagged chunks cost ~1× triage latency, not N×.
|
|
1577
|
+
*/
|
|
1578
|
+
async function triageFlagged(flagged, ctx, llmProvider) {
|
|
1579
|
+
return dedupeByRule((await Promise.all(flagged.map(({ chunk, matches }) => triageFilter(chunk, matches, ctx, llmProvider)))).flat());
|
|
1580
|
+
}
|
|
1581
|
+
/** Scan content, filter to the relevant context, triage. Returns real matches. */
|
|
1582
|
+
async function scanAndTriage(content, ctx, llmProvider) {
|
|
1583
|
+
return triageFlagged(await scanForContext(content, ctx), ctx, llmProvider);
|
|
1576
1584
|
}
|
|
1577
1585
|
/**
|
|
1578
1586
|
* Create PreToolUse hook matchers for YARA scanning.
|
|
1579
|
-
* Scans Bash commands before execution for exfiltration,
|
|
1580
|
-
*
|
|
1587
|
+
* Scans Bash commands before execution for exfiltration, destructive
|
|
1588
|
+
* operations, and supply chain violations ('command'-context rules).
|
|
1581
1589
|
*/
|
|
1582
|
-
function createPreToolUseYaraHooks() {
|
|
1590
|
+
function createPreToolUseYaraHooks(_llmProvider) {
|
|
1583
1591
|
return [{
|
|
1584
|
-
hooks: [(input) => {
|
|
1592
|
+
hooks: [async (input) => {
|
|
1585
1593
|
try {
|
|
1586
|
-
if (input.tool_name !== "Bash") return
|
|
1594
|
+
if (input.tool_name !== "Bash") return {};
|
|
1587
1595
|
const toolInput = input.tool_input;
|
|
1588
1596
|
const command = typeof toolInput?.command === "string" ? toolInput.command : "";
|
|
1589
|
-
if (!command) return
|
|
1597
|
+
if (!command) return {};
|
|
1590
1598
|
recordScan();
|
|
1591
|
-
const
|
|
1592
|
-
if (
|
|
1593
|
-
const match = highestSeverityMatch(
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
rule: match.rule.name,
|
|
1597
|
-
severity: match.rule.severity,
|
|
1598
|
-
action: "blocked",
|
|
1599
|
-
phase: "PreToolUse",
|
|
1600
|
-
tool: "Bash"
|
|
1601
|
-
});
|
|
1602
|
-
return Promise.resolve({
|
|
1599
|
+
const matches = await scanAndTriage(command, "command", void 0);
|
|
1600
|
+
if (matches.length === 0) return {};
|
|
1601
|
+
const match = highestSeverityMatch(matches);
|
|
1602
|
+
recordMatch("PreToolUse", "Bash", match, "blocked");
|
|
1603
|
+
return {
|
|
1603
1604
|
decision: "block",
|
|
1604
|
-
reason: `[YARA] ${match.rule
|
|
1605
|
-
}
|
|
1605
|
+
reason: `[YARA] ${match.rule}: ${match.metadata.description ?? "security policy violation"}. Command blocked for security.`
|
|
1606
|
+
};
|
|
1606
1607
|
} catch (error) {
|
|
1607
1608
|
logToFile("[YARA] PreToolUse hook error:", error);
|
|
1608
|
-
return
|
|
1609
|
+
return {
|
|
1609
1610
|
decision: "block",
|
|
1610
1611
|
reason: "[YARA] Scanner error — command blocked as a precaution."
|
|
1611
|
-
}
|
|
1612
|
+
};
|
|
1612
1613
|
}
|
|
1613
1614
|
}],
|
|
1614
1615
|
timeout: HOOK_TIMEOUT_MS
|
|
@@ -1618,95 +1619,84 @@ function createPreToolUseYaraHooks() {
|
|
|
1618
1619
|
* Create PostToolUse hook matchers for YARA scanning.
|
|
1619
1620
|
*
|
|
1620
1621
|
* Three matchers:
|
|
1621
|
-
* 1. Write/Edit — scan written content
|
|
1622
|
-
* 2. Read/Grep — scan read content
|
|
1622
|
+
* 1. Write/Edit — scan written content ('output'-context rules: PII, secrets)
|
|
1623
|
+
* 2. Read/Grep — scan read content ('input'-context rules: prompt injection)
|
|
1623
1624
|
* 3. Bash (skill install) — scan downloaded skill files for poisoned content
|
|
1625
|
+
*
|
|
1626
|
+
* `onTerminate` is invoked on the terminal paths (critical prompt injection in
|
|
1627
|
+
* read content, a poisoned skill, or a scanner error under fail-closed). It is
|
|
1628
|
+
* what ACTUALLY stops the run — returning `stopReason` from a PostToolUse hook
|
|
1629
|
+
* is not honored by the SDK, so the caller wires this to the run's
|
|
1630
|
+
* AbortController (see agent-interface).
|
|
1624
1631
|
*/
|
|
1625
|
-
function createPostToolUseYaraHooks() {
|
|
1632
|
+
function createPostToolUseYaraHooks(llmProvider, onTerminate) {
|
|
1626
1633
|
return [
|
|
1627
1634
|
{
|
|
1628
|
-
hooks: [(input) => {
|
|
1635
|
+
hooks: [async (input) => {
|
|
1629
1636
|
try {
|
|
1630
1637
|
const toolName = input.tool_name;
|
|
1631
|
-
if (toolName !== "Write" && toolName !== "Edit") return
|
|
1638
|
+
if (toolName !== "Write" && toolName !== "Edit") return {};
|
|
1632
1639
|
const toolInput = input.tool_input;
|
|
1633
|
-
const content = toolName === "Write" ? toolInput?.content ?? "" : toolInput?.
|
|
1634
|
-
if (!content) return
|
|
1640
|
+
const content = toolName === "Write" ? toolInput?.content ?? "" : toolInput?.new_string ?? "";
|
|
1641
|
+
if (!content) return {};
|
|
1635
1642
|
recordScan();
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
if (!result.matched) return Promise.resolve({});
|
|
1643
|
+
const flagged = await scanForContext(content, "output");
|
|
1644
|
+
if (flagged.length === 0) return {};
|
|
1639
1645
|
const filePath = toolInput?.file_path;
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1646
|
+
const activeFlagged = isWizardDocumentationPath(filePath) ? flagged.map(({ chunk, matches }) => ({
|
|
1647
|
+
chunk,
|
|
1648
|
+
matches: matches.filter((m) => m.metadata.category !== "posthog_pii")
|
|
1649
|
+
})).filter(({ matches }) => matches.length > 0) : flagged;
|
|
1650
|
+
if (activeFlagged.length === 0) {
|
|
1651
|
+
logToFile(`[YARA] posthog_pii match suppressed on wizard doc ${path.basename(filePath ?? "")} (rule: ${flagged[0]?.matches[0]?.rule})`);
|
|
1652
|
+
return {};
|
|
1647
1653
|
}
|
|
1648
|
-
const
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
action: "reverted",
|
|
1654
|
-
phase: "PostToolUse",
|
|
1655
|
-
tool
|
|
1656
|
-
});
|
|
1657
|
-
return Promise.resolve({ hookSpecificOutput: {
|
|
1654
|
+
const matches = await triageFlagged(activeFlagged, "output", llmProvider);
|
|
1655
|
+
if (matches.length === 0) return {};
|
|
1656
|
+
const match = highestSeverityMatch(matches);
|
|
1657
|
+
recordMatch("PostToolUse", toolName, match, "reverted");
|
|
1658
|
+
return { hookSpecificOutput: {
|
|
1658
1659
|
hookEventName: "PostToolUse",
|
|
1659
|
-
additionalContext: `[YARA VIOLATION] ${match.rule
|
|
1660
|
-
} }
|
|
1660
|
+
additionalContext: `[YARA VIOLATION] ${match.rule}: ${match.metadata.description ?? ""}. You MUST revert this change immediately. The content you just wrote violates security policy.`
|
|
1661
|
+
} };
|
|
1661
1662
|
} catch (error) {
|
|
1662
1663
|
logToFile("[YARA] PostToolUse Write/Edit hook error:", error);
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
} });
|
|
1664
|
+
const reason = "[YARA] Scanner error while scanning written content — session terminated as a precaution.";
|
|
1665
|
+
onTerminate(reason);
|
|
1666
|
+
return { stopReason: reason };
|
|
1667
1667
|
}
|
|
1668
1668
|
}],
|
|
1669
1669
|
timeout: HOOK_TIMEOUT_MS
|
|
1670
1670
|
},
|
|
1671
1671
|
{
|
|
1672
|
-
hooks: [(input) => {
|
|
1672
|
+
hooks: [async (input) => {
|
|
1673
1673
|
try {
|
|
1674
1674
|
const toolName = input.tool_name;
|
|
1675
|
-
if (toolName !== "Read" && toolName !== "Grep") return
|
|
1675
|
+
if (toolName !== "Read" && toolName !== "Grep") return {};
|
|
1676
1676
|
const toolResponse = input.tool_response;
|
|
1677
|
-
|
|
1678
|
-
|
|
1677
|
+
if (toolResponse == null) return {};
|
|
1678
|
+
const content = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse);
|
|
1679
|
+
if (!content) return {};
|
|
1679
1680
|
recordScan();
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
severity: match.rule.severity,
|
|
1689
|
-
action: "aborted",
|
|
1690
|
-
phase: "PostToolUse",
|
|
1691
|
-
tool
|
|
1692
|
-
});
|
|
1693
|
-
return Promise.resolve({ stopReason: `[YARA CRITICAL] ${match.rule.name}: Prompt injection detected in file content. Agent context is potentially poisoned. Session terminated for safety.` });
|
|
1681
|
+
const matches = await scanAndTriage(content, "input", llmProvider);
|
|
1682
|
+
if (matches.length === 0) return {};
|
|
1683
|
+
const match = highestSeverityMatch(matches);
|
|
1684
|
+
if (match.metadata.severity === "critical" || match.metadata.action === "block") {
|
|
1685
|
+
recordMatch("PostToolUse", toolName, match, "aborted");
|
|
1686
|
+
const reason = `[YARA CRITICAL] ${match.rule}: Prompt injection detected in file content. Agent context is potentially poisoned. Session terminated for safety.`;
|
|
1687
|
+
onTerminate(reason);
|
|
1688
|
+
return { stopReason: reason };
|
|
1694
1689
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
rule: match.rule.name,
|
|
1698
|
-
severity: match.rule.severity,
|
|
1699
|
-
action: "warned",
|
|
1700
|
-
phase: "PostToolUse",
|
|
1701
|
-
tool
|
|
1702
|
-
});
|
|
1703
|
-
return Promise.resolve({ hookSpecificOutput: {
|
|
1690
|
+
recordMatch("PostToolUse", toolName, match, "warned");
|
|
1691
|
+
return { hookSpecificOutput: {
|
|
1704
1692
|
hookEventName: "PostToolUse",
|
|
1705
|
-
additionalContext: `[YARA WARNING] ${match.rule
|
|
1706
|
-
} }
|
|
1693
|
+
additionalContext: `[YARA WARNING] ${match.rule}: ${match.metadata.description ?? ""}`
|
|
1694
|
+
} };
|
|
1707
1695
|
} catch (error) {
|
|
1708
1696
|
logToFile("[YARA] PostToolUse Read/Grep hook error:", error);
|
|
1709
|
-
|
|
1697
|
+
const reason = "[YARA] Scanner error while scanning read content — session terminated as a precaution.";
|
|
1698
|
+
onTerminate(reason);
|
|
1699
|
+
return { stopReason: reason };
|
|
1710
1700
|
}
|
|
1711
1701
|
}],
|
|
1712
1702
|
timeout: HOOK_TIMEOUT_MS
|
|
@@ -1723,21 +1713,18 @@ function createPostToolUseYaraHooks() {
|
|
|
1723
1713
|
const skillDir = dirMatch[1];
|
|
1724
1714
|
const cwd = input.cwd ?? process.cwd();
|
|
1725
1715
|
recordScan();
|
|
1726
|
-
const
|
|
1727
|
-
if (
|
|
1728
|
-
const match = highestSeverityMatch(
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
action: "aborted",
|
|
1734
|
-
phase: "PostToolUse",
|
|
1735
|
-
tool: "Bash (skill)"
|
|
1736
|
-
});
|
|
1737
|
-
return { stopReason: `[YARA CRITICAL] Poisoned skill detected in ${skillDir}: ${match.rule.name}. The downloaded skill contains potential prompt injection. Session terminated for safety.` };
|
|
1716
|
+
const matches = await scanSkillFiles(cwd, skillDir, llmProvider);
|
|
1717
|
+
if (matches.length === 0) return {};
|
|
1718
|
+
const match = highestSeverityMatch(matches);
|
|
1719
|
+
recordMatch("PostToolUse", "Bash (skill install)", match, "aborted");
|
|
1720
|
+
const reason = `[YARA CRITICAL] Poisoned skill detected in ${skillDir}: ${match.rule}. The downloaded skill contains potential prompt injection. Session terminated for safety.`;
|
|
1721
|
+
onTerminate(reason);
|
|
1722
|
+
return { stopReason: reason };
|
|
1738
1723
|
} catch (error) {
|
|
1739
1724
|
logToFile("[YARA] PostToolUse skill install hook error:", error);
|
|
1740
|
-
|
|
1725
|
+
const reason = "[YARA] Scanner error while scanning skill files — session terminated as a precaution.";
|
|
1726
|
+
onTerminate(reason);
|
|
1727
|
+
return { stopReason: reason };
|
|
1741
1728
|
}
|
|
1742
1729
|
}],
|
|
1743
1730
|
timeout: SKILL_SCAN_HOOK_TIMEOUT_MS
|
|
@@ -1746,12 +1733,15 @@ function createPostToolUseYaraHooks() {
|
|
|
1746
1733
|
}
|
|
1747
1734
|
/**
|
|
1748
1735
|
* Read and scan all text files in a skill directory for prompt injection.
|
|
1736
|
+
* Scans each file sequentially (single-threaded WASM — parallelism buys
|
|
1737
|
+
* nothing), unions the 'input'-context matches, then triages once across the
|
|
1738
|
+
* combined content. Returns the real (post-triage) matches.
|
|
1749
1739
|
*/
|
|
1750
|
-
async function scanSkillFiles(cwd, skillDir) {
|
|
1740
|
+
async function scanSkillFiles(cwd, skillDir, llmProvider) {
|
|
1751
1741
|
const absoluteDir = path.resolve(cwd, skillDir);
|
|
1752
1742
|
if (!fs.existsSync(absoluteDir)) {
|
|
1753
1743
|
logToFile(`[YARA] Skill directory does not exist: ${absoluteDir}`);
|
|
1754
|
-
return
|
|
1744
|
+
return [];
|
|
1755
1745
|
}
|
|
1756
1746
|
const files = await fg("**/*.{md,txt,yaml,yml,json,js,ts,py,rb,sh}", {
|
|
1757
1747
|
cwd: absoluteDir,
|
|
@@ -1759,20 +1749,68 @@ async function scanSkillFiles(cwd, skillDir) {
|
|
|
1759
1749
|
});
|
|
1760
1750
|
const fileContents = [];
|
|
1761
1751
|
for (const filePath of files) try {
|
|
1762
|
-
|
|
1763
|
-
fileContents.push({
|
|
1764
|
-
path: filePath,
|
|
1765
|
-
content
|
|
1766
|
-
});
|
|
1752
|
+
fileContents.push(fs.readFileSync(filePath, "utf-8"));
|
|
1767
1753
|
} catch (err) {
|
|
1768
1754
|
logToFile(`[YARA] Could not read skill file ${filePath}:`, err);
|
|
1769
1755
|
}
|
|
1770
1756
|
if (fileContents.length === 0) {
|
|
1771
1757
|
logToFile(`[YARA] No text files found in skill directory: ${absoluteDir}`);
|
|
1772
|
-
return
|
|
1758
|
+
return [];
|
|
1773
1759
|
}
|
|
1774
1760
|
logToFile(`[YARA] Scanning ${fileContents.length} files in skill directory: ${skillDir}`);
|
|
1775
|
-
|
|
1761
|
+
const flagged = [];
|
|
1762
|
+
for (const content of fileContents) flagged.push(...await scanForContext(content, "input"));
|
|
1763
|
+
return triageFlagged(flagged, "input", llmProvider);
|
|
1764
|
+
}
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region src/lib/agent/triage-provider.ts
|
|
1767
|
+
/**
|
|
1768
|
+
* LLM provider for warlock security-scan triage.
|
|
1769
|
+
*
|
|
1770
|
+
* Warlock's triageMatches() takes a consumer-supplied `(prompt) => Promise<string>`
|
|
1771
|
+
* to run a second pass that filters false positives out of YARA matches. This
|
|
1772
|
+
* builds that provider on top of the wizard's existing PostHog LLM gateway auth
|
|
1773
|
+
* — the same ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN that initializeAgent()
|
|
1774
|
+
* sets for the agent SDK. The gateway speaks the standard Anthropic Messages API,
|
|
1775
|
+
* so we POST to it directly with axios (the plain @anthropic-ai/sdk isn't a dep).
|
|
1776
|
+
*/
|
|
1777
|
+
const TRIAGE_MODEL = "claude-haiku-4-5";
|
|
1778
|
+
const TRIAGE_MAX_TOKENS = 16384;
|
|
1779
|
+
const TRIAGE_TIMEOUT_MS = 2e4;
|
|
1780
|
+
/**
|
|
1781
|
+
* Build the triage LLM provider from the gateway auth on process.env (set by
|
|
1782
|
+
* initializeAgent before any agent run). Returns undefined if auth isn't
|
|
1783
|
+
* configured — callers then skip triage and fail closed (act on every flagged
|
|
1784
|
+
* match), so a missing key never silently disables the scanner.
|
|
1785
|
+
*/
|
|
1786
|
+
function createTriageLLMProvider() {
|
|
1787
|
+
const baseURL = process.env.ANTHROPIC_BASE_URL;
|
|
1788
|
+
const authToken = process.env.ANTHROPIC_AUTH_TOKEN;
|
|
1789
|
+
if (!baseURL || !authToken) {
|
|
1790
|
+
logToFile("[YARA] triage provider unavailable (no gateway auth) — flagged scans will fail closed");
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
logToFile(`[YARA] triage provider ready (model: ${TRIAGE_MODEL})`);
|
|
1794
|
+
return async (prompt) => {
|
|
1795
|
+
const content = (await axios.post(`${baseURL}/v1/messages`, {
|
|
1796
|
+
model: TRIAGE_MODEL,
|
|
1797
|
+
max_tokens: TRIAGE_MAX_TOKENS,
|
|
1798
|
+
temperature: 0,
|
|
1799
|
+
messages: [{
|
|
1800
|
+
role: "user",
|
|
1801
|
+
content: prompt
|
|
1802
|
+
}]
|
|
1803
|
+
}, {
|
|
1804
|
+
headers: {
|
|
1805
|
+
Authorization: `Bearer ${authToken}`,
|
|
1806
|
+
"anthropic-version": "2023-06-01",
|
|
1807
|
+
"content-type": "application/json"
|
|
1808
|
+
},
|
|
1809
|
+
timeout: TRIAGE_TIMEOUT_MS
|
|
1810
|
+
})).data?.content;
|
|
1811
|
+
if (Array.isArray(content)) return content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
1812
|
+
return "";
|
|
1813
|
+
};
|
|
1776
1814
|
}
|
|
1777
1815
|
//#endregion
|
|
1778
1816
|
//#region src/lib/agent/commandments.ts
|
|
@@ -1789,6 +1827,7 @@ const WIZARD_COMMANDMENTS = [
|
|
|
1789
1827
|
"When installing packages, start the installation as a background task and then continue with other work. Do not block waiting for installs to finish unless explicitly instructed.",
|
|
1790
1828
|
"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.",
|
|
1791
1829
|
"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.",
|
|
1830
|
+
"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.",
|
|
1792
1831
|
"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.",
|
|
1793
1832
|
"Do not spawn subagents unless explicitly instructed to do so.",
|
|
1794
1833
|
"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.",
|
|
@@ -1844,10 +1883,15 @@ const AgentSignals = {
|
|
|
1844
1883
|
/**
|
|
1845
1884
|
* Parses the signal-bearing lines out of agent output and discards the rest.
|
|
1846
1885
|
*
|
|
1847
|
-
* The agent and SDK communicate non-content events (auth/API errors,
|
|
1848
|
-
*
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1886
|
+
* The agent and SDK communicate non-content events (auth/API errors, missing
|
|
1887
|
+
* MCP/resource, the end-of-run remark) by emitting marker strings inside
|
|
1888
|
+
* their prose. `AgentOutputSignals` keeps only the lines that carry such a
|
|
1889
|
+
* marker, so the buffer stays bounded regardless of run length.
|
|
1890
|
+
*
|
|
1891
|
+
* YARA violations are deliberately NOT detected here: scanning the agent's
|
|
1892
|
+
* prose for "[YARA ...]" markers false-positives when the agent merely
|
|
1893
|
+
* *mentions* a non-terminal block. Terminal YARA outcomes flow through the
|
|
1894
|
+
* hooks' onTerminate callback (`yaraViolationReason` in runAgent) instead.
|
|
1851
1895
|
*/
|
|
1852
1896
|
/**
|
|
1853
1897
|
* Single source of truth for the substrings runAgent scans agent output for.
|
|
@@ -1858,8 +1902,6 @@ const AgentSignals = {
|
|
|
1858
1902
|
*/
|
|
1859
1903
|
const OUTPUT_SIGNALS = {
|
|
1860
1904
|
API_ERROR: "API Error:",
|
|
1861
|
-
YARA_CRITICAL: "[YARA CRITICAL]",
|
|
1862
|
-
YARA_SCANNER_ERROR: "[YARA] Scanner error",
|
|
1863
1905
|
MCP_MISSING: AgentSignals.ERROR_MCP_MISSING,
|
|
1864
1906
|
RESOURCE_MISSING: AgentSignals.ERROR_RESOURCE_MISSING,
|
|
1865
1907
|
WIZARD_REMARK: AgentSignals.WIZARD_REMARK
|
|
@@ -1885,9 +1927,6 @@ var AgentOutputSignals = class {
|
|
|
1885
1927
|
hasApiErrorStatus(code) {
|
|
1886
1928
|
return this.text.includes(`${OUTPUT_SIGNALS.API_ERROR} ${code}`);
|
|
1887
1929
|
}
|
|
1888
|
-
hasYaraViolation() {
|
|
1889
|
-
return this.has("YARA_CRITICAL") || this.has("YARA_SCANNER_ERROR");
|
|
1890
|
-
}
|
|
1891
1930
|
/** Joined `API Error: …` lines for the user-facing message, or undefined. */
|
|
1892
1931
|
apiErrorMessage() {
|
|
1893
1932
|
const m = this.text.match(new RegExp(`${OUTPUT_SIGNALS.API_ERROR} [^\\n]+`, "g"));
|
|
@@ -2166,12 +2205,31 @@ function createStopHook(featureQueue, signals, requestRemark = true) {
|
|
|
2166
2205
|
};
|
|
2167
2206
|
}
|
|
2168
2207
|
/**
|
|
2169
|
-
*
|
|
2170
|
-
*
|
|
2208
|
+
* Global identifiers attached to every LLM gateway trace for a run. They ride on
|
|
2209
|
+
* each `$ai_generation` the gateway emits (as `X-POSTHOG-PROPERTY-*` headers via
|
|
2210
|
+
* `buildAgentEnv`), so traces are filterable by program, framework, run, and build
|
|
2211
|
+
* type for cost attribution and dashboards. `skill_id` is omitted when the run has
|
|
2212
|
+
* none.
|
|
2171
2213
|
*/
|
|
2172
|
-
function
|
|
2173
|
-
|
|
2174
|
-
|
|
2214
|
+
function buildRunTags(args) {
|
|
2215
|
+
return {
|
|
2216
|
+
program_id: args.programId,
|
|
2217
|
+
integration: args.integration,
|
|
2218
|
+
run_id: args.runId,
|
|
2219
|
+
build: args.build,
|
|
2220
|
+
...args.skillId ? { skill_id: args.skillId } : {}
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Whether the Warlock/YARA kill switch is engaged for this run. Off by default:
|
|
2225
|
+
* scanning is disabled only when the feature flag resolves to the explicit
|
|
2226
|
+
* string 'true', or the local POSTHOG_WIZARD_WARLOCK_DISABLED env override is
|
|
2227
|
+
* set. A missing flag, an empty flag map (the safe default returned when the
|
|
2228
|
+
* flag fetch fails), or any other value all leave scanning ON — a network blip
|
|
2229
|
+
* must never silently disable a security control.
|
|
2230
|
+
*/
|
|
2231
|
+
function isWarlockDisabled(flags = {}) {
|
|
2232
|
+
return flags["wizard-warlock-disabled"] === "true" || runtimeEnv("POSTHOG_WIZARD_WARLOCK_DISABLED") === "true";
|
|
2175
2233
|
}
|
|
2176
2234
|
/**
|
|
2177
2235
|
* Whether this run uses the experimental task-queue orchestrator. Gated by the
|
|
@@ -2440,6 +2498,7 @@ async function initializeAgent(config, options) {
|
|
|
2440
2498
|
gatewayUrl,
|
|
2441
2499
|
apiKeyPresent: !!config.posthogApiKey
|
|
2442
2500
|
});
|
|
2501
|
+
prewarmYaraScanner();
|
|
2443
2502
|
return agentRunConfig;
|
|
2444
2503
|
} catch (error) {
|
|
2445
2504
|
getUI().log.error(`Failed to initialize agent: ${error.message}`);
|
|
@@ -2449,25 +2508,13 @@ async function initializeAgent(config, options) {
|
|
|
2449
2508
|
}
|
|
2450
2509
|
}
|
|
2451
2510
|
/**
|
|
2452
|
-
* Check agent output for YARA scanner violations.
|
|
2453
|
-
* Used in both the success and catch paths of runAgent.
|
|
2454
|
-
*/
|
|
2455
|
-
function checkYaraViolation(signals, spinner) {
|
|
2456
|
-
if (signals.hasYaraViolation()) {
|
|
2457
|
-
logToFile("Agent error: YARA_VIOLATION");
|
|
2458
|
-
spinner.stop("Security violation detected");
|
|
2459
|
-
return { error: "WIZARD_YARA_VIOLATION" };
|
|
2460
|
-
}
|
|
2461
|
-
return null;
|
|
2462
|
-
}
|
|
2463
|
-
/**
|
|
2464
2511
|
* Execute an agent with the provided prompt and options
|
|
2465
2512
|
* Handles the full lifecycle: spinner, execution, error handling
|
|
2466
2513
|
*
|
|
2467
2514
|
* @returns An object containing any error detected in the agent's output
|
|
2468
2515
|
*/
|
|
2469
2516
|
async function runAgent(agentConfig, prompt, options, spinner, config, middleware) {
|
|
2470
|
-
const { spinnerMessage = "Customizing your PostHog setup...", successMessage = "PostHog integration complete", errorMessage = "Integration failed", abortCases = [] } = config ?? {};
|
|
2517
|
+
const { spinnerMessage = "Customizing your PostHog setup...", successMessage = "PostHog integration complete", errorMessage = "Integration failed", abortCases = [], emitStepEvents = false } = config ?? {};
|
|
2471
2518
|
logToFile("Starting agent run");
|
|
2472
2519
|
const { query } = await getSDKModule();
|
|
2473
2520
|
spinner.start(spinnerMessage);
|
|
@@ -2529,10 +2576,24 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
|
|
|
2529
2576
|
};
|
|
2530
2577
|
const abortController = new AbortController();
|
|
2531
2578
|
let abortReason = null;
|
|
2579
|
+
let yaraViolationReason = null;
|
|
2532
2580
|
try {
|
|
2533
2581
|
const disallow = new Set(agentConfig.disallowedTools ?? []);
|
|
2534
2582
|
const allowedTools = [...BASE_ALLOWED_TOOLS, ...agentConfig.allowedTools ?? []].filter((t) => !disallow.has(t));
|
|
2535
2583
|
const inheritedMcpServerNames = Object.keys(agentConfig.mcpServers);
|
|
2584
|
+
const triageProvider = createTriageLLMProvider();
|
|
2585
|
+
const onYaraTerminate = (reason) => {
|
|
2586
|
+
if (yaraViolationReason) return;
|
|
2587
|
+
yaraViolationReason = reason;
|
|
2588
|
+
logToFile(`[YARA] terminating run: ${reason}`);
|
|
2589
|
+
abortController.abort();
|
|
2590
|
+
signalDone();
|
|
2591
|
+
};
|
|
2592
|
+
const warlockDisabled = isWarlockDisabled(agentConfig.wizardFlags);
|
|
2593
|
+
if (warlockDisabled) {
|
|
2594
|
+
logToFile("[warlock] kill switch active — YARA scanning disabled for run");
|
|
2595
|
+
analytics.wizardCapture("warlock disabled", { reason: "kill-switch" });
|
|
2596
|
+
}
|
|
2536
2597
|
const response = query({
|
|
2537
2598
|
prompt: createPromptStream(),
|
|
2538
2599
|
options: {
|
|
@@ -2621,8 +2682,8 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
|
|
|
2621
2682
|
if (options.debug) debug("CLI stderr:", data);
|
|
2622
2683
|
},
|
|
2623
2684
|
hooks: {
|
|
2624
|
-
PreToolUse: createPreToolUseYaraHooks(),
|
|
2625
|
-
PostToolUse: createPostToolUseYaraHooks(),
|
|
2685
|
+
PreToolUse: warlockDisabled ? [] : createPreToolUseYaraHooks(triageProvider),
|
|
2686
|
+
PostToolUse: warlockDisabled ? [] : createPostToolUseYaraHooks(triageProvider, onYaraTerminate),
|
|
2626
2687
|
Stop: [{
|
|
2627
2688
|
hooks: [createStopHook(config?.additionalFeatureQueue ?? [], signals, config?.requestRemark ?? true)],
|
|
2628
2689
|
timeout: 30
|
|
@@ -2648,7 +2709,7 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
|
|
|
2648
2709
|
}
|
|
2649
2710
|
loggedInitialContext = true;
|
|
2650
2711
|
}
|
|
2651
|
-
handleSDKMessage(message, options, spinner, signals, receivedSuccessResult, tasks, isOrchestratorEnabled(agentConfig.wizardFlags ?? {}));
|
|
2712
|
+
handleSDKMessage(message, options, spinner, signals, receivedSuccessResult, tasks, isOrchestratorEnabled(agentConfig.wizardFlags ?? {}), emitStepEvents);
|
|
2652
2713
|
if (abortCases.length > 0 && !abortReason && message.type === "assistant") {
|
|
2653
2714
|
const content = message.message?.content;
|
|
2654
2715
|
if (Array.isArray(content)) {
|
|
@@ -2698,6 +2759,11 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
|
|
|
2698
2759
|
signalDone();
|
|
2699
2760
|
}
|
|
2700
2761
|
}
|
|
2762
|
+
if (yaraViolationReason) {
|
|
2763
|
+
logToFile("Agent error: YARA_VIOLATION");
|
|
2764
|
+
spinner.stop("Security violation detected");
|
|
2765
|
+
return { error: "WIZARD_YARA_VIOLATION" };
|
|
2766
|
+
}
|
|
2701
2767
|
if (abortReason) {
|
|
2702
2768
|
spinner.stop("Wizard aborted");
|
|
2703
2769
|
return {
|
|
@@ -2705,8 +2771,6 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
|
|
|
2705
2771
|
message: abortReason
|
|
2706
2772
|
};
|
|
2707
2773
|
}
|
|
2708
|
-
const yaraResult = checkYaraViolation(signals, spinner);
|
|
2709
|
-
if (yaraResult) return yaraResult;
|
|
2710
2774
|
if (signals.has("MCP_MISSING")) {
|
|
2711
2775
|
logToFile("Agent error: MCP_MISSING");
|
|
2712
2776
|
spinner.stop("Agent could not access PostHog MCP");
|
|
@@ -2738,6 +2802,11 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
|
|
|
2738
2802
|
return completeWithSuccess();
|
|
2739
2803
|
} catch (error) {
|
|
2740
2804
|
signalDone();
|
|
2805
|
+
if (yaraViolationReason) {
|
|
2806
|
+
logToFile("Agent error: YARA_VIOLATION");
|
|
2807
|
+
spinner.stop("Security violation detected");
|
|
2808
|
+
return { error: "WIZARD_YARA_VIOLATION" };
|
|
2809
|
+
}
|
|
2741
2810
|
if (abortReason) {
|
|
2742
2811
|
spinner.stop("Wizard aborted");
|
|
2743
2812
|
return {
|
|
@@ -2746,8 +2815,6 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
|
|
|
2746
2815
|
};
|
|
2747
2816
|
}
|
|
2748
2817
|
if (receivedSuccessResult) return completeWithSuccess(error);
|
|
2749
|
-
const yaraResult = checkYaraViolation(signals, spinner);
|
|
2750
|
-
if (yaraResult) return yaraResult;
|
|
2751
2818
|
const apiErrorMessage = signals.apiErrorMessage() ?? "Unknown API error";
|
|
2752
2819
|
if (signals.hasApiErrorStatus(429)) {
|
|
2753
2820
|
logToFile("Agent error (caught): RATE_LIMIT");
|
|
@@ -2833,11 +2900,22 @@ function handleTaskUpdate(block, store) {
|
|
|
2833
2900
|
const existing = store.tasks.get(input.taskId);
|
|
2834
2901
|
if (!existing) return;
|
|
2835
2902
|
if (input.status === "deleted") store.tasks.delete(input.taskId);
|
|
2836
|
-
else
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2903
|
+
else {
|
|
2904
|
+
if (store.emitStepEvents && input.status && input.status !== existing.status && (input.status === "in_progress" || input.status === "completed")) {
|
|
2905
|
+
const keys = [...store.tasks.keys()];
|
|
2906
|
+
analytics.wizardCapture("step", {
|
|
2907
|
+
step_name: existing.activeForm ?? input.activeForm ?? existing.content ?? input.subject,
|
|
2908
|
+
status: input.status,
|
|
2909
|
+
step_index: keys.indexOf(input.taskId),
|
|
2910
|
+
step_count: keys.length
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
store.tasks.set(input.taskId, {
|
|
2914
|
+
content: input.subject ?? existing.content,
|
|
2915
|
+
status: input.status ?? existing.status,
|
|
2916
|
+
activeForm: input.activeForm ?? existing.activeForm
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2841
2919
|
store.sync();
|
|
2842
2920
|
}
|
|
2843
2921
|
function handleTaskGet(_block, _store) {}
|
|
@@ -2887,7 +2965,7 @@ function extractTaskIdFromResult(content) {
|
|
|
2887
2965
|
}
|
|
2888
2966
|
}
|
|
2889
2967
|
}
|
|
2890
|
-
function handleSDKMessage(message, options, spinner, signals, receivedSuccessResult = false, tasks, suppressTaskRender = false) {
|
|
2968
|
+
function handleSDKMessage(message, options, spinner, signals, receivedSuccessResult = false, tasks, suppressTaskRender = false, emitStepEvents = false) {
|
|
2891
2969
|
const STATUS_RANK = {
|
|
2892
2970
|
completed: 0,
|
|
2893
2971
|
in_progress: 1
|
|
@@ -2922,7 +3000,8 @@ function handleSDKMessage(message, options, spinner, signals, receivedSuccessRes
|
|
|
2922
3000
|
}
|
|
2923
3001
|
if (block.type === "tool_use" && tasks && Object.values(TaskTool).includes(block.name)) dispatchTaskToolUse(block, {
|
|
2924
3002
|
tasks,
|
|
2925
|
-
sync: syncTasks
|
|
3003
|
+
sync: syncTasks,
|
|
3004
|
+
emitStepEvents
|
|
2926
3005
|
});
|
|
2927
3006
|
if (block.type === "tool_use") {
|
|
2928
3007
|
const stage = classifyToolToStage(block.name);
|
|
@@ -2977,6 +3056,6 @@ function handleSDKMessage(message, options, spinner, signals, receivedSuccessRes
|
|
|
2977
3056
|
}
|
|
2978
3057
|
}
|
|
2979
3058
|
//#endregion
|
|
2980
|
-
export {
|
|
3059
|
+
export { AUDIT_CHECKS_FILE as C, coerceAuditChecks as D, AUDIT_SEVERITY_STYLE as E, getAuditChecks as O, TaskStatus as S, AUDIT_REPORT_FILE as T, downloadSkill as _, runAgent as a, QUEUE_DIR_NAME as b, recoverOrphanedSettingsBackups as c, flushScanReport as d, formatScanReport as f, WIZARD_TOOL_NAMES as g, SETUP_REPORT_FILE as h, isOrchestratorEnabled as i, restoreClaudeSettings as l, EVENT_PLAN_FILE as m, buildRunTags as n, backupAndFixClaudeSettings as o, writeScanReport as p, initializeAgent as r, checkAllSettingsConflicts as s, buildAgentEnv as t, AgentSignals as u, fetchSkillMenu as v, AUDIT_CHECKS_KEY as w, QueueStore as x, installSkillById as y };
|
|
2981
3060
|
|
|
2982
|
-
//# sourceMappingURL=agent-interface-
|
|
3061
|
+
//# sourceMappingURL=agent-interface-DT_uyR45.js.map
|