@the-open-engine/zeroshot 6.36.0 → 6.38.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/index.js +99 -29
- package/cli/lib/setup-scanner-worker.js +2 -9
- package/lib/agent-cli-provider/adapters/claude-recovery.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude-recovery.js +17 -3
- package/lib/agent-cli-provider/adapters/claude-recovery.js.map +1 -1
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/index.js +7 -0
- package/lib/agent-cli-provider/adapters/index.js.map +1 -1
- package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/pi.js +86 -97
- package/lib/agent-cli-provider/adapters/pi.js.map +1 -1
- package/lib/agent-cli-provider/contract-parse.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-parse.js +5 -1
- package/lib/agent-cli-provider/contract-parse.js.map +1 -1
- package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
- package/lib/agent-cli-provider/invoke-evidence.js +8 -1
- package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
- package/lib/agent-cli-provider/pi/credentials.d.ts +2 -0
- package/lib/agent-cli-provider/pi/credentials.d.ts.map +1 -0
- package/lib/agent-cli-provider/pi/credentials.js +51 -0
- package/lib/agent-cli-provider/pi/credentials.js.map +1 -0
- package/lib/agent-cli-provider/pi/events.d.ts +5 -0
- package/lib/agent-cli-provider/pi/events.d.ts.map +1 -0
- package/lib/agent-cli-provider/pi/events.js +124 -0
- package/lib/agent-cli-provider/pi/events.js.map +1 -0
- package/lib/agent-cli-provider/pi/protocol-state.d.ts +6 -0
- package/lib/agent-cli-provider/pi/protocol-state.d.ts.map +1 -0
- package/lib/agent-cli-provider/pi/protocol-state.js +138 -0
- package/lib/agent-cli-provider/pi/protocol-state.js.map +1 -0
- package/lib/agent-cli-provider/pi/release.d.ts +4 -0
- package/lib/agent-cli-provider/pi/release.d.ts.map +1 -0
- package/lib/agent-cli-provider/pi/release.js +7 -0
- package/lib/agent-cli-provider/pi/release.js.map +1 -0
- package/lib/agent-cli-provider/probe-availability.d.ts +5 -0
- package/lib/agent-cli-provider/probe-availability.d.ts.map +1 -0
- package/lib/agent-cli-provider/probe-availability.js +11 -0
- package/lib/agent-cli-provider/probe-availability.js.map +1 -0
- package/lib/agent-cli-provider/provider-registry.d.ts +13 -9
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +42 -8
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +15 -5
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +3 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster-worker/engine-adapter.js +9 -1
- package/lib/docker-config.js +6 -1
- package/lib/provider-credential-path.js +36 -0
- package/lib/start-cluster.js +8 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +25 -76
- package/src/agent/agent-liveness-poll.js +93 -0
- package/src/agent/agent-task-executor.js +98 -23
- package/src/agent/output-extraction.js +115 -0
- package/src/agent/pi-terminal-lifecycle.js +242 -0
- package/src/agent/provider-control-plane.js +26 -0
- package/src/agent/provider-terminal-failure.js +49 -24
- package/src/agent-cli-provider/adapters/claude-recovery.ts +15 -2
- package/src/agent-cli-provider/adapters/index.ts +6 -0
- package/src/agent-cli-provider/adapters/pi.ts +105 -125
- package/src/agent-cli-provider/contract-parse.ts +8 -1
- package/src/agent-cli-provider/invoke-evidence.ts +11 -1
- package/src/agent-cli-provider/pi/credentials.ts +47 -0
- package/src/agent-cli-provider/pi/events.ts +148 -0
- package/src/agent-cli-provider/pi/protocol-state.ts +163 -0
- package/src/agent-cli-provider/pi/release.ts +4 -0
- package/src/agent-cli-provider/probe-availability.ts +19 -0
- package/src/agent-cli-provider/provider-registry.ts +48 -9
- package/src/agent-cli-provider/single-agent-runtime.ts +27 -6
- package/src/agent-cli-provider/types.ts +3 -0
- package/src/config-validator.js +1 -3
- package/src/foreground-benchmark-files.js +81 -0
- package/src/foreground-benchmark-result.js +233 -0
- package/src/foreground-benchmark-run.js +72 -0
- package/src/isolation-manager.js +43 -29
- package/src/orchestrator.js +33 -1
- package/task-lib/runner.js +7 -0
- package/task-lib/watcher-output-runtime.js +16 -5
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const { VALID_PROVIDERS } = require('../lib/provider-names');
|
|
3
|
+
|
|
4
|
+
const RESULT_SCHEMA = 'zeroshot-benchmark-result/v1';
|
|
5
|
+
const TELEMETRY_SCHEMA = 'zeroshot-benchmark-telemetry/v1';
|
|
6
|
+
const EMPTY_DIAGNOSTIC = Object.freeze({
|
|
7
|
+
byteLength: 0,
|
|
8
|
+
sha256: crypto.createHash('sha256').update('').digest('hex'),
|
|
9
|
+
});
|
|
10
|
+
const TASK_FAILURE_REASONS = new Set(['max_iterations', 'structured_output_invalid']);
|
|
11
|
+
const PROVIDER_CODES = new Set(['crash', 'refusal']);
|
|
12
|
+
const PROVIDERS = new Set(VALID_PROVIDERS);
|
|
13
|
+
const PROVIDER_EVENTS = new Set(['terminal_error', 'turn.failed']);
|
|
14
|
+
const PROVIDER_CATEGORIES = new Set([
|
|
15
|
+
'authentication',
|
|
16
|
+
'permanent',
|
|
17
|
+
'quota',
|
|
18
|
+
'transient',
|
|
19
|
+
'unknown',
|
|
20
|
+
]);
|
|
21
|
+
const PROVIDER_KINDS = new Set([
|
|
22
|
+
'permanent-pattern',
|
|
23
|
+
'rate-limit',
|
|
24
|
+
'retryable-pattern',
|
|
25
|
+
'status-permanent',
|
|
26
|
+
'status-retryable',
|
|
27
|
+
'code-retryable',
|
|
28
|
+
'unknown-retryable',
|
|
29
|
+
]);
|
|
30
|
+
const TOKEN_FIELDS = [
|
|
31
|
+
'inputTokens',
|
|
32
|
+
'outputTokens',
|
|
33
|
+
'cacheReadInputTokens',
|
|
34
|
+
'cacheCreationInputTokens',
|
|
35
|
+
'totalCostUsd',
|
|
36
|
+
'count',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function requireObject(value, label) {
|
|
40
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
41
|
+
throw new Error(`${label} must be an object`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireClosedText(value, allowed, label) {
|
|
47
|
+
if (typeof value !== 'string' || !allowed.has(value)) {
|
|
48
|
+
throw new Error(`${label} is outside the closed result contract`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requireDiagnostic(value) {
|
|
54
|
+
if (
|
|
55
|
+
value &&
|
|
56
|
+
Number.isSafeInteger(value.byteLength) &&
|
|
57
|
+
value.byteLength >= 0 &&
|
|
58
|
+
typeof value.sha256 === 'string' &&
|
|
59
|
+
/^[a-f0-9]{64}$/.test(value.sha256)
|
|
60
|
+
) {
|
|
61
|
+
return { byteLength: value.byteLength, sha256: value.sha256 };
|
|
62
|
+
}
|
|
63
|
+
throw new Error('provider diagnostic is outside the closed result contract');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function terminalData(message) {
|
|
67
|
+
return requireObject(requireObject(message.content, 'terminal content').data, 'terminal data');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isExplicitTaskFailure(message, data) {
|
|
71
|
+
if (message.sender === 'orchestrator' || !TASK_FAILURE_REASONS.has(data.reason)) return false;
|
|
72
|
+
if (data.reason === 'max_iterations') return message.receiver === 'system';
|
|
73
|
+
return message.receiver === 'broadcast' && data.code === 'STRUCTURED_OUTPUT_INVALID';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function classifyTerminal(message) {
|
|
77
|
+
if (message.topic === 'CLUSTER_COMPLETE') {
|
|
78
|
+
return {
|
|
79
|
+
outcome: 'completed',
|
|
80
|
+
terminalOwner: 'task',
|
|
81
|
+
code: 'ok',
|
|
82
|
+
kind: 'workflow_complete',
|
|
83
|
+
retryable: false,
|
|
84
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
85
|
+
provider: null,
|
|
86
|
+
event: null,
|
|
87
|
+
category: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (message.topic !== 'CLUSTER_FAILED') {
|
|
92
|
+
throw new Error(`unsupported terminal topic: ${message.topic}`);
|
|
93
|
+
}
|
|
94
|
+
const data = terminalData(message);
|
|
95
|
+
if (isExplicitTaskFailure(message, data)) {
|
|
96
|
+
return {
|
|
97
|
+
outcome: 'task_failure',
|
|
98
|
+
terminalOwner: 'task',
|
|
99
|
+
code: data.reason,
|
|
100
|
+
kind: 'declared_failure',
|
|
101
|
+
retryable: false,
|
|
102
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
103
|
+
provider: null,
|
|
104
|
+
event: null,
|
|
105
|
+
category: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (data.reason === 'provider_execution_failed') {
|
|
109
|
+
if (typeof data.retryable !== 'boolean') {
|
|
110
|
+
throw new Error('provider retryable must be boolean');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
outcome: 'provider_failure',
|
|
114
|
+
terminalOwner: 'provider',
|
|
115
|
+
code: requireClosedText(data.code, PROVIDER_CODES, 'provider code'),
|
|
116
|
+
kind: requireClosedText(data.kind, PROVIDER_KINDS, 'provider kind'),
|
|
117
|
+
retryable: data.retryable,
|
|
118
|
+
diagnostic: requireDiagnostic(data.diagnostic),
|
|
119
|
+
provider: requireClosedText(data.provider, PROVIDERS, 'provider'),
|
|
120
|
+
event: requireClosedText(data.event, PROVIDER_EVENTS, 'provider event'),
|
|
121
|
+
category: requireClosedText(data.category, PROVIDER_CATEGORIES, 'provider category'),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
outcome: 'engine_failure',
|
|
126
|
+
terminalOwner: 'engine',
|
|
127
|
+
code: 'engine_failed',
|
|
128
|
+
kind: 'declared_failure',
|
|
129
|
+
retryable: false,
|
|
130
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
131
|
+
provider: null,
|
|
132
|
+
event: null,
|
|
133
|
+
category: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateStoppedAgents(agents) {
|
|
138
|
+
if (!Array.isArray(agents)) throw new Error('agents must be an array');
|
|
139
|
+
for (const agent of agents) {
|
|
140
|
+
const state = requireObject(agent, 'agent state');
|
|
141
|
+
if (state.pid !== null && state.pid !== undefined) {
|
|
142
|
+
throw new Error(`agent ${String(state.id)} still has a live process identity`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateRunId(runId) {
|
|
148
|
+
const parts = typeof runId === 'string' ? runId.split('-') : [];
|
|
149
|
+
const valid =
|
|
150
|
+
parts.length >= 2 &&
|
|
151
|
+
parts.every(
|
|
152
|
+
(part) => part.length > 0 && [...part].every((character) => /[a-z0-9]/.test(character))
|
|
153
|
+
);
|
|
154
|
+
if (!valid) {
|
|
155
|
+
throw new Error('runId must be a canonical cluster id');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildBenchmarkResult({ runId, terminalMessages, agents }) {
|
|
160
|
+
validateRunId(runId);
|
|
161
|
+
if (!Array.isArray(terminalMessages) || terminalMessages.length !== 1) {
|
|
162
|
+
throw new Error('foreground run must have exactly one terminal event');
|
|
163
|
+
}
|
|
164
|
+
validateStoppedAgents(agents);
|
|
165
|
+
return {
|
|
166
|
+
schema: RESULT_SCHEMA,
|
|
167
|
+
runId,
|
|
168
|
+
...classifyTerminal(terminalMessages[0]),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildCancelledResult({ runId, agents }) {
|
|
173
|
+
validateRunId(runId);
|
|
174
|
+
validateStoppedAgents(agents);
|
|
175
|
+
return {
|
|
176
|
+
schema: RESULT_SCHEMA,
|
|
177
|
+
runId,
|
|
178
|
+
outcome: 'cancelled',
|
|
179
|
+
terminalOwner: 'controller',
|
|
180
|
+
code: 'cancelled',
|
|
181
|
+
kind: 'controlled_cancellation',
|
|
182
|
+
retryable: false,
|
|
183
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
184
|
+
provider: null,
|
|
185
|
+
event: null,
|
|
186
|
+
category: null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeTokenEntry(value, label) {
|
|
191
|
+
const source = requireObject(value, label);
|
|
192
|
+
const entry = {};
|
|
193
|
+
for (const field of TOKEN_FIELDS) {
|
|
194
|
+
const amount = source[field] ?? 0;
|
|
195
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0) {
|
|
196
|
+
throw new Error(`${label}.${field} must be a finite non-negative number`);
|
|
197
|
+
}
|
|
198
|
+
entry[field] = amount;
|
|
199
|
+
}
|
|
200
|
+
return entry;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildTelemetry(runId, snapshot) {
|
|
204
|
+
validateRunId(runId);
|
|
205
|
+
const source = requireObject(snapshot, 'telemetry snapshot');
|
|
206
|
+
if (!Number.isSafeInteger(source.messageCount) || source.messageCount < 0) {
|
|
207
|
+
throw new Error('telemetry messageCount must be a non-negative safe integer');
|
|
208
|
+
}
|
|
209
|
+
const roles = requireObject(source.tokensByRole, 'tokensByRole');
|
|
210
|
+
const names = Object.keys(roles).sort();
|
|
211
|
+
if (names.length > 64) throw new Error('telemetry role count exceeds 64');
|
|
212
|
+
const tokensByRole = {};
|
|
213
|
+
for (const name of names) {
|
|
214
|
+
const validRoleName =
|
|
215
|
+
name === '_total' ||
|
|
216
|
+
(name.length <= 128 &&
|
|
217
|
+
/^[a-zA-Z0-9]$/.test(name[0] || '') &&
|
|
218
|
+
[...name].every((character) => /[a-zA-Z0-9_.-]/.test(character)));
|
|
219
|
+
if (!validRoleName) {
|
|
220
|
+
throw new Error('telemetry role name is invalid');
|
|
221
|
+
}
|
|
222
|
+
tokensByRole[name] = normalizeTokenEntry(roles[name], `tokensByRole.${name}`);
|
|
223
|
+
}
|
|
224
|
+
return { schema: TELEMETRY_SCHEMA, runId, messageCount: source.messageCount, tokensByRole };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
RESULT_SCHEMA,
|
|
229
|
+
TELEMETRY_SCHEMA,
|
|
230
|
+
buildBenchmarkResult,
|
|
231
|
+
buildCancelledResult,
|
|
232
|
+
buildTelemetry,
|
|
233
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { buildBenchmarkResult, buildCancelledResult } = require('./foreground-benchmark-result');
|
|
2
|
+
const { writeBenchmarkResultBundle } = require('./foreground-benchmark-files');
|
|
3
|
+
|
|
4
|
+
const TERMINAL_TOPICS = ['CLUSTER_COMPLETE', 'CLUSTER_FAILED'];
|
|
5
|
+
const VERIFIER_ELIGIBLE = new Set(['completed', 'task_failure']);
|
|
6
|
+
const EXIT_CODES = Object.freeze({
|
|
7
|
+
provider_failure: 20,
|
|
8
|
+
engine_failure: 21,
|
|
9
|
+
cancelled: 22,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function isForegroundStatusSettled(status) {
|
|
13
|
+
return (
|
|
14
|
+
status &&
|
|
15
|
+
['stopped', 'killed'].includes(status.state) &&
|
|
16
|
+
status.isZombie === false &&
|
|
17
|
+
Array.isArray(status.agents) &&
|
|
18
|
+
status.agents.every((agent) => agent && (agent.pid === null || agent.pid === undefined))
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function terminalMessages(cluster, clusterId) {
|
|
23
|
+
const messages = TERMINAL_TOPICS.flatMap((topic) =>
|
|
24
|
+
cluster.messageBus.query({ cluster_id: clusterId, topic })
|
|
25
|
+
);
|
|
26
|
+
return messages.sort((left, right) => {
|
|
27
|
+
const a = BigInt(left.sequence);
|
|
28
|
+
const b = BigInt(right.sequence);
|
|
29
|
+
if (a < b) return -1;
|
|
30
|
+
if (a > b) return 1;
|
|
31
|
+
return 0;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requireSettledStatus(orchestrator, clusterId) {
|
|
36
|
+
const status = orchestrator.getStatus(clusterId);
|
|
37
|
+
if (!isForegroundStatusSettled(status)) {
|
|
38
|
+
throw new Error(`foreground cluster is not settled: ${String(status?.state || 'unavailable')}`);
|
|
39
|
+
}
|
|
40
|
+
return status;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeForegroundResult({ orchestrator, cluster, clusterId, resultPath, cancelled }) {
|
|
44
|
+
const status = requireSettledStatus(orchestrator, clusterId);
|
|
45
|
+
const terminals = terminalMessages(cluster, clusterId);
|
|
46
|
+
let result;
|
|
47
|
+
if (cancelled && terminals.length === 0) {
|
|
48
|
+
result = buildCancelledResult({ runId: clusterId, agents: status.agents });
|
|
49
|
+
} else {
|
|
50
|
+
result = buildBenchmarkResult({
|
|
51
|
+
runId: clusterId,
|
|
52
|
+
terminalMessages: terminals,
|
|
53
|
+
agents: status.agents,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const snapshot = cluster.messageBus.readSnapshot(clusterId);
|
|
57
|
+
return writeBenchmarkResultBundle(resultPath, result, snapshot);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function exitCodeForResult(result) {
|
|
61
|
+
if (VERIFIER_ELIGIBLE.has(result.outcome)) return 0;
|
|
62
|
+
const exitCode = EXIT_CODES[result.outcome];
|
|
63
|
+
if (exitCode === undefined) throw new Error(`unsupported result outcome: ${result.outcome}`);
|
|
64
|
+
return exitCode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
exitCodeForResult,
|
|
69
|
+
isForegroundStatusSettled,
|
|
70
|
+
terminalMessages,
|
|
71
|
+
writeForegroundResult,
|
|
72
|
+
};
|
package/src/isolation-manager.js
CHANGED
|
@@ -30,6 +30,7 @@ const {
|
|
|
30
30
|
isUsableEnvValue,
|
|
31
31
|
validateProviderEnvAuth,
|
|
32
32
|
} = require('../lib/docker-config');
|
|
33
|
+
const { resolveProviderCredentialPaths } = require('../lib/provider-credential-path');
|
|
33
34
|
const { getProvider } = require('./providers');
|
|
34
35
|
const { readRepoSettings } = require('../lib/repo-settings');
|
|
35
36
|
const { provisionClaudeCredentials } = require('./claude-credentials');
|
|
@@ -151,6 +152,34 @@ function expandHomePath(value) {
|
|
|
151
152
|
return value.replace(/^~(?=\/|$)/, os.homedir());
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
function appendCredentialMount(plan, mount, explicitContainerPaths, claudeContainerPath) {
|
|
156
|
+
if (mount.container === claudeContainerPath) {
|
|
157
|
+
console.warn(
|
|
158
|
+
`[IsolationManager] Skipping mount for ${mount.host} -> ${mount.container} ` +
|
|
159
|
+
'(Claude config is managed by zeroshot).'
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const hostPath = expandHomePath(mount.host);
|
|
165
|
+
let stat;
|
|
166
|
+
try {
|
|
167
|
+
stat = fs.statSync(hostPath);
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (hostPath.endsWith('config') && !stat.isFile()) return;
|
|
172
|
+
|
|
173
|
+
plan.mountedHosts.push(hostPath);
|
|
174
|
+
const mountSpec = mount.readonly
|
|
175
|
+
? `${hostPath}:${mount.container}:ro`
|
|
176
|
+
: `${hostPath}:${mount.container}`;
|
|
177
|
+
plan.args.push('-v', mountSpec);
|
|
178
|
+
if (explicitContainerPaths.has(mount.container)) {
|
|
179
|
+
plan.explicitMountContainerPaths.push(mount.container);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
154
183
|
function pathContains(base, target) {
|
|
155
184
|
const resolvedBase = path.resolve(base);
|
|
156
185
|
const resolvedTarget = path.resolve(target);
|
|
@@ -325,7 +354,6 @@ class IsolationManager {
|
|
|
325
354
|
this._removeContainerByName(containerName);
|
|
326
355
|
|
|
327
356
|
workDir = await this._prepareIsolatedWorkspace(clusterId, workDir, reuseExisting);
|
|
328
|
-
|
|
329
357
|
// The cluster config dir carries Claude credentials (via provisionClaudeCredentials) and the
|
|
330
358
|
// Claude-specific AskUserQuestion-blocking hook (~/.claude/settings.json PreToolUse), which
|
|
331
359
|
// only the `claude` CLI reads. Creating and mounting it for every provider — regardless of
|
|
@@ -534,33 +562,7 @@ class IsolationManager {
|
|
|
534
562
|
const claudeContainerPath = path.posix.join(containerHome, '.claude');
|
|
535
563
|
|
|
536
564
|
for (const mount of mounts) {
|
|
537
|
-
|
|
538
|
-
console.warn(
|
|
539
|
-
`[IsolationManager] Skipping mount for ${mount.host} -> ${mount.container} ` +
|
|
540
|
-
'(Claude config is managed by zeroshot).'
|
|
541
|
-
);
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const hostPath = expandHomePath(mount.host);
|
|
546
|
-
|
|
547
|
-
try {
|
|
548
|
-
const stat = fs.statSync(hostPath);
|
|
549
|
-
if (hostPath.endsWith('config') && !stat.isFile()) {
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
} catch {
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const mountSpec = mount.readonly
|
|
557
|
-
? `${hostPath}:${mount.container}:ro`
|
|
558
|
-
: `${hostPath}:${mount.container}`;
|
|
559
|
-
plan.args.push('-v', mountSpec);
|
|
560
|
-
plan.mountedHosts.push(hostPath);
|
|
561
|
-
if (explicitContainerPaths.has(mount.container)) {
|
|
562
|
-
plan.explicitMountContainerPaths.push(mount.container);
|
|
563
|
-
}
|
|
565
|
+
appendCredentialMount(plan, mount, explicitContainerPaths, claudeContainerPath);
|
|
564
566
|
}
|
|
565
567
|
|
|
566
568
|
const { envToPass, explicitNames } = this._collectDockerEnvVars(
|
|
@@ -568,6 +570,16 @@ class IsolationManager {
|
|
|
568
570
|
userMountConfig,
|
|
569
571
|
settings
|
|
570
572
|
);
|
|
573
|
+
const hostOnlyEnvs = new Set(
|
|
574
|
+
mountConfig
|
|
575
|
+
.filter((preset) => typeof preset === 'string')
|
|
576
|
+
.map((preset) => MOUNT_PRESETS[preset]?.hostEnv)
|
|
577
|
+
.filter((name) => typeof name === 'string')
|
|
578
|
+
);
|
|
579
|
+
for (const hostOnlyEnv of hostOnlyEnvs) {
|
|
580
|
+
delete envToPass[hostOnlyEnv];
|
|
581
|
+
explicitNames.delete(hostOnlyEnv);
|
|
582
|
+
}
|
|
571
583
|
for (const [key, value] of Object.entries(envToPass)) {
|
|
572
584
|
plan.args.push('-e', `${key}=${value}`);
|
|
573
585
|
plan.forwardedEnv[key] = value;
|
|
@@ -680,7 +692,9 @@ class IsolationManager {
|
|
|
680
692
|
|
|
681
693
|
// A mount only counts if it carries the secret (credentialInMount !== false).
|
|
682
694
|
const credentialPaths = provider.getCredentialPaths ? provider.getCredentialPaths() : [];
|
|
683
|
-
const expandedCreds =
|
|
695
|
+
const expandedCreds = resolveProviderCredentialPaths({ ...metadata, credentialPaths }).filter(
|
|
696
|
+
(credPath) => fs.existsSync(credPath)
|
|
697
|
+
);
|
|
684
698
|
const hasCredentialMount =
|
|
685
699
|
docker.credentialInMount !== false &&
|
|
686
700
|
mountedHosts.some((hostPath) =>
|
package/src/orchestrator.js
CHANGED
|
@@ -111,6 +111,35 @@ function applyModelOverride(agentConfig, modelOverride) {
|
|
|
111
111
|
*/
|
|
112
112
|
const VALID_OPERATIONS = ['add_agents', 'remove_agents', 'update_agent', 'publish', 'load_config'];
|
|
113
113
|
|
|
114
|
+
function isValidPreparedWorktreeDescriptor(prepared, cwd) {
|
|
115
|
+
if (!prepared) return false;
|
|
116
|
+
if (typeof prepared !== 'object') return false;
|
|
117
|
+
if (path.resolve(String(prepared.path)) !== cwd) return false;
|
|
118
|
+
if (path.resolve(String(prepared.repoRoot)) !== cwd) return false;
|
|
119
|
+
if (typeof prepared.branch !== 'string') return false;
|
|
120
|
+
if (prepared.branch.length === 0) return false;
|
|
121
|
+
if (prepared.branch.length > 255) return false;
|
|
122
|
+
return /^[0-9a-f]{40}$/.test(String(prepared.baseSha));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolvePreparedWorktree(options) {
|
|
126
|
+
const prepared = options.preparedWorktree;
|
|
127
|
+
if (!prepared) return null;
|
|
128
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
129
|
+
if (options.worktree !== true || options.isolation) {
|
|
130
|
+
throw new Error('Invalid prepared worktree authority');
|
|
131
|
+
}
|
|
132
|
+
if (!isValidPreparedWorktreeDescriptor(prepared, cwd)) {
|
|
133
|
+
throw new Error('Invalid prepared worktree authority');
|
|
134
|
+
}
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
path: cwd,
|
|
137
|
+
repoRoot: cwd,
|
|
138
|
+
branch: prepared.branch,
|
|
139
|
+
baseSha: prepared.baseSha,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
/**
|
|
115
144
|
* Workflow-triggering topics that indicate cluster state progression
|
|
116
145
|
* These are the topics that MATTER for resume - not AGENT_OUTPUT noise
|
|
@@ -1854,7 +1883,10 @@ class Orchestrator {
|
|
|
1854
1883
|
let worktreeInfo = null;
|
|
1855
1884
|
let isolationImage = null;
|
|
1856
1885
|
|
|
1857
|
-
if (options.
|
|
1886
|
+
if (options.preparedWorktree) {
|
|
1887
|
+
worktreeInfo = resolvePreparedWorktree(options);
|
|
1888
|
+
this._log(`[Orchestrator] Using prepared worktree: ${worktreeInfo.path}`);
|
|
1889
|
+
} else if (options.isolation) {
|
|
1858
1890
|
// Resolve the provider first so the capability gate and the cluster's image variant (which
|
|
1859
1891
|
// installs the provider CLI as a Docker-cached layer) both see the same provider. Providers
|
|
1860
1892
|
// baked into the base image (e.g. Claude) resolve back to the base image unchanged.
|
package/task-lib/runner.js
CHANGED
|
@@ -625,6 +625,13 @@ export function shouldUseAttachableWatcher(options, providerName) {
|
|
|
625
625
|
return false;
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
+
// Benchmark runs are non-interactive and keep the cluster process in the foreground. Use the
|
|
629
|
+
// pipe watcher so task completion is observed only after stdout/stderr close; a PTY exit can
|
|
630
|
+
// race its final buffered output on remote runtimes and lose the terminal structured result.
|
|
631
|
+
if (resolveTaskExecutionContext() === 'benchmark') {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
|
|
628
635
|
// The rpc-stdio lane owns bidirectional correlated RPC over stdio itself (see
|
|
629
636
|
// omp-rpc-driver.ts) and always uses rpc-watcher.js instead of the attachable PTY watcher.
|
|
630
637
|
if (getProviderRegistryEntry(providerName).invoke.lane === 'rpc-stdio') {
|
|
@@ -103,7 +103,12 @@ function createCodexOutputPassthrough({ log, captureProviderSession }) {
|
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
function createBoundedLinePassthrough({
|
|
106
|
+
function createBoundedLinePassthrough({
|
|
107
|
+
log,
|
|
108
|
+
handleLine,
|
|
109
|
+
deferRawUntilOverflow = false,
|
|
110
|
+
linePrefix = '',
|
|
111
|
+
}) {
|
|
107
112
|
const decoder = new StringDecoder('utf8');
|
|
108
113
|
let atLineStart = true;
|
|
109
114
|
let lineTimestamp = null;
|
|
@@ -160,7 +165,7 @@ function createBoundedLinePassthrough({ log, handleLine, deferRawUntilOverflow =
|
|
|
160
165
|
while (offset < text.length) {
|
|
161
166
|
if (atLineStart) {
|
|
162
167
|
lineTimestamp = Date.now();
|
|
163
|
-
appendRaw(logged, `[${lineTimestamp}]`);
|
|
168
|
+
appendRaw(logged, `[${lineTimestamp}]${linePrefix}`);
|
|
164
169
|
atLineStart = false;
|
|
165
170
|
}
|
|
166
171
|
const newline = text.indexOf('\n', offset);
|
|
@@ -361,6 +366,10 @@ export function createWatcherOutputRuntime({
|
|
|
361
366
|
});
|
|
362
367
|
const stderrPassthrough = createBoundedLinePassthrough({
|
|
363
368
|
log,
|
|
369
|
+
// Pi reserves stdout for its JSON protocol and deliberately routes ordinary writes to
|
|
370
|
+
// stderr. Keep those diagnostics in the task log with explicit provenance so followers can
|
|
371
|
+
// exclude them from strict JSON validation.
|
|
372
|
+
linePrefix: providerName === 'pi' ? '[ZEROSHOT][PROVIDER_STDERR] ' : '',
|
|
364
373
|
handleLine: (line, timestamp) => maybeHandleFatalError(line, timestamp),
|
|
365
374
|
});
|
|
366
375
|
|
|
@@ -370,7 +379,7 @@ export function createWatcherOutputRuntime({
|
|
|
370
379
|
if (!detected) return false;
|
|
371
380
|
fatalError = detected;
|
|
372
381
|
if (silentJsonMode) log(`[${timestamp}]${line}\n`);
|
|
373
|
-
log(`[${timestamp}][FATAL] ${detected}\n`);
|
|
382
|
+
log(`[${timestamp}][ZEROSHOT][FATAL] ${detected}\n`);
|
|
374
383
|
stopProvider(timestamp);
|
|
375
384
|
return true;
|
|
376
385
|
}
|
|
@@ -396,13 +405,15 @@ export function createWatcherOutputRuntime({
|
|
|
396
405
|
fatalError =
|
|
397
406
|
`Provider structured output exceeded the ${MAX_WATCHER_CONTROL_RECORD_BYTES}-byte ` +
|
|
398
407
|
'watcher inspection limit; complete output remains in the task log';
|
|
399
|
-
log(`[${timestamp}][FATAL] ${fatalError}\n`);
|
|
408
|
+
log(`[${timestamp}][ZEROSHOT][FATAL] ${fatalError}\n`);
|
|
400
409
|
stopProvider(timestamp);
|
|
401
410
|
return;
|
|
402
411
|
}
|
|
403
412
|
captureProviderSession(line);
|
|
404
413
|
if (silentJsonMode && !line.trim()) return;
|
|
405
|
-
|
|
414
|
+
// Pi reserves stdout for JSON lifecycle events. Error text inside an assistant message may be
|
|
415
|
+
// followed by an automatic retry, so only Pi stderr can prove a pre-agent startup failure.
|
|
416
|
+
if (providerName !== 'pi') maybeHandleFatalError(line, timestamp);
|
|
406
417
|
if (captureStreamingError(line, timestamp)) return;
|
|
407
418
|
if (silentJsonMode) {
|
|
408
419
|
maybeCaptureStructuredOutput(line);
|