tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit c47db865e2e826e5bbc7e46d1452c691910831d2
parent 99020e68b307489f5e68deaae9f12d66fa5b9d5b
Author: Nick Grato <ngrato@gmail.com>
Date:   Mon, 17 Nov 2025 23:05:51 +0000

Bug 1998270  - hook up mozilla account + token passing api key flow r=pdahiya,firefox-ai-ml-reviewers

For first iteration just adding token as an option to the openai pipeline and calling it through the smart assist feature.

Differential Revision: https://phabricator.services.mozilla.com/D271303

Diffstat:
Mbrowser/components/genai/SmartAssistEngine.sys.mjs | 21+++++++++++++++++++++
Mbrowser/components/genai/content/smart-assist.mjs | 31+++++++++++++++++++++++++++++++
Mbrowser/components/genai/tests/xpcshell/test_smart_assist_engine.js | 3+++
Mtoolkit/components/ml/content/backends/OpenAIPipeline.mjs | 15++++++++++-----
Mtoolkit/components/ml/tests/browser/browser_ml_openai.js | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/ml/tests/browser/head.js | 10+++++++++-
6 files changed, 175 insertions(+), 6 deletions(-)

diff --git a/browser/components/genai/SmartAssistEngine.sys.mjs b/browser/components/genai/SmartAssistEngine.sys.mjs @@ -11,6 +11,11 @@ ChromeUtils.defineESModuleGetters(lazy, { /* eslint-disable-next-line mozilla/reject-import-system-module-from-non-system */ import { createEngine } from "chrome://global/content/ml/EngineProcess.sys.mjs"; +import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs"; +import { + OAUTH_CLIENT_ID, + SCOPE_PROFILE, +} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; const toolsConfig = [ { @@ -72,6 +77,20 @@ export const SmartAssistEngine = { _createEngine: createEngine, + async _getFxAccountToken() { + try { + const fxAccounts = getFxAccountsSingleton(); + const token = await fxAccounts.getOAuthToken({ + scope: SCOPE_PROFILE, + client_id: OAUTH_CLIENT_ID, + }); + return token; + } catch (error) { + console.error("Error obtaining FxA token:", error); + throw error; + } + }, + /** * Creates an OpenAI engine instance configured with Smart Assists preferences. * @@ -107,6 +126,7 @@ export const SmartAssistEngine = { */ async *fetchWithHistory(messages) { const engineInstance = await this.createOpenAIEngine(); + const fxAccountToken = await this._getFxAccountToken(); // We'll mutate a local copy of the thread as we loop let convo = Array.isArray(messages) ? [...messages] : []; @@ -115,6 +135,7 @@ export const SmartAssistEngine = { const streamModelResponse = () => engineInstance.runWithGenerator({ streamOptions: { enabled: true }, + fxAccountToken, tool_choice: "auto", tools: toolsConfig, args: convo, diff --git a/browser/components/genai/content/smart-assist.mjs b/browser/components/genai/content/smart-assist.mjs @@ -14,6 +14,8 @@ ChromeUtils.defineESModuleGetters(lazy, { SmartAssistEngine: "moz-src:///browser/components/genai/SmartAssistEngine.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + SpecialMessageActions: + "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", }); const FULL_PAGE_URL = "chrome://browser/content/genai/smartAssistPage.html"; @@ -226,6 +228,25 @@ export class SmartAssist extends MozLitElement { this._applyNewTabOverride(isChecked); } + /** + * Initiates the Firefox Account sign-in flow for MLPA authentication. + */ + + _signIn() { + lazy.SpecialMessageActions.handleAction( + { + type: "FXA_SIGNIN_FLOW", + data: { + entrypoint: "ai-window", + extraParams: { + service: "ai-window", + }, + }, + }, + window.browsingContext.topChromeWindow.gBrowser.selectedBrowser + ); + } + render() { const iconSrc = this.showLog ? "chrome://global/skin/icons/arrow-down.svg" @@ -315,7 +336,17 @@ export class SmartAssist extends MozLitElement { > ${this.inputAction.label} </moz-button> + <hr/> + <h3>The following Elements are for testing purposes</h3> + <p>Sign in for MLPA authentication.</p> + <moz-button + type="primary" + size="small" + @click=${this._signIn} + > + Sign in + </moz-button> <!-- Footer - New Tab Override --> ${ this.mode === "sidebar" diff --git a/browser/components/genai/tests/xpcshell/test_smart_assist_engine.js b/browser/components/genai/tests/xpcshell/test_smart_assist_engine.js @@ -83,6 +83,7 @@ add_task(async function test_fetchWithHistory_streams_and_forwards_args() { }; sb.stub(SmartAssistEngine, "_createEngine").resolves(fakeEngine); + sb.stub(SmartAssistEngine, "_getFxAccountToken").resolves("mock_token"); const messages = [ { role: "system", content: "You are helpful" }, @@ -121,6 +122,7 @@ add_task( try { const err = new Error("creation failed (generic)"); const stub = sb.stub(SmartAssistEngine, "_createEngine").rejects(err); + sb.stub(SmartAssistEngine, "_getFxAccountToken").resolves("mock_token"); const messages = [{ role: "user", content: "Hi" }]; // Must CONSUME the async generator to trigger the rejection @@ -157,6 +159,7 @@ add_task(async function test_fetchWithHistory_propagates_stream_error() { }, }; sb.stub(SmartAssistEngine, "_createEngine").resolves(fakeEngine); + sb.stub(SmartAssistEngine, "_getFxAccountToken").resolves("mock_token"); const consume = async () => { let acc = ""; diff --git a/toolkit/components/ml/content/backends/OpenAIPipeline.mjs b/toolkit/components/ml/content/backends/OpenAIPipeline.mjs @@ -327,9 +327,16 @@ export class OpenAIPipeline { lazy.console.debug("Running OpenAI pipeline"); try { const { baseURL, apiKey, modelId } = this.#options; + const fxAccountToken = request.fxAccountToken + ? request.fxAccountToken + : null; + const defaultHeaders = fxAccountToken + ? { Authorization: `Bearer ${fxAccountToken}` } + : undefined; const client = new OpenAIPipeline.OpenAILib.OpenAI({ baseURL: baseURL ? baseURL : "http://localhost:11434/v1", apiKey: apiKey || "ollama", + ...(defaultHeaders ? { defaultHeaders } : {}), }); const stream = request.streamOptions?.enabled || false; const tools = request.tools || []; @@ -349,11 +356,9 @@ export class OpenAIPipeline { port, }; - if (stream) { - return await this.#handleStreamingResponse(args); - } - - return await this.#handleNonStreamingResponse(args); + return stream + ? await this.#handleStreamingResponse(args) + : await this.#handleNonStreamingResponse(args); } catch (error) { const backendError = this.#errorFactory(error); port?.postMessage({ done: true, ok: false, error: backendError }); diff --git a/toolkit/components/ml/tests/browser/browser_ml_openai.js b/toolkit/components/ml/tests/browser/browser_ml_openai.js @@ -251,3 +251,104 @@ add_task(async function test_openai_client_tools_streaming() { await stopMockOpenAI(mockServer); } }); + +/** + * Test that fxAccountToken is properly passed to the OpenAI client + */ +add_task(async function test_openai_fxaccount_token() { + const records = [ + { + ...BASE_ENGINE_OPTIONS, + id: "74a71cfd-1734-44e6-85c0-69cf3e874138", + }, + ]; + const { cleanup } = await setup({ records }); + + // Mock server that checks for fxAccount token in headers + let capturedFxaHeader = null; + const { server: mockServer, port } = startMockOpenAI({ + echo: "Response with FxA token", + onRequest: req => { + try { + if (req.hasHeader("authorization")) { + capturedFxaHeader = req.getHeader("authorization"); + } + } catch (e) { + info("Failed to get authorization header: " + e); + } + }, + }); + + const fxAccountToken = "test_fxa_token_12345"; + + const engineInstance = await createEngine({ + ...BASE_ENGINE_OPTIONS, + apiKey: "test-api-key", + baseURL: `http://localhost:${port}/v1`, + backend: "openai", + }); + + const request = { + args: [ + { + role: "user", + content: "Test request with FxA token", + }, + ], + fxAccountToken, + }; + + try { + info("Run the inference with fxAccountToken"); + const result = await engineInstance.run(request); + + Assert.equal( + result.finalOutput, + "This is a mock summary for testing end-to-end flow.", + "Should get expected response" + ); + + // Verify that the FxA token was sent in the request headers + const expectedValue = `Bearer ${fxAccountToken}`; + + Assert.equal( + capturedFxaHeader, + expectedValue, + `FxA token should be included in request headers. Expected: ${expectedValue}, Got: ${capturedFxaHeader}` + ); + + info("Test without fxAccountToken - should not include header"); + + // Create another engine without fxAccountToken + const engineWithoutToken = await createEngine({ + ...BASE_ENGINE_OPTIONS, + apiKey: "test-api-key", + baseURL: `http://localhost:${port}/v1`, + backend: "openai", + // No fxAccountToken + }); + + capturedFxaHeader = null; // Reset captured header + const requestWithoutToken = { + args: [ + { + role: "user", + content: "Test request with FxA token", + }, + ], + // No fxAccountToken + }; + await engineWithoutToken.run(requestWithoutToken); + + // Verify Authorization header is the API key when no FxA token is provided + Assert.equal( + capturedFxaHeader, + "Bearer test-api-key", + "Authorization should fall back to API key when no FxA token is provided" + ); + } finally { + await EngineProcess.destroyMLEngine(); + await cleanup(); + await stopMockOpenAI(mockServer); + } +}); diff --git a/toolkit/components/ml/tests/browser/head.js b/toolkit/components/ml/tests/browser/head.js @@ -783,12 +783,20 @@ function readRequestBody(request) { }); } -function startMockOpenAI({ echo = "This gets echoed." } = {}) { +function startMockOpenAI({ + echo = "This gets echoed.", + onRequest = null, +} = {}) { const server = new HttpServer(); server.registerPathHandler("/v1/chat/completions", (request, response) => { info("GET /v1/chat/completions"); + // Call the onRequest callback if provided to allow test inspection + if (onRequest) { + onRequest(request); + } + let bodyText = ""; if (request.method === "POST") { try {