agentxchain 2.64.0 → 2.66.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 +1 -1
- package/src/commands/accept-turn.js +6 -0
- package/src/commands/step.js +6 -0
- package/src/commands/verify.js +10 -46
- package/src/lib/context-compressor.js +3 -0
- package/src/lib/context-section-parser.js +6 -0
- package/src/lib/dispatch-bundle.js +70 -0
- package/src/lib/governed-state.js +10 -0
- package/src/lib/policy-evaluator.js +32 -0
- package/src/lib/verification-replay.js +71 -0
package/package.json
CHANGED
|
@@ -166,6 +166,12 @@ export async function acceptTurnCommand(opts = {}) {
|
|
|
166
166
|
if (accepted?.cost?.usd != null) {
|
|
167
167
|
console.log(` ${chalk.dim('Cost:')} $${formatUsd(accepted.cost.usd)}`);
|
|
168
168
|
}
|
|
169
|
+
if (accepted?.verification_replay) {
|
|
170
|
+
const verifiedAt = accepted.verification_replay.verified_at
|
|
171
|
+
? ` at ${accepted.verification_replay.verified_at}`
|
|
172
|
+
: '';
|
|
173
|
+
console.log(` ${chalk.dim('Replay:')} ${accepted.verification_replay.overall} (${accepted.verification_replay.matched_commands}/${accepted.verification_replay.replayed_commands})${verifiedAt}`);
|
|
174
|
+
}
|
|
169
175
|
if (result.budget_warning) {
|
|
170
176
|
console.log(` ${chalk.yellow('Budget warning:')} ${result.budget_warning}`);
|
|
171
177
|
}
|
package/src/commands/step.js
CHANGED
|
@@ -1023,6 +1023,12 @@ function printAcceptSummary(result) {
|
|
|
1023
1023
|
if (accepted?.cost?.usd != null) {
|
|
1024
1024
|
console.log(` ${chalk.dim('Cost:')} $${(accepted.cost.usd || 0).toFixed(2)}`);
|
|
1025
1025
|
}
|
|
1026
|
+
if (accepted?.verification_replay) {
|
|
1027
|
+
const verifiedAt = accepted.verification_replay.verified_at
|
|
1028
|
+
? ` at ${accepted.verification_replay.verified_at}`
|
|
1029
|
+
: '';
|
|
1030
|
+
console.log(` ${chalk.dim('Replay:')} ${accepted.verification_replay.overall} (${accepted.verification_replay.matched_commands}/${accepted.verification_replay.replayed_commands})${verifiedAt}`);
|
|
1031
|
+
}
|
|
1026
1032
|
console.log('');
|
|
1027
1033
|
|
|
1028
1034
|
if (result.state?.status === 'completed') {
|
package/src/commands/verify.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
2
|
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
4
3
|
import { getActiveTurns } from '../lib/governed-state.js';
|
|
5
4
|
import { normalizeVerification } from '../lib/repo-observer.js';
|
|
@@ -8,6 +7,10 @@ import { getTurnStagingResultPath } from '../lib/turn-paths.js';
|
|
|
8
7
|
import { resolve } from 'node:path';
|
|
9
8
|
import { loadExportArtifact, verifyExportArtifact } from '../lib/export-verifier.js';
|
|
10
9
|
import { verifyProtocolConformance } from '../lib/protocol-conformance.js';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS,
|
|
12
|
+
replayVerificationMachineEvidence,
|
|
13
|
+
} from '../lib/verification-replay.js';
|
|
11
14
|
|
|
12
15
|
export async function verifyProtocolCommand(opts, command) {
|
|
13
16
|
const requestedTier = Number.parseInt(String(opts.tier || '1'), 10);
|
|
@@ -113,7 +116,7 @@ export async function verifyTurnCommand(turnId, opts = {}) {
|
|
|
113
116
|
process.exit(2);
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
const timeoutMs = Number.parseInt(String(opts.timeout ||
|
|
119
|
+
const timeoutMs = Number.parseInt(String(opts.timeout || String(DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS)), 10);
|
|
117
120
|
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
118
121
|
console.log(chalk.red('verify turn requires a positive integer --timeout in milliseconds.'));
|
|
119
122
|
process.exit(2);
|
|
@@ -142,10 +145,6 @@ export async function verifyTurnCommand(turnId, opts = {}) {
|
|
|
142
145
|
const runtimeType = config.runtimes?.[selectedTurn.runtime_id]?.type || 'manual';
|
|
143
146
|
const declaredStatus = turnResult.verification?.status || 'skipped';
|
|
144
147
|
const normalizedStatus = normalizeVerification(turnResult.verification, runtimeType).status;
|
|
145
|
-
const machineEvidence = Array.isArray(turnResult.verification?.machine_evidence)
|
|
146
|
-
? turnResult.verification.machine_evidence
|
|
147
|
-
: [];
|
|
148
|
-
|
|
149
148
|
const payload = {
|
|
150
149
|
turn_id: selectedTurnId,
|
|
151
150
|
role: selectedTurn.assigned_role,
|
|
@@ -159,23 +158,13 @@ export async function verifyTurnCommand(turnId, opts = {}) {
|
|
|
159
158
|
warnings: validation.warnings || [],
|
|
160
159
|
},
|
|
161
160
|
timeout_ms: timeoutMs,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
...replayVerificationMachineEvidence({
|
|
162
|
+
root,
|
|
163
|
+
verification: turnResult.verification,
|
|
164
|
+
timeoutMs,
|
|
165
|
+
}),
|
|
166
166
|
};
|
|
167
167
|
|
|
168
|
-
if (machineEvidence.length === 0) {
|
|
169
|
-
payload.reason = 'No verification.machine_evidence commands were declared. commands/evidence_summary are not executable proof.';
|
|
170
|
-
emitTurnVerification(payload, opts.json);
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
payload.commands = machineEvidence.map((entry, index) => replayEvidenceCommand(root, entry, index, timeoutMs));
|
|
175
|
-
payload.replayed_commands = payload.commands.length;
|
|
176
|
-
payload.matched_commands = payload.commands.filter((entry) => entry.matched).length;
|
|
177
|
-
payload.overall = payload.commands.every((entry) => entry.matched) ? 'match' : 'mismatch';
|
|
178
|
-
|
|
179
168
|
emitTurnVerification(payload, opts.json);
|
|
180
169
|
process.exit(payload.overall === 'match' ? 0 : 1);
|
|
181
170
|
}
|
|
@@ -310,31 +299,6 @@ function emitTurnValidationFailure(validation, jsonMode) {
|
|
|
310
299
|
console.log('');
|
|
311
300
|
}
|
|
312
301
|
|
|
313
|
-
function replayEvidenceCommand(root, entry, index, timeoutMs) {
|
|
314
|
-
const result = spawnSync(entry.command, {
|
|
315
|
-
cwd: root,
|
|
316
|
-
encoding: 'utf8',
|
|
317
|
-
shell: true,
|
|
318
|
-
timeout: timeoutMs,
|
|
319
|
-
maxBuffer: 1024 * 1024,
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const timedOut = result.error?.code === 'ETIMEDOUT';
|
|
323
|
-
const actualExitCode = Number.isInteger(result.status) ? result.status : null;
|
|
324
|
-
const errorMessage = result.error?.message || null;
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
index,
|
|
328
|
-
command: entry.command,
|
|
329
|
-
declared_exit_code: entry.exit_code,
|
|
330
|
-
actual_exit_code: actualExitCode,
|
|
331
|
-
matched: actualExitCode === entry.exit_code,
|
|
332
|
-
timed_out: timedOut,
|
|
333
|
-
signal: result.signal || null,
|
|
334
|
-
error: errorMessage,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
302
|
function emitTurnVerification(payload, jsonMode) {
|
|
339
303
|
if (jsonMode) {
|
|
340
304
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -15,6 +15,9 @@ import { renderContextSections } from './context-section-parser.js';
|
|
|
15
15
|
const COMPRESSION_STEPS = [
|
|
16
16
|
{ id: 'budget', action: 'drop' },
|
|
17
17
|
{ id: 'phase_gate_status', action: 'drop' },
|
|
18
|
+
{ id: 'decision_history', action: 'drop' },
|
|
19
|
+
{ id: 'workflow_artifacts', action: 'drop' },
|
|
20
|
+
{ id: 'last_turn_verification', action: 'drop' },
|
|
18
21
|
{ id: 'gate_required_files', action: 'drop' },
|
|
19
22
|
{ id: 'last_turn_objections', action: 'drop' },
|
|
20
23
|
{ id: 'last_turn_decisions', action: 'drop' },
|
|
@@ -3,11 +3,14 @@ const CONTEXT_TITLE = '# Execution Context';
|
|
|
3
3
|
const SECTION_DEFINITIONS = [
|
|
4
4
|
{ id: 'current_state', header: 'Current State', required: true },
|
|
5
5
|
{ id: 'budget', header: null, required: false },
|
|
6
|
+
{ id: 'project_goal', header: 'Project Goal', required: true },
|
|
7
|
+
{ id: 'inherited_run_context', header: 'Inherited Run Context', required: true },
|
|
6
8
|
{ id: 'last_turn_header', header: 'Last Accepted Turn', required: true },
|
|
7
9
|
{ id: 'last_turn_summary', header: null, required: false },
|
|
8
10
|
{ id: 'last_turn_decisions', header: null, required: false },
|
|
9
11
|
{ id: 'last_turn_objections', header: null, required: false },
|
|
10
12
|
{ id: 'last_turn_verification', header: null, required: false },
|
|
13
|
+
{ id: 'decision_history', header: 'Decision History', required: false },
|
|
11
14
|
{ id: 'blockers', header: 'Blockers', required: true },
|
|
12
15
|
{ id: 'escalation', header: 'Escalation', required: true },
|
|
13
16
|
{ id: 'workflow_artifacts', header: 'Workflow Artifacts', required: false },
|
|
@@ -79,6 +82,8 @@ export function renderContextSections(sections) {
|
|
|
79
82
|
sectionMap.get('budget')?.content,
|
|
80
83
|
]);
|
|
81
84
|
|
|
85
|
+
appendTopLevelSection(lines, 'Project Goal', [sectionMap.get('project_goal')?.content]);
|
|
86
|
+
appendTopLevelSection(lines, 'Inherited Run Context', [sectionMap.get('inherited_run_context')?.content]);
|
|
82
87
|
appendTopLevelSection(lines, 'Last Accepted Turn', [
|
|
83
88
|
sectionMap.get('last_turn_header')?.content,
|
|
84
89
|
sectionMap.get('last_turn_summary')?.content,
|
|
@@ -87,6 +92,7 @@ export function renderContextSections(sections) {
|
|
|
87
92
|
sectionMap.get('last_turn_verification')?.content,
|
|
88
93
|
]);
|
|
89
94
|
|
|
95
|
+
appendTopLevelSection(lines, 'Decision History', [sectionMap.get('decision_history')?.content]);
|
|
90
96
|
appendTopLevelSection(lines, 'Blockers', [sectionMap.get('blockers')?.content]);
|
|
91
97
|
appendTopLevelSection(lines, 'Escalation', [sectionMap.get('escalation')?.content]);
|
|
92
98
|
appendTopLevelSection(lines, 'Workflow Artifacts', [sectionMap.get('workflow_artifacts')?.content]);
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
} from './turn-paths.js';
|
|
31
31
|
|
|
32
32
|
const HISTORY_PATH = '.agentxchain/history.jsonl';
|
|
33
|
+
const LEDGER_PATH = '.agentxchain/decision-ledger.jsonl';
|
|
34
|
+
const DECISION_HISTORY_MAX_ENTRIES = 50;
|
|
33
35
|
const FILE_PREVIEW_MAX_FILES = 5;
|
|
34
36
|
const FILE_PREVIEW_MAX_LINES = 120;
|
|
35
37
|
const PROPOSAL_SUMMARY_MAX_LINES = 80;
|
|
@@ -691,6 +693,12 @@ function renderContext(state, config, root, turn, role) {
|
|
|
691
693
|
}
|
|
692
694
|
}
|
|
693
695
|
|
|
696
|
+
// Cumulative decision history from the decision ledger
|
|
697
|
+
const decisionHistoryLines = renderDecisionHistory(root, warnings);
|
|
698
|
+
if (decisionHistoryLines.length > 0) {
|
|
699
|
+
lines.push(...decisionHistoryLines);
|
|
700
|
+
}
|
|
701
|
+
|
|
694
702
|
// Blockers / escalation
|
|
695
703
|
if (state.blocked_on) {
|
|
696
704
|
lines.push('## Blockers');
|
|
@@ -1102,6 +1110,68 @@ function writeDispatchIndex(root, state, warningsByTurn = {}) {
|
|
|
1102
1110
|
);
|
|
1103
1111
|
}
|
|
1104
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* Read agent-authored decisions from the decision ledger and render as a
|
|
1115
|
+
* markdown section for CONTEXT.md.
|
|
1116
|
+
*
|
|
1117
|
+
* Returns an array of markdown lines (including the ## header) or an empty
|
|
1118
|
+
* array when there are no agent-authored decisions.
|
|
1119
|
+
*/
|
|
1120
|
+
function renderDecisionHistory(root, warnings = []) {
|
|
1121
|
+
const ledgerPath = join(root, LEDGER_PATH);
|
|
1122
|
+
if (!existsSync(ledgerPath)) return [];
|
|
1123
|
+
|
|
1124
|
+
let content;
|
|
1125
|
+
try {
|
|
1126
|
+
content = readFileSync(ledgerPath, 'utf8').trim();
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
warnings.push(`Failed to read ${LEDGER_PATH}: ${err.message}`);
|
|
1129
|
+
return [];
|
|
1130
|
+
}
|
|
1131
|
+
if (!content) return [];
|
|
1132
|
+
|
|
1133
|
+
// Parse all lines, skip malformed ones
|
|
1134
|
+
const rawLines = content.split('\n');
|
|
1135
|
+
const agentDecisions = [];
|
|
1136
|
+
for (const line of rawLines) {
|
|
1137
|
+
if (!line.trim()) continue;
|
|
1138
|
+
try {
|
|
1139
|
+
const entry = JSON.parse(line);
|
|
1140
|
+
// Agent-authored decisions have an `id` field; system entries do not
|
|
1141
|
+
if (entry.id) {
|
|
1142
|
+
agentDecisions.push(entry);
|
|
1143
|
+
}
|
|
1144
|
+
} catch {
|
|
1145
|
+
warnings.push(`Skipped malformed decision-ledger line`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (agentDecisions.length === 0) return [];
|
|
1150
|
+
|
|
1151
|
+
const totalCount = agentDecisions.length;
|
|
1152
|
+
const displayed = totalCount > DECISION_HISTORY_MAX_ENTRIES
|
|
1153
|
+
? agentDecisions.slice(totalCount - DECISION_HISTORY_MAX_ENTRIES)
|
|
1154
|
+
: agentDecisions;
|
|
1155
|
+
|
|
1156
|
+
const lines = [];
|
|
1157
|
+
lines.push('## Decision History');
|
|
1158
|
+
lines.push('');
|
|
1159
|
+
lines.push('| ID | Phase | Role | Statement |');
|
|
1160
|
+
lines.push('|----|-------|------|-----------|');
|
|
1161
|
+
for (const d of displayed) {
|
|
1162
|
+
// Escape pipes in statement to avoid breaking the table
|
|
1163
|
+
const stmt = (d.statement || '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
1164
|
+
lines.push(`| ${d.id} | ${d.phase || ''} | ${d.role || ''} | ${stmt} |`);
|
|
1165
|
+
}
|
|
1166
|
+
if (totalCount > DECISION_HISTORY_MAX_ENTRIES) {
|
|
1167
|
+
lines.push('');
|
|
1168
|
+
lines.push(`_Showing ${DECISION_HISTORY_MAX_ENTRIES} of ${totalCount} decisions. Full ledger at ${LEDGER_PATH}._`);
|
|
1169
|
+
}
|
|
1170
|
+
lines.push('');
|
|
1171
|
+
|
|
1172
|
+
return lines;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1105
1175
|
function readLastHistoryEntry(root, warnings = []) {
|
|
1106
1176
|
const historyPath = join(root, HISTORY_PATH);
|
|
1107
1177
|
if (!existsSync(historyPath)) return null;
|
|
@@ -44,6 +44,10 @@ import { emitRunEvent } from './run-events.js';
|
|
|
44
44
|
import { writeSessionCheckpoint } from './session-checkpoint.js';
|
|
45
45
|
import { recordRunHistory } from './run-history.js';
|
|
46
46
|
import { buildDefaultRunProvenance } from './run-provenance.js';
|
|
47
|
+
import {
|
|
48
|
+
replayVerificationMachineEvidence,
|
|
49
|
+
summarizeVerificationReplay,
|
|
50
|
+
} from './verification-replay.js';
|
|
47
51
|
|
|
48
52
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
49
53
|
|
|
@@ -2367,6 +2371,9 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2367
2371
|
const normalizedVerification = normalizeVerification(turnResult.verification, runtimeType);
|
|
2368
2372
|
const artifactType = turnResult.artifact?.type || 'review';
|
|
2369
2373
|
const derivedRef = deriveAcceptedRef(observation, artifactType, state.accepted_integration_ref);
|
|
2374
|
+
const verificationReplay = (config.policies || []).some((policy) => policy?.rule === 'require_reproducible_verification')
|
|
2375
|
+
? replayVerificationMachineEvidence({ root, verification: turnResult.verification })
|
|
2376
|
+
: null;
|
|
2370
2377
|
|
|
2371
2378
|
// Policy evaluation — declarative governance rules (spec: POLICY_ENGINE_SPEC.md)
|
|
2372
2379
|
const policyResult = evaluatePolicies(config.policies || [], {
|
|
@@ -2375,6 +2382,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2375
2382
|
turnStatus: turnResult.status,
|
|
2376
2383
|
turnCostUsd: readTurnCostUsd(turnResult),
|
|
2377
2384
|
history: historyEntries,
|
|
2385
|
+
verificationReplay,
|
|
2378
2386
|
});
|
|
2379
2387
|
|
|
2380
2388
|
if (policyResult.blocks.length > 0) {
|
|
@@ -2572,6 +2580,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2572
2580
|
artifacts_created: turnResult.artifacts_created || [],
|
|
2573
2581
|
verification: turnResult.verification || {},
|
|
2574
2582
|
normalized_verification: normalizedVerification,
|
|
2583
|
+
...(verificationReplay ? { verification_replay: summarizeVerificationReplay(verificationReplay) } : {}),
|
|
2575
2584
|
artifact: turnResult.artifact || {},
|
|
2576
2585
|
observed_artifact: observedArtifact,
|
|
2577
2586
|
proposed_next_role: turnResult.proposed_next_role,
|
|
@@ -3176,6 +3185,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3176
3185
|
hookResults,
|
|
3177
3186
|
...(budgetWarning ? { budget_warning: budgetWarning } : {}),
|
|
3178
3187
|
...(policyResult.warnings.length > 0 ? { policy_warnings: policyResult.warnings } : {}),
|
|
3188
|
+
...(verificationReplay ? { verification_replay: summarizeVerificationReplay(verificationReplay) } : {}),
|
|
3179
3189
|
};
|
|
3180
3190
|
}
|
|
3181
3191
|
|
|
@@ -78,6 +78,32 @@ const RULE_EVALUATORS = {
|
|
|
78
78
|
}
|
|
79
79
|
return { triggered: false, message: '' };
|
|
80
80
|
},
|
|
81
|
+
|
|
82
|
+
require_reproducible_verification: (_params, ctx) => {
|
|
83
|
+
const replay = ctx.verificationReplay;
|
|
84
|
+
if (!replay) {
|
|
85
|
+
return {
|
|
86
|
+
triggered: true,
|
|
87
|
+
message: 'verification replay context is missing at acceptance time',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (replay.overall === 'match') {
|
|
92
|
+
return { triggered: false, message: '' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (replay.overall === 'not_reproducible') {
|
|
96
|
+
return {
|
|
97
|
+
triggered: true,
|
|
98
|
+
message: replay.reason || 'verification.machine_evidence did not provide executable proof',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
triggered: true,
|
|
104
|
+
message: `verification replay mismatch: ${replay.matched_commands || 0}/${replay.replayed_commands || 0} commands matched declared exit codes`,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
81
107
|
};
|
|
82
108
|
|
|
83
109
|
export const VALID_POLICY_RULES = Object.keys(RULE_EVALUATORS);
|
|
@@ -185,6 +211,12 @@ function validatePolicyParams(rule, params, prefix) {
|
|
|
185
211
|
}
|
|
186
212
|
}
|
|
187
213
|
break;
|
|
214
|
+
|
|
215
|
+
case 'require_reproducible_verification':
|
|
216
|
+
if (params != null && typeof params !== 'object') {
|
|
217
|
+
errors.push(`${prefix}: params must be an object when provided`);
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
188
220
|
}
|
|
189
221
|
|
|
190
222
|
return errors;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS = 30_000;
|
|
4
|
+
|
|
5
|
+
export function replayVerificationMachineEvidence({ root, verification, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS }) {
|
|
6
|
+
const verifiedAt = new Date().toISOString();
|
|
7
|
+
const machineEvidence = Array.isArray(verification?.machine_evidence)
|
|
8
|
+
? verification.machine_evidence
|
|
9
|
+
: [];
|
|
10
|
+
|
|
11
|
+
const payload = {
|
|
12
|
+
verified_at: verifiedAt,
|
|
13
|
+
timeout_ms: timeoutMs,
|
|
14
|
+
overall: 'not_reproducible',
|
|
15
|
+
replayed_commands: 0,
|
|
16
|
+
matched_commands: 0,
|
|
17
|
+
commands: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (machineEvidence.length === 0) {
|
|
21
|
+
payload.reason = 'No verification.machine_evidence commands were declared. commands/evidence_summary are not executable proof.';
|
|
22
|
+
return payload;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
payload.commands = machineEvidence.map((entry, index) => replayEvidenceCommand(root, entry, index, timeoutMs));
|
|
26
|
+
payload.replayed_commands = payload.commands.length;
|
|
27
|
+
payload.matched_commands = payload.commands.filter((entry) => entry.matched).length;
|
|
28
|
+
payload.overall = payload.commands.every((entry) => entry.matched) ? 'match' : 'mismatch';
|
|
29
|
+
|
|
30
|
+
return payload;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function replayEvidenceCommand(root, entry, index, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS) {
|
|
34
|
+
const result = spawnSync(entry.command, {
|
|
35
|
+
cwd: root,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
shell: true,
|
|
38
|
+
timeout: timeoutMs,
|
|
39
|
+
maxBuffer: 1024 * 1024,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const timedOut = result.error?.code === 'ETIMEDOUT';
|
|
43
|
+
const actualExitCode = Number.isInteger(result.status) ? result.status : null;
|
|
44
|
+
const errorMessage = result.error?.message || null;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
index,
|
|
48
|
+
command: entry.command,
|
|
49
|
+
declared_exit_code: entry.exit_code,
|
|
50
|
+
actual_exit_code: actualExitCode,
|
|
51
|
+
matched: actualExitCode === entry.exit_code,
|
|
52
|
+
timed_out: timedOut,
|
|
53
|
+
signal: result.signal || null,
|
|
54
|
+
error: errorMessage,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function summarizeVerificationReplay(payload) {
|
|
59
|
+
if (!payload) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
verified_at: payload.verified_at || null,
|
|
65
|
+
overall: payload.overall,
|
|
66
|
+
replayed_commands: payload.replayed_commands || 0,
|
|
67
|
+
matched_commands: payload.matched_commands || 0,
|
|
68
|
+
timeout_ms: payload.timeout_ms || DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS,
|
|
69
|
+
...(payload.reason ? { reason: payload.reason } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|