From ff1dc905431c3437a9ddc0c1993acd936f6a7b9d Mon Sep 17 00:00:00 2001 From: Russell Brenner Date: Tue, 12 May 2026 15:24:27 +1000 Subject: [PATCH 1/3] feat: add bearerToken setting for explicit Authorization header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a dedicated `bearerToken` field to LLM provider settings that, when set, is sent as the `Authorization: Bearer` header. This takes precedence over `apiKey` when both are present. When `bearerToken` is blank, the existing `apiKey` behavior is unchanged — this is fully backwards compatible. Validation relaxed: either `apiKey` or `bearerToken` satisfies the auth requirement (both admin and onscreen agent paths). Co-Authored-By: Mira AI-Generated: true --- app/L0/_all/mod/_core/admin/views/agent/api.js | 15 ++++++++++----- app/L0/_all/mod/_core/admin/views/agent/config.js | 1 + app/L0/_all/mod/_core/onscreen_agent/api.js | 15 ++++++++++----- app/L0/_all/mod/_core/onscreen_agent/config.js | 1 + 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/L0/_all/mod/_core/admin/views/agent/api.js b/app/L0/_all/mod/_core/admin/views/agent/api.js index e36a2f31..1bec08ab 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/api.js +++ b/app/L0/_all/mod/_core/admin/views/agent/api.js @@ -6,12 +6,14 @@ import { mergeConsecutiveChatMessages } from "/mod/_core/framework/js/chat-messa import * as proxyUrl from "/mod/_core/framework/js/proxy-url.js"; import { getHuggingFaceManager } from "/mod/_core/huggingface/manager.js"; -function createHeaders(apiKey) { +function createHeaders(apiKey, bearerToken) { const headers = { "Content-Type": "application/json" }; - if (apiKey) { + if (bearerToken) { + headers.Authorization = `Bearer ${bearerToken}`; + } else if (apiKey) { headers.Authorization = `Bearer ${apiKey}`; } @@ -442,7 +444,10 @@ export const prepareAdminAgentApiRequest = globalThis.space.extend( return { apiEndpoint, - headers: createHeaders(String(effectiveSettings?.apiKey || "").trim()), + headers: createHeaders( + String(effectiveSettings?.apiKey || "").trim(), + String(effectiveSettings?.bearerToken || "").trim() + ), messages: Array.isArray(messages) ? messages : [], method: "POST", promptContext: normalizedPromptContext, @@ -461,8 +466,8 @@ async function streamAdminAgentApiCompletion({ promptContext, settings, systemPr throw new Error("Set an API endpoint before sending a message."); } - if (!settings.apiKey.trim()) { - throw new Error("Set an API key before sending a message."); + if (!settings.apiKey.trim() && !(settings.bearerToken || "").trim()) { + throw new Error("Set an API key or authorization bearer token before sending a message."); } if (!settings.model.trim()) { diff --git a/app/L0/_all/mod/_core/admin/views/agent/config.js b/app/L0/_all/mod/_core/admin/views/agent/config.js index 2a00ef4a..35d4c655 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/config.js +++ b/app/L0/_all/mod/_core/admin/views/agent/config.js @@ -15,6 +15,7 @@ export const ADMIN_CHAT_LOCAL_PROVIDER = { export const DEFAULT_ADMIN_CHAT_SETTINGS = { apiEndpoint: "https://openrouter.ai/api/v1/chat/completions", apiKey: "", + bearerToken: "", huggingfaceDtype: "q4", huggingfaceModel: "", localProvider: ADMIN_CHAT_LOCAL_PROVIDER.HUGGINGFACE, diff --git a/app/L0/_all/mod/_core/onscreen_agent/api.js b/app/L0/_all/mod/_core/onscreen_agent/api.js index 510d23a6..e9367cf0 100644 --- a/app/L0/_all/mod/_core/onscreen_agent/api.js +++ b/app/L0/_all/mod/_core/onscreen_agent/api.js @@ -211,12 +211,14 @@ function normalizeCompletionMessagesForLocal(messages) { .filter(Boolean); } -function createApiRequestHeaders(apiKey) { +function createApiRequestHeaders(apiKey, bearerToken) { const headers = { "Content-Type": "application/json" }; - if (apiKey) { + if (bearerToken) { + headers.Authorization = `Bearer ${bearerToken}`; + } else if (apiKey) { headers.Authorization = `Bearer ${apiKey}`; } @@ -303,8 +305,8 @@ export class OnscreenAgentApiLlmClient extends OnscreenAgentLlmClient { throw new Error("Set an API endpoint before sending a message."); } - if (!settings.apiKey.trim()) { - throw new Error("Set an API key before sending a message."); + if (!settings.apiKey.trim() && !(settings.bearerToken || "").trim()) { + throw new Error("Set an API key or authorization bearer token before sending a message."); } if (!settings.model.trim()) { @@ -421,7 +423,10 @@ export const prepareOnscreenAgentApiRequest = globalThis.space.extend( return { apiEndpoint, - headers: createApiRequestHeaders(String(effectiveSettings?.apiKey || "").trim()), + headers: createApiRequestHeaders( + String(effectiveSettings?.apiKey || "").trim(), + String(effectiveSettings?.bearerToken || "").trim() + ), messages: Array.isArray(effectivePreparedRequest?.messages) ? effectivePreparedRequest.messages : [], method: "POST", preparedRequest: effectivePreparedRequest, diff --git a/app/L0/_all/mod/_core/onscreen_agent/config.js b/app/L0/_all/mod/_core/onscreen_agent/config.js index bcca393a..9d5e92e8 100644 --- a/app/L0/_all/mod/_core/onscreen_agent/config.js +++ b/app/L0/_all/mod/_core/onscreen_agent/config.js @@ -21,6 +21,7 @@ export const ONSCREEN_AGENT_HIDDEN_EDGE = Object.freeze({ export const DEFAULT_ONSCREEN_AGENT_SETTINGS = { apiEndpoint: "https://openrouter.ai/api/v1/chat/completions", apiKey: "", + bearerToken: "", huggingfaceDtype: "q4", huggingfaceModel: "", localProvider: ONSCREEN_AGENT_LOCAL_PROVIDER.HUGGINGFACE, From f4551f9881bf30b9e0ca23fdcca3a9b1666fbeeb Mon Sep 17 00:00:00 2001 From: Russell Brenner Date: Tue, 12 May 2026 15:31:06 +1000 Subject: [PATCH 2/3] feat: add bearerToken UI field and encrypted storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Authorization Bearer Token input to both admin and onscreen agent settings panels. The field sits below API Key in the provider settings form and is encrypted at rest via userCrypto, same as apiKey. Storage layer handles bearerToken through the full lifecycle: load from YAML config → decrypt → settings object, and settings object → encrypt → save to YAML config. Co-Authored-By: Mira AI-Generated: true --- .../_all/mod/_core/admin/views/agent/panel.html | 7 +++++++ .../_all/mod/_core/admin/views/agent/storage.js | 16 +++++++++++++++- app/L0/_all/mod/_core/onscreen_agent/panel.html | 7 +++++++ app/L0/_all/mod/_core/onscreen_agent/storage.js | 16 +++++++++++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/app/L0/_all/mod/_core/admin/views/agent/panel.html b/app/L0/_all/mod/_core/admin/views/agent/panel.html index bf67faaa..e5cb464f 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/panel.html +++ b/app/L0/_all/mod/_core/admin/views/agent/panel.html @@ -210,6 +210,13 @@

Provider and model configuration

API Key +
diff --git a/app/L0/_all/mod/_core/admin/views/agent/storage.js b/app/L0/_all/mod/_core/admin/views/agent/storage.js index 2dbeb99d..ebab0493 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/storage.js +++ b/app/L0/_all/mod/_core/admin/views/agent/storage.js @@ -138,11 +138,16 @@ async function normalizeStoredConfig(runtime, parsedConfig) { runtime, storedConfig.api_key || storedConfig.apiKey || config.DEFAULT_ADMIN_CHAT_SETTINGS.apiKey || "" ); + const storedBearerToken = await decodeStoredApiKey( + runtime, + storedConfig.bearer_token || storedConfig.bearerToken || config.DEFAULT_ADMIN_CHAT_SETTINGS.bearerToken || "" + ); return { settings: { apiEndpoint: String(storedConfig.api_endpoint || storedConfig.apiEndpoint || config.DEFAULT_ADMIN_CHAT_SETTINGS.apiEndpoint || "").trim(), apiKey: storedApiKey.value, + bearerToken: storedBearerToken.value, huggingfaceDtype: String( storedConfig.huggingface_dtype || storedConfig.huggingfaceDtype || config.DEFAULT_ADMIN_CHAT_SETTINGS.huggingfaceDtype || "" ).trim(), @@ -156,7 +161,9 @@ async function normalizeStoredConfig(runtime, parsedConfig) { promptBudgetRatios: normalizeStoredPromptBudgetRatios(storedConfig), provider, storedApiKeyLocked: storedApiKey.locked, - storedApiKeyValue: storedApiKey.storedValue + storedApiKeyValue: storedApiKey.storedValue, + storedBearerTokenLocked: storedBearerToken.locked, + storedBearerTokenValue: storedBearerToken.storedValue }, systemPrompt: String( storedConfig.custom_system_prompt || @@ -173,6 +180,11 @@ async function buildStoredConfigPayload(runtime, { settings, systemPrompt }) { const payload = { api_endpoint: String(settings?.apiEndpoint || config.DEFAULT_ADMIN_CHAT_SETTINGS.apiEndpoint || "").trim(), api_key: await encodeStoredApiKey(runtime, settings), + bearer_token: await encodeStoredApiKey(runtime, { + apiKey: settings?.bearerToken, + storedApiKeyValue: settings?.storedBearerTokenValue, + storedApiKeyLocked: settings?.storedBearerTokenLocked + }), huggingface_dtype: String(settings?.huggingfaceDtype || config.DEFAULT_ADMIN_CHAT_SETTINGS.huggingfaceDtype || "").trim(), huggingface_model: String(settings?.huggingfaceModel || config.DEFAULT_ADMIN_CHAT_SETTINGS.huggingfaceModel || "").trim(), local_provider: config.normalizeAdminChatLocalProvider(settings?.localProvider), @@ -220,6 +232,8 @@ export async function saveAdminChatConfig(nextConfig) { if (nextConfig?.settings && typeof nextConfig.settings === "object") { nextConfig.settings.storedApiKeyLocked = false; nextConfig.settings.storedApiKeyValue = String(payload.api_key || "").trim(); + nextConfig.settings.storedBearerTokenLocked = false; + nextConfig.settings.storedBearerTokenValue = String(payload.bearer_token || "").trim(); } } catch (error) { throw new Error(`Unable to save admin chat config: ${error.message}`); diff --git a/app/L0/_all/mod/_core/onscreen_agent/panel.html b/app/L0/_all/mod/_core/onscreen_agent/panel.html index a4510bdc..81f7cbe5 100644 --- a/app/L0/_all/mod/_core/onscreen_agent/panel.html +++ b/app/L0/_all/mod/_core/onscreen_agent/panel.html @@ -360,6 +360,13 @@

Model, credentials, params, and instructions

API Key +
diff --git a/app/L0/_all/mod/_core/onscreen_agent/storage.js b/app/L0/_all/mod/_core/onscreen_agent/storage.js index 6a9b22f2..185431b5 100644 --- a/app/L0/_all/mod/_core/onscreen_agent/storage.js +++ b/app/L0/_all/mod/_core/onscreen_agent/storage.js @@ -202,6 +202,10 @@ async function normalizeStoredConfig(runtime, parsedConfig) { runtime, storedConfig.api_key || storedConfig.apiKey || config.DEFAULT_ONSCREEN_AGENT_SETTINGS.apiKey || "" ); + const storedBearerToken = await decodeStoredApiKey( + runtime, + storedConfig.bearer_token || storedConfig.bearerToken || config.DEFAULT_ONSCREEN_AGENT_SETTINGS.bearerToken || "" + ); const legacyDisplayMode = storedConfig.collapsed === true ? DISPLAY_MODE_COMPACT @@ -213,6 +217,7 @@ async function normalizeStoredConfig(runtime, parsedConfig) { settings: { apiEndpoint: String(storedConfig.api_endpoint || storedConfig.apiEndpoint || config.DEFAULT_ONSCREEN_AGENT_SETTINGS.apiEndpoint || "").trim(), apiKey: storedApiKey.value, + bearerToken: storedBearerToken.value, huggingfaceDtype: String( storedConfig.huggingface_dtype || storedConfig.huggingfaceDtype || @@ -232,7 +237,9 @@ async function normalizeStoredConfig(runtime, parsedConfig) { promptBudgetRatios: normalizeStoredPromptBudgetRatios(storedConfig), provider, storedApiKeyLocked: storedApiKey.locked, - storedApiKeyValue: storedApiKey.storedValue + storedApiKeyValue: storedApiKey.storedValue, + storedBearerTokenLocked: storedBearerToken.locked, + storedBearerTokenValue: storedBearerToken.storedValue }, systemPrompt: String( storedConfig.custom_system_prompt || @@ -278,6 +285,11 @@ async function buildStoredConfigPayload(runtime, { settings, systemPrompt }) { const payload = { api_endpoint: String(settings?.apiEndpoint || config.DEFAULT_ONSCREEN_AGENT_SETTINGS.apiEndpoint || "").trim(), api_key: await encodeStoredApiKey(runtime, settings), + bearer_token: await encodeStoredApiKey(runtime, { + apiKey: settings?.bearerToken, + storedApiKeyValue: settings?.storedBearerTokenValue, + storedApiKeyLocked: settings?.storedBearerTokenLocked + }), huggingface_dtype: String(settings?.huggingfaceDtype || config.DEFAULT_ONSCREEN_AGENT_SETTINGS.huggingfaceDtype || "").trim(), huggingface_model: String(settings?.huggingfaceModel || config.DEFAULT_ONSCREEN_AGENT_SETTINGS.huggingfaceModel || "").trim(), local_provider: config.normalizeOnscreenAgentLocalProvider(settings?.localProvider), @@ -446,6 +458,8 @@ export async function saveOnscreenAgentConfig(nextConfig) { if (nextConfig?.settings && typeof nextConfig.settings === "object") { nextConfig.settings.storedApiKeyLocked = false; nextConfig.settings.storedApiKeyValue = String(payload.api_key || "").trim(); + nextConfig.settings.storedBearerTokenLocked = false; + nextConfig.settings.storedBearerTokenValue = String(payload.bearer_token || "").trim(); } } catch (error) { throw new Error(`Unable to save onscreen agent config: ${error.message}`); From daa2914eb1a233ed11eca2cd0bea7e04cb5a6346 Mon Sep 17 00:00:00 2001 From: Russell Brenner Date: Tue, 12 May 2026 15:37:26 +1000 Subject: [PATCH 3/3] fix: include bearerToken in saveSettingsFromDialog bearerToken was bound to the settingsDraft via x-model but was not copied into this.settings when the form was saved, so the value was lost on submit. Also preserve storedBearerTokenLocked and storedBearerTokenValue through the save cycle. Co-Authored-By: Mira AI-Generated: true --- app/L0/_all/mod/_core/admin/views/agent/store.js | 5 ++++- app/L0/_all/mod/_core/onscreen_agent/store.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/L0/_all/mod/_core/admin/views/agent/store.js b/app/L0/_all/mod/_core/admin/views/agent/store.js index 4aacf7b4..1cefa25e 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/store.js +++ b/app/L0/_all/mod/_core/admin/views/agent/store.js @@ -1877,6 +1877,7 @@ const model = { this.settings = { apiEndpoint: (this.settingsDraft.apiEndpoint || "").trim(), apiKey: (this.settingsDraft.apiKey || "").trim(), + bearerToken: (this.settingsDraft.bearerToken || "").trim(), huggingfaceDtype: (this.settingsDraft.huggingfaceDtype || "").trim(), huggingfaceModel: normalizeHuggingFaceModelInput(this.settingsDraft.huggingfaceModel || ""), localProvider, @@ -1886,7 +1887,9 @@ const model = { promptBudgetRatios: clonePromptBudgetRatios(this.settingsDraft.promptBudgetRatios), provider, storedApiKeyLocked: this.settings.storedApiKeyLocked === true, - storedApiKeyValue: String(this.settings.storedApiKeyValue || "") + storedApiKeyValue: String(this.settings.storedApiKeyValue || ""), + storedBearerTokenLocked: this.settings.storedBearerTokenLocked === true, + storedBearerTokenValue: String(this.settings.storedBearerTokenValue || "") }; try { diff --git a/app/L0/_all/mod/_core/onscreen_agent/store.js b/app/L0/_all/mod/_core/onscreen_agent/store.js index ae4fba33..ac4bf281 100644 --- a/app/L0/_all/mod/_core/onscreen_agent/store.js +++ b/app/L0/_all/mod/_core/onscreen_agent/store.js @@ -4604,6 +4604,7 @@ const model = { this.settings = { apiEndpoint: (this.settingsDraft.apiEndpoint || "").trim(), apiKey: (this.settingsDraft.apiKey || "").trim(), + bearerToken: (this.settingsDraft.bearerToken || "").trim(), huggingfaceDtype: (this.settingsDraft.huggingfaceDtype || "").trim(), huggingfaceModel: normalizeHuggingFaceModelInput(this.settingsDraft.huggingfaceModel || ""), localProvider, @@ -4613,7 +4614,9 @@ const model = { promptBudgetRatios: clonePromptBudgetRatios(this.settingsDraft.promptBudgetRatios), provider, storedApiKeyLocked: this.settings.storedApiKeyLocked === true, - storedApiKeyValue: String(this.settings.storedApiKeyValue || "") + storedApiKeyValue: String(this.settings.storedApiKeyValue || ""), + storedBearerTokenLocked: this.settings.storedBearerTokenLocked === true, + storedBearerTokenValue: String(this.settings.storedBearerTokenValue || "") }; this.systemPrompt = draftPrompt; this.systemPromptDraft = draftPrompt;