@the-open-engine/zeroshot 6.39.1 → 6.39.2

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.
Files changed (73) hide show
  1. package/README.md +33 -1
  2. package/cli/index.js +30 -7
  3. package/cluster-templates/conductor-bootstrap.json +1 -1
  4. package/lib/agent-cli-provider/adapters/claude.js +1 -1
  5. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  7. package/lib/agent-cli-provider/adapters/codex.js +26 -1
  8. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  9. package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/adapters/omp.js +1 -1
  11. package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
  12. package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts +4 -0
  13. package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts.map +1 -0
  14. package/lib/agent-cli-provider/omp/sdk-environment-policy.js +37 -0
  15. package/lib/agent-cli-provider/omp/sdk-environment-policy.js.map +1 -0
  16. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.d.ts.map +1 -1
  17. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js +4 -15
  18. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js.map +1 -1
  19. package/lib/agent-cli-provider/omp/sdk-process-result.d.ts +2 -0
  20. package/lib/agent-cli-provider/omp/sdk-process-result.d.ts.map +1 -1
  21. package/lib/agent-cli-provider/omp/sdk-process-result.js +2 -2
  22. package/lib/agent-cli-provider/omp/sdk-process-result.js.map +1 -1
  23. package/lib/agent-cli-provider/omp/sdk-process-runner.d.ts.map +1 -1
  24. package/lib/agent-cli-provider/omp/sdk-process-runner.js +6 -1
  25. package/lib/agent-cli-provider/omp/sdk-process-runner.js.map +1 -1
  26. package/lib/agent-cli-provider/provider-registry.d.ts +3 -1
  27. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  28. package/lib/agent-cli-provider/provider-registry.js +9 -2
  29. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  30. package/lib/agent-cli-provider/single-agent-runtime.d.ts +4 -1
  31. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  32. package/lib/agent-cli-provider/single-agent-runtime.js +18 -15
  33. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  34. package/lib/agent-cli-provider/types.d.ts +3 -0
  35. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  36. package/lib/agent-cli-provider/types.js.map +1 -1
  37. package/lib/cluster-worker/engine-adapter-common.js +18 -0
  38. package/lib/cluster-worker/engine-adapter.js +2 -1
  39. package/lib/git-remote-utils.js +22 -5
  40. package/npm-shrinkwrap.json +2 -2
  41. package/package.json +2 -1
  42. package/scripts/omp/runtime-identities.js +31 -18
  43. package/scripts/omp/runtime.js +2 -0
  44. package/src/agent/agent-lifecycle.js +17 -1
  45. package/src/agent/output-extraction-failures.js +73 -0
  46. package/src/agent/output-extraction-failures.ts +100 -0
  47. package/src/agent/output-extraction-types.ts +5 -0
  48. package/src/agent/provider-terminal-failure.js +2 -2
  49. package/src/agent/provider-terminal-failure.ts +4 -2
  50. package/src/agent-cli-provider/adapters/claude.ts +1 -1
  51. package/src/agent-cli-provider/adapters/codex.ts +36 -1
  52. package/src/agent-cli-provider/adapters/omp.ts +2 -1
  53. package/src/agent-cli-provider/omp/sdk-environment-policy.ts +38 -0
  54. package/src/agent-cli-provider/omp/sdk-process-private-runtime.ts +4 -16
  55. package/src/agent-cli-provider/omp/sdk-process-result.ts +4 -2
  56. package/src/agent-cli-provider/omp/sdk-process-runner.ts +7 -1
  57. package/src/agent-cli-provider/provider-registry.ts +10 -2
  58. package/src/agent-cli-provider/single-agent-runtime.ts +24 -15
  59. package/src/agent-cli-provider/types.ts +3 -0
  60. package/src/agents/git-pusher-template.js +61 -71
  61. package/src/attach/socket-paths.js +1 -1
  62. package/src/attach/socket-paths.ts +1 -1
  63. package/src/foreground-benchmark-run.js +24 -12
  64. package/src/legacy-lib/git-remote-utils.ts +23 -5
  65. package/src/omp-session-verifier.js +8 -5
  66. package/src/orchestrator.js +42 -0
  67. package/src/preflight.js +31 -6
  68. package/src/providers/index.js +3 -3
  69. package/src/task-execution-context.js +22 -0
  70. package/src/watcher-prompt-channel.js +1 -1
  71. package/src/worktree-claude-config.js +25 -0
  72. package/src/worktree-claude-config.ts +28 -0
  73. package/task-lib/runner.js +57 -22
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const { MAX_SUMMARY_BYTES } = require('./contracts');
4
+
5
+ const DEFAULT_COMPLETION_SUMMARY = 'Cluster completed';
3
6
  const PUBLIC_ADAPTER_METHODS = Object.freeze([
4
7
  'start',
5
8
  'status',
@@ -8,6 +11,20 @@ const PUBLIC_ADAPTER_METHODS = Object.freeze([
8
11
  'close',
9
12
  ]);
10
13
 
14
+ function completionSummary(value) {
15
+ const summary = typeof value === 'string' ? value : DEFAULT_COMPLETION_SUMMARY;
16
+ if (Buffer.byteLength(summary, 'utf8') <= MAX_SUMMARY_BYTES) return summary;
17
+ let bounded = '';
18
+ let bytes = 0;
19
+ for (const character of summary) {
20
+ const characterBytes = Buffer.byteLength(character, 'utf8');
21
+ if (bytes + characterBytes > MAX_SUMMARY_BYTES) break;
22
+ bounded += character;
23
+ bytes += characterBytes;
24
+ }
25
+ return bounded;
26
+ }
27
+
11
28
  function createAdapterFacade(adapter) {
12
29
  return Object.freeze(
13
30
  Object.fromEntries(PUBLIC_ADAPTER_METHODS.map((name) => [name, adapter[name].bind(adapter)]))
@@ -30,6 +47,7 @@ function frozenResourceStatus(resource, state, details) {
30
47
  }
31
48
 
32
49
  module.exports = {
50
+ completionSummary,
33
51
  createAdapterFacade,
34
52
  declaredFailureEvent,
35
53
  frozenResourceStatus,
@@ -3,6 +3,7 @@
3
3
  const { cloneJson } = require('./object-utils');
4
4
  const { inputFromRequest } = require('./engine-input');
5
5
  const {
6
+ completionSummary,
6
7
  createAdapterFacade,
7
8
  declaredFailureEvent,
8
9
  frozenResourceStatus,
@@ -26,7 +27,7 @@ function terminalEventFromMessage(message) {
26
27
  if (message.topic === 'CLUSTER_COMPLETE') {
27
28
  return {
28
29
  type: 'complete',
29
- summary: data.reason || message.content?.text,
30
+ summary: completionSummary(data.reason || message.content?.text),
30
31
  ...completionPayload(data),
31
32
  };
32
33
  }
@@ -124,6 +124,24 @@ function parseGitRemoteUrl(remoteUrl) {
124
124
  }
125
125
  return null;
126
126
  }
127
+ function parseFetchRemoteLine(line) {
128
+ // Partial clones append their filter after the fetch marker, for example
129
+ // `origin https://github.com/org/repo.git (fetch) [blob:none]`.
130
+ const fetchMarker = ' (fetch)';
131
+ const fetchMarkerIndex = line.lastIndexOf(fetchMarker);
132
+ if (fetchMarkerIndex < 1)
133
+ return null;
134
+ const suffix = line.slice(fetchMarkerIndex + fetchMarker.length);
135
+ if (suffix.length > 0 && !(suffix.startsWith(' [') && suffix.endsWith(']')))
136
+ return null;
137
+ const remoteLine = line.slice(0, fetchMarkerIndex);
138
+ const separatorIndex = remoteLine.search(/\s/);
139
+ if (separatorIndex < 1) {
140
+ return null;
141
+ }
142
+ const remoteUrl = remoteLine.slice(separatorIndex).trim();
143
+ return remoteUrl.length > 0 ? [remoteLine.slice(0, separatorIndex), remoteUrl] : null;
144
+ }
127
145
  /**
128
146
  * Detect git repository context from current working directory.
129
147
  * Returns provider context extracted from git remote URL.
@@ -147,14 +165,13 @@ function detectGitContext(cwd = process.cwd()) {
147
165
  });
148
166
  const supportedRemotes = new Map();
149
167
  for (const line of remoteOutput.split(/\r?\n/)) {
150
- const match = /^(\S+)\s+(.+)\s+\(fetch\)$/.exec(line);
151
- if (!match) {
168
+ const remoteParts = parseFetchRemoteLine(line);
169
+ if (!remoteParts) {
152
170
  continue;
153
171
  }
154
- const remoteCandidate = match[1];
155
- const remoteUrl = match[2];
172
+ const [remoteCandidate, remoteUrl] = remoteParts;
156
173
  const remote = normalizeGitRemoteName(remoteCandidate);
157
- if (!remote || remoteUrl === undefined) {
174
+ if (!remote) {
158
175
  continue;
159
176
  }
160
177
  const context = parseGitRemoteUrl(remoteUrl);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.39.1",
3
+ "version": "6.39.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@the-open-engine/zeroshot",
9
- "version": "6.39.1",
9
+ "version": "6.39.2",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.39.1",
3
+ "version": "6.39.2",
4
4
  "description": "Independent executor–verifier orchestration for software changes.",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -47,6 +47,7 @@
47
47
  "typecheck:task-lib": "tsc --project tsconfig.task-lib.json",
48
48
  "typecheck:hosted-target": "tsc --project tsconfig.hosted-target.json",
49
49
  "typecheck:hosted-session": "tsc --project tsconfig.hosted-session.json",
50
+ "pretest:hosted": "npm run build:legacy-runtime",
50
51
  "test:hosted": "npm run build:cluster && node --test tests/private-hosted-cli/*.test.js && npm run test:hosted-target",
51
52
  "test:hosted-target": "npm run build:cluster && npm run build:target && node --test tests/hosted-target/*.test.ts",
52
53
  "test:target": "npm run build:agent-cli-provider && npm run build:target && node --test tests/target/*.test.ts",
@@ -10,6 +10,7 @@ const BUN_PACKAGE_NAME = 'bun';
10
10
  const BUN_RUNTIME_VERSION = '1.3.14';
11
11
  const SIDECAR_RELATIVE_PATH = path.join('scripts', 'omp', 'sidecar.ts');
12
12
  const HOST_SUPERVISOR_RELATIVE_PATH = path.join('scripts', 'omp', 'host-supervisor.ts');
13
+ const ACTIVE_CONTAINER_INVOCATION = Symbol('active-container-invocation');
13
14
  const HOST_SDK_PLATFORM_KEYS = Object.freeze(['linux:arm64', 'linux:x64']);
14
15
 
15
16
  const BUN_PLATFORM_PACKAGES = Object.freeze({
@@ -227,24 +228,26 @@ function resolveOmpSdkRuntime(options = {}) {
227
228
  if (!fileExists(hostSupervisorPath)) {
228
229
  throw new Error(`OMP SDK host supervisor is missing: ${hostSupervisorPath}`);
229
230
  }
230
- let containmentProbe;
231
- try {
232
- containmentProbe = JSON.parse(
233
- String(readContainmentProbe(bunExecutable, hostSupervisorPath)).trim()
234
- );
235
- } catch (error) {
236
- throw new Error(`Unable to attest Linux subreaper/pidfd containment: ${error.message}`);
237
- }
238
- if (
239
- containmentProbe?.protocolVersion !== 1 ||
240
- containmentProbe?.type !== 'cleanup-attestation' ||
241
- containmentProbe?.status !== 'clean' ||
242
- containmentProbe?.mode !== 'linux-subreaper-pidfd' ||
243
- containmentProbe?.subreaper !== true ||
244
- containmentProbe?.pidfd !== true ||
245
- containmentProbe?.ownedProcessCount !== 0
246
- ) {
247
- throw new Error('Linux subreaper/pidfd containment probe returned invalid evidence');
231
+ if (runtimeOptions.activeInvocation !== ACTIVE_CONTAINER_INVOCATION) {
232
+ let containmentProbe;
233
+ try {
234
+ containmentProbe = JSON.parse(
235
+ String(readContainmentProbe(bunExecutable, hostSupervisorPath)).trim()
236
+ );
237
+ } catch (error) {
238
+ throw new Error(`Unable to attest Linux subreaper/pidfd containment: ${error.message}`);
239
+ }
240
+ if (
241
+ containmentProbe?.protocolVersion !== 1 ||
242
+ containmentProbe?.type !== 'cleanup-attestation' ||
243
+ containmentProbe?.status !== 'clean' ||
244
+ containmentProbe?.mode !== 'linux-subreaper-pidfd' ||
245
+ containmentProbe?.subreaper !== true ||
246
+ containmentProbe?.pidfd !== true ||
247
+ containmentProbe?.ownedProcessCount !== 0
248
+ ) {
249
+ throw new Error('Linux subreaper/pidfd containment probe returned invalid evidence');
250
+ }
248
251
  }
249
252
  return Object.freeze({
250
253
  bunExecutable,
@@ -261,6 +264,15 @@ function resolveOmpSdkRuntime(options = {}) {
261
264
  sidecarPath,
262
265
  });
263
266
  }
267
+
268
+ function resolveOmpSdkContainerRuntime(options = {}) {
269
+ // Image inspection may run as PID 1, where no parent process exists to supervise. The actual
270
+ // provider invocation still starts the supervisor and requires its final cleanup attestation.
271
+ return resolveOmpSdkRuntime({
272
+ ...options,
273
+ activeInvocation: ACTIVE_CONTAINER_INVOCATION,
274
+ });
275
+ }
264
276
  module.exports = {
265
277
  BUN_PACKAGE_INTEGRITY,
266
278
  BUN_PACKAGE_NAME,
@@ -279,5 +291,6 @@ module.exports = {
279
291
  REQUIRED_OMP_SDK_SOURCES,
280
292
  SIDECAR_RELATIVE_PATH,
281
293
  assertPackageIdentity,
294
+ resolveOmpSdkContainerRuntime,
282
295
  resolveOmpSdkRuntime,
283
296
  };
@@ -12,6 +12,7 @@ const {
12
12
  REQUIRED_OMP_SDK_SOURCES,
13
13
  SIDECAR_RELATIVE_PATH,
14
14
  assertPackageIdentity,
15
+ resolveOmpSdkContainerRuntime,
15
16
  resolveOmpSdkRuntime,
16
17
  } = require('./runtime-identities');
17
18
  const { validateOmpSdkReleaseAssets } = require('./runtime-release');
@@ -28,6 +29,7 @@ module.exports = {
28
29
  REQUIRED_OMP_SDK_SOURCES,
29
30
  SIDECAR_RELATIVE_PATH,
30
31
  assertPackageIdentity,
32
+ resolveOmpSdkContainerRuntime,
31
33
  resolveOmpSdkRuntime,
32
34
  validateOmpSdkReleaseAssets,
33
35
  };
@@ -221,6 +221,7 @@ function start(agent) {
221
221
  */
222
222
  async function stop(agent) {
223
223
  stopLivenessCheck(agent);
224
+ agent._cancelStartDelay?.();
224
225
 
225
226
  const hasNestedExecutions = agent.nestedExecutions?.hasActive === true;
226
227
  if (!agent.running && !agent.currentTask && !hasNestedExecutions && !agent._currentExecution) {
@@ -440,7 +441,21 @@ async function applyValidatorJitter(agent) {
440
441
  `[Agent ${agent.id}] Adding ${Math.round(jitterMs / 1000)}s jitter to prevent lock contention`
441
442
  );
442
443
  }
443
- await new Promise((resolve) => setTimeout(resolve, jitterMs));
444
+ await new Promise((resolve) => {
445
+ let settled = false;
446
+ let timer;
447
+ let cancel;
448
+ const finish = () => {
449
+ if (settled) return;
450
+ settled = true;
451
+ clearTimeout(timer);
452
+ if (agent._cancelStartDelay === cancel) agent._cancelStartDelay = null;
453
+ resolve();
454
+ };
455
+ cancel = finish;
456
+ timer = setTimeout(finish, jitterMs);
457
+ agent._cancelStartDelay = cancel;
458
+ });
444
459
  }
445
460
 
446
461
  function publishTaskStarted(agent, triggeringMessage) {
@@ -641,6 +656,7 @@ async function runTaskAttempt(agent, triggeringMessage) {
641
656
  // Spawn provider task
642
657
  agent.state = 'executing_task';
643
658
  await applyValidatorJitter(agent);
659
+ if (!agent.running) return;
644
660
  publishTaskStarted(agent, triggeringMessage);
645
661
 
646
662
  let result;
@@ -5,6 +5,20 @@ const piExtraction = require("./output-extraction-pi");
5
5
  const { MAX_CLI_ERROR_BYTES, cliErrorDetail } = errorDetail;
6
6
  const { isObjectRecord, parseJsonRecordLine } = jsonExtraction;
7
7
  const { extractSettledPiFailure } = piExtraction;
8
+ const OMP_SDK_ERROR_CODES = new Map([
9
+ ['invalid-request', 'request'],
10
+ ['model-resolution', 'model'],
11
+ ['model-fallback', 'model'],
12
+ ['provider-auth', 'auth'],
13
+ ['provider-rate-limit', 'rate-limit'],
14
+ ['provider-timeout', 'timeout'],
15
+ ['provider-error', 'provider'],
16
+ ['schema-violation', 'schema'],
17
+ ['cancelled', 'cancelled'],
18
+ ['sdk-error', 'sdk'],
19
+ ['cleanup-error', 'cleanup'],
20
+ ['internal-error', 'internal'],
21
+ ]);
8
22
  function firstTruthyValue(...values) {
9
23
  for (const value of values) {
10
24
  if (value)
@@ -54,6 +68,63 @@ function opencodeFailureFromObject(value) {
54
68
  const source = firstTruthyValue(recordField(recordField(error, 'data'), 'message'), recordField(error, 'message'), recordField(error, 'name'));
55
69
  return { ...cliErrorDetail(source, 'Session error'), provider: 'opencode' };
56
70
  }
71
+ function hasExactKeys(value, expected) {
72
+ return (Object.keys(value).length === expected.length &&
73
+ expected.every((key) => Object.prototype.hasOwnProperty.call(value, key)));
74
+ }
75
+ function isOmpSdkIdentity(value, nameField, expectedName, expectedVersion) {
76
+ return (isObjectRecord(value) &&
77
+ value[nameField] === expectedName &&
78
+ value.version === expectedVersion &&
79
+ hasExactKeys(value, [nameField, 'version']));
80
+ }
81
+ function ompSdkErrorMetadata(value) {
82
+ if (!isObjectRecord(value))
83
+ return null;
84
+ const { category, code, redacted, retryable } = value;
85
+ if (typeof code !== 'string' ||
86
+ typeof category !== 'string' ||
87
+ OMP_SDK_ERROR_CODES.get(code) !== category ||
88
+ typeof retryable !== 'boolean' ||
89
+ redacted !== true ||
90
+ !hasExactKeys(value, ['category', 'code', 'redacted', 'retryable'])) {
91
+ return null;
92
+ }
93
+ return { category, code, retryable };
94
+ }
95
+ function ompProviderCategory(error) {
96
+ if (error.category === 'auth')
97
+ return 'authentication';
98
+ return error.retryable ? 'transient' : 'permanent';
99
+ }
100
+ function isOmpSdkRunId(value) {
101
+ return (typeof value === 'string' &&
102
+ value.length <= 128 &&
103
+ /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value));
104
+ }
105
+ function isOmpSdkErrorEnvelope(value) {
106
+ return (value.protocolVersion === 1 &&
107
+ value.type === 'error' &&
108
+ isOmpSdkRunId(value.runId) &&
109
+ hasExactKeys(value, ['backend', 'error', 'protocolVersion', 'runId', 'runtime', 'type']) &&
110
+ isOmpSdkIdentity(value.backend, 'id', 'omp-sdk', '17.2.1') &&
111
+ isOmpSdkIdentity(value.runtime, 'name', 'bun', '1.3.14'));
112
+ }
113
+ function ompSdkFailureFromObject(value) {
114
+ const error = ompSdkErrorMetadata(value.error);
115
+ if (error === null || !isOmpSdkErrorEnvelope(value))
116
+ return null;
117
+ const detail = `OMP SDK ${error.code} (${error.category})`;
118
+ return {
119
+ ...cliErrorDetail(detail, 'OMP SDK turn failed'),
120
+ provider: 'omp',
121
+ providerCategory: ompProviderCategory(error),
122
+ providerClassification: {
123
+ retryable: error.retryable,
124
+ kind: error.retryable ? 'unknown-retryable' : 'permanent-pattern',
125
+ },
126
+ };
127
+ }
57
128
  function failureFromProviderObject(value, providerName) {
58
129
  if (!isObjectRecord(value))
59
130
  return null;
@@ -65,6 +136,8 @@ function failureFromProviderObject(value, providerName) {
65
136
  return geminiFailureFromObject(value);
66
137
  if (providerName === 'opencode')
67
138
  return opencodeFailureFromObject(value);
139
+ if (providerName === 'omp')
140
+ return ompSdkFailureFromObject(value);
68
141
  return null;
69
142
  }
70
143
  function vertexModelFromResult(parsed, useVertex) {
@@ -12,6 +12,21 @@ const { MAX_CLI_ERROR_BYTES, cliErrorDetail } = errorDetail;
12
12
  const { isObjectRecord, parseJsonRecordLine } = jsonExtraction;
13
13
  const { extractSettledPiFailure } = piExtraction;
14
14
 
15
+ const OMP_SDK_ERROR_CODES = new Map([
16
+ ['invalid-request', 'request'],
17
+ ['model-resolution', 'model'],
18
+ ['model-fallback', 'model'],
19
+ ['provider-auth', 'auth'],
20
+ ['provider-rate-limit', 'rate-limit'],
21
+ ['provider-timeout', 'timeout'],
22
+ ['provider-error', 'provider'],
23
+ ['schema-violation', 'schema'],
24
+ ['cancelled', 'cancelled'],
25
+ ['sdk-error', 'sdk'],
26
+ ['cleanup-error', 'cleanup'],
27
+ ['internal-error', 'internal'],
28
+ ]);
29
+
15
30
  function firstTruthyValue(...values: readonly unknown[]): unknown {
16
31
  for (const value of values) {
17
32
  if (value) return value;
@@ -71,12 +86,97 @@ function opencodeFailureFromObject(value: JsonRecord): CliFailure | null {
71
86
  return { ...cliErrorDetail(source, 'Session error'), provider: 'opencode' };
72
87
  }
73
88
 
89
+ function hasExactKeys(value: JsonRecord, expected: readonly string[]): boolean {
90
+ return (
91
+ Object.keys(value).length === expected.length &&
92
+ expected.every((key) => Object.prototype.hasOwnProperty.call(value, key))
93
+ );
94
+ }
95
+
96
+ function isOmpSdkIdentity(
97
+ value: unknown,
98
+ nameField: 'id' | 'name',
99
+ expectedName: string,
100
+ expectedVersion: string
101
+ ): boolean {
102
+ return (
103
+ isObjectRecord(value) &&
104
+ value[nameField] === expectedName &&
105
+ value.version === expectedVersion &&
106
+ hasExactKeys(value, [nameField, 'version'])
107
+ );
108
+ }
109
+
110
+ interface OmpSdkErrorMetadata {
111
+ readonly category: string;
112
+ readonly code: string;
113
+ readonly retryable: boolean;
114
+ }
115
+
116
+ function ompSdkErrorMetadata(value: unknown): OmpSdkErrorMetadata | null {
117
+ if (!isObjectRecord(value)) return null;
118
+ const { category, code, redacted, retryable } = value;
119
+ if (
120
+ typeof code !== 'string' ||
121
+ typeof category !== 'string' ||
122
+ OMP_SDK_ERROR_CODES.get(code) !== category ||
123
+ typeof retryable !== 'boolean' ||
124
+ redacted !== true ||
125
+ !hasExactKeys(value, ['category', 'code', 'redacted', 'retryable'])
126
+ ) {
127
+ return null;
128
+ }
129
+ return { category, code, retryable };
130
+ }
131
+
132
+ function ompProviderCategory(
133
+ error: OmpSdkErrorMetadata
134
+ ): NonNullable<CliFailure['providerCategory']> {
135
+ if (error.category === 'auth') return 'authentication';
136
+ return error.retryable ? 'transient' : 'permanent';
137
+ }
138
+
139
+ function isOmpSdkRunId(value: unknown): value is string {
140
+ return (
141
+ typeof value === 'string' &&
142
+ value.length <= 128 &&
143
+ /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value)
144
+ );
145
+ }
146
+
147
+ function isOmpSdkErrorEnvelope(value: JsonRecord): boolean {
148
+ return (
149
+ value.protocolVersion === 1 &&
150
+ value.type === 'error' &&
151
+ isOmpSdkRunId(value.runId) &&
152
+ hasExactKeys(value, ['backend', 'error', 'protocolVersion', 'runId', 'runtime', 'type']) &&
153
+ isOmpSdkIdentity(value.backend, 'id', 'omp-sdk', '17.2.1') &&
154
+ isOmpSdkIdentity(value.runtime, 'name', 'bun', '1.3.14')
155
+ );
156
+ }
157
+
158
+ function ompSdkFailureFromObject(value: JsonRecord): CliFailure | null {
159
+ const error = ompSdkErrorMetadata(value.error);
160
+ if (error === null || !isOmpSdkErrorEnvelope(value)) return null;
161
+ const detail = `OMP SDK ${error.code} (${error.category})`;
162
+ return {
163
+ ...cliErrorDetail(detail, 'OMP SDK turn failed'),
164
+ provider: 'omp',
165
+ providerCategory: ompProviderCategory(error),
166
+ providerClassification: {
167
+ retryable: error.retryable,
168
+ kind: error.retryable ? 'unknown-retryable' : 'permanent-pattern',
169
+ },
170
+ };
171
+ }
172
+
74
173
  function failureFromProviderObject(value: unknown, providerName: string): CliFailure | null {
75
174
  if (!isObjectRecord(value)) return null;
76
175
  if (providerName === 'claude') return claudeFailureFromObject(value);
77
176
  if (providerName === 'codex') return codexFailureFromObject(value);
78
177
  if (providerName === 'gemini') return geminiFailureFromObject(value);
79
178
  if (providerName === 'opencode') return opencodeFailureFromObject(value);
179
+ if (providerName === 'omp') return ompSdkFailureFromObject(value);
80
180
  return null;
81
181
  }
82
182
 
@@ -16,6 +16,11 @@ export interface CliErrorDetail {
16
16
 
17
17
  export interface CliFailure extends CliErrorDetail {
18
18
  provider: string;
19
+ providerCategory?: 'authentication' | 'permanent' | 'transient';
20
+ providerClassification?: {
21
+ retryable: boolean;
22
+ kind: 'permanent-pattern' | 'unknown-retryable';
23
+ };
19
24
  }
20
25
 
21
26
  export interface CliError {
@@ -73,8 +73,8 @@ function extractProviderFailure(output, providerName) {
73
73
  const cliError = extractCliFailure(output, providerName);
74
74
  if (!cliError)
75
75
  return null;
76
- const classification = classifyProviderFailure(providerName, cliError.error);
77
- const category = categoryForProviderFailure(cliError.error, classification);
76
+ const classification = cliError.providerClassification ?? classifyProviderFailure(providerName, cliError.error);
77
+ const category = cliError.providerCategory ?? categoryForProviderFailure(cliError.error, classification);
78
78
  return {
79
79
  error: `Provider ${cliError.provider} failed (${category}; ${classification.kind})`,
80
80
  provider: cliError.provider,
@@ -89,8 +89,10 @@ function extractProviderFailure(output: string, providerName: string): ProviderF
89
89
  const cliError = extractCliFailure(output, providerName);
90
90
  if (!cliError) return null;
91
91
 
92
- const classification = classifyProviderFailure(providerName, cliError.error);
93
- const category = categoryForProviderFailure(cliError.error, classification);
92
+ const classification =
93
+ cliError.providerClassification ?? classifyProviderFailure(providerName, cliError.error);
94
+ const category =
95
+ cliError.providerCategory ?? categoryForProviderFailure(cliError.error, classification);
94
96
  return {
95
97
  error: `Provider ${cliError.provider} failed (${category}; ${classification.kind})`,
96
98
  provider: cliError.provider,
@@ -315,7 +315,7 @@ export const claudeAdapter: StructuredOutputRecoveryAdapter = {
315
315
  displayName: 'Claude',
316
316
  binary: 'claude',
317
317
  adapterVersion: '1',
318
- credentialEnvKeys: ['ANTHROPIC_API_KEY', 'CLAUDE_API_KEY'],
318
+ credentialEnvKeys: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_API_KEY'],
319
319
  modelCatalog: MODEL_CATALOG,
320
320
  levelMapping: LEVEL_MAPPING,
321
321
  defaultLevel: 'level2',
@@ -73,6 +73,7 @@ function detectCliFeatures(
73
73
  supportsIgnoreUserConfig: !unknown && /--ignore-user-config\b/.test(help),
74
74
  supportsIgnoreRules: !unknown && /--ignore-rules\b/.test(help),
75
75
  supportsStrictConfig: !unknown && /--strict-config\b/.test(help),
76
+ supportsAddDir: !unknown && /--add-dir\b/.test(help),
76
77
  unknown,
77
78
  };
78
79
  }
@@ -119,6 +120,14 @@ function addAutoApproveArgs(args: string[], options: BuildProviderCommandOptions
119
120
  ? 'danger-full-access'
120
121
  : 'workspace-write';
121
122
  args.push('--sandbox', sandboxMode, '--config', 'approval_policy="never"');
123
+ if (sandboxMode === 'workspace-write') {
124
+ args.push('--config', 'sandbox_workspace_write.network_access=true');
125
+ }
126
+ if (features.supportsAddDir) {
127
+ for (const directory of new Set(options.additionalWritableDirectories ?? [])) {
128
+ args.push('--add-dir', directory);
129
+ }
130
+ }
122
131
  }
123
132
  }
124
133
 
@@ -129,6 +138,18 @@ function addSkipGitArgs(args: string[], options: BuildProviderCommandOptions): v
129
138
  }
130
139
  }
131
140
 
141
+ function codexExecAuthEnv(
142
+ authEnv: Readonly<Record<string, string>>
143
+ ): Readonly<Record<string, string>> {
144
+ const openAiApiKey = authEnv.OPENAI_API_KEY;
145
+ if (openAiApiKey === undefined) return authEnv;
146
+ const { OPENAI_API_KEY: _openAiApiKey, ...rest } = authEnv;
147
+ return {
148
+ ...rest,
149
+ CODEX_API_KEY: rest.CODEX_API_KEY ?? openAiApiKey,
150
+ };
151
+ }
152
+
132
153
  function applySchemaArgs(
133
154
  args: string[],
134
155
  cleanup: string[],
@@ -175,6 +196,19 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
175
196
  )
176
197
  );
177
198
  }
199
+ if (
200
+ options.autoApprove &&
201
+ (options.additionalWritableDirectories?.length ?? 0) > 0 &&
202
+ features.supportsAddDir === false
203
+ ) {
204
+ warnings.push(
205
+ warning(
206
+ 'codex',
207
+ 'codex-add-dir',
208
+ 'Codex CLI does not support --add-dir; Git worktree metadata outside cwd may be read-only.'
209
+ )
210
+ );
211
+ }
178
212
  if (options.jsonSchema && features.supportsOutputSchema === false) {
179
213
  warnings.push(
180
214
  warning(
@@ -216,6 +250,7 @@ function buildCommand(context: string, options: BuildProviderCommandOptions = {}
216
250
  }
217
251
  const args: string[] = ['exec'];
218
252
  const cleanup: string[] = [];
253
+ const authEnv = options.authEnv ?? {};
219
254
  const resumeSessionId =
220
255
  options.resumeSessionId && optionFeatures(options).supportsResume !== false
221
256
  ? options.resumeSessionId
@@ -244,7 +279,7 @@ function buildCommand(context: string, options: BuildProviderCommandOptions = {}
244
279
  return commandSpec({
245
280
  binary: 'codex',
246
281
  args,
247
- env: {},
282
+ env: codexExecAuthEnv(authEnv),
248
283
  ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
249
284
  cleanup,
250
285
  cleanupMetadata: cleanup.map((schemaFile) => ({
@@ -73,7 +73,8 @@ const LEVEL_MAPPING: Readonly<Record<ModelLevel, LevelModelSpec>> = {
73
73
  const CREDENTIAL_ENV_KEYS = OMP_CREDENTIAL_ENV_KEYS;
74
74
 
75
75
  const VERSION_TOKEN_PATTERN = /(?<![\w.])17\.2\.1(?![\w.])/;
76
- const MODEL_ID_PATTERN = /^@?[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u;
76
+ const MODEL_ID_PATTERN =
77
+ /^(?=.{1,128}$)(?:@?[A-Za-z0-9][A-Za-z0-9._:/-]*|[a-z0-9][a-z0-9._-]*\/~[A-Za-z0-9][A-Za-z0-9._:/-]*)$/u;
77
78
 
78
79
  function detectCliFeatures(helpText?: string | null, versionText?: string | null): OmpCliFeatures {
79
80
  const help = helpText ?? '';
@@ -0,0 +1,38 @@
1
+ import { getProviderRegistryEntry } from '../provider-registry';
2
+ import type { PreparedEnvironmentPolicy } from '../types';
3
+
4
+ const MINIMAL_ENV_NAMES = [
5
+ 'ALL_PROXY',
6
+ 'HTTP_PROXY',
7
+ 'HTTPS_PROXY',
8
+ 'LANG',
9
+ 'LC_ALL',
10
+ 'NO_PROXY',
11
+ 'PATH',
12
+ 'SSL_CERT_DIR',
13
+ 'SSL_CERT_FILE',
14
+ 'TZ',
15
+ ] as const;
16
+
17
+ const ALLOWED_ENV_NAMES = new Set([
18
+ ...MINIMAL_ENV_NAMES,
19
+ ...(getProviderRegistryEntry('omp').configurationEnvKeys ?? []),
20
+ ]);
21
+
22
+ export function isOmpSdkEnvironmentName(name: string): boolean {
23
+ return ALLOWED_ENV_NAMES.has(name.toUpperCase());
24
+ }
25
+
26
+ export function ompSdkEnvironmentPolicy(
27
+ environment: NodeJS.ProcessEnv = process.env
28
+ ): PreparedEnvironmentPolicy {
29
+ const values: Record<string, string> = {};
30
+ for (const name of ALLOWED_ENV_NAMES) {
31
+ const value = environment[name];
32
+ if (value !== undefined) values[name] = value;
33
+ }
34
+ return {
35
+ inherit: 'minimal',
36
+ values: Object.freeze(values),
37
+ };
38
+ }