@posthog/wizard 2.38.2 → 2.39.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 (96) hide show
  1. package/README.md +70 -13
  2. package/dist/{add-mcp-server-to-clients-CHSzsAG8.js → add-mcp-server-to-clients-Ci0I1vbS.js} +5 -5
  3. package/dist/{add-mcp-server-to-clients-CHSzsAG8.js.map → add-mcp-server-to-clients-Ci0I1vbS.js.map} +1 -1
  4. package/dist/{agent-interface-DExARWay.js → agent-interface-BR3zAUpM.js} +30 -623
  5. package/dist/agent-interface-BR3zAUpM.js.map +1 -0
  6. package/dist/{agent-runner-CMfTmM3B.js → agent-runner-B1fzwFzc.js} +33 -18
  7. package/dist/agent-runner-B1fzwFzc.js.map +1 -0
  8. package/dist/{analytics-Ciob-x8a.js → analytics-C7Oc3yWZ.js} +3 -2
  9. package/dist/analytics-C7Oc3yWZ.js.map +1 -0
  10. package/dist/analytics-tVERo8XR.js +2 -0
  11. package/dist/{api-T_WHs3qZ.js → api-DSG-l8Ql.js} +3 -3
  12. package/dist/{api-T_WHs3qZ.js.map → api-DSG-l8Ql.js.map} +1 -1
  13. package/dist/bin.js +387 -105
  14. package/dist/bin.js.map +1 -1
  15. package/dist/{ci-install-DJN5aT7W.js → ci-install-DeAd2HYP.js} +4 -4
  16. package/dist/{ci-install-DJN5aT7W.js.map → ci-install-DeAd2HYP.js.map} +1 -1
  17. package/dist/{debug-D1lORUFs.js → debug-BZnZ7Gvh.js} +37 -10
  18. package/dist/debug-BZnZ7Gvh.js.map +1 -0
  19. package/dist/{debug-Cne-cLxd.js → debug-DtgfvuKF.js} +1 -1
  20. package/dist/{defaults-a0e6IRjP.js → defaults-BxiMLkQY.js} +1 -1
  21. package/dist/{defaults-a0e6IRjP.js.map → defaults-BxiMLkQY.js.map} +1 -1
  22. package/dist/{env-api-key-DEl3LJBv.js → env-api-key-DDVkPQmo.js} +1 -1
  23. package/dist/{env-api-key-DEl3LJBv.js.map → env-api-key-DDVkPQmo.js.map} +1 -1
  24. package/dist/{environment-BEsDCA2q.js → environment-QNoUem74.js} +3 -3
  25. package/dist/{environment-BEsDCA2q.js.map → environment-QNoUem74.js.map} +1 -1
  26. package/dist/{file-utils-Cn3dzlo-.js → file-utils-UqdMTR9x.js} +2 -2
  27. package/dist/{file-utils-Cn3dzlo-.js.map → file-utils-UqdMTR9x.js.map} +1 -1
  28. package/dist/{headless-ui-DGunpK-s.js → headless-ui-DtFktXIa.js} +2 -2
  29. package/dist/{headless-ui-DGunpK-s.js.map → headless-ui-DtFktXIa.js.map} +1 -1
  30. package/dist/{host-resolution-BnUJZKId.js → host-resolution-DjQvur4X.js} +3 -3
  31. package/dist/{host-resolution-BnUJZKId.js.map → host-resolution-DjQvur4X.js.map} +1 -1
  32. package/dist/{interactive-BB2IDSWP.js → interactive-_8v2Ymw2.js} +2 -2
  33. package/dist/{interactive-BB2IDSWP.js.map → interactive-_8v2Ymw2.js.map} +1 -1
  34. package/dist/{mcp-CgA5vyiS.js → mcp-BX34188X.js} +2 -2
  35. package/dist/{mcp-CgA5vyiS.js.map → mcp-BX34188X.js.map} +1 -1
  36. package/dist/{mcp-prompt-streaming-CGAkq9qR.js → mcp-prompt-streaming-l1zsmQQc.js} +4 -4
  37. package/dist/{mcp-prompt-streaming-CGAkq9qR.js.map → mcp-prompt-streaming-l1zsmQQc.js.map} +1 -1
  38. package/dist/{non-interactive-Dko5F3vh.js → non-interactive-BBCGIDP5.js} +2 -2
  39. package/dist/{non-interactive-Dko5F3vh.js.map → non-interactive-BBCGIDP5.js.map} +1 -1
  40. package/dist/{package-manager-WKKQADHM.js → package-manager-FY9qPU4z.js} +2 -2
  41. package/dist/{package-manager-WKKQADHM.js.map → package-manager-FY9qPU4z.js.map} +1 -1
  42. package/dist/{playground-UtT6zbjE.js → playground-B9Gesj-w.js} +5 -5
  43. package/dist/{playground-UtT6zbjE.js.map → playground-B9Gesj-w.js.map} +1 -1
  44. package/dist/{posthog-BESrI6-T.js → posthog-BHbtnNQE.js} +1 -1
  45. package/dist/{posthog-BESrI6-T.js.map → posthog-BHbtnNQE.js.map} +1 -1
  46. package/dist/{posthog-integration-CWXSng0W.js → posthog-integration-CMNtIV97.js} +14 -14
  47. package/dist/{posthog-integration-CWXSng0W.js.map → posthog-integration-CMNtIV97.js.map} +1 -1
  48. package/dist/{provisioning-CgomM26T.js → provisioning-BInfIJy5.js} +4 -4
  49. package/dist/{provisioning-CgomM26T.js.map → provisioning-BInfIJy5.js.map} +1 -1
  50. package/dist/{registry-BQIX-25O.js → registry-BsPnELr-.js} +4 -4
  51. package/dist/{registry-BQIX-25O.js.map → registry-BsPnELr-.js.map} +1 -1
  52. package/dist/security-CSlaLY6P.js +204 -0
  53. package/dist/security-CSlaLY6P.js.map +1 -0
  54. package/dist/{setup-utils-DLGNk8T6.js → setup-utils-DfRLklZ3.js} +9 -9
  55. package/dist/{setup-utils-DLGNk8T6.js.map → setup-utils-DfRLklZ3.js.map} +1 -1
  56. package/dist/{start-tui-BjCKghQq.js → start-tui-H_BSAFdp.js} +29 -27
  57. package/dist/start-tui-H_BSAFdp.js.map +1 -0
  58. package/dist/{steps-XjV9CiqH.js → steps-z89LzfJG.js} +7 -7
  59. package/dist/{steps-XjV9CiqH.js.map → steps-z89LzfJG.js.map} +1 -1
  60. package/dist/{store-CDIxU0rr.js → store-B3B76DgD.js} +4 -4
  61. package/dist/{store-CDIxU0rr.js.map → store-B3B76DgD.js.map} +1 -1
  62. package/dist/{subagent-S5Yh16IJ.js → subagent-CV59ljO1.js} +2 -2
  63. package/dist/{subagent-S5Yh16IJ.js.map → subagent-CV59ljO1.js.map} +1 -1
  64. package/dist/{task-stream-rC4IEUHC.js → task-stream-d-R9J0g4.js} +2 -2
  65. package/dist/{task-stream-rC4IEUHC.js.map → task-stream-d-R9J0g4.js.map} +1 -1
  66. package/dist/{tasks-wbjOdifO.js → tasks-DkX454Mg.js} +2 -2
  67. package/dist/{tasks-wbjOdifO.js.map → tasks-DkX454Mg.js.map} +1 -1
  68. package/dist/{telemetry-BfnR7gtf.js → telemetry-BCIe6Kli.js} +3 -3
  69. package/dist/{telemetry-BfnR7gtf.js.map → telemetry-BCIe6Kli.js.map} +1 -1
  70. package/dist/{terminal-BvRwVeQp.js → terminal-CPXW44sU.js} +13 -12
  71. package/dist/{terminal-BvRwVeQp.js.map → terminal-CPXW44sU.js.map} +1 -1
  72. package/dist/{tools-CbtYlZV4.js → tools-C4BqVrt6.js} +4 -4
  73. package/dist/{tools-CbtYlZV4.js.map → tools-C4BqVrt6.js.map} +1 -1
  74. package/dist/types-evL_rPob.js +46 -0
  75. package/dist/types-evL_rPob.js.map +1 -0
  76. package/dist/{urls-CilgPbS-.js → urls-D1AehCBN.js} +2 -2
  77. package/dist/{urls-CilgPbS-.js.map → urls-D1AehCBN.js.map} +1 -1
  78. package/dist/warlock-smoke-test.ts +165 -0
  79. package/dist/{wizard-abort-Cp5Y7f8C.js → wizard-abort-5fOIcY8w.js} +3 -3
  80. package/dist/{wizard-abort-Cp5Y7f8C.js.map → wizard-abort-5fOIcY8w.js.map} +1 -1
  81. package/dist/{wizard-abort-DHrEGqaC.js → wizard-abort-Ce2RceZ1.js} +1 -1
  82. package/dist/{wizard-tools-DQTfAzSV.js → wizard-tools-xvIrJeNN.js} +26 -50
  83. package/dist/wizard-tools-xvIrJeNN.js.map +1 -0
  84. package/dist/yara-hooks-7MEXlgqm.js +2 -0
  85. package/dist/yara-hooks-DNn1Jqih.js +635 -0
  86. package/dist/yara-hooks-DNn1Jqih.js.map +1 -0
  87. package/package.json +4 -3
  88. package/dist/agent-interface-DExARWay.js.map +0 -1
  89. package/dist/agent-runner-CMfTmM3B.js.map +0 -1
  90. package/dist/analytics-Ciob-x8a.js.map +0 -1
  91. package/dist/analytics-DbfiRvPS.js +0 -2
  92. package/dist/debug-D1lORUFs.js.map +0 -1
  93. package/dist/security-pJOYY5LO.js +0 -431
  94. package/dist/security-pJOYY5LO.js.map +0 -1
  95. package/dist/start-tui-BjCKghQq.js.map +0 -1
  96. package/dist/wizard-tools-DQTfAzSV.js.map +0 -1
@@ -1,17 +1,16 @@
1
1
  import { n as __require } from "./rolldown-runtime-B_-DWIq7.js";
2
- import { V as POSTHOG_FLAG_HEADER_PREFIX, W as POSTHOG_PROPERTY_HEADER_PREFIX, a as getLogFilePath, at as WIZARD_REMARK_EVENT_NAME, f as getUI, o as initLogFile, pt as runtimeEnv, r as debug, s as logToFile, st as WIZARD_USER_AGENT, tt as WIZARD_ORCHESTRATOR_FLAG_KEY, u as WIZARD_YARA_REPORT_FILE } from "./debug-D1lORUFs.js";
3
- import { n as analytics } from "./analytics-Ciob-x8a.js";
4
- import { t as HostResolution } from "./host-resolution-BnUJZKId.js";
2
+ import { V as POSTHOG_FLAG_HEADER_PREFIX, W as POSTHOG_PROPERTY_HEADER_PREFIX, a as getLogFilePath, at as WIZARD_REMARK_EVENT_NAME, f as getUI, o as initLogFile, pt as runtimeEnv, r as debug, s as logToFile, st as WIZARD_USER_AGENT, tt as WIZARD_ORCHESTRATOR_FLAG_KEY } from "./debug-BZnZ7Gvh.js";
3
+ import { n as analytics } from "./analytics-C7Oc3yWZ.js";
4
+ import { t as HostResolution } from "./host-resolution-DjQvur4X.js";
5
5
  import { n as ADDITIONAL_FEATURE_PROMPTS } from "./wizard-session-BoXAZEOf.js";
6
- import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-Cp5Y7f8C.js";
7
- import { h as AUDIT_REPORT_FILE, n as createWizardToolsServer, t as WIZARD_TOOL_NAMES } from "./wizard-tools-DQTfAzSV.js";
6
+ import { i as wizardAbort, n as registerCleanup, t as WizardError } from "./wizard-abort-5fOIcY8w.js";
7
+ import { n as createWizardToolsServer, t as WIZARD_TOOL_NAMES } from "./wizard-tools-xvIrJeNN.js";
8
+ import { l as prewarmYaraScanner, n as createPostToolUseYaraHooks, r as createPreToolUseYaraHooks } from "./yara-hooks-DNn1Jqih.js";
8
9
  import { createRequire } from "node:module";
9
10
  import * as fs$1 from "fs";
10
- import fs from "fs";
11
11
  import path from "path";
12
12
  import { spawnSync } from "node:child_process";
13
13
  import axios from "axios";
14
- import fg from "fast-glob";
15
14
  import * as os from "os";
16
15
  //#region src/utils/custom-headers.ts
17
16
  /**
@@ -177,604 +176,6 @@ const LINTING_TOOLS = [
177
176
  "yamllint"
178
177
  ];
179
178
  //#endregion
180
- //#region src/lib/skill-install.ts
181
- /**
182
- * Check if command is a PostHog skill installation from MCP.
183
- * We control the MCP server, so we only need to verify:
184
- * 1. It installs to .claude/skills/
185
- * 2. It downloads from our GitHub releases or localhost (dev)
186
- *
187
- * Extracted to its own module to avoid a circular dependency
188
- * between agent-interface.ts and yara-hooks.ts.
189
- */
190
- function isSkillInstallCommand(command) {
191
- if (!command.startsWith("mkdir -p .claude/skills/")) return false;
192
- const urlMatch = command.match(/curl -sL ['"]([^'"]+)['"]/);
193
- if (!urlMatch) return false;
194
- const url = urlMatch[1];
195
- return url.startsWith("https://github.com/PostHog/context-mill/releases/") || /^http:\/\/localhost:\d+\//.test(url);
196
- }
197
- //#endregion
198
- //#region src/lib/programs/events-audit/constants.ts
199
- /**
200
- * Leaf-level constants for the events-audit program.
201
- *
202
- * Kept separate from `index.ts` so files like `yara-hooks.ts` can import
203
- * the filename constants without dragging in `index.ts`'s heavier imports
204
- * (agent-runner, audit/seed, etc.) — which can create import cycles.
205
- */
206
- const SETUP_REPORT_FILE = "posthog-events-audit-report.md";
207
- const EVENT_INVENTORY_FILE = ".posthog-events-inventory.json";
208
- /** Per-part filename pattern emitted by events-audit subagents (e.g. `.posthog-events-inventory.part-3.json`). */
209
- const EVENT_INVENTORY_PART_PATTERN = /^\.posthog-events-inventory\.part-\d+\.json$/;
210
- //#endregion
211
- //#region src/lib/programs/posthog-integration/constants.ts
212
- /**
213
- * Leaf-level constants for the posthog-integration program.
214
- *
215
- * Kept separate from `index.ts` so files like `yara-hooks.ts` can import
216
- * the filename constants without dragging in `index.ts`'s heavier imports
217
- * (agent-interface, framework-config, etc.) — which would create an import
218
- * cycle through agent-interface → yara-hooks.
219
- */
220
- const EVENT_PLAN_FILE = ".posthog-events.json";
221
- //#endregion
222
- //#region src/lib/yara-hooks.ts
223
- /**
224
- * YARA hook wiring for the Claude Agent SDK.
225
- *
226
- * Creates PreToolUse and PostToolUse hook callback arrays that integrate the
227
- * real @posthog/warlock security scanner (YARA-X via WASM) into the wizard's
228
- * agent loop. These hooks are registered in the SDK's query() options alongside
229
- * the existing Stop hook.
230
- *
231
- * PreToolUse hooks block dangerous commands before execution. PostToolUse hooks
232
- * detect violations in written code and prompt injection in read content, and
233
- * scan context-mill skill downloads.
234
- *
235
- * Warlock owns the rules; this file owns the *policy* — how a match maps to a
236
- * block / revert / terminate response, plus the optional LLM triage pass that
237
- * filters false positives before we act.
238
- *
239
- * Naming: "yara" is the technique — scanning content against YARA rules — so the
240
- * wizard-side wiring keeps that name. "warlock" is the engine package that runs
241
- * the rules. Both names showing up here is intentional.
242
- *
243
- * This is Layer 2 (L2) in the wizard's defense-in-depth model, complementing the
244
- * prompt-based commandments (L0) and the canUseTool() allowlist (L1).
245
- */
246
- let warlockModulePromise = null;
247
- function getWarlock() {
248
- if (!warlockModulePromise) warlockModulePromise = import("@posthog/warlock").catch((err) => {
249
- warlockModulePromise = null;
250
- throw err;
251
- });
252
- return warlockModulePromise;
253
- }
254
- /**
255
- * Pay the WASM-init + rule-compile cost up front, off the hook path, so the
256
- * first real tool-call scan doesn't eat cold-start under a hook timeout.
257
- * Best-effort: a failure here is non-fatal — hooks still fail closed per scan.
258
- */
259
- async function prewarmYaraScanner() {
260
- try {
261
- await (await getWarlock()).scan("");
262
- logToFile("[YARA] warlock pre-warmed");
263
- } catch (err) {
264
- logToFile("[YARA] warlock pre-warm failed:", err);
265
- }
266
- }
267
- let scanCount = 0;
268
- const scanViolations = [];
269
- function recordScan() {
270
- scanCount++;
271
- }
272
- /**
273
- * Log a match to the run log + PostHog, and record it in the run's scan report.
274
- * Single entry point so every violation is reported consistently.
275
- */
276
- function recordMatch(phase, tool, match, action) {
277
- const triage = match.triage;
278
- logYaraMatch(phase, tool, match, action);
279
- scanViolations.push({
280
- rule: match.rule,
281
- severity: match.metadata.severity ?? "unknown",
282
- category: match.metadata.category ?? "unknown",
283
- action,
284
- phase,
285
- tool,
286
- description: match.metadata.description ?? "",
287
- triageVerdict: triage?.verdict,
288
- triageReason: triage?.reason
289
- });
290
- }
291
- /**
292
- * Scan bookkeeping for harnesses that run a scanner outside these hooks (pi):
293
- * counts the scan and routes violations through `recordMatch`, so they reach
294
- * the run log, per-match telemetry, and the end-of-run scan report.
295
- */
296
- function recordExternalScan(phase, tool, violations, action) {
297
- recordScan();
298
- for (const v of violations) recordMatch(phase, tool, {
299
- rule: v.rule,
300
- metadata: {
301
- severity: v.severity,
302
- category: v.category,
303
- description: v.description
304
- },
305
- matchedStrings: []
306
- }, action);
307
- }
308
- /** Format the scan report summary. Returns null if no scans occurred */
309
- function formatScanReport() {
310
- if (scanCount === 0) return null;
311
- const lines = ["", "— YARA Scanner Summary —"];
312
- const violationCount = scanViolations.length;
313
- const cleanCount = scanCount - violationCount;
314
- lines.push(`✓ ${scanCount} tool calls scanned, ${violationCount} violation${violationCount !== 1 ? "s" : ""} detected`);
315
- if (violationCount > 0) {
316
- lines.push("");
317
- for (const v of scanViolations) {
318
- const tag = v.action.toUpperCase();
319
- lines.push(` [${tag}] ${v.rule} (${v.severity.toUpperCase()}) — ${v.phase}:${v.tool}`);
320
- }
321
- }
322
- if (cleanCount > 0) {
323
- lines.push("");
324
- lines.push(`No violations: ✓ ${cleanCount} clean scan${cleanCount !== 1 ? "s" : ""}`);
325
- }
326
- lines.push("");
327
- return lines.join("\n");
328
- }
329
- /** Write the scan report to a JSON file. Returns the file path, or null if no scans occurred. */
330
- function writeScanReport() {
331
- if (scanCount === 0) return null;
332
- const report = {
333
- summary: {
334
- totalScans: scanCount,
335
- violations: scanViolations.length,
336
- clean: scanCount - scanViolations.length
337
- },
338
- violations: scanViolations
339
- };
340
- try {
341
- fs.writeFileSync(WIZARD_YARA_REPORT_FILE, JSON.stringify(report, null, 2));
342
- } catch (err) {
343
- logToFile("[YARA] Failed to write scan report:", err);
344
- return null;
345
- }
346
- return WIZARD_YARA_REPORT_FILE;
347
- }
348
- const CATEGORY_DESCRIPTIONS = {
349
- prompt_injection: "hit hidden instructions in a file it read, a possible attempt to hijack the setup",
350
- exfiltration: "tried to send data to an outside location",
351
- destructive_operations: "tried to run a destructive command",
352
- supply_chain: "tried to pull in untrusted code",
353
- posthog_pii: "tried to write personal data (PII) into your project",
354
- posthog_hardcoded_key: "tried to hardcode a PostHog key into your project",
355
- hardcoded_secret: "tried to hardcode a secret into your project"
356
- };
357
- /** Most recent violation that terminated the run, or null. */
358
- function lastTerminatingViolation() {
359
- for (let i = scanViolations.length - 1; i >= 0; i--) if (scanViolations[i].action === "aborted") return scanViolations[i];
360
- return null;
361
- }
362
- /**
363
- * Build the user-facing copy for the security abort screen: what the scanner
364
- * caught and what we did about it. This screen renders only because the run was
365
- * stopped, so the "what we did" half is constant. Falls back to a generic line
366
- * when no specific violation was recorded (e.g. the scanner itself errored and
367
- * failed closed before a match was logged).
368
- */
369
- function formatYaraAbortMessage() {
370
- const v = lastTerminatingViolation();
371
- return `Security check stopped the setup.
372
-
373
- The agent ${(v && CATEGORY_DESCRIPTIONS[v.category]) ?? "did something our security scanner flagged"}. We stopped the run to keep your project safe.\nNo unsafe changes were left in your project.
374
-
375
- If this looks wrong, let us know: wizard@posthog.com`;
376
- }
377
- /** Timeout for scan hooks (PreToolUse, PostToolUse Write/Edit/Read/Grep) */
378
- const HOOK_TIMEOUT_MS = 3e4;
379
- /** Timeout for the skill install hook (filesystem I/O + multiple scans) */
380
- const SKILL_SCAN_HOOK_TIMEOUT_MS = 3e4;
381
- /**
382
- * Chunk size for scanning (100 KB). Oversized content is scanned in
383
- * overlapping chunks so coverage is complete — nothing is silently truncated.
384
- * The size also bounds what a single triage call pastes into the LLM prompt
385
- * (~25K tokens), so triage always sees the chunk its matches came from.
386
- */
387
- const SCAN_CHUNK_SIZE = 1e5;
388
- /**
389
- * Overlap between adjacent chunks so a pattern straddling a chunk boundary
390
- * still lands whole inside at least one chunk. YARA rule strings are at most
391
- * a few hundred bytes; 4 KB is generous.
392
- */
393
- const SCAN_CHUNK_OVERLAP = 4096;
394
- function logYaraMatch(phase, tool, match, action) {
395
- const severity = match.metadata.severity ?? "unknown";
396
- const category = match.metadata.category ?? "unknown";
397
- const ruleAction = match.metadata.action ?? "unknown";
398
- const description = match.metadata.description ?? "";
399
- logToFile(`[YARA] ${phase}:${tool} [${action.toUpperCase()}] rule "${match.rule}" (severity: ${severity}, category: ${category}, action: ${ruleAction})\n Description: ${description}`);
400
- analytics.wizardCapture("yara rule matched", {
401
- rule: match.rule,
402
- severity: match.metadata.severity,
403
- category: match.metadata.category,
404
- rule_action: match.metadata.action,
405
- action,
406
- phase,
407
- tool,
408
- description: match.metadata.description,
409
- triage_verdict: match.triage?.verdict
410
- });
411
- }
412
- /**
413
- * Send the run's full scan report to PostHog so maintainers can see why a run
414
- * was flagged, warned, or aborted. This is the maintainer-facing report — it is
415
- * NEVER shown to the user. No-op if nothing was scanned. The free-text triage
416
- * reason is omitted to avoid sending scanned content off the user's machine.
417
- */
418
- function captureScanReport() {
419
- if (scanCount === 0) return;
420
- analytics.wizardCapture("yara scan report", {
421
- total_scans: scanCount,
422
- violation_count: scanViolations.length,
423
- clean_count: scanCount - scanViolations.length,
424
- violations: scanViolations.map((v) => ({
425
- rule: v.rule,
426
- severity: v.severity,
427
- category: v.category,
428
- action: v.action,
429
- phase: v.phase,
430
- tool: v.tool,
431
- description: v.description,
432
- triage_verdict: v.triageVerdict
433
- }))
434
- });
435
- scanCount = 0;
436
- scanViolations.length = 0;
437
- }
438
- /**
439
- * End-of-run flush for the scan report. Wired once at the runner seam so every
440
- * harness (linear, orchestrator, and any future shape) reports without having
441
- * to know warlock exists — scanning is already harness-agnostic (it runs from
442
- * SDK Pre/PostToolUse hooks), and this keeps reporting at a single shared seam.
443
- *
444
- * Order matters: write the local --yara-report file FIRST (it reads scanCount /
445
- * scanViolations), THEN send telemetry. captureScanReport() zeroes scan state,
446
- * which is what makes this whole function idempotent — a second call from
447
- * another termination path (e.g. finally after an abort already flushed) finds
448
- * scanCount === 0 and every step no-ops.
449
- */
450
- function flushScanReport(session) {
451
- if (session.yaraReport) {
452
- const reportPath = writeScanReport();
453
- if (reportPath) {
454
- const summary = formatScanReport();
455
- getUI().log.info(`YARA scan report: ${reportPath}${summary ?? ""}`);
456
- }
457
- }
458
- captureScanReport();
459
- }
460
- const WIZARD_DOC_BASENAMES = new Set([
461
- EVENT_INVENTORY_FILE,
462
- SETUP_REPORT_FILE,
463
- AUDIT_REPORT_FILE,
464
- EVENT_PLAN_FILE
465
- ]);
466
- const WIZARD_DOC_PATTERNS = [EVENT_INVENTORY_PART_PATTERN];
467
- function isWizardDocumentationPath(filePath) {
468
- if (!filePath) return false;
469
- const basename = path.basename(filePath);
470
- if (WIZARD_DOC_BASENAMES.has(basename)) return true;
471
- return WIZARD_DOC_PATTERNS.some((re) => re.test(basename));
472
- }
473
- const SEVERITY_RANK = {
474
- critical: 4,
475
- high: 3,
476
- medium: 2,
477
- low: 1
478
- };
479
- /** Return the highest-severity match from a list of matches. */
480
- function highestSeverityMatch(matches) {
481
- return matches.reduce((worst, m) => (SEVERITY_RANK[m.metadata.severity ?? ""] ?? 0) > (SEVERITY_RANK[worst.metadata.severity ?? ""] ?? 0) ? m : worst);
482
- }
483
- /**
484
- * Keep only matches whose rule targets this content surface. Rules carry a
485
- * `scan_context` of 'command' | 'input' | 'output'. An undefined context is
486
- * treated as "applies everywhere" (fail-safe — a rule that forgets the tag
487
- * still gets enforced rather than silently skipped).
488
- */
489
- function matchesForContext(matches, ctx) {
490
- return matches.filter((m) => {
491
- const c = m.metadata.scan_context;
492
- return c === ctx || c === void 0;
493
- });
494
- }
495
- /**
496
- * Drop false positives via warlock's LLM triage. Fail-closed: if no provider is
497
- * available, or the triage call throws, every match is treated as real — we
498
- * never silently suppress a flagged match.
499
- *
500
- * Every overruled match is reported to PostHog (rule metadata only, never the
501
- * free-text reason) so maintainers can alert on triage-overrule patterns — the
502
- * signal that someone is either tripping a noisy rule or trying to talk the
503
- * triage model out of a real finding.
504
- */
505
- async function triageFilter(content, matches, ctx, llmProvider) {
506
- if (matches.length === 0) return [];
507
- if (!llmProvider) {
508
- logToFile(`[YARA] triage skipped (no provider) — treating ${matches.length} match(es) as real`);
509
- return matches;
510
- }
511
- try {
512
- const triaged = await (await getWarlock()).triageMatches(content, matches, llmProvider);
513
- const kept = triaged.filter((m) => m.triage.verdict === "true_positive");
514
- for (const m of triaged) {
515
- if (m.triage.verdict === "true_positive") continue;
516
- logToFile(`[YARA] triage overruled rule "${m.rule}" (${m.metadata.severity ?? "unknown"}) — not acting on it`);
517
- analytics.wizardCapture("yara triage overruled", {
518
- rule: m.rule,
519
- severity: m.metadata.severity,
520
- category: m.metadata.category,
521
- scan_context: ctx
522
- });
523
- }
524
- logToFile(`[YARA] triage: ${matches.length} flagged → ${kept.length} kept as real`);
525
- return kept;
526
- } catch (err) {
527
- logToFile("[YARA] triage failed — treating all matches as real:", err);
528
- return matches;
529
- }
530
- }
531
- /**
532
- * Split oversized content into overlapping chunks so the scanner covers all
533
- * of it. Content that fits in one chunk is returned as-is.
534
- */
535
- function chunkContent(content) {
536
- if (content.length <= SCAN_CHUNK_SIZE) return [content];
537
- const chunks = [];
538
- const step = SCAN_CHUNK_SIZE - SCAN_CHUNK_OVERLAP;
539
- for (let start = 0; start < content.length; start += step) {
540
- chunks.push(content.slice(start, start + SCAN_CHUNK_SIZE));
541
- if (start + SCAN_CHUNK_SIZE >= content.length) break;
542
- }
543
- return chunks;
544
- }
545
- /**
546
- * Scan content against warlock rules — chunked when oversized, so nothing is
547
- * skipped — and keep only matches for this content surface. Chunks scan
548
- * sequentially (the WASM engine is single-threaded; parallelism buys nothing).
549
- * Returns each flagged chunk with its matches so triage can later judge every
550
- * match against the exact content it came from.
551
- */
552
- async function scanForContext(content, ctx) {
553
- const warlock = await getWarlock();
554
- const chunks = chunkContent(content);
555
- if (chunks.length > 1) {
556
- logToFile(`[YARA] content is ${content.length} chars — scanning ${chunks.length} overlapping chunks`);
557
- analytics.wizardCapture("yara scan chunked", {
558
- content_length: content.length,
559
- chunk_count: chunks.length,
560
- scan_context: ctx
561
- });
562
- }
563
- const flagged = [];
564
- for (const chunk of chunks) {
565
- const result = await warlock.scan(chunk);
566
- if (!result.matched) continue;
567
- const matches = matchesForContext(result.matches, ctx);
568
- if (matches.length === 0) continue;
569
- flagged.push({
570
- chunk,
571
- matches
572
- });
573
- }
574
- return flagged;
575
- }
576
- /** The overlap between chunks can surface the same rule twice; count it once. */
577
- function dedupeByRule(matches) {
578
- const seen = /* @__PURE__ */ new Set();
579
- return matches.filter((m) => {
580
- if (seen.has(m.rule)) return false;
581
- seen.add(m.rule);
582
- return true;
583
- });
584
- }
585
- /**
586
- * Triage each flagged chunk against its own content — the evidence is always
587
- * inside the window the LLM sees. Triage calls run in parallel (each is just
588
- * an HTTP round trip) so N flagged chunks cost ~1× triage latency, not N×.
589
- */
590
- async function triageFlagged(flagged, ctx, llmProvider) {
591
- return dedupeByRule((await Promise.all(flagged.map(({ chunk, matches }) => triageFilter(chunk, matches, ctx, llmProvider)))).flat());
592
- }
593
- /** Scan content, filter to the relevant context, triage. Returns real matches. */
594
- async function scanAndTriage(content, ctx, llmProvider) {
595
- return triageFlagged(await scanForContext(content, ctx), ctx, llmProvider);
596
- }
597
- /**
598
- * Create PreToolUse hook matchers for YARA scanning.
599
- * Scans Bash commands before execution for exfiltration, destructive
600
- * operations, and supply chain violations ('command'-context rules).
601
- */
602
- function createPreToolUseYaraHooks(_llmProvider) {
603
- return [{
604
- hooks: [async (input) => {
605
- try {
606
- if (input.tool_name !== "Bash") return {};
607
- const toolInput = input.tool_input;
608
- const command = typeof toolInput?.command === "string" ? toolInput.command : "";
609
- if (!command) return {};
610
- recordScan();
611
- const matches = await scanAndTriage(command, "command", void 0);
612
- if (matches.length === 0) return {};
613
- const match = highestSeverityMatch(matches);
614
- recordMatch("PreToolUse", "Bash", match, "blocked");
615
- return {
616
- decision: "block",
617
- reason: `[YARA] ${match.rule}: ${match.metadata.description ?? "security policy violation"}. Command blocked for security.`
618
- };
619
- } catch (error) {
620
- logToFile("[YARA] PreToolUse hook error:", error);
621
- return {
622
- decision: "block",
623
- reason: "[YARA] Scanner error — command blocked as a precaution."
624
- };
625
- }
626
- }],
627
- timeout: HOOK_TIMEOUT_MS
628
- }];
629
- }
630
- /**
631
- * Create PostToolUse hook matchers for YARA scanning.
632
- *
633
- * Three matchers:
634
- * 1. Write/Edit — scan written content ('output'-context rules: PII, secrets)
635
- * 2. Read/Grep — scan read content ('input'-context rules: prompt injection)
636
- * 3. Bash (skill install) — scan downloaded skill files for poisoned content
637
- *
638
- * `onTerminate` is invoked on the terminal paths (critical prompt injection in
639
- * read content, a poisoned skill, or a scanner error under fail-closed). It is
640
- * what ACTUALLY stops the run — returning `stopReason` from a PostToolUse hook
641
- * is not honored by the SDK, so the caller wires this to the run's
642
- * AbortController (see agent-interface).
643
- */
644
- function createPostToolUseYaraHooks(llmProvider, onTerminate) {
645
- return [
646
- {
647
- hooks: [async (input) => {
648
- try {
649
- const toolName = input.tool_name;
650
- if (toolName !== "Write" && toolName !== "Edit") return {};
651
- const toolInput = input.tool_input;
652
- const content = toolName === "Write" ? toolInput?.content ?? "" : toolInput?.new_string ?? "";
653
- if (!content) return {};
654
- recordScan();
655
- const flagged = await scanForContext(content, "output");
656
- if (flagged.length === 0) return {};
657
- const filePath = toolInput?.file_path;
658
- const activeFlagged = isWizardDocumentationPath(filePath) ? flagged.map(({ chunk, matches }) => ({
659
- chunk,
660
- matches: matches.filter((m) => m.metadata.category !== "posthog_pii")
661
- })).filter(({ matches }) => matches.length > 0) : flagged;
662
- if (activeFlagged.length === 0) {
663
- logToFile(`[YARA] posthog_pii match suppressed on wizard doc ${path.basename(filePath ?? "")} (rule: ${flagged[0]?.matches[0]?.rule})`);
664
- return {};
665
- }
666
- const matches = await triageFlagged(activeFlagged, "output", llmProvider);
667
- if (matches.length === 0) return {};
668
- const match = highestSeverityMatch(matches);
669
- recordMatch("PostToolUse", toolName, match, "reverted");
670
- return { hookSpecificOutput: {
671
- hookEventName: "PostToolUse",
672
- additionalContext: `[YARA VIOLATION] ${match.rule}: ${match.metadata.description ?? ""}. You MUST revert this change immediately. The content you just wrote violates security policy.`
673
- } };
674
- } catch (error) {
675
- logToFile("[YARA] PostToolUse Write/Edit hook error:", error);
676
- const reason = "[YARA] Scanner error while scanning written content — session terminated as a precaution.";
677
- onTerminate(reason);
678
- return { stopReason: reason };
679
- }
680
- }],
681
- timeout: HOOK_TIMEOUT_MS
682
- },
683
- {
684
- hooks: [async (input) => {
685
- try {
686
- const toolName = input.tool_name;
687
- if (toolName !== "Read" && toolName !== "Grep") return {};
688
- const toolResponse = input.tool_response;
689
- if (toolResponse == null) return {};
690
- const content = typeof toolResponse === "string" ? toolResponse : JSON.stringify(toolResponse);
691
- if (!content) return {};
692
- recordScan();
693
- const matches = await scanAndTriage(content, "input", llmProvider);
694
- if (matches.length === 0) return {};
695
- const match = highestSeverityMatch(matches);
696
- if (match.metadata.severity === "critical" || match.metadata.action === "block") {
697
- recordMatch("PostToolUse", toolName, match, "aborted");
698
- const reason = `[YARA CRITICAL] ${match.rule}: Prompt injection detected in file content. Agent context is potentially poisoned. Session terminated for safety.`;
699
- onTerminate(reason);
700
- return { stopReason: reason };
701
- }
702
- recordMatch("PostToolUse", toolName, match, "warned");
703
- return { hookSpecificOutput: {
704
- hookEventName: "PostToolUse",
705
- additionalContext: `[YARA WARNING] ${match.rule}: ${match.metadata.description ?? ""}`
706
- } };
707
- } catch (error) {
708
- logToFile("[YARA] PostToolUse Read/Grep hook error:", error);
709
- const reason = "[YARA] Scanner error while scanning read content — session terminated as a precaution.";
710
- onTerminate(reason);
711
- return { stopReason: reason };
712
- }
713
- }],
714
- timeout: HOOK_TIMEOUT_MS
715
- },
716
- {
717
- hooks: [async (input) => {
718
- try {
719
- if (input.tool_name !== "Bash") return {};
720
- const toolInput = input.tool_input;
721
- const command = typeof toolInput?.command === "string" ? toolInput.command : "";
722
- if (!isSkillInstallCommand(command)) return {};
723
- const dirMatch = command.match(/mkdir -p (.claude\/skills\/[^\s&]+)/);
724
- if (!dirMatch) return {};
725
- const skillDir = dirMatch[1];
726
- const cwd = input.cwd ?? process.cwd();
727
- recordScan();
728
- const matches = await scanSkillFiles(cwd, skillDir, llmProvider);
729
- if (matches.length === 0) return {};
730
- const match = highestSeverityMatch(matches);
731
- recordMatch("PostToolUse", "Bash (skill install)", match, "aborted");
732
- const reason = `[YARA CRITICAL] Poisoned skill detected in ${skillDir}: ${match.rule}. The downloaded skill contains potential prompt injection. Session terminated for safety.`;
733
- onTerminate(reason);
734
- return { stopReason: reason };
735
- } catch (error) {
736
- logToFile("[YARA] PostToolUse skill install hook error:", error);
737
- const reason = "[YARA] Scanner error while scanning skill files — session terminated as a precaution.";
738
- onTerminate(reason);
739
- return { stopReason: reason };
740
- }
741
- }],
742
- timeout: SKILL_SCAN_HOOK_TIMEOUT_MS
743
- }
744
- ];
745
- }
746
- /**
747
- * Read and scan all text files in a skill directory for prompt injection.
748
- * Scans each file sequentially (single-threaded WASM — parallelism buys
749
- * nothing), unions the 'input'-context matches, then triages once across the
750
- * combined content. Returns the real (post-triage) matches.
751
- */
752
- async function scanSkillFiles(cwd, skillDir, llmProvider) {
753
- const absoluteDir = path.resolve(cwd, skillDir);
754
- if (!fs.existsSync(absoluteDir)) {
755
- logToFile(`[YARA] Skill directory does not exist: ${absoluteDir}`);
756
- return [];
757
- }
758
- const files = await fg("**/*.{md,txt,yaml,yml,json,js,ts,py,rb,sh}", {
759
- cwd: absoluteDir,
760
- absolute: true
761
- });
762
- const fileContents = [];
763
- for (const filePath of files) try {
764
- fileContents.push(fs.readFileSync(filePath, "utf-8"));
765
- } catch (err) {
766
- logToFile(`[YARA] Could not read skill file ${filePath}:`, err);
767
- }
768
- if (fileContents.length === 0) {
769
- logToFile(`[YARA] No text files found in skill directory: ${absoluteDir}`);
770
- return [];
771
- }
772
- logToFile(`[YARA] Scanning ${fileContents.length} files in skill directory: ${skillDir}`);
773
- const flagged = [];
774
- for (const content of fileContents) flagged.push(...await scanForContext(content, "input"));
775
- return triageFlagged(flagged, "input", llmProvider);
776
- }
777
- //#endregion
778
179
  //#region src/lib/agent/triage-provider.ts
779
180
  /**
780
181
  * LLM provider for warlock security-scan triage.
@@ -790,14 +191,20 @@ const TRIAGE_MODEL = "claude-haiku-4-5";
790
191
  const TRIAGE_MAX_TOKENS = 16384;
791
192
  const TRIAGE_TIMEOUT_MS = 2e4;
792
193
  /**
793
- * Build the triage LLM provider from the gateway auth on process.env (set by
794
- * initializeAgent before any agent run). Returns undefined if auth isn't
194
+ * Build the triage LLM provider. Auth comes from the explicit `auth` param
195
+ * when given (the pi harness — it auths the gateway programmatically and never
196
+ * sets the env vars), otherwise from the gateway auth on process.env (set by
197
+ * initializeAgent on the anthropic path). Returns undefined if neither is
795
198
  * configured — callers then skip triage and fail closed (act on every flagged
796
199
  * match), so a missing key never silently disables the scanner.
200
+ *
201
+ * The triage model is independent of the agent's model: it is a classifier
202
+ * judging scan matches, and the gateway routes it by model id regardless of
203
+ * which model runs the coding agent.
797
204
  */
798
- function createTriageLLMProvider() {
799
- const baseURL = process.env.ANTHROPIC_BASE_URL;
800
- const authToken = process.env.ANTHROPIC_AUTH_TOKEN;
205
+ function createTriageLLMProvider(auth) {
206
+ const baseURL = auth?.baseURL ?? process.env.ANTHROPIC_BASE_URL;
207
+ const authToken = auth?.authToken ?? process.env.ANTHROPIC_AUTH_TOKEN;
801
208
  if (!baseURL || !authToken) {
802
209
  logToFile("[YARA] triage provider unavailable (no gateway auth) — flagged scans will fail closed");
803
210
  return;
@@ -1509,15 +916,15 @@ function buildRunTags(args) {
1509
916
  };
1510
917
  }
1511
918
  /**
1512
- * Whether the Warlock/YARA kill switch is engaged for this run. Off by default:
1513
- * scanning is disabled only when the feature flag resolves to the explicit
1514
- * string 'true', or the local POSTHOG_WIZARD_WARLOCK_DISABLED env override is
1515
- * set. A missing flag, an empty flag map (the safe default returned when the
1516
- * flag fetch fails), or any other value all leave scanning ON — a network blip
1517
- * must never silently disable a security control.
919
+ * Whether Warlock/YARA scanning is disabled for this run. Off by default:
920
+ * scanning is disabled only by the local POSTHOG_WIZARD_WARLOCK_DISABLED env
921
+ * override, set to the explicit string 'true'. This is a local/CI escape hatch
922
+ * only — there is deliberately no remote flag to switch off a security control
923
+ * (we rely on version-locking @posthog/warlock + the release-gate smoke test
924
+ * instead). Any other value leaves scanning ON.
1518
925
  */
1519
- function isWarlockDisabled(flags = {}) {
1520
- return flags["wizard-warlock-disabled"] === "true" || runtimeEnv("POSTHOG_WIZARD_WARLOCK_DISABLED") === "true";
926
+ function isWarlockDisabled() {
927
+ return runtimeEnv("POSTHOG_WIZARD_WARLOCK_DISABLED") === "true";
1521
928
  }
1522
929
  /**
1523
930
  * Build env for the SDK subprocess: process.env plus ANTHROPIC_CUSTOM_HEADERS, which always
@@ -1880,10 +1287,10 @@ async function runAgent(agentConfig, prompt, options, spinner, config, middlewar
1880
1287
  abortController.abort();
1881
1288
  signalDone();
1882
1289
  };
1883
- const warlockDisabled = isWarlockDisabled(agentConfig.wizardFlags);
1290
+ const warlockDisabled = isWarlockDisabled();
1884
1291
  if (warlockDisabled) {
1885
- logToFile("[warlock] kill switch active — YARA scanning disabled for run");
1886
- analytics.wizardCapture("warlock disabled", { reason: "kill-switch" });
1292
+ logToFile("[warlock] scanning disabled for run (local env override)");
1293
+ analytics.wizardCapture("warlock disabled", { reason: "env-override" });
1887
1294
  }
1888
1295
  const response = query({
1889
1296
  prompt: createPromptStream(),
@@ -2383,6 +1790,6 @@ function handleSDKMessage(message, options, spinner, signals, receivedSuccessRes
2383
1790
  }
2384
1791
  }
2385
1792
  //#endregion
2386
- export { SETUP_REPORT_FILE as C, EVENT_PLAN_FILE as S, formatScanReport as _, wizardCanUseTool as a, recordExternalScan as b, classifySettingsConflicts as c, sanitizeAgentSubprocessEnv as d, AgentOutputSignals as f, flushScanReport as g, getWizardCommandments as h, runAgent as i, recoverOrphanedSettingsBackups as l, REMARK_INSTRUCTION as m, buildRunTags as n, backupAndFixClaudeSettings as o, AgentSignals as p, initializeAgent as r, checkAllSettingsConflicts as s, buildAgentEnv as t, restoreClaudeSettings as u, formatYaraAbortMessage as v, writeScanReport as x, isWizardDocumentationPath as y };
1793
+ export { wizardCanUseTool as a, classifySettingsConflicts as c, sanitizeAgentSubprocessEnv as d, AgentOutputSignals as f, createTriageLLMProvider as g, getWizardCommandments as h, runAgent as i, recoverOrphanedSettingsBackups as l, REMARK_INSTRUCTION as m, buildRunTags as n, backupAndFixClaudeSettings as o, AgentSignals as p, initializeAgent as r, checkAllSettingsConflicts as s, buildAgentEnv as t, restoreClaudeSettings as u };
2387
1794
 
2388
- //# sourceMappingURL=agent-interface-DExARWay.js.map
1795
+ //# sourceMappingURL=agent-interface-BR3zAUpM.js.map