@posthog/agent 2.3.504 → 2.3.508

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/agent.js CHANGED
@@ -4030,7 +4030,7 @@ import { v7 as uuidv7 } from "uuid";
4030
4030
  // package.json
4031
4031
  var package_default = {
4032
4032
  name: "@posthog/agent",
4033
- version: "2.3.504",
4033
+ version: "2.3.508",
4034
4034
  repository: "https://github.com/PostHog/code",
4035
4035
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
4036
4036
  exports: {
@@ -8681,6 +8681,24 @@ function toolContent() {
8681
8681
  return new ToolContentBuilder();
8682
8682
  }
8683
8683
 
8684
+ // src/adapters/claude/permissions/posthog-exec-gate.ts
8685
+ var POSTHOG_EXEC_TOOL_RE = /^mcp__posthog(?:_[^_]+)*__exec$/;
8686
+ var POSTHOG_CALL_COMMAND_RE = /^\s*call\s+(?:--json\s+)?([a-zA-Z0-9_-]+)/;
8687
+ var POSTHOG_DESTRUCTIVE_SUBTOOL_RE = /(^|-)(partial-update|update|delete|destroy)(-|$)/i;
8688
+ function isPostHogExecTool(toolName) {
8689
+ return POSTHOG_EXEC_TOOL_RE.test(toolName);
8690
+ }
8691
+ function extractPostHogSubTool(toolInput) {
8692
+ if (!toolInput || typeof toolInput !== "object") return null;
8693
+ const command = toolInput.command;
8694
+ if (typeof command !== "string") return null;
8695
+ const match = command.match(POSTHOG_CALL_COMMAND_RE);
8696
+ return match ? match[1] ?? null : null;
8697
+ }
8698
+ function isPostHogDestructiveSubTool(subTool) {
8699
+ return POSTHOG_DESTRUCTIVE_SUBTOOL_RE.test(subTool);
8700
+ }
8701
+
8684
8702
  // src/adapters/claude/hooks.ts
8685
8703
  function extractTextFromToolResponse(response) {
8686
8704
  if (typeof response === "string") return response;
@@ -8821,6 +8839,19 @@ var createPreToolUseHook = (settingsManager, logger) => async (input, _toolUseID
8821
8839
  `[PreToolUseHook] Tool: ${toolName}, Decision: ${permissionCheck.decision}, Rule: ${permissionCheck.rule}`
8822
8840
  );
8823
8841
  }
8842
+ if (permissionCheck.decision === "allow" && isPostHogExecTool(toolName)) {
8843
+ const subTool = extractPostHogSubTool(toolInput);
8844
+ if (subTool && isPostHogDestructiveSubTool(subTool)) {
8845
+ return {
8846
+ continue: true,
8847
+ hookSpecificOutput: {
8848
+ hookEventName: "PreToolUse",
8849
+ permissionDecision: "ask",
8850
+ permissionDecisionReason: `Destructive PostHog sub-tool '${subTool}' requires explicit approval`
8851
+ }
8852
+ };
8853
+ }
8854
+ }
8824
8855
  switch (permissionCheck.decision) {
8825
8856
  case "allow":
8826
8857
  return {
@@ -10415,6 +10446,12 @@ async function emitToolDenial(context, message) {
10415
10446
  }
10416
10447
  });
10417
10448
  }
10449
+ async function buildDenialResult(context, response) {
10450
+ const feedback = response._meta?.customInput?.trim();
10451
+ const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
10452
+ await emitToolDenial(context, message);
10453
+ return { behavior: "deny", message, interrupt: !feedback };
10454
+ }
10418
10455
  function getPlanFromFile(session, fileContentCache) {
10419
10456
  return session.lastPlanContent || (session.lastPlanFilePath ? fileContentCache[session.lastPlanFilePath] : void 0);
10420
10457
  }
@@ -10639,12 +10676,8 @@ async function handleDefaultPermissionFlow(context) {
10639
10676
  behavior: "allow",
10640
10677
  updatedInput: toolInput
10641
10678
  };
10642
- } else {
10643
- const feedback = response._meta?.customInput?.trim();
10644
- const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
10645
- await emitToolDenial(context, message);
10646
- return { behavior: "deny", message, interrupt: !feedback };
10647
10679
  }
10680
+ return buildDenialResult(context, response);
10648
10681
  }
10649
10682
  function parseMcpToolName(toolName) {
10650
10683
  const parts2 = toolName.split("__");
@@ -10707,10 +10740,61 @@ ${metadata2.description}` : "";
10707
10740
  updatedInput: toolInput
10708
10741
  };
10709
10742
  }
10710
- const feedback = response._meta?.customInput?.trim();
10711
- const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
10712
- await emitToolDenial(context, message);
10713
- return { behavior: "deny", message, interrupt: !feedback };
10743
+ return buildDenialResult(context, response);
10744
+ }
10745
+ async function handlePostHogExecApprovalFlow(context, subTool) {
10746
+ const { toolName, toolInput, toolUseID, client, sessionId, session } = context;
10747
+ const response = await client.requestPermission({
10748
+ options: [
10749
+ { kind: "allow_once", name: "Yes", optionId: "allow" },
10750
+ {
10751
+ kind: "allow_always",
10752
+ name: "Yes, always allow",
10753
+ optionId: "allow_always"
10754
+ },
10755
+ {
10756
+ kind: "reject_once",
10757
+ name: "Type here to tell the agent what to do differently",
10758
+ optionId: "reject",
10759
+ _meta: { customInput: true }
10760
+ }
10761
+ ],
10762
+ sessionId,
10763
+ toolCall: {
10764
+ toolCallId: toolUseID,
10765
+ title: `The agent wants to run \`${subTool}\` on PostHog`,
10766
+ kind: "other",
10767
+ content: [
10768
+ {
10769
+ type: "content",
10770
+ content: text(
10771
+ "This will modify live PostHog data. Approve to run this sub-tool."
10772
+ )
10773
+ }
10774
+ ],
10775
+ rawInput: { ...toolInput, toolName }
10776
+ }
10777
+ });
10778
+ if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
10779
+ throw new Error("Tool use aborted");
10780
+ }
10781
+ if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
10782
+ if (response.outcome.optionId === "allow_always") {
10783
+ try {
10784
+ await session.settingsManager.addPostHogExecApproval(subTool);
10785
+ } catch (error) {
10786
+ context.logger.warn(
10787
+ "[canUseTool] Failed to persist PostHog exec approval",
10788
+ { error: error instanceof Error ? error.message : String(error) }
10789
+ );
10790
+ }
10791
+ }
10792
+ return {
10793
+ behavior: "allow",
10794
+ updatedInput: toolInput
10795
+ };
10796
+ }
10797
+ return buildDenialResult(context, response);
10714
10798
  }
10715
10799
  function handlePlanFileException(context) {
10716
10800
  const { session, toolName, toolInput } = context;
@@ -10792,6 +10876,24 @@ async function canUseTool(context) {
10792
10876
  if (approvalState === "needs_approval") {
10793
10877
  return handleMcpApprovalFlow(context);
10794
10878
  }
10879
+ if (isPostHogExecTool(toolName)) {
10880
+ const subTool = extractPostHogSubTool(toolInput);
10881
+ if (subTool && isPostHogDestructiveSubTool(subTool)) {
10882
+ if (session.permissionMode === "auto" || session.permissionMode === "bypassPermissions") {
10883
+ return {
10884
+ behavior: "allow",
10885
+ updatedInput: toolInput
10886
+ };
10887
+ }
10888
+ if (session.settingsManager.hasPostHogExecApproval(subTool)) {
10889
+ return {
10890
+ behavior: "allow",
10891
+ updatedInput: toolInput
10892
+ };
10893
+ }
10894
+ return handlePostHogExecApprovalFlow(context, subTool);
10895
+ }
10896
+ }
10795
10897
  }
10796
10898
  if (isToolAllowedForMode(toolName, session.permissionMode)) {
10797
10899
  return {
@@ -16318,6 +16420,7 @@ var SettingsManager = class {
16318
16420
  ask: []
16319
16421
  };
16320
16422
  const merged = { permissions };
16423
+ const posthogApprovedExecTools = /* @__PURE__ */ new Set();
16321
16424
  for (const settings of allSettings) {
16322
16425
  if (settings.permissions) {
16323
16426
  if (settings.permissions.allow) {
@@ -16345,6 +16448,14 @@ var SettingsManager = class {
16345
16448
  if (settings.model) {
16346
16449
  merged.model = settings.model;
16347
16450
  }
16451
+ if (settings.posthogApprovedExecTools) {
16452
+ for (const tool of settings.posthogApprovedExecTools) {
16453
+ posthogApprovedExecTools.add(tool);
16454
+ }
16455
+ }
16456
+ }
16457
+ if (posthogApprovedExecTools.size > 0) {
16458
+ merged.posthogApprovedExecTools = Array.from(posthogApprovedExecTools);
16348
16459
  }
16349
16460
  this.mergedSettings = merged;
16350
16461
  }
@@ -16409,6 +16520,39 @@ var SettingsManager = class {
16409
16520
  const next = { ...existing, permissions };
16410
16521
  await fs7.promises.mkdir(path11.dirname(filePath), { recursive: true });
16411
16522
  await writeFileAtomic(filePath, `${JSON.stringify(next, null, 2)}
16523
+ `);
16524
+ this.localSettings = next;
16525
+ this.mergeAllSettings();
16526
+ } finally {
16527
+ this.writeMutex.release();
16528
+ }
16529
+ }
16530
+ hasPostHogExecApproval(subTool) {
16531
+ return this.mergedSettings.posthogApprovedExecTools?.includes(subTool) ?? false;
16532
+ }
16533
+ /**
16534
+ * Persists an approved PostHog MCP `exec` sub-tool (e.g. `experiment-update`)
16535
+ * to the local settings file so future calls skip the prompt. Mirrors
16536
+ * `addAllowRules` — serialised via `writeMutex`, atomic temp-file + rename.
16537
+ */
16538
+ async addPostHogExecApproval(subTool) {
16539
+ if (!subTool) return;
16540
+ if (!this.initialized) await this.initialize();
16541
+ await this.writeMutex.acquire();
16542
+ try {
16543
+ const filePath = this.getLocalSettingsPath();
16544
+ const existing = await readSettingsFileForUpdate(filePath);
16545
+ const current2 = new Set(existing.posthogApprovedExecTools ?? []);
16546
+ if (current2.has(subTool)) {
16547
+ return;
16548
+ }
16549
+ current2.add(subTool);
16550
+ const next = {
16551
+ ...existing,
16552
+ posthogApprovedExecTools: Array.from(current2)
16553
+ };
16554
+ await fs7.promises.mkdir(path11.dirname(filePath), { recursive: true });
16555
+ await writeFileAtomic(filePath, `${JSON.stringify(next, null, 2)}
16412
16556
  `);
16413
16557
  this.localSettings = next;
16414
16558
  this.mergeAllSettings();