8.2 KiB
Daemon Sentry Initialization
Context
Warp's remote server daemon (remote-server-daemon) is a long-lived headless process on the SSH host that serves remote terminal sessions. Previously it bypassed run_internal and initialize_app entirely, instead calling a separate init_common → run_daemon_app path that stood up a minimal headless AppBuilder with hand-picked singletons. This meant the daemon missed all initialization that initialize_app performs — most critically Sentry crash reporting, but also feature flags, profiling, resource limits, and any future singletons added to initialize_app.
The proxy (remote-server-proxy) is a thin stdio↔Unix-socket byte bridge. It only needs logging to stderr; it does not need initialize_app or crash reporting.
The daemon also had no way to know who was using it. Sentry reports from daemon crashes had no user identity, and there was no mechanism to dynamically enable or disable crash reporting when the user toggles their privacy preference on the client.
Relevant files (before this change)
app/src/lib.rs—init_common()duplicated early setup;run_internal()calledinit_commontheninitialize_app; daemon/proxy both calledinit_commondirectly and never reachedrun_internal.app/src/remote_server/mod.rs—run_daemon_app()built a bespoke headlessAppBuilderwith a subset of singletons.app/src/remote_server/unix/mod.rs—run_daemon()bound the socket, then calledrun_daemon_app.app/src/remote_server/server_model.rs—handle_initializeonly stored the auth token.crates/remote_server/src/auth.rs—RemoteServerAuthContextcarried only auth token + identity key closures.crates/remote_server/proto/remote_server.proto—Initializehad onlyauth_token.
Proposed changes
1. Unify daemon startup through run_internal
Delete init_common and inline its steps at the top of run_internal. Delete run_daemon_app. The daemon now enters through:
run_daemon(identity_key) → run_internal(LaunchMode::RemoteServerDaemon { identity_key })
→ early init (logging, feature flags, profiling, resource limits, TLS)
→ AppBuilder::new_headless + initialize_app (full singleton chain including Sentry)
→ launch() which matches LaunchMode::RemoteServerDaemon and calls launch_daemon()
LaunchMode::RemoteServerDaemon becomes a struct variant carrying the identity_key: String so launch() can pass it to socket binding. All existing match arms update to RemoteServerDaemon { .. }.
launch_daemon() (new function in unix/mod.rs) binds the Unix socket, writes the PID file, spawns the accept loop, and registers ServerModel as a singleton — the same work run_daemon_app did, minus the AppBuilder and generic singletons that initialize_app now provides.
Key files:
app/src/lib.rs (375-380)—LaunchMode::RemoteServerDaemon { identity_key }struct variantapp/src/lib.rs (642-658)— proxy gets inline logging-only init; daemon callsrun_internalapp/src/lib.rs (2379-2393)—launch()arm callsremote_server::unix::launch_daemon()app/src/remote_server/mod.rs—run_daemon_appdeletedapp/src/remote_server/unix/mod.rs (23-40)—run_daemoncallsrun_internal;launch_daemonis the new socket-binding entrypoint
The proxy keeps a minimal inline init (logging to stderr only) and never calls run_internal.
2. Pipe user identity and crash-reporting preference to the daemon
Extend the protocol so the client sends user identity and crash-reporting preference to the daemon during the Initialize handshake, and can update the preference dynamically afterward.
Proto (crates/remote_server/proto/remote_server.proto):
Initializegainsuser_id,user_email,crash_reporting_enabledfields.- New
UpdatePreferencesmessage withcrash_reporting_enabled, added toClientMessage.oneof.
Auth context (crates/remote_server/src/auth.rs):
RemoteServerAuthContextgains three new closures:get_user_id,get_user_email,get_crash_reporting_enabled.app/src/remote_server/auth_context.rs—server_api_auth_context()sources them fromAuthStateand a sharedArc<RwLock<bool>>for the crash reporting flag.app/src/terminal/writeable_pty/remote_server_controller.rs— reads initial value fromPrivacySettings.
Client (crates/remote_server/src/client/mod.rs):
initialize()acceptsuser_id,user_email,crash_reporting_enabledand sends them in theInitializemessage.- New
update_preferences()fire-and-forget method sendsUpdatePreferences.
Manager (crates/remote_server/src/manager.rs):
run_connect_and_handshakereads user info from auth context and passes toclient.initialize().- New
all_connected_clients()iterator for broadcasting preference changes.
Daemon handlers (app/src/remote_server/server_model.rs):
handle_initialize— after storing the auth token, callscrash_reporting::set_user_id()orcrash_reporting::uninit_sentry()depending oncrash_reporting_enabled.- New
handle_update_preferences— dynamically enables/disables Sentry. Uses newcrash_reporting::is_initialized()to avoid double-init. apply_initialize_authextracted for testability withoutModelContext.
Event wiring (app/src/remote_server/mod.rs):
wire_auth_token_rotationnow also subscribes toPrivacySettingsChangedEvent. When the user toggles crash reporting, it callsupdate_preferences()on every connected daemon viaall_connected_clients().
Crash reporting (app/src/crash_reporting/mod.rs):
- New
is_initialized()helper checks theRUST_SENTRY_CLIENT_GUARDstate.
Data flow
sequenceDiagram
participant Client as Warp Client
participant Manager as RemoteServerManager
participant Daemon as ServerModel (daemon)
participant Sentry
Client->>Manager: connect (SSH)
Manager->>Manager: read auth_context (token, user_id, email, crash_pref)
Manager->>Daemon: Initialize { auth_token, user_id, user_email, crash_reporting_enabled }
Daemon->>Sentry: set_user_id(user_id, email) or uninit_sentry()
Daemon-->>Manager: InitializeResponse { server_version, host_id }
Note over Client,Daemon: Later, user toggles privacy settings
Client->>Client: PrivacySettingsChangedEvent
Client->>Manager: wire_auth_token_rotation subscriber
Manager->>Daemon: UpdatePreferences { crash_reporting_enabled: false }
Daemon->>Sentry: uninit_sentry()
Testing and validation
Unit tests (updated)
server_model_tests.rs— tests useapply_initialize_authinstead ofhandle_initializeto avoid needingModelContext. All four existing auth-token tests updated with newInitializefields.client_tests.rs—initialize()calls updated with new params (user_id,user_email,crash_reporting_enabled).protocol_tests.rs— round-trip and request-ID extraction tests include newInitializefields.ssh_transport.rs—RemoteServerAuthContext::newcalls updated with three new closure args.
E2E validation (manual, completed)
- Deployed daemon with a temporary
panic!("TEST DAEMON CRASH")inhandle_initializeafter Sentry setup. - Connected from Warp client → daemon crashed → verified event appeared in Sentry (
warp-client-localproject) within seconds. - Confirmed Sentry event contains:
- Correct user ID (
evlWdsVMvZYciWUIi6ZkKwVBrEH2) piped from the client. mechanism: panic,level: fatal.warp.client_type: warp-clitag.- Host/OS metadata (
Linux,x86_64).
- Correct user ID (
- Removed test panic before merging.
Risks and mitigations
- Daemon now runs the full
initialize_appsingleton chain. This is heavier than the old bespoke init, but the daemon is already a long-lived process and the overhead is one-time at startup. Any singleton that shouldn't run headless should already be gated byLaunchModechecks (several exist, e.g. GUI window creation, settings UI). - User identity is piped in plaintext over the Unix socket. This is local-only (client↔daemon on the same host), same trust boundary as the auth token that was already sent.
UpdatePreferencesis fire-and-forget. If delivery fails (daemon disconnected), the daemon will pick up the current preference on nextInitialize. No ack/retry needed.