Computer Gateway 深度解析
这一页只看 OpenBot 的一个局部模块,用真实源码行号解释它承担的契约、失败路径和与主架构的关系。
导入链与契约
▸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 写事实。
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";
核心实现,逐段拆
▸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。
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 才真正拒绝。
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",
关键流程
▸统一动作路径
真正的动作方法只是 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 都走同一套审计和拒绝语义。
边界 / 失败模式
▸失败先被命名
- 未知 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 无法治理。