agentic-workflow-manager 9.11.11 → 9.12.0
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/index.js +39 -4
- package/dist/src/commands/model-policy/index.js +253 -0
- package/dist/src/commands/plan/index.js +53 -12
- package/dist/src/commands/preflight/checks.js +18 -0
- package/dist/src/commands/watch/apply.js +71 -5
- package/dist/src/commands/watch/supervisor.js +63 -3
- package/dist/src/core/admission/index.js +20 -6
- package/dist/src/core/journal/types.js +21 -5
- 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 +125 -16
- 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 +117 -3
- 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 +12 -5
- package/dist/tests/commands/job/routing.test.js +2 -2
- 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/plan/index.test.js +2 -2
- package/dist/tests/commands/preflight/preflight.test.js +65 -0
- package/dist/tests/commands/watch/apply.test.js +116 -10
- package/dist/tests/commands/watch/dispatch-routing-identity.test.js +23 -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 +165 -12
- 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/resolve.test.js +17 -0
- package/dist/tests/core/model-policy/setup-readiness.test.js +83 -0
- package/dist/tests/helpers/routing-approval.js +14 -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
|
@@ -9,6 +9,8 @@ exports.resolveLineageEscalation = resolveLineageEscalation;
|
|
|
9
9
|
exports.routingReport = routingReport;
|
|
10
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
11
11
|
const types_1 = require("../journal/types");
|
|
12
|
+
const resolve_1 = require("./resolve");
|
|
13
|
+
const capabilities_1 = require("./capabilities");
|
|
12
14
|
function assertTimestamp(value) { if (typeof value !== 'string' || !Number.isFinite(Date.parse(value)))
|
|
13
15
|
throw new Error('routing timestamp is invalid'); }
|
|
14
16
|
function canonical(value) { if (Array.isArray(value))
|
|
@@ -19,6 +21,26 @@ function canonical(value) { if (Array.isArray(value))
|
|
|
19
21
|
function digest(value) { return crypto_1.default.createHash('sha256').update(canonical(value)).digest('hex'); }
|
|
20
22
|
function clone(state) { const next = structuredClone(state); if (!Array.isArray(next.routingAttempts))
|
|
21
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
|
+
}
|
|
22
44
|
function reserveRoutingAttempt(state, input, now) {
|
|
23
45
|
assertTimestamp(now);
|
|
24
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))
|
|
@@ -33,24 +55,62 @@ function reserveRoutingAttempt(state, input, now) {
|
|
|
33
55
|
next.implementationLineages ??= [];
|
|
34
56
|
const priorLineage = next.implementationLineages.find(lineage => lineage.id === input.lineageId);
|
|
35
57
|
if (input.envelope.role === 'implementer' && priorLineage === undefined)
|
|
36
|
-
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 });
|
|
37
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))
|
|
38
60
|
throw new Error('routing lineage binding mismatch');
|
|
39
61
|
const attempts = next.routingAttempts.filter(item => item.lineageId === input.lineageId && item.envelope.role === 'implementer');
|
|
40
62
|
if (input.envelope.role === 'implementer' && attempts.length >= 3)
|
|
41
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
|
+
}
|
|
42
73
|
if (input.envelope.role === 'implementer') {
|
|
43
74
|
const initial = input.envelope.requestedProfile;
|
|
44
75
|
if (initial === 'full')
|
|
45
76
|
throw new Error('routing implementer profile is invalid');
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
|
50
84
|
}
|
|
85
|
+
const approval = input.approval?.();
|
|
86
|
+
if (!approval)
|
|
87
|
+
throw new Error('routing reservation requires current approval');
|
|
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;
|
|
95
|
+
const resolved = input.envelope.role === 'implementer' && input.envelope.effectiveProfile === 'judgment' && input.envelope.resolved.effort.kind === 'explicit' && input.envelope.resolved.effort.value === 'high'
|
|
96
|
+
? (0, resolve_1.resolveEscalatedSelection)({ ...selectionInput, role: 'implementer', requestedProfile: 'judgment', expectedEffort: 'high' })
|
|
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)
|
|
103
|
+
throw new Error('routing reservation approval mismatch');
|
|
51
104
|
const attempt = attempts.length + 1;
|
|
52
105
|
const id = `route-${crypto_1.default.createHash('sha256').update(`${input.lineageId}\0${attempt}\0${envelopeDigest}\0${input.fingerprint}`).digest('hex').slice(0, 24)}`;
|
|
53
|
-
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
|
+
}
|
|
54
114
|
const lineage = next.implementationLineages.find(candidate => candidate.id === input.lineageId);
|
|
55
115
|
if (lineage !== undefined)
|
|
56
116
|
lineage.attempts = attempt;
|
|
@@ -58,25 +118,53 @@ function reserveRoutingAttempt(state, input, now) {
|
|
|
58
118
|
}
|
|
59
119
|
function observeRoutingAttempt(state, input, now) {
|
|
60
120
|
assertTimestamp(now);
|
|
61
|
-
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)))
|
|
62
122
|
throw new Error('routing observation is invalid');
|
|
63
123
|
const next = clone(state);
|
|
64
124
|
const attempt = next.routingAttempts.find(item => item.id === input.attemptId);
|
|
65
125
|
if (!attempt)
|
|
66
126
|
throw new Error('routing attempt is unknown');
|
|
67
127
|
if (attempt.state === 'active') {
|
|
68
|
-
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)
|
|
69
136
|
return next;
|
|
70
137
|
throw new Error('routing observation is immutable');
|
|
71
138
|
}
|
|
72
139
|
if (attempt.state !== 'reserved')
|
|
73
140
|
throw new Error('routing attempt cannot be observed');
|
|
74
|
-
|
|
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
|
+
}
|
|
75
147
|
attempt.nativeAgentId = input.nativeAgentId;
|
|
76
148
|
if (input.observed)
|
|
77
149
|
attempt.observed = structuredClone(input.observed);
|
|
78
|
-
if (input.
|
|
79
|
-
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
|
+
}
|
|
80
168
|
return next;
|
|
81
169
|
}
|
|
82
170
|
/** Computes, but never persists, the only permitted implementation retry.
|
|
@@ -88,15 +176,24 @@ function resolveLineageEscalation(state, lineageId, initial) {
|
|
|
88
176
|
const attempts = (state.routingAttempts ?? []).filter((attempt) => attempt.lineageId === lineageId && attempt.envelope.role === 'implementer').sort((left, right) => left.attempt - right.attempt);
|
|
89
177
|
if (attempts.some((attempt) => ['reserved', 'active', 'unknown'].includes(attempt.state)))
|
|
90
178
|
throw new Error('routing lineage has a live or unknown attempt');
|
|
91
|
-
if (attempts.length === 0)
|
|
92
|
-
|
|
179
|
+
if (attempts.length === 0) {
|
|
180
|
+
const lineage = state.implementationLineages?.find(candidate => candidate.id === lineageId);
|
|
181
|
+
return { profile: initial, effort: lineage?.initialEffort ?? 'medium' };
|
|
182
|
+
}
|
|
93
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
|
+
}
|
|
94
190
|
if (last.verdict !== 'fail')
|
|
95
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';
|
|
96
193
|
if (last.envelope.effectiveProfile === 'mechanical')
|
|
97
|
-
return { profile: 'integration', effort:
|
|
194
|
+
return { profile: 'integration', effort: routeEffort };
|
|
98
195
|
if (last.envelope.effectiveProfile === 'integration')
|
|
99
|
-
return { profile: 'judgment', effort:
|
|
196
|
+
return { profile: 'judgment', effort: routeEffort };
|
|
100
197
|
if (last.envelope.effectiveProfile === 'judgment' && last.envelope.resolved.effort.kind === 'explicit' && last.envelope.resolved.effort.value === 'medium')
|
|
101
198
|
return { profile: 'judgment', effort: 'high' };
|
|
102
199
|
throw new Error('routing lineage escalation exhausted');
|
|
@@ -107,6 +204,7 @@ function routingReport(state) {
|
|
|
107
204
|
const attempts = state.routingAttempts ?? [];
|
|
108
205
|
const plannedByRole = {};
|
|
109
206
|
const actualByRole = {};
|
|
207
|
+
const nativeReconciledByRole = {};
|
|
110
208
|
const byState = {};
|
|
111
209
|
const unavailable = {};
|
|
112
210
|
const verdicts = {};
|
|
@@ -114,11 +212,22 @@ function routingReport(state) {
|
|
|
114
212
|
plannedByRole[attempt.envelope.role] = (plannedByRole[attempt.envelope.role] ?? 0) + 1;
|
|
115
213
|
if (attempt.nativeAgentId !== undefined)
|
|
116
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;
|
|
117
217
|
byState[attempt.state] = (byState[attempt.state] ?? 0) + 1;
|
|
118
218
|
for (const reason of attempt.envelope.unavailableEvidence)
|
|
119
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;
|
|
120
222
|
if (attempt.verdict !== undefined)
|
|
121
223
|
verdicts[attempt.verdict] = (verdicts[attempt.verdict] ?? 0) + 1;
|
|
122
224
|
}
|
|
123
|
-
|
|
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 } : {}) };
|
|
124
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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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.readMachineKey = readMachineKey;
|
|
7
|
+
exports.ensureMachineKey = ensureMachineKey;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
12
|
+
const KEY_NAME = 'routing-machine.key';
|
|
13
|
+
function keyPath(root) {
|
|
14
|
+
if (typeof root !== 'string' || !path_1.default.isAbsolute(root) || path_1.default.normalize(root) !== root || root.includes('\0'))
|
|
15
|
+
throw new Error('routing machine root must be an absolute normalized path');
|
|
16
|
+
for (let dir = root;; dir = path_1.default.dirname(dir)) {
|
|
17
|
+
const stat = fs_1.default.lstatSync(dir);
|
|
18
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
19
|
+
throw new Error('routing machine root has unsafe or symlinked ancestor');
|
|
20
|
+
if (dir === path_1.default.dirname(dir))
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
return path_1.default.join(root, KEY_NAME);
|
|
24
|
+
}
|
|
25
|
+
/** Read-only status path; never initializes machine state during diagnostics. */
|
|
26
|
+
function readMachineKey(root) {
|
|
27
|
+
const file = keyPath(root);
|
|
28
|
+
try {
|
|
29
|
+
const stat = fs_1.default.lstatSync(file);
|
|
30
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
31
|
+
throw new Error('routing machine key is unsafe or symlinked');
|
|
32
|
+
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)
|
|
33
|
+
throw new Error('routing machine key permissions are unsafe');
|
|
34
|
+
const bytes = native_bridge_1.secureFs.readRegularFile(file, 32).bytes;
|
|
35
|
+
if (bytes.length !== 32)
|
|
36
|
+
throw new Error('routing machine key length is invalid');
|
|
37
|
+
return Buffer.from(bytes);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code === 'ENOENT')
|
|
41
|
+
return null;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Only explicit setup may call this producer. No secret bytes are reported. */
|
|
46
|
+
function ensureMachineKey(root) {
|
|
47
|
+
keyPath(root);
|
|
48
|
+
return native_bridge_1.secureFs.withProjectLease(root, () => {
|
|
49
|
+
const existing = readMachineKey(root);
|
|
50
|
+
if (existing)
|
|
51
|
+
return existing;
|
|
52
|
+
const bytes = (0, crypto_1.randomBytes)(32);
|
|
53
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, KEY_NAME, bytes, { mode: 'create', createParents: false });
|
|
54
|
+
return bytes;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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.isNativeClaudeAgentDeclaration = isNativeClaudeAgentDeclaration;
|
|
7
|
+
exports.readClaudeAgentDeclaredModel = readClaudeAgentDeclaredModel;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
12
|
+
const observedDeclarations = new WeakMap();
|
|
13
|
+
function isNativeClaudeAgentDeclaration(value) {
|
|
14
|
+
return !!value && typeof value === 'object' && observedDeclarations.get(value) === JSON.stringify(value);
|
|
15
|
+
}
|
|
16
|
+
function safeDirectory(dir) {
|
|
17
|
+
for (let current = dir;; current = path_1.default.dirname(current)) {
|
|
18
|
+
const stat = fs_1.default.lstatSync(current);
|
|
19
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
20
|
+
throw new Error('Claude agent definition has an unsafe ancestor');
|
|
21
|
+
if (current === path_1.default.dirname(current))
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** An explicit custom-agent model is necessary, but the effective model must
|
|
26
|
+
* still be independently observed in the native child transcript. Built-in
|
|
27
|
+
* agents, aliases and an inherited/default model cannot enroll a selection. */
|
|
28
|
+
function readClaudeAgentDeclaredModel(input) {
|
|
29
|
+
if (!input || typeof input !== 'object' || !path_1.default.isAbsolute(input.cwd) || !path_1.default.isAbsolute(input.configDir)
|
|
30
|
+
|| path_1.default.normalize(input.cwd) !== input.cwd || path_1.default.normalize(input.configDir) !== input.configDir
|
|
31
|
+
|| input.cwd.includes('\0') || input.configDir.includes('\0')
|
|
32
|
+
|| typeof input.agentType !== 'string' || !/^awm-[A-Za-z0-9][A-Za-z0-9_-]{0,123}$/.test(input.agentType))
|
|
33
|
+
throw new Error('Claude agent definition identity is invalid');
|
|
34
|
+
const dirs = [path_1.default.join(input.configDir, 'agents'), path_1.default.join(input.cwd, '.claude', 'agents')];
|
|
35
|
+
const declared = [];
|
|
36
|
+
for (const dir of dirs) {
|
|
37
|
+
try {
|
|
38
|
+
safeDirectory(dir);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code === 'ENOENT')
|
|
42
|
+
continue;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
const names = fs_1.default.readdirSync(dir).filter(name => name.endsWith('.md'));
|
|
46
|
+
if (names.length > 256)
|
|
47
|
+
throw new Error('Claude agent definitions exceed limit');
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
const file = path_1.default.join(dir, name);
|
|
50
|
+
const stat = fs_1.default.lstatSync(file);
|
|
51
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
52
|
+
throw new Error('Claude agent definition is unsafe');
|
|
53
|
+
const bytes = native_bridge_1.secureFs.readRegularFile(file, 1024 * 1024).bytes;
|
|
54
|
+
const content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
55
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content);
|
|
56
|
+
if (!match)
|
|
57
|
+
continue;
|
|
58
|
+
let frontmatter;
|
|
59
|
+
try {
|
|
60
|
+
frontmatter = js_yaml_1.default.load(match[1]);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error('Claude agent frontmatter is invalid');
|
|
64
|
+
}
|
|
65
|
+
if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter))
|
|
66
|
+
continue;
|
|
67
|
+
const row = frontmatter;
|
|
68
|
+
if (row.name !== input.agentType)
|
|
69
|
+
continue;
|
|
70
|
+
if (typeof row.description !== 'string' || !row.description.trim())
|
|
71
|
+
throw new Error('Claude agent description is missing');
|
|
72
|
+
if (typeof row.model !== 'string' || !/^claude-[a-z0-9][a-z0-9.-]{1,126}$/.test(row.model))
|
|
73
|
+
throw new Error('Claude agent requires an explicit full model ID; aliases and inherit are unverified');
|
|
74
|
+
declared.push(row.model);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (declared.length !== 1)
|
|
78
|
+
throw new Error(declared.length ? 'Claude agent definition is ambiguous' : 'Claude agent has no explicit custom definition');
|
|
79
|
+
const result = { agentType: input.agentType, modelId: declared[0] };
|
|
80
|
+
observedDeclarations.set(result, JSON.stringify(result));
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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.readClaudeModelConfigDigest = readClaudeModelConfigDigest;
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
11
|
+
const json_1 = require("../plan/json");
|
|
12
|
+
const MODEL_KEYS = ['model', 'effortLevel', 'availableModels', 'enforceAvailableModels', 'modelOverrides'];
|
|
13
|
+
const ENV_KEYS = ['ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL_FORCE'];
|
|
14
|
+
function safeDirectory(dir) {
|
|
15
|
+
for (let current = dir;; current = path_1.default.dirname(current)) {
|
|
16
|
+
const stat = fs_1.default.lstatSync(current);
|
|
17
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
18
|
+
throw new Error('Claude settings have an unsafe or symlinked ancestor');
|
|
19
|
+
if (current === path_1.default.dirname(current))
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function settings(file) {
|
|
24
|
+
let stat;
|
|
25
|
+
try {
|
|
26
|
+
stat = fs_1.default.lstatSync(file);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === 'ENOENT')
|
|
30
|
+
return null;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
safeDirectory(path_1.default.dirname(file));
|
|
34
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
35
|
+
throw new Error('Claude settings file is unsafe or symlinked');
|
|
36
|
+
const bytes = native_bridge_1.secureFs.readRegularFile(file, 1024 * 1024).bytes;
|
|
37
|
+
const value = (0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
38
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
39
|
+
throw new Error('Claude settings must be an object');
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function agentDefinitions(dir) {
|
|
43
|
+
let stat;
|
|
44
|
+
try {
|
|
45
|
+
stat = fs_1.default.lstatSync(dir);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code === 'ENOENT')
|
|
49
|
+
return [];
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
safeDirectory(path_1.default.dirname(dir));
|
|
53
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
54
|
+
throw new Error('Claude agents directory is unsafe or symlinked');
|
|
55
|
+
const names = fs_1.default.readdirSync(dir).filter(name => name.endsWith('.md')).sort();
|
|
56
|
+
if (names.length > 256)
|
|
57
|
+
throw new Error('Claude agent definitions exceed limit');
|
|
58
|
+
return names.map(name => {
|
|
59
|
+
const file = path_1.default.join(dir, name);
|
|
60
|
+
const entry = fs_1.default.lstatSync(file);
|
|
61
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
62
|
+
throw new Error('Claude agent definition is unsafe or symlinked');
|
|
63
|
+
const bytes = native_bridge_1.secureFs.readRegularFile(file, 1024 * 1024).bytes;
|
|
64
|
+
return [name, (0, crypto_1.createHash)('sha256').update(bytes).digest('hex')];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Fingerprints only locally visible model controls. Managed/server-side policy
|
|
68
|
+
* is not represented here; a native child observation remains mandatory. */
|
|
69
|
+
function readClaudeModelConfigDigest(input) {
|
|
70
|
+
if (!input || typeof input !== 'object' || !path_1.default.isAbsolute(input.cwd) || !path_1.default.isAbsolute(input.configDir)
|
|
71
|
+
|| path_1.default.normalize(input.cwd) !== input.cwd || path_1.default.normalize(input.configDir) !== input.configDir
|
|
72
|
+
|| input.cwd.includes('\0') || input.configDir.includes('\0') || !Buffer.isBuffer(input.key) || input.key.length !== 32
|
|
73
|
+
|| !input.environment || typeof input.environment !== 'object' || Array.isArray(input.environment))
|
|
74
|
+
throw new Error('Claude model config input is invalid');
|
|
75
|
+
safeDirectory(input.cwd);
|
|
76
|
+
safeDirectory(input.configDir);
|
|
77
|
+
const files = [path_1.default.join(input.configDir, 'settings.json'), path_1.default.join(input.cwd, '.claude', 'settings.json'), path_1.default.join(input.cwd, '.claude', 'settings.local.json')];
|
|
78
|
+
const projection = files.map(file => {
|
|
79
|
+
const value = settings(file);
|
|
80
|
+
if (!value)
|
|
81
|
+
return null;
|
|
82
|
+
const model = {};
|
|
83
|
+
for (const key of MODEL_KEYS)
|
|
84
|
+
if (Object.prototype.hasOwnProperty.call(value, key))
|
|
85
|
+
model[key] = value[key];
|
|
86
|
+
if (value.env && typeof value.env === 'object' && !Array.isArray(value.env)) {
|
|
87
|
+
const env = {};
|
|
88
|
+
for (const key of ENV_KEYS)
|
|
89
|
+
if (Object.prototype.hasOwnProperty.call(value.env, key))
|
|
90
|
+
env[key] = value.env[key];
|
|
91
|
+
if (Object.keys(env).length)
|
|
92
|
+
model.env = env;
|
|
93
|
+
}
|
|
94
|
+
return model;
|
|
95
|
+
});
|
|
96
|
+
const environment = {};
|
|
97
|
+
for (const key of ENV_KEYS) {
|
|
98
|
+
const value = input.environment[key];
|
|
99
|
+
if (value !== undefined) {
|
|
100
|
+
if (typeof value !== 'string' || value.length > 4096)
|
|
101
|
+
throw new Error('Claude model environment is invalid');
|
|
102
|
+
environment[key] = value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const agents = [agentDefinitions(path_1.default.join(input.configDir, 'agents')), agentDefinitions(path_1.default.join(input.cwd, '.claude', 'agents'))];
|
|
106
|
+
return (0, crypto_1.createHmac)('sha256', input.key).update(JSON.stringify({ projection, agents, environment })).digest('hex');
|
|
107
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.queryClaudeAuthStatus = queryClaudeAuthStatus;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
/** `claude auth status` is documented as a JSON, no-inference command. The
|
|
7
|
+
* account shape varies by auth method; incomplete identity stays unverified. */
|
|
8
|
+
function queryClaudeAuthStatus(input) {
|
|
9
|
+
if (!input || typeof input !== 'object' || typeof input.command !== 'string' || input.command.length === 0 || input.command.length > 4096
|
|
10
|
+
|| !Array.isArray(input.args) || input.args.length > 16 || input.args.some(value => typeof value !== 'string' || value.length > 65536)
|
|
11
|
+
|| !Number.isInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 30000
|
|
12
|
+
|| !Buffer.isBuffer(input.key) || input.key.length !== 32)
|
|
13
|
+
throw new Error('Claude auth query is invalid');
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const child = (0, child_process_1.spawn)(input.command, input.args, { stdio: ['ignore', 'pipe', 'pipe'], shell: false });
|
|
16
|
+
let settled = false;
|
|
17
|
+
let buffer = '';
|
|
18
|
+
const finish = (error, facts) => {
|
|
19
|
+
if (settled)
|
|
20
|
+
return;
|
|
21
|
+
settled = true;
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
if (error) {
|
|
24
|
+
child.kill();
|
|
25
|
+
reject(error);
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
resolve(facts);
|
|
29
|
+
};
|
|
30
|
+
const timer = setTimeout(() => finish(new Error('Claude auth status timed out')), input.timeoutMs);
|
|
31
|
+
child.on('error', () => finish(new Error('Claude CLI could not be started')));
|
|
32
|
+
child.stdout.setEncoding('utf8');
|
|
33
|
+
child.stdout.on('data', (chunk) => { buffer += chunk; if (buffer.length > 64 * 1024)
|
|
34
|
+
finish(new Error('Claude auth status exceeds 64 KiB')); });
|
|
35
|
+
child.on('exit', code => {
|
|
36
|
+
if (code !== 0)
|
|
37
|
+
return finish(new Error('Claude auth status failed or is logged out'));
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(buffer);
|
|
40
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
41
|
+
throw new Error('shape');
|
|
42
|
+
const value = parsed;
|
|
43
|
+
if (value.loggedIn !== true)
|
|
44
|
+
throw new Error('login');
|
|
45
|
+
const fields = ['authMethod', 'apiProvider', 'email', 'orgId'];
|
|
46
|
+
const identified = fields.every(field => typeof value[field] === 'string' && value[field].length > 0 && value[field].length <= 320);
|
|
47
|
+
const subscriptionType = value.subscriptionType;
|
|
48
|
+
if (subscriptionType !== null && subscriptionType !== undefined && (typeof subscriptionType !== 'string' || subscriptionType.length > 128))
|
|
49
|
+
throw new Error('subscription');
|
|
50
|
+
const accountScopeDigest = identified ? (0, crypto_1.createHmac)('sha256', input.key).update(JSON.stringify(fields.map(field => value[field]).concat([subscriptionType ?? null]))).digest('hex') : null;
|
|
51
|
+
finish(undefined, { provenance: 'claude-auth-status', accountState: accountScopeDigest ? 'identified' : 'unverified', accountScopeDigest, inferenceDispatched: false });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
finish(new Error('Claude auth status response is invalid'));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|