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,221 @@
|
|
|
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
|
+
exports.claudeAgentIdDigest = claudeAgentIdDigest;
|
|
7
|
+
exports.readStoredEventReceipt = readStoredEventReceipt;
|
|
8
|
+
exports.publishCodexObservation = publishCodexObservation;
|
|
9
|
+
exports.publishClaudeObservation = publishClaudeObservation;
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const paths_1 = require("../paths");
|
|
14
|
+
const json_1 = require("../plan/json");
|
|
15
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
16
|
+
const capabilities_1 = require("./capabilities");
|
|
17
|
+
const capabilities_v2_1 = require("./capabilities-v2");
|
|
18
|
+
const native_codex_1 = require("./native-codex");
|
|
19
|
+
const native_claude_1 = require("./native-claude");
|
|
20
|
+
const native_claude_transcript_1 = require("./native-claude-transcript");
|
|
21
|
+
const native_claude_agent_definition_1 = require("./native-claude-agent-definition");
|
|
22
|
+
const machine_key_1 = require("./machine-key");
|
|
23
|
+
const validate_1 = require("./validate");
|
|
24
|
+
const MAX_RECEIPT_BYTES = 256 * 1024;
|
|
25
|
+
const SEAL_DOMAIN = 'AWM routing-capabilities/v2 local receipt\0';
|
|
26
|
+
const CLAUDE_AGENT_DOMAIN = 'AWM Claude native agent ID/v1\0';
|
|
27
|
+
function claudeAgentIdDigest(agentId, key) {
|
|
28
|
+
if (typeof agentId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(agentId) || !Buffer.isBuffer(key) || key.length !== 32)
|
|
29
|
+
throw new Error('Claude native agent identity is invalid');
|
|
30
|
+
return (0, crypto_1.createHmac)('sha256', key).update(CLAUDE_AGENT_DOMAIN).update(agentId).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
function receiptPath(runtime) {
|
|
33
|
+
const valid = (0, capabilities_1.validateRuntimeKey)(runtime);
|
|
34
|
+
return path_1.default.join((0, paths_1.awmHome)(), 'routing-capabilities-v2', valid.target, `${valid.kind}.json`);
|
|
35
|
+
}
|
|
36
|
+
function safeParents(file, create) {
|
|
37
|
+
const chain = [];
|
|
38
|
+
for (let dir = path_1.default.dirname(file);; dir = path_1.default.dirname(dir)) {
|
|
39
|
+
chain.unshift(dir);
|
|
40
|
+
if (dir === path_1.default.dirname(dir))
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
for (const dir of chain) {
|
|
44
|
+
try {
|
|
45
|
+
const stat = fs_1.default.lstatSync(dir);
|
|
46
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
47
|
+
throw new Error('event receipt has unsafe or symlinked ancestor');
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== 'ENOENT' || !create)
|
|
51
|
+
throw error;
|
|
52
|
+
fs_1.default.mkdirSync(dir, { mode: 0o700 });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function readFile(file) {
|
|
57
|
+
try {
|
|
58
|
+
safeParents(file, false);
|
|
59
|
+
const stat = fs_1.default.lstatSync(file);
|
|
60
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
61
|
+
throw new Error('event receipt is unsafe or symlinked');
|
|
62
|
+
const bytes = native_bridge_1.secureFs.readRegularFile(file, MAX_RECEIPT_BYTES).bytes;
|
|
63
|
+
const parsed = (0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
64
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
65
|
+
throw new Error('event receipt envelope is invalid');
|
|
66
|
+
const envelope = parsed;
|
|
67
|
+
if (Object.keys(envelope).length !== 2 || !Object.keys(envelope).every(key => ['receipt', 'mac'].includes(key))
|
|
68
|
+
|| typeof envelope.mac !== 'string' || !/^[a-f0-9]{64}$/.test(envelope.mac))
|
|
69
|
+
throw new Error('event receipt seal is invalid');
|
|
70
|
+
const receipt = (0, capabilities_v2_1.validateEventReceipt)(envelope.receipt);
|
|
71
|
+
const key = (0, machine_key_1.readMachineKey)((0, paths_1.awmHome)());
|
|
72
|
+
if (!key)
|
|
73
|
+
throw new Error('event receipt machine key is missing');
|
|
74
|
+
const expected = seal(receipt, key);
|
|
75
|
+
if (!(0, crypto_1.timingSafeEqual)(Buffer.from(envelope.mac, 'hex'), Buffer.from(expected, 'hex')))
|
|
76
|
+
throw new Error('event receipt seal does not match this machine');
|
|
77
|
+
return receipt;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.code === 'ENOENT')
|
|
81
|
+
return null;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function seal(receipt, key) {
|
|
86
|
+
return (0, crypto_1.createHmac)('sha256', key).update(SEAL_DOMAIN).update(JSON.stringify(receipt)).digest('hex');
|
|
87
|
+
}
|
|
88
|
+
function readStoredEventReceipt(runtime) {
|
|
89
|
+
try {
|
|
90
|
+
const value = readFile(receiptPath(runtime));
|
|
91
|
+
return value === null ? { state: 'absent' } : { state: 'present', receipt: value };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return { state: 'invalid', reason: error.message.slice(0, 4096) };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function sameSelection(left, right) {
|
|
98
|
+
return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind
|
|
99
|
+
&& (left.effort.kind !== 'explicit' || left.effort.value === right.effort.value);
|
|
100
|
+
}
|
|
101
|
+
function sameScope(left, right) {
|
|
102
|
+
return left.runtime.target === right.runtime.target && left.runtime.kind === right.runtime.kind && left.runtime.version === right.runtime.version
|
|
103
|
+
&& left.runtime.accountScopeDigest === right.runtime.accountScopeDigest && left.binaryDigest === right.binaryDigest && left.configDigest === right.configDigest;
|
|
104
|
+
}
|
|
105
|
+
/** The only Codex v2 writer takes a native observation, never a candidate JSON.
|
|
106
|
+
* The CLI must call the native adapter and derive scope from this machine. */
|
|
107
|
+
function publishCodexObservation(input) {
|
|
108
|
+
if (!input || typeof input !== 'object' || !(0, native_codex_1.isNativeCodexChildObservation)(input.observation))
|
|
109
|
+
throw new Error('Codex native provenance is required');
|
|
110
|
+
if (input.observation.provenance !== 'codex-app-server-thread-read'
|
|
111
|
+
|| input.observation.nativeDispatchVerified !== true || input.observation.acceptedSelectionVerified !== true
|
|
112
|
+
|| input.observation.actualModelVerified !== false || input.observation.tokenUsage !== 'unknown')
|
|
113
|
+
throw new Error('Codex native selection is unverified');
|
|
114
|
+
const scope = input.scope;
|
|
115
|
+
if (!scope || scope.runtime?.target !== 'codex' || input.observation.childRuntimeVersion !== scope.runtime.version)
|
|
116
|
+
throw new Error('Codex native runtime version mismatch');
|
|
117
|
+
if (!sameSelection(input.observation.configuredSelection, input.expectedSelection))
|
|
118
|
+
throw new Error('Codex observed selection does not match approved selection');
|
|
119
|
+
(0, validate_1.assertDigest)(input.mappingDigest, 'mappingDigest');
|
|
120
|
+
if (!(input.now instanceof Date) || !Number.isFinite(input.now.getTime()))
|
|
121
|
+
throw new Error('event receipt clock is invalid');
|
|
122
|
+
const recordedAt = input.now.toISOString();
|
|
123
|
+
if (typeof input.observation.observedAt !== 'string' || !Number.isFinite(Date.parse(input.observation.observedAt)))
|
|
124
|
+
throw new Error('Codex native turn timestamp is unavailable');
|
|
125
|
+
const eventDigest = (0, crypto_1.createHash)('sha256').update(JSON.stringify({ parent: input.observation.parentThreadId, child: input.observation.childThreadId,
|
|
126
|
+
turn: input.observation.turnId, selection: input.observation.configuredSelection, runtimeVersion: input.observation.childRuntimeVersion,
|
|
127
|
+
accountScopeDigest: scope.runtime.accountScopeDigest })).digest('hex');
|
|
128
|
+
const claim = { selection: input.expectedSelection, mappingDigest: input.mappingDigest, eventDigest,
|
|
129
|
+
source: 'codex-turn-context', observedAt: input.observation.observedAt, actualModel: 'unverified', tokenUsage: 'unknown' };
|
|
130
|
+
const initial = (0, capabilities_v2_1.validateEventReceipt)({ schema: 'routing-capabilities/v2', ...scope, recordedAt, claims: [claim] });
|
|
131
|
+
const file = receiptPath(initial.runtime);
|
|
132
|
+
const root = (0, paths_1.awmHome)();
|
|
133
|
+
return native_bridge_1.secureFs.withProjectLease(root, () => {
|
|
134
|
+
const key = (0, machine_key_1.readMachineKey)(root);
|
|
135
|
+
if (!key)
|
|
136
|
+
throw new Error('event receipt machine key is missing; run explicit setup');
|
|
137
|
+
safeParents(file, true);
|
|
138
|
+
const current = readFile(file);
|
|
139
|
+
if (current && sameScope(current, scope)) {
|
|
140
|
+
const prior = current.claims.find(item => sameSelection(item.selection, claim.selection));
|
|
141
|
+
if (prior && prior.mappingDigest === claim.mappingDigest && prior.eventDigest === claim.eventDigest)
|
|
142
|
+
return current;
|
|
143
|
+
}
|
|
144
|
+
const ageMs = input.now.getTime() - Date.parse(input.observation.observedAt);
|
|
145
|
+
if (ageMs < 0 || ageMs > 15 * 60 * 1000)
|
|
146
|
+
throw new Error('Codex native turn is not fresh for enrollment');
|
|
147
|
+
const claims = current && sameScope(current, scope) ? current.claims.filter(item => !sameSelection(item.selection, claim.selection)).concat(claim) : [claim];
|
|
148
|
+
const next = (0, capabilities_v2_1.validateEventReceipt)({ ...initial, claims });
|
|
149
|
+
const destination = path_1.default.relative(root, file).split(path_1.default.sep).join('/');
|
|
150
|
+
const bytes = Buffer.from(`${JSON.stringify({ receipt: next, mac: seal(next, key) }, null, 2)}\n`);
|
|
151
|
+
if (current) {
|
|
152
|
+
const observed = native_bridge_1.secureFs.readRegularFile(file, MAX_RECEIPT_BYTES);
|
|
153
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, destination, bytes, { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity });
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, destination, bytes, { mode: 'create', createParents: false });
|
|
157
|
+
return next;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** Bind a native hook pair to the provider's assistant transcript. The transcript
|
|
161
|
+
* reports a model, not effective effort or paid usage, so only runtime-default
|
|
162
|
+
* effort may be enrolled and usage remains unknown. */
|
|
163
|
+
function publishClaudeObservation(input) {
|
|
164
|
+
if (!input || typeof input !== 'object' || !(0, native_claude_1.isNativeClaudeSubagentLifecycle)(input.lifecycle)
|
|
165
|
+
|| !(0, native_claude_transcript_1.isNativeClaudeTranscriptObservation)(input.transcript))
|
|
166
|
+
throw new Error('Claude native hook and transcript provenance are required');
|
|
167
|
+
if (!(0, native_claude_agent_definition_1.isNativeClaudeAgentDeclaration)(input.declaredAgent) || input.declaredAgent.agentType !== input.lifecycle.agentType
|
|
168
|
+
|| input.declaredAgent.modelId !== input.transcript.actualModel)
|
|
169
|
+
throw new Error('Claude declared agent model provenance is required');
|
|
170
|
+
if (input.scope?.runtime?.target !== 'claude-code' || input.lifecycle.agentId !== input.transcript.agentId
|
|
171
|
+
|| input.expectedSelection?.selector?.kind !== 'model' || input.expectedSelection.selector.id !== input.transcript.actualModel
|
|
172
|
+
|| input.expectedSelection.effort.kind !== 'runtime-default')
|
|
173
|
+
throw new Error('Claude native selection does not match approved model or effort');
|
|
174
|
+
(0, validate_1.assertDigest)(input.mappingDigest, 'mappingDigest');
|
|
175
|
+
if (!(input.now instanceof Date) || !Number.isFinite(input.now.getTime()))
|
|
176
|
+
throw new Error('event receipt clock is invalid');
|
|
177
|
+
const eventDigest = (0, crypto_1.createHash)('sha256').update(JSON.stringify({ session: input.lifecycle.sessionId, agent: input.lifecycle.agentId,
|
|
178
|
+
transcriptDigest: input.transcript.evidenceDigest, accountScopeDigest: input.scope.runtime.accountScopeDigest })).digest('hex');
|
|
179
|
+
const nativeAgentIdDigest = claudeAgentIdDigest(input.lifecycle.agentId, (0, machine_key_1.readMachineKey)((0, paths_1.awmHome)()));
|
|
180
|
+
const nativeEvent = { nativeAgentIdDigest, eventDigest, observedAt: input.transcript.observedAt };
|
|
181
|
+
const claim = { selection: input.expectedSelection, mappingDigest: input.mappingDigest, eventDigest,
|
|
182
|
+
nativeAgentType: input.lifecycle.agentType,
|
|
183
|
+
nativeAgentIdDigest, nativeEvents: [nativeEvent],
|
|
184
|
+
source: 'claude-assistant-transcript', observedAt: input.transcript.observedAt,
|
|
185
|
+
actualModel: { id: input.transcript.actualModel, evidenceDigest: input.transcript.evidenceDigest }, tokenUsage: 'unknown' };
|
|
186
|
+
const initial = (0, capabilities_v2_1.validateEventReceipt)({ schema: 'routing-capabilities/v2', ...input.scope, recordedAt: input.now.toISOString(), claims: [claim] });
|
|
187
|
+
const file = receiptPath(initial.runtime);
|
|
188
|
+
const root = (0, paths_1.awmHome)();
|
|
189
|
+
return native_bridge_1.secureFs.withProjectLease(root, () => {
|
|
190
|
+
const key = (0, machine_key_1.readMachineKey)(root);
|
|
191
|
+
if (!key)
|
|
192
|
+
throw new Error('event receipt machine key is missing; run explicit setup');
|
|
193
|
+
safeParents(file, true);
|
|
194
|
+
const current = readFile(file);
|
|
195
|
+
if (current && sameScope(current, input.scope)) {
|
|
196
|
+
const prior = current.claims.find(item => sameSelection(item.selection, claim.selection));
|
|
197
|
+
if (prior && prior.mappingDigest === claim.mappingDigest && prior.eventDigest === claim.eventDigest)
|
|
198
|
+
return current;
|
|
199
|
+
}
|
|
200
|
+
const ageMs = input.now.getTime() - Date.parse(input.transcript.observedAt);
|
|
201
|
+
if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs > 15 * 60 * 1000)
|
|
202
|
+
throw new Error('Claude native transcript is not fresh for enrollment');
|
|
203
|
+
const prior = current && sameScope(current, input.scope) ? current.claims.find(item => sameSelection(item.selection, claim.selection)) : undefined;
|
|
204
|
+
const previousEvents = prior?.nativeEvents ?? (prior?.nativeAgentIdDigest ? [{ nativeAgentIdDigest: prior.nativeAgentIdDigest,
|
|
205
|
+
eventDigest: prior.eventDigest, observedAt: prior.observedAt }] : []);
|
|
206
|
+
const recentEvents = previousEvents.filter(item => input.now.getTime() - Date.parse(item.observedAt) <= 15 * 60_000
|
|
207
|
+
&& item.nativeAgentIdDigest !== nativeAgentIdDigest && item.eventDigest !== eventDigest);
|
|
208
|
+
const mergedClaim = { ...claim, nativeEvents: recentEvents.concat(nativeEvent).slice(-64) };
|
|
209
|
+
const claims = current && sameScope(current, input.scope) ? current.claims.filter(item => !sameSelection(item.selection, claim.selection)).concat(mergedClaim) : [mergedClaim];
|
|
210
|
+
const next = (0, capabilities_v2_1.validateEventReceipt)({ ...initial, claims });
|
|
211
|
+
const destination = path_1.default.relative(root, file).split(path_1.default.sep).join('/');
|
|
212
|
+
const bytes = Buffer.from(`${JSON.stringify({ receipt: next, mac: seal(next, key) }, null, 2)}\n`);
|
|
213
|
+
if (current) {
|
|
214
|
+
const observed = native_bridge_1.secureFs.readRegularFile(file, MAX_RECEIPT_BYTES);
|
|
215
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, destination, bytes, { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity });
|
|
216
|
+
}
|
|
217
|
+
else
|
|
218
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, destination, bytes, { mode: 'create', createParents: false });
|
|
219
|
+
return next;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
exports.recordRoutingHookStatus = recordRoutingHookStatus;
|
|
7
|
+
exports.readRoutingHookStatus = readRoutingHookStatus;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const paths_1 = require("../paths");
|
|
11
|
+
const json_1 = require("../plan/json");
|
|
12
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
13
|
+
const fresh = () => ({ schema: 'routing-hook-status/v1', start: { count: 0 }, stop: { count: 0 } });
|
|
14
|
+
function target() { return path_1.default.join((0, paths_1.awmHome)(), 'routing-hook-status.json'); }
|
|
15
|
+
function safeParents(file) {
|
|
16
|
+
const chain = [];
|
|
17
|
+
for (let dir = path_1.default.dirname(file);; dir = path_1.default.dirname(dir)) {
|
|
18
|
+
chain.unshift(dir);
|
|
19
|
+
if (dir === path_1.default.dirname(dir))
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
for (const dir of chain) {
|
|
23
|
+
const stat = fs_1.default.lstatSync(dir);
|
|
24
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
25
|
+
throw new Error('routing hook status has an unsafe ancestor');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function read() {
|
|
29
|
+
const file = target();
|
|
30
|
+
safeParents(file);
|
|
31
|
+
let stat;
|
|
32
|
+
try {
|
|
33
|
+
stat = fs_1.default.lstatSync(file);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT')
|
|
37
|
+
return { status: fresh(), observed: null };
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
41
|
+
throw new Error('routing hook status is unsafe');
|
|
42
|
+
const observed = native_bridge_1.secureFs.readRegularFile(file, 8192);
|
|
43
|
+
const value = (0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(observed.bytes));
|
|
44
|
+
if (!value || value.schema !== 'routing-hook-status/v1' || !value.start || !value.stop
|
|
45
|
+
|| !Number.isSafeInteger(value.start.count) || !Number.isSafeInteger(value.stop.count)
|
|
46
|
+
|| value.start.count < 0 || value.stop.count < 0)
|
|
47
|
+
throw new Error('routing hook status is corrupt');
|
|
48
|
+
return { status: value, observed };
|
|
49
|
+
}
|
|
50
|
+
/** Failure is a redacted code, never provider payload or an exception string.
|
|
51
|
+
* Returns true only for a newly alertable failure episode. */
|
|
52
|
+
function recordRoutingHookStatus(phase, result, now = new Date()) {
|
|
53
|
+
if (phase !== 'start' && phase !== 'stop')
|
|
54
|
+
throw new Error('routing hook phase is invalid');
|
|
55
|
+
if (result !== 'success' && !/^[A-Z0-9_]{3,64}$/.test(result))
|
|
56
|
+
throw new Error('routing hook reason code is invalid');
|
|
57
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
58
|
+
throw new Error('routing hook clock is invalid');
|
|
59
|
+
const root = (0, paths_1.awmHome)();
|
|
60
|
+
return native_bridge_1.secureFs.withProjectLease(root, () => {
|
|
61
|
+
const { status, observed } = read();
|
|
62
|
+
const item = status[phase];
|
|
63
|
+
const pending = item.lastFailureAt !== undefined && (item.lastSuccessAt === undefined || item.lastFailureAt > item.lastSuccessAt);
|
|
64
|
+
const alert = result !== 'success' && (!pending || item.reasonCode !== result);
|
|
65
|
+
if (result === 'success')
|
|
66
|
+
item.lastSuccessAt = now.toISOString();
|
|
67
|
+
else {
|
|
68
|
+
item.lastFailureAt = now.toISOString();
|
|
69
|
+
item.reasonCode = result;
|
|
70
|
+
item.count += 1;
|
|
71
|
+
}
|
|
72
|
+
const bytes = Buffer.from(`${JSON.stringify(status)}\n`);
|
|
73
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, path_1.default.basename(target()), bytes, observed
|
|
74
|
+
? { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity }
|
|
75
|
+
: { mode: 'create', createParents: false });
|
|
76
|
+
return alert;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function readRoutingHookStatus() {
|
|
80
|
+
try {
|
|
81
|
+
const { status, observed } = read();
|
|
82
|
+
if (!observed)
|
|
83
|
+
return { state: 'absent' };
|
|
84
|
+
const failures = ['start', 'stop'].flatMap(phase => {
|
|
85
|
+
const item = status[phase];
|
|
86
|
+
return item.lastFailureAt && item.reasonCode && (!item.lastSuccessAt || item.lastFailureAt > item.lastSuccessAt)
|
|
87
|
+
? [{ phase, reasonCode: item.reasonCode, count: item.count }] : [];
|
|
88
|
+
});
|
|
89
|
+
return failures.length ? { state: 'failed', failures } : { state: 'healthy' };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { state: 'invalid' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -21,6 +21,26 @@ function canonical(value) { if (Array.isArray(value))
|
|
|
21
21
|
function digest(value) { return crypto_1.default.createHash('sha256').update(canonical(value)).digest('hex'); }
|
|
22
22
|
function clone(state) { const next = structuredClone(state); if (!Array.isArray(next.routingAttempts))
|
|
23
23
|
next.routingAttempts = []; return next; }
|
|
24
|
+
function noteRoutingIncident(state, target, selection, reasonCode, at, fallback, sameObligationAlreadyCounted = false) {
|
|
25
|
+
if (!/^[A-Z0-9_]{3,64}$/.test(reasonCode))
|
|
26
|
+
throw new Error('routing incident reason is invalid');
|
|
27
|
+
state.routingIncidents ??= [];
|
|
28
|
+
// One incident/alert per selected route in a run even if later failures
|
|
29
|
+
// refine the cause; retain the first actionable reason.
|
|
30
|
+
const id = crypto_1.default.createHash('sha256').update(`${state.journalId}\0${state.cycle.startedAt}\0${target}\0${canonical(selection)}`).digest('hex').slice(0, 24);
|
|
31
|
+
const prior = state.routingIncidents.find(item => item.id === id);
|
|
32
|
+
if (prior) {
|
|
33
|
+
prior.affectedObligations += Number(!sameObligationAlreadyCounted);
|
|
34
|
+
prior.fallbackCount += Number(fallback);
|
|
35
|
+
prior.blockedCount += Number(!fallback);
|
|
36
|
+
prior.lastAt = at;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (state.routingIncidents.length >= 4096)
|
|
40
|
+
throw new Error('routing incident limit exhausted');
|
|
41
|
+
state.routingIncidents.push({ schema: 'routing-incident/v1', id, runStartedAt: state.cycle.startedAt, target, selection: structuredClone(selection), reasonCode,
|
|
42
|
+
firstAt: at, lastAt: at, affectedObligations: 1, fallbackCount: Number(fallback), blockedCount: Number(!fallback), alertState: 'pending' });
|
|
43
|
+
}
|
|
24
44
|
function reserveRoutingAttempt(state, input, now) {
|
|
25
45
|
assertTimestamp(now);
|
|
26
46
|
if (!(0, types_1.isWellFormedState)(state) || !input || typeof input.obligationId !== 'string' || input.obligationId.length === 0 || input.obligationId.length > 128 || typeof input.lineageId !== 'string' || input.lineageId.length === 0 || input.lineageId.length > 128 || !(0, types_1.isRoutingEnvelope)(input.envelope) || !/^[a-f0-9]{64}$/.test(input.fingerprint))
|
|
@@ -35,34 +55,62 @@ function reserveRoutingAttempt(state, input, now) {
|
|
|
35
55
|
next.implementationLineages ??= [];
|
|
36
56
|
const priorLineage = next.implementationLineages.find(lineage => lineage.id === input.lineageId);
|
|
37
57
|
if (input.envelope.role === 'implementer' && priorLineage === undefined)
|
|
38
|
-
next.implementationLineages.push({ id: input.lineageId, obligationId: input.obligationId, sliceId: input.envelope.sliceId, planDigest: input.envelope.planDigest, executionDigest: input.envelope.executionDigest, initialProfile: input.envelope.requestedProfile, initialEffort: input.envelope.resolved.effort.kind === '
|
|
58
|
+
next.implementationLineages.push({ id: input.lineageId, obligationId: input.obligationId, sliceId: input.envelope.sliceId, planDigest: input.envelope.planDigest, executionDigest: input.envelope.executionDigest, initialProfile: input.envelope.requestedProfile, initialEffort: input.envelope.resolved.effort.kind === 'runtime-default' ? 'runtime-default' : input.envelope.resolved.effort.value === 'high' ? 'high' : 'medium', attempts: 0 });
|
|
39
59
|
if (priorLineage !== undefined && (priorLineage.obligationId !== input.obligationId || priorLineage.planDigest !== input.envelope.planDigest || priorLineage.executionDigest !== input.envelope.executionDigest || priorLineage.sliceId !== input.envelope.sliceId || priorLineage.initialProfile !== input.envelope.requestedProfile))
|
|
40
60
|
throw new Error('routing lineage binding mismatch');
|
|
41
61
|
const attempts = next.routingAttempts.filter(item => item.lineageId === input.lineageId && item.envelope.role === 'implementer');
|
|
42
62
|
if (input.envelope.role === 'implementer' && attempts.length >= 3)
|
|
43
63
|
throw new Error('routing implementation budget exhausted');
|
|
64
|
+
const fullFallback = input.envelope.role === 'implementer' && input.envelope.effectiveProfile === 'full';
|
|
65
|
+
if (fullFallback && attempts.some(item => item.envelope.effectiveProfile === 'full'))
|
|
66
|
+
throw new Error('routing full fallback cannot recursively retry a lineage');
|
|
67
|
+
if (fullFallback && attempts.length > 0) {
|
|
68
|
+
const previous = attempts[attempts.length - 1];
|
|
69
|
+
if (previous.state !== 'blocked' || previous.verdict !== 'inconclusive'
|
|
70
|
+
|| !['PROVIDER_REJECTED', 'SELECTION_MISMATCH', 'PROVENANCE_MISSING'].includes(previous.reasonCode ?? ''))
|
|
71
|
+
throw new Error('routing full fallback requires a terminal native routing failure');
|
|
72
|
+
}
|
|
44
73
|
if (input.envelope.role === 'implementer') {
|
|
45
74
|
const initial = input.envelope.requestedProfile;
|
|
46
75
|
if (initial === 'full')
|
|
47
76
|
throw new Error('routing implementer profile is invalid');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
if (!fullFallback) {
|
|
78
|
+
const expected = resolveLineageEscalation(next, input.lineageId, initial);
|
|
79
|
+
const actualEffort = input.envelope.resolved.effort.kind === 'explicit' ? input.envelope.resolved.effort.value : 'runtime-default';
|
|
80
|
+
const judgmentHighAfterIntegration = expected.profile === 'judgment' && expected.effort === 'medium' && actualEffort === 'high' && attempts.length > 0 && attempts[attempts.length - 1].envelope.effectiveProfile === 'integration';
|
|
81
|
+
if (input.envelope.effectiveProfile !== expected.profile || (actualEffort !== expected.effort && !judgmentHighAfterIntegration))
|
|
82
|
+
throw new Error('routing effective escalation mismatch');
|
|
83
|
+
}
|
|
53
84
|
}
|
|
54
85
|
const approval = input.approval?.();
|
|
55
86
|
if (!approval)
|
|
56
87
|
throw new Error('routing reservation requires current approval');
|
|
57
|
-
|
|
88
|
+
if ((approval.eventReceipt || approval.eventScope) && (!approval.eventReceipt || !approval.eventScope || canonical(approval.eventScope.runtime) !== canonical(input.envelope.runtime)))
|
|
89
|
+
throw new Error('routing reservation event scope mismatch');
|
|
90
|
+
const selectionInput = { role: input.envelope.role, requestedProfile: input.envelope.role === 'implementer' ? input.envelope.effectiveProfile : 'full', runtime: (0, capabilities_1.validateRuntimeKey)(input.envelope.runtime), policy: approval.policy, capabilities: approval.capabilities, eventReceipt: approval.eventReceipt, eventScope: approval.eventScope, circuitIncidents: next.routingIncidents, now: approval.checkedAt };
|
|
91
|
+
const mapping = approval.policy?.content.mappings.find(item => item.target === input.envelope.runtime.target && item.runtimeKind === input.envelope.runtime.kind);
|
|
92
|
+
const optimizedSelection = mapping && input.envelope.role === 'implementer' && input.envelope.requestedProfile !== 'full' ? mapping.profiles[input.envelope.requestedProfile] : undefined;
|
|
93
|
+
const circuitReason = optimizedSelection && next.routingIncidents?.find(item => item.target === input.envelope.runtime.target && canonical(item.selection) === canonical(optimizedSelection)
|
|
94
|
+
&& ['PROVIDER_REJECTED', 'SELECTION_MISMATCH', 'PROVENANCE_MISSING'].includes(item.reasonCode))?.reasonCode;
|
|
58
95
|
const resolved = input.envelope.role === 'implementer' && input.envelope.effectiveProfile === 'judgment' && input.envelope.resolved.effort.kind === 'explicit' && input.envelope.resolved.effort.value === 'high'
|
|
59
96
|
? (0, resolve_1.resolveEscalatedSelection)({ ...selectionInput, role: 'implementer', requestedProfile: 'judgment', expectedEffort: 'high' })
|
|
60
|
-
:
|
|
61
|
-
|
|
97
|
+
: approval.eventReceipt && approval.eventScope
|
|
98
|
+
? (0, resolve_1.resolveEventWithFallback)({ role: selectionInput.role, requestedProfile: input.envelope.role === 'implementer' ? fullFallback ? input.envelope.requestedProfile : input.envelope.effectiveProfile : 'full', policy: approval.policy, receipt: approval.eventReceipt, scope: approval.eventScope, now: approval.checkedAt, circuitReason })
|
|
99
|
+
: (0, resolve_1.resolveSelection)(selectionInput);
|
|
100
|
+
if (fullFallback && (resolved.state !== 'resolved' || !resolved.fallbackReason))
|
|
101
|
+
throw new Error('routing full fallback requires a current optimized-route failure');
|
|
102
|
+
if (resolved.state !== 'resolved' || resolved.effectiveProfile !== input.envelope.effectiveProfile || canonical(resolved.selection) !== canonical(input.envelope.resolved) || resolved.policyDigest !== input.envelope.policyDigest || resolved.capabilityDigest !== input.envelope.capabilityDigest || resolved.outcome !== input.envelope.outcome || canonical(resolved.unavailableEvidence) !== canonical(input.envelope.unavailableEvidence) || resolved.nativeAgentType !== input.envelope.nativeAgentType)
|
|
62
103
|
throw new Error('routing reservation approval mismatch');
|
|
63
104
|
const attempt = attempts.length + 1;
|
|
64
105
|
const id = `route-${crypto_1.default.createHash('sha256').update(`${input.lineageId}\0${attempt}\0${envelopeDigest}\0${input.fingerprint}`).digest('hex').slice(0, 24)}`;
|
|
65
|
-
next.routingAttempts.push({ schema: 'routing-attempt/v1', id, obligationId: input.obligationId, lineageId: input.lineageId, attempt, envelope: structuredClone(input.envelope), envelopeDigest, fingerprint: input.fingerprint, state: 'reserved' });
|
|
106
|
+
next.routingAttempts.push({ schema: 'routing-attempt/v1', id, obligationId: input.obligationId, lineageId: input.lineageId, attempt, envelope: structuredClone(input.envelope), envelopeDigest, fingerprint: input.fingerprint, state: 'reserved', reservedAt: now, ...(approval.eventReceipt ? { nativeEvidenceRequired: true } : {}), ...(resolved.state === 'resolved' && resolved.fallbackReason ? { reasonCode: resolved.fallbackReason } : {}) });
|
|
107
|
+
if (resolved.state === 'resolved' && resolved.fallbackReason) {
|
|
108
|
+
const mapping = approval.policy?.content.mappings.find(item => item.target === input.envelope.runtime.target && item.runtimeKind === input.envelope.runtime.kind);
|
|
109
|
+
if (!mapping || input.envelope.role !== 'implementer' || input.envelope.requestedProfile === 'full')
|
|
110
|
+
throw new Error('routing fallback lacks approved optimized selection');
|
|
111
|
+
const sameObligationAlreadyCounted = attempts.some(item => item.obligationId === input.obligationId && item.state === 'blocked' && item.reasonCode === resolved.fallbackReason);
|
|
112
|
+
noteRoutingIncident(next, input.envelope.runtime.target, mapping.profiles[input.envelope.requestedProfile], resolved.fallbackReason, now, true, sameObligationAlreadyCounted);
|
|
113
|
+
}
|
|
66
114
|
const lineage = next.implementationLineages.find(candidate => candidate.id === input.lineageId);
|
|
67
115
|
if (lineage !== undefined)
|
|
68
116
|
lineage.attempts = attempt;
|
|
@@ -70,25 +118,53 @@ function reserveRoutingAttempt(state, input, now) {
|
|
|
70
118
|
}
|
|
71
119
|
function observeRoutingAttempt(state, input, now) {
|
|
72
120
|
assertTimestamp(now);
|
|
73
|
-
if (!(0, types_1.isWellFormedState)(state) || !input || typeof input.attemptId !== 'string' || input.attemptId.length === 0 || typeof input.nativeAgentId !== 'string' || input.nativeAgentId.length === 0 || input.nativeAgentId.length > 128 || (input.observed !== undefined && !(0, types_1.isRoutingSelection)(input.observed)) || (input.unavailableReason !== undefined && (typeof input.unavailableReason !== 'string' || input.unavailableReason.length === 0 || input.unavailableReason.length > 128)))
|
|
121
|
+
if (!(0, types_1.isWellFormedState)(state) || !input || typeof input.attemptId !== 'string' || input.attemptId.length === 0 || typeof input.nativeAgentId !== 'string' || input.nativeAgentId.length === 0 || input.nativeAgentId.length > 128 || (input.observed !== undefined && !(0, types_1.isRoutingSelection)(input.observed)) || (input.observedBackendModel !== undefined && (typeof input.observedBackendModel !== 'string' || input.observedBackendModel.length === 0 || input.observedBackendModel.length > 128 || input.nativeEventDigest === undefined)) || (input.unavailableReason !== undefined && (typeof input.unavailableReason !== 'string' || input.unavailableReason.length === 0 || input.unavailableReason.length > 128)))
|
|
74
122
|
throw new Error('routing observation is invalid');
|
|
75
123
|
const next = clone(state);
|
|
76
124
|
const attempt = next.routingAttempts.find(item => item.id === input.attemptId);
|
|
77
125
|
if (!attempt)
|
|
78
126
|
throw new Error('routing attempt is unknown');
|
|
79
127
|
if (attempt.state === 'active') {
|
|
80
|
-
if (attempt.nativeAgentId === input.nativeAgentId && canonical(attempt.observed) === canonical(input.observed))
|
|
128
|
+
if (attempt.nativeAgentId === input.nativeAgentId && canonical(attempt.observed) === canonical(input.observed) && attempt.observedBackendModel === input.observedBackendModel)
|
|
129
|
+
return next;
|
|
130
|
+
throw new Error('routing observation is immutable');
|
|
131
|
+
}
|
|
132
|
+
if (attempt.state === 'blocked') {
|
|
133
|
+
const mismatch = input.observed !== undefined && canonical(input.observed) !== canonical(attempt.envelope.resolved);
|
|
134
|
+
const reason = mismatch ? 'SELECTION_MISMATCH' : ['PROVIDER_REJECTED', 'PROVENANCE_MISSING'].includes(input.unavailableReason ?? '') ? input.unavailableReason : undefined;
|
|
135
|
+
if (attempt.nativeAgentId === input.nativeAgentId && canonical(attempt.observed) === canonical(input.observed) && reason === attempt.reasonCode)
|
|
81
136
|
return next;
|
|
82
137
|
throw new Error('routing observation is immutable');
|
|
83
138
|
}
|
|
84
139
|
if (attempt.state !== 'reserved')
|
|
85
140
|
throw new Error('routing attempt cannot be observed');
|
|
86
|
-
|
|
141
|
+
if (input.nativeEventDigest !== undefined) {
|
|
142
|
+
if (!/^[a-f0-9]{64}$/.test(input.nativeEventDigest) || next.routingAttempts.some(item => item.id !== attempt.id
|
|
143
|
+
&& (item.nativeEventDigest === input.nativeEventDigest || (item.nativeEvidenceRequired && item.nativeAgentId === input.nativeAgentId))))
|
|
144
|
+
throw new Error('native routing event was already consumed');
|
|
145
|
+
attempt.nativeEventDigest = input.nativeEventDigest;
|
|
146
|
+
}
|
|
87
147
|
attempt.nativeAgentId = input.nativeAgentId;
|
|
88
148
|
if (input.observed)
|
|
89
149
|
attempt.observed = structuredClone(input.observed);
|
|
90
|
-
if (input.
|
|
91
|
-
attempt.
|
|
150
|
+
if (input.observedBackendModel)
|
|
151
|
+
attempt.observedBackendModel = input.observedBackendModel;
|
|
152
|
+
const mismatch = input.observed !== undefined && canonical(input.observed) !== canonical(attempt.envelope.resolved);
|
|
153
|
+
const unavailable = input.unavailableReason === 'PROVIDER_REJECTED' || input.unavailableReason === 'PROVENANCE_MISSING';
|
|
154
|
+
if (mismatch || unavailable) {
|
|
155
|
+
const reason = mismatch ? 'SELECTION_MISMATCH' : input.unavailableReason;
|
|
156
|
+
attempt.state = 'blocked';
|
|
157
|
+
attempt.verdict = 'inconclusive';
|
|
158
|
+
attempt.reasonCode = reason;
|
|
159
|
+
// Agent-supplied negative evidence may conservatively trip a breaker,
|
|
160
|
+
// but never mints or renews native capability evidence.
|
|
161
|
+
noteRoutingIncident(next, attempt.envelope.runtime.target, attempt.envelope.resolved, reason, now, false);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
attempt.state = 'active';
|
|
165
|
+
if (input.unavailableReason)
|
|
166
|
+
attempt.reasonCode = input.unavailableReason;
|
|
167
|
+
}
|
|
92
168
|
return next;
|
|
93
169
|
}
|
|
94
170
|
/** Computes, but never persists, the only permitted implementation retry.
|
|
@@ -105,12 +181,19 @@ function resolveLineageEscalation(state, lineageId, initial) {
|
|
|
105
181
|
return { profile: initial, effort: lineage?.initialEffort ?? 'medium' };
|
|
106
182
|
}
|
|
107
183
|
const last = attempts[attempts.length - 1];
|
|
184
|
+
if (last.envelope.effectiveProfile === 'full')
|
|
185
|
+
throw new Error('routing full fallback is exhausted');
|
|
186
|
+
if (last.verdict === 'inconclusive' && ['PROVIDER_REJECTED', 'SELECTION_MISMATCH', 'PROVENANCE_MISSING'].includes(last.reasonCode ?? '')) {
|
|
187
|
+
const lineage = state.implementationLineages?.find(candidate => candidate.id === lineageId);
|
|
188
|
+
return { profile: initial, effort: lineage?.initialEffort ?? 'medium' };
|
|
189
|
+
}
|
|
108
190
|
if (last.verdict !== 'fail')
|
|
109
191
|
throw new Error('routing lineage advances only after a terminal failed verdict');
|
|
192
|
+
const routeEffort = last.envelope.resolved.effort.kind === 'runtime-default' ? 'runtime-default' : 'medium';
|
|
110
193
|
if (last.envelope.effectiveProfile === 'mechanical')
|
|
111
|
-
return { profile: 'integration', effort:
|
|
194
|
+
return { profile: 'integration', effort: routeEffort };
|
|
112
195
|
if (last.envelope.effectiveProfile === 'integration')
|
|
113
|
-
return { profile: 'judgment', effort:
|
|
196
|
+
return { profile: 'judgment', effort: routeEffort };
|
|
114
197
|
if (last.envelope.effectiveProfile === 'judgment' && last.envelope.resolved.effort.kind === 'explicit' && last.envelope.resolved.effort.value === 'medium')
|
|
115
198
|
return { profile: 'judgment', effort: 'high' };
|
|
116
199
|
throw new Error('routing lineage escalation exhausted');
|
|
@@ -121,6 +204,7 @@ function routingReport(state) {
|
|
|
121
204
|
const attempts = state.routingAttempts ?? [];
|
|
122
205
|
const plannedByRole = {};
|
|
123
206
|
const actualByRole = {};
|
|
207
|
+
const nativeReconciledByRole = {};
|
|
124
208
|
const byState = {};
|
|
125
209
|
const unavailable = {};
|
|
126
210
|
const verdicts = {};
|
|
@@ -128,11 +212,22 @@ function routingReport(state) {
|
|
|
128
212
|
plannedByRole[attempt.envelope.role] = (plannedByRole[attempt.envelope.role] ?? 0) + 1;
|
|
129
213
|
if (attempt.nativeAgentId !== undefined)
|
|
130
214
|
actualByRole[attempt.envelope.role] = (actualByRole[attempt.envelope.role] ?? 0) + 1;
|
|
215
|
+
if (attempt.nativeEventDigest !== undefined && ['active', 'complete'].includes(attempt.state))
|
|
216
|
+
nativeReconciledByRole[attempt.envelope.role] = (nativeReconciledByRole[attempt.envelope.role] ?? 0) + 1;
|
|
131
217
|
byState[attempt.state] = (byState[attempt.state] ?? 0) + 1;
|
|
132
218
|
for (const reason of attempt.envelope.unavailableEvidence)
|
|
133
219
|
unavailable[reason] = (unavailable[reason] ?? 0) + 1;
|
|
220
|
+
if (attempt.envelope.role === 'implementer' && attempt.envelope.effectiveProfile === 'full' && attempt.reasonCode)
|
|
221
|
+
unavailable[attempt.reasonCode] = (unavailable[attempt.reasonCode] ?? 0) + 1;
|
|
134
222
|
if (attempt.verdict !== undefined)
|
|
135
223
|
verdicts[attempt.verdict] = (verdicts[attempt.verdict] ?? 0) + 1;
|
|
136
224
|
}
|
|
137
|
-
|
|
225
|
+
const incidents = (state.routingIncidents ?? []).map(({ reasonCode, selection, affectedObligations, fallbackCount, blockedCount, alertState }) => ({ reasonCode, selection, affectedObligations, fallbackCount, blockedCount, alertState }));
|
|
226
|
+
const reconciled = Object.values(nativeReconciledByRole).reduce((total, count) => total + count, 0);
|
|
227
|
+
const selections = attempts.map(attempt => ({ role: attempt.envelope.role, configured: attempt.envelope.resolved,
|
|
228
|
+
accepted: attempt.nativeEventDigest && ['active', 'complete'].includes(attempt.state) ? attempt.observed ?? 'unknown' : 'unknown',
|
|
229
|
+
backendModel: attempt.nativeEventDigest && ['active', 'complete'].includes(attempt.state) ? attempt.observedBackendModel ?? 'unknown' : 'unknown' }));
|
|
230
|
+
return { schema: 'routing-report/v1', attempts: attempts.length, plannedByRole, actualByRole, selections,
|
|
231
|
+
...(reconciled ? { nativeReconciledByRole } : {}), byState, retries: attempts.filter((attempt) => attempt.attempt > 1).length, fallbacks: attempts.filter((attempt) => attempt.envelope.role === 'implementer' && attempt.envelope.effectiveProfile === 'full' && attempt.envelope.requestedProfile !== 'full').length, unavailable, verdicts, administrativeRepairs: attempts.filter((attempt) => attempt.administrativeRepair === true).length,
|
|
232
|
+
measurement: { configuredSelection: 'envelope-recorded', acceptedSelection: reconciled === 0 ? 'not-natively-reconciled' : reconciled === attempts.length ? 'natively-reconciled' : 'partially-reconciled', backendModel: selections.some(item => item.backendModel !== 'unknown') ? 'provider-reported' : 'unknown', tokenUsage: 'unknown', savings: 'unverified' }, ...(incidents.length ? { incidents } : {}) };
|
|
138
233
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
exports.queryLocalClaudeScope = queryLocalClaudeScope;
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const paths_1 = require("../paths");
|
|
10
|
+
const machine_key_1 = require("./machine-key");
|
|
11
|
+
const native_claude_machine_1 = require("./native-claude-machine");
|
|
12
|
+
const native_claude_config_1 = require("./native-claude-config");
|
|
13
|
+
const native_runtime_1 = require("./native-runtime");
|
|
14
|
+
const hook_diagnostics_1 = require("./hook-diagnostics");
|
|
15
|
+
/** Read-only local facts; no model inference or machine-key creation. */
|
|
16
|
+
async function queryLocalClaudeScope(cwd, runtimeKind, deps = {}) {
|
|
17
|
+
if (typeof cwd !== 'string' || !path_1.default.isAbsolute(cwd) || cwd.includes('\0') || typeof runtimeKind !== 'string'
|
|
18
|
+
|| !/^[a-z0-9][a-z0-9_-]{0,127}$/.test(runtimeKind))
|
|
19
|
+
throw new Error('local Claude scope input is invalid');
|
|
20
|
+
const hooks = (deps.readRoutingHookStatus ?? hook_diagnostics_1.readRoutingHookStatus)();
|
|
21
|
+
if (deps.requireHookHealthy !== false && (hooks.state === 'failed' || hooks.state === 'invalid'))
|
|
22
|
+
return { state: 'unverified', reason: 'HOOK_CAPTURE_FAILED' };
|
|
23
|
+
const key = (deps.readMachineKey ?? machine_key_1.readMachineKey)((0, paths_1.awmHome)());
|
|
24
|
+
if (!key)
|
|
25
|
+
return { state: 'unverified', reason: 'MACHINE_KEY_MISSING' };
|
|
26
|
+
try {
|
|
27
|
+
const [auth, runtime] = await Promise.all([
|
|
28
|
+
(deps.queryClaudeAuthStatus ?? native_claude_machine_1.queryClaudeAuthStatus)({ command: 'claude', args: ['auth', 'status'], timeoutMs: 10000, key }),
|
|
29
|
+
(deps.queryRuntimeExecutable ?? native_runtime_1.queryRuntimeExecutable)({ command: 'claude', args: ['--version'], timeoutMs: 10000 }),
|
|
30
|
+
]);
|
|
31
|
+
if (auth.accountState !== 'identified' || !auth.accountScopeDigest)
|
|
32
|
+
return { state: 'unverified', reason: 'ACCOUNT_UNVERIFIED' };
|
|
33
|
+
const configDigest = (deps.readClaudeModelConfigDigest ?? native_claude_config_1.readClaudeModelConfigDigest)({ cwd, configDir: path_1.default.resolve(process.env.CLAUDE_CONFIG_DIR ?? path_1.default.join(os_1.default.homedir(), '.claude')),
|
|
34
|
+
key, environment: process.env });
|
|
35
|
+
return { state: 'current', scope: { runtime: { target: 'claude-code', kind: runtimeKind, version: runtime.version,
|
|
36
|
+
accountScopeDigest: auth.accountScopeDigest }, binaryDigest: runtime.binaryDigest, configDigest } };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { state: 'unverified', reason: 'NATIVE_QUERY_FAILED' };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
exports.queryLocalCodexScope = queryLocalCodexScope;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const paths_1 = require("../paths");
|
|
9
|
+
const machine_key_1 = require("./machine-key");
|
|
10
|
+
const native_codex_machine_1 = require("./native-codex-machine");
|
|
11
|
+
const native_runtime_1 = require("./native-runtime");
|
|
12
|
+
/** Read-only, no-token local facts for the same cwd as the routed work. */
|
|
13
|
+
async function queryLocalCodexScope(cwd, runtimeKind, deps = {}) {
|
|
14
|
+
if (typeof cwd !== 'string' || !path_1.default.isAbsolute(cwd) || cwd.includes('\0') || typeof runtimeKind !== 'string'
|
|
15
|
+
|| !/^[a-z0-9][a-z0-9_-]{0,127}$/.test(runtimeKind))
|
|
16
|
+
throw new Error('local Codex scope input is invalid');
|
|
17
|
+
const key = (deps.readMachineKey ?? machine_key_1.readMachineKey)((0, paths_1.awmHome)());
|
|
18
|
+
if (!key)
|
|
19
|
+
return { state: 'unverified', reason: 'MACHINE_KEY_MISSING' };
|
|
20
|
+
try {
|
|
21
|
+
const [machine, executable] = await Promise.all([
|
|
22
|
+
(deps.queryCodexMachineFacts ?? native_codex_machine_1.queryCodexMachineFacts)({ command: 'codex', args: ['app-server', '--stdio'], timeoutMs: 10000, cwd, key }),
|
|
23
|
+
(deps.queryRuntimeExecutable ?? native_runtime_1.queryRuntimeExecutable)({ command: 'codex', args: ['--version'], timeoutMs: 10000 }),
|
|
24
|
+
]);
|
|
25
|
+
if (machine.accountState !== 'identified' || !machine.accountScopeDigest)
|
|
26
|
+
return { state: 'unverified', reason: 'ACCOUNT_UNVERIFIED' };
|
|
27
|
+
return { state: 'current', scope: { runtime: { target: 'codex', kind: runtimeKind, version: executable.version,
|
|
28
|
+
accountScopeDigest: machine.accountScopeDigest }, binaryDigest: executable.binaryDigest, configDigest: machine.configDigest } };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { state: 'unverified', reason: 'NATIVE_QUERY_FAILED' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.queryLocalEventScope = queryLocalEventScope;
|
|
4
|
+
const local_claude_scope_1 = require("./local-claude-scope");
|
|
5
|
+
const local_codex_scope_1 = require("./local-codex-scope");
|
|
6
|
+
/** Provider-specific, read-only scope discovery for dispatch-time drift checks. */
|
|
7
|
+
async function queryLocalEventScope(cwd, target, runtimeKind) {
|
|
8
|
+
if (target === 'codex')
|
|
9
|
+
return (0, local_codex_scope_1.queryLocalCodexScope)(cwd, runtimeKind);
|
|
10
|
+
if (target === 'claude-code')
|
|
11
|
+
return (0, local_claude_scope_1.queryLocalClaudeScope)(cwd, runtimeKind);
|
|
12
|
+
return { state: 'unverified', reason: 'PROVIDER_UNSUPPORTED' };
|
|
13
|
+
}
|