basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6export const SERVE_PROTOCOL_VERSION = 'v1';7export const SUPPORTED_SERVE_PROTOCOL_VERSIONS = [8 SERVE_PROTOCOL_VERSION,9];10export const SERVE_CAPABILITY_REGISTRY = {11 health: { since: 'v1' },12 daemon_status: { since: 'v1' },13 capabilities: { since: 'v1' },14 session_create: { since: 'v1' },15 session_scope_override: { since: 'v1' },16 session_load: { since: 'v1' },17 session_resume: { since: 'v1' },18 // Deprecated alias — kept until @agentclientprotocol/sdk graduates19 // the underlying ACP method from unstable_resumeSession to resumeSession.20 unstable_session_resume: { since: 'v1' },21 session_list: { since: 'v1' },22 session_prompt: { since: 'v1' },23 session_cancel: { since: 'v1' },24 session_events: { since: 'v1' },25 session_artifacts: { since: 'v1' },26 // Daemon emits `slow_client_warning` synthetic frames at 75% queue27 // fill and honors `?maxQueued=N` (range [16, 2048]) on28 // `GET /session/:id/events`. Old daemons silently lack both — SDK29 // clients pre-flight this tag before opting in.30 slow_client_warning: { since: 'v1' },31 // SDK consumers can detect `KnownDaemonEvent` schema support without32 // pinning against this SDK release — `narrowDaemonEvent` falls back33 // to `kind: 'unknown'` for daemons that don't advertise the tag,34 // so the tag is purely informational.35 typed_event_schema: { since: 'v1' },36 session_set_model: { since: 'v1' },37 client_identity: { since: 'v1' },38 client_heartbeat: { since: 'v1' },39 session_permission_vote: { since: 'v1' },40 permission_vote: { since: 'v1' },41 workspace_mcp: { since: 'v1' },42 workspace_skills: { since: 'v1' },43 workspace_providers: { since: 'v1' },44 auth_provider_install: { since: 'v1' },45 // Workspace memory CRUD (`GET/POST /workspace/memory`). Daemon exposes46 // hierarchical QWEN.md state and accepts append/replace writes scoped47 // to either the bound workspace or the global ~/.qwen directory.48 workspace_memory: { since: 'v1' },49 workspace_memory_remember: {50 since: 'v1',51 modes: ['workspace', 'clean'],52 },53 workspace_memory_forget: { since: 'v1' },54 workspace_memory_dream: { since: 'v1' },55 // Workspace agents CRUD (`GET/POST /workspace/agents` +56 // `GET/POST/DELETE /workspace/agents/:agentType`). Wraps57 // `SubagentManager` over HTTP so remote clients can list / read /58 // create / update / delete project- and user-level subagent59 // definitions. Built-in / extension agents stay read-only.60 workspace_agents: { since: 'v1' },61 workspace_agent_generate: { since: 'v1' },62 workspace_env: { since: 'v1' },63 workspace_preflight: { since: 'v1' },64 session_context: { since: 'v1' },65 session_context_usage: { since: 'v1' },66 session_supported_commands: { since: 'v1' },67 session_tasks: { since: 'v1' },68 session_stats: { since: 'v1' },69 session_lsp: { since: 'v1' },70 session_status: { since: 'v1' },71 session_close: { since: 'v1' },72 session_archive: { since: 'v1' },73 session_metadata: { since: 'v1' },74 session_organization: { since: 'v1' },75 session_export: { since: 'v1' },76 // Daemon supports the MCP client guardrail surface: an in-process77 // counter exposed on `GET /workspace/mcp`, a `--mcp-client-budget=N`78 // flag with `--mcp-budget-mode={enforce, warn, off}`, and a79 // `disabledReason: 'budget'` tag on per-server cells when refused at80 // discovery. `modes` enumerates the implemented behaviors.81 mcp_guardrails: { since: 'v1', modes: ['warn', 'enforce'] },82 workspace_mcp_manage: { since: 'v1' },83 // Daemon emits typed push events for MCP budget state crossings:84 // `mcp_budget_warning` and `mcp_child_refused_batch`. Always-on;85 // orthogonal to `mcp_guardrails` (the snapshot surface).86 mcp_guardrail_events: { since: 'v1' },87 // Always-on. Daemon supports runtime MCP server mutation via88 // `POST /workspace/mcp/servers` (add) and89 // `DELETE /workspace/mcp/servers/:name` (remove). SDK clients90 // pre-flight this tag before calling those routes.91 mcp_server_runtime_mutation: { since: 'v1' },92 // Daemon supports the read-only workspace file surface:93 // `GET /file`, `GET /list`, `GET /glob`, `GET /stat`. The four94 // routes are gated as a single feature because they share the same95 // backing `WorkspaceFileSystem` boundary and failure shape.96 workspace_file_read: { since: 'v1' },97 // Daemon supports bounded raw byte reads via `GET /file/bytes`.98 // Separate from `workspace_file_read` because older daemons may99 // advertise the text/list/stat/glob surface without byte-window100 // support.101 workspace_file_bytes: { since: 'v1' },102 // Daemon supports hash-aware text mutation routes103 // (`POST /file/write`, `POST /file/edit`) behind the strict mutation104 // gate. Clients should still pre-flight `require_auth` separately for105 // deployment posture; this tag only means the route contract exists.106 workspace_file_write: { since: 'v1' },107 // Daemon hosts the session-level approval-mode108 // control route `POST /session/:id/approval-mode` (gated by the109 // mutation gate, strict). The route accepts `{mode, persist?}` —110 // `persist:true` also writes `tools.approvalMode` to workspace111 // settings via the daemon's `loadedSettings` handle. SDK helper:112 // `DaemonClient.setSessionApprovalMode`.113 session_approval_mode_control: { since: 'v1' },114 // `POST /workspace/tools/:name/enable` toggles a115 // tool name in the workspace's `tools.disabled` settings list. The116 // bridge writes the settings file directly (no ACP roundtrip) and117 // fan-outs a `tool_toggled` event to all live session SSE buses.118 // Already-registered tools in active sessions are NOT retroactively119 // unregistered — the toggle takes effect on the next ACP child spawn120 // (`tools.disabled` is consulted at `Config` construction time).121 workspace_tool_toggle: { since: 'v1' },122 workspace_settings: { since: 'v1' },123 // `GET /workspace/permissions` is always available when this tag is124 // advertised. `POST /workspace/permissions` updates the active ACP125 // child and returns `permission_session_required` when no live ACP126 // session exists; the tag means the route contract exists, not that127 // the current daemon state can accept a write.128 workspace_permissions: { since: 'v1' },129 workspace_voice: { since: 'v1' },130 workspace_voice_transcription: { since: 'v1', modes: ['batch'] },131 // Inspect bound workspace trust and request local operator action.132 // Remote clients cannot directly write trustedFolders.json.133 workspace_trust: { since: 'v1' },134 // `POST /workspace/init` scaffolds an empty135 // `QWEN.md` (or whatever `getCurrentGeminiMdFilename()` returns) at136 // the bound workspace root. Body: `{force?: boolean}`. Default137 // refuses with 409 when the file already exists; `force: true`138 // overwrites. Mechanical only — does NOT call the LLM. To AI-fill139 // the file, the caller should follow up with140 // `POST /session/:id/prompt`.141 workspace_init: { since: 'v1' },142 // `POST /workspace/setup-github` installs the fixed143 // qwen-code-action workflow set into the bound workspace after144 // explicit consent. The route reuses the interactive `/setup-github`145 // release lookup, workflow download, and `.gitignore` update logic.146 workspace_github_setup: { since: 'v1' },147 // `POST /workspace/mcp/:server/restart` performs148 // a single-server MCP restart (disconnect + reconnect + rediscover)149 // through the ACP child's `McpClientManager`. Pre-checks the live150 // budget snapshot: when the target server is not151 // already in `reservedSlots` AND the live count would exceed the152 // configured budget under `enforce` mode, returns 200 with153 // `{restarted:false, skipped:true, reason:'budget_would_exceed'}`154 // rather than triggering a refusal cascade. Other skip reasons:155 // `'in_flight'` (concurrent discovery in progress), `'disabled'`156 // (server is configured but explicitly disabled).157 workspace_mcp_restart: { since: 'v1' },158 // Daemon hosts `POST /session/:id/recap`, which159 // generates a one-sentence "where did I leave off" summary by160 // running `generateSessionRecap` (`core/services/sessionRecap.ts`) as161 // a side-query against the fast model. Non-strict mutation gate —162 // posture mirrors `/session/:id/prompt` (token cost, not state163 // mutation). The route returns `{sessionId, recap}` where `recap`164 // may be `null` for too-short histories or transient model failures165 // (best-effort, never throws). SDK helper: `DaemonClient.recapSession`.166 session_recap: { since: 'v1' },167 // Side question (/btw) against the session's conversation context.168 // Single-turn, tool-free LLM call via runForkedAgent (cache path).169 session_btw: { since: 'v1' },170 // Direct daemon-side shell execution for an existing session.171 // Advertised CONDITIONALLY: operators must explicitly enable it and172 // configure bearer auth. Clients must still send a session-bound173 // X-Qwen-Client-Id when calling the route.174 session_shell_command: { since: 'v1' },175 // Daemon hosts a workspace-shared MCP transport176 // pool (`QwenAgent.mcpPool`); `GET /workspace/mcp` reflects pool-level177 // accounting (`entryCount`, `entrySummary` on each per-server cell).178 // Advertised CONDITIONALLY — the kill switch179 // `QWEN_SERVE_NO_MCP_POOL=1` env var falls back to per-session MCP180 // clients and the tag is omitted so SDK consumers181 // pre-flighting on the tag get accurate "pool is on" semantics.182 mcp_workspace_pool: { since: 'v1' },183 // `POST /workspace/mcp/:server/restart`184 // accepts an optional `?entryIndex=N` (or `*`) query parameter185 // and may return the new `{entries: RestartResult[]}` shape when186 // the pool holds multiple entries for the same server name (e.g.187 // sessions injected divergent OAuth headers). Single-entry188 // restarts continue to return the legacy `{restarted, durationMs}`189 // shape for compatibility with pre-F2 SDK clients. Advertised190 // CONDITIONALLY in lockstep with `mcp_workspace_pool`: pool191 // off → both tags absent, pool on → both tags present. Operators192 // pre-flighting on this tag can branch on whether the response193 // shape may include `entries[]`.194 mcp_pool_restart: { since: 'v1' },195 // Daemon was booted with `--require-auth` (or196 // `requireAuth: true`), so even loopback callers must carry a bearer197 // token. Advertised CONDITIONALLY — only when the flag is on — so198 // SDK clients can branch on its presence to surface a clear "this199 // deployment requires auth" hint instead of speculatively trying200 // requests and parsing the resulting 401 body. Loopback developer201 // defaults (no flag) omit the tag, preserving the bit-for-bit shape202 // older clients expect.203 require_auth: { since: 'v1' },204 // Daemon was booted with `--allow-origin <pattern>`205 // (at least one entry, including the `*` literal). Advertised206 // CONDITIONALLY — only when the flag is set — so browser SDK clients207 // can pre-flight whether the daemon will honor their cross-origin208 // request before issuing it (and parsing a 403). The configured209 // pattern list is intentionally NOT echoed in the capabilities210 // envelope — browser webui knows its own origin, and surfacing the211 // list would let an unauthenticated `/capabilities` reader212 // enumerate every trusted origin, which is useful recon for a213 // misconfigured deployment.214 allow_origin: { since: 'v1' },215 // Daemon exposes the device-flow auth surface216 // (`POST /workspace/auth/device-flow`, GET/DELETE on `/:id`, and217 // `GET /workspace/auth/status`). Advertised UNCONDITIONALLY: the218 // routes themselves return `400 unsupported_provider` if the daemon219 // can't satisfy a specific provider, so clients always probe via the220 // route. The list of supported providers is surfaced through the221 // status route (extension data on `/capabilities` would inflate the222 // descriptor shape; we keep the registry uniform).223 auth_device_flow: { since: 'v1' },224 permission_mediation: {225 since: 'v1',226 modes: ['first-responder', 'designated', 'consensus', 'local-only'],227 },228 prompt_absolute_deadline: { since: 'v1' },229 writer_idle_timeout: { since: 'v1' },230 non_blocking_prompt: { since: 'v1' },231 session_language: { since: 'v1' },232 session_rewind: { since: 'v1' },233 workspace_hooks: { since: 'v1' },234 session_hooks: { since: 'v1' },235 workspace_extensions: { since: 'v1' },236 session_branch: { since: 'v1' },237 rate_limit: { since: 'v1' },238 workspace_reload: { since: 'v1' },239 // Phase 2 "reverse tool channel" (issue #5626). A connected WS client (e.g.240 // the Chrome extension) can host an MCP server that the daemon's agent241 // calls by carrying `mcp_message` JSON-RPC frames over the daemon WS,242 // reusing the SDK-MCP-server control-plane pattern. Inbound WS frame types:243 // `mcp_register` { server }, `mcp_message` { id, server, payload }244 // (bidirectional, request/response correlated by `id`), `mcp_unregister`245 // { server }. Advertised CONDITIONALLY so clients pre-flight this tag before246 // attempting to register a client-hosted server. `runQwenServe` enables it247 // only when explicitly requested by option or env.248 client_mcp_over_ws: { since: 'v1' },249 // Plan C "CDP tunnel" (issue #5626): the daemon exposes a `/cdp` WebSocket250 // where a loopback CDP client drives ONE real tab251 // via the extension's `chrome.debugger`, tunneled over `/acp` as `cdp_*`252 // frames. Advertised when explicitly enabled or when the daemon is serving a253 // Chrome extension origin.254 cdp_tunnel_over_ws: { since: 'v1' },255 // Daemon hosts the `/voice/stream` WebSocket: the browser captures audio and256 // streams raw PCM, the daemon transcribes server-side via the configured257 // `voiceModel` (credentials never reach the client). Advertised258 // UNCONDITIONALLY (like `auth_device_flow`): presence means the endpoint259 // exists, not that a voice model is configured. The WS returns an `error`260 // frame when no transcribable `voiceModel` is set, so clients probe by261 // connecting rather than reading ambient settings into `/capabilities` (which262 // would make the envelope depend on the user's home config). `modes`263 // enumerates the two transcription paths (realtime vs. on-stop batch).264 voice_transcribe: { since: 'v1', modes: ['streaming', 'batch'] },265};266/**267 * Subset of `ServeFeature` whose advertisement depends on runtime config268 * (currently just `require_auth`, which is announced only when the269 * daemon was started with `--require-auth`). Each entry pairs the270 * feature key with a predicate over `AdvertiseFeatureToggles` — the271 * toggle decision lives next to the feature key, so adding a new272 * conditional tag is **two coordinated changes** instead of four:273 *274 * 1. Register the tag in `SERVE_CAPABILITY_REGISTRY` above with its275 * `since` protocol version (just like baseline tags).276 * 2. Add an entry to THIS Map mapping the tag to a toggle predicate277 * (extend `AdvertiseFeatureToggles` first if the predicate needs a278 * new field to read).279 *280 * The previous `Set` + per-feature `if`-branch shape needed FOUR281 * coordinated changes (registry, set, toggles interface, predicate282 * branch) and silently fail-CLOSED when the branch was missed —283 * fail-CLOSED is good, but invisible to the contributor adding the284 * tag. The Map shape collapses the predicate-decision and the285 * set-membership into one entry, so a future contributor either286 * registers the predicate (advertised when toggle on) or doesn't287 * register the tag in the Map at all (advertised unconditionally288 * like baseline tags) — both are intentional, neither is a silent289 * miss.290 *291 * Reviewed-through-failure: the292 * `every conditional tag advertises when its toggle is on` test in293 * `server.test.ts` iterates this Map's keys, so a future tag added294 * here whose predicate isn't honored by `getAdvertisedServeFeatures`295 * fails the suite — adoption-of-record for the Map shape rather than296 * relying on a hand-maintained invariant.297 */298export const CONDITIONAL_SERVE_FEATURES = new Map([299 ['require_auth', (toggles) => toggles.requireAuth === true],300 ['mcp_workspace_pool', (toggles) => toggles.mcpPoolActive === true],301 ['mcp_pool_restart', (toggles) => toggles.mcpPoolActive === true],302 ['allow_origin', (toggles) => toggles.allowOriginActive === true],303 [304 'prompt_absolute_deadline',305 (toggles) => typeof toggles.promptDeadlineMs === 'number' &&306 toggles.promptDeadlineMs > 0,307 ],308 [309 'writer_idle_timeout',310 (toggles) => typeof toggles.writerIdleTimeoutMs === 'number' &&311 toggles.writerIdleTimeoutMs > 0,312 ],313 ['workspace_settings', (toggles) => toggles.persistSettingAvailable === true],314 ['workspace_voice', (toggles) => toggles.persistSettingAvailable === true],315 [316 'workspace_voice_transcription',317 (toggles) => toggles.voiceTranscriptionAvailable === true,318 ],319 [320 'session_shell_command',321 (toggles) => toggles.sessionShellCommandEnabled === true,322 ],323 ['rate_limit', (toggles) => toggles.rateLimit === true],324 ['workspace_reload', (toggles) => toggles.reloadAvailable === true],325 ['client_mcp_over_ws', (toggles) => toggles.clientMcpOverWsEnabled === true],326 ['cdp_tunnel_over_ws', (toggles) => toggles.cdpTunnelOverWsEnabled === true],327 [328 // Advertised whenever the `/voice/stream` WS endpoint exists. A configured329 // token (or `--require-auth`) no longer suppresses it: browsers can't set330 // an `Authorization` header on a WebSocket, so the Web Shell carries the331 // bearer token in the `Sec-WebSocket-Protocol` subprotocol, which the ACP332 // upgrade listener verifies (see acp-http/index.ts).333 'voice_transcribe',334 (toggles) => toggles.voiceWsAvailable !== false,335 ],336]);337export const SERVE_FEATURES = Object.freeze(Object.keys(SERVE_CAPABILITY_REGISTRY));338function serveProtocolVersionIndex(version) {339 return SUPPORTED_SERVE_PROTOCOL_VERSIONS.indexOf(version);340}341function isFeatureAvailableInProtocol(feature, protocolVersion) {342 return (serveProtocolVersionIndex(SERVE_CAPABILITY_REGISTRY[feature].since) <=343 serveProtocolVersionIndex(protocolVersion));344}345export function getRegisteredServeFeatures() {346 return [...SERVE_FEATURES];347}348export function getAdvertisedServeFeatures(protocolVersion = SERVE_PROTOCOL_VERSION, toggles = {}) {349 return SERVE_FEATURES.filter((feature) => {350 if (!isFeatureAvailableInProtocol(feature, protocolVersion))351 return false;352 // Conditional tags route through the per-feature toggle predicate;353 // baseline tags (no Map entry) advertise unconditionally. Without354 // this gate every daemon would advertise the conditional tags355 // regardless of operator opt-in, breaking the "tag presence =356 // behavior is on" contract clients depend on.357 const predicate = CONDITIONAL_SERVE_FEATURES.get(feature);358 if (predicate !== undefined)359 return predicate(toggles);360 return true;361 });362}363export function getServeFeatures() {364 return getAdvertisedServeFeatures();365}366export function getServeProtocolVersions() {367 return {368 current: SERVE_PROTOCOL_VERSION,369 supported: [...SUPPORTED_SERVE_PROTOCOL_VERSIONS],370 };371}372//# sourceMappingURL=capabilities.js.map