Skip to content

Commit 287fc39

Browse files
committed
fix: reinitialize CopilotSession on model reload
When switching models via /models or /reload, the gateway now properly reinitializes the CopilotSession if the new model requires it. Previously, the session was only created at startup, causing 'missing authentication header' errors when changing models at runtime. - Add SharedCopilotSession type for mutable shared state - Extract init_copilot_session() helper function - Update Reload handler to reinitialize session - Read session from shared state before API calls Fixes #134
1 parent 8639e46 commit 287fc39

File tree

1 file changed

+102
-76
lines changed
  • crates/rustyclaw-core/src/gateway

1 file changed

+102
-76
lines changed

crates/rustyclaw-core/src/gateway/mod.rs

Lines changed: 102 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ pub type SharedConfig = Arc<RwLock<Config>>;
116116
/// Shared model context, updated on reload.
117117
pub type SharedModelCtx = Arc<RwLock<Option<Arc<ModelContext>>>>;
118118

119+
/// Shared Copilot session, updated when provider changes.
120+
pub type SharedCopilotSession = Arc<RwLock<Option<Arc<CopilotSession>>>>;
121+
119122
/// Shared task manager for first-class task orchestration.
120123
pub type SharedTaskManager = Arc<crate::tasks::TaskManager>;
121124

@@ -196,6 +199,79 @@ fn try_import_openclaw_token(vault: &mut SecretsManager) -> Option<CopilotSessio
196199
))
197200
}
198201

202+
/// Initialize a CopilotSession if the provider requires one.
203+
///
204+
/// Checks multiple sources in order:
205+
/// 1. Imported session token from vault (GITHUB_COPILOT_SESSION)
206+
/// 2. Fresh token from OpenClaw (~/.openclaw/credentials/github-copilot.token.json)
207+
/// 3. OAuth token from model context
208+
async fn init_copilot_session(
209+
provider: &str,
210+
api_key: Option<&str>,
211+
vault: &SharedVault,
212+
) -> Option<Arc<CopilotSession>> {
213+
if !crate_providers::needs_copilot_session(provider) {
214+
return None;
215+
}
216+
217+
let mut vault_guard = vault.lock().await;
218+
let session_result = vault_guard.get_secret("GITHUB_COPILOT_SESSION", true);
219+
220+
let mut session_from_import = match &session_result {
221+
Ok(Some(json_str)) => {
222+
debug!("Found GITHUB_COPILOT_SESSION in vault");
223+
serde_json::from_str::<serde_json::Value>(json_str)
224+
.ok()
225+
.and_then(|json| {
226+
let token = json.get("session_token")?.as_str()?.to_string();
227+
let expires_at = json.get("expires_at")?.as_i64()?;
228+
229+
let now = std::time::SystemTime::now()
230+
.duration_since(std::time::UNIX_EPOCH)
231+
.unwrap_or_default()
232+
.as_secs() as i64;
233+
234+
let remaining = expires_at - now;
235+
debug!(remaining_seconds = remaining, "Session expiry check");
236+
237+
if remaining > 60 {
238+
Some(CopilotSession::from_session_token(token, expires_at))
239+
} else {
240+
debug!("Session expired or expiring soon");
241+
None
242+
}
243+
})
244+
}
245+
Ok(None) => {
246+
debug!("GITHUB_COPILOT_SESSION not found in vault");
247+
None
248+
}
249+
Err(e) => {
250+
warn!(error = %e, "Failed to read GITHUB_COPILOT_SESSION");
251+
None
252+
}
253+
};
254+
255+
// If vault session is expired/missing, try to auto-import from OpenClaw
256+
if session_from_import.is_none() {
257+
if let Some(session) = try_import_openclaw_token(&mut vault_guard) {
258+
session_from_import = Some(session);
259+
}
260+
}
261+
drop(vault_guard);
262+
263+
if let Some(session) = session_from_import {
264+
debug!("Using imported session token");
265+
Some(Arc::new(session))
266+
} else if let Some(oauth) = api_key {
267+
debug!("Falling back to OAuth token");
268+
Some(Arc::new(CopilotSession::new(oauth.to_string())))
269+
} else {
270+
warn!("No OAuth token available for Copilot provider");
271+
None
272+
}
273+
}
274+
199275
/// Run the gateway WebSocket server.
200276
///
201277
/// Accepts connections in a loop until the `cancel` token is triggered,
@@ -278,81 +354,14 @@ pub async fn run_gateway(
278354
(None, None) => None,
279355
};
280356

281-
// If the provider uses Copilot session tokens, check for:
282-
// 1. Imported session token (GITHUB_COPILOT_SESSION) - use until expiry
283-
// 2. Fresh token from OpenClaw (~/.openclaw/credentials/github-copilot.token.json)
284-
// 3. OAuth token (GITHUB_COPILOT_TOKEN) - can refresh sessions
285-
let copilot_session: Option<Arc<CopilotSession>> = if model_ctx
286-
.as_ref()
287-
.map(|ctx| crate_providers::needs_copilot_session(&ctx.provider))
288-
.unwrap_or(false)
289-
{
290-
// First check for imported session token in our vault
291-
let mut vault_guard = vault.lock().await;
292-
let session_result = vault_guard.get_secret("GITHUB_COPILOT_SESSION", true);
293-
294-
let mut session_from_import = match &session_result {
295-
Ok(Some(json_str)) => {
296-
debug!("Found GITHUB_COPILOT_SESSION in vault");
297-
serde_json::from_str::<serde_json::Value>(json_str)
298-
.ok()
299-
.and_then(|json| {
300-
let token = json.get("session_token")?.as_str()?.to_string();
301-
let expires_at = json.get("expires_at")?.as_i64()?;
302-
303-
let now = std::time::SystemTime::now()
304-
.duration_since(std::time::UNIX_EPOCH)
305-
.unwrap_or_default()
306-
.as_secs() as i64;
307-
308-
let remaining = expires_at - now;
309-
debug!(remaining_seconds = remaining, "Session expiry check");
310-
311-
if remaining > 60 {
312-
Some(CopilotSession::from_session_token(token, expires_at))
313-
} else {
314-
debug!("Session expired or expiring soon");
315-
None
316-
}
317-
})
318-
}
319-
Ok(None) => {
320-
debug!("GITHUB_COPILOT_SESSION not found in vault");
321-
None
322-
}
323-
Err(e) => {
324-
warn!(error = %e, "Failed to read GITHUB_COPILOT_SESSION");
325-
None
326-
}
327-
};
328-
329-
// If vault session is expired/missing, try to auto-import from OpenClaw
330-
if session_from_import.is_none() {
331-
if let Some(session) = try_import_openclaw_token(&mut vault_guard) {
332-
session_from_import = Some(session);
333-
}
334-
}
335-
drop(vault_guard);
336-
337-
if let Some(session) = session_from_import {
338-
eprintln!("DEBUG: Using imported session token");
339-
debug!("Using imported session token");
340-
Some(Arc::new(session))
341-
} else {
342-
// Fall back to OAuth token
343-
if let Some(oauth) = model_ctx.as_ref().and_then(|ctx| ctx.api_key.clone()) {
344-
eprintln!("DEBUG: Falling back to OAuth token");
345-
debug!("Falling back to OAuth token");
346-
Some(Arc::new(CopilotSession::new(oauth)))
347-
} else {
348-
eprintln!("DEBUG: No OAuth token available either - copilot_session will be None!");
349-
warn!("No OAuth token available either");
350-
None
351-
}
352-
}
357+
// Initialize Copilot session if needed (uses the new helper function)
358+
let copilot_session: Option<Arc<CopilotSession>> = if let Some(ref ctx) = model_ctx {
359+
init_copilot_session(&ctx.provider, ctx.api_key.as_deref(), &vault).await
353360
} else {
354361
None
355362
};
363+
// Wrap in shared type so it can be updated when models change
364+
let shared_copilot_session: SharedCopilotSession = Arc::new(RwLock::new(copilot_session));
356365

357366
let model_ctx = model_ctx.map(Arc::new);
358367

@@ -386,7 +395,8 @@ pub async fn run_gateway(
386395
let messenger_models = model_registry.clone();
387396
let messenger_cancel = cancel.child_token();
388397
let mgr_clone = shared_mgr.clone();
389-
let messenger_copilot = copilot_session.clone();
398+
// Read current copilot session from shared state
399+
let messenger_copilot = shared_copilot_session.read().await.clone();
390400

391401
eprintln!("DEBUG: Spawning messenger loop task...");
392402
tokio::spawn(async move {
@@ -436,7 +446,7 @@ pub async fn run_gateway(
436446
let (stream, peer) = accepted?;
437447
let shared_cfg = shared_config.clone();
438448
let shared_ctx = shared_model_ctx.clone();
439-
let session_clone = copilot_session.clone();
449+
let shared_session = shared_copilot_session.clone();
440450
let vault_clone = vault.clone();
441451
let skill_clone = skill_mgr.clone();
442452
let task_mgr_clone = task_mgr.clone();
@@ -460,7 +470,7 @@ pub async fn run_gateway(
460470

461471
if let Err(err) = handle_connection(
462472
boxed_stream, peer, shared_cfg, shared_ctx,
463-
session_clone, vault_clone, skill_clone, task_mgr_clone,
473+
shared_session, vault_clone, skill_clone, task_mgr_clone,
464474
observer_clone, limiter_clone, child_cancel,
465475
).await {
466476
debug!(peer = %peer, error = %err, "Connection error");
@@ -603,7 +613,7 @@ async fn handle_connection(
603613
peer: SocketAddr,
604614
shared_config: SharedConfig,
605615
shared_model_ctx: SharedModelCtx,
606-
copilot_session: Option<Arc<CopilotSession>>,
616+
shared_copilot_session: SharedCopilotSession,
607617
vault: SharedVault,
608618
skill_mgr: SharedSkillManager,
609619
task_mgr: SharedTaskManager,
@@ -838,6 +848,9 @@ async fn handle_connection(
838848
.await
839849
.context("Failed to send model_connecting status")?;
840850

851+
// Read current copilot session from shared state
852+
let copilot_session = shared_copilot_session.read().await.clone();
853+
841854
match providers::validate_model_connection(
842855
&http,
843856
&probe_ctx,
@@ -1215,6 +1228,17 @@ async fn handle_connection(
12151228
("(none)".to_string(), "(none)".to_string())
12161229
};
12171230

1231+
// Reinitialize Copilot session if the new model needs it
1232+
if let Some(ref ctx) = new_model_ctx {
1233+
let new_session = init_copilot_session(
1234+
&ctx.provider,
1235+
ctx.api_key.as_deref(),
1236+
&vault,
1237+
).await;
1238+
let mut session = shared_copilot_session.write().await;
1239+
*session = new_session;
1240+
}
1241+
12181242
{
12191243
let mut cfg = shared_config.write().await;
12201244
*cfg = new_config;
@@ -1282,6 +1306,8 @@ async fn handle_connection(
12821306

12831307
// Re-read model_ctx from shared state for each dispatch
12841308
let current_model_ctx = shared_model_ctx.read().await.clone();
1309+
// Re-read copilot session from shared state
1310+
let copilot_session = shared_copilot_session.read().await.clone();
12851311
let workspace_dir = config.workspace_dir();
12861312

12871313
// Inject thread context into system prompt if available

0 commit comments

Comments
 (0)