@posthog/wizard 2.30.0 → 2.32.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.
Files changed (59) hide show
  1. package/README.md +35 -1
  2. package/dist/README.md +33 -0
  3. package/dist/{add-mcp-server-to-clients-D0G4cmPf.js → add-mcp-server-to-clients-Dc0yssCM.js} +4 -4
  4. package/dist/{add-mcp-server-to-clients-D0G4cmPf.js.map → add-mcp-server-to-clients-Dc0yssCM.js.map} +1 -1
  5. package/dist/{agent-interface-CYFyWCMj.js → agent-interface-c7B2JZEd.js} +483 -416
  6. package/dist/agent-interface-c7B2JZEd.js.map +1 -0
  7. package/dist/{agent-runner-HTfFCUrR.js → agent-runner-Am34bBUT.js} +39 -23
  8. package/dist/agent-runner-Am34bBUT.js.map +1 -0
  9. package/dist/{analytics-CdT0VV8s.js → analytics-CBIKy9PZ.js} +14 -6
  10. package/dist/analytics-CBIKy9PZ.js.map +1 -0
  11. package/dist/{api-D9CerM6x.js → api-Blg3nvvZ.js} +3 -3
  12. package/dist/{api-D9CerM6x.js.map → api-Blg3nvvZ.js.map} +1 -1
  13. package/dist/bin.js +103 -53
  14. package/dist/bin.js.map +1 -1
  15. package/dist/{ci-install-CTydrjHu.js → ci-install-51ntd9x5.js} +4 -4
  16. package/dist/{ci-install-CTydrjHu.js.map → ci-install-51ntd9x5.js.map} +1 -1
  17. package/dist/{debug-D9giWww2.js → debug-AdvgwKEw.js} +9 -10
  18. package/dist/debug-AdvgwKEw.js.map +1 -0
  19. package/dist/{debug-Bmq9KH4W.js → debug-cy_jyRb4.js} +1 -1
  20. package/dist/{environment-C6j-a4Gz.js → environment-B6TW5v9d.js} +3 -3
  21. package/dist/{environment-C6j-a4Gz.js.map → environment-B6TW5v9d.js.map} +1 -1
  22. package/dist/{file-utils-BiElGS_N.js → file-utils-8tUk_eEX.js} +2 -2
  23. package/dist/{file-utils-BiElGS_N.js.map → file-utils-8tUk_eEX.js.map} +1 -1
  24. package/dist/{interactive-C0Vssetd.js → interactive-CApktTrj.js} +2 -2
  25. package/dist/{interactive-C0Vssetd.js.map → interactive-CApktTrj.js.map} +1 -1
  26. package/dist/{mcp-prompt-streaming-DQXxG2Pg.js → mcp-prompt-streaming-BKcU9yuz.js} +4 -4
  27. package/dist/{mcp-prompt-streaming-DQXxG2Pg.js.map → mcp-prompt-streaming-BKcU9yuz.js.map} +1 -1
  28. package/dist/{non-interactive-DX-N3ZEb.js → non-interactive-DejTdRTW.js} +2 -2
  29. package/dist/{non-interactive-DX-N3ZEb.js.map → non-interactive-DejTdRTW.js.map} +1 -1
  30. package/dist/{package-manager-cIPAT7g3.js → package-manager-DBfgSXNn.js} +2 -2
  31. package/dist/{package-manager-cIPAT7g3.js.map → package-manager-DBfgSXNn.js.map} +1 -1
  32. package/dist/{playground-DQI2vpr0.js → playground-BOg2U1AT.js} +4 -4
  33. package/dist/{playground-DQI2vpr0.js.map → playground-BOg2U1AT.js.map} +1 -1
  34. package/dist/{posthog-integration-EUokB9U1.js → posthog-integration-gLhOUdPJ.js} +13 -14
  35. package/dist/{posthog-integration-EUokB9U1.js.map → posthog-integration-gLhOUdPJ.js.map} +1 -1
  36. package/dist/{provisioning-BCCeBATw.js → provisioning-DuzclqPB.js} +3 -3
  37. package/dist/{provisioning-BCCeBATw.js.map → provisioning-DuzclqPB.js.map} +1 -1
  38. package/dist/{registry-DCxIW2G5.js → registry-Dbl-5SnO.js} +4 -4
  39. package/dist/{registry-DCxIW2G5.js.map → registry-Dbl-5SnO.js.map} +1 -1
  40. package/dist/{setup-utils-DmX3o2bT.js → setup-utils-B6wbp3s0.js} +8 -8
  41. package/dist/{setup-utils-DmX3o2bT.js.map → setup-utils-B6wbp3s0.js.map} +1 -1
  42. package/dist/{start-tui-B9dCp0hW.js → start-tui-IoQh-Nhj.js} +13 -13
  43. package/dist/{start-tui-B9dCp0hW.js.map → start-tui-IoQh-Nhj.js.map} +1 -1
  44. package/dist/{steps-3XbXMf0T.js → steps-CJrqlHbo.js} +7 -7
  45. package/dist/{steps-3XbXMf0T.js.map → steps-CJrqlHbo.js.map} +1 -1
  46. package/dist/{telemetry-GFq8wmz0.js → telemetry-1m0CyTry.js} +3 -3
  47. package/dist/{telemetry-GFq8wmz0.js.map → telemetry-1m0CyTry.js.map} +1 -1
  48. package/dist/{terminal-oI1dOWQI.js → terminal-BKI4i72f.js} +9 -9
  49. package/dist/{terminal-oI1dOWQI.js.map → terminal-BKI4i72f.js.map} +1 -1
  50. package/dist/{urls-93eQ-Rd0.js → urls-B3JumpLT.js} +2 -2
  51. package/dist/{urls-93eQ-Rd0.js.map → urls-B3JumpLT.js.map} +1 -1
  52. package/dist/{wizard-abort-dmkJqxAb.js → wizard-abort-D7SzKUgE.js} +1 -1
  53. package/dist/{wizard-abort-BehJBPpy.js → wizard-abort-PqLMKSh1.js} +3 -3
  54. package/dist/{wizard-abort-BehJBPpy.js.map → wizard-abort-PqLMKSh1.js.map} +1 -1
  55. package/package.json +16 -52
  56. package/dist/agent-interface-CYFyWCMj.js.map +0 -1
  57. package/dist/agent-runner-HTfFCUrR.js.map +0 -1
  58. package/dist/analytics-CdT0VV8s.js.map +0 -1
  59. package/dist/debug-D9giWww2.js.map +0 -1
@@ -1,14 +1,15 @@
1
1
  import { n as __require } from "./rolldown-runtime-B_-DWIq7.js";
2
- import { $ as WIZARD_VARIANTS, 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, et as WIZARD_VARIANT_FLAG_KEY, f as skillTmpPath, o as initLogFile, p as getUI, r as debug, s as logToFile, u as WIZARD_YARA_REPORT_FILE } from "./debug-D9giWww2.js";
3
- import { t as analytics } from "./analytics-CdT0VV8s.js";
4
- import { i as getLlmGatewayUrlFromHost } from "./urls-93eQ-Rd0.js";
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-AdvgwKEw.js";
3
+ import { t as analytics } from "./analytics-CBIKy9PZ.js";
4
+ import { i as getLlmGatewayUrlFromHost } from "./urls-B3JumpLT.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-BehJBPpy.js";
6
+ import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-PqLMKSh1.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
- * integrate the YARA scanner into the wizard's agent loop. These
1484
- * hooks are registered in the SDK's query() options alongside the
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.
1486
1268
  *
1487
- * PreToolUse hooks block dangerous commands before execution.
1488
- * PostToolUse hooks detect violations in written code and prompt
1489
- * injection in read content, and scan context-mill skill downloads.
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.
1272
+ *
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).
1490
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.
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
- function recordViolation(entry) {
1497
- scanViolations.push(entry);
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 synchronous scan hooks (PreToolUse, PostToolUse Write/Edit/Read) */
1540
- const HOOK_TIMEOUT_MS = 60;
1541
- /** Timeout for skill install hook (involves filesystem I/O) */
1542
- const SKILL_SCAN_HOOK_TIMEOUT_MS = 120;
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
- logToFile(`[YARA] ${phase}:${tool} [${action.toUpperCase()}] rule "${match.rule.name}" (severity: ${match.rule.severity}, category: ${match.rule.category})\n Description: ${match.rule.description}\n Matched text: "${match.matchedText.substring(0, 200)}"`);
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.name,
1547
- severity: match.rule.severity,
1548
- category: match.rule.category,
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
- ".posthog-events-inventory.json",
1556
- "posthog-events-audit-report.md",
1557
- "posthog-audit-report.md",
1558
- ".posthog-events.json"
1449
+ EVENT_INVENTORY_FILE,
1450
+ SETUP_REPORT_FILE,
1451
+ AUDIT_REPORT_FILE,
1452
+ EVENT_PLAN_FILE
1559
1453
  ]);
1560
- const WIZARD_DOC_PATTERNS = [/^\.posthog-events-inventory\.part-\d+\.json$/];
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.rule.severity] ?? 0) > (SEVERITY_RANK[worst.rule.severity] ?? 0) ? m : worst);
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
- * destructive operations, and supply chain violations.
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 Promise.resolve({});
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 Promise.resolve({});
1597
+ if (!command) return {};
1590
1598
  recordScan();
1591
- const result = scan(command, "PreToolUse", "Bash");
1592
- if (!result.matched) return Promise.resolve({});
1593
- const match = highestSeverityMatch(result.matches);
1594
- logYaraMatch("PreToolUse", "Bash", match, "blocked");
1595
- recordViolation({
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.name}: ${match.rule.description}. Command blocked for security.`
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 Promise.resolve({
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 for PII, secrets, config violations
1622
- * 2. Read/Grep — scan read content for prompt injection
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 Promise.resolve({});
1638
+ if (toolName !== "Write" && toolName !== "Edit") return {};
1632
1639
  const toolInput = input.tool_input;
1633
- const content = toolName === "Write" ? toolInput?.content ?? "" : toolInput?.new_str ?? "";
1634
- if (!content) return Promise.resolve({});
1640
+ const content = toolName === "Write" ? toolInput?.content ?? "" : toolInput?.new_string ?? "";
1641
+ if (!content) return {};
1635
1642
  recordScan();
1636
- const tool = toolName;
1637
- const result = scan(content, "PostToolUse", tool);
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
- if (isWizardDocumentationPath(filePath)) {
1641
- const nonPiiMatches = result.matches.filter((m) => m.rule.category !== "posthog_pii");
1642
- if (nonPiiMatches.length === 0) {
1643
- logToFile(`[YARA] posthog_pii match suppressed on wizard doc ${path.basename(filePath ?? "")} (rule: ${result.matches[0]?.rule.name})`);
1644
- return Promise.resolve({});
1645
- }
1646
- result.matches = nonPiiMatches;
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 match = highestSeverityMatch(result.matches);
1649
- logYaraMatch("PostToolUse", tool, match, "reverted");
1650
- recordViolation({
1651
- rule: match.rule.name,
1652
- severity: match.rule.severity,
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.name}: ${match.rule.description}. You MUST revert this change immediately. The content you just wrote violates security policy.`
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
- return Promise.resolve({ hookSpecificOutput: {
1664
- hookEventName: "PostToolUse",
1665
- additionalContext: "[YARA] Scanner error — you MUST revert this change as a precaution."
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 Promise.resolve({});
1675
+ if (toolName !== "Read" && toolName !== "Grep") return {};
1676
1676
  const toolResponse = input.tool_response;
1677
- const content = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse ?? "");
1678
- if (!content) return Promise.resolve({});
1677
+ if (toolResponse == null) return {};
1678
+ const content = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse);
1679
+ if (!content) return {};
1679
1680
  recordScan();
1680
- const tool = toolName;
1681
- const result = scan(content, "PostToolUse", tool);
1682
- if (!result.matched) return Promise.resolve({});
1683
- const match = highestSeverityMatch(result.matches);
1684
- if (match.rule.severity === "critical") {
1685
- logYaraMatch("PostToolUse", tool, match, "aborted");
1686
- recordViolation({
1687
- rule: match.rule.name,
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
- logYaraMatch("PostToolUse", tool, match, "warned");
1696
- recordViolation({
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.name}: ${match.rule.description}`
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
- return Promise.resolve({ stopReason: "[YARA] Scanner error while scanning read content — session terminated as a precaution." });
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 result = await scanSkillFiles(cwd, skillDir);
1727
- if (!result.matched) return {};
1728
- const match = highestSeverityMatch(result.matches);
1729
- logYaraMatch("PostToolUse", "Bash (skill install)", match, "aborted");
1730
- recordViolation({
1731
- rule: match.rule.name,
1732
- severity: match.rule.severity,
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
- return { stopReason: "[YARA] Scanner error while scanning skill files — session terminated as a precaution." };
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 { matched: false };
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
- const content = fs.readFileSync(filePath, "utf-8");
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 { matched: false };
1758
+ return [];
1773
1759
  }
1774
1760
  logToFile(`[YARA] Scanning ${fileContents.length} files in skill directory: ${skillDir}`);
1775
- return scanSkillDirectory(fileContents);
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, YARA
1848
- * violations, missing MCP/resource, the end-of-run remark) by emitting marker
1849
- * strings inside their prose. `AgentOutputSignals` keeps only the lines that
1850
- * carry such a marker, so the buffer stays bounded regardless of run length.
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
- * Select wizard metadata from WIZARD_VARIANTS using the variant feature flag.
2170
- * If the flag is missing or the value is not in config, returns the "base" variant (VARIANT: "base").
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.
2213
+ */
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.
2171
2230
  */
2172
- function buildWizardMetadata(flags = {}) {
2173
- const variantKey = flags[WIZARD_VARIANT_FLAG_KEY];
2174
- return { ...(variantKey && WIZARD_VARIANTS[variantKey]) ?? WIZARD_VARIANTS["base"] };
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,18 +2508,6 @@ 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
  *
@@ -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
@@ -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");
@@ -2989,6 +3056,6 @@ function handleSDKMessage(message, options, spinner, signals, receivedSuccessRes
2989
3056
  }
2990
3057
  }
2991
3058
  //#endregion
2992
- export { AUDIT_SEVERITY_STYLE as C, AUDIT_REPORT_FILE as S, getAuditChecks as T, QUEUE_DIR_NAME as _, runAgent as a, AUDIT_CHECKS_FILE as b, recoverOrphanedSettingsBackups as c, formatScanReport as d, writeScanReport as f, installSkillById as g, fetchSkillMenu as h, isOrchestratorEnabled as i, restoreClaudeSettings as l, downloadSkill as m, buildWizardMetadata as n, backupAndFixClaudeSettings as o, WIZARD_TOOL_NAMES as p, initializeAgent as r, checkAllSettingsConflicts as s, buildAgentEnv as t, AgentSignals as u, QueueStore as v, coerceAuditChecks as w, AUDIT_CHECKS_KEY as x, TaskStatus as y };
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 };
2993
3060
 
2994
- //# sourceMappingURL=agent-interface-CYFyWCMj.js.map
3061
+ //# sourceMappingURL=agent-interface-c7B2JZEd.js.map