返回知识中心
身份

按任务隔离 AI Agent 浏览器配置文件

为每个 Agent 任务分配独立配置文件,验证存储分离,并理解共享配置文件的网络身份边界。

BotBrowser Team

文档中心

想直接进入 身份 文档吗?

这篇文章属于知识中心的指南库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

一个需要处理多项任务的 Agent,必须在每次浏览器启动前得到明确的配置文件分配。BotBrowser 可以使用运营方提供的配置文件启动并展示页面结果,但它不会安排任务、选择分配,也不保证检测器如何判定会话。taskId 与 profileId 的映射由运营方负责。

TL;DR

  • 启动前创建任务到配置文件的映射;重试不能静默继承上一任务的配置文件。
  • 为每个配置文件提供独立存储分区和已批准的网络策略。共享配置文件可能把 Cookie、本地存储、权限和相同的配置路线带给下一任务。
  • 使用下面的可运行 fixture,以两个持久目录证明隔离,再用故意共享目录的变体展示泄漏。
  • 通过 fixture 只能证明页面可读到的内容,不能证明检测器结论、启动浏览器的进程,或符合第三方平台限制。

目录

两条任务通道获得独立配置文件和存储,共享配置文件通道显示 Cookie 与本地存储泄漏

为什么 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 目录可用就继承它。

来源

实践结论

每次 Agent 任务启动前分配 profileId、存储和网络策略。使用两个目录以及故意共享目录的失败案例证明存储边界,再通过运营方控制的 endpoint 检查网络。让页面观察、启动证据和第三方分类各自保持独立。这样按任务隔离 Agent 配置文件就可复核,同时不承诺检测器结论,也不忽略任何平台规则。

#Ai Agents#配置文件#Isolation#存储#Network Identity

本文属于指南: 浏览器身份表面:一致性实用总览

在 BotBrowser 里跑一遍同样的检查

加载 profile,让刚读到的这些信号在浏览器内部保持一致,并在正式上线前先在自己的机器上确认结果。