@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.
Files changed (73) hide show
  1. package/docker/zeroshot-cluster/Dockerfile +7 -0
  2. package/lib/agent-cli-provider/adapters/omp.d.ts +3 -1
  3. package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/omp.js +252 -269
  5. package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
  6. package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -1
  7. package/lib/agent-cli-provider/contract-invoke.js +68 -14
  8. package/lib/agent-cli-provider/contract-invoke.js.map +1 -1
  9. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/contract-options.js +5 -3
  11. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  12. package/lib/agent-cli-provider/index.d.ts +6 -0
  13. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  14. package/lib/agent-cli-provider/index.js +12 -1
  15. package/lib/agent-cli-provider/index.js.map +1 -1
  16. package/lib/agent-cli-provider/omp-release.d.ts +6 -1
  17. package/lib/agent-cli-provider/omp-release.d.ts.map +1 -1
  18. package/lib/agent-cli-provider/omp-release.js +41 -2
  19. package/lib/agent-cli-provider/omp-release.js.map +1 -1
  20. package/lib/agent-cli-provider/omp-rpc-bounds.d.ts +7 -0
  21. package/lib/agent-cli-provider/omp-rpc-bounds.d.ts.map +1 -0
  22. package/lib/agent-cli-provider/omp-rpc-bounds.js +27 -0
  23. package/lib/agent-cli-provider/omp-rpc-bounds.js.map +1 -0
  24. package/lib/agent-cli-provider/omp-rpc-driver.d.ts +40 -0
  25. package/lib/agent-cli-provider/omp-rpc-driver.d.ts.map +1 -0
  26. package/lib/agent-cli-provider/omp-rpc-driver.js +494 -0
  27. package/lib/agent-cli-provider/omp-rpc-driver.js.map +1 -0
  28. package/lib/agent-cli-provider/omp-rpc-events.d.ts +28 -0
  29. package/lib/agent-cli-provider/omp-rpc-events.d.ts.map +1 -0
  30. package/lib/agent-cli-provider/omp-rpc-events.js +264 -0
  31. package/lib/agent-cli-provider/omp-rpc-events.js.map +1 -0
  32. package/lib/agent-cli-provider/omp-rpc-protocol.d.ts +1 -1
  33. package/lib/agent-cli-provider/omp-rpc-protocol.d.ts.map +1 -1
  34. package/lib/agent-cli-provider/omp-rpc-protocol.js +8 -3
  35. package/lib/agent-cli-provider/omp-rpc-protocol.js.map +1 -1
  36. package/lib/agent-cli-provider/omp-rpc-session.d.ts +17 -0
  37. package/lib/agent-cli-provider/omp-rpc-session.d.ts.map +1 -0
  38. package/lib/agent-cli-provider/omp-rpc-session.js +6 -0
  39. package/lib/agent-cli-provider/omp-rpc-session.js.map +1 -0
  40. package/lib/agent-cli-provider/provider-registry.d.ts +33 -17
  41. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  42. package/lib/agent-cli-provider/provider-registry.js +63 -11
  43. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  44. package/lib/agent-cli-provider/types.d.ts +13 -11
  45. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  46. package/lib/agent-cli-provider/types.js.map +1 -1
  47. package/lib/docker-config.js +122 -5
  48. package/package.json +2 -1
  49. package/src/agent/agent-lifecycle.js +12 -1
  50. package/src/agent/provider-session.js +1 -0
  51. package/src/agent-cli-provider/adapters/omp.ts +276 -329
  52. package/src/agent-cli-provider/contract-invoke.ts +86 -15
  53. package/src/agent-cli-provider/contract-options.ts +5 -3
  54. package/src/agent-cli-provider/index.ts +13 -0
  55. package/src/agent-cli-provider/omp-release.ts +50 -1
  56. package/src/agent-cli-provider/omp-rpc-bounds.ts +28 -0
  57. package/src/agent-cli-provider/omp-rpc-driver.ts +611 -0
  58. package/src/agent-cli-provider/omp-rpc-events.ts +318 -0
  59. package/src/agent-cli-provider/omp-rpc-protocol.ts +8 -3
  60. package/src/agent-cli-provider/omp-rpc-session.ts +20 -0
  61. package/src/agent-cli-provider/provider-registry.ts +118 -23
  62. package/src/agent-cli-provider/types.ts +14 -11
  63. package/src/command-cleanup-ownership.js +307 -0
  64. package/src/isolation-manager.js +535 -89
  65. package/src/omp-config-overlay.js +96 -0
  66. package/src/orchestrator.js +31 -6
  67. package/src/preflight.js +50 -5
  68. package/src/watcher-prompt-channel.js +209 -0
  69. package/task-lib/command-spec-cleanup.js +1 -262
  70. package/task-lib/provider-helper-runtime.js +6 -0
  71. package/task-lib/rpc-watcher.js +186 -0
  72. package/task-lib/runner.js +40 -6
  73. package/task-lib/watcher-output-runtime.js +32 -0
@@ -0,0 +1,318 @@
1
+ import { OmpRpcProtocolError, type OmpRpcInboundFrame } from './omp-rpc-protocol';
2
+ import { MAX_NORMALIZED_OUTPUT_BYTES } from './omp-rpc-bounds';
3
+ import {
4
+ getArray,
5
+ getBoolean,
6
+ getNumber,
7
+ getOptionalString,
8
+ getRecord,
9
+ getString,
10
+ isRecord,
11
+ stringifyContent,
12
+ tryParseJson,
13
+ } from './json';
14
+ import type { OutputEvent, ProviderParseResult, ProviderParserState } from './types';
15
+
16
+ export interface OmpRpcEventState {
17
+ lastAssistantText: string;
18
+ lastAssistantThinking: string;
19
+ lastToolId: string | null | undefined;
20
+ normalizedBytes: number;
21
+ }
22
+
23
+ export function createOmpRpcEventState(): OmpRpcEventState {
24
+ return {
25
+ lastAssistantText: '',
26
+ lastAssistantThinking: '',
27
+ lastToolId: undefined,
28
+ normalizedBytes: 0,
29
+ };
30
+ }
31
+
32
+ function assistantSnapshot(message: Record<string, unknown>): { text: string; thinking: string } {
33
+ if (getString(message, 'role') !== 'assistant') return { text: '', thinking: '' };
34
+
35
+ let text = '';
36
+ let thinking = '';
37
+ for (const item of getArray(message, 'content')) {
38
+ if (!isRecord(item)) continue;
39
+ const type = getString(item, 'type');
40
+ if (type === 'text') {
41
+ text += getString(item, 'text') ?? '';
42
+ } else if (type === 'thinking') {
43
+ thinking += getString(item, 'thinking') ?? '';
44
+ }
45
+ }
46
+
47
+ return { text, thinking };
48
+ }
49
+
50
+ function snapshotDelta(previous: string, current: string): string | null {
51
+ if (!current || previous === current) return null;
52
+ if (current.startsWith(previous)) return current.slice(previous.length) || null;
53
+ return current;
54
+ }
55
+
56
+ function chargeBytes(state: OmpRpcEventState, value: unknown): void {
57
+ const byteLength = Buffer.byteLength(stringifyContent(value), 'utf8');
58
+ state.normalizedBytes += byteLength;
59
+ if (state.normalizedBytes > MAX_NORMALIZED_OUTPUT_BYTES) {
60
+ throw new OmpRpcProtocolError(
61
+ 'output-bound-exceeded',
62
+ `Normalized OMP RPC output exceeded the ${MAX_NORMALIZED_OUTPUT_BYTES}-byte cap for this task.`
63
+ );
64
+ }
65
+ }
66
+
67
+ function emitText(state: OmpRpcEventState, text: string): OutputEvent {
68
+ chargeBytes(state, text);
69
+ return { type: 'text', text };
70
+ }
71
+
72
+ function emitThinking(state: OmpRpcEventState, text: string): OutputEvent {
73
+ chargeBytes(state, text);
74
+ return { type: 'thinking', text };
75
+ }
76
+
77
+ function normalizeMessageStart(
78
+ frame: OmpRpcInboundFrame,
79
+ state: OmpRpcEventState
80
+ ): readonly OutputEvent[] {
81
+ const message = getRecord(frame, 'message');
82
+ if (message !== null && getString(message, 'role') === 'assistant') {
83
+ state.lastAssistantText = '';
84
+ state.lastAssistantThinking = '';
85
+ }
86
+ return [];
87
+ }
88
+
89
+ function normalizeAssistantMessageEvent(
90
+ event: Record<string, unknown>,
91
+ state: OmpRpcEventState
92
+ ): readonly OutputEvent[] {
93
+ const type = getString(event, 'type');
94
+ if (type === 'text_delta') {
95
+ const delta = getString(event, 'delta');
96
+ if (!delta) return [];
97
+ state.lastAssistantText += delta;
98
+ return [emitText(state, delta)];
99
+ }
100
+ if (type === 'thinking_delta') {
101
+ const delta = getString(event, 'delta');
102
+ if (!delta) return [];
103
+ state.lastAssistantThinking += delta;
104
+ return [emitThinking(state, delta)];
105
+ }
106
+ return [];
107
+ }
108
+
109
+ function normalizeMessageSnapshot(
110
+ message: Record<string, unknown>,
111
+ state: OmpRpcEventState
112
+ ): readonly OutputEvent[] {
113
+ const snapshot = assistantSnapshot(message);
114
+ const events: OutputEvent[] = [];
115
+
116
+ const textDelta = snapshotDelta(state.lastAssistantText, snapshot.text);
117
+ if (textDelta) events.push(emitText(state, textDelta));
118
+ if (snapshot.text) state.lastAssistantText = snapshot.text;
119
+
120
+ const thinkingDelta = snapshotDelta(state.lastAssistantThinking, snapshot.thinking);
121
+ if (thinkingDelta) events.push(emitThinking(state, thinkingDelta));
122
+ if (snapshot.thinking) state.lastAssistantThinking = snapshot.thinking;
123
+
124
+ return events;
125
+ }
126
+
127
+ function normalizeMessageUpdate(
128
+ frame: OmpRpcInboundFrame,
129
+ state: OmpRpcEventState
130
+ ): readonly OutputEvent[] {
131
+ const assistantMessageEvent = getRecord(frame, 'assistantMessageEvent');
132
+ if (assistantMessageEvent !== null) {
133
+ const events = normalizeAssistantMessageEvent(assistantMessageEvent, state);
134
+ if (events.length > 0) return events;
135
+ }
136
+ const message = getRecord(frame, 'message');
137
+ return message === null ? [] : normalizeMessageSnapshot(message, state);
138
+ }
139
+
140
+ function normalizeMessageEnd(
141
+ frame: OmpRpcInboundFrame,
142
+ state: OmpRpcEventState
143
+ ): readonly OutputEvent[] {
144
+ const message = getRecord(frame, 'message');
145
+ return message === null ? [] : normalizeMessageSnapshot(message, state);
146
+ }
147
+
148
+ function normalizeToolExecutionStart(
149
+ frame: OmpRpcInboundFrame,
150
+ state: OmpRpcEventState
151
+ ): readonly OutputEvent[] {
152
+ const toolId = getOptionalString(frame, 'toolCallId');
153
+ state.lastToolId = toolId;
154
+ const input = frame.args ?? {};
155
+ chargeBytes(state, input);
156
+ return [
157
+ {
158
+ type: 'tool_call',
159
+ toolName: getOptionalString(frame, 'toolName'),
160
+ toolId,
161
+ input,
162
+ },
163
+ ];
164
+ }
165
+
166
+ function normalizeToolExecutionUpdate(
167
+ frame: OmpRpcInboundFrame,
168
+ state: OmpRpcEventState
169
+ ): readonly OutputEvent[] {
170
+ const toolId = getOptionalString(frame, 'toolCallId') ?? state.lastToolId;
171
+ if (toolId !== undefined) state.lastToolId = toolId;
172
+ if (!Object.prototype.hasOwnProperty.call(frame, 'partialResult')) return [];
173
+ chargeBytes(state, frame.partialResult);
174
+ return [
175
+ {
176
+ type: 'tool_result',
177
+ toolId,
178
+ content: frame.partialResult,
179
+ isError: false,
180
+ },
181
+ ];
182
+ }
183
+
184
+ function normalizeToolExecutionEnd(
185
+ frame: OmpRpcInboundFrame,
186
+ state: OmpRpcEventState
187
+ ): readonly OutputEvent[] {
188
+ const toolId = getOptionalString(frame, 'toolCallId') ?? state.lastToolId;
189
+ const content = frame.result ?? '';
190
+ chargeBytes(state, content);
191
+ return [
192
+ {
193
+ type: 'tool_result',
194
+ toolId,
195
+ content,
196
+ isError: getBoolean(frame, 'isError') ?? false,
197
+ },
198
+ ];
199
+ }
200
+
201
+ function normalizeTurnEnd(
202
+ frame: OmpRpcInboundFrame,
203
+ state: OmpRpcEventState
204
+ ): readonly OutputEvent[] {
205
+ const message = getRecord(frame, 'message');
206
+ const usage = message ? (getRecord(message, 'usage') ?? {}) : {};
207
+ const stopReason = message ? getString(message, 'stopReason') : null;
208
+ const errorMessage = message ? getString(message, 'errorMessage') : null;
209
+ const snapshot = message ? assistantSnapshot(message) : { text: '', thinking: '' };
210
+ const success = stopReason !== 'error' && stopReason !== 'aborted' && !errorMessage;
211
+ const result = success ? snapshot.text || state.lastAssistantText || null : null;
212
+ chargeBytes(state, { result, error: errorMessage ?? stopReason ?? null });
213
+ return [
214
+ {
215
+ type: 'result',
216
+ success,
217
+ result,
218
+ error: success ? null : (errorMessage ?? stopReason ?? 'OMP turn failed'),
219
+ inputTokens: getNumber(usage, 'input') ?? 0,
220
+ outputTokens: getNumber(usage, 'output') ?? 0,
221
+ cacheReadInputTokens: getNumber(usage, 'cacheRead') ?? 0,
222
+ cacheCreationInputTokens: getNumber(usage, 'cacheWrite') ?? 0,
223
+ cost: getRecord(usage, 'cost') ?? null,
224
+ modelUsage: usage,
225
+ },
226
+ ];
227
+ }
228
+
229
+ /**
230
+ * Normalize one decoded OMP RPC v2 event frame into zero or more OutputEvents. Only the frame
231
+ * types below produce output; every other known-pre-negotiation frame (agent_start/agent_end,
232
+ * turn_start, ready, response, session_info_update, subagent_*, ...) is protocol/control plumbing
233
+ * the driver consumes directly and must never leak into log/attach/ledger output.
234
+ */
235
+ export function normalizeOmpRpcFrame(
236
+ frame: OmpRpcInboundFrame,
237
+ state: OmpRpcEventState
238
+ ): readonly OutputEvent[] {
239
+ switch (frame.type) {
240
+ case 'message_start':
241
+ return normalizeMessageStart(frame, state);
242
+ case 'message_update':
243
+ return normalizeMessageUpdate(frame, state);
244
+ case 'message_end':
245
+ return normalizeMessageEnd(frame, state);
246
+ case 'tool_execution_start':
247
+ return normalizeToolExecutionStart(frame, state);
248
+ case 'tool_execution_update':
249
+ return normalizeToolExecutionUpdate(frame, state);
250
+ case 'tool_execution_end':
251
+ return normalizeToolExecutionEnd(frame, state);
252
+ case 'turn_end':
253
+ return normalizeTurnEnd(frame, state);
254
+ default:
255
+ return [];
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Parse one line of the rpc-stdio lane's already-normalized OutputEvent JSON stream (produced
261
+ * from `OmpRpcTaskResult.events` by `contract-invoke.ts` / `rpc-watcher.js`). This is the
262
+ * `ProviderAdapter.parseEvent` counterpart to `normalizeOmpRpcFrame` above: the driver normalizes
263
+ * raw RPC frames to OutputEvent once, and this function is a validating passthrough over that
264
+ * already-normalized shape so generic log/attach tooling built on `adapter.parseEvent` keeps
265
+ * working for the omp provider without re-deriving events from raw frames. Each case rebuilds a
266
+ * literal matching its OutputEvent variant instead of casting the parsed record, so an
267
+ * unexpectedly-shaped line fails closed (returns null) rather than passing through untyped.
268
+ */
269
+ export function parseNormalizedOmpRpcEventLine(
270
+ line: string,
271
+ _state: ProviderParserState
272
+ ): ProviderParseResult {
273
+ const parsed = tryParseJson(line);
274
+ if (!isRecord(parsed)) return null;
275
+ switch (parsed.type) {
276
+ case 'text':
277
+ case 'thinking': {
278
+ const text = getString(parsed, 'text');
279
+ return text === null ? null : { type: parsed.type, text };
280
+ }
281
+ case 'tool_call':
282
+ return {
283
+ type: 'tool_call',
284
+ toolName: getOptionalString(parsed, 'toolName'),
285
+ toolId: getOptionalString(parsed, 'toolId'),
286
+ input: parsed.input,
287
+ };
288
+ case 'tool_result':
289
+ return {
290
+ type: 'tool_result',
291
+ toolId: getOptionalString(parsed, 'toolId'),
292
+ content: parsed.content,
293
+ isError: parsed.isError,
294
+ };
295
+ case 'result': {
296
+ if (typeof parsed.success !== 'boolean') return null;
297
+ const inputTokens = getNumber(parsed, 'inputTokens');
298
+ const outputTokens = getNumber(parsed, 'outputTokens');
299
+ const cacheReadInputTokens = getNumber(parsed, 'cacheReadInputTokens');
300
+ const cacheCreationInputTokens = getNumber(parsed, 'cacheCreationInputTokens');
301
+ return {
302
+ type: 'result',
303
+ success: parsed.success,
304
+ result: parsed.result,
305
+ error: parsed.error,
306
+ cost: parsed.cost,
307
+ duration: parsed.duration,
308
+ ...(inputTokens === null ? {} : { inputTokens }),
309
+ ...(outputTokens === null ? {} : { outputTokens }),
310
+ ...(cacheReadInputTokens === null ? {} : { cacheReadInputTokens }),
311
+ ...(cacheCreationInputTokens === null ? {} : { cacheCreationInputTokens }),
312
+ modelUsage: parsed.modelUsage,
313
+ };
314
+ }
315
+ default:
316
+ return null;
317
+ }
318
+ }
@@ -218,7 +218,11 @@ export class OmpRpcFrameDecoder {
218
218
  this.inflightReassemblyBytes = 0;
219
219
  }
220
220
 
221
- push(chunk: Uint8Array): readonly OmpRpcInboundFrame[] {
221
+ // `negotiatedV2` defaults to true so callers that don't care about pre-negotiation gating
222
+ // (e.g. the fixture-replay tests below) see unchanged behavior; the RPC driver passes its
223
+ // live negotiation state so a bare `rpc_chunk` physical frame is rejected before negotiation
224
+ // succeeds, even though the decoder would otherwise buffer it silently pending reassembly.
225
+ push(chunk: Uint8Array, negotiatedV2 = true): readonly OmpRpcInboundFrame[] {
222
226
  if (this.finished) {
223
227
  throw new OmpRpcProtocolError('decoder-finished', 'push() called after finish().');
224
228
  }
@@ -247,7 +251,7 @@ export class OmpRpcFrameDecoder {
247
251
  );
248
252
  }
249
253
 
250
- const frame = this.consumeLine(lineBytes);
254
+ const frame = this.consumeLine(lineBytes, negotiatedV2);
251
255
  if (frame !== null) frames.push(frame);
252
256
  }
253
257
 
@@ -270,7 +274,7 @@ export class OmpRpcFrameDecoder {
270
274
  }
271
275
  }
272
276
 
273
- private consumeLine(lineBytes: Buffer): OmpRpcInboundFrame | null {
277
+ private consumeLine(lineBytes: Buffer, negotiatedV2: boolean): OmpRpcInboundFrame | null {
274
278
  let value: unknown;
275
279
  try {
276
280
  value = JSON.parse(lineBytes.toString('utf8'));
@@ -285,6 +289,7 @@ export class OmpRpcFrameDecoder {
285
289
  }
286
290
 
287
291
  const type = getString(value, 'type');
292
+ assertNoPreNegotiationRpcChunk(type ?? '', negotiatedV2);
288
293
  if (type !== 'rpc_chunk') {
289
294
  if (this.pendingReassemblies.size > 0) {
290
295
  throw new OmpRpcProtocolError(
@@ -0,0 +1,20 @@
1
+ // Session-launch types for the OMP RPC v2 driver. This slice launches sessionless
2
+ // (`--no-session`) only; `fresh`/`resume` are typed for Subissue 4's session-flag activation but
3
+ // are unreachable from any caller in this slice.
4
+
5
+ export interface VerifiedOmpPartition {
6
+ readonly path: string;
7
+ }
8
+
9
+ export interface VerifiedOmpSessionFile {
10
+ readonly path: string;
11
+ }
12
+
13
+ export type OmpSessionLaunch =
14
+ | { readonly kind: 'none' }
15
+ | { readonly kind: 'fresh'; readonly partition: VerifiedOmpPartition }
16
+ | {
17
+ readonly kind: 'resume';
18
+ readonly partition: VerifiedOmpPartition;
19
+ readonly file: VerifiedOmpSessionFile;
20
+ };
@@ -2,13 +2,22 @@ import { createAcpAdapter } from './adapters/acp';
2
2
  import { claudeAdapter } from './adapters/claude';
3
3
  import { codexAdapter } from './adapters/codex';
4
4
  import { copilotAdapter } from './adapters/copilot';
5
- import { gatewayAdapter, gatewaySettingsDefaults, validateGatewaySettings } from './adapters/gateway';
5
+ import {
6
+ gatewayAdapter,
7
+ gatewaySettingsDefaults,
8
+ validateGatewaySettings,
9
+ } from './adapters/gateway';
6
10
  import { geminiAdapter } from './adapters/gemini';
7
11
  import { opencodeAdapter } from './adapters/opencode';
8
12
  import { ompAdapter } from './adapters/omp';
9
13
  import { piAdapter } from './adapters/pi';
10
14
  import { resolveClaudeCommand } from './claude-command';
11
- import { OMP_INSTALL_COMMAND } from './omp-release';
15
+ import {
16
+ OMP_AUTH_INSTRUCTIONS,
17
+ OMP_DOCKER_INSTALL_COMMAND,
18
+ OMP_DOCKER_PLATFORM,
19
+ OMP_INSTALL_COMMAND,
20
+ } from './omp-release';
12
21
  import type { ModelLevel, ProviderAdapter, StructuredOutputRecoveryAdapter } from './types';
13
22
 
14
23
  export type ProviderCapabilityState = boolean | 'experimental';
@@ -44,7 +53,15 @@ export interface AcpStdioProviderInvokeSpec {
44
53
  readonly transport: 'stdio';
45
54
  }
46
55
 
47
- export type ProviderInvokeSpec = SpawnProviderInvokeSpec | AcpStdioProviderInvokeSpec;
56
+ export interface RpcStdioProviderInvokeSpec {
57
+ readonly lane: 'rpc-stdio';
58
+ readonly protocol: 'omp-v2';
59
+ }
60
+
61
+ export type ProviderInvokeSpec =
62
+ | SpawnProviderInvokeSpec
63
+ | AcpStdioProviderInvokeSpec
64
+ | RpcStdioProviderInvokeSpec;
48
65
 
49
66
  export type ProviderCommandSpec = FixedProviderCommandSpec | ConfiguredClaudeCommandSpec;
50
67
 
@@ -59,8 +76,24 @@ export interface ProviderDockerMountPreset {
59
76
  readonly readonly: boolean;
60
77
  }
61
78
 
79
+ export interface ProviderDockerEnvAuth {
80
+ // At least one of these env vars must carry a usable (non-empty, non-whitespace) value for the
81
+ // effective container plan to be considered automatically authenticated. This is the AUTOMATIC
82
+ // allowlist only; a registry-known credential outside it is accepted when — and only when — the
83
+ // user explicitly opted it in (dockerEnvPassthrough / --mount).
84
+ // Providers with no `mount` (env-only) fail closed when nothing is satisfied.
85
+ readonly requireOneOf: readonly string[];
86
+ // Each inner group must be all-set-or-all-unset (e.g. a broker URL + token pair); a partial
87
+ // group is treated as malformed auth, not "missing".
88
+ readonly requireTogether?: readonly (readonly string[])[];
89
+ // Env vars whose value must parse as an absolute http(s) URL to count as set. A non-URL value
90
+ // is malformed auth, not "missing".
91
+ readonly requireUrl?: readonly string[];
92
+ }
93
+
62
94
  export interface ProviderDockerMetadata {
63
- readonly mount: ProviderDockerMountPreset;
95
+ // Omitted for env-only providers with zero automatic credential mounts (e.g. omp).
96
+ readonly mount?: ProviderDockerMountPreset;
64
97
  readonly envPassthrough: readonly string[];
65
98
  // False when the mounted dir doesn't hold the secret (auth is via an envPassthrough token).
66
99
  readonly credentialInMount?: boolean;
@@ -68,6 +101,14 @@ export interface ProviderDockerMetadata {
68
101
  // a docker-cached build layer for the per-provider image variant. Omit for providers already
69
102
  // baked into the base image (e.g. Claude) or not installable via a single command.
70
103
  readonly install?: string;
104
+ // Docker platform (e.g. 'linux/amd64') passed to both image build and container run. Omitted
105
+ // providers keep today's unset (host-native) behavior.
106
+ readonly platform?: string;
107
+ // $HOME-placeholder directories created owner-only inside the container for this provider's
108
+ // config/session state (never mounted/copied from the host).
109
+ readonly configRoots?: readonly string[];
110
+ // Fail-closed env/broker auth requirement, checked against the effective container env plan.
111
+ readonly envAuth?: ProviderDockerEnvAuth;
71
112
  }
72
113
 
73
114
  interface ProviderRegistryEntryBase {
@@ -146,6 +187,10 @@ const ACP_STDIO_INVOKE = Object.freeze({
146
187
  lane: 'acp-stdio',
147
188
  transport: 'stdio',
148
189
  }) as AcpStdioProviderInvokeSpec;
190
+ const RPC_STDIO_INVOKE = Object.freeze({
191
+ lane: 'rpc-stdio',
192
+ protocol: 'omp-v2',
193
+ }) as RpcStdioProviderInvokeSpec;
149
194
 
150
195
  const kiroAdapter = createAcpAdapter({
151
196
  provider: 'kiro',
@@ -393,8 +438,7 @@ export const providerRegistry = [
393
438
  binary: 'pi',
394
439
  command: { kind: 'fixed', command: 'pi', args: [] },
395
440
  invoke: SPAWN_INVOKE,
396
- installInstructions:
397
- 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.80.3',
441
+ installInstructions: 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.80.3',
398
442
  authInstructions: 'pi\n/login',
399
443
  credentialPaths: ['~/.pi'],
400
444
  credentialEnvKeys: piAdapter.credentialEnvKeys,
@@ -428,34 +472,79 @@ export const providerRegistry = [
428
472
  {
429
473
  id: 'omp',
430
474
  default: false,
431
- aliases: [],
432
- displayName: 'OMP',
475
+ aliases: ['oh-my-pi'],
476
+ displayName: 'OMP (Oh My Pi)',
433
477
  binary: 'omp',
434
478
  command: { kind: 'fixed', command: 'omp', args: [] },
435
- invoke: SPAWN_INVOKE,
479
+ invoke: RPC_STDIO_INVOKE,
436
480
  installInstructions: OMP_INSTALL_COMMAND,
437
- authInstructions: 'omp\n/login',
481
+ authInstructions: OMP_AUTH_INSTRUCTIONS,
438
482
  credentialPaths: ['~/.omp'],
439
483
  credentialEnvKeys: ompAdapter.credentialEnvKeys,
440
484
  settingsFields: [],
441
485
  availabilityProbe: 'help-or-version',
486
+ // Written out explicitly rather than spread from STANDARD_CAPABILITIES, which defaults
487
+ // dockerIsolation to true; OMP's Docker path is env/broker-only and sessionless (see
488
+ // AGENTS.md OMP Docker section) rather than the standard credential-mount + resume shape.
442
489
  capabilities: {
443
- ...STANDARD_CAPABILITIES,
490
+ dockerIsolation: true,
491
+ worktreeIsolation: true,
444
492
  mcpServers: false,
445
493
  jsonSchema: false,
494
+ streamJson: true,
495
+ thinkingMode: true,
446
496
  reasoningEffort: true,
497
+ sessionResume: false,
498
+ webSearch: false,
447
499
  },
448
500
  docs: {
449
501
  label: 'OMP',
450
502
  setupHeading: 'OMP Setup',
451
503
  },
452
504
  docker: {
453
- mount: {
454
- host: '~/.omp',
455
- container: '$HOME/.omp',
456
- readonly: true,
505
+ // No `mount`: OMP's Docker credential surface is env/broker-only with zero automatic
506
+ // mounts. `~/.omp`, agent.db, WAL/SHM files, and host refresh tokens are never mounted or
507
+ // copied into the container.
508
+ //
509
+ // envPassthrough is deliberately narrower than `credentialEnvKeys` above (the full adapter
510
+ // credential inventory, used for host inspection/redaction). Exact 5-name automatic
511
+ // allowlist per the maintainer's authoritative clarification (verified verbatim via
512
+ // `gh api repos/the-open-engine/zeroshot/issues/comments/5160272623`): "the exact automatic
513
+ // OMP Docker environment allowlist is only ANTHROPIC_API_KEY, OPENAI_API_KEY,
514
+ // GEMINI_API_KEY, OMP_AUTH_BROKER_URL, and OMP_AUTH_BROKER_TOKEN ... ANTHROPIC_OAUTH_TOKEN,
515
+ // ANTHROPIC_FOUNDRY_API_KEY, GOOGLE_API_KEY, OPENROUTER_API_KEY, and every other
516
+ // credential/path require explicit dockerEnvPassthrough/mount opt-in; OAuth users should
517
+ // prefer the auth broker so host refresh/access tokens do not cross automatically." This
518
+ // supersedes PLAN_READY step 2's nine-name list. Any validator flagging this as missing the
519
+ // four excluded names is checking stale plan text against a clarification it never read.
520
+ platform: OMP_DOCKER_PLATFORM,
521
+ install: OMP_DOCKER_INSTALL_COMMAND,
522
+ configRoots: ['$HOME/.omp'],
523
+ credentialInMount: false,
524
+ envPassthrough: [
525
+ 'ANTHROPIC_API_KEY',
526
+ 'GEMINI_API_KEY',
527
+ 'OMP_AUTH_BROKER_TOKEN',
528
+ 'OMP_AUTH_BROKER_URL',
529
+ 'OPENAI_API_KEY',
530
+ ],
531
+ envAuth: {
532
+ // The four automatic *credential* names (the fifth allowlist entry, OMP_AUTH_BROKER_URL,
533
+ // is a locator, not a credential — it authenticates nothing on its own). Any other
534
+ // registry-known OMP credential (ANTHROPIC_OAUTH_TOKEN, OPENROUTER_API_KEY, …) is usable
535
+ // only via explicit dockerEnvPassthrough/--mount opt-in, never automatic forwarding.
536
+ requireOneOf: [
537
+ 'ANTHROPIC_API_KEY',
538
+ 'GEMINI_API_KEY',
539
+ 'OMP_AUTH_BROKER_TOKEN',
540
+ 'OPENAI_API_KEY',
541
+ ],
542
+ requireTogether: [['OMP_AUTH_BROKER_URL', 'OMP_AUTH_BROKER_TOKEN']],
543
+ // Per OMP v17.2.1 docs/environment-variables.md, OMP_AUTH_BROKER_URL is the broker's base
544
+ // URL (e.g. https://broker.tailnet:8765); anything that is not an http(s) URL is
545
+ // malformed broker config, and OMP hard-errors on a broker URL with no resolvable token.
546
+ requireUrl: ['OMP_AUTH_BROKER_URL'],
457
547
  },
458
- envPassthrough: [],
459
548
  },
460
549
  defaultLevels: {
461
550
  min: ompAdapter.defaultMinLevel,
@@ -559,12 +648,16 @@ function validateWebSearchSettings(
559
648
  type RegistryProviderId = (typeof providerRegistry)[number]['id'];
560
649
  type RegistryProviderAlias = (typeof providerRegistry)[number]['aliases'][number];
561
650
 
562
- export const providerIds = providerRegistry.map((entry) => entry.id) as readonly RegistryProviderId[];
563
- export const providerAliases = providerRegistry.flatMap((entry) => entry.aliases) as readonly RegistryProviderAlias[];
564
- export const knownProviderNames = providerRegistry.flatMap((entry) => [entry.id, ...entry.aliases]) as readonly (
565
- | RegistryProviderId
566
- | RegistryProviderAlias
567
- )[];
651
+ export const providerIds = providerRegistry.map(
652
+ (entry) => entry.id
653
+ ) as readonly RegistryProviderId[];
654
+ export const providerAliases = providerRegistry.flatMap(
655
+ (entry) => entry.aliases
656
+ ) as readonly RegistryProviderAlias[];
657
+ export const knownProviderNames = providerRegistry.flatMap((entry) => [
658
+ entry.id,
659
+ ...entry.aliases,
660
+ ]) as readonly (RegistryProviderId | RegistryProviderAlias)[];
568
661
 
569
662
  export const providerAliasMap: Readonly<Record<string, RegistryProviderId>> = Object.freeze(
570
663
  providerRegistry.reduce<Record<string, RegistryProviderId>>((result, entry) => {
@@ -602,7 +695,9 @@ export function listProviderRegistryEntries(): readonly ProviderRegistryEntry[]
602
695
  return providerRegistry;
603
696
  }
604
697
 
605
- export function findProviderRegistryEntry(name: string | null | undefined): ProviderRegistryEntry | undefined {
698
+ export function findProviderRegistryEntry(
699
+ name: string | null | undefined
700
+ ): ProviderRegistryEntry | undefined {
606
701
  if (!name) return undefined;
607
702
  const normalized = normalizeProviderName(name);
608
703
  return providerRegistry.find((entry) => entry.id === normalized);
@@ -168,16 +168,16 @@ export interface PiCliFeatures extends BaseCliFeatures {
168
168
 
169
169
  export interface OmpCliFeatures extends BaseCliFeatures {
170
170
  readonly provider: 'omp';
171
- readonly supportsModeJson: boolean;
172
- readonly supportsPrint: boolean;
173
- readonly supportsCwd: boolean;
174
- readonly supportsAutoApprove: boolean;
171
+ readonly supportsRpcMode: boolean;
172
+ readonly supportsConfig: boolean;
175
173
  readonly supportsModel: boolean;
176
174
  readonly supportsThinking: boolean;
177
- readonly supportsNoExtensions: boolean;
178
- readonly supportsNoSkills: boolean;
179
- readonly supportsNoRules: boolean;
175
+ readonly supportsApprovalMode: boolean;
180
176
  readonly supportsNoTitle: boolean;
177
+ readonly supportsNoSession: boolean;
178
+ readonly supportsSessionDir: boolean;
179
+ readonly supportsResume: boolean;
180
+ readonly versionMatches: boolean;
181
181
  }
182
182
 
183
183
  export interface CopilotCliFeatures extends BaseCliFeatures {
@@ -256,11 +256,13 @@ export interface CliFeatureOverrides {
256
256
  readonly supportsRecoveryIsolation?: boolean;
257
257
  readonly supportsNoContextFiles?: boolean;
258
258
  readonly supportsNoApprove?: boolean;
259
- readonly supportsModeJson?: boolean;
260
- readonly supportsPrint?: boolean;
259
+ readonly supportsRpcMode?: boolean;
260
+ readonly supportsConfig?: boolean;
261
261
  readonly supportsThinking?: boolean;
262
- readonly supportsNoRules?: boolean;
262
+ readonly supportsApprovalMode?: boolean;
263
263
  readonly supportsNoTitle?: boolean;
264
+ readonly supportsSessionDir?: boolean;
265
+ readonly versionMatches?: boolean;
264
266
  readonly supportsJsonOutput?: boolean;
265
267
  readonly supportsAllowAll?: boolean;
266
268
  readonly supportsNoAskUser?: boolean;
@@ -412,7 +414,8 @@ export type ErrorClassificationKind =
412
414
  | 'code-retryable'
413
415
  | 'permanent-pattern'
414
416
  | 'retryable-pattern'
415
- | 'unknown-retryable';
417
+ | 'unknown-retryable'
418
+ | 'cancelled';
416
419
 
417
420
  export interface ErrorClassification {
418
421
  readonly retryable: boolean;