@the-open-engine/zeroshot 6.3.0 → 6.5.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 (149) hide show
  1. package/cli/commands/providers.js +11 -5
  2. package/cli/index.js +83 -17
  3. package/cli/lib/first-run.js +14 -3
  4. package/docker/zeroshot-cluster/Dockerfile +11 -1
  5. package/lib/agent-cli-provider/acp-stdio-runner.d.ts +4 -0
  6. package/lib/agent-cli-provider/acp-stdio-runner.d.ts.map +1 -0
  7. package/lib/agent-cli-provider/acp-stdio-runner.js +259 -0
  8. package/lib/agent-cli-provider/acp-stdio-runner.js.map +1 -0
  9. package/lib/agent-cli-provider/adapters/acp.d.ts +23 -0
  10. package/lib/agent-cli-provider/adapters/acp.d.ts.map +1 -0
  11. package/lib/agent-cli-provider/adapters/acp.js +373 -0
  12. package/lib/agent-cli-provider/adapters/acp.js.map +1 -0
  13. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  14. package/lib/agent-cli-provider/adapters/claude.js +2 -29
  15. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  16. package/lib/agent-cli-provider/adapters/copilot-parser.d.ts +3 -0
  17. package/lib/agent-cli-provider/adapters/copilot-parser.d.ts.map +1 -0
  18. package/lib/agent-cli-provider/adapters/copilot-parser.js +154 -0
  19. package/lib/agent-cli-provider/adapters/copilot-parser.js.map +1 -0
  20. package/lib/agent-cli-provider/adapters/copilot.d.ts +3 -0
  21. package/lib/agent-cli-provider/adapters/copilot.d.ts.map +1 -0
  22. package/lib/agent-cli-provider/adapters/copilot.js +176 -0
  23. package/lib/agent-cli-provider/adapters/copilot.js.map +1 -0
  24. package/lib/agent-cli-provider/adapters/gateway.d.ts +6 -0
  25. package/lib/agent-cli-provider/adapters/gateway.d.ts.map +1 -0
  26. package/lib/agent-cli-provider/adapters/gateway.js +161 -0
  27. package/lib/agent-cli-provider/adapters/gateway.js.map +1 -0
  28. package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
  29. package/lib/agent-cli-provider/adapters/gemini.js +9 -2
  30. package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
  31. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  32. package/lib/agent-cli-provider/adapters/index.js +6 -34
  33. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  34. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  35. package/lib/agent-cli-provider/adapters/opencode.js +5 -1
  36. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  37. package/lib/agent-cli-provider/adapters/pi.d.ts +3 -0
  38. package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -0
  39. package/lib/agent-cli-provider/adapters/pi.js +331 -0
  40. package/lib/agent-cli-provider/adapters/pi.js.map +1 -0
  41. package/lib/agent-cli-provider/claude-command.d.ts +6 -0
  42. package/lib/agent-cli-provider/claude-command.d.ts.map +1 -0
  43. package/lib/agent-cli-provider/claude-command.js +43 -0
  44. package/lib/agent-cli-provider/claude-command.js.map +1 -0
  45. package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -1
  46. package/lib/agent-cli-provider/contract-actions.js +13 -17
  47. package/lib/agent-cli-provider/contract-actions.js.map +1 -1
  48. package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -1
  49. package/lib/agent-cli-provider/contract-invoke.js +10 -3
  50. package/lib/agent-cli-provider/contract-invoke.js.map +1 -1
  51. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  52. package/lib/agent-cli-provider/contract-options.js +57 -2
  53. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  54. package/lib/agent-cli-provider/contract-support.d.ts +1 -0
  55. package/lib/agent-cli-provider/contract-support.d.ts.map +1 -1
  56. package/lib/agent-cli-provider/contract-support.js +25 -3
  57. package/lib/agent-cli-provider/contract-support.js.map +1 -1
  58. package/lib/agent-cli-provider/errors.d.ts +1 -0
  59. package/lib/agent-cli-provider/errors.d.ts.map +1 -1
  60. package/lib/agent-cli-provider/errors.js +14 -5
  61. package/lib/agent-cli-provider/errors.js.map +1 -1
  62. package/lib/agent-cli-provider/gateway-client.d.ts +32 -0
  63. package/lib/agent-cli-provider/gateway-client.d.ts.map +1 -0
  64. package/lib/agent-cli-provider/gateway-client.js +135 -0
  65. package/lib/agent-cli-provider/gateway-client.js.map +1 -0
  66. package/lib/agent-cli-provider/gateway-runner.d.ts +9 -0
  67. package/lib/agent-cli-provider/gateway-runner.d.ts.map +1 -0
  68. package/lib/agent-cli-provider/gateway-runner.js +294 -0
  69. package/lib/agent-cli-provider/gateway-runner.js.map +1 -0
  70. package/lib/agent-cli-provider/gateway-tools.d.ts +14 -0
  71. package/lib/agent-cli-provider/gateway-tools.d.ts.map +1 -0
  72. package/lib/agent-cli-provider/gateway-tools.js +495 -0
  73. package/lib/agent-cli-provider/gateway-tools.js.map +1 -0
  74. package/lib/agent-cli-provider/index.d.ts +3 -2
  75. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  76. package/lib/agent-cli-provider/index.js +14 -1
  77. package/lib/agent-cli-provider/index.js.map +1 -1
  78. package/lib/agent-cli-provider/invoke-evidence.d.ts +2 -2
  79. package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
  80. package/lib/agent-cli-provider/invoke-evidence.js +17 -3
  81. package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
  82. package/lib/agent-cli-provider/provider-registry.d.ts +437 -0
  83. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -0
  84. package/lib/agent-cli-provider/provider-registry.js +411 -0
  85. package/lib/agent-cli-provider/provider-registry.js.map +1 -0
  86. package/lib/agent-cli-provider/single-agent-runtime.d.ts +7 -0
  87. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  88. package/lib/agent-cli-provider/single-agent-runtime.js +139 -19
  89. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  90. package/lib/agent-cli-provider/types.d.ts +106 -4
  91. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  92. package/lib/agent-cli-provider/types.js.map +1 -1
  93. package/lib/detached-startup.js +4 -2
  94. package/lib/docker-config.js +24 -13
  95. package/lib/provider-defaults.js +15 -4
  96. package/lib/provider-detection.js +2 -0
  97. package/lib/provider-names.js +32 -12
  98. package/lib/repo-settings.js +15 -1
  99. package/lib/run-mode.js +18 -6
  100. package/lib/run-plan.js +32 -0
  101. package/lib/settings/claude-auth.js +3 -6
  102. package/lib/settings.js +17 -2
  103. package/lib/setup-apply.js +300 -0
  104. package/lib/setup-journal.js +109 -0
  105. package/lib/setup-plan.js +406 -0
  106. package/lib/setup-undo.js +88 -0
  107. package/lib/start-cluster.js +34 -9
  108. package/lib/stream-json-parser.js +7 -5
  109. package/package.json +33 -3
  110. package/scripts/live-provider-smoke.js +221 -0
  111. package/src/agent/agent-lifecycle.js +7 -4
  112. package/src/agent/agent-quality-gate-schema.js +2 -2
  113. package/src/agent/agent-task-executor.js +41 -2
  114. package/src/agent-cli-provider/acp-stdio-runner.ts +314 -0
  115. package/src/agent-cli-provider/adapters/acp.ts +493 -0
  116. package/src/agent-cli-provider/adapters/claude.ts +2 -32
  117. package/src/agent-cli-provider/adapters/copilot-parser.ts +166 -0
  118. package/src/agent-cli-provider/adapters/copilot.ts +231 -0
  119. package/src/agent-cli-provider/adapters/gateway.ts +186 -0
  120. package/src/agent-cli-provider/adapters/gemini.ts +9 -2
  121. package/src/agent-cli-provider/adapters/index.ts +5 -36
  122. package/src/agent-cli-provider/adapters/opencode.ts +4 -1
  123. package/src/agent-cli-provider/adapters/pi.ts +410 -0
  124. package/src/agent-cli-provider/claude-command.ts +47 -0
  125. package/src/agent-cli-provider/contract-actions.ts +14 -17
  126. package/src/agent-cli-provider/contract-invoke.ts +12 -3
  127. package/src/agent-cli-provider/contract-options.ts +72 -3
  128. package/src/agent-cli-provider/contract-support.ts +30 -3
  129. package/src/agent-cli-provider/errors.ts +14 -4
  130. package/src/agent-cli-provider/gateway-client.ts +170 -0
  131. package/src/agent-cli-provider/gateway-runner.ts +353 -0
  132. package/src/agent-cli-provider/gateway-tools.ts +616 -0
  133. package/src/agent-cli-provider/index.ts +28 -0
  134. package/src/agent-cli-provider/invoke-evidence.ts +24 -3
  135. package/src/agent-cli-provider/provider-registry.ts +515 -0
  136. package/src/agent-cli-provider/single-agent-runtime.ts +192 -18
  137. package/src/agent-cli-provider/types.ts +145 -4
  138. package/src/config-validator.js +33 -15
  139. package/src/isolation-manager.js +124 -19
  140. package/src/ledger.js +3 -0
  141. package/src/message-bus.js +12 -2
  142. package/src/orchestrator.js +37 -17
  143. package/src/preflight.js +83 -30
  144. package/src/providers/capabilities.js +15 -42
  145. package/src/providers/index.js +52 -72
  146. package/src/worktree-claude-config.js +17 -0
  147. package/task-lib/commands/run.js +1 -0
  148. package/task-lib/provider-helper-runtime.js +11 -0
  149. package/task-lib/runner.js +7 -0
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+
8
+ const helper = require('../lib/agent-cli-provider');
9
+
10
+ const SENTINEL = process.env.ZEROSHOT_LIVE_SENTINEL || 'ZEROSHOT_LIVE_SMOKE_OK';
11
+ const TIMEOUT_MS = Number(process.env.ZEROSHOT_LIVE_TIMEOUT_MS || 120000);
12
+ const DEFAULT_PROMPT = `Reply with exactly this text and nothing else: ${SENTINEL}`;
13
+
14
+ function fail(message) {
15
+ console.error(message);
16
+ process.exitCode = 1;
17
+ }
18
+
19
+ function usage() {
20
+ return [
21
+ 'Live provider smoke requires explicit provider selection.',
22
+ '',
23
+ 'Examples:',
24
+ ' ZEROSHOT_LIVE_PROVIDERS=all npm run test:providers:live',
25
+ ' ZEROSHOT_LIVE_PROVIDERS=claude,codex,gemini npm run test:providers:live',
26
+ ' ZEROSHOT_LIVE_PROVIDERS=pi npm run test:providers:live',
27
+ ' ZEROSHOT_LIVE_PROVIDERS=copilot npm run test:providers:live',
28
+ ' ZEROSHOT_LIVE_PROVIDERS=gateway \\',
29
+ ' ZEROSHOT_LIVE_GATEWAY_BASE_URL=https://openrouter.ai/api/v1 \\',
30
+ ' ZEROSHOT_LIVE_GATEWAY_API_KEY=... \\',
31
+ ' ZEROSHOT_LIVE_GATEWAY_MODEL=openai/gpt-5.4 \\',
32
+ ' npm run test:providers:live',
33
+ '',
34
+ 'This command invokes real provider CLIs or a real gateway endpoint. It is intentionally',
35
+ 'not part of the normal CI suite because it can require user auth and paid API calls.',
36
+ ].join('\n');
37
+ }
38
+
39
+ function parseProviders() {
40
+ const raw = process.env.ZEROSHOT_LIVE_PROVIDERS;
41
+ if (!raw) {
42
+ console.error(usage());
43
+ process.exit(2);
44
+ }
45
+
46
+ const providers = [];
47
+ const allProviders = helper.listProviderAdapters();
48
+ for (const item of raw.split(',')) {
49
+ const trimmed = item.trim();
50
+ if (trimmed.toLowerCase() === 'all') {
51
+ for (const provider of allProviders) {
52
+ if (!providers.includes(provider)) providers.push(provider);
53
+ }
54
+ continue;
55
+ }
56
+
57
+ const normalized = helper.normalizeProviderName(trimmed);
58
+ if (!normalized) continue;
59
+ if (!allProviders.includes(normalized)) {
60
+ throw new Error(
61
+ `Unknown provider "${item}". Valid providers: ${allProviders.join(', ')}, all`
62
+ );
63
+ }
64
+ if (!providers.includes(normalized)) providers.push(normalized);
65
+ }
66
+
67
+ if (providers.length === 0) {
68
+ throw new Error('ZEROSHOT_LIVE_PROVIDERS did not contain any provider names.');
69
+ }
70
+ return providers;
71
+ }
72
+
73
+ function readJsonEnv(key) {
74
+ const value = process.env[key];
75
+ if (!value) return undefined;
76
+ try {
77
+ return JSON.parse(value);
78
+ } catch (error) {
79
+ throw new Error(`${key} must be valid JSON: ${error.message}`);
80
+ }
81
+ }
82
+
83
+ function liveGatewayOptions(cwd) {
84
+ const baseUrl = process.env.ZEROSHOT_LIVE_GATEWAY_BASE_URL;
85
+ const apiKey = process.env.ZEROSHOT_LIVE_GATEWAY_API_KEY;
86
+ const model = process.env.ZEROSHOT_LIVE_GATEWAY_MODEL;
87
+ const headers = readJsonEnv('ZEROSHOT_LIVE_GATEWAY_HEADERS_JSON');
88
+
89
+ for (const [key, value] of [
90
+ ['ZEROSHOT_LIVE_GATEWAY_BASE_URL', baseUrl],
91
+ ['ZEROSHOT_LIVE_GATEWAY_API_KEY', apiKey],
92
+ ['ZEROSHOT_LIVE_GATEWAY_MODEL', model],
93
+ ]) {
94
+ if (!value) throw new Error(`gateway live smoke requires ${key}.`);
95
+ }
96
+
97
+ return {
98
+ cwd,
99
+ gateway: {
100
+ baseUrl,
101
+ apiKey,
102
+ model,
103
+ ...(headers === undefined ? {} : { headers }),
104
+ toolPolicy: {
105
+ roots: ['.'],
106
+ commands: [],
107
+ },
108
+ },
109
+ };
110
+ }
111
+
112
+ function providerOptions(provider, cwd) {
113
+ if (provider === 'gateway') return liveGatewayOptions(cwd);
114
+
115
+ return {
116
+ cwd,
117
+ outputFormat: 'stream-json',
118
+ };
119
+ }
120
+
121
+ function eventText(event) {
122
+ if (!event || typeof event !== 'object') return '';
123
+ if (event.type === 'text' || event.type === 'thinking') return event.text || '';
124
+ if (event.type === 'result') {
125
+ return [event.result, event.text, event.message].filter(Boolean).join('\n');
126
+ }
127
+ return '';
128
+ }
129
+
130
+ function summarizeResult(envelope) {
131
+ if (!envelope || typeof envelope !== 'object') return 'no envelope';
132
+ if (!envelope.ok) {
133
+ const error = envelope.error || {};
134
+ return `${error.code || 'error'}: ${error.message || 'unknown failure'}`;
135
+ }
136
+
137
+ const result = envelope.result || {};
138
+ const events = Array.isArray(result.events) ? result.events : [];
139
+ const text = events.map(eventText).join('\n').trim();
140
+ const last = events.at(-1);
141
+ return JSON.stringify(
142
+ {
143
+ provider: envelope.provider,
144
+ exitCode: result.exitCode,
145
+ signal: result.signal,
146
+ timedOut: result.timedOut,
147
+ classification: result.classification || null,
148
+ eventCount: events.length,
149
+ lastEventType: last && typeof last === 'object' ? last.type : null,
150
+ text: text.slice(0, 500),
151
+ },
152
+ null,
153
+ 2
154
+ );
155
+ }
156
+
157
+ function assertSmokePassed(provider, response) {
158
+ if (response.exitCode !== 0) {
159
+ throw new Error(
160
+ `${provider} contract exited ${response.exitCode}: ${summarizeResult(response.envelope)}`
161
+ );
162
+ }
163
+ if (!response.envelope.ok) {
164
+ throw new Error(`${provider} returned a contract error: ${summarizeResult(response.envelope)}`);
165
+ }
166
+
167
+ const result = response.envelope.result || {};
168
+ const events = Array.isArray(result.events) ? result.events : [];
169
+ const text = events.map(eventText).join('\n');
170
+ const finalResult = events.findLast((event) => event && event.type === 'result');
171
+
172
+ if (result.timedOut) {
173
+ throw new Error(`${provider} timed out after ${result.timeoutMs || TIMEOUT_MS}ms.`);
174
+ }
175
+ if (finalResult && finalResult.success === false) {
176
+ throw new Error(
177
+ `${provider} produced a failed result event: ${summarizeResult(response.envelope)}`
178
+ );
179
+ }
180
+ if (!text.includes(SENTINEL)) {
181
+ throw new Error(
182
+ `${provider} did not return sentinel ${SENTINEL}.\nResult:\n${summarizeResult(response.envelope)}`
183
+ );
184
+ }
185
+ }
186
+
187
+ async function smokeProvider(provider) {
188
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `zeroshot-live-${provider}-`));
189
+ try {
190
+ const prompt = process.env.ZEROSHOT_LIVE_PROMPT || DEFAULT_PROMPT;
191
+ const response = await helper.runProviderExecutable(
192
+ JSON.stringify({
193
+ schemaVersion: 1,
194
+ command: 'invoke',
195
+ provider,
196
+ context: prompt,
197
+ options: providerOptions(provider, tempDir),
198
+ timeoutMs: TIMEOUT_MS,
199
+ })
200
+ );
201
+
202
+ assertSmokePassed(provider, response);
203
+ console.log(`✓ ${provider} live smoke returned ${SENTINEL}`);
204
+ } finally {
205
+ fs.rmSync(tempDir, { recursive: true, force: true });
206
+ }
207
+ }
208
+
209
+ async function main() {
210
+ const providers = parseProviders();
211
+ console.log(`Running live provider smoke for: ${providers.join(', ')}`);
212
+ console.log(`Sentinel: ${SENTINEL}`);
213
+
214
+ for (const provider of providers) {
215
+ await smokeProvider(provider);
216
+ }
217
+ }
218
+
219
+ main().catch((error) => {
220
+ fail(error.stack || error.message || String(error));
221
+ });
@@ -61,10 +61,6 @@ async function createValidatorIsolation(agent, isolationConfig) {
61
61
 
62
62
  const cluster = agent.cluster || {};
63
63
  const workDir = agent.config?.cwd || cluster.worktree?.path || cluster.cwd || process.cwd();
64
- const image = isolationConfig.image;
65
- await IsolationManager.ensureImage(image);
66
-
67
- const manager = new IsolationManager({ image });
68
64
  const providerName = normalizeProviderName(
69
65
  (agent._resolveProvider && agent._resolveProvider()) ||
70
66
  cluster.config?.forceProvider ||
@@ -72,6 +68,11 @@ async function createValidatorIsolation(agent, isolationConfig) {
72
68
  loadSettings().defaultProvider ||
73
69
  'claude'
74
70
  );
71
+ // Run validators on the provider's image variant (installs its CLI as a Docker-cached layer).
72
+ const image = IsolationManager.imageForProvider(providerName, isolationConfig.image);
73
+ await IsolationManager.ensureImage(image, true, IsolationManager.providerBuildArgs(providerName));
74
+
75
+ const manager = new IsolationManager({ image });
75
76
 
76
77
  const isolationClusterId = `${cluster.id}-validators`;
77
78
  const containerId = await manager.createContainer(isolationClusterId, {
@@ -200,6 +201,8 @@ function start(agent) {
200
201
  * @returns {Promise<void>}
201
202
  */
202
203
  async function stop(agent) {
204
+ stopLivenessCheck(agent);
205
+
203
206
  if (!agent.running) {
204
207
  return;
205
208
  }
@@ -41,11 +41,11 @@ function buildQualityGateSchema() {
41
41
  required: ['command', 'exitCode', 'output'],
42
42
  },
43
43
  completedAt: {
44
- type: ['string', 'number'],
44
+ anyOf: [{ type: 'string' }, { type: 'number' }],
45
45
  description: 'When the gate completed, as an ISO string or numeric timestamp.',
46
46
  },
47
47
  timestamp: {
48
- type: ['string', 'number'],
48
+ anyOf: [{ type: 'string' }, { type: 'number' }],
49
49
  description: 'Alternate completion timestamp, as an ISO string or numeric timestamp.',
50
50
  },
51
51
  stale: {
@@ -14,12 +14,15 @@ const { spawn, spawnSync } = require('child_process');
14
14
  const path = require('path');
15
15
  const fs = require('fs');
16
16
  const os = require('os');
17
- const { parseProviderChunk } = require('../providers');
17
+ const { parseProviderChunk, getProvider } = require('../providers');
18
18
  const { getTask } = require('../../task-lib/store.js');
19
19
  const { loadSettings } = require('../../lib/settings.js');
20
20
  const { resolveClaudeAuth } = require('../../lib/settings/claude-auth.js');
21
21
  const { prependWorktreeToolBinToEnv } = require('../worktree-tooling-env.js');
22
- const { prepareClaudeConfigDir } = require('../worktree-claude-config.js');
22
+ const {
23
+ prepareClaudeConfigDir,
24
+ resolveRepoMcpConfigPath,
25
+ } = require('../worktree-claude-config.js');
23
26
  const { buildRawLogOnlyMetadata } = require('./context-replay-policy');
24
27
 
25
28
  function runCommandWithTimeout(command, args, options = {}, callback = null) {
@@ -733,9 +736,45 @@ function buildTaskRunArgs({ agent, providerName, modelSpec, runOutputFormat }) {
733
736
  args.push('--json-schema', schema);
734
737
  }
735
738
 
739
+ // MCP servers: providers whose CLI accepts an MCP config flag (e.g. Copilot's
740
+ // --additional-mcp-config) cannot use the Claude config-dir overlay, so forward the repo's
741
+ // `.mcp.json` (the same MCP source Claude consumes) inline via `--mcp-config`.
742
+ for (const mcpArg of resolveMcpConfigArgs(agent, providerName)) {
743
+ args.push(mcpArg);
744
+ }
745
+
736
746
  return args;
737
747
  }
738
748
 
749
+ /**
750
+ * Build the `--mcp-config` args for a task-run invocation, or [] when they don't apply.
751
+ *
752
+ * Only providers whose adapter models an MCP config CLI flag receive it — Claude consumes MCP via
753
+ * the config-dir `.mcp.json` overlay (see prepareClaudeConfigDir) and needs no flag. The repo
754
+ * `.mcp.json` content is inlined (not passed as an @<path> reference) so the identical value works
755
+ * under local, worktree, and Docker isolation without host/container path translation.
756
+ */
757
+ function resolveMcpConfigArgs(agent, providerName) {
758
+ if (!providerModelsMcpConfigFlag(providerName)) return [];
759
+
760
+ const mcpPath = resolveRepoMcpConfigPath({
761
+ cwd: agent.config?.cwd || process.cwd(),
762
+ worktreePath: agent.worktree?.path || null,
763
+ });
764
+ if (!mcpPath) return [];
765
+
766
+ const content = fs.readFileSync(mcpPath, 'utf8').trim();
767
+ if (content.length === 0) return [];
768
+
769
+ return ['--mcp-config', content];
770
+ }
771
+
772
+ /** True when the provider's adapter models an MCP config CLI flag (currently only Copilot). */
773
+ function providerModelsMcpConfigFlag(providerName) {
774
+ const adapter = getProvider(providerName).adapter;
775
+ return 'supportsMcpConfig' in adapter.detectCliFeatures('');
776
+ }
777
+
739
778
  function maybeLogStreamJsonNotice(agent, runOutputFormat) {
740
779
  if (agent.config.jsonSchema && runOutputFormat !== 'json' && !agent.quiet) {
741
780
  agent._log(
@@ -0,0 +1,314 @@
1
+ /* eslint-disable max-lines-per-function */
2
+ import { spawn } from 'node:child_process';
3
+ import { omitProcessControlEnv, omitUnsafeProviderEnv } from './env-safety';
4
+ import type { CommandSpec, ProviderId } from './types';
5
+ import type { ProcessResult, ProcessRunnerOptions } from './process-runner';
6
+
7
+ const DEFAULT_TIMEOUT_KILL_GRACE_MS = 100;
8
+
9
+ interface JsonRpcRequest {
10
+ readonly jsonrpc: '2.0';
11
+ readonly id: number;
12
+ readonly method: string;
13
+ readonly params: Record<string, unknown>;
14
+ }
15
+
16
+ interface JsonRpcResponse {
17
+ readonly jsonrpc?: string;
18
+ readonly id?: number | string | null;
19
+ readonly result?: unknown;
20
+ readonly error?: unknown;
21
+ }
22
+
23
+ interface PendingRequest {
24
+ resolve(response: JsonRpcResponse): void;
25
+ reject(error: Error): void;
26
+ }
27
+
28
+ function spawnOptions(commandSpec: CommandSpec): {
29
+ readonly shell: false;
30
+ readonly env: NodeJS.ProcessEnv;
31
+ readonly stdio: ['pipe', 'pipe', 'pipe'];
32
+ cwd?: string;
33
+ } {
34
+ const options: {
35
+ readonly shell: false;
36
+ readonly env: NodeJS.ProcessEnv;
37
+ readonly stdio: ['pipe', 'pipe', 'pipe'];
38
+ cwd?: string;
39
+ } = {
40
+ shell: false,
41
+ env: { ...omitProcessControlEnv(process.env), ...omitUnsafeProviderEnv(commandSpec.env) },
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ };
44
+ if (commandSpec.cwd !== undefined) options.cwd = commandSpec.cwd;
45
+ return options;
46
+ }
47
+
48
+ function processResult(input: {
49
+ readonly stdout: readonly Buffer[];
50
+ readonly stderr: readonly Buffer[];
51
+ readonly exitCode: number | null;
52
+ readonly signal: string | null;
53
+ readonly startedAt: number;
54
+ readonly timedOut: boolean;
55
+ readonly timeoutMs?: number;
56
+ }): ProcessResult {
57
+ return {
58
+ stdout: Buffer.concat(input.stdout).toString('utf8'),
59
+ stderr: Buffer.concat(input.stderr).toString('utf8'),
60
+ exitCode: input.exitCode,
61
+ signal: input.signal,
62
+ durationMs: Date.now() - input.startedAt,
63
+ timedOut: input.timedOut,
64
+ ...(input.timedOut && input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
65
+ };
66
+ }
67
+
68
+ function isRecord(value: unknown): value is Record<string, unknown> {
69
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
70
+ }
71
+
72
+ function parseProtocolLine(line: string): Record<string, unknown> {
73
+ let parsed: unknown;
74
+ try {
75
+ parsed = JSON.parse(line);
76
+ } catch (error) {
77
+ const reason = error instanceof Error ? error.message : 'Unknown JSON parse error.';
78
+ throw new Error(`malformed ACP stdout JSON: ${reason}`);
79
+ }
80
+ if (!isRecord(parsed)) throw new Error('malformed ACP stdout JSON: expected a JSON object.');
81
+ return parsed;
82
+ }
83
+
84
+ function createPromptParams(sessionId: string, prompt: string): Record<string, unknown> {
85
+ return {
86
+ sessionId,
87
+ prompt: [
88
+ {
89
+ role: 'user',
90
+ content: [{ type: 'text', text: prompt }],
91
+ },
92
+ ],
93
+ };
94
+ }
95
+
96
+ export function runAcpStdioPrompt(
97
+ provider: ProviderId,
98
+ commandSpec: CommandSpec,
99
+ prompt: string,
100
+ options: ProcessRunnerOptions = {}
101
+ ): Promise<ProcessResult> {
102
+ return new Promise<ProcessResult>((resolve, reject) => {
103
+ const startedAt = Date.now();
104
+ const stdout: Buffer[] = [];
105
+ const stderr: Buffer[] = [];
106
+ const child = spawn(commandSpec.binary, [...commandSpec.args], spawnOptions(commandSpec));
107
+ const pending = new Map<number, PendingRequest>();
108
+ let stdoutBuffer = '';
109
+ let nextId = 1;
110
+ let timedOut = false;
111
+ let protocolFailure: Error | null = null;
112
+ let sessionId: string | null = null;
113
+ let closeResolved = false;
114
+ let timeout: NodeJS.Timeout | undefined;
115
+ let timeoutKill: NodeJS.Timeout | undefined;
116
+
117
+ function cleanupTimers(): void {
118
+ if (timeout !== undefined) clearTimeout(timeout);
119
+ if (timeoutKill !== undefined) clearTimeout(timeoutKill);
120
+ }
121
+
122
+ function finalize(exitCode: number | null, signal: string | null): void {
123
+ if (closeResolved) return;
124
+ closeResolved = true;
125
+ cleanupTimers();
126
+ if (protocolFailure) {
127
+ const failureMessage = protocolFailure.message;
128
+ if (!stderr.some((chunk) => chunk.toString('utf8').includes(failureMessage))) {
129
+ stderr.push(Buffer.from(`${failureMessage}\n`, 'utf8'));
130
+ }
131
+ }
132
+ const effectiveExitCode =
133
+ protocolFailure && exitCode === 0 && signal === null ? 1 : exitCode;
134
+ resolve(
135
+ processResult({
136
+ stdout,
137
+ stderr,
138
+ exitCode: effectiveExitCode,
139
+ signal,
140
+ startedAt,
141
+ timedOut,
142
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
143
+ })
144
+ );
145
+ }
146
+
147
+ function failClosed(message: string): void {
148
+ if (protocolFailure) return;
149
+ protocolFailure = new Error(`${provider} ACP stdio fail-closed: ${message}`);
150
+ for (const request of pending.values()) {
151
+ request.reject(protocolFailure);
152
+ }
153
+ pending.clear();
154
+ if (sessionId && child.stdin && !child.stdin.destroyed) {
155
+ child.stdin.write(
156
+ `${JSON.stringify({
157
+ jsonrpc: '2.0',
158
+ method: 'session/cancel',
159
+ params: { sessionId },
160
+ })}\n`
161
+ );
162
+ }
163
+ child.stdin?.end();
164
+ child.kill('SIGTERM');
165
+ }
166
+
167
+ function handleProtocolMessage(message: Record<string, unknown>): void {
168
+ const responseId = typeof message.id === 'number' ? message.id : null;
169
+ if (responseId !== null && (message.result !== undefined || message.error !== undefined)) {
170
+ const request = pending.get(responseId);
171
+ if (request) {
172
+ pending.delete(responseId);
173
+ request.resolve(message);
174
+ }
175
+ return;
176
+ }
177
+
178
+ const method = typeof message.method === 'string' ? message.method : null;
179
+ if (!method) return;
180
+ if (method === 'session/update') return;
181
+ if (method.startsWith('_')) return;
182
+ if (method === 'session/request_permission') {
183
+ failClosed('unsupported session/request_permission callback.');
184
+ return;
185
+ }
186
+ if (method.startsWith('fs/')) {
187
+ failClosed(`unsupported ${method} callback.`);
188
+ return;
189
+ }
190
+ if (method.startsWith('terminal/')) {
191
+ failClosed(`unsupported ${method} callback.`);
192
+ return;
193
+ }
194
+ if (method.startsWith('session/')) {
195
+ failClosed(`unsupported ${method} session-control callback.`);
196
+ }
197
+ }
198
+
199
+ function consumeStdoutLine(line: string): void {
200
+ try {
201
+ handleProtocolMessage(parseProtocolLine(line));
202
+ } catch (error) {
203
+ failClosed(error instanceof Error ? error.message : 'malformed ACP stdout JSON.');
204
+ }
205
+ }
206
+
207
+ function flushStdoutRemainder(): void {
208
+ const line = stdoutBuffer.trim();
209
+ stdoutBuffer = '';
210
+ if (line) consumeStdoutLine(line);
211
+ }
212
+
213
+ function flushStdout(data: Buffer): void {
214
+ stdout.push(data);
215
+ stdoutBuffer += data.toString('utf8');
216
+ let newline = stdoutBuffer.indexOf('\n');
217
+ while (newline !== -1) {
218
+ const line = stdoutBuffer.slice(0, newline).trim();
219
+ stdoutBuffer = stdoutBuffer.slice(newline + 1);
220
+ if (line) consumeStdoutLine(line);
221
+ newline = stdoutBuffer.indexOf('\n');
222
+ }
223
+ }
224
+
225
+ function sendRequest(method: string, params: Record<string, unknown>): Promise<JsonRpcResponse> {
226
+ const id = nextId;
227
+ nextId += 1;
228
+ const request: JsonRpcRequest = {
229
+ jsonrpc: '2.0',
230
+ id,
231
+ method,
232
+ params,
233
+ };
234
+ return new Promise<JsonRpcResponse>((requestResolve, requestReject) => {
235
+ pending.set(id, { resolve: requestResolve, reject: requestReject });
236
+ child.stdin?.write(`${JSON.stringify(request)}\n`);
237
+ });
238
+ }
239
+
240
+ async function driveProtocol(): Promise<void> {
241
+ const initialize = await sendRequest('initialize', {
242
+ protocolVersion: 1,
243
+ clientInfo: {
244
+ name: '@the-open-engine/zeroshot',
245
+ version: '1',
246
+ },
247
+ clientCapabilities: {
248
+ fs: false,
249
+ terminal: false,
250
+ promptCapabilities: {
251
+ image: false,
252
+ },
253
+ },
254
+ });
255
+ if (initialize.error !== undefined) {
256
+ child.stdin?.end();
257
+ return;
258
+ }
259
+
260
+ const session = await sendRequest('session/new', {
261
+ cwd: commandSpec.cwd ?? process.cwd(),
262
+ mcpServers: [],
263
+ });
264
+ if (session.error !== undefined) {
265
+ child.stdin?.end();
266
+ return;
267
+ }
268
+ const sessionResult = isRecord(session.result) ? session.result : {};
269
+ const nextSessionId = typeof sessionResult.sessionId === 'string' ? sessionResult.sessionId : null;
270
+ if (!nextSessionId) {
271
+ failClosed('session/new response omitted sessionId.');
272
+ return;
273
+ }
274
+ sessionId = nextSessionId;
275
+
276
+ await sendRequest('session/prompt', createPromptParams(sessionId, prompt));
277
+ child.stdin?.end();
278
+ }
279
+
280
+ if (options.timeoutMs !== undefined) {
281
+ timeout = setTimeout(() => {
282
+ timedOut = true;
283
+ if (sessionId && child.stdin && !child.stdin.destroyed) {
284
+ child.stdin.write(
285
+ `${JSON.stringify({
286
+ jsonrpc: '2.0',
287
+ method: 'session/cancel',
288
+ params: { sessionId },
289
+ })}\n`
290
+ );
291
+ }
292
+ child.kill('SIGTERM');
293
+ timeoutKill = setTimeout(() => {
294
+ if (!closeResolved) child.kill('SIGKILL');
295
+ }, options.timeoutKillGraceMs ?? DEFAULT_TIMEOUT_KILL_GRACE_MS);
296
+ }, options.timeoutMs);
297
+ }
298
+
299
+ child.stdout?.on('data', flushStdout);
300
+ child.stdout?.once('end', flushStdoutRemainder);
301
+ child.stderr?.on('data', (data: Buffer) => stderr.push(data));
302
+ child.once('error', (error) => {
303
+ cleanupTimers();
304
+ reject(error);
305
+ });
306
+ child.once('close', (exitCode, signal) => {
307
+ finalize(exitCode, signal);
308
+ });
309
+
310
+ driveProtocol().catch((error: unknown) => {
311
+ failClosed(error instanceof Error ? error.message : 'ACP protocol failed.');
312
+ });
313
+ });
314
+ }