@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,326 @@
1
+ import { type } from "arktype";
2
+ import type { ToolDefinition } from "../extensibility/extensions";
3
+ import securityPublishDescription from "../prompts/tools/security-publish.md" with { type: "text" };
4
+ import type {
5
+ SecurityCoverage,
6
+ SecurityEvidence,
7
+ SecurityFinding,
8
+ SecurityLocation,
9
+ SecurityScan,
10
+ SecurityScanBundle,
11
+ SecurityScanPlan,
12
+ } from "./contracts";
13
+ import {
14
+ createSecurityEvidenceId,
15
+ createSecurityFindingFingerprint,
16
+ createSecurityFindingId,
17
+ createSecurityOccurrenceId,
18
+ } from "./contracts";
19
+ import { pathMatchesSecurityScope } from "./preflight";
20
+ import { createNativeSecurityProducer, createNativeSecurityProvenance } from "./provenance";
21
+ import { exportSecurityBundleToSarif } from "./sarif";
22
+ import { type SecurityStore, writeSecurityBundleToDirectory } from "./store";
23
+
24
+ const publishLocationSchema = type({
25
+ path: type("string > 0").describe("repository-relative source path"),
26
+ start_line: type("number.integer >= 1").describe("1-indexed first source line"),
27
+ "end_line?": type("number.integer >= 1").describe("1-indexed last source line"),
28
+ "start_column?": type("number.integer >= 1").describe("1-indexed first source column"),
29
+ "end_column?": type("number.integer >= 1").describe("1-indexed last source column"),
30
+ "role?": type("string").describe("entrypoint, root_control, sink, or supporting role"),
31
+ });
32
+
33
+ const publishEvidenceSchema = type({
34
+ label: "string > 0",
35
+ explanation: "string",
36
+ "excerpt?": "string",
37
+ "location?": publishLocationSchema,
38
+ });
39
+
40
+ const publishFindingSchema = type({
41
+ rule_id: "string > 0",
42
+ title: "string > 0",
43
+ summary: "string",
44
+ severity: "'critical' | 'high' | 'medium' | 'low' | 'informational'",
45
+ confidence: "'high' | 'medium' | 'low'",
46
+ category: "string > 0",
47
+ "anchor?": "string",
48
+ "cwe?": "string[]",
49
+ locations: publishLocationSchema.array().atLeastLength(1),
50
+ "evidence?": publishEvidenceSchema.array(),
51
+ "remediation?": "string",
52
+ "validation?": "'unvalidated' | 'validated' | 'partial'",
53
+ });
54
+
55
+ const publishSurfaceSchema = type({
56
+ label: "string > 0",
57
+ disposition: "'reported' | 'no_issue_found' | 'rejected' | 'not_applicable' | 'needs_follow_up'",
58
+ "risk_area?": "string",
59
+ "notes?": "string",
60
+ "receipt_refs?": "string[]",
61
+ });
62
+
63
+ const publishDeferredSchema = type({
64
+ reason: "string > 0",
65
+ "paths?": "string[]",
66
+ "surface_ids?": "string[]",
67
+ });
68
+
69
+ export const securityPublishSchema = type({
70
+ findings: publishFindingSchema.array(),
71
+ coverage: {
72
+ completeness: "'complete' | 'partial' | 'unknown'",
73
+ "surfaces?": publishSurfaceSchema.array(),
74
+ "explicit_exclusions?": type({ pattern: "string", reason: "string" }).array(),
75
+ "deferred?": publishDeferredSchema.array(),
76
+ "open_questions?": type({ question: "string > 0", "follow_up_prompt?": "string" }).array(),
77
+ },
78
+ report: "string",
79
+ });
80
+
81
+ export type SecurityPublishParams = typeof securityPublishSchema.infer;
82
+
83
+ export interface SecurityPublishDetails {
84
+ scanId: string;
85
+ findingCount: number;
86
+ status: "completed";
87
+ }
88
+
89
+ export interface SecurityPublicationOptions {
90
+ plan: SecurityScanPlan;
91
+ scanId: string;
92
+ store: SecurityStore;
93
+ startedAt: string;
94
+ sessionId?: string;
95
+ operationId?: string;
96
+ onPublished?: (bundle: SecurityScanBundle) => void | Promise<void>;
97
+ }
98
+
99
+ function normalizePublishedPath(input: string): string {
100
+ const normalized = input.replaceAll("\\", "/").replace(/^\.\//, "");
101
+ const segments = normalized.split("/");
102
+ if (
103
+ !normalized ||
104
+ normalized.startsWith("/") ||
105
+ /^[a-zA-Z]:\//.test(normalized) ||
106
+ segments.some(segment => segment === "..")
107
+ ) {
108
+ throw new Error(`Security finding paths must be repository-relative: ${input}`);
109
+ }
110
+ return normalized;
111
+ }
112
+
113
+ function toLocation(
114
+ input: SecurityPublishParams["findings"][number]["locations"][number],
115
+ plan: SecurityScanPlan,
116
+ ): SecurityLocation {
117
+ const normalizedPath = normalizePublishedPath(input.path);
118
+ if (!pathMatchesSecurityScope(normalizedPath, plan.target.includePaths, plan.target.excludePaths)) {
119
+ throw new Error(`Security finding path is outside the immutable scan scope: ${input.path}`);
120
+ }
121
+ const location: SecurityLocation = {
122
+ path: normalizedPath,
123
+ startLine: input.start_line,
124
+ };
125
+ if (input.end_line !== undefined) location.endLine = input.end_line;
126
+ if (input.start_column !== undefined) location.startColumn = input.start_column;
127
+ if (input.end_column !== undefined) location.endColumn = input.end_column;
128
+ if (input.role !== undefined) location.role = input.role;
129
+ return location;
130
+ }
131
+
132
+ function coverageMode(plan: SecurityScanPlan): SecurityCoverage["mode"] {
133
+ switch (plan.target.kind) {
134
+ case "ref_diff":
135
+ return "diff";
136
+ case "working_tree":
137
+ return "working_tree";
138
+ case "scoped_path":
139
+ return "scoped_path";
140
+ default:
141
+ return "repository";
142
+ }
143
+ }
144
+
145
+ function inventoryStrategy(plan: SecurityScanPlan): SecurityCoverage["inventoryStrategy"] {
146
+ switch (plan.target.kind) {
147
+ case "ref_diff":
148
+ return "diff";
149
+ case "scoped_path":
150
+ return "scoped_path";
151
+ default:
152
+ return "repository";
153
+ }
154
+ }
155
+
156
+ function buildFinding(
157
+ input: SecurityPublishParams["findings"][number],
158
+ options: SecurityPublicationOptions,
159
+ createdAt: string,
160
+ ): SecurityFinding {
161
+ const locations = input.locations.map(location => toLocation(location, options.plan));
162
+ const fingerprint = createSecurityFindingFingerprint({
163
+ ruleId: input.rule_id,
164
+ category: input.category,
165
+ anchor: input.anchor,
166
+ locations,
167
+ });
168
+ const evidence: SecurityEvidence[] = (input.evidence ?? []).map((item, index) => {
169
+ const entry: SecurityEvidence = {
170
+ id: createSecurityEvidenceId(fingerprint, item.label, index),
171
+ kind: "code",
172
+ label: item.label,
173
+ explanation: item.explanation,
174
+ };
175
+ if (item.excerpt !== undefined) entry.excerpt = item.excerpt;
176
+ if (item.location !== undefined) entry.location = toLocation(item.location, options.plan);
177
+ return entry;
178
+ });
179
+ const finding: SecurityFinding = {
180
+ id: createSecurityFindingId(fingerprint),
181
+ scanId: options.scanId,
182
+ fingerprint,
183
+ ruleId: input.rule_id,
184
+ title: input.title,
185
+ summary: input.summary,
186
+ severity: { level: input.severity },
187
+ confidence: { level: input.confidence },
188
+ taxonomy: { category: input.category, cwe: input.cwe ?? [] },
189
+ occurrences: [
190
+ {
191
+ id: createSecurityOccurrenceId(fingerprint, locations),
192
+ locations,
193
+ evidenceIds: evidence.map(item => item.id),
194
+ },
195
+ ],
196
+ evidence,
197
+ validation: { status: input.validation ?? "unvalidated", evidenceIds: [] },
198
+ disposition: { status: "open" },
199
+ provenance: createNativeSecurityProvenance({
200
+ createdAt,
201
+ account: options.plan.account,
202
+ planFingerprint: options.plan.fingerprint,
203
+ workflowFingerprint: options.plan.workflowFingerprint,
204
+ sessionId: options.sessionId,
205
+ }),
206
+ };
207
+ if (input.anchor !== undefined) finding.anchor = input.anchor;
208
+ if (input.remediation !== undefined) finding.remediation = input.remediation;
209
+ return finding;
210
+ }
211
+
212
+ function buildCoverage(params: SecurityPublishParams, plan: SecurityScanPlan): SecurityCoverage {
213
+ const surfaces: SecurityCoverage["surfaces"] = (params.coverage.surfaces ?? []).map((surface, index) => {
214
+ const entry: SecurityCoverage["surfaces"][number] = {
215
+ id: `surface-${index + 1}`,
216
+ label: surface.label,
217
+ disposition: surface.disposition,
218
+ receiptRefs: surface.receipt_refs ?? [],
219
+ };
220
+ if (surface.risk_area !== undefined) entry.riskArea = surface.risk_area;
221
+ if (surface.notes !== undefined) entry.notes = surface.notes;
222
+ return entry;
223
+ });
224
+ const deferred: SecurityCoverage["deferred"] = (params.coverage.deferred ?? []).map((item, index) => {
225
+ const entry: SecurityCoverage["deferred"][number] = {
226
+ id: `deferred-${index + 1}`,
227
+ reason: item.reason,
228
+ };
229
+ if (item.paths !== undefined) entry.paths = item.paths;
230
+ if (item.surface_ids !== undefined) entry.surfaceIds = item.surface_ids;
231
+ return entry;
232
+ });
233
+ const coverage: SecurityCoverage = {
234
+ mode: coverageMode(plan),
235
+ completeness: params.coverage.completeness,
236
+ inventoryStrategy: inventoryStrategy(plan),
237
+ includePaths: [...plan.target.includePaths],
238
+ excludePaths: [...plan.target.excludePaths],
239
+ surfaces,
240
+ explicitExclusions: params.coverage.explicit_exclusions ?? [],
241
+ deferred,
242
+ };
243
+ if (params.coverage.open_questions !== undefined) {
244
+ coverage.openQuestions = params.coverage.open_questions.map(item => {
245
+ const question: NonNullable<SecurityCoverage["openQuestions"]>[number] = { question: item.question };
246
+ if (item.follow_up_prompt !== undefined) question.followUpPrompt = item.follow_up_prompt;
247
+ return question;
248
+ });
249
+ }
250
+ return coverage;
251
+ }
252
+
253
+ export function createSecurityPublicationTool(
254
+ options: SecurityPublicationOptions,
255
+ ): ToolDefinition<typeof securityPublishSchema, SecurityPublishDetails> {
256
+ let published = false;
257
+ return {
258
+ name: "security_publish",
259
+ label: "Publish Security Scan",
260
+ description: securityPublishDescription.trim(),
261
+ parameters: securityPublishSchema,
262
+ approval: "write",
263
+ strict: true,
264
+ async execute(_toolCallId, params) {
265
+ if (published) throw new Error(`Security scan ${options.scanId} has already been published`);
266
+ published = true;
267
+ let persisted = false;
268
+ try {
269
+ const completedAt = new Date().toISOString();
270
+ const findingsByFingerprint = new Map<string, SecurityFinding>();
271
+ for (const input of params.findings) {
272
+ const finding = buildFinding(input, options, completedAt);
273
+ if (!findingsByFingerprint.has(finding.fingerprint)) {
274
+ findingsByFingerprint.set(finding.fingerprint, finding);
275
+ }
276
+ }
277
+ const findings = [...findingsByFingerprint.values()];
278
+ const producer = createNativeSecurityProducer();
279
+ const provenance = createNativeSecurityProvenance({
280
+ createdAt: options.startedAt,
281
+ account: options.plan.account,
282
+ planFingerprint: options.plan.fingerprint,
283
+ workflowFingerprint: options.plan.workflowFingerprint,
284
+ sessionId: options.sessionId,
285
+ operationId: options.operationId,
286
+ });
287
+ const scan: SecurityScan = {
288
+ documentType: "omp-security.scan",
289
+ schemaVersion: "1.0",
290
+ id: options.scanId,
291
+ projectKey: options.store.projectKey,
292
+ status: "completed",
293
+ createdAt: options.plan.createdAt,
294
+ startedAt: options.startedAt,
295
+ completedAt,
296
+ plan: options.plan,
297
+ target: options.plan.target,
298
+ producer,
299
+ provenance,
300
+ findingIds: findings.map(finding => finding.id),
301
+ coverage: buildCoverage(params, options.plan),
302
+ reportRef: "report.md",
303
+ sarifRef: "results.sarif",
304
+ };
305
+ const provisional: SecurityScanBundle = { scan, findings, report: params.report };
306
+ const bundle: SecurityScanBundle = { ...provisional, sarif: exportSecurityBundleToSarif(provisional) };
307
+ await writeSecurityBundleToDirectory(options.plan.output.root, bundle);
308
+ await options.store.putBundle(bundle);
309
+ persisted = true;
310
+ await options.onPublished?.(bundle);
311
+ return {
312
+ content: [
313
+ {
314
+ type: "text",
315
+ text: `Published security scan ${options.scanId} with ${findings.length} finding(s).`,
316
+ },
317
+ ],
318
+ details: { scanId: options.scanId, findingCount: findings.length, status: "completed" },
319
+ };
320
+ } catch (error) {
321
+ if (!persisted) published = false;
322
+ throw error;
323
+ }
324
+ },
325
+ };
326
+ }
@@ -0,0 +1,93 @@
1
+ import type { IsoBackendKind } from "@oh-my-pi/pi-natives";
2
+ import type { IsolationContext } from "../task/isolation-runner";
3
+ import { prepareIsolationContext } from "../task/isolation-runner";
4
+ import type { IsolationHandle, WorktreeBaseline } from "../task/worktree";
5
+ import { cleanupIsolation, ensureIsolation } from "../task/worktree";
6
+
7
+ export interface SecurityRemediationRequest {
8
+ cwd: string;
9
+ findingIds: string[];
10
+ isolationId?: string;
11
+ preferredBackend?: IsoBackendKind;
12
+ }
13
+
14
+ export interface SecurityRemediationWorkspace {
15
+ id: string;
16
+ repositoryRoot: string;
17
+ worktreePath: string;
18
+ findingIds: string[];
19
+ backend: IsoBackendKind;
20
+ fellBack: boolean;
21
+ fallbackReason: string | null;
22
+ cleanup(): Promise<void>;
23
+ }
24
+
25
+ export interface SecurityRemediationDependencies {
26
+ prepareContext?: (cwd: string) => Promise<IsolationContext>;
27
+ createIsolation?: (repositoryRoot: string, id: string, preferred?: IsoBackendKind) => Promise<IsolationHandle>;
28
+ cleanupIsolation?: (handle: IsolationHandle) => Promise<void>;
29
+ createId?: () => string;
30
+ }
31
+
32
+ function createRemediationId(): string {
33
+ return `security-remediation-${Bun.randomUUIDv7().replaceAll("-", "")}`;
34
+ }
35
+
36
+ function repoBaselineDirty(baseline: WorktreeBaseline): string[] {
37
+ const dirty: string[] = [];
38
+ if (baseline.root.staged.trim()) dirty.push("staged changes");
39
+ if (baseline.root.unstaged.trim()) dirty.push("unstaged changes");
40
+ if (baseline.root.untracked.length > 0 || baseline.root.untrackedPatch.trim()) dirty.push("untracked files");
41
+ for (const nested of baseline.nested) {
42
+ if (
43
+ nested.baseline.staged.trim() ||
44
+ nested.baseline.unstaged.trim() ||
45
+ nested.baseline.untracked.length > 0 ||
46
+ nested.baseline.untrackedPatch.trim()
47
+ ) {
48
+ dirty.push(`dirty nested repository ${nested.relativePath}`);
49
+ }
50
+ }
51
+ return dirty;
52
+ }
53
+
54
+ export function assertSecurityRemediationBaselineClean(baseline: WorktreeBaseline): void {
55
+ const dirty = repoBaselineDirty(baseline);
56
+ if (dirty.length === 0) return;
57
+ throw new Error(
58
+ [
59
+ `Security remediation refuses a dirty working tree (${dirty.join(", ")}).`,
60
+ "Commit or stash the changes before creating an isolated remediation workspace.",
61
+ ].join(" "),
62
+ );
63
+ }
64
+
65
+ export async function prepareSecurityRemediationWorkspace(
66
+ request: SecurityRemediationRequest,
67
+ dependencies: SecurityRemediationDependencies = {},
68
+ ): Promise<SecurityRemediationWorkspace> {
69
+ const findingIds = [...new Set(request.findingIds.map(id => id.trim()).filter(Boolean))];
70
+ if (findingIds.length === 0) throw new Error("Security remediation requires at least one finding id");
71
+ const prepareContext = dependencies.prepareContext ?? prepareIsolationContext;
72
+ const createIsolation = dependencies.createIsolation ?? ensureIsolation;
73
+ const disposeIsolation = dependencies.cleanupIsolation ?? cleanupIsolation;
74
+ const context = await prepareContext(request.cwd);
75
+ assertSecurityRemediationBaselineClean(context.baseline);
76
+ const id = request.isolationId?.trim() || dependencies.createId?.() || createRemediationId();
77
+ const handle = await createIsolation(context.repoRoot, id, request.preferredBackend);
78
+ let cleaned = false;
79
+ return {
80
+ id,
81
+ repositoryRoot: context.repoRoot,
82
+ worktreePath: handle.mergedDir,
83
+ findingIds,
84
+ backend: handle.backend,
85
+ fellBack: handle.fellBack,
86
+ fallbackReason: handle.fallbackReason,
87
+ async cleanup() {
88
+ if (cleaned) return;
89
+ cleaned = true;
90
+ await disposeIsolation(handle);
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,50 @@
1
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
2
+ import type { InternalResource } from "../internal-urls";
3
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead } from "../session/streaming-output";
4
+
5
+ export interface SecurityResourceOptions {
6
+ url: string;
7
+ content: string;
8
+ contentType: InternalResource["contentType"];
9
+ isDirectory?: boolean;
10
+ }
11
+
12
+ function boundedJson(content: string): { content: string; truncated: boolean } {
13
+ const sanitized = sanitizeText(content);
14
+ const truncated = truncateHead(sanitized, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
15
+ if (!truncated.truncated) return { content: sanitized, truncated: false };
16
+ return {
17
+ content: `${JSON.stringify(
18
+ {
19
+ truncated: true,
20
+ originalBytes: truncated.totalBytes,
21
+ originalLines: truncated.totalLines,
22
+ preview: truncated.content,
23
+ },
24
+ null,
25
+ 2,
26
+ )}\n`,
27
+ truncated: true,
28
+ };
29
+ }
30
+
31
+ export function createSecurityResource(options: SecurityResourceOptions): InternalResource {
32
+ const bounded =
33
+ options.contentType === "application/json"
34
+ ? boundedJson(options.content)
35
+ : (() => {
36
+ const sanitized = sanitizeText(options.content);
37
+ const truncated = truncateHead(sanitized, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
38
+ return { content: truncated.content, truncated: truncated.truncated };
39
+ })();
40
+ return {
41
+ url: options.url,
42
+ content: bounded.content,
43
+ contentType: options.contentType,
44
+ size: Buffer.byteLength(bounded.content),
45
+ isDirectory: options.isDirectory,
46
+ notes: bounded.truncated
47
+ ? [`Security resource truncated to ${DEFAULT_MAX_LINES} lines / ${DEFAULT_MAX_BYTES} bytes.`]
48
+ : undefined,
49
+ };
50
+ }
@@ -0,0 +1,78 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import type { SecurityFinding, SecurityScanBundle } from "./contracts";
3
+
4
+ function sarifLevel(finding: SecurityFinding): "error" | "warning" | "note" | "none" {
5
+ switch (finding.severity.level) {
6
+ case "critical":
7
+ case "high":
8
+ return "error";
9
+ case "medium":
10
+ return "warning";
11
+ case "low":
12
+ return "note";
13
+ default:
14
+ return "none";
15
+ }
16
+ }
17
+
18
+ export function exportSecurityBundleToSarif(bundle: SecurityScanBundle): Record<string, unknown> {
19
+ const rules = new Map<string, SecurityFinding>();
20
+ for (const finding of bundle.findings) {
21
+ if (!rules.has(finding.ruleId)) rules.set(finding.ruleId, finding);
22
+ }
23
+ return {
24
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
25
+ version: "2.1.0",
26
+ runs: [
27
+ {
28
+ tool: {
29
+ driver: {
30
+ name: bundle.scan.producer.name,
31
+ version: bundle.scan.producer.version,
32
+ informationUri: "https://omp.sh",
33
+ rules: [...rules.values()].map(finding => ({
34
+ id: finding.ruleId,
35
+ name: finding.ruleId,
36
+ shortDescription: { text: finding.title },
37
+ fullDescription: { text: finding.summary },
38
+ properties: {
39
+ tags: [...finding.taxonomy.cwe, ...(finding.taxonomy.tags ?? [])],
40
+ "security-severity": finding.severity.score,
41
+ },
42
+ })),
43
+ },
44
+ },
45
+ results: bundle.findings.map(finding => ({
46
+ ruleId: finding.ruleId,
47
+ level: sarifLevel(finding),
48
+ message: { text: finding.summary },
49
+ locations: finding.occurrences.flatMap(occurrence =>
50
+ occurrence.locations.map(location => ({
51
+ physicalLocation: {
52
+ artifactLocation: { uri: location.path, uriBaseId: "%SRCROOT%" },
53
+ region: {
54
+ startLine: location.startLine,
55
+ endLine: location.endLine,
56
+ startColumn: location.startColumn,
57
+ endColumn: location.endColumn,
58
+ },
59
+ },
60
+ })),
61
+ ),
62
+ fingerprints: { "omp-security/v1": finding.fingerprint },
63
+ properties: {
64
+ findingId: finding.id,
65
+ confidence: finding.confidence.level,
66
+ validation: finding.validation.status,
67
+ disposition: finding.disposition.status,
68
+ category: finding.taxonomy.category,
69
+ "security-severity": finding.severity.score,
70
+ },
71
+ })),
72
+ originalUriBaseIds: {
73
+ "%SRCROOT%": { uri: pathToFileURL(bundle.scan.target.repositoryRoot).href.replace(/\/?$/, "/") },
74
+ },
75
+ },
76
+ ],
77
+ };
78
+ }