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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ All notable changes to WebBrain are documented in this file.

This changelog was generated from the repository Git history and release tags. Versions without a Git tag are inferred from version-bump commits and the current `package.json` / browser manifest versions.

## [24.1.0] - 2026-07-16

### Changed
- import/export configs

## [24.0.2] - 2026-07-16

### Added
- Added `/export --config` to download a portable JSON snapshot of settings and `/import <json>` or `/import --file` to restore one in Chrome and Firefox.
- Included provider, vision, transcription, and CAPTCHA API keys in configuration snapshots, with an explicit plaintext-secret warning before export and import.

### Changed
- Limited configuration snapshots to settings-backed state, excluding device-bound sync identifiers and tokens, conversations, traces, jobs, and usage counters.

### Tests
- Added mirrored coverage for configuration schema completeness, round trips, validation failures, file selection, slash-command routing, and background import/export handlers.

## [24.0.1] - 2026-07-16

### Added
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,14 +330,23 @@ WebBrain accepts slash commands as the first thing on a line in the input box. T
| `/reset` | Clear the conversation and all per-conversation flags |
| `/screenshot [--full-page]` | Capture the visible tab, or the full scrollable page with `--full-page` (Chrome only) |
| `/record [--full-screen] [--transcribe]` | Record the current tab, or a selected screen/window with `--full-screen` (Chrome only); add `--transcribe` to save a transcript after stop |
| `/export [--traces]` | Download version-stamped conversation Markdown, or export the version-stamped tool chain with `--traces` |
| `/export [--traces \| --config]` | Download version-stamped conversation Markdown, export the version-stamped tool chain with `--traces`, or export a Settings snapshot with `--config` |
| `/import <json>` | Import a Settings snapshot pasted inline |
| `/import --file` | Choose and import a Settings snapshot JSON file |
| `/profile` | Toggle profile auto-fill on/off without opening Settings |
| `/vision` | Toggle vision mode (screenshot understanding) on the active provider |
| `/ask` | Switch to Ask mode before sending |
| `/act` | Switch to Act mode before sending |
| `/dev` | Switch to Dev mode before sending |
| `/plan` | Switch to Ask mode with planning intent |

Configuration snapshots use the `webbrain-config/1` schema and include all
portable Settings values, including provider, vision, transcription, and
CapSolver API keys, profile data, user memory, custom skills, and permission
choices. The JSON is plaintext and should be stored securely. Device-bound
Cloud Sync sessions/device IDs, conversations, traces, scheduled jobs, usage
counters, and accumulated spend are not exported.

Advanced run-capture suffixes are intentionally omitted from `/help` and
autocomplete. Append `/record [--save-as <filename>]` to the end of a normal
prompt to start recording the current tab immediately before the run, then stop
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
8 changes: 8 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ trailing `/record [--save-as <filename>]` wraps a Chrome run in tab recording,
while trailing `/screenshot [--save-as <filename>]` saves before/after viewport
captures in both browsers. The panel strips the suffix before agent dispatch,
starts capture before `chat`, and finalizes it from the run's `finally` path.

Settings transfer is also slash-driven. `/export --config` asks the background
for an allowlisted, default-resolved `webbrain-config/1` snapshot, and
`/import <json>` or `/import --file` validates that schema before replacing the
portable Settings state and rehydrating providers and live agent settings.
Provider and auxiliary-model API keys are intentionally included in plaintext;
device-bound Cloud Sync credentials and device IDs, conversation/runtime data,
scheduled jobs, usage counters, and spend history are intentionally excluded.
If a run activates another tab, the screenshot finalizer reactivates the
originating run tab before capturing its after state.

Expand Down
14 changes: 14 additions & 0 deletions docs/privacy-and-data-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@ maximum prompt characters injected. `/memory --add <text>` writes an explicit me
immediately without an extractor call. Export/import JSON is local-only and is
the v1 bridge for moving memory between browser profiles.

### Configuration Snapshot Transfer

`/export --config` creates a local plaintext `webbrain-config/1` JSON file, and
`/import <json>` or `/import --file` reads that snapshot locally before writing
the validated Settings values to extension storage. The snapshot intentionally
includes provider, vision, transcription, and CapSolver API keys as well as
profile text, user memory, custom skills, and saved permission choices. Users
should treat the file like a credential backup and store it securely.

The snapshot does not include device-bound Cloud Sync authentication/session
state, the WebBrain Cloud device ID, conversations, traces, scheduled jobs,
usage counters, or accumulated spend. Import does not upload the JSON to
WebBrain Cloud or to the configured LLM provider.

### Optional Encrypted Cloud Sync

Active WebBrain Cloud subscribers may explicitly enable encrypted profile sync in
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webbrain",
"version": "24.0.2",
"version": "24.1.0",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"private": true,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion src/chrome/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WebBrain Chrome/Edge Extension — Architecture

> Version 24.0.2 · Manifest V3 · Service Worker background
> Version 24.1.0 · Manifest V3 · Service Worker background

## High-Level Overview

Expand Down
2 changes: 1 addition & 1 deletion src/chrome/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "WebBrain",
"version": "24.0.2",
"version": "24.1.0",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"permissions": [
"sidePanel",
Expand Down
46 changes: 46 additions & 0 deletions src/chrome/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ import {
parseUserMemoryExtractionResult,
} from './agent/user-memory.js';
import { PROFILE_SYNC_DATA_KEYS, PROFILE_SYNC_KEYS, ProfileSyncManager } from './profile-sync.js';
import {
CONFIG_STORAGE_KEYS,
createConfigExport,
parseConfigImport,
} from './config-transfer.js';

/**
* WebBrain Service Worker (Background Script)
Expand Down Expand Up @@ -1960,6 +1965,47 @@ async function handleMessage(msg, sender) {
return { ok: true, ...(await agent.exportTraces(tabId)) };
}

case 'export_config': {
const stored = await chrome.storage.local.get(CONFIG_STORAGE_KEYS);
const config = createConfigExport(stored, {
locale: msg.locale,
webbrainVersion: chrome.runtime.getManifest().version,
});
return {
ok: true,
json: JSON.stringify(config, null, 2),
settingCount: CONFIG_STORAGE_KEYS.length,
};
}

case 'import_config': {
const imported = parseConfigImport(msg.json);
await chrome.storage.local.set(imported.settings);
await providerManager.load();
await Promise.all([
loadMaxSteps(),
loadClarifyTimeout(),
loadAutoScreenshot(),
loadSiteAdapters(),
loadScreenshotRedaction(),
loadStrictSecretMode(),
loadProfile(),
syncAgentUserMemoryFromStorage(),
loadCustomSkills(),
loadCaptchaSolver(),
loadPlanBeforeAct(),
loadPlanReviewSettings(),
loadApiMutationObserverSetting(),
agent._ensureGateSetting({ force: true }),
]);
agent._refreshSystemPrompts();
return {
ok: true,
settingCount: CONFIG_STORAGE_KEYS.length,
ignoredKeys: imported.ignoredKeys,
};
}

case 'get_progress': {
const tabId = msg.tabId || sender.tab?.id;
if (!tabId) return { ok: false, error: 'No tab ID' };
Expand Down
213 changes: 213 additions & 0 deletions src/chrome/src/config-transfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import {
CUSTOM_SKILLS_STORAGE_KEY,
DEFAULT_SKILLS_REMOVED_STORAGE_KEY,
} from './agent/skills.js';
import {
USER_MEMORY_AUTO_CAPTURE_KEY,
USER_MEMORY_DEFAULT_MAX_PROMPT_CHARS,
USER_MEMORY_ENABLED_KEY,
USER_MEMORY_FORM_CAPTURE_KEY,
USER_MEMORY_MAX_PROMPT_CHARS_KEY,
USER_MEMORY_STORAGE_KEY,
} from './agent/user-memory.js';

export const CONFIG_SCHEMA = 'webbrain-config/1';
export const MAX_CONFIG_IMPORT_CHARS = 10_000_000;

// This is intentionally an allowlist of user-controlled Settings state. It
// excludes conversations, traces, schedules, usage counters, the WebBrain
// Cloud device ID, and Cloud Sync session/token metadata.
export const DEFAULT_CONFIG_SETTINGS = Object.freeze({
wbLocale: 'en',
themeMode: 'system',
verboseMode: false,
selectionShortcutEnabled: true,
helpImproveWebBrain: true,
screenshotFallback: true,
maxAgentSteps: 130,
requestTimeoutMs: 120_000,
clarifyTimeoutSec: 60,
clarifyTimeoutSemanticsV2: true,
autoScreenshot: 'state_change',
useSiteAdapters: true,
voiceInputEnabled: true,
apiMutationObserverEnabled: false,
planBeforeActMode: 'try',
planBeforeAct: true,
planReviewMode: 'confidence',
planReviewConfidenceThreshold: 75,
notifySound: true,
completionConfetti: true,
tracingEnabled: false,
strictSecretMode: false,
agentAllowLocalNetwork: false,
scheduledTasksEnabled: true,
scheduledRequireConsequentialConfirmation: true,
providerFilter: 'all',
costAllowanceSessionUsd: 10,
costAllowanceTotalUsd: 10,
screenshotRedaction: false,
askBeforeConsequentialActions: true,
wb_permissions: [],
providers: {},
activeProvider: 'webbrain_cloud',
visionModel: null,
transcriptionModel: null,
profileEnabled: false,
profileText: '',
[USER_MEMORY_STORAGE_KEY]: { version: 1, records: [] },
[USER_MEMORY_ENABLED_KEY]: true,
[USER_MEMORY_AUTO_CAPTURE_KEY]: false,
[USER_MEMORY_FORM_CAPTURE_KEY]: false,
[USER_MEMORY_MAX_PROMPT_CHARS_KEY]: USER_MEMORY_DEFAULT_MAX_PROMPT_CHARS,
[CUSTOM_SKILLS_STORAGE_KEY]: [],
[DEFAULT_SKILLS_REMOVED_STORAGE_KEY]: [],
captchaSolverEnabled: false,
capsolverApiKey: '',
});

export const CONFIG_STORAGE_KEYS = Object.freeze(Object.keys(DEFAULT_CONFIG_SETTINGS));
const CONFIG_STORAGE_KEY_SET = new Set(CONFIG_STORAGE_KEYS);

const BOOLEAN_KEYS = new Set([
'verboseMode',
'selectionShortcutEnabled',
'helpImproveWebBrain',
'screenshotFallback',
'clarifyTimeoutSemanticsV2',
'useSiteAdapters',
'voiceInputEnabled',
'apiMutationObserverEnabled',
'planBeforeAct',
'notifySound',
'completionConfetti',
'tracingEnabled',
'strictSecretMode',
'agentAllowLocalNetwork',
'scheduledTasksEnabled',
'scheduledRequireConsequentialConfirmation',
'screenshotRedaction',
'askBeforeConsequentialActions',
'profileEnabled',
USER_MEMORY_ENABLED_KEY,
USER_MEMORY_AUTO_CAPTURE_KEY,
USER_MEMORY_FORM_CAPTURE_KEY,
'captchaSolverEnabled',
]);
const NUMBER_KEYS = new Set([
'maxAgentSteps',
'requestTimeoutMs',
'clarifyTimeoutSec',
'planReviewConfidenceThreshold',
'costAllowanceSessionUsd',
'costAllowanceTotalUsd',
USER_MEMORY_MAX_PROMPT_CHARS_KEY,
]);
const STRING_KEYS = new Set([
'wbLocale',
'themeMode',
'autoScreenshot',
'planBeforeActMode',
'planReviewMode',
'providerFilter',
'activeProvider',
'profileText',
'capsolverApiKey',
]);
const ARRAY_KEYS = new Set([
'wb_permissions',
CUSTOM_SKILLS_STORAGE_KEY,
DEFAULT_SKILLS_REMOVED_STORAGE_KEY,
]);
const NULLABLE_OBJECT_KEYS = new Set(['visionModel', 'transcriptionModel']);

function isPlainObject(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}

function clone(value) {
return structuredClone(value);
}

function sanitizeProviders(value, { strict = false } = {}) {
if (!isPlainObject(value)) return {};
const providers = clone(value);
for (const [id, config] of Object.entries(providers)) {
if (!isPlainObject(config)) {
if (strict) throw new Error(`Invalid provider configuration "${id}".`);
delete providers[id];
continue;
}
delete config.deviceGuid;
}
return providers;
}

function validSettingValue(key, value) {
if (BOOLEAN_KEYS.has(key)) return typeof value === 'boolean';
if (NUMBER_KEYS.has(key)) return typeof value === 'number' && Number.isFinite(value);
if (STRING_KEYS.has(key)) return typeof value === 'string';
if (ARRAY_KEYS.has(key)) return Array.isArray(value);
if (NULLABLE_OBJECT_KEYS.has(key)) return value === null || isPlainObject(value);
if (key === 'providers' || key === USER_MEMORY_STORAGE_KEY) return isPlainObject(value);
return true;
}

function normalizeSettings(source, { strict = false } = {}) {
const settings = clone(DEFAULT_CONFIG_SETTINGS);
if (!isPlainObject(source)) {
if (strict) throw new Error('Configuration settings must be a JSON object.');
return settings;
}

for (const key of CONFIG_STORAGE_KEYS) {
if (!Object.hasOwn(source, key)) continue;
const value = source[key];
if (!validSettingValue(key, value)) {
if (strict) throw new Error(`Invalid value for configuration setting "${key}".`);
continue;
}
settings[key] = clone(value);
}
settings.providers = sanitizeProviders(settings.providers, { strict });
return settings;
}

export function createConfigExport(stored = {}, options = {}) {
const settings = normalizeSettings(stored);
if (typeof options.locale === 'string' && options.locale) settings.wbLocale = options.locale;
return {
schema: CONFIG_SCHEMA,
exportedAt: new Date(options.exportedAt ?? Date.now()).toISOString(),
webbrainVersion: String(options.webbrainVersion || 'unknown'),
warning: 'Contains plaintext provider API keys and other sensitive Settings data. Store securely.',
settings,
};
}

export function parseConfigImport(json) {
const text = String(json || '');
if (text.length > MAX_CONFIG_IMPORT_CHARS) throw new Error('Configuration JSON is too large.');
if (!text.trim()) throw new Error('Paste configuration JSON or use /import --file.');

let parsed;
try {
parsed = JSON.parse(text);
} catch {
throw new Error('Configuration is not valid JSON.');
}
if (!isPlainObject(parsed) || parsed.schema !== CONFIG_SCHEMA) {
throw new Error(`Expected a ${CONFIG_SCHEMA} export.`);
}
if (!isPlainObject(parsed.settings)) {
throw new Error('Configuration settings must be a JSON object.');
}

return {
settings: normalizeSettings(parsed.settings, { strict: true }),
ignoredKeys: Object.keys(parsed.settings).filter((key) => !CONFIG_STORAGE_KEY_SET.has(key)),
sourceVersion: typeof parsed.webbrainVersion === 'string' ? parsed.webbrainVersion : '',
};
}
Loading