@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,357 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import type {
5
+ SecurityCoverage,
6
+ SecurityFinding,
7
+ SecurityLocation,
8
+ SecurityProducer,
9
+ SecurityProvenance,
10
+ SecurityScanBundle,
11
+ SecuritySeverityLevel,
12
+ } from "../contracts";
13
+ import {
14
+ canonicalSecurityJson,
15
+ createSecurityFindingFingerprint,
16
+ createSecurityFindingId,
17
+ createSecurityOccurrenceId,
18
+ createSecurityScanId,
19
+ encodeSecurityProjectKey,
20
+ parseSecurityScanBundle,
21
+ } from "../contracts";
22
+
23
+ interface SarifRegion {
24
+ startLine?: number;
25
+ endLine?: number;
26
+ startColumn?: number;
27
+ endColumn?: number;
28
+ }
29
+
30
+ interface SarifArtifactLocation {
31
+ uri?: string;
32
+ uriBaseId?: string;
33
+ }
34
+
35
+ interface SarifPhysicalLocation {
36
+ artifactLocation?: SarifArtifactLocation;
37
+ region?: SarifRegion;
38
+ }
39
+
40
+ interface SarifResult {
41
+ ruleId?: string;
42
+ level?: string;
43
+ message?: { text?: string; markdown?: string };
44
+ locations?: Array<{ physicalLocation?: SarifPhysicalLocation }>;
45
+ fingerprints?: Record<string, string>;
46
+ partialFingerprints?: Record<string, string>;
47
+ properties?: Record<string, unknown>;
48
+ }
49
+
50
+ interface SarifRule {
51
+ id?: string;
52
+ name?: string;
53
+ shortDescription?: { text?: string };
54
+ properties?: { tags?: unknown } & Record<string, unknown>;
55
+ }
56
+
57
+ interface SarifRun {
58
+ tool?: { driver?: { name?: string; version?: string; rules?: SarifRule[] } };
59
+ results?: SarifResult[];
60
+ originalUriBaseIds?: Record<string, { uri?: string }>;
61
+ }
62
+
63
+ interface SarifLog {
64
+ version?: string;
65
+ runs?: SarifRun[];
66
+ }
67
+
68
+ export interface SarifImportOptions {
69
+ repositoryRoot: string;
70
+ sourcePath?: string;
71
+ createdAt?: string;
72
+ createScanId?: () => string;
73
+ }
74
+
75
+ function severityFromSarif(result: SarifResult): SecuritySeverityLevel {
76
+ const score = Number(result.properties?.["security-severity"]);
77
+ if (Number.isFinite(score)) {
78
+ if (score >= 9) return "critical";
79
+ if (score >= 7) return "high";
80
+ if (score >= 4) return "medium";
81
+ if (score > 0) return "low";
82
+ }
83
+ switch (result.level) {
84
+ case "error":
85
+ return "high";
86
+ case "warning":
87
+ return "medium";
88
+ case "note":
89
+ return "low";
90
+ default:
91
+ return "informational";
92
+ }
93
+ }
94
+
95
+ function pathIsWithin(candidate: string, root: string): boolean {
96
+ return candidate === root || candidate.startsWith(`${root}${path.sep}`);
97
+ }
98
+
99
+ async function resolveSarifArtifactPath(
100
+ artifact: SarifArtifactLocation,
101
+ run: SarifRun,
102
+ repositoryRoot: string,
103
+ ): Promise<string> {
104
+ const uri = artifact.uri;
105
+ if (!uri) throw new Error("SARIF artifact location is missing its URI");
106
+ const rootUrl = pathToFileURL(`${repositoryRoot}${path.sep}`);
107
+ let baseUrl = rootUrl;
108
+ if (artifact.uriBaseId) {
109
+ const declaredBase = run.originalUriBaseIds?.[artifact.uriBaseId]?.uri;
110
+ if (!declaredBase && artifact.uriBaseId !== "%SRCROOT%") {
111
+ throw new Error(`SARIF artifact uses an unknown URI base: ${artifact.uriBaseId}`);
112
+ }
113
+ baseUrl = declaredBase ? new URL(declaredBase, rootUrl) : rootUrl;
114
+ }
115
+ const resolvedUrl = new URL(uri.replaceAll("\\", "/"), baseUrl);
116
+ if (resolvedUrl.protocol !== "file:") {
117
+ throw new Error(`SARIF artifact URI must resolve to a repository file: ${uri}`);
118
+ }
119
+ const absolute = path.resolve(fileURLToPath(resolvedUrl));
120
+ if (!pathIsWithin(absolute, repositoryRoot)) {
121
+ throw new Error(`SARIF artifact resolves outside the repository: ${uri}`);
122
+ }
123
+ const canonical = await fs.realpath(absolute).catch(error => {
124
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return absolute;
125
+ throw error;
126
+ });
127
+ if (!pathIsWithin(canonical, repositoryRoot)) {
128
+ throw new Error(`SARIF artifact resolves outside the repository through a symbolic link: ${uri}`);
129
+ }
130
+ return path.relative(repositoryRoot, canonical).replaceAll(path.sep, "/");
131
+ }
132
+
133
+ async function normalizeSarifLocations(
134
+ result: SarifResult,
135
+ run: SarifRun,
136
+ repositoryRoot: string,
137
+ ): Promise<SecurityLocation[]> {
138
+ const locations: SecurityLocation[] = [];
139
+ for (const item of result.locations ?? []) {
140
+ const physical = item.physicalLocation;
141
+ const artifact = physical?.artifactLocation;
142
+ const region = physical?.region;
143
+ const startLine = region?.startLine;
144
+ if (!artifact?.uri || !startLine || startLine < 1) continue;
145
+ const location: SecurityLocation = {
146
+ path: await resolveSarifArtifactPath(artifact, run, repositoryRoot),
147
+ startLine,
148
+ role: "primary",
149
+ };
150
+ if (region.endLine !== undefined) location.endLine = region.endLine;
151
+ if (region.startColumn !== undefined) location.startColumn = region.startColumn;
152
+ if (region.endColumn !== undefined) location.endColumn = region.endColumn;
153
+ locations.push(location);
154
+ }
155
+ return locations.length > 0 ? locations : [{ path: "unknown", startLine: 1, role: "unknown" }];
156
+ }
157
+
158
+ function stringRecord(value: unknown): Record<string, string> {
159
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
160
+ const result: Record<string, string> = {};
161
+ for (const [key, item] of Object.entries(value)) {
162
+ if (typeof item === "string") result[key] = item;
163
+ }
164
+ return result;
165
+ }
166
+
167
+ function tagsForRule(rule: SarifRule | undefined): string[] {
168
+ const tags = rule?.properties?.tags;
169
+ return Array.isArray(tags) ? tags.filter((tag): tag is string => typeof tag === "string") : [];
170
+ }
171
+ function selectFirstVendorFingerprint(vendorFingerprints: Record<string, string>): string | undefined {
172
+ for (const [, value] of Object.entries(vendorFingerprints).sort(([left], [right]) =>
173
+ left < right ? -1 : left > right ? 1 : 0,
174
+ )) {
175
+ if (value) return value;
176
+ }
177
+ return undefined;
178
+ }
179
+
180
+ function semanticResultAnchor(
181
+ ruleId: string,
182
+ category: string,
183
+ message: string,
184
+ locations: readonly SecurityLocation[],
185
+ ): string {
186
+ return `sarif-result/v1:sha256:${Bun.SHA256.hash(
187
+ canonicalSecurityJson({
188
+ ruleId,
189
+ category,
190
+ message,
191
+ locations,
192
+ }),
193
+ "hex",
194
+ )}`;
195
+ }
196
+ const canonicalValidationStatuses: Record<string, true> = {
197
+ unvalidated: true,
198
+ validated: true,
199
+ rejected: true,
200
+ partial: true,
201
+ error: true,
202
+ };
203
+
204
+ const canonicalDispositionStatuses: Record<string, true> = {
205
+ open: true,
206
+ false_positive: true,
207
+ accepted_risk: true,
208
+ fixed: true,
209
+ wont_fix: true,
210
+ };
211
+
212
+ function importedValidationStatus(value: unknown): SecurityFinding["validation"]["status"] {
213
+ return typeof value === "string" && canonicalValidationStatuses[value] === true
214
+ ? (value as SecurityFinding["validation"]["status"])
215
+ : "unvalidated";
216
+ }
217
+
218
+ function importedDispositionStatus(value: unknown): SecurityFinding["disposition"]["status"] {
219
+ return typeof value === "string" && canonicalDispositionStatuses[value] === true
220
+ ? (value as SecurityFinding["disposition"]["status"])
221
+ : "open";
222
+ }
223
+
224
+ export async function importSarif(input: unknown, options: SarifImportOptions): Promise<SecurityScanBundle> {
225
+ const sarif = input as SarifLog;
226
+ if (sarif.version !== "2.1.0" || !Array.isArray(sarif.runs)) {
227
+ throw new Error("Expected SARIF 2.1.0 input");
228
+ }
229
+ const canonicalRoot = await fs.realpath(path.resolve(options.repositoryRoot));
230
+ const scanId = options.createScanId?.() ?? createSecurityScanId();
231
+ const createdAt = options.createdAt ?? new Date().toISOString();
232
+ const findings: SecurityFinding[] = [];
233
+ const seenFingerprints = new Set<string>();
234
+ let producerName = "SARIF importer";
235
+ let producerVersion: string | undefined;
236
+
237
+ for (const run of sarif.runs) {
238
+ const driver = run.tool?.driver;
239
+ producerName = driver?.name || producerName;
240
+ producerVersion = driver?.version ?? producerVersion;
241
+ const rules = new Map((driver?.rules ?? []).filter(rule => rule.id).map(rule => [rule.id as string, rule]));
242
+ for (const result of run.results ?? []) {
243
+ const ruleId = result.ruleId || "sarif.unknown";
244
+ const rule = rules.get(ruleId);
245
+ const locations = await normalizeSarifLocations(result, run, canonicalRoot);
246
+ const vendorFingerprints = {
247
+ ...stringRecord(result.fingerprints),
248
+ ...stringRecord(result.partialFingerprints),
249
+ };
250
+ const firstVendorFingerprint = selectFirstVendorFingerprint(vendorFingerprints);
251
+ const category =
252
+ typeof result.properties?.category === "string"
253
+ ? result.properties.category
254
+ : ruleId.split(/[./-]/)[0] || "security";
255
+ const message = result.message?.text ?? result.message?.markdown ?? rule?.shortDescription?.text ?? ruleId;
256
+ const anchor = firstVendorFingerprint ?? semanticResultAnchor(ruleId, category, message, locations);
257
+ const fingerprint = createSecurityFindingFingerprint({
258
+ ruleId,
259
+ category,
260
+ anchor,
261
+ locations,
262
+ });
263
+ if (seenFingerprints.has(fingerprint)) continue;
264
+ seenFingerprints.add(fingerprint);
265
+ const tags = tagsForRule(rule);
266
+ const provenance: SecurityProvenance = {
267
+ producer: { kind: "sarif-import", name: producerName },
268
+ createdAt,
269
+ importedAt: new Date().toISOString(),
270
+ vendorFingerprints,
271
+ };
272
+ if (producerVersion !== undefined) provenance.producer.version = producerVersion;
273
+ if (options.sourcePath) provenance.metadata = { sourcePath: options.sourcePath };
274
+ const finding: SecurityFinding = {
275
+ id: createSecurityFindingId(fingerprint),
276
+ scanId,
277
+ fingerprint,
278
+ ruleId,
279
+ title: rule?.shortDescription?.text ?? rule?.name ?? ruleId,
280
+ summary: message,
281
+ severity: { level: severityFromSarif(result) },
282
+ confidence: { level: "medium", rationale: "Imported from a SARIF producer" },
283
+ taxonomy: {
284
+ category,
285
+ cwe: tags.filter(tag => /^CWE-\d+$/i.test(tag)).map(tag => tag.toUpperCase()),
286
+ tags,
287
+ },
288
+ occurrences: [{ id: createSecurityOccurrenceId(fingerprint, locations), locations, evidenceIds: [] }],
289
+ evidence: [],
290
+ validation: { status: importedValidationStatus(result.properties?.validation), evidenceIds: [] },
291
+ disposition: { status: importedDispositionStatus(result.properties?.disposition) },
292
+ provenance,
293
+ };
294
+ finding.anchor = anchor;
295
+ const score = Number(result.properties?.["security-severity"]);
296
+ if (Number.isFinite(score)) finding.severity.score = score;
297
+ findings.push(finding);
298
+ }
299
+ }
300
+
301
+ const coverage: SecurityCoverage = {
302
+ mode: "imported",
303
+ completeness: "unknown",
304
+ inventoryStrategy: "imported",
305
+ includePaths: [],
306
+ excludePaths: [],
307
+ surfaces: [],
308
+ explicitExclusions: [],
309
+ deferred: [{ id: "sarif-coverage", reason: "SARIF does not define repository coverage" }],
310
+ };
311
+ const producer: SecurityProducer = { kind: "sarif-import", name: producerName };
312
+ if (producerVersion !== undefined) producer.version = producerVersion;
313
+ const scanProvenance: SecurityProvenance = {
314
+ producer,
315
+ createdAt,
316
+ importedAt: new Date().toISOString(),
317
+ };
318
+ if (options.sourcePath) scanProvenance.metadata = { sourcePath: options.sourcePath };
319
+ return parseSecurityScanBundle({
320
+ scan: {
321
+ documentType: "omp-security.scan",
322
+ schemaVersion: "1.0",
323
+ id: scanId,
324
+ projectKey: encodeSecurityProjectKey(canonicalRoot),
325
+ status: "completed",
326
+ createdAt,
327
+ completedAt: createdAt,
328
+ target: {
329
+ kind: "imported",
330
+ repositoryRoot: canonicalRoot,
331
+ displayName: path.basename(canonicalRoot),
332
+ includePaths: [],
333
+ excludePaths: [],
334
+ treeDigest: Bun.SHA256.hash(JSON.stringify(input), "hex"),
335
+ },
336
+ producer,
337
+ provenance: scanProvenance,
338
+ findingIds: findings.map(finding => finding.id),
339
+ coverage,
340
+ reportRef: "report.md",
341
+ sarifRef: "results.sarif",
342
+ },
343
+ findings,
344
+ report: `# Imported SARIF security results\n\nProducer: ${producerName}\n\nFindings: ${findings.length}\n`,
345
+ sarif: input as Record<string, unknown>,
346
+ });
347
+ }
348
+
349
+ export async function importSarifFile(
350
+ filePath: string,
351
+ options: Omit<SarifImportOptions, "sourcePath">,
352
+ ): Promise<SecurityScanBundle> {
353
+ return importSarif(JSON.parse(await Bun.file(filePath).text()) as unknown, {
354
+ ...options,
355
+ sourcePath: path.resolve(filePath),
356
+ });
357
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./auth";
2
+ export * from "./cloud";
3
+ export * from "./comparison";
4
+ export * from "./contracts";
5
+ export * from "./coordinator";
6
+ export * from "./importers";
7
+ export * from "./preflight";
8
+ export * from "./provenance";
9
+ export * from "./publication";
10
+ export * from "./remediation";
11
+ export * from "./resource-output";
12
+ export * from "./sarif";
13
+ export * from "./store";