@posthog/wizard 2.62.0 → 2.64.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 (103) hide show
  1. package/README.md +48 -6
  2. package/dist/{add-mcp-server-to-clients-Bzl7_wB3.js → add-mcp-server-to-clients-D9l1dyw9.js} +4 -4
  3. package/dist/{add-mcp-server-to-clients-Bzl7_wB3.js.map → add-mcp-server-to-clients-D9l1dyw9.js.map} +1 -1
  4. package/dist/{agent-interface-C90hyvs1.js → agent-interface-BQfwaSe-.js} +24 -16
  5. package/dist/agent-interface-BQfwaSe-.js.map +1 -0
  6. package/dist/{agent-prompt-loader-CBj2DWAJ.js → agent-prompt-loader-BRKe3VE6.js} +15 -7
  7. package/dist/agent-prompt-loader-BRKe3VE6.js.map +1 -0
  8. package/dist/{agent-runner-DA5YfjOD.js → agent-runner-B4ZdNkj4.js} +215 -42
  9. package/dist/agent-runner-B4ZdNkj4.js.map +1 -0
  10. package/dist/analytics-BlE8Yjoh.js +2 -0
  11. package/dist/{analytics-BdKBzz4M.js → analytics-Duy9Vfk-.js} +2 -2
  12. package/dist/{analytics-BdKBzz4M.js.map → analytics-Duy9Vfk-.js.map} +1 -1
  13. package/dist/{api-aTkzJ9vG.js → api-DEb25KDf.js} +3 -3
  14. package/dist/{api-aTkzJ9vG.js.map → api-DEb25KDf.js.map} +1 -1
  15. package/dist/bin.js +256 -43
  16. package/dist/bin.js.map +1 -1
  17. package/dist/{bounded-fs-BU2L5mjO.js → bounded-fs-CvYIzQbg.js} +3 -3
  18. package/dist/{bounded-fs-BU2L5mjO.js.map → bounded-fs-CvYIzQbg.js.map} +1 -1
  19. package/dist/check-screens.tsx +15 -0
  20. package/dist/{ci-install-BnTAUOz8.js → ci-install-k2HWuLQ3.js} +4 -4
  21. package/dist/{ci-install-BnTAUOz8.js.map → ci-install-k2HWuLQ3.js.map} +1 -1
  22. package/dist/{debug-BTPYy9p7.js → debug-3FJsmlmC.js} +15 -8
  23. package/dist/debug-3FJsmlmC.js.map +1 -0
  24. package/dist/{debug-D2L-fM_I.js → debug-D7eikh3K.js} +1 -1
  25. package/dist/{env-api-key-cjD4yfGT.js → env-api-key-uk6HInaB.js} +4 -7
  26. package/dist/env-api-key-uk6HInaB.js.map +1 -0
  27. package/dist/{environment-lV2X1gS-.js → environment-Cyq61jKV.js} +4 -4
  28. package/dist/{environment-lV2X1gS-.js.map → environment-Cyq61jKV.js.map} +1 -1
  29. package/dist/{file-watcher-qqQBl5Em.js → file-watcher-DZDDYHrv.js} +2 -2
  30. package/dist/{file-watcher-qqQBl5Em.js.map → file-watcher-DZDDYHrv.js.map} +1 -1
  31. package/dist/{headless-ui-TSoOsa3j.js → headless-ui-MpPKNNH9.js} +2 -2
  32. package/dist/{headless-ui-TSoOsa3j.js.map → headless-ui-MpPKNNH9.js.map} +1 -1
  33. package/dist/{interactive-DMoXEiKo.js → interactive-BDCUPSmr.js} +2 -2
  34. package/dist/{interactive-DMoXEiKo.js.map → interactive-BDCUPSmr.js.map} +1 -1
  35. package/dist/{mcp-Dqu0_QzN.js → mcp-DOH54TGI.js} +2 -2
  36. package/dist/{mcp-Dqu0_QzN.js.map → mcp-DOH54TGI.js.map} +1 -1
  37. package/dist/{mcp-prompt-streaming-B4cYVDVP.js → mcp-prompt-streaming-BKY-srkT.js} +3 -3
  38. package/dist/{mcp-prompt-streaming-B4cYVDVP.js.map → mcp-prompt-streaming-BKY-srkT.js.map} +1 -1
  39. package/dist/{non-interactive-EG_lII4R.js → non-interactive-DOG-p4ku.js} +2 -2
  40. package/dist/{non-interactive-EG_lII4R.js.map → non-interactive-DOG-p4ku.js.map} +1 -1
  41. package/dist/{orchestrator-tools-DrhpNOKK.js → orchestrator-tools-CfaYl_W4.js} +13 -10
  42. package/dist/orchestrator-tools-CfaYl_W4.js.map +1 -0
  43. package/dist/{package-manager-z33XHEWO.js → package-manager-62MxCa1Y.js} +2 -2
  44. package/dist/{package-manager-z33XHEWO.js.map → package-manager-62MxCa1Y.js.map} +1 -1
  45. package/dist/{pi-CVJB0Ban.js → pi-DohBskuT.js} +11 -11
  46. package/dist/{pi-CVJB0Ban.js.map → pi-DohBskuT.js.map} +1 -1
  47. package/dist/{playground-BjWqoWhf.js → playground-DaTeK9-L.js} +6 -6
  48. package/dist/{playground-BjWqoWhf.js.map → playground-DaTeK9-L.js.map} +1 -1
  49. package/dist/{posthog-3ijs_66W.js → posthog-DvM4V-to.js} +2 -2
  50. package/dist/{posthog-3ijs_66W.js.map → posthog-DvM4V-to.js.map} +1 -1
  51. package/dist/{posthog-integration-C3wuB5B1.js → posthog-integration-BHqFQd4U.js} +54 -24
  52. package/dist/{posthog-integration-C3wuB5B1.js.map → posthog-integration-BHqFQd4U.js.map} +1 -1
  53. package/dist/{provisioning-IKacrvWB.js → provisioning-B41dKvno.js} +3 -3
  54. package/dist/{provisioning-IKacrvWB.js.map → provisioning-B41dKvno.js.map} +1 -1
  55. package/dist/{queue-tools-zM8_mxx6.js → queue-tools-DED9kPuc.js} +117 -19
  56. package/dist/queue-tools-DED9kPuc.js.map +1 -0
  57. package/dist/{registry-DF_aKyS0.js → registry-Dh9aqVC6.js} +5 -5
  58. package/dist/{registry-DF_aKyS0.js.map → registry-Dh9aqVC6.js.map} +1 -1
  59. package/dist/{security-CVFKNYBn.js → security-HZDvt7uh.js} +5 -5
  60. package/dist/{security-CVFKNYBn.js.map → security-HZDvt7uh.js.map} +1 -1
  61. package/dist/{setup-utils-iINkc0xu.js → setup-utils-DJF-LjYE.js} +101 -19
  62. package/dist/setup-utils-DJF-LjYE.js.map +1 -0
  63. package/dist/{start-tui-CjtL-ls7.js → start-tui-0p-Ybwxe.js} +40 -18
  64. package/dist/start-tui-0p-Ybwxe.js.map +1 -0
  65. package/dist/{steps-DzUGHdfY.js → steps-BSp0kHUG.js} +7 -7
  66. package/dist/{steps-DzUGHdfY.js.map → steps-BSp0kHUG.js.map} +1 -1
  67. package/dist/{store-DtckpbSC.js → store-BgOhG49V.js} +5 -4
  68. package/dist/store-BgOhG49V.js.map +1 -0
  69. package/dist/{subagent-cD_bF490.js → subagent-DDsAb9we.js} +2 -2
  70. package/dist/{subagent-cD_bF490.js.map → subagent-DDsAb9we.js.map} +1 -1
  71. package/dist/{task-eIt2iATF.js → task-CCZAWF2U.js} +12 -12
  72. package/dist/{task-eIt2iATF.js.map → task-CCZAWF2U.js.map} +1 -1
  73. package/dist/{task-stream-DGBUveUD.js → task-stream-B0Eo4KX1.js} +4 -4
  74. package/dist/{task-stream-DGBUveUD.js.map → task-stream-B0Eo4KX1.js.map} +1 -1
  75. package/dist/{tasks-wQ3V6Ft6.js → tasks-DRqU9G95.js} +2 -2
  76. package/dist/{tasks-wQ3V6Ft6.js.map → tasks-DRqU9G95.js.map} +1 -1
  77. package/dist/{telemetry-BWYDFAwf.js → telemetry-4VTeK1h9.js} +3 -3
  78. package/dist/{telemetry-BWYDFAwf.js.map → telemetry-4VTeK1h9.js.map} +1 -1
  79. package/dist/{terminal-yKJZx1UC.js → terminal-CDqEDXWF.js} +16 -13
  80. package/dist/terminal-CDqEDXWF.js.map +1 -0
  81. package/dist/{tools-CDlaJb_e.js → tools-CjNt0TUI.js} +6 -6
  82. package/dist/{tools-CDlaJb_e.js.map → tools-CjNt0TUI.js.map} +1 -1
  83. package/dist/{wizard-abort-CztG5WTv.js → wizard-abort-BeuIRy5a.js} +3 -3
  84. package/dist/{wizard-abort-CztG5WTv.js.map → wizard-abort-BeuIRy5a.js.map} +1 -1
  85. package/dist/{wizard-abort-Cjk5z73h.js → wizard-abort-DNHeIZ8N.js} +1 -1
  86. package/dist/wizard-session-BUhlJ1b8.js.map +1 -1
  87. package/dist/yara-hooks-BE3OXk2V.js +2 -0
  88. package/dist/{yara-hooks-DeLjTIiJ.js → yara-hooks-CCGjiVA2.js} +4 -4
  89. package/dist/{yara-hooks-DeLjTIiJ.js.map → yara-hooks-CCGjiVA2.js.map} +1 -1
  90. package/package.json +1 -1
  91. package/dist/agent-interface-C90hyvs1.js.map +0 -1
  92. package/dist/agent-prompt-loader-CBj2DWAJ.js.map +0 -1
  93. package/dist/agent-runner-DA5YfjOD.js.map +0 -1
  94. package/dist/analytics-CijpxQFN.js +0 -2
  95. package/dist/debug-BTPYy9p7.js.map +0 -1
  96. package/dist/env-api-key-cjD4yfGT.js.map +0 -1
  97. package/dist/orchestrator-tools-DrhpNOKK.js.map +0 -1
  98. package/dist/queue-tools-zM8_mxx6.js.map +0 -1
  99. package/dist/setup-utils-iINkc0xu.js.map +0 -1
  100. package/dist/start-tui-CjtL-ls7.js.map +0 -1
  101. package/dist/store-DtckpbSC.js.map +0 -1
  102. package/dist/terminal-yKJZx1UC.js.map +0 -1
  103. package/dist/yara-hooks-DVGKhwaz.js +0 -2
@@ -1,7 +1,7 @@
1
- import { s as logToFile } from "./debug-BTPYy9p7.js";
2
- import { n as analytics } from "./analytics-BdKBzz4M.js";
3
- import { a as wizardCanUseTool } from "./agent-interface-C90hyvs1.js";
4
- import { C as scanVerdict, c as isWizardDocumentationPath, d as repeatBlockReason, f as scanAndTriage, i as createRepeatBlockTracker, u as recordExternalScan } from "./yara-hooks-DeLjTIiJ.js";
1
+ import { s as logToFile } from "./debug-3FJsmlmC.js";
2
+ import { n as analytics } from "./analytics-Duy9Vfk-.js";
3
+ import { a as wizardCanUseTool } from "./agent-interface-BQfwaSe-.js";
4
+ import { C as scanVerdict, c as isWizardDocumentationPath, d as repeatBlockReason, f as scanAndTriage, i as createRepeatBlockTracker, u as recordExternalScan } from "./yara-hooks-CCGjiVA2.js";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
7
  //#region src/lib/agent/runner/harness/pi/security.ts
@@ -339,4 +339,4 @@ function createSecurityExtension(ctx = {}) {
339
339
  //#endregion
340
340
  export { createSecurityExtension };
341
341
 
342
- //# sourceMappingURL=security-CVFKNYBn.js.map
342
+ //# sourceMappingURL=security-HZDvt7uh.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"security-CVFKNYBn.js","names":[],"sources":["../src/lib/agent/runner/harness/pi/security.ts"],"sourcesContent":["/**\n * Fail-closed security for the pi backend (#525). pi has no built-in\n * permission layer, so we attach an extension that intercepts every tool call\n * — built-in (bash/read/edit/write/grep) AND custom — through pi's `tool_call`\n * hook and reuses the EXACT anthropic policy: `wizardCanUseTool` (the bash\n * allowlist + .env fencing) plus the YARA pre-scan. A `tool_result` hook\n * post-scans output. Both fail closed: a scanner error blocks, and a critical\n * post-scan violation latches so every subsequent tool call is blocked and the\n * run terminates as a YARA violation.\n *\n * Scanning runs through @posthog/warlock via yara-hooks' `scanAndTriage` — the\n * same engine, rules, chunking, and LLM triage policy as the anthropic\n * harness. pi handlers are async (pi's ExtensionHandler accepts promises), so\n * the WASM scan awaits inline.\n *\n * This is the one fence. Subagents run their own pi session with the SAME\n * extension installed (see subagent.ts), so a child cannot escape it.\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport type { LLMProvider, ScanMatch } from '@posthog/warlock';\nimport { wizardCanUseTool } from '@lib/agent/agent-interface';\nimport {\n createRepeatBlockTracker,\n isWizardDocumentationPath,\n recordExternalScan,\n repeatBlockReason,\n scanAndTriage,\n type RepeatBlockTracker,\n} from '@lib/yara-hooks';\nimport { scanVerdict, type ScanContext } from '@lib/yara-policy';\nimport { logToFile } from '@utils/debug';\nimport { analytics } from '@utils/analytics';\n\n/** warlock ScanMatch → the report shape `recordExternalScan` expects. */\nfunction toReportViolation(m: ScanMatch) {\n return {\n rule: m.rule,\n severity: m.metadata.severity ?? 'unknown',\n category: m.metadata.category ?? 'unknown',\n description: m.metadata.description ?? '',\n };\n}\n\n/** Runaway backstop: hard cap on tool calls per (sub)agent session. */\nexport const MAX_TOOL_CALLS = 250;\n\n/**\n * Passive telemetry for an unfixed upstream failure: gpt-5.x streaming with\n * tools leaks its function-call grammar into string values, and the leak lands\n * on disk (run 9704a73e shipped `…functions.complete_task (commentary…` plus a\n * DEL byte inside a customer's global-error.tsx). Observe-only — never blocks.\n * Prior art: https://github.com/BerriAI/litellm/issues/14260 (closed stale),\n * https://community.openai.com/t/1386422 (gpt-5.6-luna, no fix).\n */\nconst TRANSPORT_LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [\n // The litellm#14260 signature, scoped to the wizard's own tool names — a\n // generic `functions.*` would false-positive on real code (Firebase).\n {\n pattern: /functions\\.(?:complete_task|enqueue_task|read_handoffs)/,\n label: 'leaked tool-call tokens',\n },\n {\n pattern: /<\\|(?:channel|constrain|message|call|end|start|return)\\|>/,\n label: 'leaked channel markers',\n },\n // C0 controls and DEL minus tab/newline/CR — never valid in source text.\n // eslint-disable-next-line no-control-regex\n { pattern: /[\\0-\\x08\\x0B\\f\\x0E-\\x1F\\x7F]/, label: 'control characters' },\n];\n\n/** Capture one event per leaking write/edit: which pattern fired and where it sat — no matched text, ever. */\nexport function observeTransportLeak(tool: string, content: string): void {\n for (const { pattern, label } of TRANSPORT_LEAK_PATTERNS) {\n const match = pattern.exec(content);\n if (!match) continue;\n analytics.wizardCapture('file content leak observed', {\n tool,\n leak: label,\n leak_offset: match.index,\n content_length: content.length,\n // End-of-string leaks are the upstream decoder signature.\n at_end: content.length - match.index < 80,\n });\n logToFile(\n `[transport-leak] observed ${tool}: ${label} at ${match.index}/${content.length}`,\n );\n return;\n }\n}\n\nexport interface ToolGateContext {\n disallowedTools?: readonly string[];\n /** True while a wizard_ask overlay is open (interactive); blocks Write/Edit. */\n getWizardAskPending?: () => boolean;\n /** Scan-triage classifier, resolved once in bootstrap. Absent → every flagged match is acted on. */\n triageProvider?: LLMProvider;\n /**\n * Per-run tracker of YARA-blocked payloads. When the agent retries content\n * that was already blocked, the block reason escalates (\"change the code,\n * not the retry\" → \"report it and move on\"). The extension wires one in;\n * absent (tests calling `evaluateToolCall` directly) every block reads as\n * a first attempt.\n */\n repeatTracker?: RepeatBlockTracker;\n /** Project root; rm is only allowed for files resolving strictly inside it. */\n workingDirectory?: string;\n}\n\nexport interface GateDecision {\n block: boolean;\n reason?: string;\n}\n\nconst str = (v: unknown): string => (typeof v === 'string' ? v : '');\n\n// Shell metacharacters that chain, substitute, or redirect. A command carrying\n// any of these is more than one action, so we never treat it as a plain `rm`.\nconst SHELL_OPERATORS = /[;&|`$(){}<>\\n'\"\\\\]/;\n\n/** True when a target resolves to a file strictly inside the project root. */\nfunction isDeletableProjectFile(\n target: string,\n root: string,\n p: typeof path,\n): boolean {\n if (target.startsWith('-')) return false; // a flag, not a file\n if (/[*?[\\]~]/.test(target)) return false; // glob / home expansion\n if (p.basename(target).startsWith('.env')) return false; // secrets\n\n const resolved = p.resolve(root, target);\n return resolved !== root && resolved.startsWith(root + p.sep);\n}\n\n/**\n * A plain `rm [-f] <file...>` whose every target is a file inside the project\n * root — the deletion shape the anthropic arm permits (there bash isn't\n * allowlisted; the shared YARA destructive-delete rules catch the dangerous\n * forms). pi rescues the same shape so the fall-through YARA scan can judge\n * it, but refuses any shell operator so it can't smuggle a second command.\n * Path checks run through the host's `path` (win32 on Windows, posix elsewhere);\n * pi always executes commands via a POSIX bash, so targets use forward slashes.\n */\nexport function isScopedFileRemoval(\n command: string,\n rawRoot: string | undefined,\n p: typeof path = path,\n): boolean {\n if (!rawRoot) return false; // no root to contain against → never rescue\n const root = p.resolve(rawRoot);\n const trimmed = command.trim();\n if (SHELL_OPERATORS.test(trimmed)) return false;\n\n const [executable, ...args] = trimmed.split(/\\s+/);\n if (executable !== 'rm') return false;\n\n if (args[0] === '-f') args.shift();\n if (args.length === 0) return false;\n\n return args.every((arg) => isDeletableProjectFile(arg, root, p));\n}\n\n/** A write that removes more than this fraction of an existing file's\n * non-whitespace content reads as a destructive whole-file rewrite. */\nconst OVERWRITE_SHRINK_LIMIT = 0.3;\n\n/** Existing files below this many non-whitespace chars are stubs; replacing one\n * wholesale is legitimate, so the shrink guard leaves them alone. */\nconst OVERWRITE_MIN_CHARS = 400;\n\nconst nonWhitespaceLength = (s: string): number => s.replace(/\\s+/g, '').length;\n\n/**\n * Refuse a write that replaces an existing file with far less content than it\n * held, steering the agent back to targeted edits. Prior art — the destructive\n * whole-file rewrite is a known model failure that targeted-edit formats exist to\n * prevent: Aider's search/replace and unified-diff, OpenAI's apply_patch,\n * Anthropic's str_replace with replace_all; pi's edit tool has no replace_all, so\n * a rejected scoped edit can fall back to a lossy full write. Additive by design:\n * the wizard only adds instrumentation, so a large shrink is the rewrite, not intent.\n */\nexport function overwriteShrinkReason(\n existing: string,\n next: string,\n): string | undefined {\n const before = nonWhitespaceLength(existing);\n if (before < OVERWRITE_MIN_CHARS) return undefined;\n const after = nonWhitespaceLength(next);\n if (after >= before * (1 - OVERWRITE_SHRINK_LIMIT)) return undefined;\n const removed = Math.round((1 - after / before) * 100);\n return `This write replaces an existing file and removes ~${removed}% of its content. Make targeted edits instead of rewriting the whole file; only write a file in full when creating it.`;\n}\n\n/**\n * Read the file a write would replace and flag a destructive shrink. A path that\n * does not exist yet is a new file, so it never triggers; with no project root to\n * resolve against (unit tests calling `evaluateToolCall` bare), it stays inert.\n */\nasync function overwriteShrinkBlock(\n input: Record<string, unknown>,\n workingDirectory: string | undefined,\n): Promise<string | undefined> {\n const target = str(input.path);\n if (!workingDirectory || !target) return undefined;\n let existing: string;\n try {\n existing = await fs.promises.readFile(\n path.resolve(workingDirectory, target),\n 'utf8',\n );\n } catch {\n return undefined; // new file (ENOENT) or unreadable — nothing to preserve\n }\n return overwriteShrinkReason(existing, str(input.content));\n}\n\n/**\n * Translate a pi tool name to the claude-cased name + input the shared policy\n * expects. pi field names (from the live tool stream): bash{command},\n * read/edit/write{path}, write adds {content}, edit adds {edits}, grep{path}.\n */\nfunction toClaudePolicyCall(\n toolName: string,\n input: Record<string, unknown>,\n): { name: string; input: Record<string, unknown> } {\n switch (toolName) {\n case 'bash':\n return { name: 'Bash', input: { command: str(input.command) } };\n case 'read':\n return { name: 'Read', input: { file_path: input.path } };\n case 'write':\n return { name: 'Write', input: { file_path: input.path } };\n case 'edit':\n return { name: 'Edit', input: { file_path: input.path } };\n case 'grep':\n return { name: 'Grep', input: { path: input.path } };\n default:\n // Custom tools (load_skill_menu, set_env_values, dispatch_agent, …) +\n // find/ls: no path/command, policy allows (their own handlers are fenced).\n return { name: toolName, input };\n }\n}\n\n/**\n * The agent-facing block message. Includes the rule's remediation so a blocked\n * agent can self-correct (e.g. \"move PII to identify()/$set\") instead of\n * retrying near-identical content.\n */\nfunction blockMessage(m: ScanMatch): string {\n const remediation = m.metadata.remediation\n ? ` Fix: ${m.metadata.remediation}.`\n : '';\n return `[YARA] ${m.rule}: ${\n m.metadata.description ?? 'security rule matched'\n }.${remediation} Blocked for security.`;\n}\n\n/**\n * YARA scan of the content a tool is about to act on, BEFORE it executes.\n * - bash → scan the command ('command'-context rules: exfiltration,\n * destructive, force-push). No triage — a flagged command always blocks,\n * same as the anthropic PreToolUse hook, so the LLM call would be wasted.\n * - write/edit → scan the content being written ('output'-context rules:\n * hardcoded keys, PII), WITH triage, and with the same wizard-doc\n * `posthog_pii` suppression the anthropic path uses so the agent's own\n * event-plan files aren't blocked.\n * Returns a block reason, or undefined to allow. Read/grep are post-scanned on\n * their output (in the tool_result hook), not here.\n */\nasync function preExecutionYaraBlock(\n toolName: string,\n input: Record<string, unknown>,\n llmProvider: LLMProvider | undefined,\n repeatTracker?: RepeatBlockTracker,\n): Promise<string | undefined> {\n let content: string;\n let ctx: ScanContext;\n let triage: LLMProvider | undefined;\n // Report labels for the scan report (phase/tool), matching the anthropic\n // hooks so pi and anthropic runs read the same in telemetry.\n let phase: string;\n let tool: string;\n switch (toolName) {\n case 'bash':\n content = str(input.command);\n ctx = 'command';\n triage = undefined;\n phase = 'PreToolUse';\n tool = 'Bash';\n break;\n case 'write':\n content = str(input.content);\n ctx = 'output';\n triage = llmProvider;\n phase = 'PostToolUse';\n tool = 'Write';\n break;\n case 'edit':\n // Scan ONLY the replacement text. `edits` is [{oldText, newText}] and\n // oldText is pre-existing file content the agent is changing — scanning\n // it blocks edits whose *surroundings* contain a violation, and even\n // edits that are removing one (field FPs: capture edits adjacent to\n // existing identify() PII; pre-existing violations blocking their own\n // replacement). Matches the anthropic path, which scans new_string only.\n content = (Array.isArray(input.edits) ? input.edits : [])\n .map((e) => str((e as Record<string, unknown>)?.newText))\n .join('\\n');\n ctx = 'output';\n triage = llmProvider;\n phase = 'PostToolUse';\n tool = 'Edit';\n break;\n default:\n return undefined;\n }\n if (!content) return undefined;\n if (ctx === 'output') observeTransportLeak(tool, content);\n\n let matches = await scanAndTriage(content, ctx, triage);\n if (ctx === 'output' && isWizardDocumentationPath(str(input.path))) {\n matches = matches.filter((m) => m.metadata.category !== 'posthog_pii');\n }\n recordExternalScan(phase, tool, matches.map(toReportViolation), 'blocked');\n if (matches.length === 0) return undefined;\n\n const attempt = repeatTracker?.attempt(toolName, content) ?? 1;\n return repeatBlockReason(attempt, toolName, blockMessage(matches[0]));\n}\n\n/**\n * The pure gate decision for a single tool call. Reuses `wizardCanUseTool`\n * (deny → block) then the YARA content scan (match → block). Fail-closed: any\n * thrown error blocks.\n */\nexport async function evaluateToolCall(\n toolName: string,\n input: Record<string, unknown>,\n ctx: ToolGateContext = {},\n llmProvider?: LLMProvider,\n): Promise<GateDecision> {\n try {\n const policy = toClaudePolicyCall(toolName, input);\n const decision = wizardCanUseTool(policy.name, policy.input, {\n disallowedTools: ctx.disallowedTools,\n wizardAskPending: ctx.getWizardAskPending?.() ?? false,\n });\n // The allowlist is a pi-only restriction; the anthropic arm runs bash\n // unrestricted and leans on the shared YARA scan. Let a plain `rm` of\n // project files through to that same scan so pi matches that behavior.\n const allowedLikeAnthropic =\n toolName === 'bash' &&\n isScopedFileRemoval(str(input.command), ctx.workingDirectory);\n\n if (decision.behavior === 'deny' && !allowedLikeAnthropic) {\n return { block: true, reason: decision.message };\n }\n\n const yaraReason = await preExecutionYaraBlock(\n toolName,\n input,\n llmProvider,\n ctx.repeatTracker,\n );\n if (yaraReason) return { block: true, reason: yaraReason };\n\n if (toolName === 'write') {\n const shrinkReason = await overwriteShrinkBlock(\n input,\n ctx.workingDirectory,\n );\n if (shrinkReason) return { block: true, reason: shrinkReason };\n }\n\n return { block: false };\n } catch (err) {\n logToFile('[pi-security] gate error — failing closed:', err);\n return {\n block: true,\n reason: 'Security check failed; tool blocked (fail-closed).',\n };\n }\n}\n\n/** pi result tool names whose OUTPUT gets scanned for 'input'-context rules. */\nfunction isPostScanTool(toolName: string): boolean {\n return toolName === 'read' || toolName === 'bash';\n}\n\n/** Mutable state the backend reads after the run to classify the outcome. */\nexport interface SecurityState {\n criticalViolation: boolean;\n blockedCount: number;\n toolCalls: number;\n}\n\n/**\n * Build the pi security extension + the shared state the backend inspects.\n * Install the returned factory via `extensionFactories`; pass the same factory\n * into every subagent session so the fence is inherited.\n */\nexport function createSecurityExtension(ctx: ToolGateContext = {}): {\n factory: (pi: PiExtensionApiLike) => void;\n state: SecurityState;\n} {\n const state: SecurityState = {\n criticalViolation: false,\n blockedCount: 0,\n toolCalls: 0,\n };\n\n // One triage provider per extension. Undefined (no gateway auth) means\n // triage is skipped and every flagged match is acted on — fail closed.\n const llmProvider = ctx.triageProvider;\n\n // One tracker per extension = per (sub)agent session, so an identical\n // payload retried after a block gets the escalating reason.\n const gateCtx: ToolGateContext = {\n ...ctx,\n repeatTracker: ctx.repeatTracker ?? createRepeatBlockTracker(),\n };\n\n const factory = (pi: PiExtensionApiLike): void => {\n pi.on('tool_call', async (event) => {\n // A latched post-scan violation blocks everything that follows.\n if (state.criticalViolation) {\n return {\n block: true,\n reason: 'Run terminated by a security violation.',\n };\n }\n state.toolCalls += 1;\n if (state.toolCalls > MAX_TOOL_CALLS) {\n return {\n block: true,\n reason: `Stopped: exceeded ${MAX_TOOL_CALLS} tool calls (runaway guard).`,\n };\n }\n const decision = await evaluateToolCall(\n event.toolName,\n event.input ?? {},\n gateCtx,\n llmProvider,\n );\n if (decision.block) {\n state.blockedCount += 1;\n logToFile(`[pi-security] BLOCK ${event.toolName}: ${decision.reason}`);\n return { block: true, reason: decision.reason };\n }\n return {};\n });\n\n pi.on('tool_result', async (event) => {\n if (!isPostScanTool(event.toolName)) return {};\n const text = (event.content ?? [])\n .map((c) => (c && c.type === 'text' ? c.text : ''))\n .join('\\n');\n if (!text) return {};\n try {\n // 'input'-context rules (prompt injection in read content), with\n // triage — same policy as the anthropic PostToolUse Read hook.\n const matches = await scanAndTriage(text, 'input', llmProvider);\n const verdict = scanVerdict(matches);\n // Count the scan even when clean, like the anthropic hooks do.\n recordExternalScan(\n 'PostToolUse',\n event.toolName === 'read' ? 'Read' : 'Bash',\n matches.map(toReportViolation),\n verdict?.action ?? 'warned',\n );\n if (!verdict) return {};\n if (verdict.terminal) {\n state.criticalViolation = true;\n logToFile(\n `[pi-security] POST-SCAN VIOLATION ${event.toolName}: ${verdict.match.rule}`,\n );\n } else {\n logToFile(\n `[pi-security] POST-SCAN WARNING ${event.toolName}: ${\n verdict.match.rule\n } (severity: ${verdict.match.metadata.severity ?? 'unknown'})`,\n );\n }\n } catch (err) {\n // Fail closed: a scanner error on output latches a violation.\n state.criticalViolation = true;\n logToFile('[pi-security] post-scan error — failing closed:', err);\n }\n return {};\n });\n };\n\n return { factory, state };\n}\n\n/**\n * Minimal structural type for pi's ExtensionAPI — just the `on` overloads we\n * use. Kept local so this module has no value import from the pi SDK (so the\n * CommonJS unit tests can load it directly). Handlers may return promises —\n * pi's real ExtensionHandler is `(event, ctx) => Promise<R | void> | R | void`.\n */\nexport interface PiExtensionApiLike {\n on(\n event: 'tool_call',\n handler: (event: {\n toolName: string;\n input?: Record<string, unknown>;\n }) =>\n | { block?: boolean; reason?: string }\n | Promise<{ block?: boolean; reason?: string }>,\n ): void;\n on(\n event: 'tool_result',\n handler: (event: {\n toolName: string;\n content?: Array<{ type: string; text?: string }>;\n isError?: boolean;\n }) => Record<string, never> | Promise<Record<string, never>>,\n ): void;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"security-HZDvt7uh.js","names":[],"sources":["../src/lib/agent/runner/harness/pi/security.ts"],"sourcesContent":["/**\n * Fail-closed security for the pi backend (#525). pi has no built-in\n * permission layer, so we attach an extension that intercepts every tool call\n * — built-in (bash/read/edit/write/grep) AND custom — through pi's `tool_call`\n * hook and reuses the EXACT anthropic policy: `wizardCanUseTool` (the bash\n * allowlist + .env fencing) plus the YARA pre-scan. A `tool_result` hook\n * post-scans output. Both fail closed: a scanner error blocks, and a critical\n * post-scan violation latches so every subsequent tool call is blocked and the\n * run terminates as a YARA violation.\n *\n * Scanning runs through @posthog/warlock via yara-hooks' `scanAndTriage` — the\n * same engine, rules, chunking, and LLM triage policy as the anthropic\n * harness. pi handlers are async (pi's ExtensionHandler accepts promises), so\n * the WASM scan awaits inline.\n *\n * This is the one fence. Subagents run their own pi session with the SAME\n * extension installed (see subagent.ts), so a child cannot escape it.\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport type { LLMProvider, ScanMatch } from '@posthog/warlock';\nimport { wizardCanUseTool } from '@lib/agent/agent-interface';\nimport {\n createRepeatBlockTracker,\n isWizardDocumentationPath,\n recordExternalScan,\n repeatBlockReason,\n scanAndTriage,\n type RepeatBlockTracker,\n} from '@lib/yara-hooks';\nimport { scanVerdict, type ScanContext } from '@lib/yara-policy';\nimport { logToFile } from '@utils/debug';\nimport { analytics } from '@utils/analytics';\n\n/** warlock ScanMatch → the report shape `recordExternalScan` expects. */\nfunction toReportViolation(m: ScanMatch) {\n return {\n rule: m.rule,\n severity: m.metadata.severity ?? 'unknown',\n category: m.metadata.category ?? 'unknown',\n description: m.metadata.description ?? '',\n };\n}\n\n/** Runaway backstop: hard cap on tool calls per (sub)agent session. */\nexport const MAX_TOOL_CALLS = 250;\n\n/**\n * Passive telemetry for an unfixed upstream failure: gpt-5.x streaming with\n * tools leaks its function-call grammar into string values, and the leak lands\n * on disk (run 9704a73e shipped `…functions.complete_task (commentary…` plus a\n * DEL byte inside a customer's global-error.tsx). Observe-only — never blocks.\n * Prior art: https://github.com/BerriAI/litellm/issues/14260 (closed stale),\n * https://community.openai.com/t/1386422 (gpt-5.6-luna, no fix).\n */\nconst TRANSPORT_LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [\n // The litellm#14260 signature, scoped to the wizard's own tool names — a\n // generic `functions.*` would false-positive on real code (Firebase).\n {\n pattern: /functions\\.(?:complete_task|enqueue_task|read_handoffs)/,\n label: 'leaked tool-call tokens',\n },\n {\n pattern: /<\\|(?:channel|constrain|message|call|end|start|return)\\|>/,\n label: 'leaked channel markers',\n },\n // C0 controls and DEL minus tab/newline/CR — never valid in source text.\n // eslint-disable-next-line no-control-regex\n { pattern: /[\\0-\\x08\\x0B\\f\\x0E-\\x1F\\x7F]/, label: 'control characters' },\n];\n\n/** Capture one event per leaking write/edit: which pattern fired and where it sat — no matched text, ever. */\nexport function observeTransportLeak(tool: string, content: string): void {\n for (const { pattern, label } of TRANSPORT_LEAK_PATTERNS) {\n const match = pattern.exec(content);\n if (!match) continue;\n analytics.wizardCapture('file content leak observed', {\n tool,\n leak: label,\n leak_offset: match.index,\n content_length: content.length,\n // End-of-string leaks are the upstream decoder signature.\n at_end: content.length - match.index < 80,\n });\n logToFile(\n `[transport-leak] observed ${tool}: ${label} at ${match.index}/${content.length}`,\n );\n return;\n }\n}\n\nexport interface ToolGateContext {\n disallowedTools?: readonly string[];\n /** True while a wizard_ask overlay is open (interactive); blocks Write/Edit. */\n getWizardAskPending?: () => boolean;\n /** Scan-triage classifier, resolved once in bootstrap. Absent → every flagged match is acted on. */\n triageProvider?: LLMProvider;\n /**\n * Per-run tracker of YARA-blocked payloads. When the agent retries content\n * that was already blocked, the block reason escalates (\"change the code,\n * not the retry\" → \"report it and move on\"). The extension wires one in;\n * absent (tests calling `evaluateToolCall` directly) every block reads as\n * a first attempt.\n */\n repeatTracker?: RepeatBlockTracker;\n /** Project root; rm is only allowed for files resolving strictly inside it. */\n workingDirectory?: string;\n}\n\nexport interface GateDecision {\n block: boolean;\n reason?: string;\n}\n\nconst str = (v: unknown): string => (typeof v === 'string' ? v : '');\n\n// Shell metacharacters that chain, substitute, or redirect. A command carrying\n// any of these is more than one action, so we never treat it as a plain `rm`.\nconst SHELL_OPERATORS = /[;&|`$(){}<>\\n'\"\\\\]/;\n\n/** True when a target resolves to a file strictly inside the project root. */\nfunction isDeletableProjectFile(\n target: string,\n root: string,\n p: typeof path,\n): boolean {\n if (target.startsWith('-')) return false; // a flag, not a file\n if (/[*?[\\]~]/.test(target)) return false; // glob / home expansion\n if (p.basename(target).startsWith('.env')) return false; // secrets\n\n const resolved = p.resolve(root, target);\n return resolved !== root && resolved.startsWith(root + p.sep);\n}\n\n/**\n * A plain `rm [-f] <file...>` whose every target is a file inside the project\n * root — the deletion shape the anthropic arm permits (there bash isn't\n * allowlisted; the shared YARA destructive-delete rules catch the dangerous\n * forms). pi rescues the same shape so the fall-through YARA scan can judge\n * it, but refuses any shell operator so it can't smuggle a second command.\n * Path checks run through the host's `path` (win32 on Windows, posix elsewhere);\n * pi always executes commands via a POSIX bash, so targets use forward slashes.\n */\nexport function isScopedFileRemoval(\n command: string,\n rawRoot: string | undefined,\n p: typeof path = path,\n): boolean {\n if (!rawRoot) return false; // no root to contain against → never rescue\n const root = p.resolve(rawRoot);\n const trimmed = command.trim();\n if (SHELL_OPERATORS.test(trimmed)) return false;\n\n const [executable, ...args] = trimmed.split(/\\s+/);\n if (executable !== 'rm') return false;\n\n if (args[0] === '-f') args.shift();\n if (args.length === 0) return false;\n\n return args.every((arg) => isDeletableProjectFile(arg, root, p));\n}\n\n/** A write that removes more than this fraction of an existing file's\n * non-whitespace content reads as a destructive whole-file rewrite. */\nconst OVERWRITE_SHRINK_LIMIT = 0.3;\n\n/** Existing files below this many non-whitespace chars are stubs; replacing one\n * wholesale is legitimate, so the shrink guard leaves them alone. */\nconst OVERWRITE_MIN_CHARS = 400;\n\nconst nonWhitespaceLength = (s: string): number => s.replace(/\\s+/g, '').length;\n\n/**\n * Refuse a write that replaces an existing file with far less content than it\n * held, steering the agent back to targeted edits. Prior art — the destructive\n * whole-file rewrite is a known model failure that targeted-edit formats exist to\n * prevent: Aider's search/replace and unified-diff, OpenAI's apply_patch,\n * Anthropic's str_replace with replace_all; pi's edit tool has no replace_all, so\n * a rejected scoped edit can fall back to a lossy full write. Additive by design:\n * the wizard only adds instrumentation, so a large shrink is the rewrite, not intent.\n */\nexport function overwriteShrinkReason(\n existing: string,\n next: string,\n): string | undefined {\n const before = nonWhitespaceLength(existing);\n if (before < OVERWRITE_MIN_CHARS) return undefined;\n const after = nonWhitespaceLength(next);\n if (after >= before * (1 - OVERWRITE_SHRINK_LIMIT)) return undefined;\n const removed = Math.round((1 - after / before) * 100);\n return `This write replaces an existing file and removes ~${removed}% of its content. Make targeted edits instead of rewriting the whole file; only write a file in full when creating it.`;\n}\n\n/**\n * Read the file a write would replace and flag a destructive shrink. A path that\n * does not exist yet is a new file, so it never triggers; with no project root to\n * resolve against (unit tests calling `evaluateToolCall` bare), it stays inert.\n */\nasync function overwriteShrinkBlock(\n input: Record<string, unknown>,\n workingDirectory: string | undefined,\n): Promise<string | undefined> {\n const target = str(input.path);\n if (!workingDirectory || !target) return undefined;\n let existing: string;\n try {\n existing = await fs.promises.readFile(\n path.resolve(workingDirectory, target),\n 'utf8',\n );\n } catch {\n return undefined; // new file (ENOENT) or unreadable — nothing to preserve\n }\n return overwriteShrinkReason(existing, str(input.content));\n}\n\n/**\n * Translate a pi tool name to the claude-cased name + input the shared policy\n * expects. pi field names (from the live tool stream): bash{command},\n * read/edit/write{path}, write adds {content}, edit adds {edits}, grep{path}.\n */\nfunction toClaudePolicyCall(\n toolName: string,\n input: Record<string, unknown>,\n): { name: string; input: Record<string, unknown> } {\n switch (toolName) {\n case 'bash':\n return { name: 'Bash', input: { command: str(input.command) } };\n case 'read':\n return { name: 'Read', input: { file_path: input.path } };\n case 'write':\n return { name: 'Write', input: { file_path: input.path } };\n case 'edit':\n return { name: 'Edit', input: { file_path: input.path } };\n case 'grep':\n return { name: 'Grep', input: { path: input.path } };\n default:\n // Custom tools (load_skill_menu, set_env_values, dispatch_agent, …) +\n // find/ls: no path/command, policy allows (their own handlers are fenced).\n return { name: toolName, input };\n }\n}\n\n/**\n * The agent-facing block message. Includes the rule's remediation so a blocked\n * agent can self-correct (e.g. \"move PII to identify()/$set\") instead of\n * retrying near-identical content.\n */\nfunction blockMessage(m: ScanMatch): string {\n const remediation = m.metadata.remediation\n ? ` Fix: ${m.metadata.remediation}.`\n : '';\n return `[YARA] ${m.rule}: ${\n m.metadata.description ?? 'security rule matched'\n }.${remediation} Blocked for security.`;\n}\n\n/**\n * YARA scan of the content a tool is about to act on, BEFORE it executes.\n * - bash → scan the command ('command'-context rules: exfiltration,\n * destructive, force-push). No triage — a flagged command always blocks,\n * same as the anthropic PreToolUse hook, so the LLM call would be wasted.\n * - write/edit → scan the content being written ('output'-context rules:\n * hardcoded keys, PII), WITH triage, and with the same wizard-doc\n * `posthog_pii` suppression the anthropic path uses so the agent's own\n * event-plan files aren't blocked.\n * Returns a block reason, or undefined to allow. Read/grep are post-scanned on\n * their output (in the tool_result hook), not here.\n */\nasync function preExecutionYaraBlock(\n toolName: string,\n input: Record<string, unknown>,\n llmProvider: LLMProvider | undefined,\n repeatTracker?: RepeatBlockTracker,\n): Promise<string | undefined> {\n let content: string;\n let ctx: ScanContext;\n let triage: LLMProvider | undefined;\n // Report labels for the scan report (phase/tool), matching the anthropic\n // hooks so pi and anthropic runs read the same in telemetry.\n let phase: string;\n let tool: string;\n switch (toolName) {\n case 'bash':\n content = str(input.command);\n ctx = 'command';\n triage = undefined;\n phase = 'PreToolUse';\n tool = 'Bash';\n break;\n case 'write':\n content = str(input.content);\n ctx = 'output';\n triage = llmProvider;\n phase = 'PostToolUse';\n tool = 'Write';\n break;\n case 'edit':\n // Scan ONLY the replacement text. `edits` is [{oldText, newText}] and\n // oldText is pre-existing file content the agent is changing — scanning\n // it blocks edits whose *surroundings* contain a violation, and even\n // edits that are removing one (field FPs: capture edits adjacent to\n // existing identify() PII; pre-existing violations blocking their own\n // replacement). Matches the anthropic path, which scans new_string only.\n content = (Array.isArray(input.edits) ? input.edits : [])\n .map((e) => str((e as Record<string, unknown>)?.newText))\n .join('\\n');\n ctx = 'output';\n triage = llmProvider;\n phase = 'PostToolUse';\n tool = 'Edit';\n break;\n default:\n return undefined;\n }\n if (!content) return undefined;\n if (ctx === 'output') observeTransportLeak(tool, content);\n\n let matches = await scanAndTriage(content, ctx, triage);\n if (ctx === 'output' && isWizardDocumentationPath(str(input.path))) {\n matches = matches.filter((m) => m.metadata.category !== 'posthog_pii');\n }\n recordExternalScan(phase, tool, matches.map(toReportViolation), 'blocked');\n if (matches.length === 0) return undefined;\n\n const attempt = repeatTracker?.attempt(toolName, content) ?? 1;\n return repeatBlockReason(attempt, toolName, blockMessage(matches[0]));\n}\n\n/**\n * The pure gate decision for a single tool call. Reuses `wizardCanUseTool`\n * (deny → block) then the YARA content scan (match → block). Fail-closed: any\n * thrown error blocks.\n */\nexport async function evaluateToolCall(\n toolName: string,\n input: Record<string, unknown>,\n ctx: ToolGateContext = {},\n llmProvider?: LLMProvider,\n): Promise<GateDecision> {\n try {\n const policy = toClaudePolicyCall(toolName, input);\n const decision = wizardCanUseTool(policy.name, policy.input, {\n disallowedTools: ctx.disallowedTools,\n wizardAskPending: ctx.getWizardAskPending?.() ?? false,\n });\n // The allowlist is a pi-only restriction; the anthropic arm runs bash\n // unrestricted and leans on the shared YARA scan. Let a plain `rm` of\n // project files through to that same scan so pi matches that behavior.\n const allowedLikeAnthropic =\n toolName === 'bash' &&\n isScopedFileRemoval(str(input.command), ctx.workingDirectory);\n\n if (decision.behavior === 'deny' && !allowedLikeAnthropic) {\n return { block: true, reason: decision.message };\n }\n\n const yaraReason = await preExecutionYaraBlock(\n toolName,\n input,\n llmProvider,\n ctx.repeatTracker,\n );\n if (yaraReason) return { block: true, reason: yaraReason };\n\n if (toolName === 'write') {\n const shrinkReason = await overwriteShrinkBlock(\n input,\n ctx.workingDirectory,\n );\n if (shrinkReason) return { block: true, reason: shrinkReason };\n }\n\n return { block: false };\n } catch (err) {\n logToFile('[pi-security] gate error — failing closed:', err);\n return {\n block: true,\n reason: 'Security check failed; tool blocked (fail-closed).',\n };\n }\n}\n\n/** pi result tool names whose OUTPUT gets scanned for 'input'-context rules. */\nfunction isPostScanTool(toolName: string): boolean {\n return toolName === 'read' || toolName === 'bash';\n}\n\n/** Mutable state the backend reads after the run to classify the outcome. */\nexport interface SecurityState {\n criticalViolation: boolean;\n blockedCount: number;\n toolCalls: number;\n}\n\n/**\n * Build the pi security extension + the shared state the backend inspects.\n * Install the returned factory via `extensionFactories`; pass the same factory\n * into every subagent session so the fence is inherited.\n */\nexport function createSecurityExtension(ctx: ToolGateContext = {}): {\n factory: (pi: PiExtensionApiLike) => void;\n state: SecurityState;\n} {\n const state: SecurityState = {\n criticalViolation: false,\n blockedCount: 0,\n toolCalls: 0,\n };\n\n // One triage provider per extension. Undefined (no gateway auth) means\n // triage is skipped and every flagged match is acted on — fail closed.\n const llmProvider = ctx.triageProvider;\n\n // One tracker per extension = per (sub)agent session, so an identical\n // payload retried after a block gets the escalating reason.\n const gateCtx: ToolGateContext = {\n ...ctx,\n repeatTracker: ctx.repeatTracker ?? createRepeatBlockTracker(),\n };\n\n const factory = (pi: PiExtensionApiLike): void => {\n pi.on('tool_call', async (event) => {\n // A latched post-scan violation blocks everything that follows.\n if (state.criticalViolation) {\n return {\n block: true,\n reason: 'Run terminated by a security violation.',\n };\n }\n state.toolCalls += 1;\n if (state.toolCalls > MAX_TOOL_CALLS) {\n return {\n block: true,\n reason: `Stopped: exceeded ${MAX_TOOL_CALLS} tool calls (runaway guard).`,\n };\n }\n const decision = await evaluateToolCall(\n event.toolName,\n event.input ?? {},\n gateCtx,\n llmProvider,\n );\n if (decision.block) {\n state.blockedCount += 1;\n logToFile(`[pi-security] BLOCK ${event.toolName}: ${decision.reason}`);\n return { block: true, reason: decision.reason };\n }\n return {};\n });\n\n pi.on('tool_result', async (event) => {\n if (!isPostScanTool(event.toolName)) return {};\n const text = (event.content ?? [])\n .map((c) => (c && c.type === 'text' ? c.text : ''))\n .join('\\n');\n if (!text) return {};\n try {\n // 'input'-context rules (prompt injection in read content), with\n // triage — same policy as the anthropic PostToolUse Read hook.\n const matches = await scanAndTriage(text, 'input', llmProvider);\n const verdict = scanVerdict(matches);\n // Count the scan even when clean, like the anthropic hooks do.\n recordExternalScan(\n 'PostToolUse',\n event.toolName === 'read' ? 'Read' : 'Bash',\n matches.map(toReportViolation),\n verdict?.action ?? 'warned',\n );\n if (!verdict) return {};\n if (verdict.terminal) {\n state.criticalViolation = true;\n logToFile(\n `[pi-security] POST-SCAN VIOLATION ${event.toolName}: ${verdict.match.rule}`,\n );\n } else {\n logToFile(\n `[pi-security] POST-SCAN WARNING ${event.toolName}: ${\n verdict.match.rule\n } (severity: ${verdict.match.metadata.severity ?? 'unknown'})`,\n );\n }\n } catch (err) {\n // Fail closed: a scanner error on output latches a violation.\n state.criticalViolation = true;\n logToFile('[pi-security] post-scan error — failing closed:', err);\n }\n return {};\n });\n };\n\n return { factory, state };\n}\n\n/**\n * Minimal structural type for pi's ExtensionAPI — just the `on` overloads we\n * use. Kept local so this module has no value import from the pi SDK (so the\n * CommonJS unit tests can load it directly). Handlers may return promises —\n * pi's real ExtensionHandler is `(event, ctx) => Promise<R | void> | R | void`.\n */\nexport interface PiExtensionApiLike {\n on(\n event: 'tool_call',\n handler: (event: {\n toolName: string;\n input?: Record<string, unknown>;\n }) =>\n | { block?: boolean; reason?: string }\n | Promise<{ block?: boolean; reason?: string }>,\n ): void;\n on(\n event: 'tool_result',\n handler: (event: {\n toolName: string;\n content?: Array<{ type: string; text?: string }>;\n isError?: boolean;\n }) => Record<string, never> | Promise<Record<string, never>>,\n ): void;\n}\n"],"mappings":"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"}
@@ -1,11 +1,12 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-B_-DWIq7.js";
2
- import { H as POSTHOG_DEV_CLIENT_ID, I as ISSUES_URL, R as OAUTH_PORTS, X as POSTHOG_STATUS_PAGE_URL, Y as POSTHOG_PROXY_CLIENT_ID, f as getUI, k as DUMMY_PROJECT_API_KEY, ot as WIZARD_OAUTH_SCOPES, pt as WIZARD_USER_AGENT, rt as WIZARD_CONTACT_EMAIL, s as logToFile, z as OAUTH_TIMEOUT_MS } from "./debug-BTPYy9p7.js";
3
- import { n as analytics } from "./analytics-BdKBzz4M.js";
4
- import { i as withUtm, n as openTrackedLink, t as withProgress } from "./telemetry-BWYDFAwf.js";
5
- import { r as readFileHead } from "./bounded-fs-BU2L5mjO.js";
6
- import { a as detectRegion, c as getLlmGatewayUrl, d as resolveBaseUrl, l as getOAuthUrl, n as provisionNewAccount, o as getCloudUrl, s as getHost, t as ProvisionedAccountUnreadableError, u as getUiHostFromHost } from "./provisioning-IKacrvWB.js";
7
- import { a as fetchUserData, r as fetchProjectData } from "./api-aTkzJ9vG.js";
8
- import { i as wizardAbort } from "./wizard-abort-CztG5WTv.js";
2
+ import { H as POSTHOG_DEV_CLIENT_ID, I as ISSUES_URL, R as OAUTH_PORTS, X as POSTHOG_STATUS_PAGE_URL, Y as POSTHOG_PROXY_CLIENT_ID, f as getUI, k as DUMMY_PROJECT_API_KEY, ot as WIZARD_OAUTH_SCOPES, pt as WIZARD_USER_AGENT, rt as WIZARD_CONTACT_EMAIL, s as logToFile, z as OAUTH_TIMEOUT_MS } from "./debug-3FJsmlmC.js";
3
+ import { n as analytics } from "./analytics-Duy9Vfk-.js";
4
+ import { i as withUtm, n as openTrackedLink, t as withProgress } from "./telemetry-4VTeK1h9.js";
5
+ import { r as readFileHead } from "./bounded-fs-CvYIzQbg.js";
6
+ import { a as detectRegion, c as getLlmGatewayUrl, d as resolveBaseUrl, l as getOAuthUrl, n as provisionNewAccount, o as getCloudUrl, s as getHost, t as ProvisionedAccountUnreadableError, u as getUiHostFromHost } from "./provisioning-B41dKvno.js";
7
+ import { a as fetchUserData, r as fetchProjectData } from "./api-DEb25KDf.js";
8
+ import "./wizard-session-BUhlJ1b8.js";
9
+ import { i as wizardAbort } from "./wizard-abort-BeuIRy5a.js";
9
10
  import { major, minVersion } from "semver";
10
11
  import * as fs$2 from "fs";
11
12
  import * as path$1 from "path";
@@ -279,6 +280,34 @@ const WAREHOUSE_SOURCE_SCOPE_ADDITIONS = ["external_data_source:read", "external
279
280
  */
280
281
  const CONNECT_SLACK_SCOPE_ADDITIONS = ["integration:read"];
281
282
  /**
283
+ * Extra scopes the replay-vision program needs on top of `WIZARD_OAUTH_SCOPES`.
284
+ * The same set self-driving's step 6c uses, narrowed to just this flow:
285
+ * • replay_scanner:read / replay_scanner:write — the scanner tasks list the
286
+ * team's existing scanners and create the ones scoped to the product's key
287
+ * flows (`vision-scanners-list` / `-create`, plus the advisory
288
+ * `vision-scanners-estimate-create` / `vision-quota-retrieve`). The scope
289
+ * OBJECT is `replay_scanner`; `vision-scanners-*` are MCP tool names.
290
+ * • session_recording:read — the scanner API pairs it with
291
+ * `replay_scanner:*`, since a scanner's config indirectly exposes
292
+ * recording contents. Configuring a scanner fails without it.
293
+ * • product_enablement:write — the enable-replay task's server half turns on
294
+ * Session Replay (`products-enable`) so there are recordings to scan.
295
+ *
296
+ * Without these the PostHog MCP omits the tools from the catalog it serves
297
+ * this token, every scanner task takes its "tool unknown" skip path, and the
298
+ * run reports success having created nothing (run 69afc6f8).
299
+ *
300
+ * No OAuth-ceiling edit needed — all are unprivileged public scope objects
301
+ * covered by the apps' `@default` sentinel, and self-driving already requests
302
+ * every one of them.
303
+ */
304
+ const REPLAY_VISION_SCOPE_ADDITIONS = [
305
+ "session_recording:read",
306
+ "product_enablement:write",
307
+ "replay_scanner:read",
308
+ "replay_scanner:write"
309
+ ];
310
+ /**
282
311
  * Per-program scope additions, layered on top of `WIZARD_OAUTH_SCOPES`.
283
312
  *
284
313
  * Programs not listed here request the unchanged base set. Use this
@@ -295,7 +324,8 @@ const PROGRAM_SCOPE_ADDITIONS = {
295
324
  "self-driving": SELF_DRIVING_SCOPE_ADDITIONS,
296
325
  "warehouse-source": WAREHOUSE_SOURCE_SCOPE_ADDITIONS,
297
326
  "posthog-integration": [...CONNECT_SLACK_SCOPE_ADDITIONS, ...WAREHOUSE_SOURCE_SCOPE_ADDITIONS],
298
- slack: CONNECT_SLACK_SCOPE_ADDITIONS
327
+ slack: CONNECT_SLACK_SCOPE_ADDITIONS,
328
+ "replay-vision": REPLAY_VISION_SCOPE_ADDITIONS
299
329
  };
300
330
  /**
301
331
  * Resolve the OAuth scope list to request for a given program. Returns
@@ -621,9 +651,23 @@ const WIZARD_COMPLETION_SCOPE = "event_definition:write";
621
651
  function parseOAuthScopes(scope) {
622
652
  return scope.split(/\s+/).filter(Boolean);
623
653
  }
654
+ /**
655
+ * Requested scopes the grant came back without.
656
+ *
657
+ * A token can legitimately carry fewer scopes than the wizard asked for: the
658
+ * consent screen lets the user deselect any scope the OAuth app doesn't mark
659
+ * required, and anything outside the app's ceiling is clamped server-side.
660
+ * Neither path is an error — `/oauth/token` just returns a narrower `scope`.
661
+ * Diff it at login, where the gap is fixable, rather than letting the run
662
+ * discover it as a permission failure on some API call minutes in.
663
+ */
664
+ function missingOAuthScopes(requested, grantedScope) {
665
+ const granted = new Set(parseOAuthScopes(grantedScope));
666
+ return requested.filter((scope) => !granted.has(scope));
667
+ }
624
668
  function assertWizardCompletionScope(scope) {
625
669
  if (parseOAuthScopes(scope).includes("event_definition:write")) return;
626
- throw new Error(`Your existing PostHog Wizard authorization is missing the ${WIZARD_COMPLETION_SCOPE} permission required to finish setup. Reconnect the wizard and approve the updated permissions. If PostHog reuses the old approval, revoke the existing Wizard authorization first, then rerun the wizard.`);
670
+ throw new Error(`This run was authorized without the ${WIZARD_COMPLETION_SCOPE} permission, which the wizard needs to finish setup. Please try again, approving all permissions on the PostHog authorization screen. If that screen does not reappear, revoke the existing PostHog Wizard authorization in your PostHog settings first.`);
627
671
  }
628
672
  const AUTHORIZATION_TIMEOUT_MESSAGE = "Authorization timed out";
629
673
  function isAuthorizationTimeout(error) {
@@ -836,6 +880,26 @@ async function exchangeCodeForToken(code, codeVerifier, callbackUrl, baseUrl) {
836
880
  logToFile(`[oauth] token exchange succeeded, granted scopes: ${token.scope}${token.posthog_region ? `, region: ${token.posthog_region}` : ""}${token.scoped_teams ? `, scoped_teams: [${token.scoped_teams.join(", ")}]` : ""}${token.scoped_organizations ? `, scoped_organizations: ${token.scoped_organizations.length}` : ""}`);
837
881
  return token;
838
882
  }
883
+ /**
884
+ * Warn — at login, while the user is still watching — when the grant came back
885
+ * narrower than the request, and record the gap so narrowed runs are countable.
886
+ * Non-fatal by design: deselecting an optional scope is the user's call, and
887
+ * most flows survive it. The one scope the wizard cannot run without has its
888
+ * own hard check (`assertWizardCompletionScope`).
889
+ */
890
+ function reportNarrowedGrant(requestedScopes, grantedScope) {
891
+ const missing = missingOAuthScopes(requestedScopes, grantedScope);
892
+ if (missing.length === 0) return;
893
+ logToFile(`[oauth] grant narrower than request, missing: ${missing.join(" ")}`);
894
+ analytics.wizardCapture("oauth grant narrowed", {
895
+ requested_scopes: [...requestedScopes].sort().join(" "),
896
+ granted_scopes: parseOAuthScopes(grantedScope).sort().join(" "),
897
+ missing_scopes: missing.join(" "),
898
+ missing_scope_count: missing.length
899
+ });
900
+ const plural = missing.length > 1;
901
+ getUI().log.warn(`Your PostHog authorization is missing ${plural ? `${missing.length} permissions` : "a permission"} the wizard asked for: ${missing.join(", ")}. Setup will continue, but steps that need ${plural ? "them" : "it"} may fail. To grant ${plural ? "them" : "it"}, re-run the wizard and approve all permissions on the authorization screen. If that screen does not reappear, revoke the existing PostHog Wizard authorization in your PostHog settings first.`);
902
+ }
839
903
  async function performOAuthFlow(config) {
840
904
  const clientId = getOAuthClientId(config.baseUrl);
841
905
  const oauthUrl = getOAuthUrl(config.baseUrl);
@@ -891,6 +955,7 @@ async function performOAuthFlow(config) {
891
955
  getUI().setLoginUrl(null);
892
956
  getUI().setAuthorizeUrl(null);
893
957
  loginSpinner.stop("Authorization complete!");
958
+ reportNarrowedGrant(config.scopes, token.scope);
894
959
  return token;
895
960
  } catch (e) {
896
961
  const error = e instanceof Error ? e : /* @__PURE__ */ new Error("Unknown error");
@@ -1151,10 +1216,11 @@ async function getOrAskForProjectData(_options) {
1151
1216
  projectId: projectData.id,
1152
1217
  roleAtOrganization,
1153
1218
  user,
1154
- project: projectData.project
1219
+ project: projectData.project,
1220
+ missingScopes: []
1155
1221
  };
1156
1222
  }
1157
- const { host, projectApiKey, accessToken, projectId, roleAtOrganization, user, project } = await withProgress("login", () => askForWizardLogin({
1223
+ const { host, projectApiKey, accessToken, projectId, roleAtOrganization, user, project, missingScopes } = await withProgress("login", () => askForWizardLogin({
1158
1224
  signup: _options.signup,
1159
1225
  email: _options.email,
1160
1226
  region: _options.region,
@@ -1180,7 +1246,8 @@ ${cloudUrl}/settings/project#variables`);
1180
1246
  projectId,
1181
1247
  roleAtOrganization: roleAtOrganization ?? null,
1182
1248
  user: user ?? null,
1183
- project: project ?? null
1249
+ project: project ?? null,
1250
+ missingScopes: missingScopes ?? []
1184
1251
  };
1185
1252
  }
1186
1253
  async function fetchProjectDataWithApiKey(apiKey, cloudUrl) {
@@ -1203,8 +1270,9 @@ async function fetchProjectDataById(apiKey, projectId, cloudUrl) {
1203
1270
  }
1204
1271
  async function askForWizardLogin(options) {
1205
1272
  if (options.signup) return askForProvisioningSignup(options.email, options.region, options.baseUrl, options.localMcp);
1273
+ const requestedScopes = [...getOAuthScopesForProgram(options.programId)];
1206
1274
  const tokenResponse = await performOAuthFlow({
1207
- scopes: [...getOAuthScopesForProgram(options.programId)],
1275
+ scopes: requestedScopes,
1208
1276
  signup: false,
1209
1277
  projectId: options.projectId,
1210
1278
  baseUrl: options.baseUrl
@@ -1213,12 +1281,25 @@ async function askForWizardLogin(options) {
1213
1281
  assertWizardCompletionScope(tokenResponse.scope);
1214
1282
  } catch (error) {
1215
1283
  const scopeError = error instanceof Error ? error : /* @__PURE__ */ new Error("OAuth scope check failed");
1284
+ const missing = missingOAuthScopes(requestedScopes, tokenResponse.scope);
1216
1285
  analytics.captureException(scopeError, {
1217
1286
  step: "wizard_login",
1218
1287
  missing_scope: "event_definition:write"
1219
1288
  });
1220
- getUI().log.error(scopeError.message);
1221
- await abort();
1289
+ await wizardAbort({
1290
+ message: scopeError.message,
1291
+ outroData: {
1292
+ kind: "error",
1293
+ message: "Setup needs permissions that were not granted",
1294
+ body: [
1295
+ "Missing permissions:",
1296
+ ...missing.map((scope) => ` • ${scope}`),
1297
+ "",
1298
+ "Re-run the wizard and approve all permissions on the PostHog authorization screen.",
1299
+ "If that screen does not reappear, revoke the existing PostHog Wizard authorization in your PostHog settings first."
1300
+ ].join("\n")
1301
+ }
1302
+ });
1222
1303
  }
1223
1304
  const resolution = resolveGrantedProject(options.projectId, tokenResponse.scoped_teams);
1224
1305
  if (!resolution.ok) {
@@ -1229,7 +1310,7 @@ async function askForWizardLogin(options) {
1229
1310
  granted_project_id: resolution.granted
1230
1311
  });
1231
1312
  getUI().log.error(error.message);
1232
- await abort();
1313
+ await abort(error.message);
1233
1314
  }
1234
1315
  const projectId = resolution.ok ? resolution.projectId : void 0;
1235
1316
  if (projectId === void 0) {
@@ -1239,7 +1320,7 @@ async function askForWizardLogin(options) {
1239
1320
  has_scoped_teams: !!tokenResponse.scoped_teams
1240
1321
  });
1241
1322
  getUI().log.error(error.message);
1242
- await abort();
1323
+ await abort(error.message);
1243
1324
  }
1244
1325
  const host = await HostResolution.fromAccessToken(tokenResponse.access_token, {
1245
1326
  region: tokenResponse.posthog_region,
@@ -1257,7 +1338,8 @@ async function askForWizardLogin(options) {
1257
1338
  projectId,
1258
1339
  roleAtOrganization: userData.role_at_organization ?? null,
1259
1340
  user: userData,
1260
- project: projectData
1341
+ project: projectData,
1342
+ missingScopes: missingOAuthScopes(requestedScopes, tokenResponse.scope)
1261
1343
  };
1262
1344
  getUI().log.success("Login complete.");
1263
1345
  analytics.setTag("opened-wizard-link", true);
@@ -1321,4 +1403,4 @@ async function askForProvisioningSignup(email, region, baseUrl, localMcp) {
1321
1403
  //#endregion
1322
1404
  export { createVersionBucket as a, detectAllPackageManagers as c, tryGetPackageJson as i, isUsingTypeScript as n, extractOAuthCode as o, setup_utils_exports as r, HostResolution as s, getOrAskForProjectData as t };
1323
1405
 
1324
- //# sourceMappingURL=setup-utils-iINkc0xu.js.map
1406
+ //# sourceMappingURL=setup-utils-DJF-LjYE.js.map