agentxchain 2.130.0 → 2.130.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/scripts/release-preflight.sh +18 -5
- package/src/commands/connector.js +1 -0
- package/src/commands/doctor.js +8 -19
- package/src/lib/connector-probe.js +42 -27
- package/src/lib/connector-validate.js +21 -0
- package/src/lib/governed-state.js +2 -1
- package/src/lib/runtime-spawn-context.js +163 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentxchain",
|
|
3
|
-
"version": "2.130.
|
|
3
|
+
"version": "2.130.1",
|
|
4
4
|
"description": "CLI for AgentXchain — governed multi-agent software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"dev": "node bin/agentxchain.js",
|
|
25
25
|
"test": "npm run test:vitest && npm run test:node",
|
|
26
26
|
"test:vitest": "vitest run --reporter=verbose",
|
|
27
|
-
"test:
|
|
27
|
+
"test:beta": "node --test test/beta-tester-scenarios/*.test.js",
|
|
28
|
+
"test:node": "node --test test/*.test.js test/beta-tester-scenarios/*.test.js",
|
|
28
29
|
"preflight:release": "bash scripts/release-preflight.sh",
|
|
29
30
|
"preflight:release:strict": "bash scripts/release-preflight.sh --strict",
|
|
30
31
|
"check:release-alignment": "node scripts/check-release-alignment.mjs",
|
|
@@ -110,24 +110,37 @@ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
|
|
|
110
110
|
echo "[3/7] Release-gate tests (targeted subset)"
|
|
111
111
|
# In publish-gate mode, run only release-critical tests to avoid CI hangs.
|
|
112
112
|
# The full test suite is a pre-tag responsibility, not a publish-time gate.
|
|
113
|
-
|
|
113
|
+
GATE_TEST_PATTERNS=(
|
|
114
114
|
test/release-preflight.test.js
|
|
115
115
|
test/release-docs-content.test.js
|
|
116
116
|
test/release-notes-gate.test.js
|
|
117
117
|
test/release-identity-hardening.test.js
|
|
118
118
|
test/normalized-config.test.js
|
|
119
119
|
test/conformance.test.js
|
|
120
|
+
test/beta-tester-scenarios/*.test.js
|
|
120
121
|
)
|
|
121
122
|
GATE_TEST_ARGS=()
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
shopt -s nullglob
|
|
124
|
+
for pattern in "${GATE_TEST_PATTERNS[@]}"; do
|
|
125
|
+
for t in $pattern; do
|
|
124
126
|
GATE_TEST_ARGS+=("$t")
|
|
125
|
-
|
|
127
|
+
done
|
|
126
128
|
done
|
|
129
|
+
shopt -u nullglob
|
|
127
130
|
if [[ ${#GATE_TEST_ARGS[@]} -eq 0 ]]; then
|
|
128
131
|
fail "No release-gate test files found"
|
|
129
132
|
else
|
|
130
|
-
|
|
133
|
+
BETA_TEST_COUNT=0
|
|
134
|
+
for t in "${GATE_TEST_ARGS[@]}"; do
|
|
135
|
+
if [[ "$t" == test/beta-tester-scenarios/*.test.js ]]; then
|
|
136
|
+
BETA_TEST_COUNT=$((BETA_TEST_COUNT + 1))
|
|
137
|
+
fi
|
|
138
|
+
done
|
|
139
|
+
if [[ "$BETA_TEST_COUNT" -eq 0 ]]; then
|
|
140
|
+
fail "No beta-tester scenario tests found for release-gate verification"
|
|
141
|
+
TEST_OUTPUT=""
|
|
142
|
+
TEST_STATUS=1
|
|
143
|
+
elif run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 node --test "${GATE_TEST_ARGS[@]}"; then
|
|
131
144
|
TEST_STATUS=0
|
|
132
145
|
else
|
|
133
146
|
TEST_STATUS=$?
|
|
@@ -105,6 +105,7 @@ export async function connectorCheckCommand(runtimeId, options = {}) {
|
|
|
105
105
|
const result = await probeConfiguredConnectors(context.config, {
|
|
106
106
|
runtimeId: runtimeId || null,
|
|
107
107
|
timeoutMs,
|
|
108
|
+
root: context.root,
|
|
108
109
|
onProbeStart: options.json ? null : (probeRuntimeId, runtime) => {
|
|
109
110
|
console.log(` ${chalk.dim('…')} Probing ${chalk.bold(probeRuntimeId)} ${chalk.dim(`(${runtime.type})`)}`);
|
|
110
111
|
},
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
|
-
import { execFileSync
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { loadConfig, loadLock, findProjectRoot, loadProjectState } from '../lib/config.js';
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { detectActiveTurnBindingDrift, detectStateBundleDesync } from '../lib/governed-state.js';
|
|
21
21
|
import { findPendingApprovedIntents } from '../lib/intake.js';
|
|
22
22
|
import { checkCleanBaseline } from '../lib/repo-observer.js';
|
|
23
|
+
import { probeRuntimeSpawnContext } from '../lib/runtime-spawn-context.js';
|
|
23
24
|
|
|
24
25
|
export async function doctorCommand(opts = {}) {
|
|
25
26
|
const root = findProjectRoot(process.cwd());
|
|
@@ -90,7 +91,7 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
90
91
|
const runtimes = (normalized && normalized.runtimes) || rawConfig.runtimes || {};
|
|
91
92
|
const rolesByRuntime = buildRolesByRuntime(normalized?.roles || {});
|
|
92
93
|
for (const [rtId, rt] of Object.entries(runtimes)) {
|
|
93
|
-
const check = checkRuntimeReachable(rtId, rt, rolesByRuntime[rtId] || []);
|
|
94
|
+
const check = checkRuntimeReachable(root, rtId, rt, rolesByRuntime[rtId] || []);
|
|
94
95
|
checks.push(check);
|
|
95
96
|
}
|
|
96
97
|
const connectorProbe = getConnectorProbeRecommendation(runtimes);
|
|
@@ -484,7 +485,7 @@ function buildCliVersionCheck(cliVersionHealth) {
|
|
|
484
485
|
};
|
|
485
486
|
}
|
|
486
487
|
|
|
487
|
-
function checkRuntimeReachable(rtId, rt, boundRoleEntries = []) {
|
|
488
|
+
function checkRuntimeReachable(root, rtId, rt, boundRoleEntries = []) {
|
|
488
489
|
const base = { id: `runtime_${rtId}`, name: `Runtime: ${rtId}` };
|
|
489
490
|
|
|
490
491
|
if (!rt || !rt.type) {
|
|
@@ -496,14 +497,8 @@ function checkRuntimeReachable(rtId, rt, boundRoleEntries = []) {
|
|
|
496
497
|
return attachRuntimeContract({ ...base, level: 'pass', detail: 'Manual runtime (no binary needed)' }, rtId, rt, boundRoleEntries);
|
|
497
498
|
|
|
498
499
|
case 'local_cli': {
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
try {
|
|
502
|
-
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
|
|
503
|
-
return attachRuntimeContract({ ...base, level: 'pass', detail: `${cmd} binary found` }, rtId, rt, boundRoleEntries);
|
|
504
|
-
} catch {
|
|
505
|
-
return attachRuntimeContract({ ...base, level: 'fail', detail: `${cmd} not found in PATH` }, rtId, rt, boundRoleEntries);
|
|
506
|
-
}
|
|
500
|
+
const probe = probeRuntimeSpawnContext(root, rt, { runtimeId: rtId });
|
|
501
|
+
return attachRuntimeContract({ ...base, level: probe.ok ? 'pass' : 'fail', detail: probe.detail }, rtId, rt, boundRoleEntries);
|
|
507
502
|
}
|
|
508
503
|
|
|
509
504
|
case 'api_proxy': {
|
|
@@ -523,14 +518,8 @@ function checkRuntimeReachable(rtId, rt, boundRoleEntries = []) {
|
|
|
523
518
|
if (transport === 'streamable_http') {
|
|
524
519
|
return attachRuntimeContract({ ...base, level: 'warn', detail: 'Remote MCP endpoint (cannot verify at doctor time)' }, rtId, rt, boundRoleEntries);
|
|
525
520
|
}
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
try {
|
|
529
|
-
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
|
|
530
|
-
return attachRuntimeContract({ ...base, level: 'pass', detail: `${cmd} binary found` }, rtId, rt, boundRoleEntries);
|
|
531
|
-
} catch {
|
|
532
|
-
return attachRuntimeContract({ ...base, level: 'fail', detail: `${cmd} not found in PATH` }, rtId, rt, boundRoleEntries);
|
|
533
|
-
}
|
|
521
|
+
const probe = probeRuntimeSpawnContext(root, rt, { runtimeId: rtId });
|
|
522
|
+
return attachRuntimeContract({ ...base, level: probe.ok ? 'pass' : 'fail', detail: probe.detail }, rtId, rt, boundRoleEntries);
|
|
534
523
|
}
|
|
535
524
|
|
|
536
525
|
case 'remote_agent':
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
buildProviderHeaders,
|
|
5
3
|
buildProviderRequest,
|
|
6
4
|
PROVIDER_ENDPOINTS,
|
|
7
5
|
} from './adapters/api-proxy-adapter.js';
|
|
6
|
+
import { probeRuntimeSpawnContext } from './runtime-spawn-context.js';
|
|
8
7
|
|
|
9
8
|
const PROBEABLE_RUNTIME_TYPES = new Set(['local_cli', 'api_proxy', 'mcp', 'remote_agent']);
|
|
10
9
|
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
@@ -35,8 +34,8 @@ const KNOWN_CLI_AUTHORITY_FLAGS = [
|
|
|
35
34
|
* Maps binary name to expected transport.
|
|
36
35
|
*/
|
|
37
36
|
const KNOWN_CLI_TRANSPORTS = {
|
|
38
|
-
claude: 'stdin',
|
|
39
|
-
codex: 'argv',
|
|
37
|
+
claude: ['stdin'],
|
|
38
|
+
codex: ['argv', 'stdin'],
|
|
40
39
|
};
|
|
41
40
|
|
|
42
41
|
function formatCommand(command, args = []) {
|
|
@@ -78,11 +77,6 @@ function commandHead(runtime) {
|
|
|
78
77
|
return null;
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
function resolveBinary(command) {
|
|
82
|
-
const resolver = process.platform === 'win32' ? 'where' : 'which';
|
|
83
|
-
execFileSync(resolver, [command], { stdio: 'ignore' });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
80
|
function resolveProviderEndpoint(runtime) {
|
|
87
81
|
if (typeof runtime?.base_url === 'string' && runtime.base_url.trim()) {
|
|
88
82
|
return runtime.base_url.trim();
|
|
@@ -153,7 +147,7 @@ async function probeHttp({ url, method = 'GET', headers = {}, body, timeoutMs })
|
|
|
153
147
|
}
|
|
154
148
|
}
|
|
155
149
|
|
|
156
|
-
async function probeLocalCommand(runtimeId, runtime, probeKindLabel) {
|
|
150
|
+
async function probeLocalCommand(runtimeId, runtime, probeKindLabel, options = {}) {
|
|
157
151
|
const head = commandHead(runtime);
|
|
158
152
|
const base = {
|
|
159
153
|
runtime_id: runtimeId,
|
|
@@ -170,22 +164,22 @@ async function probeLocalCommand(runtimeId, runtime, probeKindLabel) {
|
|
|
170
164
|
};
|
|
171
165
|
}
|
|
172
166
|
|
|
173
|
-
|
|
174
|
-
|
|
167
|
+
const spawnProbe = probeRuntimeSpawnContext(options.root || process.cwd(), runtime, { runtimeId });
|
|
168
|
+
if (spawnProbe.ok) {
|
|
175
169
|
return {
|
|
176
170
|
...base,
|
|
177
171
|
level: 'pass',
|
|
178
|
-
command: head,
|
|
179
|
-
detail:
|
|
180
|
-
};
|
|
181
|
-
} catch {
|
|
182
|
-
return {
|
|
183
|
-
...base,
|
|
184
|
-
level: 'fail',
|
|
185
|
-
command: head,
|
|
186
|
-
detail: `${head} was not found on PATH`,
|
|
172
|
+
command: spawnProbe.command || head,
|
|
173
|
+
detail: spawnProbe.detail,
|
|
187
174
|
};
|
|
188
175
|
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...base,
|
|
179
|
+
level: 'fail',
|
|
180
|
+
command: spawnProbe.command || head,
|
|
181
|
+
detail: spawnProbe.detail,
|
|
182
|
+
};
|
|
189
183
|
}
|
|
190
184
|
|
|
191
185
|
async function probeApiProxy(runtimeId, runtime, timeoutMs) {
|
|
@@ -331,6 +325,27 @@ function analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles) {
|
|
|
331
325
|
if (boundRoles.length === 0) return { warnings };
|
|
332
326
|
|
|
333
327
|
const authoritativeRoles = boundRoles.filter((r) => r.write_authority === 'authoritative');
|
|
328
|
+
const isCodex = binaryName === 'codex' || binaryName.endsWith('/codex');
|
|
329
|
+
|
|
330
|
+
if (isCodex) {
|
|
331
|
+
if (commandTokens[1] !== 'exec') {
|
|
332
|
+
warnings.push({
|
|
333
|
+
probe_kind: 'command_intent',
|
|
334
|
+
level: 'warn',
|
|
335
|
+
detail: 'OpenAI Codex CLI governed local runs should use the non-interactive "exec" subcommand. Top-level "codex" is the interactive entrypoint.',
|
|
336
|
+
fix: 'Use ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox", "{prompt}"]',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (commandTokens.includes('--quiet')) {
|
|
341
|
+
warnings.push({
|
|
342
|
+
probe_kind: 'command_intent',
|
|
343
|
+
level: 'warn',
|
|
344
|
+
detail: 'OpenAI Codex CLI rejects "--quiet" in governed local_cli commands on the current CLI. The command exits before the turn starts.',
|
|
345
|
+
fix: 'Remove "--quiet" and use ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox", "{prompt}"]',
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
334
349
|
|
|
335
350
|
// Check known CLI authority flags
|
|
336
351
|
const knownCli = KNOWN_CLI_AUTHORITY_FLAGS.find((entry) => binaryName === entry.binary || binaryName.endsWith(`/${entry.binary}`));
|
|
@@ -359,7 +374,7 @@ function analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles) {
|
|
|
359
374
|
|
|
360
375
|
// Prompt transport validation
|
|
361
376
|
const transport = runtime.prompt_transport || 'dispatch_bundle_only';
|
|
362
|
-
const
|
|
377
|
+
const knownTransports = KNOWN_CLI_TRANSPORTS[binaryName];
|
|
363
378
|
|
|
364
379
|
if (transport === 'argv' && !commandTokens.some((token) => token.includes('{prompt}'))) {
|
|
365
380
|
warnings.push({
|
|
@@ -370,13 +385,13 @@ function analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles) {
|
|
|
370
385
|
});
|
|
371
386
|
}
|
|
372
387
|
|
|
373
|
-
if (
|
|
388
|
+
if (knownTransports && !knownTransports.includes(transport) && transport !== 'dispatch_bundle_only') {
|
|
374
389
|
const transportLabel = knownCli ? knownCli.label : binaryName;
|
|
375
390
|
warnings.push({
|
|
376
391
|
probe_kind: 'transport_intent',
|
|
377
392
|
level: 'warn',
|
|
378
|
-
detail: `${transportLabel} typically uses "${
|
|
379
|
-
fix: `Set prompt_transport to "${
|
|
393
|
+
detail: `${transportLabel} typically uses ${knownTransports.map((value) => `"${value}"`).join(' or ')} transport, but this runtime is configured with "${transport}"`,
|
|
394
|
+
fix: `Set prompt_transport to ${knownTransports.map((value) => `"${value}"`).join(' or ')} or "dispatch_bundle_only"`,
|
|
380
395
|
});
|
|
381
396
|
}
|
|
382
397
|
|
|
@@ -414,7 +429,7 @@ export async function probeConnectorRuntime(runtimeId, runtime, options = {}) {
|
|
|
414
429
|
}
|
|
415
430
|
|
|
416
431
|
if (runtime.type === 'local_cli') {
|
|
417
|
-
const result = await probeLocalCommand(runtimeId, runtime, 'command_presence');
|
|
432
|
+
const result = await probeLocalCommand(runtimeId, runtime, 'command_presence', options);
|
|
418
433
|
// Add authority-intent and transport analysis when roles are available
|
|
419
434
|
if (roles) {
|
|
420
435
|
const { warnings } = analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles);
|
|
@@ -437,7 +452,7 @@ export async function probeConnectorRuntime(runtimeId, runtime, options = {}) {
|
|
|
437
452
|
if (runtime.transport === 'streamable_http') {
|
|
438
453
|
return probeHttpRuntime(runtimeId, runtime, timeoutMs);
|
|
439
454
|
}
|
|
440
|
-
return probeLocalCommand(runtimeId, runtime, 'command_presence');
|
|
455
|
+
return probeLocalCommand(runtimeId, runtime, 'command_presence', options);
|
|
441
456
|
}
|
|
442
457
|
|
|
443
458
|
return probeHttpRuntime(runtimeId, runtime, timeoutMs);
|
|
@@ -22,6 +22,7 @@ import { dispatchMcp } from './adapters/mcp-adapter.js';
|
|
|
22
22
|
import { dispatchRemoteAgent } from './adapters/remote-agent-adapter.js';
|
|
23
23
|
import { getDispatchPromptPath, getTurnStagingResultPath } from './turn-paths.js';
|
|
24
24
|
import { validateStagedTurnResult } from './turn-result-validator.js';
|
|
25
|
+
import { probeRuntimeSpawnContext } from './runtime-spawn-context.js';
|
|
25
26
|
|
|
26
27
|
const VALIDATABLE_RUNTIME_TYPES = new Set(['local_cli', 'api_proxy', 'mcp', 'remote_agent']);
|
|
27
28
|
const DEFAULT_VALIDATE_TIMEOUT_MS = 120_000;
|
|
@@ -130,6 +131,26 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
|
|
|
130
131
|
};
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
if (runtime.type === 'local_cli' || (runtime.type === 'mcp' && (runtime.transport || 'stdio') !== 'streamable_http')) {
|
|
135
|
+
const spawnProbe = probeRuntimeSpawnContext(scratchRoot, scratchContext.config.runtimes[runtimeId], { runtimeId });
|
|
136
|
+
if (!spawnProbe.ok) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
exitCode: 1,
|
|
140
|
+
overall: 'fail',
|
|
141
|
+
runtime_id: runtimeId,
|
|
142
|
+
runtime_type: runtime.type,
|
|
143
|
+
role_id: roleSelection.roleId,
|
|
144
|
+
timeout_ms: timeoutMs,
|
|
145
|
+
warnings,
|
|
146
|
+
dispatch: null,
|
|
147
|
+
validation: null,
|
|
148
|
+
error: spawnProbe.detail,
|
|
149
|
+
scratch_root: scratchRoot,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
133
154
|
const initResult = initializeGovernedRun(scratchRoot, scratchContext.config, {
|
|
134
155
|
provenance: {
|
|
135
156
|
trigger: 'connector_validate',
|
|
@@ -2488,7 +2488,8 @@ export function reissueTurn(root, config, opts = {}) {
|
|
|
2488
2488
|
const oldRuntimeId = oldTurn.runtime_id;
|
|
2489
2489
|
|
|
2490
2490
|
// Resolve current runtime binding (may have changed in config)
|
|
2491
|
-
|
|
2491
|
+
// BUG-25 fix: normalized config uses runtime_id, raw config uses runtime
|
|
2492
|
+
const currentRuntimeId = role.runtime_id || role.runtime;
|
|
2492
2493
|
const currentRuntime = config.runtimes?.[currentRuntimeId];
|
|
2493
2494
|
if (!currentRuntime) {
|
|
2494
2495
|
return { ok: false, error: `Runtime "${currentRuntimeId}" not found in config for role "${roleId}"` };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 500;
|
|
6
|
+
const PROMPT_PLACEHOLDER = 'AgentXchain spawn-context probe';
|
|
7
|
+
|
|
8
|
+
function resolveLocalCliPromptTransport(runtime) {
|
|
9
|
+
const valid = new Set(['argv', 'stdin', 'dispatch_bundle_only']);
|
|
10
|
+
if (valid.has(runtime?.prompt_transport)) {
|
|
11
|
+
return runtime.prompt_transport;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parts = Array.isArray(runtime?.command)
|
|
15
|
+
? runtime.command
|
|
16
|
+
: [runtime?.command, ...(Array.isArray(runtime?.args) ? runtime.args : [])];
|
|
17
|
+
const hasPrompt = parts.some((part) => typeof part === 'string' && part.includes('{prompt}'));
|
|
18
|
+
return hasPrompt ? 'argv' : 'dispatch_bundle_only';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveLocalCliInvocation(runtime) {
|
|
22
|
+
if (!runtime?.command) {
|
|
23
|
+
return { command: null, args: [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const transport = resolveLocalCliPromptTransport(runtime);
|
|
27
|
+
|
|
28
|
+
if (Array.isArray(runtime.command)) {
|
|
29
|
+
const first = runtime.command[0] || '';
|
|
30
|
+
const headParts = typeof first === 'string' && first.includes(' ') ? first.split(/\s+/) : [first];
|
|
31
|
+
const [command, ...headArgs] = headParts;
|
|
32
|
+
const rest = [...headArgs, ...runtime.command.slice(1)];
|
|
33
|
+
const args = transport === 'argv'
|
|
34
|
+
? rest.map((arg) => arg === '{prompt}' ? PROMPT_PLACEHOLDER : arg)
|
|
35
|
+
: rest.filter((arg) => arg !== '{prompt}');
|
|
36
|
+
return { command, args };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const args = transport === 'argv'
|
|
40
|
+
? (runtime.args || []).map((arg) => arg === '{prompt}' ? PROMPT_PLACEHOLDER : arg)
|
|
41
|
+
: (runtime.args || []).filter((arg) => arg !== '{prompt}');
|
|
42
|
+
return { command: runtime.command, args };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveMcpInvocation(runtime) {
|
|
46
|
+
if (!runtime?.command) {
|
|
47
|
+
return { command: null, args: [] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(runtime.command)) {
|
|
51
|
+
const [command, ...args] = runtime.command;
|
|
52
|
+
return { command, args };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
command: runtime.command,
|
|
57
|
+
args: Array.isArray(runtime.args) ? runtime.args : [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveInvocation(runtime) {
|
|
62
|
+
if (runtime?.type === 'local_cli') {
|
|
63
|
+
return resolveLocalCliInvocation(runtime);
|
|
64
|
+
}
|
|
65
|
+
return resolveMcpInvocation(runtime);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildResolutionFix(command) {
|
|
69
|
+
const commandValue = String(command || '');
|
|
70
|
+
const commandBase = basename(commandValue);
|
|
71
|
+
|
|
72
|
+
if (commandBase === 'codex' || commandBase === 'codex.exe') {
|
|
73
|
+
return 'Set "command" to the absolute path, e.g. "/Applications/Codex.app/Contents/Resources/codex", or add Codex to PATH in the dispatch spawn context.';
|
|
74
|
+
}
|
|
75
|
+
if (commandValue.includes('~')) {
|
|
76
|
+
return 'Expand "~" to an absolute path in "command". Shell expansion does not apply to governed dispatch.';
|
|
77
|
+
}
|
|
78
|
+
return 'Set "command" to an absolute path or add it to PATH in the dispatch spawn context.';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function probeRuntimeSpawnContext(root, runtime, options = {}) {
|
|
82
|
+
const runtimeId = options.runtimeId || null;
|
|
83
|
+
const cwd = runtime?.cwd ? join(root, runtime.cwd) : root;
|
|
84
|
+
const { command, args } = resolveInvocation(runtime);
|
|
85
|
+
|
|
86
|
+
if (!command) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
runtime_id: runtimeId,
|
|
90
|
+
command: null,
|
|
91
|
+
cwd,
|
|
92
|
+
detail: 'No command configured for the dispatch spawn context.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!existsSync(cwd)) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
runtime_id: runtimeId,
|
|
100
|
+
command,
|
|
101
|
+
cwd,
|
|
102
|
+
detail: `Runtime cwd "${runtime.cwd}" does not exist in the dispatch spawn context.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const probe = spawnSync(command, args, {
|
|
107
|
+
cwd,
|
|
108
|
+
env: { ...process.env, AGENTXCHAIN_SPAWN_PROBE: '1' },
|
|
109
|
+
stdio: 'ignore',
|
|
110
|
+
timeout: options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (probe.error) {
|
|
115
|
+
const errorCode = probe.error.code || 'spawn_error';
|
|
116
|
+
if (errorCode === 'ETIMEDOUT') {
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
runtime_id: runtimeId,
|
|
120
|
+
command,
|
|
121
|
+
cwd,
|
|
122
|
+
timed_out: true,
|
|
123
|
+
detail: `"${command}" launched in the dispatch spawn context but exceeded the short probe timeout. Treating this as resolvable.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (errorCode === 'ENOENT') {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
runtime_id: runtimeId,
|
|
130
|
+
command,
|
|
131
|
+
cwd,
|
|
132
|
+
error_code: errorCode,
|
|
133
|
+
detail: `"${command}" is not resolvable in the dispatch spawn context. ${buildResolutionFix(command)}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (errorCode === 'EACCES') {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
runtime_id: runtimeId,
|
|
140
|
+
command,
|
|
141
|
+
cwd,
|
|
142
|
+
error_code: errorCode,
|
|
143
|
+
detail: `"${command}" exists but is not executable in the dispatch spawn context. Mark it executable or point "command" at the real executable path.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
runtime_id: runtimeId,
|
|
149
|
+
command,
|
|
150
|
+
cwd,
|
|
151
|
+
error_code: errorCode,
|
|
152
|
+
detail: `Dispatch spawn probe failed for "${command}": ${probe.error.message}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
runtime_id: runtimeId,
|
|
159
|
+
command,
|
|
160
|
+
cwd,
|
|
161
|
+
detail: `"${command}" is resolvable in the dispatch spawn context.`,
|
|
162
|
+
};
|
|
163
|
+
}
|