sneakoscope 8.0.2 → 8.0.3

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 +47 -23
  2. package/config/skills-hash-ledger.v1.json +1 -1
  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 +55 -13
  6. package/dist/bin/sks-dispatch.js +1 -1
  7. package/dist/cli/command-manifest-lite.js +4 -0
  8. package/dist/cli/command-registry.js +4 -0
  9. package/dist/cli/install-helpers-codex-lb-chain.js +187 -0
  10. package/dist/cli/install-helpers-codex-lb-config.js +74 -8
  11. package/dist/cli/install-helpers.js +89 -44
  12. package/dist/commands/codex-lb.js +117 -19
  13. package/dist/commands/doctor.js +697 -37
  14. package/dist/config/skills-manifest.json +3 -2
  15. package/dist/core/agents/agent-runner-codex-exec.js +5 -3
  16. package/dist/core/codex/codex-config-guard.js +238 -52
  17. package/dist/core/codex-adapter.js +5 -3
  18. package/dist/core/codex-app/menubar/constants.js +10 -1
  19. package/dist/core/codex-app/menubar/global-install.js +288 -0
  20. package/dist/core/codex-app/menubar/installer.js +79 -14
  21. package/dist/core/codex-app/menubar/launch-agent.js +267 -45
  22. package/dist/core/codex-app/menubar/paths.js +3 -0
  23. package/dist/core/codex-app/menubar/rollback.js +55 -7
  24. package/dist/core/codex-app/menubar/status.js +75 -4
  25. package/dist/core/codex-control/codex-0139-image-path-real-probe.js +3 -1
  26. package/dist/core/codex-control/codex-0139-web-search-probe.js +3 -1
  27. package/dist/core/codex-control/codex-app-server-runtime-env.js +13 -4
  28. package/dist/core/codex-control/codex-app-server-v2-client.js +3 -3
  29. package/dist/core/codex-exec-output-schema.js +4 -1
  30. package/dist/core/codex-lb/codex-lb-env.js +63 -72
  31. package/dist/core/codex-lb/codex-lb-setup.js +15 -2
  32. package/dist/core/codex-lb/desktop-center-credentials.js +517 -58
  33. package/dist/core/codex-lb/desktop-controller.js +153 -24
  34. package/dist/core/codex-lb/routing-truth.js +327 -0
  35. package/dist/core/codex-native/core-skill-manifest.js +1 -1
  36. package/dist/core/commands/basic-cli.js +7 -1
  37. package/dist/core/commands/qa-loop-command.js +3 -1
  38. package/dist/core/commands/telegram-command.js +241 -0
  39. package/dist/core/config-adopt/config-adopt-command.js +35 -0
  40. package/dist/core/config-adopt/config-adopt.js +346 -0
  41. package/dist/core/config-adopt/index.js +3 -0
  42. package/dist/core/doctor/command-alias-cleanup.js +4 -1
  43. package/dist/core/doctor/current-project-guidance-nested.js +37 -6
  44. package/dist/core/doctor/current-project-guidance.js +11 -6
  45. package/dist/core/doctor/doctor-readiness-matrix.js +14 -0
  46. package/dist/core/doctor/retired-managed-residue-private.js +1 -1
  47. package/dist/core/doctor/sks-menubar-doctor.js +4 -0
  48. package/dist/core/feature-fixtures.js +8 -0
  49. package/dist/core/hooks-runtime/hook-context.js +9 -0
  50. package/dist/core/hooks-runtime/managed-skill-admission.js +54 -4
  51. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +69 -1
  52. package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +2 -2
  53. package/dist/core/hooks-runtime/subagent-skill-availability-evidence.js +3 -3
  54. package/dist/core/hooks-runtime/subagent-skill-availability-guards.js +43 -5
  55. package/dist/core/hooks-runtime/subagent-skill-availability.js +64 -30
  56. package/dist/core/hooks-runtime/subagent-stop-hook.js +15 -2
  57. package/dist/core/hooks-runtime.js +148 -4
  58. package/dist/core/imagegen/imagegen-capability.js +46 -0
  59. package/dist/core/imagegen/require-imagegen.js +2 -2
  60. package/dist/core/init/skills.js +17 -3
  61. package/dist/core/install/installed-package-smoke.js +8 -2
  62. package/dist/core/naruto/hardware-capacity-probe.js +9 -3
  63. package/dist/core/naruto/naruto-concurrency-governor.js +68 -24
  64. package/dist/core/perf/import-graph-budget.js +0 -1
  65. package/dist/core/product-design-app-server.js +13 -1
  66. package/dist/core/release/package-size-budget.js +2 -2
  67. package/dist/core/release/release-pack-receipt.js +1 -2
  68. package/dist/core/routes/dollar-manifest-lite.js +1 -1
  69. package/dist/core/routes.js +1 -1
  70. package/dist/core/security/private-credential-file.js +209 -0
  71. package/dist/core/subagents/agent-catalog.js +17 -32
  72. package/dist/core/subagents/naruto-help-contract.js +5 -5
  73. package/dist/core/subagents/official-subagent-config.js +16 -6
  74. package/dist/core/subagents/official-subagent-preparation.js +34 -15
  75. package/dist/core/subagents/official-subagent-prompt.js +4 -4
  76. package/dist/core/subagents/official-subagent-runner.js +27 -4
  77. package/dist/core/subagents/thread-budget.js +19 -12
  78. package/dist/core/subagents/wave-lifecycle.js +18 -6
  79. package/dist/core/telegram/access.js +54 -0
  80. package/dist/core/telegram/client.js +144 -0
  81. package/dist/core/telegram/confirmation.js +37 -0
  82. package/dist/core/telegram/doctor.js +151 -0
  83. package/dist/core/telegram/index.js +9 -0
  84. package/dist/core/telegram/keychain.js +265 -0
  85. package/dist/core/telegram/liveness.js +109 -0
  86. package/dist/core/telegram/poller.js +90 -0
  87. package/dist/core/telegram/redaction.js +15 -0
  88. package/dist/core/telegram/types.js +1 -0
  89. package/dist/core/update/update-diagnostics.js +52 -0
  90. package/dist/core/update/update-migration-state.js +5 -1
  91. package/dist/core/update/update-operation.js +2 -1
  92. package/dist/core/update/update-stage-diagnostics.js +38 -0
  93. package/dist/core/update/update-status.js +2 -9
  94. package/dist/core/update-check.js +147 -31
  95. package/dist/core/version.js +1 -1
  96. package/dist/core/zellij/zellij-launcher.js +23 -4
  97. package/dist/native/sks-menubar/Sources/AppDelegate.swift +13 -0
  98. package/dist/native/sks-menubar/Sources/AppIdentity.swift +7 -0
  99. package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +3 -2
  100. package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +115 -0
  101. package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +17 -0
  102. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +190 -0
  103. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +100 -59
  104. package/dist/native/sks-menubar/Sources/SingletonInstanceGuard.swift +328 -0
  105. package/dist/native/sks-menubar/Sources/TelegramPrivateFileStore.swift +320 -0
  106. package/dist/native/sks-menubar/Sources/TelegramPrivateFileSupport.swift +303 -0
  107. package/dist/native/sks-menubar/Sources/TelegramProcessGateway.swift +238 -0
  108. package/dist/native/sks-menubar/Sources/TelegramRuntimeSupport.swift +56 -0
  109. package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
  110. package/dist/native/sks-menubar/Sources/TelegramTransport.swift +474 -0
  111. package/dist/native/sks-menubar/Sources/main.swift +15 -1
  112. package/dist/native/sks-menubar/Tests/OperationCoordinatorTests.swift +4 -3
  113. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +129 -0
  114. package/dist/native/sks-menubar/Tests/SingletonInstanceGuardTests.swift +183 -0
  115. package/dist/native/sks-menubar/Tests/TelegramRuntimeSmokeTests.swift +267 -0
  116. package/dist/native/sks-menubar/Tests/TelegramRuntimeTests.swift +222 -0
  117. package/dist/scripts/codex-lb-fast-mode-truth-check.js +1 -1
  118. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +1 -1
  119. package/dist/scripts/current-command-surface-check.js +2 -4
  120. package/dist/scripts/official-subagent-workflow-check.js +1 -1
  121. package/dist/scripts/packlist-performance-check.js +1 -2
  122. package/package.json +1 -1
@@ -4,16 +4,16 @@ import path from 'node:path';
4
4
  import { exists, PACKAGE_VERSION, readJson, readText, sha256 } from '../../fsx.js';
5
5
  import { smokeSksMenuBarAction } from './action-runner.js';
6
6
  import { isCodexAppRunningByBundleId, readMenuBarConfig } from './config.js';
7
- import { inspectLaunchdService, isMenuBarProcessRunning, removeLaunchAgent, restartLaunchAgent } from './launch-agent.js';
7
+ import { inspectLaunchdService, inspectRunningMenuBarProcess, removeLaunchAgent, restartLaunchAgent, waitForRunningMenuBarVersion } from './launch-agent.js';
8
8
  import { sksMenuBarPaths } from './paths.js';
9
9
  import { inspectInstalledResources } from './resources.js';
10
10
  import { inspectMenuBarArtifactSet } from './rollback.js';
11
11
  import { inspectSignature } from './signature.js';
12
12
  import { cleanupRetiredRemoteBridgeLaunchAgent, quarantineRetiredRemoteBridgeBindings } from './migration.js';
13
+ import { inspectProjectMenuBarCanonicalState } from './global-install.js';
13
14
  export async function inspectSksMenuBarStatus(opts = {}) {
14
15
  const paths = sksMenuBarPaths(opts.home || opts.env?.HOME, opts.root);
15
16
  const installed = await exists(paths.executable_path);
16
- const running = installed ? await isMenuBarProcessRunning(paths.executable_path, opts.env) : false;
17
17
  const actionText = await readText(paths.action_script_path, '');
18
18
  const nodeBin = shellAssignment(actionText, 'NODE_BIN');
19
19
  const sksEntry = shellAssignment(actionText, 'SKS_ENTRY');
@@ -25,6 +25,23 @@ export async function inspectSksMenuBarStatus(opts = {}) {
25
25
  const config = await readMenuBarConfig(paths.config_path);
26
26
  const codexRunning = config.codex_bundle_id ? await isCodexAppRunningByBundleId(config.codex_bundle_id, opts.env) : null;
27
27
  const launchd = await inspectLaunchdService(opts.env);
28
+ const runningProcess = await inspectRunningMenuBarProcess(paths, launchd.pid, opts.env);
29
+ const running = runningProcess.ok;
30
+ const installedVersion = buildStamp?.package_version || null;
31
+ const persistedVersionProbe = await readJson(paths.version_probe_path, null);
32
+ let versionProbe = evaluatePersistedMenuBarVersionProbe({
33
+ probe: persistedVersionProbe,
34
+ expectedVersion,
35
+ runningProcess,
36
+ persistedPath: paths.version_probe_path
37
+ });
38
+ if (!versionProbe.ok
39
+ && runningProcess.ok
40
+ && runningProcess.package_version === expectedVersion
41
+ && (versionProbe.error === 'menubar_version_probe_missing'
42
+ || versionProbe.error === 'menubar_version_probe_stale')) {
43
+ versionProbe = await waitForRunningMenuBarVersion(paths, expectedVersion, launchd.pid, opts.env);
44
+ }
28
45
  const legacyVerification = buildStamp?.legacy_v1
29
46
  ? await inspectMenuBarArtifactSet({
30
47
  appPath: paths.app_path,
@@ -36,15 +53,30 @@ export async function inspectSksMenuBarStatus(opts = {}) {
36
53
  : null;
37
54
  const signature = legacyVerification?.signature || await inspectSignature(paths.app_path, opts.env);
38
55
  const resources = legacyVerification?.resources || await inspectInstalledResources({ resourcesDir: paths.resources_path, buildStamp });
56
+ const canonicalState = await inspectProjectMenuBarCanonicalState({
57
+ paths,
58
+ root: paths.root,
59
+ ...(opts.env ? { env: opts.env } : {})
60
+ });
39
61
  const blockers = [];
40
62
  const warnings = [];
41
- const runtime = evaluateMenuBarRuntimeReadiness({ installed, running, launchd });
63
+ const runtime = evaluateMenuBarRuntimeReadiness({
64
+ installed,
65
+ running,
66
+ verifiedProcessMatchesExpectedVersion: runningProcess.ok
67
+ && runningProcess.package_version === expectedVersion,
68
+ launchd
69
+ });
42
70
  if (!installed)
43
71
  blockers.push('menubar_app_missing');
44
72
  if (runtime.blocker)
45
73
  blockers.push(runtime.blocker);
46
74
  if (runtime.warning)
47
75
  warnings.push(runtime.warning);
76
+ if (installed && launchd.checked && launchd.ok && !runningProcess.ok)
77
+ blockers.push('menubar_runtime_identity_unverified');
78
+ if (installed && launchd.checked && launchd.ok && !versionProbe.ok)
79
+ blockers.push(versionProbe.error || 'menubar_version_probe_failed');
48
80
  if (installed && !actionSmoke.executable)
49
81
  blockers.push('action_script_not_executable');
50
82
  if (installed && !actionSmoke.ok)
@@ -63,11 +95,21 @@ export async function inspectSksMenuBarStatus(opts = {}) {
63
95
  blockers.push('menubar_install_transaction_pending');
64
96
  if (await exists(paths.rollback_transaction_path))
65
97
  blockers.push('menubar_rollback_transaction_pending');
98
+ blockers.push(...canonicalState.blockers);
99
+ if (canonicalState.verified_duplicates.length > 0)
100
+ blockers.push('menubar_canonical_only_verified_duplicate_present');
101
+ if (canonicalState.unverified_collisions.length > 0)
102
+ blockers.push('menubar_canonical_only_unverified_duplicate_present');
103
+ warnings.push(...canonicalState.warnings);
66
104
  if (!config.codex_bundle_id)
67
105
  warnings.push('codex_sync_disabled');
68
106
  return {
69
107
  schema: 'sks.menubar-status.v1', ok: blockers.length === 0, platform: process.platform,
70
- installed, running, paths, launchd,
108
+ installed, running, installed_version: installedVersion, running_process: runningProcess,
109
+ menubar_version_probe: versionProbe,
110
+ duplicate_install_candidates: canonicalState.candidate_paths,
111
+ paths,
112
+ launchd,
71
113
  action_target: {
72
114
  node_bin: nodeBin,
73
115
  node_exists: nodeBin ? await isExecutable(nodeBin) : false,
@@ -94,10 +136,39 @@ export async function inspectSksMenuBarStatus(opts = {}) {
94
136
  next_actions: blockers.length ? defaultNextActions() : ['sks menubar status --json']
95
137
  };
96
138
  }
139
+ export function evaluatePersistedMenuBarVersionProbe(input) {
140
+ const base = {
141
+ schema: 'sks.menubar-version-probe.v1',
142
+ checked: true,
143
+ expected_version: input.expectedVersion,
144
+ running_version: input.runningProcess.package_version,
145
+ pid: input.runningProcess.pid,
146
+ generated_at: input.probe?.generated_at || new Date(0).toISOString(),
147
+ persisted_path: input.persistedPath
148
+ };
149
+ if (!input.probe || input.probe.schema !== 'sks.menubar-version-probe.v1') {
150
+ return { ...base, ok: false, error: 'menubar_version_probe_missing' };
151
+ }
152
+ if (!input.runningProcess.ok) {
153
+ return { ...base, ok: false, error: input.runningProcess.error || 'menubar_runtime_identity_unverified' };
154
+ }
155
+ if (input.probe.expected_version !== input.expectedVersion
156
+ || input.probe.running_version !== input.runningProcess.package_version
157
+ || input.probe.pid !== input.runningProcess.pid) {
158
+ return { ...base, ok: false, error: 'menubar_version_probe_stale' };
159
+ }
160
+ if (input.runningProcess.package_version !== input.expectedVersion) {
161
+ return { ...base, ok: false, error: 'menubar_running_version_mismatch' };
162
+ }
163
+ return { ...base, generated_at: input.probe.generated_at, ok: true, error: null };
164
+ }
97
165
  export function evaluateMenuBarRuntimeReadiness(input) {
98
166
  if (!input.installed || !input.launchd.checked || input.launchd.ok) {
99
167
  return { blocker: null, warning: null };
100
168
  }
169
+ if (input.running && input.verifiedProcessMatchesExpectedVersion === true) {
170
+ return { blocker: null, warning: 'launchd_not_running_process_active' };
171
+ }
101
172
  return {
102
173
  blocker: 'launchd_not_running',
103
174
  warning: input.running ? 'launchd_not_running_process_active' : null
@@ -4,6 +4,7 @@ import { buildCodexExecArgs, findCodexBinary, runCodexExec } from '../codex-adap
4
4
  import { ensureDir, runProcess, writeBinaryAtomic } from '../fsx.js';
5
5
  import { buildImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
6
6
  import { codex0139ProbeTail, skippedCodex0139Probe } from './codex-0139-real-probes.js';
7
+ import { prepareCodexAppServerRuntimeEnv } from './codex-app-server-runtime-env.js';
7
8
  const ONE_BY_ONE_PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC', 'base64');
8
9
  export async function runCodex0139ImageReferencedPathRealProbe(input) {
9
10
  const started = Date.now();
@@ -55,6 +56,7 @@ export async function runCodex0139ImageReferencedPathRealProbe(input) {
55
56
  ].join(' ');
56
57
  const extraArgs = ['-c', 'mcp_servers={}', '--image', inputB, '--skip-git-repo-check', '--ephemeral'];
57
58
  const args = buildCodexExecArgs({ root: tempDir, prompt, outputFile, json: true, extraArgs });
59
+ const runtimeEnv = await prepareCodexAppServerRuntimeEnv({ env: input.env || process.env });
58
60
  const result = await runCodexExec({
59
61
  root: tempDir,
60
62
  recoveryRoot: input.root,
@@ -65,7 +67,7 @@ export async function runCodex0139ImageReferencedPathRealProbe(input) {
65
67
  timeoutMs: input.timeoutMs || 60000,
66
68
  maxBufferBytes: 512 * 1024,
67
69
  codexBin,
68
- env: input.env || process.env,
70
+ env: runtimeEnv,
69
71
  ...(typeof input.recoveryFetch === 'function' ? { recoveryFetch: input.recoveryFetch } : {}),
70
72
  ...(typeof input.runProcessImpl === 'function' ? { runProcessImpl: input.runProcessImpl } : {})
71
73
  }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { buildCodexExecArgs, findCodexBinary, runCodexExec } from '../codex-adapter.js';
4
4
  import { ensureDir, runProcess, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
5
5
  import { codex0139ProbeTail, skippedCodex0139Probe } from './codex-0139-real-probes.js';
6
+ import { prepareCodexAppServerRuntimeEnv } from './codex-app-server-runtime-env.js';
6
7
  export async function runCodex0139WebSearchRealProbe(input) {
7
8
  const started = Date.now();
8
9
  if (!input.allowNetwork) {
@@ -18,6 +19,7 @@ export async function runCodex0139WebSearchRealProbe(input) {
18
19
  const outputFile = path.join(tempDir, 'last-message.txt');
19
20
  const prompt = 'In code mode, use standalone web search to find the title of https://example.com. Return JSON {"used_web_search":true,"answer":"...","sources":[...]}.';
20
21
  const args = buildCodexExecArgs({ root: tempDir, prompt, outputFile, json: true, extraArgs: ['-c', 'mcp_servers={}'] });
22
+ const runtimeEnv = await prepareCodexAppServerRuntimeEnv({ env: input.env || process.env });
21
23
  const result = await runCodexExec({
22
24
  root: tempDir,
23
25
  recoveryRoot: input.root,
@@ -30,7 +32,7 @@ export async function runCodex0139WebSearchRealProbe(input) {
30
32
  stdoutFile: path.join(tempDir, 'codex.stdout.log'),
31
33
  stderrFile: path.join(tempDir, 'codex.stderr.log'),
32
34
  codexBin,
33
- env: input.env || process.env,
35
+ env: runtimeEnv,
34
36
  ...(typeof input.recoveryFetch === 'function' ? { recoveryFetch: input.recoveryFetch } : {}),
35
37
  ...(typeof input.runProcessImpl === 'function' ? { runProcessImpl: input.runProcessImpl } : {})
36
38
  }).catch((err) => ({
@@ -7,12 +7,21 @@ export const CODEX_APP_SERVER_PROVIDER_CREDENTIALS_UNAVAILABLE = 'codex_app_serv
7
7
  export const CODEX_APP_SERVER_CONFIG_READ_FAILED = 'codex_app_server_config_read_failed';
8
8
  export class CodexAppServerRuntimeEnvError extends Error {
9
9
  code;
10
- constructor(code) {
11
- super(code);
10
+ operatorActions;
11
+ constructor(code, operatorActions = []) {
12
+ super(operatorActions.length > 0 ? `${code}: ${operatorActions.join(' ')}` : code);
12
13
  this.code = code;
14
+ this.operatorActions = operatorActions;
13
15
  this.name = 'CodexAppServerRuntimeEnvError';
14
16
  }
15
17
  }
18
+ export function codexAppServerProviderCredentialGuidance(home) {
19
+ return [
20
+ `Store the key in ${path.join(home, '.codex', 'sks-codex-lb.env')} (owner-only mode 0600).`,
21
+ 'Run: sks codex-lb setup --host <domain> --api-key-stdin --yes',
22
+ 'Alternatively, provide CODEX_LB_API_KEY in the launching environment.'
23
+ ];
24
+ }
16
25
  export function codexAppServerExecutablePath(input) {
17
26
  const home = path.resolve(input.home || process.env.HOME || os.homedir());
18
27
  const candidates = [
@@ -75,10 +84,10 @@ export async function prepareCodexAppServerRuntimeEnv(input = {}) {
75
84
  });
76
85
  }
77
86
  catch {
78
- throw new CodexAppServerRuntimeEnvError(CODEX_APP_SERVER_PROVIDER_CREDENTIALS_UNAVAILABLE);
87
+ throw new CodexAppServerRuntimeEnvError(CODEX_APP_SERVER_PROVIDER_CREDENTIALS_UNAVAILABLE, codexAppServerProviderCredentialGuidance(home));
79
88
  }
80
89
  if (!loaded?.configured || !loaded.secret_api_key || !loaded.base_url) {
81
- throw new CodexAppServerRuntimeEnvError(CODEX_APP_SERVER_PROVIDER_CREDENTIALS_UNAVAILABLE);
90
+ throw new CodexAppServerRuntimeEnvError(CODEX_APP_SERVER_PROVIDER_CREDENTIALS_UNAVAILABLE, codexAppServerProviderCredentialGuidance(home));
82
91
  }
83
92
  env.CODEX_LB_API_KEY = loaded.secret_api_key;
84
93
  env.CODEX_LB_BASE_URL = loaded.base_url;
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { nowIso, PACKAGE_VERSION } from '../fsx.js';
3
3
  import { resolveCodexRuntime } from '../codex-runtime/resolve-codex-runtime.js';
4
+ import { prepareCodexAppServerRuntimeEnv } from './codex-app-server-runtime-env.js';
4
5
  export class CodexAppServerRequestError extends Error {
5
6
  method;
6
7
  kind;
@@ -333,13 +334,12 @@ export async function createCodexAppServerV2Client(options = {}) {
333
334
  });
334
335
  if (!runtime.identity)
335
336
  throw new Error(`Codex runtime not found: ${runtime.blockers.join(',')}`);
336
- const clientOptions = { command: runtime.identity.realpath };
337
+ const runtimeEnv = await prepareCodexAppServerRuntimeEnv({ env: options.env || process.env });
338
+ const clientOptions = { command: runtime.identity.realpath, env: runtimeEnv };
337
339
  if (options.args !== undefined)
338
340
  clientOptions.args = options.args;
339
341
  if (options.cwd !== undefined)
340
342
  clientOptions.cwd = options.cwd;
341
- if (options.env !== undefined)
342
- clientOptions.env = options.env;
343
343
  if (options.timeoutMs !== undefined)
344
344
  clientOptions.timeoutMs = options.timeoutMs;
345
345
  if (options.maxFrameBytes !== undefined)
@@ -4,6 +4,7 @@ import { ensureDir, exists, packageRoot, readJson, runProcess, which } from './f
4
4
  import { codexVersionPolicy, compareSemverLike, parseCodexVersionText } from './codex-compat/codex-version-policy.js';
5
5
  import { validateJsonSchemaRecursive } from './json-schema-validator.js';
6
6
  import { inspectCodexLbCliLaunchRecovery } from './codex-control/codex-lb-launch-recovery.js';
7
+ import { prepareCodexAppServerRuntimeEnv } from './codex-control/codex-app-server-runtime-env.js';
7
8
  export async function detectCodexExecOutputSchemaSyntax(opts = {}) {
8
9
  const codexBin = opts.codexBin || await which('codex').catch(() => null);
9
10
  if (!codexBin) {
@@ -169,7 +170,9 @@ export async function buildCodexExecResumeOutputSchemaArgs(input) {
169
170
  }
170
171
  export async function runCodexExecResumeWithOutputSchema(input, opts = {}) {
171
172
  const root = opts.cwd || packageRoot();
172
- const env = opts.env || process.env;
173
+ const env = await (opts.prepareCodexRuntimeEnvImpl || prepareCodexAppServerRuntimeEnv)({
174
+ env: opts.env || process.env
175
+ });
173
176
  const toolOutputRecovery = await inspectCodexLbCliLaunchRecovery({
174
177
  root,
175
178
  env,
@@ -1,7 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
+ import fsp from 'node:fs/promises';
3
4
  import { exists, readJson, readText } from '../fsx.js';
4
5
  import { escapeRegExp } from '../text/regex.js';
6
+ import { PrivateCredentialFileError, readPrivateCredentialFile } from '../security/private-credential-file.js';
5
7
  export const CODEX_LB_SECURE_KEYCHAIN_SERVICE = 'com.sneakoscope.codex-lb.api-key.v2';
6
8
  export const CODEX_LB_LEGACY_KEYCHAIN_SERVICE = 'sks-codex-lb';
7
9
  export function codexLbEnvPath(home = process.env.HOME || os.homedir()) {
@@ -158,15 +160,13 @@ export async function loadCodexLbEnv(opts = {}) {
158
160
  if (opts.allowProjectSecrets)
159
161
  sourcePriority.push('project-local');
160
162
  const processEnv = pickEnv(opts.processEnv || process.env);
161
- const envFile = await readEnvFile(envPaths[0]);
162
- const projectLocal = opts.allowProjectSecrets ? await readEnvFile(path.join(opts.root || process.cwd(), '.env')) : {};
163
+ const envFile = await readEnvFile(envPaths[0], { privateFile: true });
164
+ const projectLocal = opts.allowProjectSecrets
165
+ ? await readEnvFile(path.join(opts.root || process.cwd(), '.env'))
166
+ : {};
163
167
  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: '' };
167
168
  const apiKeyCandidates = [
168
169
  { source: 'env-file', apiKey: envFile.apiKey },
169
- { source: 'keychain', apiKey: keychain.apiKey },
170
170
  { source: 'process.env', apiKey: processEnv.apiKey },
171
171
  ...(opts.allowProjectSecrets ? [{ source: 'project-local', apiKey: String(projectLocal.apiKey || '') }] : [])
172
172
  ].filter((candidate) => Boolean(candidate.apiKey));
@@ -174,9 +174,7 @@ export async function loadCodexLbEnv(opts = {}) {
174
174
  ...candidate,
175
175
  sha256: await sha256Full(candidate.apiKey)
176
176
  })));
177
- const selectedApiKey = metadata.valid
178
- ? candidateFingerprints.find((candidate) => candidate.sha256 === metadata.apiKeySha256) || candidateFingerprints[0]
179
- : candidateFingerprints[0];
177
+ const selectedApiKey = candidateFingerprints[0];
180
178
  const keySource = selectedApiKey?.source || 'missing';
181
179
  const apiKey = selectedApiKey?.apiKey || '';
182
180
  const configuredBaseUrl = normalizeCodexLbBaseUrl(envFile.baseUrl || processEnv.baseUrl || projectLocal.baseUrl || '');
@@ -187,20 +185,23 @@ export async function loadCodexLbEnv(opts = {}) {
187
185
  apiKeySha256,
188
186
  configuredBaseUrl
189
187
  });
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
- : [];
188
+ const resolutionBlockers = [
189
+ ...envFile.blockers,
190
+ ...(apiKey ? [] : ['codex_lb_api_key_missing'])
191
+ ];
197
192
  const baseUrl = binding.present && metadata.baseUrl ? metadata.baseUrl : configuredBaseUrl;
198
- const apiKeyUsable = Boolean(apiKey) && binding.blockers.length === 0 && keychainBlockers.length === 0;
193
+ const apiKeyUsable = Boolean(apiKey) && binding.blockers.length === 0 && resolutionBlockers.length === 0;
199
194
  const missing = [
200
195
  ...(apiKey ? [] : ['CODEX_LB_API_KEY']),
201
196
  ...(baseUrl ? [] : ['CODEX_LB_BASE_URL']),
202
197
  ...(binding.blockers.length ? ['CODEX_LB_CREDENTIAL_BINDING'] : []),
203
- ...(keychainBlockers.length ? ['CODEX_LB_KEYCHAIN'] : [])
198
+ ...(envFile.blockers.length ? ['CODEX_LB_CREDENTIAL_FILE'] : [])
199
+ ];
200
+ const guidance = [
201
+ ...envFile.guidance,
202
+ ...(!apiKey
203
+ ? [`Configure CODEX_LB_API_KEY with \`sks codex-lb setup --host <domain> --api-key-stdin --yes\`, put it in ${envPaths[0]} (mode 0600), or export CODEX_LB_API_KEY.`]
204
+ : [])
204
205
  ];
205
206
  return {
206
207
  schema: 'sks.codex-lb-env.v1',
@@ -217,32 +218,20 @@ export async function loadCodexLbEnv(opts = {}) {
217
218
  fingerprint: apiKeySha256 ? apiKeySha256.slice(0, 16) : null
218
219
  },
219
220
  secret_api_key: apiKeyUsable ? apiKey : null,
221
+ blockers: [...new Set([...binding.blockers, ...resolutionBlockers])],
222
+ guidance: [...new Set(guidance)],
220
223
  credential_binding: {
221
224
  ...binding,
222
- blockers: [...binding.blockers, ...keychainBlockers]
225
+ blockers: [...new Set([...binding.blockers, ...resolutionBlockers])]
223
226
  },
224
227
  env_paths: envPaths,
225
228
  keychain: {
226
- checked: keychain.checked,
227
- available: keychain.available,
228
- status: keychain.status
229
+ checked: false,
230
+ available: false,
231
+ status: 'not_used'
229
232
  }
230
233
  };
231
234
  }
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
- }
246
235
  async function readCodexLbCredentialMetadata(file) {
247
236
  if (!(await exists(file)))
248
237
  return { present: false, valid: false, baseUrl: '', apiKeySha256: '' };
@@ -322,44 +311,46 @@ function evaluateCodexLbCredentialBinding(input) {
322
311
  blockers: []
323
312
  };
324
313
  }
325
- export async function writeCodexLbKeychain(apiKey, opts = {}) {
326
- const key = String(apiKey || '').trim();
327
- if (!key)
328
- return { ok: false, status: 'missing_api_key' };
329
- if (process.platform !== 'darwin' && !opts.forceMacos)
330
- return { ok: false, status: 'not_macos' };
331
- const account = opts.account || process.env.USER || 'sks';
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);
336
- return {
337
- ok: false,
338
- status: 'keychain_acl_helper_unavailable',
339
- account,
340
- service,
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).'
346
- };
347
- }
348
- async function readMacKeychain(opts = {}) {
349
- if (process.platform !== 'darwin' && !opts.forceMacos)
350
- return { checked: false, available: false, status: 'not_macos', apiKey: '' };
351
- return {
352
- checked: false,
353
- available: false,
354
- status: 'keychain_acl_helper_unavailable',
355
- apiKey: ''
356
- };
357
- }
358
- async function readEnvFile(file) {
359
- const text = await readText(file, '');
314
+ async function readEnvFile(file, opts = {}) {
315
+ let text = '';
316
+ const blockers = [];
317
+ const guidance = [];
318
+ if (opts.privateFile) {
319
+ let pathExists = false;
320
+ try {
321
+ await fsp.lstat(file);
322
+ pathExists = true;
323
+ }
324
+ catch (error) {
325
+ if (error?.code !== 'ENOENT') {
326
+ blockers.push('codex_lb_env_file_read_failed');
327
+ guidance.push(`Replace the unreadable credential file at ${file} with \`sks codex-lb setup --host <domain> --api-key-stdin --yes\`.`);
328
+ }
329
+ }
330
+ if (!pathExists) {
331
+ return { apiKey: '', baseUrl: '', blockers, guidance };
332
+ }
333
+ try {
334
+ text = (await readPrivateCredentialFile(path.dirname(file), file, 'codex_lb_env_file')).bytes.toString('utf8');
335
+ }
336
+ catch (error) {
337
+ if (!(error instanceof PrivateCredentialFileError) || error.code !== 'missing') {
338
+ const code = error instanceof PrivateCredentialFileError ? error.code : 'read_failed';
339
+ blockers.push(`codex_lb_env_file_${code}`);
340
+ guidance.push(code === 'mode_not_0600'
341
+ ? `Run \`chmod 600 ${file}\` or \`sks doctor --fix\`, then retry.`
342
+ : `Replace the unsafe credential file at ${file} with \`sks codex-lb setup --host <domain> --api-key-stdin --yes\`.`);
343
+ }
344
+ }
345
+ }
346
+ else {
347
+ text = await readText(file, '');
348
+ }
360
349
  return {
361
350
  apiKey: parseShellEnvValue(text, 'CODEX_LB_API_KEY'),
362
- baseUrl: parseShellEnvValue(text, 'CODEX_LB_BASE_URL')
351
+ baseUrl: parseShellEnvValue(text, 'CODEX_LB_BASE_URL'),
352
+ blockers,
353
+ guidance
363
354
  };
364
355
  }
365
356
  function pickEnv(env) {
@@ -63,7 +63,9 @@ export function buildCodexLbSetupPlan(answers, opts = {}) {
63
63
  actions.push({
64
64
  type: 'write_cli_provider',
65
65
  target: configPath,
66
- effect: 'write an unselected CLI-only codex-lb provider using CODEX_LB_API_KEY without changing Desktop OAuth'
66
+ effect: answers.use_as_default_provider === true
67
+ ? 'atomically write and select the codex-lb provider using CODEX_LB_API_KEY without changing auth.json'
68
+ : 'write an unselected CLI-only codex-lb provider using CODEX_LB_API_KEY without changing Desktop OAuth'
67
69
  });
68
70
  }
69
71
  if (answers.write_env_file) {
@@ -77,7 +79,18 @@ export function buildCodexLbSetupPlan(answers, opts = {}) {
77
79
  });
78
80
  }
79
81
  if (answers.sync_launchctl) {
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' });
82
+ const cliProviderSelected = desktopMode === 'cli-provider'
83
+ && answers.use_as_default_provider === true;
84
+ actions.push({
85
+ type: 'sync_launchctl',
86
+ target: 'macOS launchctl user environment',
87
+ effect: cliProviderSelected
88
+ ? 'set CODEX_LB_API_KEY and CODEX_LB_BASE_URL from the canonical owner-only env file for selected CLI-provider mode'
89
+ : 'remove CODEX_LB_API_KEY outside selected CLI-provider mode and reconcile the non-secret base URL for the chosen mode',
90
+ command: cliProviderSelected
91
+ ? 'launchctl setenv CODEX_LB_API_KEY <canonical-env-file-value>; launchctl setenv CODEX_LB_BASE_URL ...; launchctl unsetenv OPENROUTER_API_KEY'
92
+ : 'launchctl unsetenv CODEX_LB_API_KEY OPENROUTER_API_KEY; reconcile CODEX_LB_BASE_URL for the chosen mode'
93
+ });
81
94
  }
82
95
  if (answers.install_shell_profile !== 'skip') {
83
96
  actions.push({ type: 'install_shell_profile_snippet', target: profileTargets(home, answers.install_shell_profile).join(', '), effect: `install managed shell snippet for ${answers.install_shell_profile}` });