@the-open-engine/zeroshot 6.22.0 → 6.24.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/docker/zeroshot-cluster/Dockerfile +7 -0
- 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 +6 -1
- package/lib/agent-cli-provider/omp-release.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp-release.js +41 -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 +33 -17
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +63 -11
- 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/lib/docker-config.js +122 -5
- package/package.json +2 -1
- package/src/agent/agent-lifecycle.js +12 -1
- package/src/agent/provider-session.js +1 -0
- 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 +50 -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 +118 -23
- package/src/agent-cli-provider/types.ts +14 -11
- package/src/command-cleanup-ownership.js +307 -0
- package/src/isolation-manager.js +535 -89
- package/src/omp-config-overlay.js +96 -0
- package/src/orchestrator.js +31 -6
- package/src/preflight.js +50 -5
- 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
|
@@ -1,265 +1,4 @@
|
|
|
1
|
-
import { lstat, realpath, rm, unlink } from 'fs/promises';
|
|
2
|
-
import { lstatSync, realpathSync, rmSync, unlinkSync } from 'fs';
|
|
3
|
-
import { tmpdir } from 'os';
|
|
4
|
-
import { basename, dirname, isAbsolute, resolve } from 'path';
|
|
5
1
|
import { createRequire } from 'module';
|
|
6
2
|
|
|
7
3
|
const require = createRequire(import.meta.url);
|
|
8
|
-
const {
|
|
9
|
-
isCanonicalClaudeSettingsOverlayDirectory,
|
|
10
|
-
isClaudeSettingsOverlayDirectory,
|
|
11
|
-
} = require('../src/worktree-claude-config');
|
|
12
|
-
|
|
13
|
-
const CLEANUP_METADATA_KEYS = ['kind', 'path', 'provider', 'reason'];
|
|
14
|
-
const SCHEMA_DIRECTORY_PATTERN = /^zeroshot-schema-[A-Za-z0-9_-]+$/u;
|
|
15
|
-
const SCHEMA_FILE_PATTERN =
|
|
16
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.json$/u;
|
|
17
|
-
const POLICY_DIRECTORY_PATTERN = /^zeroshot-gemini-policy-[A-Za-z0-9_-]+$/u;
|
|
18
|
-
const POLICY_FILE_PATTERN =
|
|
19
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.toml$/u;
|
|
20
|
-
const OPENCODE_CONFIG_DIRECTORY_PATTERN = /^zeroshot-opencode-config-[A-Za-z0-9_-]+$/u;
|
|
21
|
-
|
|
22
|
-
function assertClosedCleanupMetadata(cleanupPath, metadata) {
|
|
23
|
-
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
24
|
-
throw new Error(`Refusing cleanup without closed ownership metadata: ${cleanupPath}`);
|
|
25
|
-
}
|
|
26
|
-
const keys = Object.keys(metadata).sort();
|
|
27
|
-
if (
|
|
28
|
-
keys.length !== CLEANUP_METADATA_KEYS.length ||
|
|
29
|
-
keys.some((key, index) => key !== CLEANUP_METADATA_KEYS[index])
|
|
30
|
-
) {
|
|
31
|
-
throw new Error(`Refusing cleanup with open ownership metadata: ${cleanupPath}`);
|
|
32
|
-
}
|
|
33
|
-
if (metadata.path !== cleanupPath) {
|
|
34
|
-
throw new Error(`Refusing cleanup with mismatched ownership path: ${cleanupPath}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function createCleanupPlan(commandSpec) {
|
|
39
|
-
if (!Array.isArray(commandSpec?.cleanup) || !Array.isArray(commandSpec?.cleanupMetadata)) {
|
|
40
|
-
throw new Error('Refusing cleanup with malformed cleanup collections');
|
|
41
|
-
}
|
|
42
|
-
if (commandSpec.cleanup.length !== commandSpec.cleanupMetadata.length) {
|
|
43
|
-
throw new Error('Refusing cleanup without one closed metadata receipt per path');
|
|
44
|
-
}
|
|
45
|
-
const uniquePaths = new Set(commandSpec.cleanup);
|
|
46
|
-
if (uniquePaths.size !== commandSpec.cleanup.length) {
|
|
47
|
-
throw new Error('Refusing cleanup with duplicate cleanup paths');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return commandSpec.cleanup.map((cleanupPath) => {
|
|
51
|
-
if (typeof cleanupPath !== 'string' || cleanupPath.length === 0) {
|
|
52
|
-
throw new Error('Refusing cleanup with a non-string or empty path');
|
|
53
|
-
}
|
|
54
|
-
const matches = commandSpec.cleanupMetadata.filter(
|
|
55
|
-
(metadata) => metadata?.path === cleanupPath
|
|
56
|
-
);
|
|
57
|
-
if (matches.length !== 1) {
|
|
58
|
-
throw new Error(`Refusing cleanup without exact ownership metadata: ${cleanupPath}`);
|
|
59
|
-
}
|
|
60
|
-
assertClosedCleanupMetadata(cleanupPath, matches[0]);
|
|
61
|
-
return { cleanupPath, metadata: matches[0] };
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function assertOwnedTempDirectory(cleanupPath, metadata) {
|
|
66
|
-
const isOpenCodeConfig =
|
|
67
|
-
metadata.provider === 'opencode' && metadata.reason === 'isolated-config';
|
|
68
|
-
if (isOpenCodeConfig) {
|
|
69
|
-
const canonical =
|
|
70
|
-
isAbsolute(cleanupPath) &&
|
|
71
|
-
resolve(cleanupPath) === cleanupPath &&
|
|
72
|
-
dirname(cleanupPath) === resolve(tmpdir()) &&
|
|
73
|
-
OPENCODE_CONFIG_DIRECTORY_PATTERN.test(basename(cleanupPath));
|
|
74
|
-
if (!canonical) {
|
|
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
|
-
if (
|
|
98
|
-
metadata.provider !== 'claude' ||
|
|
99
|
-
metadata.reason !== 'settings-overlay' ||
|
|
100
|
-
!isCanonicalClaudeSettingsOverlayDirectory(cleanupPath)
|
|
101
|
-
) {
|
|
102
|
-
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
103
|
-
}
|
|
104
|
-
if (isClaudeSettingsOverlayDirectory(cleanupPath)) {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
lstatSync(cleanupPath);
|
|
109
|
-
} catch (error) {
|
|
110
|
-
if (error?.code === 'ENOENT') return true;
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function assertCanonicalSchemaPath(cleanupPath, metadata) {
|
|
117
|
-
const isCodexSchema =
|
|
118
|
-
metadata.kind === 'temp-file' &&
|
|
119
|
-
metadata.provider === 'codex' &&
|
|
120
|
-
metadata.reason === 'output-schema';
|
|
121
|
-
const isGeminiPolicy =
|
|
122
|
-
metadata.kind === 'temp-file' &&
|
|
123
|
-
metadata.provider === 'gemini' &&
|
|
124
|
-
metadata.reason === 'admin-policy';
|
|
125
|
-
if (
|
|
126
|
-
(!isCodexSchema && !isGeminiPolicy) ||
|
|
127
|
-
!isAbsolute(cleanupPath) ||
|
|
128
|
-
resolve(cleanupPath) !== cleanupPath
|
|
129
|
-
) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
`Refusing unowned ${isGeminiPolicy ? 'admin-policy' : 'output-schema'} cleanup: ${cleanupPath}`
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
const ownedDirectory = dirname(cleanupPath);
|
|
135
|
-
const matchesOwnedName = isCodexSchema
|
|
136
|
-
? SCHEMA_DIRECTORY_PATTERN.test(basename(ownedDirectory)) &&
|
|
137
|
-
SCHEMA_FILE_PATTERN.test(basename(cleanupPath))
|
|
138
|
-
: POLICY_DIRECTORY_PATTERN.test(basename(ownedDirectory)) &&
|
|
139
|
-
POLICY_FILE_PATTERN.test(basename(cleanupPath));
|
|
140
|
-
if (dirname(ownedDirectory) !== resolve(tmpdir()) || !matchesOwnedName) {
|
|
141
|
-
throw new Error(
|
|
142
|
-
`Refusing non-canonical ${isGeminiPolicy ? 'admin-policy' : 'output-schema'} cleanup: ${cleanupPath}`
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function assertOwnedSchemaFile(cleanupPath, metadata) {
|
|
148
|
-
assertCanonicalSchemaPath(cleanupPath, metadata);
|
|
149
|
-
const schemaDirectory = dirname(cleanupPath);
|
|
150
|
-
const [tempRoot, realSchemaDirectory, directoryStat, schemaStat] = await Promise.all([
|
|
151
|
-
realpath(tmpdir()),
|
|
152
|
-
realpath(schemaDirectory),
|
|
153
|
-
lstat(schemaDirectory),
|
|
154
|
-
lstat(cleanupPath),
|
|
155
|
-
]);
|
|
156
|
-
if (
|
|
157
|
-
directoryStat.isSymbolicLink() ||
|
|
158
|
-
!directoryStat.isDirectory() ||
|
|
159
|
-
dirname(realSchemaDirectory) !== tempRoot ||
|
|
160
|
-
basename(realSchemaDirectory) !== basename(schemaDirectory) ||
|
|
161
|
-
schemaStat.isSymbolicLink() ||
|
|
162
|
-
!schemaStat.isFile()
|
|
163
|
-
) {
|
|
164
|
-
throw new Error(`Refusing non-regular or escaped output-schema cleanup: ${cleanupPath}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function assertOwnedSchemaFileSync(cleanupPath, metadata) {
|
|
169
|
-
assertCanonicalSchemaPath(cleanupPath, metadata);
|
|
170
|
-
const schemaDirectory = dirname(cleanupPath);
|
|
171
|
-
const tempRoot = realpathSync(tmpdir());
|
|
172
|
-
const realSchemaDirectory = realpathSync(schemaDirectory);
|
|
173
|
-
const directoryStat = lstatSync(schemaDirectory);
|
|
174
|
-
const schemaStat = lstatSync(cleanupPath);
|
|
175
|
-
if (
|
|
176
|
-
directoryStat.isSymbolicLink() ||
|
|
177
|
-
!directoryStat.isDirectory() ||
|
|
178
|
-
dirname(realSchemaDirectory) !== tempRoot ||
|
|
179
|
-
basename(realSchemaDirectory) !== basename(schemaDirectory) ||
|
|
180
|
-
schemaStat.isSymbolicLink() ||
|
|
181
|
-
!schemaStat.isFile()
|
|
182
|
-
) {
|
|
183
|
-
throw new Error(`Refusing non-regular or escaped output-schema cleanup: ${cleanupPath}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async function removeCleanupPath(cleanupPath, metadata) {
|
|
188
|
-
if (metadata.kind === 'temp-directory') {
|
|
189
|
-
if (assertOwnedTempDirectory(cleanupPath, metadata)) return;
|
|
190
|
-
await rm(cleanupPath, { recursive: true, force: true });
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
await assertOwnedSchemaFile(cleanupPath, metadata);
|
|
194
|
-
await unlink(cleanupPath);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function removeCleanupPathSync(cleanupPath, metadata) {
|
|
198
|
-
if (metadata.kind === 'temp-directory') {
|
|
199
|
-
if (assertOwnedTempDirectory(cleanupPath, metadata)) return;
|
|
200
|
-
rmSync(cleanupPath, { recursive: true, force: true });
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
assertOwnedSchemaFileSync(cleanupPath, metadata);
|
|
204
|
-
unlinkSync(cleanupPath);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Build one idempotent cleanup owner for a provider command. Callers may run it
|
|
209
|
-
* only after the persisted provider termination boundary is terminal.
|
|
210
|
-
*/
|
|
211
|
-
export function createCommandSpecCleanup(commandSpec, logFailure) {
|
|
212
|
-
let started = false;
|
|
213
|
-
let result = true;
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
async run() {
|
|
217
|
-
if (started) return result;
|
|
218
|
-
started = true;
|
|
219
|
-
let cleanupPlan;
|
|
220
|
-
try {
|
|
221
|
-
cleanupPlan = createCleanupPlan(commandSpec);
|
|
222
|
-
} catch (error) {
|
|
223
|
-
logFailure('<command-cleanup>', error);
|
|
224
|
-
result = false;
|
|
225
|
-
return result;
|
|
226
|
-
}
|
|
227
|
-
let succeeded = true;
|
|
228
|
-
for (const { cleanupPath, metadata } of cleanupPlan) {
|
|
229
|
-
try {
|
|
230
|
-
await removeCleanupPath(cleanupPath, metadata);
|
|
231
|
-
} catch (error) {
|
|
232
|
-
if (error?.code === 'ENOENT') continue;
|
|
233
|
-
succeeded = false;
|
|
234
|
-
logFailure(cleanupPath, error);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
result = succeeded;
|
|
238
|
-
return result;
|
|
239
|
-
},
|
|
240
|
-
runSync() {
|
|
241
|
-
if (started) return result;
|
|
242
|
-
started = true;
|
|
243
|
-
let cleanupPlan;
|
|
244
|
-
try {
|
|
245
|
-
cleanupPlan = createCleanupPlan(commandSpec);
|
|
246
|
-
} catch (error) {
|
|
247
|
-
logFailure('<command-cleanup>', error);
|
|
248
|
-
result = false;
|
|
249
|
-
return result;
|
|
250
|
-
}
|
|
251
|
-
let succeeded = true;
|
|
252
|
-
for (const { cleanupPath, metadata } of cleanupPlan) {
|
|
253
|
-
try {
|
|
254
|
-
removeCleanupPathSync(cleanupPath, metadata);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
if (error?.code === 'ENOENT') continue;
|
|
257
|
-
succeeded = false;
|
|
258
|
-
logFailure(cleanupPath, error);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
result = succeeded;
|
|
262
|
-
return result;
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
}
|
|
4
|
+
export const { createCommandSpecCleanup } = require('../src/command-cleanup-ownership');
|
|
@@ -13,8 +13,13 @@ try {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const {
|
|
16
|
+
ABORT_GRACE_MS,
|
|
17
|
+
DEFAULT_OMP_RPC_DECODER_LIMITS,
|
|
18
|
+
EXIT_GRACE_MS,
|
|
16
19
|
NO_MESSAGES_RETURNED,
|
|
20
|
+
OMP_SUPPORTED_VERSION,
|
|
17
21
|
STREAMING_MODE_ERROR,
|
|
22
|
+
buildOmpPrompt,
|
|
18
23
|
buildProviderCommand,
|
|
19
24
|
classifyProviderError,
|
|
20
25
|
detectProviderFatalError,
|
|
@@ -32,6 +37,7 @@ export const {
|
|
|
32
37
|
recoverProviderStructuredOutput,
|
|
33
38
|
resolveProviderCommand,
|
|
34
39
|
resolveModelSpec,
|
|
40
|
+
runOmpRpcTask,
|
|
35
41
|
supportsProviderCapability,
|
|
36
42
|
providerAliasMap,
|
|
37
43
|
providerAliases,
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Watcher process for the rpc-stdio invoke lane (OMP RPC v2). Runs detached from the parent,
|
|
5
|
+
* drives the session over stdio via the shared runOmpRpcTask driver (task-lib/runner.js resolves
|
|
6
|
+
* this script instead of watcher.js whenever the resolved provider's invoke.lane is 'rpc-stdio'),
|
|
7
|
+
* and updates task status on completion. Foreground (contract-invoke.ts) and this detached watcher
|
|
8
|
+
* both call runOmpRpcTask and therefore produce identical result semantics; the only difference is
|
|
9
|
+
* that this watcher persists OmpRpcSpawnEvidence via updateTask before the prompt is written.
|
|
10
|
+
*
|
|
11
|
+
* The prompt itself never appears in argv (`ps` and /proc/<pid>/cmdline would expose it for as long
|
|
12
|
+
* as this watcher lives). task-lib/runner.js sends it over the private stdin pipe described in
|
|
13
|
+
* src/watcher-prompt-channel.js, and OMP is not spawned until a complete payload has arrived.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { appendFileSync } from 'fs';
|
|
17
|
+
import { getTask, updateTask } from './store.js';
|
|
18
|
+
import { createCommandSpecCleanup } from './command-spec-cleanup.js';
|
|
19
|
+
import {
|
|
20
|
+
completeWatcherFailure,
|
|
21
|
+
completePendingWatcherCancellation,
|
|
22
|
+
completeWatcherTask,
|
|
23
|
+
createRpcWatcherOutputRuntime,
|
|
24
|
+
terminateWatcherProvider,
|
|
25
|
+
} from './watcher-output-runtime.js';
|
|
26
|
+
import { createRequire } from 'module';
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const {
|
|
30
|
+
ABORT_GRACE_MS,
|
|
31
|
+
DEFAULT_OMP_RPC_DECODER_LIMITS,
|
|
32
|
+
EXIT_GRACE_MS,
|
|
33
|
+
OMP_SUPPORTED_VERSION,
|
|
34
|
+
runOmpRpcTask,
|
|
35
|
+
} = require('./provider-helper-runtime.js');
|
|
36
|
+
const { receiveWatcherPrompt } = require('../src/watcher-prompt-channel.js');
|
|
37
|
+
|
|
38
|
+
// No overall task timeout: detached tasks run until the provider produces a terminal frame,
|
|
39
|
+
// matching watcher.js's unbounded child.on('close') wait for every other lane.
|
|
40
|
+
const NO_TIMEOUT_MS = 2_147_483_647;
|
|
41
|
+
|
|
42
|
+
const [, , taskId, cwd, logFile, , configJson] = process.argv;
|
|
43
|
+
const config = configJson ? JSON.parse(configJson) : {};
|
|
44
|
+
const commandSpec = config.commandSpec || {};
|
|
45
|
+
|
|
46
|
+
function log(msg) {
|
|
47
|
+
appendFileSync(logFile, msg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function emergencyLog(msg) {
|
|
51
|
+
try {
|
|
52
|
+
log(msg);
|
|
53
|
+
} catch {
|
|
54
|
+
process.stderr.write(msg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const commandCleanup = createCommandSpecCleanup(commandSpec, (cleanupPath, error) => {
|
|
59
|
+
emergencyLog(`[${Date.now()}][CLEANUP] Failed to delete ${cleanupPath}: ${error.message}\n`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const outputRuntime = createRpcWatcherOutputRuntime({ log });
|
|
63
|
+
|
|
64
|
+
let crashStarted = false;
|
|
65
|
+
let spawnEvidence = null;
|
|
66
|
+
|
|
67
|
+
function terminateOwnedProviderBoundary(exitObserved = false) {
|
|
68
|
+
if (!spawnEvidence) return true;
|
|
69
|
+
return terminateWatcherProvider(
|
|
70
|
+
{ pid: spawnEvidence.pid },
|
|
71
|
+
{ exitObserved, platform: process.platform }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function crashWithError(error, source) {
|
|
76
|
+
if (crashStarted) return;
|
|
77
|
+
crashStarted = true;
|
|
78
|
+
await completeWatcherFailure({
|
|
79
|
+
taskId,
|
|
80
|
+
error,
|
|
81
|
+
source,
|
|
82
|
+
commandCleanup,
|
|
83
|
+
terminateProvider: terminateOwnedProviderBoundary,
|
|
84
|
+
updateTask,
|
|
85
|
+
emergencyLog,
|
|
86
|
+
});
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
process.on('uncaughtException', (error) => {
|
|
91
|
+
void crashWithError(error, 'uncaughtException');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
process.on('unhandledRejection', (reason) => {
|
|
95
|
+
void crashWithError(reason, 'unhandledRejection');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
async function run() {
|
|
99
|
+
if (
|
|
100
|
+
await completePendingWatcherCancellation({
|
|
101
|
+
taskId,
|
|
102
|
+
getTask,
|
|
103
|
+
commandCleanup,
|
|
104
|
+
terminateProvider: () => true,
|
|
105
|
+
updateTask,
|
|
106
|
+
emergencyLog,
|
|
107
|
+
})
|
|
108
|
+
) {
|
|
109
|
+
process.exit(0);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fail closed before any provider process exists: an absent, truncated, over-contract, or
|
|
114
|
+
// early-closed prompt channel must end the task through the same ownership-aware cleanup path as
|
|
115
|
+
// any other crash, never spawn OMP with a partial instruction. The 1 MiB ceiling is the pinned
|
|
116
|
+
// physical RPC frame limit — a larger prompt could never be written as a `prompt` command anyway.
|
|
117
|
+
let prompt;
|
|
118
|
+
try {
|
|
119
|
+
prompt = await receiveWatcherPrompt(process.stdin, {
|
|
120
|
+
maxBytes: DEFAULT_OMP_RPC_DECODER_LIMITS.maxPhysicalFrameBytes,
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
await crashWithError(error, 'prompt-channel');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
|
|
129
|
+
const result = await runOmpRpcTask(
|
|
130
|
+
{
|
|
131
|
+
commandSpec: { ...commandSpec, cwd: commandSpec.cwd || cwd },
|
|
132
|
+
prompt,
|
|
133
|
+
expectedVersion: OMP_SUPPORTED_VERSION,
|
|
134
|
+
session: { kind: 'none' },
|
|
135
|
+
signal: controller.signal,
|
|
136
|
+
timeoutMs: NO_TIMEOUT_MS,
|
|
137
|
+
abortGraceMs: ABORT_GRACE_MS,
|
|
138
|
+
exitGraceMs: EXIT_GRACE_MS,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
onSpawn: async (evidence) => {
|
|
142
|
+
spawnEvidence = evidence;
|
|
143
|
+
await updateTask(taskId, {
|
|
144
|
+
pid: evidence.pid,
|
|
145
|
+
processGroupId: evidence.processGroupId,
|
|
146
|
+
terminationStrategy: evidence.terminationStrategy,
|
|
147
|
+
});
|
|
148
|
+
if (getTask(taskId)?.cancelRequested) {
|
|
149
|
+
crashStarted = true;
|
|
150
|
+
controller.abort();
|
|
151
|
+
await completePendingWatcherCancellation({
|
|
152
|
+
taskId,
|
|
153
|
+
getTask,
|
|
154
|
+
commandCleanup,
|
|
155
|
+
terminateProvider: terminateOwnedProviderBoundary,
|
|
156
|
+
updateTask,
|
|
157
|
+
emergencyLog,
|
|
158
|
+
});
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
onEvent: (event) => {
|
|
163
|
+
outputRuntime.logEvent(event);
|
|
164
|
+
return Promise.resolve();
|
|
165
|
+
},
|
|
166
|
+
onSession: async () => {},
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (crashStarted) return; // Cancelled mid-spawn; the onSpawn hook already exited the process.
|
|
171
|
+
|
|
172
|
+
const completion = outputRuntime.complete(result);
|
|
173
|
+
await completeWatcherTask({
|
|
174
|
+
taskId,
|
|
175
|
+
completion,
|
|
176
|
+
commandCleanup,
|
|
177
|
+
terminateProvider: () => terminateOwnedProviderBoundary(true),
|
|
178
|
+
updateTask,
|
|
179
|
+
emergencyLog,
|
|
180
|
+
});
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
run().catch((error) => {
|
|
185
|
+
void crashWithError(error, 'run');
|
|
186
|
+
});
|
package/task-lib/runner.js
CHANGED
|
@@ -6,7 +6,11 @@ import { addTask, generateId, ensureDirs } from './store.js';
|
|
|
6
6
|
import { createRequire } from 'module';
|
|
7
7
|
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
buildOmpPrompt,
|
|
11
|
+
getProviderRegistryEntry,
|
|
12
|
+
prepareSingleAgentProviderCommand,
|
|
13
|
+
} = require('./provider-helper-runtime.js');
|
|
10
14
|
const {
|
|
11
15
|
ISOLATED_SETTINGS_FILE_ENV,
|
|
12
16
|
ISOLATED_SETTINGS_FILE_MARKER,
|
|
@@ -18,6 +22,7 @@ const {
|
|
|
18
22
|
isClaudeSettingsOverlayPath,
|
|
19
23
|
} = require('../src/worktree-claude-config');
|
|
20
24
|
const { TASK_SPAWN_OWNERSHIP_TOKEN_ENV } = require('../src/task-spawn-cleanup-ownership');
|
|
25
|
+
const { sendWatcherPrompt } = require('../src/watcher-prompt-channel');
|
|
21
26
|
export {
|
|
22
27
|
isOwnedProcessTreeRunning,
|
|
23
28
|
isProcessRunning,
|
|
@@ -79,6 +84,12 @@ export function spawnTask(prompt, options = {}) {
|
|
|
79
84
|
logFile,
|
|
80
85
|
finalArgs: commandSpec.args,
|
|
81
86
|
watcherConfig,
|
|
87
|
+
// Prompt bytes never enter argv: the rpc-stdio watcher receives them over a private pipe
|
|
88
|
+
// (src/watcher-prompt-channel.js). Every other lane passes the prompt to the provider through
|
|
89
|
+
// commandSpec.args, which the provider process — not a long-lived watcher — then owns.
|
|
90
|
+
...(isRpcStdioLane(providerName)
|
|
91
|
+
? { rpcPrompt: buildOmpPrompt(prompt, prepared.options || {}) }
|
|
92
|
+
: {}),
|
|
82
93
|
});
|
|
83
94
|
|
|
84
95
|
return task;
|
|
@@ -250,6 +261,13 @@ export function buildTaskRecord({
|
|
|
250
261
|
};
|
|
251
262
|
}
|
|
252
263
|
|
|
264
|
+
function isRpcStdioLane(providerName) {
|
|
265
|
+
return getProviderRegistryEntry(providerName).invoke.lane === 'rpc-stdio';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The returned object is JSON-serialized into the detached watcher's argv, so it must never carry
|
|
269
|
+
// prompt or other task content: `ps` and /proc/<pid>/cmdline expose argv to every local user for
|
|
270
|
+
// the whole lifetime of the watcher.
|
|
253
271
|
function buildWatcherConfig(outputFormat, jsonSchema, options, providerName, commandSpec) {
|
|
254
272
|
return {
|
|
255
273
|
outputFormat,
|
|
@@ -259,13 +277,13 @@ function buildWatcherConfig(outputFormat, jsonSchema, options, providerName, com
|
|
|
259
277
|
provider: providerName,
|
|
260
278
|
command: commandSpec.binary,
|
|
261
279
|
env: commandSpec.env || {},
|
|
262
|
-
commandSpec: buildWatcherCommandSpec(commandSpec),
|
|
280
|
+
commandSpec: buildWatcherCommandSpec(commandSpec, isRpcStdioLane(providerName)),
|
|
263
281
|
};
|
|
264
282
|
}
|
|
265
283
|
|
|
266
|
-
function buildWatcherCommandSpec(commandSpec) {
|
|
284
|
+
function buildWatcherCommandSpec(commandSpec, keepArgs = false) {
|
|
267
285
|
const watcherCommandSpec = { ...commandSpec };
|
|
268
|
-
delete watcherCommandSpec.args;
|
|
286
|
+
if (!keepArgs) delete watcherCommandSpec.args;
|
|
269
287
|
return watcherCommandSpec;
|
|
270
288
|
}
|
|
271
289
|
|
|
@@ -274,6 +292,12 @@ export function shouldUseAttachableWatcher(options, providerName) {
|
|
|
274
292
|
return false;
|
|
275
293
|
}
|
|
276
294
|
|
|
295
|
+
// The rpc-stdio lane owns bidirectional correlated RPC over stdio itself (see
|
|
296
|
+
// omp-rpc-driver.ts) and always uses rpc-watcher.js instead of the attachable PTY watcher.
|
|
297
|
+
if (isRpcStdioLane(providerName)) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
277
301
|
// Claude strict structured output still needs the non-PTY watcher. Claude
|
|
278
302
|
// can treat PTY notifications as streaming commands and reject the run.
|
|
279
303
|
// Other providers, including Codex, support their structured-output mode in
|
|
@@ -282,23 +306,33 @@ export function shouldUseAttachableWatcher(options, providerName) {
|
|
|
282
306
|
}
|
|
283
307
|
|
|
284
308
|
function resolveWatcherScript(options, providerName) {
|
|
309
|
+
if (isRpcStdioLane(providerName)) {
|
|
310
|
+
return join(__dirname, 'rpc-watcher.js');
|
|
311
|
+
}
|
|
285
312
|
const useAttachable = shouldUseAttachableWatcher(options, providerName);
|
|
286
313
|
return useAttachable ? join(__dirname, 'attachable-watcher.js') : join(__dirname, 'watcher.js');
|
|
287
314
|
}
|
|
288
315
|
|
|
289
|
-
function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfig }) {
|
|
316
|
+
function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfig, rpcPrompt }) {
|
|
317
|
+
const sendsPrompt = typeof rpcPrompt === 'string';
|
|
290
318
|
const watcherEnv = buildWatcherEnv();
|
|
291
319
|
const watcher = fork(
|
|
292
320
|
watcherScript,
|
|
293
321
|
[id, cwd, logFile, JSON.stringify(finalArgs), JSON.stringify(watcherConfig)],
|
|
294
322
|
{
|
|
295
323
|
detached: true,
|
|
296
|
-
|
|
324
|
+
// A prompt-carrying lane needs a real pipe on fd 0 to receive it; every other lane keeps
|
|
325
|
+
// stdio fully ignored. fork() requires an explicit 'ipc' slot once stdio is an array.
|
|
326
|
+
stdio: sendsPrompt ? ['pipe', 'ignore', 'ignore', 'ipc'] : 'ignore',
|
|
297
327
|
windowsHide: true,
|
|
298
328
|
env: watcherEnv,
|
|
299
329
|
}
|
|
300
330
|
);
|
|
301
331
|
|
|
332
|
+
// Only the pipe write holds the event loop (until it drains), never the watcher itself: the
|
|
333
|
+
// parent still exits without waiting for the detached task to finish.
|
|
334
|
+
if (sendsPrompt) sendWatcherPrompt(watcher.stdin, rpcPrompt);
|
|
335
|
+
|
|
302
336
|
watcher.unref();
|
|
303
337
|
watcher.disconnect(); // Close IPC channel so parent can exit
|
|
304
338
|
}
|
|
@@ -140,6 +140,38 @@ export function completeWatcherFailure({ error, source, ...completionOptions })
|
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Output runtime for the rpc-stdio lane (see task-lib/rpc-watcher.js). Unlike
|
|
145
|
+
* createWatcherOutputRuntime, there is no raw stdout/stderr byte stream to parse: the
|
|
146
|
+
* OMP RPC driver (omp-rpc-driver.ts) already normalizes every frame into an OutputEvent before
|
|
147
|
+
* calling onEvent, so this runtime only ever logs already-normalized events — never raw RPC
|
|
148
|
+
* frames, prompt text, or control payloads.
|
|
149
|
+
*/
|
|
150
|
+
export function createRpcWatcherOutputRuntime({ log }) {
|
|
151
|
+
function logEvent(event) {
|
|
152
|
+
log(`[${Date.now()}]${JSON.stringify(event)}\n`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function complete(result) {
|
|
156
|
+
const lastResult = [...result.events].reverse().find((event) => event.type === 'result');
|
|
157
|
+
const turnFailed = lastResult !== undefined && lastResult.success === false;
|
|
158
|
+
const success = result.stopReason === 'completed' && !turnFailed;
|
|
159
|
+
const resolvedCode = success ? 0 : 1;
|
|
160
|
+
log(`\n${'='.repeat(50)}\n`);
|
|
161
|
+
log(`Finished: ${new Date().toISOString()}\n`);
|
|
162
|
+
log(
|
|
163
|
+
`Stop reason: ${result.stopReason}, Exit code: ${result.exitCode}, Signal: ${result.signal}\n`
|
|
164
|
+
);
|
|
165
|
+
return {
|
|
166
|
+
resolvedCode,
|
|
167
|
+
status: success ? 'completed' : 'failed',
|
|
168
|
+
error: success ? null : (lastResult && lastResult.error) || result.stopReason,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { logEvent, complete };
|
|
173
|
+
}
|
|
174
|
+
|
|
143
175
|
export function createWatcherOutputRuntime({
|
|
144
176
|
config,
|
|
145
177
|
providerName,
|