@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
@@ -0,0 +1,708 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import type { Model } from "@oh-my-pi/pi-ai";
4
+ import { logger, prompt } from "@oh-my-pi/pi-utils";
5
+ import type { AsyncJobManager } from "../async/job-manager";
6
+ import type { ModelRegistry } from "../config/model-registry";
7
+ import type { Settings } from "../config/settings";
8
+ import type { ToolDefinition } from "../extensibility/extensions";
9
+ import securityReviewerPrompt from "../prompts/agents/security-reviewer.md" with { type: "text" };
10
+ import securityCoordinatorPrompt from "../prompts/security/scan-coordinator.md" with { type: "text" };
11
+ import securityRequestPrompt from "../prompts/security/scan-request.md" with { type: "text" };
12
+ import securityPublishDescription from "../prompts/tools/security-publish.md" with { type: "text" };
13
+ import { createAgentSession } from "../sdk";
14
+ import type { AgentSession } from "../session/agent-session";
15
+ import type { AuthStorage } from "../session/auth-storage";
16
+ import { SessionManager } from "../session/session-manager";
17
+ import * as git from "../utils/git";
18
+ import { createExactSecurityOAuthResolver, selectSecurityAccount } from "./auth";
19
+ import type {
20
+ SecurityCoverage,
21
+ SecurityModelRef,
22
+ SecurityScan,
23
+ SecurityScanBundle,
24
+ SecurityScanPlan,
25
+ SecurityTargetKind,
26
+ } from "./contracts";
27
+ import { createSecurityScanId } from "./contracts";
28
+ import type { SecurityGitAdapter, SecurityTargetRequest } from "./preflight";
29
+ import {
30
+ assertSecurityScanPlanFresh,
31
+ createSecurityScanPlan,
32
+ DEFAULT_SECURITY_GIT_ADAPTER,
33
+ prepareSecurityOutputDirectory,
34
+ } from "./preflight";
35
+ import {
36
+ createNativeSecurityProducer,
37
+ createNativeSecurityProvenance,
38
+ createSecurityWorkflowFingerprint,
39
+ } from "./provenance";
40
+ import { createSecurityPublicationTool } from "./publication";
41
+ import { SecurityStore, writeSecurityBundleToDirectory } from "./store";
42
+
43
+ const SECURITY_SESSION_TOOLS = ["read", "grep", "glob", "lsp", "ast_grep", "task", "security_publish"];
44
+ const SECURITY_WORKFLOW_FINGERPRINT = createSecurityWorkflowFingerprint([
45
+ securityCoordinatorPrompt,
46
+ securityRequestPrompt,
47
+ securityReviewerPrompt,
48
+ securityPublishDescription,
49
+ ]);
50
+
51
+ export type SecurityOperationPhase =
52
+ | "queued"
53
+ | "preparing"
54
+ | "reviewing"
55
+ | "publishing"
56
+ | "completed"
57
+ | "partial"
58
+ | "cancelled"
59
+ | "failed";
60
+
61
+ export interface SecurityOperationSnapshot {
62
+ operationId: string;
63
+ planId: string;
64
+ scanId: string;
65
+ phase: SecurityOperationPhase;
66
+ createdAt: string;
67
+ updatedAt: string;
68
+ jobId?: string;
69
+ sessionFile?: string;
70
+ findingCount: number;
71
+ error?: string;
72
+ }
73
+
74
+ export interface SecurityCoordinatorHost {
75
+ cwd: string;
76
+ settings: Settings;
77
+ authStorage: AuthStorage;
78
+ modelRegistry: ModelRegistry;
79
+ activeModel?: Model;
80
+ sessionId?: string;
81
+ agentId?: string;
82
+ asyncJobManager?: AsyncJobManager;
83
+ }
84
+
85
+ export interface SecurityPreflightInput {
86
+ target?: SecurityTargetRequest;
87
+ knowledgeBasePaths?: string[];
88
+ outputRoot?: string;
89
+ archiveExisting?: boolean;
90
+ credentialId?: number;
91
+ model?: Model;
92
+ thinkingLevel?: string;
93
+ signal?: AbortSignal;
94
+ }
95
+
96
+ export interface SecurityStartInput {
97
+ planId: string;
98
+ }
99
+
100
+ export interface SecurityScanSession {
101
+ prompt(
102
+ text: string,
103
+ options?: { expandPromptTemplates?: boolean; synthetic?: boolean; userInitiated?: boolean },
104
+ ): Promise<boolean>;
105
+ waitForIdle(): Promise<void>;
106
+ getSessionStats?(): {
107
+ tokens: {
108
+ input: number;
109
+ output: number;
110
+ reasoning: number;
111
+ cacheRead: number;
112
+ cacheWrite: number;
113
+ total: number;
114
+ };
115
+ cost: number;
116
+ premiumRequests: number;
117
+ };
118
+ abort(options?: { reason?: string }): Promise<void>;
119
+ dispose(): Promise<void>;
120
+ readonly sessionFile?: string;
121
+ }
122
+
123
+ export interface SecurityScanSessionFactoryInput {
124
+ host: SecurityCoordinatorHost;
125
+ plan: SecurityScanPlan;
126
+ executionRoot: string;
127
+ scanId: string;
128
+ model: Model;
129
+ publicationTool: ToolDefinition;
130
+ sessionManager: SessionManager;
131
+ }
132
+
133
+ export type SecurityScanSessionFactory = (input: SecurityScanSessionFactoryInput) => Promise<SecurityScanSession>;
134
+
135
+ export interface SecurityCoordinatorDependencies {
136
+ createSession?: SecurityScanSessionFactory;
137
+ openStore?: (repositoryRoot: string) => Promise<SecurityStore>;
138
+ gitAdapter?: SecurityGitAdapter;
139
+ now?: () => Date;
140
+ createOperationId?: () => string;
141
+ }
142
+
143
+ interface SecurityOperationRecord {
144
+ snapshot: SecurityOperationSnapshot;
145
+ promise: Promise<void>;
146
+ abortController?: AbortController;
147
+ }
148
+
149
+ function toIsoTimestamp(now: () => Date): string {
150
+ return now().toISOString();
151
+ }
152
+
153
+ function securityConfigSnapshot(settings: Settings): Record<string, boolean> {
154
+ return { securityEnabled: settings.get("security.enabled") };
155
+ }
156
+
157
+ function createOperationId(): string {
158
+ return `secop_${Bun.randomUUIDv7().replaceAll("-", "")}`;
159
+ }
160
+
161
+ function mapCoverageMode(targetKind: SecurityTargetKind): SecurityCoverage["mode"] {
162
+ switch (targetKind) {
163
+ case "ref_diff":
164
+ return "diff";
165
+ case "working_tree":
166
+ return "working_tree";
167
+ case "scoped_path":
168
+ return "scoped_path";
169
+ case "imported":
170
+ return "imported";
171
+ default:
172
+ return "repository";
173
+ }
174
+ }
175
+
176
+ function initialCoverage(plan: SecurityScanPlan): SecurityCoverage {
177
+ return {
178
+ mode: mapCoverageMode(plan.target.kind),
179
+ completeness: "unknown",
180
+ inventoryStrategy:
181
+ plan.target.kind === "ref_diff" ? "diff" : plan.target.kind === "scoped_path" ? "scoped_path" : "repository",
182
+ includePaths: plan.target.includePaths,
183
+ excludePaths: plan.target.excludePaths,
184
+ surfaces: [],
185
+ explicitExclusions: [],
186
+ deferred: [{ id: "scan-pending", reason: "Security review has not completed" }],
187
+ };
188
+ }
189
+
190
+ function initialBundle(
191
+ store: SecurityStore,
192
+ plan: SecurityScanPlan,
193
+ scanId: string,
194
+ operationId: string,
195
+ startedAt: string,
196
+ status: SecurityScan["status"] = "running",
197
+ ): SecurityScanBundle {
198
+ const producer = createNativeSecurityProducer();
199
+ const provenance = createNativeSecurityProvenance({
200
+ createdAt: startedAt,
201
+ account: plan.account,
202
+ planFingerprint: plan.fingerprint,
203
+ operationId,
204
+ workflowFingerprint: plan.workflowFingerprint,
205
+ });
206
+ return {
207
+ scan: {
208
+ documentType: "omp-security.scan",
209
+ schemaVersion: "1.0",
210
+ id: scanId,
211
+ projectKey: store.projectKey,
212
+ status,
213
+ createdAt: plan.createdAt,
214
+ startedAt,
215
+ plan,
216
+ target: plan.target,
217
+ producer,
218
+ provenance,
219
+ findingIds: [],
220
+ coverage: initialCoverage(plan),
221
+ },
222
+ findings: [],
223
+ };
224
+ }
225
+
226
+ async function createDefaultSecuritySession(input: SecurityScanSessionFactoryInput): Promise<AgentSession> {
227
+ const scanSettings = await input.host.settings.cloneForCwd(input.executionRoot);
228
+ const modelSelector = `${input.model.provider}/${input.model.id}`;
229
+ scanSettings.override("retry.modelFallback", false);
230
+ scanSettings.override("retry.usageAwareFallback", false);
231
+ scanSettings.override("retry.fallbackChains", {});
232
+ scanSettings.override("task.agentModelOverrides", {
233
+ ...scanSettings.get("task.agentModelOverrides"),
234
+ "security-reviewer": modelSelector,
235
+ });
236
+ scanSettings.override("task.agentPrewalk", {
237
+ ...scanSettings.get("task.agentPrewalk"),
238
+ "security-reviewer": "off",
239
+ });
240
+ const { session } = await createAgentSession({
241
+ cwd: input.executionRoot,
242
+ authStorage: input.host.authStorage,
243
+ modelRegistry: input.host.modelRegistry,
244
+ settings: scanSettings,
245
+ model: input.model,
246
+ getApiKey: createExactSecurityOAuthResolver({
247
+ authStorage: input.host.authStorage,
248
+ account: input.plan.account,
249
+ }),
250
+ providerSessionId: `security:${input.scanId}`,
251
+ sessionManager: input.sessionManager,
252
+ customTools: [input.publicationTool],
253
+ toolNames: SECURITY_SESSION_TOOLS,
254
+ restrictToolNames: true,
255
+ allowRestrictedCustomTools: true,
256
+ spawns: "security-reviewer",
257
+ appendSystemPrompt: securityCoordinatorPrompt.trim(),
258
+ disableExtensionDiscovery: true,
259
+ enableMCP: false,
260
+ enableIrc: false,
261
+ enableLsp: true,
262
+ lspReadOnly: true,
263
+ hasUI: false,
264
+ autoApprove: true,
265
+ skipPythonPreflight: true,
266
+ agentId: `Security-${input.scanId.slice(-12)}`,
267
+ agentDisplayName: "security",
268
+ });
269
+ return session;
270
+ }
271
+
272
+ function requestText(plan: SecurityScanPlan, executionRoot: string, diffText?: string): string {
273
+ return prompt
274
+ .render(securityRequestPrompt, {
275
+ repositoryRoot: executionRoot,
276
+ targetKind: plan.target.kind,
277
+ revision: plan.target.revision ?? "",
278
+ baseRevision: plan.target.baseRevision ?? "",
279
+ headRevision: plan.target.headRevision ?? "",
280
+ includePaths: plan.target.includePaths.length > 0 ? plan.target.includePaths.join(", ") : "all in-scope paths",
281
+ excludePaths: plan.target.excludePaths.length > 0 ? plan.target.excludePaths.join(", ") : "none",
282
+ knowledgeBases:
283
+ plan.knowledgeBases.length > 0 ? plan.knowledgeBases.map(item => item.path).join(", ") : "none",
284
+ planFingerprint: plan.fingerprint,
285
+ diffText: diffText ?? "",
286
+ })
287
+ .trim();
288
+ }
289
+
290
+ function terminalText(snapshot: SecurityOperationSnapshot): string {
291
+ return [
292
+ `Security scan ${snapshot.scanId}: ${snapshot.phase}.`,
293
+ `Operation: ${snapshot.operationId}`,
294
+ `Plan: ${snapshot.planId}`,
295
+ `Findings: ${snapshot.findingCount}`,
296
+ snapshot.error ? `Error: ${snapshot.error}` : undefined,
297
+ ]
298
+ .filter((line): line is string => line !== undefined)
299
+ .join("\n");
300
+ }
301
+
302
+ interface PreparedSecurityExecutionTarget {
303
+ cwd: string;
304
+ diffText?: string;
305
+ cleanup(): Promise<void>;
306
+ }
307
+
308
+ const ACTIVE_SECURITY_OPERATIONS = new Set<string>();
309
+
310
+ function operationIdFromBundle(bundle: SecurityScanBundle): string | undefined {
311
+ const value = bundle.scan.provenance.metadata?.operationId;
312
+ return typeof value === "string" && value.length > 0 ? value : undefined;
313
+ }
314
+
315
+ function operationPhaseFromStatus(status: SecurityScan["status"]): SecurityOperationPhase {
316
+ return status === "running" || status === "planned" ? "failed" : status;
317
+ }
318
+
319
+ async function prepareSecurityExecutionTarget(
320
+ plan: SecurityScanPlan,
321
+ store: SecurityStore,
322
+ scanId: string,
323
+ adapter: SecurityGitAdapter,
324
+ signal: AbortSignal,
325
+ ): Promise<PreparedSecurityExecutionTarget> {
326
+ if (plan.target.kind !== "ref_diff") {
327
+ return { cwd: plan.repositoryRoot, cleanup: async () => undefined };
328
+ }
329
+ const headRevision = plan.target.headRevision;
330
+ const baseRevision = plan.target.baseRevision;
331
+ if (!headRevision || !baseRevision) throw new Error("ref_diff security plan is missing resolved revisions");
332
+ const targetsRoot = path.join(store.projectDirectory, "targets");
333
+ await fs.mkdir(targetsRoot, { recursive: true, mode: 0o700 });
334
+ if (process.platform !== "win32") await fs.chmod(targetsRoot, 0o700);
335
+ const cwd = path.join(targetsRoot, scanId);
336
+ let added = false;
337
+ try {
338
+ await git.worktree.add(plan.repositoryRoot, cwd, headRevision, { detach: true, signal });
339
+ added = true;
340
+ const diffText = await adapter.diffTree(plan.repositoryRoot, baseRevision, headRevision, signal);
341
+ return {
342
+ cwd,
343
+ diffText,
344
+ async cleanup() {
345
+ const removed = await git.worktree.tryRemove(plan.repositoryRoot, cwd, { force: true });
346
+ if (!removed) await fs.rm(cwd, { recursive: true, force: true });
347
+ },
348
+ };
349
+ } catch (error) {
350
+ if (added) await git.worktree.tryRemove(plan.repositoryRoot, cwd, { force: true });
351
+ await fs.rm(cwd, { recursive: true, force: true });
352
+ throw error;
353
+ }
354
+ }
355
+
356
+ export class SecurityCoordinator {
357
+ readonly #host: SecurityCoordinatorHost;
358
+ readonly #createSession: SecurityScanSessionFactory;
359
+ readonly #openStore: (repositoryRoot: string) => Promise<SecurityStore>;
360
+ readonly #gitAdapter: SecurityGitAdapter;
361
+ readonly #now: () => Date;
362
+ readonly #createOperationId: () => string;
363
+ readonly #operations = new Map<string, SecurityOperationRecord>();
364
+ #recovery?: Promise<void>;
365
+
366
+ constructor(host: SecurityCoordinatorHost, dependencies: SecurityCoordinatorDependencies = {}) {
367
+ this.#host = host;
368
+ this.#createSession = dependencies.createSession ?? createDefaultSecuritySession;
369
+ this.#openStore = dependencies.openStore ?? (cwd => SecurityStore.openForCwd(cwd));
370
+ this.#gitAdapter = dependencies.gitAdapter ?? DEFAULT_SECURITY_GIT_ADAPTER;
371
+ this.#now = dependencies.now ?? (() => new Date());
372
+ this.#createOperationId = dependencies.createOperationId ?? createOperationId;
373
+ }
374
+ async #ensureRecovered(): Promise<void> {
375
+ this.#recovery ??= this.#recoverInterruptedOperations();
376
+ await this.#recovery;
377
+ }
378
+
379
+ async #recoverInterruptedOperations(): Promise<void> {
380
+ const store = await this.#openStore(this.#host.cwd);
381
+ for (const summary of await store.listScans()) {
382
+ const bundle = await store.getBundle(summary.id);
383
+ if (!bundle) continue;
384
+ const operationId = operationIdFromBundle(bundle);
385
+ if (!operationId || this.#operations.has(operationId) || ACTIVE_SECURITY_OPERATIONS.has(operationId)) continue;
386
+ if (bundle.scan.status === "running" || bundle.scan.status === "planned") {
387
+ const message = "Security scan was interrupted by a process restart";
388
+ bundle.scan.status = "failed";
389
+ bundle.scan.completedAt = toIsoTimestamp(this.#now);
390
+ bundle.scan.error = message;
391
+ await store.putBundle(bundle);
392
+ if (bundle.scan.target.kind === "ref_diff") {
393
+ const targetPath = path.join(store.projectDirectory, "targets", bundle.scan.id);
394
+ await git.worktree.tryRemove(bundle.scan.target.repositoryRoot, targetPath, { force: true });
395
+ await fs.rm(targetPath, { recursive: true, force: true });
396
+ }
397
+ }
398
+ const snapshot: SecurityOperationSnapshot = {
399
+ operationId,
400
+ planId: bundle.scan.plan?.id ?? "",
401
+ scanId: bundle.scan.id,
402
+ phase: operationPhaseFromStatus(bundle.scan.status),
403
+ createdAt: bundle.scan.createdAt,
404
+ updatedAt: bundle.scan.completedAt ?? bundle.scan.startedAt ?? bundle.scan.createdAt,
405
+ findingCount: bundle.findings.length,
406
+ };
407
+ if (bundle.scan.error !== undefined) snapshot.error = bundle.scan.error;
408
+ this.#operations.set(operationId, { snapshot, promise: Promise.resolve() });
409
+ }
410
+ }
411
+
412
+ async preflight(input: SecurityPreflightInput = {}): Promise<SecurityScanPlan> {
413
+ if (!this.#host.settings.get("security.enabled")) {
414
+ throw new Error("Security is disabled; enable security.enabled before planning a scan");
415
+ }
416
+ const model = input.model ?? this.#host.activeModel;
417
+ if (!model) throw new Error("Security scan preflight requires an active model");
418
+ const account = selectSecurityAccount(
419
+ this.#host.authStorage,
420
+ model.provider,
421
+ input.credentialId,
422
+ this.#host.sessionId,
423
+ );
424
+ const store = await this.#openStore(this.#host.cwd);
425
+ const workRoot = path.join(store.projectDirectory, "work");
426
+ await fs.mkdir(workRoot, { recursive: true, mode: 0o700 });
427
+ if (process.platform !== "win32") await fs.chmod(workRoot, 0o700);
428
+ const modelRef: SecurityModelRef = { provider: model.provider, modelId: model.id };
429
+ if (input.thinkingLevel !== undefined) modelRef.thinkingLevel = input.thinkingLevel;
430
+ const plan = await createSecurityScanPlan(
431
+ {
432
+ cwd: this.#host.cwd,
433
+ target: input.target ?? { kind: "repository" },
434
+ knowledgeBasePaths: input.knowledgeBasePaths,
435
+ outputRoot: input.outputRoot ?? path.join(workRoot, Bun.randomUUIDv7()),
436
+ archiveExisting: input.archiveExisting,
437
+ model: modelRef,
438
+ account,
439
+ config: securityConfigSnapshot(this.#host.settings),
440
+ workflowFingerprint: SECURITY_WORKFLOW_FINGERPRINT,
441
+ signal: input.signal,
442
+ },
443
+ this.#gitAdapter,
444
+ );
445
+ await store.putPlan(plan);
446
+ return plan;
447
+ }
448
+
449
+ async start(input: SecurityStartInput): Promise<SecurityOperationSnapshot> {
450
+ if (!this.#host.settings.get("security.enabled")) {
451
+ throw new Error("Security is disabled; enable security.enabled before starting a scan");
452
+ }
453
+ await this.#ensureRecovered();
454
+ const store = await this.#openStore(this.#host.cwd);
455
+ const plan = await store.getPlan(input.planId);
456
+ if (!plan) throw new Error(`Unknown security scan plan: ${input.planId}`);
457
+ await assertSecurityScanPlanFresh(
458
+ plan,
459
+ {
460
+ config: securityConfigSnapshot(this.#host.settings),
461
+ workflowFingerprint: SECURITY_WORKFLOW_FINGERPRINT,
462
+ },
463
+ this.#gitAdapter,
464
+ );
465
+ const operationId = this.#createOperationId();
466
+ const scanId = createSecurityScanId();
467
+ const createdAt = toIsoTimestamp(this.#now);
468
+ const snapshot: SecurityOperationSnapshot = {
469
+ operationId,
470
+ planId: plan.id,
471
+ scanId,
472
+ phase: "queued",
473
+ createdAt,
474
+ updatedAt: createdAt,
475
+ findingCount: 0,
476
+ };
477
+ const record: SecurityOperationRecord = { snapshot, promise: Promise.resolve() };
478
+ this.#operations.set(operationId, record);
479
+ ACTIVE_SECURITY_OPERATIONS.add(operationId);
480
+ const run = async (signal: AbortSignal, reportProgress?: (text: string) => Promise<void>): Promise<void> => {
481
+ await this.#run(record, plan, store, signal, reportProgress);
482
+ };
483
+ const manager = this.#host.asyncJobManager;
484
+ if (manager) {
485
+ const jobId = manager.register(
486
+ "task",
487
+ `Security scan ${scanId}`,
488
+ async ({ signal, reportProgress }) => {
489
+ await run(signal, text => reportProgress(text, { operationId, scanId, phase: record.snapshot.phase }));
490
+ return terminalText(record.snapshot);
491
+ },
492
+ { id: operationId, ownerId: this.#host.agentId },
493
+ );
494
+ record.snapshot.jobId = jobId;
495
+ record.promise = manager.getJob(jobId)?.promise ?? Promise.resolve();
496
+ } else {
497
+ const abortController = new AbortController();
498
+ record.abortController = abortController;
499
+ record.promise = run(abortController.signal);
500
+ }
501
+ return { ...record.snapshot };
502
+ }
503
+
504
+ async status(operationId: string): Promise<SecurityOperationSnapshot | null> {
505
+ await this.#ensureRecovered();
506
+ let record = this.#operations.get(operationId);
507
+ if (!record && !ACTIVE_SECURITY_OPERATIONS.has(operationId)) {
508
+ // The operation may have run under another session's coordinator; once it
509
+ // is terminal its bundle is on disk, so rescan before reporting unknown.
510
+ await this.#recoverInterruptedOperations();
511
+ record = this.#operations.get(operationId);
512
+ }
513
+ return record ? { ...record.snapshot } : null;
514
+ }
515
+
516
+ async listOperations(): Promise<SecurityOperationSnapshot[]> {
517
+ await this.#ensureRecovered();
518
+ return [...this.#operations.values()]
519
+ .map(record => ({ ...record.snapshot }))
520
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
521
+ }
522
+
523
+ async cancel(operationId: string): Promise<boolean> {
524
+ await this.#ensureRecovered();
525
+ const record = this.#operations.get(operationId);
526
+ if (!record) return false;
527
+ if (["completed", "partial", "cancelled", "failed"].includes(record.snapshot.phase)) return false;
528
+ if (record.snapshot.jobId && this.#host.asyncJobManager) {
529
+ return this.#host.asyncJobManager.cancel(record.snapshot.jobId, { ownerId: this.#host.agentId });
530
+ }
531
+ record.abortController?.abort(new Error("Security scan cancelled"));
532
+ return true;
533
+ }
534
+
535
+ async wait(operationId: string): Promise<SecurityOperationSnapshot> {
536
+ await this.#ensureRecovered();
537
+ const record = this.#operations.get(operationId);
538
+ if (!record) throw new Error(`Unknown security operation: ${operationId}`);
539
+ await record.promise;
540
+ return { ...record.snapshot };
541
+ }
542
+
543
+ #update(record: SecurityOperationRecord, phase: SecurityOperationPhase, error?: string): void {
544
+ record.snapshot.phase = phase;
545
+ record.snapshot.updatedAt = toIsoTimestamp(this.#now);
546
+ record.snapshot.error = error;
547
+ }
548
+
549
+ async #run(
550
+ record: SecurityOperationRecord,
551
+ plan: SecurityScanPlan,
552
+ store: SecurityStore,
553
+ signal: AbortSignal,
554
+ reportProgress?: (text: string) => Promise<void>,
555
+ ): Promise<void> {
556
+ const startedAt = toIsoTimestamp(this.#now);
557
+ let session: SecurityScanSession | undefined;
558
+ let publishedBundle: SecurityScanBundle | undefined;
559
+ let executionTarget: PreparedSecurityExecutionTarget | undefined;
560
+ try {
561
+ await store.putBundle(
562
+ initialBundle(store, plan, record.snapshot.scanId, record.snapshot.operationId, startedAt),
563
+ );
564
+ if (signal.aborted) throw signal.reason ?? new Error("Security scan cancelled");
565
+ await prepareSecurityOutputDirectory(plan.output, record.snapshot.scanId);
566
+ this.#update(record, "preparing");
567
+ await reportProgress?.("Preparing OMP-native security scan");
568
+ executionTarget = await prepareSecurityExecutionTarget(
569
+ plan,
570
+ store,
571
+ record.snapshot.scanId,
572
+ this.#gitAdapter,
573
+ signal,
574
+ );
575
+ const activeModel = this.#host.activeModel;
576
+ const model =
577
+ activeModel?.provider === plan.model.provider && activeModel.id === plan.model.modelId
578
+ ? activeModel
579
+ : this.#host.modelRegistry.find(plan.model.provider, plan.model.modelId);
580
+ if (!model)
581
+ throw new Error(`Security scan model is unavailable: ${plan.model.provider}/${plan.model.modelId}`);
582
+ const sessionsDirectory = path.join(store.projectDirectory, "sessions");
583
+ await fs.mkdir(sessionsDirectory, { recursive: true, mode: 0o700 });
584
+ const sessionManager = SessionManager.create(executionTarget.cwd, sessionsDirectory);
585
+ const publicationTool = createSecurityPublicationTool({
586
+ plan,
587
+ scanId: record.snapshot.scanId,
588
+ store,
589
+ startedAt,
590
+ sessionId: `security:${record.snapshot.scanId}`,
591
+ operationId: record.snapshot.operationId,
592
+ onPublished: async bundle => {
593
+ publishedBundle = bundle;
594
+ record.snapshot.findingCount = bundle.findings.length;
595
+ this.#update(record, "publishing");
596
+ },
597
+ });
598
+ session = await this.#createSession({
599
+ host: this.#host,
600
+ plan,
601
+ scanId: record.snapshot.scanId,
602
+ executionRoot: executionTarget.cwd,
603
+ model,
604
+ // Bare `ToolDefinition` erases the concrete schema; the sdk.ts
605
+ // `as unknown as CustomTool` precedent applies to the same variance wall.
606
+ publicationTool: publicationTool as unknown as ToolDefinition,
607
+ sessionManager,
608
+ });
609
+ record.snapshot.sessionFile = session.sessionFile;
610
+ const abortSession = (): void => {
611
+ void session?.abort({ reason: "Security scan cancelled" });
612
+ };
613
+ signal.addEventListener("abort", abortSession, { once: true });
614
+ try {
615
+ if (signal.aborted) throw signal.reason ?? new Error("Security scan cancelled");
616
+ this.#update(record, "reviewing");
617
+ await reportProgress?.("Reviewing repository with OMP security workers");
618
+ await session.prompt(requestText(plan, executionTarget.cwd, executionTarget.diffText), {
619
+ expandPromptTemplates: false,
620
+ synthetic: true,
621
+ userInitiated: false,
622
+ });
623
+ await session.waitForIdle();
624
+ record.snapshot.sessionFile = session.sessionFile;
625
+ if (publishedBundle) {
626
+ const stats = session.getSessionStats?.();
627
+ publishedBundle.scan.metrics = {
628
+ runtimeMs: Math.max(0, this.#now().getTime() - new Date(startedAt).getTime()),
629
+ ...(stats
630
+ ? {
631
+ tokenUsage: { ...stats.tokens },
632
+ cost: stats.cost,
633
+ premiumRequests: stats.premiumRequests,
634
+ }
635
+ : {}),
636
+ };
637
+ await writeSecurityBundleToDirectory(plan.output.root, publishedBundle);
638
+ await store.putBundle(publishedBundle);
639
+ }
640
+ } finally {
641
+ signal.removeEventListener("abort", abortSession);
642
+ }
643
+ if (signal.aborted) throw signal.reason ?? new Error("Security scan cancelled");
644
+ if (publishedBundle) {
645
+ this.#update(record, "completed");
646
+ await reportProgress?.(`Published ${publishedBundle.findings.length} security finding(s)`);
647
+ return;
648
+ }
649
+ const partial = initialBundle(
650
+ store,
651
+ plan,
652
+ record.snapshot.scanId,
653
+ record.snapshot.operationId,
654
+ startedAt,
655
+ "partial",
656
+ );
657
+ partial.scan.completedAt = toIsoTimestamp(this.#now);
658
+ partial.scan.error = "The scan session ended without publishing a canonical result";
659
+ this.#update(record, "partial", partial.scan.error);
660
+ await store.putBundle(partial);
661
+ } catch (error) {
662
+ if (publishedBundle) {
663
+ // The canonical bundle is already persisted by security_publish; a late
664
+ // failure (metrics/output-directory write) degrades, not invalidates it.
665
+ logger.warn("Security scan post-publication step failed", {
666
+ scanId: record.snapshot.scanId,
667
+ error: error instanceof Error ? error.message : String(error),
668
+ });
669
+ record.snapshot.findingCount = publishedBundle.findings.length;
670
+ this.#update(record, "completed");
671
+ return;
672
+ }
673
+ const message = error instanceof Error ? error.message : String(error);
674
+ const cancelled = signal.aborted;
675
+ const terminal = initialBundle(
676
+ store,
677
+ plan,
678
+ record.snapshot.scanId,
679
+ record.snapshot.operationId,
680
+ startedAt,
681
+ cancelled ? "cancelled" : "failed",
682
+ );
683
+ terminal.scan.completedAt = toIsoTimestamp(this.#now);
684
+ terminal.scan.error = message;
685
+ this.#update(record, cancelled ? "cancelled" : "failed", message);
686
+ await store.putBundle(terminal);
687
+ } finally {
688
+ await session?.dispose().catch(() => undefined);
689
+ await executionTarget?.cleanup().catch(() => undefined);
690
+ ACTIVE_SECURITY_OPERATIONS.delete(record.snapshot.operationId);
691
+ }
692
+ }
693
+ }
694
+
695
+ const COORDINATORS = new Map<string, SecurityCoordinator>();
696
+
697
+ export function getSecurityCoordinator(host: SecurityCoordinatorHost): SecurityCoordinator {
698
+ const key = `${path.resolve(host.cwd)}\u0000${host.sessionId ?? "sessionless"}`;
699
+ const existing = COORDINATORS.get(key);
700
+ if (existing) return existing;
701
+ const coordinator = new SecurityCoordinator(host);
702
+ COORDINATORS.set(key, coordinator);
703
+ return coordinator;
704
+ }
705
+
706
+ export function resetSecurityCoordinatorsForTests(): void {
707
+ COORDINATORS.clear();
708
+ }