@the-open-engine/zeroshot 6.17.0 → 6.18.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 +9 -13
- package/lib/agent-cli-provider/adapters/claude.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +30 -0
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +37 -0
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.js +115 -4
- package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +108 -0
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +12 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +2 -2
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +2 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +19 -7
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +4 -0
- 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 +35 -4
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +30 -3
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster/errors.cjs +4 -1
- package/lib/cluster/errors.d.ts +2 -0
- package/lib/cluster/errors.mjs +2 -0
- package/lib/cluster/index.cjs +8 -1
- package/lib/cluster/index.d.ts +4 -1
- package/lib/cluster/index.mjs +4 -1
- package/lib/cluster/json-source.cjs +61 -0
- package/lib/cluster/json-source.d.ts +4 -0
- package/lib/cluster/json-source.mjs +55 -0
- package/lib/cluster/payload-value.cjs +76 -0
- package/lib/cluster/payload-value.d.ts +7 -0
- package/lib/cluster/payload-value.mjs +72 -0
- package/lib/cluster/validators.cjs +21 -0
- package/lib/cluster/validators.d.ts +4 -1
- package/lib/cluster/validators.mjs +19 -1
- package/lib/provider-names.js +5 -0
- package/package.json +2 -2
- package/src/agent/agent-task-executor.js +194 -410
- package/src/agent/output-extraction.js +39 -14
- package/src/agent/output-reformatter.js +154 -111
- package/src/agent-cli-provider/adapters/claude.ts +39 -2
- package/src/agent-cli-provider/adapters/codex.ts +56 -2
- package/src/agent-cli-provider/adapters/gemini.ts +102 -6
- package/src/agent-cli-provider/adapters/opencode.ts +85 -2
- package/src/agent-cli-provider/contract-options.ts +16 -0
- package/src/agent-cli-provider/index.ts +4 -0
- package/src/agent-cli-provider/provider-registry.ts +23 -3
- package/src/agent-cli-provider/single-agent-runtime.ts +50 -4
- package/src/agent-cli-provider/types.ts +36 -2
- package/src/agent-wrapper.js +2 -2
- package/src/cluster/errors.ts +1 -0
- package/src/cluster/index.ts +4 -0
- package/src/cluster/json-source.ts +71 -0
- package/src/cluster/payload-value.ts +84 -0
- package/src/cluster/validators.ts +31 -2
- package/src/orchestrator.js +33 -13
- package/task-lib/attachable-watcher.js +1 -0
- package/task-lib/command-spec-cleanup.js +57 -11
- package/task-lib/commands/run.js +1 -0
- package/task-lib/provider-session-capture.js +8 -0
- package/task-lib/runner.js +9 -6
- package/task-lib/watcher.js +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ClusterRequestError } from './errors.js';
|
|
2
|
+
import { MAX_FRAME_BYTES } from './generated/protocol.js';
|
|
3
|
+
|
|
4
|
+
export const MAX_REQUEST_BYTES = MAX_FRAME_BYTES;
|
|
5
|
+
|
|
6
|
+
export function decodeBoundedJson(bytes: Uint8Array): unknown {
|
|
7
|
+
if (bytes.length > MAX_REQUEST_BYTES) {
|
|
8
|
+
throw new ClusterRequestError(
|
|
9
|
+
`request payload of ${bytes.length} bytes exceeds the ${MAX_REQUEST_BYTES} byte limit`,
|
|
10
|
+
'OVERSIZED_JSON',
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
let text: string;
|
|
14
|
+
try {
|
|
15
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
16
|
+
} catch {
|
|
17
|
+
throw new ClusterRequestError('request payload is not valid UTF-8', 'INVALID_UTF8');
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(text) as unknown;
|
|
21
|
+
} catch {
|
|
22
|
+
throw new ClusterRequestError('request payload is not valid JSON', 'MALFORMED_JSON');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Callers resolve `--graph`/`--input` specifiers ('-' vs. a file path) to a byte
|
|
27
|
+
// source themselves; opening the filesystem stream stays outside src/cluster so this
|
|
28
|
+
// module keeps no host-specific dependency (not even the `NodeJS` ambient namespace,
|
|
29
|
+
// which isn't resolvable from a consumer's declaration-only install) beyond the
|
|
30
|
+
// standard async-iterable protocol every Node Readable already implements — mirrors
|
|
31
|
+
// how WebSocketLike is injected instead of importing 'ws' directly, see socket.ts.
|
|
32
|
+
export async function readBoundedSource(
|
|
33
|
+
source: AsyncIterable<Uint8Array | string>,
|
|
34
|
+
): Promise<Uint8Array> {
|
|
35
|
+
const encoder = new TextEncoder();
|
|
36
|
+
const chunks: Uint8Array[] = [];
|
|
37
|
+
let total = 0;
|
|
38
|
+
for await (const chunk of source) {
|
|
39
|
+
const bytes = typeof chunk === 'string' ? encoder.encode(chunk) : new Uint8Array(chunk);
|
|
40
|
+
total += bytes.length;
|
|
41
|
+
if (total > MAX_REQUEST_BYTES) {
|
|
42
|
+
throw new ClusterRequestError(
|
|
43
|
+
`request payload exceeds the ${MAX_REQUEST_BYTES} byte limit`,
|
|
44
|
+
'OVERSIZED_JSON',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
chunks.push(bytes);
|
|
48
|
+
}
|
|
49
|
+
const assembled = new Uint8Array(total);
|
|
50
|
+
let offset = 0;
|
|
51
|
+
for (const bytes of chunks) {
|
|
52
|
+
assembled.set(bytes, offset);
|
|
53
|
+
offset += bytes.length;
|
|
54
|
+
}
|
|
55
|
+
return assembled;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function assertDistinctRequestSources(
|
|
59
|
+
graph: string,
|
|
60
|
+
input: string | undefined,
|
|
61
|
+
): asserts input is string {
|
|
62
|
+
if (input === undefined) {
|
|
63
|
+
throw new ClusterRequestError('--input is required for every hosted run', 'MISSING_INPUT');
|
|
64
|
+
}
|
|
65
|
+
if (graph === '-' && input === '-') {
|
|
66
|
+
throw new ClusterRequestError(
|
|
67
|
+
'graph and input cannot both read from stdin',
|
|
68
|
+
'AMBIGUOUS_STDIN_SOURCE',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { PayloadType } from './generated/protocol.js';
|
|
2
|
+
import { ClusterRequestError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
export type InputValidationIssue = {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly code: 'TYPE_MISMATCH' | 'MISSING_REQUIRED_FIELD' | 'UNKNOWN_FIELD' | 'UNKNOWN_ENUM_LABEL';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type InternalIssue = InputValidationIssue & { readonly expectedKind: string };
|
|
10
|
+
|
|
11
|
+
function isJsonInteger(value: unknown): boolean {
|
|
12
|
+
return typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function evaluate(type: PayloadType, value: unknown, path: string): InternalIssue | null {
|
|
16
|
+
switch (type.kind) {
|
|
17
|
+
case 'null':
|
|
18
|
+
return value === null ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'null' };
|
|
19
|
+
case 'boolean':
|
|
20
|
+
return typeof value === 'boolean' ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'boolean' };
|
|
21
|
+
case 'integer':
|
|
22
|
+
return isJsonInteger(value) ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'integer' };
|
|
23
|
+
case 'number':
|
|
24
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
25
|
+
? null
|
|
26
|
+
: { path, code: 'TYPE_MISMATCH', expectedKind: 'number' };
|
|
27
|
+
case 'string':
|
|
28
|
+
return typeof value === 'string' ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'string' };
|
|
29
|
+
case 'enum':
|
|
30
|
+
if (typeof value !== 'string') return { path, code: 'TYPE_MISMATCH', expectedKind: 'enum' };
|
|
31
|
+
return type.values.includes(value)
|
|
32
|
+
? null
|
|
33
|
+
: { path, code: 'UNKNOWN_ENUM_LABEL', expectedKind: 'enum' };
|
|
34
|
+
case 'array': {
|
|
35
|
+
if (!Array.isArray(value)) return { path, code: 'TYPE_MISMATCH', expectedKind: 'array' };
|
|
36
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
37
|
+
const issue = evaluate(type.items, value[index], `${path}/${index}`);
|
|
38
|
+
if (issue) return issue;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
case 'record': {
|
|
43
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
44
|
+
return { path, code: 'TYPE_MISMATCH', expectedKind: 'record' };
|
|
45
|
+
}
|
|
46
|
+
const record = value as Readonly<Record<string, unknown>>;
|
|
47
|
+
const fieldNames = Object.keys(type.fields).sort();
|
|
48
|
+
for (const name of fieldNames) {
|
|
49
|
+
const field = type.fields[name];
|
|
50
|
+
if (!field) continue;
|
|
51
|
+
const fieldPath = `${path}/${name}`;
|
|
52
|
+
if (Object.prototype.hasOwnProperty.call(record, name)) {
|
|
53
|
+
const issue = evaluate(field.type, record[name], fieldPath);
|
|
54
|
+
if (issue) return issue;
|
|
55
|
+
} else if (field.required) {
|
|
56
|
+
return { path: fieldPath, code: 'MISSING_REQUIRED_FIELD', expectedKind: field.type.kind };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const declared = new Set(fieldNames);
|
|
60
|
+
const unknownField = Object.keys(record).sort().find((name) => !declared.has(name));
|
|
61
|
+
return unknownField
|
|
62
|
+
? { path: `${path}/${unknownField}`, code: 'UNKNOWN_FIELD', expectedKind: 'record' }
|
|
63
|
+
: null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function firstInputValidationIssue(
|
|
69
|
+
type: PayloadType,
|
|
70
|
+
value: unknown,
|
|
71
|
+
path = '',
|
|
72
|
+
): InputValidationIssue | null {
|
|
73
|
+
const issue = evaluate(type, value, path);
|
|
74
|
+
return issue ? { path: issue.path, code: issue.code } : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function assertInputValue(type: PayloadType, value: unknown): void {
|
|
78
|
+
const issue = evaluate(type, value, '');
|
|
79
|
+
if (!issue) return;
|
|
80
|
+
throw new ClusterRequestError(
|
|
81
|
+
`input value at '${issue.path || '/'}' failed validation: ${issue.code}, expected ${issue.expectedKind}`,
|
|
82
|
+
'INVALID_INPUT',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
METHOD_RESULT_DEFINITIONS,
|
|
5
5
|
PROTOCOL_VERSION,
|
|
6
6
|
} from './generated/protocol.js';
|
|
7
|
-
import type { ClusterMethod } from './generated/protocol.js';
|
|
7
|
+
import type { ClusterMethod, GraphProfile, GraphSpec, ServerCapabilities } from './generated/protocol.js';
|
|
8
8
|
import { CLUSTER_PROTOCOL_SCHEMA } from './generated/protocol-schema.js';
|
|
9
|
-
import { ClusterProtocolError } from './errors.js';
|
|
9
|
+
import { ClusterProtocolError, ClusterRequestError } from './errors.js';
|
|
10
10
|
|
|
11
11
|
const ajv = new Ajv2020({
|
|
12
12
|
allErrors: true,
|
|
@@ -54,3 +54,32 @@ export function assertMethodResult(method: ClusterMethod, value: unknown): void
|
|
|
54
54
|
throw error;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
function assertRequestDefinition(definition: string, value: unknown, code: string): void {
|
|
59
|
+
const validate = validatorFor(definition);
|
|
60
|
+
if (validate(value)) return;
|
|
61
|
+
const details = (validate.errors ?? []).map((error) =>
|
|
62
|
+
`${error.instancePath || '/'} ${error.message ?? 'is invalid'}`
|
|
63
|
+
).join('; ');
|
|
64
|
+
throw new ClusterRequestError(`${definition} validation failed: ${details}`, code);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function assertGraphSpec(value: unknown): asserts value is GraphSpec {
|
|
68
|
+
assertRequestDefinition('GraphSpec', value, 'INVALID_GRAPH');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function assertGraphProfile(value: unknown): asserts value is GraphProfile {
|
|
72
|
+
assertRequestDefinition('GraphProfile', value, 'INVALID_GRAPH_PROFILE');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function assertGraphProfileSupported(
|
|
76
|
+
profile: GraphProfile,
|
|
77
|
+
capabilities: ServerCapabilities,
|
|
78
|
+
): void {
|
|
79
|
+
if (!capabilities.graphProfiles?.includes(profile)) {
|
|
80
|
+
throw new ClusterRequestError(
|
|
81
|
+
`graph profile ${profile} is not among the server's advertised graphProfiles`,
|
|
82
|
+
'UNSUPPORTED_GRAPH_PROFILE',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/orchestrator.js
CHANGED
|
@@ -270,6 +270,7 @@ class Orchestrator {
|
|
|
270
270
|
|
|
271
271
|
// Track if orchestrator is closed (prevents _saveClusters race conditions during cleanup)
|
|
272
272
|
this.closed = false;
|
|
273
|
+
this._conductorWatchdogs = new Set();
|
|
273
274
|
|
|
274
275
|
// Track if clusters are loaded (for lazy loading pattern)
|
|
275
276
|
this._clustersLoaded = options.skipLoad === true;
|
|
@@ -1654,6 +1655,28 @@ class Orchestrator {
|
|
|
1654
1655
|
const timeoutMs = 30000;
|
|
1655
1656
|
let watchdogTimer = null;
|
|
1656
1657
|
let completedAt = null;
|
|
1658
|
+
const watchdog = {
|
|
1659
|
+
clear: () => {
|
|
1660
|
+
if (!watchdogTimer) {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
clearTimeout(watchdogTimer);
|
|
1664
|
+
watchdogTimer = null;
|
|
1665
|
+
this._conductorWatchdogs.delete(watchdog);
|
|
1666
|
+
const elapsed = completedAt ? Date.now() - completedAt : 0;
|
|
1667
|
+
this._log(
|
|
1668
|
+
`✅ CLUSTER_OPERATIONS received (${elapsed}ms after conductor completed) - watchdog cleared`
|
|
1669
|
+
);
|
|
1670
|
+
},
|
|
1671
|
+
dispose: () => {
|
|
1672
|
+
if (watchdogTimer) {
|
|
1673
|
+
clearTimeout(watchdogTimer);
|
|
1674
|
+
watchdogTimer = null;
|
|
1675
|
+
}
|
|
1676
|
+
this._conductorWatchdogs.delete(watchdog);
|
|
1677
|
+
},
|
|
1678
|
+
};
|
|
1679
|
+
this._conductorWatchdogs.add(watchdog);
|
|
1657
1680
|
|
|
1658
1681
|
this._subscribeToClusterTopic(messageBus, clusterId, 'AGENT_LIFECYCLE', (message) => {
|
|
1659
1682
|
const event = message.content?.data?.event;
|
|
@@ -1666,6 +1689,11 @@ class Orchestrator {
|
|
|
1666
1689
|
);
|
|
1667
1690
|
|
|
1668
1691
|
watchdogTimer = setTimeout(() => {
|
|
1692
|
+
watchdogTimer = null;
|
|
1693
|
+
this._conductorWatchdogs.delete(watchdog);
|
|
1694
|
+
if (this.closed || messageBus._closed) {
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1669
1697
|
const clusterOps = messageBus.query({
|
|
1670
1698
|
cluster_id: clusterId,
|
|
1671
1699
|
topic: 'CLUSTER_OPERATIONS',
|
|
@@ -1700,19 +1728,7 @@ class Orchestrator {
|
|
|
1700
1728
|
}
|
|
1701
1729
|
});
|
|
1702
1730
|
|
|
1703
|
-
return
|
|
1704
|
-
clear: () => {
|
|
1705
|
-
if (!watchdogTimer) {
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1708
|
-
clearTimeout(watchdogTimer);
|
|
1709
|
-
watchdogTimer = null;
|
|
1710
|
-
const elapsed = completedAt ? Date.now() - completedAt : 0;
|
|
1711
|
-
this._log(
|
|
1712
|
-
`✅ CLUSTER_OPERATIONS received (${elapsed}ms after conductor completed) - watchdog cleared`
|
|
1713
|
-
);
|
|
1714
|
-
},
|
|
1715
|
-
};
|
|
1731
|
+
return watchdog;
|
|
1716
1732
|
}
|
|
1717
1733
|
|
|
1718
1734
|
_registerClusterOperationsHandler(
|
|
@@ -2414,6 +2430,10 @@ class Orchestrator {
|
|
|
2414
2430
|
return;
|
|
2415
2431
|
}
|
|
2416
2432
|
this.closed = true;
|
|
2433
|
+
for (const watchdog of this._conductorWatchdogs) {
|
|
2434
|
+
watchdog.dispose();
|
|
2435
|
+
}
|
|
2436
|
+
this._conductorWatchdogs.clear();
|
|
2417
2437
|
|
|
2418
2438
|
for (const cluster of this.clusters.values()) {
|
|
2419
2439
|
if (typeof cluster.snapshotter?.stop === 'function') {
|
|
@@ -111,6 +111,7 @@ const providerSessionCapture = createProviderSessionCapture({
|
|
|
111
111
|
requestedSessionId: persistedTask?.requestedResumeSessionId || null,
|
|
112
112
|
initialSessionId: persistedTask?.sessionId || null,
|
|
113
113
|
initialSessionIdConflict: persistedTask?.sessionIdConflict === true,
|
|
114
|
+
disabled: config.structuredOutputRecovery === true,
|
|
114
115
|
});
|
|
115
116
|
let outputBuffer = '';
|
|
116
117
|
|
|
@@ -14,6 +14,10 @@ const CLEANUP_METADATA_KEYS = ['kind', 'path', 'provider', 'reason'];
|
|
|
14
14
|
const SCHEMA_DIRECTORY_PATTERN = /^zeroshot-schema-[A-Za-z0-9_-]+$/u;
|
|
15
15
|
const SCHEMA_FILE_PATTERN =
|
|
16
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;
|
|
17
21
|
|
|
18
22
|
function assertClosedCleanupMetadata(cleanupPath, metadata) {
|
|
19
23
|
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
@@ -59,6 +63,37 @@ function createCleanupPlan(commandSpec) {
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
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
|
+
|
|
62
97
|
if (
|
|
63
98
|
metadata.provider !== 'claude' ||
|
|
64
99
|
metadata.reason !== 'settings-overlay' ||
|
|
@@ -79,22 +114,33 @@ function assertOwnedTempDirectory(cleanupPath, metadata) {
|
|
|
79
114
|
}
|
|
80
115
|
|
|
81
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';
|
|
82
125
|
if (
|
|
83
|
-
|
|
84
|
-
metadata.provider !== 'codex' ||
|
|
85
|
-
metadata.reason !== 'output-schema' ||
|
|
126
|
+
(!isCodexSchema && !isGeminiPolicy) ||
|
|
86
127
|
!isAbsolute(cleanupPath) ||
|
|
87
128
|
resolve(cleanupPath) !== cleanupPath
|
|
88
129
|
) {
|
|
89
|
-
throw new Error(
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Refusing unowned ${isGeminiPolicy ? 'admin-policy' : 'output-schema'} cleanup: ${cleanupPath}`
|
|
132
|
+
);
|
|
90
133
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
);
|
|
98
144
|
}
|
|
99
145
|
}
|
|
100
146
|
|
package/task-lib/commands/run.js
CHANGED
|
@@ -40,6 +40,7 @@ export async function runTask(prompt, options = {}) {
|
|
|
40
40
|
jsonSchema,
|
|
41
41
|
mcpConfig: options.mcpConfig,
|
|
42
42
|
silentJsonOutput,
|
|
43
|
+
structuredOutputRecovery: options.structuredOutputRecovery === true,
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
console.log(chalk.green(`\n✓ Task spawned: ${chalk.cyan(task.id)}`));
|
|
@@ -71,7 +71,15 @@ export function createProviderSessionCapture({
|
|
|
71
71
|
requestedSessionId = null,
|
|
72
72
|
initialSessionId = null,
|
|
73
73
|
initialSessionIdConflict = false,
|
|
74
|
+
disabled = false,
|
|
74
75
|
}) {
|
|
76
|
+
if (disabled) {
|
|
77
|
+
return {
|
|
78
|
+
captureLine() {},
|
|
79
|
+
getCompletionError: () => null,
|
|
80
|
+
getCompletionUpdate: () => ({ resumeIdentityVerified: false }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
75
83
|
let currentSessionId = initialSessionId;
|
|
76
84
|
let sessionIdConflict = initialSessionIdConflict;
|
|
77
85
|
let persistenceError = null;
|
package/task-lib/runner.js
CHANGED
|
@@ -116,19 +116,21 @@ function resolveJsonSchema(options, outputFormat) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
function buildProviderOptions(options, runtime, modelSelection) {
|
|
119
|
+
const structuredOutputRecovery = options.structuredOutputRecovery === true;
|
|
119
120
|
return {
|
|
120
121
|
outputFormat: runtime.outputFormat,
|
|
121
122
|
jsonSchema: runtime.jsonSchema,
|
|
122
123
|
cwd: runtime.cwd,
|
|
123
|
-
autoApprove:
|
|
124
|
+
autoApprove: !structuredOutputRecovery,
|
|
124
125
|
...(modelSelection === undefined ? {} : { modelSpec: modelSelection.modelSpec }),
|
|
125
|
-
...mcpConfigOption(options),
|
|
126
|
+
...(structuredOutputRecovery ? {} : mcpConfigOption(options)),
|
|
126
127
|
...claudeSettingsFileOption(),
|
|
127
|
-
...(options.resume ? { resumeSessionId: options.resume } : {}),
|
|
128
|
+
...(!structuredOutputRecovery && options.resume ? { resumeSessionId: options.resume } : {}),
|
|
128
129
|
...(process.env.ZEROSHOT_OPENCODE_AGENT?.trim()
|
|
129
130
|
? { agentName: process.env.ZEROSHOT_OPENCODE_AGENT.trim() }
|
|
130
131
|
: {}),
|
|
131
|
-
...(options.continue ? { continueSession: true } : {}),
|
|
132
|
+
...(!structuredOutputRecovery && options.continue ? { continueSession: true } : {}),
|
|
133
|
+
...(structuredOutputRecovery ? { structuredOutputRecovery: true } : {}),
|
|
132
134
|
};
|
|
133
135
|
}
|
|
134
136
|
|
|
@@ -218,10 +220,10 @@ export function buildTaskRecord({
|
|
|
218
220
|
// accepted that session identity.
|
|
219
221
|
sessionId: null,
|
|
220
222
|
sessionIdConflict: false,
|
|
221
|
-
requestedResumeSessionId: options.resume || null,
|
|
223
|
+
requestedResumeSessionId: options.structuredOutputRecovery ? null : options.resume || null,
|
|
222
224
|
// Resumed tasks start fail-closed. Only the watcher terminal transaction
|
|
223
225
|
// may prove that the requested identity completed without conflict.
|
|
224
|
-
resumeIdentityVerified: !options.resume,
|
|
226
|
+
resumeIdentityVerified: options.structuredOutputRecovery || !options.resume,
|
|
225
227
|
logFile,
|
|
226
228
|
createdAt: new Date().toISOString(),
|
|
227
229
|
updatedAt: new Date().toISOString(),
|
|
@@ -253,6 +255,7 @@ function buildWatcherConfig(outputFormat, jsonSchema, options, providerName, com
|
|
|
253
255
|
outputFormat,
|
|
254
256
|
jsonSchema,
|
|
255
257
|
silentJsonOutput: options.silentJsonOutput || false,
|
|
258
|
+
structuredOutputRecovery: options.structuredOutputRecovery === true,
|
|
256
259
|
provider: providerName,
|
|
257
260
|
command: commandSpec.binary,
|
|
258
261
|
env: commandSpec.env || {},
|
package/task-lib/watcher.js
CHANGED
|
@@ -64,6 +64,7 @@ const providerSessionCapture = createProviderSessionCapture({
|
|
|
64
64
|
requestedSessionId: storedTask?.requestedResumeSessionId || null,
|
|
65
65
|
initialSessionId: storedTask?.sessionId || null,
|
|
66
66
|
initialSessionIdConflict: storedTask?.sessionIdConflict === true,
|
|
67
|
+
disabled: config.structuredOutputRecovery === true,
|
|
67
68
|
});
|
|
68
69
|
|
|
69
70
|
let crashStarted = false;
|