sneakoscope 10.5.2 → 10.5.4

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 (51) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/command-manifest-lite.js +1 -1
  5. package/dist/cli/router.js +9 -3
  6. package/dist/commands/bridge.js +8 -3
  7. package/dist/config/skills-manifest.json +1 -1
  8. package/dist/core/agents/native-worker-backend-router.js +20 -12
  9. package/dist/core/codex-app/codex-app-restart-policy.js +4 -0
  10. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +15 -1
  11. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +2 -1
  12. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +72 -12
  13. package/dist/core/codex-lb/desktop-controller-v3.js +1 -1
  14. package/dist/core/commands/align-command.js +2 -1
  15. package/dist/core/commands/naruto-command.js +44 -0
  16. package/dist/core/commands/naruto-config-command.js +23 -0
  17. package/dist/core/commands/pipeline-command.js +2 -1
  18. package/dist/core/decisions/child-model-choice.js +2 -1
  19. package/dist/core/decisions/cli.js +9 -2
  20. package/dist/core/hooks-runtime/hook-context.js +4 -4
  21. package/dist/core/hooks-runtime/jev-spawn-routing.js +8 -8
  22. package/dist/core/hooks-runtime/spawn-role-pin-heal.js +1 -1
  23. package/dist/core/hooks-runtime/subagent-context.js +3 -3
  24. package/dist/core/hooks-runtime/subagent-spawn-policy.js +18 -11
  25. package/dist/core/hooks-runtime.js +1 -1
  26. package/dist/core/pipeline-internals/runtime-core.js +2 -1
  27. package/dist/core/routes.js +1 -1
  28. package/dist/core/safety/command-contract/contracts.js +11 -1
  29. package/dist/core/safety/command-contract/types.js +1 -1
  30. package/dist/core/subagents/child-model-allowlist.js +107 -8
  31. package/dist/core/subagents/child-model-plan.js +18 -27
  32. package/dist/core/subagents/model-tiers.js +45 -10
  33. package/dist/core/subagents/naruto-command-args.js +48 -7
  34. package/dist/core/subagents/naruto-command-input-contract.js +40 -4
  35. package/dist/core/subagents/naruto-execution-mode.js +62 -0
  36. package/dist/core/subagents/naruto-help-contract.js +9 -0
  37. package/dist/core/subagents/naruto-host-credentials.js +24 -5
  38. package/dist/core/subagents/official-subagent-config.js +6 -6
  39. package/dist/core/subagents/official-subagent-preparation.js +8 -6
  40. package/dist/core/subagents/official-subagent-prompt.js +11 -12
  41. package/dist/core/subagents/official-subagent-runner.js +5 -8
  42. package/dist/core/subagents/read-only-list-role.js +27 -6
  43. package/dist/core/subagents/role-model-preferences.js +21 -8
  44. package/dist/core/update/update-migration-state.js +10 -0
  45. package/dist/core/version.js +1 -1
  46. package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +20 -4
  47. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -4
  48. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +105 -1
  49. package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +66 -11
  50. package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +43 -22
  51. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { consultJevTurnModel } from '../decisions/integration.js';
2
2
  import { chooseChildModel, fallbackChildModel } from '../decisions/child-model-choice.js';
3
3
  import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-assets-manifest.js';
4
- import { OPENROUTER_ONLY_SCHEMA, effectiveChildModelAllowlist, isSubagentModelEffort } from '../subagents/child-model-allowlist.js';
4
+ import { effectiveChildModelAllowlist, childModelListLabel, childModelListProfile, listChildModelEffort } from '../subagents/child-model-allowlist.js';
5
5
  import { subagentModelProfile } from '../subagents/model-policy.js';
6
6
  import { effortForTier, latestModelForTier, latestTierModelSet } from '../subagents/model-tiers.js';
7
7
  import { readRoleModelPreferences } from '../subagents/role-model-preferences.js';
@@ -52,12 +52,12 @@ export async function jevSpawnRouting(root, state, payload) {
52
52
  if (!input)
53
53
  return { input: null, route: null };
54
54
  const allowlist = effectiveChildModelAllowlist();
55
- if (allowlist.mode === 'openrouter_only')
55
+ if (allowlist.mode !== 'tiers')
56
56
  return openRouterOnlySpawnRouting(root, state, input, allowlist);
57
57
  return { input: await tierSpawnRewrite(root, state, input), route: null };
58
58
  }
59
59
  function listState(allowlist) {
60
- return { schema: OPENROUTER_ONLY_SCHEMA, enabled: true, subagent_models: allowlist.entries, restore: null, updated_at: null };
60
+ return { subagent_models: allowlist.entries };
61
61
  }
62
62
  async function openRouterOnlySpawnRouting(root, parentState, input, allowlist) {
63
63
  if (fullHistoryForkContext(input))
@@ -70,13 +70,14 @@ async function openRouterOnlySpawnRouting(root, parentState, input, allowlist) {
70
70
  const agent = String(input.agent_type || input.agentType || '').trim() || null;
71
71
  const task = spawnTask(input, { items: true });
72
72
  const choice = task
73
- ? await chooseChildModel({ root, task, role: agent, requestedModel: requested, state })
73
+ ? await chooseChildModel({ root, task, role: agent, requestedModel: requested, state, profile: childModelListProfile(allowlist) })
74
74
  .catch(() => fallbackChildModel(state, requested, 'consult_failed'))
75
75
  : fallbackChildModel(state, requested, 'empty_task');
76
76
  if (!choice)
77
77
  return { input: null, route: null };
78
78
  const route = {
79
- mode: 'openrouter_only',
79
+ mode: allowlist.mode,
80
+ ...(allowlist.mode === 'configured' ? { profile: allowlist.profile } : {}),
80
81
  model: choice.entry.model,
81
82
  source: choice.source,
82
83
  reason: choice.reason
@@ -90,8 +91,7 @@ async function openRouterOnlySpawnRouting(root, parentState, input, allowlist) {
90
91
  else
91
92
  delete next.agent_type;
92
93
  }
93
- const effort = choice.entry.reasoning_effort
94
- || (isSubagentModelEffort(input.reasoning_effort) ? input.reasoning_effort : null);
94
+ const effort = listChildModelEffort(choice.entry, input.reasoning_effort);
95
95
  if (effort)
96
96
  next.reasoning_effort = effort;
97
97
  else
@@ -149,5 +149,5 @@ export function openRouterOnlyJevTurnLine(allowlist, effort, orchestrationRequir
149
149
  const list = allowlist.entries.length
150
150
  ? `every spawn_agent child runs a model from the user's subagent list (${renderChildModelList(allowlist)}); Jev picks the list model for each spawn from the user's criteria, and any other model is denied.`
151
151
  : `the subagent model list is empty, so spawn_agent is denied until the user adds models in ${SUBAGENT_MODELS_SETTINGS_HINT}.`;
152
- return `${hint}SKS OpenRouter Only mode: ${list}`;
152
+ return `${hint}SKS ${childModelListLabel(allowlist)} mode: ${list}`;
153
153
  }
@@ -8,7 +8,7 @@ export async function healStaleRolePinForSpawn(payload, root) {
8
8
  const agent = String(input.agent_type || input.agentType || '').trim();
9
9
  if (!agent || !stalePinForAgentType(agent, { root }))
10
10
  return false;
11
- if (effectiveChildModelAllowlist().mode === 'openrouter_only')
11
+ if (effectiveChildModelAllowlist().mode !== 'tiers')
12
12
  return false;
13
13
  await refreshStaleManagedRolePins({ root });
14
14
  return true;
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { readJson } from '../fsx.js';
3
3
  import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-assets-manifest.js';
4
4
  import { latestModelForTier } from '../subagents/model-tiers.js';
5
- import { effectiveChildModelAllowlist } from '../subagents/child-model-allowlist.js';
5
+ import { effectiveChildModelAllowlist, childModelListLabel } from '../subagents/child-model-allowlist.js';
6
6
  import { jevEnabled, readDecisionConfig } from '../decisions/config.js';
7
7
  import { renderChildModelList } from './subagent-spawn-policy.js';
8
8
  export async function sealedSubagentRoutingContext(artifactDir, payload = {}) {
@@ -16,11 +16,11 @@ export async function sealedSubagentRoutingContext(artifactDir, payload = {}) {
16
16
  if (!agentName)
17
17
  return '';
18
18
  const allowlist = effectiveChildModelAllowlist();
19
- if (allowlist.mode === 'openrouter_only') {
19
+ if (allowlist.mode !== 'tiers') {
20
20
  return [
21
21
  'SKS sealed child routing:',
22
22
  `- custom agent: ${agentName}`,
23
- `- model and model_reasoning_effort: sealed on the spawn call from the OpenRouter Only subagent list (${renderChildModelList(allowlist) || 'empty'})`,
23
+ `- model and model_reasoning_effort: sealed on the spawn call from the ${childModelListLabel(allowlist)} subagent list (${renderChildModelList(allowlist) || 'empty'})`,
24
24
  '- keep this sealed profile; do not retarget model/effort or spawn nested agents'
25
25
  ].join('\n');
26
26
  }
@@ -1,29 +1,30 @@
1
1
  import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-assets-manifest.js';
2
- import { READ_ONLY_LIST_ROLE } from '../subagents/read-only-list-role.js';
2
+ import { READ_ONLY_LIST_ROLE, readOnlyListRoleInstalled } from '../subagents/read-only-list-role.js';
3
3
  import { stalePinBlockReason, stalePinForAgentType } from '../subagents/role-model-pins.js';
4
4
  import { isSpawnAgentToolName, spawnPayloadToolName } from './spawn-tool-name.js';
5
- import { effectiveChildModelAllowlist, isAllowedChildModel } from '../subagents/child-model-allowlist.js';
5
+ import { effectiveChildModelAllowlist, childModelListLabel, isAllowedChildModel } from '../subagents/child-model-allowlist.js';
6
6
  export const SUBAGENT_MODELS_SETTINGS_HINT = 'SKS Control Center > Subagent Models';
7
7
  export function renderChildModelList(allowlist) {
8
- if (allowlist.mode !== 'openrouter_only')
8
+ if (allowlist.mode === 'tiers')
9
9
  return allowlist.models.join(', ');
10
10
  return allowlist.entries.map((entry) => `${entry.model}${entry.default ? ' (default)' : ''}`).join(', ');
11
11
  }
12
12
  export function renderChildModelCriteria(allowlist) {
13
- if (allowlist.mode !== 'openrouter_only')
13
+ if (allowlist.mode === 'tiers')
14
14
  return allowlist.models.join(', ');
15
15
  return allowlist.entries
16
16
  .map((entry) => `${entry.model}${entry.default ? ' (default)' : ''}: ${entry.criteria || 'general work'}`)
17
17
  .join('; ');
18
18
  }
19
19
  function openRouterOnlyBlockReason(allowlist) {
20
- if (!allowlist.models.length && allowlist.mode === 'openrouter_only' && allowlist.unroutable?.length) {
21
- return `SKS OpenRouter Only mode: no model on the subagent list has an OpenRouter route right now (${allowlist.unroutable.join(', ')}), so no child may be spawned. Ask the user to replace them in ${SUBAGENT_MODELS_SETTINGS_HINT}, then retry spawn_agent. Do not implement the slice in the parent instead.`;
20
+ const label = childModelListLabel(allowlist);
21
+ if (!allowlist.models.length && allowlist.unroutable?.length) {
22
+ return `SKS ${label} mode: no model on the subagent list is available with its saved effort right now (${allowlist.unroutable.join(', ')}), so no child may be spawned. Ask the user to replace them in ${SUBAGENT_MODELS_SETTINGS_HINT}, then retry spawn_agent. Do not implement the slice in the parent instead.`;
22
23
  }
23
24
  if (!allowlist.models.length) {
24
- return `SKS OpenRouter Only mode is on but the subagent model list is empty, so no child may be spawned. Ask the user to add models in ${SUBAGENT_MODELS_SETTINGS_HINT} (or to turn OpenRouter Only off), then retry spawn_agent. Do not implement the slice in the parent instead.`;
25
+ return `SKS ${label} mode: the subagent model list is empty or unavailable, so no child may be spawned. Ask the user to review the list in ${SUBAGENT_MODELS_SETTINGS_HINT}, then retry spawn_agent. Do not implement the slice in the parent instead.`;
25
26
  }
26
- return `SKS OpenRouter Only mode: children may run only a model on the user's subagent list: ${renderChildModelList(allowlist)}. Every other model is denied. Retry spawn_agent with one of them (the SKS hook routes each spawn to a list model, by Jev when Jev mode is on) and fork_turns="none" or a positive bounded turn count. Include the complete slice contract in message; do not inherit the parent model. The user adds models in ${SUBAGENT_MODELS_SETTINGS_HINT}.`;
27
+ return `SKS ${label} mode: children may run only a model on the user's subagent list: ${renderChildModelList(allowlist)}. Every other model is denied. Retry spawn_agent with one of them (the SKS hook routes each spawn to a list model, by Jev when Jev mode is on) and fork_turns="none" or a positive bounded turn count. Include the complete slice contract in message; do not inherit the parent model. The user adds models in ${SUBAGENT_MODELS_SETTINGS_HINT}.`;
27
28
  }
28
29
  export function fullHistoryForkContext(input) {
29
30
  return input.fork_context === true || String(input.fork_context ?? '').trim().toLowerCase() === 'true';
@@ -37,16 +38,22 @@ export function subagentSpawnPolicyBlockReason(payload = {}, opts = {}) {
37
38
  return null;
38
39
  const input = payload.tool_input || payload.toolInput || payload.tool?.input || {};
39
40
  const allowlist = effectiveChildModelAllowlist();
40
- if (allowlist.mode !== 'openrouter_only' && opts.narutoParent === false)
41
+ if (allowlist.mode === 'tiers' && opts.narutoParent === false)
41
42
  return null;
42
- if (allowlist.mode === 'openrouter_only') {
43
+ if (allowlist.mode !== 'tiers') {
43
44
  if (!isAllowedChildModel(input.model, allowlist))
44
45
  return openRouterOnlyBlockReason(allowlist);
45
46
  if (fullHistoryForkContext(input))
46
47
  return `${FORK_BLOCK_REASON} Omit fork_context (fork_context=true is a full-history fork).`;
47
48
  const agent = String(input.agent_type || input.agentType || '').trim();
49
+ if (allowlist.mode === 'configured' && agent && agent !== READ_ONLY_LIST_ROLE.codex_name) {
50
+ return `SKS ${childModelListLabel(allowlist)} subagent list: agent_type "${agent}" may pin its own model or effort, which Codex uses instead of the spawn values. Omit agent_type and include the role brief in message; read-only slices must use agent_type="${READ_ONLY_LIST_ROLE.codex_name}".`;
51
+ }
52
+ if (allowlist.mode === 'configured' && agent === READ_ONLY_LIST_ROLE.codex_name && opts.root && !readOnlyListRoleInstalled(opts.root)) {
53
+ return `SKS could not verify the model-less read-only role for this project. A missing role or custom role/config override must be resolved before retrying ${READ_ONLY_LIST_ROLE.codex_name}; do not remove the role to bypass its read-only sandbox.`;
54
+ }
48
55
  if (agent && managedOfficialSubagentRoleByName(agent)) {
49
- return `SKS OpenRouter Only mode: agent_type "${agent}" pins a tier model, so it cannot run a list model. Omit agent_type and put the role brief in message; read-only slices pass agent_type="${READ_ONLY_LIST_ROLE.codex_name}".`;
56
+ return `SKS ${childModelListLabel(allowlist)} mode: agent_type "${agent}" pins a tier model, so it cannot run a list model. Omit agent_type and put the role brief in message; read-only slices pass agent_type="${READ_ONLY_LIST_ROLE.codex_name}".`;
50
57
  }
51
58
  }
52
59
  else {
@@ -310,7 +310,7 @@ async function attachJevTurnRouting(root, payload, result, orchestrationRequired
310
310
  : `Jev rated this turn as ${decision.effort}-effort work (${decision.model} tier). Treat it as this turn's reasoning hint; the parent model, effort, and service tier stay as the user set them.`
311
311
  : '';
312
312
  const allowlist = effectiveChildModelAllowlist();
313
- const line = allowlist.mode === 'openrouter_only'
313
+ const line = allowlist.mode !== 'tiers'
314
314
  ? openRouterOnlyJevTurnLine(allowlist, decision.effort, orchestrationRequired)
315
315
  : tierLine;
316
316
  if (!line)
@@ -29,6 +29,7 @@ import { classifyTaskProfile, gateProfileForTask } from '../runtime/task-profile
29
29
  import { chooseVerificationBudget } from '../runtime/verification-budget.js';
30
30
  import { stopFinalizationRitualsEnforced } from '../verification-profile.js';
31
31
  import { HARD_NARUTO_MAX_THREADS } from '../subagents/thread-budget.js';
32
+ import { narutoUsesCurrentSession } from '../subagents/naruto-execution-mode.js';
32
33
  import { NARUTO_GATE_FILENAME, NARUTO_SUMMARY_FILENAME, SUBAGENT_PLAN_FILENAME, prepareOfficialSubagentMission } from '../subagents/official-subagent-preparation.js';
33
34
  import { buildSubagentEvidence, readSubagentEvents, recordSubagentEvent, SUBAGENT_EVIDENCE_FILENAME, SUBAGENT_EVENT_LOG_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME } from '../subagents/subagent-evidence.js';
34
35
  import { effectiveSubagentTarget, normalizeLegacySubagentCountFields, subagentCountContractBlockers } from '../subagents/wave-lifecycle.js';
@@ -601,7 +602,7 @@ export async function prepareRoute(root, prompt, state = {}, opts = {}) {
601
602
  ...selectedRoute,
602
603
  requiredSkills: managedSkillNamesForPrompt(selectedRoute, cleanPrompt)
603
604
  } : null;
604
- const codexThreadId = String(process.env.CODEX_THREAD_ID || '').trim();
605
+ const codexThreadId = narutoUsesCurrentSession() ? String(process.env.CODEX_THREAD_ID || '').trim() : '';
605
606
  const sessionKey = opts.sessionKey || codexThreadId || state?._session_key || null;
606
607
  const madSksAuthorization = hasMadSksSignal(cleanPrompt);
607
608
  const task = stripDollarCommand(stripMadSksSignal(cleanPrompt)) || stripMadSksSignal(stripDollarCommand(cleanPrompt)) || String(cleanPrompt || '').trim();
@@ -206,7 +206,7 @@ export const ROUTES = [
206
206
  reasoningPolicy: 'high',
207
207
  stopGate: 'naruto-gate.json',
208
208
  coverage_required: true,
209
- cliEntrypoint: 'sks naruto run "task" [--agents N] [--max-threads N] [--trusted-project] | sks naruto status|subagents|proof | sks naruto parent-summary --mission M-... --stdin',
209
+ cliEntrypoint: 'sks naruto run "task" [--agents N] [--max-threads N] [--trusted-project] | sks naruto status|subagents|proof | sks naruto execution status | sks naruto execution set --mode auto|current-session|standalone [--restart] | sks naruto parent-summary --mission M-... --stdin',
210
210
  examples: ['$Naruto run review twelve independent packages with --agents 12', '$Work']
211
211
  },
212
212
  {
@@ -4,12 +4,22 @@ const ARGUMENT_BUILDERS = {
4
4
  none: () => [],
5
5
  'json-only': jsonFlag,
6
6
  naruto: (input) => {
7
+ const action = stringValue(input.action, input.prompt || input.task ? 'run' : 'help');
8
+ if (action === 'execution') {
9
+ const executionCommand = stringValue(input.execution_command, 'status');
10
+ return [
11
+ 'execution',
12
+ ...(executionCommand === 'status' ? [] : [executionCommand]),
13
+ ...valueFlag(input, 'mode', '--mode'),
14
+ ...booleanFlag(input, 'restart', '--restart'),
15
+ ...jsonFlag(input)
16
+ ];
17
+ }
7
18
  const task = typeof input.prompt === 'string'
8
19
  ? input.prompt
9
20
  : typeof input.task === 'string'
10
21
  ? input.task
11
22
  : '';
12
- const action = stringValue(input.action, task ? 'run' : 'help');
13
23
  return [
14
24
  action,
15
25
  ...(task ? [task] : []),
@@ -1 +1 @@
1
- export const NARUTO_ACTIONS = ['run', 'status', 'subagents', 'proof', 'parent-summary', 'help'];
1
+ export const NARUTO_ACTIONS = ['run', 'status', 'subagents', 'proof', 'parent-summary', 'execution', 'help'];
@@ -1,14 +1,20 @@
1
1
  import fs from 'node:fs';
2
+ import fsp from 'node:fs/promises';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { nowIso, writeJsonAtomic } from '../fsx.js';
5
6
  import { isOpenRouterModelId } from '../imagegen/imagegen-config.js';
6
- import { latestTierModelSet } from './model-tiers.js';
7
+ import { latestTierModelSet, selectableChildModels } from './model-tiers.js';
8
+ import { normalizeCodexModelId } from '../codex-app/codex-model-catalog.js';
9
+ import { withFileLock } from '../locks/file-lock.js';
10
+ import { ensureConfinedDirectory, inspectConfinedPath } from '../managed-path-safety.js';
7
11
  export const OPENROUTER_ONLY_SCHEMA = 'sks.openrouter-only.v1';
8
12
  export const OPENROUTER_ONLY_FILENAME = 'sks-openrouter-only.json';
9
13
  export const MAX_SUBAGENT_MODELS = 16;
10
14
  export const MAX_SUBAGENT_CRITERIA_CHARS = 240;
11
15
  export const SUBAGENT_MODEL_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
16
+ export const NATIVE_SUBAGENT_MODEL_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
17
+ export const SUBAGENT_MODEL_LISTS_SCHEMA = 'sks.subagent-model-lists.v1';
12
18
  export function openRouterOnlyStatePath(input = {}) {
13
19
  const env = input.env || process.env;
14
20
  return path.join(input.home || env.HOME || os.homedir(), '.codex', 'sks', OPENROUTER_ONLY_FILENAME);
@@ -19,8 +25,9 @@ export function defaultOpenRouterOnlyState() {
19
25
  export function canonicalChildModelId(value) {
20
26
  return String(value ?? '').trim().toLowerCase();
21
27
  }
22
- export function isSubagentModelEffort(value) {
23
- return typeof value === 'string' && SUBAGENT_MODEL_EFFORTS.includes(value);
28
+ export function isSubagentModelEffort(value, profile = 'openrouter_only') {
29
+ const efforts = profile === 'openrouter_only' ? SUBAGENT_MODEL_EFFORTS : NATIVE_SUBAGENT_MODEL_EFFORTS;
30
+ return typeof value === 'string' && efforts.includes(value);
24
31
  }
25
32
  function cleanCriteria(value) {
26
33
  return String(value ?? '')
@@ -29,7 +36,7 @@ function cleanCriteria(value) {
29
36
  .trim()
30
37
  .slice(0, MAX_SUBAGENT_CRITERIA_CHARS);
31
38
  }
32
- export function normalizeSubagentModelList(raw) {
39
+ export function normalizeSubagentModelList(raw, profile = 'openrouter_only') {
33
40
  const rows = Array.isArray(raw) ? raw : [];
34
41
  const issues = [];
35
42
  const entries = [];
@@ -37,7 +44,7 @@ export function normalizeSubagentModelList(raw) {
37
44
  rows.forEach((row, index) => {
38
45
  const record = row && typeof row === 'object' ? row : {};
39
46
  const model = String(record.model ?? '').trim();
40
- if (!isOpenRouterModelId(model)) {
47
+ if (!(profile === 'openrouter_only' ? isOpenRouterModelId(model) : normalizeCodexModelId(model))) {
41
48
  issues.push({ index, code: 'subagent_model_id_invalid' });
42
49
  return;
43
50
  }
@@ -47,7 +54,7 @@ export function normalizeSubagentModelList(raw) {
47
54
  return;
48
55
  }
49
56
  const effort = record.reasoning_effort ?? null;
50
- if (effort !== null && effort !== '' && !isSubagentModelEffort(effort)) {
57
+ if (effort !== null && effort !== '' && !isSubagentModelEffort(effort, profile)) {
51
58
  issues.push({ index, code: 'subagent_model_effort_invalid' });
52
59
  return;
53
60
  }
@@ -57,9 +64,9 @@ export function normalizeSubagentModelList(raw) {
57
64
  }
58
65
  seen.add(key);
59
66
  entries.push({
60
- model: key,
67
+ model: profile === 'openrouter_only' ? key : model,
61
68
  criteria: cleanCriteria(record.criteria),
62
- reasoning_effort: isSubagentModelEffort(effort) ? effort : null,
69
+ reasoning_effort: isSubagentModelEffort(effort, profile) ? effort : null,
63
70
  default: record.default === true
64
71
  });
65
72
  });
@@ -116,6 +123,78 @@ export function subagentEntryForModel(state, model) {
116
123
  return null;
117
124
  return state.subagent_models.find((entry) => canonicalChildModelId(entry.model) === key) || null;
118
125
  }
126
+ export function childModelListProfile(list) {
127
+ return list.mode === 'openrouter_only' ? 'openrouter_only' : list.profile;
128
+ }
129
+ export function childModelListLabel(list) {
130
+ return list.mode === 'openrouter_only' ? 'OpenRouter Only' : list.profile === 'codex_lb' ? 'Codex-LB' : 'OpenAI OAuth';
131
+ }
132
+ export function listChildModelEffort(entry, requested) {
133
+ const supported = entry.supported_reasoning_efforts ?? SUBAGENT_MODEL_EFFORTS;
134
+ const effort = entry.reasoning_effort || String(requested || '');
135
+ return supported.includes(effort) && isSubagentModelEffort(effort, 'openai') ? effort : null;
136
+ }
137
+ export function subagentModelListsPath(input = {}) {
138
+ return path.join(path.dirname(openRouterOnlyStatePath(input)), 'sks-subagent-model-lists.json');
139
+ }
140
+ export function nativeSubagentModelProfile(input = {}) {
141
+ if (input.nativeProfile)
142
+ return input.nativeProfile;
143
+ const file = path.join(path.dirname(openRouterOnlyStatePath(input)), 'desktop-bridge-settings.json');
144
+ try {
145
+ if (fs.statSync(file).size > 4 * 1024 * 1024)
146
+ return 'openai';
147
+ return JSON.parse(fs.readFileSync(file, 'utf8'))?.auth_priority_enabled === true ? 'codex_lb' : 'openai';
148
+ }
149
+ catch {
150
+ return 'openai';
151
+ }
152
+ }
153
+ export function readNativeSubagentModelStore(input = {}) {
154
+ const file = subagentModelListsPath(input);
155
+ const empty = { schema: SUBAGENT_MODEL_LISTS_SCHEMA, profiles: { codex_lb: [], openai: [] }, updated_at: null };
156
+ try {
157
+ if (fs.statSync(file).size > 256 * 1024)
158
+ throw new Error('oversized');
159
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
160
+ if (raw?.schema !== SUBAGENT_MODEL_LISTS_SCHEMA || !raw.profiles || Array.isArray(raw.profiles))
161
+ throw new Error('schema');
162
+ for (const profile of ['codex_lb', 'openai']) {
163
+ if (!Array.isArray(raw.profiles[profile]))
164
+ throw new Error('profile');
165
+ const normalized = normalizeSubagentModelList(raw.profiles[profile], profile);
166
+ if (normalized.issues.length)
167
+ throw new Error('entries');
168
+ empty.profiles[profile] = normalized.entries;
169
+ }
170
+ empty.updated_at = typeof raw.updated_at === 'string' ? raw.updated_at : null;
171
+ return { path: file, store: empty, blockers: [] };
172
+ }
173
+ catch (error) {
174
+ return { path: file, store: empty, blockers: error?.code === 'ENOENT' ? [] : ['subagent_model_lists_unreadable'] };
175
+ }
176
+ }
177
+ export async function writeNativeSubagentModels(profile, raw, input = {}) {
178
+ const { entries, issues } = normalizeSubagentModelList(raw, profile);
179
+ if (!Array.isArray(raw) || issues.length)
180
+ throw new Error('subagent_models_payload_invalid');
181
+ const file = subagentModelListsPath(input);
182
+ const root = path.dirname(path.dirname(file));
183
+ await fsp.mkdir(root, { recursive: true });
184
+ await ensureConfinedDirectory(root, path.dirname(file));
185
+ await withFileLock({ lockPath: `${file}.lock`, timeoutMs: 5_000, staleMs: 30_000 }, async () => {
186
+ if ((await inspectConfinedPath(root, file)).leafSymlink)
187
+ throw new Error('subagent_model_lists_unsafe_path');
188
+ const current = readNativeSubagentModelStore(input);
189
+ if (current.blockers.length)
190
+ throw new Error(current.blockers[0]);
191
+ await writeJsonAtomic(file, {
192
+ ...current.store,
193
+ profiles: { ...current.store.profiles, [profile]: entries },
194
+ updated_at: nowIso()
195
+ }, { mode: 0o600 });
196
+ });
197
+ }
119
198
  function openRouterRoutedModels(input) {
120
199
  const env = input.env || process.env;
121
200
  const file = path.join(input.home || env.HOME || os.homedir(), '.codex', 'sks', 'sks-bridge-route-policy.json');
@@ -158,6 +237,26 @@ export function effectiveChildModelAllowlist(input = {}) {
158
237
  unroutable
159
238
  };
160
239
  }
240
+ const profile = nativeSubagentModelProfile(input);
241
+ const saved = readNativeSubagentModelStore(input);
242
+ const selected = saved.blockers.length ? [] : saved.store.profiles[profile];
243
+ if (selected.length || saved.blockers.length) {
244
+ const available = selectableChildModels(profile, input);
245
+ const entries = selected.flatMap((entry) => {
246
+ const model = available.find((row) => canonicalChildModelId(row.public_id) === canonicalChildModelId(entry.model));
247
+ return model && (!entry.reasoning_effort || model.reasoning_efforts.includes(entry.reasoning_effort))
248
+ ? [{ ...entry, model: model.public_id, supported_reasoning_efforts: model.reasoning_efforts }]
249
+ : [];
250
+ });
251
+ const preferred = entries.findIndex((entry) => entry.default);
252
+ entries.forEach((entry, index) => { entry.default = index === (preferred < 0 ? 0 : preferred); });
253
+ return {
254
+ mode: 'configured', profile, models: entries.map((entry) => entry.model), entries,
255
+ default_model: defaultSubagentEntry({ subagent_models: entries })?.model ?? null,
256
+ unroutable: selected.filter((row) => !entries.some((entry) => canonicalChildModelId(entry.model) === canonicalChildModelId(row.model))).map((row) => row.model),
257
+ blockers: saved.blockers
258
+ };
259
+ }
161
260
  return { mode: 'tiers', models: [...latestTierModelSet(input)], entries: [], default_model: null };
162
261
  }
163
262
  export function isAllowedChildModel(model, allowlist) {
@@ -1,40 +1,29 @@
1
1
  import { chooseChildModels } from '../decisions/child-model-choice.js';
2
2
  import { MAX_JEV_ROUTING_ROLES } from '../decisions/routing.js';
3
- import { SUBAGENT_MODEL_EFFORTS, defaultSubagentEntry, readOpenRouterOnlyStateSync, routableSubagentState } from './child-model-allowlist.js';
4
- import { latestTierModelSet } from './model-tiers.js';
3
+ import { defaultSubagentEntry, effectiveChildModelAllowlist, childModelListProfile, listChildModelEffort } from './child-model-allowlist.js';
5
4
  import { readCodexMainModel } from './naruto-host-credentials.js';
6
5
  import { READ_ONLY_LIST_ROLE } from './read-only-list-role.js';
7
6
  export function readChildModelPlanContext(location = {}) {
8
- const stored = readOpenRouterOnlyStateSync(location);
9
- if (!stored.enabled) {
10
- return { allowlist: { mode: 'tiers', models: [...latestTierModelSet(location)], entries: [], default_model: null }, list: null };
11
- }
12
- const { state, unroutable } = routableSubagentState(stored, location);
13
- const allowlist = {
14
- mode: 'openrouter_only',
15
- models: state.subagent_models.map((entry) => entry.model),
16
- entries: state.subagent_models,
17
- default_model: defaultSubagentEntry(state)?.model ?? null,
18
- unroutable
19
- };
7
+ const allowlist = effectiveChildModelAllowlist(location);
8
+ if (allowlist.mode === 'tiers')
9
+ return { allowlist, list: null };
10
+ const state = { subagent_models: allowlist.entries };
20
11
  return { allowlist, list: { state, allowlist, mainModel: readCodexMainModel(location) } };
21
12
  }
22
13
  export function listEntryEffort(entry, roleEffort) {
23
- if (entry.reasoning_effort)
24
- return entry.reasoning_effort;
25
- const effort = String(roleEffort || '');
26
- return SUBAGENT_MODEL_EFFORTS.includes(effort) ? effort : null;
14
+ return listChildModelEffort(entry, roleEffort);
27
15
  }
28
- function routedRow(row, choice) {
16
+ function routedRow(row, choice, list) {
17
+ const prefix = list.mode === 'openrouter_only' ? 'openrouter_only' : 'configured_list';
29
18
  return {
30
19
  ...row,
31
- routed_provider: 'openrouter',
20
+ routed_provider: list.mode === 'openrouter_only' ? 'openrouter' : list.profile === 'codex_lb' ? 'codex-lb' : 'openai',
32
21
  routed_model: choice?.entry.model ?? null,
33
22
  routed_model_reasoning_effort: choice ? listEntryEffort(choice.entry, row.model_reasoning_effort) : null,
34
- routed_model_policy: choice?.source === 'jev' ? 'openrouter_only_jev' : 'openrouter_only_default',
23
+ routed_model_policy: `${prefix}_${choice?.source === 'jev' ? 'jev' : 'default'}`,
35
24
  routing_dynamic: true,
36
- role_model_preference_source: 'ignored_openrouter_only',
37
- openrouter_only_choice: choice
25
+ role_model_preference_source: `ignored_${prefix}`,
26
+ [list.mode === 'openrouter_only' ? 'openrouter_only_choice' : 'subagent_list_choice']: choice
38
27
  ? { model: choice.entry.model, source: choice.source, reason: choice.reason, default_entry: choice.entry.default }
39
28
  : { model: null, source: 'default', reason: 'subagent_list_empty', default_entry: false }
40
29
  };
@@ -42,7 +31,7 @@ function routedRow(row, choice) {
42
31
  export function listBaselineAgentRouting(agentRouting, list) {
43
32
  const fallback = defaultSubagentEntry(list.state);
44
33
  const choice = fallback ? { entry: fallback, source: 'default', reason: 'baseline' } : null;
45
- return Object.fromEntries(Object.entries(agentRouting).map(([name, row]) => [name, routedRow(row, choice)]));
34
+ return Object.fromEntries(Object.entries(agentRouting).map(([name, row]) => [name, routedRow(row, choice, list.allowlist)]));
46
35
  }
47
36
  export function childModelLanes(input) {
48
37
  const assigned = new Set(input.slices.map((slice) => String(slice.agent || '')).filter(Boolean));
@@ -65,6 +54,7 @@ export async function chooseListRoleModels(input) {
65
54
  goal: input.goal || 'Plan official subagent roles.',
66
55
  lanes,
67
56
  state: input.list.state,
57
+ profile: childModelListProfile(input.list.allowlist),
68
58
  ...(input.env ? { env: input.env } : {})
69
59
  });
70
60
  return { lanes: lanes.map((lane) => lane.id), choices };
@@ -94,8 +84,8 @@ export function applyListRoleModels(agents, chosen, list, readOnlyRoleInstalled
94
84
  for (const [name, row] of Object.entries(agents)) {
95
85
  const choice = chosen?.choices[name]
96
86
  ?? (fallback ? { entry: fallback, source: 'default', reason: routed.has(name) ? 'keep_baseline' : capped } : null);
97
- next[name] = routedRow(row, choice);
98
- roles[name] = next[name].openrouter_only_choice;
87
+ next[name] = routedRow(row, choice, list.allowlist);
88
+ roles[name] = next[name].openrouter_only_choice ?? next[name].subagent_list_choice;
99
89
  if (choice?.source === 'jev')
100
90
  jevModels[name] = choice.entry.model;
101
91
  }
@@ -104,6 +94,7 @@ export function applyListRoleModels(agents, chosen, list, readOnlyRoleInstalled
104
94
  jevModels,
105
95
  evidence: {
106
96
  enabled: true,
97
+ ...(list.allowlist.mode === 'configured' ? { profile: list.allowlist.profile } : {}),
107
98
  main_model: list.mainModel,
108
99
  default_subagent_model: fallback?.model ?? null,
109
100
  subagent_models: list.state.subagent_models.map((entry) => ({ ...entry })),
@@ -111,7 +102,7 @@ export function applyListRoleModels(agents, chosen, list, readOnlyRoleInstalled
111
102
  jev_decided_roles: Object.keys(jevModels),
112
103
  roles,
113
104
  read_only_role: { name: READ_ONLY_LIST_ROLE.codex_name, installed: readOnlyRoleInstalled },
114
- warnings: readOnlyRoleInstalled === false ? ['openrouter_only_read_only_role_missing'] : []
105
+ warnings: readOnlyRoleInstalled === false ? [`${list.allowlist.mode === 'openrouter_only' ? 'openrouter_only' : 'subagent_list'}_read_only_role_missing`] : []
115
106
  }
116
107
  };
117
108
  }
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { codexHomePath, readTopLevelTomlString } from '../codex-app/codex-model-catalog.js';
5
+ import { codexHomePath, inferProviderFromModel, normalizeCodexModelId, readTopLevelTomlString } from '../codex-app/codex-model-catalog.js';
6
6
  const MODELS_CACHE_FILENAME = 'models_cache.json';
7
7
  function modelsCachePath(input) {
8
8
  return path.join(codexHomePath(input), MODELS_CACHE_FILENAME);
@@ -150,19 +150,30 @@ export function effortForTier(tier, resolved = resolveLatestModelTiers()) {
150
150
  const byDistance = [...order].sort((a, b) => Math.abs(order.indexOf(a) - index) - Math.abs(order.indexOf(b) - index));
151
151
  return byDistance.find((effort) => supported.includes(effort)) || wanted;
152
152
  }
153
- function readCandidateRows(file) {
153
+ function readModelRows(file) {
154
154
  let parsed;
155
155
  try {
156
+ const stat = fs.statSync(file);
157
+ if (!stat.isFile() || stat.size > 16 * 1024 * 1024)
158
+ return [];
156
159
  parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
157
160
  }
158
161
  catch {
159
162
  return [];
160
163
  }
161
164
  const models = Array.isArray(parsed?.models) ? parsed.models : Array.isArray(parsed) ? parsed : [];
165
+ return models.slice(0, 1024).filter((row) => row && typeof row === 'object' && !Array.isArray(row));
166
+ }
167
+ function modelEfforts(row) {
168
+ return Array.isArray(row.supported_reasoning_levels)
169
+ ? [...new Set(row.supported_reasoning_levels
170
+ .map((level) => String(typeof level === 'string' ? level : level?.effort || '').trim().toLowerCase())
171
+ .filter(Boolean))]
172
+ : [];
173
+ }
174
+ function readCandidateRows(file) {
162
175
  const rows = [];
163
- for (const row of models.slice(0, 1024)) {
164
- if (!row || typeof row !== 'object')
165
- continue;
176
+ for (const row of readModelRows(file)) {
166
177
  const slug = String(row.slug || row.model || row.id || '').trim();
167
178
  const id = parseGptModelId(slug);
168
179
  if (!id)
@@ -170,15 +181,39 @@ function readCandidateRows(file) {
170
181
  const visibility = String(row.visibility || 'list').toLowerCase();
171
182
  if (visibility === 'hide' || visibility === 'hidden')
172
183
  continue;
173
- const efforts = Array.isArray(row.supported_reasoning_levels)
174
- ? row.supported_reasoning_levels
175
- .map((level) => String(typeof level === 'string' ? level : level?.effort || '').trim().toLowerCase())
176
- .filter(Boolean)
177
- : [];
184
+ const efforts = modelEfforts(row);
178
185
  rows.push({ slug, version: id.version, family: id.family, efforts });
179
186
  }
180
187
  return rows;
181
188
  }
189
+ export function selectableChildModels(profile, input = {}) {
190
+ const file = profile === 'codex_lb' ? configuredCatalogPath(input) : modelsCachePath(input);
191
+ if (!file)
192
+ return [];
193
+ const models = [];
194
+ const seen = new Set();
195
+ for (const row of readModelRows(file)) {
196
+ const model = normalizeCodexModelId(row.slug || row.public_id || row.model || row.id);
197
+ const provider = String(row.provider_id || row.provider || inferProviderFromModel(model));
198
+ if (!model || seen.has(model.toLowerCase()) || ['hide', 'hidden'].includes(String(row.visibility).toLowerCase())
199
+ || row.multi_agent_version === 'disabled'
200
+ || (profile === 'openai' && model.includes('/'))
201
+ || provider !== (profile === 'codex_lb' ? 'codex-lb' : 'openai'))
202
+ continue;
203
+ seen.add(model.toLowerCase());
204
+ models.push({ public_id: model, display_name: String(row.display_name || model), reasoning_efforts: modelEfforts(row) });
205
+ }
206
+ const newest = new Map();
207
+ for (const row of models) {
208
+ const id = parseGptModelId(row.public_id);
209
+ if (id && (!newest.has(id.family) || compareModelVersions(id.version, newest.get(id.family)) > 0))
210
+ newest.set(id.family, id.version);
211
+ }
212
+ return models.filter((row) => {
213
+ const id = parseGptModelId(row.public_id);
214
+ return !id || compareModelVersions(id.version, newest.get(id.family)) === 0;
215
+ });
216
+ }
182
217
  export function compareModelVersions(a, b) {
183
218
  const length = Math.max(a.length, b.length);
184
219
  for (let index = 0; index < length; index += 1) {