@the-open-engine/zeroshot 6.3.0 → 6.5.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/cli/commands/providers.js +11 -5
- package/cli/index.js +83 -17
- package/cli/lib/first-run.js +14 -3
- package/docker/zeroshot-cluster/Dockerfile +11 -1
- package/lib/agent-cli-provider/acp-stdio-runner.d.ts +4 -0
- package/lib/agent-cli-provider/acp-stdio-runner.d.ts.map +1 -0
- package/lib/agent-cli-provider/acp-stdio-runner.js +259 -0
- package/lib/agent-cli-provider/acp-stdio-runner.js.map +1 -0
- package/lib/agent-cli-provider/adapters/acp.d.ts +23 -0
- package/lib/agent-cli-provider/adapters/acp.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/acp.js +373 -0
- package/lib/agent-cli-provider/adapters/acp.js.map +1 -0
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +2 -29
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/copilot-parser.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/copilot-parser.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/copilot-parser.js +154 -0
- package/lib/agent-cli-provider/adapters/copilot-parser.js.map +1 -0
- package/lib/agent-cli-provider/adapters/copilot.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/copilot.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/copilot.js +176 -0
- package/lib/agent-cli-provider/adapters/copilot.js.map +1 -0
- package/lib/agent-cli-provider/adapters/gateway.d.ts +6 -0
- package/lib/agent-cli-provider/adapters/gateway.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/gateway.js +161 -0
- package/lib/agent-cli-provider/adapters/gateway.js.map +1 -0
- package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.js +9 -2
- package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/index.js +6 -34
- package/lib/agent-cli-provider/adapters/index.js.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +5 -1
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/adapters/pi.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/pi.js +331 -0
- package/lib/agent-cli-provider/adapters/pi.js.map +1 -0
- package/lib/agent-cli-provider/claude-command.d.ts +6 -0
- package/lib/agent-cli-provider/claude-command.d.ts.map +1 -0
- package/lib/agent-cli-provider/claude-command.js +43 -0
- package/lib/agent-cli-provider/claude-command.js.map +1 -0
- package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-actions.js +13 -17
- package/lib/agent-cli-provider/contract-actions.js.map +1 -1
- package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-invoke.js +10 -3
- package/lib/agent-cli-provider/contract-invoke.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +57 -2
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/contract-support.d.ts +1 -0
- package/lib/agent-cli-provider/contract-support.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-support.js +25 -3
- package/lib/agent-cli-provider/contract-support.js.map +1 -1
- package/lib/agent-cli-provider/errors.d.ts +1 -0
- package/lib/agent-cli-provider/errors.d.ts.map +1 -1
- package/lib/agent-cli-provider/errors.js +14 -5
- package/lib/agent-cli-provider/errors.js.map +1 -1
- package/lib/agent-cli-provider/gateway-client.d.ts +32 -0
- package/lib/agent-cli-provider/gateway-client.d.ts.map +1 -0
- package/lib/agent-cli-provider/gateway-client.js +135 -0
- package/lib/agent-cli-provider/gateway-client.js.map +1 -0
- package/lib/agent-cli-provider/gateway-runner.d.ts +9 -0
- package/lib/agent-cli-provider/gateway-runner.d.ts.map +1 -0
- package/lib/agent-cli-provider/gateway-runner.js +294 -0
- package/lib/agent-cli-provider/gateway-runner.js.map +1 -0
- package/lib/agent-cli-provider/gateway-tools.d.ts +14 -0
- package/lib/agent-cli-provider/gateway-tools.d.ts.map +1 -0
- package/lib/agent-cli-provider/gateway-tools.js +495 -0
- package/lib/agent-cli-provider/gateway-tools.js.map +1 -0
- package/lib/agent-cli-provider/index.d.ts +3 -2
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +14 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/invoke-evidence.d.ts +2 -2
- package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
- package/lib/agent-cli-provider/invoke-evidence.js +17 -3
- package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +437 -0
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -0
- package/lib/agent-cli-provider/provider-registry.js +411 -0
- package/lib/agent-cli-provider/provider-registry.js.map +1 -0
- package/lib/agent-cli-provider/single-agent-runtime.d.ts +7 -0
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +139 -19
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +106 -4
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/detached-startup.js +4 -2
- package/lib/docker-config.js +24 -13
- package/lib/provider-defaults.js +15 -4
- package/lib/provider-detection.js +2 -0
- package/lib/provider-names.js +32 -12
- package/lib/repo-settings.js +15 -1
- package/lib/run-mode.js +18 -6
- package/lib/run-plan.js +32 -0
- package/lib/settings/claude-auth.js +3 -6
- package/lib/settings.js +17 -2
- package/lib/setup-apply.js +300 -0
- package/lib/setup-journal.js +109 -0
- package/lib/setup-plan.js +406 -0
- package/lib/setup-undo.js +88 -0
- package/lib/start-cluster.js +34 -9
- package/lib/stream-json-parser.js +7 -5
- package/package.json +33 -3
- package/scripts/live-provider-smoke.js +221 -0
- package/src/agent/agent-lifecycle.js +7 -4
- package/src/agent/agent-quality-gate-schema.js +2 -2
- package/src/agent/agent-task-executor.js +41 -2
- package/src/agent-cli-provider/acp-stdio-runner.ts +314 -0
- package/src/agent-cli-provider/adapters/acp.ts +493 -0
- package/src/agent-cli-provider/adapters/claude.ts +2 -32
- package/src/agent-cli-provider/adapters/copilot-parser.ts +166 -0
- package/src/agent-cli-provider/adapters/copilot.ts +231 -0
- package/src/agent-cli-provider/adapters/gateway.ts +186 -0
- package/src/agent-cli-provider/adapters/gemini.ts +9 -2
- package/src/agent-cli-provider/adapters/index.ts +5 -36
- package/src/agent-cli-provider/adapters/opencode.ts +4 -1
- package/src/agent-cli-provider/adapters/pi.ts +410 -0
- package/src/agent-cli-provider/claude-command.ts +47 -0
- package/src/agent-cli-provider/contract-actions.ts +14 -17
- package/src/agent-cli-provider/contract-invoke.ts +12 -3
- package/src/agent-cli-provider/contract-options.ts +72 -3
- package/src/agent-cli-provider/contract-support.ts +30 -3
- package/src/agent-cli-provider/errors.ts +14 -4
- package/src/agent-cli-provider/gateway-client.ts +170 -0
- package/src/agent-cli-provider/gateway-runner.ts +353 -0
- package/src/agent-cli-provider/gateway-tools.ts +616 -0
- package/src/agent-cli-provider/index.ts +28 -0
- package/src/agent-cli-provider/invoke-evidence.ts +24 -3
- package/src/agent-cli-provider/provider-registry.ts +515 -0
- package/src/agent-cli-provider/single-agent-runtime.ts +192 -18
- package/src/agent-cli-provider/types.ts +145 -4
- package/src/config-validator.js +33 -15
- package/src/isolation-manager.js +124 -19
- package/src/ledger.js +3 -0
- package/src/message-bus.js +12 -2
- package/src/orchestrator.js +37 -17
- package/src/preflight.js +83 -30
- package/src/providers/capabilities.js +15 -42
- package/src/providers/index.js +52 -72
- package/src/worktree-claude-config.js +17 -0
- package/task-lib/commands/run.js +1 -0
- package/task-lib/provider-helper-runtime.js +11 -0
- package/task-lib/runner.js +7 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { contractError, invalidField } from './contract-errors';
|
|
5
|
+
import { getString, isRecord, unknownToMessage } from './json';
|
|
6
|
+
import type {
|
|
7
|
+
GatewayBuildOptions,
|
|
8
|
+
GatewayToolPolicy,
|
|
9
|
+
ResolvedGatewayBuildOptions,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 10_000;
|
|
13
|
+
|
|
14
|
+
export class GatewayPolicyError extends Error {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'GatewayPolicyError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GatewayToolExecutionResult {
|
|
22
|
+
readonly content: unknown;
|
|
23
|
+
readonly isError: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RunCommandRequest {
|
|
27
|
+
readonly command: string;
|
|
28
|
+
readonly args: readonly string[];
|
|
29
|
+
readonly cwd?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ApplyPatchRequest {
|
|
33
|
+
readonly path: string;
|
|
34
|
+
readonly content?: string | undefined;
|
|
35
|
+
readonly search?: string | undefined;
|
|
36
|
+
readonly replace?: string | undefined;
|
|
37
|
+
readonly replaceAll: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeGatewayBuildOptions(
|
|
41
|
+
value: unknown,
|
|
42
|
+
field: string,
|
|
43
|
+
cwd: string
|
|
44
|
+
): GatewayBuildOptions | undefined {
|
|
45
|
+
if (value === undefined) return undefined;
|
|
46
|
+
if (!isRecord(value)) {
|
|
47
|
+
invalidField(field, `${field} must be an object.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result: Record<string, unknown> = {};
|
|
51
|
+
const baseUrl = optionalString(value.baseUrl, `${field}.baseUrl`);
|
|
52
|
+
const apiKey = optionalString(value.apiKey, `${field}.apiKey`);
|
|
53
|
+
const model = optionalNullableString(value.model, `${field}.model`);
|
|
54
|
+
const headers = optionalStringRecord(value.headers, `${field}.headers`);
|
|
55
|
+
const toolPolicy = optionalGatewayToolPolicy(value.toolPolicy, `${field}.toolPolicy`, cwd);
|
|
56
|
+
|
|
57
|
+
if (baseUrl !== undefined) result.baseUrl = baseUrl;
|
|
58
|
+
if (typeof apiKey === 'string') result.apiKey = apiKey;
|
|
59
|
+
if (model !== undefined) result.model = model;
|
|
60
|
+
if (headers !== undefined) result.headers = headers;
|
|
61
|
+
if (toolPolicy !== undefined) result.toolPolicy = toolPolicy;
|
|
62
|
+
|
|
63
|
+
return result as GatewayBuildOptions;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveGatewayConfiguration(
|
|
67
|
+
value: GatewayBuildOptions | undefined,
|
|
68
|
+
field: string,
|
|
69
|
+
cwd: string
|
|
70
|
+
): ResolvedGatewayBuildOptions {
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
throw contractError({
|
|
73
|
+
code: 'invalid-field',
|
|
74
|
+
field,
|
|
75
|
+
exitCode: 2,
|
|
76
|
+
message: `${field} is required for the gateway provider.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const baseUrl = requiredNonEmptyString(value.baseUrl, `${field}.baseUrl`);
|
|
80
|
+
const apiKey = requiredNonEmptyString(value.apiKey, `${field}.apiKey`);
|
|
81
|
+
const model = requiredNonEmptyString(value.model, `${field}.model`);
|
|
82
|
+
const headers = value.headers ?? {};
|
|
83
|
+
const toolPolicy = requiredGatewayToolPolicy(value.toolPolicy, `${field}.toolPolicy`, cwd);
|
|
84
|
+
|
|
85
|
+
assertValidGatewayBaseUrl(baseUrl, `${field}.baseUrl`);
|
|
86
|
+
return {
|
|
87
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
88
|
+
apiKey,
|
|
89
|
+
headers,
|
|
90
|
+
model,
|
|
91
|
+
toolPolicy,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateGatewaySettings(settings: Record<string, unknown>): string | null {
|
|
96
|
+
try {
|
|
97
|
+
optionalString(settings.baseUrl, 'providerSettings.gateway.baseUrl');
|
|
98
|
+
optionalString(settings.apiKey, 'providerSettings.gateway.apiKey');
|
|
99
|
+
optionalNullableString(settings.model, 'providerSettings.gateway.model');
|
|
100
|
+
optionalStringRecord(settings.headers, 'providerSettings.gateway.headers');
|
|
101
|
+
optionalGatewayToolPolicy(
|
|
102
|
+
settings.toolPolicy,
|
|
103
|
+
'providerSettings.gateway.toolPolicy',
|
|
104
|
+
process.cwd()
|
|
105
|
+
);
|
|
106
|
+
return null;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return unknownToMessage(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function normalizeGatewayToolPolicy(
|
|
113
|
+
value: GatewayToolPolicy,
|
|
114
|
+
field: string,
|
|
115
|
+
cwd: string
|
|
116
|
+
): GatewayToolPolicy {
|
|
117
|
+
const roots = normalizeGatewayRoots(value.roots, `${field}.roots`, cwd);
|
|
118
|
+
const commands = normalizeGatewayCommands(value.commands, `${field}.commands`);
|
|
119
|
+
const timeoutMs = optionalFiniteInteger(value.commandTimeoutMs, `${field}.commandTimeoutMs`);
|
|
120
|
+
return timeoutMs === undefined
|
|
121
|
+
? { roots, commands }
|
|
122
|
+
: { roots, commands, commandTimeoutMs: timeoutMs };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function executeGatewayToolCall(
|
|
126
|
+
toolName: string,
|
|
127
|
+
input: unknown,
|
|
128
|
+
policy: GatewayToolPolicy
|
|
129
|
+
): Promise<GatewayToolExecutionResult> {
|
|
130
|
+
switch (toolName) {
|
|
131
|
+
case 'read_file':
|
|
132
|
+
return { content: await readGatewayFile(input, policy), isError: false };
|
|
133
|
+
case 'apply_patch':
|
|
134
|
+
return { content: await applyGatewayPatch(input, policy), isError: false };
|
|
135
|
+
case 'run_command':
|
|
136
|
+
return runGatewayCommand(input, policy);
|
|
137
|
+
default:
|
|
138
|
+
throw new GatewayPolicyError(`Unsupported gateway tool: ${toolName}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function optionalGatewayToolPolicy(
|
|
143
|
+
value: unknown,
|
|
144
|
+
field: string,
|
|
145
|
+
cwd: string
|
|
146
|
+
): GatewayToolPolicy | undefined {
|
|
147
|
+
if (value === undefined || value === null) return undefined;
|
|
148
|
+
if (!isRecord(value)) {
|
|
149
|
+
invalidField(field, `${field} must be an object.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return normalizeGatewayToolPolicy(
|
|
153
|
+
{
|
|
154
|
+
roots: stringArray(value.roots, `${field}.roots`),
|
|
155
|
+
commands: stringArray(value.commands, `${field}.commands`),
|
|
156
|
+
...(value.commandTimeoutMs === undefined
|
|
157
|
+
? {}
|
|
158
|
+
: {
|
|
159
|
+
commandTimeoutMs: requiredFiniteInteger(
|
|
160
|
+
value.commandTimeoutMs,
|
|
161
|
+
`${field}.commandTimeoutMs`
|
|
162
|
+
),
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
field,
|
|
166
|
+
cwd
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function requiredGatewayToolPolicy(
|
|
171
|
+
value: GatewayToolPolicy | undefined,
|
|
172
|
+
field: string,
|
|
173
|
+
cwd: string
|
|
174
|
+
): GatewayToolPolicy {
|
|
175
|
+
if (value === undefined) {
|
|
176
|
+
throw contractError({
|
|
177
|
+
code: 'invalid-field',
|
|
178
|
+
field,
|
|
179
|
+
exitCode: 2,
|
|
180
|
+
message: `${field} is required for the gateway provider.`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return normalizeGatewayToolPolicy(value, field, cwd);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeGatewayRoots(value: readonly string[], field: string, cwd: string): readonly string[] {
|
|
187
|
+
if (value.length === 0) {
|
|
188
|
+
invalidField(field, `${field} must contain at least one root path.`);
|
|
189
|
+
}
|
|
190
|
+
return value.map((root, index) => {
|
|
191
|
+
const normalized = root.trim();
|
|
192
|
+
if (!normalized) {
|
|
193
|
+
invalidField(`${field}[${index}]`, `${field}[${index}] must be a non-empty string.`);
|
|
194
|
+
}
|
|
195
|
+
return path.resolve(cwd, normalized);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizeGatewayCommands(value: readonly string[], field: string): readonly string[] {
|
|
200
|
+
return value.map((command, index) => {
|
|
201
|
+
const normalized = command.trim();
|
|
202
|
+
if (!normalized) {
|
|
203
|
+
invalidField(`${field}[${index}]`, `${field}[${index}] must be a non-empty string.`);
|
|
204
|
+
}
|
|
205
|
+
if (/\s/.test(normalized)) {
|
|
206
|
+
invalidField(`${field}[${index}]`, `${field}[${index}] must not contain whitespace.`);
|
|
207
|
+
}
|
|
208
|
+
return normalized;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function assertWithinRoots(
|
|
213
|
+
targetPath: string,
|
|
214
|
+
roots: readonly string[],
|
|
215
|
+
field: string,
|
|
216
|
+
options: { readonly allowMissingLeaf?: boolean } = {}
|
|
217
|
+
): Promise<string> {
|
|
218
|
+
const resolvedRoots = await resolveGatewayRoots(roots);
|
|
219
|
+
if (resolvedRoots.length === 0) {
|
|
220
|
+
throw new GatewayPolicyError('toolPolicy.roots must include at least one root.');
|
|
221
|
+
}
|
|
222
|
+
const candidates = getGatewayTargetCandidates(targetPath, resolvedRoots);
|
|
223
|
+
const firstExistingMatch = await resolveFirstMatchingGatewayTarget(candidates, resolvedRoots, {
|
|
224
|
+
throwOnMissing: !options.allowMissingLeaf,
|
|
225
|
+
});
|
|
226
|
+
if (firstExistingMatch !== undefined) return firstExistingMatch;
|
|
227
|
+
if (options.allowMissingLeaf) {
|
|
228
|
+
const missingLeafMatch = await resolveFirstMatchingGatewayTarget(candidates, resolvedRoots, {
|
|
229
|
+
allowMissingLeaf: true,
|
|
230
|
+
});
|
|
231
|
+
if (missingLeafMatch !== undefined) return missingLeafMatch;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
throw new GatewayPolicyError(`${field} must stay within toolPolicy.roots.`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface ResolvedGatewayRoot {
|
|
238
|
+
readonly configuredPath: string;
|
|
239
|
+
readonly realPath: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function resolveGatewayRoots(roots: readonly string[]): Promise<readonly ResolvedGatewayRoot[]> {
|
|
243
|
+
return Promise.all(
|
|
244
|
+
roots.map(async (configuredPath) => ({
|
|
245
|
+
configuredPath,
|
|
246
|
+
realPath: await realpath(configuredPath),
|
|
247
|
+
}))
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getGatewayTargetCandidates(
|
|
252
|
+
targetPath: string,
|
|
253
|
+
roots: readonly ResolvedGatewayRoot[]
|
|
254
|
+
): readonly string[] {
|
|
255
|
+
if (path.isAbsolute(targetPath)) {
|
|
256
|
+
return [path.resolve(targetPath)];
|
|
257
|
+
}
|
|
258
|
+
return roots.map((root) => path.resolve(root.configuredPath, targetPath));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function resolveFirstMatchingGatewayTarget(
|
|
262
|
+
candidates: readonly string[],
|
|
263
|
+
resolvedRoots: readonly ResolvedGatewayRoot[],
|
|
264
|
+
options: { readonly allowMissingLeaf?: boolean; readonly throwOnMissing?: boolean } = {}
|
|
265
|
+
): Promise<string | undefined> {
|
|
266
|
+
let firstMissingError: NodeJS.ErrnoException | undefined;
|
|
267
|
+
|
|
268
|
+
for (const candidate of candidates) {
|
|
269
|
+
let resolvedTarget: string;
|
|
270
|
+
try {
|
|
271
|
+
resolvedTarget = options.allowMissingLeaf
|
|
272
|
+
? await resolveGatewayTargetPath(candidate)
|
|
273
|
+
: await realpath(candidate);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (isNodeErrorWithCode(error, 'ENOENT')) {
|
|
276
|
+
firstMissingError ??= error;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (resolvedRoots.some((root) => isWithinRoot(resolvedTarget, root.realPath))) {
|
|
283
|
+
return resolvedTarget;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (firstMissingError !== undefined && options.throwOnMissing) {
|
|
288
|
+
throw firstMissingError;
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function resolveGatewayTargetPath(candidatePath: string): Promise<string> {
|
|
294
|
+
try {
|
|
295
|
+
return await realpath(candidatePath);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (!isNodeErrorWithCode(error, 'ENOENT')) throw error;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const missingSegments: string[] = [];
|
|
301
|
+
let current = candidatePath;
|
|
302
|
+
while (true) {
|
|
303
|
+
const parent = path.dirname(current);
|
|
304
|
+
if (parent === current) {
|
|
305
|
+
throw new GatewayPolicyError(`Unable to resolve ${candidatePath} within toolPolicy.roots.`);
|
|
306
|
+
}
|
|
307
|
+
missingSegments.unshift(path.basename(current));
|
|
308
|
+
current = parent;
|
|
309
|
+
try {
|
|
310
|
+
const realCurrent = await realpath(current);
|
|
311
|
+
return path.join(realCurrent, ...missingSegments);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (!isNodeErrorWithCode(error, 'ENOENT')) throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isWithinRoot(targetPath: string, rootPath: string): boolean {
|
|
319
|
+
const relative = path.relative(rootPath, targetPath);
|
|
320
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isNodeErrorWithCode(error: unknown, code: string): error is NodeJS.ErrnoException {
|
|
324
|
+
return isRecord(error) && error.code === code;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function readGatewayFileInput(input: unknown): string {
|
|
328
|
+
if (!isRecord(input)) {
|
|
329
|
+
throw new Error('read_file input must be an object.');
|
|
330
|
+
}
|
|
331
|
+
const targetPath = getString(input, 'path');
|
|
332
|
+
if (!targetPath || !targetPath.trim()) {
|
|
333
|
+
throw new Error('read_file.path must be a non-empty string.');
|
|
334
|
+
}
|
|
335
|
+
return targetPath;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function readGatewayFile(
|
|
339
|
+
input: unknown,
|
|
340
|
+
policy: GatewayToolPolicy
|
|
341
|
+
): Promise<Record<string, unknown>> {
|
|
342
|
+
const targetPath = await assertWithinRoots(readGatewayFileInput(input), policy.roots, 'read_file.path');
|
|
343
|
+
const content = await readFile(targetPath, 'utf8');
|
|
344
|
+
return { path: targetPath, content };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function readApplyPatchInput(input: unknown): ApplyPatchRequest {
|
|
348
|
+
if (!isRecord(input)) {
|
|
349
|
+
throw new Error('apply_patch input must be an object.');
|
|
350
|
+
}
|
|
351
|
+
const targetPath = getString(input, 'path');
|
|
352
|
+
if (!targetPath || !targetPath.trim()) {
|
|
353
|
+
throw new Error('apply_patch.path must be a non-empty string.');
|
|
354
|
+
}
|
|
355
|
+
const content = optionalRuntimeString(input.content, 'apply_patch.content');
|
|
356
|
+
const search = optionalRuntimeString(input.search, 'apply_patch.search');
|
|
357
|
+
const replace = optionalRuntimeString(input.replace, 'apply_patch.replace');
|
|
358
|
+
const replaceAll = optionalBoolean(input.replaceAll, 'apply_patch.replaceAll') ?? false;
|
|
359
|
+
if (content === undefined) {
|
|
360
|
+
if (search === undefined || replace === undefined) {
|
|
361
|
+
throw new Error('apply_patch requires either content or both search and replace.');
|
|
362
|
+
}
|
|
363
|
+
if (search.length === 0) {
|
|
364
|
+
throw new Error('apply_patch.search must be a non-empty string.');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { path: targetPath, content, search, replace, replaceAll };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function applyGatewayPatch(
|
|
371
|
+
input: unknown,
|
|
372
|
+
policy: GatewayToolPolicy
|
|
373
|
+
): Promise<Record<string, unknown>> {
|
|
374
|
+
const request = readApplyPatchInput(input);
|
|
375
|
+
const targetPath = await assertWithinRoots(request.path, policy.roots, 'apply_patch.path', {
|
|
376
|
+
allowMissingLeaf: true,
|
|
377
|
+
});
|
|
378
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
379
|
+
|
|
380
|
+
if (request.content !== undefined) {
|
|
381
|
+
await writeFile(targetPath, request.content, 'utf8');
|
|
382
|
+
return { path: targetPath, bytesWritten: Buffer.byteLength(request.content, 'utf8') };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const current = await readFile(targetPath, 'utf8');
|
|
386
|
+
const search = request.search;
|
|
387
|
+
const replace = request.replace;
|
|
388
|
+
if (search === undefined || replace === undefined) {
|
|
389
|
+
throw new Error('apply_patch requires either content or both search and replace.');
|
|
390
|
+
}
|
|
391
|
+
if (!current.includes(search)) {
|
|
392
|
+
throw new Error('apply_patch.search did not match the target file.');
|
|
393
|
+
}
|
|
394
|
+
const next = request.replaceAll ? current.split(search).join(replace) : current.replace(search, replace);
|
|
395
|
+
await writeFile(targetPath, next, 'utf8');
|
|
396
|
+
return {
|
|
397
|
+
path: targetPath,
|
|
398
|
+
bytesWritten: Buffer.byteLength(next, 'utf8'),
|
|
399
|
+
replaced: request.replaceAll ? 'all' : 'first',
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function readRunCommandInput(input: unknown): RunCommandRequest {
|
|
404
|
+
if (!isRecord(input)) {
|
|
405
|
+
throw new Error('run_command input must be an object.');
|
|
406
|
+
}
|
|
407
|
+
const command = getString(input, 'command');
|
|
408
|
+
if (!command || !command.trim()) {
|
|
409
|
+
throw new Error('run_command.command must be a non-empty string.');
|
|
410
|
+
}
|
|
411
|
+
if (/\s/.test(command)) {
|
|
412
|
+
throw new GatewayPolicyError('run_command.command must not contain whitespace.');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const rawArgs = requiredArrayIfPresent(input, 'args', 'run_command.args');
|
|
416
|
+
const args = rawArgs.map((item, index) => {
|
|
417
|
+
if (typeof item !== 'string') {
|
|
418
|
+
throw new Error(`run_command.args[${index}] must be a string.`);
|
|
419
|
+
}
|
|
420
|
+
return item;
|
|
421
|
+
});
|
|
422
|
+
const cwd = optionalRuntimeString(input.cwd, 'run_command.cwd');
|
|
423
|
+
return { command, args, cwd };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function runGatewayCommand(
|
|
427
|
+
input: unknown,
|
|
428
|
+
policy: GatewayToolPolicy
|
|
429
|
+
): Promise<GatewayToolExecutionResult> {
|
|
430
|
+
const request = readRunCommandInput(input);
|
|
431
|
+
if (!policy.commands.includes(request.command)) {
|
|
432
|
+
throw new GatewayPolicyError(
|
|
433
|
+
`run_command.command "${request.command}" is not allowlisted by toolPolicy.commands.`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const timeoutMs = policy.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
438
|
+
const result = request.cwd
|
|
439
|
+
? await spawnGatewayCommand(
|
|
440
|
+
request.command,
|
|
441
|
+
request.args,
|
|
442
|
+
await assertWithinRoots(request.cwd, policy.roots, 'run_command.cwd'),
|
|
443
|
+
timeoutMs
|
|
444
|
+
)
|
|
445
|
+
: await spawnGatewayCommand(
|
|
446
|
+
request.command,
|
|
447
|
+
request.args,
|
|
448
|
+
await resolveDefaultGatewayCommandCwd(policy.roots),
|
|
449
|
+
timeoutMs
|
|
450
|
+
);
|
|
451
|
+
return {
|
|
452
|
+
isError: result.exitCode !== 0 || result.signal !== null,
|
|
453
|
+
content: result,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function resolveDefaultGatewayCommandCwd(roots: readonly string[]): Promise<string> {
|
|
458
|
+
const resolvedRoots = await resolveGatewayRoots(roots);
|
|
459
|
+
if (resolvedRoots.length === 0) {
|
|
460
|
+
throw new GatewayPolicyError('toolPolicy.roots must include at least one root.');
|
|
461
|
+
}
|
|
462
|
+
const [firstRoot] = resolvedRoots;
|
|
463
|
+
if (!firstRoot) {
|
|
464
|
+
throw new GatewayPolicyError('toolPolicy.roots must include at least one root.');
|
|
465
|
+
}
|
|
466
|
+
return firstRoot.realPath;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function spawnGatewayCommand(
|
|
470
|
+
command: string,
|
|
471
|
+
args: readonly string[],
|
|
472
|
+
cwd: string,
|
|
473
|
+
timeoutMs: number
|
|
474
|
+
): Promise<Record<string, unknown>> {
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
const child = spawn(command, [...args], {
|
|
477
|
+
cwd,
|
|
478
|
+
env: {
|
|
479
|
+
PATH: process.env.PATH ?? '',
|
|
480
|
+
HOME: process.env.HOME ?? '',
|
|
481
|
+
},
|
|
482
|
+
shell: false,
|
|
483
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
484
|
+
});
|
|
485
|
+
const stdout: Buffer[] = [];
|
|
486
|
+
const stderr: Buffer[] = [];
|
|
487
|
+
let timedOut = false;
|
|
488
|
+
const timeout = setTimeout(() => {
|
|
489
|
+
timedOut = true;
|
|
490
|
+
child.kill('SIGKILL');
|
|
491
|
+
}, timeoutMs);
|
|
492
|
+
|
|
493
|
+
child.stdout?.on('data', (chunk: Buffer) => stdout.push(chunk));
|
|
494
|
+
child.stderr?.on('data', (chunk: Buffer) => stderr.push(chunk));
|
|
495
|
+
child.once('error', (error) => {
|
|
496
|
+
clearTimeout(timeout);
|
|
497
|
+
reject(error);
|
|
498
|
+
});
|
|
499
|
+
child.once('close', (exitCode, signal) => {
|
|
500
|
+
clearTimeout(timeout);
|
|
501
|
+
resolve({
|
|
502
|
+
command,
|
|
503
|
+
args,
|
|
504
|
+
cwd,
|
|
505
|
+
exitCode,
|
|
506
|
+
signal,
|
|
507
|
+
timedOut,
|
|
508
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
509
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function requiredNonEmptyString(value: string | null | undefined, field: string): string {
|
|
516
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
517
|
+
throw contractError({
|
|
518
|
+
code: 'invalid-field',
|
|
519
|
+
field,
|
|
520
|
+
exitCode: 2,
|
|
521
|
+
message: `${field} must be a non-empty string.`,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function optionalString(value: unknown, field: string): string | undefined {
|
|
526
|
+
if (value === undefined || value === null) return undefined;
|
|
527
|
+
if (typeof value === 'string') return value;
|
|
528
|
+
invalidField(field, `${field} must be a string.`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function optionalBoolean(value: unknown, field: string): boolean | undefined {
|
|
532
|
+
if (value === undefined) return undefined;
|
|
533
|
+
if (typeof value === 'boolean') return value;
|
|
534
|
+
throw new Error(`${field} must be a boolean.`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function optionalRuntimeString(value: unknown, field: string): string | undefined {
|
|
538
|
+
if (value === undefined) return undefined;
|
|
539
|
+
if (typeof value === 'string') return value;
|
|
540
|
+
throw new Error(`${field} must be a string.`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function optionalNullableString(value: unknown, field: string): string | null | undefined {
|
|
544
|
+
if (value === undefined) return undefined;
|
|
545
|
+
if (value === null) return null;
|
|
546
|
+
if (typeof value === 'string') return value;
|
|
547
|
+
invalidField(field, `${field} must be a string or null.`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function optionalStringRecord(
|
|
551
|
+
value: unknown,
|
|
552
|
+
field: string
|
|
553
|
+
): Readonly<Record<string, string>> | undefined {
|
|
554
|
+
if (value === undefined || value === null) return undefined;
|
|
555
|
+
if (!isRecord(value)) {
|
|
556
|
+
invalidField(field, `${field} must be an object with string values.`);
|
|
557
|
+
}
|
|
558
|
+
const result: Record<string, string> = {};
|
|
559
|
+
for (const [key, item] of Object.entries(value)) {
|
|
560
|
+
if (typeof item !== 'string') {
|
|
561
|
+
invalidField(`${field}.${key}`, `${field}.${key} must be a string.`);
|
|
562
|
+
}
|
|
563
|
+
result[key] = item;
|
|
564
|
+
}
|
|
565
|
+
return result;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function stringArray(value: unknown, field: string): readonly string[] {
|
|
569
|
+
if (!Array.isArray(value)) {
|
|
570
|
+
invalidField(field, `${field} must be an array of strings.`);
|
|
571
|
+
}
|
|
572
|
+
return value.map((item, index) => {
|
|
573
|
+
if (typeof item !== 'string') {
|
|
574
|
+
invalidField(`${field}[${index}]`, `${field}[${index}] must be a string.`);
|
|
575
|
+
}
|
|
576
|
+
return item;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function requiredArrayIfPresent(
|
|
581
|
+
record: Record<string, unknown>,
|
|
582
|
+
key: string,
|
|
583
|
+
field: string
|
|
584
|
+
): readonly unknown[] {
|
|
585
|
+
const value = record[key];
|
|
586
|
+
if (value === undefined) return [];
|
|
587
|
+
if (!Array.isArray(value)) {
|
|
588
|
+
throw new Error(`${field} must be an array.`);
|
|
589
|
+
}
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function optionalFiniteInteger(value: unknown, field: string): number | undefined {
|
|
594
|
+
if (value === undefined) return undefined;
|
|
595
|
+
return requiredFiniteInteger(value, field);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function requiredFiniteInteger(value: unknown, field: string): number {
|
|
599
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value;
|
|
600
|
+
invalidField(field, `${field} must be a positive integer.`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function assertValidGatewayBaseUrl(value: string, field: string): void {
|
|
604
|
+
try {
|
|
605
|
+
const parsed = new URL(value);
|
|
606
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
607
|
+
invalidField(field, `${field} must use http or https.`);
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
invalidField(field, `${field} must be a valid URL.`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function normalizeBaseUrl(value: string): string {
|
|
615
|
+
return value.replace(/\/+$/, '');
|
|
616
|
+
}
|
|
@@ -35,20 +35,40 @@ export {
|
|
|
35
35
|
export {
|
|
36
36
|
detectRuntimeProviderCliFeatures,
|
|
37
37
|
prepareSingleAgentProviderCommand,
|
|
38
|
+
probeRuntimeProviderCli,
|
|
38
39
|
type PreparedSingleAgentProviderCommand,
|
|
40
|
+
type RuntimeProviderProbe,
|
|
39
41
|
type SingleAgentProviderCommandInput,
|
|
40
42
|
} from './single-agent-runtime';
|
|
43
|
+
export {
|
|
44
|
+
findProviderRegistryEntry,
|
|
45
|
+
getProviderRegistryEntry,
|
|
46
|
+
knownProviderNames,
|
|
47
|
+
listProviderRegistryEntries,
|
|
48
|
+
normalizeProviderName,
|
|
49
|
+
providerAliasMap,
|
|
50
|
+
providerAliases,
|
|
51
|
+
providerIds,
|
|
52
|
+
providerRegistry,
|
|
53
|
+
resolveProviderCommand,
|
|
54
|
+
supportsProviderCapability,
|
|
55
|
+
} from './provider-registry';
|
|
41
56
|
|
|
42
57
|
export type {
|
|
43
58
|
AgentCliProviderHelperMetadata,
|
|
44
59
|
BuildProviderCommandOptions,
|
|
45
60
|
CleanupMetadata,
|
|
46
61
|
CliFeatureOverrides,
|
|
62
|
+
AcpCliFeatures,
|
|
47
63
|
ClaudeCliFeatures,
|
|
48
64
|
CodexCliFeatures,
|
|
65
|
+
CopilotCliFeatures,
|
|
49
66
|
CommandSpec,
|
|
50
67
|
ErrorClassification,
|
|
51
68
|
ErrorClassificationKind,
|
|
69
|
+
GatewayBuildOptions,
|
|
70
|
+
GatewayCliFeatures,
|
|
71
|
+
GatewayToolPolicy,
|
|
52
72
|
GeminiCliFeatures,
|
|
53
73
|
KnownProviderName,
|
|
54
74
|
LevelModelSpec,
|
|
@@ -59,11 +79,19 @@ export type {
|
|
|
59
79
|
OpencodeCliFeatures,
|
|
60
80
|
OutputEvent,
|
|
61
81
|
OutputFormat,
|
|
82
|
+
PiCliFeatures,
|
|
62
83
|
ProviderAdapter,
|
|
84
|
+
ProviderCapabilities,
|
|
85
|
+
ProviderCapabilityState,
|
|
86
|
+
ProviderCommandSpec,
|
|
87
|
+
ProviderDocsMetadata,
|
|
88
|
+
ProviderInvokeSpec,
|
|
63
89
|
ProviderAlias,
|
|
64
90
|
ProviderCliFeatures,
|
|
65
91
|
ProviderId,
|
|
92
|
+
ProviderRegistryEntry,
|
|
66
93
|
RedactionMetadata,
|
|
94
|
+
ResolvedGatewayBuildOptions,
|
|
67
95
|
ResolvedModelSpec,
|
|
68
96
|
ResultEvent,
|
|
69
97
|
TextEvent,
|