Agentic Browser Memory Layer: Durable, Privacy‑Safe State for Auto‑Agent AI Browsers via Storage Partitioning, Encrypted Session Vaults, and Cross‑Tab Task Graphs
Modern auto‑agent systems running in the browser need memory that is durable enough to span long tasks, yet constrained and provably safe enough to operate in untrusted, poly‑origin environments. The tension is obvious: the more you remember, the more you risk. The browser is simultaneously the most secure and the most hostile runtime you can choose.
This article proposes a concrete architecture for an agentic browser memory layer that is durable, privacy‑safe, and operations‑friendly. The core ideas are:
- Per‑origin storage partitioning for attack surface reduction and coherent policy enforcement.
- Encrypted session vaults with TTLs and leases to constrain blast radius and enable safe concurrent access.
- Cross‑tab orchestration using web locks, broadcast channels, and a task DAG abstraction.
- PII minimization at ingress, with field‑level encryption, bucketing, and redaction.
- Replayable snapshots built on event sourcing plus compression for deterministic, auditable replays.
- CI/CD cleanup hooks that perform schema upgrades, quotas, purges, and redaction checks as part of release hygiene.
The target audience is engineers building browser‑resident AI agents: extension authors, enterprise IT integrators, and product teams who want their agents to be helpful without behaving like spyware.
The design is opinionated: favor simpler primitives shipped in modern browsers (IndexedDB, SubtleCrypto, Web Locks, BroadcastChannel, Service Workers) over elaborate custom infra. When you need more, layer capabilities like Native Messaging or an extension service worker. Keep state narrowly scoped, aggressively expired, and continuously audited.
Executive summary
- Use the browser’s storage partitioning as your first boundary. Organize memory per top‑level site and per agent capability. Avoid cross‑site data flows unless you can prove need and consent.
- Treat all agent memory as encrypted at rest. Derive per‑origin, per‑session keys from a user‑bound root secret; enforce TTLs and renewable leases.
- Coordinate work across tabs with Web Locks and BroadcastChannel; model agent plans as a DAG of tasks with explicit inputs and outputs.
- Minimize PII by default. Detect, classify, and redact early. Prefer salted hashes, tokens, and reversible encryption only when needed.
- Make memory replayable. Persist an event log plus snapshots so that you can reproduce, audit, and debug behavior deterministically.
- Bake cleanup into your release pipeline. Schema migrations, TTL sweeps, quota checks, and DLP lints should run on every deploy.
Problem framing and threat model
Browser agents differ from server‑side agents in three important ways:
- The browser’s origin model (same‑origin policy, storage partitioning) is your default boundary; violating it invites data exfiltration.
- The agent shares a renderer with human browsing, extensions, and third‑party scripts. XSS and compromised libraries are practical threats.
- Persistence is a privilege. Indexes, quotas, and device loss matter. Assuming reliable backend storage undermines privacy goals and user trust.
Threats to explicitly model:
- In‑tab XSS or supply‑chain JS compromise attempting to read agent memory.
- Another tab from a different top‑level site attempting to coerce cross‑origin access.
- Malicious or over‑privileged extensions.
- Device theft, disk inspection, or profile sync misuse.
- Timing and side‑channel leakage across same‑process renderers.
- Accidental retention of high‑risk PII beyond intended TTLs.
Non‑goals for this design:
- Perfect secrecy against a fully compromised browser or OS. If the platform is rooted, assume all bets are off.
- Near‑infinite storage. We target practical quotas and predictable performance.
Architectural overview
The Agentic Browser Memory Layer (ABML) is a set of conventions and lightweight libraries that runs in three deployment modes:
- In‑page agent: runs in the page’s JS context or an injected content script.
- Extension agent: runs in an extension service worker and communicates with content scripts.
- Hybrid with Native Messaging: secrets sealed via an OS keystore through a native host.
Core components:
- Partitioned store: an IndexedDB namespace per top‑level site and per agent capability.
- Encrypted session vault: an encrypted key‑value store with TTL and renewable leases.
- Task graph orchestrator: a cross‑tab coordinator exposing a DAG abstraction.
- PII gate: a module that classifies, redacts, and encrypts sensitive fields at ingress.
- Snapshot engine: event log plus periodic compressed snapshots for replay.
- Ops hooks: lifecycle tasks triggered on service worker activation or version bump.
The rest of this document details each component with practical patterns and reference snippets.
Per‑origin storage partitioning as a first principle
Storage partitioning reduces cross‑site tracking by scoping third‑party storage to the top‑level site context. Use it to scope your agent memory even if you operate in first‑party contexts.
Key practices:
- Adopt top‑level site + agent capability as your partition key: e.g., example.com::summarizer, example.com::navigator.
- Never write to a shared, global store unless the user opts into cross‑site memory.
- Prefer IndexedDB as your primary store. It is async, transactional, and quota‑aware.
- Use Cache Storage for fetch artifacts and blob payloads; keep the DB for metadata.
Useful references:
- Storage partitioning proposals and implementation notes: see Chromium and WebKit docs.
- CHIPS (partitioned cookies) and First‑Party Sets for nuanced cookie behaviors.
Partitioned database layout
- Database name: abml::<topLevelSite>::<agentId>
- Object stores:
- kv: encrypted key‑value pairs; key is logical path
- events: append‑only event log
- leases: single row for lock and TTL metadata
- tasks: task DAG nodes and edges
- idx: small secondary indexes for fast queries
Avoid cross‑partition reads. If you must, go through an explicit export/import flow with user consent and clear logging.
Encrypted session vaults with TTLs and leases
Encrypt all agent memory at rest. Assume any tab or extension code could attempt to read local storage. Defense in depth means the DB is opaque without a runtime key.
Design goals:
- Each partition has a unique symmetric key.
- Keys are derived from a user‑bound root secret and scoped by partition plus session.
- Vault entries include TTLs; background GC sweeps expired entries.
- Leases provide advisory locks for cross‑tab concurrency with fail‑open renewal.
Key derivation
If you operate entirely in the web sandbox, you can derive keys from a user secret (passphrase, passkey via WebAuthn) and store only a wrapped key in IndexedDB. In an extension, you can also seal a root key via Native Messaging to the OS keystore.
Reference flow (SubtleCrypto):
// utilities
const text = s => new TextEncoder().encode(s)
async function importKeyMaterial(bytes) {
return crypto.subtle.importKey('raw', bytes, 'PBKDF2', false, ['deriveKey'])
}
async function deriveAesGcmKey(rootBytes, saltBytes, iterations = 200_000) {
const keyMat = await importKeyMaterial(rootBytes)
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: saltBytes, iterations, hash: 'SHA-256' },
keyMat,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
)
}
async function exportRawKey(key) {
const raw = await crypto.subtle.exportKey('raw', key)
return new Uint8Array(raw)
}
Per‑partition key derivation example:
// inputs: userSecret (Uint8Array), topLevelSite, agentId, sessionId
const salt = text(`abml|${topLevelSite}|${agentId}|${sessionId}`)
const aesKey = await deriveAesGcmKey(userSecret, salt)
For better UX, gate root secret provisioning with WebAuthn or passkeys, and derive a wrapping key to store the per‑partition AES key encrypted in IndexedDB. On startup, re‑unlock via the authenticator rather than asking for a password.
Encrypting and storing values
async function encryptJson(aesKey, obj) {
const iv = crypto.getRandomValues(new Uint8Array(12))
const plaintext = new TextEncoder().encode(JSON.stringify(obj))
const ciphertext = new Uint8Array(
await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, plaintext)
)
return { iv: Array.from(iv), ciphertext: Array.from(ciphertext) }
}
async function decryptJson(aesKey, payload) {
const iv = new Uint8Array(payload.iv)
const ct = new Uint8Array(payload.ciphertext)
const plaintext = new Uint8Array(
await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, ct)
)
return JSON.parse(new TextDecoder().decode(plaintext))
}
Store TTL metadata alongside each value. The TTL lives outside the encrypted blob (so the GC can sweep without keys) but never store sensitive fields in plaintext metadata.
// schema example for kv store
// key: logical path (e.g., 'sessions/42/tools/search-cache')
// value: { ttl: epochMs, updatedAt: epochMs, payload: encrypted }
Implementing leases
The Web Locks API provides a standard way to coordinate across tabs and workers. Use a single lock name per partition and sub‑locks for shared resources.
const leaseName = `abml:lease:${topLevelSite}:${agentId}`
async function withLease(cb) {
return navigator.locks.request(leaseName, { mode: 'exclusive' }, cb)
}
// pattern: renew a soft TTL inside the lease
async function renewLease(db, ttlMs) {
const now = Date.now()
const until = now + ttlMs
await putLeaseRow(db, { until, renewedAt: now })
}
If Web Locks is unavailable, implement an advisory lease in IndexedDB using compare‑and‑swap on a single row with a monotonic timestamp. Include a small grace period to avoid thundering herds on expiration.
GC sweeps
Run a sweep at startup and periodically via a service worker alarm or a BroadcastChannel heartbeat. Delete expired rows from kv, tasks, and events; compact if size thresholds are hit. Use StorageManager.estimate() to back‑off when usage approaches quota.
Cross‑tab orchestration with a task DAG
Agents do multi‑step work: browse, search, extract, synthesize, and act. Model these as a DAG with explicit inputs and outputs, persisted in the tasks store.
Key properties:
- Node: id, type, inputs, outputs, status (pending, running, done, failed), ttl, createdAt, updatedAt.
- Edge: from, to, condition (optional predicate over outputs).
- Deterministic id derivation from content so nodes are idempotent.
Cross‑tab coordination primitives:
- Web Locks: choose a leader to schedule work and manage leases.
- BroadcastChannel: notify workers across tabs about new nodes and status changes.
- SharedWorker or extension service worker: run long‑lived executors detached from page lifecycle.
Minimal orchestrator skeleton
class TaskOrchestrator {
constructor({ topLevelSite, agentId, db }) {
this.site = topLevelSite
this.agentId = agentId
this.db = db
this.bc = new BroadcastChannel(`abml:${topLevelSite}:${agentId}:tasks`)
this.bc.onmessage = e => this.onMessage(e.data)
}
async start() {
await navigator.locks.request(
`abml:scheduler:${this.site}:${this.agentId}`,
{ mode: 'exclusive', steal: false },
async () => {
await this.loop()
}
)
}
async loop() {
while (true) {
const next = await this.takeRunnableNode()
if (!next) {
await this.idle()
continue
}
this.dispatch(next)
}
}
async dispatch(node) {
this.bc.postMessage({ type: 'run', nodeId: node.id })
}
onMessage(msg) {
if (msg.type === 'done') {
this.updateNode(msg.nodeId, { status: 'done', outputs: msg.outputs })
}
}
}
In executors, listen for run messages, claim a per‑node lock, read inputs, execute, write outputs, and signal completion.
Idempotency and replay safety
- Derive a node id as a hash of type + normalized inputs.
- Cache outputs keyed by node id. Rewrites simply update updatedAt.
- Design tasks to be pure where possible; side‑effects (e.g., network posts) should be encapsulated in special nodes with replay guards.
PII minimization: classify, redact, encrypt
Default stance: treat all inbound data as potentially sensitive. Minimize on write; never rely on ad‑hoc redaction later.
Techniques:
- Field classification: label fields as public, quasi‑identifier, sensitive.
- Bucketing and hashing: replace exact values with coarse bins or salted hashes when exact recall is unnecessary.
- Tokenization: store a reversible encrypted token only when the workflow requires reidentification.
- Prompt‑time redaction: for LLM calls, mask sensitive fields before constructing prompts; optionally attach retrieval hooks that rehydrate just‑in‑time.
Lightweight PII gate
const PII = {
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
phone: /\b\+?[0-9]{7,15}\b/g,
cc: /\b(?:\d[ -]*?){13,19}\b/g
}
function classifyFields(obj) {
// naive heuristic for demonstration
const json = JSON.stringify(obj)
const findings = []
for (const [type, rx] of Object.entries(PII)) {
if (rx.test(json)) findings.push(type)
}
return findings
}
function redact(obj) {
// apply regex‑based masks in serialized form
let s = JSON.stringify(obj)
s = s.replace(PII.email, '[email]')
s = s.replace(PII.phone, '[phone]')
s = s.replace(PII.cc, '[card]')
return JSON.parse(s)
}
async function minimizeForStorage(aesKey, obj, policy) {
const findings = classifyFields(obj)
let data = obj
if (policy.redact) data = redact(data)
// selectively encrypt fields at rest while keeping indexable metadata minimal
const payload = await encryptJson(aesKey, data)
return { payload, pii: findings, updatedAt: Date.now() }
}
For higher fidelity, add an on‑device classifier (small WASM model) and a persistent allowlist of fields you permit to store in clear for indexing. Everything else goes into the encrypted payload.
Field‑level encryption
When you must search by a field, e.g., an order id, store a keyed salted hash of the value for lookup, and encrypt the original.
async function keyedHash(key, msg) {
const macKey = await crypto.subtle.importKey(
'raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
)
const mac = await crypto.subtle.sign('HMAC', macKey, new TextEncoder().encode(msg))
return Array.from(new Uint8Array(mac)).map(b => b.toString(16).padStart(2, '0')).join('')
}
Replayable snapshots: event sourcing meets compression
Deterministic replay is essential for debugging and audits. Store an append‑only event log with sufficient information to rebuild state and understand agent decisions.
Event types to include:
- observation: DOM extraction, network response summary
- decision: model prompts and hashed outputs
- action: clicks, form submits, fetch posts, with ids and nonces
- mutation: writes to kv; include before and after hashes
- error: stack, message, causal chain
Do not store raw PII. For prompts and responses, store redacted versions and salted hashes of raw text. For network responses, store headers and a sketch of body content (e.g., MinHash) for change detection.
Snapshotting strategy
- Keep the event log append‑only.
- Periodically write a compacted snapshot of derived state and a watermark index into events.
- Use the Compression Streams API to compress large blobs before storing.
async function compressBytes(bytes) {
const cs = new CompressionStream('gzip')
const writer = cs.writable.getWriter()
writer.write(bytes)
writer.close()
const res = await new Response(cs.readable).arrayBuffer()
return new Uint8Array(res)
}
async function snapshotState(db) {
const state = await exportDerivedState(db) // app‑specific
const bytes = new TextEncoder().encode(JSON.stringify(state))
const gz = await compressBytes(bytes)
await putSnapshotRow(db, { createdAt: Date.now(), payload: Array.from(gz) })
}
Replayer skeleton
async function* eventsFrom(db, watermark) {
// yield events after watermark in order
}
async function replay(db, toWatermark) {
let state = await latestSnapshot(db)
for await (const ev of eventsFrom(db, state.watermark)) {
state = apply(state, ev)
if (ev.id === toWatermark) break
}
return state
}
Replays should run in a locked context, offline by default, and never fetch external resources unless explicitly permitted.
Storage choices and patterns
IndexedDB is the default choice. It provides:
- Transactions and object stores with indexes.
- Structured clone for complex objects.
- Quota integration.
Patterns:
- Use a tiny wrapper to unify gets/puts and versioned schema migrations.
- Split large payloads across Cache Storage for blobs and IDB for metadata.
- Chunk values above a threshold and write in a single transaction.
function openDb(name, version, upgrade) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, version)
req.onupgradeneeded = e => upgrade(req.result, e.oldVersion, e.newVersion)
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}
async function put(store, key, val) {
return new Promise((resolve, reject) => {
const tx = store.db.transaction(store.name, 'readwrite')
const os = tx.objectStore(store.name)
os.put(val, key)
tx.oncomplete = () => resolve()
tx.onerror = () => reject(tx.error)
})
}
Coordinating tabs and workers
Use a combination of:
- Web Locks for mutual exclusion.
- BroadcastChannel for pub‑sub style updates.
- Service Worker or SharedWorker for long‑lived agents.
Leader election is trivial with Web Locks; the holder of abml:scheduler:<site>:<agentId> is the leader. On release, tabs will compete to acquire the lock and one will continue scheduling.
Back‑pressure strategies:
- Track a moving window of runnable tasks sized by StorageManager.estimate() and CPU heuristics (e.g., navigator.hardwareConcurrency).
- Use an adaptive retry policy with jitter for failed network tasks.
Putting it together: reference workflow
- On first run, provision a root secret via WebAuthn or user passphrase.
- Derive a per‑partition AES key and unlock the vault.
- Initialize the DB schema; write a lease row with a short TTL.
- Start the scheduler; acquire the leadership lock and broadcast readiness.
- As tasks arrive, pass their inputs through the PII gate; store minimized, encrypted payloads with TTLs.
- Append event log entries for observations, decisions, actions.
- Periodically snapshot and GC expired rows.
- On version bump, run ops hooks to migrate, purge, and reindex.
Security considerations and hardening
- Cross‑origin isolation and COOP/COEP: where feasible, operate in cross‑origin isolated contexts to enable stronger primitives and limit shared memory attacks.
- Content Security Policy: set a strict CSP in extension pages and any agent UI surfaces to minimize XSS risk.
- SameSite and CHIPS: when interacting with cookies, default to SameSite=Lax or Strict; use CHIPS for partitioned third‑party cookies where needed.
- Integrity and provenance: if loading agent scripts dynamically, use Subresource Integrity and version pinning.
- Extension permissions minimization: declare the narrowest set of host permissions; prefer declarativeNetRequest over webRequest when possible.
- Side‑channel awareness: avoid timing‑based fingerprints in cross‑origin data flows; batch operations and add jitter.
Operationalizing memory: CI/CD cleanup hooks
Treat memory hygiene as part of shipping.
Recommended hooks:
- Schema migration: bump IDB version; in onupgradeneeded, migrate or purge old stores.
- TTL sweep: at install and activate, sweep expired items aggressively.
- Quota check: fail the build or log a warning when projected footprint exceeds budgets.
- DLP lint: run a static and dynamic check that no sensitive fields are written in cleartext.
- Snapshot validator: attempt a replay on a canary profile to ensure forward compatibility.
Extension service worker example
self.addEventListener('install', event => {
event.waitUntil((async () => {
const db = await openDb('abml::install', 1, upgradeSchema)
await sweepExpiredAllPartitions(db)
})())
})
self.addEventListener('activate', event => {
event.waitUntil((async () => {
await clients.claim()
await snapshotAllActivePartitions()
})())
})
CLI‑driven maintenance
Build a developer CLI that connects to the extension via runtime messaging and exposes:
- abml ls: list partitions, sizes, and TTL stats
- abml gc: force a GC sweep
- abml dump <partition>: export redacted snapshot for audits
- abml wipe <partition>: secure delete a partition
Secure delete note: you cannot guarantee physical overwrite in browser storage; simulate by deleting references and overwriting with random blobs before delete to mitigate simple recovery, then rely on quota eviction.
Performance and reliability considerations
- Encryption overhead: AES‑GCM in WebCrypto is hardware‑accelerated on most platforms; expect a few microseconds per KB. Chunk large values above 1–4 MB.
- Compression: gzip with CompressionStream is efficient for text; use brotli if available in your target browsers.
- Quotas: Storage quotas vary; use StorageManager.estimate() and keep per‑partition budgets small (e.g., 5–50 MB) to avoid eviction.
- Crash recovery: transactions in IDB are atomic; design writes to be idempotent so replays can heal partial progress.
- Offline behavior: queue network actions as tasks with a precondition check; the scheduler can defer until navigator.onLine is true.
Example: implementing a secure cache for extracted web content
Suppose your agent extracts summaries and citations from a documentation site. You want to cache paragraph embeddings and summaries for 24 hours, searchable by section id, without storing raw PII like emails.
Steps:
- For each section, compute a stable content hash (e.g., SHA‑256 of normalized text).
- Run PII gate to redact emails and phone numbers from the summary; store only redacted summary.
- Encrypt payload { summaryRedacted, embedding, citations } with the partition AES key.
- Store index keys: section id, content hash, createdAt. Use keyed salted hash for any sensitive identifiers.
- Set TTL to now + 24h. GC sweeps will remove stale entries.
async function putSectionSummary(db, aesKey, sectionId, text, citations) {
const normalized = text.replace(/\s+/g, ' ').trim()
const summary = await summarize(normalized) // model call
const red = redact(summary)
const payload = await encryptJson(aesKey, { summary: red, citations })
const ttl = Date.now() + 24 * 3600 * 1000
const key = `sections/${sectionId}`
await putKV(db, key, { ttl, updatedAt: Date.now(), payload })
await appendEvent(db, { type: 'mutation', key, ttl })
}
Testing and verification
- Unit tests: verify encryption round‑trip, TTL expiration, lease renewals, and DAG scheduling logic with mocks.
- Property tests: ensure idempotency of task nodes and purity of apply(state, event).
- Security tests: simulate XSS in a content script and confirm encrypted values are opaque and unusable without a key.
- Privacy tests: feed synthetic data with known PII and validate the PII gate removes or encrypts fields as configured.
- Replay tests: capture a run in a test partition and ensure replay yields identical derived state.
Migration and versioning
Version everything: schema, snapshot format, task node types, and PII policies.
- Keep an explicit registry of task types with semantic versions.
- During onupgradeneeded, provide a migration plan per object store, with safe fallbacks to purge.
- For snapshot format, add a magic header with version and a small index to ease forward compatibility.
What to do when you need cross‑site memory
Sometimes an agent must carry knowledge across sites, like a to‑do list or personal preferences.
- Use a separate, explicit cross‑site partition: abml::global::<agentId>.
- Store only redacted, minimal facts (e.g., embedding vectors, coarse tags).
- Require explicit user consent and provide a UI to inspect and wipe global memory.
- Consider binding decryption to user presence via WebAuthn get() with user verification so a background exfiltration cannot unlock the vault.
Limitations and honest trade‑offs
- Browser storage is not a secure enclave. Encryption at rest mitigates casual and many scripted attacks, but a fully compromised runtime can exfiltrate keys.
- Quotas are limited and eviction can happen; you must design for data loss of cached, derived content.
- Deterministic replay must abstract over dynamic content and network non‑determinism; tests will still require mocks and fixtures.
Checklist: shipping an agentic memory layer safely
- Partition by top‑level site and agent capability.
- Encrypt everything at rest; derive keys per partition and session.
- Enforce TTLs and renewable leases; sweep aggressively.
- Coordinate tabs with Web Locks; communicate with BroadcastChannel.
- Model plans as a DAG; ensure idempotency and replay guards for side‑effects.
- Minimize PII: redact, hash, tokenize; store cleartext only when justified.
- Persist an event log; snapshot periodically with compression; build a replayer.
- Add CI/CD hooks for migration, GC, quota checks, and DLP lints.
- Document user‑facing controls: view, export (redacted), and wipe memory.
Conclusion
An agent that remembers is useful; an agent that remembers safely is trustworthy. In the browser, trust is earned through principled scoping, strong encryption, explicit orchestration, and relentless hygiene. The approach outlined here keeps memory local, partitioned, encrypted, and ephemeral by default, while still giving you the tools to coordinate complex tasks and reproduce behavior when things go wrong.
If you adopt these patterns, you will ship browser‑resident agents that are helpful without being creepy, robust without being brittle, and auditable without exposing users to unnecessary risk.
Further reading
- Web Locks API: MDN and WICG explainer.
- IndexedDB 2.0: MDN docs and Chrome platform status.
- Storage partitioning: Chromium and WebKit posts on partitioned storage and tracking prevention.
- Compression Streams API: MDN.
- Web Crypto API: MDN.
- CHIPS and First‑Party Sets: Chrome developer articles.