qwen-code Daemon 多客户端权限仲裁:四种调解策略、取消哨兵与跨连接投票路由机制
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
qwen-code 的qwen servedaemon 允许同一会话被多个客户端同时连接,当 ACP 子进程内的 agent 发起requestPermission时,"谁有权批准、如何计票、迟到票如何处理"就成了一个必须显式建模的问题。本文基于仓库文档 04-permission-mediation.md 与对应源码,完整讲解MultiClientPermissionMediator的四类仲裁策略(first-responder/designated/consensus/local-only)、取消哨兵(cancel sentinel)的跨策略逃生通道、request()的 N1 同步注册不变量、共识法定人数的默认公式与 M=2 边界、启动期策略校验,以及投票经 ACP 与 REST 两条传输路径到达中介器的完整授权检查链。读完本文,你可以理解该仲裁机制的状态模型与事件契约,并能在多客户端协作、按租户隔离审批、企业变更评审、本地工作站等场景下正确配置policy.permissionStrategy与policy.consensusQuorum。
为什么需要多客户端权限仲裁
在sessionScope: 'single'模式下,同一个会话内的所有已连接客户端都能"看到" agent 发出的权限请求,且任何客户端都可以响应。如果没有仲裁层,会产生三类典型故障:
- 迟到票无处安放:请求已被某个客户端解决后,另一个客户端的投票到达,daemon 无法给出结构化答复;
- 竞态:两个客户端抢答同一个请求,后到者必须得到可预期的拒绝结果;
- 越权覆盖:一个恶意(或行为异常)的客户端可以覆盖发起 prompt 的"原始客户端"的审批意图。
MultiClientPermissionMediator(permissionMediator.ts)实现了PermissionMediator契约(permission.ts),独占bridge 的全部 pending 与 resolved 权限状态——bridge 层(httpAcpBridge/bridge.ts)不再自己维护pendingPermissionsMap 或 resolved LRU。它通过PermissionPolicy声明的四种策略分派投票,每种策略对应一类部署形态:
| 策略 | 判定规则 | 典型场景 |
|---|---|---|
first-responder | 第一张有效票胜出;后到的投票方收到permission_already_resolved。 | 实时的跨客户端协作体验(默认)。 |
designated | 只有 prompt 的originatorClientId可以裁决;其他客户端看到permission_forbidden{designated_mismatch}。 | 按租户隔离的 SaaS,UI 界面必须拥有自己的审批权。 |
consensus | 在 v1 的 client-id 快照上做 N-of-M 法定人数(quorum);中间过程通过permission_partial_vote事件让 UI 渲染进度。 | 需要两名操作员一致同意的企业变更评审。 |
local-only | 拒绝任何非 loopback 投票方;阻塞直到 loopback 客户端裁决。 | 不允许远程控制授予权限提升的工作站。 |
v1 安全边界:
X-Qwen-Client-Id是客户端自报的(self-reported)。designated与consensus在 v1 中尚无持有证明(proof-of-possession):能观察到originatorClientId的客户端可以复用该 id。此外,{outcome:'cancelled'}在策略分派之前就走取消哨兵通道,因此即便是local-only也无法把 cancel 当作"受策略保护的裁决"。若需要强隔离,应把 daemon 绑定到 loopback,或置于带认证的反向代理之后(详见安全说明:v1 客户端身份是自报的一节)。
契约面:接口、策略类型与投票结果
PermissionMediator 公开接口
bridge 与该机制交互只需要三个方法(permission.ts):
interface PermissionMediator { readonly policy: PermissionPolicy; request( record: PermissionRequestRecord, timeoutMs: number, ): Promise<PermissionResolution>; vote(vote: PermissionVote): PermissionVoteOutcome; forgetSession(sessionId: string): void; }MultiClientPermissionMediator在此之外还提供peekSessionFor(requestId)、pendingCount、内部审计发布器(PermissionAuditPublisher)等能力。值得注意的是BridgeClient只依赖request()这一半能力(结构子类型化),即它不需要知道投票如何裁决,只等待 Promise 结果。
核心类型
type PermissionPolicy = | 'first-responder' | 'designated' | 'consensus' | 'local-only'; type PermissionVoteOutcome = | { kind: 'resolved'; resolvedOptionId: string } | { kind: 'recorded'; votesNeeded: number } // consensus 中间态 | { kind: 'already_resolved'; resolvedOptionId: string } | { kind: 'forbidden'; reason: 'designated_mismatch' | 'remote_not_allowed' } | { kind: 'unknown_request' }; type PermissionResolution = | { kind: 'option'; optionId: string } | { kind: 'cancelled'; reason: 'timeout' | 'session_closed' | 'agent_cancelled'; };两个容易踩坑的契约细节,源码注释里都有明确交代(permission.ts):
designated_mismatch是重载的:designated策略下表示"投票方不是 prompt 发起者";consensus策略下表示"投票方clientId为 undefined 或不在 issue 时刻的votersAtIssue快照里"。中介器在审计日志中区分两者,但线上(wire)形状只有一个——这是刻意保持契约封闭的设计,未来版本才可能拆分该联合类型。PermissionVote.fromLoopback由 daemon 盖章,永远不是客户端自报的(permission.ts)。local-only依据它拒绝远端投票,无论clientId是什么。
请求记录:PermissionRequestRecord
request()接收的记录(permission.ts)承载了策略分派所需的全部上下文:
interface PermissionRequestRecord { readonly requestId: string; // ACP RequestPermission 请求 id,会话内唯一 readonly sessionId: string; // 权限作用域永远是 per-session(v1 无 workspace 级权限) readonly promptId?: string; // 产生该权限请求的已准入 prompt readonly originatorClientId: string | undefined; // 触发底层 prompt 的客户端 readonly allowedOptionIds: ReadonlySet<string>; // agent 声明的合法选项 readonly issuedAtMs: number; // 请求发出的墙钟时间戳 }其中allowedOptionIds是投票校验的第一道闸:任何提交了集合之外optionId的投票,在到达策略分派之前就会被抛出InvalidPermissionOptionError(permissionMediator.ts)。
中介器内部状态:Pending 与 Resolved FIFO
每个 pending 请求以requestId为键,其条目MediatorPending(permissionMediator.ts)携带:
policy—— 在request()时刻捕获。运行期改变 daemon 全局策略不会影响在途请求;record字段集(requestId、sessionId、originatorClientId、allowedOptionIds、issuedAtMs);resolve/reject闭包(用于结算request()返回的 Promise);votersAtIssue(仅 consensus 使用)——issue 时刻会话已注册clientId的快照;不在集合内的后到投票被拒绝;tallies(仅 consensus 使用)——Map<optionId, Set<clientId>>,按选项计票;timeoutHandle—— 在request()内部setTimeout挂起的 Node 定时器(N1 不变量,见下文);- 审计轨迹(每次投票都会写审计记录)。
源码中tallies与votersAtIssue对非 consensus 请求也一律以空容器存在,避免对policy做可判别联合类型——内存代价是每个 pending 约 120 字节,相对 per-session pending 上限 64 可忽略(见 permissionMediator.ts 的注释)。
Resolved FIFO用于让重复投票得到结构化的already_resolved而不是unknown_request:
- 容量上限
MAX_RESOLVED_PERMISSION_RECORDS = 512(permissionMediator.ts); - 淘汰是FIFO:
rememberResolved通过resolvedOrder.shift()丢弃最老记录,与PermissionAuditRing的 FIFO 修正保持一致(DeepSeek 评审 #4335); - 每条记录只存
{requestId, sessionId, outcome},512 条在常规 UI 重连/竞态窗口内远小于 100 KB(permissionMediator.ts)。
被挤出 FIFO 之后,同一requestId的迟到票只能得到{unknown_request}。
工作流程
request():N1 同步注册不变量
关键在于定时器必须在条目对外可见之前挂起。request()的全部注册动作——快照投票者、构造 pending、pending.set、审计、setTimeout——都发生在 Promise 执行器内、没有任何await(permissionMediator.ts)。桥接层的调用序列是publish → mediator.request → await:
EventBus.publish是同步的(分发到内存订阅者队列,不产生事件循环让出);- Promise 执行器对这次调用也是同步的(同步注册不变量)。
由此,forgetSession不可能落在"事件已发布、pending 尚未注册"的窗口里。如果这个窗口存在,forgetSession会漏掉新的 pending,条目将永久挂起——bridge 的 per-sessionpromptQueue会永远阻塞。反之,若注册晚了,一个在pending.set与setTimeout之间到达的forgetSession也会留下一个没有超时的 pending。
此外,request()返回的 Promise一经返回就永不 reject:超时、会话关闭、投票者取消、emit/审计异常全部编码为{kind:'cancelled', reason:...}。唯一的同步抛出是CancelSentinelCollisionError(哨兵冲突,见下节),它在构造 Promise 之前发生,调用方必须自行 try/catch(permissionMediator.ts)。
一个细节:当 consensus 策略下 issue 时刻votersAtIssue为空(会话在 publish 与 request 之间的极窄竞态窗口内被拆除)时,该请求只能经投票者取消、会话取消或启用的permissionTimeoutMs解决;中介器会向 stderr 写一条面包屑(breadcrumb),避免操作员面对一个静默挂起的权限请求(permissionMediator.ts)。
vote()分派
vote()的实现(permissionMediator.ts)按顺序执行:
- 查找 pending:不在
pending中时,先查 resolved FIFO——命中则重新广播permission_already_resolved(让迟到的 SSE 订阅者看到结论)并返回already_resolved;否则unknown_request。若命中但sessionId不匹配,同样返回unknown_request,不泄露信息; - 哨兵短路:
optionId === CANCEL_VOTE_SENTINEL时,先写voted审计记录再结算为{cancelled, agent_cancelled},完全绕过策略分派; - 选项校验:
optionId不在allowedOptionIds内则抛InvalidPermissionOptionError(路由层映射为 400); - 策略 switch:四个 case 各由私有处理器(
voteFirstResponder/voteDesignated/voteConsensus/voteLocalOnly)负责,末尾用never穷尽性检查保证未来新增策略字面量而忘记补 case 会在编译期失败(permissionMediator.ts)。
几个值得注意的策略细节:
designated的无发起者回退:当originatorClientId === undefined时,voteDesignated直接降级为 first-responder 行为(permissionMediator.ts)。这与 permission.ts 头注释中"无 originator 的 prompt 回退到 first-responder"一致;- consensus 不允许改票:若该
clientId已在任一选项桶中,重复投票只返回recorded(附votesNeeded),不会把票从旧桶移到新桶; permission_partial_vote只在 consensus 下触发,携带votesReceived/votesNeeded/quorum/optionTallies,UI 可以据此渲染进度;permission_forbidden在 designated / consensus / local-only 下触发,first-responder永不触发。
每次拒绝(forbidden)与超时都会写一条 stderr 面包屑(writeForbiddenStderr、超时定时器回调),因为审计环与 SSE 事件都是瞬时观测面——操作员 tail daemon stderr 时若无这些行,就完全看不到投票被拒(permissionMediator.ts)。
结算顺序:resolveEntry 的清理阶梯
所有裁决路径(投票胜出、超时、会话关闭、哨兵取消)都汇聚到resolveEntry,其清理顺序是一条硬化的不变量(permissionMediator.ts):
clearTimeout—— 保证定时器永远不会在半清理的条目上触发;- 从
pending删除 —— 状态迁移前半段,新投票不再可达; - 广播
permission_resolved(尽力而为,emit 失败不阻塞 Promise 结算)——刻意放在写 resolved 之前,使 emit 期间同步重入的投票看到"pending 与 resolved 均空"(静默 false),与旧行为逐字节兼容; - 写入 resolved FIFO;
- 写审计
recordResolved; - 最后结算 Promise —— 重入回调看到的是自洽状态。
幂等保护:入口先做this.pending.get(requestId) !== pending身份检查,覆盖"定时器与末票在同一 tick 竞争"和"LRU 淘汰后复用同一 requestId"两种情形。
forgetSession()
在会话关闭、逐出、bridge 关停时被调用。对每个record.sessionId === sessionId的 pending 条目:
- 取消超时定时器;
- 将 Promise 结算为
{kind:'cancelled', reason:'session_closed'}; - 追加审计记录;
- 从
pending移除。
实现先快照匹配键再逐个结算,避免迭代中修改 Map(permissionMediator.ts)。bridge 的会话拆除路径总是在 channel-kill 窗口之前调用forgetSession,保证 pending 权限不会比会话活得更久。
取消哨兵:一条刻意的跨策略逃生通道
CANCEL_VOTE_SENTINEL = '__cancelled__'(permissionMediator.ts)。桥接层把投票者的{outcome:'cancelled'}在调用mediator.vote之前映射为该哨兵;中介器识别哨兵时先于策略分派处理——无论clientId、loopback 与否、是否在投票者集合内,local-only与consensusdaemon 都能被任何投出{outcome:'cancelled'}的投票方取消。源码注释明确这是有意的:投票者 cancel 是 agent 侧的 abort 路径;若威胁模型要求策略门控的 cancel,那属于未来的契约变更,"documented here so a future maintainer doesn't accidentally 'fix' the bypass"(permissionMediator.ts)。
为防止哨兵被注入,有两道守卫:
- bridge 层拒绝线路票:
respondToSessionPermission检查response.outcome.outcome === 'selected' && optionId === CANCEL_VOTE_SENTINEL,抛InvalidPermissionOptionError——恶意线路客户端不能通过谎报optionId注入 cancel(bridge.ts); - mediator 层拒绝 agent 侧冲突:
request()发现allowedOptionIds包含哨兵字符串时,同步抛出CancelSentinelCollisionError——agent 合法地以'__cancelled__'作为选项标签时不能被伪装成取消(permissionMediator.ts)。
两个错误类与PermissionForbiddenError、InvalidClientIdError一起定义在 bridgeErrors.ts。
审计侧还保留了 wire 无法表达的区分:agent_cancelled(agent 在任何投票者裁决前取消了底层 prompt)与voter_cancelled(投票者投出 cancelled)在线上的PermissionResolution形状相同,但审计日志里的PermissionDecisionReason是不同的类型(permissionMediator.ts)——这是为取证保留的、刻意与冻结契约共存的"重载"。
状态与生命周期要点
- 策略按请求捕获:未来即使提供 daemon 级策略热切换,在途请求的规则也不会变;
votersAtIssue快照语义(consensus):请求发出后才连上的客户端可以投票,但其clientId若未在 issue 时刻注册于会话,投票会被拒绝为designated_mismatch(复用 designated 的原因码以保持契约封闭);- resolved 条目最多在 FIFO 中存活 512 条,淘汰后重复投票得到
{unknown_request}; - 事件面:
permission_partial_vote仅 consensus;permission_forbidden仅 designated / consensus / local-only; - 中介器通过
pendingCountgetter 向 bridge 暴露全 daemon 在途 pending 数,供操作员发现卡死的 FIFO。
配置与能力声明
| 来源 | 开关 | 作用 |
|---|---|---|
settings.json | policy.permissionStrategy | 当前生效的中介器策略。 |
settings.json | policy.consensusQuorum | consensus 的法定人数 N。 |
BridgeOptions | permissionPolicy、permissionConsensusQuorum、permissionAudit | 编程式覆盖。 |
| 能力标签 | permission_mediation(恒定存在;modes: ['first-responder', 'designated', 'consensus', 'local-only']) | 本构建支持的策略集合。 |
| 能力包络 | policy.permission | 该 daemon 当前运行的策略。 |
能力标签在 capabilities.ts 注册:
permission_mediation: { since: 'v1', modes: ['first-responder', 'designated', 'consensus', 'local-only'], },/capabilities让 SDK 客户端可以在依赖permission_partial_vote/permission_forbidden事件之前做预检。默认行为:policy.permissionStrategy未显式配置时,daemon 使用first-responder;designated、consensus、local-only只有在settings.json中设置后才生效。
启动期策略校验
runQwenServe.validatePolicyConfig(policyConfig)(run-qwen-serve.ts)在启动时校验合并后的settings.jsonpolicy.*段,对操作失误抛InvalidPolicyConfigError:
policy.permissionStrategy存在但不在四个受支持字面量内。合法集合在运行期从SERVE_CAPABILITY_REGISTRY.permission_mediation.modes派生——能力广播、settings schema 枚举、PermissionPolicy联合类型与运行期校验通过单一编辑点保持一致;policy.consensusQuorum存在但不是正整数。
还有一个软告警:当consensusQuorum已设置而permissionStrategy !== 'consensus'时,启动时向 stderr 发警告,并且该值被丢弃(返回undefined),使公开契约与警告内容一致。
InvalidPolicyConfigError(run-qwen-serve.ts)导出供instanceof判定:runQwenServe用它区分操作者配置错误(重新抛出、显式启动失败)与 settings 读取 I/O 故障(回退默认值)。测试用例在 run-qwen-serve.test.ts 中锁定了这些契约(合法字面量逐一举例、0/-1/1.5/NaN均被拒、非 consensus 策略下的警告与值丢弃)。
共识法定人数:默认公式与 M=2 边界
启用consensus且未设置policy.consensusQuorum时,中介器用consensusQuorumFor计算N = floor(M/2) + 1(permissionMediator.ts):
Math.max(1, Math.floor(m / 2) + 1);M(votersAtIssue.size) | 默认 N | 行为 |
|---|---|---|
| 1 | 1 | 单一投票者立即裁决。 |
| 2 | 2 | 需要全票一致(unanimous)。 |
| 3 | 2 | 简单多数。 |
| 4 | 3 | 超过半数(supermajority)。 |
| 5 | 3 | 简单多数。 |
| 6 | 4 | 超过半数(supermajority)。 |
M = 2 的边界:分裂投票(A 选 X、B 选 Y)时没有任何选项能达到一致,请求只能通过投票者取消、会话取消或可选的交互超时解决。permissionResponseTimeoutMs默认禁用;配置后,分裂状态会在该期限以{cancelled, timeout}解决。中介器会对"需要全票"的 consensus 请求向 stderr 写一条每中介器生命周期仅一次的去重面包屑——因为 M=2 时全票一致是常态而非罕见边界,逐请求打印会在繁忙会话里刷屏(permissionMediator.ts)。
若 operator 希望 M = 2 时"先投先赢",可显式设置policy.consensusQuorum: 1;更严格的配置(如 M = 4 要求全票)用同一字段。设置覆盖值时会被封顶到 M(Math.min(override, Math.max(m, 1))),防止 N > M 的死配置造成死锁;封顶生效时同样写一次性 stderr 面包屑。
安全说明:v1 客户端身份是自报的
X-Qwen-Client-Id由 HTTP 客户端提供。v1 中 daemon 校验其格式([A-Za-z0-9._:-]{1,128})并在clientIds中跟踪已附加的客户端 id,但不做持有证明。任何能在 SSE 流中观察到originatorClientId的客户端,都可以用同一个 id 注册并在后续请求中冒充该发起者。
对各策略的影响:
first-responder:不受影响,因为判定不依赖身份;designated:可被远端客户端复用originatorClientId冒充;consensus:门控在 issue 时刻的votersAtIssue快照上;若被冒充的 id 在请求发出时已附加到会话,它就可以投票;local-only:免疫 id 冒充,因为fromLoopback: boolean由 daemon 从连接远端地址盖章,而非客户端提供。
未来的 pair-token 机制将从POST /session签发 per-session 密钥,并要求designated/consensus投票携带——该机制在 v1 中不存在。
跨连接投票路由
两条投票传输路径
投票可通过两条独立传输路径到达中介器:
- ACP 传输(同连接响应):
permission_requestbridge 事件以session/request_permissionJSON-RPC 请求的形式投递到所属连接的 session 级 SSE/WS 流。客户端在同一连接上用 JSON-RPC 响应作答。dispatcher 的resolveClientResponse把连接本地 JSON-RPC id 映射回 bridge 的requestId,调用bridge.respondToSessionPermission(dispatch.ts); - REST API(跨连接):任何 HTTP 客户端——包括不同 ACP 连接上的、甚至根本没有 ACP 连接的——都可以通过
POST /session/:id/permission/:requestId投票。遗留路由POST /permission/:requestId(URL 中无 session)先用peekSessionFor(requestId)解析出会话,再委托到同一条respondToSessionPermission路径(routes/permission.ts)。
REST 路由在构造 bridge context 时用detectFromLoopback(req)读取内核级的req.socket.remoteAddress盖章 loopback 位(routes/permission.ts),两条路径都不从可伪造的头部(X-Forwarded-For、Forwarded等)推导 loopback。
连接本地的两级 ID 方案
ACP 传输用两级 id 映射线路与 bridge:
| 层 | ID 格式 | 作用域 | 用途 |
|---|---|---|---|
| JSON-RPC 消息 id | _qwen_perm_N(字符串,每连接单调) | 连接本地 | 在 session 流上关联 JSON-RPC 请求-响应对。 |
| Bridge 请求 id | 不透明字符串(agent/中介器生成的 UUID) | daemon 全局 | 在所有路由与中介器的 pending/resolved Map 中标识该权限请求。 |
bridge 请求 id 通过_meta厂商扩展透传,客户端走 REST 投票时可以携带它:
{ "method": "session/request_permission", "id": "_qwen_perm_3", "params": { "sessionId": "<session-id>", "toolCall": { "name": "shell" }, "options": [{ "optionId": "allow", "name": "Allow" }], "_meta": { "qwen": { "requestId": "<bridge-request-id>" } } } }连接把映射存于conn.pending: Map<jsonRpcId, PendingClientRequest>,其中PendingClientRequest.bridgeRequestId就是 bridge 级 id(见 connection-registry.ts)。
投票授权检查链
respondToSessionPermission(sessionId, requestId, response, context)按严格顺序执行以下检查(bridge.ts):
- 会话存在性——
sessionId指向的会话必须存活(byId.has(sessionId)),否则SessionNotFoundError; - 跨会话拒绝——
peekSessionFor(requestId)解析该请求实际所属的会话;若属于另一个会话,投票被拒(返回false/ 404),且不暴露会话成员信息; - 未知请求护栏——
peekSessionFor返回undefined(请求已超时、被 FIFO 淘汰或从未存在)时,在任何clientId校验之前拒绝(false/ 404)。这是防 oracle 攻击的:没有它,用伪造clientId的探测就能区分"该会话有此客户端"(通过校验 → 404)与"客户端未知"(InvalidClientIdError→ 400); - 客户端身份校验——
resolveTrustedClientId(entry, context?.clientId)验证 REST 携带的X-Qwen-Client-Id(或 ACP 的 bridge 盖章 clientId)已注册在该会话的clientIds中。匿名投票(clientId === undefined)放行,交给策略分派处理;未注册 id 抛InvalidClientIdError(路由映射为 400); - 取消哨兵强制——线路票
{outcome:'selected', optionId:'__cancelled__'}被InvalidPermissionOptionError拒绝,防止哨兵注入; - 中介器
vote()分派——校验通过的投票转入permissionMediator.vote(...),按当前策略处理。
ACP 传输的投票响应格式
客户端对session/request_permission的响应是标准 JSON-RPC:
接受(选择选项):
{ "jsonrpc": "2.0", "id": "_qwen_perm_3", "result": { "outcome": { "outcome": "selected", "optionId": "allow" } } }取消:
{ "jsonrpc": "2.0", "id": "_qwen_perm_3", "result": { "outcome": { "outcome": "cancelled" } } }错误响应(dispatcher 映射为 cancel):
{ "jsonrpc": "2.0", "id": "_qwen_perm_3", "error": { "code": -32000, "message": "user declined" } }resolveClientResponse中的失败恢复
当bridge.respondToSessionPermission抛出(例如投票体畸形)时,dispatcher 回退到显式取消(cancelAbandonedPermission),保证中介器不会永久卡住(dispatch.ts)。若投票与取消双双抛出(双重失败),pending条目被保留,留待连接拆除时的abandonPendingForSession重试。此外,若respondToSessionPermission返回false(无对应 pending——重复或竞态投票),registry 条目同样不删除:删除会把"没有 pending"与"bridge 拒绝了一个存在的投票"混为一谈,并使另一条连接上的合法重试收到误导性的 "no pending" 错误(dispatch.ts 中该分支的注释明确了此契约)。
注意事项与已知限制
- 取消哨兵在策略分派之前路由,这是设计——
local-only与consensusdaemon 都能被任何投{outcome:'cancelled'}的投票方取消。该逃生通道在 permissionMediator.ts 中有文档说明,是 agent 侧的 abort 路径;若部署需要策略门控的 cancel,应等待未来契约变更,不要用路由级检查打补丁掩盖; designated与consensus在PermissionVoteOutcome中重载designated_mismatch:中介器写的是分开的审计记录,但线上形状只有一个,未来协议版本可能拆分该联合;- **匿名投票者(无
X-Qwen-Client-Id)**只在first-responder与local-only(loopback)下被接受;designated与consensus拒绝匿名投票(consensus 路径中clientId === undefined直接落入designated_mismatch分支); votersAtIssue快照语义意味着客户端集合频繁变动的 consensus 部署中,合法客户端可能因为在请求发出之后才连接而被拒。operator 应在发出变更评审 prompt 之前预注册协作者 client id。
相关文档与源码索引
- 依赖文档:03-acp-bridge.md(bridge 如何把
BridgeClient.requestPermission接到mediator.request)、10-event-bus.md(partial-vote 与 forbidden 帧如何到达客户端)、09-event-schema.md(permission_*事件载荷契约)、08-session-lifecycle.md(forgetSession()在每次会话终止时被调用)、02-serve-runtime.md(PermissionAuditRing,512 条审计记录的 FIFO); - 契约冻结文件:permission.ts;
- 中介器实现与测试:permissionMediator.ts、permissionMediator.test.ts;
- 桥接层投票路由与授权:bridge.ts;
- 错误类型:bridgeErrors.ts(
CancelSentinelCollisionError、InvalidPermissionOptionError、PermissionForbiddenError、InvalidClientIdError); - ACP 传输投票路径:acp-http/dispatch.ts(
resolveClientResponse)、acp-http/connection-registry.ts(AcpConnection.pending连接本地请求映射); - REST 投票路由:routes/permission.ts;审计环与发布器:permission-audit.ts;
- 启动期校验:run-qwen-serve.ts 及其测试 run-qwen-serve.test.ts。
上述机制对应 issue #4175 的 F3 系列工作;从源码结构看,中介器以"单一类 +switch(entry.policy)"取代了策略子类,注释中说明每个策略分支只有 5–15 行,子类化只会增加样板代码而无实质收益。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考