mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 00:02: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>
123 lines
5.2 KiB
TypeScript
123 lines
5.2 KiB
TypeScript
/**
|
|
* Keeps the remote relay's Unix socket path inside `sockaddr_un.sun_path`.
|
|
*
|
|
* The default endpoint is `$HOME/.orca-remote/relay-<fullVersion>/relay-<id>.sock`,
|
|
* whose fixed suffix already costs ~66 bytes. A managed-hosting `$HOME` such as
|
|
* `/var/www/<uuid>` pushes the whole path past the kernel cap and libuv reports only
|
|
* `listen EINVAL`, so the relay never starts (#10726). When that happens the socket
|
|
* moves to a fixed-length base whose length no longer depends on `$HOME`.
|
|
*
|
|
* Windows relays bind named pipes (`\\.\pipe\...`), which have no `sun_path` limit.
|
|
*/
|
|
import { createHash } from 'node:crypto'
|
|
import { isWindowsRemoteHost, type RemoteHostPlatform } from './ssh-remote-platform'
|
|
import { unixSocketPathByteLimit } from '../../shared/unix-socket-path-limit'
|
|
|
|
export function remoteUnixSocketPathByteLimit(host: RemoteHostPlatform): number | null {
|
|
if (isWindowsRemoteHost(host)) {
|
|
return null
|
|
}
|
|
return unixSocketPathByteLimit(host.os === 'darwin' ? 'darwin' : 'linux')
|
|
}
|
|
|
|
export function remoteSocketPathFitsLimit(host: RemoteHostPlatform, sockPath: string): boolean {
|
|
const limit = remoteUnixSocketPathByteLimit(host)
|
|
return limit === null || Buffer.byteLength(sockPath, 'utf8') <= limit
|
|
}
|
|
|
|
/** Fixed-length, per-uid base. `/tmp` is the only POSIX directory whose length is not user-dependent. */
|
|
export const SHORT_RELAY_SOCKET_DIR_PREFIX = '/tmp/.orca-relay-'
|
|
|
|
export function shortRelaySocketDirForUid(uid: string): string {
|
|
return `${SHORT_RELAY_SOCKET_DIR_PREFIX}${uid}`
|
|
}
|
|
|
|
/**
|
|
* The version segment the relocated socket lives under, named to match the version
|
|
* directories in `$HOME/.orca-remote` so one sweep pattern covers both bases.
|
|
*
|
|
* Why it has to exist: `relaySocketNameForInstanceId` hashes the *target*, not the
|
|
* build, so the filename alone is version-independent. Under `$HOME` the enclosing
|
|
* `relay-<fullVersion>` directory supplies that dimension; without it here, the next
|
|
* Orca build would bind the exact path the previous build's relay still holds. The
|
|
* daemon handshake compares build hashes exactly, so that meeting is a version
|
|
* mismatch — and if the incumbent holds live work, `resolveRelayEndpointBeforeRelaunch`
|
|
* raises `RelayEndpointHeldError` and the user cannot connect at all until the old
|
|
* relay is stopped. The version is hashed rather than spelled out because the whole
|
|
* point of this base is a bounded length.
|
|
*/
|
|
export function shortRelayVersionSegment(relayVersionDirName: string): string {
|
|
return `relay-${createHash('sha256').update(relayVersionDirName).digest('hex').slice(0, 12)}`
|
|
}
|
|
|
|
/**
|
|
* The whole hashed socket name is kept — shortening happens by replacing the
|
|
* variable-length directory, never by truncating the hash, so two targets on one
|
|
* host can never land on the same socket.
|
|
*/
|
|
export function shortRelaySocketPath(shortVersionDir: string, sockName: string): string {
|
|
return `${shortVersionDir}/${sockName}`
|
|
}
|
|
|
|
const SHORT_DIR_MARKER = 'ORCA-RELAY-SHORT-SOCKET-DIR'
|
|
|
|
/**
|
|
* Create (or adopt) the per-uid short socket directory and its version segment, and
|
|
* print the segment's path.
|
|
*
|
|
* Validate before mutating, never the other way round: an unconditional `chmod` follows a
|
|
* symlink, so a path planted by another user would have its *target's* mode rewritten before
|
|
* the owner check could reject it. A fresh `mkdir` under `umask 077` already yields 0700 and
|
|
* proves we own it, so the only path that adopts an existing entry is the one that first
|
|
* proves — via `ls -ldn`, which reports the entry itself rather than what it points at — that
|
|
* it is a real directory, owned by this uid, already 0700. Nothing else is touched.
|
|
*/
|
|
export function resolveShortRelaySocketDirCommand(versionSegment: string): string {
|
|
return [
|
|
'uid=$(id -u) || exit 1',
|
|
`dir="${SHORT_RELAY_SOCKET_DIR_PREFIX}$uid"`,
|
|
'umask 077',
|
|
...adoptOwnedDirectoryCommand('$dir'),
|
|
// The version segment is validated the same way rather than trusted: `$dir` being
|
|
// 0700 and ours does not prove what an earlier run left inside it still is.
|
|
`ver="$dir/${versionSegment}"`,
|
|
...adoptOwnedDirectoryCommand('$ver'),
|
|
`printf '%s %s\n' '${SHORT_DIR_MARKER}' "$ver"`
|
|
].join('\n')
|
|
}
|
|
|
|
function adoptOwnedDirectoryCommand(target: string): string[] {
|
|
return [
|
|
`if mkdir "${target}" 2>/dev/null; then`,
|
|
' :',
|
|
'else',
|
|
// Why the sub(): ls decorates the mode with a trailing marker for extended attributes (@),
|
|
// ACLs (+) or an SELinux context (.), so an exact match would refuse a directory we own.
|
|
` entry=$(ls -ldn "${target}" 2>/dev/null | awk 'NR==1{sub(/[.@+]$/, "", $1); print $1" "$3}')`,
|
|
' case "$entry" in',
|
|
' "drwx------ $uid") ;;',
|
|
' *) exit 1 ;;',
|
|
' esac',
|
|
'fi'
|
|
]
|
|
}
|
|
|
|
/** Tolerates login-shell banner noise ahead of the marker line. */
|
|
export function parseShortRelaySocketDir(output: string, versionSegment: string): string | null {
|
|
for (const line of output.split('\n')) {
|
|
const trimmed = line.trim()
|
|
if (!trimmed.startsWith(`${SHORT_DIR_MARKER} `)) {
|
|
continue
|
|
}
|
|
const dir = trimmed.slice(SHORT_DIR_MARKER.length + 1).trim()
|
|
if (
|
|
dir.startsWith(`${SHORT_RELAY_SOCKET_DIR_PREFIX}`) &&
|
|
dir.endsWith(`/${versionSegment}`) &&
|
|
!/[\r\n]/.test(dir)
|
|
) {
|
|
return dir
|
|
}
|
|
}
|
|
return null
|
|
}
|