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
|
@@ -4,6 +4,7 @@ exports.Supervisor = exports.MAX_SENSOR_DEFERRALS = exports.DEFAULT_SUPERVISOR_C
|
|
|
4
4
|
exports.routingFacts = routingFacts;
|
|
5
5
|
exports.defaultDispatchAdmission = defaultDispatchAdmission;
|
|
6
6
|
exports.admissionForConfig = admissionForConfig;
|
|
7
|
+
exports.recoveryWhitelistBlocker = recoveryWhitelistBlocker;
|
|
7
8
|
exports.runSupervisorLoop = runSupervisorLoop;
|
|
8
9
|
// Loop foreground (R4.4/R4.5): tick = apply -> collect/spawn -> stall -> gate.
|
|
9
10
|
// COMPLETE exige gate verde (que exige cero vivos): drenaje ANTES de declarar.
|
|
@@ -15,6 +16,9 @@ const registry_contracts_1 = require("../../core/admission/registry-contracts");
|
|
|
15
16
|
const config_1 = require("../../utils/config");
|
|
16
17
|
const store_2 = require("../../core/model-policy/store");
|
|
17
18
|
const capabilities_1 = require("../../core/model-policy/capabilities");
|
|
19
|
+
const event_store_1 = require("../../core/model-policy/event-store");
|
|
20
|
+
const local_event_scope_1 = require("../../core/model-policy/local-event-scope");
|
|
21
|
+
const journal_1 = require("../../core/model-policy/journal");
|
|
18
22
|
const adapter_1 = require("../../core/journal/adapter");
|
|
19
23
|
const process_1 = require("../../core/journal/process");
|
|
20
24
|
const gate_1 = require("../job/gate");
|
|
@@ -24,6 +28,7 @@ const apply_1 = require("./apply");
|
|
|
24
28
|
const runner_1 = require("./runner");
|
|
25
29
|
const tracks_1 = require("./tracks");
|
|
26
30
|
const generations_1 = require("./generations");
|
|
31
|
+
const types_1 = require("../../core/journal/types");
|
|
27
32
|
/** #166: compact v2 admission requires the runtime identity, and the supervisor
|
|
28
33
|
* never supplied it — so `awm watch` blocked on every tick with
|
|
29
34
|
* ADMISSION_ROUTING_FACTS_REQUIRED even with every other gate green.
|
|
@@ -56,10 +61,39 @@ function defaultDispatchAdmission(repoRoot, branch, provider, identity, deps = {
|
|
|
56
61
|
}
|
|
57
62
|
const plan = (0, validate_1.validatePlanFile)(binding.path, repoRoot);
|
|
58
63
|
const preferences = (0, config_1.readPreferences)();
|
|
64
|
+
let routing;
|
|
65
|
+
if ((provider === 'codex' || provider === 'claude-code') && identity) {
|
|
66
|
+
const runtime = (0, capabilities_1.validateRuntimeKey)({ target: provider, kind: identity.kind, version: identity.version, accountScopeDigest: identity.accountScopeDigest });
|
|
67
|
+
const stored = (deps.readStoredEventReceipt ?? event_store_1.readStoredEventReceipt)(runtime);
|
|
68
|
+
if (stored.state === 'absent')
|
|
69
|
+
routing = routingFacts(provider, identity, repoRoot, deps);
|
|
70
|
+
else {
|
|
71
|
+
const at = deps.now?.() ?? new Date();
|
|
72
|
+
if (!(at instanceof Date) || !Number.isFinite(at.getTime()))
|
|
73
|
+
throw new Error('routing observation time must be a finite Date');
|
|
74
|
+
const policy = (deps.readEffectivePolicy ?? store_2.readEffectivePolicy)(repoRoot);
|
|
75
|
+
routing = { runtime, policy: policy.state === 'approved' ? policy.policy : undefined, now: at };
|
|
76
|
+
if (stored.state === 'invalid')
|
|
77
|
+
routing.eventIssue = { code: 'ROUTING_CAPABILITY_INVALID', message: `Sealed native event receipt is invalid; run awm model-policy setup --provider ${provider}.` };
|
|
78
|
+
else {
|
|
79
|
+
const local = await (provider === 'codex' && deps.queryLocalCodexScope ? deps.queryLocalCodexScope(repoRoot, identity.kind) : (0, local_event_scope_1.queryLocalEventScope)(repoRoot, provider, identity.kind));
|
|
80
|
+
if (local.state === 'current' && JSON.stringify(local.scope.runtime) === JSON.stringify(runtime)) {
|
|
81
|
+
routing.eventReceipt = stored.receipt;
|
|
82
|
+
routing.eventScope = local.scope;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const reason = local.state === 'unverified' ? local.reason : 'RUNTIME_IDENTITY_MISMATCH';
|
|
86
|
+
routing.eventIssue = { code: `ROUTING_${reason}`, message: `${provider} runtime/account scope changed or cannot be verified (${reason}); run awm model-policy setup --provider ${provider} --json.` };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else
|
|
92
|
+
routing = routingFacts(provider, identity, repoRoot, deps);
|
|
59
93
|
return (deps.admit ?? registry_contracts_1.admitRegistryPlan)({ plan, provider, cwd: repoRoot, enabledAgents: preferences.enabledAgents,
|
|
60
94
|
executionMode: 'desatendido', requireCurrent: true, verifySensors: true,
|
|
61
95
|
journalState: observed.state, journalCorrupt: observed.corrupt, planPath: binding.path,
|
|
62
|
-
routing
|
|
96
|
+
routing,
|
|
63
97
|
controllerAutonomy: autonomy });
|
|
64
98
|
};
|
|
65
99
|
}
|
|
@@ -169,9 +203,11 @@ function admissionForConfig(repoRoot, branch, cfg, deps = {}) {
|
|
|
169
203
|
exports.MAX_SENSOR_DEFERRALS = 3;
|
|
170
204
|
const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
|
|
171
205
|
function recoveryWhitelistBlocker(state) {
|
|
172
|
-
if (state.cycle.status === 'BLOCKED')
|
|
173
|
-
return '
|
|
174
|
-
|
|
206
|
+
if (state.cycle.status === 'BLOCKED') {
|
|
207
|
+
return state.cycle.blockedReason === 'recovery no autorizado: hay conflictos durables de requests'
|
|
208
|
+
? 'hay conflictos durables de requests' : 'el ciclo está bloqueado';
|
|
209
|
+
}
|
|
210
|
+
if ((0, types_1.activeRequestProblems)(state).length > 0)
|
|
175
211
|
return 'hay conflictos durables de requests';
|
|
176
212
|
// Pending work is the normal reason to launch (or retry launching) the
|
|
177
213
|
// controller. It is not recovery evidence and therefore cannot turn a
|
|
@@ -414,7 +450,31 @@ class Supervisor {
|
|
|
414
450
|
if (r0.corrupt || r0.state === null)
|
|
415
451
|
throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
|
|
416
452
|
const gen = (0, generations_1.activeGeneration)(r0.state);
|
|
417
|
-
|
|
453
|
+
let eventScope;
|
|
454
|
+
if ((this.cfg.provider === 'codex' || this.cfg.provider === 'claude-code') && this.cfg.routingIdentity) {
|
|
455
|
+
const asserted = (0, capabilities_1.validateRuntimeKey)({ target: this.cfg.provider, ...this.cfg.routingIdentity });
|
|
456
|
+
const event = (0, event_store_1.readStoredEventReceipt)(asserted);
|
|
457
|
+
if (event.state === 'present') {
|
|
458
|
+
const local = await (0, local_event_scope_1.queryLocalEventScope)(this.repoRoot, asserted.target, asserted.kind);
|
|
459
|
+
if (local.state === 'current' && JSON.stringify(local.scope.runtime) === JSON.stringify(asserted))
|
|
460
|
+
eventScope = local.scope;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
(0, apply_1.consumePendingRequests)(this.repoRoot, this.branch, gen?.token ?? null, eventScope);
|
|
464
|
+
const alertRead = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
465
|
+
if (alertRead.corrupt || !alertRead.state)
|
|
466
|
+
throw new Error('routing alert cannot read durable journal');
|
|
467
|
+
const pendingAlerts = alertRead.state.routingIncidents?.filter(item => item.alertState === 'pending') ?? [];
|
|
468
|
+
if (pendingAlerts.length > 0) {
|
|
469
|
+
for (const incident of pendingAlerts) {
|
|
470
|
+
const alert = { id: incident.id, reasonCode: incident.reasonCode, selection: incident.selection,
|
|
471
|
+
fallbackCount: incident.fallbackCount, remedy: `awm model-policy setup --provider ${incident.target} --json` };
|
|
472
|
+
process.stderr.write(`AWM routing alert ${JSON.stringify(alert)}\n`);
|
|
473
|
+
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'routing-incident', ...alert });
|
|
474
|
+
incident.alertState = 'reported';
|
|
475
|
+
}
|
|
476
|
+
(0, store_1.writeJournal)(this.repoRoot, this.branch, alertRead.state);
|
|
477
|
+
}
|
|
418
478
|
// P1/P2 (R4.1-R4.10): a lo sumo un side effect de bootstrap de tracks
|
|
419
479
|
// por tick, ANTES de tocar jobs — mientras la cohorte está PREPARING,
|
|
420
480
|
// ningún job de track se despacha (eso lo maneja `runnerTick` con los
|
|
@@ -674,8 +734,12 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
|
|
|
674
734
|
// 'frozen' (Task 10): mismo camino de salida que 'complete' —
|
|
675
735
|
// drenar ownership y liberar el lock, el track ya cumplió su
|
|
676
736
|
// freeze y no debe seguir despachando ni corriendo su loop.
|
|
677
|
-
if (out === 'complete' || out === 'frozen')
|
|
737
|
+
if (out === 'complete' || out === 'frozen') {
|
|
738
|
+
const finalState = (0, store_1.readJournal)(repoRoot, branch);
|
|
739
|
+
if (!finalState.corrupt && finalState.state?.routingIncidents?.length)
|
|
740
|
+
process.stderr.write(`AWM final routing summary ${JSON.stringify((0, journal_1.routingReport)(finalState.state))}\n`);
|
|
678
741
|
break;
|
|
742
|
+
}
|
|
679
743
|
// 'custody': NO liberar lock, NO salir — seguir auditando (R4.5)
|
|
680
744
|
await new Promise((resolve) => {
|
|
681
745
|
let settled = false;
|
|
@@ -246,20 +246,24 @@ async function admitPlan(input) {
|
|
|
246
246
|
: diagnostic('ADMISSION_CONTROLLER_AUTONOMY_REQUIRED', `Unattended dispatch on ${provider} requires an explicit controller autonomy posture; without it the controller blocks on its first tool call.`);
|
|
247
247
|
return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution });
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
const routing = plan.schema === 'compact-slices/v2' ? input.routing ?? await input.routingReader?.() : input.routing;
|
|
250
|
+
return completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution, cliCurrentness);
|
|
250
251
|
}
|
|
251
252
|
const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
|
|
252
253
|
const resolution = { outcome: capabilities.interactiveExecution === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
253
254
|
if (resolution.outcome === 'blocked')
|
|
254
255
|
return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified interactive execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, capabilityResolution: resolution });
|
|
255
|
-
|
|
256
|
+
const routing = plan.schema === 'compact-slices/v2' ? input.routing ?? await input.routingReader?.() : input.routing;
|
|
257
|
+
return completeAdmission({ ...input, routing, routingReader: undefined }, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution, cliCurrentness);
|
|
256
258
|
}
|
|
257
259
|
function completeAdmission(input, plan, provider, executionMode, journal, currentness, sensors, capabilityResolution, cliCurrentness = 'not-checked') {
|
|
258
260
|
if (plan.schema === 'compact-slices/v1')
|
|
259
261
|
return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution, forecast: forecast(plan.manifest.slices.length), diagnostics: [] };
|
|
260
|
-
const routing = input.routing
|
|
261
|
-
if (
|
|
262
|
-
return blocked(input, [diagnostic(
|
|
262
|
+
const routing = input.routing;
|
|
263
|
+
if (routing?.eventIssue)
|
|
264
|
+
return blocked(input, [diagnostic(routing.eventIssue.code, routing.eventIssue.message)], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
265
|
+
if (!routing?.runtime || !routing.policy || (!routing.capabilities && !(routing.eventReceipt && routing.eventScope)))
|
|
266
|
+
return blocked(input, [diagnostic('ADMISSION_ROUTING_FACTS_REQUIRED', 'Compact v2 requires an approved policy, current capability evidence, and runtime identity.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
263
267
|
let runtime;
|
|
264
268
|
try {
|
|
265
269
|
runtime = (0, capabilities_1.validateRuntimeKey)(routing.runtime);
|
|
@@ -269,12 +273,22 @@ function completeAdmission(input, plan, provider, executionMode, journal, curren
|
|
|
269
273
|
}
|
|
270
274
|
if (runtime.target !== provider)
|
|
271
275
|
return blocked(input, [diagnostic('ADMISSION_ROUTING_PROVIDER_MISMATCH', 'Compact v2 routing runtime target must exactly match the admitted provider.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
276
|
+
if (routing.eventReceipt && routing.eventScope && JSON.stringify(runtime) !== JSON.stringify(routing.eventScope.runtime))
|
|
277
|
+
return blocked(input, [diagnostic('ADMISSION_ROUTING_RUNTIME_MISMATCH', 'Event-scoped routing facts do not match the admitted runtime.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
272
278
|
const now = routing.now ?? new Date();
|
|
273
279
|
for (const slice of plan.manifest.slices) {
|
|
274
|
-
const
|
|
280
|
+
const profile = slice.implementerProfile;
|
|
281
|
+
const resolved = routing.eventReceipt && routing.eventScope
|
|
282
|
+
? (0, resolve_1.resolveEventWithFallback)({ role: 'implementer', requestedProfile: profile, policy: routing.policy, receipt: routing.eventReceipt, scope: routing.eventScope, now })
|
|
283
|
+
: (0, resolve_1.resolveSelection)({ role: 'implementer', requestedProfile: profile, policy: routing.policy, capabilities: routing.capabilities, runtime, now });
|
|
275
284
|
if (resolved.state === 'blocked')
|
|
276
285
|
return blocked(input, resolved.diagnostics, { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
277
286
|
}
|
|
287
|
+
if (routing.eventReceipt && routing.eventScope) {
|
|
288
|
+
const full = (0, resolve_1.resolveEventSelection)({ role: 'code-quality-reviewer', requestedProfile: 'full', policy: routing.policy, receipt: routing.eventReceipt, scope: routing.eventScope, now });
|
|
289
|
+
if (full.state === 'blocked')
|
|
290
|
+
return blocked(input, full.diagnostics, { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
291
|
+
}
|
|
278
292
|
return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution, forecast: forecast(plan.manifest.slices.length), routingForecast: routingForecast(plan, routing), diagnostics: [] };
|
|
279
293
|
}
|
|
280
294
|
// Structural exhaustiveness guard: adding an AgentTarget requires an explicit entry above.
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.digestOf = digestOf;
|
|
7
7
|
exports.emitRequest = emitRequest;
|
|
8
|
+
exports.isWellFormedEnvelope = isWellFormedEnvelope;
|
|
8
9
|
exports.listPendingRequests = listPendingRequests;
|
|
9
10
|
exports.applyOutcome = applyOutcome;
|
|
10
11
|
exports.ackFor = ackFor;
|
|
@@ -6,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
};
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.GENERATION_STATES = exports.EXECUTION_STATES = void 0;
|
|
9
|
+
exports.activeRequestProblems = activeRequestProblems;
|
|
9
10
|
exports.isControllerRecoveryAction = isControllerRecoveryAction;
|
|
10
11
|
exports.emptyState = emptyState;
|
|
11
12
|
exports.isWellFormedState = isWellFormedState;
|
|
@@ -14,6 +15,7 @@ exports.isRoutingEnvelope = isRoutingEnvelope;
|
|
|
14
15
|
exports.isWellFormedProcessRef = isWellFormedProcessRef;
|
|
15
16
|
exports.isWellFormedJob = isWellFormedJob;
|
|
16
17
|
const crypto_1 = __importDefault(require("crypto"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
17
19
|
const types_1 = require("../tracks/types");
|
|
18
20
|
const paths_1 = require("./paths");
|
|
19
21
|
exports.EXECUTION_STATES = [
|
|
@@ -35,6 +37,19 @@ const COHORT_PHASES = [
|
|
|
35
37
|
];
|
|
36
38
|
const _cohortPhasesComplete = true;
|
|
37
39
|
const VERIFICATION_KINDS = ['test', 'lint', 'sensors', 'review', 'qa', 'interlock', 'track-integration'];
|
|
40
|
+
function activeRequestProblems(state) {
|
|
41
|
+
return state.requestProblems.filter(problem => {
|
|
42
|
+
const resolution = problem.resolution;
|
|
43
|
+
if (!resolution || !problem.requestId || !problem.taskId || !problem.generationToken
|
|
44
|
+
|| path_1.default.basename(problem.file) !== `${problem.requestId}.json`
|
|
45
|
+
|| resolution.generationToken !== problem.generationToken)
|
|
46
|
+
return true;
|
|
47
|
+
const applied = state.appliedRequests[resolution.replacementRequestId];
|
|
48
|
+
return applied?.outcome !== 'applied' || applied.resultRef !== problem.taskId
|
|
49
|
+
|| applied.payloadDigest !== resolution.replacementPayloadDigest
|
|
50
|
+
|| !state.tasks.some(task => task.id === problem.taskId);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
38
53
|
function isControllerRecoveryAction(x) {
|
|
39
54
|
return isObj(x) && Object.keys(x).length === 2
|
|
40
55
|
&& x.schema === 'controller-recovery/v1'
|
|
@@ -94,6 +109,8 @@ function isWellFormedState(x) {
|
|
|
94
109
|
return false;
|
|
95
110
|
if (x.routingAttempts !== undefined && (!Array.isArray(x.routingAttempts) || x.routingAttempts.length > 4096 || !x.routingAttempts.every(isWellFormedRoutingAttempt)))
|
|
96
111
|
return false;
|
|
112
|
+
if (x.routingIncidents !== undefined && (!Array.isArray(x.routingIncidents) || x.routingIncidents.length > 4096 || !x.routingIncidents.every(isWellFormedRoutingIncident)))
|
|
113
|
+
return false;
|
|
97
114
|
if (x.implementationLineages !== undefined && (!Array.isArray(x.implementationLineages) || x.implementationLineages.length > 4096 || !x.implementationLineages.every(isWellFormedImplementationLineage)))
|
|
98
115
|
return false;
|
|
99
116
|
if (!Array.isArray(x.requestProblems) || !x.requestProblems.every(isWellFormedRequestProblem))
|
|
@@ -211,20 +228,47 @@ function isWellFormedAppliedRequest(x) {
|
|
|
211
228
|
&& (x.resultRef === undefined || typeof x.resultRef === 'string');
|
|
212
229
|
}
|
|
213
230
|
function isWellFormedRoutingAttempt(x) {
|
|
214
|
-
return isObj(x) && Object.keys(x).every(key => ['schema', 'id', 'obligationId', 'lineageId', 'attempt', 'envelope', 'envelopeDigest', 'fingerprint', 'state', 'nativeAgentId', 'observed', 'verdict', 'reasonCode', 'administrativeRepair'].includes(key))
|
|
231
|
+
return isObj(x) && Object.keys(x).every(key => ['schema', 'id', 'obligationId', 'lineageId', 'attempt', 'envelope', 'envelopeDigest', 'fingerprint', 'state', 'reservedAt', 'nativeEvidenceRequired', 'nativeEventDigest', 'nativeAgentId', 'observed', 'observedBackendModel', 'verdict', 'reasonCode', 'administrativeRepair'].includes(key))
|
|
215
232
|
&& x.schema === 'routing-attempt/v1'
|
|
216
233
|
&& typeof x.id === 'string' && typeof x.obligationId === 'string' && typeof x.lineageId === 'string' && Number.isSafeInteger(x.attempt) && x.attempt > 0
|
|
217
234
|
&& typeof x.envelopeDigest === 'string' && /^[a-f0-9]{64}$/.test(x.envelopeDigest) && typeof x.fingerprint === 'string' && /^[a-f0-9]{64}$/.test(x.fingerprint)
|
|
218
|
-
&& ['reserved', 'active', 'complete', 'blocked', 'unknown'].includes(String(x.state)) && (x.nativeAgentId === undefined || typeof x.nativeAgentId === 'string') && (x.observed === undefined || isRoutingSelection(x.observed)) && (x.verdict === undefined || ['pass', 'fail', 'inconclusive'].includes(String(x.verdict))) && (x.reasonCode === undefined || typeof x.reasonCode === 'string') && (x.administrativeRepair === undefined || x.administrativeRepair === true) && isRoutingEnvelope(x.envelope);
|
|
235
|
+
&& ['reserved', 'active', 'complete', 'blocked', 'unknown'].includes(String(x.state)) && (x.reservedAt === undefined || (typeof x.reservedAt === 'string' && Number.isFinite(Date.parse(x.reservedAt)))) && (x.nativeEvidenceRequired === undefined || x.nativeEvidenceRequired === true) && (x.nativeEventDigest === undefined || isDigest(x.nativeEventDigest)) && (x.nativeAgentId === undefined || typeof x.nativeAgentId === 'string') && (x.observed === undefined || isRoutingSelection(x.observed)) && (x.observedBackendModel === undefined || (typeof x.observedBackendModel === 'string' && x.observedBackendModel.length > 0 && x.observedBackendModel.length <= 128)) && (x.verdict === undefined || ['pass', 'fail', 'inconclusive'].includes(String(x.verdict))) && (x.reasonCode === undefined || typeof x.reasonCode === 'string') && (x.administrativeRepair === undefined || x.administrativeRepair === true) && isRoutingEnvelope(x.envelope);
|
|
236
|
+
}
|
|
237
|
+
function isWellFormedRoutingIncident(x) {
|
|
238
|
+
if (!isObj(x) || Object.keys(x).length !== 12 || !Object.keys(x).every(key => ['schema', 'id', 'runStartedAt', 'target', 'selection', 'reasonCode', 'firstAt', 'lastAt', 'affectedObligations', 'fallbackCount', 'blockedCount', 'alertState'].includes(key)))
|
|
239
|
+
return false;
|
|
240
|
+
return x.schema === 'routing-incident/v1' && typeof x.id === 'string' && /^[a-f0-9]{24}$/.test(x.id)
|
|
241
|
+
&& typeof x.runStartedAt === 'string' && Number.isFinite(Date.parse(x.runStartedAt))
|
|
242
|
+
&& typeof x.target === 'string' && x.target.length > 0 && x.target.length <= 128
|
|
243
|
+
&& isRoutingSelection(x.selection) && typeof x.reasonCode === 'string' && /^[A-Z0-9_]{3,64}$/.test(x.reasonCode)
|
|
244
|
+
&& typeof x.firstAt === 'string' && Number.isFinite(Date.parse(x.firstAt))
|
|
245
|
+
&& typeof x.lastAt === 'string' && Number.isFinite(Date.parse(x.lastAt))
|
|
246
|
+
&& Number.isSafeInteger(x.affectedObligations) && x.affectedObligations > 0
|
|
247
|
+
&& Number.isSafeInteger(x.fallbackCount) && x.fallbackCount >= 0
|
|
248
|
+
&& Number.isSafeInteger(x.blockedCount) && x.blockedCount >= 0
|
|
249
|
+
&& ['pending', 'reported'].includes(String(x.alertState));
|
|
219
250
|
}
|
|
220
251
|
function isDigest(value) { return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value); }
|
|
221
252
|
function isRoutingSelection(x) { return isObj(x) && Object.keys(x).every(key => key === 'selector' || key === 'effort') && isObj(x.selector) && Object.keys(x.selector).every(key => key === 'kind' || key === 'id') && ['model', 'tier'].includes(String(x.selector.kind)) && typeof x.selector.id === 'string' && x.selector.id.length > 0 && x.selector.id.length <= 128 && isObj(x.effort) && ((x.effort.kind === 'runtime-default' && Object.keys(x.effort).length === 1) || (x.effort.kind === 'explicit' && Object.keys(x.effort).length === 2 && typeof x.effort.value === 'string' && x.effort.value.length > 0 && x.effort.value.length <= 128)); }
|
|
222
|
-
function isRoutingEnvelope(x) { if (!isObj(x) || !Object.keys(x).every(key => ['schema', 'runtime', 'role', 'planDigest', 'executionDigest', 'sliceId', 'requestedProfile', 'effectiveProfile', 'resolved', 'policyDigest', 'capabilityDigest', 'outcome', 'unavailableEvidence'].includes(key)))
|
|
223
|
-
return false; const runtime = x.runtime; return x.schema === 'routing-envelope/v1' && isObj(runtime) && Object.keys(runtime).every(key => ['target', 'kind', 'version', 'accountScopeDigest'].includes(key)) && ['codex', 'claude-code', 'cursor', 'copilot', 'opencode', 'antigravity'].includes(String(runtime.target)) && typeof runtime.kind === 'string' && runtime.kind.length > 0 && runtime.kind.length <= 128 && typeof runtime.version === 'string' && runtime.version.length > 0 && runtime.version.length <= 128 && isDigest(runtime.accountScopeDigest) && typeof x.role === 'string' && ROUTING_ROLES.includes(x.role) && isDigest(x.planDigest) && isDigest(x.executionDigest) && (x.sliceId === undefined || (typeof x.sliceId === 'string' && x.sliceId.length > 0 && x.sliceId.length <= 128)) && ['mechanical', 'integration', 'judgment', 'full'].includes(String(x.requestedProfile)) && ['mechanical', 'integration', 'judgment', 'full'].includes(String(x.effectiveProfile)) && isRoutingSelection(x.resolved) && isDigest(x.policyDigest) && isDigest(x.capabilityDigest) && ['native', 'degraded'].includes(String(x.outcome)) && Array.isArray(x.unavailableEvidence) && x.unavailableEvidence.length <= UNAVAILABLE_EVIDENCE.length && x.unavailableEvidence.every(value => typeof value === 'string' && UNAVAILABLE_EVIDENCE.includes(value)); }
|
|
224
|
-
function isWellFormedImplementationLineage(x) { return isObj(x) && Object.keys(x).every(key => ['id', 'obligationId', 'sliceId', 'planDigest', 'executionDigest', 'initialProfile', 'initialEffort', 'attempts'].includes(key)) && typeof x.id === 'string' && x.id.length > 0 && typeof x.obligationId === 'string' && x.obligationId.length > 0 && typeof x.sliceId === 'string' && x.sliceId.length > 0 && isDigest(x.planDigest) && isDigest(x.executionDigest) && ['mechanical', 'integration', 'judgment'].includes(String(x.initialProfile)) && ['medium', 'high'].includes(String(x.initialEffort)) && Number.isSafeInteger(x.attempts) && x.attempts >= 0 && x.attempts <= 3; }
|
|
253
|
+
function isRoutingEnvelope(x) { if (!isObj(x) || !Object.keys(x).every(key => ['schema', 'runtime', 'role', 'planDigest', 'executionDigest', 'sliceId', 'requestedProfile', 'effectiveProfile', 'resolved', 'policyDigest', 'capabilityDigest', 'outcome', 'unavailableEvidence', 'nativeAgentType'].includes(key)))
|
|
254
|
+
return false; const runtime = x.runtime; return x.schema === 'routing-envelope/v1' && isObj(runtime) && Object.keys(runtime).every(key => ['target', 'kind', 'version', 'accountScopeDigest'].includes(key)) && ['codex', 'claude-code', 'cursor', 'copilot', 'opencode', 'antigravity'].includes(String(runtime.target)) && typeof runtime.kind === 'string' && runtime.kind.length > 0 && runtime.kind.length <= 128 && typeof runtime.version === 'string' && runtime.version.length > 0 && runtime.version.length <= 128 && isDigest(runtime.accountScopeDigest) && typeof x.role === 'string' && ROUTING_ROLES.includes(x.role) && isDigest(x.planDigest) && isDigest(x.executionDigest) && (x.sliceId === undefined || (typeof x.sliceId === 'string' && x.sliceId.length > 0 && x.sliceId.length <= 128)) && ['mechanical', 'integration', 'judgment', 'full'].includes(String(x.requestedProfile)) && ['mechanical', 'integration', 'judgment', 'full'].includes(String(x.effectiveProfile)) && isRoutingSelection(x.resolved) && isDigest(x.policyDigest) && isDigest(x.capabilityDigest) && ['native', 'degraded'].includes(String(x.outcome)) && Array.isArray(x.unavailableEvidence) && x.unavailableEvidence.length <= UNAVAILABLE_EVIDENCE.length && x.unavailableEvidence.every(value => typeof value === 'string' && UNAVAILABLE_EVIDENCE.includes(value)) && (x.nativeAgentType === undefined || (runtime.target === 'claude-code' && typeof x.nativeAgentType === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(x.nativeAgentType))); }
|
|
255
|
+
function isWellFormedImplementationLineage(x) { return isObj(x) && Object.keys(x).every(key => ['id', 'obligationId', 'sliceId', 'planDigest', 'executionDigest', 'initialProfile', 'initialEffort', 'attempts'].includes(key)) && typeof x.id === 'string' && x.id.length > 0 && typeof x.obligationId === 'string' && x.obligationId.length > 0 && typeof x.sliceId === 'string' && x.sliceId.length > 0 && isDigest(x.planDigest) && isDigest(x.executionDigest) && ['mechanical', 'integration', 'judgment'].includes(String(x.initialProfile)) && ['medium', 'high', 'runtime-default'].includes(String(x.initialEffort)) && Number.isSafeInteger(x.attempts) && x.attempts >= 0 && x.attempts <= 3; }
|
|
225
256
|
function isWellFormedRequestProblem(x) {
|
|
226
257
|
return isObj(x) && typeof x.file === 'string' && (x.kind === 'corrupt' || x.kind === 'rejected')
|
|
227
|
-
&& typeof x.detail === 'string' && typeof x.at === 'string'
|
|
258
|
+
&& typeof x.detail === 'string' && typeof x.at === 'string'
|
|
259
|
+
&& (x.requestId === undefined || typeof x.requestId === 'string')
|
|
260
|
+
&& (x.taskId === undefined || typeof x.taskId === 'string')
|
|
261
|
+
&& (x.generationToken === undefined || typeof x.generationToken === 'string')
|
|
262
|
+
&& (x.resolution === undefined || (x.kind === 'rejected' && isObj(x.resolution)
|
|
263
|
+
&& Object.keys(x.resolution).every(key => ['replacementRequestId', 'replacementPayloadDigest', 'reason', 'generationToken', 'at'].includes(key))
|
|
264
|
+
&& typeof x.requestId === 'string' && x.requestId.length > 0
|
|
265
|
+
&& typeof x.taskId === 'string' && x.taskId.length > 0
|
|
266
|
+
&& typeof x.generationToken === 'string' && x.generationToken.length > 0
|
|
267
|
+
&& typeof x.resolution.replacementRequestId === 'string' && x.resolution.replacementRequestId.length > 0
|
|
268
|
+
&& typeof x.resolution.replacementPayloadDigest === 'string' && /^[a-f0-9]{64}$/.test(x.resolution.replacementPayloadDigest)
|
|
269
|
+
&& typeof x.resolution.reason === 'string' && x.resolution.reason.trim().length > 0
|
|
270
|
+
&& typeof x.resolution.generationToken === 'string' && x.resolution.generationToken.length > 0
|
|
271
|
+
&& typeof x.resolution.at === 'string' && Number.isFinite(Date.parse(x.resolution.at))));
|
|
228
272
|
}
|
|
229
273
|
function isWellFormedCustodyDecision(x) {
|
|
230
274
|
return isObj(x) && typeof x.at === 'string' && x.decision === 'resume'
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateEventReceipt = validateEventReceipt;
|
|
4
|
+
exports.evaluateEventReceipt = evaluateEventReceipt;
|
|
5
|
+
const capabilities_1 = require("./capabilities");
|
|
6
|
+
const validate_1 = require("./validate");
|
|
7
|
+
function record(value, label) {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
9
|
+
throw new Error(`${label} must be an object`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function exact(value, keys, label) {
|
|
13
|
+
if (Object.keys(value).length !== keys.length || Object.keys(value).some(key => !keys.includes(key)))
|
|
14
|
+
throw new Error(`${label} has unknown or missing fields`);
|
|
15
|
+
}
|
|
16
|
+
function digest(value, label) {
|
|
17
|
+
(0, validate_1.assertDigest)(value, label);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function selection(value) {
|
|
21
|
+
const item = record(value, 'selection');
|
|
22
|
+
exact(item, ['selector', 'effort'], 'selection');
|
|
23
|
+
const selector = record(item.selector, 'selection.selector');
|
|
24
|
+
exact(selector, ['kind', 'id'], 'selection.selector');
|
|
25
|
+
if (selector.kind !== 'model' && selector.kind !== 'tier')
|
|
26
|
+
throw new Error('selection.selector.kind is invalid');
|
|
27
|
+
if (typeof selector.id !== 'string' || selector.id.length === 0 || selector.id.length > 128 || /[\u0000-\u001f\u007f-\u009f]/.test(selector.id))
|
|
28
|
+
throw new Error('selection.selector.id is invalid');
|
|
29
|
+
const effort = record(item.effort, 'selection.effort');
|
|
30
|
+
if (effort.kind === 'runtime-default') {
|
|
31
|
+
exact(effort, ['kind'], 'selection.effort');
|
|
32
|
+
return { selector: { kind: selector.kind, id: selector.id }, effort: { kind: 'runtime-default' } };
|
|
33
|
+
}
|
|
34
|
+
exact(effort, ['kind', 'value'], 'selection.effort');
|
|
35
|
+
if (effort.kind !== 'explicit' || typeof effort.value !== 'string' || effort.value.length === 0 || effort.value.length > 128 || /[\u0000-\u001f\u007f-\u009f]/.test(effort.value))
|
|
36
|
+
throw new Error('selection.effort is invalid');
|
|
37
|
+
return { selector: { kind: selector.kind, id: selector.id }, effort: { kind: 'explicit', value: effort.value } };
|
|
38
|
+
}
|
|
39
|
+
function key(value) { return `${value.selector.kind}\0${value.selector.id}\0${value.effort.kind}\0${value.effort.kind === 'explicit' ? value.effort.value : ''}`; }
|
|
40
|
+
function finiteTokens(value, label) {
|
|
41
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
42
|
+
throw new Error(`${label} is invalid`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
/** Schema v2 is internal native evidence, never a candidate for v1 approval. */
|
|
46
|
+
function validateEventReceipt(value) {
|
|
47
|
+
const item = record(value, 'event receipt');
|
|
48
|
+
exact(item, ['schema', 'runtime', 'binaryDigest', 'configDigest', 'recordedAt', 'claims'], 'event receipt');
|
|
49
|
+
if (item.schema !== 'routing-capabilities/v2')
|
|
50
|
+
throw new Error('event receipt schema is invalid');
|
|
51
|
+
const runtime = (0, capabilities_1.validateRuntimeKey)(item.runtime);
|
|
52
|
+
if (runtime.target !== 'codex' && runtime.target !== 'claude-code')
|
|
53
|
+
throw new Error('event receipt provider target is unsupported');
|
|
54
|
+
const binaryDigest = digest(item.binaryDigest, 'binaryDigest');
|
|
55
|
+
const configDigest = digest(item.configDigest, 'configDigest');
|
|
56
|
+
const recordedAt = (0, validate_1.canonicalUtcTimestamp)(item.recordedAt, 'recordedAt');
|
|
57
|
+
if (!Array.isArray(item.claims) || item.claims.length === 0 || item.claims.length > 64)
|
|
58
|
+
throw new Error('event receipt claims are invalid');
|
|
59
|
+
const claims = item.claims.map((raw, index) => {
|
|
60
|
+
const claim = record(raw, `claims[${index}]`);
|
|
61
|
+
exact(claim, claim.source === 'claude-assistant-transcript'
|
|
62
|
+
? ['selection', 'mappingDigest', 'eventDigest', 'source', 'nativeAgentType', ...(claim.nativeAgentIdDigest === undefined ? [] : ['nativeAgentIdDigest']), ...(claim.nativeEvents === undefined ? [] : ['nativeEvents']), 'observedAt', 'actualModel', 'tokenUsage']
|
|
63
|
+
: ['selection', 'mappingDigest', 'eventDigest', 'source', 'observedAt', 'actualModel', 'tokenUsage'], `claims[${index}]`);
|
|
64
|
+
const selected = selection(claim.selection);
|
|
65
|
+
const mappingDigest = digest(claim.mappingDigest, `claims[${index}].mappingDigest`);
|
|
66
|
+
const eventDigest = digest(claim.eventDigest, `claims[${index}].eventDigest`);
|
|
67
|
+
if (claim.source !== 'codex-turn-context' && claim.source !== 'claude-assistant-transcript')
|
|
68
|
+
throw new Error(`claims[${index}].source is invalid`);
|
|
69
|
+
if ((runtime.target === 'codex') !== (claim.source === 'codex-turn-context'))
|
|
70
|
+
throw new Error(`claims[${index}].source does not match provider`);
|
|
71
|
+
if (claim.source === 'claude-assistant-transcript' && (typeof claim.nativeAgentType !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(claim.nativeAgentType)))
|
|
72
|
+
throw new Error(`claims[${index}].nativeAgentType is invalid`);
|
|
73
|
+
if (claim.nativeAgentIdDigest !== undefined) {
|
|
74
|
+
if (claim.source !== 'claude-assistant-transcript')
|
|
75
|
+
throw new Error(`claims[${index}].nativeAgentIdDigest is invalid`);
|
|
76
|
+
digest(claim.nativeAgentIdDigest, `claims[${index}].nativeAgentIdDigest`);
|
|
77
|
+
}
|
|
78
|
+
let nativeEvents;
|
|
79
|
+
if (claim.nativeEvents !== undefined) {
|
|
80
|
+
if (claim.source !== 'claude-assistant-transcript' || !Array.isArray(claim.nativeEvents)
|
|
81
|
+
|| claim.nativeEvents.length === 0 || claim.nativeEvents.length > 64)
|
|
82
|
+
throw new Error(`claims[${index}].nativeEvents is invalid`);
|
|
83
|
+
nativeEvents = claim.nativeEvents.map((raw, eventIndex) => {
|
|
84
|
+
const event = record(raw, `claims[${index}].nativeEvents[${eventIndex}]`);
|
|
85
|
+
exact(event, ['nativeAgentIdDigest', 'eventDigest', 'observedAt'], `claims[${index}].nativeEvents[${eventIndex}]`);
|
|
86
|
+
const observedAt = (0, validate_1.canonicalUtcTimestamp)(event.observedAt, `claims[${index}].nativeEvents[${eventIndex}].observedAt`);
|
|
87
|
+
if (Date.parse(observedAt) > Date.parse(recordedAt))
|
|
88
|
+
throw new Error('Claude native event is newer than receipt');
|
|
89
|
+
return { nativeAgentIdDigest: digest(event.nativeAgentIdDigest, 'nativeAgentIdDigest'),
|
|
90
|
+
eventDigest: digest(event.eventDigest, 'eventDigest'), observedAt };
|
|
91
|
+
});
|
|
92
|
+
if (new Set(nativeEvents.map(event => event.nativeAgentIdDigest)).size !== nativeEvents.length
|
|
93
|
+
|| new Set(nativeEvents.map(event => event.eventDigest)).size !== nativeEvents.length)
|
|
94
|
+
throw new Error('Claude native events are duplicated');
|
|
95
|
+
}
|
|
96
|
+
const observedAt = (0, validate_1.canonicalUtcTimestamp)(claim.observedAt, `claims[${index}].observedAt`);
|
|
97
|
+
if (Date.parse(observedAt) > Date.parse(recordedAt))
|
|
98
|
+
throw new Error(`claims[${index}] is newer than receipt`);
|
|
99
|
+
let actualModel;
|
|
100
|
+
if (claim.actualModel === 'unverified')
|
|
101
|
+
actualModel = 'unverified';
|
|
102
|
+
else {
|
|
103
|
+
const actual = record(claim.actualModel, `claims[${index}].actualModel`);
|
|
104
|
+
exact(actual, ['id', 'evidenceDigest'], `claims[${index}].actualModel`);
|
|
105
|
+
if (claim.source !== 'claude-assistant-transcript' || typeof actual.id !== 'string' || actual.id.length === 0 || actual.id.length > 128)
|
|
106
|
+
throw new Error(`claims[${index}].actualModel is invalid`);
|
|
107
|
+
actualModel = { id: actual.id, evidenceDigest: digest(actual.evidenceDigest, `claims[${index}].actualModel.evidenceDigest`) };
|
|
108
|
+
}
|
|
109
|
+
let tokenUsage;
|
|
110
|
+
if (claim.tokenUsage === 'unknown')
|
|
111
|
+
tokenUsage = 'unknown';
|
|
112
|
+
else {
|
|
113
|
+
const usage = record(claim.tokenUsage, `claims[${index}].tokenUsage`);
|
|
114
|
+
exact(usage, ['input', 'output'], `claims[${index}].tokenUsage`);
|
|
115
|
+
tokenUsage = { input: finiteTokens(usage.input, 'input tokens'), output: finiteTokens(usage.output, 'output tokens') };
|
|
116
|
+
}
|
|
117
|
+
return { selection: selected, mappingDigest, eventDigest, source: claim.source,
|
|
118
|
+
...(claim.source === 'claude-assistant-transcript' ? { nativeAgentType: claim.nativeAgentType,
|
|
119
|
+
...(claim.nativeAgentIdDigest === undefined ? {} : { nativeAgentIdDigest: claim.nativeAgentIdDigest }),
|
|
120
|
+
...(nativeEvents === undefined ? {} : { nativeEvents }) } : {}), observedAt, actualModel, tokenUsage };
|
|
121
|
+
});
|
|
122
|
+
if (new Set(claims.map(claim => key(claim.selection))).size !== claims.length)
|
|
123
|
+
throw new Error('event receipt has duplicate selections');
|
|
124
|
+
return { schema: 'routing-capabilities/v2', runtime, binaryDigest, configDigest, recordedAt, claims };
|
|
125
|
+
}
|
|
126
|
+
/** Local, read-only comparison: no TTL, network call, rewrite or paid probe. */
|
|
127
|
+
function evaluateEventReceipt(value, scopeValue, selectedValue, mappingDigestValue, now) {
|
|
128
|
+
const receipt = validateEventReceipt(value);
|
|
129
|
+
const scope = record(scopeValue, 'event scope');
|
|
130
|
+
exact(scope, ['runtime', 'binaryDigest', 'configDigest'], 'event scope');
|
|
131
|
+
const runtime = (0, capabilities_1.validateRuntimeKey)(scope.runtime);
|
|
132
|
+
const binaryDigest = digest(scope.binaryDigest, 'binaryDigest');
|
|
133
|
+
const configDigest = digest(scope.configDigest, 'configDigest');
|
|
134
|
+
const selected = selection(selectedValue);
|
|
135
|
+
const mappingDigest = digest(mappingDigestValue, 'mappingDigest');
|
|
136
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
137
|
+
throw new Error('event currentness clock is invalid');
|
|
138
|
+
if (Date.parse(receipt.recordedAt) > now.getTime())
|
|
139
|
+
return { state: 'drift', reason: 'FUTURE_EVIDENCE' };
|
|
140
|
+
if (receipt.runtime.target !== runtime.target || receipt.runtime.kind !== runtime.kind || receipt.runtime.version !== runtime.version || receipt.binaryDigest !== binaryDigest)
|
|
141
|
+
return { state: 'drift', reason: 'RUNTIME_DRIFT' };
|
|
142
|
+
if (receipt.runtime.accountScopeDigest !== runtime.accountScopeDigest)
|
|
143
|
+
return { state: 'drift', reason: 'ACCOUNT_DRIFT' };
|
|
144
|
+
if (receipt.configDigest !== configDigest)
|
|
145
|
+
return { state: 'drift', reason: 'CONFIG_DRIFT' };
|
|
146
|
+
const claim = receipt.claims.find(item => key(item.selection) === key(selected));
|
|
147
|
+
if (!claim)
|
|
148
|
+
return { state: 'drift', reason: 'SELECTION_UNENROLLED' };
|
|
149
|
+
if (claim.mappingDigest !== mappingDigest)
|
|
150
|
+
return { state: 'drift', reason: 'POLICY_DRIFT' };
|
|
151
|
+
return { state: 'current' };
|
|
152
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.capabilityReceiptDigest = capabilityReceiptDigest;
|
|
|
8
8
|
exports.validateCapabilityReceipt = validateCapabilityReceipt;
|
|
9
9
|
exports.receiptIsCurrent = receiptIsCurrent;
|
|
10
10
|
exports.routingDiagnostic = routingDiagnostic;
|
|
11
|
+
exports.storedCapabilityPath = storedCapabilityPath;
|
|
11
12
|
exports.capabilityReceiptPath = capabilityReceiptPath;
|
|
12
13
|
exports.readCapabilities = readCapabilities;
|
|
13
14
|
exports.approveCapabilities = approveCapabilities;
|
|
@@ -107,7 +108,12 @@ function validateCapabilityReceipt(value) {
|
|
|
107
108
|
}
|
|
108
109
|
function receiptIsCurrent(receipt, runtime, now) { return receipt.runtime.target === runtime.target && receipt.runtime.kind === runtime.kind && receipt.runtime.version === runtime.version && receipt.runtime.accountScopeDigest === runtime.accountScopeDigest && Number.isFinite(now.getTime()) && Date.parse(receipt.recordedAt) <= now.getTime() && now.getTime() <= Date.parse(receipt.expiresAt); }
|
|
109
110
|
function routingDiagnostic(code, message) { return { code, message }; }
|
|
110
|
-
function
|
|
111
|
+
function storedCapabilityPath(target, kind) {
|
|
112
|
+
if (typeof target !== 'string' || !providers_1.AGENT_TARGETS.includes(target) || typeof kind !== 'string' || !RUNTIME_KIND.test(kind))
|
|
113
|
+
throw new Error('stored capability target or kind is invalid');
|
|
114
|
+
return path_1.default.join((0, paths_1.awmHome)(), 'routing-capabilities', target, `${kind}.json`);
|
|
115
|
+
}
|
|
116
|
+
function capabilityReceiptPath(runtime) { const valid = validateRuntimeKey(runtime); return storedCapabilityPath(valid.target, valid.kind); }
|
|
111
117
|
function safeParents(file) { for (let dir = path_1.default.dirname(path_1.default.resolve(file));; dir = path_1.default.dirname(dir)) {
|
|
112
118
|
try {
|
|
113
119
|
const stat = fs_1.default.lstatSync(dir);
|
|
@@ -0,0 +1,120 @@
|
|
|
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.recordClaudeHookStart = recordClaudeHookStart;
|
|
7
|
+
exports.readClaudeHookPair = readClaudeHookPair;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const paths_1 = require("../paths");
|
|
12
|
+
const json_1 = require("../plan/json");
|
|
13
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
14
|
+
const machine_key_1 = require("./machine-key");
|
|
15
|
+
const native_claude_1 = require("./native-claude");
|
|
16
|
+
const DOMAIN = 'AWM Claude hook start/v1\0';
|
|
17
|
+
const MAX_BYTES = 8192;
|
|
18
|
+
function id(value, label) {
|
|
19
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value))
|
|
20
|
+
throw new Error(`${label} is invalid`);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function event(value, kind) {
|
|
24
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.hook_event_name !== kind)
|
|
25
|
+
throw new Error(`Claude ${kind} hook is invalid`);
|
|
26
|
+
const row = value;
|
|
27
|
+
id(row.session_id, 'Claude session id');
|
|
28
|
+
id(row.agent_id, 'Claude agent id');
|
|
29
|
+
if (typeof row.agent_type !== 'string' || row.agent_type.length === 0 || row.agent_type.length > 128
|
|
30
|
+
|| /[\u0000-\u001f\u007f-\u009f]/.test(row.agent_type))
|
|
31
|
+
throw new Error('Claude agent type is invalid');
|
|
32
|
+
return row;
|
|
33
|
+
}
|
|
34
|
+
function fileFor(sessionId, agentId) {
|
|
35
|
+
const digest = (0, crypto_1.createHash)('sha256').update(`${sessionId}\0${agentId}`).digest('hex');
|
|
36
|
+
return path_1.default.join((0, paths_1.awmHome)(), 'claude-hook-starts', `${digest}.json`);
|
|
37
|
+
}
|
|
38
|
+
function mac(value, key) { return (0, crypto_1.createHmac)('sha256', key).update(DOMAIN).update(JSON.stringify(value)).digest('hex'); }
|
|
39
|
+
function safeDirectory(dir, create) {
|
|
40
|
+
const chain = [];
|
|
41
|
+
for (let current = dir;; current = path_1.default.dirname(current)) {
|
|
42
|
+
chain.unshift(current);
|
|
43
|
+
if (current === path_1.default.dirname(current))
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
for (const item of chain) {
|
|
47
|
+
try {
|
|
48
|
+
const stat = fs_1.default.lstatSync(item);
|
|
49
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
50
|
+
throw new Error('Claude hook event has unsafe ancestor');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code !== 'ENOENT' || !create)
|
|
54
|
+
throw error;
|
|
55
|
+
fs_1.default.mkdirSync(item, { mode: 0o700 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Persist bounded hook identity only; never a prompt, transcript or account ID. */
|
|
60
|
+
function recordClaudeHookStart(value, now) {
|
|
61
|
+
const row = event(value, 'SubagentStart');
|
|
62
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
63
|
+
throw new Error('Claude hook clock is invalid');
|
|
64
|
+
const root = (0, paths_1.awmHome)();
|
|
65
|
+
const key = (0, machine_key_1.readMachineKey)(root);
|
|
66
|
+
if (!key)
|
|
67
|
+
throw new Error('Claude hook enrollment requires explicit machine setup');
|
|
68
|
+
const start = { hook_event_name: 'SubagentStart', session_id: row.session_id, agent_id: row.agent_id,
|
|
69
|
+
agent_type: row.agent_type, recorded_at: now.toISOString() };
|
|
70
|
+
const file = fileFor(start.session_id, start.agent_id);
|
|
71
|
+
native_bridge_1.secureFs.withProjectLease(root, () => {
|
|
72
|
+
safeDirectory(path_1.default.dirname(file), true);
|
|
73
|
+
const content = Buffer.from(`${JSON.stringify({ start, mac: mac(start, key) })}\n`);
|
|
74
|
+
let observed = null;
|
|
75
|
+
try {
|
|
76
|
+
const stat = fs_1.default.lstatSync(file);
|
|
77
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
78
|
+
throw new Error('Claude hook start is unsafe');
|
|
79
|
+
observed = native_bridge_1.secureFs.readRegularFile(file, MAX_BYTES);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code !== 'ENOENT')
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, path_1.default.relative(root, file).split(path_1.default.sep).join('/'), content, observed ? { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity } : { mode: 'create', createParents: false });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Link Stop to a sealed Start event on this machine. Pending events expire
|
|
89
|
+
* after one day; this is not the capability receipt lifetime. */
|
|
90
|
+
function readClaudeHookPair(stopValue, now) {
|
|
91
|
+
const stop = event(stopValue, 'SubagentStop');
|
|
92
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
93
|
+
throw new Error('Claude hook clock is invalid');
|
|
94
|
+
const key = (0, machine_key_1.readMachineKey)((0, paths_1.awmHome)());
|
|
95
|
+
if (!key)
|
|
96
|
+
throw new Error('Claude hook machine key is missing');
|
|
97
|
+
const file = fileFor(stop.session_id, stop.agent_id);
|
|
98
|
+
safeDirectory(path_1.default.dirname(file), false);
|
|
99
|
+
const stat = fs_1.default.lstatSync(file);
|
|
100
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
101
|
+
throw new Error('Claude hook start is unsafe');
|
|
102
|
+
const parsed = (0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(native_bridge_1.secureFs.readRegularFile(file, MAX_BYTES).bytes));
|
|
103
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
104
|
+
throw new Error('Claude hook start envelope is invalid');
|
|
105
|
+
const envelope = parsed;
|
|
106
|
+
if (Object.keys(envelope).length !== 2 || typeof envelope.mac !== 'string' || !/^[a-f0-9]{64}$/.test(envelope.mac)
|
|
107
|
+
|| !envelope.start || typeof envelope.start !== 'object' || Array.isArray(envelope.start))
|
|
108
|
+
throw new Error('Claude hook start seal is invalid');
|
|
109
|
+
const start = envelope.start;
|
|
110
|
+
if (Object.keys(start).length !== 5 || start.hook_event_name !== 'SubagentStart' || typeof start.recorded_at !== 'string'
|
|
111
|
+
|| !Number.isFinite(Date.parse(start.recorded_at)) || start.session_id !== stop.session_id || start.agent_id !== stop.agent_id)
|
|
112
|
+
throw new Error('Claude hook start identity is invalid');
|
|
113
|
+
if (!(0, crypto_1.timingSafeEqual)(Buffer.from(envelope.mac, 'hex'), Buffer.from(mac(start, key), 'hex')))
|
|
114
|
+
throw new Error('Claude hook start seal does not match');
|
|
115
|
+
const ageMs = now.getTime() - Date.parse(start.recorded_at);
|
|
116
|
+
if (ageMs < 0 || ageMs > 24 * 60 * 60 * 1000)
|
|
117
|
+
throw new Error('Claude hook start is stale');
|
|
118
|
+
const lifecycle = (0, native_claude_1.normalizeClaudeSubagentLifecycle)(start, stop);
|
|
119
|
+
return { lifecycle, transcriptPath: stop.agent_transcript_path };
|
|
120
|
+
}
|