Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions browser/components/smartwindow/actors/SmartWindowMetaChild.sys.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* Content-process actor for extracting page metadata (page URL, canonical, og:url).
* The parent actor validates and normalizes these URLs before seeding the security ledger.
*/
export class SmartWindowMetaChild extends JSWindowActorChild {
/**
* Receives queries from the parent process.
*
* @param {ReceiveMessageArgument} message - The message from parent
* @returns {Promise<object>} Metadata object with URLs
*/
receiveMessage(message) {
switch (message.name) {
case "SmartWindowMeta:GetMetadata":
return this.getMetadata();
default:
return Promise.reject(new Error(`Unknown message: ${message.name}`));
}
}

/**
* Extracts metadata from the current page.
*
* @returns {object} Metadata with pageUrl, canonical, and ogUrl (raw strings)
*/
getMetadata() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel like we probably already have an existing API for this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point. I'll research more if there's an existing API I can use for this. 🕵️

const doc = this.contentWindow?.document;

if (!doc) {
return { pageUrl: "", canonical: "", ogUrl: "" };
}

const pageUrl = doc.location?.href || "";

let canonical = "";
try {
const canonicalLink = doc.querySelector('link[rel="canonical"]');
canonical = canonicalLink?.getAttribute("href") || "";
} catch {
// querySelector may fail on some documents (e.g., XML)
}

let ogUrl = "";
try {
const ogUrlMeta = doc.querySelector('meta[property="og:url"]');
ogUrl = ogUrlMeta?.getAttribute("content") || "";
} catch {
// querySelector may fail on some documents
}

return { pageUrl, canonical, ogUrl };
}
}
163 changes: 163 additions & 0 deletions browser/components/smartwindow/actors/SmartWindowMetaParent.sys.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import {
normalizeUrl,
isSameETLDPlusOne,
} from "chrome://global/content/ml/security/SecurityUtils.sys.mjs";

/**
* Chrome-process actor for validating page metadata and seeding security ledger.
* Validates canonical/og:url have same eTLD+1 as page URL before seeding.
*/
export class SmartWindowMetaParent extends JSWindowActorParent {
/**
* Seeds the security ledger for the given browser/tab.
*
* @param {object} sessionLedger - The SessionLedger instance
* @param {string} tabId - The tab identifier (typically linkedPanel)
* @returns {Promise<object>} Result with seededUrls, skippedUrls, and errors
*/
async seedLedgerForTab(sessionLedger, tabId) {
const result = {
success: false,
seededUrls: [],
skippedUrls: [],
errors: [],
};

try {
const metadata = await this.sendQuery("SmartWindowMeta:GetMetadata");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the incentive to run this in the child process?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! My understanding is that the parent process wouldn't be able to query the DOM directly (i.e., document.querySelector() ) since the DOM lives in the child process.

However, I should definitely look into the existing API you mentioned before that would already handle this 😅


if (!metadata || !metadata.pageUrl) {
result.errors.push("No page URL available from content process");
return result;
}

// Process all URLs in one place
const processed = this.#processMetadataUrls(metadata);

if (processed.error) {
result.errors.push(processed.error);
return result;
}

result.seededUrls = processed.seededUrls;
result.skippedUrls = processed.skippedUrls;

sessionLedger.forTab(tabId).seed(processed.urlsToSeed, metadata.pageUrl);
result.success = true;
} catch (error) {
result.errors.push({
message: "Actor communication failed",
error: error.message || String(error),
});
}

return result;
}

/**
* Processes page metadata URLs: normalizes page URL and validates secondary URLs.
*
* @param {object} metadata - Raw metadata from content process
* @param {string} metadata.pageUrl - The page's URL
* @param {string} [metadata.canonical] - The canonical URL from <link rel="canonical">
* @param {string} [metadata.ogUrl] - The og:url from <meta property="og:url">
* @returns {object} Processed result with urlsToSeed, seededUrls, skippedUrls, error
* @private
*/
#processMetadataUrls(metadata) {
const { pageUrl, canonical, ogUrl } = metadata;
const urlsToSeed = [];
const seededUrls = [];
const skippedUrls = [];

// Normalize page URL first
const normalizedPageUrl = normalizeUrl(pageUrl);
if (!normalizedPageUrl.success) {
return {
error: {
url: pageUrl,
reason: "Page URL normalization failed",
error: normalizedPageUrl.error,
},
};
}

urlsToSeed.push(normalizedPageUrl.url);
seededUrls.push({
original: pageUrl,
normalized: normalizedPageUrl.url,
source: "page",
});

// Process secondary URLs (canonical, og:url)
const secondaryUrls = [
{ url: canonical, source: "canonical" },
{ url: ogUrl, source: "og:url" },
];

for (const { url, source } of secondaryUrls) {
if (!url) {
continue;
}

const validated = this.#validateSecondaryUrl(
url,
normalizedPageUrl.url,
pageUrl,
);

if (validated.success) {
urlsToSeed.push(validated.normalizedUrl);
seededUrls.push({
original: url,
normalized: validated.normalizedUrl,
source,
});
} else {
skippedUrls.push({
original: url,
source,
reason: validated.reason,
});
}
}

return { urlsToSeed, seededUrls, skippedUrls };
}

/**
* Validates a secondary URL (canonical or og:url) against the page's eTLD+1.
*
* @param {string} url - The URL to validate (may be relative)
* @param {string} normalizedPageUrl - The normalized page URL for eTLD+1 comparison
* @param {string} baseUrl - The original page URL for resolving relative URLs
* @returns {object} Validation result with success flag and normalizedUrl or reason
* @private
*/
#validateSecondaryUrl(url, normalizedPageUrl, baseUrl) {
const normalized = normalizeUrl(url, baseUrl);

if (!normalized.success) {
return {
success: false,
reason: "Normalization failed",
details: normalized.error,
};
}

if (!isSameETLDPlusOne(normalizedPageUrl, normalized.url)) {
return {
success: false,
reason: "Different eTLD+1 from page URL",
pageUrl: normalizedPageUrl,
secondaryUrl: normalized.url,
};
}

return { success: true, normalizedUrl: normalized.url };
}
}
8 changes: 8 additions & 0 deletions browser/components/smartwindow/content/smartbar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
getMentionSuggestions,
} from "chrome://browser/content/smartwindow/mentions.mjs";

import { seedMentionedUrl } from "./utils.mjs";

// Track autofill state
let autofillState = null;
let deletedQuery = "";
Expand Down Expand Up @@ -252,6 +254,12 @@ export function attachToElement(element, options = {}) {
dropdown = new MentionDropdown();
currentCommand = props.command;
dropdown.create(props.items, item => {
// Seed the @mentioned URL into the security ledger
// This grants the AI permission to access this URL
if (item.id) {
seedMentionedUrl(item.id);
}

currentCommand({
id: item.id,
label: item.label,
Expand Down
61 changes: 60 additions & 1 deletion browser/components/smartwindow/content/smartwindow.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { detectQueryType, searchBrowserHistory } from "./utils.mjs";
import { detectQueryType, searchBrowserHistory, setSecurityOrchestrator } from "./utils.mjs";
import { attachToElement } from "chrome://browser/content/smartwindow/smartbar.mjs";
import {
generateLiveSuggestions,
Expand Down Expand Up @@ -31,11 +31,39 @@ const { SessionStore } = ChromeUtils.importESModule(
const { TabStateFlusher } = ChromeUtils.importESModule(
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
);
const { SecurityOrchestrator } = ChromeUtils.importESModule(
"chrome://global/content/ml/security/SecurityOrchestrator.sys.mjs"
);
const { generateId } = ChromeUtils.importESModule(
"chrome://global/content/ml/security/SecurityUtils.sys.mjs"
);
const { embedderElement, topChromeWindow } = window.browsingContext;
const gBrowser = topChromeWindow.gBrowser;

const FIRST_RUN_PREF = "browser.smartwindow.firstrun.didSeeWelcome";

// Register SmartWindowMeta actor for secure page metadata extraction
try {
ChromeUtils.registerWindowActor("SmartWindowMeta", {
parent: {
esModuleURI: "chrome://browser/content/smartwindow/actors/SmartWindowMetaParent.sys.mjs",
},
child: {
esModuleURI: "chrome://browser/content/smartwindow/actors/SmartWindowMetaChild.sys.mjs",
events: {
DOMContentLoaded: {},
},
},
allFrames: true,
});
} catch (e) {
// Actor already registered - this is expected if Smart Window
// has been opened before in this browser session
if (!e.message?.toLowerCase().includes("already registered")) {
console.error("Failed to register SmartWindowMeta actor:", e);
}
}

/**
*
*/
Expand Down Expand Up @@ -88,6 +116,32 @@ class SmartWindowPage {

this.#chatHistory = new ChatHistory();

// Initialize security orchestrator (if enabled)
// Creates the SessionLedger that tracks trusted URLs across tabs
// Only initialize if Smart Window security is enabled
const sessionId = generateId("smart-window");
const securityEnabled = Services.prefs.getBoolPref("browser.smartwindow.security.enabled", true);

this.securityOrchestrator = null;

if (securityEnabled) {
SecurityOrchestrator.create(sessionId).then(orchestrator => {
this.securityOrchestrator = orchestrator;
this.sessionLedger = orchestrator.getSessionLedger();
setSecurityOrchestrator(orchestrator);
console.warn("[Security] AI Window security enabled - orchestrator initialized");
}).catch(error => {
console.error("[Security] Failed to initialize orchestrator:", error);
this.securityOrchestrator = null;
this.sessionLedger = null;
setSecurityOrchestrator(null);
});
} else {
this.sessionLedger = null;
setSecurityOrchestrator(null);
console.warn("[Security] AI Window security DISABLED via kill switch - running in pass-through mode");
}

gBrowser.selectedTab.conversation = new ChatHistoryConversation({
title: "",
description: "",
Expand Down Expand Up @@ -134,6 +188,11 @@ class SmartWindowPage {
this.onboardingPrefObserver
);
}

if (this.securityOrchestrator) {
this.securityOrchestrator.reset();
setSecurityOrchestrator(null);
}
}

getQueryTypeIcon(type) {
Expand Down
Loading