@posthog/wizard 2.41.0 → 2.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-mcp-server-to-clients-Bvv6Vh5y.js → add-mcp-server-to-clients-fGAJ6wRG.js} +5 -5
- package/dist/{add-mcp-server-to-clients-Bvv6Vh5y.js.map → add-mcp-server-to-clients-fGAJ6wRG.js.map} +1 -1
- package/dist/{agent-interface-CFeI8jzW.js → agent-interface-dPcuJv8g.js} +8 -9
- package/dist/agent-interface-dPcuJv8g.js.map +1 -0
- package/dist/{agent-runner-B0pWHl5j.js → agent-runner-3bDJ_WLl.js} +120 -63
- package/dist/agent-runner-3bDJ_WLl.js.map +1 -0
- package/dist/analytics-B33Hy7zS.js +2 -0
- package/dist/{analytics-C1qxjbYZ.js → analytics-jQRtyqzG.js} +5 -2
- package/dist/analytics-jQRtyqzG.js.map +1 -0
- package/dist/{api-CEMR_Aa6.js → api-Bweisj9K.js} +3 -3
- package/dist/{api-CEMR_Aa6.js.map → api-Bweisj9K.js.map} +1 -1
- package/dist/bin.js +66 -65
- package/dist/bin.js.map +1 -1
- package/dist/{ci-install-BBZscv3z.js → ci-install-BoATzVVs.js} +4 -4
- package/dist/{ci-install-BBZscv3z.js.map → ci-install-BoATzVVs.js.map} +1 -1
- package/dist/{debug-BMkCmZdo.js → debug--QPXcpzv.js} +4 -5
- package/dist/{debug-BMkCmZdo.js.map → debug--QPXcpzv.js.map} +1 -1
- package/dist/{debug-DBrsvcym.js → debug-DCQHRXu9.js} +1 -1
- package/dist/{defaults-BxiMLkQY.js → defaults-CeNThA-1.js} +1 -1
- package/dist/{defaults-BxiMLkQY.js.map → defaults-CeNThA-1.js.map} +1 -1
- package/dist/{env-api-key-DDVkPQmo.js → env-api-key-cjD4yfGT.js} +1 -1
- package/dist/{env-api-key-DDVkPQmo.js.map → env-api-key-cjD4yfGT.js.map} +1 -1
- package/dist/{environment-B7DmpnXD.js → environment-Xdauxvph.js} +3 -3
- package/dist/{environment-B7DmpnXD.js.map → environment-Xdauxvph.js.map} +1 -1
- package/dist/{file-utils-BCxAu-vq.js → file-utils-BJhGBl-W.js} +2 -2
- package/dist/{file-utils-BCxAu-vq.js.map → file-utils-BJhGBl-W.js.map} +1 -1
- package/dist/{headless-ui-NFf-5XOW.js → headless-ui-XdCxxQ0b.js} +2 -2
- package/dist/{headless-ui-NFf-5XOW.js.map → headless-ui-XdCxxQ0b.js.map} +1 -1
- package/dist/{interactive-CBrzEFly.js → interactive-CwtUeGJT.js} +2 -2
- package/dist/{interactive-CBrzEFly.js.map → interactive-CwtUeGJT.js.map} +1 -1
- package/dist/{mcp-C7dn0DIC.js → mcp-DIuzSJ8f.js} +15 -19
- package/dist/mcp-DIuzSJ8f.js.map +1 -0
- package/dist/{mcp-prompt-streaming-uXF2xskx.js → mcp-prompt-streaming-CCS2EkfY.js} +8 -10
- package/dist/mcp-prompt-streaming-CCS2EkfY.js.map +1 -0
- package/dist/{non-interactive-CDYGf19o.js → non-interactive-Cjb5uLLn.js} +2 -2
- package/dist/{non-interactive-CDYGf19o.js.map → non-interactive-Cjb5uLLn.js.map} +1 -1
- package/dist/{package-manager-BHAvpGav.js → package-manager-D_05Bmr0.js} +2 -2
- package/dist/{package-manager-BHAvpGav.js.map → package-manager-D_05Bmr0.js.map} +1 -1
- package/dist/{playground-B8OhYzyc.js → playground-BtB7RvUT.js} +26 -21
- package/dist/playground-BtB7RvUT.js.map +1 -0
- package/dist/{posthog-BHbtnNQE.js → posthog-DwKlq0HM.js} +2 -2
- package/dist/posthog-DwKlq0HM.js.map +1 -0
- package/dist/{posthog-integration-G839iLK6.js → posthog-integration-Cw1QDMFi.js} +24 -25
- package/dist/posthog-integration-Cw1QDMFi.js.map +1 -0
- package/dist/{provisioning-BOhuDX6i.js → provisioning-CmpiiTaR.js} +67 -6
- package/dist/provisioning-CmpiiTaR.js.map +1 -0
- package/dist/{registry-CC26CjWO.js → registry-DPriFJQI.js} +4 -4
- package/dist/{registry-CC26CjWO.js.map → registry-DPriFJQI.js.map} +1 -1
- package/dist/{security-wlqqLk44.js → security-DJrWWO0M.js} +4 -4
- package/dist/{security-wlqqLk44.js.map → security-DJrWWO0M.js.map} +1 -1
- package/dist/{setup-utils-NwB2Qj6N.js → setup-utils-C8yipMUt.js} +137 -32
- package/dist/setup-utils-C8yipMUt.js.map +1 -0
- package/dist/{start-tui-DQndpyTU.js → start-tui-BMZL7mKh.js} +33 -30
- package/dist/start-tui-BMZL7mKh.js.map +1 -0
- package/dist/{steps-CC_Ge4sR.js → steps-Dhq5VReA.js} +7 -7
- package/dist/{steps-CC_Ge4sR.js.map → steps-Dhq5VReA.js.map} +1 -1
- package/dist/{store-1-pQpHlY.js → store-Bu1xO0_5.js} +5 -5
- package/dist/{store-1-pQpHlY.js.map → store-Bu1xO0_5.js.map} +1 -1
- package/dist/{subagent-FMDybA8X.js → subagent-mVY-kDU0.js} +2 -2
- package/dist/{subagent-FMDybA8X.js.map → subagent-mVY-kDU0.js.map} +1 -1
- package/dist/{task-stream-d-R9J0g4.js → task-stream-Ne-9ZOnf.js} +3 -3
- package/dist/{task-stream-d-R9J0g4.js.map → task-stream-Ne-9ZOnf.js.map} +1 -1
- package/dist/{tasks-BfDJpHgK.js → tasks-2af2kDE3.js} +2 -2
- package/dist/{tasks-BfDJpHgK.js.map → tasks-2af2kDE3.js.map} +1 -1
- package/dist/{telemetry-CcysC4ji.js → telemetry-wGk3UsDw.js} +3 -3
- package/dist/{telemetry-CcysC4ji.js.map → telemetry-wGk3UsDw.js.map} +1 -1
- package/dist/{terminal-CWpmzTgT.js → terminal-CnYduoIp.js} +49 -25
- package/dist/terminal-CnYduoIp.js.map +1 -0
- package/dist/{tools-BsOywKLp.js → tools-BTq5sLH9.js} +4 -4
- package/dist/{tools-BsOywKLp.js.map → tools-BTq5sLH9.js.map} +1 -1
- package/dist/{types-evL_rPob.js → types-y3HkjtSq.js} +1 -1
- package/dist/{types-evL_rPob.js.map → types-y3HkjtSq.js.map} +1 -1
- package/dist/{wizard-abort-56bo4Zhs.js → wizard-abort-CSMnla3D.js} +4 -4
- package/dist/{wizard-abort-56bo4Zhs.js.map → wizard-abort-CSMnla3D.js.map} +1 -1
- package/dist/{wizard-abort-7oUXxutu.js → wizard-abort-D8Lj609C.js} +1 -1
- package/dist/{wizard-session-BoXAZEOf.js → wizard-session-BudcixsA.js} +1 -2
- package/dist/wizard-session-BudcixsA.js.map +1 -0
- package/dist/{wizard-session-C8W_6X0j.js → wizard-session-DXivb7kv.js} +1 -1
- package/dist/{wizard-tools-BXd4PCO3.js → wizard-tools-BBLQk1YC.js} +4 -4
- package/dist/{wizard-tools-BXd4PCO3.js.map → wizard-tools-BBLQk1YC.js.map} +1 -1
- package/dist/wizard-ui-WZ48rUgr.js.map +1 -1
- package/dist/yara-hooks-B42cYxIF.js +2 -0
- package/dist/{yara-hooks-BZmYgr9V.js → yara-hooks-Ciy8d0ok.js} +4 -4
- package/dist/{yara-hooks-BZmYgr9V.js.map → yara-hooks-Ciy8d0ok.js.map} +1 -1
- package/package.json +1 -1
- package/dist/agent-interface-CFeI8jzW.js.map +0 -1
- package/dist/agent-runner-B0pWHl5j.js.map +0 -1
- package/dist/analytics-C1qxjbYZ.js.map +0 -1
- package/dist/analytics-_8xKpUAa.js +0 -2
- package/dist/host-resolution-UZELMVTQ.js +0 -115
- package/dist/host-resolution-UZELMVTQ.js.map +0 -1
- package/dist/mcp-C7dn0DIC.js.map +0 -1
- package/dist/mcp-prompt-streaming-uXF2xskx.js.map +0 -1
- package/dist/playground-B8OhYzyc.js.map +0 -1
- package/dist/posthog-BHbtnNQE.js.map +0 -1
- package/dist/posthog-integration-G839iLK6.js.map +0 -1
- package/dist/provisioning-BOhuDX6i.js.map +0 -1
- package/dist/setup-utils-NwB2Qj6N.js.map +0 -1
- package/dist/start-tui-DQndpyTU.js.map +0 -1
- package/dist/terminal-CWpmzTgT.js.map +0 -1
- package/dist/urls-PfZ1TYOf.js +0 -67
- package/dist/urls-PfZ1TYOf.js.map +0 -1
- package/dist/wizard-session-BoXAZEOf.js.map +0 -1
- package/dist/yara-hooks-D0h7Vu7A.js +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { s as logToFile } from "./debug
|
|
2
|
-
import { a as wizardCanUseTool, g as createTriageLLMProvider } from "./agent-interface-
|
|
3
|
-
import { c as isWizardDocumentationPath, d as repeatBlockReason, f as scanAndTriage, i as createRepeatBlockTracker, u as recordExternalScan } from "./yara-hooks-
|
|
1
|
+
import { s as logToFile } from "./debug--QPXcpzv.js";
|
|
2
|
+
import { a as wizardCanUseTool, g as createTriageLLMProvider } from "./agent-interface-dPcuJv8g.js";
|
|
3
|
+
import { c as isWizardDocumentationPath, d as repeatBlockReason, f as scanAndTriage, i as createRepeatBlockTracker, u as recordExternalScan } from "./yara-hooks-Ciy8d0ok.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-
|
|
300
|
+
//# sourceMappingURL=security-DJrWWO0M.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-DJrWWO0M.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 {
|
|
3
|
-
import { n as analytics } from "./analytics-
|
|
4
|
-
import { i as withUtm, n as openTrackedLink, t as withProgress } from "./telemetry-
|
|
5
|
-
import { a as getOAuthUrl, s as resolveBaseUrl } from "./
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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 { $ as WIZARD_OAUTH_SCOPES, E as DUMMY_PROJECT_API_KEY, F as OAUTH_TIMEOUT_MS, G as POSTHOG_STATUS_PAGE_URL, M as ISSUES_URL, P as OAUTH_PORTS, R as POSTHOG_DEV_CLIENT_ID, W as POSTHOG_PROXY_CLIENT_ID, Z as WIZARD_CONTACT_EMAIL, f as getUI, ft as runtimeEnv, ot as WIZARD_USER_AGENT, s as logToFile } from "./debug--QPXcpzv.js";
|
|
3
|
+
import { n as analytics } from "./analytics-jQRtyqzG.js";
|
|
4
|
+
import { i as withUtm, n as openTrackedLink, t as withProgress } from "./telemetry-wGk3UsDw.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-CmpiiTaR.js";
|
|
6
|
+
import { a as fetchUserData, r as fetchProjectData } from "./api-Bweisj9K.js";
|
|
7
|
+
import { i as wizardAbort } from "./wizard-abort-CSMnla3D.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
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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,
|
|
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-
|
|
1278
|
+
//# sourceMappingURL=setup-utils-C8yipMUt.js.map
|