refactor(protocol): generate TypeScript contracts and enforce complete tool policies - #389
Draft
NianJiuZst wants to merge 1 commit into
Draft
NianJiuZst wants to merge 1 commit into
NianJiuZst wants to merge 1 commit into
Conversation
…licies Refs Tencent#388. Derive extension contracts and validators from the Rust catalog, enforce exhaustive handler and execution policies, and preserve evaluation settlement across caller deadlines.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #388 when the agreed implementation is merged.
Draft implementation for design discussion. The issue is an RFC, and the architecture, compatibility policy, scope, and rollout still need maintainer agreement. This PR makes the proposal concrete; it is not a request to merge an already-approved design. The evaluation-settlement change below is a deliberate behavior change and a candidate for a separate PR if maintainers prefer staged delivery.
Problem and intended result
A protocol change currently requires coordinating Rust payload definitions, handwritten TypeScript mirrors, schema exports, dispatcher registrations, and several method classifications. These descriptions can drift independently: for example, Rust already defines
user_aborted, while the old TypeScript error-code union omitted it. Type assertions at the JSON boundary also provide no runtime validation.This prototype makes Rust the source for shared protocol facts, generates the extension's contract artifacts, validates payloads at the boundary, and makes missing handlers or policies a compilation error. Component-specific execution decisions remain explicit and owned by the component that executes them.
1. One method catalog, with typed payload roots
crates/bsk-protocol/src/method.rsnow declares each method once:The macro generates the Rust
Methodenum,Method::ALL, wire names, execution ownership, effect classifications, and the parameter/result schema registrations. Adding a method requires all five pieces of information.src/catalog.rstraverses the complete catalog and uses one schema generator per direction, following the actual definition references assigned by Schemars. It also includes shared error, handshake, cancellation, and trace definitions. Debug-action ownership/effects and protocol-version constants are exported from Rust; the CLI parser and extension capability list consume those definitions.dump-schemaderives method exports from the same catalog, retaining named compatibility aliases for reusable documentation/trace schemas. Its--out-diroption allows inspection without adding generated schema files to the checkout.Daemon-private payloads intentionally remain private; catalog entries using arbitrary JSON are not presented as fully validated extension DTOs.
2. Generate types and validators from the same definitions
The generator runs the Rust exporter with locked Cargo dependencies, checks catalog uniqueness and payload references, uses
json-schema-to-typescriptfor declarations, and uses Ajv standalone compilation plus esbuild for browser-compatible validators. The current export contains 41 extension-owned methods and 193 schema definitions.Schemars is upgraded from 0.8 to 1.2.2 so deserialization and serialization contracts can be exported separately. This matters because a defaulted field may be optional when received but always present when sent. Existing trace v2/v3 schema customizations are migrated with their classification rules preserved.
Generated immutable normalizers convert legacy
nullon optional typed fields to omission. Arbitrary JSON values and dictionary entries retain theirnullvalues. Conditional schemas are projected into TypeScript unions, while runtime validators retain constraints such as numeric bounds and array lengths.The extension ships generated validation functions; it does not ship a runtime schema compiler or load schemas over the network.
3. Validate the transport boundary and use typed dispatch
transport/protocol.tsowns the decoding/validation boundary. Malformed frames are rejected by envelope guards, unsupported methods fail explicitly, and invalid parameters produceinvalid_paramsbefore dispatch. Handshake responses use the same validation/normalization approach, and cancellation keeps its fast path with validated parameters.Handwritten payload mirrors are replaced by generated imports/reexports in transport, debug, session, tab, and window modules. Transport envelopes and local interpretation helpers remain handwritten where appropriate.
A handler result that violates the sender contract becomes
protocol_errorwitheffect_state: "unknown": a bad response does not prove that the browser action had no effect. Failed session-start result validation also cleans up the newly allocated session. Handshake audit fields and session-stop window-release metadata receive named Rust envelope types instead of ad hoc JSON additions.4. Require complete handlers and explicit local policies
ToolMapis generated from extension-owned catalog entries. The handler table must satisfy:Every method key is required, with its corresponding parameter and result types. The dispatcher no longer maintains a separate switch full of per-method payload assertions.
ToolHandlerMaprequires every extension methodtools/policy.tssatisfiesRecord<ExtensionToolMethod, ToolPolicy>daemon/tool_policy.rsexhaustively matchesMethod, without a wildcard defaultThis intentionally does not combine all behavior into one universal policy table. For example, being accessible through a background-tab targeting path does not mean that arbitrary
evaluatecode is read-only. Shared facts come from the catalog; local decisions remain explicit. The compiler guarantees coverage and type compatibility, not the correctness of a policy's chosen value.5. Make evaluation settlement explicit
The daemon's new policy distinguishes a caller deadline from the end of arbitrary page JavaScript execution. For
tool.evaluate, a deadline sends cancellation and allows cleanup grace, but a cancel acknowledgement is not treated as proof that the evaluation stopped.If the original response is still outstanding, the caller can receive
execution_pendingwhile the daemon retains the original response receiver and keeps the session busy. Activity is reported assettling; the original result releases the queue. The operation is not replayed. A lost connection produces an unconfirmed outcome, or the existing connection lifecycle invalidates the session; neither makes the old session safe for new commands. Other sessions remain independent.This tracks the awaited evaluation only. A script can schedule later independent work, and cancellation cannot roll back browser effects. Maintainer feedback is specifically requested on keeping this change here versus separating it from contract generation.
6. Developer workflow and CI
Commit the Rust changes and generated artifacts together.
protocol:checkregenerates expected content in memory and fails on missing, changed, or unexpected files. Frontend CI installs Rust and runs this check before typechecking.Adding an extension tool now means: declare its contract and shared effect, regenerate, implement the handler, and deliberately choose both components' policies. Missing cases fail compilation. Generation is an explicit command, not a file watcher. There is one authoritative generation/check path; the earlier local comparison scripts are not included or referenced by the published change.
Validation and evidence
Validation was run locally on macOS with Rust 1.94.1, Node 26.10.0, and the repository's pnpm 10.17.0. Frontend checks used a clean export of the staged source files with the existing installed dependencies, so untracked local audit/prototype files were not part of the checked source.
pnpm protocol:checkcargo fmt --all -- --checkand staged whitespace checkcargo test --workspace --lockedcargo clippy --workspace --lib --bins --locked -- -D warningspnpm lintpnpm --filter @browser-skill/extension compilepnpm ext:testpnpm ext:buildAdditional out-of-tree checks performed during implementation:
tool.evaluatefrom the handler and policy maps produced missing-key errors forToolHandlerMapandRecord<ExtensionToolMethod, ToolPolicy>.{"cancel_ack_did_not_unlock":true,"late_result":"idle","real_daemon_and_websocket":true} {"cancel_ack_did_not_unlock":true,"late_result":"invalidated","real_daemon_and_websocket":true}Known validation limits: all-target Clippy currently fails on the pre-existing
redundant_iter_clonedwarning in unchangedcrates/bsk-cli/src/cli/update.rs:1428; production-target Clippy passes. Browser-gated tests were skipped, and the daemon settlement probe used a simulated extension, not a real Chrome end-to-end run. Cross-platform and released-peer compatibility are not established by these local results. Hosted CI is reported separately by GitHub checks.No new test cases, test scripts, fixtures, screenshots, or local audit artifacts are committed. Existing tests receive only adaptations needed for the changed interfaces/schema API, required request fields, and asynchronous response timing; additional probe evidence is described here.
Compatibility and decisions before leaving Draft
1.3and minimum compatible protocol1.0; this is not proof that every previously tolerated peer payload remains compatible. Strict validation may expose existing inconsistencies, so the compatibility matrix and any version change need discussion.entries: [], while Serde readers retain default acceptance of omission.这是供设计讨论的 Draft 实现。 Issue 仍是 RFC,架构、兼容策略、改动范围和落地方式都需要与维护者讨论决定。本 PR 用具体实现帮助评估方案,不代表设计已经获准合并。下面的 evaluate 结束确认属于明确的行为变化;如果维护者希望分阶段推进,可以拆成独立 PR。
问题与预期结果
目前修改协议,需要协调 Rust 参数/结果定义、手写 TS 镜像、Schema 导出、处理函数注册和多处方法分类。这些描述可能独立漂移。例如 Rust 已有
user_aborted,旧 TS 错误码联合类型却没有;JSON 边界上的类型断言也不会提供运行时校验。本原型以 Rust 作为共享协议信息的来源,生成扩展使用的协议产物,在边界检查数据,并让遗漏处理函数或策略成为编译错误。组件特有的执行决策仍由各自显式维护。
1. 用一份方法目录关联协议与类型
crates/bsk-protocol/src/method.rs中,每个方法只登记一次:宏据此生成 Rust
Method枚举、Method::ALL、通信方法名、执行归属、影响类别,以及参数和结果的 Schema 注册信息。新增方法必须提供完整信息。src/catalog.rs遍历完整目录,分别为输入、输出使用统一的 Schema 生成器,并沿用 Schemars 实际分配的引用名称。它还导出共享错误、握手、取消和录制轨迹定义。Debug action 的归属/影响,以及协议版本常量,也从 Rust 导出,由 CLI 参数解析和扩展能力列表消费。dump-schema从同一目录推导方法导出列表,为可复用的文档/轨迹 Schema 保留兼容文件名;通过--out-dir可以在仓库外检查结果。Daemon 私有参数继续保持私有;目录中使用任意 JSON 的条目,不会被宣称为已经完整校验的扩展 DTO。
2. 从同一份定义生成类型与校验器
生成器使用锁定的 Cargo 依赖运行 Rust 导出程序,检查方法唯一性和类型引用,再通过
json-schema-to-typescript生成声明,通过 Ajv standalone 和 esbuild 生成浏览器可用的校验函数。当前覆盖 41 个由扩展执行的方法、193 个 Schema 定义。Schemars 从 0.8 升级到 1.2.2,分别导出反序列化和序列化协议。例如带默认值的字段,接收时可以省略,发送时却可能始终存在,两者不能混为一谈。既有 trace v2/v3 自定义 Schema 已迁移,并保留原有分类规则。
生成的不可变规范化函数,把可选类型字段的旧式
null转成字段省略;任意 JSON 和字典值中的null保留。条件 Schema 投影为 TS 联合类型,数值范围、数组长度等约束仍由运行时校验器检查。扩展打包的是已生成的校验函数,不携带运行时 Schema 编译器,也不通过网络加载 Schema。
3. 在通信边界校验,再进入类型化分发
transport/protocol.ts负责解码与校验边界。非法消息封装会被拒绝,未知方法明确报错,非法参数在进入处理函数之前返回invalid_params。握手响应使用相同的校验/规范化过程;取消请求保留快速通道,同时校验参数。Transport、debug、session、tab、window 中重复的协议类型改为导入或重新导出生成类型。适合留在本地的消息封装和错误解释辅助逻辑继续手写。
如果处理函数结果不符合发送协议,会返回带
effect_state: "unknown"的protocol_error,因为返回值不合法不能证明浏览器操作没有发生。会话启动结果校验失败时,也会清理刚分配的会话。握手审计字段和停止会话时的窗口保留信息改为具名 Rust 封装类型,避免临时向 JSON 中插字段。4. 强制处理函数与本地策略完整覆盖
ToolMap来自目录中由扩展执行的方法。处理函数表必须符合:每个方法都是必填项,并绑定自己的参数和结果类型。Dispatcher 不再依赖另一份包含逐方法参数断言的大 switch。
ToolHandlerMap要求覆盖全部扩展方法tools/policy.ts满足Record<ExtensionToolMethod, ToolPolicy>daemon/tool_policy.rs穷尽匹配Method,没有通配默认分支这里没有把所有行为塞进一个通用策略表。例如可以通过某条后台标签页路径访问,不代表任意
evaluate代码就是只读操作。共享事实来自目录,本地决策仍显式维护。编译器保证覆盖完整、类型匹配,不能证明每个策略取值在业务上一定正确。5. 明确 evaluate 的结束确认
Daemon 策略区分“调用方等待超时”和“页面 JavaScript 执行结束”。
tool.evaluate到期时会发送取消,并等待清理宽限期,但取消确认不会被视为脚本已经停止的证据。如果原响应仍未返回,调用方可以收到
execution_pending,同时 daemon 保留原响应接收器,并继续让该会话保持忙碌。活动状态显示为settling,原操作最终响应到达后才释放队列,期间不会重放操作。断连会产生未确认结果,或者由已有连接生命周期使会话失效;两种情况都不会把旧会话重新当作可安全执行新命令的会话。其他会话保持独立。这里追踪的是等待中的 evaluate 本身。脚本仍可能安排之后独立执行的工作,取消也不能回滚浏览器影响。希望维护者明确讨论:这部分行为变化应保留在本 PR,还是从协议生成改动中拆出。
6. 开发流程与 CI
Rust 修改与生成文件一起提交。
protocol:check在内存中重新计算预期内容,生成文件缺失、变化或出现额外文件都会失败。前端 CI 安装 Rust,并在类型检查前运行该检查。新增扩展工具的流程变为:声明协议和共享影响、重新生成、实现处理函数、主动选择两端策略。遗漏会导致编译失败。生成需要显式运行命令,当前没有文件变化监听。正式改动只保留一条权威生成/检查路径,不包含或引用此前的本地比较脚本。
验证与证据
本地环境为 macOS、Rust 1.94.1、Node 26.10.0、仓库指定的 pnpm 10.17.0。前端检查使用从暂存区导出的干净源码副本,并复用已安装依赖;本地未跟踪的审计文件和原型文件没有进入被检查的源码。
pnpm protocol:checkcargo fmt --all -- --check与暂存区空白检查cargo test --workspace --lockedcargo clippy --workspace --lib --bins --locked -- -D warningspnpm lintpnpm --filter @browser-skill/extension compilepnpm ext:testpnpm ext:build实现期间还在仓库外进行了以下验证:
tool.evaluate处理函数和策略,分别得到ToolHandlerMap、Record<ExtensionToolMethod, ToolPolicy>的缺项错误。{"cancel_ack_did_not_unlock":true,"late_result":"idle","real_daemon_and_websocket":true} {"cancel_ack_did_not_unlock":true,"late_result":"invalidated","real_daemon_and_websocket":true}已知验证边界: 全目标 Clippy 仍被未修改的
crates/bsk-cli/src/cli/update.rs:1428中已有的redundant_iter_cloned警告阻断;生产目标 Clippy 通过。依赖浏览器条件的测试仍跳过,daemon 结束确认探针使用模拟扩展,不是真实 Chrome 端到端验证。本地结果不能证明跨平台和所有已发布版本的兼容性。托管 CI 状态以 GitHub checks 单独展示。提交中没有新增测试用例、测试脚本、fixture、截图或本地审计素材。既有测试仅做接口/Schema API、必填请求字段和异步响应时序所需的适配;额外探针的证据放在本说明中。
兼容性与结束 Draft 前需要决定的事项
1.3、最低兼容版本1.0,这不能证明过去所有被容忍的消息都仍兼容。严格校验可能暴露既有不一致,需要讨论兼容矩阵及是否调整版本。entries: [],Serde 读取时仍通过默认值接受字段省略。