Skip to content

fix(terminal): bypass rejected macOS login sessions - #9301

Merged
brennanb2025 merged 6 commits into
mainfrom
brennanb2025/codex-1-4-145
Jul 18, 2026
Merged

brennanb2025 merged 6 commits into
mainfrom
brennanb2025/codex-1-4-145

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Probe the macOS login(1) account/session path before using it to launch a terminal shell.
  • Run the one-time probe asynchronously so PAM cannot block Electron's main loop or the daemon's existing terminal/RPC traffic.
  • Require both a child-emitted marker and a clean exit because login(1) can return zero after an EOF-driven failed authentication prompt.
  • Send EOF immediately, cap captured output at 1 KiB, and enforce a 500 ms SIGKILL timeout; cache and deduplicate the result per PTY-host process.
  • Fall back to direct shell spawning when PAM rejects or the probe cannot run, preventing terminals from getting stranded at Login incorrect / login:.
  • Run the probe from the account home so a detached daemon whose launch worktree was deleted still works.
  • Bump the daemon protocol to v23 so old v22 sessions can remain attached while new terminal sessions route through a daemon with the fixed spawn behavior.
  • Preserve the TCC-attributed login(1) launch path on compatible macOS accounts.

ELI5

On macOS, Orca starts terminal shells through a system login helper so command-line tools can keep the right privacy permissions. On some Macs, account policy rejects that helper even though the user is already signed in. The terminal then gets stuck showing “Login incorrect” instead of opening a shell.

This change gives the helper one quick test before using it. If the test succeeds, Orca keeps the permission-friendly behavior. If it fails or hangs, Orca safely opens the shell the normal way. The test runs only once, does not freeze the app, and has a half-second hard limit. The daemon version is also bumped so an old background process cannot keep using the broken behavior.

Electron + real Codex account proof

Tested in the Electron build from this PR worktree with the machine's real ChatGPT-backed Codex login. Orca spawned Codex CLI 0.144.5 through a fresh visible terminal, and the authenticated model returned the deterministic response ORCA_E2E_AUTH_OK.

Electron terminal spawning an authenticated Codex CLI session

The screenshot contains no token, email address, account ID, or other credential.

Reliability contract

  • Invariant: every fresh terminal reaches the configured shell; a rejected or hung PAM session must never leave the PTY at a login: prompt. Existing terminals and unrelated daemon RPCs stay responsive during detection.
  • Failure sources: PAM rejection/hang, misleading zero exit after EOF, excessive child output, deleted daemon cwd, concurrent restores, and a still-running pre-fix daemon.
  • Success oracle: only the exact fixed marker from the requested child program plus a clean process exit enables the wrapper.
  • Reliability gate: stdin EOF is immediate, lifetime is capped at 500 ms with SIGKILL, output is capped at 1 KiB, concurrent callers share one child, pending creations are cancelable before subprocess registration, and failure is cached as direct-spawn fallback.
  • Coverage: both the in-process local provider and detached daemon await preparation at their async fresh-spawn boundary. Non-macOS paths are unchanged; SSH/remote macOS evaluates capability in the PTY-host process that actually launches the shell.
  • Performance budget: no synchronous child process remains. Healthy live probes completed in roughly 55-70 ms on the test Mac, once per PTY-host process; the hard worst-case delay is 500 ms and concurrent restores do not multiply probes.
  • Diagnostics / accepted gap: one warning records fallback without logging PAM output. ORCA_DISABLE_MACOS_LOGIN_SHELL=1 remains the escape hatch. A mid-process account-policy change takes effect after PTY-host restart; until then the cached result deliberately favors a usable direct shell.

Testing

  • Changed-file oxfmt and oxlint pass (10 files)
  • pnpm run typecheck:node
  • 445 focused tests across the wrapper, local fallback, daemon server/main/entry, and IPC suites
  • Daemon protocol gate test
  • Daemon concurrency test proves ping remains responsive while preparation is pending and concurrent creates yield one PTY subprocess
  • Cancellation tests prove daemon kill / cancelCreateOrAttach / shutdown and local shutdown prevent pending subprocess creation, cancel all concurrent generations, and permit later session-ID reuse
  • Reliability-gate manifest and max-lines ratchet checks
  • Live healthy macOS preflight (roughly 55-70 ms)
  • Live rejected-user probe exits without enabling the wrapper
  • Real Electron terminal + authenticated Codex CLI spawn, shown above
  • Full pnpm lint reaches an unrelated existing exhaustiveness error in src/renderer/src/components/skills/skill-freshness-group.tsx:101; changed-file lint passes
  • Full pty-subprocess.test.ts has two existing deleted-cwd cases that exceed Vitest's 5 s timeout on this Mac; the changed protocol assertion passes and the macOS case already took about 9 s before this update
  • Fresh PR CI: full repository test suite, unpacked Electron build, packaged CLI smoke, Windows terminal restart E2E, and Linux/Windows native smoke
  • Full local pnpm build not run; node typecheck, local Electron dev build, CLI build, and macOS computer-use sidecar build pass

AI Review Report

A deeper review found and fixed three material regressions in the initial implementation:

  1. spawnSync could stall Electron or the daemon event loop for 40-500 ms, freezing UI and live terminal traffic. The probe is now async, bounded, and deduplicated.
  2. Keeping daemon protocol v22 allowed an already-running buggy v22 daemon to accept the updated client and continue creating broken terminals. Protocol v23 forces new sessions onto the fixed daemon while retaining legacy-session discovery.
  3. Moving the probe to an async boundary created a close-during-preparation race. Pending creations are now generation-scoped and canceled before any subprocess can be registered.

The final review covered local and daemon spawn boundaries, concurrency, process cleanup, caching, deleted-cwd behavior, daemon migration, shell fallback, cross-platform gates, SSH execution, and failure diagnostics. No additional change met the bar for a meaningful senior-level fix.

Security audit

The probe uses fixed absolute system binaries, passes the resolved username as a literal argv element without shell interpolation, bounds time and captured output, closes stdin, and never logs captured output. No secrets, credentials, dependencies, IPC schemas, or persisted settings are added. A rejected probe only reduces behavior to the established direct-shell path.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a cached macOS login(1) PAM preflight using execFile before shell wrapping for TCC attribution. Successful results enable /usr/bin/login wrapping; failures fall back to direct spawning and warn once. Preparation is integrated into PTY spawning with cancellation handling for pending requests. The daemon protocol advances to version 23 while retaining version 22 compatibility. Tests cover preflight execution, caching, environment handling, fallback behavior, disabled execution, cancellation, and PTY integration.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: handling rejected macOS login sessions.
Description check ✅ Passed The description covers summary, testing, review, and security details; only the explicit Screenshots/Notes headings are missing.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/providers/macos-tcc-login-shell.ts (1)

1-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Node path utilities for the macOS executable paths.

Construct the /usr/bin/login and /usr/bin/printf paths with path.join (or an equivalent Node path utility) instead of hard-coding slash-separated paths.

As per coding guidelines, **/*.{js,jsx,ts,tsx} files must use path.join or Electron/Node path utilities for file paths and must not assume / or \.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0b8dcf46-7b8f-4465-b662-814916dbc7ab

📥 Commits

Reviewing files that changed from the base of the PR and between 0f1b9ef and b261cde.

📒 Files selected for processing (2)
  • src/main/providers/macos-tcc-login-shell.test.ts
  • src/main/providers/macos-tcc-login-shell.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/providers/macos-tcc-login-shell.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: aa9dfc20-28d4-485c-9f7a-d643cebd90aa

📥 Commits

Reviewing files that changed from the base of the PR and between 2af2eb3 and 2d505dd.

📒 Files selected for processing (10)
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/daemon-server.test.ts
  • src/main/daemon/daemon-server.ts
  • src/main/daemon/pty-subprocess.test.ts
  • src/main/daemon/types.ts
  • src/main/ipc/pty.test.ts
  • src/main/providers/local-pty-provider.ts
  • src/main/providers/macos-tcc-login-shell.test.ts
  • src/main/providers/macos-tcc-login-shell.ts

Comment thread src/main/daemon/daemon-server.ts Resolved
@AmethystLiang AmethystLiang self-assigned this Jul 18, 2026
@AmethystLiang
AmethystLiang self-requested a review July 18, 2026 16:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 65fcae62-6fd1-4735-be39-3dca475f1c62

📥 Commits

Reviewing files that changed from the base of the PR and between 2d505dd and 128241e.

📒 Files selected for processing (4)
  • src/main/daemon/daemon-server.test.ts
  • src/main/daemon/daemon-server.ts
  • src/main/providers/local-pty-provider.test.ts
  • src/main/providers/local-pty-provider.ts

Comment thread src/main/daemon/daemon-server.ts Resolved
Comment thread src/main/daemon/daemon-server.ts Resolved
@brennanb2025
brennanb2025 merged commit bb71a3a into main Jul 18, 2026
7 checks passed
brennanb2025 added a commit that referenced this pull request Jul 20, 2026
…ups) (#9606)

* fix(terminal): harden macOS login-preflight spawn path (#9301 follow-ups)

Follow-ups to the #9301 two-pass review. Re-derived severities: none are
release blockers; these are low-likelihood robustness/observability fixes.

F1 macos-tcc-login-shell: only cache a conclusive PAM verdict. A probe our
own SIGKILL timeout / ETIMEDOUT / maxBuffer killed proves nothing about PAM,
so it no longer sticks the whole process into direct-spawn — it fails open
for that spawn, re-probes next time, and self-heals once PAM answers.

F2 daemon-entry: prepareMacosTccLoginShell returns the outcome; the daemon
emits a structured macos-login-preflight {ok, reason} log on degrade, since
detached daemons destroy stderr and lose the console.warn.

F3 local-pty-provider: an atomic guard before the tracking write coalesces a
concurrent same-session-id spawn onto the winner instead of orphaning an
untracked, unkillable PTY.

F4 daemon-server: preparations carry their clientId; a control-socket close
cancels only that client's pending preps, so a mid-preflight disconnect no
longer leaves a durable orphan daemon PTY.

F7 warm the PAM probe at daemon startup (mirrors warmPwshAvailabilityCache);
note the pipe-vs-PTY probe fidelity limit; test that kill with no pending
prep still surfaces SessionNotFoundError.

Deliberately skipped F5 (self-limits to next reload; a real fix needs
speculative in-flight-generation tracking) and F6 (inherent to legacy
v22 attach; self-heals once daemons are v23).

* fix(terminal): bound preflight recovery work

* fix(terminal): cancel preflight when daemon stream closes
brennanb2025 added a commit that referenced this pull request Jul 22, 2026
…9826)

* fix(daemon): retire macOS daemons whose login session died (#7936)

A daemon that survives a full macOS logout is unsalvageable: its PAM
context can no longer host login(1) spawns (every new PTY becomes a
'Login incorrect' prompt zombie) and its Mach bootstrap namespace has
lost the system DNS resolver, so terminals it hosts have no egress.
Today it also keeps the #9301 preflight's cached 'accepted' verdict, so
it keeps wrapping spawns in login(1) forever; only a manual daemon
restart recovers.

GUI-spawned daemons now watch for login-session death from the inside:
a fresh cache-bypassing PAM probe (triggered by PTY-exit bursts, fresh
client hellos, and a slow periodic timer) must conclusively reject
three consecutive times AND the in-process system resolver must be
degraded; then the daemon exits crash-style so session meta stays
unclean and the replacement daemon cold-restores scrollback. A
conclusive rejection also flips the spawn-wrapper cache off
immediately.

Headless serve/SSH daemons never get the watch (they must survive their
spawning session ending), and a session that never conclusively
accepted login(1) never arms it — a PAM anomaly alone can't kill a
healthy daemon (fast user switching keeps accepting, so switched-away
sessions are preserved).

* test(daemon): e2e seam to drive login-session death oracles from a verdict file

A dead macOS login session cannot be fabricated without root (PAM owns
audit-session teardown), so live lifecycle QA drives the death watch's
probe and resolver oracles from ORCA_E2E_LOGIN_SESSION_PROBE_FILE:
'alive' → accepted/healthy, 'dead' → rejected/unhealthy, anything else
inconclusive — with compressed watch timing. Mirrors the existing
ORCA_E2E_DAEMON_INIT_DELAY_MS seam; inert unless the env var is set.

* fix(daemon): close the hang-shaped gap in login-session death detection

The conclusive-PAM-verdict trigger had one blind failure shape: login(1)
hanging at the prompt past the probe bound (killed → inconclusive
forever → the watch never fires). Three changes close it:

- The death-watch probe gets its own 4s bound (the 500ms preflight bound
  exists for spawn-path latency, which doesn't apply off-path), so a
  slow-but-answering PAM stack isn't misread as a hang.
- An inconclusive pipe probe escalates to a PTY-hosted probe via
  script(1) — a dead session's PAM stack may only misbehave under a real
  tty (the pipe-vs-PTY fidelity limit the preflight documents).
- A streak of timeout-killed probes (which a live session never
  produces) is a second retirement trigger, at a higher threshold (5)
  and still gated on the degraded resolver, logged with a distinct
  cause so field logs discriminate the two paths.

Every dead-session behavior — fast reject, prompt-then-EOF, or hang —
now fires retirement; all inconclusive states still fail toward
preserving the daemon.

* fix(daemon): keep login-session retirement conclusive

* fix(daemon): stop login watch before clean shutdown

* fix(daemon): make login-session PTY probe reliable

* fix(daemon): close login-session watch races

* fix(daemon): ignore health probes for login watch activity
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
* fix(terminal): bypass rejected macOS login sessions

* fix(terminal): bound macOS login preflight

* test(terminal): isolate macOS login preflight

* fix(terminal): avoid blocking macOS login preflight

* fix(terminal): cancel pending PTY preparation

* fix(terminal): cancel preparation on daemon shutdown
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…follow-ups) (stablyai#9606)

* fix(terminal): harden macOS login-preflight spawn path (stablyai#9301 follow-ups)

Follow-ups to the stablyai#9301 two-pass review. Re-derived severities: none are
release blockers; these are low-likelihood robustness/observability fixes.

F1 macos-tcc-login-shell: only cache a conclusive PAM verdict. A probe our
own SIGKILL timeout / ETIMEDOUT / maxBuffer killed proves nothing about PAM,
so it no longer sticks the whole process into direct-spawn — it fails open
for that spawn, re-probes next time, and self-heals once PAM answers.

F2 daemon-entry: prepareMacosTccLoginShell returns the outcome; the daemon
emits a structured macos-login-preflight {ok, reason} log on degrade, since
detached daemons destroy stderr and lose the console.warn.

F3 local-pty-provider: an atomic guard before the tracking write coalesces a
concurrent same-session-id spawn onto the winner instead of orphaning an
untracked, unkillable PTY.

F4 daemon-server: preparations carry their clientId; a control-socket close
cancels only that client's pending preps, so a mid-preflight disconnect no
longer leaves a durable orphan daemon PTY.

F7 warm the PAM probe at daemon startup (mirrors warmPwshAvailabilityCache);
note the pipe-vs-PTY probe fidelity limit; test that kill with no pending
prep still surfaces SessionNotFoundError.

Deliberately skipped F5 (self-limits to next reload; a real fix needs
speculative in-flight-generation tracking) and F6 (inherent to legacy
v22 attach; self-heals once daemons are v23).

* fix(terminal): bound preflight recovery work

* fix(terminal): cancel preflight when daemon stream closes
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…7936) (stablyai#9826)

* fix(daemon): retire macOS daemons whose login session died (stablyai#7936)

A daemon that survives a full macOS logout is unsalvageable: its PAM
context can no longer host login(1) spawns (every new PTY becomes a
'Login incorrect' prompt zombie) and its Mach bootstrap namespace has
lost the system DNS resolver, so terminals it hosts have no egress.
Today it also keeps the stablyai#9301 preflight's cached 'accepted' verdict, so
it keeps wrapping spawns in login(1) forever; only a manual daemon
restart recovers.

GUI-spawned daemons now watch for login-session death from the inside:
a fresh cache-bypassing PAM probe (triggered by PTY-exit bursts, fresh
client hellos, and a slow periodic timer) must conclusively reject
three consecutive times AND the in-process system resolver must be
degraded; then the daemon exits crash-style so session meta stays
unclean and the replacement daemon cold-restores scrollback. A
conclusive rejection also flips the spawn-wrapper cache off
immediately.

Headless serve/SSH daemons never get the watch (they must survive their
spawning session ending), and a session that never conclusively
accepted login(1) never arms it — a PAM anomaly alone can't kill a
healthy daemon (fast user switching keeps accepting, so switched-away
sessions are preserved).

* test(daemon): e2e seam to drive login-session death oracles from a verdict file

A dead macOS login session cannot be fabricated without root (PAM owns
audit-session teardown), so live lifecycle QA drives the death watch's
probe and resolver oracles from ORCA_E2E_LOGIN_SESSION_PROBE_FILE:
'alive' → accepted/healthy, 'dead' → rejected/unhealthy, anything else
inconclusive — with compressed watch timing. Mirrors the existing
ORCA_E2E_DAEMON_INIT_DELAY_MS seam; inert unless the env var is set.

* fix(daemon): close the hang-shaped gap in login-session death detection

The conclusive-PAM-verdict trigger had one blind failure shape: login(1)
hanging at the prompt past the probe bound (killed → inconclusive
forever → the watch never fires). Three changes close it:

- The death-watch probe gets its own 4s bound (the 500ms preflight bound
  exists for spawn-path latency, which doesn't apply off-path), so a
  slow-but-answering PAM stack isn't misread as a hang.
- An inconclusive pipe probe escalates to a PTY-hosted probe via
  script(1) — a dead session's PAM stack may only misbehave under a real
  tty (the pipe-vs-PTY fidelity limit the preflight documents).
- A streak of timeout-killed probes (which a live session never
  produces) is a second retirement trigger, at a higher threshold (5)
  and still gated on the degraded resolver, logged with a distinct
  cause so field logs discriminate the two paths.

Every dead-session behavior — fast reject, prompt-then-EOF, or hang —
now fires retirement; all inconclusive states still fail toward
preserving the daemon.

* fix(daemon): keep login-session retirement conclusive

* fix(daemon): stop login watch before clean shutdown

* fix(daemon): make login-session PTY probe reliable

* fix(daemon): close login-session watch races

* fix(daemon): ignore health probes for login watch activity
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants