sneakoscope 8.3.2 → 8.6.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 (122) hide show
  1. package/README.md +1 -1
  2. package/config/architecture-map-policy.v1.json +168 -0
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/commands/bridge.js +19 -1
  6. package/dist/commands/doctor-profile.js +1 -1
  7. package/dist/commands/doctor.js +97 -16
  8. package/dist/config/skills-manifest.json +1 -1
  9. package/dist/core/align/align-route.js +8 -2
  10. package/dist/core/align/code-navigation-align.js +88 -32
  11. package/dist/core/architecture-map-pipeline.js +173 -0
  12. package/dist/core/architecture-map-review.js +180 -0
  13. package/dist/core/codex/agent-config-file-repair.js +17 -3
  14. package/dist/core/codex/codex-config-guard.js +3 -1
  15. package/dist/core/codex/codex-feature-flags.js +10 -0
  16. package/dist/core/codex/codex-project-config-policy.js +54 -30
  17. package/dist/core/codex/codex-startup-config-postcheck.js +5 -0
  18. package/dist/core/codex-app/codex-app-fast-ui-repair.js +2 -1
  19. package/dist/core/codex-app/menubar/constants.js +1 -0
  20. package/dist/core/codex-hooks/codex-hook-state-writer.js +2 -0
  21. package/dist/core/codex-lb/codex-lb-env.js +25 -3
  22. package/dist/core/codex-lb/combined-catalog/builder.js +7 -2
  23. package/dist/core/codex-lb/combined-catalog/model-selection.js +71 -0
  24. package/dist/core/codex-lb/combined-catalog/normalize.js +20 -3
  25. package/dist/core/codex-lb/desktop-bridge/security.js +11 -8
  26. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +75 -4
  27. package/dist/core/codex-lb/desktop-controller-v3.js +5 -1
  28. package/dist/core/codex-lb/request-route-resolver.js +21 -18
  29. package/dist/core/commands/triwiki-atlas-command.js +372 -0
  30. package/dist/core/commands/triwiki-command.js +12 -1
  31. package/dist/core/doctor/doctor-dirty-planner.js +16 -1
  32. package/dist/core/doctor/doctor-idempotence.js +11 -0
  33. package/dist/core/doctor/doctor-repair-postcheck.js +49 -0
  34. package/dist/core/doctor/doctor-transaction.js +47 -2
  35. package/dist/core/doctor/legacy-global-hook-cleanup.js +1 -1
  36. package/dist/core/fsx.js +14 -0
  37. package/dist/core/git-hygiene/git-policy.js +4 -0
  38. package/dist/core/git-hygiene/gitignore-writer.js +2 -0
  39. package/dist/core/gx-renderer.js +2 -1
  40. package/dist/core/init.js +40 -17
  41. package/dist/core/managed-paths.js +1 -0
  42. package/dist/core/perf/release-latency-slo.js +57 -11
  43. package/dist/core/pipeline-internals/pipeline-stage-builder.js +181 -0
  44. package/dist/core/pipeline-internals/runtime-core.js +18 -172
  45. package/dist/core/pipeline-internals/runtime-gates.js +12 -1
  46. package/dist/core/release/content-digest-cache.js +70 -0
  47. package/dist/core/release/gate-manifest.js +9 -0
  48. package/dist/core/release/main-push-guard.js +4 -3
  49. package/dist/core/release/package-dist-snapshot.js +18 -2
  50. package/dist/core/release/physical-release-gates.js +26 -3
  51. package/dist/core/release/release-authorization-snapshot.js +4 -0
  52. package/dist/core/release/release-gate-cache-v2.js +27 -3
  53. package/dist/core/release/release-gate-contract.js +2 -0
  54. package/dist/core/release/release-gate-dag.js +3 -0
  55. package/dist/core/release/release-inspection-memo.js +69 -0
  56. package/dist/core/release/release-pack-content-scanner.js +5 -0
  57. package/dist/core/release/release-pack-receipt.js +28 -18
  58. package/dist/core/release/release-pack-tarball.js +10 -0
  59. package/dist/core/release/release-upgrade-baseline.js +4 -0
  60. package/dist/core/routes.js +1 -1
  61. package/dist/core/safety/ssot-authority-inventory.js +114 -0
  62. package/dist/core/safety/ssot-guard.js +2 -44
  63. package/dist/core/subagents/agent-catalog.js +4 -2
  64. package/dist/core/subagents/official-subagent-config.js +92 -15
  65. package/dist/core/subagents/official-subagent-preparation.js +1 -0
  66. package/dist/core/triwiki/context-graph/architecture/baseline.js +81 -0
  67. package/dist/core/triwiki/context-graph/architecture/capsule.js +34 -0
  68. package/dist/core/triwiki/context-graph/architecture/contracts.js +67 -0
  69. package/dist/core/triwiki/context-graph/architecture/delta.js +149 -0
  70. package/dist/core/triwiki/context-graph/architecture/finding-factory.js +28 -0
  71. package/dist/core/triwiki/context-graph/architecture/findings.js +173 -0
  72. package/dist/core/triwiki/context-graph/architecture/fingerprint.js +75 -0
  73. package/dist/core/triwiki/context-graph/architecture/index.js +15 -0
  74. package/dist/core/triwiki/context-graph/architecture/input-bundle.js +66 -0
  75. package/dist/core/triwiki/context-graph/architecture/metrics.js +137 -0
  76. package/dist/core/triwiki/context-graph/architecture/policy.js +321 -0
  77. package/dist/core/triwiki/context-graph/architecture/slice.js +100 -0
  78. package/dist/core/triwiki/context-graph/architecture/ssot-analysis.js +59 -0
  79. package/dist/core/triwiki/context-graph/architecture/topology-overlay.js +106 -0
  80. package/dist/core/triwiki/context-graph/architecture/validation.js +145 -0
  81. package/dist/core/triwiki/context-graph/architecture/voxel-context.js +39 -0
  82. package/dist/core/triwiki/context-graph/extractors/index.js +4 -0
  83. package/dist/core/triwiki/context-graph/projections/index.js +2 -0
  84. package/dist/core/triwiki/context-graph/projections/mermaid/architecture-delta.js +68 -0
  85. package/dist/core/triwiki/context-graph/projections/mermaid/ast.js +49 -0
  86. package/dist/core/triwiki/context-graph/projections/mermaid/change-impact.js +43 -0
  87. package/dist/core/triwiki/context-graph/projections/mermaid/contracts.js +5 -0
  88. package/dist/core/triwiki/context-graph/projections/mermaid/escape.js +45 -0
  89. package/dist/core/triwiki/context-graph/projections/mermaid/ids.js +73 -0
  90. package/dist/core/triwiki/context-graph/projections/mermaid/index.js +113 -0
  91. package/dist/core/triwiki/context-graph/projections/mermaid/manifest.js +37 -0
  92. package/dist/core/triwiki/context-graph/projections/mermaid/module-dependency.js +21 -0
  93. package/dist/core/triwiki/context-graph/projections/mermaid/ownership-workstream.js +21 -0
  94. package/dist/core/triwiki/context-graph/projections/mermaid/project-topology.js +13 -0
  95. package/dist/core/triwiki/context-graph/projections/mermaid/public-surface.js +17 -0
  96. package/dist/core/triwiki/context-graph/projections/mermaid/reduce.js +44 -0
  97. package/dist/core/triwiki/context-graph/projections/mermaid/risk-domains.js +23 -0
  98. package/dist/core/triwiki/context-graph/projections/mermaid/runtime-control.js +20 -0
  99. package/dist/core/triwiki/context-graph/projections/mermaid/selection-ledger.js +94 -0
  100. package/dist/core/triwiki/context-graph/projections/mermaid/serializer.js +81 -0
  101. package/dist/core/triwiki/context-graph/projections/mermaid/ssot-provenance.js +17 -0
  102. package/dist/core/triwiki/context-graph/projections/mermaid/verification-coverage.js +17 -0
  103. package/dist/core/triwiki/context-graph/projections/mermaid/view-builder-common.js +1 -0
  104. package/dist/core/triwiki/context-graph/projections/mermaid/view-builder.js +173 -0
  105. package/dist/core/triwiki/context-graph/projections/mermaid/view-specs.js +33 -0
  106. package/dist/core/triwiki/context-graph/store/architecture-map-store.js +71 -0
  107. package/dist/core/triwiki/triwiki-proof-bank-index.js +26 -1
  108. package/dist/core/triwiki/triwiki-proof-bank.js +82 -2
  109. package/dist/core/version.js +1 -1
  110. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +2 -2
  111. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +178 -0
  112. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +10 -1
  113. package/dist/scripts/architecture-map-check.js +299 -0
  114. package/dist/scripts/lib/ensure-dist-fresh.js +48 -1
  115. package/dist/scripts/release-metadata-check.js +2 -0
  116. package/package.json +6 -3
  117. package/release-gates.v2.json +204 -0
  118. package/schemas/architecture-baseline.v1.schema.json +47 -0
  119. package/schemas/architecture-map-manifest.v1.schema.json +40 -0
  120. package/schemas/architecture-map-policy.v1.schema.json +147 -0
  121. package/schemas/architecture-review.v1.schema.json +46 -0
  122. package/schemas/mermaid-projection.v1.schema.json +47 -0
@@ -118,20 +118,8 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
118
118
  let backupPath = null;
119
119
  let userConfigPath = null;
120
120
  let profileConfigPath = null;
121
- if (opts.apply && changed) {
122
- backupPath = `${configPath}.bak-${Date.now().toString(36)}`;
123
- await ensureDir(path.dirname(configPath));
124
- await fsp.copyFile(configPath, backupPath);
125
- await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
126
- await writeCodexConfigGuarded({
127
- root,
128
- configPath,
129
- before: String(original || ''),
130
- cause: 'codex-project-config-policy',
131
- mutate: () => projectText
132
- });
133
- actions.push('project_config_rewritten_with_backup');
134
- }
121
+ const writeBlockers = [];
122
+ let machineLocalMoved = true;
135
123
  if (opts.apply && split.machine_text.trim()) {
136
124
  await ensureDir(codexHome);
137
125
  userConfigPath = codexHomeConfigPath;
@@ -139,15 +127,39 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
139
127
  const machineText = preserveExistingGlobalFastModeConfig(String(currentUser || ''), split.machine_text);
140
128
  const dedupedUser = removeConfigIds(String(currentUser || ''), configIds(machineText));
141
129
  const commentLine = `# SKS moved machine-local Codex config from ${path.relative(root, configPath) || configPath} at ${nowIso()}`;
142
- const mergedUser = mergeMachineLocalIntoUserConfig(dedupedUser, machineText.trim(), commentLine);
143
- await writeCodexConfigGuarded({
130
+ const mergedUser = mergeMachineLocalIntoUserConfig(stripSksMovedComments(dedupedUser), machineText.trim(), commentLine);
131
+ const homeWrite = await writeCodexConfigGuarded({
144
132
  root,
145
133
  configPath: userConfigPath,
146
134
  before: String(currentUser || ''),
147
135
  cause: 'codex-project-config-policy-machine-local',
148
136
  mutate: () => mergedUser
149
137
  });
150
- actions.push('machine_local_keys_moved_to_codex_home_config');
138
+ machineLocalMoved = homeWrite.ok;
139
+ if (homeWrite.ok)
140
+ actions.push('machine_local_keys_moved_to_codex_home_config');
141
+ else
142
+ writeBlockers.push(`machine_local_move_refused:${homeWrite.status}`);
143
+ }
144
+ if (opts.apply && changed && machineLocalMoved) {
145
+ backupPath = `${configPath}.bak-${Date.now().toString(36)}`;
146
+ await ensureDir(path.dirname(configPath));
147
+ await fsp.copyFile(configPath, backupPath);
148
+ await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
149
+ const projectWrite = await writeCodexConfigGuarded({
150
+ root,
151
+ configPath,
152
+ before: String(original || ''),
153
+ cause: 'codex-project-config-policy',
154
+ mutate: () => projectText
155
+ });
156
+ if (projectWrite.ok)
157
+ actions.push('project_config_rewritten_with_backup');
158
+ else
159
+ writeBlockers.push(`project_config_write_refused:${projectWrite.status}`);
160
+ }
161
+ else if (opts.apply && changed) {
162
+ writeBlockers.push('project_config_rewrite_skipped_machine_local_move_refused');
151
163
  }
152
164
  if (profileName)
153
165
  actions.push('legacy_project_profile_selector_removed');
@@ -157,7 +169,7 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
157
169
  root,
158
170
  config_path: configPath,
159
171
  codex_home: codexHome,
160
- ok: parseSmoke.ok && split.blockers.length === 0,
172
+ ok: parseSmoke.ok && split.blockers.length === 0 && writeBlockers.length === 0,
161
173
  changed,
162
174
  applied: opts.apply === true,
163
175
  backup_path: backupPath,
@@ -175,7 +187,11 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
175
187
  deprecated_approval_policy_fixed: split.deprecated_approval_policy_fixed,
176
188
  actions,
177
189
  parse_smoke: parseSmoke,
178
- blockers: [...split.blockers, ...(parseSmoke.ok ? [] : ['project_config_rewrite_parse_smoke_failed'])]
190
+ blockers: [
191
+ ...split.blockers,
192
+ ...writeBlockers,
193
+ ...(parseSmoke.ok ? [] : ['project_config_rewrite_parse_smoke_failed'])
194
+ ]
179
195
  };
180
196
  if (opts.writeReport !== false)
181
197
  await writeJsonAtomic(reportPath, { ...report, report_path: reportPath });
@@ -201,28 +217,36 @@ export async function repairCodexConfigStructure(configPathInput, opts = {}) {
201
217
  }
202
218
  const hoist = hoistMisplacedMachineLocalKeys(String(original));
203
219
  let backupPath = null;
220
+ let applied = false;
221
+ const writeBlockers = [];
204
222
  if (opts.apply && hoist.changed) {
205
223
  backupPath = `${configPath}.struct-bak-${Date.now().toString(36)}`;
206
224
  await ensureDir(path.dirname(configPath));
207
225
  await fsp.copyFile(configPath, backupPath);
208
226
  await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
209
- await writeCodexConfigGuarded({
227
+ const guarded = await writeCodexConfigGuarded({
210
228
  configPath,
211
229
  before: String(original || ''),
212
230
  cause: 'codex-config-structure-repair',
213
231
  mutate: () => hoist.text
214
232
  });
233
+ applied = guarded.ok;
234
+ if (!guarded.ok)
235
+ writeBlockers.push(`config_write_guard:${guarded.status}`);
215
236
  }
216
237
  const parseSmoke = tomlRewriteSmoke(hoist.text);
217
238
  return {
218
239
  config_path: configPath,
219
- ok: parseSmoke.ok,
220
- status: hoist.changed ? (opts.apply ? 'structure_repaired' : 'structure_repair_available') : 'structure_ok',
240
+ ok: parseSmoke.ok && writeBlockers.length === 0,
241
+ status: hoist.changed
242
+ ? (applied ? 'structure_repaired' : opts.apply ? 'structure_repair_refused' : 'structure_repair_available')
243
+ : 'structure_ok',
221
244
  changed: hoist.changed,
222
- applied: opts.apply === true && hoist.changed,
245
+ applied,
223
246
  hoisted_keys: hoist.hoisted_keys,
224
247
  backup_path: backupPath,
225
- parse_smoke: parseSmoke
248
+ parse_smoke: parseSmoke,
249
+ blockers: writeBlockers
226
250
  };
227
251
  }
228
252
  function isCodexConfigToml(file) {
@@ -451,12 +475,6 @@ function tomlRewriteSmoke(text) {
451
475
  invalid_table_header: badHeader || null
452
476
  };
453
477
  }
454
- function profileTableBody(blocks, profile) {
455
- const block = blocks.find((item) => item.table === `profiles.${profile}`);
456
- if (!block)
457
- return '';
458
- return block.text.split('\n').filter((line) => !/^\s*\[/.test(line)).join('\n');
459
- }
460
478
  function configIds(text) {
461
479
  const ids = { keys: new Set(), tables: new Set() };
462
480
  for (const block of tomlBlocks(text)) {
@@ -512,6 +530,12 @@ function mergeMachineLocalIntoUserConfig(userText, machineText, commentLine) {
512
530
  }
513
531
  return normalizeProjectText(sections.join('\n\n'));
514
532
  }
533
+ function stripSksMovedComments(text) {
534
+ return normalizeProjectText(String(text || '')
535
+ .split('\n')
536
+ .filter((line) => !/^\s*#\s*SKS moved machine-local Codex config\b/i.test(line))
537
+ .join('\n'));
538
+ }
515
539
  function collectTomlSections(text, preamble, tables) {
516
540
  for (const block of tomlBlocks(text)) {
517
541
  if (block.table)
@@ -16,9 +16,14 @@ export async function postcheckCodexStartupConfig(input) {
16
16
  const rolePlan = await installOfficialSubagentAgentConfigs(root, { apply: false });
17
17
  const tomlSmoke = tomlSyntaxSmoke(text);
18
18
  const orphanChildTables = orphanMcpChildTables(text);
19
+ const multiAgentV2Disabled = officialConfig.multiAgentV2.enabled === false;
19
20
  const blockers = [
20
21
  ...(!configPresent ? ['project_official_subagent_config_missing'] : []),
21
22
  ...(!tomlValidation.ok ? ['project_official_subagent_config_toml_parse_failed'] : []),
23
+ ...(multiAgentV2Disabled ? ['official_subagent_multi_agent_v2_disabled'] : []),
24
+ ...(multiAgentV2Disabled && officialConfig.enabled === false
25
+ ? ['official_subagent_agents_disabled']
26
+ : []),
22
27
  ...officialConfig.blockers.map((item) => `official_subagent_config:${item}`),
23
28
  ...rolePlan.missing.map((file) => `missing_official_subagent_agent:${file}`),
24
29
  ...rolePlan.stale.map((file) => `stale_official_subagent_agent:${file}`),
@@ -17,7 +17,8 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
17
17
  const candidates = [
18
18
  { scope: 'project', file: path.join(resolvedRoot, '.codex', 'config.toml'), mode: 'project_forbidden_keys' },
19
19
  { scope: 'codex_home', file: path.join(home, 'config.toml'), mode: 'sks_caused_host_owned_keys' }
20
- ];
20
+ ].filter((candidate) => candidate.mode !== 'project_forbidden_keys'
21
+ || path.resolve(candidate.file) !== path.resolve(home, 'config.toml'));
21
22
  const actions = [];
22
23
  const detectedProjectLocalForbiddenKeys = [];
23
24
  const unsafeReasons = [];
@@ -24,6 +24,7 @@ export const NATIVE_SOURCE_FILES = [
24
24
  'ProvidersReliability.swift',
25
25
  'ProvidersRoutingTruth.swift',
26
26
  'ProvidersOpenRouter.swift',
27
+ 'ProvidersModelExposure.swift',
27
28
  'ProvidersBridgeCatalog.swift',
28
29
  'RemoteCodingViewController.swift',
29
30
  'DiagnosticsViewController.swift',
@@ -160,6 +160,8 @@ function upsertTomlTable(text, table, block) {
160
160
  break;
161
161
  }
162
162
  }
163
+ while (end > start + 1 && !String(lines[end - 1] || '').trim())
164
+ end -= 1;
163
165
  lines.splice(start, end - start, ...blockLines);
164
166
  return lines.join('\n').replace(/\n{3,}/g, '\n\n');
165
167
  }
@@ -135,6 +135,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
135
135
  ok: false,
136
136
  status: 'blocked',
137
137
  models: [],
138
+ model_rows: [],
138
139
  model_efforts: {},
139
140
  http_status: null,
140
141
  blockers: loaded.missing.length ? loaded.missing.map((item) => `codex_lb_missing:${item}`) : ['codex_lb_not_configured']
@@ -147,6 +148,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
147
148
  ok: false,
148
149
  status: 'blocked',
149
150
  models: [],
151
+ model_rows: [],
150
152
  model_efforts: {},
151
153
  http_status: null,
152
154
  blockers: [transportBlocker]
@@ -167,6 +169,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
167
169
  ok: false,
168
170
  status: 'blocked',
169
171
  models: [],
172
+ model_rows: [],
170
173
  model_efforts: {},
171
174
  http_status: null,
172
175
  blockers: [`codex_lb_${safeBridgeErrorCode(error).replace(/^bridge_/, '')}`]
@@ -175,15 +178,17 @@ export async function readCodexLbModelCatalog(opts = {}) {
175
178
  try {
176
179
  const timeoutMs = Math.max(250, Number(opts.timeoutMs || 5000));
177
180
  const modelsUrl = new URL(`${loaded.base_url}/models`);
178
- const response = opts.fetchImpl
179
- ? await fetchImpl(modelsUrl, {
181
+ const request = async (url) => (opts.fetchImpl
182
+ ? await fetchImpl(url, {
180
183
  headers: gatewayHeaders,
181
184
  redirect: 'error',
182
185
  signal: AbortSignal.timeout(timeoutMs)
183
186
  })
184
- : await requestPinnedCodexLbCatalog(modelsUrl, remote, gatewayHeaders, timeoutMs);
187
+ : await requestPinnedCodexLbCatalog(url, remote, gatewayHeaders, timeoutMs));
188
+ const response = await request(modelsUrl);
185
189
  const payload = await response.json().catch(() => null);
186
190
  const models = normalizeCodexLbModelCatalogPayload(payload);
191
+ const modelRows = codexLbModelCatalogRows(payload);
187
192
  const apiEfforts = normalizeCodexModelEffortCatalogPayload(payload);
188
193
  const cachePath = path.join(String(process.env.CODEX_HOME || path.join(os.homedir(), '.codex')), 'models_cache.json');
189
194
  const cachePayload = await readJson(cachePath, null).catch(() => null);
@@ -195,6 +200,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
195
200
  ok,
196
201
  status: ok ? 'ready' : 'blocked',
197
202
  models,
203
+ model_rows: modelRows,
198
204
  model_efforts: modelEfforts,
199
205
  http_status: response.status,
200
206
  blockers: [
@@ -209,6 +215,7 @@ export async function readCodexLbModelCatalog(opts = {}) {
209
215
  ok: false,
210
216
  status: 'blocked',
211
217
  models: [],
218
+ model_rows: [],
212
219
  model_efforts: {},
213
220
  http_status: null,
214
221
  blockers: ['codex_lb_model_catalog_unavailable']
@@ -287,6 +294,21 @@ export function normalizeCodexLbModelCatalogPayload(payload) {
287
294
  .filter(Boolean);
288
295
  return [...new Set(models)];
289
296
  }
297
+ export function codexLbModelCatalogRows(payload) {
298
+ const rows = Array.isArray(payload?.models) ? payload.models : Array.isArray(payload?.data) ? payload.data : [];
299
+ const seen = new Set();
300
+ const preserved = [];
301
+ for (const row of rows) {
302
+ if (!row || typeof row !== 'object' || Array.isArray(row))
303
+ continue;
304
+ const slug = String(row.id || row.model || row.slug || row.name || '').trim();
305
+ if (!slug || seen.has(slug))
306
+ continue;
307
+ seen.add(slug);
308
+ preserved.push({ ...row, slug });
309
+ }
310
+ return preserved;
311
+ }
290
312
  export function normalizeCodexLbBaseUrl(input = '') {
291
313
  let host = String(input || '').trim();
292
314
  if (!host)
@@ -2,14 +2,18 @@ import { buildBridgeRouteIndex, sha256Stable } from '../route-index.js';
2
2
  import { COMBINED_BRIDGE_CATALOG_SCHEMA } from './contracts.js';
3
3
  import { normalizeProviderCatalog, providerCatalogStatus, routeProviderState } from './normalize.js';
4
4
  import { compareModels, unique } from './shared.js';
5
+ import { applyBridgeModelSelection, availableModelRows, emptyBridgeModelSelection } from './model-selection.js';
5
6
  export function buildCombinedBridgeCatalog(registry, options) {
6
7
  const createdAt = options.created_at || new Date().toISOString();
7
8
  const normalized = {
8
9
  'codex-lb': normalizeProviderCatalog(options.catalogs['codex-lb']),
9
10
  openrouter: normalizeProviderCatalog(options.catalogs.openrouter)
10
11
  };
11
- const models = [...normalized['codex-lb'].models, ...normalized.openrouter.models]
12
+ const selection = options.selection || emptyBridgeModelSelection(createdAt);
13
+ const allModels = [...normalized['codex-lb'].models, ...normalized.openrouter.models]
12
14
  .sort(compareModels);
15
+ const availableOpenRouterModels = availableModelRows(allModels, selection);
16
+ const models = options.selection ? applyBridgeModelSelection(allModels, selection) : allModels;
13
17
  const routeBuild = buildBridgeRouteIndex({
14
18
  models,
15
19
  providers: {
@@ -81,6 +85,7 @@ export function buildCombinedBridgeCatalog(registry, options) {
81
85
  route_index: routeBuild.route_index,
82
86
  status,
83
87
  blockers,
84
- warnings: status.warnings
88
+ warnings: status.warnings,
89
+ available_openrouter_models: availableOpenRouterModels
85
90
  };
86
91
  }
@@ -0,0 +1,71 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { writeJsonAtomic } from '../../fsx.js';
4
+ export const BRIDGE_MODEL_SELECTION_SCHEMA = 'sks.bridge-model-selection.v1';
5
+ export const BRIDGE_MODEL_SELECTION_FILENAME = 'sks-bridge-model-selection.json';
6
+ export const BRIDGE_AVAILABLE_MODELS_FILENAME = 'sks-bridge-available-models.json';
7
+ export const MAX_SELECTED_OPENROUTER_MODELS = 64;
8
+ export function bridgeModelSelectionPath(home) {
9
+ return path.join(path.resolve(home), '.codex', 'sks', BRIDGE_MODEL_SELECTION_FILENAME);
10
+ }
11
+ export function bridgeAvailableModelsPath(home) {
12
+ return path.join(path.resolve(home), '.codex', 'sks', BRIDGE_AVAILABLE_MODELS_FILENAME);
13
+ }
14
+ export function emptyBridgeModelSelection(now) {
15
+ return { schema: BRIDGE_MODEL_SELECTION_SCHEMA, updated_at: now, openrouter: { mode: 'selected', public_ids: [] } };
16
+ }
17
+ export function normalizeBridgeModelSelection(value, now) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value))
19
+ return emptyBridgeModelSelection(now);
20
+ const row = value;
21
+ if (row.schema !== BRIDGE_MODEL_SELECTION_SCHEMA)
22
+ return emptyBridgeModelSelection(now);
23
+ const openrouter = row.openrouter && typeof row.openrouter === 'object' && !Array.isArray(row.openrouter)
24
+ ? row.openrouter
25
+ : {};
26
+ const ids = Array.isArray(openrouter.public_ids)
27
+ ? openrouter.public_ids.map((id) => String(id || '').trim()).filter(Boolean)
28
+ : [];
29
+ return {
30
+ schema: BRIDGE_MODEL_SELECTION_SCHEMA,
31
+ updated_at: typeof row.updated_at === 'string' && row.updated_at ? row.updated_at : now,
32
+ openrouter: { mode: 'selected', public_ids: [...new Set(ids)].sort().slice(0, MAX_SELECTED_OPENROUTER_MODELS) }
33
+ };
34
+ }
35
+ export async function readBridgeModelSelection(home, now = new Date().toISOString()) {
36
+ return (await readBridgeModelSelectionState(home, now)).selection;
37
+ }
38
+ export async function readBridgeModelSelectionState(home, now = new Date().toISOString()) {
39
+ try {
40
+ const raw = await fsp.readFile(bridgeModelSelectionPath(home), 'utf8');
41
+ return { selection: normalizeBridgeModelSelection(JSON.parse(raw), now), stored: true };
42
+ }
43
+ catch {
44
+ return { selection: emptyBridgeModelSelection(now), stored: false };
45
+ }
46
+ }
47
+ export async function writeBridgeModelSelection(home, selection) {
48
+ await writeJsonAtomic(bridgeModelSelectionPath(home), selection, { mode: 0o600 });
49
+ }
50
+ export function applyBridgeModelSelection(models, selection) {
51
+ const selected = new Set(selection.openrouter.public_ids);
52
+ return models.filter((model) => model.provider_id !== 'openrouter' || selected.has(model.public_id));
53
+ }
54
+ export function availableModelRows(models, selection) {
55
+ const selected = new Set(selection.openrouter.public_ids);
56
+ return models
57
+ .filter((model) => model.provider_id === 'openrouter')
58
+ .map((model) => ({
59
+ public_id: model.public_id,
60
+ display_name: model.display_name,
61
+ selected: selected.has(model.public_id)
62
+ }))
63
+ .sort((left, right) => left.display_name.localeCompare(right.display_name));
64
+ }
65
+ export function pruneSelection(selection, available) {
66
+ const known = new Set(available.map((row) => row.public_id));
67
+ const kept = selection.openrouter.public_ids.filter((id) => known.has(id));
68
+ if (kept.length === selection.openrouter.public_ids.length)
69
+ return selection;
70
+ return { ...selection, openrouter: { mode: 'selected', public_ids: kept } };
71
+ }
@@ -30,9 +30,10 @@ export function normalizeProviderCatalog(input) {
30
30
  if (!publicId || !upstreamModel)
31
31
  return null;
32
32
  const features = row?.features && typeof row.features === 'object' ? row.features : {};
33
+ const reasoning = features.reasoning === true;
33
34
  const capabilities = [
34
35
  ...(features.tools === true ? ['tools'] : []),
35
- ...(features.reasoning === true ? ['reasoning'] : []),
36
+ ...(reasoning ? ['reasoning'] : []),
36
37
  ...(features.vision === true ? ['vision'] : []),
37
38
  ...(features.audio === true ? ['audio'] : [])
38
39
  ];
@@ -45,7 +46,11 @@ export function normalizeProviderCatalog(input) {
45
46
  capabilities,
46
47
  source_catalog_generation: input.generation || 'unknown',
47
48
  route_key: `openrouter:${publicId}`,
48
- ...codexModelInfoFields(publicId, null, { tools: capabilities.includes('tools') })
49
+ ...(reasoning ? { default_reasoning_level: OPENROUTER_DEFAULT_REASONING_LEVEL } : {}),
50
+ ...codexModelInfoFields(publicId, {
51
+ supported_reasoning_levels: reasoning ? openRouterReasoningLevels() : [],
52
+ supports_reasoning_summary_parameter: reasoning
53
+ }, { tools: capabilities.includes('tools') })
49
54
  });
50
55
  }).filter(isModel);
51
56
  if (models.length === 0)
@@ -113,6 +118,7 @@ function normalizeCodexLbBridgeCatalogModels(payload, sourceCatalogGeneration) {
113
118
  ...(row.supports_audio === true || row.audio === true ? ['audio'] : [])
114
119
  ];
115
120
  models.push({
121
+ ...row,
116
122
  public_id: publicId,
117
123
  provider_id: 'codex-lb',
118
124
  upstream_model: publicId,
@@ -151,6 +157,16 @@ function sanitizeCodexLbModelRow(row, index) {
151
157
  function isPlainObject(value) {
152
158
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
153
159
  }
160
+ export const OPENROUTER_REASONING_LEVELS = [
161
+ { effort: 'low', description: 'Fast responses with lighter reasoning' },
162
+ { effort: 'medium', description: 'Balances speed and reasoning depth for everyday tasks' },
163
+ { effort: 'high', description: 'Greater reasoning depth for complex problems' },
164
+ { effort: 'xhigh', description: 'Extra high reasoning depth for complex problems' }
165
+ ];
166
+ export const OPENROUTER_DEFAULT_REASONING_LEVEL = 'medium';
167
+ function openRouterReasoningLevels() {
168
+ return OPENROUTER_REASONING_LEVELS.map((level) => ({ ...level }));
169
+ }
154
170
  export function codexModelInfoFields(slug, row, fallback) {
155
171
  const summaries = typeof row?.supports_reasoning_summary_parameter === 'boolean'
156
172
  ? row.supports_reasoning_summary_parameter
@@ -158,7 +174,7 @@ export function codexModelInfoFields(slug, row, fallback) {
158
174
  return {
159
175
  slug,
160
176
  supported_reasoning_levels: Array.isArray(row?.supported_reasoning_levels)
161
- ? row.supported_reasoning_levels.map(String)
177
+ ? row.supported_reasoning_levels.map((level) => (level && typeof level === 'object' ? level : String(level)))
162
178
  : [],
163
179
  shell_type: typeof row?.shell_type === 'string' && row.shell_type ? row.shell_type : 'shell_command',
164
180
  visibility: typeof row?.visibility === 'string' && row.visibility ? row.visibility : 'list',
@@ -186,6 +202,7 @@ function canonicalModel(model) {
186
202
  if (!publicId || !upstream)
187
203
  return null;
188
204
  return {
205
+ ...model,
189
206
  public_id: publicId,
190
207
  provider_id: model.provider_id,
191
208
  upstream_model: upstream,
@@ -158,22 +158,25 @@ export function resolveBridgeRequestRoute(request, policy, pins) {
158
158
  const sessionId = request.session_id ? canonicalSessionId(request.session_id) : null;
159
159
  const pin = sessionId ? pins.find((entry) => entry.thread_id === sessionId) || null : null;
160
160
  if (pin && pin.public_model === publicModel) {
161
- if (pin.public_model !== publicModel
162
- || pin.catalog_generation !== policy.catalog_generation
163
- || pin.route_policy_generation !== policy.policy_generation) {
164
- throw new DesktopBridgeError('session_pin_route_unavailable');
165
- }
166
161
  const current = policy.model_routes[publicModel];
167
162
  if (!current || current.provider_id !== pin.provider_id || current.upstream_model !== pin.upstream_model) {
168
163
  throw new DesktopBridgeError('session_pin_route_unavailable');
169
164
  }
165
+ const aged = pin.catalog_generation !== policy.catalog_generation
166
+ || pin.route_policy_generation !== policy.policy_generation;
170
167
  return {
171
168
  provider_id: pin.provider_id,
172
169
  public_model: publicModel,
173
170
  upstream_model: pin.upstream_model,
174
- catalog_generation: pin.catalog_generation,
175
- route_policy_generation: pin.route_policy_generation,
176
- session_pin: pin,
171
+ catalog_generation: policy.catalog_generation,
172
+ route_policy_generation: policy.policy_generation,
173
+ session_pin: aged
174
+ ? {
175
+ ...pin,
176
+ catalog_generation: policy.catalog_generation,
177
+ route_policy_generation: policy.policy_generation,
178
+ }
179
+ : pin,
177
180
  };
178
181
  }
179
182
  const route = policy.model_routes[publicModel];
@@ -1,4 +1,6 @@
1
- import { readText } from '../../fsx.js';
1
+ import { readText, writeJsonAtomic } from '../../fsx.js';
2
+ import { readTopLevelTomlString } from '../../codex-app/codex-model-catalog.js';
3
+ import { BRIDGE_MODEL_SELECTION_SCHEMA, MAX_SELECTED_OPENROUTER_MODELS, bridgeAvailableModelsPath, pruneSelection, readBridgeModelSelectionState, writeBridgeModelSelection } from '../combined-catalog/model-selection.js';
2
4
  import { listOpenRouterModels } from '../../providers/openrouter/openrouter-account.js';
3
5
  import { buildCombinedBridgeCatalog, readActiveCombinedBridgeCatalog, stageCombinedBridgeCatalog } from '../combined-catalog.js';
4
6
  import { codexLbEnvPath, codexLbMetadataPath, loadCodexLbEnv, readCodexLbModelCatalog } from '../codex-lb-env.js';
@@ -40,7 +42,21 @@ export async function syncCatalogInternal(options) {
40
42
  const catalogs = await fetchProviderCatalogs(initialRegistry, rawCredentials, paths, options);
41
43
  const credentials = await resolveValidatedCredentials(options, paths);
42
44
  const registry = await resolveBridgeProviderRegistry({ home: paths.home, credentials, storedRegistry });
43
- const build = buildCombinedBridgeCatalog(registry, { catalogs, created_at: nowIso(options) });
45
+ const selectionState = await readBridgeModelSelectionState(paths.home, nowIso(options));
46
+ const selection = selectionState.stored
47
+ ? selectionState.selection
48
+ : seedSelectionFromConfiguredModel(selectionState.selection, await readText(paths.configPath, ''));
49
+ const build = buildCombinedBridgeCatalog(registry, { catalogs, created_at: nowIso(options), selection });
50
+ if (!selectionState.stored)
51
+ await writeBridgeModelSelection(paths.home, selection).catch(() => undefined);
52
+ await writeJsonAtomic(bridgeAvailableModelsPath(paths.home), {
53
+ schema: 'sks.bridge-available-models.v1',
54
+ updated_at: nowIso(options),
55
+ openrouter: build.available_openrouter_models
56
+ }, { mode: 0o600 }).catch(() => undefined);
57
+ const pruned = pruneSelection(selection, build.available_openrouter_models);
58
+ if (pruned !== selection)
59
+ await writeBridgeModelSelection(paths.home, pruned).catch(() => undefined);
44
60
  const staging = await stageCombinedBridgeCatalog({
45
61
  build,
46
62
  catalogPath: paths.catalogPath,
@@ -200,11 +216,12 @@ async function fetchProviderCatalogs(registry, credentials, paths, options) {
200
216
  home: paths.home,
201
217
  validationPath: paths.validationPath
202
218
  });
219
+ const codexLbModels = result.model_rows.length > 0 ? result.model_rows : result.models;
203
220
  return [providerId, {
204
221
  provider_id: providerId,
205
222
  state: result.ok ? 'verified' : 'failed',
206
- generation: result.ok ? sha256Stable({ provider_id: providerId, models: result.models }) : null,
207
- models: { models: result.models },
223
+ generation: result.ok ? sha256Stable({ provider_id: providerId, models: codexLbModels }) : null,
224
+ models: { models: codexLbModels },
208
225
  checked_at: checkedAt,
209
226
  expires_at: expiresAt,
210
227
  blockers: result.blockers,
@@ -255,3 +272,57 @@ function validationFailureState(blockers) {
255
272
  return blockers.some((blocker) => /(?:401|403|auth|rejected|invalid_key|unauthorized)/i.test(blocker))
256
273
  ? 'rejected' : 'unavailable';
257
274
  }
275
+ export async function listSelectableModels(options) {
276
+ const paths = controllerPaths(options);
277
+ const selection = (await readBridgeModelSelectionState(paths.home, nowIso(options))).selection;
278
+ const available = await readText(bridgeAvailableModelsPath(paths.home), '');
279
+ let rows = [];
280
+ try {
281
+ const parsed = available ? JSON.parse(available) : null;
282
+ if (parsed && Array.isArray(parsed.openrouter))
283
+ rows = parsed.openrouter;
284
+ }
285
+ catch {
286
+ rows = [];
287
+ }
288
+ const selected = new Set(selection.openrouter.public_ids);
289
+ const status = await desktopBridgeStatusV3(options);
290
+ return commandResult('models.list', true, status, {
291
+ codex_lb_exposure: 'all',
292
+ openrouter: {
293
+ mode: 'selected',
294
+ selected_count: selection.openrouter.public_ids.length,
295
+ max_selected: MAX_SELECTED_OPENROUTER_MODELS,
296
+ available_count: rows.length,
297
+ models: rows.map((row) => ({
298
+ public_id: String(row?.public_id || ''),
299
+ display_name: String(row?.display_name || row?.public_id || ''),
300
+ selected: selected.has(String(row?.public_id || ''))
301
+ })).filter((row) => row.public_id)
302
+ }
303
+ }, rows.length === 0 ? ['bridge_available_models_unknown'] : [], options);
304
+ }
305
+ export async function selectExposedModels(publicIds, options) {
306
+ const paths = controllerPaths(options);
307
+ const requested = [...new Set(publicIds.map((id) => String(id || '').trim()).filter(Boolean))].sort();
308
+ if (requested.length > MAX_SELECTED_OPENROUTER_MODELS) {
309
+ throw new Error('bridge_model_selection_limit_exceeded');
310
+ }
311
+ await writeBridgeModelSelection(paths.home, {
312
+ schema: BRIDGE_MODEL_SELECTION_SCHEMA,
313
+ updated_at: nowIso(options),
314
+ openrouter: { mode: 'selected', public_ids: requested }
315
+ });
316
+ const sync = await syncCatalogInternal(options);
317
+ const status = await desktopBridgeStatusV3(options);
318
+ const activation = sync.activation && typeof sync.activation === 'object'
319
+ ? sync.activation
320
+ : {};
321
+ return commandResult('models.select', sync.ok === true, status, { selected_count: requested.length, catalog_sync: sync }, sync.ok === true ? [] : stringArray(activation.blockers), options);
322
+ }
323
+ function seedSelectionFromConfiguredModel(selection, configText) {
324
+ const configured = readTopLevelTomlString(configText, 'model');
325
+ if (!configured || !configured.includes('/'))
326
+ return selection;
327
+ return { ...selection, openrouter: { mode: 'selected', public_ids: [configured] } };
328
+ }
@@ -1,4 +1,4 @@
1
- import { syncCatalog } from './desktop-controller-v3/catalog.js';
1
+ import { listSelectableModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
2
2
  import { explainRoute, ensureDesktopBridge, repairDesktopBridge, rollbackDesktopBridge, setDefaultProvider, unmanageDesktopBridge } from './desktop-controller-v3/lifecycle-commands.js';
3
3
  import { configureProvider, removeCredential, setProviderState, validateProvider } from './desktop-controller-v3/provider-commands.js';
4
4
  import { commandResult, safeCode } from './desktop-controller-v3/shared.js';
@@ -40,6 +40,10 @@ export async function executeDesktopBridgeCommandV3(request, options = {}) {
40
40
  if (request.operation === 'provider.remove-credential') {
41
41
  return removeCredential(request.provider_id, options);
42
42
  }
43
+ if (request.operation === 'models.list')
44
+ return listSelectableModels(options);
45
+ if (request.operation === 'models.select')
46
+ return selectExposedModels(request.public_ids, options);
43
47
  if (request.operation === 'catalog.sync')
44
48
  return syncCatalog(options);
45
49
  if (request.operation === 'catalog.status') {