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