agentic-workflow-manager 9.11.12 → 9.12.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/dist/src/commands/doctor.js +23 -1
- package/dist/src/commands/hooks/claude.js +61 -8
- package/dist/src/commands/hooks/resync.js +1 -1
- package/dist/src/commands/job/gate.js +6 -1
- package/dist/src/commands/job/index.js +61 -5
- package/dist/src/commands/model-policy/index.js +253 -0
- package/dist/src/commands/plan/index.js +53 -9
- package/dist/src/commands/preflight/checks.js +18 -0
- package/dist/src/commands/watch/apply.js +69 -5
- package/dist/src/commands/watch/archive-unused.js +2 -0
- package/dist/src/commands/watch/index.js +24 -0
- package/dist/src/commands/watch/init.js +2 -1
- package/dist/src/commands/watch/request-recovery.js +153 -0
- package/dist/src/commands/watch/supervisor.js +70 -6
- package/dist/src/core/admission/index.js +20 -6
- package/dist/src/core/journal/requests.js +1 -0
- package/dist/src/core/journal/types.js +50 -6
- package/dist/src/core/model-policy/capabilities-v2.js +152 -0
- package/dist/src/core/model-policy/capabilities.js +7 -1
- package/dist/src/core/model-policy/claude-hook-events.js +120 -0
- package/dist/src/core/model-policy/event-store.js +221 -0
- package/dist/src/core/model-policy/hook-diagnostics.js +94 -0
- package/dist/src/core/model-policy/journal.js +113 -18
- package/dist/src/core/model-policy/local-claude-scope.js +41 -0
- package/dist/src/core/model-policy/local-codex-scope.js +33 -0
- package/dist/src/core/model-policy/local-event-scope.js +13 -0
- package/dist/src/core/model-policy/machine-key.js +56 -0
- package/dist/src/core/model-policy/native-claude-agent-definition.js +82 -0
- package/dist/src/core/model-policy/native-claude-config.js +107 -0
- package/dist/src/core/model-policy/native-claude-machine.js +58 -0
- package/dist/src/core/model-policy/native-claude-transcript.js +80 -0
- package/dist/src/core/model-policy/native-claude.js +41 -0
- package/dist/src/core/model-policy/native-codex-machine.js +116 -0
- package/dist/src/core/model-policy/native-codex-rollout.js +111 -0
- package/dist/src/core/model-policy/native-codex.js +302 -0
- package/dist/src/core/model-policy/native-routing-observe.js +106 -0
- package/dist/src/core/model-policy/native-routing-proof.js +52 -0
- package/dist/src/core/model-policy/native-runtime.js +86 -0
- package/dist/src/core/model-policy/resolve.js +96 -6
- package/dist/src/core/model-policy/selection-policy-digest.js +22 -0
- package/dist/src/core/model-policy/setup-readiness.js +77 -0
- package/dist/src/providers/index.js +3 -1
- package/dist/tests/commands/doctor-routing.test.js +22 -0
- package/dist/tests/commands/doctor.test.js +5 -0
- package/dist/tests/commands/hooks/install.test.js +6 -0
- package/dist/tests/commands/hooks/resync.test.js +3 -0
- package/dist/tests/commands/hooks/status.test.js +12 -1
- package/dist/tests/commands/job/routing-file.test.js +32 -0
- package/dist/tests/commands/job/routing-report-cli.test.js +4 -3
- package/dist/tests/commands/model-policy/claude-hook-flow.test.js +104 -0
- package/dist/tests/commands/model-policy/index.test.js +179 -0
- package/dist/tests/commands/preflight/preflight.test.js +65 -0
- package/dist/tests/commands/watch/apply.test.js +84 -0
- package/dist/tests/commands/watch/dispatch-routing-identity.test.js +23 -0
- package/dist/tests/commands/watch/request-recovery.test.js +253 -0
- package/dist/tests/core/admission/index.test.js +21 -0
- package/dist/tests/core/model-policy/capabilities-v2.test.js +42 -0
- package/dist/tests/core/model-policy/claude-hook-events.test.js +40 -0
- package/dist/tests/core/model-policy/event-store-claude.test.js +64 -0
- package/dist/tests/core/model-policy/event-store.test.js +151 -0
- package/dist/tests/core/model-policy/hook-diagnostics.test.js +34 -0
- package/dist/tests/core/model-policy/journal.test.js +112 -1
- package/dist/tests/core/model-policy/local-claude-scope.test.js +31 -0
- package/dist/tests/core/model-policy/machine-key.test.js +38 -0
- package/dist/tests/core/model-policy/native-claude-agent-definition.test.js +31 -0
- package/dist/tests/core/model-policy/native-claude-config.test.js +53 -0
- package/dist/tests/core/model-policy/native-claude-machine.test.js +18 -0
- package/dist/tests/core/model-policy/native-claude-transcript.test.js +45 -0
- package/dist/tests/core/model-policy/native-claude.test.js +21 -0
- package/dist/tests/core/model-policy/native-codex-machine.test.js +34 -0
- package/dist/tests/core/model-policy/native-codex-rollout.test.js +44 -0
- package/dist/tests/core/model-policy/native-codex.test.js +133 -0
- package/dist/tests/core/model-policy/native-routing-observe.test.js +60 -0
- package/dist/tests/core/model-policy/native-routing-proof.test.js +30 -0
- package/dist/tests/core/model-policy/native-runtime.test.js +14 -0
- package/dist/tests/core/model-policy/resolve-event.test.js +65 -0
- package/dist/tests/core/model-policy/setup-readiness.test.js +83 -0
- package/dist/tests/providers/index.test.js +1 -1
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
11
|
+
const requests_1 = require("../../../src/core/journal/requests");
|
|
12
|
+
const apply_1 = require("../../../src/commands/watch/apply");
|
|
13
|
+
const gate_1 = require("../../../src/commands/job/gate");
|
|
14
|
+
const paths_1 = require("../../../src/core/journal/paths");
|
|
15
|
+
const lock_1 = require("../../../src/commands/watch/lock");
|
|
16
|
+
const request_recovery_1 = require("../../../src/commands/watch/request-recovery");
|
|
17
|
+
const supervisor_1 = require("../../../src/commands/watch/supervisor");
|
|
18
|
+
const archive_unused_1 = require("../../../src/commands/watch/archive-unused");
|
|
19
|
+
const generations_1 = require("../../../src/commands/watch/generations");
|
|
20
|
+
describe('issue #194: recovery of a rejected task request', () => {
|
|
21
|
+
let repo;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-recover-request-'));
|
|
24
|
+
(0, child_process_1.execFileSync)('git', ['init', '-q', '-b', 'rama'], { cwd: repo });
|
|
25
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', 'commit', '--allow-empty', '-qm', 'seed'], { cwd: repo });
|
|
26
|
+
(0, store_1.initJournal)(repo, 'rama');
|
|
27
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
28
|
+
state.schema = 2;
|
|
29
|
+
state.planBinding = {
|
|
30
|
+
path: 'docs/plans/cycle.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1',
|
|
31
|
+
executionMode: 'desatendido', boundAt: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
state.requiredVerifiers = ['test', 'sensors'];
|
|
34
|
+
state.generations.push({ n: 1, token: 'g1', state: 'active', launchedAt: new Date().toISOString() });
|
|
35
|
+
state.cycle.status = 'BLOCKED';
|
|
36
|
+
state.cycle.blockedReason = 'recovery no autorizado: hay conflictos durables de requests';
|
|
37
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => fs_1.default.rmSync(repo, { recursive: true, force: true }));
|
|
40
|
+
function rejectedTask() {
|
|
41
|
+
const rejected = (0, requests_1.emitRequest)(repo, 'rama', {
|
|
42
|
+
kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'bad-task',
|
|
43
|
+
payload: { entity: 'task', taskId: 'S1', title: 'slice', verificationPlan: [{ id: 'test-s1', kind: 'test' }], reviewObligations: [] },
|
|
44
|
+
});
|
|
45
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').rejectedInvalid).toBe(1);
|
|
46
|
+
expect(fs_1.default.existsSync(`${rejected.file}.rejected`)).toBe(true);
|
|
47
|
+
expect(fs_1.default.readFileSync((0, paths_1.eventsPath)(repo, 'rama'), 'utf8')).toMatch(/request-rejected-invalid/);
|
|
48
|
+
return rejected;
|
|
49
|
+
}
|
|
50
|
+
function correctedTask(taskId = 'S1', kinds = ['test', 'sensors']) {
|
|
51
|
+
return (0, requests_1.emitRequest)(repo, 'rama', {
|
|
52
|
+
kind: 'register-entity', generationToken: 'g1', idempotencyKey: `corrected-${taskId}-${kinds.join('-')}`,
|
|
53
|
+
payload: { entity: 'task', taskId, title: 'slice', verificationPlan: kinds.map(kind => ({ id: `${kind}-${taskId}`, kind })), reviewObligations: [] },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
test('rejection → corrected request → audited recovery → next action remains gated by real work', () => {
|
|
57
|
+
const rejected = rejectedTask();
|
|
58
|
+
const replacement = correctedTask();
|
|
59
|
+
expect((0, gate_1.computeGate)((0, store_1.readJournal)(repo, 'rama').state, false, () => null).reasons.some(reason => reason.category === 'request-problem')).toBe(true);
|
|
60
|
+
(0, request_recovery_1.recoverRejectedTask)(repo, 'rama', {
|
|
61
|
+
rejectedRequestId: rejected.requestId, replacementRequestId: replacement.requestId,
|
|
62
|
+
generationToken: 'g1', reason: 'corregido plan de verificación', resume: true,
|
|
63
|
+
});
|
|
64
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
65
|
+
expect(state.tasks.map(task => task.id)).toEqual(['S1']);
|
|
66
|
+
expect(state.appliedRequests[replacement.requestId]).toMatchObject({ outcome: 'applied', resultRef: 'S1' });
|
|
67
|
+
expect(state.requestProblems[0]).toMatchObject({ kind: 'rejected', resolution: { replacementRequestId: replacement.requestId, generationToken: 'g1', reason: 'corregido plan de verificación' } });
|
|
68
|
+
expect(state.cycle.status).toBe('IN_PROGRESS');
|
|
69
|
+
expect(state.custodyDecisions).toEqual([expect.objectContaining({ decision: 'resume', generationToken: 'g1' })]);
|
|
70
|
+
expect(fs_1.default.existsSync(`${rejected.file}.rejected`)).toBe(true);
|
|
71
|
+
expect(fs_1.default.existsSync(replacement.file)).toBe(false);
|
|
72
|
+
expect(fs_1.default.readFileSync((0, paths_1.eventsPath)(repo, 'rama'), 'utf8')).toMatch(/request-recovered/);
|
|
73
|
+
const gate = (0, gate_1.computeGate)(state, false, () => null);
|
|
74
|
+
expect(gate.reasons.some(reason => reason.category === 'request-problem')).toBe(false);
|
|
75
|
+
expect(gate.pass).toBe(false);
|
|
76
|
+
expect(gate.reasons.some(reason => reason.category === 'pending-task')).toBe(true);
|
|
77
|
+
(0, request_recovery_1.recoverRejectedTask)(repo, 'rama', {
|
|
78
|
+
rejectedRequestId: rejected.requestId, replacementRequestId: replacement.requestId,
|
|
79
|
+
generationToken: 'g1', reason: 'corregido plan de verificación', resume: true,
|
|
80
|
+
});
|
|
81
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(1);
|
|
82
|
+
// Crash after state publication but before queue cleanup: supervisor
|
|
83
|
+
// replay consumes the same request ID without registering S1 twice.
|
|
84
|
+
fs_1.default.writeFileSync(replacement.file, JSON.stringify({ requestId: replacement.requestId,
|
|
85
|
+
kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'corrected-S1-test-sensors',
|
|
86
|
+
payload: { entity: 'task', taskId: 'S1', title: 'slice', verificationPlan: [
|
|
87
|
+
{ id: 'test-S1', kind: 'test' }, { id: 'sensors-S1', kind: 'sensors' }
|
|
88
|
+
], reviewObligations: [] } }));
|
|
89
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').applied).toBe(0);
|
|
90
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(1);
|
|
91
|
+
});
|
|
92
|
+
test('wrong task and still-invalid correction leave rejection active without partial writes', () => {
|
|
93
|
+
const rejected = rejectedTask();
|
|
94
|
+
const wrong = correctedTask('S2');
|
|
95
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId, replacementRequestId: wrong.requestId, generationToken: 'g1', reason: 'wrong task', resume: true })).toThrow(/mismo taskId/);
|
|
96
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.requestProblems[0].resolution).toBeUndefined();
|
|
97
|
+
fs_1.default.rmSync(wrong.file);
|
|
98
|
+
const invalid = correctedTask('S1', ['test']);
|
|
99
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId, replacementRequestId: invalid.requestId, generationToken: 'g1', reason: 'still invalid', resume: true })).toThrow(/sensors/);
|
|
100
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
101
|
+
expect(state.tasks).toHaveLength(0);
|
|
102
|
+
expect(state.requestProblems[0].resolution).toBeUndefined();
|
|
103
|
+
expect(state.cycle.status).toBe('BLOCKED');
|
|
104
|
+
});
|
|
105
|
+
test('stale generation, missing archive, and live supervisor lock cannot resolve a rejection', () => {
|
|
106
|
+
const rejected = rejectedTask();
|
|
107
|
+
const replacement = correctedTask();
|
|
108
|
+
const input = { rejectedRequestId: rejected.requestId, replacementRequestId: replacement.requestId,
|
|
109
|
+
generationToken: 'g1', reason: 'corregido', resume: true };
|
|
110
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { ...input, generationToken: 'g0' })).toThrow(/generation/);
|
|
111
|
+
const lock = (0, lock_1.acquireLock)(repo);
|
|
112
|
+
try {
|
|
113
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', input)).toThrow(/supervisor activo/);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
(0, lock_1.releaseLock)(repo, lock);
|
|
117
|
+
}
|
|
118
|
+
fs_1.default.renameSync(`${rejected.file}.rejected`, `${rejected.file}.held`);
|
|
119
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', input)).toThrow();
|
|
120
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.requestProblems[0].resolution).toBeUndefined();
|
|
121
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
test('an unrelated active problem prevents --resume and leaves the correction queued', () => {
|
|
124
|
+
const rejected = rejectedTask();
|
|
125
|
+
const replacement = correctedTask();
|
|
126
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
127
|
+
state.requestProblems.push({ file: path_1.default.join((0, paths_1.requestsDir)(repo, 'rama'), 'other.json'), kind: 'corrupt', detail: 'bad envelope', at: new Date().toISOString() });
|
|
128
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
129
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId,
|
|
130
|
+
replacementRequestId: replacement.requestId, generationToken: 'g1', reason: 'corregido', resume: true })).toThrow(/todas las requests/);
|
|
131
|
+
expect(fs_1.default.existsSync(replacement.file)).toBe(true);
|
|
132
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
test('a resolution-shaped state without an applied replacement is still an active gate problem', () => {
|
|
135
|
+
rejectedTask();
|
|
136
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
137
|
+
state.requestProblems[0].resolution = { replacementRequestId: 'req-1-0000000000-deadbeef', replacementPayloadDigest: 'a'.repeat(64), reason: 'forged', generationToken: 'g1', at: new Date().toISOString() };
|
|
138
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
139
|
+
expect((0, gate_1.computeGate)((0, store_1.readJournal)(repo, 'rama').state, false, () => null).reasons.some(reason => reason.category === 'request-problem')).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
test('an unrelated applied task cannot be cited as the replacement of S1', () => {
|
|
142
|
+
rejectedTask();
|
|
143
|
+
const unrelated = correctedTask('S2');
|
|
144
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').applied).toBe(1);
|
|
145
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
146
|
+
state.requestProblems[0].resolution = { replacementRequestId: unrelated.requestId, replacementPayloadDigest: state.appliedRequests[unrelated.requestId].payloadDigest, reason: 'wrong link', generationToken: 'g1', at: new Date().toISOString() };
|
|
147
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
148
|
+
expect((0, gate_1.computeGate)((0, store_1.readJournal)(repo, 'rama').state, false, () => null).reasons.some(reason => reason.category === 'request-problem')).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
test('a corrupt request cannot be laundered through a task-shaped resolution', () => {
|
|
151
|
+
const replacement = correctedTask('S1');
|
|
152
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').applied).toBe(1);
|
|
153
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
154
|
+
state.requestProblems.push({ file: path_1.default.join((0, paths_1.requestsDir)(repo, 'rama'), 'req-1-0000000000-deadbeef.json'),
|
|
155
|
+
kind: 'corrupt', detail: 'bad JSON', at: new Date().toISOString(), requestId: 'req-1-0000000000-deadbeef',
|
|
156
|
+
taskId: 'S1', generationToken: 'g1', resolution: { replacementRequestId: replacement.requestId,
|
|
157
|
+
replacementPayloadDigest: state.appliedRequests[replacement.requestId].payloadDigest,
|
|
158
|
+
reason: 'forged', generationToken: 'g1', at: new Date().toISOString() } });
|
|
159
|
+
expect(() => (0, store_1.writeJournal)(repo, 'rama', state)).toThrow(/forma invalida/);
|
|
160
|
+
});
|
|
161
|
+
test('resume rejects a different custody cause even if its text mentions requests', () => {
|
|
162
|
+
const rejected = rejectedTask();
|
|
163
|
+
const replacement = correctedTask();
|
|
164
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
165
|
+
state.cycle.blockedReason = 'other failure; also mentions conflictos durables de requests';
|
|
166
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
167
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId,
|
|
168
|
+
replacementRequestId: replacement.requestId, generationToken: 'g1', reason: 'corrected', resume: true })).toThrow(/custodia BLOCKED por requests/);
|
|
169
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.tasks).toHaveLength(0);
|
|
170
|
+
});
|
|
171
|
+
test('a blocked request remains the specific custody cause across supervisor ticks', () => {
|
|
172
|
+
rejectedTask();
|
|
173
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
174
|
+
expect((0, supervisor_1.recoveryWhitelistBlocker)(state)).toBe('hay conflictos durables de requests');
|
|
175
|
+
});
|
|
176
|
+
test('an independent custody cause is never overwritten by an active request problem', () => {
|
|
177
|
+
rejectedTask();
|
|
178
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
179
|
+
state.cycle.blockedReason = 'veredicto de sensores no concluyente';
|
|
180
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
181
|
+
expect((0, supervisor_1.recoveryWhitelistBlocker)((0, store_1.readJournal)(repo, 'rama').state)).toBe('el ciclo está bloqueado');
|
|
182
|
+
});
|
|
183
|
+
test('public journal status exposes the latest generation token after supervisor shutdown', () => {
|
|
184
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
185
|
+
state.generations[0].state = 'terminated';
|
|
186
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
187
|
+
expect((0, archive_unused_1.watchJournalStatus)(repo, 'rama')).toMatchObject({ latestGeneration: { n: 1, token: 'g1', state: 'terminated' } });
|
|
188
|
+
});
|
|
189
|
+
test('recovery without resume remains eligible for a later explicit resume', () => {
|
|
190
|
+
const rejected = rejectedTask();
|
|
191
|
+
const replacement = correctedTask();
|
|
192
|
+
const input = { rejectedRequestId: rejected.requestId, replacementRequestId: replacement.requestId,
|
|
193
|
+
generationToken: 'g1', reason: 'corregido', resume: false };
|
|
194
|
+
(0, request_recovery_1.recoverRejectedTask)(repo, 'rama', input);
|
|
195
|
+
const blocker = (0, supervisor_1.recoveryWhitelistBlocker)((0, store_1.readJournal)(repo, 'rama').state);
|
|
196
|
+
(0, generations_1.enterCustody)(repo, 'rama', `recovery no autorizado: ${blocker}`);
|
|
197
|
+
(0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { ...input, resume: true });
|
|
198
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.cycle.status).toBe('IN_PROGRESS');
|
|
199
|
+
});
|
|
200
|
+
test('a correction from a newer generation cannot silently resolve the archived rejection', () => {
|
|
201
|
+
const rejected = rejectedTask();
|
|
202
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
203
|
+
state.generations[0].state = 'superseded';
|
|
204
|
+
state.generations.push({ n: 2, token: 'g2', state: 'active', launchedAt: new Date().toISOString() });
|
|
205
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
206
|
+
const replacement = (0, requests_1.emitRequest)(repo, 'rama', { kind: 'register-entity', generationToken: 'g2', idempotencyKey: 'corrected-g2',
|
|
207
|
+
payload: { entity: 'task', taskId: 'S1', verificationPlan: [{ id: 't2', kind: 'test' }, { id: 's2', kind: 'sensors' }], reviewObligations: [] } });
|
|
208
|
+
expect(() => (0, request_recovery_1.recoverRejectedTask)(repo, 'rama', { rejectedRequestId: rejected.requestId,
|
|
209
|
+
replacementRequestId: replacement.requestId, generationToken: 'g2', reason: 'different generation', resume: true })).toThrow(/generation/);
|
|
210
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.requestProblems[0].resolution).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
test('public watch recover-request command applies the correction under custody', () => {
|
|
213
|
+
const rejected = rejectedTask();
|
|
214
|
+
const replacement = correctedTask();
|
|
215
|
+
const cli = path_1.default.resolve(__dirname, '../../../src/index.ts');
|
|
216
|
+
const tsNode = require.resolve('ts-node/dist/bin.js');
|
|
217
|
+
const output = (0, child_process_1.execFileSync)(process.execPath, [tsNode, '--compiler-options', '{"module":"CommonJS"}', cli, 'watch', 'recover-request',
|
|
218
|
+
'--rejected', rejected.requestId, '--replacement', replacement.requestId,
|
|
219
|
+
'--generation', 'g1', '--reason', 'operador verificó reemplazo', '--resume'], { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
220
|
+
expect(JSON.parse(output)).toMatchObject({ recovered: true, replacementRequestId: replacement.requestId });
|
|
221
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.cycle.status).toBe('IN_PROGRESS');
|
|
222
|
+
});
|
|
223
|
+
test('job register rejects missing sensors before writing a request file', () => {
|
|
224
|
+
const cli = path_1.default.resolve(__dirname, '../../../src/index.ts');
|
|
225
|
+
const tsNode = require.resolve('ts-node/dist/bin.js');
|
|
226
|
+
const payload = JSON.stringify({ taskId: 'S1', verificationPlan: [{ id: 't1', kind: 'test' }], reviewObligations: [] });
|
|
227
|
+
let diagnostic = '';
|
|
228
|
+
try {
|
|
229
|
+
(0, child_process_1.execFileSync)(process.execPath, [tsNode, '--compiler-options', '{"module":"CommonJS"}', cli,
|
|
230
|
+
'job', 'register', '--generation', 'g1', '--entity', 'task', '--json', payload], { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
diagnostic = String(error.stderr ?? '');
|
|
234
|
+
}
|
|
235
|
+
expect(diagnostic).toMatch(/verificationPlan.*sensors/);
|
|
236
|
+
expect(fs_1.default.readdirSync((0, paths_1.requestsDir)(repo, 'rama')).filter(name => name.endsWith('.json'))).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
test('JSON cannot override --entity to bypass task prevalidation', () => {
|
|
239
|
+
const cli = path_1.default.resolve(__dirname, '../../../src/index.ts');
|
|
240
|
+
const tsNode = require.resolve('ts-node/dist/bin.js');
|
|
241
|
+
const payload = JSON.stringify({ entity: 'task', taskId: 'S1', verificationPlan: [{ id: 't1', kind: 'test' }], reviewObligations: [] });
|
|
242
|
+
let diagnostic = '';
|
|
243
|
+
try {
|
|
244
|
+
(0, child_process_1.execFileSync)(process.execPath, [tsNode, '--compiler-options', '{"module":"CommonJS"}', cli,
|
|
245
|
+
'job', 'register', '--generation', 'g1', '--entity', 'cycle-plan', '--json', payload], { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
diagnostic = String(error.stderr ?? '');
|
|
249
|
+
}
|
|
250
|
+
expect(diagnostic).toMatch(/entity.*coincidir/);
|
|
251
|
+
expect(fs_1.default.readdirSync((0, paths_1.requestsDir)(repo, 'rama')).filter(name => name.endsWith('.json'))).toHaveLength(0);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const admission_1 = require("../../../src/core/admission");
|
|
4
4
|
const types_1 = require("../../../src/core/journal/types");
|
|
5
|
+
const selection_policy_digest_1 = require("../../../src/core/model-policy/selection-policy-digest");
|
|
5
6
|
const valid = {
|
|
6
7
|
state: 'valid', schema: 'compact-slices/v1', planDigest: 'a'.repeat(64),
|
|
7
8
|
manifest: {
|
|
@@ -50,6 +51,26 @@ describe('admitPlan', () => {
|
|
|
50
51
|
const report = await (0, admission_1.admitPlan)({ plan: v2(profile), provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], routing: routing() });
|
|
51
52
|
expect(report).toMatchObject({ state: 'admitted', routingForecast: { implementerProfiles: { [profile]: 1 }, trackB: { state: 'unavailable', lowerBound: 1 }, controller: { state: 'unavailable', lowerBound: 0 } } });
|
|
52
53
|
});
|
|
54
|
+
it('awaits read-only routing evidence only after earlier admission gates pass', async () => {
|
|
55
|
+
const reader = jest.fn(async () => routing());
|
|
56
|
+
expect(await (0, admission_1.admitPlan)({ plan: v2(), provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], routingReader: reader })).toMatchObject({ state: 'admitted' });
|
|
57
|
+
expect(reader).toHaveBeenCalledTimes(1);
|
|
58
|
+
});
|
|
59
|
+
it('admits a +25h event-scoped Codex enrollment only when the full fallback is independently proved', async () => {
|
|
60
|
+
const evidence = routing();
|
|
61
|
+
evidence.policy.content.mappings[0].degradation.allowMissingObservedIdentity = true;
|
|
62
|
+
const mapping = evidence.policy.content.mappings[0];
|
|
63
|
+
const eventScope = { runtime: evidence.runtime, binaryDigest: 'b'.repeat(64), configDigest: 'c'.repeat(64) };
|
|
64
|
+
const claimed = [mapping.profiles.mechanical, mapping.fullCapability];
|
|
65
|
+
const eventReceipt = { schema: 'routing-capabilities/v2', ...eventScope, recordedAt: '2026-09-17T00:00:00.000Z', claims: claimed.map(selection => ({ selection,
|
|
66
|
+
mappingDigest: (0, selection_policy_digest_1.selectionPolicyDigest)(mapping, selection), eventDigest: sha, source: 'codex-turn-context', observedAt: '2026-09-17T00:00:00.000Z', actualModel: 'unverified', tokenUsage: 'unknown' })) };
|
|
67
|
+
const routingFacts = { runtime: evidence.runtime, policy: evidence.policy, eventReceipt, eventScope, now: new Date('2026-09-18T01:00:00.000Z') };
|
|
68
|
+
expect(await (0, admission_1.admitPlan)({ plan: v2(), provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], routing: routingFacts })).toMatchObject({ state: 'admitted' });
|
|
69
|
+
expect(await (0, admission_1.admitPlan)({ plan: v2(), provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], routing: { ...routingFacts,
|
|
70
|
+
eventReceipt: { ...eventReceipt, claims: [eventReceipt.claims[1]] } } })).toMatchObject({ state: 'admitted' });
|
|
71
|
+
expect(await (0, admission_1.admitPlan)({ plan: v2(), provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], routing: { ...routingFacts,
|
|
72
|
+
eventReceipt: { ...eventReceipt, claims: [eventReceipt.claims[0]] } } })).toMatchObject({ state: 'blocked', diagnostics: [{ code: 'ROUTING_SELECTION_UNENROLLED' }] });
|
|
73
|
+
});
|
|
53
74
|
it('blocks a valid cross-provider policy and receipt instead of admitting codex under another runtime target', async () => {
|
|
54
75
|
const evidence = routing();
|
|
55
76
|
const foreign = structuredClone(evidence);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const capabilities_v2_1 = require("../../../src/core/model-policy/capabilities-v2");
|
|
4
|
+
const digestA = 'a'.repeat(64);
|
|
5
|
+
const digestB = 'b'.repeat(64);
|
|
6
|
+
const digestC = 'c'.repeat(64);
|
|
7
|
+
const selection = (id) => ({ selector: { kind: 'model', id }, effort: { kind: 'explicit', value: 'high' } });
|
|
8
|
+
const receipt = () => ({
|
|
9
|
+
schema: 'routing-capabilities/v2',
|
|
10
|
+
runtime: { target: 'codex', kind: 'native', version: '0.156.1', accountScopeDigest: digestA },
|
|
11
|
+
binaryDigest: digestB, configDigest: digestC, recordedAt: '2026-09-23T00:00:00.000Z',
|
|
12
|
+
claims: [
|
|
13
|
+
{ selection: selection('gpt-6-luna'), mappingDigest: digestA, eventDigest: digestB, source: 'codex-turn-context', observedAt: '2026-09-23T00:00:00.000Z', actualModel: 'unverified', tokenUsage: 'unknown' },
|
|
14
|
+
{ selection: selection('gpt-6-astra'), mappingDigest: digestB, eventDigest: digestC, source: 'codex-turn-context', observedAt: '2026-09-23T00:00:00.000Z', actualModel: 'unverified', tokenUsage: 'unknown' },
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
describe('event-scoped routing capabilities v2', () => {
|
|
18
|
+
const scope = { runtime: receipt().runtime, binaryDigest: digestB, configDigest: digestC };
|
|
19
|
+
const later = new Date('2026-09-24T01:00:00.000Z');
|
|
20
|
+
it('keeps an unchanged native claim current after 25 hours without renewal', () => {
|
|
21
|
+
expect((0, capabilities_v2_1.validateEventReceipt)(receipt())).toMatchObject({ schema: 'routing-capabilities/v2' });
|
|
22
|
+
expect((0, capabilities_v2_1.evaluateEventReceipt)(receipt(), scope, selection('gpt-6-luna'), digestA, later)).toEqual({ state: 'current' });
|
|
23
|
+
});
|
|
24
|
+
it('invalidates only a changed selection mapping, not its neighbor', () => {
|
|
25
|
+
expect((0, capabilities_v2_1.evaluateEventReceipt)(receipt(), scope, selection('gpt-6-luna'), digestC, later)).toEqual({ state: 'drift', reason: 'POLICY_DRIFT' });
|
|
26
|
+
expect((0, capabilities_v2_1.evaluateEventReceipt)(receipt(), scope, selection('gpt-6-astra'), digestB, later)).toEqual({ state: 'current' });
|
|
27
|
+
});
|
|
28
|
+
it('fails closed on runtime, account, binary and config drift', () => {
|
|
29
|
+
const value = receipt();
|
|
30
|
+
expect((0, capabilities_v2_1.evaluateEventReceipt)(value, { ...scope, runtime: { ...scope.runtime, version: '0.157.0' } }, selection('gpt-6-luna'), digestA, later)).toEqual({ state: 'drift', reason: 'RUNTIME_DRIFT' });
|
|
31
|
+
expect((0, capabilities_v2_1.evaluateEventReceipt)(value, { ...scope, runtime: { ...scope.runtime, accountScopeDigest: digestB } }, selection('gpt-6-luna'), digestA, later)).toEqual({ state: 'drift', reason: 'ACCOUNT_DRIFT' });
|
|
32
|
+
expect((0, capabilities_v2_1.evaluateEventReceipt)(value, { ...scope, binaryDigest: digestC }, selection('gpt-6-luna'), digestA, later)).toEqual({ state: 'drift', reason: 'RUNTIME_DRIFT' });
|
|
33
|
+
expect((0, capabilities_v2_1.evaluateEventReceipt)(value, { ...scope, configDigest: digestB }, selection('gpt-6-luna'), digestA, later)).toEqual({ state: 'drift', reason: 'CONFIG_DRIFT' });
|
|
34
|
+
});
|
|
35
|
+
it('rejects malformed proof, duplicate claims, future records and missing scope', () => {
|
|
36
|
+
expect(() => (0, capabilities_v2_1.validateEventReceipt)({ ...receipt(), claims: [receipt().claims[0], receipt().claims[0]] })).toThrow(/duplicate/i);
|
|
37
|
+
expect(() => (0, capabilities_v2_1.validateEventReceipt)({ ...receipt(), claims: [{ ...receipt().claims[0], eventDigest: 'pretend' }] })).toThrow(/digest/i);
|
|
38
|
+
expect((0, capabilities_v2_1.evaluateEventReceipt)({ ...receipt(), recordedAt: '2026-09-25T00:00:00.000Z' }, scope, selection('gpt-6-luna'), digestA, later)).toEqual({ state: 'drift', reason: 'FUTURE_EVIDENCE' });
|
|
39
|
+
expect(() => (0, capabilities_v2_1.evaluateEventReceipt)(receipt(), { ...scope, configDigest: '' }, selection('gpt-6-luna'), digestA, later)).toThrow(/configDigest/i);
|
|
40
|
+
expect(() => (0, capabilities_v2_1.validateEventReceipt)({ ...receipt(), runtime: { ...receipt().runtime, target: 'antigravity' }, claims: [{ ...receipt().claims[0], source: 'claude-assistant-transcript' }] })).toThrow(/provider|target/i);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const claude_hook_events_1 = require("../../../src/core/model-policy/claude-hook-events");
|
|
10
|
+
const machine_key_1 = require("../../../src/core/model-policy/machine-key");
|
|
11
|
+
describe('sealed Claude hook pair', () => {
|
|
12
|
+
const start = { hook_event_name: 'SubagentStart', session_id: 'session-1', agent_id: 'agent-1', agent_type: 'plugin:reviewer' };
|
|
13
|
+
const stop = { hook_event_name: 'SubagentStop', session_id: 'session-1', agent_id: 'agent-1', agent_type: 'plugin:reviewer',
|
|
14
|
+
agent_transcript_path: '/tmp/agent-1.jsonl', stop_hook_active: false };
|
|
15
|
+
it('links start/stop without storing prompt text, rejects tampering and stale pending events', () => {
|
|
16
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-claude-hook-'));
|
|
17
|
+
const previous = process.env.AWM_HOME;
|
|
18
|
+
process.env.AWM_HOME = root;
|
|
19
|
+
try {
|
|
20
|
+
(0, machine_key_1.ensureMachineKey)(root);
|
|
21
|
+
(0, claude_hook_events_1.recordClaudeHookStart)({ ...start, prompt: 'private prompt' }, new Date('2026-09-23T22:00:00.000Z'));
|
|
22
|
+
const file = path_1.default.join(root, 'claude-hook-starts', fs_1.default.readdirSync(path_1.default.join(root, 'claude-hook-starts'))[0]);
|
|
23
|
+
expect(fs_1.default.readFileSync(file, 'utf8')).not.toContain('private prompt');
|
|
24
|
+
const pair = (0, claude_hook_events_1.readClaudeHookPair)(stop, new Date('2026-09-23T22:01:00.000Z'));
|
|
25
|
+
expect(pair.lifecycle).toMatchObject({ sessionId: 'session-1', agentId: 'agent-1' });
|
|
26
|
+
expect(() => (0, claude_hook_events_1.readClaudeHookPair)(stop, new Date('2026-09-25T22:01:00.000Z'))).toThrow(/stale/i);
|
|
27
|
+
const envelope = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
28
|
+
envelope.start.agent_type = 'other';
|
|
29
|
+
fs_1.default.writeFileSync(file, JSON.stringify(envelope));
|
|
30
|
+
expect(() => (0, claude_hook_events_1.readClaudeHookPair)(stop, new Date('2026-09-23T22:01:00.000Z'))).toThrow(/seal/i);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (previous === undefined)
|
|
34
|
+
delete process.env.AWM_HOME;
|
|
35
|
+
else
|
|
36
|
+
process.env.AWM_HOME = previous;
|
|
37
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const event_store_1 = require("../../../src/core/model-policy/event-store");
|
|
10
|
+
const machine_key_1 = require("../../../src/core/model-policy/machine-key");
|
|
11
|
+
const native_claude_1 = require("../../../src/core/model-policy/native-claude");
|
|
12
|
+
const native_claude_transcript_1 = require("../../../src/core/model-policy/native-claude-transcript");
|
|
13
|
+
const native_claude_agent_definition_1 = require("../../../src/core/model-policy/native-claude-agent-definition");
|
|
14
|
+
describe('Claude native event enrollment', () => {
|
|
15
|
+
it('binds a matching hook pair and transcript; keeps an unchanged claim beyond 24 hours', () => {
|
|
16
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-claude-event-'));
|
|
17
|
+
const previous = process.env.AWM_HOME;
|
|
18
|
+
process.env.AWM_HOME = root;
|
|
19
|
+
try {
|
|
20
|
+
(0, machine_key_1.ensureMachineKey)(root);
|
|
21
|
+
const configDir = path_1.default.join(root, 'claude');
|
|
22
|
+
const file = path_1.default.join(configDir, 'projects', 'p', 's', 'subagents', 'agent-agent-1.jsonl');
|
|
23
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
24
|
+
fs_1.default.writeFileSync(file, `${JSON.stringify({ type: 'assistant', timestamp: '2026-09-23T22:00:00.000Z', message: { model: 'claude-sonnet-4-6', content: [] } })}\n`);
|
|
25
|
+
fs_1.default.mkdirSync(path_1.default.join(configDir, 'agents'));
|
|
26
|
+
fs_1.default.writeFileSync(path_1.default.join(configDir, 'agents', 'worker.md'), '---\nname: awm-worker\ndescription: Test worker\nmodel: claude-sonnet-4-6\n---\n');
|
|
27
|
+
const start = { hook_event_name: 'SubagentStart', session_id: 'session-1', agent_id: 'agent-1', agent_type: 'awm-worker' };
|
|
28
|
+
const stop = { hook_event_name: 'SubagentStop', session_id: 'session-1', agent_id: 'agent-1', agent_type: 'awm-worker', agent_transcript_path: file, stop_hook_active: false };
|
|
29
|
+
const lifecycle = (0, native_claude_1.normalizeClaudeSubagentLifecycle)(start, stop);
|
|
30
|
+
const transcript = (0, native_claude_transcript_1.readClaudeSubagentTranscript)({ configDir, file, agentId: 'agent-1' });
|
|
31
|
+
const scope = { runtime: { target: 'claude-code', kind: 'native', version: '2.1.0', accountScopeDigest: 'a'.repeat(64) }, binaryDigest: 'b'.repeat(64), configDigest: 'c'.repeat(64) };
|
|
32
|
+
const expectedSelection = { selector: { kind: 'model', id: 'claude-sonnet-4-6' }, effort: { kind: 'runtime-default' } };
|
|
33
|
+
const input = { scope, lifecycle, transcript, expectedSelection, mappingDigest: 'd'.repeat(64), now: new Date('2026-09-23T22:01:00.000Z') };
|
|
34
|
+
expect(() => (0, event_store_1.publishClaudeObservation)(input)).toThrow(/declared agent/i);
|
|
35
|
+
const declaredAgent = (0, native_claude_agent_definition_1.readClaudeAgentDeclaredModel)({ cwd: root, configDir, agentType: 'awm-worker' });
|
|
36
|
+
const enrolled = { ...input, declaredAgent };
|
|
37
|
+
const first = (0, event_store_1.publishClaudeObservation)(enrolled);
|
|
38
|
+
expect(first.claims[0]).toMatchObject({ actualModel: { id: 'claude-sonnet-4-6' }, tokenUsage: 'unknown' });
|
|
39
|
+
expect((0, event_store_1.publishClaudeObservation)({ ...enrolled, now: new Date('2026-09-25T00:00:00.000Z') })).toEqual(first);
|
|
40
|
+
expect((0, event_store_1.readStoredEventReceipt)(scope.runtime)).toEqual({ state: 'present', receipt: first });
|
|
41
|
+
expect(() => (0, event_store_1.publishClaudeObservation)({ ...enrolled, expectedSelection: { ...expectedSelection, selector: { kind: 'model', id: 'claude-opus-4-7' } } })).toThrow(/selection/i);
|
|
42
|
+
expect(() => (0, event_store_1.publishClaudeObservation)({ ...enrolled, expectedSelection: { ...expectedSelection, effort: { kind: 'explicit', value: 'high' } } })).toThrow(/selection|effort/i);
|
|
43
|
+
expect(() => (0, event_store_1.publishClaudeObservation)({ ...enrolled, lifecycle: { ...lifecycle } })).toThrow(/provenance/i);
|
|
44
|
+
expect(() => (0, event_store_1.publishClaudeObservation)({ ...enrolled, declaredAgent: { ...declaredAgent } })).toThrow(/declared agent/i);
|
|
45
|
+
const secondFile = path_1.default.join(configDir, 'projects', 'p', 's', 'subagents', 'agent-agent-2.jsonl');
|
|
46
|
+
fs_1.default.writeFileSync(secondFile, `${JSON.stringify({ type: 'assistant', timestamp: '2026-09-23T22:02:00.000Z', message: { model: 'claude-sonnet-4-6', content: [] } })}\n`);
|
|
47
|
+
const secondLifecycle = (0, native_claude_1.normalizeClaudeSubagentLifecycle)({ ...start, agent_id: 'agent-2' }, { ...stop, agent_id: 'agent-2', agent_transcript_path: secondFile });
|
|
48
|
+
const secondTranscript = (0, native_claude_transcript_1.readClaudeSubagentTranscript)({ configDir, file: secondFile, agentId: 'agent-2' });
|
|
49
|
+
const second = (0, event_store_1.publishClaudeObservation)({ ...enrolled, lifecycle: secondLifecycle, transcript: secondTranscript,
|
|
50
|
+
now: new Date('2026-09-23T22:03:00.000Z') });
|
|
51
|
+
expect(second.claims[0].nativeEvents).toEqual(expect.arrayContaining([
|
|
52
|
+
expect.objectContaining({ nativeAgentIdDigest: (0, event_store_1.claudeAgentIdDigest)('agent-1', (0, machine_key_1.ensureMachineKey)(root)) }),
|
|
53
|
+
expect.objectContaining({ nativeAgentIdDigest: (0, event_store_1.claudeAgentIdDigest)('agent-2', (0, machine_key_1.ensureMachineKey)(root)) }),
|
|
54
|
+
]));
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (previous === undefined)
|
|
58
|
+
delete process.env.AWM_HOME;
|
|
59
|
+
else
|
|
60
|
+
process.env.AWM_HOME = previous;
|
|
61
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const event_store_1 = require("../../../src/core/model-policy/event-store");
|
|
10
|
+
const native_codex_1 = require("../../../src/core/model-policy/native-codex");
|
|
11
|
+
const machine_key_1 = require("../../../src/core/model-policy/machine-key");
|
|
12
|
+
const a = 'a'.repeat(64);
|
|
13
|
+
const b = 'b'.repeat(64);
|
|
14
|
+
const c = 'c'.repeat(64);
|
|
15
|
+
const runtime = { target: 'codex', kind: 'native', version: '0.156.1', accountScopeDigest: a };
|
|
16
|
+
const scope = { runtime, binaryDigest: b, configDigest: c };
|
|
17
|
+
const observation = (id) => ({ provenance: 'codex-app-server-thread-read', parentThreadId: 'parent-1', childThreadId: `child-${id}`, turnId: `turn-${id}`, nativeDispatchVerified: true,
|
|
18
|
+
configuredSelection: { selector: { kind: 'model', id }, effort: { kind: 'explicit', value: 'high' } }, acceptedSelectionVerified: true, childRuntimeVersion: '0.156.1', actualModelVerified: false, tokenUsage: 'unknown' });
|
|
19
|
+
const enrollment = (id, now, native = observation(id)) => ({ scope, observation: native, expectedSelection: observation(id).configuredSelection, mappingDigest: a, now });
|
|
20
|
+
async function nativeObservation(id) {
|
|
21
|
+
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-native-event-'));
|
|
22
|
+
const file = path_1.default.join(home, 'sessions', 'rollout.jsonl');
|
|
23
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file));
|
|
24
|
+
fs_1.default.writeFileSync(file, [
|
|
25
|
+
{ type: 'session_meta', payload: { id: `child-${id}`, source: { subagent: { thread_spawn: { parent_thread_id: 'parent-1' } } }, cli_version: '0.156.1' } },
|
|
26
|
+
{ timestamp: '2026-09-23T00:59:00.000Z', type: 'turn_context', payload: { turn_id: `turn-${id}`, model: id, effort: 'high' } },
|
|
27
|
+
].map(value => JSON.stringify(value)).join('\n'));
|
|
28
|
+
const response = { thread: { id: `child-${id}`, parentThreadId: 'parent-1', model: id, reasoningEffort: 'high', path: file,
|
|
29
|
+
turns: [{ id: `turn-${id}`, status: 'completed', items: [] }] } };
|
|
30
|
+
const server = `
|
|
31
|
+
const readline = require('readline');
|
|
32
|
+
const input = readline.createInterface({ input: process.stdin });
|
|
33
|
+
input.on('line', line => {
|
|
34
|
+
const request = JSON.parse(line);
|
|
35
|
+
if (request.method === 'initialize') process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + '\\n');
|
|
36
|
+
if (request.method === 'thread/read') process.stdout.write(JSON.stringify({ id: request.id, result: ${JSON.stringify(response)} }) + '\\n');
|
|
37
|
+
});`;
|
|
38
|
+
try {
|
|
39
|
+
return await (0, native_codex_1.queryCodexChildThreadObservation)({ command: process.execPath, args: ['-e', server], timeoutMs: 5000, parentThreadId: 'parent-1', childThreadId: `child-${id}`, codexHome: home });
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
describe('event-scoped capability store', () => {
|
|
46
|
+
it('publishes native claims once and preserves a second selection without renewal', async () => {
|
|
47
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-event-store-'));
|
|
48
|
+
const previous = process.env.AWM_HOME;
|
|
49
|
+
process.env.AWM_HOME = root;
|
|
50
|
+
try {
|
|
51
|
+
expect((0, event_store_1.readStoredEventReceipt)(runtime)).toEqual({ state: 'absent' });
|
|
52
|
+
(0, machine_key_1.ensureMachineKey)(root);
|
|
53
|
+
const luna = await nativeObservation('gpt-6-luna');
|
|
54
|
+
const first = (0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-23T01:00:00.000Z'), luna));
|
|
55
|
+
expect(first.claims).toHaveLength(1);
|
|
56
|
+
const file = path_1.default.join(root, 'routing-capabilities-v2', 'codex', 'native.json');
|
|
57
|
+
const before = fs_1.default.readFileSync(file);
|
|
58
|
+
(0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-24T03:00:00.000Z'), luna));
|
|
59
|
+
expect(fs_1.default.readFileSync(file)).toEqual(before);
|
|
60
|
+
const second = (0, event_store_1.publishCodexObservation)(enrollment('gpt-6-sol', new Date('2026-09-23T01:00:00.000Z'), await nativeObservation('gpt-6-sol')));
|
|
61
|
+
expect(second.claims).toHaveLength(2);
|
|
62
|
+
const read = (0, event_store_1.readStoredEventReceipt)(runtime);
|
|
63
|
+
expect(read.state).toBe('present');
|
|
64
|
+
if (read.state !== 'present')
|
|
65
|
+
throw new Error('expected a present receipt');
|
|
66
|
+
expect(read.receipt.claims.map(item => item.selection.selector.id)).toEqual(['gpt-6-luna', 'gpt-6-sol']);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
if (previous === undefined)
|
|
70
|
+
delete process.env.AWM_HOME;
|
|
71
|
+
else
|
|
72
|
+
process.env.AWM_HOME = previous;
|
|
73
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
it('fails closed on a symlinked destination', async () => {
|
|
77
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-event-store-'));
|
|
78
|
+
const previous = process.env.AWM_HOME;
|
|
79
|
+
process.env.AWM_HOME = root;
|
|
80
|
+
const file = path_1.default.join(root, 'routing-capabilities-v2', 'codex', 'native.json');
|
|
81
|
+
(0, machine_key_1.ensureMachineKey)(root);
|
|
82
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
83
|
+
const outside = path_1.default.join(root, 'outside.json');
|
|
84
|
+
fs_1.default.writeFileSync(outside, 'untouched');
|
|
85
|
+
fs_1.default.symlinkSync(outside, file);
|
|
86
|
+
try {
|
|
87
|
+
const native = await nativeObservation('gpt-6-luna');
|
|
88
|
+
expect(() => (0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-23T01:00:00.000Z'), native))).toThrow(/symlink|unsafe/i);
|
|
89
|
+
expect(fs_1.default.readFileSync(outside, 'utf8')).toBe('untouched');
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
if (previous === undefined)
|
|
93
|
+
delete process.env.AWM_HOME;
|
|
94
|
+
else
|
|
95
|
+
process.env.AWM_HOME = previous;
|
|
96
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
it('refuses an unverified selection or a child from another runtime version', async () => {
|
|
100
|
+
const native = await nativeObservation('gpt-6-luna');
|
|
101
|
+
native.acceptedSelectionVerified = false;
|
|
102
|
+
expect(() => (0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-23T01:00:00.000Z'), native))).toThrow(/native provenance/i);
|
|
103
|
+
native.acceptedSelectionVerified = true;
|
|
104
|
+
native.childRuntimeVersion = '0.156.0';
|
|
105
|
+
expect(() => (0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-23T01:00:00.000Z'), native))).toThrow(/native provenance/i);
|
|
106
|
+
});
|
|
107
|
+
it('rejects a caller-authored accepted-looking observation without native provenance', () => {
|
|
108
|
+
expect(() => (0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-23T01:00:00.000Z')))).toThrow(/native provenance/i);
|
|
109
|
+
});
|
|
110
|
+
it('rejects an old native turn for a new enrollment but retains an already stored claim after 25 hours', async () => {
|
|
111
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-event-store-'));
|
|
112
|
+
const previous = process.env.AWM_HOME;
|
|
113
|
+
process.env.AWM_HOME = root;
|
|
114
|
+
try {
|
|
115
|
+
(0, machine_key_1.ensureMachineKey)(root);
|
|
116
|
+
const native = await nativeObservation('gpt-6-luna');
|
|
117
|
+
expect(() => (0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-24T03:00:00.000Z'), native))).toThrow(/fresh/i);
|
|
118
|
+
const first = (0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-23T01:00:00.000Z'), native));
|
|
119
|
+
const later = (0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-24T03:00:00.000Z'), native));
|
|
120
|
+
expect(later).toEqual(first);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (previous === undefined)
|
|
124
|
+
delete process.env.AWM_HOME;
|
|
125
|
+
else
|
|
126
|
+
process.env.AWM_HOME = previous;
|
|
127
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
it('rejects a forged on-disk claim even if its JSON shape and digests look valid', async () => {
|
|
131
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-event-store-'));
|
|
132
|
+
const previous = process.env.AWM_HOME;
|
|
133
|
+
process.env.AWM_HOME = root;
|
|
134
|
+
try {
|
|
135
|
+
(0, machine_key_1.ensureMachineKey)(root);
|
|
136
|
+
(0, event_store_1.publishCodexObservation)(enrollment('gpt-6-luna', new Date('2026-09-23T01:00:00.000Z'), await nativeObservation('gpt-6-luna')));
|
|
137
|
+
const file = path_1.default.join(root, 'routing-capabilities-v2', 'codex', 'native.json');
|
|
138
|
+
const envelope = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
139
|
+
envelope.receipt.claims[0].selection.selector.id = 'gpt-6-astra';
|
|
140
|
+
fs_1.default.writeFileSync(file, JSON.stringify(envelope));
|
|
141
|
+
expect((0, event_store_1.readStoredEventReceipt)(runtime)).toMatchObject({ state: 'invalid' });
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
if (previous === undefined)
|
|
145
|
+
delete process.env.AWM_HOME;
|
|
146
|
+
else
|
|
147
|
+
process.env.AWM_HOME = previous;
|
|
148
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|