远程终端控制系统正式开发规格
版本:0.1。日期:2026-10-20。状态:补编设计稿;“正式开发规格”为既定文件名称,不代表已批准上线。
本规格定义首个交付闭环的架构、身份、数据、API 和 Agent 协议。它依据根目录 AGENTS.md 编写,实施状态见 阅读导航,参数及待确认事项见 设计决策与参数基线。后者的 P01–P19 是本资料集数值默认值的唯一维护位置。
目标与首版边界
超级管理员创建账号、登记设备并分配设备和能力。操作员通过 Web 向获授权 Windows 设备发起结构化任务。Agent 主动连接服务器,执行前检查任务与本地策略,回报可追踪的结果。观察者只能只读访问获授权范围。
首版实现管理员初始化、登录与撤销、用户管理、设备配对与凭据生命周期、设备和能力授权、任务与事件、审计及 browser.open_url。不开放公开注册、任意 Shell、个人浏览器接管、隐蔽运行和提权绕过。聊天、模型、BYOK、审批、Skill/MCP、桌面和文件能力按后续规格独立开启。
当前没有应用代码;以下接口、表、模块及校验均是开发目标。具体 JSON Schema、迁移和自动化测试必须在实现时生成并相互校验,不能用本规格替代可运行的契约。
系统组成
flowchart LR
W[React Web] -->|HTTPS| N[Nginx]
W -->|WSS 控制台订阅| N
A[Windows Agent] -->|主动 HTTPS 与 WSS| N
N --> F[单实例 FastAPI]
F --> M[(MySQL 8.4)]
F --> Q[受管理分发与恢复扫描]
Q --> M
A --> J[本地执行日志]
A --> B[独立自动化浏览器]
B --> E[受控网络出口]
首版采用 D01 单实例、单 worker,进程退出后由 MySQL 恢复分发状态。连接对象可以保存在内存,授权、任务、去重和事件不能只存在内存。数据库不可用时停止新分发和新执行许可;Agent 已逻辑开始的任务按预算收尾并本地落盘,恢复后再回报。
| 模块 | 职责 | 禁止越界 |
|---|---|---|
| API Router | 请求解析、身份依赖、响应和关联 ID | 不复制业务授权规则 |
| Identity Service | 用户、会话、设备凭据、配对与 MFA | 不把用户会话发给 Agent |
| Authorization Service | 统一对象及能力授权、撤销版本 | 不信任客户端 user_id 或角色 |
| Task Service | 内容固定、幂等、状态转换、执行许可与取消 | 不在创建事务中直接调用浏览器 |
| Dispatcher | 领取持久待分发项、检查授权和连接代次、投递 | 不依据内存队列判定任务已完成 |
| Event 与 Audit Service | 追加事件、回放、审计和脱敏 | 不允许普通业务调用编辑历史 |
| Repository | 事务、查询、锁和数据库约束 | 不在路由散落原始 SQL |
| Agent Transport | 认证、协议协商、心跳、重连与回报 | 不直接执行载荷中的命令字符串 |
| Agent Policy 与 Executor | 参数、本地策略、许可检查和类型化执行 | 不执行未知能力、不自动扩展权限 |
Web 采用 React、TypeScript 严格模式、Vite、Ant Design、TanStack Query。API 采用 Python 3.12+、FastAPI、Pydantic v2、SQLAlchemy 2.x Async、Alembic 和 mysql+asyncmy。浏览器由 Python Agent 的 Playwright 独立上下文承担。
身份与授权
Web 身份
采用 D03 同源 Cookie 会话。服务端存会话摘要、用户 ID、认证级别、空闲和绝对到期时间、撤销时间,不使用无法即时撤销的长期浏览器 JWT。登录、登出、重置访问、角色或授权变更均留下审计。
生产管理员完成 MFA 才能获得管理权限;待 MFA 验证的登录只允许完成挑战或登出。管理员初始化采用部署端一次性流程,在数据库事务中锁定初始化记录;不设置默认密码,不开放常驻匿名初始化 HTTP 接口。Argon2id 参数由目标环境基准测试后固定。
状态变更接口检查 CSRF token 与 Origin。浏览器 WebSocket 使用同源会话 Cookie,并验证 Origin;握手、订阅、补历史和推送都检查用户与对象权限。会话到期或撤销后关闭连接,不允许已建立的连接无限续用身份。
设备身份
管理员创建短期一次性配对码。/agent/enroll 在事务中消耗配对码、建立设备与独立凭据并提交审计。响应只展示一次设备秘密;若响应丢失或本地保护失败,不得重用配对码,撤销孤立登记后重新配对。
设备在 HTTPS 请求和 WSS 握手的 Authorization 头中提交独立凭据,长期秘密不出现在 URL 中。服务端保存凭据哈希,Agent 按 D05 保护秘密。每次有效重连递增设备 connection_epoch 并关闭旧连接;旧连接不能分发、发起开始请求或提交实时回报。
设备凭据轮换采用“准备新凭据、Agent 受保护保存、用新凭据确认、原凭据撤销”的流程。准备状态的新凭据仅可查询本次轮换和确认,不可连接任务通道或执行任务;有效期按 P17,旧凭据不能延长已失效的轮换。同一轮换确认成功但响应丢失时,新凭据再次确认返回原成功结果,不重复轮换。新秘密只在准备成功时展示一次,丢失该响应则等待准备态到期后重新准备,不从状态查询找回明文。无法安全恢复时撤销并重新配对。撤销立即阻止后续握手和执行许可。
对象边界
普通用户读取任务正文和结果必须同时满足本人所有、当前设备读取权和有效身份;设备被收回后不得靠旧任务或订阅继续读取。管理员可以查看设备、任务状态、请求者、错误码和审计等运维摘要,查看他人任务正文、输出或私人聊天需要另行授权和审计,首版不开放该排障正文通道。
创建、待分发领取、逻辑执行开始各检查一次当前授权。权限缓存必须受撤销版本约束,不得以缓存 TTL 代替撤销。详细矩阵见 用户权限与模型密钥。
关系数据模型
所有业务 UUID 主外键使用标准小写字符串 CHAR(36),采用一致的 ascii_bin 比较规则;文本采用 utf8mb4,账号匹配规则在 Schema 中固定。应用时间为带时区 UTC,持久化字段为 DATETIME(6),读写适配器负责恢复 UTC 语义,API 输出 Z 或 +00:00。
MySQL 对 DATETIME 不像 TIMESTAMP 那样自动执行会话时区转换,因此连接时区设为 UTC 之外,仍需明确应用转换和往返测试。参见 MySQL 8.4 日期时间类型。
| 表 | 关键字段 | 约束与用途 |
|---|---|---|
system_bootstrap |
id、initialized_at |
单行锁,保证一次性管理员初始化 |
users |
id、username、password_hash、role、status、authz_version |
账号唯一;角色及状态受约束;禁用后递增授权版本 |
user_sessions |
id、user_id、token_hash、auth_level、last_active_at、expires_at、revoked_at |
token hash 唯一;按用户及到期时间索引;MFA 未完成不能升级权限 |
user_mfa |
id、user_id、secret_ref、status、recovery_code_hashes、enrollment_expires_at |
用户唯一;TOTP 秘密独立加密,恢复码一次性且仅存哈希 |
device_pairing_codes |
id、code_hash、expires_at、consumed_at、created_by |
hash 唯一;消耗与设备创建同事务 |
devices |
id、name、status、last_seen_at、connection_epoch、policy_version |
注册后无操作员授权;状态与连接代次使用行锁 |
device_credentials |
id、device_id、secret_hash、status、created_at、revoked_at |
hash 唯一;区分待确认、有效与撤销;不存秘密明文 |
credential_rotations |
id、device_id、request_id、前后凭据 ID、status、expires_at |
请求 ID 去重;设备锁保证最多一个未结束轮换;查询不返回秘密 |
device_grants |
id、user_id、device_id、can_view、revoked_at |
用户设备对唯一,撤销保留记录 |
capability_grants |
id、device_grant_id、task_type、enabled |
授权与能力对唯一;能力许可不能替代设备读取权 |
tasks |
id、requested_by、device_id、task_type、schema_version、params、content_hash、status、时间与预算、version、last_event_seq |
参数为 JSON;请求者及设备外键;状态、设备、创建时间有查询索引 |
task_idempotency |
id、requested_by、idempotency_key、content_hash、task_id、terminal_status |
用户与键唯一;正文清理后保留墓碑,阻止重用键执行 |
task_dispatches |
id、task_id、connection_epoch、lease_until、delivery_state |
task 唯一;租约只控制投递,不授予执行权 |
execution_capacity |
固定单行 id、max_running |
行锁串行化全局执行名额领取;首版不以进程变量计数 |
execution_slots |
id、device_id、task_id、acquired_at |
device 与 task 分别唯一;仅存仍占用名额,释放历史进入任务事件 |
execution_permits |
id、task_id、dispatch_id、connection_epoch、authorized_at、expires_at、execution_deadline、attempt_id |
task 唯一;许可为认证通道中的绑定记录,不额外生成可转交的 bearer 秘密 |
task_events |
id、task_id、seq、event_type、payload、source_message_id、occurred_at、recorded_at |
task 与 seq 唯一;接收方去重约束;事件与状态同事务 |
agent_messages |
id、device_id、message_id、payload_hash、acknowledged_at |
device 与 message 唯一;同 ID 不同内容拒绝;回报确认在提交之后 |
audit_events |
id、主体、动作、对象引用、结果码、correlation_id、脱敏原因、时间 |
运行账号仅追加和按授权查询;清理由独立维护身份执行 |
表中“时间与预算”等是逻辑分组,不是实际列名;迁移前展开为具体字段,至少包含任务信封列出的全部时间和预算。外键避免级联删除审计、去重和任务依据;账户及设备使用禁用、撤销流程,清理流程显式处理关联保留。
全局状态版本使用乐观比较,关键竞争同时使用行锁。事务锁顺序统一为:用户 → 全局执行容量 → 设备 → 设备授权 → 任务 → 分发、名额及许可记录 → 事件计数。只需部分对象时保持相对顺序;释放名额遵循相同顺序。全局容量行锁下核对当前名额行数并插入本任务名额,保证 P01;不能依赖单 worker 避免异步并发。死锁重试只能重做尚未提交的数据库事务,不能重做外部副作用。
未来模型密钥、聊天、工作流、审批、Skill 和 MCP 表随对应模块建迁移,本期不预建可写入但无授权的空壳功能。
HTTP API
所有下表接口均为规划。业务 API 使用 /api/v1;表内路径省略此前缀。服务端从认证主体派生所有者字段,禁止请求体中的 user_id 覆盖当前身份。列表采用游标分页,数量上限按 P07。
| 方法与路径 | 输入或职责 | 授权及响应要点 |
|---|---|---|
POST /auth/login |
username、password |
限速;创建会话或只允许 MFA 的挑战;不返回设备凭据 |
POST /auth/mfa/verify |
challenge_id、code |
受限预认证会话;挑战一次性、短期有效、限速 |
POST /auth/password/change |
current_password 或一次性初始访问证明、new_password |
绑定当前受限或有效会话;撤销旧会话;需要 MFA 时仍不得跳过 |
POST /auth/mfa/enroll |
当前密码重新验证证明 | 本人受限绑定会话或最近验证过的会话;绑定秘密只展示一次、不进入日志或 localStorage |
POST /auth/mfa/enroll/confirm |
enrollment_id、code |
验证新 TOTP 后激活并一次性展示恢复码;不得越过仍未完成的密码更新要求 |
POST /auth/mfa/recover |
challenge_id、recovery_code |
一次性恢复码原子消费;仅获得重新绑定 MFA 的受限会话;恢复后才可进入管理权限 |
POST /auth/logout |
结束当前会话 | 撤销服务端会话并关闭关联控制台连接 |
GET /auth/session |
当前身份、认证级别、权限摘要及 CSRF token | 只返回本人,不返回秘密或密码哈希 |
GET /admin/users |
列表 | 仅 super_admin |
POST /admin/users |
username、role、一次性初始访问材料 |
禁止日志记录秘密;首次登录要求设置本人密码 |
PATCH /admin/users/{user_id} |
禁用、启用或角色调整 | 禁止禁用最后一个有效管理员;变更与撤销会话同事务 |
POST /admin/users/{user_id}/reset-access |
重置访问 | 敏感动作重新验证;撤销旧会话,初始材料只展示一次 |
GET /admin/devices |
全部设备运维摘要 | 不因此开放用户内容 |
POST /admin/pairing-codes |
创建配对码 | 仅管理员,明文只展示一次 |
POST /admin/devices/{device_id}/revoke |
撤销设备、凭据和新执行 | 记录原因;在途任务按取消及不确定结果规则处理 |
PUT /admin/users/{user_id}/devices/{device_id}/grant |
can_view、精确 capabilities 列表 |
替换明确授权集合,事务更新授权版本;空集合不能隐含所有能力 |
DELETE /admin/users/{user_id}/devices/{device_id}/grant |
收回设备授权 | 失效缓存及订阅,取消尚未开始任务 |
GET /admin/tasks |
状态、错误码等运维摘要 | 不返回他人 params、结果正文或聊天内容 |
GET /admin/audit-events |
脱敏审计查询 | 不提供编辑或删除接口 |
GET /me/devices |
本人当前可见设备及能力 | 不接受其他用户 ID 查询 |
POST /me/tasks |
device_id、task_type、schema_version、params、可选 timeout_seconds |
请求头 Idempotency-Key 必填;检查执行角色及全部授权 |
GET /me/tasks |
本人且当前仍可访问的任务 | 运维管理员全局摘要使用另一接口 |
GET /me/tasks/{task_id} |
任务快照和本人可见结果 | 同时校验所有权与当前设备权限 |
GET /me/tasks/{task_id}/events |
after_seq 游标 |
只返回该任务的连续持久化事件 |
POST /me/tasks/{task_id}/cancel |
请求取消 | 本人、有效身份及当前设备执行权;重复请求不重复副作用 |
POST /agent/enroll |
pairing_code、device_name、agent_version |
不使用 Web Cookie;一次性配对事务;返回 device ID 与独立秘密 |
POST /agent/credentials/rotate |
request_id,凭据轮换准备 |
当前有效设备凭据;首个成功响应返回 rotation_id、新凭据与期限;同请求重放只返回状态 |
GET /agent/credentials/rotations/{rotation_id} |
轮换状态 | 当前有效凭据或该轮换的待确认凭据;只查本设备,不返回秘密 |
POST /agent/credentials/confirm |
rotation_id,确认新秘密已保存 |
用该轮换的新凭据认证;原子激活并撤销旧凭据;已激活的新凭据重复确认返回原结果 |
POST /agent/tasks/{task_id}/start |
dispatch_id、attempt_id、content_hash、connection_epoch |
独立设备认证;当前有效 WSS 连接及对账完成;返回短期一次性开始许可 |
GET /health/live |
进程存活 | 不泄露依赖、配置或版本详情 |
GET /health/ready |
数据库、迁移兼容、后台分发可用性 | 依赖异常返回未就绪;详细诊断仅运维授权可见 |
身份辅助流程期限按 P18,密码与 MFA 初始化状态都完成前不能获得普通业务会话。MFA 秘密即使在 BYOK 模块关闭时也需要独立加密存储;恢复码遗失的管理员恢复由经验证的部署端流程处理,必须审计,不能靠匿名 HTTP 入口关闭 MFA。任务核实修正属于受审计的管理服务操作,首版不开放任意改状态 HTTP 接口。
开始许可响应
/agent/tasks/{task_id}/start 首次成功返回 permit_id、task_id、device_id、dispatch_id、attempt_id、connection_epoch、content_hash、authorized_at、permit_expires_at 和 execution_deadline。许可身份由有效 TLS 通道、设备认证和数据库绑定共同保证;permit_id 本身不授予第三方执行权。
permit_expires_at = authorized_at + P14许可有效期,execution_deadline = authorized_at + timeout_seconds。Agent 从请求发出时的单调时间开始保守计算两种剩余预算,收到响应时已耗尽许可则拒绝开始;不从响应抵达时重新计时,也不使用时钟容差延长期限。服务端时钟、Agent 单调时钟任一达到适用截止即停止新增动作并收尾。
请求幂等和错误
创建任务先检查身份、当前设备和能力权限,再查用户作用域内的幂等记录。内容摘要覆盖规范化后的设备、能力、Schema 版本、参数和生效的超时;不包含服务端生成的 ID、创建时间和到期时间。固定 JSON 规范化算法并提供跨语言向量,禁止各端依赖默认序列化碰巧一致。
第一次创建返回 201;相同键与相同内容返回原任务 200,不延长 TTL;相同键不同内容返回 409 IDEMPOTENCY_CONFLICT。若正文已清理但墓碑仍在,返回 410 TASK_RESULT_EXPIRED 和可见的最小摘要,不重建任务。当前权限已撤销时,即使键命中也不得返回任务详情。
统一错误结构如下,details 只包含已声明且不泄密的字段:
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "资源不存在或不可访问",
"correlation_id": "91943978-e11f-4e3c-b685-c4ac8c02c066",
"details": {}
}
}
| HTTP | 稳定错误码示例 | 使用边界 |
|---|---|---|
| 401 | AUTH_REQUIRED、SESSION_EXPIRED |
不暴露凭据、账号存在性或内部认证细节 |
| 403 | ACTION_FORBIDDEN、CAPABILITY_FORBIDDEN |
对已可见对象的动作拒绝 |
| 404 | RESOURCE_NOT_FOUND |
不存在或对象级无权访问采用一致表现 |
| 409 | IDEMPOTENCY_CONFLICT、TASK_STATE_CONFLICT、EXECUTION_ALREADY_CLAIMED |
不重放副作用 |
| 410 | TASK_RESULT_EXPIRED |
墓碑仍有效,正文已按策略清理 |
| 413 / 422 | PAYLOAD_TOO_LARGE / INVALID_REQUEST、UNSUPPORTED_TASK_TYPE、UNSUPPORTED_SCHEMA_VERSION |
拒绝未知字段、版本及类型 |
| 429 | RATE_LIMITED |
明确重试窗口,不通过错误区分未知用户名 |
| 503 | SERVICE_NOT_READY |
持久化、授权检查或审计无法完成时拒绝新执行 |
任务执行错误另含 TASK_EXPIRED、TASK_REVOKED、LOCAL_POLICY_DENIED、TARGET_NOT_ALLOWED、EXECUTION_TIMEOUT、OUTCOME_UNCONFIRMED、PROTOCOL_VERSION_UNSUPPORTED。错误码由协议源统一维护,禁止直接暴露堆栈、SQL、URL 秘密或设备路径。
Agent 与控制台协议
WebSocket 路径是 /ws/agent、/ws/console,不添加 /api/v1。首版只协商 protocol_version="1.0";任务 schema_version=1。协议号不自动跟随应用版本。每个消息都包含下列字段,未知字段拒绝:
| 字段 | 类型 | 语义 |
|---|---|---|
protocol_version |
string | 协商后的明确版本 |
message_type |
enum string | 消息类型决定 payload Schema |
message_id |
UUID string | 发送方生成;相同消息重传保持 ID 与内容 |
sent_at |
UTC ISO 8601 string | 原始发送时间;不取代服务端接收时间 |
correlation_id |
UUID string | 请求与任务的追踪标识,不是授权凭据 |
payload |
object | 各消息的严格类型载荷 |
所有大小上限按 P07;时间按 P14 检查;重传不刷新任务到期。去重记录比较载荷摘要,同消息 ID 不同内容为协议错误。Agent 的旧连接实时事件直接拒绝;重连后允许由当前连接回报本设备原任务的持久历史,不把旧 epoch 的任务历史误当作新的执行许可。
| 消息 | 方向 | payload 契约要点 |
|---|---|---|
agent.hello |
Agent → 服务端 | agent_version、supported_protocol_versions、journal_state;日志丢失不得报告正常 |
server.welcome |
服务端 → Agent | selected_protocol_version、connection_epoch、server_time、policy_version |
task.status_query |
服务端 → Agent | 分页的 task_ids;查询所有未确认任务,本地额外记录也须申报 |
task.status_report |
Agent → 服务端 | 有界 items:task ID、dispatch ID、attempt ID、本地阶段、已存结果摘要、最后回报消息 ID;不得直接覆盖服务端状态 |
agent.ready |
Agent → 服务端 | connection_epoch;仅双方对账结束后接收,表示可以接收新投递 |
agent.heartbeat |
Agent → 服务端 | connection_epoch、agent_version、active_task_ids;不得借此改变任务终态 |
task.dispatch |
服务端 → Agent | dispatch_id、connection_epoch、content_hash、task;尚不授予开始执行权 |
task.started |
Agent → 服务端 | task_id、dispatch_id、attempt_id、permit_id、started_at;记录实际执行观察点 |
task.succeeded |
Agent → 服务端 | 任务与许可引用、finished_at、严格 result |
task.failed |
Agent → 服务端 | 任务与许可引用、finished_at、error_code、脱敏 message、side_effects_possible;确认超时且已停止时 EXECUTION_TIMEOUT 映射为任务 timeout,其他明确失败映射为 failed |
task.rejected |
Agent → 服务端 | task 与 dispatch 引用、error_code、脱敏 message;尚未执行时的拒绝,落为 failed |
task.cancel |
服务端 → Agent | task_id、dispatch_id、cancel_request_id、受控 reason_code |
task.cancelled |
Agent → 服务端 | 任务与取消请求引用、确认停止时间、side_effects_possible;确认不代表已发生动作已撤销 |
task.executor_stopped |
Agent → 服务端 | 任务与许可引用、stopped_at、停止依据摘要;结果仍未知时可单独确认执行器已停止,不据此伪造业务结果 |
event.ack |
接收方 → 发送方 | ack_message_id、可选 task_id 与 seq;只在持久提交后确认,不对 ack 再 ack |
protocol.error |
任一端 → 对端 | code、脱敏 message、related_message_id;严重错误关闭连接 |
console.subscribe |
Web → 服务端 | task_id、after_seq;每个任务独立游标 |
console.event |
服务端 → Web | task_id、seq、event_type、授权后裁剪的 data |
该表确定消息集合和核心载荷;工程阶段需将每个可选字段、上限和联合类型固化为 JSON Schema。未固化 Schema 的消息不得上线。
任务信封示例
以下是字段示例,域名不会因此自动加入允许清单:
{
"task_id": "c7597b92-2542-4ff6-9d0e-6c09e64b56fe",
"device_id": "6ef3fdd0-750b-422c-9276-a2da3a33568d",
"task_type": "browser.open_url",
"schema_version": 1,
"params": {"url": "https://example.com/"},
"issued_at": "2026-10-20T02:00:00Z",
"expires_at": "2026-10-20T02:05:00Z",
"timeout_seconds": 60,
"idempotency_key": "2cd188b7-6522-48cb-bd09-a17784fb3983"
}
任务内容创建后不可原地修改。用户调整 URL、设备、能力或超时需要创建新任务和新键;将来涉及审批时必须重新审批。
执行与撤销的事务边界
- 创建事务持久化任务、幂等记录、首个事件、待分发项和审计,然后返回任务 ID。
- 分发器重新检查授权、TTL、设备与当前连接,持久领取投递记录,状态进入
dispatched后发送。该状态表示已进入交付过程,不证明 Agent 已接收。 - Agent 严格校验信封、设备绑定、类型、时钟与本地网络策略,先落盘 task、dispatch、内容摘要及新
attempt_id。重复投递只返回已有记录。 - Agent 使用当前设备凭据调用
/agent/tasks/{task_id}/start。服务端按统一锁序检查用户、授权、设备、连接代次、TTL、状态和内容摘要,在同一事务中原子领取全局及设备名额、创建唯一许可、将任务置running并追加task.execution_authorized事件。无名额时不发许可,任务保持待执行状态且不延长 TTL。 - 上一步的事务提交是逻辑执行开始点。
task.started另外记录实际开始。撤销先提交则许可必须失败;许可先提交则按已经开始的任务处理,请求取消而不能声称阻止了所有后续物理动作。 - Agent 校验许可引用、期限、当前连接和本地策略,持久标记执行意图,再调用执行器。许可响应丢失、许可过期、Agent 崩溃或日志无法确定时不申请第二次许可;进入对账。
- 执行器在预算内收尾,结果先落盘再回报。服务端验证许可与状态,事件、去重和结果同事务提交后发送确认;丢失确认时 Agent 只重传同一结果。
开始接口的同一 attempt_id 重复请求只返回已领取状态,不重新签发可开始执行的许可。首版允许因此保守地产生不确定结果,也不能为了提高成功率重复执行。Agent 可证明未开始且已停止时提供明确拒绝或失败证据;不能证明时使用 unknown_outcome。
本方案不能承诺跨网络和外部网页副作用的 exactly-once。它通过单次许可、持久日志和不确定结果冻结,避免用自动重试掩盖执行歧义。
任务状态转换
唯一持久状态集合如下:
pending, awaiting_approval, dispatched, running,
succeeded, failed, timeout, cancel_requested, cancelled,
expired, unknown_outcome
| 当前状态 | 允许目标 | 必要条件 |
|---|---|---|
pending |
dispatched、expired、failed、cancel_requested |
分发检查通过;或确认从未获许可且已到期;或授权/策略失败;或有效取消请求 |
awaiting_approval |
pending、expired、failed、cancel_requested |
后续审批模块启用后适用,首版不创建此状态任务 |
dispatched |
running、failed、expired、cancel_requested |
开始许可事务;执行前明确拒绝;无许可且过期;取消请求 |
running |
succeeded、failed、timeout、cancel_requested、unknown_outcome |
已验证结果;明确失败;确认预算用尽且执行已停止;取消请求;结果无法确认 |
cancel_requested |
cancelled、succeeded、failed、timeout、unknown_outcome |
已验证停止确认;竞态中的真实结果;明确超时停止;确认窗口结束仍无法确定 |
unknown_outcome |
经核实后的 succeeded、failed、timeout、cancelled |
可靠证据、条件更新和核实审计;禁止重新分发、转回 pending 或发新许可 |
succeeded、failed、timeout、cancelled、expired |
无普通转换 | 重复事件只返回已有确认;冲突事件追加异常审计,不覆盖结果 |
task.rejected 仅在尚未执行时接受,写入该事件并将任务落为 failed。收到过期任务后的 Agent 拒绝也按此处理;服务端自行发现尚未执行的任务超过 TTL 时才使用 expired。
timeout 需要已确认执行预算耗尽、执行停止的证据,仍应保留“此前可能有副作用”的信息。只有控制台等待超时、心跳丢失或结果缺失时,不得标为 timeout 来暗示执行已结束,应继续对账或进入 unknown_outcome。
服务端到达 execution_deadline 但未收到明确停止结果时,持久提交停止请求并按 P16 等待确认。Agent 若确认因预算耗尽停止,回报 task.failed 与 EXECUTION_TIMEOUT;只有用户取消导致的停止才回报取消确认。确认窗口结束仍无法核实则进入 unknown_outcome,不释放未经证实停止的名额。
取消请求先进入 cancel_requested,保存 P16 的 cancel_confirmation_deadline。从未领取分发且能够在事务内证明未发出的任务,由分发器作内部停止确认并追加取消确认事件;否则需要 Agent 确认。离线时不能伪造确认。取消和成功竞争时保留已验证的实际结果。
成功、失败、超时或取消结果须同时证明该执行器已经停止才能释放名额;结果不明时可用独立的 task.executor_stopped 证据释放名额,但任务仍保持 unknown_outcome。仅有离线、许可过期、取消窗口到期或任务被置为未知结果,都不能释放仍可能执行的设备名额。无法取得停止证据的设备保持隔离,人工核实也必须记录证据。
核实 unknown_outcome 时记录证据引用、核实主体、原因、原状态、新状态和关联事件。可信的迟到结果仍须通过去重、许可、设备身份及状态校验后进入核实流程,不能由任意晚到消息直接覆盖。
事件顺序与恢复
每个任务使用独立的连续 seq,分配时锁定该任务并与事件同事务提交。控制台以 (task_id, after_seq) 补历史,不用全局自增 ID 冒充提交顺序。数据库读取是权威,内存推送只提示“有新事件”;订阅建立、历史补齐与实时切换都从持久化游标连续读取。
断线重连时先认证与协商,再分页核对服务端未确认任务及 Agent 本地额外记录。对账完成前不接受新开始请求。已过期、已撤销、已终结和结果不明的任务不得重做;已有结果只补回报。两端记录冲突时冻结该任务并审计,不靠选择较晚时间戳解决。
任务内容与普通事件清理后,历史查询返回明确的结果已到期状态,不把缺失事件当空历史继续跳读。幂等墓碑、未知结果记录和待确认本地日志遵循 P09、P15。
数据库从旧备份恢复属于灾难恢复,不能当作普通进程重启。先关闭登录与分发,核对备份之后的独立撤销证据和执行记录;无法获得可靠记录时使全部旧 Web 会话与设备凭据失效,重新核准用户、设备和授权,再重新配对。可能已执行的任务冻结为待核实结果,不从恢复库的旧 pending 状态自动执行;凭据重配也不能清除这类历史。
浏览器能力
browser.open_url Schema 1 只接受 params.url,不接受脚本、代理、浏览器启动参数、下载路径、Cookie 或请求头。Agent 每任务新建独立上下文,关闭下载、持久用户配置和未经批准的弹窗,结束后关闭上下文。成功表示主文档达到约定的 DOMContentLoaded 并取得允许的最终 URL,不表示业务提交成功或页面全部资源加载完成。
结果对象声明 final_url、title、http_status、duration_ms、truncated,均受 P08 限制;URL 敏感查询值脱敏后展示,原始参数仅任务所有者按授权访问。HTTP 4xx/5xx 作为明确导航失败回报,状态码不是业务成功证明。
浏览器网络流量、请求数和重定向上限按 P19;累计配额在实际出口和请求层分别执行。不能先把超大响应完全读入内存再截断,也不能只限制最终任务结果而放任浏览器持续下载。
访问策略至少覆盖:
- 规范化域名、协议、端口和 URL,拒绝凭据 URL、歧义 IP、IP 字面量及非 HTTP/HTTPS 主文档地址;不记录 URL 中的秘密。
- P11 精确域名清单分别批准导航与子资源目的地;默认清单为空。所有解析地址均检查禁止网段,IPv4、IPv6、映射地址和 DNS 重绑定不得绕过。
- 出站代理自行解析、校验并连接同一个批准地址,禁止检查一次后交由浏览器另行解析。每次新连接重新检查,保持 Host/SNI 与证书校验。
- 重定向、iframe、脚本、图片、fetch、WebSocket 和新窗口都受相同网络边界约束;首版阻止 Service Worker 和不需要的旁路协议。
- 浏览器进程网络限制使直接连接、隐式代理绕过、QUIC/WebRTC 等不能避开受控出口;代理不可用则失败关闭。不得为实现隔离关闭安全工具或绕过 UAC。
- 若任何子资源触发禁止目标,阻断请求并将任务按策略失败处理,不把“主页面能打开”当作完整安全通过。
Playwright 提供代理和请求路由能力,并特别说明 Service Worker 可能使路由观察不到请求。这些能力本身不等同于完整网络隔离。参见 Playwright 网络文档。D04 验证未通过前不能开放此能力;测试设备的网络限制安装条件按 Q03 确认。
验收与后续扩展
首版同时交付协议、迁移、拒绝路径、故障恢复和真实 Windows 证据。MySQL 迁移必须在 8.4 测试实例验证;Agent 模拟与真实浏览器执行分别记录。具体用例见 验收用例与证据矩阵。
生产上线前满足 七项要求 中全部适用门禁。后续模块不得绕过本规格的身份、任务、授权、事件和审计;未启用模块的接口、执行器和工具注册应均拒绝调用。