diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt index 54837bf4d65..4cd9bf6b250 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt @@ -2,7 +2,34 @@ _orca_shell_features=",${ORCA_SHELL_FEATURES:-}," builtin unset ORCA_SHELL_FEATURES __orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; } -__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$" +if __orca_has_feature identity; then + if [ -n "${WSL_DISTRO_NAME:-}" ]; then + __orca_emit_shell_identity() { + local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro + local IFS=' ' + _orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0 + _orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0 + _orca_tail=${_orca_stat##*) } + # zsh does not perform implicit word splitting for unquoted parameters; + # read the proc-stat fields explicitly so field 22 (starttime) is parsed + # correctly in both zsh and bash. + builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest </dev/null) || return 0 + _orca_distro=${WSL_DISTRO_NAME:-} + case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac + case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac + case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac + case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac + printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty" + } + __orca_emit_shell_identity + unset -f __orca_emit_shell_identity + else + printf "\033]777;orca-shell-start:%s\007" "$$" + fi +fi # Why a plain variable: the channel is consumed and destroyed in these first # lines, so nothing this shell later spawns can see or inherit the selection. __orca_ready_marker="" diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt index 3d3403ad099..15d44296694 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt @@ -30,7 +30,34 @@ builtin unset ORCA_SHELL_FEATURES builtin typeset -g _orca_histfile="${ORCA_HISTFILE:-}" builtin unset ORCA_HISTFILE __orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) } -__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$" +if __orca_has_feature identity; then + if [ -n "${WSL_DISTRO_NAME:-}" ]; then + __orca_emit_shell_identity() { + local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro + local IFS=' ' + _orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0 + _orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0 + _orca_tail=${_orca_stat##*) } + # zsh does not perform implicit word splitting for unquoted parameters; + # read the proc-stat fields explicitly so field 22 (starttime) is parsed + # correctly in both zsh and bash. + builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest </dev/null) || return 0 + _orca_distro=${WSL_DISTRO_NAME:-} + case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac + case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac + case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac + case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac + printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty" + } + __orca_emit_shell_identity + unset -f __orca_emit_shell_identity + else + printf "\033]777;orca-shell-start:%s\007" "$$" + fi +fi __orca_osc133_precmd() { local exit_code=$? if [[ -n "${__orca_in_command:-}" ]]; then diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt index dc14486cdb7..d5e98f471c0 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt @@ -2,7 +2,34 @@ _orca_shell_features=",${ORCA_SHELL_FEATURES:-}," builtin unset ORCA_SHELL_FEATURES __orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; } -__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$" +if __orca_has_feature identity; then + if [ -n "${WSL_DISTRO_NAME:-}" ]; then + __orca_emit_shell_identity() { + local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro + local IFS=' ' + _orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0 + _orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0 + _orca_tail=${_orca_stat##*) } + # zsh does not perform implicit word splitting for unquoted parameters; + # read the proc-stat fields explicitly so field 22 (starttime) is parsed + # correctly in both zsh and bash. + builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest </dev/null) || return 0 + _orca_distro=${WSL_DISTRO_NAME:-} + case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac + case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac + case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac + case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac + printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty" + } + __orca_emit_shell_identity + unset -f __orca_emit_shell_identity + else + printf "\033]777;orca-shell-start:%s\007" "$$" + fi +fi # Why a plain variable: the channel is consumed and destroyed in these first # lines, so nothing this shell later spawns can see or inherit the selection. __orca_ready_marker="" diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt index 10e9e144fc0..be8741c7a43 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt @@ -30,7 +30,34 @@ builtin unset ORCA_SHELL_FEATURES builtin typeset -g _orca_histfile="${ORCA_HISTFILE:-}" builtin unset ORCA_HISTFILE __orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) } -__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$" +if __orca_has_feature identity; then + if [ -n "${WSL_DISTRO_NAME:-}" ]; then + __orca_emit_shell_identity() { + local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro + local IFS=' ' + _orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0 + _orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0 + _orca_tail=${_orca_stat##*) } + # zsh does not perform implicit word splitting for unquoted parameters; + # read the proc-stat fields explicitly so field 22 (starttime) is parsed + # correctly in both zsh and bash. + builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest </dev/null) || return 0 + _orca_distro=${WSL_DISTRO_NAME:-} + case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac + case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac + case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac + case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac + printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty" + } + __orca_emit_shell_identity + unset -f __orca_emit_shell_identity + else + printf "\033]777;orca-shell-start:%s\007" "$$" + fi +fi __orca_osc133_precmd() { local exit_code=$? if [[ -n "${__orca_in_command:-}" ]]; then diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt index de9c8f95248..d649b49d144 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt @@ -2,7 +2,34 @@ _orca_shell_features=",${ORCA_SHELL_FEATURES:-}," builtin unset ORCA_SHELL_FEATURES __orca_has_feature() { [[ "$_orca_shell_features" == *",$1,"* ]]; } -__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$" +if __orca_has_feature identity; then + if [ -n "${WSL_DISTRO_NAME:-}" ]; then + __orca_emit_shell_identity() { + local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro + local IFS=' ' + _orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0 + _orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0 + _orca_tail=${_orca_stat##*) } + # zsh does not perform implicit word splitting for unquoted parameters; + # read the proc-stat fields explicitly so field 22 (starttime) is parsed + # correctly in both zsh and bash. + builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest </dev/null) || return 0 + _orca_distro=${WSL_DISTRO_NAME:-} + case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac + case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac + case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac + case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac + printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty" + } + __orca_emit_shell_identity + unset -f __orca_emit_shell_identity + else + printf "\033]777;orca-shell-start:%s\007" "$$" + fi +fi # Why a plain variable: the channel is consumed and destroyed in these first # lines, so nothing this shell later spawns can see or inherit the selection. __orca_ready_marker="" diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt index 394bc4a6d10..2681331549f 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt @@ -30,7 +30,34 @@ builtin unset ORCA_SHELL_FEATURES builtin typeset -g _orca_histfile="${ORCA_HISTFILE:-}" builtin unset ORCA_HISTFILE __orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) } -__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$" +if __orca_has_feature identity; then + if [ -n "${WSL_DISTRO_NAME:-}" ]; then + __orca_emit_shell_identity() { + local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro + local IFS=' ' + _orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0 + _orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0 + _orca_tail=${_orca_stat##*) } + # zsh does not perform implicit word splitting for unquoted parameters; + # read the proc-stat fields explicitly so field 22 (starttime) is parsed + # correctly in both zsh and bash. + builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest </dev/null) || return 0 + _orca_distro=${WSL_DISTRO_NAME:-} + case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac + case "$_orca_distro" in ""|*[!A-Za-z0-9._-]*) return 0 ;; esac + case "$_orca_start" in ""|*[!0-9]*) return 0 ;; esac + case "$_orca_tty" in /dev/pts/[0-9]*) ;; *) return 0 ;; esac + printf "\033]777;orca-shell-start:v2:%s:%s:%s:%s:%s\007" "$_orca_distro" "$_orca_boot" "$$" "$_orca_start" "$_orca_tty" + } + __orca_emit_shell_identity + unset -f __orca_emit_shell_identity + else + printf "\033]777;orca-shell-start:%s\007" "$$" + fi +fi __orca_deferred_init() { # Why first: this body runs after the user's own config, so it would otherwise # inherit whatever options that config left set. Under NO_UNSET an unset diff --git a/src/main/daemon/daemon-pty-session-inventory.ts b/src/main/daemon/daemon-pty-session-inventory.ts index d6203b69524..8735a2efe67 100644 --- a/src/main/daemon/daemon-pty-session-inventory.ts +++ b/src/main/daemon/daemon-pty-session-inventory.ts @@ -57,6 +57,7 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti const distros = new Set( result.sessions .filter((session) => session.isAlive && session.wslDistro) + .filter((session) => session.wslShellAnchor) .map((session) => session.wslDistro as string) ) await Promise.all( diff --git a/src/main/daemon/session-wsl-shell-anchor-tracker.ts b/src/main/daemon/session-wsl-shell-anchor-tracker.ts index fe24a8413e5..38a077176b8 100644 --- a/src/main/daemon/session-wsl-shell-anchor-tracker.ts +++ b/src/main/daemon/session-wsl-shell-anchor-tracker.ts @@ -1,5 +1,6 @@ import { createShellStartupIdentityScanState, + drainShellStartupIdentityHeldBytes, scanForShellStartupIdentity, type ShellStartupIdentityScanState } from '../shell-startup-identity-scanner' @@ -17,6 +18,22 @@ export class SessionWslShellAnchorTracker { return this._anchor } + drainHeldBytes(): string { + if (!this.scanState) { + return '' + } + const held = drainShellStartupIdentityHeldBytes(this.scanState) + this.scanState = null + return held + } + + drainInto(accept: (data: string) => void): void { + const held = this.drainHeldBytes() + if (held.length > 0) { + accept(held) + } + } + scan(data: string): string { if (!this.scanState) { return data diff --git a/src/main/daemon/session.test.ts b/src/main/daemon/session.test.ts index 93993e38d78..552646356f1 100644 --- a/src/main/daemon/session.test.ts +++ b/src/main/daemon/session.test.ts @@ -184,6 +184,18 @@ describe('Session', () => { }) }) + it('drains a partial WSL identity marker when the subprocess exits', () => { + createSession({ wslDistro: 'Ubuntu' }) + const received: string[] = [] + session.attachClient({ onData: (data) => received.push(data), onExit: () => {} }) + const partial = '\x1b]777;orca-shell-start:v2:Ubuntu:01234567' + + subprocess.simulateData(partial) + subprocess.simulateExit(0) + + expect(received.join('')).toContain(partial) + }) + it('does not confirm shell ownership from historical replay bytes', () => { createSession({ historySeedChunks: ['\x1b[?1049hOLD-TUI\x1b]133;D;137\x07old-shell-marker'] diff --git a/src/main/daemon/session.ts b/src/main/daemon/session.ts index 25935631e51..a7eb732a00c 100644 --- a/src/main/daemon/session.ts +++ b/src/main/daemon/session.ts @@ -291,6 +291,7 @@ export class Session { // Why: `wasTerminating` below must be read BEFORE the `_state = 'exited'` flip — it guards the // "dispose while kill() in flight" case and the invariant needs the pre-flip `_state`; do NOT move it down. this.shellReady.releaseDeviceAttributes() + this.wslShellAnchorTracker.drainInto((data) => this.shellReady.ingestSubprocessData(data)) this.shellReady.releaseHeldBytes() this.startupIngress.drainAndClose() // Why after drainAndClose (and before clearClients below): a dispose @@ -362,6 +363,7 @@ export class Session { this.shellReady.releaseDeviceAttributes() this.shellReady.disposePromptReadinessProbe() + this.wslShellAnchorTracker.drainInto((data) => this.shellReady.ingestSubprocessData(data)) this.shellReady.releaseHeldBytes() this.startupIngress.drainAndClose() // Why after drainAndClose: drained ingress bytes re-enter the barrier and can diff --git a/src/main/providers/local-pty-session-activation.ts b/src/main/providers/local-pty-session-activation.ts index 40d0d776f31..ce78364f5e2 100644 --- a/src/main/providers/local-pty-session-activation.ts +++ b/src/main/providers/local-pty-session-activation.ts @@ -36,6 +36,7 @@ import { destroyPtyProcess, createPtyPhysicalExit } from './local-pty-terminatio import { writeStartupCommandWhenShellReady } from './local-pty-shell-ready-startup-command' import { createShellStartupIdentityScanState, + drainShellStartupIdentityHeldBytes, scanForShellStartupIdentity, type ShellStartupIdentityScanState } from '../shell-startup-identity-scanner' @@ -165,6 +166,15 @@ export function activateLocalPtySession(args: { if (process.platform !== 'win32') { ;(proc as unknown as { kill: (sig?: string) => void }).kill = () => {} } + // If the PTY dies mid-marker, return the withheld OSC bytes to the normal + // ingress path so teardown cannot silently eat user-visible output. + if (wslIdentityScanState) { + const heldIdentityBytes = drainShellStartupIdentityHeldBytes(wslIdentityScanState) + wslIdentityScanState = null + if (heldIdentityBytes.length > 0) { + readiness.acceptData(heldIdentityBytes) + } + } readiness.prepareForExit() clearPtyState(id) startupIngress.drainAndClose() diff --git a/src/main/providers/local-pty-session-operations.ts b/src/main/providers/local-pty-session-operations.ts index 699c49f22b4..748b9334a0e 100644 --- a/src/main/providers/local-pty-session-operations.ts +++ b/src/main/providers/local-pty-session-operations.ts @@ -128,7 +128,9 @@ export async function listLocalPtyProcesses(): Promise { const wslByDistro = new Map() for (const [id] of entries) { const distro = ptyWslDistroById.get(id) - if (distro) { + // Without a shell anchor the inventory cannot resolve a foreground process; + // avoid paying for a guest scan whose verdict is necessarily unverifiable. + if (distro && ptyWslShellAnchors.has(id)) { const ids = wslByDistro.get(distro) ?? [] ids.push(id) wslByDistro.set(distro, ids) @@ -141,7 +143,12 @@ export async function listLocalPtyProcesses(): Promise { }) ) - return entries.map(([id, proc]) => { + return entries.flatMap(([id, proc]) => { + // Inventory reads are asynchronous; a PTY may have exited while they ran. + // Do not publish a row for an incarnation that is no longer authoritative. + if (ptyProcesses.get(id) !== proc) { + return [] + } const distro = ptyWslDistroById.get(id) let title = proc.process || ptyShellName.get(id) || 'shell' let foregroundProcessEvidence: ForegroundProcessEvidence | undefined @@ -178,16 +185,18 @@ export async function listLocalPtyProcesses(): Promise { } } } - return { - id, - ...(ptyIncarnations.get(id) ? { incarnationId: ptyIncarnations.get(id) } : {}), - cwd: ptyInitialCwd.get(id) ?? '', - title, - ...(ptyWorktreeId.get(id) ? { worktreeId: ptyWorktreeId.get(id) } : {}), - ...(ptyTerminalHandle.get(id) ? { terminalHandle: ptyTerminalHandle.get(id) } : {}), - ...(ptyWslDistroById.has(id) ? { wslDistro: ptyWslDistroById.get(id) ?? null } : {}), - ...(foregroundProcessEvidence ? { foregroundProcessEvidence } : {}) - } + return [ + { + id, + ...(ptyIncarnations.get(id) ? { incarnationId: ptyIncarnations.get(id) } : {}), + cwd: ptyInitialCwd.get(id) ?? '', + title, + ...(ptyWorktreeId.get(id) ? { worktreeId: ptyWorktreeId.get(id) } : {}), + ...(ptyTerminalHandle.get(id) ? { terminalHandle: ptyTerminalHandle.get(id) } : {}), + ...(ptyWslDistroById.has(id) ? { wslDistro: ptyWslDistroById.get(id) ?? null } : {}), + ...(foregroundProcessEvidence ? { foregroundProcessEvidence } : {}) + } + ] }) } diff --git a/src/main/pty/wsl-orca-env.test.ts b/src/main/pty/wsl-orca-env.test.ts index 71663a84874..13df470fbe8 100644 --- a/src/main/pty/wsl-orca-env.test.ts +++ b/src/main/pty/wsl-orca-env.test.ts @@ -16,6 +16,16 @@ describe('addOrcaWslInteropEnv', () => { expect(env.WSLENV).toBe('ORCA_TERMINAL_HANDLE/u:ORCA_SHELL_READY_ROOT/p:ORCA_SHELL_FEATURES/u') }) + it('drops unknown inherited shell features before adding canonical WSL features', () => { + const env: Record = { + ORCA_SHELL_FEATURES: 'overlay,evil, markers,overlay, identity ' + } + + addOrcaWslInteropEnv(env) + + expect(env.ORCA_SHELL_FEATURES).toBe('overlay,markers,identity') + }) + // Why this is published at all: the wrapper tree is content-addressed, so the // in-guest login script cannot rebuild its path from ORCA_USER_DATA_PATH -- it // cannot derive the hash segment. Without this the guest finds no wrapper and diff --git a/src/main/pty/wsl-orca-env.ts b/src/main/pty/wsl-orca-env.ts index a3079909883..9382bfe158c 100644 --- a/src/main/pty/wsl-orca-env.ts +++ b/src/main/pty/wsl-orca-env.ts @@ -9,6 +9,7 @@ import { SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV } from '../../shared/setup-agent-sequencing' import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root' +import { SHELL_STARTUP_FEATURES, type ShellStartupFeature } from '../shell-startup-features' const WSLENV_ENTRY_SEPARATOR = ':' @@ -61,7 +62,13 @@ export function addOrcaWslInteropEnv(env: Record): void { // WSL's host-side process is always wsl.exe. Ask the guest wrapper to emit // its shell identity marker so process evidence can anchor to the guest PID. // The feature selection crosses through WSLENV, never the wsl.exe argv. - const existingFeatures = env.ORCA_SHELL_FEATURES?.split(',').filter(Boolean) ?? [] + // A WSL pane can inherit this channel from an Orca shell launched earlier; + // retain only canonical feature names before adding this launch's overlay. + const existingFeatures = (env.ORCA_SHELL_FEATURES?.split(',') ?? []) + .map((feature) => feature.trim()) + .filter((feature): feature is ShellStartupFeature => + (SHELL_STARTUP_FEATURES as readonly string[]).includes(feature) + ) const overlayFeatures = [ 'ORCA_OPENCODE_CONFIG_DIR', 'ORCA_MIMOCODE_HOME', diff --git a/src/main/shell-templates.ts b/src/main/shell-templates.ts index 11e9cf35cf6..3a7ac115db9 100644 --- a/src/main/shell-templates.ts +++ b/src/main/shell-templates.ts @@ -45,11 +45,16 @@ export const SHELL_STARTUP_IDENTITY_MARKER_BLOCK = `if __orca_has_feature identi if [ -n "\${WSL_DISTRO_NAME:-}" ]; then __orca_emit_shell_identity() { local _orca_boot _orca_stat _orca_tail _orca_start _orca_tty _orca_distro + local IFS=' ' _orca_boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || return 0 _orca_stat=$(cat /proc/$$/stat 2>/dev/null) || return 0 _orca_tail=\${_orca_stat##*) } - set -- $_orca_tail - _orca_start=\${20:-} + # zsh does not perform implicit word splitting for unquoted parameters; + # read the proc-stat fields explicitly so field 22 (starttime) is parsed + # correctly in both zsh and bash. + builtin read -r _orca_state _orca_f2 _orca_f3 _orca_f4 _orca_f5 _orca_f6 _orca_f7 _orca_f8 _orca_f9 _orca_f10 _orca_f11 _orca_f12 _orca_f13 _orca_f14 _orca_f15 _orca_f16 _orca_f17 _orca_f18 _orca_f19 _orca_start _orca_rest </dev/null) || return 0 _orca_distro=\${WSL_DISTRO_NAME:-} case "$_orca_boot" in *[!A-Fa-f0-9-]*|"") return 0 ;; esac