@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
  3. package/dist/cli.js +10009 -9654
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/model-discovery.d.ts +12 -0
  13. package/dist/types/config/settings-schema.d.ts +73 -8
  14. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  15. package/dist/types/cursor.d.ts +193 -8
  16. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  17. package/dist/types/edit/hashline/diff.d.ts +9 -1
  18. package/dist/types/edit/index.d.ts +8 -1
  19. package/dist/types/edit/streaming.d.ts +8 -1
  20. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  21. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
  23. package/dist/types/internal-urls/index.d.ts +1 -0
  24. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  25. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  28. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  29. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  30. package/dist/types/lsp/client.d.ts +2 -0
  31. package/dist/types/main.d.ts +2 -0
  32. package/dist/types/mcp/manager.d.ts +33 -4
  33. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  35. package/dist/types/modes/components/session-selector.d.ts +6 -0
  36. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  38. package/dist/types/modes/components/usage-row.d.ts +2 -0
  39. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  44. package/dist/types/modes/theme/theme.d.ts +12 -1
  45. package/dist/types/modes/types.d.ts +2 -1
  46. package/dist/types/sdk.d.ts +18 -0
  47. package/dist/types/security/auth.d.ts +30 -0
  48. package/dist/types/security/cloud.d.ts +79 -0
  49. package/dist/types/security/comparison.d.ts +49 -0
  50. package/dist/types/security/contracts/ids.d.ts +15 -0
  51. package/dist/types/security/contracts/index.d.ts +4 -0
  52. package/dist/types/security/contracts/schemas.d.ts +660 -0
  53. package/dist/types/security/contracts/types.d.ts +234 -0
  54. package/dist/types/security/contracts/validation.d.ts +5 -0
  55. package/dist/types/security/coordinator.d.ts +100 -0
  56. package/dist/types/security/importers/codex-security.d.ts +7 -0
  57. package/dist/types/security/importers/index.d.ts +2 -0
  58. package/dist/types/security/importers/sarif.d.ts +9 -0
  59. package/dist/types/security/index.d.ts +13 -0
  60. package/dist/types/security/preflight.d.ts +61 -0
  61. package/dist/types/security/provenance.d.ts +24 -0
  62. package/dist/types/security/publication.d.ts +78 -0
  63. package/dist/types/security/remediation.d.ts +27 -0
  64. package/dist/types/security/resource-output.d.ts +8 -0
  65. package/dist/types/security/sarif.d.ts +2 -0
  66. package/dist/types/security/store.d.ts +40 -0
  67. package/dist/types/session/agent-session-events.d.ts +2 -0
  68. package/dist/types/session/agent-session-types.d.ts +36 -1
  69. package/dist/types/session/agent-session.d.ts +4 -2
  70. package/dist/types/session/claude-session-store.d.ts +13 -0
  71. package/dist/types/session/codex-session-store.d.ts +14 -0
  72. package/dist/types/session/credential-pin.d.ts +58 -0
  73. package/dist/types/session/foreign-session-import.d.ts +15 -0
  74. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  75. package/dist/types/session/foreign-session-store.d.ts +23 -0
  76. package/dist/types/session/session-advisors.d.ts +47 -2
  77. package/dist/types/session/session-entries.d.ts +19 -1
  78. package/dist/types/session/session-handoff.d.ts +4 -1
  79. package/dist/types/session/session-loader.d.ts +2 -0
  80. package/dist/types/session/session-maintenance.d.ts +1 -1
  81. package/dist/types/session/session-manager.d.ts +21 -0
  82. package/dist/types/session/session-tools.d.ts +25 -1
  83. package/dist/types/session/turn-recovery.d.ts +10 -4
  84. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  85. package/dist/types/system-prompt.d.ts +4 -1
  86. package/dist/types/task/executor.d.ts +14 -2
  87. package/dist/types/task/render.d.ts +7 -0
  88. package/dist/types/thinking.d.ts +18 -6
  89. package/dist/types/tools/acp-bridge.d.ts +28 -5
  90. package/dist/types/tools/builtin-names.d.ts +1 -1
  91. package/dist/types/tools/grep.d.ts +17 -1
  92. package/dist/types/tools/hub/launch.d.ts +5 -1
  93. package/dist/types/tools/index.d.ts +10 -2
  94. package/dist/types/tools/path-utils.d.ts +19 -0
  95. package/dist/types/tools/read.d.ts +2 -0
  96. package/dist/types/tools/security-scan.d.ts +96 -0
  97. package/dist/types/tools/xdev.d.ts +2 -1
  98. package/dist/types/utils/changelog.d.ts +19 -0
  99. package/dist/types/utils/git.d.ts +10 -0
  100. package/dist/types/utils/jj.d.ts +11 -4
  101. package/dist/types/web/search/types.d.ts +2 -2
  102. package/package.json +12 -12
  103. package/scripts/security-compare.ts +40 -0
  104. package/src/advisor/runtime.ts +138 -12
  105. package/src/advisor/transcript-recorder.ts +56 -0
  106. package/src/auto-thinking/classifier.ts +48 -10
  107. package/src/cli/args.ts +6 -0
  108. package/src/cli/flag-tables.ts +2 -0
  109. package/src/cli/gallery-fixtures/fs.ts +25 -0
  110. package/src/cli/session-picker.ts +34 -17
  111. package/src/cli/ttsr-cli.ts +19 -1
  112. package/src/cli.ts +9 -0
  113. package/src/collab/guest.ts +37 -4
  114. package/src/collab/host.ts +1 -0
  115. package/src/commands/launch.ts +6 -0
  116. package/src/config/model-discovery.ts +32 -5
  117. package/src/config/model-registry.ts +107 -25
  118. package/src/config/settings-schema.ts +77 -6
  119. package/src/config/settings.ts +25 -0
  120. package/src/cursor-bridge-tools.ts +81 -0
  121. package/src/cursor.ts +463 -12
  122. package/src/edit/edit-clipboard.ts +23 -0
  123. package/src/edit/hashline/diff.ts +49 -10
  124. package/src/edit/hashline/execute.ts +38 -6
  125. package/src/edit/hashline/filesystem.ts +27 -3
  126. package/src/edit/index.ts +12 -2
  127. package/src/edit/renderer.ts +4 -4
  128. package/src/edit/streaming.ts +17 -2
  129. package/src/eval/py/prelude.py +7 -3
  130. package/src/export/html/template.js +1 -1
  131. package/src/extensibility/extensions/runner.ts +70 -0
  132. package/src/extensibility/extensions/types.ts +30 -0
  133. package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
  134. package/src/internal-urls/index.ts +1 -0
  135. package/src/internal-urls/local-protocol.ts +3 -2
  136. package/src/internal-urls/mcp-protocol.ts +4 -1
  137. package/src/internal-urls/router.ts +4 -1
  138. package/src/internal-urls/security-protocol.ts +261 -0
  139. package/src/internal-urls/types.ts +1 -1
  140. package/src/launch/terminal-output-worker-client.ts +53 -0
  141. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  142. package/src/launch/terminal-output-worker.ts +23 -0
  143. package/src/lsp/client.ts +18 -3
  144. package/src/lsp/index.ts +7 -0
  145. package/src/lsp/render.ts +1 -1
  146. package/src/main.ts +127 -56
  147. package/src/mcp/manager.ts +120 -18
  148. package/src/modes/acp/acp-agent.ts +15 -9
  149. package/src/modes/components/chat-transcript-builder.ts +30 -11
  150. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  151. package/src/modes/components/read-tool-group.ts +159 -17
  152. package/src/modes/components/session-selector.ts +19 -3
  153. package/src/modes/components/status-line/component.ts +381 -54
  154. package/src/modes/components/tool-execution.ts +78 -31
  155. package/src/modes/components/usage-row.ts +10 -5
  156. package/src/modes/controllers/event-controller.ts +26 -4
  157. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  158. package/src/modes/controllers/selector-controller.ts +94 -36
  159. package/src/modes/controllers/tan-command-controller.ts +14 -0
  160. package/src/modes/interactive-mode.ts +31 -17
  161. package/src/modes/rpc/host-uris.ts +6 -0
  162. package/src/modes/rpc/rpc-client.ts +18 -0
  163. package/src/modes/rpc/rpc-mode.ts +16 -1
  164. package/src/modes/rpc/rpc-types.ts +11 -0
  165. package/src/modes/theme/theme.ts +151 -6
  166. package/src/modes/types.ts +2 -1
  167. package/src/modes/utils/ui-helpers.ts +38 -21
  168. package/src/prompts/agents/security-reviewer.md +75 -0
  169. package/src/prompts/security/scan-coordinator.md +7 -0
  170. package/src/prompts/security/scan-request.md +21 -0
  171. package/src/prompts/security/validate-request.md +8 -0
  172. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  173. package/src/prompts/system/plan-mode-active.md +2 -2
  174. package/src/prompts/system/system-prompt.md +6 -0
  175. package/src/prompts/system/xdev-mount-notice.md +1 -1
  176. package/src/prompts/tools/bash.md +15 -17
  177. package/src/prompts/tools/checkpoint.md +1 -1
  178. package/src/prompts/tools/glob.md +7 -6
  179. package/src/prompts/tools/grep.md +7 -6
  180. package/src/prompts/tools/security-publish.md +1 -0
  181. package/src/prompts/tools/security-scan.md +1 -0
  182. package/src/sdk.ts +212 -21
  183. package/src/security/auth.ts +98 -0
  184. package/src/security/cloud.ts +686 -0
  185. package/src/security/comparison.ts +255 -0
  186. package/src/security/contracts/ids.ts +111 -0
  187. package/src/security/contracts/index.ts +4 -0
  188. package/src/security/contracts/schemas.ts +201 -0
  189. package/src/security/contracts/types.ts +254 -0
  190. package/src/security/contracts/validation.ts +65 -0
  191. package/src/security/coordinator.ts +708 -0
  192. package/src/security/importers/codex-security.ts +387 -0
  193. package/src/security/importers/index.ts +2 -0
  194. package/src/security/importers/sarif.ts +357 -0
  195. package/src/security/index.ts +13 -0
  196. package/src/security/preflight.ts +405 -0
  197. package/src/security/provenance.ts +106 -0
  198. package/src/security/publication.ts +326 -0
  199. package/src/security/remediation.ts +93 -0
  200. package/src/security/resource-output.ts +50 -0
  201. package/src/security/sarif.ts +78 -0
  202. package/src/security/store.ts +430 -0
  203. package/src/session/agent-session-events.ts +1 -0
  204. package/src/session/agent-session-types.ts +40 -1
  205. package/src/session/agent-session.ts +302 -175
  206. package/src/session/claude-session-store.ts +426 -0
  207. package/src/session/codex-session-store.ts +673 -0
  208. package/src/session/credential-pin.ts +93 -0
  209. package/src/session/foreign-session-import.ts +52 -0
  210. package/src/session/foreign-session-jsonl.ts +29 -0
  211. package/src/session/foreign-session-store.ts +26 -0
  212. package/src/session/model-controls.ts +11 -3
  213. package/src/session/session-advisors.ts +140 -16
  214. package/src/session/session-entries.ts +21 -1
  215. package/src/session/session-handoff.ts +20 -3
  216. package/src/session/session-loader.ts +25 -9
  217. package/src/session/session-maintenance.ts +59 -11
  218. package/src/session/session-manager.ts +61 -0
  219. package/src/session/session-tools.ts +107 -5
  220. package/src/session/settings-stream-fn.ts +1 -0
  221. package/src/session/turn-recovery.ts +97 -51
  222. package/src/slash-commands/builtin-registry.ts +152 -3
  223. package/src/slash-commands/helpers/security.ts +451 -0
  224. package/src/system-prompt.ts +7 -2
  225. package/src/task/agents.ts +2 -0
  226. package/src/task/executor.ts +20 -4
  227. package/src/task/render.ts +20 -4
  228. package/src/task/structured-subagent.ts +6 -4
  229. package/src/thinking.ts +29 -10
  230. package/src/tools/acp-bridge.ts +52 -8
  231. package/src/tools/browser.ts +5 -0
  232. package/src/tools/builtin-names.ts +1 -0
  233. package/src/tools/checkpoint.ts +0 -13
  234. package/src/tools/grep.ts +64 -8
  235. package/src/tools/hub/launch.ts +10 -15
  236. package/src/tools/index.ts +37 -5
  237. package/src/tools/path-utils.ts +91 -0
  238. package/src/tools/read.ts +3 -0
  239. package/src/tools/security-scan.ts +287 -0
  240. package/src/tools/write.ts +9 -3
  241. package/src/tools/xdev.ts +39 -14
  242. package/src/utils/changelog.ts +144 -4
  243. package/src/utils/git.ts +61 -14
  244. package/src/utils/jj.ts +19 -13
  245. package/src/web/search/providers/codex.ts +34 -0
  246. package/src/web/search/types.ts +1 -1
package/src/sdk.ts CHANGED
@@ -19,6 +19,7 @@ import type {
19
19
  ProviderSessionState,
20
20
  SimpleStreamOptions,
21
21
  } from "@oh-my-pi/pi-ai";
22
+ import { resolveApiKeyOnce } from "@oh-my-pi/pi-ai/auth-retry";
22
23
  import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
23
24
  import {
24
25
  getOpenAICodexTransportDetails,
@@ -33,6 +34,7 @@ import {
33
34
  discoverWatchdogFiles,
34
35
  formatActiveRepoWatchdogPrompt,
35
36
  formatAdvisorContextPrompt,
37
+ loadAdvisorTranscriptCosts,
36
38
  } from "./advisor";
37
39
  import { AsyncJobManager } from "./async";
38
40
  import { AutoLearnController, buildAutoLearnInstructions } from "./autolearn/controller";
@@ -60,7 +62,8 @@ import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate
60
62
  import { applyProviderGlobalsFromSettings } from "./config/provider-globals";
61
63
  import { buildServiceTierByFamily } from "./config/service-tier";
62
64
  import { Settings, type SkillsSettings } from "./config/settings";
63
- import { CursorExecHandlers } from "./cursor";
65
+ import { CursorExecHandlers, type CursorMcpResourceAdapter } from "./cursor";
66
+ import { createBridgeEditTool, createBridgeGrepFactory } from "./cursor-bridge-tools";
64
67
  import "./discovery";
65
68
  import { initializeWithSettings } from "./discovery";
66
69
  import { disposeAllJuliaKernelSessions, disposeJuliaKernelSessionsByOwner } from "./eval/jl/executor";
@@ -349,6 +352,13 @@ export interface CreateAgentSessionOptions {
349
352
  authStorage?: AuthStorage;
350
353
  /** Model registry. Default: discoverModels(authStorage, agentDir) */
351
354
  modelRegistry?: ModelRegistry;
355
+ /**
356
+ * Request credential resolver. Defaults to the model registry's normal
357
+ * session-affine resolver. Security scans use this narrow seam to keep one
358
+ * durable OAuth row pinned for the operation without changing ordinary
359
+ * provider routing.
360
+ */
361
+ getApiKey?: AgentOptions["getApiKey"];
352
362
 
353
363
  /** Model to use. Default: from settings, else first available */
354
364
  model?: Model;
@@ -469,6 +479,8 @@ export interface CreateAgentSessionOptions {
469
479
 
470
480
  /** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
471
481
  enableLsp?: boolean;
482
+ /** Restrict LSP to navigation and diagnostics even when enabled. Defaults to true for restricted sessions. */
483
+ lspReadOnly?: boolean;
472
484
  /** Whether this invocation may expose IRC. `false` removes it even for subagents. */
473
485
  enableIrc?: boolean;
474
486
  /** Skip subprocess-kernel availability checks and prelude warmup */
@@ -477,6 +489,12 @@ export interface CreateAgentSessionOptions {
477
489
  toolNames?: string[];
478
490
  /** Limit the session to explicitly supplied tool names, without discovered extras. */
479
491
  restrictToolNames?: boolean;
492
+ /**
493
+ * Permit only caller-supplied SDK custom tools inside a restricted session.
494
+ * They must still be named in {@link toolNames}; discovered extensions, MCP,
495
+ * and ambient custom tools remain disabled. Default: false.
496
+ */
497
+ allowRestrictedCustomTools?: boolean;
480
498
 
481
499
  /** Output schema for structured completion (subagents). */
482
500
  outputSchema?: unknown;
@@ -607,6 +625,10 @@ export type * from "./extensibility/extensions";
607
625
  export type { Skill } from "./extensibility/skills";
608
626
  export type { FileSlashCommand } from "./extensibility/slash-commands";
609
627
  export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp";
628
+ // Agent registry: pass a private instance per `createAgentSession` when
629
+ // embedding several concurrent top-level sessions in one process (the default
630
+ // global registry admits only one "Main" per process generation).
631
+ export { type AgentRef, AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
610
632
  export type { Tool } from "./tools";
611
633
  export { buildDirectoryTree, buildWorkspaceTree, type DirectoryTree, type WorkspaceTree } from "./workspace-tree";
612
634
 
@@ -811,6 +833,8 @@ export interface BuildSystemPromptOptions {
811
833
  appendPrompt?: string;
812
834
  inlineToolDescriptors?: boolean;
813
835
  includeWorkspaceTree?: boolean;
836
+ /** Include the read-only security:// resource inventory entry. Default: false. */
837
+ securityEnabled?: boolean;
814
838
  }
815
839
 
816
840
  /**
@@ -836,6 +860,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
836
860
  appendSystemPrompt: options.appendPrompt,
837
861
  inlineToolDescriptors: options.inlineToolDescriptors,
838
862
  includeWorkspaceTree: options.includeWorkspaceTree,
863
+ securityEnabled: options.securityEnabled,
839
864
  toolNames,
840
865
  tools: promptTools,
841
866
  });
@@ -1572,7 +1597,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1572
1597
  let hasSession = false;
1573
1598
  let hasRegistered = false;
1574
1599
  const restrictToolNames = options.restrictToolNames === true;
1575
- const enableLsp = !restrictToolNames && (options.enableLsp ?? true);
1600
+ const enableLsp = options.enableLsp ?? !restrictToolNames;
1601
+ const lspReadOnly = options.lspReadOnly ?? restrictToolNames;
1576
1602
  const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
1577
1603
  // Only the first top-level session in a process owns an AsyncJobManager.
1578
1604
  // Subagents inherit the parent's manager via `AsyncJobManager.instance()`
@@ -1639,10 +1665,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1639
1665
  setActiveToolNames,
1640
1666
  toolRegistry,
1641
1667
  hasUI: options.hasUI ?? false,
1668
+ getApiKey: options.getApiKey,
1642
1669
  get additionalDirectories() {
1643
1670
  return sessionManager.getAdditionalDirectories();
1644
1671
  },
1645
1672
  enableLsp,
1673
+ lspReadOnly,
1646
1674
  enableIrc: restrictToolNames ? false : options.enableIrc,
1647
1675
  restrictToolNames,
1648
1676
  get hasEditTool() {
@@ -1772,7 +1800,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1772
1800
  getArtifactsDir,
1773
1801
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
1774
1802
  };
1775
- if (options.localProtocolOptions) {
1803
+ if (options.localProtocolOptions && !options.parentTaskPrefix) {
1776
1804
  LocalProtocolHandler.setOverride(options.localProtocolOptions);
1777
1805
  }
1778
1806
  toolSession.getArtifactsDir = getArtifactsDir;
@@ -2513,6 +2541,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2513
2541
  () => (hasSession ? createSessionMemoryRuntimeContext(session, agentDir, cwd) : undefined),
2514
2542
  settings,
2515
2543
  localProtocolOptions,
2544
+ () => (hasSession ? session.getAsyncJobSnapshot() : null),
2516
2545
  );
2517
2546
 
2518
2547
  credentialDisabledTarget = extensionRunner;
@@ -2537,9 +2566,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2537
2566
  const toolContextStore = new ToolContextStore(getSessionContext);
2538
2567
 
2539
2568
  const registeredTools = restrictToolNames ? [] : extensionRunner.getAllRegisteredTools();
2540
- const sdkCustomTools = restrictToolNames
2541
- ? []
2542
- : (options.customTools?.filter(tool => !isLegacyBuiltinToolDefinition(tool)) ?? []);
2569
+ const sdkCustomTools =
2570
+ restrictToolNames && options.allowRestrictedCustomTools !== true
2571
+ ? []
2572
+ : (options.customTools?.filter(tool => !isLegacyBuiltinToolDefinition(tool)) ?? []);
2543
2573
  const allCustomTools = [
2544
2574
  ...registeredTools,
2545
2575
  ...sdkCustomTools.map(tool => {
@@ -2582,6 +2612,43 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2582
2612
  for (const tool of toolRegistry.values()) {
2583
2613
  toolRegistry.set(tool.name, new ExtensionToolWrapper(tool, extensionRunner));
2584
2614
  }
2615
+ // Cursor's own client owns file edits, so `edit` is not advertised to the
2616
+ // model (commit 8ba0498eb: full-file `write` is used instead). The exec
2617
+ // bridge is a different consumer: the server sends native `pi_edit`
2618
+ // frames regardless of the advertised catalog, and answering them needs
2619
+ // a real tool.
2620
+ //
2621
+ // It must be a `replace`-mode instance. `PiEditExecArgs` carries
2622
+ // `old_text`/`new_text` pairs, which is exactly `replace`'s schema and
2623
+ // nothing else's — under the default `hashline` mode the frame's args do
2624
+ // not match the tool's parameters at all. The registry instance follows
2625
+ // the session's configured mode, so the bridge builds its own.
2626
+ //
2627
+ // The grant is captured HERE, before the Cursor branch below deletes
2628
+ // `edit` from the registry, and independently of the session's provider:
2629
+ // a session that starts on another provider can switch to Cursor later,
2630
+ // and the roster is built once, at session creation. Reading the registry
2631
+ // at frame time would see the switched-to state, not the grant.
2632
+ const editWasGranted = toolRegistry.has("edit");
2633
+ // Built on first use rather than eagerly: a session that never reaches
2634
+ // Cursor never constructs it.
2635
+ let cursorBridgeEditTool: AgentTool | undefined;
2636
+ const getCursorBridgeEditTool = (): AgentTool | undefined => {
2637
+ // Only when the session actually granted `edit`. `createTools` omits
2638
+ // it entirely for a restricted tool set, and the bridge answers native
2639
+ // frames that arrive regardless of the advertised catalog — so
2640
+ // building one unconditionally would hand a read-only agent a
2641
+ // mutating tool it was denied (the issue #5680 escalation).
2642
+ if (!editWasGranted) return undefined;
2643
+ cursorBridgeEditTool ??= createBridgeEditTool(toolSession, extensionRunner);
2644
+ return cursorBridgeEditTool;
2645
+ };
2646
+ // Whether this session granted a file-writing tool. Same capture-early
2647
+ // reasoning, plus `write` may be auto-registered further down as an xdev
2648
+ // transport. The exec bridge answers native `delete` and
2649
+ // resource-download frames that mutate files without running a registry
2650
+ // tool, so it needs the grant as the session actually made it.
2651
+ const cursorCanMutateFiles = editWasGranted || toolRegistry.has("write");
2585
2652
  if (model?.provider === "cursor") {
2586
2653
  toolRegistry.delete("edit");
2587
2654
  builtInRegistryToolNames.delete("edit");
@@ -2624,15 +2691,52 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2624
2691
  if (!state) return undefined;
2625
2692
  return resolveMountedXdevExecutable(state, name);
2626
2693
  };
2694
+ // Cursor's resource frames ask what THIS client's servers advertise; only
2695
+ // live connections have any. Built once: the advisor bridges answer from
2696
+ // the same connections the primary does.
2697
+ const cursorMcpResources: CursorMcpResourceAdapter | undefined = mcpManager && {
2698
+ serverNames: () => mcpManager.getConnectedServers(),
2699
+ getServerResources: async name => {
2700
+ // The manager registers a server's tools before its background
2701
+ // resource load finishes, so a frame arriving in that window
2702
+ // would read an empty cache and report "advertises nothing".
2703
+ await mcpManager.ensureServerResources(name);
2704
+ return mcpManager.getServerResources(name);
2705
+ },
2706
+ readServerResource: (name, uri) => mcpManager.readServerResource(name, uri),
2707
+ };
2627
2708
  const cursorExecHandlers = new CursorExecHandlers({
2628
2709
  cwd,
2710
+ // The session's cwd moves (`/cd`, resume, branch restore) while this
2711
+ // bridge is built once at startup. Path-confining frames — the native
2712
+ // `delete` and a `download_path` resource read — resolve against
2713
+ // whichever of the two they are given, so without the live resolver the
2714
+ // primary would write into the workspace the session has left while
2715
+ // reporting success for the path the server asked about. The advisor
2716
+ // bridge already passes one.
2717
+ getCwd: () => sessionManager.getCwd(),
2629
2718
  tools: toolRegistry,
2630
2719
  getExecutableTool: resolveDeviceTool,
2720
+ // `pi_edit` needs the `replace`-mode instance specifically, and the
2721
+ // registry may still hold the session's own `edit` (any mode) when
2722
+ // this session did not start on Cursor.
2723
+ getEditReplaceTool: getCursorBridgeEditTool,
2631
2724
  getToolContext: () => toolContextStore.getContext(),
2725
+ mcpResources: cursorMcpResources,
2632
2726
  emitEvent: event => cursorEventEmitter?.(event),
2633
2727
  getTodoPhases: () => session.getTodoPhases(),
2634
2728
  setTodoPhases: phases => session.setTodoPhases(phases),
2635
2729
  persistTodoPhases: phases => sessionManager.appendCustomEntry(USER_TODO_EDIT_CUSTOM_TYPE, { phases }),
2730
+ // `pi_grep` carries its own context width and match cap, which the
2731
+ // shared grep instance fixed at construction cannot express. Gated on
2732
+ // the grant: the factory builds a fresh tool and `executeTool` prefers
2733
+ // it over the registry, so installing it unconditionally would let a
2734
+ // session without `grep` search anyway.
2735
+ createGrepTool: toolRegistry.has("grep") ? createBridgeGrepFactory(toolSession, extensionRunner) : undefined,
2736
+ // The native `delete` and resource-download frames mutate files
2737
+ // without running a registry tool, so this grant is the only thing
2738
+ // standing between a restricted session and a workspace write.
2739
+ allowDirectFileMutation: cursorCanMutateFiles,
2636
2740
  });
2637
2741
 
2638
2742
  // Resolve the inline-descriptors setting against the session-start model.
@@ -2746,6 +2850,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2746
2850
  workspaceTree: workspaceTreePromise,
2747
2851
  includeWorkspaceTree,
2748
2852
  memoryRootEnabled: memoryBackend?.id === "local",
2853
+ securityEnabled: settings.get("security.enabled"),
2749
2854
  model: getActiveModelString(),
2750
2855
  includeModelInPrompt: settings.get("includeModelInPrompt"),
2751
2856
  personality: agentKind === "sub" ? "none" : settings.get("personality"),
@@ -2793,6 +2898,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2793
2898
  }
2794
2899
  }
2795
2900
  }
2901
+ // Checkpoint and rewind are a pair: `createTools` auto-includes the sister
2902
+ // tool in the registry, but an explicit `toolNames` list would otherwise
2903
+ // drop it from the ACTIVE set — leaving the agent able to checkpoint but
2904
+ // unable to rewind (or vice versa). Mirror the pairing here. Unlike the
2905
+ // manage_skill/learn mirror above, this is a safety pairing — it applies
2906
+ // to restricted sessions too.
2907
+ if (explicitlyRequestedToolNames) {
2908
+ if (builtInToolNames.includes("checkpoint") && !explicitlyRequestedToolNames.includes("rewind")) {
2909
+ explicitlyRequestedToolNames.push("rewind");
2910
+ } else if (builtInToolNames.includes("rewind") && !explicitlyRequestedToolNames.includes("checkpoint")) {
2911
+ explicitlyRequestedToolNames.push("checkpoint");
2912
+ }
2913
+ }
2796
2914
  const requestedToolNames = explicitlyRequestedToolNames ?? toolNamesFromRegistry;
2797
2915
  const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
2798
2916
  const defaultInactiveToolNames = new Set(
@@ -3027,7 +3145,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3027
3145
  kimiApiFormat,
3028
3146
  preferWebsockets: preferOpenAICodexWebsockets,
3029
3147
  getToolContext: tc => toolContextStore.getContext(tc),
3030
- getApiKey: requestModel => modelRegistry.resolver(requestModel, agent.sessionId),
3148
+ getApiKey: options.getApiKey ?? (requestModel => modelRegistry.resolver(requestModel, agent.sessionId)),
3031
3149
  streamFn: (streamModel, context, streamOptions) => {
3032
3150
  if (notifyFirstChatDispatch) {
3033
3151
  const cb = notifyFirstChatDispatch;
@@ -3112,7 +3230,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3112
3230
  advisorToolBuilds.push(BUILTIN_TOOLS[name as keyof typeof BUILTIN_TOOLS](advisorToolSession));
3113
3231
  }
3114
3232
  const built = await Promise.all(advisorToolBuilds);
3115
- const advisorTools: Tool[] = built.filter((tool): tool is Tool => tool != null).map(wrapToolWithMetaNotice);
3233
+ // Wrapped like every registry tool: `ExtensionToolWrapper` is where the
3234
+ // approval mode, per-tool `tools.approval.<tool>` policies and
3235
+ // `autoApprove` are enforced. The advisor's loop and its Cursor exec
3236
+ // bridge both run these instances directly, so a raw one would execute a
3237
+ // `bash`/`write` the user configured as `ask` or `deny`. Meta-notice
3238
+ // first, matching the registry's wrap order.
3239
+ const advisorTools: Tool[] = built
3240
+ .filter((tool): tool is Tool => tool != null)
3241
+ .map(tool => new ExtensionToolWrapper(wrapToolWithMetaNotice(tool), extensionRunner) as Tool);
3116
3242
 
3117
3243
  const advisorWatchdogPrompts = [...watchdogFiles];
3118
3244
  if (activeRepoContext) {
@@ -3126,6 +3252,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3126
3252
  // Owned only when this session created the manager; subagents receive a
3127
3253
  // parent's manager via `options.mcpManager` and MUST NOT disconnect it.
3128
3254
  const ownedMcpManager = options.mcpManager ? undefined : mcpManager;
3255
+ // A resumed session already has advisor turns on disk; without this the status
3256
+ // line would restart its `(adv)` total at zero for the rest of the session.
3257
+ const initialAdvisorCosts = await loadAdvisorTranscriptCosts(sessionManager.getSessionFile());
3129
3258
  session = new AgentSession({
3130
3259
  advisorWatchdogPrompt,
3131
3260
  advisorContextPrompt,
@@ -3140,6 +3269,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3140
3269
  planYolo: options.planYolo,
3141
3270
  serviceTierByFamily: initialServiceTierByFamily,
3142
3271
  sessionManager,
3272
+ initialAdvisorCosts,
3143
3273
  settings,
3144
3274
  autoApprove: options.autoApprove,
3145
3275
  evalKernelOwnerId,
@@ -3218,6 +3348,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3218
3348
  providerPromptCacheKeySource,
3219
3349
  parentEvalSessionId: options.parentEvalSessionId,
3220
3350
  advisorTools,
3351
+ // Same per-call `grep` seam the primary bridge gets, built against the
3352
+ // advisor's own tool session so a `pi_grep` frame's context width and
3353
+ // match cap are honored there too.
3354
+ advisorCreateGrepTool: createBridgeGrepFactory(advisorToolSession, extensionRunner),
3355
+ // Same `replace`-mode requirement as the primary bridge; the advisor
3356
+ // path gates it on the advisor's own `edit` grant.
3357
+ advisorCreateEditTool: () => createBridgeEditTool(advisorToolSession, extensionRunner),
3358
+ // The advisor's bridge tools are wrapped for approval, but the wrapper
3359
+ // reads the mode and per-tool policies only from the execute-time
3360
+ // context — the primary bridge passes the same store.
3361
+ advisorGetToolContext: () => toolContextStore.getContext(),
3362
+ // Same live connections the primary bridge reads; an advisor's
3363
+ // resource frame would otherwise report every server as empty.
3364
+ advisorMcpResources: cursorMcpResources,
3221
3365
  titleSystemPrompt: options.titleSystemPrompt,
3222
3366
  });
3223
3367
  hasSession = true;
@@ -3245,6 +3389,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3245
3389
  throw new Error(`Agent "${resolvedAgentId}" was replaced during session initialization.`);
3246
3390
  }
3247
3391
  hasRegistered = true;
3392
+ // MCP notification bridge cleanup — assigned when the bridge is wired below,
3393
+ // invoked from the dispose wrapper AND registered as a postmortem so both
3394
+ // explicit-dispose (SDK embedders that reuse the process across sessions) and
3395
+ // process-exit paths tear the listener down. Nulled after use so the closure
3396
+ // graph (`extensionRunner`, `session`) can be GC'd instead of retained by the
3397
+ // process-global postmortem list.
3398
+ let unsubscribeMcpNotifications: (() => void) | undefined;
3399
+ let unregisterMcpPostmortem: (() => void) | undefined;
3400
+
3248
3401
  {
3249
3402
  const originalDispose = session.dispose.bind(session);
3250
3403
  session.dispose = async () => {
@@ -3275,6 +3428,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3275
3428
  } finally {
3276
3429
  unregisterUnlessParked();
3277
3430
  unsubscribeCredentialDisabled?.();
3431
+ unsubscribeMcpNotifications?.();
3432
+ unregisterMcpPostmortem?.();
3433
+ // Drop refs so the process-global postmortem list doesn't retain
3434
+ // the bridge closure past explicit dispose.
3435
+ unsubscribeMcpNotifications = undefined;
3436
+ unregisterMcpPostmortem = undefined;
3278
3437
  }
3279
3438
  };
3280
3439
  }
@@ -3291,7 +3450,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3291
3450
  if (codexTransport.websocketPreferred) {
3292
3451
  void (async () => {
3293
3452
  try {
3294
- const codexPrewarmApiKey = await modelRegistry.getApiKey(codexModel, providerSessionId);
3453
+ const codexPrewarmApiKey = options.getApiKey
3454
+ ? // `getApiKey` returns a value-or-promise union; unwrap the promise,
3455
+ // then resolve the result if it is itself an ApiKeyResolver.
3456
+ await resolveApiKeyOnce(await options.getApiKey(codexModel))
3457
+ : await modelRegistry.getApiKey(codexModel, providerSessionId);
3295
3458
  if (!codexPrewarmApiKey) return;
3296
3459
  await logger.time("prewarmOpenAICodexResponses", prewarmOpenAICodexResponses, codexModel, {
3297
3460
  apiKey: codexPrewarmApiKey,
@@ -3447,19 +3610,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3447
3610
  }
3448
3611
  }
3449
3612
 
3450
- // Wire MCP manager callbacks to session for reactive tool updates.
3451
- // Skip when reusing a parent's manager the parent owns the callbacks.
3613
+ // MCP manager wiring has two ownership models:
3614
+ // * Single-slot callbacks (tools/prompts/resources changed)exactly one
3615
+ // owner per manager. When reusing a parent's manager (subagent path,
3616
+ // see task/executor.ts), the parent already owns these slots so we
3617
+ // MUST NOT overwrite them. Guarded by `!options.mcpManager`.
3618
+ // * Notification listener — multi-listener by design. Every session with
3619
+ // an MCP manager (fresh OR reused) needs its own bridge to its own
3620
+ // `extensionRunner` so extensions loaded in that session receive frames.
3621
+ // Guarded only by `mcpManager` (see the second `if` below).
3452
3622
  if (mcpManager && !options.mcpManager) {
3453
- mcpManager.setOnToolsChanged(tools => {
3454
- void (async () => {
3455
- try {
3456
- await session.refreshMCPTools(tools);
3457
- } catch (error) {
3458
- logger.warn("MCP tool refresh failed", {
3459
- error: error instanceof Error ? error.message : String(error),
3460
- });
3461
- }
3462
- })();
3623
+ mcpManager.setOnToolsChanged(async tools => {
3624
+ try {
3625
+ await session.refreshMCPTools(tools);
3626
+ } catch (error) {
3627
+ logger.warn("MCP tool refresh failed", {
3628
+ error: error instanceof Error ? error.message : String(error),
3629
+ });
3630
+ }
3463
3631
  });
3464
3632
  // Wire prompt refresh → rebuild MCP prompt slash commands
3465
3633
  mcpManager.setOnPromptsChanged(serverName => {
@@ -3492,6 +3660,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3492
3660
  });
3493
3661
  }
3494
3662
 
3663
+ if (mcpManager) {
3664
+ // Bridge server-initiated notifications to this session's extension
3665
+ // handlers. Multi-listener registration: fresh-manager and reused-manager
3666
+ // sessions both install their own listener here, so a subagent's
3667
+ // extensions get frames even though the parent owns the single-slot
3668
+ // tool/prompt/resource callbacks above. MCPManager fires known
3669
+ // list/update refreshes internally, then invokes all registered
3670
+ // listeners with (server, method, params) for every frame (including
3671
+ // server-custom methods). Two-layer buffering protects the startup
3672
+ // race: MCPManager buffers frames received before the first
3673
+ // `addNotificationListener` subscriber (drains here); ExtensionRunner
3674
+ // buffers frames received before `initialize()` and drains them on
3675
+ // init. Both drop-oldest under pressure at cap 100.
3676
+ unsubscribeMcpNotifications = mcpManager.addNotificationListener((server, method, params) => {
3677
+ void extensionRunner.emitMcpNotification({ server, method, params });
3678
+ });
3679
+ // postmortem.register returns a cancel function; capture it so explicit
3680
+ // session.dispose can remove this from the global list (see finally above).
3681
+ unregisterMcpPostmortem = postmortem.register("mcp-notification-listener-cleanup", () =>
3682
+ unsubscribeMcpNotifications?.(),
3683
+ );
3684
+ }
3685
+
3495
3686
  startDeferredMCPDiscovery?.(session);
3496
3687
 
3497
3688
  return {
@@ -0,0 +1,98 @@
1
+ import type { AgentOptions } from "@oh-my-pi/pi-agent-core";
2
+ import type { OAuthAccessResolution } from "@oh-my-pi/pi-ai";
3
+ import type { ApiKeyResolver } from "@oh-my-pi/pi-ai/auth-retry";
4
+ import type { AuthStorage } from "../session/auth-storage";
5
+ import type { SecurityAccountRef } from "./contracts";
6
+
7
+ export interface ExactSecurityOAuthOptions {
8
+ authStorage: AuthStorage;
9
+ account: SecurityAccountRef;
10
+ }
11
+
12
+ export function assertSecurityIdentityMatches(
13
+ account: SecurityAccountRef,
14
+ resolution: {
15
+ credentialId?: number;
16
+ accountId?: string;
17
+ email?: string;
18
+ orgId?: string;
19
+ orgName?: string;
20
+ },
21
+ ): void {
22
+ if (
23
+ account.credentialId !== resolution.credentialId ||
24
+ (account.accountId !== undefined && account.accountId !== resolution.accountId) ||
25
+ (account.email !== undefined && account.email !== resolution.email) ||
26
+ (account.organizationId !== undefined && account.organizationId !== resolution.orgId) ||
27
+ (account.organizationName !== undefined && account.organizationName !== resolution.orgName)
28
+ ) {
29
+ throw new Error("Security scan authentication identity mismatch");
30
+ }
31
+ }
32
+
33
+ export function selectSecurityAccount(
34
+ authStorage: AuthStorage,
35
+ provider: string,
36
+ requestedCredentialId?: number,
37
+ sessionId?: string,
38
+ ): SecurityAccountRef {
39
+ const accounts = authStorage.listOAuthAccounts(provider, sessionId);
40
+ const selected =
41
+ requestedCredentialId !== undefined
42
+ ? accounts.find(account => account.credentialId === requestedCredentialId)
43
+ : (accounts.find(account => account.active) ?? (accounts.length === 1 ? accounts[0] : undefined));
44
+ if (!selected) {
45
+ if (accounts.length === 0) throw new Error(`Security scans require a stored OAuth account for ${provider}`);
46
+ if (requestedCredentialId !== undefined) {
47
+ throw new Error(`Security OAuth credential ${requestedCredentialId} is not available for ${provider}`);
48
+ }
49
+ throw new Error(
50
+ `Multiple OAuth accounts are available for ${provider}; supply credentialId to pin one exact account`,
51
+ );
52
+ }
53
+ const account: SecurityAccountRef = { provider, credentialId: selected.credentialId };
54
+ if (selected.accountId !== undefined) account.accountId = selected.accountId;
55
+ if (selected.email !== undefined) account.email = selected.email;
56
+ if (selected.orgId !== undefined) account.organizationId = selected.orgId;
57
+ if (selected.orgName !== undefined) account.organizationName = selected.orgName;
58
+ return account;
59
+ }
60
+
61
+ export async function resolveExactSecurityOAuthAccess(
62
+ authStorage: AuthStorage,
63
+ account: SecurityAccountRef,
64
+ options: { forceRefresh: boolean; signal?: AbortSignal },
65
+ ): Promise<Extract<OAuthAccessResolution, { ok: true }>> {
66
+ const resolution = await authStorage.getOAuthAccessByCredentialId(account.provider, account.credentialId, options);
67
+ if (!resolution) throw new Error("The pinned security OAuth credential is unavailable");
68
+ assertSecurityIdentityMatches(account, resolution);
69
+ if (!resolution.ok) throw new Error("The pinned security OAuth credential could not be resolved");
70
+ return resolution;
71
+ }
72
+
73
+ /**
74
+ * Build a request credential resolver pinned to one durable OAuth row.
75
+ *
76
+ * Initial resolution and refresh both target the same row. The auth driver's
77
+ * final sibling-rotation step returns `undefined`, so an unavailable account
78
+ * fails the scan rather than crossing an account/workspace boundary.
79
+ */
80
+ export function createExactSecurityOAuthResolver(
81
+ options: ExactSecurityOAuthOptions,
82
+ ): NonNullable<AgentOptions["getApiKey"]> {
83
+ const { account, authStorage } = options;
84
+ return model => {
85
+ if (model.provider !== account.provider) {
86
+ throw new Error("Security scan authentication provider mismatch");
87
+ }
88
+ const resolver: ApiKeyResolver = async context => {
89
+ if (context.lastChance) return undefined;
90
+ const resolution = await resolveExactSecurityOAuthAccess(authStorage, account, {
91
+ forceRefresh: context.error !== undefined,
92
+ signal: context.signal,
93
+ });
94
+ return resolution.accessToken;
95
+ };
96
+ return resolver;
97
+ };
98
+ }