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
@@ -85,7 +85,7 @@ export async function writeCodexConfigGuarded(input) {
85
85
  if (input.verifyUnchangedBeforeWrite === true) {
86
86
  const observed = await readConfigCommitSnapshot(configPath);
87
87
  const expectedExists = input.expectedBeforeExists ?? true;
88
- if (!configCommitSnapshotMatches(observed, expectedExists, before)) {
88
+ if (!configCommitSnapshotMatches(observed, expectedExists, before, input.expectedBeforeMode)) {
89
89
  return recordConcurrentConfigChange({
90
90
  root,
91
91
  reportPath: input.reportPath,
@@ -93,6 +93,9 @@ export async function writeCodexConfigGuarded(input) {
93
93
  configPath,
94
94
  before,
95
95
  expectedExists,
96
+ ...(input.expectedBeforeMode === undefined
97
+ ? {}
98
+ : { expectedBeforeMode: input.expectedBeforeMode }),
96
99
  observed,
97
100
  backupPath: null,
98
101
  repairedKeys: preserved.keys,
@@ -103,7 +106,30 @@ export async function writeCodexConfigGuarded(input) {
103
106
  else {
104
107
  await writeTextAtomic(configPath, next, { mode: 0o600 });
105
108
  }
106
- const result = { ok: true, status: 'present', config_path: configPath, backup_path: null, changed: false, repaired_keys: preserved.keys, forbidden_top_level: forbiddenTopLevel };
109
+ const expectedAfterExists = input.verifyUnchangedBeforeWrite === true
110
+ ? (input.expectedBeforeExists ?? true)
111
+ : true;
112
+ const expectedAfterMode = expectedAfterExists
113
+ ? input.verifyUnchangedBeforeWrite === true
114
+ ? (input.expectedBeforeMode === undefined ? null : Number(input.expectedBeforeMode) & 0o777)
115
+ : 0o600
116
+ : null;
117
+ const result = {
118
+ ok: true,
119
+ status: 'present',
120
+ config_path: configPath,
121
+ backup_path: null,
122
+ changed: false,
123
+ repaired_keys: preserved.keys,
124
+ forbidden_top_level: forbiddenTopLevel,
125
+ expected_after: {
126
+ exists: expectedAfterExists,
127
+ text: expectedAfterExists
128
+ ? (input.verifyUnchangedBeforeWrite === true ? before : next)
129
+ : '',
130
+ mode: expectedAfterMode
131
+ }
132
+ };
107
133
  if (preserved.keys.length || forbiddenTopLevel.length) {
108
134
  await recordCodexConfigGuard(root, input.reportPath, {
109
135
  cause,
@@ -125,6 +151,9 @@ export async function writeCodexConfigGuarded(input) {
125
151
  before,
126
152
  next,
127
153
  expectedExists,
154
+ ...(input.expectedBeforeMode === undefined
155
+ ? {}
156
+ : { expectedBeforeMode: input.expectedBeforeMode }),
128
157
  backupTag: input.backupTag || cause
129
158
  });
130
159
  if (!commit.ok) {
@@ -135,6 +164,9 @@ export async function writeCodexConfigGuarded(input) {
135
164
  configPath,
136
165
  before,
137
166
  expectedExists,
167
+ ...(input.expectedBeforeMode === undefined
168
+ ? {}
169
+ : { expectedBeforeMode: input.expectedBeforeMode }),
138
170
  observed: commit.observed,
139
171
  backupPath: commit.backupPath,
140
172
  repairedKeys: preserved.keys,
@@ -157,7 +189,12 @@ export async function writeCodexConfigGuarded(input) {
157
189
  backup_path: backupPath,
158
190
  changed: true,
159
191
  repaired_keys: preserved.keys,
160
- forbidden_top_level: forbiddenTopLevel
192
+ forbidden_top_level: forbiddenTopLevel,
193
+ expected_after: {
194
+ exists: true,
195
+ text: next,
196
+ mode: 0o600
197
+ }
161
198
  };
162
199
  const reportPath = await recordCodexConfigGuard(root, input.reportPath, {
163
200
  cause,
@@ -181,10 +218,13 @@ export async function writeCodexConfigGuarded(input) {
181
218
  });
182
219
  return { ...result, report_path: reportPath };
183
220
  }
184
- function configCommitSnapshotMatches(observed, expectedExists, expectedText) {
221
+ function configCommitSnapshotMatches(observed, expectedExists, expectedText, expectedMode) {
185
222
  return observed.ok
186
223
  && observed.exists === expectedExists
187
- && sha256(observed.text) === sha256(expectedText);
224
+ && sha256(observed.text) === sha256(expectedText)
225
+ && (!expectedExists
226
+ || expectedMode === undefined
227
+ || observed.mode === (Number(expectedMode) & 0o777));
188
228
  }
189
229
  async function recordConcurrentConfigChange(input) {
190
230
  const result = {
@@ -203,6 +243,10 @@ async function recordConcurrentConfigChange(input) {
203
243
  status: result.status,
204
244
  expected_exists: input.expectedExists,
205
245
  observed_exists: input.observed.exists,
246
+ expected_mode: input.expectedBeforeMode === undefined
247
+ ? null
248
+ : Number(input.expectedBeforeMode) & 0o777,
249
+ observed_mode: input.observed.mode,
206
250
  expected_sha256: sha256(input.before),
207
251
  observed_sha256: input.observed.ok ? sha256(input.observed.text) : null,
208
252
  observed_status: input.observed.status,
@@ -219,7 +263,7 @@ async function commitCodexConfigIfUnchanged(input) {
219
263
  try {
220
264
  await writeTextAtomic(candidatePath, input.next, { mode: 0o600 });
221
265
  const observed = await readConfigCommitSnapshot(input.configPath);
222
- if (!configCommitSnapshotMatches(observed, input.expectedExists, input.before)) {
266
+ if (!configCommitSnapshotMatches(observed, input.expectedExists, input.before, input.expectedBeforeMode)) {
223
267
  return { ok: false, backupPath, observed };
224
268
  }
225
269
  if (input.expectedExists) {
@@ -231,7 +275,7 @@ async function commitCodexConfigIfUnchanged(input) {
231
275
  return { ok: false, backupPath: null, observed: await readConfigCommitSnapshot(input.configPath) };
232
276
  }
233
277
  const claimed = await readConfigCommitSnapshot(backupPath);
234
- if (!configCommitSnapshotMatches(claimed, true, input.before)) {
278
+ if (!configCommitSnapshotMatches(claimed, true, input.before, input.expectedBeforeMode)) {
235
279
  await restoreClaimedConfigIfAbsent(backupPath, input.configPath, claimed);
236
280
  return { ok: false, backupPath, observed: claimed };
237
281
  }
@@ -283,19 +327,29 @@ async function readConfigCommitSnapshot(configPath) {
283
327
  stat = await fsp.lstat(configPath);
284
328
  }
285
329
  catch (error) {
286
- if (error?.code === 'ENOENT')
287
- return { ok: true, exists: false, text: '', status: 'missing' };
288
- return { ok: false, exists: false, text: '', status: 'read_failed' };
330
+ if (error?.code === 'ENOENT') {
331
+ return { ok: true, exists: false, text: '', status: 'missing', mode: null };
332
+ }
333
+ return { ok: false, exists: false, text: '', status: 'read_failed', mode: null };
334
+ }
335
+ const mode = stat.mode & 0o777;
336
+ if (stat.isSymbolicLink()) {
337
+ return { ok: false, exists: true, text: '', status: 'symlink', mode };
338
+ }
339
+ if (!stat.isFile()) {
340
+ return { ok: false, exists: true, text: '', status: 'non_regular', mode };
289
341
  }
290
- if (stat.isSymbolicLink())
291
- return { ok: false, exists: true, text: '', status: 'symlink' };
292
- if (!stat.isFile())
293
- return { ok: false, exists: true, text: '', status: 'non_regular' };
294
342
  try {
295
- return { ok: true, exists: true, text: await fsp.readFile(configPath, 'utf8'), status: 'regular' };
343
+ return {
344
+ ok: true,
345
+ exists: true,
346
+ text: await fsp.readFile(configPath, 'utf8'),
347
+ status: 'regular',
348
+ mode
349
+ };
296
350
  }
297
351
  catch {
298
- return { ok: false, exists: true, text: '', status: 'read_failed' };
352
+ return { ok: false, exists: true, text: '', status: 'read_failed', mode };
299
353
  }
300
354
  }
301
355
  export function extractTomlTable(text, tableName) {
@@ -0,0 +1,121 @@
1
+ import { runProcess } from '../fsx.js';
2
+ export const CODEX_OAUTH_CALLBACK_PORT = 1455;
3
+ export const OAUTH_CALLBACK_RECOVERY_GUIDANCE = 'If the OAuth callback ends on a dead page, replace localhost with 127.0.0.1 in the address bar and retry immediately; the authorization code is short-lived and one-time.';
4
+ const DEFAULT_TIMEOUT_MS = 1_500;
5
+ const MAX_OUTPUT_BYTES = 64 * 1024;
6
+ const LSOF_ARGS = ['-nP', `-iTCP:${CODEX_OAUTH_CALLBACK_PORT}`, '-sTCP:LISTEN'];
7
+ export async function inspectOAuthCallbackPortConflict(input = {}) {
8
+ const execute = input.run || runProcess;
9
+ let result;
10
+ try {
11
+ result = await execute('lsof', LSOF_ARGS, {
12
+ timeoutMs: Math.max(1, Math.min(input.timeoutMs ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS)),
13
+ maxOutputBytes: MAX_OUTPUT_BYTES
14
+ });
15
+ }
16
+ catch (error) {
17
+ return diagnostic(isUnavailableError(error) ? 'unavailable' : 'probe_failed');
18
+ }
19
+ if (result.timedOut)
20
+ return diagnostic('timeout');
21
+ if (result.code === -1 && isUnavailableError(result.stderr))
22
+ return diagnostic('unavailable');
23
+ const listeners = parseLsofListeners(result.stdout);
24
+ if (result.code !== 0 && listeners.length === 0) {
25
+ if (result.code === 1 && String(result.stdout || '').trim() === '' && String(result.stderr || '').trim() === '') {
26
+ return diagnostic('clear');
27
+ }
28
+ return diagnostic(result.code === -1 ? 'unavailable' : 'probe_failed');
29
+ }
30
+ const loopbackCodex = listeners.filter((listener) => (listener.scope === 'loopback_ipv4'
31
+ && isCodexProcessName(listener.command)));
32
+ const competing = listeners.filter((listener) => (listener.scope === 'wildcard' || listener.scope === 'ipv6'));
33
+ const conflict = loopbackCodex.some((codexListener) => (competing.some((listener) => listener.pid !== codexListener.pid)));
34
+ return {
35
+ ...diagnostic(conflict ? 'conflict' : 'clear'),
36
+ listeners,
37
+ warnings: conflict ? ['oauth_callback_port_1455_conflict'] : []
38
+ };
39
+ }
40
+ export function oauthCallbackRecoveryGuidance(failureOutput, diagnosticResult) {
41
+ return diagnosticResult.conflict && isCodexAuthenticationFailure(failureOutput)
42
+ ? [OAUTH_CALLBACK_RECOVERY_GUIDANCE]
43
+ : [];
44
+ }
45
+ export function oauthCallbackDoctorGuidance(diagnosticResult) {
46
+ return diagnosticResult.conflict ? [OAUTH_CALLBACK_RECOVERY_GUIDANCE] : [];
47
+ }
48
+ export function isCodexAuthenticationFailure(output) {
49
+ return /(?:not\s+(?:logged|signed)\s+in|not\s+authenticated|authentication\s+(?:is\s+)?required|login\s+required|please\s+(?:run\s+)?(?:codex\s+)?log\s*in|unauthenticated|unauthorized|http\s*401|\b401\s+unauthorized\b)/i
50
+ .test(String(output || ''));
51
+ }
52
+ function parseLsofListeners(output) {
53
+ const listeners = new Map();
54
+ for (const line of String(output || '').split(/\r?\n/)) {
55
+ const match = line.match(/^(\S+)\s+(\d+)\s+.*?\bTCP\s+(.+?)\s+\(LISTEN\)\s*$/);
56
+ if (!match)
57
+ continue;
58
+ const command = safeProcessName(match[1]);
59
+ const pid = Number(match[2]);
60
+ const address = safeListenerAddress(match[3]);
61
+ if (!command || !Number.isSafeInteger(pid) || pid <= 0 || !address)
62
+ continue;
63
+ const listener = {
64
+ command,
65
+ pid,
66
+ address,
67
+ scope: listenerScope(address)
68
+ };
69
+ listeners.set(listenerKey(listener), listener);
70
+ }
71
+ return [...listeners.values()];
72
+ }
73
+ function listenerScope(address) {
74
+ if (address === `127.0.0.1:${CODEX_OAUTH_CALLBACK_PORT}`)
75
+ return 'loopback_ipv4';
76
+ if (address === `*:${CODEX_OAUTH_CALLBACK_PORT}`
77
+ || address === `0.0.0.0:${CODEX_OAUTH_CALLBACK_PORT}`
78
+ || address === `:::${CODEX_OAUTH_CALLBACK_PORT}`
79
+ || address === `[::]:${CODEX_OAUTH_CALLBACK_PORT}`) {
80
+ return 'wildcard';
81
+ }
82
+ const colonCount = address.match(/:/g)?.length || 0;
83
+ if ((address.startsWith('[') || colonCount >= 2) && /(?:^|:|\])1455$/.test(address))
84
+ return 'ipv6';
85
+ return 'other';
86
+ }
87
+ function safeProcessName(value) {
88
+ const name = String(value || '').trim();
89
+ return name.length <= 64 && /^[A-Za-z0-9._+-]+$/.test(name) ? name : '';
90
+ }
91
+ function safeListenerAddress(value) {
92
+ const address = String(value || '').trim();
93
+ return address.length <= 96 && /^[0-9A-Fa-f.*:[\]-]+$/.test(address) ? address : '';
94
+ }
95
+ function isCodexProcessName(command) {
96
+ const normalized = command.toLowerCase();
97
+ if (normalized === 'codex')
98
+ return true;
99
+ const appServerName = 'codex-app-server';
100
+ return normalized.length >= 'codex-app'.length
101
+ && normalized.length <= appServerName.length
102
+ && appServerName.startsWith(normalized);
103
+ }
104
+ function listenerKey(listener) {
105
+ return `${listener.command}:${listener.pid}:${listener.address}`;
106
+ }
107
+ function isUnavailableError(error) {
108
+ const text = error instanceof Error ? `${error.name} ${error.message}` : String(error || '');
109
+ return /\bENOENT\b|not found|no such file or directory/i.test(text);
110
+ }
111
+ function diagnostic(status) {
112
+ return {
113
+ schema: 'sks.codex-oauth-callback-port-diagnostic.v1',
114
+ port: CODEX_OAUTH_CALLBACK_PORT,
115
+ status,
116
+ available: !['unavailable', 'timeout', 'probe_failed'].includes(status),
117
+ conflict: status === 'conflict',
118
+ listeners: [],
119
+ warnings: []
120
+ };
121
+ }
@@ -74,7 +74,8 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
74
74
  home: path.dirname(home),
75
75
  configPath: path.join(home, 'config.toml'),
76
76
  codexLbEnvPath: path.join(home, 'sks-codex-lb.env'),
77
- ...(input.codexLbModelCatalog ? { codexLbModelCatalog: input.codexLbModelCatalog } : {})
77
+ ...(input.codexLbModelCatalog ? { codexLbModelCatalog: input.codexLbModelCatalog } : {}),
78
+ ...(input.desktopPickerEvidence ? { desktopPickerEvidence: input.desktopPickerEvidence } : {})
78
79
  });
79
80
  const changed = actions.some((action) => action.changed);
80
81
  const applied = actions.some((action) => action.status === 'repaired');
@@ -84,12 +85,16 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
84
85
  const selectedProviderBlockers = Array.isArray(providerModelUi.selected_provider_blockers)
85
86
  ? providerModelUi.selected_provider_blockers
86
87
  : [];
88
+ const selectedProviderReady = providerModelUi.effective_ready === true;
87
89
  const safeAutoApply = changed && !requiresConfirmation;
88
90
  const manual = changed && !input.apply;
89
91
  const blockers = [
90
92
  ...(requiresConfirmation ? ['codex_app_fast_ui_repair_requires_confirmation'] : []),
91
93
  ...(manual && !safeAutoApply ? ['codex_app_fast_ui_repair_requires_explicit_apply'] : []),
92
94
  ...selectedProviderBlockers.map((blocker) => `selected_provider:${blocker}`),
95
+ ...(!selectedProviderReady && selectedProviderBlockers.length === 0
96
+ ? ['selected_provider:routing_or_picker_unverified']
97
+ : []),
93
98
  ...(after.indicators.secret_leak_suspected ? ['codex_app_ui_repair_secret_leak_suspected'] : [])
94
99
  ];
95
100
  const report = {
@@ -104,7 +109,7 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
104
109
  unsafe_repair_reasons: [...new Set(unsafeReasons)],
105
110
  permissions_hardened: permissionsHardened,
106
111
  fast_selector: pending ? 'manual_action_required' : applied ? 'repaired' : before.indicators.fast_selector === 'maybe_hidden_or_locked' ? 'manual_action_required' : 'ok',
107
- provider_selector: selectedProviderBlockers.length ? 'manual_action_required' : 'selected_provider_ready',
112
+ provider_selector: selectedProviderReady ? 'selected_provider_ready' : 'manual_action_required',
108
113
  provider_model_ui: providerModelUi,
109
114
  provider_actions: providerModelUi.ui_actions || [],
110
115
  provider_blockers: providerModelUi.blockers || [],
@@ -30,6 +30,9 @@ export const NATIVE_SOURCE_FILES = [
30
30
  'SettingsViewController.swift',
31
31
  'OperationCoordinator.swift',
32
32
  'ProcessClient.swift',
33
+ 'ProcessExecutionState.swift',
34
+ 'ProcessIdentityGuard.swift',
35
+ 'SecureProcessEnvelope.swift',
33
36
  'NotificationCoordinator.swift',
34
37
  'AlertFactory.swift',
35
38
  'AppIdentity.swift'
@@ -19,6 +19,14 @@ export function sksMenuBarRestartDeferred(env = process.env) {
19
19
  return env.SKS_UPDATE_DEFER_MENUBAR_RESTART === '1'
20
20
  || env.SKS_SKIP_SKS_MENUBAR_LAUNCH === '1';
21
21
  }
22
+ export function menuBarCredentialEnvironmentBlocker(input) {
23
+ if (!input.cleanupOk)
24
+ return 'launch_secret_env_cleanup_incomplete';
25
+ if (input.compatibilityConfigured && input.compatibilityRestored !== true) {
26
+ return 'desktop_compat_launch_env_restore_incomplete';
27
+ }
28
+ return null;
29
+ }
22
30
  export async function installSksMenuBar(opts = {}) {
23
31
  const apply = opts.apply === true;
24
32
  const env = opts.env || process.env;
@@ -89,9 +97,18 @@ export async function installSksMenuBar(opts = {}) {
89
97
  if (retiredBindings.status === 'quarantined') {
90
98
  actions.push(`quarantined ${retiredBindings.retired_binding_count} retired remote bridge binding(s)`);
91
99
  }
92
- cleanup = await cleanupMacLaunchSecretEnvironment({ env });
93
- if (!cleanup.ok)
94
- warnings.push('launch_secret_env_cleanup_incomplete');
100
+ const injectedTestLaunchctl = (env.SKS_TEST_FORBID_REAL_HOME === '1'
101
+ || env.SKS_TEST_ISOLATION === '1')
102
+ ? env.SKS_MENUBAR_LAUNCHCTL
103
+ : undefined;
104
+ cleanup = await cleanupMacLaunchSecretEnvironment({
105
+ env,
106
+ ...(injectedTestLaunchctl ? { launchctlBin: injectedTestLaunchctl } : {})
107
+ });
108
+ const cleanupBlocker = menuBarCredentialEnvironmentBlocker({ cleanupOk: cleanup.ok });
109
+ if (cleanupBlocker) {
110
+ return blocked(cleanupBlocker, 'Global GUI secret environment cleanup could not be verified.');
111
+ }
95
112
  try {
96
113
  const { syncDesktopCenterLaunchCredentials } = await import('../../codex-lb/desktop-center-credentials.js');
97
114
  const { CODEX_LB_DESKTOP_COMPAT_MARKER } = await import('../../../cli/install-helpers-codex-lb-config.js');
@@ -100,15 +117,21 @@ export async function installSksMenuBar(opts = {}) {
100
117
  if (configText.includes(CODEX_LB_DESKTOP_COMPAT_MARKER)) {
101
118
  const restored = await syncDesktopCenterLaunchCredentials({
102
119
  mode: 'desktop-dual-auth-compat',
103
- home: paths.home
120
+ home: paths.home,
121
+ ...(injectedTestLaunchctl ? { launchctlBin: injectedTestLaunchctl } : {})
104
122
  });
105
- if (!restored.ok)
106
- warnings.push('desktop_compat_launch_env_restore_incomplete');
107
- else
108
- actions.push('restored Center Codex LB credentials into Desktop launch environment');
123
+ const restoreBlocker = menuBarCredentialEnvironmentBlocker({
124
+ cleanupOk: true,
125
+ compatibilityConfigured: true,
126
+ compatibilityRestored: restored.ok
127
+ });
128
+ if (restoreBlocker) {
129
+ return blocked(restored.blockers[0] || restoreBlocker, 'Desktop compatibility cannot be restored without exposing a global GUI secret; use the local bridge.');
130
+ }
109
131
  }
110
132
  }
111
- catch {
133
+ catch (error) {
134
+ return blocked('desktop_compat_launch_env_restore_failed', error instanceof Error ? error.message : String(error));
112
135
  }
113
136
  const swiftc = env.SKS_MENUBAR_SWIFTC || await which('swiftc').catch(() => null) || '/usr/bin/swiftc';
114
137
  const codesign = env.SKS_MENUBAR_CODESIGN || await which('codesign').catch(() => null) || '/usr/bin/codesign';
@@ -79,7 +79,22 @@ export async function launchMenuBar(input) {
79
79
  if (input.open) {
80
80
  const opened = await runProcess(input.open, [input.paths.app_path], { timeoutMs: 8_000, maxOutputBytes: 16 * 1024 })
81
81
  .catch((error) => ({ code: 1, stdout: '', stderr: error instanceof Error ? error.message : String(error) }));
82
- return { requested: true, method: 'open-fallback', ok: opened.code === 0, bootstrap_code: bootstrap.code, bootstrap_timed_out: false, open_code: opened.code, error: opened.code === 0 ? null : String(opened.stderr || bootstrap.stderr).trim() };
82
+ const running = opened.code === 0
83
+ && await waitForMenuBarProcessRunning(input.paths.executable_path, input.env);
84
+ return {
85
+ requested: true,
86
+ method: 'open-fallback',
87
+ ok: running,
88
+ bootstrap_code: bootstrap.code,
89
+ bootstrap_timed_out: false,
90
+ open_code: opened.code,
91
+ open_verified_running: running,
92
+ error: running
93
+ ? null
94
+ : opened.code === 0
95
+ ? 'open_succeeded_process_not_running'
96
+ : String(opened.stderr || bootstrap.stderr).trim()
97
+ };
83
98
  }
84
99
  return { requested: true, method: 'launchctl', ok: false, bootstrap_code: bootstrap.code, bootstrap_timed_out: false, error: String(bootstrap.stderr || bootstrap.stdout || 'launchctl_bootstrap_failed').trim() };
85
100
  }
@@ -166,16 +181,37 @@ export async function removeLaunchAgent(paths, env) {
166
181
  }
167
182
  return { actions, warnings, blockers };
168
183
  }
169
- export async function isMenuBarProcessRunning(executablePath) {
170
- const pgrep = await which('pgrep').catch(() => null) || '/usr/bin/pgrep';
171
- const result = await runProcess(pgrep, ['-f', executablePath], { timeoutMs: 2_000, maxOutputBytes: 8 * 1024 }).catch(() => ({ code: 1 }));
184
+ export async function isMenuBarProcessRunning(executablePath, env = process.env) {
185
+ const injectedPgrep = env.SKS_MENUBAR_TEST_PROCESS_TOOLS === '1'
186
+ ? env.SKS_MENUBAR_PGREP
187
+ : null;
188
+ const pgrep = injectedPgrep
189
+ || (await exists('/usr/bin/pgrep') ? '/usr/bin/pgrep' : null);
190
+ if (!pgrep)
191
+ return false;
192
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
193
+ const args = [
194
+ ...(uid === null ? [] : ['-U', String(uid)]),
195
+ '-f',
196
+ '-x',
197
+ `^${posixExtendedRegexLiteral(path.resolve(executablePath))}$`
198
+ ];
199
+ const result = await runProcess(pgrep, args, { timeoutMs: 2_000, maxOutputBytes: 8 * 1024 }).catch(() => ({ code: 1 }));
172
200
  return result.code === 0;
173
201
  }
174
202
  async function terminateExistingMenuBarProcess(executablePath) {
175
203
  if (!(await exists(executablePath)))
176
204
  return;
177
- const pkill = await which('pkill').catch(() => null) || '/usr/bin/pkill';
178
- await runProcess(pkill, ['-f', executablePath], { timeoutMs: 3_000, maxOutputBytes: 8 * 1024 }).catch(() => undefined);
205
+ if (!await exists('/usr/bin/pkill'))
206
+ return;
207
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
208
+ const args = [
209
+ ...(uid === null ? [] : ['-U', String(uid)]),
210
+ '-f',
211
+ '-x',
212
+ `^${posixExtendedRegexLiteral(path.resolve(executablePath))}$`
213
+ ];
214
+ await runProcess('/usr/bin/pkill', args, { timeoutMs: 3_000, maxOutputBytes: 8 * 1024 }).catch(() => undefined);
179
215
  }
180
216
  export function launchDomain() {
181
217
  const uid = typeof process.getuid === 'function' ? process.getuid() : null;
@@ -208,6 +244,19 @@ async function waitForLaunchdServiceRunning(launchctl, service, env) {
208
244
  await delay(Math.min(pollIntervalMs, remainingAfterProbeMs));
209
245
  }
210
246
  }
247
+ async function waitForMenuBarProcessRunning(executablePath, env) {
248
+ const timeoutMs = timeoutFromEnv(env, 'SKS_MENUBAR_OPEN_READBACK_TIMEOUT_MS', 5_000);
249
+ const intervalMs = timeoutFromEnv(env, 'SKS_MENUBAR_OPEN_READBACK_INTERVAL_MS', 100);
250
+ const deadline = Date.now() + timeoutMs;
251
+ while (true) {
252
+ if (await isMenuBarProcessRunning(executablePath, env))
253
+ return true;
254
+ const remainingMs = deadline - Date.now();
255
+ if (remainingMs <= 0)
256
+ return false;
257
+ await delay(Math.min(intervalMs, remainingMs));
258
+ }
259
+ }
211
260
  async function printLaunchdService(launchctl, service, timeoutMs) {
212
261
  const result = await runProcess(launchctl, ['print', service], { timeoutMs, maxOutputBytes: 32 * 1024 })
213
262
  .catch((error) => failedProcess(error));
@@ -238,6 +287,9 @@ function launchdProbeError(code, stderr, state, pid, activeCount) {
238
287
  function errorToken(value) {
239
288
  return value.trim().toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9._-]/g, '_');
240
289
  }
290
+ function posixExtendedRegexLiteral(value) {
291
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
292
+ }
241
293
  function delay(ms) {
242
294
  return new Promise((resolve) => setTimeout(resolve, ms));
243
295
  }
@@ -1,6 +1,6 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import { nowIso, readText, runProcess, sha256, which, writeTextAtomic } from '../../fsx.js';
3
+ import { exists, nowIso, readText, runProcess, sha256, which, writeTextAtomic } from '../../fsx.js';
4
4
  import { ensureConfinedDirectory, inspectConfinedPath, removeManagedPathVerified } from '../../managed-path-safety.js';
5
5
  import { SECRET_LAUNCH_ENV_KEYS } from './constants.js';
6
6
  const RETIRED_REMOTE_BRIDGE_LABEL = 'com.sneakoscope.telegram-hub';
@@ -76,7 +76,7 @@ export async function cleanupRetiredRemoteBridgeLaunchAgent(opts = {}) {
76
76
  maxOutputBytes: 16 * 1024
77
77
  }).catch((error) => processFailure(error))]
78
78
  : [];
79
- if (inspected.exists) {
79
+ if (serviceLoaded && inspected.exists) {
80
80
  attempts.push(await run(launchctl, ['bootout', domain, launchAgentPath], {
81
81
  timeoutMs: 5_000,
82
82
  maxOutputBytes: 16 * 1024
@@ -231,7 +231,8 @@ export async function cleanupMacLaunchSecretEnvironment(opts = {}) {
231
231
  return { ok: true, status: 'not_macos', variables: [...SECRET_LAUNCH_ENV_KEYS], cleaned: [], failed: [], next_actions: [] };
232
232
  }
233
233
  const env = opts.env || process.env;
234
- const launchctl = env.SKS_MENUBAR_LAUNCHCTL || await which('launchctl').catch(() => null) || '/bin/launchctl';
234
+ const launchctl = opts.launchctlBin
235
+ || (await exists('/bin/launchctl') ? '/bin/launchctl' : null);
235
236
  if (!launchctl) {
236
237
  return {
237
238
  ok: false, status: 'launchctl_missing', variables: [...SECRET_LAUNCH_ENV_KEYS], cleaned: [],
@@ -241,8 +242,17 @@ export async function cleanupMacLaunchSecretEnvironment(opts = {}) {
241
242
  }
242
243
  const cleaned = [];
243
244
  const failed = [];
245
+ const childEnv = { ...env };
246
+ for (const key of SECRET_LAUNCH_ENV_KEYS)
247
+ delete childEnv[key];
248
+ const run = opts.runProcessImpl || runProcess;
244
249
  for (const key of SECRET_LAUNCH_ENV_KEYS) {
245
- const result = await runProcess(launchctl, ['unsetenv', key], { timeoutMs: 3_000, maxOutputBytes: 8 * 1024 })
250
+ const result = await run(launchctl, ['unsetenv', key], {
251
+ timeoutMs: 3_000,
252
+ maxOutputBytes: 8 * 1024,
253
+ env: childEnv,
254
+ envMode: 'replace'
255
+ })
246
256
  .catch((error) => ({ code: 1, stdout: '', stderr: error instanceof Error ? error.message : String(error) }));
247
257
  if (result.code === 0)
248
258
  cleaned.push(key);
@@ -267,6 +277,7 @@ function processFailure(error) {
267
277
  function launchdServiceAbsent(result) {
268
278
  if (result.timedOut)
269
279
  return false;
270
- return /could not find service|no such (?:process|file)|not found|not loaded|does not exist/i
271
- .test(`${result.stdout || ''}\n${result.stderr || ''}`);
280
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
281
+ return /could not find service|no such (?:process|file)|not found|not loaded|does not exist/i.test(output)
282
+ || (result.code === 113 && /\bbad request\b/i.test(output));
272
283
  }
@@ -13,7 +13,7 @@ import { cleanupRetiredRemoteBridgeLaunchAgent, quarantineRetiredRemoteBridgeBin
13
13
  export async function inspectSksMenuBarStatus(opts = {}) {
14
14
  const paths = sksMenuBarPaths(opts.home || opts.env?.HOME, opts.root);
15
15
  const installed = await exists(paths.executable_path);
16
- const running = installed ? await isMenuBarProcessRunning(paths.executable_path) : false;
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');
@@ -38,10 +38,13 @@ export async function inspectSksMenuBarStatus(opts = {}) {
38
38
  const resources = legacyVerification?.resources || await inspectInstalledResources({ resourcesDir: paths.resources_path, buildStamp });
39
39
  const blockers = [];
40
40
  const warnings = [];
41
+ const runtime = evaluateMenuBarRuntimeReadiness({ installed, running, launchd });
41
42
  if (!installed)
42
43
  blockers.push('menubar_app_missing');
43
- if (installed && launchd.checked && !launchd.ok)
44
- blockers.push('launchd_not_running');
44
+ if (runtime.blocker)
45
+ blockers.push(runtime.blocker);
46
+ if (runtime.warning)
47
+ warnings.push(runtime.warning);
45
48
  if (installed && !actionSmoke.executable)
46
49
  blockers.push('action_script_not_executable');
47
50
  if (installed && !actionSmoke.ok)
@@ -91,6 +94,15 @@ export async function inspectSksMenuBarStatus(opts = {}) {
91
94
  next_actions: blockers.length ? defaultNextActions() : ['sks menubar status --json']
92
95
  };
93
96
  }
97
+ export function evaluateMenuBarRuntimeReadiness(input) {
98
+ if (!input.installed || !input.launchd.checked || input.launchd.ok) {
99
+ return { blocker: null, warning: null };
100
+ }
101
+ return {
102
+ blocker: 'launchd_not_running',
103
+ warning: input.running ? 'launchd_not_running_process_active' : null
104
+ };
105
+ }
94
106
  export function evaluateActionScriptIntegrity(actionText, buildStamp) {
95
107
  const scriptSha256 = actionText ? sha256(actionText) : null;
96
108
  const expectedScriptSha256 = buildStamp?.action_script_sha256 || null;