@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.
- package/CHANGELOG.md +110 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
- package/dist/cli.js +10009 -9654
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-discovery.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +73 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/internal-urls/security-protocol.d.ts +15 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +18 -0
- package/dist/types/security/auth.d.ts +30 -0
- package/dist/types/security/cloud.d.ts +79 -0
- package/dist/types/security/comparison.d.ts +49 -0
- package/dist/types/security/contracts/ids.d.ts +15 -0
- package/dist/types/security/contracts/index.d.ts +4 -0
- package/dist/types/security/contracts/schemas.d.ts +660 -0
- package/dist/types/security/contracts/types.d.ts +234 -0
- package/dist/types/security/contracts/validation.d.ts +5 -0
- package/dist/types/security/coordinator.d.ts +100 -0
- package/dist/types/security/importers/codex-security.d.ts +7 -0
- package/dist/types/security/importers/index.d.ts +2 -0
- package/dist/types/security/importers/sarif.d.ts +9 -0
- package/dist/types/security/index.d.ts +13 -0
- package/dist/types/security/preflight.d.ts +61 -0
- package/dist/types/security/provenance.d.ts +24 -0
- package/dist/types/security/publication.d.ts +78 -0
- package/dist/types/security/remediation.d.ts +27 -0
- package/dist/types/security/resource-output.d.ts +8 -0
- package/dist/types/security/sarif.d.ts +2 -0
- package/dist/types/security/store.d.ts +40 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/slash-commands/helpers/security.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/executor.d.ts +14 -2
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +10 -2
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/security-scan.d.ts +96 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/scripts/security-compare.ts +40 -0
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-discovery.ts +32 -5
- package/src/config/model-registry.ts +107 -25
- package/src/config/settings-schema.ts +77 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/eval/py/prelude.py +7 -3
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/internal-urls/router.ts +4 -1
- package/src/internal-urls/security-protocol.ts +261 -0
- package/src/internal-urls/types.ts +1 -1
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +7 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +15 -9
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +31 -17
- package/src/modes/rpc/host-uris.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/agents/security-reviewer.md +75 -0
- package/src/prompts/security/scan-coordinator.md +7 -0
- package/src/prompts/security/scan-request.md +21 -0
- package/src/prompts/security/validate-request.md +8 -0
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/grep.md +7 -6
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/sdk.ts +212 -21
- package/src/security/auth.ts +98 -0
- package/src/security/cloud.ts +686 -0
- package/src/security/comparison.ts +255 -0
- package/src/security/contracts/ids.ts +111 -0
- package/src/security/contracts/index.ts +4 -0
- package/src/security/contracts/schemas.ts +201 -0
- package/src/security/contracts/types.ts +254 -0
- package/src/security/contracts/validation.ts +65 -0
- package/src/security/coordinator.ts +708 -0
- package/src/security/importers/codex-security.ts +387 -0
- package/src/security/importers/index.ts +2 -0
- package/src/security/importers/sarif.ts +357 -0
- package/src/security/index.ts +13 -0
- package/src/security/preflight.ts +405 -0
- package/src/security/provenance.ts +106 -0
- package/src/security/publication.ts +326 -0
- package/src/security/remediation.ts +93 -0
- package/src/security/resource-output.ts +50 -0
- package/src/security/sarif.ts +78 -0
- package/src/security/store.ts +430 -0
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +302 -175
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +152 -3
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +20 -4
- package/src/task/render.ts +20 -4
- package/src/task/structured-subagent.ts +6 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +37 -5
- package/src/tools/path-utils.ts +91 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/security-scan.ts +287 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- 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 =
|
|
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 =
|
|
2541
|
-
|
|
2542
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
3451
|
-
//
|
|
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
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
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
|
+
}
|