diff --git a/.github/workflows/sdk-java.yml b/.github/workflows/sdk-java.yml index 8339e25c696..f36caf98c16 100644 --- a/.github/workflows/sdk-java.yml +++ b/.github/workflows/sdk-java.yml @@ -7,6 +7,7 @@ on: - 'release/**' paths: - 'packages/sdk-java/**' + - 'packages/core/src/config/approval-modes.json' - 'packages/cli/src/commands/serve.ts' - 'packages/cli/src/serve/**' - 'packages/cli/src/acp-integration/**' @@ -25,6 +26,7 @@ on: - 'release/**' paths: - 'packages/sdk-java/**' + - 'packages/core/src/config/approval-modes.json' - 'packages/cli/src/commands/serve.ts' - 'packages/cli/src/serve/**' - 'packages/cli/src/acp-integration/**' diff --git a/.github/workflows/sdk-python.yml b/.github/workflows/sdk-python.yml index 33b8b60aa7e..41b7710e851 100644 --- a/.github/workflows/sdk-python.yml +++ b/.github/workflows/sdk-python.yml @@ -10,6 +10,7 @@ on: - 'release/**' paths: - 'packages/sdk-python/**' + - 'packages/core/src/config/approval-modes.json' - '.github/workflows/sdk-python.yml' push: branches: @@ -17,6 +18,7 @@ on: - 'release/**' paths: - 'packages/sdk-python/**' + - 'packages/core/src/config/approval-modes.json' - '.github/workflows/sdk-python.yml' jobs: diff --git a/docs/design/2026-08-23-approval-mode-contract.md b/docs/design/2026-08-23-approval-mode-contract.md new file mode 100644 index 00000000000..96fbc1b712d --- /dev/null +++ b/docs/design/2026-08-23-approval-mode-contract.md @@ -0,0 +1,36 @@ +# Approval Mode Contract + +## Decision + +Keep `ApprovalMode` and `APPROVAL_MODES` in core as the runtime source of +truth. TypeScript packages derive their local types and validators from either +that core contract or the TypeScript SDK's checked tuple. Python and Java keep +native public types, with their accepted values checked against a small JSON +fixture that is also checked against core. + +This avoids adding a runtime dependency from the published SDKs to core and +does not introduce code generation for one five-value domain. + +## Changes + +- Export the string-union form of core's `ApprovalMode`. +- Replace repeated TypeScript unions and validation arrays with the core or + SDK contract. +- Type the CLI and TypeScript SDK system-message `permission_mode` fields with + the shared union. +- Check core, TypeScript, Python, and Java accepted values against one fixture + in their existing test suites. +- Trigger the Python and Java SDK workflows when the fixture changes. + +Adjacent value domains such as channel modes, hook permission decisions, and +desktop cycling preferences remain independent because their supported values +and semantics intentionally differ. + +## Verification + +- Core approval-mode tests. +- CLI ACP and non-interactive type checking plus focused tests. +- TypeScript SDK drift and query-option tests. +- Python validation tests. +- Java permission-mode tests. +- Relevant lint, typecheck, and build commands. diff --git a/packages/acp-bridge/src/bridgeClient.ts b/packages/acp-bridge/src/bridgeClient.ts index 1ab568ea3e6..ab9a4b12f4a 100644 --- a/packages/acp-bridge/src/bridgeClient.ts +++ b/packages/acp-bridge/src/bridgeClient.ts @@ -20,6 +20,7 @@ import type { WriteTextFileResponse, } from '@agentclientprotocol/sdk'; import { RequestError } from '@agentclientprotocol/sdk'; +import { APPROVAL_MODES } from '@qwen-code/qwen-code-core'; import type { BridgeEvent, EventBus } from './eventBus.js'; // Wire constants shared with the child-side caller (`Session.ts`) and, for the // SSE event type, the SDK validator + browser consumer — single sources of truth @@ -539,17 +540,12 @@ const MAX_SUGGESTION_LENGTH = 500; const EARLY_EVENT_TTL_MS = 60_000; // Known approval-mode ids accepted on the in-session `current_mode_update` -// demux path. Mirrors the `modeMap` keys in `Session.setMode` (CLI); an id -// outside this set is dropped before it fans out to SSE clients / the SDK -// reducer. Keep the two in lockstep. Exported so the bridge's reconcile and +// demux path. An id outside this set is dropped before it fans out to SSE +// clients / the SDK reducer. Exported so the bridge's reconcile and // snapshot-seed paths apply the same enum backstop to agent-supplied mode ids. -export const KNOWN_APPROVAL_MODES: ReadonlySet = new Set([ - 'plan', - 'default', - 'auto-edit', - 'auto', - 'yolo', -]); +export const KNOWN_APPROVAL_MODES: ReadonlySet = new Set( + APPROVAL_MODES, +); /** * Human-readable label for a `fs.Stats` object's kind, used in the diff --git a/packages/cli/src/acp-integration/acpAgent.test.ts b/packages/cli/src/acp-integration/acpAgent.test.ts index 6ad752811cf..1bb6d82c0bf 100644 --- a/packages/cli/src/acp-integration/acpAgent.test.ts +++ b/packages/cli/src/acp-integration/acpAgent.test.ts @@ -291,8 +291,12 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({ })), dispose: vi.fn(), })), - APPROVAL_MODE_INFO: {}, - APPROVAL_MODES: [], + APPROVAL_MODE_INFO: ( + await importOriginal() + ).APPROVAL_MODE_INFO, + APPROVAL_MODES: ( + await importOriginal() + ).APPROVAL_MODES, applyReasoningEffort: ( config: { setReasoningEffort(effort: string | undefined): void; @@ -7745,7 +7749,10 @@ describe('QwenAgent MCP SSE/HTTP support', () => { workspaceCwd: '/tmp', state: { models: { currentModelId: 'm(api-key)', availableModels: [] }, - modes: { currentModeId: 'default', availableModes: [] }, + modes: { + currentModeId: 'default', + availableModes: APPROVAL_MODES.map((id) => ({ id })), + }, }, }); expect(supportedCommands).toEqual({ diff --git a/packages/cli/src/acp-integration/acpAgent.ts b/packages/cli/src/acp-integration/acpAgent.ts index 45ff95d5d62..89b02318748 100644 --- a/packages/cli/src/acp-integration/acpAgent.ts +++ b/packages/cli/src/acp-integration/acpAgent.ts @@ -1362,7 +1362,7 @@ const QWEN_CORE_SETTING_DEFINITIONS = { 'general.language': { type: 'string' }, 'tools.approvalMode': { type: 'enum', - values: ['plan', 'default', 'auto-edit', 'auto', 'yolo'], + values: APPROVAL_MODES, }, 'general.vimMode': { type: 'boolean' }, 'general.enableAutoUpdate': { type: 'boolean' }, diff --git a/packages/cli/src/acp-integration/session/types.ts b/packages/cli/src/acp-integration/session/types.ts index 5a97ef3a73a..eee132fe524 100644 --- a/packages/cli/src/acp-integration/session/types.ts +++ b/packages/cli/src/acp-integration/session/types.ts @@ -5,6 +5,7 @@ */ import type { + ApprovalModeValue, Config, ToolArtifact, ToolResultBoundaryArtifact, @@ -17,12 +18,7 @@ import type { } from '@agentclientprotocol/sdk'; import type { MessageRewriteMiddleware } from './rewrite/index.js'; -export type ApprovalModeValue = - | 'plan' - | 'default' - | 'auto-edit' - | 'auto' - | 'yolo'; +export type { ApprovalModeValue }; /** * Interface for sending session updates to the ACP client. diff --git a/packages/cli/src/commands/channel/config-utils.ts b/packages/cli/src/commands/channel/config-utils.ts index 68c55d66d4e..79d568b129a 100644 --- a/packages/cli/src/commands/channel/config-utils.ts +++ b/packages/cli/src/commands/channel/config-utils.ts @@ -4,17 +4,12 @@ import type { ChannelWebhookSourceConfig, ChannelWebhookTargetConfig, } from '@qwen-code/channel-base'; +import { APPROVAL_MODES } from '@qwen-code/qwen-code-core'; import { resolveChannelCwd } from './channel-cwd.js'; import { getPlugin, supportedTypes } from './channel-registry.js'; const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/; -const CHANNEL_APPROVAL_MODES = new Set([ - 'plan', - 'default', - 'auto-edit', - 'auto', - 'yolo', -]); +const CHANNEL_APPROVAL_MODES = new Set(APPROVAL_MODES); export { findCliEntryPath } from './cli-entry-path.js'; diff --git a/packages/cli/src/commands/channel/runtime.test.ts b/packages/cli/src/commands/channel/runtime.test.ts index 9d1b9221731..fb29c04763c 100644 --- a/packages/cli/src/commands/channel/runtime.test.ts +++ b/packages/cli/src/commands/channel/runtime.test.ts @@ -14,7 +14,10 @@ import { sessionsPath, } from './runtime.js'; -vi.mock('@qwen-code/qwen-code-core', () => ({ +vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({ + APPROVAL_MODES: ( + await importOriginal() + ).APPROVAL_MODES, Storage: { getGlobalQwenDir: () => '/tmp/qwen' }, hashDaemonWorkspace: (workspace: string) => workspace === '/workspace' ? 'workspace-hash' : 'other-hash', diff --git a/packages/cli/src/commands/review/run.ts b/packages/cli/src/commands/review/run.ts index 54c0a127271..30ec7099614 100644 --- a/packages/cli/src/commands/review/run.ts +++ b/packages/cli/src/commands/review/run.ts @@ -26,7 +26,10 @@ // reached a verdict" from "blocking verdict" (opt-in via --fail-on). import type { CommandModule } from 'yargs'; -import { isUnusableScriptEntry } from '@qwen-code/qwen-code-core'; +import { + APPROVAL_MODES, + isUnusableScriptEntry, +} from '@qwen-code/qwen-code-core'; import { spawn, execFileSync } from 'node:child_process'; import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; @@ -677,7 +680,7 @@ export const runCommand: CommandModule = { .option('approval-mode', { type: 'string', default: 'yolo', - choices: ['plan', 'default', 'auto-edit', 'auto', 'yolo'], + choices: APPROVAL_MODES, describe: 'Approval mode for the child CLI. The default is yolo: headless runs cannot answer ' + 'confirmation prompts, and anything still unapproved would be auto-denied mid-review.', diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index f2c60351b7b..34f724ec56d 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -290,6 +290,27 @@ describe('parseArguments', () => { process.argv = originalArgv; }); + it('includes every approval mode description in --help', async () => { + process.argv = ['node', 'script.js', '--help']; + const output: string[] = []; + const log = vi.spyOn(console, 'log').mockImplementation((...args) => { + output.push(args.join(' ')); + }); + try { + await expect(parseArguments()).rejects.toThrow( + 'process.exit unexpectedly called with "0"', + ); + const help = output.join(''); + for (const mode of ServerConfig.APPROVAL_MODES) { + expect(help).toContain( + `${mode} (${ServerConfig.APPROVAL_MODE_INFO[mode].description})`, + ); + } + } finally { + log.mockRestore(); + } + }); + it('should throw an error when both --prompt and --prompt-interactive are used together', async () => { process.argv = [ 'node', @@ -4478,9 +4499,19 @@ describe('loadCliConfig approval mode', () => { it('should normalize approval mode values from settings', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments(); - const settings: Settings = { - tools: { approvalMode: ServerConfig.ApprovalMode.AUTO_EDIT }, - }; + const settings = { + tools: { approvalMode: 'auto_edit' }, + } as unknown as Settings; + const config = await loadCliConfig(settings, argv, undefined, []); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT); + }); + + it('should normalize legacy autoedit approval mode from settings', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const settings = { + tools: { approvalMode: 'autoedit' }, + } as unknown as Settings; const config = await loadCliConfig(settings, argv, undefined, []); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 3646f1fb863..78d166edfb8 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -6,6 +6,8 @@ import { ApprovalMode, + APPROVAL_MODE_INFO, + APPROVAL_MODES, AuthType, Config, DEFAULT_QWEN_EMBEDDING_MODEL, @@ -100,14 +102,6 @@ function resolveLocaleForExtensions(settings: Settings): string { return detectSystemLanguage(); } -const VALID_APPROVAL_MODE_VALUES = [ - 'plan', - 'default', - 'auto-edit', - 'auto', - 'yolo', -] as const; - const SKILL_LEVELS: readonly SkillLevel[] = [ 'project', 'user', @@ -121,7 +115,7 @@ function isSkillLevel(value: unknown): value is SkillLevel { function formatApprovalModeError(value: string): Error { return new Error( - `Invalid approval mode: ${value}. Valid values are: ${VALID_APPROVAL_MODE_VALUES.join( + `Invalid approval mode: ${value}. Valid values are: ${APPROVAL_MODES.join( ', ', )}`, ); @@ -129,22 +123,15 @@ function formatApprovalModeError(value: string): Error { function parseApprovalModeValue(value: string): ApprovalMode { const normalized = value.trim().toLowerCase(); - switch (normalized) { - case 'plan': - return ApprovalMode.PLAN; - case 'default': - return ApprovalMode.DEFAULT; - case 'yolo': - return ApprovalMode.YOLO; - case 'auto_edit': - case 'autoedit': - case 'auto-edit': - return ApprovalMode.AUTO_EDIT; - case 'auto': - return ApprovalMode.AUTO; - default: - throw formatApprovalModeError(value); + const canonical = + normalized === 'auto_edit' || normalized === 'autoedit' + ? ApprovalMode.AUTO_EDIT + : normalized; + const approvalMode = APPROVAL_MODES.find((mode) => mode === canonical); + if (approvalMode === undefined) { + throw formatApprovalModeError(value); } + return approvalMode; } export interface CliArgs { @@ -547,6 +534,9 @@ function normalizeOutputFormat( export async function parseArguments(): Promise { let rawArgv = hideBin(process.argv); + const approvalModeDescription = APPROVAL_MODES.map( + (mode) => `${mode} (${APPROVAL_MODE_INFO[mode].description})`, + ).join(', '); // hack: if the first argument is the CLI entry point, remove it if ( @@ -715,9 +705,8 @@ export async function parseArguments(): Promise { }) .option('approval-mode', { type: 'string', - choices: ['plan', 'default', 'auto-edit', 'auto', 'yolo'], - description: - 'Set the approval mode: plan (plan only), default (prompt for approval), auto-edit (auto-approve edit tools), auto (LLM classifier auto-approves safe actions, blocks risky ones), yolo (auto-approve all tools)', + choices: APPROVAL_MODES, + description: `Set the approval mode: ${approvalModeDescription}`, }) .option('acp', { type: 'boolean', diff --git a/packages/cli/src/nonInteractive/control/controllers/permissionController.test.ts b/packages/cli/src/nonInteractive/control/controllers/permissionController.test.ts index ab610ed0a99..349bcd55319 100644 --- a/packages/cli/src/nonInteractive/control/controllers/permissionController.test.ts +++ b/packages/cli/src/nonInteractive/control/controllers/permissionController.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it, vi } from 'vitest'; import { + ApprovalMode, InputFormat, ToolConfirmationOutcome, } from '@qwen-code/qwen-code-core'; @@ -50,6 +51,42 @@ function createRegistry(): IPendingRequestRegistry { } describe('PermissionController', () => { + it.each([ + [ApprovalMode.PLAN, 'allow'], + [ApprovalMode.DEFAULT, 'deny'], + [ApprovalMode.AUTO_EDIT, 'allow'], + [ApprovalMode.AUTO, 'allow'], + [ApprovalMode.YOLO, 'allow'], + ] as const)( + 'checks %s permission mode for can_use_tool', + async (mode, behavior) => { + const context = createContext(); + context.permissionMode = mode; + const controller = new PermissionController( + context, + createRegistry(), + 'PermissionController', + ); + + await expect( + controller.handleRequest( + { + subtype: 'can_use_tool', + tool_name: 'read_file', + tool_use_id: `tool-${mode}`, + input: {}, + permission_suggestions: null, + blocked_path: null, + }, + `request-${mode}`, + ), + ).resolves.toMatchObject({ + subtype: 'can_use_tool', + behavior, + }); + }, + ); + it('round-trips workflow approval through can_use_tool with updated input', async () => { const context = createContext(120_000); const resolvePendingApproval = vi.fn().mockResolvedValue(true); diff --git a/packages/cli/src/nonInteractive/control/controllers/permissionController.ts b/packages/cli/src/nonInteractive/control/controllers/permissionController.ts index 7f42f234015..2366d6eb0f2 100644 --- a/packages/cli/src/nonInteractive/control/controllers/permissionController.ts +++ b/packages/cli/src/nonInteractive/control/controllers/permissionController.ts @@ -18,12 +18,13 @@ import type { WaitingToolCall, ToolExecuteConfirmationDetails, ToolMcpConfirmationDetails, - ApprovalMode, TeammateApprovalRequestEvent, ToolConfirmationPayload, WorkflowApproval, } from '@qwen-code/qwen-code-core'; import { + ApprovalMode, + APPROVAL_MODES, InputFormat, ToolConfirmationOutcome, ToolNames, @@ -146,22 +147,23 @@ export class PermissionController extends BaseController { private checkPermissionMode(): { allowed: boolean; message?: string } { const mode = this.context.permissionMode; - // Map permission modes to approval logic (aligned with VALID_APPROVAL_MODE_VALUES) - switch (mode) { - case 'yolo': // Allow all tools - case 'auto-edit': // Auto-approve edit operations - case 'auto': // Auto-approve via LLM classifier — coreToolScheduler enforces the gate - case 'plan': // Auto-approve planning operations - return { allowed: true }; + if (mode === ApprovalMode.DEFAULT) { + return { + allowed: false, + message: + 'Tool execution requires manual approval. Update permission mode or approve via host.', + }; + } - case 'default': // TODO: allow all tools for test - default: - return { - allowed: false, - message: - 'Tool execution requires manual approval. Update permission mode or approve via host.', - }; + const validModes = APPROVAL_MODES as readonly PermissionMode[]; + if (validModes.includes(mode)) { + return { allowed: true }; } + + return { + allowed: false, + message: `Invalid permission mode: ${mode}. Valid values are: ${validModes.join(', ')}`, + }; } /** @@ -217,13 +219,7 @@ export class PermissionController extends BaseController { } const mode = payload.mode; - const validModes: PermissionMode[] = [ - 'default', - 'plan', - 'auto-edit', - 'auto', - 'yolo', - ]; + const validModes = APPROVAL_MODES as readonly PermissionMode[]; if (!validModes.includes(mode)) { throw new Error( diff --git a/packages/cli/src/nonInteractive/types.ts b/packages/cli/src/nonInteractive/types.ts index 53801c8b14f..6d649dbd3b7 100644 --- a/packages/cli/src/nonInteractive/types.ts +++ b/packages/cli/src/nonInteractive/types.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { ActiveGoal, + ApprovalModeValue, GoalSnapshotV2, SubagentConfig, McpToolProgressData, @@ -145,7 +146,7 @@ export interface CLISystemMessage { status: string; }>; model?: string; - permission_mode?: string; + permission_mode?: PermissionMode; slash_commands?: string[]; qwen_code_version?: string; output_style?: string; @@ -278,7 +279,7 @@ export interface CLIPartialAssistantMessage { parent_tool_use_id: string | null; } -export type PermissionMode = 'default' | 'plan' | 'auto-edit' | 'auto' | 'yolo'; +export type PermissionMode = ApprovalModeValue; /** * Permission suggestion for tool use requests diff --git a/packages/cli/src/serve/routes/workspace-git-diff.test.ts b/packages/cli/src/serve/routes/workspace-git-diff.test.ts index b41f8623b78..d8a04201202 100644 --- a/packages/cli/src/serve/routes/workspace-git-diff.test.ts +++ b/packages/cli/src/serve/routes/workspace-git-diff.test.ts @@ -24,7 +24,8 @@ import { registerWorkspaceQualifiedGitDiffRoutes, } from './workspace-git-diff.js'; -vi.mock('@qwen-code/qwen-code-core', () => ({ +vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({ + ...(await importOriginal()), fetchGitDiff: vi.fn(), fetchGitDiffHunksForFile: vi.fn(), })); diff --git a/packages/cli/src/serve/workspace-remember.test.ts b/packages/cli/src/serve/workspace-remember.test.ts index 2424edd5cc0..91f6f734fc1 100644 --- a/packages/cli/src/serve/workspace-remember.test.ts +++ b/packages/cli/src/serve/workspace-remember.test.ts @@ -35,7 +35,8 @@ const { mockDebugLogger } = vi.hoisted(() => ({ }, })); -vi.mock('@qwen-code/qwen-code-core', () => ({ +vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({ + ...(await importOriginal()), createDebugLogger: () => mockDebugLogger, })); diff --git a/packages/core/src/agents/background-agent-resume.ts b/packages/core/src/agents/background-agent-resume.ts index 0cb54e3ca3b..241e54571c4 100644 --- a/packages/core/src/agents/background-agent-resume.ts +++ b/packages/core/src/agents/background-agent-resume.ts @@ -7,7 +7,7 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import type { Content, Part } from '@google/genai'; -import type { Config } from '../config/config.js'; +import type { ApprovalModeValue, Config } from '../config/config.js'; import * as jsonl from '../utils/jsonl-utils.js'; import { createDebugLogger } from '../utils/debugLogger.js'; import { @@ -104,8 +104,6 @@ const WORKTREE_ISOLATION_BLOCKED_REASON = const INCOMPATIBLE_ISOLATION_BLOCKED_REASON = 'Background task isolation metadata is incompatible.'; -type ApprovalModeValue = 'plan' | 'default' | 'auto-edit' | 'auto' | 'yolo'; - /** * Returns true when the subagent's effective tool surface will include the * Skill tool. Mirrors `AgentCore.willHaveSkillTool()` for the resume path diff --git a/packages/core/src/config/approval-mode.ts b/packages/core/src/config/approval-mode.ts index 1697eade3b0..103535c542c 100644 --- a/packages/core/src/config/approval-mode.ts +++ b/packages/core/src/config/approval-mode.ts @@ -12,4 +12,6 @@ export enum ApprovalMode { YOLO = 'yolo', } +export type ApprovalModeValue = `${ApprovalMode}`; + export const APPROVAL_MODES = Object.values(ApprovalMode); diff --git a/packages/core/src/config/approval-modes.json b/packages/core/src/config/approval-modes.json new file mode 100644 index 00000000000..443b1871a18 --- /dev/null +++ b/packages/core/src/config/approval-modes.json @@ -0,0 +1 @@ +["plan", "default", "auto-edit", "auto", "yolo"] diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 01a980a2b96..7fd94794c7a 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -324,7 +324,11 @@ export { export type ModelInvocableCommandExecutorResult = string | { error: string }; -export { ApprovalMode, APPROVAL_MODES } from './approval-mode.js'; +export { + ApprovalMode, + APPROVAL_MODES, + type ApprovalModeValue, +} from './approval-mode.js'; /** * Thrown by `Config.setApprovalMode` when the requested mode would grant diff --git a/packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts b/packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts index 59901395eae..6c55c210c8e 100644 --- a/packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts +++ b/packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts @@ -1,6 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { PermissionMode } from '@qwen-code/sdk'; + +export type { PermissionMode }; + export interface Annotation { type: string; value: string; @@ -120,7 +124,7 @@ export interface SDKSystemMessage { status: string; }>; model?: string; - permission_mode?: string; + permission_mode?: PermissionMode; slash_commands?: string[]; qwen_code_version?: string; output_style?: string; @@ -230,8 +234,6 @@ export interface SDKPartialAssistantMessage { parent_tool_use_id: string | null; } -export type PermissionMode = 'default' | 'plan' | 'auto-edit' | 'auto' | 'yolo'; - /** * TODO: Align with `ToolCallConfirmationDetails` */ diff --git a/packages/sdk-java/qwencode/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java b/packages/sdk-java/qwencode/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java index b74f42854de..4d501107ccc 100644 --- a/packages/sdk-java/qwencode/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java +++ b/packages/sdk-java/qwencode/src/test/java/com/alibaba/qwen/code/cli/transport/PermissionModeTest.java @@ -1,11 +1,20 @@ package com.alibaba.qwen.code.cli.transport; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.alibaba.fastjson2.JSON; import com.alibaba.qwen.code.cli.protocol.data.PermissionMode; +import com.alibaba.qwen.code.daemon.DaemonApprovalMode; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class PermissionModeTest { @Test @@ -17,4 +26,32 @@ public void shouldBeReturnQwenPermissionModeValue() { assertEquals("yolo", PermissionMode.YOLO.getValue()); } + @Test + public void permissionModesMatchCoreContract() throws IOException { + Set expected = readCoreContract(); + Set cliModes = Arrays.stream(PermissionMode.values()) + .map(PermissionMode::getValue) + .collect(Collectors.toSet()); + Set daemonModes = Arrays.stream(DaemonApprovalMode.values()) + .map(DaemonApprovalMode::getWireValue) + .collect(Collectors.toSet()); + + assertEquals(expected, cliModes); + assertEquals(expected, daemonModes); + } + + private static Set readCoreContract() throws IOException { + Path directory = Path.of("").toAbsolutePath(); + while (directory != null) { + Path contract = directory.resolve( + "packages/core/src/config/approval-modes.json"); + if (Files.exists(contract)) { + return new HashSet<>(JSON.parseArray( + Files.readString(contract), String.class)); + } + directory = directory.getParent(); + } + throw new IOException("Cannot find approval-modes.json"); + } + } diff --git a/packages/sdk-python/tests/unit/test_validation.py b/packages/sdk-python/tests/unit/test_validation.py index 4e6168b9b44..c12e2cd5ab6 100644 --- a/packages/sdk-python/tests/unit/test_validation.py +++ b/packages/sdk-python/tests/unit/test_validation.py @@ -1,11 +1,13 @@ from __future__ import annotations +import json import re -from typing import Any, cast +from pathlib import Path +from typing import Any, cast, get_args import pytest from qwen_code_sdk.errors import ValidationError -from qwen_code_sdk.types import QueryOptions, TimeoutOptions +from qwen_code_sdk.types import PermissionMode, QueryOptions, TimeoutOptions from qwen_code_sdk.validation import validate_query_options VALID_UUID = "123e4567-e89b-12d3-a456-426614174000" @@ -77,12 +79,21 @@ def test_accepts_canonical_session_id_in_either_case() -> None: @pytest.mark.parametrize( "mode", - ["default", "plan", "auto-edit", "auto", "yolo"], + get_args(PermissionMode), ) def test_accepts_valid_permission_modes(mode: str) -> None: validate_query_options(QueryOptions.from_mapping({"permission_mode": mode})) +def test_permission_modes_match_core_contract() -> None: + contract_path = ( + Path(__file__).parents[3] / "core" / "src" / "config" / "approval-modes.json" + ) + expected = set(json.loads(contract_path.read_text(encoding="utf-8"))) + + assert set(get_args(PermissionMode)) == expected + + def test_rejects_invalid_permission_mode() -> None: with pytest.raises( ValidationError, diff --git a/packages/sdk-typescript/src/daemon-mcp/serve-bridge/tools/workspaceWrite.ts b/packages/sdk-typescript/src/daemon-mcp/serve-bridge/tools/workspaceWrite.ts index d770e36f5aa..5984f13e7d0 100644 --- a/packages/sdk-typescript/src/daemon-mcp/serve-bridge/tools/workspaceWrite.ts +++ b/packages/sdk-typescript/src/daemon-mcp/serve-bridge/tools/workspaceWrite.ts @@ -9,6 +9,13 @@ import { tool } from '../../tool.js'; import { formatJsonResult, formatToolError } from '../../formatters.js'; import type { BridgeState } from '../types.js'; import { handler, resolveSessionId } from '../helpers.js'; +import { PERMISSION_MODES } from '../../../types/permission-mode.js'; + +const SAFE_LOCAL_APPROVAL_MODES = new Set(['plan', 'default']); +const GLOBAL_SCOPE_APPROVAL_MODES = PERMISSION_MODES.filter( + (mode) => !SAFE_LOCAL_APPROVAL_MODES.has(mode), +); +const PERMISSION_MODE_LIST = PERMISSION_MODES.join(', '); /* eslint-disable @typescript-eslint/no-explicit-any */ export function workspaceWriteTools(state: BridgeState): any[] { @@ -80,11 +87,9 @@ export function workspaceWriteTools(state: BridgeState): any[] { tool( 'session_set_approval_mode', - 'Change the approval mode of a session (plan, default, auto-edit, auto, yolo).', + `Change the approval mode of a session (${PERMISSION_MODE_LIST}).`, { - mode: z - .enum(['plan', 'default', 'auto-edit', 'auto', 'yolo']) - .describe('Approval mode.'), + mode: z.enum(PERMISSION_MODES).describe('Approval mode.'), persist: z .boolean() .optional() @@ -97,10 +102,9 @@ export function workspaceWriteTools(state: BridgeState): any[] { handler(async (args) => { // Block dangerous modes and persistent changes without explicit opt-in if (!state.allowGlobalScope) { - const dangerousModes = ['yolo', 'auto', 'auto-edit']; - if (dangerousModes.includes(args.mode)) { + if (GLOBAL_SCOPE_APPROVAL_MODES.includes(args.mode)) { return formatToolError( - `Approval modes '${dangerousModes.join("', '")}' are restricted for security. Set QWEN_BRIDGE_ALLOW_GLOBAL_SCOPE=true to enable.`, + `Approval modes '${GLOBAL_SCOPE_APPROVAL_MODES.join("', '")}' are restricted for security. Set QWEN_BRIDGE_ALLOW_GLOBAL_SCOPE=true to enable.`, ); } if (args.persist) { diff --git a/packages/sdk-typescript/src/daemon/types.ts b/packages/sdk-typescript/src/daemon/types.ts index 7d851a36987..582e606a7b8 100644 --- a/packages/sdk-typescript/src/daemon/types.ts +++ b/packages/sdk-typescript/src/daemon/types.ts @@ -13,6 +13,11 @@ * signals breaking wire changes (per the design doc). */ +import { + PERMISSION_MODES, + type PermissionMode, +} from '../types/permission-mode.js'; + export type DaemonMode = 'http-bridge' | 'native'; /** Goal v2 wire types, duplicated here to keep the SDK independent of Core. */ @@ -2781,14 +2786,8 @@ export interface SetSessionLanguageResult { * Order matters for diagnostic UIs that render the modes in the * advertised sequence. */ -export const DAEMON_APPROVAL_MODES = [ - 'plan', - 'default', - 'auto-edit', - 'auto', - 'yolo', -] as const; -export type DaemonApprovalMode = (typeof DAEMON_APPROVAL_MODES)[number]; +export const DAEMON_APPROVAL_MODES = PERMISSION_MODES; +export type DaemonApprovalMode = PermissionMode; /** * Result body of `POST /session/:id/approval-mode`. `previous` and diff --git a/packages/sdk-typescript/src/index.ts b/packages/sdk-typescript/src/index.ts index bbf18334a25..406b7abfaa4 100644 --- a/packages/sdk-typescript/src/index.ts +++ b/packages/sdk-typescript/src/index.ts @@ -2,6 +2,7 @@ export { query } from './query/createQuery.js'; export { AbortError, isAbortError } from './types/errors.js'; export { Query } from './query/Query.js'; export { SdkLogger } from './utils/logger.js'; +export { PERMISSION_MODES } from './types/permission-mode.js'; // Daemon HTTP client (talks to `qwen serve`) export { diff --git a/packages/sdk-typescript/src/query/Query.ts b/packages/sdk-typescript/src/query/Query.ts index cbb77afdda3..13ec3e3e747 100644 --- a/packages/sdk-typescript/src/query/Query.ts +++ b/packages/sdk-typescript/src/query/Query.ts @@ -50,6 +50,7 @@ import { type SdkControlServerTransportOptions, } from '../daemon-mcp/SdkControlServerTransport.js'; import { ControlRequestType } from '../types/protocol.js'; +import type { PermissionMode } from '../types/permission-mode.js'; interface PendingControlRequest { resolve: (response: Record | null) => void; @@ -1004,7 +1005,7 @@ export class Query implements AsyncIterable { return this.sendControlRequest(ControlRequestType.CONTINUE_LAST_TURN); } - async setPermissionMode(mode: string): Promise { + async setPermissionMode(mode: PermissionMode): Promise { await this.sendControlRequest(ControlRequestType.SET_PERMISSION_MODE, { mode, }); diff --git a/packages/sdk-typescript/src/types/permission-mode.ts b/packages/sdk-typescript/src/types/permission-mode.ts new file mode 100644 index 00000000000..e8c03518639 --- /dev/null +++ b/packages/sdk-typescript/src/types/permission-mode.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +export const PERMISSION_MODES = [ + 'plan', + 'default', + 'auto-edit', + 'auto', + 'yolo', +] as const; + +export type PermissionMode = (typeof PERMISSION_MODES)[number]; diff --git a/packages/sdk-typescript/src/types/protocol.ts b/packages/sdk-typescript/src/types/protocol.ts index 97b765fbb32..026b4bbb86c 100644 --- a/packages/sdk-typescript/src/types/protocol.ts +++ b/packages/sdk-typescript/src/types/protocol.ts @@ -1,6 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { PermissionMode } from './permission-mode.js'; + +export type { PermissionMode } from './permission-mode.js'; export interface Annotation { type: string; value: string; @@ -120,7 +123,7 @@ export interface SDKSystemMessage { status: string; }>; model?: string; - permission_mode?: string; + permission_mode?: PermissionMode; slash_commands?: string[]; qwen_code_version?: string; output_style?: string; @@ -230,8 +233,6 @@ export interface SDKPartialAssistantMessage { parent_tool_use_id: string | null; } -export type PermissionMode = 'default' | 'plan' | 'auto-edit' | 'auto' | 'yolo'; - /** * Authentication types supported by the CLI. * Aligns with CLI's --auth-type parameter. diff --git a/packages/sdk-typescript/src/types/queryOptionsSchema.ts b/packages/sdk-typescript/src/types/queryOptionsSchema.ts index 20064a8dd85..b19e7e164e2 100644 --- a/packages/sdk-typescript/src/types/queryOptionsSchema.ts +++ b/packages/sdk-typescript/src/types/queryOptionsSchema.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import type { CanUseTool } from './types.js'; import type { SubagentConfig } from './protocol.js'; +import { PERMISSION_MODES } from './permission-mode.js'; const RESERVED_CLI_FLAGS = new Set([ '--input-format', @@ -200,9 +201,7 @@ export const QueryOptionsSchema = z QuerySystemPromptPresetSchema, ]) .optional(), - permissionMode: z - .enum(['default', 'plan', 'auto-edit', 'auto', 'yolo']) - .optional(), + permissionMode: z.enum(PERMISSION_MODES).optional(), canUseTool: z .custom((val) => typeof val === 'function', { message: 'canUseTool must be a function', diff --git a/packages/sdk-typescript/src/types/types.ts b/packages/sdk-typescript/src/types/types.ts index ca930178df7..f069049d103 100644 --- a/packages/sdk-typescript/src/types/types.ts +++ b/packages/sdk-typescript/src/types/types.ts @@ -306,7 +306,7 @@ export interface QueryOptions { * @see allowedTools For auto-approving specific tools * @see excludeTools For blocking specific tools */ - permissionMode?: 'default' | 'plan' | 'auto-edit' | 'auto' | 'yolo'; + permissionMode?: PermissionMode; /** * Custom permission handler for tool execution approval. diff --git a/packages/sdk-typescript/test/unit/approval-mode-drift.test.ts b/packages/sdk-typescript/test/unit/approval-mode-drift.test.ts index ca71c84e26f..12bcea27ec0 100644 --- a/packages/sdk-typescript/test/unit/approval-mode-drift.test.ts +++ b/packages/sdk-typescript/test/unit/approval-mode-drift.test.ts @@ -4,41 +4,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * Drift detector for the SDK ↔ core approval-mode contract. - * - * Two sources of truth converge here: - * 1. core's `APPROVAL_MODES` const array (= `Object.values(ApprovalMode)`, - * consumed by `Config.setApprovalMode` and the daemon route's body - * validator) - * 2. SDK's `DAEMON_APPROVAL_MODES` literal tuple (mirrored for SDK - * consumers; backs the `DaemonApprovalMode` union) - * - * The third source — the `ApprovalMode` enum itself — is structurally - * tied to `APPROVAL_MODES` via `Object.values(ApprovalMode)` at the - * core's definition site, so there's no separate assertion needed for - * it: `APPROVAL_MODES` is always a 1:1 mirror by construction. - * - * If `DAEMON_APPROVAL_MODES` drifts away from `APPROVAL_MODES` (e.g. a - * future fifth mode added to the enum but not the SDK list), this test - * fires before runtime and before the protocol docs go out of sync. - * - * Lives in the SDK package (not the CLI) because the SDK already - * depends on `@qwen-code/qwen-code-core`, so all three identifiers are - * visible — and this is the package whose contract the test is - * actually pinning. - * - * #4175 Wave 4 PR 17 (#4282 fold-in 1, wenshao review). - */ +import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; +import { KNOWN_APPROVAL_MODES } from '@qwen-code/acp-bridge/bridgeClient'; import { APPROVAL_MODES } from '@qwen-code/qwen-code-core'; -import { DAEMON_APPROVAL_MODES } from '../../src/index.js'; +import { DAEMON_APPROVAL_MODES, PERMISSION_MODES } from '../../src/index.js'; + +const crossLanguageContract = JSON.parse( + readFileSync( + new URL('../../../core/src/config/approval-modes.json', import.meta.url), + 'utf8', + ), +) as string[]; describe('approval-mode SDK ↔ core drift detection', () => { - it('DAEMON_APPROVAL_MODES (SDK) mirrors core APPROVAL_MODES exactly', () => { - // Order matters — diagnostic UIs that render modes in registration - // order stay stable across SDK / daemon versions only when the two - // tuples are sequence-equal, not just set-equal. - expect([...DAEMON_APPROVAL_MODES]).toEqual([...APPROVAL_MODES]); + it('keeps core and SDK contracts synchronized', () => { + expect([...APPROVAL_MODES]).toEqual(crossLanguageContract); + expect([...PERMISSION_MODES]).toEqual(crossLanguageContract); + expect([...KNOWN_APPROVAL_MODES]).toEqual(crossLanguageContract); + expect(DAEMON_APPROVAL_MODES).toBe(PERMISSION_MODES); }); }); diff --git a/packages/sdk-typescript/test/unit/serve-bridge.test.ts b/packages/sdk-typescript/test/unit/serve-bridge.test.ts index c74add477d1..a5c22938e47 100644 --- a/packages/sdk-typescript/test/unit/serve-bridge.test.ts +++ b/packages/sdk-typescript/test/unit/serve-bridge.test.ts @@ -577,29 +577,32 @@ describe('serve-bridge', () => { expect(result.content[0].text).toContain('Global scope is disabled'); }); - it('should reject yolo approval mode without allowGlobalScope', async () => { - const { state } = makeMockState({ - defaultSessionId: 'test-session', - }); - state.allowGlobalScope = false; + it.each(['auto-edit', 'auto', 'yolo'] as const)( + 'should reject %s approval mode without allowGlobalScope', + async (mode) => { + const { state } = makeMockState({ + defaultSessionId: 'test-session', + }); + state.allowGlobalScope = false; - const { workspaceWriteTools } = await import( - '../../src/daemon-mcp/serve-bridge/tools/workspaceWrite.js' - ); - const tools = workspaceWriteTools(state); - const approvalTool = tools.find( - (t: { name: string }) => t.name === 'session_set_approval_mode', - ); + const { workspaceWriteTools } = await import( + '../../src/daemon-mcp/serve-bridge/tools/workspaceWrite.js' + ); + const tools = workspaceWriteTools(state); + const approvalTool = tools.find( + (t: { name: string }) => t.name === 'session_set_approval_mode', + ); - const result = await approvalTool.handler( - { mode: 'yolo', session_id: 'test-session' }, - {}, - ); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('restricted for security'); - }); + const result = await approvalTool.handler( + { mode, session_id: 'test-session' }, + {}, + ); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('restricted for security'); + }, + ); - it('should reject auto-edit approval mode without allowGlobalScope', async () => { + it('should reject persistent approval mode change without allowGlobalScope', async () => { const { state } = makeMockState({ defaultSessionId: 'test-session', }); @@ -614,18 +617,46 @@ describe('serve-bridge', () => { ); const result = await approvalTool.handler( - { mode: 'auto-edit', session_id: 'test-session' }, + { mode: 'default', persist: true, session_id: 'test-session' }, {}, ); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('restricted for security'); }); - it('should reject persistent approval mode change without allowGlobalScope', async () => { - const { state } = makeMockState({ + it.each(['default', 'plan'] as const)( + 'should allow local %s approval mode changes without allowGlobalScope', + async (mode) => { + const { state, calls } = makeMockState({ + defaultSessionId: 'default-session', + }); + state.allowGlobalScope = false; + + const { workspaceWriteTools } = await import( + '../../src/daemon-mcp/serve-bridge/tools/workspaceWrite.js' + ); + const tools = workspaceWriteTools(state); + const approvalTool = tools.find( + (t: { name: string }) => t.name === 'session_set_approval_mode', + ); + + const result = await approvalTool.handler( + { mode, session_id: 'other-session' }, + {}, + ); + expect(result.isError).toBeUndefined(); + expect(calls[0]?.url).toBe( + 'http://127.0.0.1:4170/session/other-session/approval-mode', + ); + expect(JSON.parse(calls[0]?.body ?? '{}')).toEqual({ mode }); + }, + ); + + it('should allow persistent elevated approval mode changes with allowGlobalScope', async () => { + const { state, calls } = makeMockState({ defaultSessionId: 'test-session', }); - state.allowGlobalScope = false; + state.allowGlobalScope = true; const { workspaceWriteTools } = await import( '../../src/daemon-mcp/serve-bridge/tools/workspaceWrite.js' @@ -636,11 +667,17 @@ describe('serve-bridge', () => { ); const result = await approvalTool.handler( - { mode: 'default', persist: true, session_id: 'test-session' }, + { mode: 'auto', persist: true, session_id: 'test-session' }, {}, ); - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('restricted for security'); + expect(result.isError).toBeUndefined(); + expect(calls[0]?.url).toBe( + 'http://127.0.0.1:4170/session/test-session/approval-mode', + ); + expect(JSON.parse(calls[0]?.body ?? '{}')).toEqual({ + mode: 'auto', + persist: true, + }); }); it('should allow read-only agents_manage actions with global scope', async () => { diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 8149c19fdeb..e57e3e692d5 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -286,14 +286,9 @@ export class QwenAgentManager { const obj = (init || {}) as Record; const modes = obj['modes'] as | { - currentModeId?: - | 'plan' - | 'default' - | 'auto-edit' - | 'auto' - | 'yolo'; + currentModeId?: ApprovalModeValue; availableModes?: Array<{ - id: 'plan' | 'default' | 'auto-edit' | 'auto' | 'yolo'; + id: ApprovalModeValue; name: string; description: string; }>; @@ -1404,9 +1399,9 @@ export class QwenAgentManager { */ onModeInfo( callback: (info: { - currentModeId?: 'plan' | 'default' | 'auto-edit' | 'auto' | 'yolo'; + currentModeId?: ApprovalModeValue; availableModes?: Array<{ - id: 'plan' | 'default' | 'auto-edit' | 'auto' | 'yolo'; + id: ApprovalModeValue; name: string; description: string; }>; @@ -1419,11 +1414,7 @@ export class QwenAgentManager { /** * Register mode changed callback */ - onModeChanged( - callback: ( - modeId: 'plan' | 'default' | 'auto-edit' | 'auto' | 'yolo', - ) => void, - ): void { + onModeChanged(callback: (modeId: ApprovalModeValue) => void): void { this.callbacks.onModeChanged = callback; this.sessionUpdateHandler.updateCallbacks(this.callbacks); } diff --git a/packages/vscode-ide-companion/src/types/approvalModeTypes.ts b/packages/vscode-ide-companion/src/types/approvalModeTypes.ts index 5f8e52cd5a3..36a47c60128 100644 --- a/packages/vscode-ide-companion/src/types/approvalModeTypes.ts +++ b/packages/vscode-ide-companion/src/types/approvalModeTypes.ts @@ -4,29 +4,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * Enum for approval modes with UI-friendly labels - * Represents the different approval modes available in the ACP protocol - * with their corresponding user-facing display names - */ -export enum ApprovalMode { - PLAN = 'plan', - DEFAULT = 'default', - AUTO_EDIT = 'auto-edit', - AUTO = 'auto', - YOLO = 'yolo', -} +import { + DAEMON_APPROVAL_MODES, + type DaemonApprovalMode, +} from '@qwen-code/sdk/daemon'; + +type ApprovalModeByValue = { + [Mode in DaemonApprovalMode]: Mode; +}; + +const APPROVAL_MODE_BY_VALUE = Object.fromEntries( + DAEMON_APPROVAL_MODES.map((mode) => [mode, mode]), +) as ApprovalModeByValue; + +export const ApprovalMode = { + PLAN: APPROVAL_MODE_BY_VALUE.plan, + DEFAULT: APPROVAL_MODE_BY_VALUE.default, + AUTO_EDIT: APPROVAL_MODE_BY_VALUE['auto-edit'], + AUTO: APPROVAL_MODE_BY_VALUE.auto, + YOLO: APPROVAL_MODE_BY_VALUE.yolo, +} as const; + +export type ApprovalMode = DaemonApprovalMode; /** * Mapping from string values to enum values for runtime conversion */ -export const APPROVAL_MODE_MAP: Record = { - plan: ApprovalMode.PLAN, - default: ApprovalMode.DEFAULT, - 'auto-edit': ApprovalMode.AUTO_EDIT, - auto: ApprovalMode.AUTO, - yolo: ApprovalMode.YOLO, -}; +export const APPROVAL_MODE_MAP: Record = + APPROVAL_MODE_BY_VALUE; /** * UI display information for each approval mode diff --git a/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts b/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts index 11a77b21405..2d604957949 100644 --- a/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts +++ b/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts @@ -4,13 +4,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * Type for approval mode values - * Used in ACP protocol for controlling agent behavior - */ -export type ApprovalModeValue = - | 'plan' - | 'default' - | 'auto-edit' - | 'auto' - | 'yolo'; +export type { DaemonApprovalMode as ApprovalModeValue } from '@qwen-code/sdk/daemon'; diff --git a/packages/vscode-ide-companion/src/utils/acpModelInfo.ts b/packages/vscode-ide-companion/src/utils/acpModelInfo.ts index 120873f7052..7a9bf09296b 100644 --- a/packages/vscode-ide-companion/src/utils/acpModelInfo.ts +++ b/packages/vscode-ide-companion/src/utils/acpModelInfo.ts @@ -6,6 +6,7 @@ import type { ModelInfo } from '@agentclientprotocol/sdk'; import { knownTokenLimit } from '@qwen-code/qwen-code-core'; +import { DAEMON_APPROVAL_MODES } from '@qwen-code/sdk/daemon'; import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; type AcpMeta = Record; @@ -114,17 +115,9 @@ export interface SessionModeState { }>; } -const APPROVAL_MODE_VALUES: ApprovalModeValue[] = [ - 'plan', - 'default', - 'auto-edit', - 'auto', - 'yolo', -]; - const isApprovalModeValue = (value: unknown): value is ApprovalModeValue => typeof value === 'string' && - APPROVAL_MODE_VALUES.includes(value as ApprovalModeValue); + DAEMON_APPROVAL_MODES.includes(value as ApprovalModeValue); /** * Extract complete model state from ACP `session/new` result. diff --git a/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts index bbf45eb8624..7453e9043c3 100644 --- a/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts @@ -354,12 +354,7 @@ export class WebViewProvider { // Surface available modes and current mode (from ACP initialize) this.agentManager.onModeInfo((info) => { try { - const current = (info?.currentModeId || null) as - | 'plan' - | 'default' - | 'auto-edit' - | 'yolo' - | null; + const current = info?.currentModeId ?? null; this.currentModeId = current; } catch (_error) { // Ignore error when parsing mode info