@oh-my-pi/pi-coding-agent 17.2.0 → 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 (87) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/{CHANGELOG-be1f2t8h.md → CHANGELOG-dj46zzrm.md} +18 -0
  3. package/dist/cli.js +3340 -3195
  4. package/dist/types/config/model-discovery.d.ts +12 -0
  5. package/dist/types/config/settings-schema.d.ts +10 -0
  6. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +23 -0
  7. package/dist/types/internal-urls/index.d.ts +1 -0
  8. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  9. package/dist/types/internal-urls/types.d.ts +1 -1
  10. package/dist/types/sdk.d.ts +17 -0
  11. package/dist/types/security/auth.d.ts +30 -0
  12. package/dist/types/security/cloud.d.ts +79 -0
  13. package/dist/types/security/comparison.d.ts +49 -0
  14. package/dist/types/security/contracts/ids.d.ts +15 -0
  15. package/dist/types/security/contracts/index.d.ts +4 -0
  16. package/dist/types/security/contracts/schemas.d.ts +660 -0
  17. package/dist/types/security/contracts/types.d.ts +234 -0
  18. package/dist/types/security/contracts/validation.d.ts +5 -0
  19. package/dist/types/security/coordinator.d.ts +100 -0
  20. package/dist/types/security/importers/codex-security.d.ts +7 -0
  21. package/dist/types/security/importers/index.d.ts +2 -0
  22. package/dist/types/security/importers/sarif.d.ts +9 -0
  23. package/dist/types/security/index.d.ts +13 -0
  24. package/dist/types/security/preflight.d.ts +61 -0
  25. package/dist/types/security/provenance.d.ts +24 -0
  26. package/dist/types/security/publication.d.ts +78 -0
  27. package/dist/types/security/remediation.d.ts +27 -0
  28. package/dist/types/security/resource-output.d.ts +8 -0
  29. package/dist/types/security/sarif.d.ts +2 -0
  30. package/dist/types/security/store.d.ts +40 -0
  31. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  32. package/dist/types/system-prompt.d.ts +2 -0
  33. package/dist/types/task/executor.d.ts +3 -0
  34. package/dist/types/tools/builtin-names.d.ts +1 -1
  35. package/dist/types/tools/index.d.ts +6 -1
  36. package/dist/types/tools/security-scan.d.ts +96 -0
  37. package/package.json +12 -12
  38. package/scripts/security-compare.ts +40 -0
  39. package/src/config/model-discovery.ts +32 -5
  40. package/src/config/model-registry.ts +4 -0
  41. package/src/config/settings-schema.ts +12 -0
  42. package/src/eval/py/prelude.py +7 -3
  43. package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -0
  44. package/src/internal-urls/index.ts +1 -0
  45. package/src/internal-urls/router.ts +4 -1
  46. package/src/internal-urls/security-protocol.ts +261 -0
  47. package/src/internal-urls/types.ts +1 -1
  48. package/src/lsp/index.ts +3 -0
  49. package/src/modes/rpc/host-uris.ts +6 -0
  50. package/src/prompts/agents/security-reviewer.md +75 -0
  51. package/src/prompts/security/scan-coordinator.md +7 -0
  52. package/src/prompts/security/scan-request.md +21 -0
  53. package/src/prompts/security/validate-request.md +8 -0
  54. package/src/prompts/system/system-prompt.md +3 -0
  55. package/src/prompts/tools/security-publish.md +1 -0
  56. package/src/prompts/tools/security-scan.md +1 -0
  57. package/src/sdk.ts +34 -6
  58. package/src/security/auth.ts +98 -0
  59. package/src/security/cloud.ts +686 -0
  60. package/src/security/comparison.ts +255 -0
  61. package/src/security/contracts/ids.ts +111 -0
  62. package/src/security/contracts/index.ts +4 -0
  63. package/src/security/contracts/schemas.ts +201 -0
  64. package/src/security/contracts/types.ts +254 -0
  65. package/src/security/contracts/validation.ts +65 -0
  66. package/src/security/coordinator.ts +708 -0
  67. package/src/security/importers/codex-security.ts +387 -0
  68. package/src/security/importers/index.ts +2 -0
  69. package/src/security/importers/sarif.ts +357 -0
  70. package/src/security/index.ts +13 -0
  71. package/src/security/preflight.ts +405 -0
  72. package/src/security/provenance.ts +106 -0
  73. package/src/security/publication.ts +326 -0
  74. package/src/security/remediation.ts +93 -0
  75. package/src/security/resource-output.ts +50 -0
  76. package/src/security/sarif.ts +78 -0
  77. package/src/security/store.ts +430 -0
  78. package/src/slash-commands/builtin-registry.ts +21 -0
  79. package/src/slash-commands/helpers/security.ts +451 -0
  80. package/src/system-prompt.ts +4 -0
  81. package/src/task/agents.ts +2 -0
  82. package/src/task/executor.ts +3 -0
  83. package/src/task/structured-subagent.ts +6 -4
  84. package/src/tools/builtin-names.ts +1 -0
  85. package/src/tools/index.ts +9 -1
  86. package/src/tools/path-utils.ts +3 -0
  87. package/src/tools/security-scan.ts +287 -0
@@ -0,0 +1,287 @@
1
+ import type { AgentTool, AgentToolResult, ToolTier } from "@oh-my-pi/pi-agent-core";
2
+ import { type } from "arktype";
3
+ import securityScanDescription from "../prompts/tools/security-scan.md" with { type: "text" };
4
+ import { selectSecurityAccount } from "../security/auth";
5
+ import {
6
+ CodexSecurityCloudClient,
7
+ type CodexSecurityCloudConfiguration,
8
+ type CodexSecurityCloudStats,
9
+ pullCodexSecurityCloudResults,
10
+ } from "../security/cloud";
11
+ import { createSecurityEvidenceId, type SecurityEvidence, type SecurityValidationStatus } from "../security/contracts";
12
+ import type { SecurityOperationSnapshot } from "../security/coordinator";
13
+ import { getSecurityCoordinator } from "../security/coordinator";
14
+ import type { SecurityTargetRequest } from "../security/preflight";
15
+ import { SecurityStore } from "../security/store";
16
+ import type { ToolSession } from "./index";
17
+ import { ToolError } from "./tool-errors";
18
+
19
+ const securityScanSchema = type({
20
+ action:
21
+ "'preflight' | 'start' | 'status' | 'cancel' | 'validate' | 'cloud_scans' | 'cloud_start' | 'cloud_status' | 'cloud_pull'",
22
+ "plan_id?": "string",
23
+ "operation_id?": "string",
24
+ "target_kind?": "'repository' | 'scoped_path' | 'ref_diff' | 'working_tree'",
25
+ "include_paths?": "string[]",
26
+ "exclude_paths?": "string[]",
27
+ "base_revision?": "string",
28
+ "head_revision?": "string",
29
+ "knowledge_base_paths?": "string[]",
30
+ "output_root?": "string",
31
+ "archive_existing?": "boolean",
32
+ "credential_id?": "number.integer >= 1",
33
+ "scan_id?": "string",
34
+ "finding_id?": "string",
35
+ "validation_status?": "'unvalidated' | 'validated' | 'rejected' | 'partial' | 'error'",
36
+ "validation_summary?": "string",
37
+ "validation_evidence?": type({ label: "string > 0", explanation: "string" }).array(),
38
+ "cloud_configuration_id?": "string",
39
+ "repository_id?": "string",
40
+ "repository_url?": "string",
41
+ "environment_id?": "string",
42
+ "lookback_days?": "number.integer >= 1 | 'all'",
43
+ });
44
+
45
+ type SecurityScanParams = typeof securityScanSchema.infer;
46
+
47
+ export interface SecurityScanToolDetails {
48
+ action: SecurityScanParams["action"];
49
+ plan?: { id: string; fingerprint: string };
50
+ operation?: SecurityOperationSnapshot;
51
+ cancelled?: boolean;
52
+ finding?: { id: string; validationStatus: SecurityValidationStatus };
53
+ cloudConfigurations?: CodexSecurityCloudConfiguration[];
54
+ cloudStats?: CodexSecurityCloudStats;
55
+ cloudScan?: { id: string; repositoryUrl: string };
56
+ importedScan?: { id: string; findingCount: number };
57
+ }
58
+
59
+ function targetFromParams(params: SecurityScanParams): SecurityTargetRequest {
60
+ const common = { includePaths: params.include_paths, excludePaths: params.exclude_paths };
61
+ switch (params.target_kind ?? "repository") {
62
+ case "scoped_path": {
63
+ if (!params.include_paths?.some(value => value.trim().length > 0)) {
64
+ throw new ToolError("scoped_path security scans require at least one include path");
65
+ }
66
+ return { kind: "scoped_path", includePaths: params.include_paths, excludePaths: params.exclude_paths };
67
+ }
68
+ case "working_tree":
69
+ return { kind: "working_tree", ...common };
70
+ case "ref_diff":
71
+ if (!params.base_revision || !params.head_revision) {
72
+ throw new ToolError("ref_diff preflight requires base_revision and head_revision");
73
+ }
74
+ return {
75
+ kind: "ref_diff",
76
+ baseRevision: params.base_revision,
77
+ headRevision: params.head_revision,
78
+ ...common,
79
+ };
80
+ default:
81
+ return { kind: "repository", ...common };
82
+ }
83
+ }
84
+
85
+ function requireValue(value: string | undefined, label: string): string {
86
+ if (!value?.trim()) throw new ToolError(`${label} is required for this action`);
87
+ return value.trim();
88
+ }
89
+
90
+ function cloudClientForSession(session: ToolSession, credentialId?: number): CodexSecurityCloudClient {
91
+ if (!session.authStorage) throw new ToolError("Codex Security cloud requires the authentication registry");
92
+ const account = selectSecurityAccount(
93
+ session.authStorage,
94
+ "openai-codex",
95
+ credentialId,
96
+ session.getSessionId?.() ?? undefined,
97
+ );
98
+ return new CodexSecurityCloudClient({ authStorage: session.authStorage, account });
99
+ }
100
+
101
+ function textResult(text: string, details: SecurityScanToolDetails): AgentToolResult<SecurityScanToolDetails> {
102
+ return { content: [{ type: "text", text }], details };
103
+ }
104
+
105
+ export class SecurityScanTool implements AgentTool<typeof securityScanSchema, SecurityScanToolDetails> {
106
+ readonly name = "security_scan";
107
+ readonly approval: ToolTier = "exec";
108
+ readonly label = "Security Scan";
109
+ readonly loadMode = "discoverable";
110
+ readonly summary = "Run OMP-native scans and explicit Codex Security cloud operations";
111
+ readonly description = securityScanDescription.trim();
112
+ readonly parameters = securityScanSchema;
113
+ readonly strict = true;
114
+
115
+ constructor(readonly session: ToolSession) {}
116
+
117
+ async execute(
118
+ _toolCallId: string,
119
+ params: SecurityScanParams,
120
+ signal?: AbortSignal,
121
+ ): Promise<AgentToolResult<SecurityScanToolDetails>> {
122
+ if (!this.session.settings.get("security.enabled")) {
123
+ throw new ToolError("Security is disabled. Enable security.enabled before using security_scan.");
124
+ }
125
+ const coordinatorForSession = () => {
126
+ if (!this.session.modelRegistry || !this.session.authStorage) {
127
+ throw new ToolError("Security scan requires the session model and authentication registries");
128
+ }
129
+ return getSecurityCoordinator({
130
+ cwd: this.session.cwd,
131
+ settings: this.session.settings,
132
+ authStorage: this.session.authStorage,
133
+ modelRegistry: this.session.modelRegistry,
134
+ activeModel: this.session.getActiveModel?.(),
135
+ sessionId: this.session.getSessionId?.() ?? undefined,
136
+ agentId: this.session.getAgentId?.() ?? undefined,
137
+ asyncJobManager: this.session.asyncJobManager,
138
+ });
139
+ };
140
+ switch (params.action) {
141
+ case "preflight": {
142
+ const model = this.session.getActiveModel?.();
143
+ const plan = await coordinatorForSession().preflight({
144
+ target: targetFromParams(params),
145
+ knowledgeBasePaths: params.knowledge_base_paths,
146
+ outputRoot: params.output_root,
147
+ archiveExisting: params.archive_existing,
148
+ credentialId: params.credential_id,
149
+ model,
150
+ signal,
151
+ });
152
+ return textResult(
153
+ [
154
+ `Security plan ${plan.id} is ready.`,
155
+ `Fingerprint: ${plan.fingerprint}.`,
156
+ `Start it with action=start and plan_id=${plan.id}.`,
157
+ ].join(" "),
158
+ { action: params.action, plan: { id: plan.id, fingerprint: plan.fingerprint } },
159
+ );
160
+ }
161
+ case "start": {
162
+ const operation = await coordinatorForSession().start({
163
+ planId: requireValue(params.plan_id, "plan_id"),
164
+ });
165
+ return textResult(`Security scan ${operation.scanId} started as ${operation.operationId}.`, {
166
+ action: params.action,
167
+ operation,
168
+ });
169
+ }
170
+ case "status": {
171
+ const operationId = requireValue(params.operation_id, "operation_id");
172
+ const operation = await coordinatorForSession().status(operationId);
173
+ if (!operation) throw new ToolError(`Unknown security operation: ${operationId}`);
174
+ return textResult(
175
+ `Security scan ${operation.scanId}: ${operation.phase}; ${operation.findingCount} finding(s).`,
176
+ { action: params.action, operation },
177
+ );
178
+ }
179
+ case "cancel": {
180
+ const operationId = requireValue(params.operation_id, "operation_id");
181
+ const cancelled = await coordinatorForSession().cancel(operationId);
182
+ return textResult(
183
+ cancelled ? `Cancellation requested for ${operationId}.` : `No running operation ${operationId}.`,
184
+ {
185
+ action: params.action,
186
+ cancelled,
187
+ operation: (await coordinatorForSession().status(operationId)) ?? undefined,
188
+ },
189
+ );
190
+ }
191
+ case "cloud_scans": {
192
+ const configurations = await cloudClientForSession(
193
+ this.session,
194
+ params.credential_id,
195
+ ).listAllConfigurations(signal);
196
+ return textResult(
197
+ configurations.length === 0
198
+ ? "No Codex Security cloud scan configurations are available."
199
+ : configurations
200
+ .map(
201
+ item =>
202
+ `${item.id} ${item.currentStep ?? "unknown"} repo=${item.repositoryId} environment=${item.environmentId} ${item.repositoryUrl}`,
203
+ )
204
+ .join("\n"),
205
+ { action: params.action, cloudConfigurations: configurations },
206
+ );
207
+ }
208
+ case "cloud_start": {
209
+ const configuration = await cloudClientForSession(this.session, params.credential_id).startScan({
210
+ repositoryId: requireValue(params.repository_id, "repository_id"),
211
+ repositoryUrl: requireValue(params.repository_url, "repository_url"),
212
+ environmentId: requireValue(params.environment_id, "environment_id"),
213
+ lookbackDays: params.lookback_days,
214
+ signal,
215
+ });
216
+ return textResult(
217
+ `Codex Security cloud scan ${configuration.id} started for ${configuration.repositoryUrl}. This consumes cloud scan allowance.`,
218
+ {
219
+ action: params.action,
220
+ cloudScan: { id: configuration.id, repositoryUrl: configuration.repositoryUrl },
221
+ },
222
+ );
223
+ }
224
+ case "cloud_status": {
225
+ const stats = await cloudClientForSession(this.session, params.credential_id).getStats(
226
+ requireValue(params.cloud_configuration_id, "cloud_configuration_id"),
227
+ signal,
228
+ );
229
+ return textResult(
230
+ `Codex Security cloud scan ${stats.configurationId}: ${stats.currentStep ?? "unknown"}; ${stats.finishedCommits} finished commit(s), ${stats.pendingCommits} pending.`,
231
+ { action: params.action, cloudStats: stats },
232
+ );
233
+ }
234
+ case "cloud_pull": {
235
+ const store = await SecurityStore.openForCwd(this.session.cwd, { signal });
236
+ const bundle = await pullCodexSecurityCloudResults({
237
+ client: cloudClientForSession(this.session, params.credential_id),
238
+ configurationId: requireValue(params.cloud_configuration_id, "cloud_configuration_id"),
239
+ store,
240
+ signal,
241
+ });
242
+ return textResult(
243
+ `Imported ${bundle.findings.length} Codex Security cloud finding(s) as security scan ${bundle.scan.id}.`,
244
+ {
245
+ action: params.action,
246
+ importedScan: { id: bundle.scan.id, findingCount: bundle.findings.length },
247
+ },
248
+ );
249
+ }
250
+ case "validate": {
251
+ const scanId = requireValue(params.scan_id, "scan_id");
252
+ const findingId = requireValue(params.finding_id, "finding_id");
253
+ const status = params.validation_status;
254
+ if (!status) throw new ToolError("validation_status is required for this action");
255
+ const summary = requireValue(params.validation_summary, "validation_summary");
256
+ const store = await SecurityStore.openForCwd(this.session.cwd, { signal });
257
+ const finding = await store.getFinding(scanId, findingId);
258
+ if (!finding) throw new ToolError(`Unknown security finding: ${findingId}`);
259
+ const evidence: SecurityEvidence[] = (params.validation_evidence ?? []).map((item, index) => ({
260
+ id: createSecurityEvidenceId(
261
+ finding.fingerprint,
262
+ `validation:${item.label}`,
263
+ finding.evidence.length + index,
264
+ ),
265
+ kind: "validation",
266
+ label: item.label,
267
+ explanation: item.explanation,
268
+ }));
269
+ const updated = await store.updateValidation(
270
+ scanId,
271
+ findingId,
272
+ {
273
+ status,
274
+ summary,
275
+ evidenceIds: evidence.map(item => item.id),
276
+ validatedAt: new Date().toISOString(),
277
+ },
278
+ evidence,
279
+ );
280
+ return textResult(`Finding ${updated.id} validation is now ${updated.validation.status}.`, {
281
+ action: params.action,
282
+ finding: { id: updated.id, validationStatus: updated.validation.status },
283
+ });
284
+ }
285
+ }
286
+ }
287
+ }