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.
- package/README.md +13 -2
- package/config/skills-hash-ledger.v1.json +63 -0
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/bin/fast-inline.js +14 -3
- package/dist/bin/sks-dispatch.js +1 -1
- package/dist/cli/codex-lb-setup-warning-output.js +17 -0
- package/dist/cli/command-manifest-lite.js +2 -13
- package/dist/cli/install-helpers-codex-lb-config.js +475 -8
- package/dist/cli/install-helpers-install-support.js +1 -1
- package/dist/cli/install-helpers.js +764 -68
- package/dist/cli/router.js +5 -2
- package/dist/commands/codex-lb.js +77 -47
- package/dist/commands/doctor-profile.js +1 -0
- package/dist/commands/doctor.js +165 -58
- package/dist/config/skills-manifest.json +193 -97
- package/dist/core/codex/codex-config-guard.js +70 -16
- package/dist/core/codex/oauth-callback-port-diagnostic.js +121 -0
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -2
- package/dist/core/codex-app/menubar/constants.js +3 -0
- package/dist/core/codex-app/menubar/installer.js +32 -9
- package/dist/core/codex-app/menubar/launch-agent.js +58 -6
- package/dist/core/codex-app/menubar/migration.js +17 -6
- package/dist/core/codex-app/menubar/status.js +15 -3
- package/dist/core/codex-app.js +15 -20
- package/dist/core/codex-control/codex-lb-launch-recovery.js +26 -0
- package/dist/core/codex-lb/capability-runner.js +1 -0
- package/dist/core/codex-lb/codex-lb-env.js +52 -41
- package/dist/core/codex-lb/codex-lb-setup.js +85 -8
- package/dist/core/codex-lb/desktop-center-credentials.js +474 -39
- package/dist/core/codex-lb/desktop-controller.js +118 -4
- package/dist/core/codex-lb/legacy-migration.js +6 -3
- package/dist/core/codex-native/codex-native-feature-broker.js +5 -1
- package/dist/core/codex-native/core-skill-manifest.js +1 -1
- package/dist/core/codex-native/managed-skill-generation-lock.js +13 -0
- package/dist/core/codex-native/sks-skill-paths.js +97 -10
- package/dist/core/codex-runtime/codex-desktop-config-policy.js +1 -0
- package/dist/core/commands/basic-cli.js +54 -4
- package/dist/core/commands/mad-sks-command.js +1 -1
- package/dist/core/commands/menubar-command.js +24 -8
- package/dist/core/commands/naruto-command.js +42 -217
- package/dist/core/doctor/codex-config-syntax-repair.js +368 -0
- package/dist/core/doctor/doctor-native-capability-repair.js +12 -5
- package/dist/core/doctor/doctor-readiness-matrix.js +51 -4
- package/dist/core/doctor/skill-legacy-surface.js +0 -1
- package/dist/core/doctor/sks-menubar-doctor.js +21 -0
- package/dist/core/fsx.js +7 -14
- package/dist/core/hooks-runtime/hook-context.js +30 -16
- package/dist/core/hooks-runtime/managed-skill-admission.js +17 -0
- package/dist/core/hooks-runtime/managed-skill-generation-heal.js +891 -30
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +99 -6
- package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +3 -1
- package/dist/core/hooks-runtime/subagent-skill-availability-evidence.js +1 -1
- package/dist/core/hooks-runtime/subagent-skill-availability-guards.js +4 -4
- package/dist/core/hooks-runtime/subagent-skill-availability.js +9 -7
- package/dist/core/hooks-runtime/subagent-stop-hook.js +19 -0
- package/dist/core/hooks-runtime.js +41 -14
- package/dist/core/init/skills.js +270 -11
- package/dist/core/mission.js +20 -0
- package/dist/core/naruto/hardware-capacity-probe.js +3 -1
- package/dist/core/naruto/naruto-concurrency-governor.js +9 -3
- package/dist/core/pipeline-internals/runtime-core.js +68 -9
- package/dist/core/pipeline-internals/runtime-gates.js +10 -2
- package/dist/core/preflight/parallel-preflight-engine.js +18 -4
- package/dist/core/release/package-dist-snapshot.js +50 -8
- package/dist/core/routes.js +1 -1
- package/dist/core/safety/command-contract/contracts.js +10 -13
- package/dist/core/safety/command-contract/json-schema.js +38 -1
- package/dist/core/subagents/agent-catalog.js +37 -13
- package/dist/core/subagents/model-policy.js +14 -9
- package/dist/core/subagents/naruto-command-args.js +237 -0
- package/dist/core/subagents/naruto-command-input-contract.js +123 -0
- package/dist/core/subagents/naruto-help-contract.js +35 -2
- package/dist/core/subagents/naruto-launch-diagnostics.js +35 -0
- package/dist/core/subagents/official-subagent-config.js +30 -3
- package/dist/core/subagents/official-subagent-preparation.js +30 -5
- package/dist/core/subagents/official-subagent-prompt.js +4 -3
- package/dist/core/subagents/official-subagent-runner.js +12 -0
- package/dist/core/subagents/subagent-evidence.js +10 -0
- package/dist/core/subagents/thread-budget.js +1 -1
- package/dist/core/subagents/wave-lifecycle.js +41 -11
- package/dist/core/subagents/wave-parent-guidance.js +10 -3
- package/dist/core/update/update-migration-state.js +694 -63
- package/dist/core/update/update-operation.js +498 -32
- package/dist/core/update-check.js +990 -509
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/AppDelegate.swift +1 -0
- package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -1
- package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +1 -1
- package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +127 -12
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +7 -2
- package/dist/native/sks-menubar/Sources/ProcessClient.swift +287 -84
- package/dist/native/sks-menubar/Sources/ProcessExecutionState.swift +45 -0
- package/dist/native/sks-menubar/Sources/ProcessIdentityGuard.swift +196 -0
- package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +169 -0
- package/dist/native/sks-menubar/Sources/StatusItemController.swift +23 -5
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +113 -24
- package/dist/native/sks-menubar/Sources/main.swift +12 -0
- package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +14 -0
- package/dist/native/sks-menubar/Tests/OperationCoordinatorTests.swift +174 -0
- package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +4 -0
- package/dist/scripts/current-surface-update-e2e-check.js +24 -5
- package/dist/scripts/lib/ensure-dist-fresh.js +12 -8
- package/dist/scripts/mutation-callsite-analysis.js +381 -0
- package/dist/scripts/mutation-callsite-coverage-check.js +127 -62
- package/dist/scripts/official-subagent-workflow-check.js +1 -1
- package/dist/scripts/skills-manifest-continuity-check.js +19 -1
- package/package.json +5 -3
- package/runtime-required-scripts.json +5 -0
- 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
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
|
171
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
271
|
-
|
|
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 (
|
|
44
|
-
blockers.push(
|
|
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;
|