Skip to content

Commit e58ae3e

Browse files
committed
fix providers
1 parent 74f5a91 commit e58ae3e

File tree

13 files changed

+1194
-65
lines changed

13 files changed

+1194
-65
lines changed

Cargo.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ members = [
1818
]
1919

2020
[workspace.package]
21-
version = "0.2.1"
21+
version = "0.2.2"
2222
edition = "2021"
2323
license = "Apache-2.0 OR MIT"
2424
repository = "https://github.com/RightNow-AI/openfang"

crates/openfang-api/src/middleware.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ pub async fn auth(
109109
|| path == "/api/integrations/available"
110110
|| path == "/api/integrations/health"
111111
|| path.starts_with("/api/cron/")
112+
|| path.starts_with("/api/providers/github-copilot/oauth/")
112113
{
113114
return next.run(request).await;
114115
}

crates/openfang-api/src/routes.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8703,3 +8703,153 @@ fn validate_webhook_token(headers: &axum::http::HeaderMap, token_env: &str) -> b
87038703
}
87048704
provided.as_bytes().ct_eq(expected.as_bytes()).into()
87058705
}
8706+
8707+
// ══════════════════════════════════════════════════════════════════════
8708+
// GitHub Copilot OAuth Device Flow
8709+
// ══════════════════════════════════════════════════════════════════════
8710+
8711+
/// State for an in-progress device flow.
8712+
struct CopilotFlowState {
8713+
device_code: String,
8714+
interval: u64,
8715+
expires_at: Instant,
8716+
}
8717+
8718+
/// Active device flows, keyed by poll_id. Auto-expire after the flow's TTL.
8719+
static COPILOT_FLOWS: LazyLock<DashMap<String, CopilotFlowState>> = LazyLock::new(DashMap::new);
8720+
8721+
/// POST /api/providers/github-copilot/oauth/start
8722+
///
8723+
/// Initiates a GitHub device flow for Copilot authentication.
8724+
/// Returns a user code and verification URI that the user visits in their browser.
8725+
pub async fn copilot_oauth_start() -> impl IntoResponse {
8726+
// Clean up expired flows first
8727+
COPILOT_FLOWS.retain(|_, state| state.expires_at > Instant::now());
8728+
8729+
match openfang_runtime::copilot_oauth::start_device_flow().await {
8730+
Ok(resp) => {
8731+
let poll_id = uuid::Uuid::new_v4().to_string();
8732+
8733+
COPILOT_FLOWS.insert(
8734+
poll_id.clone(),
8735+
CopilotFlowState {
8736+
device_code: resp.device_code,
8737+
interval: resp.interval,
8738+
expires_at: Instant::now()
8739+
+ std::time::Duration::from_secs(resp.expires_in),
8740+
},
8741+
);
8742+
8743+
(
8744+
StatusCode::OK,
8745+
Json(serde_json::json!({
8746+
"user_code": resp.user_code,
8747+
"verification_uri": resp.verification_uri,
8748+
"poll_id": poll_id,
8749+
"expires_in": resp.expires_in,
8750+
"interval": resp.interval,
8751+
})),
8752+
)
8753+
}
8754+
Err(e) => (
8755+
StatusCode::INTERNAL_SERVER_ERROR,
8756+
Json(serde_json::json!({ "error": e })),
8757+
),
8758+
}
8759+
}
8760+
8761+
/// GET /api/providers/github-copilot/oauth/poll/{poll_id}
8762+
///
8763+
/// Poll the status of a GitHub device flow.
8764+
/// Returns `pending`, `complete`, `expired`, `denied`, or `error`.
8765+
/// On `complete`, saves the token to secrets.env and sets GITHUB_TOKEN.
8766+
pub async fn copilot_oauth_poll(
8767+
State(state): State<Arc<AppState>>,
8768+
Path(poll_id): Path<String>,
8769+
) -> impl IntoResponse {
8770+
let flow = match COPILOT_FLOWS.get(&poll_id) {
8771+
Some(f) => f,
8772+
None => {
8773+
return (
8774+
StatusCode::NOT_FOUND,
8775+
Json(serde_json::json!({"status": "not_found", "error": "Unknown poll_id"})),
8776+
)
8777+
}
8778+
};
8779+
8780+
if flow.expires_at <= Instant::now() {
8781+
drop(flow);
8782+
COPILOT_FLOWS.remove(&poll_id);
8783+
return (
8784+
StatusCode::OK,
8785+
Json(serde_json::json!({"status": "expired"})),
8786+
);
8787+
}
8788+
8789+
let device_code = flow.device_code.clone();
8790+
drop(flow);
8791+
8792+
match openfang_runtime::copilot_oauth::poll_device_flow(&device_code).await {
8793+
openfang_runtime::copilot_oauth::DeviceFlowStatus::Pending => (
8794+
StatusCode::OK,
8795+
Json(serde_json::json!({"status": "pending"})),
8796+
),
8797+
openfang_runtime::copilot_oauth::DeviceFlowStatus::Complete { access_token } => {
8798+
// Save to secrets.env
8799+
let secrets_path = state.kernel.config.home_dir.join("secrets.env");
8800+
if let Err(e) = write_secret_env(&secrets_path, "GITHUB_TOKEN", &access_token) {
8801+
return (
8802+
StatusCode::INTERNAL_SERVER_ERROR,
8803+
Json(serde_json::json!({"status": "error", "error": format!("Failed to save token: {e}")})),
8804+
);
8805+
}
8806+
8807+
// Set in current process
8808+
std::env::set_var("GITHUB_TOKEN", access_token.as_str());
8809+
8810+
// Refresh auth detection
8811+
state
8812+
.kernel
8813+
.model_catalog
8814+
.write()
8815+
.unwrap_or_else(|e| e.into_inner())
8816+
.detect_auth();
8817+
8818+
// Clean up flow state
8819+
COPILOT_FLOWS.remove(&poll_id);
8820+
8821+
(
8822+
StatusCode::OK,
8823+
Json(serde_json::json!({"status": "complete"})),
8824+
)
8825+
}
8826+
openfang_runtime::copilot_oauth::DeviceFlowStatus::SlowDown { new_interval } => {
8827+
// Update interval
8828+
if let Some(mut f) = COPILOT_FLOWS.get_mut(&poll_id) {
8829+
f.interval = new_interval;
8830+
}
8831+
(
8832+
StatusCode::OK,
8833+
Json(serde_json::json!({"status": "pending", "interval": new_interval})),
8834+
)
8835+
}
8836+
openfang_runtime::copilot_oauth::DeviceFlowStatus::Expired => {
8837+
COPILOT_FLOWS.remove(&poll_id);
8838+
(
8839+
StatusCode::OK,
8840+
Json(serde_json::json!({"status": "expired"})),
8841+
)
8842+
}
8843+
openfang_runtime::copilot_oauth::DeviceFlowStatus::AccessDenied => {
8844+
COPILOT_FLOWS.remove(&poll_id);
8845+
(
8846+
StatusCode::OK,
8847+
Json(serde_json::json!({"status": "denied"})),
8848+
)
8849+
}
8850+
openfang_runtime::copilot_oauth::DeviceFlowStatus::Error(e) => (
8851+
StatusCode::OK,
8852+
Json(serde_json::json!({"status": "error", "error": e})),
8853+
),
8854+
}
8855+
}

crates/openfang-api/src/server.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,15 @@ pub async fn build_router(
452452
)
453453
.route("/api/models/{*id}", axum::routing::get(routes::get_model))
454454
.route("/api/providers", axum::routing::get(routes::list_providers))
455+
// Copilot OAuth (must be before parametric {name} routes)
456+
.route(
457+
"/api/providers/github-copilot/oauth/start",
458+
axum::routing::post(routes::copilot_oauth_start),
459+
)
460+
.route(
461+
"/api/providers/github-copilot/oauth/poll/{poll_id}",
462+
axum::routing::get(routes::copilot_oauth_poll),
463+
)
455464
.route(
456465
"/api/providers/{name}/key",
457466
axum::routing::post(routes::set_provider_key).delete(routes::delete_provider_key),

crates/openfang-api/static/index_body.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2884,6 +2884,21 @@ <h4>LLM Providers</h4>
28842884
<template x-if="p.auth_status !== 'configured' && p.api_key_env">
28852885
<div class="text-xs text-dim mt-2">Or set <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px" x-text="p.api_key_env"></code> in your environment and restart</div>
28862886
</template>
2887+
<!-- Copilot OAuth button -->
2888+
<template x-if="p.id === 'github-copilot' && p.auth_status !== 'configured'">
2889+
<div class="mt-2">
2890+
<button class="btn btn-primary btn-sm" @click="startCopilotOAuth()" :disabled="copilotOAuth.polling" x-show="!copilotOAuth.userCode">Login with GitHub</button>
2891+
<div x-show="copilotOAuth.userCode" class="mt-2">
2892+
<div class="text-sm">Visit <a :href="copilotOAuth.verificationUri" target="_blank" x-text="copilotOAuth.verificationUri" style="color:var(--accent-light)"></a> and enter:</div>
2893+
<div style="font-size:24px;font-weight:bold;letter-spacing:4px;margin:8px 0;color:var(--accent-light)" x-text="copilotOAuth.userCode"></div>
2894+
<div class="text-xs text-dim"><span class="spinner" style="width:10px;height:10px;border-width:2px;display:inline-block;vertical-align:middle"></span> Waiting for authorization...</div>
2895+
</div>
2896+
</div>
2897+
</template>
2898+
<!-- Claude Code install hint -->
2899+
<template x-if="p.id === 'claude-code' && p.auth_status !== 'configured'">
2900+
<div class="mt-2 text-xs text-dim">Install: <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px">npm install -g @anthropic-ai/claude-code</code></div>
2901+
</template>
28872902
<!-- Actions for configured providers -->
28882903
<template x-if="p.auth_status === 'configured'">
28892904
<div class="flex gap-2 mt-2">

crates/openfang-api/static/js/pages/settings.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ function settingsPage() {
1919
providerUrlSaving: {},
2020
providerTesting: {},
2121
providerTestResults: {},
22+
copilotOAuth: { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 },
2223
loading: true,
2324
loadError: '',
2425

@@ -368,6 +369,54 @@ function settingsPage() {
368369
}
369370
},
370371

372+
async startCopilotOAuth() {
373+
this.copilotOAuth.polling = true;
374+
this.copilotOAuth.userCode = '';
375+
try {
376+
var resp = await OpenFangAPI.post('/api/providers/github-copilot/oauth/start', {});
377+
this.copilotOAuth.userCode = resp.user_code;
378+
this.copilotOAuth.verificationUri = resp.verification_uri;
379+
this.copilotOAuth.pollId = resp.poll_id;
380+
this.copilotOAuth.interval = resp.interval || 5;
381+
window.open(resp.verification_uri, '_blank');
382+
this.pollCopilotOAuth();
383+
} catch(e) {
384+
OpenFangToast.error('Failed to start Copilot login: ' + e.message);
385+
this.copilotOAuth.polling = false;
386+
}
387+
},
388+
389+
pollCopilotOAuth() {
390+
var self = this;
391+
setTimeout(async function() {
392+
if (!self.copilotOAuth.pollId) return;
393+
try {
394+
var resp = await OpenFangAPI.get('/api/providers/github-copilot/oauth/poll/' + self.copilotOAuth.pollId);
395+
if (resp.status === 'complete') {
396+
OpenFangToast.success('GitHub Copilot authenticated successfully!');
397+
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
398+
await self.loadProviders();
399+
await self.loadModels();
400+
} else if (resp.status === 'pending') {
401+
if (resp.interval) self.copilotOAuth.interval = resp.interval;
402+
self.pollCopilotOAuth();
403+
} else if (resp.status === 'expired') {
404+
OpenFangToast.error('Device code expired. Please try again.');
405+
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
406+
} else if (resp.status === 'denied') {
407+
OpenFangToast.error('Access denied by user.');
408+
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
409+
} else {
410+
OpenFangToast.error('OAuth error: ' + (resp.error || resp.status));
411+
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
412+
}
413+
} catch(e) {
414+
OpenFangToast.error('Poll error: ' + e.message);
415+
self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 };
416+
}
417+
}, self.copilotOAuth.interval * 1000);
418+
},
419+
371420
async testProvider(provider) {
372421
this.providerTesting[provider.id] = true;
373422
this.providerTestResults[provider.id] = null;

0 commit comments

Comments
 (0)