openbot
§ · Nested detail page

Computer Gateway 深度解析

这一页只看 OpenBot 的一个局部模块,用真实源码行号解释它承担的契约、失败路径和与主架构的关系。

§ 00 · IMPORT CHAIN

导入链与契约

▸Gateway 不是 HTTP proxy

Gateway 的导入链显示它同时依赖 audit、computer transport、target check、policy evaluator、provider 和 schema。Gateway 工厂从 server/src/computer/gateway.ts:205 建立,snapshot map 在 server/src/computer/gateway.ts:216 持有;locate 是 server/src/computer/gateway.ts:229,GET helper 在 server/src/computer/gateway.ts:238,POST helper 在 server/src/computer/gateway.ts:252;snapshot 调用从 server/src/computer/gateway.ts:266 进入并在 server/src/computer/gateway.ts:273 写缓存 说明它不是 HTTP proxy,而是动作治理层:provider 给地址,transport 发请求,policy 做判断,audit 写事实。

src/server/src/computer/gateway.ts · L20-63TypeScript
20import { type AuditStore, recordAuditEvent } from "../audit";
21import { ComputerUnavailableError, createComputerTransport } from "./client";
22import { checkComputerAddress } from "./target";
23export {
24  ComputerUnavailableError,
25  ElementNotFoundError,
26  NavigationRefusedError,
27  StaleSnapshotError,
28  WorkspaceRefusedError,
29  WorkspaceRequestError,
30} from "./client";
31import {
32  type ActionPolicy,
33  evaluateActionPolicy,
34  type PolicyContext,
35  type PolicyDecision,
36} from "./policy";
37import type { ComputerProvider } from "./provider";
38import type {
39  ActionResult,
40  ClickInput,
41  ComputerStatus,
42  ControlState,
43  HumanInput,
44  HumanInputResult,
45  KeyInput,
46  ListFilesInput,
47  ListFilesResult,
48  NavigateResult,
49  ReadFileInput,
50  ReadFileResult,
51  ReadResult,
52  ScreenshotResult,
53  ScrollInput,
54  SecretRequest,
55  SecretResult,
56  SnapshotElement,
57  SnapshotResult,
58  TypeInput,
59  RunCommandInput,
60  RunCommandResult,
61  WriteFileInput,
62  WriteFileResult,
63} from "./schema";
§ 01 · CORE IMPL

核心实现,逐段拆

▸snapshot cache 是策略上下文的来源

ref 解析由 server/src/computer/gateway.ts:294 执行,govern 注释从 server/src/computer/gateway.ts:302 说明顺序,函数体在 server/src/computer/gateway.ts:309;intent 来自 server/src/computer/gateway.ts:333,PolicyContext 从 server/src/computer/gateway.ts:335 组装,策略和审计分别落在 server/src/computer/gateway.ts:356 与 server/src/computer/gateway.ts:357,拒绝出口是 server/src/computer/gateway.ts:369 这组行号证明策略不是信任模型传来的 label。server/src/computer/gateway.ts:326 从 cached snapshot 找 element,server/src/computer/gateway.ts:342 到 server/src/computer/gateway.ts:350 把 role/name/type 放入 context。

src/server/src/computer/gateway.ts · L287-368TypeScript
287  /**
288   * Resolve a ref against the snapshot the server holds.
289   *
290   * Returns undefined for an unknown ref rather than throwing, because the policy still has to run:
291   * an action on an element we cannot identify must still receive a policy decision.
292   * A deny rule written against a page a Bot has not snapshotted should still refuse it.
293   */
294  function resolve(
295    botId: string,
296    ref: string | undefined,
297  ): SnapshotElement | undefined {
298    if (!ref) return undefined;
299    return snapshots.get(botId)?.elements.get(ref);
300  }
301
302  /**
303   * Decide, record, then act.
304   *
305   * The audit row is written before the action runs, not after it succeeds. An allowed action that
306   * later fails is still part of the audit sequence, and a trail that only contains successes cannot
307   * show that sequence.
308   */
309  async function govern<T>(
310    toolName: string,
311    botId: string,
312    actor: ActionActor,
313    subject: {
314      ref?: string;
315      filePath?: string;
316      targetUrl?: string;
317      key?: string;
318      /** The command a shell call is about to run, so a rule can be written against it. */
319      command?: string;
320      /** The person's Stop, on its way to the browser. See the acting methods below. */
321      signal?: AbortSignal;
322    },
323    run: () => Promise<T>,
324  ): Promise<T> {
325    const { ref, filePath } = subject;
326    const element = resolve(botId, ref);
327    const cached = snapshots.get(botId);
328    // For a navigation the relevant page is the one being opened, not the one already loaded. Using
329    // the cached URL would mean `page.host == "..."` could never match the destination, which is the
330    // only thing a rule about navigation would ever want to say.
331    const pageUrl = subject.targetUrl ?? cached?.url ?? "";
332
333    const intent = intentOf(toolName, subject.key);
334
335    const context: PolicyContext = {
336      tool: { name: toolName },
337      bot: { id: botId },
338      actor: { id: actor.id },
339      page: { url: pageUrl, host: hostOf(pageUrl) },
340      ...(intent ? { intent } : {}),
341      ...(subject.key ? { key: subject.key } : {}),
342      ...(element
343        ? {
344            element: {
345              ref: element.ref,
346              role: element.role,
347              name: element.name,
348              ...(element.type ? { type: element.type } : {}),
349            },
350          }
351        : {}),
352      ...(filePath ? { file: describeFile(filePath) } : {}),
353      ...(subject.command ? { command: subject.command } : {}),
354    };
355
356    const decision = evaluateActionPolicy(options.policy(), context);
357    await write(auditStore, {
358      toolName,
359      botId,
360      actor,
361      element,
362      ref,
363      ...(subject.key ? { key: subject.key } : {}),
364      ...(subject.command ? { command: subject.command } : {}),
365      filePath,
366      pageUrl,
367      decision,
368    });

▸policy language fail closed

ActionPolicy 类型在 server/src/computer/policy.ts:21,PolicyContext 在 server/src/computer/policy.ts:44;intent union 从 server/src/computer/policy.ts:87 开始,CEL helper 注册在 server/src/computer/policy.ts:171,matches 包装器在 server/src/computer/policy.ts:194;evaluateActionPolicy 从 server/src/computer/policy.ts:228 开始,deny loop 是 server/src/computer/policy.ts:239,allow loop 是 server/src/computer/policy.ts:254 展示 CEL 规则、deny 优先、helper functions 和 evaluateActionPolicy。重点是 server/src/computer/policy.ts:236 到 server/src/computer/policy.ts:249:deny 先执行,dry-run 只记录不阻断,enforce 才真正拒绝。

src/server/src/computer/policy.ts · L220-260TypeScript
220/**
221 * Decide whether this action may run.
222 *
223 * An absent policy denies. An unconfigured deployment is one that has not said what its Bots may do,
224 * and the safe reading of silence is "nothing", not "anything". The shipped configuration therefore
225 * states its permissions explicitly rather than relying on a default, so that what a Bot may do is
226 * always something somebody wrote down.
227 */
228export function evaluateActionPolicy(
229  policy: ActionPolicy | null | undefined,
230  context: PolicyContext,
231): PolicyDecision {
232  const mode: PolicyMode = policy?.mode ?? "enforce";
233  const deny = policy?.deny ?? [];
234  const allow = policy?.allow ?? [];
235
236  // Deny first, and a broken deny expression still denies. One typo in a rule therefore blocks the
237  // action rather than admitting it: the failure is loud, immediate and safe, and the alternative is
238  // a deployment that believes it has forbidden something it has not.
239  for (const expression of deny) {
240    if (matches(expression, context, true)) {
241      return {
242        allowed: false,
243        mode,
244        matched: expression,
245        source: "deny",
246        // dry-run records the refusal and lets the work continue, which is what makes it safe to
247        // switch on against live traffic.
248        forward: mode === "dry-run",
249        reason: describeRefusal(context, expression),
250      };
251    }
252  }
253
254  for (const expression of allow) {
255    if (matches(expression, context, false)) {
256      return {
257        allowed: true,
258        mode,
259        matched: expression,
260        source: "allow",
§ 02 · KEY FLOW

关键流程

▸统一动作路径

snapshot() → cache refs → govern(tool, subject) → resolve(ref) → build PolicyContext → evaluateActionPolicy() → write audit row → forward or ActionRefusedError

真正的动作方法只是 govern 的薄包装:server/src/computer/gateway.ts:565 包装 navigate,server/src/computer/gateway.ts:581 包装 click,server/src/computer/gateway.ts:596 包装 type,server/src/computer/gateway.ts:611 包装 key。这样所有 action 都走同一套审计和拒绝语义。

§ 03 · EDGE CASES

边界 / 失败模式

▸失败先被命名

  • 未知 ref 仍然进入 policy context,见 server/src/computer/gateway.ts:290,避免“没有 snapshot 所以不能拒绝”。
  • 允许后失败会补写 failure audit,见 server/src/computer/gateway.ts:387,避免只记录成功。
  • file path 和 command 也进入 context,见 server/src/computer/gateway.ts:352 和 server/src/computer/gateway.ts:353,否则 shell/workspace 无法治理。
Generated · 2026-08-21 Source · CopilotKit/OpenBot ← 返回 Trending 索引