Skip to content
Merged
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
40 changes: 25 additions & 15 deletions src/chrome/src/agent/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,29 +761,38 @@ export class Agent extends LoopDetector {
return this.conversationIds.get(tabId) || null;
}

/**
* Snapshot the effective runtime settings for a run. Anything we cannot
* observe is omitted rather than guessed: an absent field reads as "unknown"
* in a dump, while a hard `false`/'ask' would assert a setting the run never
* actually had — exactly the misattribution this metadata exists to prevent.
*/
_runtimeTraceConfig(provider, { tabId = null, mode = null } = {}) {
let extensionVersion = '';
let promptTier = 'full';
try { extensionVersion = chrome.runtime.getManifest().version || ''; } catch {}
try { promptTier = provider?.promptTier || 'full'; } catch {}
const effectiveMode = mode
|| (tabId != null ? this._effectiveRunMode(tabId) : 'ask');
const effectiveMode = mode || (tabId != null ? this._effectiveRunMode(tabId) : null);
return normalizeRuntimeTraceConfig({
extension_version: extensionVersion,
browser_target: 'chrome',
mode: effectiveMode,
prompt_tier: promptTier,
...(effectiveMode ? { mode: effectiveMode } : {}),
prompt_tier: this._resolvePromptTier(provider),
screenshot_redaction: this.screenshotRedaction === true,
strict_secret_mode: this.strictSecretMode === true,
plan_before_act_mode: this._normalizePlanBeforeActMode(this.planBeforeActMode),
auto_screenshot: this.autoScreenshot,
use_site_adapters: this.useSiteAdapters === true,
web_mcp_enabled: this.webMcpEnabled === true,
api_mutations_allowed: tabId != null && this.apiAllowedTabs.has(tabId),
user_memory_enabled: this.userMemoryEnabled === true,
selection_grounded: tabId != null && this.selectionGroundingScopes.has(tabId),
// Per-tab authorizations only mean something in a tab's context.
...(tabId != null ? {
api_mutations_allowed: this.apiAllowedTabs.has(tabId),
selection_grounded: this.selectionGroundingScopes.has(tabId),
} : {}),
image_detail: this.imageDetail,
max_agent_steps: this.maxSteps,
// The steps slider stores 0 for "unlimited", which the agent hydrates as
// Infinity. Round-trip that back to 0 so an unlimited run records as
// unlimited instead of being dropped as a non-integer.
max_agent_steps: Number.isFinite(this.maxSteps) ? this.maxSteps : 0,
max_image_dimension: this.maxImageDimension,
max_screenshots_per_turn: this.maxScreenshotsPerTurn,
});
Expand Down Expand Up @@ -9357,14 +9366,15 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
}

/**
* Resolve the active provider's prompt tier ('compact' | 'mid' | 'full').
* The provider getter already forces 'full' for cloud providers and applies
* the per-category defaults (local → 'mid'); we just guard the case where
* no provider is ready yet (fall back to the full prompt).
* Resolve a provider's prompt tier ('compact' | 'mid' | 'full'), defaulting
* to the active provider. The provider getter already forces 'full' for
* cloud providers and applies the per-category defaults (local → 'mid'); we
* just guard the case where no provider is ready yet (fall back to the full
* prompt).
*/
_resolvePromptTier() {
_resolvePromptTier(provider = null) {
try {
return this.providerManager.getActive().promptTier || 'full';
return (provider || this.providerManager.getActive()).promptTier || 'full';
} catch { return 'full'; }
}

Expand Down
6 changes: 5 additions & 1 deletion src/chrome/src/trace/runtime-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ const BOOLEAN_FIELDS = Object.freeze([
'selection_grounded',
]);

// Bounds keep the payload sane, not to re-validate settings: each range is a
// superset of what the agent's own normalizers can hold (steps ≤ 200 with 0 =
// unlimited, dimension ≤ 2048, screenshots ≤ 5), so a legitimate setting is
// never silently dropped for being out of range.
const INTEGER_RANGES = Object.freeze({
max_agent_steps: [0, 10_000],
max_image_dimension: [256, 16_384],
max_image_dimension: [1, 16_384],
max_screenshots_per_turn: [0, 1_000],
});

Expand Down
40 changes: 25 additions & 15 deletions src/firefox/src/agent/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -860,29 +860,38 @@ export class Agent extends LoopDetector {
return this.conversationIds.get(tabId) || null;
}

/**
* Snapshot the effective runtime settings for a run. Anything we cannot
* observe is omitted rather than guessed: an absent field reads as "unknown"
* in a dump, while a hard `false`/'ask' would assert a setting the run never
* actually had — exactly the misattribution this metadata exists to prevent.
*/
_runtimeTraceConfig(provider, { tabId = null, mode = null } = {}) {
let extensionVersion = '';
let promptTier = 'full';
try { extensionVersion = browser.runtime.getManifest().version || ''; } catch {}
try { promptTier = provider?.promptTier || 'full'; } catch {}
const effectiveMode = mode
|| (tabId != null ? this._effectiveRunMode(tabId) : 'ask');
const effectiveMode = mode || (tabId != null ? this._effectiveRunMode(tabId) : null);
return normalizeRuntimeTraceConfig({
extension_version: extensionVersion,
browser_target: 'firefox',
mode: effectiveMode,
prompt_tier: promptTier,
...(effectiveMode ? { mode: effectiveMode } : {}),
prompt_tier: this._resolvePromptTier(provider),
screenshot_redaction: this.screenshotRedaction === true,
strict_secret_mode: this.strictSecretMode === true,
plan_before_act_mode: this._normalizePlanBeforeActMode(this.planBeforeActMode),
auto_screenshot: this.autoScreenshot,
use_site_adapters: this.useSiteAdapters === true,
web_mcp_enabled: this.webMcpEnabled === true,
api_mutations_allowed: tabId != null && this.apiAllowedTabs.has(tabId),
user_memory_enabled: this.userMemoryEnabled === true,
selection_grounded: tabId != null && this.selectionGroundingScopes.has(tabId),
// Per-tab authorizations only mean something in a tab's context.
...(tabId != null ? {
api_mutations_allowed: this.apiAllowedTabs.has(tabId),
selection_grounded: this.selectionGroundingScopes.has(tabId),
} : {}),
image_detail: this.imageDetail,
max_agent_steps: this.maxSteps,
// The steps slider stores 0 for "unlimited", which the agent hydrates as
// Infinity. Round-trip that back to 0 so an unlimited run records as
// unlimited instead of being dropped as a non-integer.
max_agent_steps: Number.isFinite(this.maxSteps) ? this.maxSteps : 0,
max_image_dimension: this.maxImageDimension,
max_screenshots_per_turn: this.maxScreenshotsPerTurn,
});
Expand Down Expand Up @@ -8152,14 +8161,15 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
}

/**
* Resolve the active provider's prompt tier ('compact' | 'mid' | 'full').
* The provider getter already forces 'full' for cloud providers and applies
* the per-category defaults (local → 'mid'); we just guard the case where
* no provider is ready yet (fall back to the full prompt).
* Resolve a provider's prompt tier ('compact' | 'mid' | 'full'), defaulting
* to the active provider. The provider getter already forces 'full' for
* cloud providers and applies the per-category defaults (local → 'mid'); we
* just guard the case where no provider is ready yet (fall back to the full
* prompt).
*/
_resolvePromptTier() {
_resolvePromptTier(provider = null) {
try {
return this.providerManager.getActive().promptTier || 'full';
return (provider || this.providerManager.getActive()).promptTier || 'full';
} catch { return 'full'; }
}

Expand Down
6 changes: 5 additions & 1 deletion src/firefox/src/trace/runtime-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ const BOOLEAN_FIELDS = Object.freeze([
'selection_grounded',
]);

// Bounds keep the payload sane, not to re-validate settings: each range is a
// superset of what the agent's own normalizers can hold (steps ≤ 200 with 0 =
// unlimited, dimension ≤ 2048, screenshots ≤ 5), so a legitimate setting is
// never silently dropped for being out of range.
const INTEGER_RANGES = Object.freeze({
max_agent_steps: [0, 10_000],
max_image_dimension: [256, 16_384],
max_image_dimension: [1, 16_384],
max_screenshots_per_turn: [0, 1_000],
});

Expand Down
48 changes: 47 additions & 1 deletion test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -4139,12 +4139,22 @@ test('trace record and JSON exports carry WebBrain version metadata', () => {
assert.match(recorder, /runtimeConfig: normalizeRuntimeTraceConfig\(meta\.runtimeConfig\)/, `${label}: run record should retain only allowlisted runtime settings`);
assert.match(agent, new RegExp(`webbrainVersion: ${runtimeName}\\.runtime\\.getManifest\\(\\)\\.version`), `${label}: trace start should read the runtime manifest`);
assert.match(agent, /runtimeConfig: this\._runtimeTraceConfig\(provider, \{ tabId, mode \}\)/, `${label}: trace start should snapshot effective runtime settings`);
assert.match(
agent,
/runtimeConfig: this\._runtimeTraceConfig\(this\.providerManager\?\.getActive\?\.\(\), \{\s*tabId,\s*mode: 'act',\s*\}\)/,
`${label}: workflow runs should snapshot effective runtime settings too`,
);
assert.match(traceUi, new RegExp(`exportedByWebBrainVersion: ${runtimeName}\\.runtime\\.getManifest\\(\\)\\.version`), `${label}: JSON export should identify the exporting build`);
assert.match(traceUi, /schema: 'webbrain-trace\/1'/, `${label}: additive version metadata should retain the v1 schema`);
}
});

test('runtime trace config is versioned, bounded, and secret-free in both browsers', () => {
assert.equal(
fs.readFileSync(path.join(ROOT, 'src/chrome/src/trace/runtime-config.js'), 'utf8'),
fs.readFileSync(path.join(ROOT, 'src/firefox/src/trace/runtime-config.js'), 'utf8'),
'chrome and firefox runtime trace config modules must remain byte-identical',
);
const candidate = {
schema_version: 99,
extension_version: '24.7.0-beta.1',
Expand Down Expand Up @@ -4196,10 +4206,29 @@ test('runtime trace config is versioned, bounded, and secret-free in both browse
browser_target: 'safari',
mode: 'admin',
max_agent_steps: Infinity,
max_image_dimension: 42,
max_image_dimension: 1568.5,
max_screenshots_per_turn: -1,
screenshot_redaction: 'yes',
});
assert.deepEqual(rejected, { schema_version: 1 });

// Integer ranges exist to bound the payload, not to re-validate settings:
// every value the agent's own normalizers can produce must survive, or the
// dump silently loses the setting it was added to attribute.
for (const [field, value] of [
['max_agent_steps', 0],
['max_agent_steps', 200],
['max_image_dimension', 1],
['max_image_dimension', 2048],
['max_screenshots_per_turn', 0],
['max_screenshots_per_turn', 5],
]) {
assert.equal(
RuntimeTraceConfigCh.normalizeRuntimeTraceConfig({ [field]: value })[field],
value,
`${field}=${value} is producible by the agent and must not be dropped`,
);
}
});

test('trace recorders normalize done only from explicit loop error evidence', () => {
Expand Down Expand Up @@ -32434,6 +32463,23 @@ test('WebBrain Cloud groups every generation in a stable conversation session wi
assert.equal(main.webbrainRuntimeConfig?.strict_secret_mode, true, `${label}: strict secret setting missing`);
assert.equal(main.webbrainRuntimeConfig?.api_mutations_allowed, true, `${label}: per-tab API authorization missing`);
assert.ok(!JSON.stringify(main.webbrainRuntimeConfig).includes('apiKey'), `${label}: runtime metadata must remain an allowlist`);
assert.equal(main.webbrainRuntimeConfig?.max_agent_steps, agent.maxSteps, `${label}: step budget missing`);

// "Unlimited" steps hydrate as Infinity; record the stored 0 sentinel so an
// unlimited run is attributable instead of missing the field entirely.
const previousMaxSteps = agent.maxSteps;
agent.maxSteps = Infinity;
const unlimited = agent._cloudGenerationOptions(cloud, {}, { tabId, generationName: 'main' });
assert.equal(unlimited.webbrainRuntimeConfig?.max_agent_steps, 0, `${label}: unlimited step budget should record as 0`);
agent.maxSteps = previousMaxSteps;

// Without a tab there is no observable mode or per-tab authorization, so
// those fields must be absent rather than asserted as ask/false.
const untabbed = agent._runtimeTraceConfig(cloud);
assert.equal('mode' in untabbed, false, `${label}: unknown mode should be omitted, not guessed`);
assert.equal('api_mutations_allowed' in untabbed, false, `${label}: per-tab API authorization should be omitted without a tab`);
assert.equal('selection_grounded' in untabbed, false, `${label}: selection grounding should be omitted without a tab`);
assert.equal(untabbed.browser_target, label, `${label}: browser target should survive without a tab`);

const byoOptions = agent._cloudGenerationOptions({ config: { providerName: 'openai' } }, { temperature: 0 }, { tabId, generationName: 'memory' });
assert.deepEqual(byoOptions, { temperature: 0 }, `${label}: BYO provider received Cloud collection fields`);
Expand Down
Loading