@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.
- package/CHANGELOG.md +110 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
- package/dist/cli.js +10009 -9654
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-discovery.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +73 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/internal-urls/security-protocol.d.ts +15 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +18 -0
- package/dist/types/security/auth.d.ts +30 -0
- package/dist/types/security/cloud.d.ts +79 -0
- package/dist/types/security/comparison.d.ts +49 -0
- package/dist/types/security/contracts/ids.d.ts +15 -0
- package/dist/types/security/contracts/index.d.ts +4 -0
- package/dist/types/security/contracts/schemas.d.ts +660 -0
- package/dist/types/security/contracts/types.d.ts +234 -0
- package/dist/types/security/contracts/validation.d.ts +5 -0
- package/dist/types/security/coordinator.d.ts +100 -0
- package/dist/types/security/importers/codex-security.d.ts +7 -0
- package/dist/types/security/importers/index.d.ts +2 -0
- package/dist/types/security/importers/sarif.d.ts +9 -0
- package/dist/types/security/index.d.ts +13 -0
- package/dist/types/security/preflight.d.ts +61 -0
- package/dist/types/security/provenance.d.ts +24 -0
- package/dist/types/security/publication.d.ts +78 -0
- package/dist/types/security/remediation.d.ts +27 -0
- package/dist/types/security/resource-output.d.ts +8 -0
- package/dist/types/security/sarif.d.ts +2 -0
- package/dist/types/security/store.d.ts +40 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/slash-commands/helpers/security.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/executor.d.ts +14 -2
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +10 -2
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/security-scan.d.ts +96 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/scripts/security-compare.ts +40 -0
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-discovery.ts +32 -5
- package/src/config/model-registry.ts +107 -25
- package/src/config/settings-schema.ts +77 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/eval/py/prelude.py +7 -3
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/internal-urls/router.ts +4 -1
- package/src/internal-urls/security-protocol.ts +261 -0
- package/src/internal-urls/types.ts +1 -1
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +7 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +15 -9
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +31 -17
- package/src/modes/rpc/host-uris.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/agents/security-reviewer.md +75 -0
- package/src/prompts/security/scan-coordinator.md +7 -0
- package/src/prompts/security/scan-request.md +21 -0
- package/src/prompts/security/validate-request.md +8 -0
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/grep.md +7 -6
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/sdk.ts +212 -21
- package/src/security/auth.ts +98 -0
- package/src/security/cloud.ts +686 -0
- package/src/security/comparison.ts +255 -0
- package/src/security/contracts/ids.ts +111 -0
- package/src/security/contracts/index.ts +4 -0
- package/src/security/contracts/schemas.ts +201 -0
- package/src/security/contracts/types.ts +254 -0
- package/src/security/contracts/validation.ts +65 -0
- package/src/security/coordinator.ts +708 -0
- package/src/security/importers/codex-security.ts +387 -0
- package/src/security/importers/index.ts +2 -0
- package/src/security/importers/sarif.ts +357 -0
- package/src/security/index.ts +13 -0
- package/src/security/preflight.ts +405 -0
- package/src/security/provenance.ts +106 -0
- package/src/security/publication.ts +326 -0
- package/src/security/remediation.ts +93 -0
- package/src/security/resource-output.ts +50 -0
- package/src/security/sarif.ts +78 -0
- package/src/security/store.ts +430 -0
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +302 -175
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +152 -3
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +20 -4
- package/src/task/render.ts +20 -4
- package/src/task/structured-subagent.ts +6 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +37 -5
- package/src/tools/path-utils.ts +91 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/security-scan.ts +287 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- 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
|
+
}
|