@posthog/wizard 2.41.0 → 2.43.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 (106) hide show
  1. package/dist/{add-mcp-server-to-clients-Bvv6Vh5y.js → add-mcp-server-to-clients-BiZmRLE6.js} +5 -5
  2. package/dist/{add-mcp-server-to-clients-Bvv6Vh5y.js.map → add-mcp-server-to-clients-BiZmRLE6.js.map} +1 -1
  3. package/dist/{agent-interface-CFeI8jzW.js → agent-interface-CUaNjvqq.js} +8 -9
  4. package/dist/agent-interface-CUaNjvqq.js.map +1 -0
  5. package/dist/{agent-runner-B0pWHl5j.js → agent-runner-BuH7W62W.js} +143 -64
  6. package/dist/agent-runner-BuH7W62W.js.map +1 -0
  7. package/dist/{analytics-C1qxjbYZ.js → analytics-C_9Kh6J1.js} +5 -2
  8. package/dist/analytics-C_9Kh6J1.js.map +1 -0
  9. package/dist/analytics-o-WJHD8Z.js +2 -0
  10. package/dist/{api-CEMR_Aa6.js → api-Kgk7BFqo.js} +3 -3
  11. package/dist/{api-CEMR_Aa6.js.map → api-Kgk7BFqo.js.map} +1 -1
  12. package/dist/bin.js +87 -68
  13. package/dist/bin.js.map +1 -1
  14. package/dist/{ci-install-BBZscv3z.js → ci-install-DoARcA-j.js} +4 -4
  15. package/dist/{ci-install-BBZscv3z.js.map → ci-install-DoARcA-j.js.map} +1 -1
  16. package/dist/{debug-DBrsvcym.js → debug-CI0PLD6X.js} +1 -1
  17. package/dist/{debug-BMkCmZdo.js → debug-DNEQR5DL.js} +9 -5
  18. package/dist/debug-DNEQR5DL.js.map +1 -0
  19. package/dist/{defaults-BxiMLkQY.js → defaults-CeNThA-1.js} +1 -1
  20. package/dist/{defaults-BxiMLkQY.js.map → defaults-CeNThA-1.js.map} +1 -1
  21. package/dist/{env-api-key-DDVkPQmo.js → env-api-key-cjD4yfGT.js} +1 -1
  22. package/dist/{env-api-key-DDVkPQmo.js.map → env-api-key-cjD4yfGT.js.map} +1 -1
  23. package/dist/{environment-B7DmpnXD.js → environment-D44UP2Nu.js} +3 -3
  24. package/dist/{environment-B7DmpnXD.js.map → environment-D44UP2Nu.js.map} +1 -1
  25. package/dist/{file-utils-BCxAu-vq.js → file-utils-C2PNKLhr.js} +2 -2
  26. package/dist/{file-utils-BCxAu-vq.js.map → file-utils-C2PNKLhr.js.map} +1 -1
  27. package/dist/{headless-ui-NFf-5XOW.js → headless-ui-C3C0wWUi.js} +2 -2
  28. package/dist/{headless-ui-NFf-5XOW.js.map → headless-ui-C3C0wWUi.js.map} +1 -1
  29. package/dist/{interactive-CBrzEFly.js → interactive-k4xQiftO.js} +2 -2
  30. package/dist/{interactive-CBrzEFly.js.map → interactive-k4xQiftO.js.map} +1 -1
  31. package/dist/{mcp-C7dn0DIC.js → mcp-CJn_ryMN.js} +15 -19
  32. package/dist/mcp-CJn_ryMN.js.map +1 -0
  33. package/dist/{mcp-prompt-streaming-uXF2xskx.js → mcp-prompt-streaming-BB5nO--L.js} +8 -10
  34. package/dist/mcp-prompt-streaming-BB5nO--L.js.map +1 -0
  35. package/dist/{non-interactive-CDYGf19o.js → non-interactive-0x0Due8h.js} +2 -2
  36. package/dist/{non-interactive-CDYGf19o.js.map → non-interactive-0x0Due8h.js.map} +1 -1
  37. package/dist/{package-manager-BHAvpGav.js → package-manager-CBRR4_i4.js} +2 -2
  38. package/dist/{package-manager-BHAvpGav.js.map → package-manager-CBRR4_i4.js.map} +1 -1
  39. package/dist/{playground-B8OhYzyc.js → playground-CK617U6k.js} +26 -21
  40. package/dist/playground-CK617U6k.js.map +1 -0
  41. package/dist/{posthog-BHbtnNQE.js → posthog-DwKlq0HM.js} +2 -2
  42. package/dist/posthog-DwKlq0HM.js.map +1 -0
  43. package/dist/{posthog-integration-G839iLK6.js → posthog-integration-qRjKkLcu.js} +25 -25
  44. package/dist/posthog-integration-qRjKkLcu.js.map +1 -0
  45. package/dist/{provisioning-BOhuDX6i.js → provisioning-BMXR8fS1.js} +67 -6
  46. package/dist/provisioning-BMXR8fS1.js.map +1 -0
  47. package/dist/{registry-CC26CjWO.js → registry-CsUUdVCd.js} +4 -4
  48. package/dist/{registry-CC26CjWO.js.map → registry-CsUUdVCd.js.map} +1 -1
  49. package/dist/{security-wlqqLk44.js → security-CIP3lpp6.js} +4 -4
  50. package/dist/{security-wlqqLk44.js.map → security-CIP3lpp6.js.map} +1 -1
  51. package/dist/{setup-utils-NwB2Qj6N.js → setup-utils-DkyEOBDx.js} +137 -32
  52. package/dist/setup-utils-DkyEOBDx.js.map +1 -0
  53. package/dist/{start-tui-DQndpyTU.js → start-tui-CJlIgYjf.js} +36 -30
  54. package/dist/start-tui-CJlIgYjf.js.map +1 -0
  55. package/dist/{steps-CC_Ge4sR.js → steps-CuNjxYsA.js} +7 -7
  56. package/dist/{steps-CC_Ge4sR.js.map → steps-CuNjxYsA.js.map} +1 -1
  57. package/dist/{store-1-pQpHlY.js → store-UKAJftq6.js} +5 -5
  58. package/dist/{store-1-pQpHlY.js.map → store-UKAJftq6.js.map} +1 -1
  59. package/dist/{subagent-FMDybA8X.js → subagent-_syej145.js} +2 -2
  60. package/dist/{subagent-FMDybA8X.js.map → subagent-_syej145.js.map} +1 -1
  61. package/dist/{task-stream-d-R9J0g4.js → task-stream-Ne-9ZOnf.js} +3 -3
  62. package/dist/{task-stream-d-R9J0g4.js.map → task-stream-Ne-9ZOnf.js.map} +1 -1
  63. package/dist/{tasks-BfDJpHgK.js → tasks-CUvCA1OY.js} +2 -2
  64. package/dist/{tasks-BfDJpHgK.js.map → tasks-CUvCA1OY.js.map} +1 -1
  65. package/dist/{telemetry-CcysC4ji.js → telemetry-DFYgJHH-.js} +3 -3
  66. package/dist/{telemetry-CcysC4ji.js.map → telemetry-DFYgJHH-.js.map} +1 -1
  67. package/dist/{terminal-CWpmzTgT.js → terminal-DLFgIxhg.js} +49 -25
  68. package/dist/terminal-DLFgIxhg.js.map +1 -0
  69. package/dist/{tools-BsOywKLp.js → tools-CNyhLqlK.js} +4 -4
  70. package/dist/{tools-BsOywKLp.js.map → tools-CNyhLqlK.js.map} +1 -1
  71. package/dist/{types-evL_rPob.js → types-y3HkjtSq.js} +1 -1
  72. package/dist/{types-evL_rPob.js.map → types-y3HkjtSq.js.map} +1 -1
  73. package/dist/{wizard-abort-56bo4Zhs.js → wizard-abort-BM3pISoR.js} +4 -4
  74. package/dist/{wizard-abort-56bo4Zhs.js.map → wizard-abort-BM3pISoR.js.map} +1 -1
  75. package/dist/{wizard-abort-7oUXxutu.js → wizard-abort-DCv08-op.js} +1 -1
  76. package/dist/{wizard-session-BoXAZEOf.js → wizard-session-BudcixsA.js} +1 -2
  77. package/dist/wizard-session-BudcixsA.js.map +1 -0
  78. package/dist/{wizard-session-C8W_6X0j.js → wizard-session-DXivb7kv.js} +1 -1
  79. package/dist/{wizard-tools-BXd4PCO3.js → wizard-tools-BcuRxp1A.js} +6 -4
  80. package/dist/wizard-tools-BcuRxp1A.js.map +1 -0
  81. package/dist/wizard-ui-WZ48rUgr.js.map +1 -1
  82. package/dist/yara-hooks-DKbfESYf.js +2 -0
  83. package/dist/{yara-hooks-BZmYgr9V.js → yara-hooks-GIs-R_g1.js} +4 -4
  84. package/dist/{yara-hooks-BZmYgr9V.js.map → yara-hooks-GIs-R_g1.js.map} +1 -1
  85. package/package.json +1 -1
  86. package/dist/agent-interface-CFeI8jzW.js.map +0 -1
  87. package/dist/agent-runner-B0pWHl5j.js.map +0 -1
  88. package/dist/analytics-C1qxjbYZ.js.map +0 -1
  89. package/dist/analytics-_8xKpUAa.js +0 -2
  90. package/dist/debug-BMkCmZdo.js.map +0 -1
  91. package/dist/host-resolution-UZELMVTQ.js +0 -115
  92. package/dist/host-resolution-UZELMVTQ.js.map +0 -1
  93. package/dist/mcp-C7dn0DIC.js.map +0 -1
  94. package/dist/mcp-prompt-streaming-uXF2xskx.js.map +0 -1
  95. package/dist/playground-B8OhYzyc.js.map +0 -1
  96. package/dist/posthog-BHbtnNQE.js.map +0 -1
  97. package/dist/posthog-integration-G839iLK6.js.map +0 -1
  98. package/dist/provisioning-BOhuDX6i.js.map +0 -1
  99. package/dist/setup-utils-NwB2Qj6N.js.map +0 -1
  100. package/dist/start-tui-DQndpyTU.js.map +0 -1
  101. package/dist/terminal-CWpmzTgT.js.map +0 -1
  102. package/dist/urls-PfZ1TYOf.js +0 -67
  103. package/dist/urls-PfZ1TYOf.js.map +0 -1
  104. package/dist/wizard-session-BoXAZEOf.js.map +0 -1
  105. package/dist/wizard-tools-BXd4PCO3.js.map +0 -1
  106. package/dist/yara-hooks-D0h7Vu7A.js +0 -2
@@ -1,6 +1,6 @@
1
- import { s as logToFile } from "./debug-BMkCmZdo.js";
2
- import { a as wizardCanUseTool, g as createTriageLLMProvider } from "./agent-interface-CFeI8jzW.js";
3
- import { c as isWizardDocumentationPath, d as repeatBlockReason, f as scanAndTriage, i as createRepeatBlockTracker, u as recordExternalScan } from "./yara-hooks-BZmYgr9V.js";
1
+ import { s as logToFile } from "./debug-DNEQR5DL.js";
2
+ import { a as wizardCanUseTool, g as createTriageLLMProvider } from "./agent-interface-CUaNjvqq.js";
3
+ import { c as isWizardDocumentationPath, d as repeatBlockReason, f as scanAndTriage, i as createRepeatBlockTracker, u as recordExternalScan } from "./yara-hooks-GIs-R_g1.js";
4
4
  import fs from "fs";
5
5
  import path from "path";
6
6
  //#region src/lib/agent/runner/harness/pi/security.ts
@@ -297,4 +297,4 @@ function createSecurityExtension(ctx = {}) {
297
297
  //#endregion
298
298
  export { createSecurityExtension };
299
299
 
300
- //# sourceMappingURL=security-wlqqLk44.js.map
300
+ //# sourceMappingURL=security-CIP3lpp6.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"security-wlqqLk44.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 type ScanContext,\n} from '@lib/yara-hooks';\nimport {\n createTriageLLMProvider,\n type TriageGatewayAuth,\n} from '@lib/agent/triage-provider';\nimport { logToFile } from '@utils/debug';\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\nexport interface ToolGateContext {\n disallowedTools?: readonly string[];\n /** True while a wizard_ask overlay is open (interactive); blocks Write/Edit. */\n getWizardAskPending?: () => boolean;\n /**\n * Gateway auth for the LLM triage pass. pi auths the gateway\n * programmatically and never sets ANTHROPIC_BASE_URL/AUTH_TOKEN on the env,\n * so without this triage silently no-ops and every flagged match is acted\n * on (fail-closed, but no false-positive filtering).\n */\n triageAuth?: TriageGatewayAuth;\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\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 = createTriageLLMProvider(ctx.triageAuth);\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 recordExternalScan(\n 'PostToolUse',\n event.toolName === 'read' ? 'Read' : 'Bash',\n matches.map(toReportViolation),\n 'aborted',\n );\n if (matches.length > 0) {\n state.criticalViolation = true;\n logToFile(\n `[pi-security] POST-SCAN VIOLATION ${event.toolName}: ${matches[0].rule}`,\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-CIP3lpp6.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 type ScanContext,\n} from '@lib/yara-hooks';\nimport {\n createTriageLLMProvider,\n type TriageGatewayAuth,\n} from '@lib/agent/triage-provider';\nimport { logToFile } from '@utils/debug';\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\nexport interface ToolGateContext {\n disallowedTools?: readonly string[];\n /** True while a wizard_ask overlay is open (interactive); blocks Write/Edit. */\n getWizardAskPending?: () => boolean;\n /**\n * Gateway auth for the LLM triage pass. pi auths the gateway\n * programmatically and never sets ANTHROPIC_BASE_URL/AUTH_TOKEN on the env,\n * so without this triage silently no-ops and every flagged match is acted\n * on (fail-closed, but no false-positive filtering).\n */\n triageAuth?: TriageGatewayAuth;\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\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 = createTriageLLMProvider(ctx.triageAuth);\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 recordExternalScan(\n 'PostToolUse',\n event.toolName === 'read' ? 'Read' : 'Bash',\n matches.map(toReportViolation),\n 'aborted',\n );\n if (matches.length > 0) {\n state.criticalViolation = true;\n logToFile(\n `[pi-security] POST-SCAN VIOLATION ${event.toolName}: ${matches[0].rule}`,\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,12 +1,10 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-B_-DWIq7.js";
2
- import { D as DUMMY_PROJECT_API_KEY, F as OAUTH_PORTS, G as POSTHOG_PROXY_CLIENT_ID, I as OAUTH_TIMEOUT_MS, K as POSTHOG_STATUS_PAGE_URL, N as ISSUES_URL, Q as WIZARD_CONTACT_EMAIL, T as DEFAULT_HOST_URL, et as WIZARD_OAUTH_SCOPES, f as getUI, s as logToFile, st as WIZARD_USER_AGENT, z as POSTHOG_DEV_CLIENT_ID } from "./debug-BMkCmZdo.js";
3
- import { n as analytics } from "./analytics-C1qxjbYZ.js";
4
- import { i as withUtm, n as openTrackedLink, t as withProgress } from "./telemetry-CcysC4ji.js";
5
- import { a as getOAuthUrl, s as resolveBaseUrl } from "./urls-PfZ1TYOf.js";
6
- import { t as HostResolution } from "./host-resolution-UZELMVTQ.js";
7
- import { t as provisionNewAccount } from "./provisioning-BOhuDX6i.js";
8
- import { a as fetchUserData, r as fetchProjectData } from "./api-CEMR_Aa6.js";
9
- import { i as wizardAbort } from "./wizard-abort-56bo4Zhs.js";
2
+ import { E as DUMMY_PROJECT_API_KEY, H as POSTHOG_DEV_CLIENT_ID, I as ISSUES_URL, J as POSTHOG_PROXY_CLIENT_ID, R as OAUTH_PORTS, Y as POSTHOG_STATUS_PAGE_URL, f as getUI, gt as runtimeEnv, rt as WIZARD_OAUTH_SCOPES, s as logToFile, tt as WIZARD_CONTACT_EMAIL, ut as WIZARD_USER_AGENT, z as OAUTH_TIMEOUT_MS } from "./debug-DNEQR5DL.js";
3
+ import { n as analytics } from "./analytics-C_9Kh6J1.js";
4
+ import { i as withUtm, n as openTrackedLink, t as withProgress } from "./telemetry-DFYgJHH-.js";
5
+ import { a as getCloudUrl, c as getOAuthUrl, i as detectRegion, l as getUiHostFromHost, o as getHost, s as getLlmGatewayUrl, t as provisionNewAccount, u as resolveBaseUrl } from "./provisioning-BMXR8fS1.js";
6
+ import { a as fetchUserData, r as fetchProjectData } from "./api-Kgk7BFqo.js";
7
+ import { i as wizardAbort } from "./wizard-abort-BM3pISoR.js";
10
8
  import { major, minVersion } from "semver";
11
9
  import * as fs$2 from "fs";
12
10
  import * as path$1 from "path";
@@ -309,6 +307,116 @@ function getOAuthScopesForProgram(programId) {
309
307
  return merged;
310
308
  }
311
309
  //#endregion
310
+ //#region src/lib/host-resolution.ts
311
+ /**
312
+ * HostResolution — the single, immutable snapshot of where the wizard talks to
313
+ * PostHog for one run.
314
+ *
315
+ * One cloud region (or a pinned `--base-url`) implies a whole family of hosts:
316
+ * the event-ingestion/API host, the user-facing web app host, the CDN asset
317
+ * host, the LLM gateway, and the MCP server. Rather than re-deriving each of
318
+ * these at every call site and threading `region` + `baseUrl` around as loose
319
+ * params, resolve once at auth time and pass this frozen object around — read
320
+ * the property you need.
321
+ *
322
+ * The actual region→URL math (and the `--base-url` override) lives in
323
+ * `@utils/urls`; this class is a thin, immutable façade over those resolvers so
324
+ * the override flows through every field without callers having to know about
325
+ * it. The OAuth-server URL stays in `@utils/urls` (`getOAuthUrl`) because it is
326
+ * needed *before* a region is known, so it can't come from this post-auth object.
327
+ */
328
+ const LOCAL_MCP_URL = "http://localhost:8787/mcp";
329
+ const PROD_MCP_URL = "https://mcp.posthog.com/mcp";
330
+ function assetHostFor(region, baseUrl) {
331
+ const override = resolveBaseUrl(baseUrl);
332
+ if (override) return override;
333
+ return region === "eu" ? "https://eu-assets.i.posthog.com" : "https://us-assets.i.posthog.com";
334
+ }
335
+ function assetHostFromApiHost(apiHost) {
336
+ if (apiHost.includes("us.i.posthog.com")) return "https://us-assets.i.posthog.com";
337
+ if (apiHost.includes("eu.i.posthog.com")) return "https://eu-assets.i.posthog.com";
338
+ return apiHost;
339
+ }
340
+ function mcpUrlFor(localMcp) {
341
+ if (localMcp) return LOCAL_MCP_URL;
342
+ return runtimeEnv("MCP_URL") || PROD_MCP_URL;
343
+ }
344
+ var HostResolution = class HostResolution {
345
+ /** The resolved cloud region. `'us'` when a base URL is pinned. */
346
+ region;
347
+ /**
348
+ * Event-ingestion / REST API host (e.g. `https://us.i.posthog.com`, or the
349
+ * pinned `--base-url`). The SDK `host` written into the user's `.env`, the
350
+ * base for the wizard-session REST calls, and the host shown to the agent.
351
+ */
352
+ apiHost;
353
+ /**
354
+ * User-facing web app host (e.g. `https://us.posthog.com`). Use for any link
355
+ * we hand to the user or open in their browser (dashboards, settings, inbox,
356
+ * deep-link base).
357
+ */
358
+ appHost;
359
+ /** CDN asset host (e.g. `https://us-assets.i.posthog.com`). */
360
+ assetHost;
361
+ /** PostHog LLM gateway URL the agent SDK authenticates its model calls against. */
362
+ gatewayUrl;
363
+ /**
364
+ * PostHog MCP server URL the agent connects to. Region-independent — the
365
+ * server resolves the user's region from the bearer token — so this is driven
366
+ * only by `--local-mcp` and the `MCP_URL` override, not by region/base-url.
367
+ */
368
+ mcpUrl;
369
+ constructor(fields) {
370
+ this.region = fields.region;
371
+ this.apiHost = fields.apiHost;
372
+ this.appHost = fields.appHost;
373
+ this.assetHost = fields.assetHost;
374
+ this.gatewayUrl = fields.gatewayUrl;
375
+ this.mcpUrl = fields.mcpUrl;
376
+ Object.freeze(this);
377
+ }
378
+ /**
379
+ * Canonical path: build the host family from a resolved region. A pinned
380
+ * `--base-url` (via `opts.baseUrl`) wins for every origin; otherwise the
381
+ * region's standard hosts are used. Honors IS_DEV through `resolveBaseUrl`.
382
+ */
383
+ static fromRegion(region, opts = {}) {
384
+ const apiHost = getHost(region, opts.baseUrl);
385
+ return new HostResolution({
386
+ region,
387
+ apiHost,
388
+ appHost: getCloudUrl(region, opts.baseUrl),
389
+ assetHost: assetHostFor(region, opts.baseUrl),
390
+ gatewayUrl: getLlmGatewayUrl(apiHost),
391
+ mcpUrl: mcpUrlFor(opts.localMcp ?? false)
392
+ });
393
+ }
394
+ /**
395
+ * Build from an ingestion host string (the provisioning API returns one). The
396
+ * given host is preserved verbatim as `apiHost`; the region and the derived
397
+ * app/asset/gateway hosts are inferred from it.
398
+ */
399
+ static fromApiHost(apiHost, opts = {}) {
400
+ return new HostResolution({
401
+ region: apiHost.includes("eu.") ? "eu" : "us",
402
+ apiHost,
403
+ appHost: getUiHostFromHost(apiHost),
404
+ assetHost: assetHostFromApiHost(apiHost),
405
+ gatewayUrl: getLlmGatewayUrl(apiHost),
406
+ mcpUrl: mcpUrlFor(opts.localMcp ?? false)
407
+ });
408
+ }
409
+ /**
410
+ * Resolve the region from an access token (the us/eu probe, skipped when a
411
+ * base URL is pinned), then build the host family. Used after OAuth and after
412
+ * CI-mode API-key auth.
413
+ */
414
+ static async fromAccessToken(accessToken, opts = {}) {
415
+ const region = opts.region ?? await detectRegion(accessToken, opts.baseUrl);
416
+ return HostResolution.fromRegion(region, opts);
417
+ }
418
+ };
419
+ //#endregion
312
420
  //#region src/utils/oauth-errors.ts
313
421
  /**
314
422
  * OAuth failure presentation — the structured error the flow throws and the
@@ -993,13 +1101,12 @@ function isUsingTypeScript({ installDir }) {
993
1101
  async function getOrAskForProjectData(_options) {
994
1102
  if (_options.ci && _options.apiKey) {
995
1103
  getUI().log.info("Using provided API key (CI mode - OAuth bypassed)");
996
- const hosts = await HostResolution.fromAccessToken(_options.apiKey, {
1104
+ const host = await HostResolution.fromAccessToken(_options.apiKey, {
997
1105
  region: _options.region,
1106
+ localMcp: _options.localMcp,
998
1107
  baseUrl: _options.baseUrl
999
1108
  });
1000
- const cloudRegion = hosts.region;
1001
- const host = hosts.apiHost;
1002
- const cloudUrl = hosts.appHost;
1109
+ const cloudUrl = host.appHost;
1003
1110
  const projectData = _options.projectId != null ? await fetchProjectDataById(_options.apiKey, _options.projectId, cloudUrl) : await fetchProjectDataWithApiKey(_options.apiKey, cloudUrl);
1004
1111
  let user = null;
1005
1112
  let roleAtOrganization = null;
@@ -1018,22 +1125,22 @@ async function getOrAskForProjectData(_options) {
1018
1125
  projectApiKey: projectData.api_token,
1019
1126
  accessToken: _options.apiKey,
1020
1127
  projectId: projectData.id,
1021
- cloudRegion,
1022
1128
  roleAtOrganization,
1023
1129
  user,
1024
1130
  project: projectData.project
1025
1131
  };
1026
1132
  }
1027
- const { host, projectApiKey, accessToken, projectId, cloudRegion, roleAtOrganization, user, project } = await withProgress("login", () => askForWizardLogin({
1133
+ const { host, projectApiKey, accessToken, projectId, roleAtOrganization, user, project } = await withProgress("login", () => askForWizardLogin({
1028
1134
  signup: _options.signup,
1029
1135
  email: _options.email,
1030
1136
  region: _options.region,
1031
1137
  baseUrl: _options.baseUrl,
1032
1138
  programId: _options.programId,
1033
- projectId: _options.projectId
1139
+ projectId: _options.projectId,
1140
+ localMcp: _options.localMcp
1034
1141
  }));
1035
1142
  if (!projectApiKey) {
1036
- const cloudUrl = HostResolution.fromRegion(cloudRegion, { baseUrl: _options.baseUrl }).appHost;
1143
+ const cloudUrl = host.appHost;
1037
1144
  getUI().log.error(`Didn't receive a project token. This shouldn't happen :(
1038
1145
 
1039
1146
  Please let us know if you think this is a bug in the wizard:
@@ -1044,10 +1151,9 @@ ${cloudUrl}/settings/project#variables`);
1044
1151
  }
1045
1152
  return {
1046
1153
  accessToken,
1047
- host: host || DEFAULT_HOST_URL,
1154
+ host,
1048
1155
  projectApiKey: projectApiKey || "_YOUR_POSTHOG_PROJECT_TOKEN_",
1049
1156
  projectId,
1050
- cloudRegion,
1051
1157
  roleAtOrganization: roleAtOrganization ?? null,
1052
1158
  user: user ?? null,
1053
1159
  project: project ?? null
@@ -1072,7 +1178,7 @@ async function fetchProjectDataById(apiKey, projectId, cloudUrl) {
1072
1178
  };
1073
1179
  }
1074
1180
  async function askForWizardLogin(options) {
1075
- if (options.signup) return askForProvisioningSignup(options.email, options.region, options.baseUrl);
1181
+ if (options.signup) return askForProvisioningSignup(options.email, options.region, options.baseUrl, options.localMcp);
1076
1182
  const tokenResponse = await performOAuthFlow({
1077
1183
  scopes: [...getOAuthScopesForProgram(options.programId)],
1078
1184
  signup: false,
@@ -1100,10 +1206,11 @@ async function askForWizardLogin(options) {
1100
1206
  getUI().log.error(error.message);
1101
1207
  await abort();
1102
1208
  }
1103
- const hosts = await HostResolution.fromAccessToken(tokenResponse.access_token, { baseUrl: options.baseUrl });
1104
- const cloudRegion = hosts.region;
1105
- const cloudUrl = hosts.appHost;
1106
- const host = hosts.apiHost;
1209
+ const host = await HostResolution.fromAccessToken(tokenResponse.access_token, {
1210
+ localMcp: options.localMcp,
1211
+ baseUrl: options.baseUrl
1212
+ });
1213
+ const cloudUrl = host.appHost;
1107
1214
  const projectData = await fetchProjectData(tokenResponse.access_token, projectId, cloudUrl);
1108
1215
  const userData = await fetchUserData(tokenResponse.access_token, cloudUrl);
1109
1216
  const data = {
@@ -1112,7 +1219,6 @@ async function askForWizardLogin(options) {
1112
1219
  host,
1113
1220
  distinctId: userData.distinct_id,
1114
1221
  projectId,
1115
- cloudRegion,
1116
1222
  roleAtOrganization: userData.role_at_organization ?? null,
1117
1223
  user: userData,
1118
1224
  project: projectData
@@ -1122,7 +1228,7 @@ async function askForWizardLogin(options) {
1122
1228
  analytics.identifyUser(userData);
1123
1229
  return data;
1124
1230
  }
1125
- async function askForProvisioningSignup(email, region, baseUrl) {
1231
+ async function askForProvisioningSignup(email, region, baseUrl, localMcp) {
1126
1232
  if (!email || !email.includes("@")) {
1127
1233
  getUI().log.error("Email is required for signup. Use --email your@email.com with --signup.");
1128
1234
  await abort();
@@ -1140,16 +1246,14 @@ async function askForProvisioningSignup(email, region, baseUrl) {
1140
1246
  });
1141
1247
  spinner.stop("Account created!");
1142
1248
  getUI().log.success("Welcome to PostHog!");
1143
- const host = result.host;
1144
- const cloudRegion = host.includes("eu.") ? "eu" : "us";
1249
+ const host = HostResolution.fromApiHost(result.host, { localMcp });
1145
1250
  analytics.setTag("provisioning-signup", true);
1146
1251
  return {
1147
1252
  accessToken: result.accessToken,
1148
1253
  projectApiKey: result.projectApiKey,
1149
1254
  host,
1150
1255
  distinctId: email,
1151
- projectId: parseInt(result.projectId, 10) || 0,
1152
- cloudRegion
1256
+ projectId: parseInt(result.projectId, 10) || 0
1153
1257
  };
1154
1258
  } catch (error) {
1155
1259
  spinner.stop("Account creation failed.");
@@ -1158,7 +1262,8 @@ async function askForProvisioningSignup(email, region, baseUrl) {
1158
1262
  getUI().log.info("This email already has a PostHog account. Switching to login flow...");
1159
1263
  return askForWizardLogin({
1160
1264
  signup: false,
1161
- baseUrl
1265
+ baseUrl,
1266
+ localMcp
1162
1267
  });
1163
1268
  }
1164
1269
  getUI().log.error(`Failed to create account: ${message}`);
@@ -1168,6 +1273,6 @@ async function askForProvisioningSignup(email, region, baseUrl) {
1168
1273
  }
1169
1274
  }
1170
1275
  //#endregion
1171
- export { createVersionBucket as a, tryGetPackageJson as i, isUsingTypeScript as n, extractOAuthCode as o, setup_utils_exports as r, detectAllPackageManagers as s, getOrAskForProjectData as t };
1276
+ 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 };
1172
1277
 
1173
- //# sourceMappingURL=setup-utils-NwB2Qj6N.js.map
1278
+ //# sourceMappingURL=setup-utils-DkyEOBDx.js.map