@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,451 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { prompt } from "@oh-my-pi/pi-utils";
4
+ import { parseInternalUrl } from "../../internal-urls/parse";
5
+ import { SecurityProtocolHandler } from "../../internal-urls/security-protocol";
6
+ import validationRequestPrompt from "../../prompts/security/validate-request.md" with { type: "text" };
7
+ import { selectSecurityAccount } from "../../security/auth";
8
+ import { CodexSecurityCloudClient, pullCodexSecurityCloudResults } from "../../security/cloud";
9
+ import type { SecurityDispositionStatus } from "../../security/contracts";
10
+ import type { SecurityPreflightInput } from "../../security/coordinator";
11
+ import { getSecurityCoordinator } from "../../security/coordinator";
12
+ import { importCodexSecurityBundle, importSarifFile } from "../../security/importers";
13
+ import type { SecurityTargetRequest } from "../../security/preflight";
14
+ import { SecurityStore, writeSecurityFileAtomic } from "../../security/store";
15
+ import { shortenPath } from "../../tools/render-utils";
16
+ import { parseCommandArgs } from "../../utils/command-args";
17
+ import type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime } from "../types";
18
+ import { commandConsumed, errorMessage, parseSubcommand, usage } from "./parse";
19
+
20
+ interface SecurityPlanCliOptions {
21
+ target: SecurityTargetRequest;
22
+ knowledgeBasePaths: string[];
23
+ outputRoot?: string;
24
+ archiveExisting?: boolean;
25
+ credentialId?: number;
26
+ }
27
+
28
+ const DISPOSITIONS: ReadonlySet<SecurityDispositionStatus> = new Set([
29
+ "open",
30
+ "false_positive",
31
+ "accepted_risk",
32
+ "fixed",
33
+ "wont_fix",
34
+ ]);
35
+
36
+ function coordinatorFor(runtime: SlashCommandRuntime) {
37
+ return getSecurityCoordinator({
38
+ cwd: runtime.cwd,
39
+ settings: runtime.settings,
40
+ authStorage: runtime.session.modelRegistry.authStorage,
41
+ modelRegistry: runtime.session.modelRegistry,
42
+ activeModel: runtime.session.model,
43
+ sessionId: runtime.session.sessionId,
44
+ agentId: runtime.session.getAgentId(),
45
+ asyncJobManager: runtime.session.asyncJobManager,
46
+ });
47
+ }
48
+
49
+ function requireToken(tokens: readonly string[], index: number, flag: string): string {
50
+ const value = tokens[index];
51
+ if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`);
52
+ return value;
53
+ }
54
+
55
+ function parsePositiveCredential(value: string): number {
56
+ const credentialId = Number(value);
57
+ if (!Number.isSafeInteger(credentialId) || credentialId < 1) throw new Error(`Invalid credential id: ${value}`);
58
+ return credentialId;
59
+ }
60
+
61
+ function parsePlanOptions(rest: string): SecurityPlanCliOptions {
62
+ const tokens = parseCommandArgs(rest);
63
+ const includePaths: string[] = [];
64
+ const excludePaths: string[] = [];
65
+ const knowledgeBasePaths: string[] = [];
66
+ let kind: SecurityTargetRequest["kind"] = "repository";
67
+ let baseRevision: string | undefined;
68
+ let headRevision: string | undefined;
69
+ let outputRoot: string | undefined;
70
+ let archiveExisting = false;
71
+ let credentialId: number | undefined;
72
+ for (let index = 0; index < tokens.length; index++) {
73
+ const token = tokens[index]!;
74
+ switch (token) {
75
+ case "--path":
76
+ includePaths.push(requireToken(tokens, ++index, token));
77
+ // `--path` scopes the target; it never overrides an explicit --diff/--working-tree selection.
78
+ if (kind === "repository") kind = "scoped_path";
79
+ break;
80
+ case "--exclude":
81
+ excludePaths.push(requireToken(tokens, ++index, token));
82
+ break;
83
+ case "--working-tree":
84
+ kind = "working_tree";
85
+ break;
86
+ case "--diff":
87
+ kind = "ref_diff";
88
+ baseRevision = requireToken(tokens, ++index, token);
89
+ headRevision = requireToken(tokens, ++index, token);
90
+ break;
91
+ case "--knowledge-base":
92
+ knowledgeBasePaths.push(requireToken(tokens, ++index, token));
93
+ break;
94
+ case "--output":
95
+ outputRoot = requireToken(tokens, ++index, token);
96
+ break;
97
+ case "--archive-existing":
98
+ archiveExisting = true;
99
+ break;
100
+ case "--credential":
101
+ credentialId = parsePositiveCredential(requireToken(tokens, ++index, token));
102
+ break;
103
+ default:
104
+ throw new Error(`Unknown security plan option: ${token}`);
105
+ }
106
+ }
107
+ const common = { includePaths, excludePaths };
108
+ const target: SecurityTargetRequest =
109
+ kind === "ref_diff"
110
+ ? {
111
+ kind,
112
+ baseRevision: baseRevision ?? "",
113
+ headRevision: headRevision ?? "",
114
+ ...common,
115
+ }
116
+ : kind === "working_tree"
117
+ ? { kind, ...common }
118
+ : kind === "scoped_path"
119
+ ? { kind, ...common }
120
+ : { kind: "repository", ...common };
121
+ return { target, knowledgeBasePaths, outputRoot, archiveExisting, credentialId };
122
+ }
123
+
124
+ async function preflight(runtime: SlashCommandRuntime, rest: string) {
125
+ const options = parsePlanOptions(rest);
126
+ const input: SecurityPreflightInput = {
127
+ target: options.target,
128
+ knowledgeBasePaths: options.knowledgeBasePaths,
129
+ outputRoot: options.outputRoot,
130
+ archiveExisting: options.archiveExisting,
131
+ credentialId: options.credentialId,
132
+ model: runtime.session.model,
133
+ };
134
+ return coordinatorFor(runtime).preflight(input);
135
+ }
136
+
137
+ function scanIdFromInput(value: string): string {
138
+ const trimmed = value.trim();
139
+ const match = trimmed.match(/^security:\/\/scans\/([^/]+)/);
140
+ return match?.[1] ?? trimmed;
141
+ }
142
+
143
+ function findingTarget(value: string): { uri: string; scanId: string; findingId: string } {
144
+ const trimmed = value.trim();
145
+ const uriMatch = trimmed.match(/^security:\/\/scans\/([^/]+)\/findings\/([^/]+)$/);
146
+ if (uriMatch) return { uri: trimmed, scanId: uriMatch[1]!, findingId: uriMatch[2]! };
147
+ const [scanId, findingId] = parseCommandArgs(trimmed);
148
+ if (!scanId || !findingId) throw new Error("validate requires a finding URI or <scan-id> <finding-id>");
149
+ return { uri: `security://scans/${scanId}/findings/${findingId}`, scanId, findingId };
150
+ }
151
+
152
+ async function showResource(runtime: SlashCommandRuntime, rest: string): Promise<void> {
153
+ const raw = rest.trim();
154
+ if (!raw) throw new Error("show requires a scan id or security:// URI");
155
+ const uri = raw.startsWith("security://") ? raw : `security://scans/${scanIdFromInput(raw)}`;
156
+ const handler = new SecurityProtocolHandler(undefined, () => true);
157
+ const resource = await handler.resolve(parseInternalUrl(uri), { cwd: runtime.cwd });
158
+ await runtime.output(resource.content);
159
+ }
160
+
161
+ async function importResults(runtime: SlashCommandRuntime, rest: string): Promise<void> {
162
+ const [source] = parseCommandArgs(rest);
163
+ if (!source) throw new Error("import requires a SARIF file or Codex Security bundle directory");
164
+ const store = await SecurityStore.openForCwd(runtime.cwd);
165
+ const absolute = path.resolve(runtime.cwd, source);
166
+ const stats = await fs.stat(absolute);
167
+ const bundle = stats.isDirectory()
168
+ ? await importCodexSecurityBundle(absolute, { repositoryRoot: store.repositoryRoot })
169
+ : await importSarifFile(absolute, { repositoryRoot: store.repositoryRoot });
170
+ await store.putBundle(bundle);
171
+ await runtime.output(`Imported ${bundle.findings.length} finding(s) as security scan ${bundle.scan.id}.`);
172
+ }
173
+
174
+ async function exportResults(runtime: SlashCommandRuntime, rest: string): Promise<void> {
175
+ const tokens = parseCommandArgs(rest);
176
+ const scanId = tokens[0];
177
+ if (!scanId) throw new Error("export requires <scan-id> --output <path> [--format bundle|sarif|report]");
178
+ let outputPath: string | undefined;
179
+ let format: "bundle" | "sarif" | "report" = "bundle";
180
+ for (let index = 1; index < tokens.length; index++) {
181
+ const token = tokens[index]!;
182
+ if (token === "--output") outputPath = requireToken(tokens, ++index, token);
183
+ else if (token === "--format") {
184
+ const value = requireToken(tokens, ++index, token);
185
+ if (value !== "bundle" && value !== "sarif" && value !== "report") {
186
+ throw new Error(`Unknown export format: ${value}`);
187
+ }
188
+ format = value;
189
+ } else throw new Error(`Unknown export option: ${token}`);
190
+ }
191
+ if (!outputPath) throw new Error("export requires --output <path>");
192
+ const store = await SecurityStore.openForCwd(runtime.cwd);
193
+ const bundle = await store.getBundle(scanIdFromInput(scanId));
194
+ if (!bundle) throw new Error(`Unknown security scan: ${scanId}`);
195
+ let content: string;
196
+ if (format === "sarif") {
197
+ if (!bundle.sarif) throw new Error(`Security scan ${scanId} has no SARIF result`);
198
+ content = `${JSON.stringify(bundle.sarif, null, 2)}\n`;
199
+ } else if (format === "report") {
200
+ if (bundle.report === undefined) throw new Error(`Security scan ${scanId} has no report`);
201
+ content = bundle.report;
202
+ } else {
203
+ content = `${JSON.stringify(bundle, null, 2)}\n`;
204
+ }
205
+ const absolute = path.resolve(runtime.cwd, outputPath);
206
+ await writeSecurityFileAtomic(absolute, content, { hardenParent: false });
207
+ await runtime.output(`Exported security scan ${scanId} to ${shortenPath(absolute)}.`);
208
+ }
209
+
210
+ interface CloudCliOptions {
211
+ credentialId?: number;
212
+ configurationId?: string;
213
+ repositoryId?: string;
214
+ repositoryUrl?: string;
215
+ environmentId?: string;
216
+ lookbackDays?: number | "all";
217
+ }
218
+
219
+ function parseCloudOptions(rest: string, subcommand: string): CloudCliOptions {
220
+ const tokens = parseCommandArgs(rest);
221
+ const options: CloudCliOptions = {};
222
+ let positionalConsumed = false;
223
+ for (let index = 0; index < tokens.length; index++) {
224
+ const token = tokens[index]!;
225
+ switch (token) {
226
+ case "--credential":
227
+ options.credentialId = parsePositiveCredential(requireToken(tokens, ++index, token));
228
+ break;
229
+ case "--repo-id":
230
+ options.repositoryId = requireToken(tokens, ++index, token);
231
+ break;
232
+ case "--repo-url":
233
+ options.repositoryUrl = requireToken(tokens, ++index, token);
234
+ break;
235
+ case "--environment":
236
+ options.environmentId = requireToken(tokens, ++index, token);
237
+ break;
238
+ case "--lookback": {
239
+ const value = requireToken(tokens, ++index, token);
240
+ if (value === "all") {
241
+ options.lookbackDays = value;
242
+ break;
243
+ }
244
+ const days = Number(value);
245
+ if (!Number.isSafeInteger(days) || days < 1) throw new Error(`Invalid lookback: ${value}`);
246
+ options.lookbackDays = days;
247
+ break;
248
+ }
249
+ default:
250
+ if (!token.startsWith("--") && !positionalConsumed && (subcommand === "status" || subcommand === "pull")) {
251
+ options.configurationId = token;
252
+ positionalConsumed = true;
253
+ break;
254
+ }
255
+ throw new Error(`Unknown security cloud option: ${token}`);
256
+ }
257
+ }
258
+ return options;
259
+ }
260
+
261
+ function cloudClientFor(runtime: SlashCommandRuntime, credentialId?: number): CodexSecurityCloudClient {
262
+ const authStorage = runtime.session.modelRegistry.authStorage;
263
+ const account = selectSecurityAccount(authStorage, "openai-codex", credentialId, runtime.session.sessionId);
264
+ return new CodexSecurityCloudClient({ authStorage, account });
265
+ }
266
+
267
+ async function handleCloudCommand(runtime: SlashCommandRuntime, rest: string): Promise<void> {
268
+ const { verb, rest: optionsText } = parseSubcommand(rest);
269
+ const subcommand = verb || "scans";
270
+ const options = parseCloudOptions(optionsText, subcommand);
271
+ const client = cloudClientFor(runtime, options.credentialId);
272
+ switch (subcommand) {
273
+ case "scans": {
274
+ const configurations = await client.listAllConfigurations();
275
+ await runtime.output(
276
+ configurations.length === 0
277
+ ? "No Codex Security cloud scan configurations are available for this account."
278
+ : configurations
279
+ .map(item =>
280
+ [
281
+ item.id,
282
+ item.state ?? "unknown",
283
+ item.currentStep ?? "unknown",
284
+ `repo=${item.repositoryId}`,
285
+ `environment=${item.environmentId}`,
286
+ item.repositoryUrl,
287
+ item.remainingScans === undefined ? "" : `${item.remainingScans} scan(s) remaining`,
288
+ ]
289
+ .filter(Boolean)
290
+ .join(" "),
291
+ )
292
+ .join("\n"),
293
+ );
294
+ return;
295
+ }
296
+ case "start": {
297
+ if (!options.repositoryId || !options.repositoryUrl || !options.environmentId) {
298
+ throw new Error("cloud start requires --repo-id, --repo-url, and --environment");
299
+ }
300
+ const configuration = await client.startScan({
301
+ repositoryId: options.repositoryId,
302
+ repositoryUrl: options.repositoryUrl,
303
+ environmentId: options.environmentId,
304
+ lookbackDays: options.lookbackDays,
305
+ });
306
+ await runtime.output(
307
+ `Codex Security cloud scan ${configuration.id} started for ${configuration.repositoryUrl}. This consumes cloud scan allowance.`,
308
+ );
309
+ return;
310
+ }
311
+ case "status": {
312
+ if (!options.configurationId) throw new Error("cloud status requires a configuration id");
313
+ await runtime.output(JSON.stringify(await client.getStats(options.configurationId), null, 2));
314
+ return;
315
+ }
316
+ case "pull": {
317
+ if (!options.configurationId) throw new Error("cloud pull requires a configuration id");
318
+ const store = await SecurityStore.openForCwd(runtime.cwd);
319
+ const bundle = await pullCodexSecurityCloudResults({
320
+ client,
321
+ configurationId: options.configurationId,
322
+ store,
323
+ });
324
+ await runtime.output(
325
+ `Imported ${bundle.findings.length} Codex Security cloud finding(s) as security scan ${bundle.scan.id}.`,
326
+ );
327
+ return;
328
+ }
329
+ default:
330
+ throw new Error("Usage: /security cloud <scans|start|status|pull>");
331
+ }
332
+ }
333
+
334
+ async function updateDisposition(runtime: SlashCommandRuntime, rest: string): Promise<void> {
335
+ const [scanId, findingId, status, ...rationaleParts] = parseCommandArgs(rest);
336
+ if (!scanId || !findingId || !status) {
337
+ throw new Error("disposition requires <scan-id> <finding-id> <status> [rationale]");
338
+ }
339
+ if (!DISPOSITIONS.has(status as SecurityDispositionStatus)) throw new Error(`Unknown disposition: ${status}`);
340
+ const rationale = rationaleParts.join(" ").trim();
341
+ if (status !== "open" && !rationale) throw new Error(`${status} requires a rationale`);
342
+ const store = await SecurityStore.openForCwd(runtime.cwd);
343
+ const finding = await store.updateDisposition(scanId, findingId, {
344
+ status: status as SecurityDispositionStatus,
345
+ rationale: rationale || undefined,
346
+ updatedAt: new Date().toISOString(),
347
+ actor: "operator",
348
+ });
349
+ await runtime.output(`Finding ${finding.id} disposition is now ${finding.disposition.status}.`);
350
+ }
351
+
352
+ export async function handleSecurityCommand(
353
+ command: ParsedSlashCommand,
354
+ runtime: SlashCommandRuntime,
355
+ ): Promise<SlashCommandResult> {
356
+ if (!runtime.settings.get("security.enabled")) {
357
+ return usage("Security is disabled. Enable security.enabled before using /security.", runtime);
358
+ }
359
+ const { verb, rest } = parseSubcommand(command.args);
360
+ try {
361
+ switch (verb || "scans") {
362
+ case "plan": {
363
+ const plan = await preflight(runtime, rest);
364
+ await runtime.output(`Security plan ${plan.id} is ready. Fingerprint: ${plan.fingerprint}.`);
365
+ return commandConsumed();
366
+ }
367
+ case "scan": {
368
+ const coordinator = coordinatorFor(runtime);
369
+ const planId = rest.trim().startsWith("secplan_") ? rest.trim() : (await preflight(runtime, rest)).id;
370
+ const operation = await coordinator.start({ planId });
371
+ await runtime.output(`Security scan ${operation.scanId} started as ${operation.operationId}.`);
372
+ return commandConsumed();
373
+ }
374
+ case "status": {
375
+ const coordinator = coordinatorFor(runtime);
376
+ const operationId = rest.trim();
377
+ if (operationId) {
378
+ const operation = await coordinator.status(operationId);
379
+ if (!operation) throw new Error(`Unknown security operation: ${operationId}`);
380
+ await runtime.output(JSON.stringify(operation, null, 2));
381
+ } else {
382
+ await runtime.output(JSON.stringify(await coordinator.listOperations(), null, 2));
383
+ }
384
+ return commandConsumed();
385
+ }
386
+ case "cancel": {
387
+ const operationId = rest.trim();
388
+ if (!operationId) throw new Error("cancel requires an operation id");
389
+ await runtime.output(
390
+ (await coordinatorFor(runtime).cancel(operationId))
391
+ ? `Cancellation requested for ${operationId}.`
392
+ : `No cancellable security operation ${operationId}.`,
393
+ );
394
+ return commandConsumed();
395
+ }
396
+ case "scans": {
397
+ const scans = await (await SecurityStore.openForCwd(runtime.cwd)).listScans();
398
+ await runtime.output(
399
+ scans.length === 0
400
+ ? "No security scans are stored for this project."
401
+ : scans
402
+ .map(scan => `${scan.id} ${scan.status} ${scan.findingCount} finding(s) ${scan.producer.name}`)
403
+ .join("\n"),
404
+ );
405
+ return commandConsumed();
406
+ }
407
+ case "show":
408
+ await showResource(runtime, rest);
409
+ return commandConsumed();
410
+ case "import":
411
+ await importResults(runtime, rest);
412
+ return commandConsumed();
413
+ case "export":
414
+ await exportResults(runtime, rest);
415
+ return commandConsumed();
416
+ case "validate": {
417
+ const target = findingTarget(rest);
418
+ return {
419
+ prompt: prompt
420
+ .render(validationRequestPrompt, {
421
+ findingUri: target.uri,
422
+ scanId: target.scanId,
423
+ findingId: target.findingId,
424
+ })
425
+ .trim(),
426
+ };
427
+ }
428
+ case "compare": {
429
+ const [beforeScanId, afterScanId] = parseCommandArgs(rest);
430
+ if (!beforeScanId || !afterScanId) throw new Error("compare requires <before-scan-id> <after-scan-id>");
431
+ const report = await (await SecurityStore.openForCwd(runtime.cwd)).compare(beforeScanId, afterScanId);
432
+ await runtime.output(JSON.stringify(report, null, 2));
433
+ return commandConsumed();
434
+ }
435
+ case "cloud":
436
+ await handleCloudCommand(runtime, rest);
437
+ return commandConsumed();
438
+ case "disposition":
439
+ await updateDisposition(runtime, rest);
440
+ return commandConsumed();
441
+ default:
442
+ return usage(
443
+ "Usage: /security <plan|scan|status|cancel|scans|cloud|show|import|export|validate|compare|disposition>",
444
+ runtime,
445
+ );
446
+ }
447
+ } catch (error) {
448
+ await runtime.output(`Security: ${errorMessage(error)}`);
449
+ return commandConsumed();
450
+ }
451
+ }
@@ -528,6 +528,8 @@ export interface BuildSystemPromptOptions {
528
528
  workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
529
529
  /** Whether the local memory://root summary is active. */
530
530
  memoryRootEnabled?: boolean;
531
+ /** Whether the read-only security:// resource namespace is active. */
532
+ securityEnabled?: boolean;
531
533
  /** Active model identifier (e.g. "anthropic/claude-opus-4") used by prompt policy and optionally surfaced. */
532
534
  model?: string;
533
535
  /** Whether to surface `model` in the workstation block. Model-specific prompt policy still uses it. Default: true. */
@@ -540,8 +542,8 @@ export interface BuildSystemPromptOptions {
540
542
  renderMermaid?: boolean;
541
543
  /** Pre-resolved nested active repo context. Undefined resolves from cwd. */
542
544
  activeRepoContext?: ActiveRepoContext | null;
543
- /** Tools mounted under `xd://`; renders the protocol section when non-empty. */
544
- xdevTools?: Array<{ name: string; summary: string }>;
545
+ /** Tools mounted under `xd://`; renders the protocol section when non-empty. `dynamic` marks external devices whose summary is third-party metadata. */
546
+ xdevTools?: Array<{ name: string; summary: string; dynamic?: boolean }>;
545
547
  /** Full docs + JSON schema for every `xd://`-mounted tool, inlined into the protocol section so no discovery `read` is needed. */
546
548
  xdevDocs?: string;
547
549
  /** Whether Auto-QA grievance reporting is enabled; renders the `xd://report_issue` note. */
@@ -585,6 +587,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
585
587
  secretsEnabled = false,
586
588
  workspaceTree: providedWorkspaceTree,
587
589
  memoryRootEnabled = false,
590
+ securityEnabled = false,
588
591
  model,
589
592
  includeModelInPrompt = true,
590
593
  personality = "default",
@@ -866,10 +869,12 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
866
869
  taskIrcEnabled,
867
870
  secretsEnabled,
868
871
  hasMemoryRoot: memoryRootEnabled,
872
+ securityEnabled,
869
873
  hasObsidian: hasObsidian(),
870
874
  includeWorkspaceTree,
871
875
  renderMermaid,
872
876
  xdevTools,
877
+ hasDynamicXdevTools: xdevTools.some(mounted => mounted.dynamic === true),
873
878
  xdevDocs,
874
879
  autoQaEnabled,
875
880
  };
@@ -12,6 +12,7 @@ import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { t
12
12
  import librarianMd from "../prompts/agents/librarian.md" with { type: "text" };
13
13
  import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
14
14
  import scoutMd from "../prompts/agents/scout.md" with { type: "text" };
15
+ import securityReviewerMd from "../prompts/agents/security-reviewer.md" with { type: "text" };
15
16
  import taskMd from "../prompts/agents/task.md" with { type: "text" };
16
17
  import { AUTO_THINKING } from "../thinking";
17
18
 
@@ -44,6 +45,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
44
45
  { fileName: "scout.md", template: scoutMd },
45
46
  { fileName: "designer.md", template: designerMd },
46
47
  { fileName: "reviewer.md", template: reviewerMd },
48
+ { fileName: "security-reviewer.md", template: securityReviewerMd },
47
49
  { fileName: "librarian.md", template: librarianMd },
48
50
  {
49
51
  fileName: "task.md",
@@ -87,8 +87,10 @@ const MCP_CALL_TIMEOUT_MS = 60_000;
87
87
  * agent is driven to one forced final `yield` so partial findings come back
88
88
  * as a real report; only if it still refuses to yield within
89
89
  * {@link BUDGET_STOP_GRACE_REQUESTS} more requests is the run hard-aborted.
90
- * The `default` key applies to agents without an explicit entry and can be
91
- * overridden via the `task.softRequestBudget` setting (0 disables the guard).
90
+ * Entries are ceilings, not fixed values: the `default` key applies to agents
91
+ * without an explicit entry, and the `task.softRequestBudget` setting can only
92
+ * lower an agent's budget, never raise it above its bundled entry (0 disables
93
+ * the guard entirely).
92
94
  */
93
95
  export const SOFT_REQUEST_BUDGET: Record<string, number> = {
94
96
  scout: 100,
@@ -96,6 +98,18 @@ export const SOFT_REQUEST_BUDGET: Record<string, number> = {
96
98
  default: 200,
97
99
  };
98
100
 
101
+ /**
102
+ * Resolves the effective soft request budget for an agent. The configured
103
+ * `task.softRequestBudget` and the agent's bundled entry are both upper
104
+ * bounds, so the tighter one wins; a configured budget of 0 disables the
105
+ * guard regardless of the bundled entry.
106
+ */
107
+ export function resolveSoftRequestBudget(agentName: string, configuredBudget: number): number {
108
+ const normalized = Math.max(0, Math.trunc(configuredBudget));
109
+ if (normalized === 0) return 0;
110
+ return Math.min(normalized, SOFT_REQUEST_BUDGET[agentName] ?? normalized);
111
+ }
112
+
99
113
  /** Extra requests allowed after a budget stop for the forced yield to land before the run is hard-aborted. */
100
114
  export const BUDGET_STOP_GRACE_REQUESTS = 5;
101
115
 
@@ -292,6 +306,8 @@ export interface ExecutorOptions {
292
306
  cwd: string;
293
307
  /** Additional workspace directories to seed on the subagent session (multi-root). */
294
308
  additionalDirectories?: string[];
309
+ /** Exact provider credential resolver inherited from the parent session. */
310
+ getApiKey?: CreateAgentSessionOptions["getApiKey"];
295
311
  worktree?: string;
296
312
  agent: AgentDefinition;
297
313
  task: string;
@@ -2446,8 +2462,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2446
2462
  0,
2447
2463
  Math.trunc(Number(settings.get("task.softRequestBudget") ?? SOFT_REQUEST_BUDGET.default) || 0),
2448
2464
  );
2449
- const softRequestBudget =
2450
- configuredDefaultBudget === 0 ? 0 : (SOFT_REQUEST_BUDGET[agent.name] ?? configuredDefaultBudget);
2465
+ const softRequestBudget = resolveSoftRequestBudget(agent.name, configuredDefaultBudget);
2451
2466
  const softRequestBudgetNotice = settings.get("task.softRequestBudgetNotice") ?? false;
2452
2467
  const parentDepth = options.taskDepth ?? 0;
2453
2468
  const childDepth = parentDepth + 1;
@@ -2770,6 +2785,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2770
2785
  additionalDirectories: worktree !== undefined ? undefined : options.additionalDirectories,
2771
2786
  authStorage,
2772
2787
  modelRegistry,
2788
+ getApiKey: options.getApiKey,
2773
2789
  settings: subagentSettings,
2774
2790
  model,
2775
2791
  modelPattern: model || modelOverride === undefined ? undefined : modelPatterns,
@@ -49,6 +49,13 @@ interface TaskRenderContext {
49
49
  * commit-eligible rows do not repaint after entering native scrollback.
50
50
  */
51
51
  frozen?: boolean;
52
+ /**
53
+ * Wall clock for time-derived rows (current-tool elapsed, retry countdown).
54
+ * The component freezes it once the block settles or any of its rows enter
55
+ * native scrollback, so identical-input rebuilds stay byte-identical with
56
+ * committed history. Absent: render with the live clock.
57
+ */
58
+ nowMs?: number;
52
59
  }
53
60
  type TaskRenderOptions = RenderResultOptions & { renderContext?: TaskRenderContext };
54
61
 
@@ -888,6 +895,7 @@ function renderAgentProgress(
888
895
  frozen = false,
889
896
  seenNestedTasks?: WeakSet<object>,
890
897
  nestedDepth = 0,
898
+ nowMs = Date.now(),
891
899
  ): string[] {
892
900
  const lines: string[] = [];
893
901
 
@@ -964,7 +972,7 @@ function renderAgentProgress(
964
972
  toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
965
973
  }
966
974
  if (progress.currentToolStartMs) {
967
- const elapsed = Date.now() - progress.currentToolStartMs;
975
+ const elapsed = nowMs - progress.currentToolStartMs;
968
976
  if (elapsed > 5000) {
969
977
  toolLine += `${theme.sep.dot}${theme.fg("warning", formatDuration(elapsed))}`;
970
978
  }
@@ -986,7 +994,7 @@ function renderAgentProgress(
986
994
  // long until the next attempt. Without this, the parent UI would just
987
995
  // keep spinning while a child sleeps on a 3-hour provider rate-limit.
988
996
  if (progress.retryState && progress.status === "running") {
989
- const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
997
+ const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - nowMs);
990
998
  const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
991
999
  const summary =
992
1000
  `retrying ${progress.retryState.attempt}/${progress.retryState.maxAttempts} ${waitLabel}: ` +
@@ -1079,6 +1087,7 @@ function renderAgentProgress(
1079
1087
  frozen,
1080
1088
  seenNestedTasks,
1081
1089
  nestedDepth,
1090
+ nowMs,
1082
1091
  );
1083
1092
  for (const line of nestedLines) {
1084
1093
  lines.push(`${continuePrefix}${line}`);
@@ -1560,6 +1569,7 @@ export function renderResult(
1560
1569
  return framedBlock(theme, width => {
1561
1570
  const { expanded, isPartial, spinnerFrame } = options;
1562
1571
  const frozen = options.renderContext?.frozen === true;
1572
+ const nowMs = options.renderContext?.nowMs ?? Date.now();
1563
1573
  const lines: string[] = [];
1564
1574
 
1565
1575
  // Result rows win once any exist; progress rows for spawns without a
@@ -1577,7 +1587,9 @@ export function renderResult(
1577
1587
  lines.push(formatHiddenProgressLine(ordered.slice(0, ordered.length - visible.length), theme));
1578
1588
  }
1579
1589
  for (const progress of visible) {
1580
- lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen));
1590
+ lines.push(
1591
+ ...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen, undefined, 0, nowMs),
1592
+ );
1581
1593
  }
1582
1594
  } else if (details.results && details.results.length > 0) {
1583
1595
  const ordered = orderResultsForDisplay(details.results);
@@ -1602,7 +1614,9 @@ export function renderResult(
1602
1614
  )
1603
1615
  : [];
1604
1616
  for (const progress of supplementalProgress) {
1605
- lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen));
1617
+ lines.push(
1618
+ ...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen, undefined, 0, nowMs),
1619
+ );
1606
1620
  }
1607
1621
 
1608
1622
  const summaryParts: string[] = [];
@@ -1742,6 +1756,7 @@ function renderNestedTaskTree(
1742
1756
  frozen = false,
1743
1757
  seen: WeakSet<object> = new WeakSet<object>(),
1744
1758
  depth = 0,
1759
+ nowMs = Date.now(),
1745
1760
  ): string[] {
1746
1761
  const lines: string[] = [];
1747
1762
  for (const details of detailsList) {
@@ -1788,6 +1803,7 @@ function renderNestedTaskTree(
1788
1803
  frozen,
1789
1804
  seen,
1790
1805
  depth + 1,
1806
+ nowMs,
1791
1807
  ),
1792
1808
  );
1793
1809
  });