fix(macos): gate boot on Secure Enclave session — stop TWO FEDERATION IDENTITIES (CIRISServer#380) - #1046
fix(macos): gate boot on Secure Enclave session — stop TWO FEDERATION IDENTITIES (CIRISServer#380)#1046emooreatx wants to merge 1 commit into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Strong RCA — the intermittency is the part that explains everything, and deriving the decision from console-session state instead of probing "is this blob SE-backed?" avoids a probe that would itself be flaky. Endorsing the approach. Three findings, one blocking. Blocking — version collision with #1044Both PRs bump to Both target Recommendation: this belongs in 2.9.18 rather than after it. 2.9.18's stated goal is all-platforms login green, and macOS login is currently red, so shipping the release without this would leave the goal unmet. Merge #1044 first (it carries the substrate bump, iOS refresh and the security fixes), then rebase this and drop its Should fix — the default wait is unbounded, and that is a new permanent hang
await_secure_enclave_session() # timeout_secs=None → wait foreverConsider a Mac running the agent as a service with a console session logged in but the screen locked — a Mac mini build host or home server, screen locked overnight. That is That converts a flaky failure into a deterministic one, which is the goal — but in the wrong direction for a headed-but-unattended host, and it is the one shape most likely to be running unattended where nobody will see the message. Your instinct in the caveat was right, and I would go further than optional: give Worth choosing the number against Should fix —
|
…FEDERATION IDENTITIES)
macOS SE keygen intermittently returns OSStatus -25308 (errSecInteractionNotAllowed)
on a LOCKED console session, so the persist Engine and the node compose seal the
federation identity under different backends (SE vs software) -> two keys -> the
node refuses ("the persist Engine and this process sign as DIFFERENT keys"). Works
elsewhere: Linux/CI has no Secure Enclave (software-only deterministic), iOS reaches
SE reliably (foreground/unlocked). Only macOS with a locked console flip-flops.
Fix: a Secure Enclave session gate (ciris_engine/logic/runtime/se_session_gate.py)
run before the Engine is constructed. Classifies the macOS console session from
IOConsoleUsers (ioreg plist, no PyObjC, no identity mint):
* no console session (headless/CI): SE unavailable -> software deterministic -> proceed
* console unlocked: SE reachable -> proceed (uses SE)
* console LOCKED: SE intermittent -> WAIT with status "Waiting for an active user
session to access key material in the Secure Enclave" (CLI + KMP console parser),
polling until unlocked, then proceed on SE.
Covers headless / headed / headed-then-headless. No-op off macOS and on iOS.
Review fixes:
* bounded wait — timeout_secs now resolves an env default
(CIRIS_SE_SESSION_GATE_TIMEOUT_SECONDS, default 45s, kept under the desktop
launcher's 60s health-wait) so an unattended locked host proceeds (today's
intermittent-boot behavior) instead of hanging forever; <=0 opts into an
indefinite wait.
* per-user classify — on-console and screen-locked are read off the SAME
console user, fixing a split-any() that mislabeled fast-user-switching
(attended-unlocked A + switched-out-locked B) as LOCKED.
No version bump here — 2.9.18 lands via #1044; this rebases onto it to avoid the
constants.py collision. node_fold.py also enumerates the identity dir before
serve() for diagnosis. 12 unit tests cover classify (incl. user-switching),
wait/resume, timeout bounding, and env parsing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dwg1ssK93xkJeEZPnQcxg
b98b106 to
334b413
Compare
|
Thanks for the review — all three addressed in 1. Version collision (blocking). Dropped the version bump from this branch entirely — reverted 2. Unbounded wait → real bounded default.
3. On the lock signal: kept, per your reasoning — And noted re: the |
Bundling CIRISServer#380 (TWO FEDERATION IDENTITIES on macOS) into this release so 2.9.18 delivers its all-platforms-login goal on macOS as well as Android. The gate is a proven no-op off macOS — executed on Linux it returns NOT_APPLICABLE in 0.000s — so it cannot affect the other platforms' boot. Review findings from #1046 are in: bounded default wait (45s, deliberately under the launcher's 60s health-wait so 'screen is locked' is the signal rather than a generic timeout), per-user classify so fast-user-switching reads REACHABLE, and the version bump dropped since this release owns 2.9.18.
Problem
On macOS desktop, the backend (which the desktop app launches via
main.py --adapter api) fails to initialize:It works on Linux/CI and iOS, so it read as macOS-specific.
Root cause
The node's one federation identity is sealed in a keystore that is opened more than once per boot — first by the persist
Engine(persistence/db/core.py), then by the node compose insideciris_server.serve_with_python_adapter. On macOS theciris_keyringfactory selects the Secure Enclave signer.macOS SE key operations return
OSStatus -25308(errSecInteractionNotAllowed) when a console session's screen is locked — but intermittently across the boot's keystore opens. One open lands on SE, the next falls back to software ("Failed to initialize SE wrapper - falling back to software-only"). The two opens then seal the identity as different keys under one alias → the substrate's one-identity gate refuses.Ruled out with controlled runs:
identity_dir==get_ciris_home()/identity, byte-identical.identity/.CGSSessionScreenIsLocked=Yes; caught SE succeeding on one open and-25308-failing on the next within the same boot.Works everywhere else: Linux/CI has no Secure Enclave (software-only → deterministic); iOS reaches SE reliably (foreground app, unlocked device). Only macOS with a locked console session flip-flops.
Fix
A Secure Enclave session gate (
ciris_engine/logic/runtime/se_session_gate.py) runs right before the Engine is constructed. It classifies the macOS console session purely fromIOConsoleUsers(viaioregplist — no PyObjC dep, no fragile SE probe, and it never mints an identity):Waiting for an active user session to access key material in the Secure Enclaveon CLI + logs, polling until unlocked, then proceed on SEThis satisfies all three required modes: headless (software), headed (SE), and headed-first-then-head-away (waits for the session to return instead of minting a divergent second identity). No-op off macOS and on iOS.
Also:
node_fold.pynow enumerates the identity-dir artifacts beforeserve(), so any future two-identity refusal names the on-disk keys instead of failing opaquely.Verification (macOS, 2.9.18)
TWO FEDERATION IDENTITIES; process stays up waiting (no crash).[SE-GATE] active session detected — resuming startup→ node resolvesone identity→ backend reaches init-complete (status/SETUP,2.9.17→2.9.18).tests/ciris_engine/logic/runtime/test_se_session_gate.py) cover the classifier + wait/resume + timeout + non-macOS no-op. All pass.🤖 Generated with Claude Code