sneakoscope 10.3.7 → 10.3.8

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 (63) hide show
  1. package/README.md +12 -181
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/commands/bridge.js +61 -1
  5. package/dist/commands/codex-app.js +4 -1
  6. package/dist/config/skills-manifest.json +1 -1
  7. package/dist/core/agents/native-worker-backend-router.js +55 -0
  8. package/dist/core/codex-app/codex-app-restart-policy.js +20 -0
  9. package/dist/core/codex-app/codex-context-window.js +61 -24
  10. package/dist/core/codex-app/menubar/constants.js +4 -0
  11. package/dist/core/codex-compat/codex-hook-output-builders.js +3 -1
  12. package/dist/core/codex-lb/bridge-cli-contract.js +2 -0
  13. package/dist/core/codex-lb/desktop-bridge/exclusive-provider-guard.js +59 -0
  14. package/dist/core/codex-lb/desktop-bridge/http-forward.js +23 -15
  15. package/dist/core/codex-lb/desktop-bridge/index.js +1 -0
  16. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +10 -3
  17. package/dist/core/codex-lb/desktop-bridge/security.js +63 -1
  18. package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
  19. package/dist/core/codex-lb/desktop-bridge/state.js +3 -0
  20. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +5 -1
  21. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +31 -5
  22. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +23 -4
  23. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-catalog.js +39 -0
  24. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +286 -0
  25. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-main-model.js +101 -0
  26. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-status.js +68 -0
  27. package/dist/core/codex-lb/desktop-controller-v3/status.js +8 -5
  28. package/dist/core/codex-lb/desktop-controller-v3.js +26 -2
  29. package/dist/core/codex-lb/desktop-service.js +13 -2
  30. package/dist/core/commands/naruto-command.js +1 -1
  31. package/dist/core/decisions/child-model-choice.js +107 -0
  32. package/dist/core/decisions/cli.js +19 -2
  33. package/dist/core/decisions/routing.js +5 -4
  34. package/dist/core/hooks-runtime/hook-context.js +21 -7
  35. package/dist/core/hooks-runtime/hook-io.js +3 -2
  36. package/dist/core/hooks-runtime/jev-spawn-routing.js +83 -5
  37. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +3 -2
  38. package/dist/core/hooks-runtime/parent-orchestration-gate.js +18 -1
  39. package/dist/core/hooks-runtime/subagent-context.js +13 -2
  40. package/dist/core/hooks-runtime/subagent-spawn-policy.js +49 -6
  41. package/dist/core/hooks-runtime.js +12 -5
  42. package/dist/core/managed-assets/managed-assets-manifest.js +9 -4
  43. package/dist/core/subagents/child-model-allowlist.js +179 -0
  44. package/dist/core/subagents/child-model-plan.js +117 -0
  45. package/dist/core/subagents/naruto-host-credentials.js +82 -13
  46. package/dist/core/subagents/official-subagent-config.js +66 -15
  47. package/dist/core/subagents/official-subagent-preparation.js +45 -12
  48. package/dist/core/subagents/official-subagent-prompt.js +82 -11
  49. package/dist/core/subagents/official-subagent-runner.js +3 -0
  50. package/dist/core/subagents/read-only-list-role.js +73 -0
  51. package/dist/core/subagents/role-model-preferences.js +54 -17
  52. package/dist/core/version.js +1 -1
  53. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -0
  54. package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +314 -0
  55. package/dist/native/sks-menubar/Sources/ProcessClient.swift +2 -1
  56. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -3
  57. package/dist/native/sks-menubar/Sources/ProvidersOpenRouterOnly.swift +162 -0
  58. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +36 -18
  59. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +10 -4
  60. package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
  61. package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +201 -0
  62. package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +373 -0
  63. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
5
+ import { isOpenRouterModelId } from '../imagegen/imagegen-config.js';
6
+ import { latestTierModelSet } from './model-tiers.js';
7
+ export const OPENROUTER_ONLY_SCHEMA = 'sks.openrouter-only.v1';
8
+ export const OPENROUTER_ONLY_FILENAME = 'sks-openrouter-only.json';
9
+ export const MAX_SUBAGENT_MODELS = 16;
10
+ export const MAX_SUBAGENT_CRITERIA_CHARS = 240;
11
+ export const SUBAGENT_MODEL_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
12
+ export function openRouterOnlyStatePath(input = {}) {
13
+ const env = input.env || process.env;
14
+ return path.join(input.home || env.HOME || os.homedir(), '.codex', 'sks', OPENROUTER_ONLY_FILENAME);
15
+ }
16
+ export function defaultOpenRouterOnlyState() {
17
+ return { schema: OPENROUTER_ONLY_SCHEMA, enabled: false, subagent_models: [], restore: null, updated_at: null };
18
+ }
19
+ export function canonicalChildModelId(value) {
20
+ return String(value ?? '').trim().toLowerCase();
21
+ }
22
+ export function isSubagentModelEffort(value) {
23
+ return typeof value === 'string' && SUBAGENT_MODEL_EFFORTS.includes(value);
24
+ }
25
+ function cleanCriteria(value) {
26
+ return String(value ?? '')
27
+ .replace(/[\u0000-\u001f\u007f]+/g, ' ')
28
+ .replace(/\s+/g, ' ')
29
+ .trim()
30
+ .slice(0, MAX_SUBAGENT_CRITERIA_CHARS);
31
+ }
32
+ export function normalizeSubagentModelList(raw) {
33
+ const rows = Array.isArray(raw) ? raw : [];
34
+ const issues = [];
35
+ const entries = [];
36
+ const seen = new Set();
37
+ rows.forEach((row, index) => {
38
+ const record = row && typeof row === 'object' ? row : {};
39
+ const model = String(record.model ?? '').trim();
40
+ if (!isOpenRouterModelId(model)) {
41
+ issues.push({ index, code: 'subagent_model_id_invalid' });
42
+ return;
43
+ }
44
+ const key = canonicalChildModelId(model);
45
+ if (seen.has(key)) {
46
+ issues.push({ index, code: 'subagent_model_duplicate' });
47
+ return;
48
+ }
49
+ const effort = record.reasoning_effort ?? null;
50
+ if (effort !== null && effort !== '' && !isSubagentModelEffort(effort)) {
51
+ issues.push({ index, code: 'subagent_model_effort_invalid' });
52
+ return;
53
+ }
54
+ if (entries.length >= MAX_SUBAGENT_MODELS) {
55
+ issues.push({ index, code: 'subagent_model_list_too_long' });
56
+ return;
57
+ }
58
+ seen.add(key);
59
+ entries.push({
60
+ model: key,
61
+ criteria: cleanCriteria(record.criteria),
62
+ reasoning_effort: isSubagentModelEffort(effort) ? effort : null,
63
+ default: record.default === true
64
+ });
65
+ });
66
+ const firstDefault = entries.findIndex((entry) => entry.default);
67
+ entries.forEach((entry, index) => { entry.default = index === (firstDefault === -1 ? 0 : firstDefault); });
68
+ return { entries, issues };
69
+ }
70
+ function normalizeRestore(raw) {
71
+ if (!raw || typeof raw !== 'object')
72
+ return null;
73
+ const record = raw;
74
+ const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
75
+ return { previous_model: text(record.previous_model), applied_model: text(record.applied_model) };
76
+ }
77
+ export function normalizeOpenRouterOnlyState(raw) {
78
+ if (!raw || typeof raw !== 'object')
79
+ return defaultOpenRouterOnlyState();
80
+ const record = raw;
81
+ if (record.schema !== OPENROUTER_ONLY_SCHEMA)
82
+ return defaultOpenRouterOnlyState();
83
+ return {
84
+ schema: OPENROUTER_ONLY_SCHEMA,
85
+ enabled: record.enabled === true,
86
+ subagent_models: normalizeSubagentModelList(record.subagent_models).entries,
87
+ restore: normalizeRestore(record.restore),
88
+ updated_at: typeof record.updated_at === 'string' ? record.updated_at : null
89
+ };
90
+ }
91
+ export function readOpenRouterOnlyStateSync(input = {}) {
92
+ try {
93
+ return normalizeOpenRouterOnlyState(JSON.parse(fs.readFileSync(openRouterOnlyStatePath(input), 'utf8')));
94
+ }
95
+ catch {
96
+ return defaultOpenRouterOnlyState();
97
+ }
98
+ }
99
+ export async function readOpenRouterOnlyState(input = {}) {
100
+ return readOpenRouterOnlyStateSync(input);
101
+ }
102
+ export async function writeOpenRouterOnlyState(update, input = {}) {
103
+ const current = readOpenRouterOnlyStateSync(input);
104
+ const next = normalizeOpenRouterOnlyState({
105
+ ...current,
106
+ ...update,
107
+ schema: OPENROUTER_ONLY_SCHEMA,
108
+ updated_at: nowIso()
109
+ });
110
+ await writeJsonAtomic(openRouterOnlyStatePath(input), next, { mode: 0o600 });
111
+ return next;
112
+ }
113
+ export function defaultSubagentEntry(state) {
114
+ return state.subagent_models.find((entry) => entry.default) || state.subagent_models[0] || null;
115
+ }
116
+ export function subagentEntryForModel(state, model) {
117
+ const key = canonicalChildModelId(model);
118
+ if (!key)
119
+ return null;
120
+ return state.subagent_models.find((entry) => canonicalChildModelId(entry.model) === key) || null;
121
+ }
122
+ function openRouterRoutedModels(input) {
123
+ const env = input.env || process.env;
124
+ const file = path.join(input.home || env.HOME || os.homedir(), '.codex', 'sks', 'sks-bridge-route-policy.json');
125
+ try {
126
+ const routes = JSON.parse(fs.readFileSync(file, 'utf8'))?.model_routes;
127
+ if (!routes || typeof routes !== 'object')
128
+ return null;
129
+ const routed = new Set();
130
+ for (const [key, route] of Object.entries(routes)) {
131
+ if (route?.provider_id === 'openrouter')
132
+ routed.add(canonicalChildModelId(key.replace(/^openrouter:/i, '')));
133
+ }
134
+ return routed.size ? routed : null;
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ export function routableSubagentState(state, input = {}) {
141
+ const routed = openRouterRoutedModels(input);
142
+ if (!routed)
143
+ return { state, unroutable: [] };
144
+ const keep = state.subagent_models.filter((entry) => routed.has(canonicalChildModelId(entry.model)));
145
+ const unroutable = state.subagent_models.filter((entry) => !keep.includes(entry)).map((entry) => entry.model);
146
+ if (!unroutable.length)
147
+ return { state, unroutable };
148
+ const preferred = keep.findIndex((entry) => entry.default);
149
+ const entries = keep.map((entry, index) => ({ ...entry, default: index === (preferred === -1 ? 0 : preferred) }));
150
+ return { state: { ...state, subagent_models: entries }, unroutable };
151
+ }
152
+ export function effectiveChildModelAllowlist(input = {}) {
153
+ const stored = readOpenRouterOnlyStateSync(input);
154
+ if (stored.enabled) {
155
+ const { state, unroutable } = routableSubagentState(stored, input);
156
+ return {
157
+ mode: 'openrouter_only',
158
+ models: state.subagent_models.map((entry) => entry.model),
159
+ entries: state.subagent_models,
160
+ default_model: defaultSubagentEntry(state)?.model ?? null,
161
+ unroutable
162
+ };
163
+ }
164
+ return { mode: 'tiers', models: [...latestTierModelSet(input)], entries: [], default_model: null };
165
+ }
166
+ export function isAllowedChildModel(model, allowlist) {
167
+ const key = canonicalChildModelId(model);
168
+ if (!key)
169
+ return false;
170
+ if (allowlist.mode === 'tiers')
171
+ return allowlist.models.includes(String(model ?? '').trim());
172
+ return allowlist.models.some((candidate) => canonicalChildModelId(candidate) === key);
173
+ }
174
+ export function allowlistedChildModel(model, allowlist) {
175
+ const key = canonicalChildModelId(model);
176
+ if (!key)
177
+ return null;
178
+ return allowlist.models.find((candidate) => canonicalChildModelId(candidate) === key) ?? null;
179
+ }
@@ -0,0 +1,117 @@
1
+ import { chooseChildModels } from '../decisions/child-model-choice.js';
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';
5
+ import { readCodexMainModel } from './naruto-host-credentials.js';
6
+ import { READ_ONLY_LIST_ROLE } from './read-only-list-role.js';
7
+ 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
+ };
20
+ return { allowlist, list: { state, allowlist, mainModel: readCodexMainModel(location) } };
21
+ }
22
+ 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;
27
+ }
28
+ function routedRow(row, choice) {
29
+ return {
30
+ ...row,
31
+ routed_provider: 'openrouter',
32
+ routed_model: choice?.entry.model ?? null,
33
+ 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',
35
+ routing_dynamic: true,
36
+ role_model_preference_source: 'ignored_openrouter_only',
37
+ openrouter_only_choice: choice
38
+ ? { model: choice.entry.model, source: choice.source, reason: choice.reason, default_entry: choice.entry.default }
39
+ : { model: null, source: 'default', reason: 'subagent_list_empty', default_entry: false }
40
+ };
41
+ }
42
+ export function listBaselineAgentRouting(agentRouting, list) {
43
+ const fallback = defaultSubagentEntry(list.state);
44
+ const choice = fallback ? { entry: fallback, source: 'default', reason: 'baseline' } : null;
45
+ return Object.fromEntries(Object.entries(agentRouting).map(([name, row]) => [name, routedRow(row, choice)]));
46
+ }
47
+ export function childModelLanes(input) {
48
+ const assigned = new Set(input.slices.map((slice) => String(slice.agent || '')).filter(Boolean));
49
+ const names = Object.keys(input.agents);
50
+ const ordered = [...names.filter((name) => assigned.has(name)), ...names.filter((name) => !assigned.has(name))];
51
+ return ordered.slice(0, MAX_JEV_ROUTING_ROLES).map((name) => {
52
+ const own = input.slices.filter((slice) => slice.agent === name);
53
+ const work = own.length
54
+ ? own.map((slice) => `${slice.title}: ${slice.description}${slice.readOnly ? ' (read-only)' : ''}`).join('\n')
55
+ : `Role ${name}: ${String(input.agents[name]?.description || name)}`;
56
+ return { id: name, role: name, task: work, requestedModel: null };
57
+ });
58
+ }
59
+ export async function chooseListRoleModels(input) {
60
+ const lanes = childModelLanes(input);
61
+ try {
62
+ const choices = await chooseChildModels({
63
+ root: input.root,
64
+ workflowId: 'plan-child-models',
65
+ goal: input.goal || 'Plan official subagent roles.',
66
+ lanes,
67
+ state: input.list.state,
68
+ ...(input.env ? { env: input.env } : {})
69
+ });
70
+ return { lanes: lanes.map((lane) => lane.id), choices };
71
+ }
72
+ catch {
73
+ const fallback = defaultSubagentEntry(input.list.state);
74
+ const choices = {};
75
+ if (fallback)
76
+ for (const lane of lanes)
77
+ choices[lane.id] = { entry: fallback, source: 'default', reason: 'transport_error' };
78
+ return { lanes: lanes.map((lane) => lane.id), choices };
79
+ }
80
+ }
81
+ function uncappedReason(lanes, choices) {
82
+ const reasons = new Set(lanes.map((lane) => choices[lane]?.reason).filter((reason) => Boolean(reason)));
83
+ const decided = lanes.some((lane) => choices[lane]?.source === 'jev' || choices[lane]?.reason === 'keep_baseline');
84
+ return !decided && reasons.size === 1 ? [...reasons][0] : 'role_cap';
85
+ }
86
+ export function applyListRoleModels(agents, chosen, list, readOnlyRoleInstalled = null) {
87
+ const fallback = defaultSubagentEntry(list.state);
88
+ const lanes = chosen?.lanes || [];
89
+ const routed = new Set(lanes);
90
+ const capped = chosen ? uncappedReason(lanes, chosen.choices) : 'role_cap';
91
+ const next = {};
92
+ const roles = {};
93
+ const jevModels = {};
94
+ for (const [name, row] of Object.entries(agents)) {
95
+ const choice = chosen?.choices[name]
96
+ ?? (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;
99
+ if (choice?.source === 'jev')
100
+ jevModels[name] = choice.entry.model;
101
+ }
102
+ return {
103
+ agents: next,
104
+ jevModels,
105
+ evidence: {
106
+ enabled: true,
107
+ main_model: list.mainModel,
108
+ default_subagent_model: fallback?.model ?? null,
109
+ subagent_models: list.state.subagent_models.map((entry) => ({ ...entry })),
110
+ routed_roles: [...lanes],
111
+ jev_decided_roles: Object.keys(jevModels),
112
+ roles,
113
+ read_only_role: { name: READ_ONLY_LIST_ROLE.codex_name, installed: readOnlyRoleInstalled },
114
+ warnings: readOnlyRoleInstalled === false ? ['openrouter_only_read_only_role_missing'] : []
115
+ }
116
+ };
117
+ }
@@ -1,9 +1,27 @@
1
+ import fs from 'node:fs';
2
+ import { codexUserConfigPath, readTopLevelTomlString } from '../codex-app/codex-model-catalog.js';
3
+ import { isOpenRouterModelId } from '../imagegen/imagegen-config.js';
1
4
  import { defaultSubagentModel } from './model-policy.js';
2
5
  import { codexListedEfforts, latestTierModelSet } from './model-tiers.js';
6
+ import { SUBAGENT_MODEL_EFFORTS, defaultSubagentEntry, readOpenRouterOnlyStateSync, subagentEntryForModel } from './child-model-allowlist.js';
3
7
  export const NARUTO_AUTH_MODES = ['managed', 'host'];
4
8
  const IDENTIFIER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
5
9
  const RETIRED_DIRECT_MANAGED_PROVIDERS = new Set(['codex-lb', 'openrouter']);
6
10
  export const NARUTO_EFFORT_TIERS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
11
+ export function readCodexMainModel(location = {}) {
12
+ try {
13
+ return readTopLevelTomlString(fs.readFileSync(codexUserConfigPath(location), 'utf8'), 'model')?.trim() || null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export function readNarutoOpenRouterOnlyContext(location = {}) {
20
+ const state = readOpenRouterOnlyStateSync(location);
21
+ if (!state.enabled)
22
+ return null;
23
+ return { state, mainModel: readCodexMainModel(location) };
24
+ }
7
25
  function readFlagValue(args, name) {
8
26
  const inline = args.find((arg) => arg.startsWith(`${name}=`));
9
27
  if (inline)
@@ -78,12 +96,15 @@ export function resolveNarutoCredentialPolicy(input) {
78
96
  sources.forcedLoginMethod = args.includes('--no-forced-login-method') ? 'flag' : 'env';
79
97
  warnings.push('naruto_forced_login_method_released_without_host_auth_mode');
80
98
  }
81
- const models = [
82
- { key: 'parentModel', flag: '--parent-model', envKey: 'SKS_NARUTO_PARENT_MODEL', fallback: input.defaultParentModel, effort: false },
83
- { key: 'parentEffort', flag: '--parent-effort', envKey: 'SKS_NARUTO_PARENT_EFFORT', fallback: input.defaultParentEffort, effort: true },
84
- { key: 'subagentModel', flag: '--subagent-model', envKey: 'SKS_NARUTO_SUBAGENT_MODEL', fallback: defaultSubagentModel(), effort: false },
85
- { key: 'subagentEffort', flag: '--subagent-effort', envKey: 'SKS_NARUTO_SUBAGENT_EFFORT', fallback: input.defaultSubagentEffort, effort: true }
86
- ];
99
+ const openRouterOnly = input.openRouterOnly === undefined ? readNarutoOpenRouterOnlyContext() : input.openRouterOnly;
100
+ const models = openRouterOnly
101
+ ? openRouterOnlyModelFlags(input, openRouterOnly)
102
+ : [
103
+ { key: 'parentModel', flag: '--parent-model', envKey: 'SKS_NARUTO_PARENT_MODEL', fallback: input.defaultParentModel, effort: false },
104
+ { key: 'parentEffort', flag: '--parent-effort', envKey: 'SKS_NARUTO_PARENT_EFFORT', fallback: input.defaultParentEffort, effort: true },
105
+ { key: 'subagentModel', flag: '--subagent-model', envKey: 'SKS_NARUTO_SUBAGENT_MODEL', fallback: defaultSubagentModel(), effort: false },
106
+ { key: 'subagentEffort', flag: '--subagent-effort', envKey: 'SKS_NARUTO_SUBAGENT_EFFORT', fallback: input.defaultSubagentEffort, effort: true }
107
+ ];
87
108
  const resolvedModels = {};
88
109
  for (const entry of models) {
89
110
  const raw = resolve(args, env, entry.flag, entry.envKey);
@@ -94,7 +115,7 @@ export function resolveNarutoCredentialPolicy(input) {
94
115
  }
95
116
  const ok = entry.effort
96
117
  ? NARUTO_EFFORT_TIERS.includes(raw.value)
97
- : validIdentifier(raw.value);
118
+ : validIdentifier(raw.value) || (openRouterOnly !== null && isOpenRouterModelId(raw.value));
98
119
  if (!ok) {
99
120
  blockers.push(`naruto_${String(entry.key)}_invalid:${raw.value.slice(0, 32)}`);
100
121
  resolvedModels[entry.key] = entry.fallback;
@@ -104,12 +125,17 @@ export function resolveNarutoCredentialPolicy(input) {
104
125
  resolvedModels[entry.key] = raw.value;
105
126
  sources[entry.key] = raw.source;
106
127
  }
107
- if (!latestTierModelSet().has(String(resolvedModels.subagentModel))) {
108
- blockers.push('naruto_subagent_model_must_be_current');
109
- resolvedModels.subagentModel = defaultSubagentModel();
128
+ if (openRouterOnly) {
129
+ validateOpenRouterOnlyModels(openRouterOnly, resolvedModels, sources, blockers);
130
+ }
131
+ else {
132
+ if (!latestTierModelSet().has(String(resolvedModels.subagentModel))) {
133
+ blockers.push('naruto_subagent_model_must_be_current');
134
+ resolvedModels.subagentModel = defaultSubagentModel();
135
+ }
136
+ validateGpt56EffortPair('parent', String(resolvedModels.parentModel), String(resolvedModels.parentEffort), blockers);
137
+ validateGpt56EffortPair('subagent', String(resolvedModels.subagentModel), String(resolvedModels.subagentEffort), blockers);
110
138
  }
111
- validateGpt56EffortPair('parent', String(resolvedModels.parentModel), String(resolvedModels.parentEffort), blockers);
112
- validateGpt56EffortPair('subagent', String(resolvedModels.subagentModel), String(resolvedModels.subagentEffort), blockers);
113
139
  const envKeyRaw = resolve(args, env, '--provider-env-key', 'SKS_NARUTO_PROVIDER_ENV_KEY');
114
140
  let providerEnvKey = null;
115
141
  sources.providerEnvKey = 'default';
@@ -141,11 +167,53 @@ export function resolveNarutoCredentialPolicy(input) {
141
167
  parentEffort: String(resolvedModels.parentEffort),
142
168
  subagentModel: String(resolvedModels.subagentModel),
143
169
  subagentEffort: String(resolvedModels.subagentEffort),
170
+ childModelMode: openRouterOnly ? 'openrouter_only' : 'tiers',
144
171
  sources,
145
172
  warnings,
146
- blockers
173
+ blockers,
174
+ hint: openRouterOnly && blockers.some((blocker) => OPENROUTER_ONLY_BLOCKER.test(blocker)) ? OPENROUTER_ONLY_HINT : null
147
175
  };
148
176
  }
177
+ const OPENROUTER_ONLY_BLOCKER = /^naruto_(parent_model_not_openrouter|subagent_model_not_in_list|subagent_model_list_empty|parent_effort_unsupported_openrouter|subagent_effort_unsupported_openrouter)(:|$)/;
178
+ const OPENROUTER_ONLY_HINT = 'OpenRouter Only Mode is on: the parent runs the OpenRouter main model in Codex config.toml and children run only models on the subagent list (Control Center > Subagent Models, or `sks bridge subagent-models list`); pass --parent-model=<vendor/model>, --subagent-model=<a listed model>, and efforts low|medium|high|xhigh';
179
+ const OPENROUTER_ONLY_PARENT_EFFORT = 'xhigh';
180
+ function openRouterOnlyEffort(value, fallback) {
181
+ return value && SUBAGENT_MODEL_EFFORTS.includes(value) ? value : fallback;
182
+ }
183
+ function openRouterOnlyModelFlags(input, context) {
184
+ const listDefault = defaultSubagentEntry(context.state);
185
+ return [
186
+ { key: 'parentModel', flag: '--parent-model', envKey: 'SKS_NARUTO_PARENT_MODEL', fallback: context.mainModel || '', effort: false },
187
+ { key: 'parentEffort', flag: '--parent-effort', envKey: 'SKS_NARUTO_PARENT_EFFORT', fallback: openRouterOnlyEffort(input.defaultParentEffort, OPENROUTER_ONLY_PARENT_EFFORT), effort: true },
188
+ { key: 'subagentModel', flag: '--subagent-model', envKey: 'SKS_NARUTO_SUBAGENT_MODEL', fallback: listDefault?.model || '', effort: false },
189
+ { key: 'subagentEffort', flag: '--subagent-effort', envKey: 'SKS_NARUTO_SUBAGENT_EFFORT', fallback: openRouterOnlyEffort(listDefault?.reasoning_effort ?? input.defaultSubagentEffort, 'medium'), effort: true }
190
+ ];
191
+ }
192
+ function validateOpenRouterOnlyModels(context, resolved, sources, blockers) {
193
+ const parent = String(resolved.parentModel || '');
194
+ const parentLabel = parent.slice(0, 64) || 'unset';
195
+ if (!isOpenRouterModelId(parent))
196
+ blockers.push(`naruto_parent_model_not_openrouter:${parentLabel}`);
197
+ const listDefault = defaultSubagentEntry(context.state);
198
+ if (!listDefault) {
199
+ blockers.push('naruto_subagent_model_list_empty');
200
+ }
201
+ else {
202
+ const listed = subagentEntryForModel(context.state, resolved.subagentModel);
203
+ if (!listed)
204
+ blockers.push(`naruto_subagent_model_not_in_list:${String(resolved.subagentModel || '').slice(0, 64)}`);
205
+ const entry = listed ?? listDefault;
206
+ resolved.subagentModel = entry.model;
207
+ if (sources.subagentEffort === 'default' && entry.reasoning_effort)
208
+ resolved.subagentEffort = entry.reasoning_effort;
209
+ }
210
+ for (const scope of ['parent', 'subagent']) {
211
+ const effort = String(resolved[`${scope}Effort`] || '');
212
+ if (!SUBAGENT_MODEL_EFFORTS.includes(effort)) {
213
+ blockers.push(`naruto_${scope}_effort_unsupported_openrouter:${effort}:allowed_${SUBAGENT_MODEL_EFFORTS.join('_or_')}`);
214
+ }
215
+ }
216
+ }
149
217
  function validateGpt56EffortPair(scope, model, effort, blockers) {
150
218
  const allowed = codexListedEfforts(model);
151
219
  if (allowed && !allowed.includes(effort)) {
@@ -172,6 +240,7 @@ export function narutoCredentialPolicyReceipt(policy) {
172
240
  parent_effort: policy.parentEffort,
173
241
  subagent_model: policy.subagentModel,
174
242
  subagent_effort: policy.subagentEffort,
243
+ child_model_mode: policy.childModelMode,
175
244
  sources: policy.sources,
176
245
  warnings: policy.warnings,
177
246
  blockers: policy.blockers,
@@ -7,6 +7,8 @@ import { ensureConfinedDirectory, inspectConfinedPath } from '../managed-path-sa
7
7
  import { MANAGED_OFFICIAL_SUBAGENT_ROLES, managedOfficialSubagentRoleContent, managedOfficialSubagentRoleOwnsText } from '../managed-assets/managed-assets-manifest.js';
8
8
  import { DEFAULT_SUBAGENT_EFFORT, defaultSubagentModel as latestDefaultSubagentModel } from './model-policy.js';
9
9
  import { latestTierModelSet } from './model-tiers.js';
10
+ import { allowlistedChildModel, effectiveChildModelAllowlist } from './child-model-allowlist.js';
11
+ import { READ_ONLY_LIST_ROLE, readOnlyListRoleContent, readOnlyListRoleOwnsText } from './read-only-list-role.js';
10
12
  import { HARD_NARUTO_MAX_THREADS } from './thread-budget.js';
11
13
  import { escapeRegExp } from '../text/regex.js';
12
14
  export const DEFAULT_OFFICIAL_SUBAGENT_MAX_THREADS = 256;
@@ -52,8 +54,14 @@ export function mergeOfficialSubagentConfigResult(text = '', opts = {}) {
52
54
  next = upsertDefaultUnlessInherited(next, inheritedAgents, 'enabled', `enabled = ${DEFAULT_OFFICIAL_SUBAGENT_ENABLED}`);
53
55
  next = upsertDefaultUnlessInherited(next, inheritedAgents, 'max_depth', `max_depth = ${DEFAULT_OFFICIAL_SUBAGENT_MAX_DEPTH}`);
54
56
  next = upsertDefaultUnlessInherited(next, inheritedAgents, 'interrupt_message', `interrupt_message = ${DEFAULT_OFFICIAL_SUBAGENT_INTERRUPT_MESSAGE}`);
57
+ const childModels = opts.childModels ?? effectiveChildModelAllowlist();
55
58
  const existingDefault = /^\s*default_subagent_model\s*=\s*"([^"]*)"/m.exec(next)?.[1] || '';
56
- next = upsertTomlTableKey(next, 'agents', `default_subagent_model = "${latestTierModelSet().has(existingDefault) ? existingDefault : defaultOfficialSubagentModel()}"`);
59
+ const childDefault = childModels.mode === 'openrouter_only'
60
+ ? childModels.default_model
61
+ : latestTierModelSet().has(existingDefault) ? existingDefault : defaultOfficialSubagentModel();
62
+ if (childDefault) {
63
+ next = upsertTomlTableKey(next, 'agents', `default_subagent_model = "${childDefault}"`);
64
+ }
57
65
  next = upsertDefaultUnlessInherited(next, inheritedAgents, 'default_subagent_reasoning_effort', `default_subagent_reasoning_effort = "${DEFAULT_OFFICIAL_SUBAGENT_REASONING_EFFORT}"`);
58
66
  next = mergeOfficialMultiAgentV2FeatureConfig(next, {
59
67
  sksOwned: opts.sksOwned === true,
@@ -140,17 +148,23 @@ export async function readOfficialSubagentConfig(root, opts = {}) {
140
148
  const effectiveMaxThreads = Math.min(maxThreads.value, HARD_NARUTO_MAX_THREADS);
141
149
  const maxDepth = resolveLayeredValue(projectLayer.agents.max_depth, globalLayer.agents.max_depth, DEFAULT_OFFICIAL_SUBAGENT_MAX_DEPTH, positiveInteger);
142
150
  const interruptMessage = resolveLayeredValue(projectLayer.agents.interrupt_message, globalLayer.agents.interrupt_message, DEFAULT_OFFICIAL_SUBAGENT_INTERRUPT_MESSAGE, booleanValue);
143
- const defaultSubagentModel = resolveLayeredValue(projectLayer.agents.default_subagent_model, globalLayer.agents.default_subagent_model, defaultOfficialSubagentModel(), nonEmptyString);
151
+ const childModels = opts.childModels ?? effectiveChildModelAllowlist({ home });
152
+ const listDefault = childModels.mode === 'openrouter_only' ? childModels.default_model : null;
153
+ const defaultSubagentModel = resolveLayeredValue(projectLayer.agents.default_subagent_model, globalLayer.agents.default_subagent_model, listDefault ?? defaultOfficialSubagentModel(), nonEmptyString);
144
154
  const defaultSubagentReasoningEffort = resolveLayeredValue(projectLayer.agents.default_subagent_reasoning_effort, globalLayer.agents.default_subagent_reasoning_effort, DEFAULT_OFFICIAL_SUBAGENT_REASONING_EFFORT, nonEmptyString);
145
155
  const multiAgentV2 = resolveMultiAgentV2Layer(projectLayer.features.multi_agent_v2, globalLayer.features.multi_agent_v2, effectiveMaxThreads);
146
156
  const effectiveMultiAgentV2 = {
147
157
  ...multiAgentV2.value,
148
158
  maxConcurrentThreadsPerSession: Math.min(multiAgentV2.value.maxConcurrentThreadsPerSession, HARD_NARUTO_MAX_THREADS + 1)
149
159
  };
150
- const modelCoerced = !latestTierModelSet().has(String(defaultSubagentModel.value));
160
+ const childDefault = coercedChildDefault(String(defaultSubagentModel.value), childModels);
161
+ const modelCoerced = childDefault.coerced;
151
162
  const depthCoerced = maxDepth.value > 1;
152
163
  const warnings = [
153
- ...(modelCoerced ? [`official_subagent_model_coerced_to_latest:${defaultSubagentModel.value}:${defaultSubagentModel.source}`] : []),
164
+ ...(modelCoerced
165
+ ? [`${childModels.mode === 'openrouter_only' ? 'official_subagent_model_coerced_to_list_default' : 'official_subagent_model_coerced_to_latest'}:${defaultSubagentModel.value}:${defaultSubagentModel.source}`]
166
+ : []),
167
+ ...(childModels.mode === 'openrouter_only' && !listDefault ? ['official_subagent_model_list_empty_openrouter_only'] : []),
154
168
  ...(depthCoerced ? [`official_subagent_max_depth_coerced_to_one:${maxDepth.value}:${maxDepth.source}`] : []),
155
169
  ...capacityNormalizationWarnings(maxThreads, multiAgentV2),
156
170
  ...(projectLayer.legacyWarnings),
@@ -162,7 +176,7 @@ export async function readOfficialSubagentConfig(root, opts = {}) {
162
176
  maxDepth: depthCoerced ? DEFAULT_OFFICIAL_SUBAGENT_MAX_DEPTH : maxDepth.value,
163
177
  jobMaxRuntimeSeconds: null,
164
178
  interruptMessage: interruptMessage.value,
165
- defaultSubagentModel: modelCoerced ? defaultOfficialSubagentModel() : String(defaultSubagentModel.value),
179
+ defaultSubagentModel: childDefault.model,
166
180
  defaultSubagentReasoningEffort: defaultSubagentReasoningEffort.value,
167
181
  multiAgentV2: effectiveMultiAgentV2,
168
182
  sources: {
@@ -180,6 +194,17 @@ export async function readOfficialSubagentConfig(root, opts = {}) {
180
194
  warnings
181
195
  };
182
196
  }
197
+ function coercedChildDefault(value, childModels) {
198
+ if (childModels.mode === 'openrouter_only') {
199
+ if (!childModels.default_model)
200
+ return { model: value, coerced: false };
201
+ const listed = allowlistedChildModel(value, childModels);
202
+ return listed ? { model: listed, coerced: false } : { model: childModels.default_model, coerced: true };
203
+ }
204
+ return latestTierModelSet().has(value)
205
+ ? { model: value, coerced: false }
206
+ : { model: defaultOfficialSubagentModel(), coerced: true };
207
+ }
183
208
  export function officialSubagentConfigWarnings(text = '', inheritedText = '') {
184
209
  const project = configLayer(text, 'project');
185
210
  const inherited = configLayer(inheritedText, 'global');
@@ -212,7 +237,31 @@ export function resolveInheritedOfficialSubagentConfigPath(projectConfigPath, op
212
237
  return globalConfigPath === path.resolve(projectConfigPath) ? null : globalConfigPath;
213
238
  }
214
239
  export async function installOfficialSubagentAgentConfigs(root, opts = {}) {
215
- return installOfficialSubagentAgentConfigsAt(root, '.codex/agents', opts);
240
+ const childModels = opts.childModels ?? effectiveChildModelAllowlist();
241
+ return installOfficialSubagentAgentConfigsAt(root, '.codex/agents', {
242
+ ...(opts.apply === undefined ? {} : { apply: opts.apply }),
243
+ readOnlyListRole: childModels.mode === 'openrouter_only'
244
+ });
245
+ }
246
+ function managedRoleFiles(readOnlyListRole) {
247
+ return [
248
+ ...MANAGED_OFFICIAL_SUBAGENT_ROLES.map((role) => ({
249
+ filename: role.filename,
250
+ codexName: role.codex_name,
251
+ expected: managedOfficialSubagentRoleContent(role),
252
+ owns: (text) => managedOfficialSubagentRoleOwnsText(text, role),
253
+ marker: role.ownership_marker
254
+ })),
255
+ ...(readOnlyListRole
256
+ ? [{
257
+ filename: READ_ONLY_LIST_ROLE.filename,
258
+ codexName: READ_ONLY_LIST_ROLE.codex_name,
259
+ expected: readOnlyListRoleContent(),
260
+ owns: readOnlyListRoleOwnsText,
261
+ marker: READ_ONLY_LIST_ROLE.ownership_marker
262
+ }]
263
+ : [])
264
+ ];
216
265
  }
217
266
  export async function refreshGlobalOfficialSubagentAgentConfigs(codexHome, opts = {}) {
218
267
  const home = path.resolve(codexHome);
@@ -223,6 +272,8 @@ export async function refreshGlobalOfficialSubagentAgentConfigs(codexHome, opts
223
272
  }
224
273
  async function installOfficialSubagentAgentConfigsAt(root, relativeDir, opts) {
225
274
  const apply = opts.apply !== false;
275
+ const roleFiles = managedRoleFiles(opts.readOnlyListRole === true);
276
+ const installedAgents = roleFiles.map((role) => role.codexName);
226
277
  const agentsDir = path.join(path.resolve(root), relativeDir);
227
278
  const missing = [];
228
279
  const existing = [];
@@ -262,8 +313,8 @@ async function installOfficialSubagentAgentConfigsAt(root, relativeDir, opts) {
262
313
  schema: 'sks.official-subagent-agent-install.v1',
263
314
  ok: false,
264
315
  apply,
265
- installed_agents: MANAGED_OFFICIAL_SUBAGENT_ROLES.map((role) => role.codex_name),
266
- missing: MANAGED_OFFICIAL_SUBAGENT_ROLES.map((role) => role.filename),
316
+ installed_agents: installedAgents,
317
+ missing: roleFiles.map((role) => role.filename),
267
318
  existing,
268
319
  stale,
269
320
  created,
@@ -277,10 +328,10 @@ async function installOfficialSubagentAgentConfigsAt(root, relativeDir, opts) {
277
328
  }
278
329
  if (apply && !opts.existingOnly)
279
330
  await ensureConfinedDirectory(root, agentsDir);
280
- for (const role of MANAGED_OFFICIAL_SUBAGENT_ROLES) {
331
+ for (const role of roleFiles) {
281
332
  const absolute = path.join(agentsDir, role.filename);
282
333
  const relative = `${relativeDir}/${role.filename}`;
283
- const expected = managedOfficialSubagentRoleContent(role);
334
+ const expected = role.expected;
284
335
  const inspected = await inspectConfinedPath(root, absolute).catch(() => null);
285
336
  if (!inspected) {
286
337
  preserved.push(relative);
@@ -320,7 +371,7 @@ async function installOfficialSubagentAgentConfigsAt(root, relativeDir, opts) {
320
371
  generatedFiles.push(relative);
321
372
  continue;
322
373
  }
323
- if (managedOfficialSubagentRoleOwnsText(current, role)) {
374
+ if (role.owns(current)) {
324
375
  stale.push(relative);
325
376
  if (apply) {
326
377
  await writeTextAtomic(absolute, expected);
@@ -330,7 +381,7 @@ async function installOfficialSubagentAgentConfigsAt(root, relativeDir, opts) {
330
381
  continue;
331
382
  }
332
383
  preserved.push(relative);
333
- manualBlockers.push(manualCollisionBlocker(relative, current, role));
384
+ manualBlockers.push(manualCollisionBlocker(relative, current, role.marker));
334
385
  }
335
386
  const remainingMissing = apply
336
387
  ? missing.filter((filename) => !created.includes(`${relativeDir}/${filename}`))
@@ -339,7 +390,7 @@ async function installOfficialSubagentAgentConfigsAt(root, relativeDir, opts) {
339
390
  schema: 'sks.official-subagent-agent-install.v1',
340
391
  ok: manualBlockers.length === 0 && (apply ? remainingMissing.length === 0 : true),
341
392
  apply,
342
- installed_agents: MANAGED_OFFICIAL_SUBAGENT_ROLES.map((role) => role.codex_name),
393
+ installed_agents: installedAgents,
343
394
  missing,
344
395
  existing,
345
396
  stale,
@@ -383,8 +434,8 @@ export async function backupInvalidToml(file, text, tag) {
383
434
  await writeTextAtomic(backupPath, String(text || ''), { mode: 0o600 });
384
435
  return backupPath;
385
436
  }
386
- function manualCollisionBlocker(relative, text, role) {
387
- const markerPresent = String(text || '').includes(role.ownership_marker);
437
+ function manualCollisionBlocker(relative, text, marker) {
438
+ const markerPresent = String(text || '').includes(marker);
388
439
  return markerPresent
389
440
  ? `manual_modified_official_subagent_config:${relative}`
390
441
  : `manual_user_owned_official_subagent_collision:${relative}`;