pi-subagents 0.56.0 → 0.58.0
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 +92 -0
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +124 -21
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +41 -2
- package/docs/models.md +3 -3
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +14 -3
- package/docs/workflows.md +44 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +36 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +257 -51
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +18 -4
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +28 -3
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +33 -8
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +55 -19
- package/src/extension/schemas.ts +6 -5
- package/src/extension/tool-description.ts +18 -12
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +51 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +127 -19
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +7 -0
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +36 -15
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +239 -56
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +92 -11
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +22 -1
- package/src/runs/foreground/subagent-executor.ts +322 -102
- package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/completion-guard.ts +4 -3
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +348 -55
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +386 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
- package/src/runs/shared/model-exclusions.ts +69 -7
- package/src/runs/shared/model-fallback.ts +39 -5
- package/src/runs/shared/mutation-evidence.ts +7 -2
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +13 -1
- package/src/shared/launch-contract.ts +4 -0
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +10 -2
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +160 -10
- package/src/shared/utils.ts +6 -29
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +55 -21
- package/src/workflows/scripted-workflow.ts +299 -31
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +155 -5
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { parseExternalCliJsonlEvent, type ExternalCliParser, type ExternalCliParserProgress, type ExternalCliParserTerminal } from "./external-cli-runner.ts";
|
|
3
|
+
import type { ExternalCliPreflightSpec } from "./external-cli-preflight.ts";
|
|
4
|
+
|
|
5
|
+
const MAX_EVENT_TYPE_LENGTH = 128;
|
|
6
|
+
const MAX_ERROR_LENGTH = 4_096;
|
|
7
|
+
const MAX_OVERSIZED_TOOL_CALL_BYTES = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export const CURSOR_AGENT_ADAPTER_ID = "cursor-agent" as const;
|
|
10
|
+
export const CURSOR_AGENT_WRITER_ADAPTER_ID = "cursor-agent-writer" as const;
|
|
11
|
+
export const CURSOR_AGENT_ENV_ALLOWLIST = [
|
|
12
|
+
"PATH",
|
|
13
|
+
"HOME",
|
|
14
|
+
"USERPROFILE",
|
|
15
|
+
"CURSOR_API_KEY",
|
|
16
|
+
"HTTP_PROXY",
|
|
17
|
+
"HTTPS_PROXY",
|
|
18
|
+
"NO_PROXY",
|
|
19
|
+
"http_proxy",
|
|
20
|
+
"https_proxy",
|
|
21
|
+
"no_proxy",
|
|
22
|
+
"SSL_CERT_FILE",
|
|
23
|
+
"SSL_CERT_DIR",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
function terminalError(event: Record<string, unknown>): string {
|
|
27
|
+
for (const value of [event.result, event.error, event.message]) {
|
|
28
|
+
if (typeof value === "string" && value.trim()) return value.trim().slice(0, MAX_ERROR_LENGTH);
|
|
29
|
+
}
|
|
30
|
+
const subtype = typeof event.subtype === "string" && event.subtype ? event.subtype : "unknown";
|
|
31
|
+
return `Cursor Agent reported terminal result ${subtype}.`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createCursorAgentJsonlParser(): ExternalCliParser {
|
|
35
|
+
let eventCount = 0;
|
|
36
|
+
let terminal: ExternalCliParserTerminal | undefined;
|
|
37
|
+
return {
|
|
38
|
+
parseLine(line): ExternalCliParserProgress {
|
|
39
|
+
const event = parseExternalCliJsonlEvent(line, "Cursor Agent", MAX_EVENT_TYPE_LENGTH);
|
|
40
|
+
if (terminal) throw new Error("Cursor Agent emitted an event after its terminal state.");
|
|
41
|
+
eventCount += 1;
|
|
42
|
+
if (event.type === "error") terminal = { state: "failed", error: terminalError(event) };
|
|
43
|
+
else if (event.type === "result") {
|
|
44
|
+
if (event.subtype === "success" && event.is_error === false && typeof event.result === "string" && event.result.trim()) {
|
|
45
|
+
terminal = { state: "completed", output: event.result.trim() };
|
|
46
|
+
} else terminal = { state: "failed", error: terminalError(event) };
|
|
47
|
+
}
|
|
48
|
+
return { phase: terminal ? terminal.state : "streaming", eventCount };
|
|
49
|
+
},
|
|
50
|
+
skipOversizedLine(prefix, byteLength): ExternalCliParserProgress | undefined {
|
|
51
|
+
if (terminal || byteLength > MAX_OVERSIZED_TOOL_CALL_BYTES || !/^\s*\{\s*"type"\s*:\s*"tool_call"\s*,/.test(prefix)) return undefined;
|
|
52
|
+
eventCount += 1;
|
|
53
|
+
return { phase: "streaming", eventCount };
|
|
54
|
+
},
|
|
55
|
+
finish(): ExternalCliParserTerminal | undefined {
|
|
56
|
+
return terminal;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveCursorAgentLaunch(input: {
|
|
62
|
+
adapter: typeof CURSOR_AGENT_ADAPTER_ID | typeof CURSOR_AGENT_WRITER_ADAPTER_ID;
|
|
63
|
+
command: string;
|
|
64
|
+
cwd: string;
|
|
65
|
+
asyncDir: string;
|
|
66
|
+
stepIndex: number;
|
|
67
|
+
/** Test-only executable prefix for a fake Cursor Agent process. */
|
|
68
|
+
commandPrefixArgs?: readonly string[];
|
|
69
|
+
}): {
|
|
70
|
+
command: string;
|
|
71
|
+
args: string[];
|
|
72
|
+
finalOutputPath?: undefined;
|
|
73
|
+
promptFilePath: string;
|
|
74
|
+
temporaryDirectories: string[];
|
|
75
|
+
environment: { allowlist: readonly string[] };
|
|
76
|
+
preflight: ExternalCliPreflightSpec;
|
|
77
|
+
parser: ExternalCliParser;
|
|
78
|
+
} {
|
|
79
|
+
const writer = input.adapter === CURSOR_AGENT_WRITER_ADAPTER_ID;
|
|
80
|
+
const promptDirectory = path.join(input.asyncDir, `external-${input.stepIndex}.cursor-prompt`);
|
|
81
|
+
const promptFilePath = path.join(promptDirectory, "handoff.txt");
|
|
82
|
+
const promptRelative = path.relative(input.cwd, promptDirectory);
|
|
83
|
+
const promptOutsideWorkspace = promptRelative.startsWith("..") || path.isAbsolute(promptRelative);
|
|
84
|
+
const prefix = [...(input.commandPrefixArgs ?? [])];
|
|
85
|
+
const args = [
|
|
86
|
+
...prefix,
|
|
87
|
+
"-p",
|
|
88
|
+
"--output-format", "stream-json",
|
|
89
|
+
...(writer ? [] : ["--mode", "ask"]),
|
|
90
|
+
"--sandbox", "enabled",
|
|
91
|
+
"--workspace", input.cwd,
|
|
92
|
+
...(promptOutsideWorkspace ? ["--add-dir", promptDirectory] : []),
|
|
93
|
+
`Read the complete handoff from the private file at ${promptFilePath}. Follow it and return only the final answer.`,
|
|
94
|
+
];
|
|
95
|
+
return {
|
|
96
|
+
command: input.command,
|
|
97
|
+
args,
|
|
98
|
+
promptFilePath,
|
|
99
|
+
temporaryDirectories: [promptDirectory],
|
|
100
|
+
environment: { allowlist: CURSOR_AGENT_ENV_ALLOWLIST },
|
|
101
|
+
preflight: {
|
|
102
|
+
id: input.adapter,
|
|
103
|
+
versionArgs: [...prefix, "--version"],
|
|
104
|
+
helpArgs: [...prefix, "--help"],
|
|
105
|
+
validate(result) {
|
|
106
|
+
if (!/^\d{4}\.\d{2}\.\d{2}-[0-9a-f]+$/.test(result.version)) throw new Error(`Unsupported Cursor Agent version response: ${JSON.stringify(result.version)}.`);
|
|
107
|
+
for (const required of ["Start the Cursor Agent", "--print", "stream-json", "--sandbox <mode>", "enabled", "--workspace <path-or-name>", "--add-dir <path>", ...(writer ? ["all tools", "including write and shell"] : ["--mode <mode>", "ask:", "read-only"])]) {
|
|
108
|
+
if (!result.help.includes(required)) throw new Error(`Cursor Agent help does not document required option ${JSON.stringify(required)}.`);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
parser: createCursorAgentJsonlParser(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -49,9 +49,9 @@ const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
|
|
|
49
49
|
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "fast", "toolBudget", "acceptance", "agentContract", "gateOn"]);
|
|
50
50
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
51
51
|
...DYNAMIC_PARALLEL_KEYS,
|
|
52
|
-
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled",
|
|
53
|
-
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "capabilityCeiling", "completionGuard", "systemPrompt",
|
|
54
|
-
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "launchResolvedExtensions",
|
|
52
|
+
"outputName", "structured", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled",
|
|
53
|
+
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
|
|
54
|
+
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "launchResolvedExtensions", "requestedCwd",
|
|
55
55
|
]);
|
|
56
56
|
const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
|
|
57
57
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExternalCliReceiptMetadata,
|
|
3
|
+
ExternalCliCapabilityNarrowing,
|
|
4
|
+
ExternalCliRunnerStatus,
|
|
5
|
+
ExternalProcessStatus,
|
|
6
|
+
} from "../../shared/types.ts";
|
|
7
|
+
|
|
8
|
+
const UNSUPPORTED = {
|
|
9
|
+
steer: "The one-shot stdin adapter closes input after launch and cannot accept live steer messages.",
|
|
10
|
+
resume: "The one-shot stdin adapter has no durable external session identity.",
|
|
11
|
+
structuredOutput: "The generic external CLI adapter does not parse a trusted structured result.",
|
|
12
|
+
toolEvents: "The generic external CLI adapter treats stdout as untrusted text, not native Pi tool events.",
|
|
13
|
+
supervisor: "The generic external CLI adapter has no trusted supervisor event transport.",
|
|
14
|
+
forkContext: "Native Pi fork context is not available without an adapter-owned handoff artifact.",
|
|
15
|
+
extensionBindings: "Native Pi extension bindings are never passed to external runners.",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
const CAPABILITY_KEYS = new Set(Object.keys(UNSUPPORTED));
|
|
19
|
+
const PROMPT_FILE_UNSUPPORTED = {
|
|
20
|
+
...UNSUPPORTED,
|
|
21
|
+
steer: "The one-shot prompt-file adapter closes input after launch and cannot accept live steer messages.",
|
|
22
|
+
resume: "The one-shot prompt-file adapter does not retain a durable external session identity.",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export const CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS = [
|
|
26
|
+
"codex-exec",
|
|
27
|
+
"codex-exec-writer",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"claude-code-writer",
|
|
30
|
+
"cursor-agent",
|
|
31
|
+
"cursor-agent-writer",
|
|
32
|
+
] as const;
|
|
33
|
+
export type CodeOwnedExternalCliAdapterId = typeof CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS[number];
|
|
34
|
+
|
|
35
|
+
const CODE_OWNED_EXTERNAL_CLI_ADAPTER_ID_SET = new Set<string>(CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS);
|
|
36
|
+
export const CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL = CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS.map((id) => `'${id}'`).join(", ");
|
|
37
|
+
|
|
38
|
+
export function isCodeOwnedExternalCliAdapterId(value: unknown): value is CodeOwnedExternalCliAdapterId {
|
|
39
|
+
return typeof value === "string" && CODE_OWNED_EXTERNAL_CLI_ADAPTER_ID_SET.has(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RESERVED_READ_ONLY_ADAPTERS = [
|
|
43
|
+
{ name: "claude-code", writer: "claude-code-writer", access: "file-write" },
|
|
44
|
+
{ name: "codex-exec", writer: "codex-exec-writer", access: "workspace-write" },
|
|
45
|
+
{ name: "cursor-agent", writer: "cursor-agent-writer", access: "workspace-write" },
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
export function validateCodeOwnedProfileRunner(
|
|
49
|
+
agent: {
|
|
50
|
+
name: string;
|
|
51
|
+
localName?: string;
|
|
52
|
+
aliases?: readonly string[];
|
|
53
|
+
runner?: { type: string; adapter?: string };
|
|
54
|
+
},
|
|
55
|
+
): string | undefined {
|
|
56
|
+
const selectionNames = [agent.name, ...(agent.localName ? [agent.localName] : []), ...(agent.aliases ?? [])];
|
|
57
|
+
for (const adapter of RESERVED_READ_ONLY_ADAPTERS) {
|
|
58
|
+
if (selectionNames.includes(adapter.name) && !(agent.runner?.type === "external-cli" && agent.runner.adapter === adapter.name)) {
|
|
59
|
+
return `Selection name '${adapter.name}' is reserved for the read-only '${adapter.name}' adapter. Use '${adapter.writer}' for explicit ${adapter.access} access.`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseExternalCliCapabilityNarrowing(value: unknown, label: string): ExternalCliCapabilityNarrowing | undefined {
|
|
66
|
+
if (value === undefined) return undefined;
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
68
|
+
const input = value as Record<string, unknown>;
|
|
69
|
+
const unknown = Object.keys(input).filter((key) => !CAPABILITY_KEYS.has(key));
|
|
70
|
+
if (unknown.length > 0) throw new Error(`${label} has unsupported fields: ${unknown.join(", ")}.`);
|
|
71
|
+
for (const [key, setting] of Object.entries(input)) {
|
|
72
|
+
if (setting !== false) throw new Error(`${label}.${key} may only be false; user config cannot widen code-owned external adapter capabilities.`);
|
|
73
|
+
}
|
|
74
|
+
return input as ExternalCliCapabilityNarrowing;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveExternalCliRunnerStatus(input: {
|
|
78
|
+
adapter?: CodeOwnedExternalCliAdapterId;
|
|
79
|
+
command: string;
|
|
80
|
+
args?: string[];
|
|
81
|
+
promptDelivery?: "stdin";
|
|
82
|
+
capabilities?: ExternalCliCapabilityNarrowing;
|
|
83
|
+
}): ExternalCliRunnerStatus {
|
|
84
|
+
const codexExec = input.adapter === "codex-exec";
|
|
85
|
+
const codexExecWriter = input.adapter === "codex-exec-writer";
|
|
86
|
+
const claudeCode = input.adapter === "claude-code";
|
|
87
|
+
const claudeCodeWriter = input.adapter === "claude-code-writer";
|
|
88
|
+
const cursorAgent = input.adapter === "cursor-agent";
|
|
89
|
+
const cursorAgentWriter = input.adapter === "cursor-agent-writer";
|
|
90
|
+
const cursor = cursorAgent || cursorAgentWriter;
|
|
91
|
+
const unsupported = cursor ? PROMPT_FILE_UNSUPPORTED : UNSUPPORTED;
|
|
92
|
+
return {
|
|
93
|
+
type: "external-cli",
|
|
94
|
+
command: input.command,
|
|
95
|
+
args: input.args ?? [],
|
|
96
|
+
promptDelivery: cursor ? "prompt-file" : input.promptDelivery ?? "stdin",
|
|
97
|
+
adapter: { id: input.adapter ?? "external-cli", version: 1, executionMode: cursor ? "one-shot-prompt-file" : "one-shot-stdin" },
|
|
98
|
+
...(codexExec ? { safety: { sandbox: "read-only" as const, approvalPolicy: "never" as const, ephemeral: true as const } } : {}),
|
|
99
|
+
...(codexExecWriter ? { safety: { access: "workspace-write" as const, sandbox: "workspace-write" as const, approvalPolicy: "never" as const, ephemeral: true as const } } : {}),
|
|
100
|
+
...(claudeCode ? { safety: { access: "read-only" as const, authentication: "existing-cli-required" as const, permissionMode: "plan" as const, tools: "none" as const, mcp: "empty-strict" as const, settingSources: "user" as const, userSettingsTrust: "required" as const, sessionPersistence: false as const } } : {}),
|
|
101
|
+
...(claudeCodeWriter ? { safety: { access: "workspace-write" as const, authentication: "existing-cli-required" as const, permissionMode: "acceptEdits" as const, tools: "Read,Write,Edit,Glob,Grep" as const, mcp: "empty-strict" as const, settingSources: "user" as const, userSettingsTrust: "required" as const, sessionPersistence: false as const } } : {}),
|
|
102
|
+
...(cursorAgent ? { safety: { access: "read-only" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "ask" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
|
|
103
|
+
...(cursorAgentWriter ? { safety: { access: "workspace-write" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "print" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
|
|
104
|
+
capabilities: {
|
|
105
|
+
stop: true,
|
|
106
|
+
steer: false,
|
|
107
|
+
resume: false,
|
|
108
|
+
structuredOutput: false,
|
|
109
|
+
toolEvents: false,
|
|
110
|
+
supervisor: "unsupported",
|
|
111
|
+
forkContext: false,
|
|
112
|
+
extensionBindings: false,
|
|
113
|
+
},
|
|
114
|
+
unsupportedReasons: unsupported,
|
|
115
|
+
nonResumableReason: unsupported.resume,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function normalizeExternalCliRunnerStatus(value: unknown): ExternalCliRunnerStatus | undefined {
|
|
120
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
121
|
+
const input = value as Record<string, unknown>;
|
|
122
|
+
if (input.type !== "external-cli" || typeof input.command !== "string" || !input.command.trim()) return undefined;
|
|
123
|
+
const args = Array.isArray(input.args) && input.args.every((arg) => typeof arg === "string")
|
|
124
|
+
? input.args
|
|
125
|
+
: undefined;
|
|
126
|
+
const promptDelivery = input.promptDelivery === "stdin" ? "stdin" : undefined;
|
|
127
|
+
const adapterId = input.adapter && typeof input.adapter === "object" && !Array.isArray(input.adapter)
|
|
128
|
+
? (input.adapter as Record<string, unknown>).id
|
|
129
|
+
: undefined;
|
|
130
|
+
if (adapterId === "grok-build") {
|
|
131
|
+
return {
|
|
132
|
+
type: "external-cli",
|
|
133
|
+
command: input.command,
|
|
134
|
+
args: args ?? [],
|
|
135
|
+
promptDelivery: "prompt-file",
|
|
136
|
+
adapter: { id: "grok-build", version: 1, executionMode: "one-shot-prompt-file" },
|
|
137
|
+
capabilities: { stop: true, steer: false, resume: false, structuredOutput: false, toolEvents: false, supervisor: "unsupported", forkContext: false, extensionBindings: false },
|
|
138
|
+
unsupportedReasons: PROMPT_FILE_UNSUPPORTED,
|
|
139
|
+
nonResumableReason: PROMPT_FILE_UNSUPPORTED.resume,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const adapter = isCodeOwnedExternalCliAdapterId(adapterId) ? adapterId : undefined;
|
|
143
|
+
return resolveExternalCliRunnerStatus({ ...(adapter ? { adapter } : {}), command: input.command, ...(args ? { args } : {}), ...(promptDelivery ? { promptDelivery } : {}) });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function externalCliReceiptMetadata(input: {
|
|
147
|
+
runner: ExternalCliRunnerStatus;
|
|
148
|
+
externalProcess?: ExternalProcessStatus;
|
|
149
|
+
outputReference?: string;
|
|
150
|
+
}): ExternalCliReceiptMetadata {
|
|
151
|
+
const { runner } = input;
|
|
152
|
+
return {
|
|
153
|
+
adapter: { ...runner.adapter },
|
|
154
|
+
capabilities: { ...runner.capabilities },
|
|
155
|
+
...(runner.safety ? { safety: { ...runner.safety } } : {}),
|
|
156
|
+
...(input.externalProcess ? {
|
|
157
|
+
outputArtifacts: {
|
|
158
|
+
stdoutPath: input.externalProcess.stdoutPath,
|
|
159
|
+
stderrPath: input.externalProcess.stderrPath,
|
|
160
|
+
...(input.outputReference || input.externalProcess.finalOutputPath ? { finalOutputPath: input.outputReference ?? input.externalProcess.finalOutputPath } : {}),
|
|
161
|
+
},
|
|
162
|
+
} : input.outputReference ? { outputArtifacts: { finalOutputPath: input.outputReference } } : {}),
|
|
163
|
+
handoff: { mode: "fresh" },
|
|
164
|
+
supervisor: { mode: "unsupported", reason: runner.unsupportedReasons.supervisor },
|
|
165
|
+
nonResumableReason: runner.nonResumableReason,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
const MAX_PROBE_OUTPUT_BYTES = 256 * 1024;
|
|
6
|
+
const MAX_PROBE_TIMEOUT_MS = 5_000;
|
|
7
|
+
const MAX_CACHE_ENTRIES = 64;
|
|
8
|
+
|
|
9
|
+
export type ExternalCliPreflightInvalidationReason = "launch" | "auth" | "parser" | "permission";
|
|
10
|
+
|
|
11
|
+
export interface ExternalCliPreflightSpec {
|
|
12
|
+
id: string;
|
|
13
|
+
versionArgs: readonly string[];
|
|
14
|
+
helpArgs: readonly string[];
|
|
15
|
+
evidenceArgs?: readonly string[];
|
|
16
|
+
evidenceLabel?: string;
|
|
17
|
+
probeTimeoutMs?: number;
|
|
18
|
+
validate?: (result: ExternalCliPreflightResult) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ExternalCliPreflightResult {
|
|
22
|
+
binaryPath: string;
|
|
23
|
+
binaryMtimeMs: number;
|
|
24
|
+
version: string;
|
|
25
|
+
help: string;
|
|
26
|
+
evidence?: string;
|
|
27
|
+
cacheHit: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type CachedPreflight = Omit<ExternalCliPreflightResult, "cacheHit">;
|
|
31
|
+
|
|
32
|
+
const cache = new Map<string, CachedPreflight>();
|
|
33
|
+
const lookup = new Map<string, string>();
|
|
34
|
+
|
|
35
|
+
function resolveBinary(command: string, env: NodeJS.ProcessEnv): string {
|
|
36
|
+
if (path.isAbsolute(command) || command.includes(path.sep)) {
|
|
37
|
+
const resolved = path.resolve(command);
|
|
38
|
+
fs.accessSync(resolved, fs.constants.X_OK);
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
41
|
+
const extensions = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
42
|
+
for (const directory of (env.PATH ?? "").split(path.delimiter)) {
|
|
43
|
+
if (!directory) continue;
|
|
44
|
+
for (const extension of extensions) {
|
|
45
|
+
const candidate = path.join(directory, `${command}${extension}`);
|
|
46
|
+
try {
|
|
47
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
48
|
+
return fs.realpathSync(candidate);
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`External CLI binary '${command}' was not found on PATH.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function probeWithTimeout(binaryPath: string, args: readonly string[], env: NodeJS.ProcessEnv, label: string, timeoutMs: number, cwd?: string): string {
|
|
56
|
+
const result = spawnSync(binaryPath, [...args], {
|
|
57
|
+
cwd,
|
|
58
|
+
env,
|
|
59
|
+
encoding: "utf-8",
|
|
60
|
+
killSignal: "SIGKILL",
|
|
61
|
+
maxBuffer: MAX_PROBE_OUTPUT_BYTES,
|
|
62
|
+
timeout: timeoutMs,
|
|
63
|
+
windowsHide: true,
|
|
64
|
+
});
|
|
65
|
+
if (result.error) throw new Error(`External CLI ${label} preflight failed: ${result.error.message}`, { cause: result.error });
|
|
66
|
+
if (result.status !== 0) throw new Error(`External CLI ${label} preflight exited with code ${result.status}: ${(result.stderr || result.stdout).trim()}`);
|
|
67
|
+
return result.stdout.trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function narrowPositiveInteger(value: number | undefined, ceiling: number, label: string): number {
|
|
71
|
+
if (value === undefined) return ceiling;
|
|
72
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > ceiling) throw new Error(`${label} may only narrow the code-owned ceiling of ${ceiling}.`);
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function specKey(spec: ExternalCliPreflightSpec): string {
|
|
77
|
+
return JSON.stringify([spec.id, spec.versionArgs, spec.helpArgs, spec.evidenceArgs, spec.evidenceLabel, spec.probeTimeoutMs]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function preflightExternalCli(command: string, spec: ExternalCliPreflightSpec, env: NodeJS.ProcessEnv, cwd?: string): ExternalCliPreflightResult {
|
|
81
|
+
const binaryPath = resolveBinary(command, env);
|
|
82
|
+
const binaryMtimeMs = fs.statSync(binaryPath).mtimeMs;
|
|
83
|
+
const lookupKey = JSON.stringify([binaryPath, binaryMtimeMs, specKey(spec)]);
|
|
84
|
+
const cachedKey = lookup.get(lookupKey);
|
|
85
|
+
const cached = cachedKey ? cache.get(cachedKey) : undefined;
|
|
86
|
+
const probeTimeoutMs = narrowPositiveInteger(spec.probeTimeoutMs, MAX_PROBE_TIMEOUT_MS, "probeTimeoutMs");
|
|
87
|
+
const base = cached ?? {
|
|
88
|
+
binaryPath,
|
|
89
|
+
binaryMtimeMs,
|
|
90
|
+
version: probeWithTimeout(binaryPath, spec.versionArgs, env, "version", probeTimeoutMs, cwd),
|
|
91
|
+
help: probeWithTimeout(binaryPath, spec.helpArgs, env, "help", probeTimeoutMs, cwd),
|
|
92
|
+
};
|
|
93
|
+
const evidence = spec.evidenceArgs
|
|
94
|
+
? probeWithTimeout(binaryPath, spec.evidenceArgs, env, spec.evidenceLabel ?? "evidence", probeTimeoutMs, cwd)
|
|
95
|
+
: undefined;
|
|
96
|
+
const result = { ...base, ...(evidence !== undefined ? { evidence } : {}), cacheHit: Boolean(cached) };
|
|
97
|
+
spec.validate?.(result);
|
|
98
|
+
if (!cached) {
|
|
99
|
+
const cacheKey = JSON.stringify([binaryPath, base.version, binaryMtimeMs, specKey(spec)]);
|
|
100
|
+
cache.set(cacheKey, base);
|
|
101
|
+
lookup.set(lookupKey, cacheKey);
|
|
102
|
+
while (cache.size > MAX_CACHE_ENTRIES) {
|
|
103
|
+
const oldest = cache.keys().next().value as string;
|
|
104
|
+
cache.delete(oldest);
|
|
105
|
+
for (const [candidateLookup, candidateCache] of lookup) if (candidateCache === oldest) lookup.delete(candidateLookup);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function invalidateExternalCliPreflight(command: string, spec: ExternalCliPreflightSpec, _reason: ExternalCliPreflightInvalidationReason): void {
|
|
112
|
+
const key = specKey(spec);
|
|
113
|
+
for (const [cacheKey, entry] of cache) {
|
|
114
|
+
if (entry.binaryPath === command || entry.binaryPath.endsWith(`${path.sep}${command}`) || cacheKey.includes(key)) cache.delete(cacheKey);
|
|
115
|
+
}
|
|
116
|
+
for (const [lookupKey, cacheKey] of lookup) if (!cache.has(cacheKey)) lookup.delete(lookupKey);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function clearExternalCliPreflightCacheForTests(): void {
|
|
120
|
+
cache.clear();
|
|
121
|
+
lookup.clear();
|
|
122
|
+
}
|