Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/sdk-java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**'
Expand All @@ -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/**'
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/sdk-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ on:
- 'release/**'
paths:
- 'packages/sdk-python/**'
- 'packages/core/src/config/approval-modes.json'
- '.github/workflows/sdk-python.yml'
push:
branches:
- 'main'
- 'release/**'
paths:
- 'packages/sdk-python/**'
- 'packages/core/src/config/approval-modes.json'
- '.github/workflows/sdk-python.yml'

jobs:
Expand Down
36 changes: 36 additions & 0 deletions docs/design/2026-08-23-approval-mode-contract.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 10 additions & 3 deletions packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import('@qwen-code/qwen-code-core')>()
).APPROVAL_MODE_INFO,
APPROVAL_MODES: (
await importOriginal<typeof import('@qwen-code/qwen-code-core')>()
).APPROVAL_MODES,
applyReasoningEffort: (
config: {
setReasoningEffort(effort: string | undefined): void;
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
8 changes: 2 additions & 6 deletions packages/cli/src/acp-integration/session/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import type {
ApprovalModeValue,
Config,
ToolArtifact,
ToolResultBoundaryArtifact,
Expand All @@ -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.
Expand Down
9 changes: 2 additions & 7 deletions packages/cli/src/commands/channel/config-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(APPROVAL_MODES);

export { findCliEntryPath } from './cli-entry-path.js';

Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/commands/channel/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import('@qwen-code/qwen-code-core')>()
).APPROVAL_MODES,
Storage: { getGlobalQwenDir: () => '/tmp/qwen' },
hashDaemonWorkspace: (workspace: string) =>
workspace === '/workspace' ? 'workspace-hash' : 'other-hash',
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/commands/review/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.',
Expand Down
36 changes: 11 additions & 25 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import {
ApprovalMode,
APPROVAL_MODES,
AuthType,
Config,
DEFAULT_QWEN_EMBEDDING_MODEL,
Expand Down Expand Up @@ -100,14 +101,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',
Expand All @@ -121,30 +114,23 @@ 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(
', ',
)}`,
);
}

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);
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
if (approvalMode === undefined) {
throw formatApprovalModeError(value);
}
return approvalMode;
}

export interface CliArgs {
Expand Down Expand Up @@ -715,7 +701,7 @@ export async function parseArguments(): Promise<CliArgs> {
})
.option('approval-mode', {
type: 'string',
choices: ['plan', 'default', 'auto-edit', 'auto', 'yolo'],
choices: APPROVAL_MODES,
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)',
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
WorkflowApproval,
} from '@qwen-code/qwen-code-core';
import {
APPROVAL_MODES,
InputFormat,
ToolConfirmationOutcome,
ToolNames,
Expand Down Expand Up @@ -146,7 +147,7 @@ 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)
// Map permission modes to approval logic.
switch (mode) {
case 'yolo': // Allow all tools
case 'auto-edit': // Auto-approve edit operations
Expand Down Expand Up @@ -217,13 +218,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(
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/nonInteractive/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type {
ActiveGoal,
ApprovalModeValue,
GoalSnapshotV2,
SubagentConfig,
McpToolProgressData,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/agents/background-agent-resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/approval-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export enum ApprovalMode {
YOLO = 'yolo',
}

export type ApprovalModeValue = `${ApprovalMode}`;

export const APPROVAL_MODES = Object.values(ApprovalMode);
1 change: 1 addition & 0 deletions packages/core/src/config/approval-modes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["plan", "default", "auto-edit", "auto", "yolo"]
6 changes: 5 additions & 1 deletion packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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`
*/
Expand Down
Loading
Loading