sneakoscope 8.0.0 → 8.0.2

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 (110) hide show
  1. package/README.md +13 -2
  2. package/config/skills-hash-ledger.v1.json +63 -0
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/bin/fast-inline.js +14 -3
  6. package/dist/bin/sks-dispatch.js +1 -1
  7. package/dist/cli/codex-lb-setup-warning-output.js +17 -0
  8. package/dist/cli/command-manifest-lite.js +2 -13
  9. package/dist/cli/install-helpers-codex-lb-config.js +475 -8
  10. package/dist/cli/install-helpers-install-support.js +1 -1
  11. package/dist/cli/install-helpers.js +764 -68
  12. package/dist/cli/router.js +5 -2
  13. package/dist/commands/codex-lb.js +77 -47
  14. package/dist/commands/doctor-profile.js +1 -0
  15. package/dist/commands/doctor.js +165 -58
  16. package/dist/config/skills-manifest.json +193 -97
  17. package/dist/core/codex/codex-config-guard.js +70 -16
  18. package/dist/core/codex/oauth-callback-port-diagnostic.js +121 -0
  19. package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -2
  20. package/dist/core/codex-app/menubar/constants.js +3 -0
  21. package/dist/core/codex-app/menubar/installer.js +32 -9
  22. package/dist/core/codex-app/menubar/launch-agent.js +58 -6
  23. package/dist/core/codex-app/menubar/migration.js +17 -6
  24. package/dist/core/codex-app/menubar/status.js +15 -3
  25. package/dist/core/codex-app.js +15 -20
  26. package/dist/core/codex-control/codex-lb-launch-recovery.js +26 -0
  27. package/dist/core/codex-lb/capability-runner.js +1 -0
  28. package/dist/core/codex-lb/codex-lb-env.js +52 -41
  29. package/dist/core/codex-lb/codex-lb-setup.js +85 -8
  30. package/dist/core/codex-lb/desktop-center-credentials.js +474 -39
  31. package/dist/core/codex-lb/desktop-controller.js +118 -4
  32. package/dist/core/codex-lb/legacy-migration.js +6 -3
  33. package/dist/core/codex-native/codex-native-feature-broker.js +5 -1
  34. package/dist/core/codex-native/core-skill-manifest.js +1 -1
  35. package/dist/core/codex-native/managed-skill-generation-lock.js +13 -0
  36. package/dist/core/codex-native/sks-skill-paths.js +97 -10
  37. package/dist/core/codex-runtime/codex-desktop-config-policy.js +1 -0
  38. package/dist/core/commands/basic-cli.js +54 -4
  39. package/dist/core/commands/mad-sks-command.js +1 -1
  40. package/dist/core/commands/menubar-command.js +24 -8
  41. package/dist/core/commands/naruto-command.js +42 -217
  42. package/dist/core/doctor/codex-config-syntax-repair.js +368 -0
  43. package/dist/core/doctor/doctor-native-capability-repair.js +12 -5
  44. package/dist/core/doctor/doctor-readiness-matrix.js +51 -4
  45. package/dist/core/doctor/skill-legacy-surface.js +0 -1
  46. package/dist/core/doctor/sks-menubar-doctor.js +21 -0
  47. package/dist/core/fsx.js +7 -14
  48. package/dist/core/hooks-runtime/hook-context.js +30 -16
  49. package/dist/core/hooks-runtime/managed-skill-admission.js +17 -0
  50. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +891 -30
  51. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +99 -6
  52. package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +3 -1
  53. package/dist/core/hooks-runtime/subagent-skill-availability-evidence.js +1 -1
  54. package/dist/core/hooks-runtime/subagent-skill-availability-guards.js +4 -4
  55. package/dist/core/hooks-runtime/subagent-skill-availability.js +9 -7
  56. package/dist/core/hooks-runtime/subagent-stop-hook.js +19 -0
  57. package/dist/core/hooks-runtime.js +41 -14
  58. package/dist/core/init/skills.js +270 -11
  59. package/dist/core/mission.js +20 -0
  60. package/dist/core/naruto/hardware-capacity-probe.js +3 -1
  61. package/dist/core/naruto/naruto-concurrency-governor.js +9 -3
  62. package/dist/core/pipeline-internals/runtime-core.js +68 -9
  63. package/dist/core/pipeline-internals/runtime-gates.js +10 -2
  64. package/dist/core/preflight/parallel-preflight-engine.js +18 -4
  65. package/dist/core/release/package-dist-snapshot.js +50 -8
  66. package/dist/core/routes.js +1 -1
  67. package/dist/core/safety/command-contract/contracts.js +10 -13
  68. package/dist/core/safety/command-contract/json-schema.js +38 -1
  69. package/dist/core/subagents/agent-catalog.js +37 -13
  70. package/dist/core/subagents/model-policy.js +14 -9
  71. package/dist/core/subagents/naruto-command-args.js +237 -0
  72. package/dist/core/subagents/naruto-command-input-contract.js +123 -0
  73. package/dist/core/subagents/naruto-help-contract.js +35 -2
  74. package/dist/core/subagents/naruto-launch-diagnostics.js +35 -0
  75. package/dist/core/subagents/official-subagent-config.js +30 -3
  76. package/dist/core/subagents/official-subagent-preparation.js +30 -5
  77. package/dist/core/subagents/official-subagent-prompt.js +4 -3
  78. package/dist/core/subagents/official-subagent-runner.js +12 -0
  79. package/dist/core/subagents/subagent-evidence.js +10 -0
  80. package/dist/core/subagents/thread-budget.js +1 -1
  81. package/dist/core/subagents/wave-lifecycle.js +41 -11
  82. package/dist/core/subagents/wave-parent-guidance.js +10 -3
  83. package/dist/core/update/update-migration-state.js +694 -63
  84. package/dist/core/update/update-operation.js +498 -32
  85. package/dist/core/update-check.js +990 -509
  86. package/dist/core/version.js +1 -1
  87. package/dist/native/sks-menubar/Sources/AppDelegate.swift +1 -0
  88. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -1
  89. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +1 -1
  90. package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +127 -12
  91. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +7 -2
  92. package/dist/native/sks-menubar/Sources/ProcessClient.swift +287 -84
  93. package/dist/native/sks-menubar/Sources/ProcessExecutionState.swift +45 -0
  94. package/dist/native/sks-menubar/Sources/ProcessIdentityGuard.swift +196 -0
  95. package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +169 -0
  96. package/dist/native/sks-menubar/Sources/StatusItemController.swift +23 -5
  97. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +113 -24
  98. package/dist/native/sks-menubar/Sources/main.swift +12 -0
  99. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +14 -0
  100. package/dist/native/sks-menubar/Tests/OperationCoordinatorTests.swift +174 -0
  101. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +4 -0
  102. package/dist/scripts/current-surface-update-e2e-check.js +24 -5
  103. package/dist/scripts/lib/ensure-dist-fresh.js +12 -8
  104. package/dist/scripts/mutation-callsite-analysis.js +381 -0
  105. package/dist/scripts/mutation-callsite-coverage-check.js +127 -62
  106. package/dist/scripts/official-subagent-workflow-check.js +1 -1
  107. package/dist/scripts/skills-manifest-continuity-check.js +19 -1
  108. package/package.json +5 -3
  109. package/runtime-required-scripts.json +5 -0
  110. package/scripts/build-clean-atomic.mjs +323 -0
@@ -946,19 +946,12 @@ export async function codexProviderModelUiStatus(opts = {}) {
946
946
  && hasTomlString(codexLbProvider, 'env_key', 'CODEX_LB_API_KEY')
947
947
  && hasTomlBoolean(codexLbProvider, 'supports_websockets', true)
948
948
  && hasTomlBoolean(codexLbProvider, 'requires_openai_auth', false);
949
- const codexLbCompatProviderContractOk = codexLbProviderPresent
950
- && hasTomlString(codexLbProvider, 'name', 'OpenAI')
951
- && hasTomlString(codexLbProvider, 'wire_api', 'responses')
952
- && hasTomlBoolean(codexLbProvider, 'supports_websockets', true)
953
- && hasTomlBoolean(codexLbProvider, 'requires_openai_auth', true)
954
- && !hasTomlKey(codexLbProvider, 'env_key')
955
- && hasCodexLbGatewayEnvHeader(codexLbProvider);
956
949
  const codexLbSelectedDefault = /(?:^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(?:#.*)?(?=\n|$)/.test(topLevelToml(globalConfig));
957
950
  const codexLbDesktopMode = opts.codexLbDesktopMode
958
951
  || inferCodexLbDesktopModeForUi(globalConfig);
959
- const codexLbProviderContractOk = codexLbDesktopMode === 'desktop-dual-auth-compat'
960
- ? codexLbCompatProviderContractOk
961
- : codexLbCliProviderContractOk;
952
+ const retiredCodexLbCompatConfigured = codexLbDesktopMode === 'desktop-dual-auth-compat';
953
+ const codexLbProviderContractOk = !retiredCodexLbCompatConfigured
954
+ && codexLbCliProviderContractOk;
962
955
  const metadata = await readJsonIfExists(opts.codexLbMetadataPath || codexLbMetadataPath(home));
963
956
  const gatewayAuthTransport = normalizeCodexLbGatewayAuthTransportForUi(opts.codexLbGatewayAuthTransport || metadata?.gateway_auth_transport);
964
957
  const localCatalogBindingAllowed = shouldBindLocalModelCatalog(codexLbDesktopMode);
@@ -1009,7 +1002,11 @@ export async function codexProviderModelUiStatus(opts = {}) {
1009
1002
  ];
1010
1003
  const codexLbBlockers = [
1011
1004
  ...(codexLbProviderPresent ? [] : ['codex_lb_provider_missing']),
1012
- ...(codexLbProviderPresent && !codexLbProviderContractOk ? ['codex_lb_provider_contract_drift'] : []),
1005
+ ...(retiredCodexLbCompatConfigured
1006
+ ? ['desktop_dual_auth_compat_unavailable']
1007
+ : codexLbProviderPresent && !codexLbProviderContractOk
1008
+ ? ['codex_lb_provider_contract_drift']
1009
+ : []),
1013
1010
  ...(codexLbApiKeySource !== 'missing' ? [] : ['codex_lb_api_key_missing']),
1014
1011
  ...(codexLbBaseUrlSource !== 'missing' ? [] : ['codex_lb_base_url_missing']),
1015
1012
  ...(codexLbDesktopMode === 'desktop-native-bridge' && managedCatalogConfigured
@@ -1050,7 +1047,11 @@ export async function codexProviderModelUiStatus(opts = {}) {
1050
1047
  const installCommand = 'sks codex-app use-openrouter --model z-ai/glm-5.2';
1051
1048
  const setupCommand = 'sks codex-lb setup --host <domain> --api-key-stdin --yes';
1052
1049
  const setKeyCommand = 'sks codex-lb set-key --api-key-stdin';
1053
- const desktopPickerEvidence = opts.desktopPickerEvidence || opts.codexLbCapabilityReport?.model_picker || null;
1050
+ const desktopPickerEvidence = opts.desktopPickerEvidence
1051
+ || opts.codexLbCapabilityReport?.model_picker
1052
+ || opts.codexLbCapabilityReport?.capabilities?.model_picker
1053
+ || opts.codexLbStatus?.capabilities?.model_picker
1054
+ || null;
1054
1055
  const desktopPickerVerified = desktopPickerEvidence?.state === 'verified'
1055
1056
  || desktopPickerEvidence?.verified === true;
1056
1057
  const selectedProviderConfigured = selectedProviderBlockers.length === 0;
@@ -1110,8 +1111,8 @@ export async function codexProviderModelUiStatus(opts = {}) {
1110
1111
  key_entry_visible: true,
1111
1112
  provider_present: codexLbProviderPresent,
1112
1113
  provider_contract_ok: codexLbProviderContractOk,
1113
- provider_contract: codexLbDesktopMode === 'desktop-dual-auth-compat'
1114
- ? 'desktop-dual-auth-compat'
1114
+ provider_contract: retiredCodexLbCompatConfigured
1115
+ ? 'retired-desktop-dual-auth-compat'
1115
1116
  : 'cli-provider',
1116
1117
  selected_default: codexLbSelectedDefault,
1117
1118
  key_present: codexLbApiKeySource !== 'missing',
@@ -1187,12 +1188,6 @@ function normalizeCodexLbGatewayAuthTransportForUi(value) {
1187
1188
  ? 'authorization-bearer-compat'
1188
1189
  : 'x-codex-lb-api-key';
1189
1190
  }
1190
- function hasTomlKey(text, key) {
1191
- return new RegExp(`(?:^|\\n)\\s*${escapeRegExp(key)}\\s*=`).test(text);
1192
- }
1193
- function hasCodexLbGatewayEnvHeader(text) {
1194
- return /(?:^|\n)\s*env_http_headers\s*=\s*\{[^}\n]*"X-Codex-LB-API-Key"\s*=\s*"CODEX_LB_API_KEY"[^}\n]*\}\s*(?:#.*)?(?=\n|$)/.test(text);
1195
- }
1196
1191
  async function findDefaultPluginSource(plugin, { home, configText }) {
1197
1192
  const cached = await findPluginCache(plugin.name, { home });
1198
1193
  if (cached)
@@ -69,6 +69,12 @@ export async function inspectCodexLbCliLaunchRecovery(input) {
69
69
  if (selectedProvider !== 'codex-lb') {
70
70
  return codexLbToolOutputRecoveryNotSelected();
71
71
  }
72
+ const codexProcessSelectsCodexLb = overrides.modelProvider === 'codex-lb'
73
+ || profileConfig.modelProvider === 'codex-lb'
74
+ || userConfig.modelProvider === 'codex-lb';
75
+ if (environmentSelectsCodexLb && !codexProcessSelectsCodexLb) {
76
+ return environmentSelectionNotForwardedBlocked();
77
+ }
72
78
  const baseUrl = overrides.baseUrl
73
79
  ?? profileConfig.baseUrl
74
80
  ?? userConfig.baseUrl
@@ -124,6 +130,13 @@ export function codexLbRecoveryBlockedProcessResult(probe) {
124
130
  };
125
131
  }
126
132
  export function codexLbRecoveryBlockedMessage(probe) {
133
+ if (probe.blockers.includes('codex_lb_launch_selection_not_forwarded')) {
134
+ return [
135
+ 'Codex launch blocked: codex-lb was requested by the SKS environment but was not selected in the Codex process.',
136
+ ...probe.blockers.map((blocker) => `blocker: ${blocker}`),
137
+ ...probe.operator_actions
138
+ ].join('\n');
139
+ }
127
140
  return [
128
141
  'Codex launch blocked: selected codex-lb interrupted tool-output recovery is not verified.',
129
142
  ...probe.blockers.map((blocker) => `blocker: ${blocker}`),
@@ -291,3 +304,16 @@ function projectProviderConfigBlocked() {
291
304
  ]
292
305
  };
293
306
  }
307
+ function environmentSelectionNotForwardedBlocked() {
308
+ const base = codexLbToolOutputRecoveryNotChecked(true);
309
+ return {
310
+ ...base,
311
+ status: 'version_unverified',
312
+ blockers: ['codex_lb_launch_selection_not_forwarded'],
313
+ warnings: ['sks_provider_environment_does_not_select_the_codex_process_provider'],
314
+ operator_actions: [
315
+ `Launch Codex with \`--config model_provider='"codex-lb"'\`, or select codex-lb in the active Codex profile.`,
316
+ 'Rerun `sks codex-lb status` before launching Codex.'
317
+ ]
318
+ };
319
+ }
@@ -165,6 +165,7 @@ export function shapeCodexLbDesktopCapabilityStatus(report) {
165
165
  configured: report.configured,
166
166
  oauth_preserved: report.oauth_preserved,
167
167
  gateway_auth_transport: String(report.gateway_auth_transport.evidence.configured_gateway_auth_transport || 'unknown'),
168
+ model_picker: report.model_picker,
168
169
  verified: entries.filter(([, value]) => value.state === 'verified').map(([key]) => key),
169
170
  available_unverified: entries.filter(([, value]) => value.state === 'available_unverified').map(([key]) => key),
170
171
  blocked: Object.fromEntries(entries
@@ -1,16 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
- import { exists, readJson, readText, runProcess, which } from '../fsx.js';
3
+ import { exists, readJson, readText } from '../fsx.js';
4
4
  import { escapeRegExp } from '../text/regex.js';
5
- const KEYCHAIN_WRITER_SWIFT = `import Foundation
6
- import Security
7
- let a=CommandLine.arguments[1],s=CommandLine.arguments[2]
8
- guard let k=String(data:FileHandle.standardInput.readDataToEndOfFile(),encoding:.utf8)?.trimmingCharacters(in:.whitespacesAndNewlines),!k.isEmpty else{exit(64)}
9
- let q:[String:Any]=[kSecClass as String:kSecClassGenericPassword,kSecAttrAccount as String:a,kSecAttrService as String:s]
10
- let v:[String:Any]=[kSecValueData as String:Data(k.utf8)]
11
- var r=SecItemUpdate(q as CFDictionary,v as CFDictionary)
12
- if r==errSecItemNotFound{var n=q;n[kSecValueData as String]=Data(k.utf8);r=SecItemAdd(n as CFDictionary,nil)}
13
- if r != errSecSuccess{FileHandle.standardError.write(Data("keychain_status=\\(r)\\n".utf8));exit(1)}`;
5
+ export const CODEX_LB_SECURE_KEYCHAIN_SERVICE = 'com.sneakoscope.codex-lb.api-key.v2';
6
+ export const CODEX_LB_LEGACY_KEYCHAIN_SERVICE = 'sks-codex-lb';
14
7
  export function codexLbEnvPath(home = process.env.HOME || os.homedir()) {
15
8
  return path.join(String(home || os.homedir()), '.codex', 'sks-codex-lb.env');
16
9
  }
@@ -161,14 +154,16 @@ export async function loadCodexLbEnv(opts = {}) {
161
154
  const home = opts.home || process.env.HOME || os.homedir();
162
155
  const metadataPath = opts.metadataPath || codexLbMetadataPath(home);
163
156
  const envPaths = [opts.envPath || codexLbEnvPath(home)];
164
- const sourcePriority = ['env-file', 'keychain', 'process.env'];
157
+ const sourcePriority = ['env-file', 'process.env'];
165
158
  if (opts.allowProjectSecrets)
166
159
  sourcePriority.push('project-local');
167
160
  const processEnv = pickEnv(opts.processEnv || process.env);
168
161
  const envFile = await readEnvFile(envPaths[0]);
169
- const keychain = await readMacKeychain(opts);
170
162
  const projectLocal = opts.allowProjectSecrets ? await readEnvFile(path.join(opts.root || process.cwd(), '.env')) : {};
171
163
  const metadata = await readCodexLbCredentialMetadata(metadataPath);
164
+ const keychain = await keychainReadIsNeeded(opts, metadata, [envFile.apiKey, processEnv.apiKey, projectLocal.apiKey])
165
+ ? await readMacKeychain(opts)
166
+ : { checked: false, available: false, status: 'skipped_not_needed', apiKey: '' };
172
167
  const apiKeyCandidates = [
173
168
  { source: 'env-file', apiKey: envFile.apiKey },
174
169
  { source: 'keychain', apiKey: keychain.apiKey },
@@ -192,12 +187,20 @@ export async function loadCodexLbEnv(opts = {}) {
192
187
  apiKeySha256,
193
188
  configuredBaseUrl
194
189
  });
190
+ const keychainBlockers = keychain.status === 'keychain_read_failed'
191
+ ? ['codex_lb_keychain_read_failed']
192
+ : keychain.status === 'legacy_keychain_migration_required'
193
+ ? ['codex_lb_legacy_keychain_migration_required']
194
+ : keychain.status === 'keychain_acl_helper_unavailable'
195
+ ? ['codex_lb_keychain_acl_helper_unavailable']
196
+ : [];
195
197
  const baseUrl = binding.present && metadata.baseUrl ? metadata.baseUrl : configuredBaseUrl;
196
- const apiKeyUsable = Boolean(apiKey) && binding.blockers.length === 0;
198
+ const apiKeyUsable = Boolean(apiKey) && binding.blockers.length === 0 && keychainBlockers.length === 0;
197
199
  const missing = [
198
200
  ...(apiKey ? [] : ['CODEX_LB_API_KEY']),
199
201
  ...(baseUrl ? [] : ['CODEX_LB_BASE_URL']),
200
- ...(binding.blockers.length ? ['CODEX_LB_CREDENTIAL_BINDING'] : [])
202
+ ...(binding.blockers.length ? ['CODEX_LB_CREDENTIAL_BINDING'] : []),
203
+ ...(keychainBlockers.length ? ['CODEX_LB_KEYCHAIN'] : [])
201
204
  ];
202
205
  return {
203
206
  schema: 'sks.codex-lb-env.v1',
@@ -214,7 +217,10 @@ export async function loadCodexLbEnv(opts = {}) {
214
217
  fingerprint: apiKeySha256 ? apiKeySha256.slice(0, 16) : null
215
218
  },
216
219
  secret_api_key: apiKeyUsable ? apiKey : null,
217
- credential_binding: binding,
220
+ credential_binding: {
221
+ ...binding,
222
+ blockers: [...binding.blockers, ...keychainBlockers]
223
+ },
218
224
  env_paths: envPaths,
219
225
  keychain: {
220
226
  checked: keychain.checked,
@@ -223,6 +229,20 @@ export async function loadCodexLbEnv(opts = {}) {
223
229
  }
224
230
  };
225
231
  }
232
+ async function keychainReadIsNeeded(opts, metadata, nonKeychainKeys) {
233
+ if (opts.forceKeychainRead === true)
234
+ return true;
235
+ const candidates = nonKeychainKeys.map((key) => String(key || '').trim()).filter(Boolean);
236
+ if (candidates.length === 0)
237
+ return true;
238
+ if (!metadata.valid || !metadata.apiKeySha256)
239
+ return false;
240
+ for (const candidate of candidates) {
241
+ if ((await sha256Full(candidate)) === metadata.apiKeySha256)
242
+ return false;
243
+ }
244
+ return true;
245
+ }
226
246
  async function readCodexLbCredentialMetadata(file) {
227
247
  if (!(await exists(file)))
228
248
  return { present: false, valid: false, baseUrl: '', apiKeySha256: '' };
@@ -308,38 +328,32 @@ export async function writeCodexLbKeychain(apiKey, opts = {}) {
308
328
  return { ok: false, status: 'missing_api_key' };
309
329
  if (process.platform !== 'darwin' && !opts.forceMacos)
310
330
  return { ok: false, status: 'not_macos' };
311
- const swift = opts.swiftBin || await which('swift').catch(() => null) || (await exists('/usr/bin/swift') ? '/usr/bin/swift' : null);
312
- if (!swift)
313
- return { ok: false, status: 'keychain_writer_unavailable' };
314
331
  const account = opts.account || process.env.USER || 'sks';
315
- const service = opts.service || 'sks-codex-lb';
316
- const result = await runProcess(swift, ['-e', KEYCHAIN_WRITER_SWIFT, account, service], {
317
- input: `${key}\n`,
318
- timeoutMs: 30000,
319
- maxOutputBytes: 8192
320
- });
332
+ const service = opts.service || CODEX_LB_SECURE_KEYCHAIN_SERVICE;
333
+ const legacyService = opts.legacyService === false
334
+ ? ''
335
+ : opts.legacyService || (opts.service ? '' : CODEX_LB_LEGACY_KEYCHAIN_SERVICE);
321
336
  return {
322
- ok: result.code === 0,
323
- status: result.code === 0 ? 'stored' : 'keychain_store_failed',
337
+ ok: false,
338
+ status: 'keychain_acl_helper_unavailable',
324
339
  account,
325
340
  service,
326
- error: result.code === 0 ? null : redactSecret(result.stderr || result.stdout || 'Security.framework keychain write failed', key)
341
+ legacy_service: legacyService || null,
342
+ keychain_state_verified: true,
343
+ keychain_state_status: 'unchanged',
344
+ timed_out: false,
345
+ error: 'Dedicated signed Keychain helper unavailable; use ~/.codex/sks-codex-lb.env (mode 0600).'
327
346
  };
328
347
  }
329
348
  async function readMacKeychain(opts = {}) {
330
349
  if (process.platform !== 'darwin' && !opts.forceMacos)
331
350
  return { checked: false, available: false, status: 'not_macos', apiKey: '' };
332
- const security = opts.securityBin || await which('security').catch(() => null) || (await exists('/usr/bin/security') ? '/usr/bin/security' : null);
333
- if (!security)
334
- return { checked: true, available: false, status: 'keychain_unavailable', apiKey: '' };
335
- const account = opts.account || process.env.USER || 'sks';
336
- const service = opts.service || 'sks-codex-lb';
337
- const result = await runProcess(security, ['find-generic-password', '-a', account, '-s', service, '-w'], {
338
- timeoutMs: 5000,
339
- maxOutputBytes: 8192
340
- });
341
- const apiKey = result.code === 0 ? String(result.stdout || '').trim() : '';
342
- return { checked: true, available: true, status: apiKey ? 'found' : 'missing', apiKey };
351
+ return {
352
+ checked: false,
353
+ available: false,
354
+ status: 'keychain_acl_helper_unavailable',
355
+ apiKey: ''
356
+ };
343
357
  }
344
358
  async function readEnvFile(file) {
345
359
  const text = await readText(file, '');
@@ -383,6 +397,3 @@ async function sha256Full(value) {
383
397
  const { createHash } = await import('node:crypto');
384
398
  return createHash('sha256').update(value).digest('hex');
385
399
  }
386
- function redactSecret(text, secret) {
387
- return String(text || '').split(String(secret || '')).join('[redacted]');
388
- }
@@ -1,7 +1,8 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
+ import fsp from 'node:fs/promises';
3
4
  import { ensureDir, exists, readText, writeTextAtomic } from '../fsx.js';
4
- import { codexLbEnvPath, codexLbMetadataPath, normalizeCodexLbBaseUrl } from './codex-lb-env.js';
5
+ import { CODEX_LB_SECURE_KEYCHAIN_SERVICE, codexLbBaseUrlSecurityBlocker, codexLbEnvPath, codexLbMetadataPath, normalizeCodexLbBaseUrl } from './codex-lb-env.js';
5
6
  import { DEFAULT_CODEX_LB_DESKTOP_MODE, DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT } from './desktop-mode.js';
6
7
  export function buildCodexLbSetupPlan(answers, opts = {}) {
7
8
  const home = opts.home || process.env.HOME || os.homedir();
@@ -14,8 +15,16 @@ export function buildCodexLbSetupPlan(answers, opts = {}) {
14
15
  const blockers = [];
15
16
  if (!baseUrl)
16
17
  blockers.push('missing_host_or_base_url');
18
+ else {
19
+ const transportBlocker = codexLbBaseUrlSecurityBlocker(baseUrl);
20
+ if (transportBlocker)
21
+ blockers.push(transportBlocker);
22
+ }
17
23
  if (answers.install_shell_profile !== 'skip' && !answers.write_env_file)
18
24
  blockers.push('shell_profile_snippet_requires_env_file');
25
+ if (answers.store_keychain && answers.keychain_helper_verified !== true) {
26
+ blockers.push('keychain_acl_helper_unavailable');
27
+ }
19
28
  const actions = [];
20
29
  if (desktopMode === 'desktop-native-bridge') {
21
30
  actions.push({
@@ -35,6 +44,7 @@ export function buildCodexLbSetupPlan(answers, opts = {}) {
35
44
  });
36
45
  }
37
46
  else if (desktopMode === 'desktop-dual-auth-compat') {
47
+ blockers.push('desktop_dual_auth_compat_requires_global_secret_environment');
38
48
  if (gatewayAuthTransport !== 'x-codex-lb-api-key') {
39
49
  blockers.push('desktop_compat_requires_x_codex_lb_api_key_transport');
40
50
  }
@@ -60,7 +70,11 @@ export function buildCodexLbSetupPlan(answers, opts = {}) {
60
70
  actions.push({ type: 'write_env_file', target: envPath, effect: 'write CODEX_LB_BASE_URL and redacted CODEX_LB_API_KEY env loader with chmod 0600' });
61
71
  }
62
72
  if (answers.store_keychain) {
63
- actions.push({ type: 'store_keychain', target: 'macOS Keychain service sks-codex-lb', effect: 'store the redacted codex-lb API key through Security.framework with stdin-only secret input' });
73
+ actions.push({
74
+ type: 'store_keychain',
75
+ target: `macOS Keychain service ${CODEX_LB_SECURE_KEYCHAIN_SERVICE}`,
76
+ effect: 'blocked until SKS ships a dedicated signed Keychain helper; reusable interpreters are never trusted for secret access'
77
+ });
64
78
  }
65
79
  if (answers.sync_launchctl) {
66
80
  actions.push({ type: 'sync_launchctl', target: 'macOS launchctl user environment', effect: 'sync non-secret CODEX_LB_BASE_URL only and remove API-key launchd env', command: 'launchctl setenv CODEX_LB_BASE_URL ...; launchctl unsetenv CODEX_LB_API_KEY OPENROUTER_API_KEY' });
@@ -113,15 +127,78 @@ export async function installCodexLbShellProfileSnippet(opts) {
113
127
  if (!(await exists(opts.envPath))) {
114
128
  return { ok: true, status: 'skipped', skipped: true, reason: 'env_file_not_written', files: [] };
115
129
  }
130
+ if (Boolean(opts.expectedFiles) !== Boolean(opts.writeFileIfUnchanged)) {
131
+ return {
132
+ ok: false,
133
+ status: 'setup_cas_contract_incomplete',
134
+ files: [],
135
+ error: 'expectedFiles and writeFileIfUnchanged must be provided together'
136
+ };
137
+ }
116
138
  const home = opts.home || process.env.HOME || os.homedir();
117
- const files = profileTargets(home, opts.shellProfile);
118
- for (const file of files) {
119
- const current = await readText(file, '');
139
+ const targets = profileTargets(home, opts.shellProfile);
140
+ const files = [];
141
+ const recoveryPaths = [];
142
+ for (const file of targets) {
143
+ const expected = opts.expectedFiles?.find((entry) => path.resolve(entry.path) === path.resolve(file));
144
+ if (opts.expectedFiles && !expected) {
145
+ return {
146
+ ok: false,
147
+ status: 'setup_snapshot_missing',
148
+ files,
149
+ recovery_paths: recoveryPaths,
150
+ error: `setup_snapshot_missing:${file}`
151
+ };
152
+ }
153
+ if (expected?.existed === true && expected.kind !== 'regular') {
154
+ return {
155
+ ok: false,
156
+ status: 'unsafe_setup_write_target',
157
+ files,
158
+ recovery_paths: recoveryPaths,
159
+ error: `unsafe_setup_write_target:${file}:${expected.kind}`
160
+ };
161
+ }
162
+ const current = expected
163
+ ? (expected.existed === true
164
+ ? Buffer.from(String(expected.bytes_base64 || ''), 'base64').toString('utf8')
165
+ : '')
166
+ : await readText(file, '');
120
167
  const block = shellProfileBlock(file, opts.envPath);
121
- await ensureDir(path.dirname(file));
122
- await writeTextAtomic(file, upsertManagedBlock(current, block));
168
+ const text = upsertManagedBlock(current, block);
169
+ const existingMode = expected
170
+ ? (expected.existed === true ? Number(expected.mode) & 0o777 : null)
171
+ : await fsp.lstat(file)
172
+ .then((stat) => stat.isFile() && !stat.isSymbolicLink() ? stat.mode & 0o777 : null)
173
+ .catch(() => null);
174
+ const mode = existingMode ?? (0o666 & ~process.umask());
175
+ if (expected && opts.writeFileIfUnchanged) {
176
+ const result = await opts.writeFileIfUnchanged({ file, expected, text, mode });
177
+ if (result.recovery_path)
178
+ recoveryPaths.push(result.recovery_path);
179
+ if (!result.ok) {
180
+ return {
181
+ ok: false,
182
+ status: result.status,
183
+ files,
184
+ recovery_paths: recoveryPaths,
185
+ error: result.error || `${result.status}:${file}`
186
+ };
187
+ }
188
+ }
189
+ else {
190
+ await ensureDir(path.dirname(file));
191
+ await writeTextAtomic(file, text);
192
+ }
193
+ await opts.onFileWritten?.({ file, text, mode });
194
+ files.push(file);
123
195
  }
124
- return { ok: true, status: 'installed', files };
196
+ return {
197
+ ok: true,
198
+ status: 'installed',
199
+ files,
200
+ ...(recoveryPaths.length ? { recovery_paths: recoveryPaths } : {})
201
+ };
125
202
  }
126
203
  export function selectedCodexLbPersistenceModes(answers) {
127
204
  const modes = [];