claude-dev-env 7.6.1 → 7.7.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.
Files changed (31) hide show
  1. package/AGENTS.md +48 -71
  2. package/_shared/advisor/advisor-protocol.md +2 -2
  3. package/_shared/advisor/reference/advisor-block.md +1 -1
  4. package/_shared/advisor/reference/astra-rung.md +2 -2
  5. package/_shared/advisor/scripts/codex_astra_advisor.py +122 -23
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +76 -9
  7. package/_shared/advisor/scripts/tests/test_advisor_route_constants.py +51 -0
  8. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +52 -2
  9. package/bin/install-constants.mjs +3 -0
  10. package/bin/install-transaction.mjs +41 -44
  11. package/bin/install.cursor-rules.test.mjs +311 -3
  12. package/bin/install.mjs +156 -95
  13. package/bin/install.transaction.test.mjs +61 -1
  14. package/bin/pstack-model-preferences.codex.json +1 -0
  15. package/bin/pstack.mjs +28 -6
  16. package/bin/pstack.test.mjs +48 -0
  17. package/bin/select-pstack-models.test.mjs +375 -26
  18. package/bin/subagent-model-policy.test.mjs +399 -0
  19. package/docs/codex-compatibility.md +19 -5
  20. package/docs/subagent-model-routing.md +50 -0
  21. package/hooks/hooks.json +10 -0
  22. package/hooks/routing/subagent_model_routing.mjs +81 -0
  23. package/hooks/routing/subagent_model_routing.test.mjs +218 -0
  24. package/package.json +1 -1
  25. package/rules/pstack-models.md +22 -0
  26. package/rules/subagent-model-policy.json +85 -0
  27. package/scripts/resolve_advisor_model_route.mjs +52 -0
  28. package/scripts/select_pstack_models.mjs +203 -38
  29. package/scripts/subagent_model_policy.mjs +620 -0
  30. package/scripts/tests/resolve_advisor_model_route.test.mjs +51 -0
  31. package/scripts/tests/test_codex_compat_materializer.py +15 -0
@@ -0,0 +1,620 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const POLICY_FILE_NAME = 'subagent-model-policy.json';
6
+ const POLICY_SCHEMA_VERSION = 1;
7
+ const DEFAULT_ROLE = 'worker';
8
+ const MODEL_FIELD = 'model';
9
+ const EFFORT_FIELDS = ['reasoning_effort', 'effort'];
10
+ const ROLE_FIELDS = ['role', 'agent_type', 'agentType'];
11
+ const VALIDATED_POLICY_MARKER = Symbol('validatedPolicy');
12
+
13
+ export class SubagentModelPolicyError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'SubagentModelPolicyError';
17
+ }
18
+ }
19
+
20
+ export function defaultSubagentModelPolicyPath() {
21
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'rules', POLICY_FILE_NAME);
22
+ }
23
+
24
+ export function loadSubagentModelPolicy(policyPath = defaultSubagentModelPolicyPath()) {
25
+ const resolvedPolicyPath = resolve(policyPath);
26
+ if (!existsSync(resolvedPolicyPath)) {
27
+ throw new SubagentModelPolicyError(`policy file is missing: ${resolvedPolicyPath}`);
28
+ }
29
+ let rawPolicy;
30
+ try {
31
+ rawPolicy = JSON.parse(readFileSync(resolvedPolicyPath, 'utf8'));
32
+ } catch (error) {
33
+ const detail = error instanceof SyntaxError ? 'invalid JSON' : error.message;
34
+ throw new SubagentModelPolicyError(`policy file cannot be read: ${detail}`);
35
+ }
36
+ return validateSubagentModelPolicy(rawPolicy);
37
+ }
38
+
39
+ export function validateSubagentModelPolicy(policy) {
40
+ requireObject(policy, 'policy');
41
+ if (policy.schemaVersion !== POLICY_SCHEMA_VERSION) {
42
+ throw new SubagentModelPolicyError('policy schemaVersion must be 1');
43
+ }
44
+
45
+ const modelByName = validateModels(policy.models);
46
+ const effortDefinition = validateEfforts(policy.efforts);
47
+ const effortNames = effortDefinition.names;
48
+ const inheritanceAliases = validateStringList(
49
+ policy.inheritanceAliases,
50
+ 'inheritanceAliases',
51
+ ).map(normalizeToken);
52
+ if (inheritanceAliases.some(alias => modelByName.aliasOwner.has(alias))) {
53
+ throw new SubagentModelPolicyError('inheritance alias conflicts with a model alias');
54
+ }
55
+ const roleByAlias = validateRoles(policy.roles);
56
+ const trustedAdvisorRoles = validateTrustedAdvisorRoles(policy.roles, roleByAlias);
57
+ const approvedPairs = validateApprovedPairs(policy.approvedPairs, modelByName, effortNames, roleByAlias);
58
+ const approvedPairKeys = new Set(approvedPairs.map(pair => pairKey(pair)));
59
+ const selectorExclusions = validateSelectorExclusions(
60
+ policy.selectorExclusions ?? [],
61
+ modelByName,
62
+ effortNames,
63
+ );
64
+ const advisorDefault = validatePair(
65
+ policy.advisorDefault,
66
+ modelByName,
67
+ effortNames,
68
+ 'advisorDefault',
69
+ );
70
+ if (!isPairApproved(advisorDefault, 'advisor', approvedPairs)) {
71
+ throw new SubagentModelPolicyError('advisorDefault must be approved for advisor');
72
+ }
73
+
74
+ const replacements = validateReplacements(
75
+ policy.replacements,
76
+ modelByName,
77
+ effortNames,
78
+ approvedPairs,
79
+ 'replacements',
80
+ );
81
+ const roleReplacements = validateRoleReplacements(
82
+ policy.roleReplacements,
83
+ modelByName,
84
+ effortNames,
85
+ approvedPairs,
86
+ roleByAlias,
87
+ );
88
+ validateReplacementGraph(replacements, roleReplacements, approvedPairKeys);
89
+
90
+ const validatedPolicy = {
91
+ ...policy,
92
+ efforts: {
93
+ ...policy.efforts,
94
+ values: [...effortNames],
95
+ aliases: effortDefinition.aliases,
96
+ },
97
+ modelByName,
98
+ effortNames,
99
+ inheritanceAliases: new Set(inheritanceAliases),
100
+ roleByAlias,
101
+ trustedAdvisorRoles,
102
+ approvedPairs,
103
+ selectorExclusions,
104
+ advisorDefault,
105
+ replacements,
106
+ roleReplacements,
107
+ };
108
+ Object.defineProperty(validatedPolicy, VALIDATED_POLICY_MARKER, { value: true });
109
+ return validatedPolicy;
110
+ }
111
+
112
+ export function resolveSubagentModelRoute(request, options = {}) {
113
+ try {
114
+ const loadedPolicy = options.policy ?? loadSubagentModelPolicy(options.policyPath);
115
+ const policy = isValidatedPolicy(loadedPolicy)
116
+ ? loadedPolicy
117
+ : validateSubagentModelPolicy(loadedPolicy);
118
+ return resolveRouteWithPolicy(request, policy, options);
119
+ } catch (error) {
120
+ if (error instanceof SubagentModelPolicyError) return blockedRoute(error.message);
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ export function routeSubagentToolInput(toolInput, options = {}) {
126
+ if (!isObject(toolInput)) return blockedRoute('subagent tool input must be an object');
127
+ const route = resolveSubagentModelRoute(toolInput, options);
128
+ if (route.status === 'blocked') return { ...route, updatedInput: null };
129
+ if (route.status === 'inherited' || route.status === 'pass') {
130
+ return { ...route, updatedInput: toolInput };
131
+ }
132
+ const updatedInput = { ...toolInput, [MODEL_FIELD]: route.selected.model };
133
+ if (route.selected.effort !== null) {
134
+ const effortField = route.effortField ?? 'reasoning_effort';
135
+ updatedInput[effortField] = route.selected.effort;
136
+ if (effortField !== 'reasoning_effort' && Object.hasOwn(updatedInput, 'reasoning_effort')) {
137
+ delete updatedInput.reasoning_effort;
138
+ }
139
+ }
140
+ return { ...route, updatedInput };
141
+ }
142
+
143
+ export function isSelectorPairExcluded(model, effort, policy) {
144
+ if (effort === null || effort === undefined) return false;
145
+ const modelName = policy.modelByName.aliasOwner.get(normalizeToken(model));
146
+ if (!modelName) return false;
147
+ const normalizedEffort = normalizeToken(effort);
148
+ const effortName = policy.effortNames.has(normalizedEffort)
149
+ ? normalizedEffort
150
+ : policy.efforts.aliases[normalizedEffort];
151
+ return typeof effortName === 'string'
152
+ && policy.selectorExclusions.has(pairKey({ model: modelName, effort: effortName }));
153
+ }
154
+
155
+ function resolveRouteWithPolicy(request, policy, options) {
156
+ if (!isObject(request)) return blockedRoute('subagent tool input must be an object');
157
+ const roleResult = resolveRole(request, policy);
158
+ if (roleResult.status === 'blocked') return roleResult;
159
+ const { role } = roleResult;
160
+ if (role === 'advisor' && !hasTrustedAdvisorSession(policy, options)) {
161
+ return blockedRoute('advisor role is not trusted');
162
+ }
163
+
164
+ const effortResult = readEffort(request, policy);
165
+ if (effortResult.status === 'blocked') return effortResult;
166
+ const modelResult = readModel(request, policy);
167
+ if (modelResult.status === 'blocked') return modelResult;
168
+ let { modelName, inherited } = modelResult;
169
+ if (inherited && role === 'advisor') {
170
+ modelName = policy.advisorDefault.model;
171
+ inherited = false;
172
+ }
173
+ if (inherited) {
174
+ if (effortResult.effort !== null) {
175
+ return blockedRoute('parent inheritance cannot set an effort');
176
+ }
177
+ return {
178
+ status: 'inherited',
179
+ role,
180
+ requested: { model: null, effort: null },
181
+ selected: { model: null, effort: null },
182
+ selectedModelName: null,
183
+ effortField: effortResult.field,
184
+ diagnostic: null,
185
+ };
186
+ }
187
+
188
+ let effort = effortResult.effort;
189
+ if (effort === null && role === 'advisor') effort = policy.advisorDefault.effort;
190
+ if (effort === null) {
191
+ const modelEfforts = approvedEffortsForModel(modelName, role, policy.approvedPairs);
192
+ if (modelEfforts.length === 1) effort = modelEfforts[0];
193
+ }
194
+ if (effort === null) {
195
+ const modelEfforts = approvedEffortsForModel(modelName, role, policy.approvedPairs);
196
+ if (!options.allowMissingEffort || modelEfforts.length !== 1) {
197
+ return blockedRoute('effort is required for this model');
198
+ }
199
+ effort = modelEfforts[0];
200
+ }
201
+
202
+ const requestedPair = { model: modelName, effort };
203
+ const selectedPair = selectReplacement(requestedPair, role, policy);
204
+ if (!selectedPair) {
205
+ return blockedRoute(
206
+ `model and effort pair is not approved: ${modelName}/${effort}`,
207
+ );
208
+ }
209
+ if (!isPairApproved(selectedPair, role, policy.approvedPairs)) {
210
+ return blockedRoute('replacement pair is not approved for this role');
211
+ }
212
+ const availability = validateAvailability(selectedPair, options.availableModelIds, policy);
213
+ if (availability) return blockedRoute(availability);
214
+
215
+ const changed = selectedPair.model !== requestedPair.model
216
+ || selectedPair.effort !== requestedPair.effort
217
+ || modelResult.wasAlias
218
+ || effortResult.wasAlias
219
+ || modelResult.wasMissing
220
+ || effortResult.wasMissing;
221
+ const status = changed ? 'remapped' : 'pass';
222
+ return {
223
+ status,
224
+ role,
225
+ requested: {
226
+ model: policy.modelByName[requestedPair.model].id,
227
+ effort: requestedPair.effort,
228
+ },
229
+ selected: {
230
+ model: policy.modelByName[selectedPair.model].id,
231
+ effort: selectedPair.effort,
232
+ },
233
+ selectedModelName: selectedPair.model,
234
+ effortField: effortResult.field,
235
+ diagnostic: null,
236
+ };
237
+ }
238
+
239
+ function validateModels(models) {
240
+ requireObject(models, 'models');
241
+ const modelByName = {};
242
+ const aliasOwner = new Map();
243
+ for (const [rawName, rawModel] of Object.entries(models)) {
244
+ const name = normalizeToken(rawName);
245
+ if (!isIdentifier(name)) throw new SubagentModelPolicyError(`invalid model name: ${rawName}`);
246
+ if (Object.hasOwn(modelByName, name)) {
247
+ throw new SubagentModelPolicyError(`model name is ambiguous: ${name}`);
248
+ }
249
+ requireObject(rawModel, `model ${name}`);
250
+ const id = requireNonEmptyString(rawModel.id, `model ${name} id`);
251
+ const aliases = validateStringList(rawModel.aliases, `model ${name} aliases`);
252
+ const allAliases = [...new Set([name, id, ...aliases].map(normalizeToken))];
253
+ for (const alias of allAliases) {
254
+ const previousOwner = aliasOwner.get(alias);
255
+ if (previousOwner && previousOwner !== name) {
256
+ throw new SubagentModelPolicyError(`model alias is ambiguous: ${alias}`);
257
+ }
258
+ aliasOwner.set(alias, name);
259
+ }
260
+ modelByName[name] = { id, aliases: allAliases, aliasOwner };
261
+ }
262
+ if (Object.keys(modelByName).length === 0) throw new SubagentModelPolicyError('models must not be empty');
263
+ Object.defineProperty(modelByName, 'aliasOwner', { value: aliasOwner, enumerable: false });
264
+ return modelByName;
265
+ }
266
+
267
+ function validateEfforts(efforts) {
268
+ requireObject(efforts, 'efforts');
269
+ const names = validateStringList(efforts.values, 'efforts.values').map(normalizeToken);
270
+ if (new Set(names).size !== names.length || names.some(name => !isIdentifier(name))) {
271
+ throw new SubagentModelPolicyError('effort values must be unique identifiers');
272
+ }
273
+ const aliases = efforts.aliases;
274
+ requireObject(aliases, 'efforts.aliases');
275
+ const canonicalAliases = {};
276
+ for (const [alias, target] of Object.entries(aliases)) {
277
+ const normalizedAlias = normalizeToken(alias);
278
+ const normalizedTarget = normalizeToken(target);
279
+ if (!isIdentifier(normalizedAlias) || !names.includes(normalizedTarget)) {
280
+ throw new SubagentModelPolicyError(`effort alias is invalid: ${alias}`);
281
+ }
282
+ if (Object.hasOwn(canonicalAliases, normalizedAlias)) {
283
+ throw new SubagentModelPolicyError(`effort alias is ambiguous: ${normalizedAlias}`);
284
+ }
285
+ if (names.includes(normalizedAlias)) {
286
+ throw new SubagentModelPolicyError(`effort alias conflicts with value: ${alias}`);
287
+ }
288
+ canonicalAliases[normalizedAlias] = normalizedTarget;
289
+ }
290
+ return { names: new Set(names), aliases: canonicalAliases };
291
+ }
292
+
293
+ function validateRoles(roles) {
294
+ requireObject(roles, 'roles');
295
+ const roleByAlias = new Map();
296
+ const canonicalRoles = ['worker', 'advisor'];
297
+ for (const role of canonicalRoles) {
298
+ const aliases = validateStringList(roles[role], `roles.${role}`).map(normalizeToken);
299
+ const allAliases = [role, ...aliases];
300
+ for (const alias of allAliases) {
301
+ if (!isIdentifier(alias)) throw new SubagentModelPolicyError(`invalid role name: ${alias}`);
302
+ const previousRole = roleByAlias.get(alias);
303
+ if (previousRole && previousRole !== role) {
304
+ throw new SubagentModelPolicyError(`role alias is ambiguous: ${alias}`);
305
+ }
306
+ roleByAlias.set(alias, role);
307
+ }
308
+ }
309
+ const defaultRole = normalizeToken(roles.default);
310
+ if (defaultRole !== DEFAULT_ROLE || !roleByAlias.has(defaultRole)) {
311
+ throw new SubagentModelPolicyError('roles.default must be worker');
312
+ }
313
+ return roleByAlias;
314
+ }
315
+
316
+ function validateTrustedAdvisorRoles(roles, roleByAlias) {
317
+ const configuredRoles = roles.trustedAdvisor ?? roles.advisor.filter(
318
+ role => normalizeToken(role) !== 'advisor',
319
+ );
320
+ const trustedAdvisorRoles = validateStringList(
321
+ configuredRoles,
322
+ 'roles.trustedAdvisor',
323
+ ).map(normalizeToken);
324
+ if (new Set(trustedAdvisorRoles).size !== trustedAdvisorRoles.length) {
325
+ throw new SubagentModelPolicyError('roles.trustedAdvisor must be unique');
326
+ }
327
+ if (trustedAdvisorRoles.some(role => roleByAlias.get(role) !== 'advisor')) {
328
+ throw new SubagentModelPolicyError('roles.trustedAdvisor must name advisor roles');
329
+ }
330
+ return new Set(trustedAdvisorRoles);
331
+ }
332
+
333
+ function validateApprovedPairs(rawPairs, modelByName, effortNames, roleByAlias) {
334
+ if (!Array.isArray(rawPairs) || rawPairs.length === 0) {
335
+ throw new SubagentModelPolicyError('approvedPairs must be a non-empty array');
336
+ }
337
+ const pairs = [];
338
+ const pairScopes = new Set();
339
+ for (const rawPair of rawPairs) {
340
+ const pair = validatePair(rawPair, modelByName, effortNames, 'approvedPairs entry');
341
+ const roles = rawPair.roles === undefined || rawPair.roles === null
342
+ ? null
343
+ : validateCanonicalRoles(rawPair.roles, roleByAlias, 'approved pair roles');
344
+ const scopeKey = `${pairKey(pair)}|${roles ? roles.join(',') : '*'}`;
345
+ if (pairScopes.has(scopeKey)) throw new SubagentModelPolicyError(`duplicate pair key: ${scopeKey}`);
346
+ pairScopes.add(scopeKey);
347
+ pairs.push({ ...pair, roles });
348
+ }
349
+ return pairs;
350
+ }
351
+
352
+ function validateSelectorExclusions(rawExclusions, modelByName, effortNames) {
353
+ if (!Array.isArray(rawExclusions)) {
354
+ throw new SubagentModelPolicyError('selectorExclusions must be an array');
355
+ }
356
+ const exclusionKeys = new Set();
357
+ for (const rawExclusion of rawExclusions) {
358
+ const exclusion = validatePair(
359
+ rawExclusion,
360
+ modelByName,
361
+ effortNames,
362
+ 'selectorExclusions entry',
363
+ );
364
+ const key = pairKey(exclusion);
365
+ if (exclusionKeys.has(key)) {
366
+ throw new SubagentModelPolicyError(`duplicate selector exclusion: ${key}`);
367
+ }
368
+ exclusionKeys.add(key);
369
+ }
370
+ return exclusionKeys;
371
+ }
372
+
373
+ function validateReplacements(rawReplacements, modelByName, effortNames, approvedPairs, label) {
374
+ if (!Array.isArray(rawReplacements)) throw new SubagentModelPolicyError(`${label} must be an array`);
375
+ const replacements = new Map();
376
+ for (const rawReplacement of rawReplacements) {
377
+ requireObject(rawReplacement, `${label} entry`);
378
+ const requested = validatePair(rawReplacement.requested, modelByName, effortNames, `${label} requested`);
379
+ const selected = validatePair(rawReplacement.selected, modelByName, effortNames, `${label} selected`);
380
+ const key = pairKey(requested);
381
+ if (replacements.has(key)) throw new SubagentModelPolicyError(`ambiguous replacement: ${key}`);
382
+ if (!isPairApproved(selected, null, approvedPairs)) {
383
+ throw new SubagentModelPolicyError(`replacement target is not approved: ${pairKey(selected)}`);
384
+ }
385
+ replacements.set(key, selected);
386
+ }
387
+ return replacements;
388
+ }
389
+
390
+ function validateRoleReplacements(rawReplacements, modelByName, effortNames, approvedPairs, roleByAlias) {
391
+ if (!Array.isArray(rawReplacements)) throw new SubagentModelPolicyError('roleReplacements must be an array');
392
+ const replacements = new Map();
393
+ for (const rawReplacement of rawReplacements) {
394
+ requireObject(rawReplacement, 'roleReplacements entry');
395
+ const role = canonicalRole(rawReplacement.role, roleByAlias, 'role replacement role');
396
+ const requested = validatePair(rawReplacement.requested, modelByName, effortNames, 'role replacement requested');
397
+ const selected = validatePair(rawReplacement.selected, modelByName, effortNames, 'role replacement selected');
398
+ const key = `${role}|${pairKey(requested)}`;
399
+ if (replacements.has(key)) throw new SubagentModelPolicyError(`ambiguous role replacement: ${key}`);
400
+ if (!isPairApproved(selected, role, approvedPairs)) {
401
+ throw new SubagentModelPolicyError(`role replacement target is not approved: ${key}`);
402
+ }
403
+ replacements.set(key, selected);
404
+ }
405
+ return replacements;
406
+ }
407
+
408
+ function validateReplacementGraph(replacements, roleReplacements, approvedPairKeys) {
409
+ const baseKeys = new Set(replacements.keys());
410
+ for (const [key] of roleReplacements) {
411
+ const pair = key.slice(key.indexOf('|') + 1);
412
+ if (baseKeys.has(pair)) throw new SubagentModelPolicyError(`ambiguous role replacement: ${key}`);
413
+ }
414
+ for (const role of ['worker', 'advisor']) {
415
+ const graph = new Map(replacements);
416
+ for (const [key, selected] of roleReplacements) {
417
+ if (key.startsWith(`${role}|`)) graph.set(key.slice(role.length + 1), selected);
418
+ }
419
+ for (const [requestedKey, selected] of graph) {
420
+ const selectedKey = pairKey(selected);
421
+ if (!approvedPairKeys.has(selectedKey)) {
422
+ throw new SubagentModelPolicyError(`replacement target is not approved: ${selectedKey}`);
423
+ }
424
+ const visited = new Set();
425
+ let currentKey = requestedKey;
426
+ while (graph.has(currentKey)) {
427
+ if (visited.has(currentKey)) {
428
+ throw new SubagentModelPolicyError(`replacement cycle includes: ${currentKey}`);
429
+ }
430
+ visited.add(currentKey);
431
+ currentKey = pairKey(graph.get(currentKey));
432
+ }
433
+ if (visited.size > 1) {
434
+ throw new SubagentModelPolicyError(`replacement destination is not terminal: ${requestedKey}`);
435
+ }
436
+ }
437
+ }
438
+ }
439
+
440
+ function validatePair(rawPair, modelByName, effortNames, label) {
441
+ requireObject(rawPair, label);
442
+ const model = normalizeToken(rawPair.model);
443
+ const effort = normalizeToken(rawPair.effort);
444
+ if (!Object.hasOwn(modelByName, model)) throw new SubagentModelPolicyError(`${label} has unknown model: ${model}`);
445
+ if (!effortNames.has(effort)) throw new SubagentModelPolicyError(`${label} has unknown effort: ${effort}`);
446
+ return { model, effort };
447
+ }
448
+
449
+ function validateCanonicalRoles(rawRoles, roleByAlias, label) {
450
+ if (!Array.isArray(rawRoles) || rawRoles.length === 0) throw new SubagentModelPolicyError(`${label} must be a non-empty array`);
451
+ const roles = rawRoles.map(role => canonicalRole(role, roleByAlias, label));
452
+ if (new Set(roles).size !== roles.length) throw new SubagentModelPolicyError(`${label} must be unique`);
453
+ return roles;
454
+ }
455
+
456
+ function canonicalRole(rawRole, roleByAlias, label) {
457
+ const normalizedRole = normalizeToken(rawRole);
458
+ const role = roleByAlias.get(normalizedRole);
459
+ if (!role) throw new SubagentModelPolicyError(`${label} is unknown: ${normalizedRole}`);
460
+ return role;
461
+ }
462
+
463
+ function resolveRole(request, policy) {
464
+ const suppliedRoles = ROLE_FIELDS
465
+ .filter(field => Object.hasOwn(request, field))
466
+ .map(field => request[field]);
467
+ if (suppliedRoles.length === 0) return { status: 'pass', role: policy.roleByAlias.get(DEFAULT_ROLE) };
468
+ if (suppliedRoles.some(role => typeof role !== 'string' || role.trim() === '')) {
469
+ return blockedRoute('role must be a non-empty string');
470
+ }
471
+ const canonicalRoles = suppliedRoles.map(role => policy.roleByAlias.get(normalizeToken(role)));
472
+ if (canonicalRoles.some(role => !role) || new Set(canonicalRoles).size !== 1) {
473
+ return blockedRoute('role is unknown or ambiguous');
474
+ }
475
+ return { status: 'pass', role: canonicalRoles[0] };
476
+ }
477
+
478
+ function hasTrustedAdvisorSession(policy, options) {
479
+ const metadata = options.trustedSessionMetadata;
480
+ if (!isObject(metadata) || metadata.authorized !== true) return false;
481
+ return typeof metadata.registeredAgentType === 'string'
482
+ && policy.trustedAdvisorRoles.has(normalizeToken(metadata.registeredAgentType));
483
+ }
484
+
485
+ function readModel(request, policy) {
486
+ if (!Object.hasOwn(request, MODEL_FIELD) || request[MODEL_FIELD] === null) {
487
+ return { modelName: null, inherited: true, wasAlias: false, wasMissing: true };
488
+ }
489
+ if (typeof request[MODEL_FIELD] !== 'string' || request[MODEL_FIELD].trim() === '') {
490
+ return blockedRoute('model must be a non-empty string');
491
+ }
492
+ const normalizedModel = normalizeToken(request[MODEL_FIELD]);
493
+ if (policy.inheritanceAliases.has(normalizedModel)) {
494
+ return { modelName: null, inherited: true, wasAlias: true, wasMissing: false };
495
+ }
496
+ const modelName = policy.modelByName.aliasOwner.get(normalizedModel);
497
+ if (!modelName) return blockedRoute(`model is unknown: ${request[MODEL_FIELD]}`);
498
+ return {
499
+ modelName,
500
+ inherited: false,
501
+ wasAlias: request[MODEL_FIELD] !== policy.modelByName[modelName].id,
502
+ wasMissing: false,
503
+ };
504
+ }
505
+
506
+ function readEffort(request, policy) {
507
+ const suppliedFields = EFFORT_FIELDS.filter(field => Object.hasOwn(request, field));
508
+ if (suppliedFields.length > 1) {
509
+ return blockedRoute('effort fields are ambiguous');
510
+ }
511
+ const field = suppliedFields[0] ?? null;
512
+ if (field === null || request[field] === null) {
513
+ return { status: 'pass', effort: null, field, wasAlias: false, wasMissing: true };
514
+ }
515
+ if (typeof request[field] !== 'string' || request[field].trim() === '') {
516
+ return blockedRoute('effort must be a non-empty string');
517
+ }
518
+ const normalizedEffort = normalizeToken(request[field]);
519
+ const canonicalEffort = policy.effortNames.has(normalizedEffort)
520
+ ? normalizedEffort
521
+ : resolveEffortAlias(normalizedEffort, policy);
522
+ if (!canonicalEffort) return blockedRoute(`effort is unknown: ${request[field]}`);
523
+ return {
524
+ status: 'pass',
525
+ effort: canonicalEffort,
526
+ field,
527
+ wasAlias: request[field] !== canonicalEffort,
528
+ wasMissing: false,
529
+ };
530
+ }
531
+
532
+ function resolveEffortAlias(rawEffort, policy) {
533
+ const aliases = policy.efforts.aliases;
534
+ const target = aliases[rawEffort];
535
+ return typeof target === 'string' && policy.effortNames.has(target) ? target : null;
536
+ }
537
+
538
+ function selectReplacement(requestedPair, role, policy) {
539
+ const roleReplacement = policy.roleReplacements.get(`${role}|${pairKey(requestedPair)}`);
540
+ if (roleReplacement) return roleReplacement;
541
+ const replacement = policy.replacements.get(pairKey(requestedPair));
542
+ if (replacement) return replacement;
543
+ return isPairApproved(requestedPair, role, policy.approvedPairs) ? requestedPair : null;
544
+ }
545
+
546
+ function isPairApproved(pair, role, approvedPairs) {
547
+ return approvedPairs.some(approved => pairKey(approved) === pairKey(pair)
548
+ && (approved.roles === null
549
+ || (role === null && approved.roles.includes('worker') && approved.roles.includes('advisor'))
550
+ || (role !== null && approved.roles.includes(role))));
551
+ }
552
+
553
+ function approvedEffortsForModel(modelName, role, approvedPairs) {
554
+ return [...new Set(approvedPairs
555
+ .filter(pair => pair.model === modelName && (pair.roles === null || pair.roles.includes(role)))
556
+ .map(pair => pair.effort))];
557
+ }
558
+
559
+ function validateAvailability(selectedPair, availableModelIds, policy) {
560
+ if (availableModelIds === undefined) return null;
561
+ if (!Array.isArray(availableModelIds) || availableModelIds.some(model => typeof model !== 'string')) {
562
+ return 'available model ids must be an array of strings';
563
+ }
564
+ const availableNames = new Set();
565
+ for (const rawModel of availableModelIds) {
566
+ const modelName = policy.modelByName.aliasOwner.get(normalizeToken(rawModel));
567
+ if (modelName) availableNames.add(modelName);
568
+ }
569
+ return availableNames.has(selectedPair.model)
570
+ ? null
571
+ : `replacement model is unavailable: ${policy.modelByName[selectedPair.model].id}`;
572
+ }
573
+
574
+ function pairKey(pair) {
575
+ return `${pair.model}/${pair.effort}`;
576
+ }
577
+
578
+ function blockedRoute(diagnostic) {
579
+ return {
580
+ status: 'blocked',
581
+ role: null,
582
+ requested: null,
583
+ selected: null,
584
+ selectedModelName: null,
585
+ effortField: null,
586
+ diagnostic,
587
+ };
588
+ }
589
+
590
+ function requireObject(objectCandidate, label) {
591
+ if (!isObject(objectCandidate)) throw new SubagentModelPolicyError(`${label} must be an object`);
592
+ }
593
+
594
+ function validateStringList(stringEntries, label) {
595
+ if (!Array.isArray(stringEntries) || stringEntries.some(entry => typeof entry !== 'string' || entry.trim() === '')) {
596
+ throw new SubagentModelPolicyError(`${label} must be a non-empty string array`);
597
+ }
598
+ return stringEntries;
599
+ }
600
+
601
+ function requireNonEmptyString(stringCandidate, label) {
602
+ if (typeof stringCandidate !== 'string' || stringCandidate.trim() === '') throw new SubagentModelPolicyError(`${label} must be a non-empty string`);
603
+ return stringCandidate;
604
+ }
605
+
606
+ function normalizeToken(token) {
607
+ return typeof token === 'string' ? token.trim().toLowerCase() : '';
608
+ }
609
+
610
+ function isIdentifier(identifier) {
611
+ return /^[a-z][a-z0-9_-]*$/.test(identifier);
612
+ }
613
+
614
+ function isValidatedPolicy(policy) {
615
+ return isObject(policy) && policy[VALIDATED_POLICY_MARKER] === true;
616
+ }
617
+
618
+ function isObject(objectCandidate) {
619
+ return objectCandidate !== null && typeof objectCandidate === 'object' && !Array.isArray(objectCandidate);
620
+ }
@@ -0,0 +1,51 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import test from 'node:test';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const scriptPath = fileURLToPath(new URL('../resolve_advisor_model_route.mjs', import.meta.url));
7
+
8
+ function runBridge(input) {
9
+ return spawnSync(process.execPath, [scriptPath], {
10
+ input,
11
+ encoding: 'utf8',
12
+ });
13
+ }
14
+
15
+ test('advisor bridge selects the policy default', () => {
16
+ const result = runBridge(JSON.stringify({ reasoning_effort: 'xhigh' }));
17
+ assert.equal(result.status, 0);
18
+ assert.deepEqual(JSON.parse(result.stdout).selected, {
19
+ model: 'gpt-6-astra',
20
+ effort: 'medium',
21
+ });
22
+ });
23
+
24
+ test('advisor bridge blocks malformed JSON', () => {
25
+ const result = runBridge('{');
26
+ assert.equal(result.status, 1);
27
+ assert.deepEqual(JSON.parse(result.stdout), {
28
+ status: 'blocked',
29
+ diagnostic: 'advisor route input is not valid JSON',
30
+ });
31
+ });
32
+
33
+ test('advisor bridge blocks non-object input', () => {
34
+ const result = runBridge('[]');
35
+ assert.equal(result.status, 1);
36
+ assert.deepEqual(JSON.parse(result.stdout), {
37
+ status: 'blocked',
38
+ diagnostic: 'advisor route input must be an object',
39
+ });
40
+ });
41
+
42
+ test('advisor bridge blocks invalid policy paths', () => {
43
+ for (const policyPath of [null, {}, '']) {
44
+ const bridgeRun = runBridge(JSON.stringify({ policyPath }));
45
+ assert.equal(bridgeRun.status, 1);
46
+ assert.deepEqual(JSON.parse(bridgeRun.stdout), {
47
+ status: 'blocked',
48
+ diagnostic: 'advisor route policyPath must be a non-empty string',
49
+ });
50
+ }
51
+ });