agentxchain 2.140.0 → 2.141.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
CHANGED
|
@@ -153,7 +153,7 @@ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
|
|
|
153
153
|
test/release-notes-gate.test.js
|
|
154
154
|
test/release-identity-hardening.test.js
|
|
155
155
|
test/normalized-config.test.js
|
|
156
|
-
test/conformance.test.js
|
|
156
|
+
test/protocol-conformance.test.js
|
|
157
157
|
test/beta-scenario-emission-guard.test.js
|
|
158
158
|
test/claim-reality-preflight.test.js
|
|
159
159
|
test/beta-tester-scenarios/*.test.js
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
deriveAcceptedRef,
|
|
38
38
|
checkCleanBaseline,
|
|
39
39
|
isOperationalPath,
|
|
40
|
+
isBaselineExemptPath,
|
|
40
41
|
} from './repo-observer.js';
|
|
41
42
|
import { getMaxConcurrentTurns } from './normalized-config.js';
|
|
42
43
|
import { getTurnStagingResultPath, getTurnStagingDir, getDispatchTurnDir, getReviewArtifactPath } from './turn-paths.js';
|
|
@@ -106,6 +107,34 @@ function buildInitialPhaseGateStatus(config) {
|
|
|
106
107
|
);
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
function buildObservationDrift(beforeObservation, afterObservation) {
|
|
111
|
+
const beforeFiles = (Array.isArray(beforeObservation?.files_changed) ? beforeObservation.files_changed : [])
|
|
112
|
+
.filter((filePath) => !isBaselineExemptPath(filePath));
|
|
113
|
+
const afterFiles = (Array.isArray(afterObservation?.files_changed) ? afterObservation.files_changed : [])
|
|
114
|
+
.filter((filePath) => !isBaselineExemptPath(filePath));
|
|
115
|
+
const beforeSet = new Set(beforeFiles);
|
|
116
|
+
const afterSet = new Set(afterFiles);
|
|
117
|
+
const beforeMarkers = beforeObservation?.file_markers && typeof beforeObservation.file_markers === 'object'
|
|
118
|
+
? beforeObservation.file_markers
|
|
119
|
+
: {};
|
|
120
|
+
const afterMarkers = afterObservation?.file_markers && typeof afterObservation.file_markers === 'object'
|
|
121
|
+
? afterObservation.file_markers
|
|
122
|
+
: {};
|
|
123
|
+
|
|
124
|
+
const added = afterFiles.filter((filePath) => !beforeSet.has(filePath));
|
|
125
|
+
const removed = beforeFiles.filter((filePath) => !afterSet.has(filePath));
|
|
126
|
+
const markerChanged = beforeFiles.filter((filePath) => (
|
|
127
|
+
afterSet.has(filePath) && beforeMarkers[filePath] !== afterMarkers[filePath]
|
|
128
|
+
));
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
added,
|
|
132
|
+
removed,
|
|
133
|
+
marker_changed: markerChanged,
|
|
134
|
+
drifted_files: [...new Set([...added, ...removed, ...markerChanged])].sort(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
109
138
|
function listIntakeIntentFiles(root) {
|
|
110
139
|
const intentsDir = join(root, INTAKE_INTENTS_DIR);
|
|
111
140
|
if (!existsSync(intentsDir)) return [];
|
|
@@ -3210,11 +3239,17 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3210
3239
|
currentTurn,
|
|
3211
3240
|
historyEntries,
|
|
3212
3241
|
);
|
|
3213
|
-
const
|
|
3242
|
+
const observationAttributionOptions = {
|
|
3214
3243
|
currentDeclaredFiles: turnResult.files_changed || [],
|
|
3215
3244
|
concurrentSiblingIds: pendingConcurrentSiblingDeclarations.map((entry) => entry.turn_id),
|
|
3216
3245
|
pendingConcurrentSiblingDeclarations,
|
|
3217
|
-
}
|
|
3246
|
+
};
|
|
3247
|
+
const observation = attributeObservedChangesToTurn(
|
|
3248
|
+
rawObservation,
|
|
3249
|
+
currentTurn,
|
|
3250
|
+
historyEntries,
|
|
3251
|
+
observationAttributionOptions,
|
|
3252
|
+
);
|
|
3218
3253
|
const role = config.roles?.[turnResult.role];
|
|
3219
3254
|
const runtimeId = turnResult.runtime_id;
|
|
3220
3255
|
const runtime = config.runtimes?.[runtimeId];
|
|
@@ -3403,6 +3438,30 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3403
3438
|
? replayVerificationMachineEvidence({ root, verification: turnResult.verification })
|
|
3404
3439
|
: null;
|
|
3405
3440
|
|
|
3441
|
+
if (verificationReplay?.workspace_guard?.ok === false) {
|
|
3442
|
+
const replayError = verificationReplay.workspace_guard.cleanup_error
|
|
3443
|
+
|| 'Verification replay mutated actor-owned workspace files and cleanup failed.';
|
|
3444
|
+
transitionToFailedAcceptance(root, state, currentTurn, replayError, {
|
|
3445
|
+
error_code: 'verification_replay_drift',
|
|
3446
|
+
stage: 'verification_replay',
|
|
3447
|
+
extra: {
|
|
3448
|
+
replay_side_effect_files: verificationReplay.workspace_guard.restored_files || [],
|
|
3449
|
+
},
|
|
3450
|
+
});
|
|
3451
|
+
return {
|
|
3452
|
+
ok: false,
|
|
3453
|
+
error: replayError,
|
|
3454
|
+
validation: {
|
|
3455
|
+
...validation,
|
|
3456
|
+
ok: false,
|
|
3457
|
+
stage: 'verification_replay',
|
|
3458
|
+
error_class: 'verification_replay_error',
|
|
3459
|
+
errors: [replayError],
|
|
3460
|
+
warnings: validation.warnings,
|
|
3461
|
+
},
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3406
3465
|
// Policy evaluation — declarative governance rules (spec: POLICY_ENGINE_SPEC.md)
|
|
3407
3466
|
const policyResult = evaluatePolicies(config.policies || [], {
|
|
3408
3467
|
currentPhase: state.phase,
|
|
@@ -3638,6 +3697,38 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3638
3697
|
}
|
|
3639
3698
|
}
|
|
3640
3699
|
|
|
3700
|
+
const finalObservation = attributeObservedChangesToTurn(
|
|
3701
|
+
observeChanges(root, baseline),
|
|
3702
|
+
currentTurn,
|
|
3703
|
+
historyEntries,
|
|
3704
|
+
observationAttributionOptions,
|
|
3705
|
+
);
|
|
3706
|
+
const observationDrift = buildObservationDrift(observation, finalObservation);
|
|
3707
|
+
if (observationDrift.drifted_files.length > 0) {
|
|
3708
|
+
const driftError = `Acceptance mutated actor-owned workspace after artifact observation: ${observationDrift.drifted_files.join(', ')}`;
|
|
3709
|
+
transitionToFailedAcceptance(root, state, currentTurn, driftError, {
|
|
3710
|
+
error_code: 'acceptance_drift',
|
|
3711
|
+
stage: 'artifact_observation',
|
|
3712
|
+
extra: {
|
|
3713
|
+
added_files: observationDrift.added,
|
|
3714
|
+
removed_files: observationDrift.removed,
|
|
3715
|
+
marker_changed_files: observationDrift.marker_changed,
|
|
3716
|
+
},
|
|
3717
|
+
});
|
|
3718
|
+
return {
|
|
3719
|
+
ok: false,
|
|
3720
|
+
error: driftError,
|
|
3721
|
+
validation: {
|
|
3722
|
+
...validation,
|
|
3723
|
+
ok: false,
|
|
3724
|
+
stage: 'artifact_observation',
|
|
3725
|
+
error_class: 'artifact_error',
|
|
3726
|
+
errors: [driftError],
|
|
3727
|
+
warnings: validation.warnings,
|
|
3728
|
+
},
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3641
3732
|
const acceptedSequence = (state.turn_sequence || 0) + 1;
|
|
3642
3733
|
const historyEntry = {
|
|
3643
3734
|
turn_id: turnResult.turn_id,
|
package/src/lib/repo-observer.js
CHANGED
|
@@ -29,6 +29,8 @@ const OPERATIONAL_PATH_PREFIXES = [
|
|
|
29
29
|
'.agentxchain/intake/',
|
|
30
30
|
'.agentxchain/locks/',
|
|
31
31
|
'.agentxchain/transactions/',
|
|
32
|
+
'.agentxchain/missions/',
|
|
33
|
+
'.agentxchain/multirepo/',
|
|
32
34
|
];
|
|
33
35
|
|
|
34
36
|
// Orchestrator-owned state files that agents must never be blamed for modifying.
|
|
@@ -38,6 +40,7 @@ const ORCHESTRATOR_STATE_FILES = [
|
|
|
38
40
|
'.agentxchain/session.json',
|
|
39
41
|
'.agentxchain/history.jsonl',
|
|
40
42
|
'.agentxchain/decision-ledger.jsonl',
|
|
43
|
+
'.agentxchain/repo-decisions.jsonl',
|
|
41
44
|
'.agentxchain/lock.json',
|
|
42
45
|
'.agentxchain/hook-audit.jsonl',
|
|
43
46
|
'.agentxchain/hook-annotations.jsonl',
|
|
@@ -71,7 +74,7 @@ export function isOperationalPath(filePath) {
|
|
|
71
74
|
|| ORCHESTRATOR_STATE_FILES.includes(filePath);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
function isBaselineExemptPath(filePath) {
|
|
77
|
+
export function isBaselineExemptPath(filePath) {
|
|
75
78
|
return isOperationalPath(filePath)
|
|
76
79
|
|| BASELINE_EXEMPT_PATH_PREFIXES.some(prefix => filePath.startsWith(prefix));
|
|
77
80
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { isOperationalPath } from './repo-observer.js';
|
|
2
8
|
|
|
3
9
|
export const DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS = 30_000;
|
|
4
10
|
|
|
@@ -22,10 +28,15 @@ export function replayVerificationMachineEvidence({ root, verification, timeoutM
|
|
|
22
28
|
return payload;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
const workspaceGuard = createReplayWorkspaceGuard(root);
|
|
32
|
+
try {
|
|
33
|
+
payload.commands = machineEvidence.map((entry, index) => replayEvidenceCommand(root, entry, index, timeoutMs));
|
|
34
|
+
payload.replayed_commands = payload.commands.length;
|
|
35
|
+
payload.matched_commands = payload.commands.filter((entry) => entry.matched).length;
|
|
36
|
+
payload.overall = payload.commands.every((entry) => entry.matched) ? 'match' : 'mismatch';
|
|
37
|
+
} finally {
|
|
38
|
+
payload.workspace_guard = workspaceGuard.cleanup();
|
|
39
|
+
}
|
|
29
40
|
|
|
30
41
|
return payload;
|
|
31
42
|
}
|
|
@@ -69,3 +80,186 @@ export function summarizeVerificationReplay(payload) {
|
|
|
69
80
|
...(payload.reason ? { reason: payload.reason } : {}),
|
|
70
81
|
};
|
|
71
82
|
}
|
|
83
|
+
|
|
84
|
+
function createReplayWorkspaceGuard(root) {
|
|
85
|
+
if (!isGitRepo(root)) {
|
|
86
|
+
return {
|
|
87
|
+
cleanup() {
|
|
88
|
+
return { ok: true, restored_files: [], cleanup_error: null };
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const backupDir = mkdtempSync(join(tmpdir(), 'axc-verification-replay-'));
|
|
94
|
+
const beforeState = captureDirtyWorkspaceState(root, backupDir);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
cleanup() {
|
|
98
|
+
const restoredFiles = new Set();
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const afterFiles = collectDirtyActorFiles(root);
|
|
102
|
+
const candidateFiles = new Set([...beforeState.files.keys(), ...afterFiles]);
|
|
103
|
+
|
|
104
|
+
for (const filePath of candidateFiles) {
|
|
105
|
+
const beforeInfo = beforeState.files.get(filePath);
|
|
106
|
+
const currentMarker = getWorkspaceFileMarker(root, filePath);
|
|
107
|
+
|
|
108
|
+
if (beforeInfo) {
|
|
109
|
+
if (currentMarker !== beforeInfo.marker) {
|
|
110
|
+
restorePreReplayPath(root, filePath, beforeInfo);
|
|
111
|
+
restoredFiles.add(filePath);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
restoreCleanPath(root, filePath);
|
|
117
|
+
restoredFiles.add(filePath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const remainingDrift = detectWorkspaceDrift(root, beforeState.files);
|
|
121
|
+
if (remainingDrift.length > 0) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
restored_files: [...restoredFiles].sort(),
|
|
125
|
+
cleanup_error: `Verification replay left actor-owned workspace drift after cleanup: ${remainingDrift.join(', ')}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
restored_files: [...restoredFiles].sort(),
|
|
132
|
+
cleanup_error: null,
|
|
133
|
+
};
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
restored_files: [...restoredFiles].sort(),
|
|
138
|
+
cleanup_error: err?.message || String(err),
|
|
139
|
+
};
|
|
140
|
+
} finally {
|
|
141
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function captureDirtyWorkspaceState(root, backupDir) {
|
|
148
|
+
const files = new Map();
|
|
149
|
+
for (const filePath of collectDirtyActorFiles(root)) {
|
|
150
|
+
const absPath = join(root, filePath);
|
|
151
|
+
const backupPath = join(backupDir, filePath);
|
|
152
|
+
const existed = existsSync(absPath);
|
|
153
|
+
const marker = getWorkspaceFileMarker(root, filePath);
|
|
154
|
+
|
|
155
|
+
if (existed) {
|
|
156
|
+
mkdirSync(dirname(backupPath), { recursive: true });
|
|
157
|
+
cpSync(absPath, backupPath, { force: true, recursive: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
files.set(filePath, { existed, marker, backupPath });
|
|
161
|
+
}
|
|
162
|
+
return { files };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function detectWorkspaceDrift(root, beforeFiles) {
|
|
166
|
+
const afterFiles = collectDirtyActorFiles(root);
|
|
167
|
+
const candidates = new Set([...beforeFiles.keys(), ...afterFiles]);
|
|
168
|
+
const drift = [];
|
|
169
|
+
|
|
170
|
+
for (const filePath of candidates) {
|
|
171
|
+
const beforeInfo = beforeFiles.get(filePath);
|
|
172
|
+
const currentMarker = getWorkspaceFileMarker(root, filePath);
|
|
173
|
+
if (beforeInfo) {
|
|
174
|
+
if (currentMarker !== beforeInfo.marker) {
|
|
175
|
+
drift.push(filePath);
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
drift.push(filePath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return drift.sort();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function restorePreReplayPath(root, filePath, beforeInfo) {
|
|
186
|
+
const absPath = join(root, filePath);
|
|
187
|
+
if (!beforeInfo.existed) {
|
|
188
|
+
rmSync(absPath, { recursive: true, force: true });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
rmSync(absPath, { recursive: true, force: true });
|
|
193
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
194
|
+
cpSync(beforeInfo.backupPath, absPath, { force: true, recursive: true });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function restoreCleanPath(root, filePath) {
|
|
198
|
+
const absPath = join(root, filePath);
|
|
199
|
+
if (isTrackedPath(root, filePath)) {
|
|
200
|
+
execFileSync('git', ['restore', '--source=HEAD', '--staged', '--worktree', '--', filePath], {
|
|
201
|
+
cwd: root,
|
|
202
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
rmSync(absPath, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function collectDirtyActorFiles(root) {
|
|
211
|
+
return [...new Set([
|
|
212
|
+
...readGitLines(root, ['diff', '--name-only', 'HEAD']),
|
|
213
|
+
...readGitLines(root, ['diff', '--name-only', '--cached']),
|
|
214
|
+
...readGitLines(root, ['ls-files', '--others', '--exclude-standard']),
|
|
215
|
+
])]
|
|
216
|
+
.filter((filePath) => filePath && !isOperationalPath(filePath))
|
|
217
|
+
.sort();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readGitLines(root, args) {
|
|
221
|
+
try {
|
|
222
|
+
const output = execFileSync('git', args, {
|
|
223
|
+
cwd: root,
|
|
224
|
+
encoding: 'utf8',
|
|
225
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
226
|
+
}).trim();
|
|
227
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
228
|
+
} catch {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isTrackedPath(root, filePath) {
|
|
234
|
+
try {
|
|
235
|
+
execFileSync('git', ['ls-files', '--error-unmatch', '--', filePath], {
|
|
236
|
+
cwd: root,
|
|
237
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
238
|
+
});
|
|
239
|
+
return true;
|
|
240
|
+
} catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isGitRepo(root) {
|
|
246
|
+
try {
|
|
247
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
248
|
+
cwd: root,
|
|
249
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
250
|
+
});
|
|
251
|
+
return true;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getWorkspaceFileMarker(root, filePath) {
|
|
258
|
+
const absPath = join(root, filePath);
|
|
259
|
+
if (!existsSync(absPath)) {
|
|
260
|
+
return '__deleted__';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const content = readFileSync(absPath);
|
|
264
|
+
return createHash('sha1').update(content).digest('hex');
|
|
265
|
+
}
|