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