@the-open-engine/zeroshot 6.21.0 → 6.23.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/lib/agent-cli-provider/adapters/omp.d.ts +3 -1
- package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/omp.js +252 -269
- package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
- package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-invoke.js +68 -14
- 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 +5 -3
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +6 -0
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +12 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/omp-release.d.ts +3 -1
- package/lib/agent-cli-provider/omp-release.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp-release.js +22 -2
- package/lib/agent-cli-provider/omp-release.js.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-bounds.d.ts +7 -0
- package/lib/agent-cli-provider/omp-rpc-bounds.d.ts.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-bounds.js +27 -0
- package/lib/agent-cli-provider/omp-rpc-bounds.js.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-driver.d.ts +40 -0
- package/lib/agent-cli-provider/omp-rpc-driver.d.ts.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-driver.js +494 -0
- package/lib/agent-cli-provider/omp-rpc-driver.js.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-events.d.ts +28 -0
- package/lib/agent-cli-provider/omp-rpc-events.d.ts.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-events.js +264 -0
- package/lib/agent-cli-provider/omp-rpc-events.js.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-protocol.d.ts +1 -1
- package/lib/agent-cli-provider/omp-rpc-protocol.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-protocol.js +8 -3
- package/lib/agent-cli-provider/omp-rpc-protocol.js.map +1 -1
- package/lib/agent-cli-provider/omp-rpc-session.d.ts +17 -0
- package/lib/agent-cli-provider/omp-rpc-session.d.ts.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-session.js +6 -0
- package/lib/agent-cli-provider/omp-rpc-session.js.map +1 -0
- package/lib/agent-cli-provider/provider-registry.d.ts +15 -11
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +16 -5
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +13 -11
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +2 -1
- package/src/agent-cli-provider/adapters/omp.ts +276 -329
- package/src/agent-cli-provider/contract-invoke.ts +86 -15
- package/src/agent-cli-provider/contract-options.ts +5 -3
- package/src/agent-cli-provider/index.ts +13 -0
- package/src/agent-cli-provider/omp-release.ts +24 -1
- package/src/agent-cli-provider/omp-rpc-bounds.ts +28 -0
- package/src/agent-cli-provider/omp-rpc-driver.ts +611 -0
- package/src/agent-cli-provider/omp-rpc-events.ts +318 -0
- package/src/agent-cli-provider/omp-rpc-protocol.ts +8 -3
- package/src/agent-cli-provider/omp-rpc-session.ts +20 -0
- package/src/agent-cli-provider/provider-registry.ts +26 -7
- package/src/agent-cli-provider/types.ts +14 -11
- package/src/command-cleanup-ownership.js +307 -0
- package/src/omp-config-overlay.js +96 -0
- package/src/orchestrator.js +20 -5
- package/src/preflight.js +35 -3
- package/src/watcher-prompt-channel.js +209 -0
- package/task-lib/command-spec-cleanup.js +1 -262
- package/task-lib/provider-helper-runtime.js +6 -0
- package/task-lib/rpc-watcher.js +186 -0
- package/task-lib/runner.js +40 -6
- package/task-lib/watcher-output-runtime.js +32 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const { lstat, realpath, rm, unlink } = require('fs/promises');
|
|
2
|
+
const { lstatSync, realpathSync, rmSync, unlinkSync } = require('fs');
|
|
3
|
+
const { tmpdir } = require('os');
|
|
4
|
+
const { basename, dirname, isAbsolute, resolve } = require('path');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
isCanonicalClaudeSettingsOverlayDirectory,
|
|
8
|
+
isClaudeSettingsOverlayDirectory,
|
|
9
|
+
} = require('./worktree-claude-config');
|
|
10
|
+
const { isCanonicalOmpConfigOverlayDirectory } = require('./omp-config-overlay');
|
|
11
|
+
|
|
12
|
+
const CLEANUP_METADATA_KEYS = ['kind', 'path', 'provider', 'reason'];
|
|
13
|
+
const SCHEMA_DIRECTORY_PATTERN = /^zeroshot-schema-[A-Za-z0-9_-]+$/u;
|
|
14
|
+
const SCHEMA_FILE_PATTERN =
|
|
15
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.json$/u;
|
|
16
|
+
const POLICY_DIRECTORY_PATTERN = /^zeroshot-gemini-policy-[A-Za-z0-9_-]+$/u;
|
|
17
|
+
const POLICY_FILE_PATTERN =
|
|
18
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.toml$/u;
|
|
19
|
+
const OPENCODE_CONFIG_DIRECTORY_PATTERN = /^zeroshot-opencode-config-[A-Za-z0-9_-]+$/u;
|
|
20
|
+
|
|
21
|
+
function assertClosedCleanupMetadata(cleanupPath, metadata) {
|
|
22
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
23
|
+
throw new Error(`Refusing cleanup without closed ownership metadata: ${cleanupPath}`);
|
|
24
|
+
}
|
|
25
|
+
const keys = Object.keys(metadata).sort();
|
|
26
|
+
if (
|
|
27
|
+
keys.length !== CLEANUP_METADATA_KEYS.length ||
|
|
28
|
+
keys.some((key, index) => key !== CLEANUP_METADATA_KEYS[index])
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(`Refusing cleanup with open ownership metadata: ${cleanupPath}`);
|
|
31
|
+
}
|
|
32
|
+
if (metadata.path !== cleanupPath) {
|
|
33
|
+
throw new Error(`Refusing cleanup with mismatched ownership path: ${cleanupPath}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createCleanupPlan(commandSpec) {
|
|
38
|
+
if (!Array.isArray(commandSpec?.cleanup) || !Array.isArray(commandSpec?.cleanupMetadata)) {
|
|
39
|
+
throw new Error('Refusing cleanup with malformed cleanup collections');
|
|
40
|
+
}
|
|
41
|
+
if (commandSpec.cleanup.length !== commandSpec.cleanupMetadata.length) {
|
|
42
|
+
throw new Error('Refusing cleanup without one closed metadata receipt per path');
|
|
43
|
+
}
|
|
44
|
+
const uniquePaths = new Set(commandSpec.cleanup);
|
|
45
|
+
if (uniquePaths.size !== commandSpec.cleanup.length) {
|
|
46
|
+
throw new Error('Refusing cleanup with duplicate cleanup paths');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return commandSpec.cleanup.map((cleanupPath) => {
|
|
50
|
+
if (typeof cleanupPath !== 'string' || cleanupPath.length === 0) {
|
|
51
|
+
throw new Error('Refusing cleanup with a non-string or empty path');
|
|
52
|
+
}
|
|
53
|
+
const matches = commandSpec.cleanupMetadata.filter(
|
|
54
|
+
(metadata) => metadata?.path === cleanupPath
|
|
55
|
+
);
|
|
56
|
+
if (matches.length !== 1) {
|
|
57
|
+
throw new Error(`Refusing cleanup without exact ownership metadata: ${cleanupPath}`);
|
|
58
|
+
}
|
|
59
|
+
assertClosedCleanupMetadata(cleanupPath, matches[0]);
|
|
60
|
+
return { cleanupPath, metadata: matches[0] };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isCanonicalNamedTempDirectory(cleanupPath, pattern) {
|
|
65
|
+
return (
|
|
66
|
+
isAbsolute(cleanupPath) &&
|
|
67
|
+
resolve(cleanupPath) === cleanupPath &&
|
|
68
|
+
dirname(cleanupPath) === resolve(tmpdir()) &&
|
|
69
|
+
pattern.test(basename(cleanupPath))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertOwnedNamedTempDirectory(cleanupPath, pattern) {
|
|
74
|
+
if (!isCanonicalNamedTempDirectory(cleanupPath, pattern)) {
|
|
75
|
+
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
76
|
+
}
|
|
77
|
+
let stat;
|
|
78
|
+
try {
|
|
79
|
+
stat = lstatSync(cleanupPath);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error?.code === 'ENOENT') return true;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
const tempRoot = realpathSync(tmpdir());
|
|
85
|
+
const realDirectory = realpathSync(cleanupPath);
|
|
86
|
+
if (
|
|
87
|
+
stat.isSymbolicLink() ||
|
|
88
|
+
!stat.isDirectory() ||
|
|
89
|
+
dirname(realDirectory) !== tempRoot ||
|
|
90
|
+
basename(realDirectory) !== basename(cleanupPath)
|
|
91
|
+
) {
|
|
92
|
+
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assertOwnedOmpConfigDirectory(cleanupPath) {
|
|
98
|
+
if (!isCanonicalOmpConfigOverlayDirectory(cleanupPath)) {
|
|
99
|
+
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
100
|
+
}
|
|
101
|
+
let stat;
|
|
102
|
+
try {
|
|
103
|
+
stat = lstatSync(cleanupPath);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error?.code === 'ENOENT') return true;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
const tempRoot = realpathSync(tmpdir());
|
|
109
|
+
const realDirectory = realpathSync(cleanupPath);
|
|
110
|
+
const ownedByProcess = typeof process.getuid !== 'function' || stat.uid === process.getuid();
|
|
111
|
+
const privateMode = process.platform === 'win32' || (stat.mode & 0o777) === 0o700;
|
|
112
|
+
if (
|
|
113
|
+
stat.isSymbolicLink() ||
|
|
114
|
+
!stat.isDirectory() ||
|
|
115
|
+
!ownedByProcess ||
|
|
116
|
+
!privateMode ||
|
|
117
|
+
dirname(realDirectory) !== tempRoot ||
|
|
118
|
+
basename(realDirectory) !== basename(cleanupPath)
|
|
119
|
+
) {
|
|
120
|
+
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assertOwnedTempDirectory(cleanupPath, metadata) {
|
|
126
|
+
const isOpenCodeConfig =
|
|
127
|
+
metadata.provider === 'opencode' && metadata.reason === 'isolated-config';
|
|
128
|
+
if (isOpenCodeConfig) {
|
|
129
|
+
return assertOwnedNamedTempDirectory(cleanupPath, OPENCODE_CONFIG_DIRECTORY_PATTERN);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isOmpConfig = metadata.provider === 'omp' && metadata.reason === 'isolated-config';
|
|
133
|
+
if (isOmpConfig) {
|
|
134
|
+
return assertOwnedOmpConfigDirectory(cleanupPath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
metadata.provider !== 'claude' ||
|
|
139
|
+
metadata.reason !== 'settings-overlay' ||
|
|
140
|
+
!isCanonicalClaudeSettingsOverlayDirectory(cleanupPath)
|
|
141
|
+
) {
|
|
142
|
+
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
143
|
+
}
|
|
144
|
+
if (isClaudeSettingsOverlayDirectory(cleanupPath)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
lstatSync(cleanupPath);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error?.code === 'ENOENT') return true;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertCanonicalSchemaPath(cleanupPath, metadata) {
|
|
157
|
+
const isCodexSchema =
|
|
158
|
+
metadata.kind === 'temp-file' &&
|
|
159
|
+
metadata.provider === 'codex' &&
|
|
160
|
+
metadata.reason === 'output-schema';
|
|
161
|
+
const isGeminiPolicy =
|
|
162
|
+
metadata.kind === 'temp-file' &&
|
|
163
|
+
metadata.provider === 'gemini' &&
|
|
164
|
+
metadata.reason === 'admin-policy';
|
|
165
|
+
if (
|
|
166
|
+
(!isCodexSchema && !isGeminiPolicy) ||
|
|
167
|
+
!isAbsolute(cleanupPath) ||
|
|
168
|
+
resolve(cleanupPath) !== cleanupPath
|
|
169
|
+
) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Refusing unowned ${isGeminiPolicy ? 'admin-policy' : 'output-schema'} cleanup: ${cleanupPath}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const ownedDirectory = dirname(cleanupPath);
|
|
175
|
+
const matchesOwnedName = isCodexSchema
|
|
176
|
+
? SCHEMA_DIRECTORY_PATTERN.test(basename(ownedDirectory)) &&
|
|
177
|
+
SCHEMA_FILE_PATTERN.test(basename(cleanupPath))
|
|
178
|
+
: POLICY_DIRECTORY_PATTERN.test(basename(ownedDirectory)) &&
|
|
179
|
+
POLICY_FILE_PATTERN.test(basename(cleanupPath));
|
|
180
|
+
if (dirname(ownedDirectory) !== resolve(tmpdir()) || !matchesOwnedName) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Refusing non-canonical ${isGeminiPolicy ? 'admin-policy' : 'output-schema'} cleanup: ${cleanupPath}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function assertOwnedSchemaFile(cleanupPath, metadata) {
|
|
188
|
+
assertCanonicalSchemaPath(cleanupPath, metadata);
|
|
189
|
+
const schemaDirectory = dirname(cleanupPath);
|
|
190
|
+
const [tempRoot, realSchemaDirectory, directoryStat, schemaStat] = await Promise.all([
|
|
191
|
+
realpath(tmpdir()),
|
|
192
|
+
realpath(schemaDirectory),
|
|
193
|
+
lstat(schemaDirectory),
|
|
194
|
+
lstat(cleanupPath),
|
|
195
|
+
]);
|
|
196
|
+
if (
|
|
197
|
+
directoryStat.isSymbolicLink() ||
|
|
198
|
+
!directoryStat.isDirectory() ||
|
|
199
|
+
dirname(realSchemaDirectory) !== tempRoot ||
|
|
200
|
+
basename(realSchemaDirectory) !== basename(schemaDirectory) ||
|
|
201
|
+
schemaStat.isSymbolicLink() ||
|
|
202
|
+
!schemaStat.isFile()
|
|
203
|
+
) {
|
|
204
|
+
throw new Error(`Refusing non-regular or escaped output-schema cleanup: ${cleanupPath}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertOwnedSchemaFileSync(cleanupPath, metadata) {
|
|
209
|
+
assertCanonicalSchemaPath(cleanupPath, metadata);
|
|
210
|
+
const schemaDirectory = dirname(cleanupPath);
|
|
211
|
+
const tempRoot = realpathSync(tmpdir());
|
|
212
|
+
const realSchemaDirectory = realpathSync(schemaDirectory);
|
|
213
|
+
const directoryStat = lstatSync(schemaDirectory);
|
|
214
|
+
const schemaStat = lstatSync(cleanupPath);
|
|
215
|
+
if (
|
|
216
|
+
directoryStat.isSymbolicLink() ||
|
|
217
|
+
!directoryStat.isDirectory() ||
|
|
218
|
+
dirname(realSchemaDirectory) !== tempRoot ||
|
|
219
|
+
basename(realSchemaDirectory) !== basename(schemaDirectory) ||
|
|
220
|
+
schemaStat.isSymbolicLink() ||
|
|
221
|
+
!schemaStat.isFile()
|
|
222
|
+
) {
|
|
223
|
+
throw new Error(`Refusing non-regular or escaped output-schema cleanup: ${cleanupPath}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function removeCleanupPath(cleanupPath, metadata) {
|
|
228
|
+
if (metadata.kind === 'temp-directory') {
|
|
229
|
+
if (assertOwnedTempDirectory(cleanupPath, metadata)) return;
|
|
230
|
+
await rm(cleanupPath, { recursive: true, force: true });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await assertOwnedSchemaFile(cleanupPath, metadata);
|
|
234
|
+
await unlink(cleanupPath);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function removeCleanupPathSync(cleanupPath, metadata) {
|
|
238
|
+
if (metadata.kind === 'temp-directory') {
|
|
239
|
+
if (assertOwnedTempDirectory(cleanupPath, metadata)) return;
|
|
240
|
+
rmSync(cleanupPath, { recursive: true, force: true });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
assertOwnedSchemaFileSync(cleanupPath, metadata);
|
|
244
|
+
unlinkSync(cleanupPath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Build one idempotent cleanup owner for a provider command. Callers may run it
|
|
249
|
+
* only after the persisted provider termination boundary is terminal.
|
|
250
|
+
*/
|
|
251
|
+
function createCommandSpecCleanup(commandSpec, logFailure) {
|
|
252
|
+
let started = false;
|
|
253
|
+
let result = true;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
async run() {
|
|
257
|
+
if (started) return result;
|
|
258
|
+
started = true;
|
|
259
|
+
let cleanupPlan;
|
|
260
|
+
try {
|
|
261
|
+
cleanupPlan = createCleanupPlan(commandSpec);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
logFailure('<command-cleanup>', error);
|
|
264
|
+
result = false;
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
let succeeded = true;
|
|
268
|
+
for (const { cleanupPath, metadata } of cleanupPlan) {
|
|
269
|
+
try {
|
|
270
|
+
await removeCleanupPath(cleanupPath, metadata);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error?.code === 'ENOENT') continue;
|
|
273
|
+
succeeded = false;
|
|
274
|
+
logFailure(cleanupPath, error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
result = succeeded;
|
|
278
|
+
return result;
|
|
279
|
+
},
|
|
280
|
+
runSync() {
|
|
281
|
+
if (started) return result;
|
|
282
|
+
started = true;
|
|
283
|
+
let cleanupPlan;
|
|
284
|
+
try {
|
|
285
|
+
cleanupPlan = createCleanupPlan(commandSpec);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
logFailure('<command-cleanup>', error);
|
|
288
|
+
result = false;
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
let succeeded = true;
|
|
292
|
+
for (const { cleanupPath, metadata } of cleanupPlan) {
|
|
293
|
+
try {
|
|
294
|
+
removeCleanupPathSync(cleanupPath, metadata);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error?.code === 'ENOENT') continue;
|
|
297
|
+
succeeded = false;
|
|
298
|
+
logFailure(cleanupPath, error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
result = succeeded;
|
|
302
|
+
return result;
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = { createCommandSpecCleanup };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { randomUUID } = require('crypto');
|
|
5
|
+
|
|
6
|
+
const OVERLAY_PREFIX = 'zeroshot-omp-config-';
|
|
7
|
+
const OMP_CONFIG_OVERLAY_DIR_PATTERN = /^zeroshot-omp-config-[A-Za-z0-9_-]+$/u;
|
|
8
|
+
const OMP_CONFIG_OVERLAY_FILE_PATTERN =
|
|
9
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.yml$/u;
|
|
10
|
+
|
|
11
|
+
// Verified against tagged v17.2.1 source: packages/coding-agent/src/config/settings-schema.ts
|
|
12
|
+
// (key types/defaults) and packages/coding-agent/src/main.ts (HOST_DEFAULTED_SETTING_PATHS /
|
|
13
|
+
// RPC_BACKGROUND_DEFAULTED_SETTING_PATHS). RPC mode (docs/rpc.md) only re-applies its own
|
|
14
|
+
// neutral defaults for task.*/memory.backend/memories.enabled/advisor.*/tier.advisor/
|
|
15
|
+
// async.*/bash.autoBackground.* when a path is completely unconfigured at every settings
|
|
16
|
+
// layer; `todo.*` is explicitly caller-controlled and never host-defaulted in RPC mode
|
|
17
|
+
// (main.ts: "embedders need project-level opt-outs for reminder/prelude prompt injection").
|
|
18
|
+
// A host's ~/.omp/agent/config.yml or project .omp/config.yml can therefore change one of
|
|
19
|
+
// these workflow-altering namespaces out from under an unattended RPC worker. This overlay
|
|
20
|
+
// pins every one of them to its schema built-in default plus marketplace.autoUpdate off, so
|
|
21
|
+
// Zeroshot's worker behavior never depends on the host machine's OMP config. It deliberately
|
|
22
|
+
// never sets context/skills/rules/extensions/mcp keys, which keep flowing from native
|
|
23
|
+
// project/user config underneath this overlay.
|
|
24
|
+
const OVERLAY_BODY = `# Zeroshot-owned OMP safety overlay — do not add model/profile settings.
|
|
25
|
+
marketplace:
|
|
26
|
+
autoUpdate: "off"
|
|
27
|
+
todo:
|
|
28
|
+
enabled: true
|
|
29
|
+
reminders: true
|
|
30
|
+
remindersMax: 3
|
|
31
|
+
eager: "default"
|
|
32
|
+
task:
|
|
33
|
+
isolation:
|
|
34
|
+
mode: "none"
|
|
35
|
+
apply: true
|
|
36
|
+
merge: "patch"
|
|
37
|
+
commits: "generic"
|
|
38
|
+
eager: "default"
|
|
39
|
+
batch: true
|
|
40
|
+
maxConcurrency: 32
|
|
41
|
+
maxRecursionDepth: 2
|
|
42
|
+
disabledAgents: []
|
|
43
|
+
agentModelOverrides: {}
|
|
44
|
+
agentPrewalk: {}
|
|
45
|
+
memory:
|
|
46
|
+
backend: "off"
|
|
47
|
+
memories:
|
|
48
|
+
enabled: false
|
|
49
|
+
advisor:
|
|
50
|
+
enabled: false
|
|
51
|
+
subagents: false
|
|
52
|
+
syncBacklog: "off"
|
|
53
|
+
immuneTurns: 3
|
|
54
|
+
tier:
|
|
55
|
+
advisor: "none"
|
|
56
|
+
async:
|
|
57
|
+
enabled: true
|
|
58
|
+
maxJobs: 100
|
|
59
|
+
bash:
|
|
60
|
+
autoBackground:
|
|
61
|
+
enabled: false
|
|
62
|
+
thresholdMs: 60000
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
function createOmpConfigOverlay() {
|
|
66
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), OVERLAY_PREFIX), { mode: 0o700 });
|
|
67
|
+
try {
|
|
68
|
+
const file = path.join(dir, `${randomUUID()}.yml`);
|
|
69
|
+
fs.writeFileSync(file, OVERLAY_BODY, { flag: 'wx', mode: 0o600 });
|
|
70
|
+
if (process.platform !== 'win32') {
|
|
71
|
+
fs.chmodSync(dir, 0o700);
|
|
72
|
+
fs.chmodSync(file, 0o600);
|
|
73
|
+
}
|
|
74
|
+
return { dir, file };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isCanonicalOmpConfigOverlayDirectory(overlayDir) {
|
|
82
|
+
if (typeof overlayDir !== 'string' || !overlayDir || path.resolve(overlayDir) !== overlayDir) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
path.dirname(overlayDir) === path.resolve(os.tmpdir()) &&
|
|
87
|
+
OMP_CONFIG_OVERLAY_DIR_PATTERN.test(path.basename(overlayDir))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
OMP_CONFIG_OVERLAY_DIR_PATTERN,
|
|
93
|
+
OMP_CONFIG_OVERLAY_FILE_PATTERN,
|
|
94
|
+
createOmpConfigOverlay,
|
|
95
|
+
isCanonicalOmpConfigOverlayDirectory,
|
|
96
|
+
};
|
package/src/orchestrator.js
CHANGED
|
@@ -46,7 +46,11 @@ const { generateName } = require('./name-generator');
|
|
|
46
46
|
const configValidator = require('./config-validator');
|
|
47
47
|
const TemplateResolver = require('./template-resolver');
|
|
48
48
|
const { loadSettings } = require('../lib/settings');
|
|
49
|
-
const {
|
|
49
|
+
const {
|
|
50
|
+
normalizeProviderName,
|
|
51
|
+
getDefaultProviderId,
|
|
52
|
+
providerSupportsCapability,
|
|
53
|
+
} = require('../lib/provider-names');
|
|
50
54
|
const { resolveRunPlan } = require('../lib/run-plan');
|
|
51
55
|
const { isProcessRunning } = require('../lib/process-liveness');
|
|
52
56
|
const { getProvider } = require('./providers');
|
|
@@ -1819,14 +1823,19 @@ class Orchestrator {
|
|
|
1819
1823
|
let isolationImage = null;
|
|
1820
1824
|
|
|
1821
1825
|
if (options.isolation) {
|
|
1826
|
+
// Resolve the provider first so the capability gate and the cluster's image variant (which
|
|
1827
|
+
// installs the provider CLI as a Docker-cached layer) both see the same provider. Providers
|
|
1828
|
+
// baked into the base image (e.g. Claude) resolve back to the base image unchanged.
|
|
1829
|
+
const providerName = this._resolveClusterProvider(config);
|
|
1830
|
+
if (!providerSupportsCapability(providerName, 'dockerIsolation')) {
|
|
1831
|
+
throw new Error(
|
|
1832
|
+
`Provider ${providerName} does not support Docker isolation; run without --docker (worktree isolation is supported).`
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1822
1835
|
if (!IsolationManager.isDockerAvailable()) {
|
|
1823
1836
|
throw new Error('Docker is not available. Install Docker to use --docker mode.');
|
|
1824
1837
|
}
|
|
1825
1838
|
|
|
1826
|
-
// Resolve the provider first so the cluster runs on that provider's image variant, which
|
|
1827
|
-
// installs the provider CLI as a Docker-cached layer. Providers baked into the base image
|
|
1828
|
-
// (e.g. Claude) resolve back to the base image unchanged.
|
|
1829
|
-
const providerName = this._resolveClusterProvider(config);
|
|
1830
1839
|
const baseImage = options.isolationImage || 'zeroshot-cluster-base';
|
|
1831
1840
|
const image = IsolationManager.imageForProvider(providerName, baseImage);
|
|
1832
1841
|
await IsolationManager.ensureImage(
|
|
@@ -1850,6 +1859,12 @@ class Orchestrator {
|
|
|
1850
1859
|
});
|
|
1851
1860
|
this._log(`[Orchestrator] Container created: ${containerId} (workDir: ${workDir})`);
|
|
1852
1861
|
} else if (options.worktree) {
|
|
1862
|
+
const providerName = this._resolveClusterProvider(config);
|
|
1863
|
+
if (!providerSupportsCapability(providerName, 'worktreeIsolation')) {
|
|
1864
|
+
throw new Error(
|
|
1865
|
+
`Provider ${providerName} does not support worktree isolation; run without --worktree.`
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1853
1868
|
const workDir = options.cwd || process.cwd();
|
|
1854
1869
|
|
|
1855
1870
|
isolationManager = new IsolationManager({});
|
package/src/preflight.js
CHANGED
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
getDefaultProviderId,
|
|
25
25
|
getProviderMetadata,
|
|
26
26
|
normalizeProviderName,
|
|
27
|
+
providerSupportsCapability,
|
|
27
28
|
resolveProviderCommand,
|
|
28
29
|
} = require('../lib/provider-names');
|
|
29
30
|
const { detectGitContext } = require('../lib/git-remote-utils');
|
|
@@ -441,6 +442,32 @@ function validateGatewayProvider() {
|
|
|
441
442
|
};
|
|
442
443
|
}
|
|
443
444
|
|
|
445
|
+
function validateProviderIsolationCapabilities(providerName, options) {
|
|
446
|
+
const errors = [];
|
|
447
|
+
if (options.requireDocker && !providerSupportsCapability(providerName, 'dockerIsolation')) {
|
|
448
|
+
errors.push(
|
|
449
|
+
formatError(
|
|
450
|
+
'Provider does not support Docker isolation',
|
|
451
|
+
`Provider "${providerName}" does not advertise dockerIsolation`,
|
|
452
|
+
[
|
|
453
|
+
'Run without --docker (worktree isolation may be supported)',
|
|
454
|
+
'Or select a different provider',
|
|
455
|
+
]
|
|
456
|
+
)
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if (options.requireGit && !providerSupportsCapability(providerName, 'worktreeIsolation')) {
|
|
460
|
+
errors.push(
|
|
461
|
+
formatError(
|
|
462
|
+
'Provider does not support worktree isolation',
|
|
463
|
+
`Provider "${providerName}" does not advertise worktreeIsolation`,
|
|
464
|
+
['Run without --worktree/--pr/--ship', 'Or select a different provider']
|
|
465
|
+
)
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return errors;
|
|
469
|
+
}
|
|
470
|
+
|
|
444
471
|
function validateProvider(providerName, options) {
|
|
445
472
|
let metadata;
|
|
446
473
|
try {
|
|
@@ -456,14 +483,19 @@ function validateProvider(providerName, options) {
|
|
|
456
483
|
};
|
|
457
484
|
}
|
|
458
485
|
|
|
486
|
+
const isolationErrors = validateProviderIsolationCapabilities(providerName, options);
|
|
487
|
+
|
|
459
488
|
if (metadata.command.kind === 'configured-claude') {
|
|
460
|
-
|
|
489
|
+
const result = validateClaudeProvider(options);
|
|
490
|
+
return { errors: [...isolationErrors, ...result.errors], warnings: result.warnings };
|
|
461
491
|
}
|
|
462
492
|
if (providerName === 'gateway') {
|
|
463
|
-
|
|
493
|
+
const result = validateGatewayProvider();
|
|
494
|
+
return { errors: [...isolationErrors, ...result.errors], warnings: result.warnings };
|
|
464
495
|
}
|
|
465
496
|
|
|
466
|
-
|
|
497
|
+
const result = validateRegistryCliProvider(providerName);
|
|
498
|
+
return { errors: [...isolationErrors, ...result.errors], warnings: result.warnings };
|
|
467
499
|
}
|
|
468
500
|
|
|
469
501
|
function validateRegistryCliProvider(providerName) {
|