mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
* fix(codex): turn off Codex daemon auto-start in homes whose socket path exceeds sun_path
Codex >= 0.157 auto-starts a background app-server daemon and connects to
<CODEX_HOME>/app-server-control/app-server-control.sock. Orca's managed homes
under userData make that path longer than sun_path (104 bytes on macOS, 108 on
Linux/Windows), so every interactive codex in an Orca terminal failed with
'path must be shorter than SUN_LEN'. The config mirror now writes a marked
[features] daemon_auto_start = false into only those homes, removes it when the
home fits, and never promotes it into ~/.codex.
* fix(codex): address review of the daemon socket guard
- A runtime config.toml holding only Orca's daemon override no longer reads as a
config-sync stall, so users without ~/.codex/config.toml get no false
"missing" warning in the accounts pane.
- The legacy shared-home refresh re-applies the guard, so retained pre-rollout
panes keep daemon auto-start off after a system-default launch.
- Warn once when an inline `features = {...}` or `[[features]]` blocks the
override instead of failing silently.
- Rename the upsert's TUI-specific internals now that it serves any table.
* fix(codex): apply the daemon socket guard even when the settings mirror stalls
When the settings write-back or mirror refused (unreadable baseline, failed
write to ~/.codex, unreadable source), the whole pass returned before the
daemon guard was applied. A home whose config.toml predates the guard then
kept failing with SUN_LEN on every launch for as long as the stall lasted.
The guard now lands on those paths too; the mirror itself is unchanged.
* fix(codex): guard managed account homes when ~/.codex/config.toml is missing
* test(codex): keep reset-credit ownership checks scoped to the retry, not service construction
* test(codex): build the account mirror test without a type cast
* fix(codex): keep blocking WSL ownership checks off the no-config guard pass
Guarding account homes with no ~/.codex/config.toml ran the WSL ownership
check, a synchronous wsl.exe call per account, at startup before the window
opens and on every account switch. WSL homes are guarded by WSL launch prep,
so that pass now covers host homes only.
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
141 lines
5.5 KiB
TypeScript
141 lines
5.5 KiB
TypeScript
import { realpathSync } from 'node:fs'
|
|
import { parseWslUncPath } from '../../shared/wsl-paths'
|
|
import { unixSocketPathByteLimit } from '../../shared/unix-socket-path-limit'
|
|
import { upsertTableSettingsInContent } from './codex-config-settings-upsert'
|
|
import {
|
|
createTomlLineScanState,
|
|
getTomlTableHeader,
|
|
isTomlStructuralLine,
|
|
joinPreservingTrailingNewline,
|
|
updateTomlLineScanState
|
|
} from './config-toml-line-scan'
|
|
import { parseTomlTableHeaderPath } from './config-toml-key-path'
|
|
|
|
/**
|
|
* Codex >= 0.157 auto-starts a background app-server daemon and its client
|
|
* connects to `<CODEX_HOME>/app-server-control/app-server-control.sock`. Orca's
|
|
* managed homes live under userData, which makes that path longer than
|
|
* `sun_path`, so every interactive `codex` fails with "path must be shorter
|
|
* than SUN_LEN". Only for such homes, Orca turns daemon auto-start off.
|
|
*/
|
|
const DAEMON_SOCKET_SEGMENTS = ['app-server-control', 'app-server-control.sock']
|
|
export const CODEX_DAEMON_OVERRIDE_MARKER = '# orca: CODEX_HOME too long for the daemon socket'
|
|
const DAEMON_OVERRIDE_RAW = `false ${CODEX_DAEMON_OVERRIDE_MARKER}`
|
|
|
|
export function codexDaemonSocketPath(homePath: string, platform = process.platform): string {
|
|
const wsl = parseWslUncPath(homePath)
|
|
if (wsl) {
|
|
return [wsl.linuxPath.replace(/\/+$/, ''), ...DAEMON_SOCKET_SEGMENTS].join('/')
|
|
}
|
|
// Why: Codex canonicalizes CODEX_HOME before building the socket path, so a
|
|
// short symlinked alias still resolves to the long real path. Its
|
|
// AbsolutePathBuf strips the Windows \\?\ prefix, so none is counted here.
|
|
let canonical = homePath
|
|
try {
|
|
canonical = realpathSync.native(homePath)
|
|
} catch {
|
|
// Unresolvable homes are measured as spelled.
|
|
}
|
|
const separator = platform === 'win32' ? '\\' : '/'
|
|
return [canonical.replace(/[\\/]+$/, ''), ...DAEMON_SOCKET_SEGMENTS].join(separator)
|
|
}
|
|
|
|
export function codexDaemonSocketPathExceedsLimit(
|
|
homePath: string,
|
|
platform = process.platform
|
|
): boolean {
|
|
// Why: WSL homes run Linux Codex; Windows Codex's uds_windows also uses a 108-byte sun_path.
|
|
const os = platform === 'darwin' && !parseWslUncPath(homePath) ? 'darwin' : 'linux'
|
|
const socketPath = codexDaemonSocketPath(homePath, platform)
|
|
return Buffer.byteLength(socketPath, 'utf8') > unixSocketPathByteLimit(os)
|
|
}
|
|
|
|
const unguardableHomesWarned = new Set<string>()
|
|
|
|
/** Applies (or removes) Orca's daemon override so it tracks the home's current path. */
|
|
export function applyCodexDaemonSocketGuard(
|
|
config: string,
|
|
homePath: string,
|
|
platform = process.platform
|
|
): string {
|
|
if (!codexDaemonSocketPathExceedsLimit(homePath, platform)) {
|
|
return stripCodexDaemonOverride(config)
|
|
}
|
|
// Why: upsert rewrites an existing daemon_auto_start line in place, so re-applying is a no-op.
|
|
const guarded = upsertTableSettingsInContent(
|
|
config,
|
|
'features',
|
|
new Map([['daemon_auto_start', DAEMON_OVERRIDE_RAW]])
|
|
)
|
|
if (
|
|
!guarded.includes(CODEX_DAEMON_OVERRIDE_MARKER) &&
|
|
!/\bdaemon_auto_start\s*=\s*false\b/.test(guarded) &&
|
|
!unguardableHomesWarned.has(homePath)
|
|
) {
|
|
// Why: an inline `features = {...}` or `[[features]]` blocks the upsert; say so once instead of failing silently.
|
|
unguardableHomesWarned.add(homePath)
|
|
console.warn(
|
|
`[codex-config] Could not turn off Codex daemon auto-start in ${homePath}: its config defines features in a form Orca cannot extend. Codex may fail with "path must be shorter than SUN_LEN"; add daemon_auto_start = false to features in ~/.codex/config.toml.`
|
|
)
|
|
}
|
|
return guarded
|
|
}
|
|
|
|
/** True when a config holds nothing but Orca's daemon override, i.e. no user settings. */
|
|
export function isOnlyCodexDaemonOverride(config: string): boolean {
|
|
return (
|
|
config.includes(CODEX_DAEMON_OVERRIDE_MARKER) && stripCodexDaemonOverride(config).trim() === ''
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Removes only lines Orca wrote, plus a `[features]` table left empty by that
|
|
* removal, so the override never leaks into the user's real ~/.codex.
|
|
*/
|
|
export function stripCodexDaemonOverride(config: string): string {
|
|
if (!config.includes(CODEX_DAEMON_OVERRIDE_MARKER)) {
|
|
return config
|
|
}
|
|
const usesCrlf = config.includes('\r\n')
|
|
const lines = config.split('\n')
|
|
const kept: string[] = []
|
|
let featuresHeaderIndex = -1
|
|
let removedFromFeatures = false
|
|
const dropEmptyFeaturesTable = (): void => {
|
|
const body = kept.slice(featuresHeaderIndex + 1)
|
|
if (featuresHeaderIndex !== -1 && removedFromFeatures && body.every((l) => l.trim() === '')) {
|
|
kept.length = featuresHeaderIndex
|
|
while (kept.at(-1)?.trim() === '') {
|
|
kept.pop()
|
|
}
|
|
if (kept.length > 0) {
|
|
// Why: keep one blank line before the next table.
|
|
kept.push(usesCrlf ? '\r' : '')
|
|
}
|
|
}
|
|
}
|
|
let scan = createTomlLineScanState()
|
|
for (const line of lines) {
|
|
const structural = isTomlStructuralLine(scan)
|
|
scan = updateTomlLineScanState(scan, line)
|
|
if (structural && line.trimEnd().endsWith(CODEX_DAEMON_OVERRIDE_MARKER)) {
|
|
removedFromFeatures ||= featuresHeaderIndex !== -1
|
|
continue
|
|
}
|
|
const header = structural ? getTomlTableHeader(line) : null
|
|
if (header) {
|
|
dropEmptyFeaturesTable()
|
|
const table = parseTomlTableHeaderPath(header)
|
|
const isFeatures = table?.isArray === false && table.segments.join('.') === 'features'
|
|
featuresHeaderIndex = isFeatures ? kept.length : -1
|
|
removedFromFeatures = false
|
|
}
|
|
kept.push(line)
|
|
}
|
|
dropEmptyFeaturesTable()
|
|
while (kept.at(-1)?.trim() === '') {
|
|
kept.pop()
|
|
}
|
|
return kept.length === 0 ? '' : joinPreservingTrailingNewline(kept, usesCrlf)
|
|
}
|