Skip to content

Commit 23b45ec

Browse files
authored
Merge pull request #210 from esokullu/agent/trace-runtime-config
Add safe runtime settings to trace metadata
2 parents 1efae04 + 7c03392 commit 23b45ec

9 files changed

Lines changed: 293 additions & 4 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
PDF_PASSTHROUGH_MAX_BYTES,
3232
} from './pdf-tools.js';
3333
import * as trace from '../trace/recorder.js';
34+
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
3435
import { tracesToMarkdown } from './trace-export.js';
3536
import { solveCaptcha, detectCaptcha, injectToken, captchaParamError, captchaTypesMatch, captchaWebsiteUrl } from './captcha-solver.js';
3637
import { isCapsolverEnabled, normalizeCapsolverApiKey } from './capsolver-config.js';
@@ -738,6 +739,34 @@ export class Agent extends LoopDetector {
738739
return this.conversationIds.get(tabId) || null;
739740
}
740741

742+
_runtimeTraceConfig(provider, { tabId = null, mode = null } = {}) {
743+
let extensionVersion = '';
744+
let promptTier = 'full';
745+
try { extensionVersion = chrome.runtime.getManifest().version || ''; } catch {}
746+
try { promptTier = provider?.promptTier || 'full'; } catch {}
747+
const effectiveMode = mode
748+
|| (tabId != null ? this._effectiveRunMode(tabId) : 'ask');
749+
return normalizeRuntimeTraceConfig({
750+
extension_version: extensionVersion,
751+
browser_target: 'chrome',
752+
mode: effectiveMode,
753+
prompt_tier: promptTier,
754+
screenshot_redaction: this.screenshotRedaction === true,
755+
strict_secret_mode: this.strictSecretMode === true,
756+
plan_before_act_mode: this._normalizePlanBeforeActMode(this.planBeforeActMode),
757+
auto_screenshot: this.autoScreenshot,
758+
use_site_adapters: this.useSiteAdapters === true,
759+
web_mcp_enabled: this.webMcpEnabled === true,
760+
api_mutations_allowed: tabId != null && this.apiAllowedTabs.has(tabId),
761+
user_memory_enabled: this.userMemoryEnabled === true,
762+
selection_grounded: tabId != null && this.selectionGroundingScopes.has(tabId),
763+
image_detail: this.imageDetail,
764+
max_agent_steps: this.maxSteps,
765+
max_image_dimension: this.maxImageDimension,
766+
max_screenshots_per_turn: this.maxScreenshotsPerTurn,
767+
});
768+
}
769+
741770
_cloudGenerationOptions(provider, options = {}, { tabId = null, conversationId = null, generationName = 'main' } = {}) {
742771
if (String(provider?.config?.providerName || '').toLowerCase() !== 'webbrain-cloud') return options;
743772
const effectiveConversationId = conversationId || (tabId != null ? this.conversationIds.get(tabId) : null);
@@ -746,6 +775,7 @@ export class Agent extends LoopDetector {
746775
...options,
747776
webbrainSessionId: String(effectiveConversationId),
748777
webbrainGenerationName: String(generationName || 'main'),
778+
webbrainRuntimeConfig: this._runtimeTraceConfig(provider, { tabId }),
749779
};
750780
}
751781

@@ -6763,6 +6793,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
67636793
providerId: provider?.name,
67646794
providerClass: provider?.constructor?.name,
67656795
webbrainVersion: chrome.runtime.getManifest().version || '',
6796+
runtimeConfig: this._runtimeTraceConfig(provider, { tabId, mode }),
67666797
userMessage: typeof userMessage === 'string' ? userMessage : JSON.stringify(userMessage).slice(0, 2000),
67676798
tabUrl,
67686799
tabTitle,
@@ -13741,6 +13772,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1374113772
mode: 'act',
1374213773
model: this.providerManager?.getActive?.()?.model || '',
1374313774
providerId: this.providerManager?.activeProviderId || '',
13775+
runtimeConfig: this._runtimeTraceConfig(this.providerManager?.getActive?.(), {
13776+
tabId,
13777+
mode: 'act',
13778+
}),
1374413779
});
1374513780
let traceStatus = 'workflow_stopped';
1374613781
let finalContent = '';

src/chrome/src/providers/openai.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
shouldUseOpenAIResponsesApi,
66
supportsOpenAIAskStreaming,
77
} from './provider-compatibility.js';
8+
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
89

910
const OPENAI_RESPONSES_MIN_MAX_OUTPUT_TOKENS = 16;
1011
const KIMI_CURRENT_TOOL_REASONING_MODELS = new Set([
@@ -235,11 +236,16 @@ export class OpenAICompatibleProvider extends BaseLLMProvider {
235236
const sessionId = String(options.webbrainSessionId || '').trim();
236237
if (sessionId) body.session_id = sessionId.slice(0, 200);
237238
const generationName = String(options.webbrainGenerationName || '').trim().toLowerCase();
238-
if (generationName) {
239+
const runtimeConfig = normalizeRuntimeTraceConfig(options.webbrainRuntimeConfig);
240+
if (generationName || runtimeConfig) {
239241
const trace = body.trace && typeof body.trace === 'object' && !Array.isArray(body.trace)
240242
? body.trace
241243
: {};
242-
body.trace = { ...trace, generation_name: generationName.slice(0, 64) };
244+
body.trace = {
245+
...trace,
246+
...(generationName ? { generation_name: generationName.slice(0, 64) } : {}),
247+
...(runtimeConfig ? { runtime_config: runtimeConfig } : {}),
248+
};
243249
}
244250
}
245251

src/chrome/src/trace/recorder.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { normalizeRuntimeTraceConfig } from './runtime-config.js';
2+
13
/**
24
* Trace recorder — writes per-run traces (LLM requests/responses, tool calls,
35
* screenshots) into IndexedDB for later inspection and cross-model comparison.
@@ -130,6 +132,7 @@ export async function startRun(meta = {}) {
130132
providerId: meta.providerId || '',
131133
providerClass: meta.providerClass || '',
132134
webbrainVersion: meta.webbrainVersion || '',
135+
runtimeConfig: normalizeRuntimeTraceConfig(meta.runtimeConfig),
133136
userMessage: meta.userMessage || '',
134137
tabUrl: meta.tabUrl || '',
135138
tabTitle: meta.tabTitle || '',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const STRING_ENUMS = Object.freeze({
2+
browser_target: new Set(['chrome', 'firefox']),
3+
mode: new Set(['ask', 'act', 'dev']),
4+
prompt_tier: new Set(['compact', 'mid', 'full']),
5+
plan_before_act_mode: new Set(['off', 'try', 'strict']),
6+
auto_screenshot: new Set(['off', 'navigation', 'state_change', 'every_step']),
7+
image_detail: new Set(['auto', 'low', 'high']),
8+
});
9+
10+
const BOOLEAN_FIELDS = Object.freeze([
11+
'screenshot_redaction',
12+
'strict_secret_mode',
13+
'use_site_adapters',
14+
'web_mcp_enabled',
15+
'api_mutations_allowed',
16+
'user_memory_enabled',
17+
'selection_grounded',
18+
]);
19+
20+
const INTEGER_RANGES = Object.freeze({
21+
max_agent_steps: [0, 10_000],
22+
max_image_dimension: [256, 16_384],
23+
max_screenshots_per_turn: [0, 1_000],
24+
});
25+
26+
function safeVersion(value) {
27+
const version = String(value || '').trim();
28+
return /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/.test(version) ? version : '';
29+
}
30+
31+
/**
32+
* Runtime trace metadata crosses both a provider boundary and an export
33+
* boundary. Keep it to a small, versioned allowlist of booleans, bounded
34+
* integers, and enums so a caller can never smuggle credentials or profile
35+
* contents into a trace by passing an arbitrary settings object.
36+
*/
37+
export function normalizeRuntimeTraceConfig(value) {
38+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
39+
40+
const normalized = { schema_version: 1 };
41+
const extensionVersion = safeVersion(value.extension_version);
42+
if (extensionVersion) normalized.extension_version = extensionVersion;
43+
44+
for (const [field, allowed] of Object.entries(STRING_ENUMS)) {
45+
const candidate = String(value[field] || '').trim().toLowerCase();
46+
if (allowed.has(candidate)) normalized[field] = candidate;
47+
}
48+
for (const field of BOOLEAN_FIELDS) {
49+
if (typeof value[field] === 'boolean') normalized[field] = value[field];
50+
}
51+
for (const [field, [min, max]] of Object.entries(INTEGER_RANGES)) {
52+
const candidate = Number(value[field]);
53+
if (Number.isInteger(candidate) && candidate >= min && candidate <= max) {
54+
normalized[field] = candidate;
55+
}
56+
}
57+
58+
return normalized;
59+
}

src/firefox/src/agent/agent.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
PDF_PASSTHROUGH_MAX_BYTES,
3434
} from './pdf-tools.js';
3535
import * as trace from '../trace/recorder.js';
36+
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
3637
import { tracesToMarkdown } from './trace-export.js';
3738
import { solveCaptcha, detectCaptcha, injectToken, captchaParamError, captchaTypesMatch, captchaWebsiteUrl } from './captcha-solver.js';
3839
import { isCapsolverEnabled, normalizeCapsolverApiKey } from './capsolver-config.js';
@@ -837,6 +838,34 @@ export class Agent extends LoopDetector {
837838
return this.conversationIds.get(tabId) || null;
838839
}
839840

841+
_runtimeTraceConfig(provider, { tabId = null, mode = null } = {}) {
842+
let extensionVersion = '';
843+
let promptTier = 'full';
844+
try { extensionVersion = browser.runtime.getManifest().version || ''; } catch {}
845+
try { promptTier = provider?.promptTier || 'full'; } catch {}
846+
const effectiveMode = mode
847+
|| (tabId != null ? this._effectiveRunMode(tabId) : 'ask');
848+
return normalizeRuntimeTraceConfig({
849+
extension_version: extensionVersion,
850+
browser_target: 'firefox',
851+
mode: effectiveMode,
852+
prompt_tier: promptTier,
853+
screenshot_redaction: this.screenshotRedaction === true,
854+
strict_secret_mode: this.strictSecretMode === true,
855+
plan_before_act_mode: this._normalizePlanBeforeActMode(this.planBeforeActMode),
856+
auto_screenshot: this.autoScreenshot,
857+
use_site_adapters: this.useSiteAdapters === true,
858+
web_mcp_enabled: this.webMcpEnabled === true,
859+
api_mutations_allowed: tabId != null && this.apiAllowedTabs.has(tabId),
860+
user_memory_enabled: this.userMemoryEnabled === true,
861+
selection_grounded: tabId != null && this.selectionGroundingScopes.has(tabId),
862+
image_detail: this.imageDetail,
863+
max_agent_steps: this.maxSteps,
864+
max_image_dimension: this.maxImageDimension,
865+
max_screenshots_per_turn: this.maxScreenshotsPerTurn,
866+
});
867+
}
868+
840869
_cloudGenerationOptions(provider, options = {}, { tabId = null, conversationId = null, generationName = 'main' } = {}) {
841870
if (String(provider?.config?.providerName || '').toLowerCase() !== 'webbrain-cloud') return options;
842871
const effectiveConversationId = conversationId || (tabId != null ? this.conversationIds.get(tabId) : null);
@@ -845,6 +874,7 @@ export class Agent extends LoopDetector {
845874
...options,
846875
webbrainSessionId: String(effectiveConversationId),
847876
webbrainGenerationName: String(generationName || 'main'),
877+
webbrainRuntimeConfig: this._runtimeTraceConfig(provider, { tabId }),
848878
};
849879
}
850880

@@ -5707,6 +5737,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
57075737
providerId: provider?.name,
57085738
providerClass: provider?.constructor?.name,
57095739
webbrainVersion: browser.runtime.getManifest().version || '',
5740+
runtimeConfig: this._runtimeTraceConfig(provider, { tabId, mode }),
57105741
userMessage: typeof userMessage === 'string' ? userMessage : JSON.stringify(userMessage).slice(0, 2000),
57115742
tabUrl,
57125743
tabTitle,
@@ -12011,6 +12042,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1201112042
mode: 'act',
1201212043
model: this.providerManager?.getActive?.()?.model || '',
1201312044
providerId: this.providerManager?.activeProviderId || '',
12045+
runtimeConfig: this._runtimeTraceConfig(this.providerManager?.getActive?.(), {
12046+
tabId,
12047+
mode: 'act',
12048+
}),
1201412049
});
1201512050
let traceStatus = 'workflow_stopped';
1201612051
let finalContent = '';

src/firefox/src/providers/openai.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
shouldUseOpenAIResponsesApi,
66
supportsOpenAIAskStreaming,
77
} from './provider-compatibility.js';
8+
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
89

910
const OPENAI_RESPONSES_MIN_MAX_OUTPUT_TOKENS = 16;
1011
const KIMI_CURRENT_TOOL_REASONING_MODELS = new Set([
@@ -235,11 +236,16 @@ export class OpenAICompatibleProvider extends BaseLLMProvider {
235236
const sessionId = String(options.webbrainSessionId || '').trim();
236237
if (sessionId) body.session_id = sessionId.slice(0, 200);
237238
const generationName = String(options.webbrainGenerationName || '').trim().toLowerCase();
238-
if (generationName) {
239+
const runtimeConfig = normalizeRuntimeTraceConfig(options.webbrainRuntimeConfig);
240+
if (generationName || runtimeConfig) {
239241
const trace = body.trace && typeof body.trace === 'object' && !Array.isArray(body.trace)
240242
? body.trace
241243
: {};
242-
body.trace = { ...trace, generation_name: generationName.slice(0, 64) };
244+
body.trace = {
245+
...trace,
246+
...(generationName ? { generation_name: generationName.slice(0, 64) } : {}),
247+
...(runtimeConfig ? { runtime_config: runtimeConfig } : {}),
248+
};
243249
}
244250
}
245251

src/firefox/src/trace/recorder.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { normalizeRuntimeTraceConfig } from './runtime-config.js';
2+
13
/**
24
* Trace recorder — writes per-run traces (LLM requests/responses, tool calls,
35
* screenshots) into IndexedDB for later inspection and cross-model comparison.
@@ -114,6 +116,7 @@ export async function startRun(meta) {
114116
providerId: meta.providerId || '',
115117
providerClass: meta.providerClass || '',
116118
webbrainVersion: meta.webbrainVersion || '',
119+
runtimeConfig: normalizeRuntimeTraceConfig(meta.runtimeConfig),
117120
userMessage: meta.userMessage || '',
118121
tabUrl: meta.tabUrl || '',
119122
tabTitle: meta.tabTitle || '',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const STRING_ENUMS = Object.freeze({
2+
browser_target: new Set(['chrome', 'firefox']),
3+
mode: new Set(['ask', 'act', 'dev']),
4+
prompt_tier: new Set(['compact', 'mid', 'full']),
5+
plan_before_act_mode: new Set(['off', 'try', 'strict']),
6+
auto_screenshot: new Set(['off', 'navigation', 'state_change', 'every_step']),
7+
image_detail: new Set(['auto', 'low', 'high']),
8+
});
9+
10+
const BOOLEAN_FIELDS = Object.freeze([
11+
'screenshot_redaction',
12+
'strict_secret_mode',
13+
'use_site_adapters',
14+
'web_mcp_enabled',
15+
'api_mutations_allowed',
16+
'user_memory_enabled',
17+
'selection_grounded',
18+
]);
19+
20+
const INTEGER_RANGES = Object.freeze({
21+
max_agent_steps: [0, 10_000],
22+
max_image_dimension: [256, 16_384],
23+
max_screenshots_per_turn: [0, 1_000],
24+
});
25+
26+
function safeVersion(value) {
27+
const version = String(value || '').trim();
28+
return /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/.test(version) ? version : '';
29+
}
30+
31+
/**
32+
* Runtime trace metadata crosses both a provider boundary and an export
33+
* boundary. Keep it to a small, versioned allowlist of booleans, bounded
34+
* integers, and enums so a caller can never smuggle credentials or profile
35+
* contents into a trace by passing an arbitrary settings object.
36+
*/
37+
export function normalizeRuntimeTraceConfig(value) {
38+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
39+
40+
const normalized = { schema_version: 1 };
41+
const extensionVersion = safeVersion(value.extension_version);
42+
if (extensionVersion) normalized.extension_version = extensionVersion;
43+
44+
for (const [field, allowed] of Object.entries(STRING_ENUMS)) {
45+
const candidate = String(value[field] || '').trim().toLowerCase();
46+
if (allowed.has(candidate)) normalized[field] = candidate;
47+
}
48+
for (const field of BOOLEAN_FIELDS) {
49+
if (typeof value[field] === 'boolean') normalized[field] = value[field];
50+
}
51+
for (const [field, [min, max]] of Object.entries(INTEGER_RANGES)) {
52+
const candidate = Number(value[field]);
53+
if (Number.isInteger(candidate) && candidate >= min && candidate <= max) {
54+
normalized[field] = candidate;
55+
}
56+
}
57+
58+
return normalized;
59+
}

0 commit comments

Comments
 (0)