agentxchain 2.124.0 → 2.125.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agentxchain.js +10 -1
- package/package.json +1 -1
- package/src/commands/connector.js +93 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/init.js +4 -0
- package/src/lib/connector-validate.js +504 -0
package/bin/agentxchain.js
CHANGED
|
@@ -121,7 +121,7 @@ import { historyCommand } from '../src/commands/history.js';
|
|
|
121
121
|
import { decisionsCommand } from '../src/commands/decisions.js';
|
|
122
122
|
import { diffCommand } from '../src/commands/diff.js';
|
|
123
123
|
import { eventsCommand } from '../src/commands/events.js';
|
|
124
|
-
import { connectorCheckCommand } from '../src/commands/connector.js';
|
|
124
|
+
import { connectorCheckCommand, connectorValidateCommand } from '../src/commands/connector.js';
|
|
125
125
|
import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
|
|
126
126
|
import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/commands/chain.js';
|
|
127
127
|
import { missionAttachChainCommand, missionListCommand, missionPlanApproveCommand, missionPlanAutopilotCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
|
|
@@ -297,6 +297,15 @@ connectorCmd
|
|
|
297
297
|
.option('--timeout <ms>', 'Per-probe timeout in milliseconds', '8000')
|
|
298
298
|
.action(connectorCheckCommand);
|
|
299
299
|
|
|
300
|
+
connectorCmd
|
|
301
|
+
.command('validate <runtime_id>')
|
|
302
|
+
.description('Dispatch one synthetic governed turn through a runtime and validate the staged turn result')
|
|
303
|
+
.option('--role <role_id>', 'Validate a specific role binding for the runtime')
|
|
304
|
+
.option('-j, --json', 'Output as JSON')
|
|
305
|
+
.option('--timeout <ms>', 'Synthetic dispatch timeout in milliseconds', '120000')
|
|
306
|
+
.option('--keep-artifacts', 'Keep the scratch validation workspace even on success')
|
|
307
|
+
.action(connectorValidateCommand);
|
|
308
|
+
|
|
300
309
|
program
|
|
301
310
|
.command('demo')
|
|
302
311
|
.description('Run a complete governed lifecycle demo (no API keys required)')
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
|
|
3
3
|
import { loadProjectContext } from '../lib/config.js';
|
|
4
|
+
import { DEFAULT_VALIDATE_TIMEOUT_MS, validateConfiguredConnector } from '../lib/connector-validate.js';
|
|
4
5
|
import { DEFAULT_TIMEOUT_MS, probeConfiguredConnectors } from '../lib/connector-probe.js';
|
|
5
6
|
|
|
6
7
|
function printJson(result, exitCode) {
|
|
@@ -120,3 +121,95 @@ export async function connectorCheckCommand(runtimeId, options = {}) {
|
|
|
120
121
|
|
|
121
122
|
printText(payload, result.exitCode);
|
|
122
123
|
}
|
|
124
|
+
|
|
125
|
+
function printValidateText(result, exitCode) {
|
|
126
|
+
console.log('');
|
|
127
|
+
console.log(chalk.bold(' AgentXchain Connector Validate'));
|
|
128
|
+
console.log(chalk.dim(' ' + '─'.repeat(47)));
|
|
129
|
+
console.log(` ${chalk.dim(`Runtime:`)} ${result.runtime_id} (${result.runtime_type})`);
|
|
130
|
+
console.log(` ${chalk.dim(`Role:`)} ${result.role_id}`);
|
|
131
|
+
console.log(` ${chalk.dim(`Timeout:`)} ${result.timeout_ms}ms`);
|
|
132
|
+
console.log('');
|
|
133
|
+
|
|
134
|
+
const badge = result.overall === 'pass'
|
|
135
|
+
? chalk.green('PASS')
|
|
136
|
+
: result.overall === 'error'
|
|
137
|
+
? chalk.red('ERROR')
|
|
138
|
+
: chalk.red('FAIL');
|
|
139
|
+
const summary = result.overall === 'pass'
|
|
140
|
+
? 'Synthetic governed dispatch produced a valid turn result'
|
|
141
|
+
: (result.error || result.dispatch?.error || result.validation?.errors?.[0] || 'Connector validation failed');
|
|
142
|
+
console.log(` ${badge} ${summary}`);
|
|
143
|
+
|
|
144
|
+
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
145
|
+
console.log('');
|
|
146
|
+
for (const warning of result.warnings) {
|
|
147
|
+
console.log(` ${chalk.yellow('!')} ${warning}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.dispatch) {
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(` ${chalk.dim('Dispatch:')} ${result.dispatch.ok ? chalk.green('ok') : chalk.red('failed')}`);
|
|
154
|
+
if (result.dispatch.error) {
|
|
155
|
+
console.log(` ${chalk.dim('Detail:')} ${result.dispatch.error}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (result.validation) {
|
|
160
|
+
console.log(` ${chalk.dim('Validator:')} ${result.validation.ok ? chalk.green('ok') : chalk.red(result.validation.stage || 'failed')}`);
|
|
161
|
+
if (Array.isArray(result.validation.errors) && result.validation.errors.length > 0) {
|
|
162
|
+
console.log(` ${chalk.dim('Errors:')} ${result.validation.errors.join(' | ')}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (typeof result.cost_usd === 'number') {
|
|
167
|
+
console.log(` ${chalk.dim('Cost:')} $${result.cost_usd.toFixed(3)}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (result.scratch_root) {
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(` ${chalk.dim('Scratch:')} ${result.scratch_root}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log('');
|
|
176
|
+
process.exit(exitCode);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function connectorValidateCommand(runtimeId, options = {}) {
|
|
180
|
+
const context = loadProjectContext();
|
|
181
|
+
if (!context) {
|
|
182
|
+
const payload = { overall: 'error', error: 'No governed agentxchain.json found.' };
|
|
183
|
+
if (options.json) {
|
|
184
|
+
printJson(payload, 2);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
console.error(chalk.red('No governed agentxchain.json found. Run this inside a governed project.'));
|
|
188
|
+
process.exit(2);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const timeoutMs = Number.parseInt(options.timeout || DEFAULT_VALIDATE_TIMEOUT_MS, 10);
|
|
192
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
193
|
+
const payload = { overall: 'error', error: 'Timeout must be a positive integer.' };
|
|
194
|
+
if (options.json) {
|
|
195
|
+
printJson(payload, 2);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.error(chalk.red('Timeout must be a positive integer.'));
|
|
199
|
+
process.exit(2);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = await validateConfiguredConnector(context.root, {
|
|
203
|
+
runtimeId,
|
|
204
|
+
roleId: options.role || null,
|
|
205
|
+
timeoutMs,
|
|
206
|
+
keepArtifacts: options.keepArtifacts === true,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (options.json) {
|
|
210
|
+
printJson(result, result.exitCode ?? 1);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
printValidateText(result, result.exitCode ?? 1);
|
|
215
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -423,7 +423,7 @@ function getConnectorProbeRecommendation(runtimes) {
|
|
|
423
423
|
return {
|
|
424
424
|
recommended: true,
|
|
425
425
|
runtimeIds,
|
|
426
|
-
detail: 'run `agentxchain connector check` to live-probe api / remote HTTP runtimes
|
|
426
|
+
detail: 'run `agentxchain connector check` to live-probe api / remote HTTP runtimes, then `agentxchain connector validate <runtime_id>` to prove one binding produces valid governed turn results.',
|
|
427
427
|
};
|
|
428
428
|
}
|
|
429
429
|
|
package/src/commands/init.js
CHANGED
|
@@ -990,6 +990,10 @@ async function initGoverned(opts) {
|
|
|
990
990
|
console.log(` ${chalk.bold('agentxchain doctor')} ${chalk.dim('# verify runtimes, config, and readiness')}`);
|
|
991
991
|
if (hasLiveConnectorProbe) {
|
|
992
992
|
console.log(` ${chalk.bold('agentxchain connector check')} ${chalk.dim('# live-probe configured runtimes before the first turn')}`);
|
|
993
|
+
const firstNonManualRtId = Object.entries(config.runtimes || {}).find(([, rt]) => rt?.type && rt.type !== 'manual')?.[0];
|
|
994
|
+
if (firstNonManualRtId) {
|
|
995
|
+
console.log(` ${chalk.bold(`agentxchain connector validate ${firstNonManualRtId}`)} ${chalk.dim('# prove the runtime produces valid governed turn results')}`);
|
|
996
|
+
}
|
|
993
997
|
}
|
|
994
998
|
console.log(` ${chalk.bold('git add -A')} ${chalk.dim('# stage the governed scaffold')}`);
|
|
995
999
|
console.log(` ${chalk.bold('git commit -m "initial governed scaffold"')} ${chalk.dim('# checkpoint the starting state')}`);
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readlinkSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
symlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { loadProjectContext } from './config.js';
|
|
17
|
+
import { writeDispatchBundle } from './dispatch-bundle.js';
|
|
18
|
+
import { getActiveTurn, initializeGovernedRun, assignGovernedTurn, readTurnCostUsd } from './governed-state.js';
|
|
19
|
+
import { dispatchApiProxy } from './adapters/api-proxy-adapter.js';
|
|
20
|
+
import { dispatchLocalCli, saveDispatchLogs } from './adapters/local-cli-adapter.js';
|
|
21
|
+
import { dispatchMcp } from './adapters/mcp-adapter.js';
|
|
22
|
+
import { dispatchRemoteAgent } from './adapters/remote-agent-adapter.js';
|
|
23
|
+
import { getDispatchPromptPath, getTurnStagingResultPath } from './turn-paths.js';
|
|
24
|
+
import { validateStagedTurnResult } from './turn-result-validator.js';
|
|
25
|
+
|
|
26
|
+
const VALIDATABLE_RUNTIME_TYPES = new Set(['local_cli', 'api_proxy', 'mcp', 'remote_agent']);
|
|
27
|
+
const DEFAULT_VALIDATE_TIMEOUT_MS = 120_000;
|
|
28
|
+
|
|
29
|
+
export async function validateConfiguredConnector(sourceRoot, options = {}) {
|
|
30
|
+
const sourceContext = loadProjectContext(sourceRoot);
|
|
31
|
+
if (!sourceContext) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
exitCode: 2,
|
|
35
|
+
overall: 'error',
|
|
36
|
+
error: 'No governed agentxchain.json found.',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (sourceContext.config.protocol_mode !== 'governed') {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
exitCode: 2,
|
|
44
|
+
overall: 'error',
|
|
45
|
+
error: 'connector validate only supports governed projects.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const runtimeId = typeof options.runtimeId === 'string' ? options.runtimeId.trim() : '';
|
|
50
|
+
if (!runtimeId) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
exitCode: 2,
|
|
54
|
+
overall: 'error',
|
|
55
|
+
error: 'Runtime id is required. Usage: agentxchain connector validate <runtime_id>',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const timeoutMs = Number.parseInt(options.timeoutMs ?? DEFAULT_VALIDATE_TIMEOUT_MS, 10);
|
|
60
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
exitCode: 2,
|
|
64
|
+
overall: 'error',
|
|
65
|
+
error: 'Timeout must be a positive integer.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const runtime = sourceContext.config.runtimes?.[runtimeId];
|
|
70
|
+
if (!runtime) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
exitCode: 2,
|
|
74
|
+
overall: 'error',
|
|
75
|
+
error: `Unknown connector runtime "${runtimeId}"`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (runtime.type === 'manual') {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
exitCode: 2,
|
|
82
|
+
overall: 'error',
|
|
83
|
+
error: `Runtime "${runtimeId}" is manual. connector validate only supports automated runtimes.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (!VALIDATABLE_RUNTIME_TYPES.has(runtime.type)) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
exitCode: 2,
|
|
90
|
+
overall: 'error',
|
|
91
|
+
error: `Runtime "${runtimeId}" of type "${runtime.type}" cannot be validated by connector validate.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const roleSelection = resolveValidationRole(sourceContext.config, runtimeId, options.roleId);
|
|
96
|
+
if (!roleSelection.ok) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
exitCode: 2,
|
|
100
|
+
overall: 'error',
|
|
101
|
+
error: roleSelection.error,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const tempBase = mkdtempSync(join(tmpdir(), 'axc-connector-validate-'));
|
|
106
|
+
const scratchRoot = join(tempBase, 'workspace');
|
|
107
|
+
const warnings = [...roleSelection.warnings];
|
|
108
|
+
let keepArtifacts = options.keepArtifacts === true;
|
|
109
|
+
let dispatch = null;
|
|
110
|
+
let validation = null;
|
|
111
|
+
let costUsd = null;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
copyRepoForValidation(sourceRoot, scratchRoot);
|
|
115
|
+
initializeScratchGit(scratchRoot);
|
|
116
|
+
|
|
117
|
+
const scratchContext = loadProjectContext(scratchRoot);
|
|
118
|
+
if (!scratchContext) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
exitCode: 1,
|
|
122
|
+
overall: 'fail',
|
|
123
|
+
runtime_id: runtimeId,
|
|
124
|
+
runtime_type: runtime.type,
|
|
125
|
+
role_id: roleSelection.roleId,
|
|
126
|
+
timeout_ms: timeoutMs,
|
|
127
|
+
warnings,
|
|
128
|
+
error: 'Failed to load governed config inside scratch workspace.',
|
|
129
|
+
scratch_root: scratchRoot,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const initResult = initializeGovernedRun(scratchRoot, scratchContext.config, {
|
|
134
|
+
provenance: {
|
|
135
|
+
trigger: 'connector_validate',
|
|
136
|
+
runtime_id: runtimeId,
|
|
137
|
+
role_id: roleSelection.roleId,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
if (!initResult.ok) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
exitCode: 1,
|
|
144
|
+
overall: 'fail',
|
|
145
|
+
runtime_id: runtimeId,
|
|
146
|
+
runtime_type: runtime.type,
|
|
147
|
+
role_id: roleSelection.roleId,
|
|
148
|
+
timeout_ms: timeoutMs,
|
|
149
|
+
warnings,
|
|
150
|
+
error: initResult.error,
|
|
151
|
+
scratch_root: scratchRoot,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const assignResult = assignGovernedTurn(scratchRoot, scratchContext.config, roleSelection.roleId);
|
|
156
|
+
if (!assignResult.ok) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
exitCode: 1,
|
|
160
|
+
overall: 'fail',
|
|
161
|
+
runtime_id: runtimeId,
|
|
162
|
+
runtime_type: runtime.type,
|
|
163
|
+
role_id: roleSelection.roleId,
|
|
164
|
+
timeout_ms: timeoutMs,
|
|
165
|
+
warnings,
|
|
166
|
+
error: assignResult.error,
|
|
167
|
+
scratch_root: scratchRoot,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const state = assignResult.state;
|
|
172
|
+
const turn = getActiveTurn(state);
|
|
173
|
+
if (!turn) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
exitCode: 1,
|
|
177
|
+
overall: 'fail',
|
|
178
|
+
runtime_id: runtimeId,
|
|
179
|
+
runtime_type: runtime.type,
|
|
180
|
+
role_id: roleSelection.roleId,
|
|
181
|
+
timeout_ms: timeoutMs,
|
|
182
|
+
warnings,
|
|
183
|
+
error: 'Synthetic validation turn was not assigned.',
|
|
184
|
+
scratch_root: scratchRoot,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const bundleResult = writeDispatchBundle(scratchRoot, state, scratchContext.config, { turnId: turn.turn_id });
|
|
189
|
+
if (!bundleResult.ok) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
exitCode: 1,
|
|
193
|
+
overall: 'fail',
|
|
194
|
+
runtime_id: runtimeId,
|
|
195
|
+
runtime_type: runtime.type,
|
|
196
|
+
role_id: roleSelection.roleId,
|
|
197
|
+
timeout_ms: timeoutMs,
|
|
198
|
+
warnings,
|
|
199
|
+
error: bundleResult.error,
|
|
200
|
+
scratch_root: scratchRoot,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (Array.isArray(bundleResult.warnings)) {
|
|
204
|
+
warnings.push(...bundleResult.warnings);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
appendValidationPrompt(scratchRoot, scratchContext.config, state, turn);
|
|
208
|
+
configureRuntimeValidationTimeout(scratchContext.config, runtimeId, timeoutMs);
|
|
209
|
+
|
|
210
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
211
|
+
const adapterResult = await dispatchValidationTurn(
|
|
212
|
+
scratchRoot,
|
|
213
|
+
state,
|
|
214
|
+
scratchContext.config,
|
|
215
|
+
runtimeId,
|
|
216
|
+
{ turnId: turn.turn_id, signal },
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
dispatch = {
|
|
220
|
+
ok: adapterResult.ok,
|
|
221
|
+
error: adapterResult.error || null,
|
|
222
|
+
timed_out: adapterResult.timedOut === true,
|
|
223
|
+
aborted: adapterResult.aborted === true,
|
|
224
|
+
log_count: Array.isArray(adapterResult.logs) ? adapterResult.logs.length : 0,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (Array.isArray(adapterResult.logs) && adapterResult.logs.length > 0) {
|
|
228
|
+
saveDispatchLogs(scratchRoot, turn.turn_id, adapterResult.logs);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!adapterResult.ok) {
|
|
232
|
+
keepArtifacts = true;
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
exitCode: 1,
|
|
236
|
+
overall: 'fail',
|
|
237
|
+
runtime_id: runtimeId,
|
|
238
|
+
runtime_type: runtime.type,
|
|
239
|
+
role_id: roleSelection.roleId,
|
|
240
|
+
timeout_ms: timeoutMs,
|
|
241
|
+
warnings,
|
|
242
|
+
dispatch,
|
|
243
|
+
validation: null,
|
|
244
|
+
scratch_root: scratchRoot,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
validation = validateStagedTurnResult(scratchRoot, state, scratchContext.config, {
|
|
249
|
+
stagingPath: getTurnStagingResultPath(turn.turn_id),
|
|
250
|
+
});
|
|
251
|
+
costUsd = validation?.turnResult ? readTurnCostUsd(validation.turnResult) : null;
|
|
252
|
+
|
|
253
|
+
if (!validation.ok) {
|
|
254
|
+
keepArtifacts = true;
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
exitCode: 1,
|
|
258
|
+
overall: 'fail',
|
|
259
|
+
runtime_id: runtimeId,
|
|
260
|
+
runtime_type: runtime.type,
|
|
261
|
+
role_id: roleSelection.roleId,
|
|
262
|
+
timeout_ms: timeoutMs,
|
|
263
|
+
warnings,
|
|
264
|
+
dispatch,
|
|
265
|
+
validation: {
|
|
266
|
+
ok: false,
|
|
267
|
+
stage: validation.stage,
|
|
268
|
+
error_class: validation.error_class,
|
|
269
|
+
errors: validation.errors,
|
|
270
|
+
warnings: validation.warnings,
|
|
271
|
+
},
|
|
272
|
+
cost_usd: costUsd,
|
|
273
|
+
scratch_root: scratchRoot,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
exitCode: 0,
|
|
280
|
+
overall: 'pass',
|
|
281
|
+
runtime_id: runtimeId,
|
|
282
|
+
runtime_type: runtime.type,
|
|
283
|
+
role_id: roleSelection.roleId,
|
|
284
|
+
timeout_ms: timeoutMs,
|
|
285
|
+
warnings,
|
|
286
|
+
dispatch,
|
|
287
|
+
validation: {
|
|
288
|
+
ok: true,
|
|
289
|
+
stage: null,
|
|
290
|
+
error_class: null,
|
|
291
|
+
errors: [],
|
|
292
|
+
warnings: validation.warnings,
|
|
293
|
+
},
|
|
294
|
+
cost_usd: costUsd,
|
|
295
|
+
scratch_root: keepArtifacts ? scratchRoot : null,
|
|
296
|
+
};
|
|
297
|
+
} catch (error) {
|
|
298
|
+
keepArtifacts = true;
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
exitCode: 1,
|
|
302
|
+
overall: 'fail',
|
|
303
|
+
runtime_id: runtimeId,
|
|
304
|
+
runtime_type: runtime.type,
|
|
305
|
+
role_id: roleSelection.roleId,
|
|
306
|
+
timeout_ms: timeoutMs,
|
|
307
|
+
warnings,
|
|
308
|
+
dispatch,
|
|
309
|
+
validation,
|
|
310
|
+
error: error.message,
|
|
311
|
+
scratch_root: scratchRoot,
|
|
312
|
+
};
|
|
313
|
+
} finally {
|
|
314
|
+
if (!keepArtifacts) {
|
|
315
|
+
try {
|
|
316
|
+
rmSync(tempBase, { recursive: true, force: true });
|
|
317
|
+
} catch {}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function resolveValidationRole(config, runtimeId, requestedRoleId) {
|
|
323
|
+
const matchingRoles = Object.entries(config.roles || {})
|
|
324
|
+
.filter(([, role]) => (role.runtime_id || role.runtime) === runtimeId)
|
|
325
|
+
.map(([roleId]) => roleId)
|
|
326
|
+
.sort((a, b) => a.localeCompare(b, 'en'));
|
|
327
|
+
|
|
328
|
+
if (matchingRoles.length === 0) {
|
|
329
|
+
return { ok: false, error: `No roles are bound to runtime "${runtimeId}".` };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (requestedRoleId) {
|
|
333
|
+
if (!config.roles?.[requestedRoleId]) {
|
|
334
|
+
return { ok: false, error: `Unknown role "${requestedRoleId}".` };
|
|
335
|
+
}
|
|
336
|
+
const boundRuntimeId = config.roles[requestedRoleId].runtime_id || config.roles[requestedRoleId].runtime;
|
|
337
|
+
if (boundRuntimeId !== runtimeId) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
error: `Role "${requestedRoleId}" is not bound to runtime "${runtimeId}".`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return { ok: true, roleId: requestedRoleId, warnings: [] };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const warnings = [];
|
|
347
|
+
if (matchingRoles.length > 1) {
|
|
348
|
+
warnings.push(
|
|
349
|
+
`Runtime "${runtimeId}" is shared by multiple roles (${matchingRoles.join(', ')}). ` +
|
|
350
|
+
`Validated the first binding "${matchingRoles[0]}". Use --role to target another binding.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
ok: true,
|
|
356
|
+
roleId: matchingRoles[0],
|
|
357
|
+
warnings,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function copyRepoForValidation(sourceRoot, scratchRoot) {
|
|
362
|
+
mkdirSync(scratchRoot, { recursive: true });
|
|
363
|
+
copyTree(sourceRoot, scratchRoot);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function copyTree(sourcePath, destPath) {
|
|
367
|
+
mkdirSync(destPath, { recursive: true });
|
|
368
|
+
for (const entry of readdirSync(sourcePath, { withFileTypes: true })) {
|
|
369
|
+
if (entry.name === '.git' || entry.name === '.agentxchain') {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const sourceEntry = join(sourcePath, entry.name);
|
|
374
|
+
const destEntry = join(destPath, entry.name);
|
|
375
|
+
const stats = lstatSync(sourceEntry);
|
|
376
|
+
|
|
377
|
+
if (stats.isSymbolicLink()) {
|
|
378
|
+
symlinkSync(readlinkSync(sourceEntry), destEntry);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (stats.isDirectory()) {
|
|
383
|
+
if (entry.name === 'node_modules') {
|
|
384
|
+
symlinkSync(sourceEntry, destEntry, 'dir');
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
copyTree(sourceEntry, destEntry);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
mkdirSync(dirname(destEntry), { recursive: true });
|
|
392
|
+
cpSync(sourceEntry, destEntry, { force: true, preserveTimestamps: true });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function initializeScratchGit(root) {
|
|
397
|
+
execFileSync('git', ['init', '-q'], { cwd: root, stdio: 'ignore' });
|
|
398
|
+
execFileSync('git', ['checkout', '-q', '-b', 'main'], { cwd: root, stdio: 'ignore' });
|
|
399
|
+
execFileSync('git', ['add', '-A'], { cwd: root, stdio: 'ignore' });
|
|
400
|
+
execFileSync('git', ['commit', '-q', '-m', 'connector validation baseline'], {
|
|
401
|
+
cwd: root,
|
|
402
|
+
stdio: 'ignore',
|
|
403
|
+
env: {
|
|
404
|
+
...process.env,
|
|
405
|
+
GIT_AUTHOR_NAME: 'AgentXchain Validator',
|
|
406
|
+
GIT_AUTHOR_EMAIL: 'noreply@agentxchain.dev',
|
|
407
|
+
GIT_COMMITTER_NAME: 'AgentXchain Validator',
|
|
408
|
+
GIT_COMMITTER_EMAIL: 'noreply@agentxchain.dev',
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function appendValidationPrompt(root, config, state, turn) {
|
|
414
|
+
const promptPath = join(root, getDispatchPromptPath(turn.turn_id));
|
|
415
|
+
const role = config.roles?.[turn.assigned_role];
|
|
416
|
+
const reviewOnly = role?.write_authority === 'review_only';
|
|
417
|
+
const validationTurn = {
|
|
418
|
+
schema_version: '1.0',
|
|
419
|
+
run_id: state.run_id,
|
|
420
|
+
turn_id: turn.turn_id,
|
|
421
|
+
role: turn.assigned_role,
|
|
422
|
+
runtime_id: turn.runtime_id,
|
|
423
|
+
status: 'completed',
|
|
424
|
+
summary: 'Connector validation synthetic turn completed without modifying product files.',
|
|
425
|
+
decisions: [
|
|
426
|
+
{
|
|
427
|
+
id: 'DEC-900',
|
|
428
|
+
category: 'process',
|
|
429
|
+
statement: 'The runtime emitted a schema-valid connector validation result.',
|
|
430
|
+
rationale: 'Synthetic dispatch completed and staged a governed turn result.',
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
objections: reviewOnly ? [
|
|
434
|
+
{
|
|
435
|
+
id: 'OBJ-900',
|
|
436
|
+
severity: 'low',
|
|
437
|
+
statement: 'Validation objection: this is a synthetic proof turn, not delivery work.',
|
|
438
|
+
status: 'acknowledged',
|
|
439
|
+
},
|
|
440
|
+
] : [],
|
|
441
|
+
files_changed: [],
|
|
442
|
+
verification: {
|
|
443
|
+
status: 'skipped',
|
|
444
|
+
evidence_summary: 'Synthetic connector validation turn; no repo work requested.',
|
|
445
|
+
},
|
|
446
|
+
artifact: {
|
|
447
|
+
type: 'review',
|
|
448
|
+
ref: null,
|
|
449
|
+
},
|
|
450
|
+
proposed_next_role: 'human',
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const lines = [
|
|
454
|
+
'',
|
|
455
|
+
'## Connector Validation Override',
|
|
456
|
+
'',
|
|
457
|
+
'This dispatch is a connector-validation proof turn, not real product work.',
|
|
458
|
+
'',
|
|
459
|
+
'You must follow these extra constraints:',
|
|
460
|
+
'',
|
|
461
|
+
'1. Do not edit any product files.',
|
|
462
|
+
'2. Write exactly one governed turn result to the staged result path already provided in ASSIGNMENT.json.',
|
|
463
|
+
'3. Use `files_changed: []` and `artifact.type: "review"`.',
|
|
464
|
+
'4. Do not request a phase transition or run completion.',
|
|
465
|
+
'5. Set `proposed_next_role: "human"`.',
|
|
466
|
+
reviewOnly ? '6. Include at least one objection because this role is review_only.' : '6. Objections may be empty because this role is not review_only.',
|
|
467
|
+
'',
|
|
468
|
+
'Use this JSON shape as your starting point and replace nothing except if you need equivalent wording:',
|
|
469
|
+
'',
|
|
470
|
+
'```json',
|
|
471
|
+
JSON.stringify(validationTurn, null, 2),
|
|
472
|
+
'```',
|
|
473
|
+
'',
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
writeFileSync(promptPath, lines.join('\n'), { flag: 'a' });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function configureRuntimeValidationTimeout(config, runtimeId, timeoutMs) {
|
|
480
|
+
if (config?.runtimes?.[runtimeId] && config.runtimes[runtimeId].type === 'remote_agent') {
|
|
481
|
+
config.runtimes[runtimeId] = {
|
|
482
|
+
...config.runtimes[runtimeId],
|
|
483
|
+
timeout_ms: timeoutMs,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function dispatchValidationTurn(root, state, config, runtimeId, options = {}) {
|
|
489
|
+
const runtime = config.runtimes?.[runtimeId];
|
|
490
|
+
switch (runtime?.type) {
|
|
491
|
+
case 'local_cli':
|
|
492
|
+
return dispatchLocalCli(root, state, config, options);
|
|
493
|
+
case 'api_proxy':
|
|
494
|
+
return dispatchApiProxy(root, state, config, options);
|
|
495
|
+
case 'mcp':
|
|
496
|
+
return dispatchMcp(root, state, config, options);
|
|
497
|
+
case 'remote_agent':
|
|
498
|
+
return dispatchRemoteAgent(root, state, config, options);
|
|
499
|
+
default:
|
|
500
|
+
return { ok: false, error: `Unsupported runtime type "${runtime?.type || 'unknown'}"` };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export { DEFAULT_VALIDATE_TIMEOUT_MS, VALIDATABLE_RUNTIME_TYPES };
|