Back to Qwen Code

能力协商与协议版本

docs/developers/daemon/11-capabilities-versioning.md

0.18.110.9 KB
Original Source

能力协商与协议版本

概览

GET /capabilities 是 daemon 的 pre-flight 出口。每个 SDK 客户端应该在任何其他路由之前先读它,了解 daemon 说哪个协议版本、开了哪些 feature tag、绑定到哪个 workspace。合约:

  • 只有一个协议版本 v1 SERVE_PROTOCOL_VERSION = 'v1'SUPPORTED_SERVE_PROTOCOL_VERSIONS = ['v1']。v1 内部纯加法;frame 形态破坏性改动留给 v2。
  • 每 tag 一个 since 版本,未来 v2 可以同时广播 v1 与 v2 tag。
  • 条件广播。十个 tag(require_authmcp_workspace_poolmcp_pool_restartallow_originprompt_absolute_deadlinewriter_idle_timeoutworkspace_settingssession_shell_commandrate_limitworkspace_reload)只在对应部署开关打开时才广播;tag 存在 = 行为存在。
  • Capability tag = 行为契约。在已有 tag 下加新行为会悄悄破坏已有的 pre-flight 检查;新行为对应新 tag

完整注册表在 packages/cli/src/serve/capabilities.ts

职责

  • 声明 daemon 可能广播的每个 feature。
  • 按协议版本 + 部署开关过滤实际广播的 feature。
  • 暴露 getRegisteredServeFeatures()(全 key、不过滤)、getAdvertisedServeFeatures(version, toggles)(过滤后)、getServeProtocolVersions()(envelope:{current, supported})。
  • 守住「tag 存在 = 行为存在」的不变式 —— server.test.ts 的「every conditional tag advertises when its toggle is on」测试遍历 CONDITIONAL_SERVE_FEATURES 的 key,没写 predicate 的新 tag 直接挂测。

架构

Capability envelope

/capabilities 返回:

ts
{
  v: 1,                    // CAPABILITIES_SCHEMA_VERSION
  mode: 'http-bridge',
  features: ServeFeature[],
  workspaceCwd: string,
  protocol?: { current: 'v1', supported: ['v1'] },
  policy?: { permission: PermissionPolicy },
}

workspaceCwd 是 daemon 绑定的规范化 workspace(详见 02-serve-runtime.md)。policy.permission 是当前激活的 mediator 策略。

ServeCapabilityDescriptor

ts
interface ServeCapabilityDescriptor {
  since: ServeProtocolVersion; // current = 'v1'
  modes?: readonly string[]; // 多种操作模式时列出
}

v1 用到 modes 的两个 tag:

  • mcp_guardrails: { since: 'v1', modes: ['warn', 'enforce'] } —— 客户端依赖 refusal 行为前 pre-flight 'enforce'
  • permission_mediation: { since: 'v1', modes: ['first-responder', 'designated', 'consensus', 'local-only'] } —— 客户端在这里看到构建期支持集;daemon 的激活策略在 envelope 的 policy.permission

条件 tag

ts
export const CONDITIONAL_SERVE_FEATURES: ReadonlyMap<
  ServeFeature,
  (toggles: AdvertiseFeatureToggles) => boolean
> = new Map([
  ['require_auth', (t) => t.requireAuth === true],
  ['mcp_workspace_pool', (t) => t.mcpPoolActive === true],
  ['mcp_pool_restart', (t) => t.mcpPoolActive === true],
  ['allow_origin', (t) => t.allowOriginActive === true],
  [
    'prompt_absolute_deadline',
    (t) => typeof t.promptDeadlineMs === 'number' && t.promptDeadlineMs > 0,
  ],
  [
    'writer_idle_timeout',
    (t) =>
      typeof t.writerIdleTimeoutMs === 'number' && t.writerIdleTimeoutMs > 0,
  ],
  ['workspace_settings', (t) => t.persistSettingAvailable === true],
  ['session_shell_command', (t) => t.sessionShellCommandEnabled === true],
  ['rate_limit', (t) => t.rateLimit === true],
  ['workspace_reload', (t) => t.reloadAvailable === true],
]);

Map 形状把「predicate 判断」和「集合成员」收成一条记录。加一个新条件 tag 要两处协调修改

  1. SERVE_CAPABILITY_REGISTRY 注册 tag 及其 since
  2. CONDITIONAL_SERVE_FEATURES 加 predicate。

基线 tag(Map 里没有)无条件广播 —— 这个决定是用「不写」表达的,不需要专门维护一个 Set。

66 个 tag(v1,按域)

Foundation:healthcapabilities

Sessions:session_createsession_scope_overridesession_loadsession_resumeunstable_session_resumesession_listsession_promptsession_cancelsession_eventssession_set_modelsession_closesession_metadatasession_contextsession_context_usagesession_supported_commandssession_taskssession_statssession_approval_mode_controlsession_recapsession_btwsession_shell_command(条件)、session_languagesession_rewindsession_hookssession_branch

Streaming:slow_client_warningtyped_event_schema

Identity & heartbeat:client_identityclient_heartbeat

Permissions:session_permission_votepermission_votepermission_mediationmodes: ['first-responder', 'designated', 'consensus', 'local-only'])。

Workspace 只读快照:workspace_mcpworkspace_skillsworkspace_providersworkspace_envworkspace_preflightworkspace_hooksworkspace_extensions

Workspace 修改(Wave 4+):workspace_memoryworkspace_agentsworkspace_agent_generateworkspace_tool_toggleworkspace_settings(条件)、workspace_initworkspace_mcp_restartworkspace_mcp_manageworkspace_file_readworkspace_file_bytesworkspace_file_writeworkspace_reload(条件)。

MCP guardrails:mcp_guardrailsmodes: ['warn', 'enforce'])、mcp_guardrail_eventsmcp_server_runtime_mutationmcp_workspace_pool(条件)、mcp_pool_restart(条件)。

Prompt control:prompt_absolute_deadline(条件)、writer_idle_timeout(条件)、non_blocking_prompt

Auth:auth_provider_installauth_device_flowrequire_auth(条件)、allow_origin(条件)。

Rate limiting:rate_limit(条件)。

(粗体 = 带 modes 或条件。)

流程

Daemon 端:装 envelope

mermaid
flowchart LR
    A["GET /capabilities"] --> B["getAdvertisedServeFeatures(version, toggles)"]
    B --> C["filter by isFeatureAvailableInProtocol"]
    C --> D["for each, check CONDITIONAL_SERVE_FEATURES"]
    D --> E["yes: predicate(toggles) ? include : drop"]
    D --> F["no: include unconditionally"]
    E --> G["return ServeFeature[]"]
    F --> G
    G --> H["wrap in envelope:
{ v: 1, mode, features, workspaceCwd, protocol, policy }"]

客户端:feature pre-flight

mermaid
sequenceDiagram
    autonumber
    participant C as Client
    participant D as GET /capabilities
    participant R as Route

    C->>D: GET /capabilities
    D-->>C: { v, mode, features, workspaceCwd, protocol, policy }
    C->>C: features.includes('mcp_workspace_pool')?
    alt yes
        C->>R: rely on pool-aware response shapes
(e.g. entries[] from /workspace/mcp/:server/restart)
    else no
        C->>R: legacy single-entry response shape
    end

状态与生命周期

  • CAPABILITIES_SCHEMA_VERSION 是 wire envelope 的形状版本(当前 1)。bump 它是对 envelope 本身的 break。
  • SERVE_PROTOCOL_VERSION = 'v1' 是协议-feature 版本。v1 内加 feature 是加法;老客户端不 pre-flight 新 tag 就看不到,移除 feature 才是 v2 break。
  • EVENT_SCHEMA_VERSION = 1 是 SSE frame 的 v 字段(见 09-event-schema.md),独立版本轴;bump 事件 schema 不必 bump 协议版本,反之亦然。
  • unstable_session_resume 故意带 unstable_ 前缀,因为 ACP 的 connection.unstable_resumeSession 还可能变形状;客户端应 feature-detect 而不是固定 v1。

依赖

  • packages/cli/src/serve/server.ts 读来装 /capabilities 响应。
  • Toggle 输入:runQwenServe / createServeApp 构造 { requireAuth, mcpPoolActive, allowOriginActive, promptDeadlineMs, writerIdleTimeoutMs, persistSettingAvailable, sessionShellCommandEnabled, rateLimit, reloadAvailable } 透传到 envelope。
  • envelope 中激活的 permission 策略来自 BridgeOptions.permissionPolicy(其本身读 settings.jsonpolicy.permissionStrategy)。

配置

来源旋钮对 capabilities 的影响
参数--require-authrequire_auth tag 出现
EnvQWEN_SERVE_NO_MCP_POOL=1mcp_workspace_pool + mcp_pool_restart 不广播;MCP 事件不再盖 scope: 'workspace'
参数--mcp-client-budget=N--mcp-budget-mode={off,warn,enforce}不改 tag 集合(mcp_guardrails 永远广播),但改 per-server 预留 + refusal 行为
参数 / Env--rate-limit / QWEN_SERVE_RATE_LIMIT=1rate_limit tag 出现
嵌入选项persistSettingAvailableworkspace_settings tag 出现
参数 / 嵌入选项--enable-session-shell / sessionShellCommandEnabledsession_shell_command tag 出现
嵌入选项reloadAvailableworkspace_reload tag 出现
settings.jsonpolicy.permissionStrategy设 envelope 的 policy.permission

注意 & 已知局限

  • --require-auth 遮蔽 pre-flight。开 --require-auth 时所有路由(包括 /capabilities)都要 bearer。未认证客户端无法 pre-flight caps.features.require_auth 来发现需要认证;这种情形下401 响应体就是发现 surface(详见 12-auth-security.md)。require_auth tag 是认证后确认,给加固部署的审计 UI 用。
  • Tag 存在 = 行为存在。如果未来贡献者在已有 tag 下加行为且没 bump since,pre-flight 旧 tag 的 SDK 会默默拿到新行为。约定:新行为对应新 tag
  • unstable_* tag 可能在版本之间变形状且不 bump 协议版本。依赖时硬绑 SDK 版本。
  • 路由清单在 ../qwen-serve-protocol.md,本文刻意不重复。

参考

  • packages/cli/src/serve/capabilities.ts(整文件)
  • packages/cli/src/serve/types.tsServeOptionsCapabilitiesEnvelope
  • packages/cli/src/serve/server.ts(envelope 装配)
  • packages/acp-bridge/src/eventBus.tsEVENT_SCHEMA_VERSION
  • wire 参考:../qwen-serve-protocol.md