Agent 一接浏览器本地存储就开始串租户:从 Storage Namespace 到 Session Snapshot 的工程实战
很多浏览器 Agent 在演示环境里很稳一进多租户后台就开始出现“登的是 B 账号提交的却是 A 组织草稿”的事故。⚠️ 问题不在 DOM 识别而在浏览器本地状态没跟任务边界一起清空。Cookie 已刷新页面却仍会从旧的localStorage、IndexedDB和草稿缓存里恢复出旧租户上下文。这类问题容易误导排障团队。 日志里常见的是按钮点对了真正写入数据时才暴露组织选错和草稿串用。对浏览器 Agent 来说本地存储不是辅助信息而是与账号同等级的执行前提。图 1多租户 Agent 的真正隐患常常藏在浏览器本地状态里浏览器本地存储为什么会把 Agent 带进串租户事故很多执行器只会清 Cookie却默认同域下的其他状态天然安全。 现实正好相反localStorage会记住组织、筛选器和草稿 IDIndexedDB会保留离线数据Cache Storage还可能回放旧接口结果。复用同一个 browser context 时新租户就会踩着旧状态继续执行。更麻烦的是这种污染通常不会立刻报错。 页面标题、URL 和按钮文本都可能正确只有进入编辑、审批或导出阶段时残留状态才把任务拐弯。很多团队以为是模型理解错页面实际是工程层没有证明“当前状态属于当前租户”。️图 2同一个页面能否安全执行取决于恢复出来的是谁的状态一组回放实验把 Cookie 清理和状态隔离的差距拉开这次回放了48条真实浏览器任务覆盖文章发布、审批处理和报表导出。 基线方案只清 Cookie方案二为每个任务新建 context方案三复用 context但为每个租户分配独立storage namespace并只恢复白名单快照。 结果说明账号切换成功不等于状态已经隔离成功。✅方案任务成功率串租户率冷启动开销人工接管率仅清 Cookie68%19%0 ms14%每任务新建 context91%0.8%430 ms4%Namespace Snapshot89%1.2%110 ms5%真正有效的不是“清得更猛”而是“恢复得更准”。️ 全量清空会把登录后必须保留的偏好一起抹掉只清 Cookie 又会留下最危险的同域残影。把状态分成租户级、任务级和临时态再按白名单恢复成功率和时延才能站住。typeSnapshot{tenantId:stringlocal:Recordstring,string}functionrestoreSnapshot(currentTenantId:string,snapshot:Snapshot){if(snapshot.tenantId!currentTenantId){thrownewError(tenant mismatch)}localStorage.clear()for(const[key,value]ofObject.entries(snapshot.local)){if(key.startsWith(draft:)||key.startsWith(tenant:)){localStorage.setItem(key,value)}}}图 3问题不只是账号切换而是任务恢复了哪一份本地状态工程上真正该补的是 Storage Namespace 和 Session Snapshot更稳的做法是把浏览器状态治理成显式契约。⏱️Storage Namespace回答“数据属于哪个租户、哪个任务”Session Snapshot回答“哪些状态允许恢复”。进入关键动作前执行器至少要校验一次租户徽标、草稿 ID 和页面回显是否与快照一致否则宁可回退重开也不要带着脏状态继续提交。另一个常被忽略的点是清理动作也需要生命周期管理。⭐ 用户取消任务、浏览器崩溃重连或人工接管后旧快照如果没有及时失效下一次恢复仍会复活过期状态。更成熟的链路会给快照附上tenant_id、task_id和过期时间并把“命中过期快照率”纳入监控。这样浏览器 Agent 才具备可审计的状态边界。图 4稳定的浏览器 Agent必须先证明状态归属再执行高风险动作未来 3 到 6 个月 浏览器 Agent 会更依赖状态快照治理接下来更值得投入的不是继续给模型塞更多页面截图而是把账号、页面和本地状态收敛成同一条证据链。 一旦系统能回答“当前租户是谁、当前草稿从哪来、当前缓存是否仍有效”浏览器 Agent 的可用性才会跨过演示阶段。串租户事故的根因不是模型不会点而是工程层没把本地状态当成一等公民。你们的浏览器 Agent会在恢复草稿前校验storage fingerprint和租户回显吗