Skip to main content

ai_agent/bridge/
bridge_types.rs

1//! Bridge types.
2//!
3//! Translated from openclaudecode/src/bridge/types.ts
4
5use serde::{Deserialize, Serialize};
6
7// =============================================================================
8// CONSTANTS
9// =============================================================================
10
11/// Default per-session timeout (24 hours).
12pub const DEFAULT_SESSION_TIMEOUT_MS: u64 = 24 * 60 * 60 * 1000;
13
14/// Reusable login guidance appended to bridge auth errors.
15pub const BRIDGE_LOGIN_INSTRUCTION: &str = "Remote Control is only available with \
16    claude.ai subscriptions. Please use `/login` to sign in with your claude.ai account.";
17
18/// Full error printed when `claude remote-control` is run without auth.
19pub const BRIDGE_LOGIN_ERROR: &str = "Error: You must be logged in to use Remote Control.\n\n\
20    Remote Control is only available with claude.ai subscriptions. Please use `/login` to \
21    sign in with your claude.ai account.";
22
23/// Shown when the user disconnects Remote Control (via /remote-control or ultraplan launch).
24pub const REMOTE_CONTROL_DISCONNECTED_MSG: &str = "Remote Control disconnected.";
25
26// =============================================================================
27// PROTOCOL TYPES FOR THE ENVIRONMENTS API
28// =============================================================================
29
30/// Work data from the server
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct WorkData {
33    #[serde(rename = "type")]
34    pub data_type: String,
35    pub id: String,
36}
37
38/// Work response from poll endpoint
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct WorkResponse {
41    pub id: String,
42    #[serde(rename = "type")]
43    pub response_type: String,
44    #[serde(rename = "environment_id")]
45    pub environment_id: String,
46    pub state: String,
47    pub data: WorkData,
48    pub secret: String, // base64url-encoded JSON
49    #[serde(rename = "created_at")]
50    pub created_at: String,
51}
52
53/// Work secret decoded from the server
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct WorkSecret {
56    pub version: u32,
57    #[serde(rename = "session_ingress_token")]
58    pub session_ingress_token: String,
59    #[serde(rename = "api_base_url")]
60    pub api_base_url: String,
61    pub sources: Vec<WorkSource>,
62    pub auth: Vec<WorkAuth>,
63    #[serde(rename = "claude_code_args")]
64    pub claude_code_args: Option<std::collections::HashMap<String, String>>,
65    #[serde(rename = "mcp_config")]
66    pub mcp_config: Option<serde_json::Value>,
67    #[serde(rename = "environment_variables")]
68    pub environment_variables: Option<std::collections::HashMap<String, String>>,
69    /// Server-driven CCR v2 selector. Set by prepare_work_secret() when the
70    /// session was created via the v2 compat layer (ccr_v2_compat_enabled).
71    /// Same field the BYOC runner reads at environment-runner/sessionExecutor.ts.
72    #[serde(rename = "use_code_sessions")]
73    pub use_code_sessions: Option<bool>,
74}
75
76/// Work source (e.g., git repository)
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct WorkSource {
79    #[serde(rename = "type")]
80    pub source_type: String,
81    #[serde(rename = "git_info")]
82    pub git_info: Option<GitInfo>,
83}
84
85/// Git info for work source
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct GitInfo {
88    #[serde(rename = "type")]
89    pub info_type: String,
90    pub repo: String,
91    pub r#ref: Option<String>,
92    pub token: Option<String>,
93}
94
95/// Work auth entry
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct WorkAuth {
98    #[serde(rename = "type")]
99    pub auth_type: String,
100    pub token: String,
101}
102
103/// Session done status
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "lowercase")]
106pub enum SessionDoneStatus {
107    Completed,
108    Failed,
109    Interrupted,
110}
111
112/// Session activity type
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum SessionActivityType {
116    ToolStart,
117    Text,
118    Result,
119    Error,
120}
121
122/// Session activity for displaying tool progress
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct SessionActivity {
125    #[serde(rename = "type")]
126    pub activity_type: SessionActivityType,
127    /// e.g. "Editing src/foo.ts", "Reading package.json"
128    pub summary: String,
129    pub timestamp: u64,
130}
131
132// =============================================================================
133// SPAWN MODE
134// =============================================================================
135
136/// How `claude remote-control` chooses session working directories.
137/// - `single-session`: one session in cwd, bridge tears down when it ends
138/// - `worktree`: persistent server, every session gets an isolated git worktree
139/// - `same-dir`: persistent server, every session shares cwd (can stomp each other)
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "snake_case")]
142pub enum SpawnMode {
143    SingleSession,
144    Worktree,
145    SameDir,
146}
147
148impl Default for SpawnMode {
149    fn default() -> Self {
150        SpawnMode::SingleSession
151    }
152}
153
154// =============================================================================
155// WORKER TYPE
156// =============================================================================
157
158/// Well-known worker_type values THIS codebase produces. Sent as
159/// `metadata.worker_type` at environment registration so claude.ai can filter
160/// the session picker by origin (e.g. assistant tab only shows assistant
161/// workers). The backend treats this as an opaque string — desktop cowork
162/// sends `"cowork"`, which isn't in this union. REPL code uses this narrow
163/// type for its own exhaustiveness; wire-level fields accept any string.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "snake_case")]
166pub enum BridgeWorkerType {
167    ClaudeCode,
168    ClaudeCodeAssistant,
169}
170
171impl BridgeWorkerType {
172    pub fn as_str(&self) -> &'static str {
173        match self {
174            BridgeWorkerType::ClaudeCode => "claude_code",
175            BridgeWorkerType::ClaudeCodeAssistant => "claude_code_assistant",
176        }
177    }
178}
179
180// =============================================================================
181// BRIDGE CONFIG
182// =============================================================================
183
184/// Bridge configuration
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct BridgeConfig {
187    pub dir: String,
188    #[serde(rename = "machineName")]
189    pub machine_name: String,
190    pub branch: String,
191    #[serde(rename = "gitRepoUrl")]
192    pub git_repo_url: Option<String>,
193    #[serde(rename = "maxSessions")]
194    pub max_sessions: u32,
195    pub spawn_mode: SpawnMode,
196    pub verbose: bool,
197    pub sandbox: bool,
198    /// Client-generated UUID identifying this bridge instance.
199    #[serde(rename = "bridgeId")]
200    pub bridge_id: String,
201    /// Sent as metadata.worker_type so web clients can filter by origin.
202    /// Backend treats this as opaque — any string, not just BridgeWorkerType.
203    #[serde(rename = "workerType")]
204    pub worker_type: String,
205    /// Client-generated UUID for idempotent environment registration.
206    #[serde(rename = "environmentId")]
207    pub environment_id: String,
208    /// Backend-issued environment_id to reuse on re-register. When set, the
209    /// backend treats registration as a reconnect to the existing environment
210    /// instead of creating a new one. Used by `claude remote-control
211    /// --session-id` resume. Must be a backend-format ID — client UUIDs are
212    /// rejected with 400.
213    #[serde(rename = "reuseEnvironmentId")]
214    pub reuse_environment_id: Option<String>,
215    /// API base URL the bridge is connected to (used for polling).
216    #[serde(rename = "apiBaseUrl")]
217    pub api_base_url: String,
218    /// Session ingress base URL for WebSocket connections (may differ from apiBaseUrl locally).
219    #[serde(rename = "sessionIngressUrl")]
220    pub session_ingress_url: String,
221    /// Debug file path passed via --debug-file.
222    #[serde(rename = "debugFile")]
223    pub debug_file: Option<String>,
224    /// Per-session timeout in milliseconds. Sessions exceeding this are killed.
225    #[serde(rename = "sessionTimeoutMs")]
226    pub session_timeout_ms: Option<u64>,
227}
228
229// =============================================================================
230// PERMISSION RESPONSE EVENT
231// =============================================================================
232
233/// A control_response event sent back to a session (e.g. a permission decision).
234/// The `subtype` is `'success'` per the SDK protocol; the inner `response`
235/// carries the permission decision payload (e.g. `{ behavior: 'allow' }`).
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct PermissionResponseEvent {
238    #[serde(rename = "type")]
239    pub event_type: String,
240    pub response: PermissionResponseInner,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct PermissionResponseInner {
245    #[serde(rename = "subtype")]
246    pub response_subtype: String,
247    #[serde(rename = "request_id")]
248    pub request_id: String,
249    pub response: serde_json::Value,
250}
251
252// =============================================================================
253// BRIDGE API CLIENT (trait)
254// =============================================================================
255
256/// Bridge API client trait for dependency injection
257pub trait BridgeApiClient: Send + Sync {
258    fn register_bridge_environment(
259        &self,
260        config: &BridgeConfig,
261    ) -> impl std::future::Future<Output = Result<(String, String), String>> + Send;
262
263    fn poll_for_work(
264        &self,
265        environment_id: &str,
266        environment_secret: &str,
267        signal: Option<&std::sync::atomic::AtomicBool>,
268        reclaim_older_than_ms: Option<u64>,
269    ) -> impl std::future::Future<Output = Option<WorkResponse>> + Send;
270
271    fn acknowledge_work(
272        &self,
273        environment_id: &str,
274        work_id: &str,
275        session_token: &str,
276    ) -> impl std::future::Future<Output = Result<(), String>> + Send;
277
278    /// Stop a work item via the environments API.
279    fn stop_work(
280        &self,
281        environment_id: &str,
282        work_id: &str,
283        force: bool,
284    ) -> impl std::future::Future<Output = Result<(), String>> + Send;
285
286    /// Deregister/delete the bridge environment on graceful shutdown.
287    fn deregister_environment(
288        &self,
289        environment_id: &str,
290    ) -> impl std::future::Future<Output = Result<(), String>> + Send;
291
292    /// Send a permission response (control_response) to a session via the session events API.
293    fn send_permission_response_event(
294        &self,
295        session_id: &str,
296        event: &PermissionResponseEvent,
297        session_token: &str,
298    ) -> impl std::future::Future<Output = Result<(), String>> + Send;
299
300    /// Archive a session so it no longer appears as active on the server.
301    fn archive_session(
302        &self,
303        session_id: &str,
304    ) -> impl std::future::Future<Output = Result<(), String>> + Send;
305
306    /// Force-stop stale worker instances and re-queue a session on an environment.
307    /// Used by `--session-id` to resume a session after the original bridge died.
308    fn reconnect_session(
309        &self,
310        environment_id: &str,
311        session_id: &str,
312    ) -> impl std::future::Future<Output = Result<(), String>> + Send;
313
314    /// Send a lightweight heartbeat for an active work item, extending its lease.
315    /// Uses SessionIngressAuth (JWT, no DB hit) instead of EnvironmentSecretAuth.
316    /// Returns the server's response with lease status.
317    fn heartbeat_work(
318        &self,
319        environment_id: &str,
320        work_id: &str,
321        session_token: &str,
322    ) -> impl std::future::Future<Output = Result<HeartbeatResponse, String>> + Send;
323}
324
325/// Heartbeat response
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct HeartbeatResponse {
328    #[serde(rename = "lease_extended")]
329    pub lease_extended: bool,
330    pub state: String,
331}
332
333// =============================================================================
334// SESSION HANDLE
335// =============================================================================
336
337/// Session handle for a running session
338pub struct SessionHandle {
339    /// Session ID
340    pub session_id: String,
341    /// Flag indicating if session is done (set by kill/force_kill)
342    pub done: bool,
343    /// Kill the session gracefully
344    pub kill: Box<dyn Fn() + Send + Sync>,
345    /// Force kill the session
346    pub force_kill: Box<dyn Fn() + Send + Sync>,
347    /// Ring buffer of recent activities (last ~10)
348    pub activities: Vec<SessionActivity>,
349    /// Most recent activity
350    pub current_activity: Option<SessionActivity>,
351    /// session_ingress_token for API calls
352    pub access_token: String,
353    /// Ring buffer of last stderr lines
354    pub last_stderr: Vec<String>,
355    /// Write directly to child stdin
356    pub write_stdin: Box<dyn Fn(String) + Send + Sync>,
357    /// Update the access token for a running session (e.g. after token refresh).
358    pub update_access_token: Box<dyn Fn(String) + Send + Sync>,
359}
360
361impl SessionHandle {
362    pub fn new(session_id: String, access_token: String) -> Self {
363        Self {
364            session_id,
365            done: false,
366            kill: Box::new(|| {}),
367            force_kill: Box::new(|| {}),
368            activities: Vec::new(),
369            current_activity: None,
370            access_token,
371            last_stderr: Vec::new(),
372            write_stdin: Box::new(|_| {}),
373            update_access_token: Box::new(|_| {}),
374        }
375    }
376}
377
378// =============================================================================
379// SESSION SPAWN OPTS
380// =============================================================================
381
382/// Options for spawning a session
383pub struct SessionSpawnOpts {
384    /// Session ID
385    pub session_id: String,
386    /// SDK URL
387    pub sdk_url: String,
388    /// Access token
389    pub access_token: String,
390    /// When true, spawn the child with CCR v2 env vars (SSE transport + CCRClient).
391    pub use_ccr_v2: Option<bool>,
392    /// Required when useCcrV2 is true. Obtained from POST /worker/register.
393    pub worker_epoch: Option<i64>,
394    /// Fires once with the text of the first real user message seen on the
395    /// child's stdout (via --replay-user-messages). Lets the caller derive a
396    /// session title when none exists yet. Tool-result and synthetic user
397    /// messages are skipped.
398    pub on_first_user_message: Option<Box<dyn Fn(String) + Send + Sync>>,
399}
400
401impl Clone for SessionSpawnOpts {
402    fn clone(&self) -> Self {
403        Self {
404            session_id: self.session_id.clone(),
405            sdk_url: self.sdk_url.clone(),
406            access_token: self.access_token.clone(),
407            use_ccr_v2: self.use_ccr_v2,
408            worker_epoch: self.worker_epoch,
409            // Callbacks cannot be cloned - set to None
410            on_first_user_message: None,
411        }
412    }
413}
414
415// =============================================================================
416// SESSION SPAWNER
417// =============================================================================
418
419/// Session spawner trait for dependency injection
420pub trait SessionSpawner: Send + Sync {
421    fn spawn(&self, opts: &SessionSpawnOpts, dir: &str) -> SessionHandle;
422}
423
424// =============================================================================
425// BRIDGE LOGGER
426// =============================================================================
427
428/// Bridge logger trait for displaying status
429pub trait BridgeLogger: Send + Sync {
430    /// Print banner with configuration
431    fn print_banner(&self, config: &BridgeConfig, environment_id: &str);
432
433    /// Log session start
434    fn log_session_start(&self, session_id: &str, prompt: &str);
435
436    /// Log session complete
437    fn log_session_complete(&self, session_id: &str, duration_ms: u64);
438
439    /// Log session failed
440    fn log_session_failed(&self, session_id: &str, error: &str);
441
442    /// Log status message
443    fn log_status(&self, message: &str);
444
445    /// Log verbose message
446    fn log_verbose(&self, message: &str);
447
448    /// Log error message
449    fn log_error(&self, message: &str);
450
451    /// Log reconnection success
452    fn log_reconnected(&self, disconnected_ms: u64);
453
454    /// Set repository info for status line display
455    fn set_repo_info(&self, repo_name: &str, branch: &str);
456
457    /// Set debug log path shown above the status line (ant users)
458    fn set_debug_log_path(&self, path: &str);
459
460    /// Show idle status with repo/branch info and shimmer animation
461    fn update_idle_status(&self);
462
463    /// Transition to "Attached" state when a session starts
464    fn set_attached(&self, session_id: &str);
465
466    /// Show reconnecting status in the live display
467    fn update_reconnecting_status(&self, delay_str: &str, elapsed_str: &str);
468
469    /// Update session status
470    fn update_session_status(
471        &self,
472        session_id: &str,
473        elapsed: &str,
474        activity: &SessionActivity,
475        trail: &[String],
476    );
477
478    /// Clear status
479    fn clear_status(&self);
480
481    /// Toggle QR code visibility
482    fn toggle_qr(&self);
483
484    /// Update the "<n> of <m> sessions" indicator and spawn mode hint
485    fn update_session_count(&self, active: u32, max: u32, mode: SpawnMode);
486
487    /// Update the spawn mode shown in the session-count line
488    fn set_spawn_mode_display(&self, mode: Option<SpawnMode>);
489
490    /// Register a new session for multi-session display
491    fn add_session(&self, session_id: &str, url: &str);
492
493    /// Update the per-session activity summary in the multi-session list
494    fn update_session_activity(&self, session_id: &str, activity: &SessionActivity);
495
496    /// Set a session's display title
497    fn set_session_title(&self, session_id: &str, title: &str);
498
499    /// Remove a session from the multi-session display when it ends
500    fn remove_session(&self, session_id: &str);
501
502    /// Force a re-render of the status display
503    fn refresh_display(&self);
504}