diff --git a/skills b/skills
index 87d75f69b..26341b529 160000
--- a/skills
+++ b/skills
@@ -1 +1 @@
-Subproject commit 87d75f69b7e600ab9fc5cee9d861546e01c89fac
+Subproject commit 26341b529751b46e9be28bd7c46b1a978b5bbc7d
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index bbd2195c1..58aa58f29 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -29,8 +29,9 @@ tauri-plugin-os = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
-# HTTP client (rustls for cross-platform TLS support including Android)
-reqwest = { version = "0.12", default-features = false, features = ["json", "blocking", "rustls-tls", "stream"] }
+# HTTP client: rustls for cross-platform TLS (Android), native-tls for desktop skill HTTP
+# (some APIs/CDNs use TLS configs that rustls can't negotiate — native-tls uses OS TLS stack)
+reqwest = { version = "0.12", default-features = false, features = ["json", "blocking", "rustls-tls", "native-tls", "stream"] }
# Async runtime
tokio = { version = "1", features = ["full", "sync"] }
diff --git a/src-tauri/src/runtime/bridge/net.rs b/src-tauri/src/runtime/bridge/net.rs
index 532cf9872..b938aa68c 100644
--- a/src-tauri/src/runtime/bridge/net.rs
+++ b/src-tauri/src/runtime/bridge/net.rs
@@ -51,6 +51,7 @@ fn do_fetch(url: &str, options_json: &str) -> Result {
let timeout_secs = options.timeout.unwrap_or(30);
let client = reqwest::blocking::Client::builder()
+ .use_native_tls()
.timeout(std::time::Duration::from_secs(timeout_secs))
.build()
.map_err(|e| format!("Failed to create HTTP client: {e}"))?;
@@ -74,7 +75,17 @@ fn do_fetch(url: &str, options_json: &str) -> Result {
req = req.body(body);
}
- let resp = req.send().map_err(|e| format!("HTTP request failed: {e}"))?;
+ let resp = req.send().map_err(|e| {
+ // Walk the full error chain so the caller sees the root cause
+ // (TLS, DNS, timeout, etc.) not just "error sending request for url".
+ let mut msg = format!("HTTP request failed: {e}");
+ let mut source = std::error::Error::source(&e);
+ while let Some(cause) = source {
+ msg.push_str(&format!(" | caused by: {cause}"));
+ source = std::error::Error::source(cause);
+ }
+ msg
+ })?;
let status = resp.status().as_u16();
let headers: HashMap = resp
diff --git a/src-tauri/src/runtime/qjs_skill_instance.rs b/src-tauri/src/runtime/qjs_skill_instance.rs
index 47aa444df..bbc6e2973 100644
--- a/src-tauri/src/runtime/qjs_skill_instance.rs
+++ b/src-tauri/src/runtime/qjs_skill_instance.rs
@@ -227,6 +227,8 @@ impl QjsSkillInstance {
return;
}
+ restore_oauth_credential(&ctx, &config.skill_id).await;
+
// Call init() lifecycle
if let Err(e) = call_lifecycle(&ctx, "init").await {
let mut s = state.write();
@@ -389,6 +391,8 @@ async fn handle_message(
) -> bool {
match msg {
SkillMessage::CallTool { tool_name, arguments, reply } => {
+ // Lazy-load persisted OAuth credential before calling the tool
+ restore_oauth_credential(ctx, skill_id).await;
let result = handle_tool_call(ctx, &tool_name, arguments).await;
let _ = reply.send(result);
}
@@ -400,9 +404,23 @@ async fn handle_message(
}
SkillMessage::Stop { reply } => {
let _ = call_lifecycle(ctx, "stop").await;
+
+ // Clear OAuth credential from memory and mark as disconnected in store
+ let clear_code = r#"(function() {
+ if (typeof globalThis.oauth !== 'undefined' && globalThis.oauth.__setCredential) {
+ globalThis.oauth.__setCredential(null);
+ }
+ if (typeof globalThis.state !== 'undefined' && globalThis.state.set) {
+ globalThis.state.set('__oauth_credential', '');
+ }
+ })()"#;
+ ctx.with(|js_ctx| {
+ let _ = js_ctx.eval::(clear_code.as_bytes());
+ }).await;
state.write().status = SkillStatus::Stopped;
- log::info!("[skill:{}] Stopped", skill_id);
+ log::info!("[skill:{}] Stopped (OAuth credential cleared)", skill_id);
let _ = reply.send(());
+
return true; // Signal to stop
}
SkillMessage::SetupStart { reply } => {
@@ -444,23 +462,40 @@ async fn handle_message(
SkillMessage::Rpc { method, params, reply } => {
let result = match method.as_str() {
"oauth/complete" => {
- // Set credential on the oauth bridge, then call onOAuthComplete
- let set_cred_code = format!(
- "if (typeof globalThis.oauth !== 'undefined' && globalThis.oauth.__setCredential) {{ globalThis.oauth.__setCredential({}); }}",
- serde_json::to_string(¶ms).unwrap_or_else(|_| "null".to_string())
+ // Set credential on the oauth bridge + persist to store
+ let cred_json = serde_json::to_string(¶ms).unwrap_or_else(|_| "null".to_string());
+ let code = format!(
+ r#"(function() {{
+ if (typeof globalThis.oauth !== 'undefined' && globalThis.oauth.__setCredential) {{
+ globalThis.oauth.__setCredential({cred});
+ }}
+ if (typeof globalThis.state !== 'undefined' && globalThis.state.set) {{
+ globalThis.state.set('__oauth_credential', {cred});
+ }}
+ }})()"#,
+ cred = cred_json
);
ctx.with(|js_ctx| {
- let _ = js_ctx.eval::(set_cred_code.as_bytes());
+ let _ = js_ctx.eval::(code.as_bytes());
}).await;
+ log::info!("[skill:{}] OAuth credential set and persisted to store", skill_id);
let params_str = serde_json::to_string(¶ms).unwrap_or_else(|_| "{}".to_string());
handle_js_call(ctx, "onOAuthComplete", ¶ms_str).await
}
"oauth/revoked" => {
- // Clear credential on the oauth bridge, then call onOAuthRevoked
- let clear_code = "if (typeof globalThis.oauth !== 'undefined' && globalThis.oauth.__setCredential) { globalThis.oauth.__setCredential(null); }";
+ // Clear credential: set to empty string so it's clearly "disconnected"
+ let clear_code = r#"(function() {
+ if (typeof globalThis.oauth !== 'undefined' && globalThis.oauth.__setCredential) {
+ globalThis.oauth.__setCredential(null);
+ }
+ if (typeof globalThis.state !== 'undefined' && globalThis.state.set) {
+ globalThis.state.set('__oauth_credential', '');
+ }
+ })()"#;
ctx.with(|js_ctx| {
let _ = js_ctx.eval::(clear_code.as_bytes());
}).await;
+ log::info!("[skill:{}] OAuth credential cleared from store", skill_id);
let params_str = serde_json::to_string(¶ms).unwrap_or_else(|_| "{}".to_string());
handle_js_void_call(ctx, "onOAuthRevoked", ¶ms_str).await
.map(|()| serde_json::json!({ "ok": true }))
@@ -749,3 +784,26 @@ async fn handle_js_void_call(
.map(|_| ())
}).await
}
+
+/// Load a persisted OAuth credential from the skill's store and inject it
+/// into the JS context so tools have access to the credential.
+/// An empty string means "disconnected" — only non-empty values are restored.
+async fn restore_oauth_credential(ctx: &rquickjs::AsyncContext, skill_id: &str) {
+ let code = r#"(function() {
+ if (typeof globalThis.state === 'undefined' || typeof globalThis.oauth === 'undefined') return false;
+ var cred = globalThis.state.get('__oauth_credential');
+ if (cred && cred !== '' && globalThis.oauth.__setCredential) {
+ globalThis.oauth.__setCredential(cred);
+ return true;
+ }
+ return false;
+ })()"#;
+
+ let restored = ctx.with(|js_ctx| {
+ js_ctx.eval::(code.as_bytes()).unwrap_or(false)
+ }).await;
+
+ if restored {
+ log::info!("[skill:{}] Restored OAuth credential from store", skill_id);
+ }
+}
diff --git a/src-tauri/src/services/quickjs-libs/bootstrap.js b/src-tauri/src/services/quickjs-libs/bootstrap.js
index c79999696..fffac251c 100644
--- a/src-tauri/src/services/quickjs-libs/bootstrap.js
+++ b/src-tauri/src/services/quickjs-libs/bootstrap.js
@@ -807,12 +807,12 @@ globalThis.data = {
// OAuth Bridge API (credential management and authenticated proxy)
// ============================================================================
(function () {
- var __oauthCredential = null;
+ globalThis.__oauthCredential = null;
globalThis.oauth = {
/** Get the current OAuth credential, or null if not connected. */
getCredential: function () {
- return __oauthCredential;
+ return globalThis.__oauthCredential;
},
/**
@@ -820,7 +820,7 @@ globalThis.data = {
* Path is relative to manifest's apiBaseUrl.
*/
fetch: function (path, options) {
- if (!__oauthCredential) {
+ if (!globalThis.__oauthCredential) {
return {
status: 401,
headers: {},
@@ -828,9 +828,9 @@ globalThis.data = {
};
}
var backendUrl = __platform.env('BACKEND_URL') || 'https://api.alphahuman.xyz';
- var jwtToken = __platform.env('JWT_TOKEN') || '';
+ var jwtToken = __ops.get_session_token() || '';
var cleanPath = path.charAt(0) === '/' ? path.slice(1) : path;
- var proxyUrl = backendUrl + '/proxy/by-id/' + __oauthCredential.credentialId + '/' + cleanPath;
+ var proxyUrl = backendUrl + '/proxy/by-id/' + globalThis.__oauthCredential.credentialId + '/' + cleanPath;
var method = (options && options.method) || 'GET';
var headers = { 'Content-Type': 'application/json' };
if (jwtToken) {
@@ -856,7 +856,7 @@ globalThis.data = {
if (__oauthCredential) {
try {
var backendUrl = __platform.env('BACKEND_URL') || 'https://api.alphahuman.xyz';
- var jwtToken = __platform.env('JWT_TOKEN') || '';
+ var jwtToken = __ops.get_session_token() || '';
var revokeOpts = JSON.stringify({
method: 'DELETE',
headers: {
@@ -993,7 +993,7 @@ globalThis.model = {
*/
generate: function (prompt, options) {
var backendUrl = __platform.env('BACKEND_URL') || 'https://api.alphahuman.xyz';
- var jwtToken = __platform.env('JWT_TOKEN') || '';
+ var jwtToken = __ops.get_session_token() || '';
var body = { prompt: prompt };
if (options && options.maxTokens) body.maxTokens = options.maxTokens;
if (options && options.temperature) body.temperature = options.temperature;
@@ -1023,7 +1023,7 @@ globalThis.model = {
*/
summarize: function (text, options) {
var backendUrl = __platform.env('BACKEND_URL') || 'https://api.alphahuman.xyz';
- var jwtToken = __platform.env('JWT_TOKEN') || '';
+ var jwtToken = __ops.get_session_token() || '';
var body = { text: text };
if (options && options.maxTokens) body.maxTokens = options.maxTokens;
var result = __net.fetch(backendUrl + '/api/ai/summarize', JSON.stringify({
diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_core.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_core.rs
index b9c128e78..9d6c17b7f 100644
--- a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_core.rs
+++ b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_core.rs
@@ -81,6 +81,12 @@ pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, timer_state: Arc String {
+ let token = crate::commands::auth::SESSION_SERVICE.get_token().unwrap_or_default();
+ log::info!("[js] get_session_token: {}", token);
+ return token;
+ }))?;
+
// ========================================================================
// Timers (2)
// ========================================================================
diff --git a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_net.rs b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_net.rs
index 33c99f386..14863d5f2 100644
--- a/src-tauri/src/services/quickjs-libs/qjs_ops/ops_net.rs
+++ b/src-tauri/src/services/quickjs-libs/qjs_ops/ops_net.rs
@@ -21,7 +21,10 @@ pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, ws_state: Arc client.get(&url),
"POST" => client.post(&url),
@@ -42,7 +45,15 @@ pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, ws_state: Arc Result<(), String> {
}
pub fn js_err(msg: String) -> rquickjs::Error {
- rquickjs::Error::new_from_js_message("ops", "Error", msg)
+ rquickjs::Error::new_from_js_message("skill", "RuntimeError", msg)
}
diff --git a/src/components/ConnectionIndicator.tsx b/src/components/ConnectionIndicator.tsx
index 1871ea55a..7d930154b 100644
--- a/src/components/ConnectionIndicator.tsx
+++ b/src/components/ConnectionIndicator.tsx
@@ -9,7 +9,7 @@ interface ConnectionIndicatorProps {
const ConnectionIndicator = ({
status: overrideStatus,
- description = 'Your browser is now connected to the AlphaHuman AI. Keep the app running to keep the connection alive. You can message your assistant with the button below.',
+ description = 'Your device is now connected to the AlphaHuman AI. Keep the app running to keep the connection alive. You can message your assistant with the button below.',
className = '',
}: ConnectionIndicatorProps) => {
// Use socket store status, but allow override via props
diff --git a/src/lib/skills/manager.ts b/src/lib/skills/manager.ts
index 925670681..23938cb43 100644
--- a/src/lib/skills/manager.ts
+++ b/src/lib/skills/manager.ts
@@ -22,6 +22,7 @@ import {
setSkillStatus,
setSkillError,
setSkillSetupComplete,
+ setSkillOAuthCredential,
setSkillTools,
setSkillState,
} from "../../store/skillsSlice";
@@ -98,6 +99,15 @@ class SkillManager {
if (setupRequired) {
store.dispatch(setSkillStatus({ skillId, status: "setup_required" }));
} else {
+ // Re-inject persisted OAuth credential if available
+ const oauthCred = skillState?.oauthCredential;
+ if (oauthCred) {
+ try {
+ await runtime.oauthComplete(oauthCred);
+ } catch (err) {
+ console.warn(`[SkillManager] Failed to restore OAuth credential for ${skillId}:`, err);
+ }
+ }
// Skill is ready — list tools
await this.activateSkill(skillId);
}
@@ -251,12 +261,16 @@ class SkillManager {
const manifest = store.getState().skills.skills[skillId]?.manifest;
- await runtime.oauthComplete({
+ const credential = {
credentialId: integrationId,
provider: provider ?? manifest?.setup?.oauth?.provider ?? "unknown",
grantedScopes: manifest?.setup?.oauth?.scopes ?? [],
- });
+ };
+ await runtime.oauthComplete(credential);
+
+ // Persist credential so it survives app restarts
+ store.dispatch(setSkillOAuthCredential({ skillId, credential }));
// Mark setup as complete and activate
store.dispatch(setSkillSetupComplete({ skillId, complete: true }));
await this.activateSkill(skillId);
@@ -298,6 +312,7 @@ class SkillManager {
async disconnectSkill(skillId: string): Promise {
await this.stopSkill(skillId);
store.dispatch(setSkillSetupComplete({ skillId, complete: false }));
+ store.dispatch(setSkillOAuthCredential({ skillId, credential: undefined }));
store.dispatch(setSkillState({ skillId, state: {} }));
}
diff --git a/src/lib/skills/types.ts b/src/lib/skills/types.ts
index 5ae8f418b..16f6ab73d 100644
--- a/src/lib/skills/types.ts
+++ b/src/lib/skills/types.ts
@@ -182,11 +182,19 @@ export interface SkillHostConnectionState {
// Redux skill state shape
// ---------------------------------------------------------------------------
+export interface OAuthCredential {
+ credentialId: string;
+ provider: string;
+ grantedScopes?: string[];
+}
+
export interface SkillState {
manifest: SkillManifest;
status: SkillStatus;
error?: string;
setupComplete: boolean;
tools: SkillToolDefinition[];
+ /** Persisted OAuth credential so it survives app restarts. */
+ oauthCredential?: OAuthCredential;
}
diff --git a/src/pages/onboarding/steps/GetStartedStep.tsx b/src/pages/onboarding/steps/GetStartedStep.tsx
index 538f37571..58ca7374d 100644
--- a/src/pages/onboarding/steps/GetStartedStep.tsx
+++ b/src/pages/onboarding/steps/GetStartedStep.tsx
@@ -1,7 +1,5 @@
import { useState } from 'react';
-import ConnectionIndicator from '../../../components/ConnectionIndicator';
-
interface GetStartedStepProps {
onComplete: () => void | Promise;
}
@@ -32,8 +30,6 @@ const GetStartedStep = ({ onComplete }: GetStartedStepProps) => {
-
-
{error && {error}
}