按任务隔离 AI Agent 浏览器配置文件
为每个 Agent 任务分配独立配置文件,验证存储分离,并理解共享配置文件的网络身份边界。
BotBrowser Team
一个需要处理多项任务的 Agent,必须在每次浏览器启动前得到明确的配置文件分配。BotBrowser 可以使用运营方提供的配置文件启动并展示页面结果,但它不会安排任务、选择分配,也不保证检测器如何判定会话。taskId 与 profileId 的映射由运营方负责。
TL;DR
- 启动前创建任务到配置文件的映射;重试不能静默继承上一任务的配置文件。
- 为每个配置文件提供独立存储分区和已批准的网络策略。共享配置文件可能把 Cookie、本地存储、权限和相同的配置路线带给下一任务。
- 使用下面的可运行 fixture,以两个持久目录证明隔离,再用故意共享目录的变体展示泄漏。
- 通过 fixture 只能证明页面可读到的内容,不能证明检测器结论、启动浏览器的进程,或符合第三方平台限制。
目录
为什么 Agent 循环需要配置文件边界
Agent 循环会把一个浏览器工作流变成一串彼此独立的任务。任务 A 可能登录获授权的测试租户,而任务 B 必须在没有该租户状态的情况下开始。如果两个启动都指向同一个持久配置文件,第二个任务在第一次页面操作前就可能继承 Cookie、本地存储、权限、缓存和其他浏览器状态。
Playwright 将 BrowserContext 描述为拥有独立 Cookie 和存储的隔离会话。这个上下文边界很有用,但它不等同于选择 BotBrowser 配置文件。配置文件是启动输入,可能携带超出页面存储范围的设置。配置文件管理指南介绍所有权、版本匹配、保留期限和生命周期;Agent 调度器在为任务分配配置文件时应遵循这些原则。
相邻的多账号浏览器隔离文章面向保持账号分开的人工工作流。Agent 循环还增加了分配问题:每一个任务、重试和 worker 重启都必须得到明确配置文件。面向 AI Agent 的无头浏览器介绍 Agent 到浏览器的交接和进程证据,但不负责证明两次运行之间的身份分离。
在启动前分配配置文件
维护一条小型分配记录,让启动器无需打开受保护的数据包即可验证。记录应包含任务 ID、配置文件 ID、存储目录、网络策略引用、负责人和生命周期状态:
{
"taskId": "tenant-a-checkout-042",
"profileId": "profile-us-east-07",
"storageDir": "/var/lib/agent-sessions/profile-us-east-07",
"networkPolicyRef": "egress-us-east-07",
"state": "approved"
}
在调用 chromium.launchPersistentContext() 或等效的 BotBrowser 启动器前解析这条记录。缺失、已退役或已被租用的分配必须拒绝。重试应引用自己的分配事件,不应使用 worker 恰好留下的目录。
在 Playwright 中,持久上下文让一个浏览器进程使用一个用户数据目录。BotBrowser 启动时,运营方还要提供配置文件参数和已批准的网络设置。把这些值放在同一条分配记录中,只记录稳定标识,不记录配置文件内容或代理密钥。
配置文件分配是运维控制,不是对网站决策的承诺。它让预期边界可以复核,但不能让无关账号变得合规,不能让人忽略平台限制,也不能保证第三方接受会话。
运行存储分离 fixture
下面的 Node.js fixture 使用 Playwright 自带的 Chromium 和本地 HTTP 服务器。它在任务 A 写入无害的 Cookie 与 localStorage 值,在任务 B 读取这些值并输出是否继承。第一次运行给两个任务不同目录;第二次故意复用一个目录来展示泄漏。
// save as profile-isolation-fixture.mjs
import { chromium } from 'playwright';
import { createServer } from 'node:http';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const server = createServer((request, response) => {
response.setHeader('Content-Type', 'text/html');
response.end('<!doctype html><title>profile fixture</title><p data-fixture="profile-isolation">owned fixture</p>');
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
const origin = `http://127.0.0.1:${port}`;
async function runTask(taskId, storageDir, writeState) {
const context = await chromium.launchPersistentContext(storageDir, { headless: true });
const page = await context.newPage();
await page.goto(origin);
if (writeState) {
await page.context().addCookies([{ name: 'task_marker', value: taskId, url: origin }]);
await page.evaluate(value => localStorage.setItem('task_marker', value), taskId);
}
const observed = {
cookie: (await context.cookies()).find(cookie => cookie.name === 'task_marker')?.value ?? null,
localStorage: await page.evaluate(() => localStorage.getItem('task_marker')),
};
await context.close();
return observed;
}
const root = await mkdtemp(join(tmpdir(), 'agent-profile-fixture-'));
const separateA = await runTask('task-a', join(root, 'profile-a'), true);
const separateB = await runTask('task-b', join(root, 'profile-b'), false);
console.log(JSON.stringify({ case: 'separate', taskA: separateA, taskB: separateB }));
const sharedA = await runTask('task-a', join(root, 'shared-profile'), true);
const sharedB = await runTask('task-b', join(root, 'shared-profile'), false);
console.log(JSON.stringify({ case: 'shared', taskA: sharedA, taskB: sharedB }));
await rm(root, { recursive: true, force: true });
server.close();
使用 node profile-isolation-fixture.mjs 运行。预期形状为:
{"case":"separate","taskA":{"cookie":"task-a","localStorage":"task-a"},"taskB":{"cookie":null,"localStorage":null}}
{"case":"shared","taskA":{"cookie":"task-a","localStorage":"task-a"},"taskB":{"cookie":"task-a","localStorage":"task-a"}}
分离案例建立了页面可见的存储边界:任务 B 无法读取任务 A 的标记。共享案例是失败 fixture;两个值都证明第二次运行打开了同一个持久存储。这是可在本地运行的示例,不是 BotBrowser 运行观察结果。
Fixture 刻意使用本地 origin 和合成数据。它不记录凭据、不访问第三方服务,也不声称页面能检查所有浏览器设置。生产检查应保留同样的对比结构,使用已批准的只读 endpoint,并把分配 ID 与结果一起保存。
Fixture 检查什么以及省略什么
目标是用没有业务含义的值让泄漏可见。Cookie 只属于本地 origin,localStorage 由页面写入。由于两个值都在全新启动后读取,任务 B 的非空结果可归因于持久目录,而不是 Node.js 进程中的变量。结束时的清理会删除临时目录,让下次运行从已知状态开始。
Fixture 不检查缓存、service worker 注册、权限、IndexedDB、下载或扩展状态。如果真实工作负载需要这些表面,应在独立且获批准的 fixture 中逐一加入。每次只添加一个命名标记,记录新任务预期的空值,并对外部服务保持只读。
Fixture 也不负责选择 BotBrowser 配置文件。生产启动器可以用已解析的 --bot-profile 参数和已分配目录包装同一任务函数。包装代码应留在页面代码之外,避免把页面观察误认为命令行证据。BotBrowser Playwright 设置介绍启动集成;数据包选择和分配仍由运营方负责。
让预期结果可由机器检查
不要依靠人工比较两行 JSON。调度器可以在新任务看到标记,或共享案例看不到标记时让任务失败:
function requireValue(value, expected, label) {
if (value !== expected) throw new Error(`${label}: expected ${expected}, got ${value}`);
}
requireValue(separateB.cookie, null, 'separate cookie');
requireValue(separateB.localStorage, null, 'separate localStorage');
requireValue(sharedB.cookie, 'task-a', 'shared cookie');
requireValue(sharedB.localStorage, 'task-a', 'shared localStorage');
把断言放在 fixture 输出旁边,不要放进只检查页面加载的通用健康检查。发生失败时,在受限记录中写入 taskId、profileId、存储路径标签、浏览器版本和 fixture 结果。不要把真实 Cookie 或代理密钥写入共享日志。
把网络身份作为独立表面检查
存储分离不会自动分离网络身份。为每个配置文件绑定已批准的网络策略和区域,然后通过运营方控制的 endpoint 检查路线,让它返回请求的公网地址和区域。把结果记录在 profileId 旁边,不要从 Cookie 或页面加载成功推断。
如果运营方为两个任务分配了同一个网络策略引用,它们就会使用同一条已配置的代理和路由;仅仅复用配置文件本身,并不能证明网络路线也相同。BotBrowser 的代理配置指南分别说明了启动参数和网络路由输入。这是配置核验,不是检测器结论。更换存储目录本身不能把一条已配置的路线变成两条。
| 表面 | 示例检查 | 能证明什么 | 单独不能证明什么 |
|---|---|---|---|
| 存储 | task_marker Cookie 和 localStorage | 页面可见状态已分开 | 可执行文件、配置文件数据包或公网 IP |
| 网络 | 运营方拥有的回显 endpoint | 此时请求使用记录中的路线 | 第三方分类或未来路线稳定性 |
| 启动 | 脱敏命令和分配记录 | 运营方提供了预期的配置文件输入 | 所有页面信号都符合策略 |
BotBrowser 的按上下文指纹文档描述上下文范围内的身份输入,配置文件管理文档覆盖生命周期和所有权。用这些来源定义已批准的分配。面向 AI Agent 的无头浏览器说明了页面结果与进程证据必须分开;检查网络结果时也要保持这种区分。
Endpoint 响应应包含时间和分配引用。代理轮换、基础设施故障转移或策略更新都可能改变公网地址和区域。把它当作本次请求的测量值,而不是配置文件的永久属性。如果任务需要稳定区域,调度器应在打开页面前拒绝不符合策略的路线,并在路线不可用时关闭会话。
不要用浏览器侧猜测替代路线检查。navigator.language、时区或地理位置权限结果描述的是浏览器设置或权限,单独都无法识别公网网络路径。由调度器所属组织控制的 endpoint 可以把响应关联到分配,而不需要让第三方网站分类会话。响应应保持最小化,广泛报告要脱敏,完整记录只保留批准的期限。
关闭、重试并审计每次分配
隔离依赖生命周期管理,不只是启动命令。按以下顺序关闭任务:停止接收新工作,让页面进入受控结束状态,收集 fixture 或应用结果,关闭上下文,并记录存储处置。完成这些步骤后,调度器才能把 worker 租给下一任务。如果重分配目录时进程仍在写 Cookie,可能产生看似随机的泄漏。
每次尝试都使用不可变的分配事件。重试要创建新事件并记录诸如 navigation-timeout、fixture-mismatch 或 route-unavailable 的原因。保留上一事件及其结果,不要覆盖原来的 profileId。这样才能判断故障属于应用、启动器还是配置文件数据包。
| 启动前检查 | 在何时拒绝 | 目的 |
|---|---|---|
| 配置文件状态 | 缺失、为 retired 或未批准 | 防止未经审查的身份接收任务 |
| 存储租约 | 目录正被另一分配使用 | 避免并发写入和跨任务状态 |
| 网络策略 | 引用缺失或区域不符 | 明确路线所有权 |
| 浏览器组合 | 版本与已批准记录不一致 | 避免未审查的组合 |
| 前次关闭 | 上一拥有者未干净关闭 | 防止重用脏目录 |
检查拒绝启动时,给队列返回简短原因,把详细记录写入受限支持渠道。“存储租约仍处于活动状态”有助于操作人员;把数据包内容或完整命令行倒入共享日志没有帮助。配置文件管理介绍了让这些检查可审计的所有权与退役状态。
复核时比较三份记录:分配事件、页面可见 fixture 输出,以及运营方的启动或路线证据。它们应在任务 ID 和时间窗口上对应,但回答的是不同问题。相同 Cookie 不证明路线相同;相同路线不证明打开了预期配置文件;相同命令不证明加载了预期 origin。
对于长期运行的 worker,应在策略定义的边界安排干净重启,而不是无限期保持分配。重启不能替代隔离,但能暴露单进程中隐藏的关闭问题。记录重启是否预期、释放了哪个配置文件,以及下一任务得到的是新目录还是仍获批准的持久目录。
同一记录也能帮助处理事件。如果租户看到上一任务的横幅,先比较两个分配事件和存储标签,再检查受影响 worker 的 fixture 输出。如果只有一个 worker 显示标记,就隔离该目录并暂停新任务,等待负责人检查关闭。如果所有 worker 都显示标记,先检查调度器映射和共享卷,再改页面代码。
如何解释结果及其限制
把干净的对比当作狭窄的验收结果。它表示该 fixture 在这台机器上看到了预期存储值,并且(如果单独检查)运营方 endpoint 返回了记录的网络结果。它不表示网站会接受会话、浏览器无法与其他浏览器区分,或检测器会返回某个固定结论。
在负责该故障的层面调查。分离案例出现非空标记,指向存储目录、关闭或分配错误。网络结果变化,指向路线策略、代理健康或 endpoint 解释。启动器不匹配是进程证据,不能通过让 Agent 检查页面 JavaScript 来“修复”。
不要使用按任务配置文件来忽略账号限制或平台条款。隔离适合已获授权的租户、测试用例和隐私边界。凭据、同意、保留期限、速率限制及服务规则仍由运营方负责。
常见问题
两个 Agent 任务使用一个 Playwright BrowserContext 足够吗?
只有在上下文按照明确分配策略创建和关闭时才足够。上下文可以隔离 Cookie 和存储,但不能替代每项任务对 BotBrowser 配置文件、网络策略和负责人的决定。
Fixture 能证明两个 BotBrowser 配置文件已启动吗?
不能。它只能证明本地页面读到的存储值。应通过运营方进程证据确认可执行文件、脱敏配置文件参数和分配记录。
独立配置文件能保证检测器结果吗?
不能。配置分离消除了一个意外共享状态的类别。网站会组合许多信号并改变规则,可能因为与该 fixture 无关的原因分类会话。
失败后重试应该做什么?
关闭旧上下文,保存它的 ID 和结果,然后根据工作负载策略获取新的已批准分配。不要因为 worker 目录可用就继承它。
来源
- Playwright BrowserType API
- Playwright 浏览器上下文
- BotBrowser Playwright 设置
- BotBrowser 配置文件管理
- BotBrowser 按上下文指纹
- BotBrowser 代理配置
实践结论
每次 Agent 任务启动前分配 profileId、存储和网络策略。使用两个目录以及故意共享目录的失败案例证明存储边界,再通过运营方控制的 endpoint 检查网络。让页面观察、启动证据和第三方分类各自保持独立。这样按任务隔离 Agent 配置文件就可复核,同时不承诺检测器结论,也不忽略任何平台规则。
本文属于指南: 浏览器身份表面:一致性实用总览