From 5c4733c74d15e53f2e41030dad31f687b09c4a20 Mon Sep 17 00:00:00 2001 From: raymondginger Date: Thu, 20 Aug 2026 20:55:41 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(security):=20DANGEROUS=5FSKIP=20?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E5=91=BD=E5=90=8D=E5=8D=B1=E9=99=A9=E9=80=83?= =?UTF-8?q?=E7=94=9F=E9=80=9A=E9=81=93=20(=E5=80=9F=E9=89=B4=20Claude=20Co?= =?UTF-8?q?de)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 跳过全部权限校验的逃生通道叫 dangerous_skip 而不是沉默的 full_access, 让使用者与审计日志一眼看到这是危险选择。 与 FULL_ACCESS 同强度, 序列化 roundtrip 保留 dangerous_skip。 --- core/domain/execution_security.py | 18 ++++++++++++++- tests/domain/test_execution_security.py | 30 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/core/domain/execution_security.py b/core/domain/execution_security.py index c6f23a6b..10f8b4c3 100644 --- a/core/domain/execution_security.py +++ b/core/domain/execution_security.py @@ -11,11 +11,18 @@ class ExecutionAccessPreset(StrEnum): - """User-facing access choices shared by every DeepCode client.""" + """User-facing access choices shared by every DeepCode client. + + 借鉴 Claude Code ``--allow-dangerously-skip-permissions`` (2026-08-19): + 危险操作必须"显式命名危险" —— 跳过全部权限校验的逃生通道叫 + ``dangerous_skip`` 而不是沉默的 full_access, 让使用者与审计日志 + 都能一眼看到这是危险选择。 + """ ASK = "ask" READ_ONLY = "read_only" FULL_ACCESS = "full_access" + DANGEROUS_SKIP = "dangerous_skip" class FilesystemScope(StrEnum): @@ -178,6 +185,15 @@ def _pattern_specificity(pattern: str) -> int: FilesystemScope.UNRESTRICTED, ApprovalPolicy.NEVER, ), + # 危险逃生通道: 显式命名 (借鉴 Claude Code --allow-dangerously-skip-permissions)。 + # 与 FULL_ACCESS 同强度, 但名字自带"危险"警示, 供日志/UI 明确区分; + # 选择它等于明确声明"我知道这很危险, 仍要跳过全部权限校验"。 + ExecutionAccessPreset.DANGEROUS_SKIP: ( + ExecutionPermissionMode.FULL_AUTO, + False, + FilesystemScope.UNRESTRICTED, + ApprovalPolicy.NEVER, + ), } diff --git a/tests/domain/test_execution_security.py b/tests/domain/test_execution_security.py index d98854ad..fac9e092 100644 --- a/tests/domain/test_execution_security.py +++ b/tests/domain/test_execution_security.py @@ -39,6 +39,15 @@ FilesystemScope.UNRESTRICTED, ApprovalPolicy.NEVER, ), + # 危险逃生通道 (借鉴 Claude Code --allow-dangerously-skip-permissions): + # 与 FULL_ACCESS 同强度, 但名字显式带"危险"警示, 供日志/UI 区分。 + ( + ExecutionAccessPreset.DANGEROUS_SKIP, + ExecutionPermissionMode.FULL_AUTO, + False, + FilesystemScope.UNRESTRICTED, + ApprovalPolicy.NEVER, + ), ], ) def test_access_presets_resolve_to_canonical_security_facts( @@ -70,6 +79,27 @@ def test_legacy_profile_preserves_facts_without_claiming_full_access() -> None: assert ExecutionSecurityProfile.from_dict(profile.to_dict()) == profile +def test_dangerous_skip_is_named_escape_hatch_not_masked_full_access() -> None: + dangerous = ExecutionSecurityProfile.for_preset( + ExecutionAccessPreset.DANGEROUS_SKIP + ) + full = ExecutionSecurityProfile.for_preset(ExecutionAccessPreset.FULL_ACCESS) + + # 同强度: 解析到与 FULL_ACCESS 相同的 security facts + assert dangerous.permission_mode is full.permission_mode + assert dangerous.command_sandbox is full.command_sandbox + assert dangerous.filesystem_scope is full.filesystem_scope + assert dangerous.approval_policy is full.approval_policy + + # 但名字显式带"危险": 序列化 roundtrip 保留 dangerous_skip, 不冒充 full_access + assert dangerous.access_preset is ExecutionAccessPreset.DANGEROUS_SKIP + assert dangerous.to_dict()["accessPreset"] == "dangerous_skip" + assert ( + ExecutionSecurityProfile.from_dict(dangerous.to_dict()).access_preset + is ExecutionAccessPreset.DANGEROUS_SKIP + ) + + def test_session_access_override_parser_is_canonical_and_fail_closed() -> None: assert ( parse_access_preset_override( From 953b7f99c9c23f860b75ac6b4ec369dc8608a667 Mon Sep 17 00:00:00 2001 From: raymondginger Date: Sun, 23 Aug 2026 07:57:04 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(security):=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E4=B8=8E=20v17=20migration=20=E6=94=AF=E6=8C=81=20dangerous=5F?= =?UTF-8?q?skip=20access=20preset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config: access_preset 增加 dangerous_skip 字面量 - migrations: v17 加宽 threads.access_preset_override CHECK(含回滚, 降级时清除 dangerous_skip 值) - protocol schema + 生成 TS 类型同步 - 测试: v17 存取/downgrade roundtrip + v16 fresh 断言适配 --- core/config.py | 2 +- core/persistence/migrations.py | 44 ++ desktop/src/generated/app-server.ts | 2 +- protocol/app-server.schema.json | 664 ++++++++++++++---- tests/contract/test_protocol_schema.py | 3 +- ...st_session_execution_security_migration.py | 65 ++ .../persistence/test_web_access_migration.py | 4 +- 7 files changed, 650 insertions(+), 134 deletions(-) diff --git a/core/config.py b/core/config.py index 02345339..ead66092 100644 --- a/core/config.py +++ b/core/config.py @@ -272,7 +272,7 @@ class SecurityConfig(_Base): sandbox atomically; the environment gate remains for legacy callers. """ - access_preset: Literal["ask", "read_only", "full_access"] | None = None + access_preset: Literal["ask", "read_only", "full_access", "dangerous_skip"] | None = None permission_mode: str = "full_auto" permissions: dict[str, Any] = Field(default_factory=dict) sandbox: bool = True diff --git a/core/persistence/migrations.py b/core/persistence/migrations.py index 4e48d0fd..a8f663d6 100644 --- a/core/persistence/migrations.py +++ b/core/persistence/migrations.py @@ -1310,6 +1310,44 @@ class Migration: _REMOVE_WEB_ACCESS_POLICY_V16 = _DROP_WEB_ACCESS_POLICY_V15 _RESTORE_WEB_ACCESS_POLICY_V16 = _WEB_ACCESS_POLICY_V15 +# v17: Widen the access_preset_override CHECK to accept 'dangerous_skip' +# (借鉴 Claude Code --allow-dangerously-skip-permissions; 同强度但显式命名危险). +# SQLite cannot ALTER CHECK in-place, so backup → drop column → re-add with +# the widened constraint → restore values. +_WIDEN_DANGEROUS_SKIP_V17 = r""" +CREATE TABLE threads_preset_backup AS + SELECT id, access_preset_override FROM threads + WHERE access_preset_override IS NOT NULL; +ALTER TABLE threads DROP COLUMN access_preset_override; +ALTER TABLE threads ADD COLUMN access_preset_override TEXT CHECK ( + access_preset_override IS NULL OR + access_preset_override IN ('ask', 'read_only', 'full_access', 'dangerous_skip') +); +UPDATE threads +SET access_preset_override = ( + SELECT access_preset_override FROM threads_preset_backup + WHERE threads_preset_backup.id = threads.id +); +DROP TABLE threads_preset_backup; +""" +_NARROW_DANGEROUS_SKIP_V17 = r""" +CREATE TABLE threads_preset_backup AS + SELECT id, access_preset_override FROM threads + WHERE access_preset_override IS NOT NULL + AND access_preset_override <> 'dangerous_skip'; +ALTER TABLE threads DROP COLUMN access_preset_override; +ALTER TABLE threads ADD COLUMN access_preset_override TEXT CHECK ( + access_preset_override IS NULL OR + access_preset_override IN ('ask', 'read_only', 'full_access') +); +UPDATE threads +SET access_preset_override = ( + SELECT access_preset_override FROM threads_preset_backup + WHERE threads_preset_backup.id = threads.id +); +DROP TABLE threads_preset_backup; +""" + MIGRATIONS = ( Migration(1, "initial_domain", _INITIAL_SCHEMA, _DROP_INITIAL_SCHEMA), Migration( @@ -1402,6 +1440,12 @@ class Migration: _REMOVE_WEB_ACCESS_POLICY_V16, _RESTORE_WEB_ACCESS_POLICY_V16, ), + Migration( + 17, + "widen_dangerous_skip_preset", + _WIDEN_DANGEROUS_SKIP_V17, + _NARROW_DANGEROUS_SKIP_V17, + ), ) LATEST_SCHEMA_VERSION = MIGRATIONS[-1].version diff --git a/desktop/src/generated/app-server.ts b/desktop/src/generated/app-server.ts index 883dda20..37320725 100644 --- a/desktop/src/generated/app-server.ts +++ b/desktop/src/generated/app-server.ts @@ -38,7 +38,7 @@ export type ThreadMode = "code" | "paper" | "brief" | "review" | "goal"; /** * User-facing tool access preset shared by Desktop and CLI. */ -export type ExecutionAccessPreset = "ask" | "read_only" | "full_access"; +export type ExecutionAccessPreset = "ask" | "read_only" | "full_access" | "dangerous_skip"; export type ApprovalDecision = "approved_once" | "approved_session" | "denied"; export type ExecutionPermissionMode = "default" | "plan" | "full_auto"; export type AutomationStatus = "enabled" | "paused" | "retired"; diff --git a/protocol/app-server.schema.json b/protocol/app-server.schema.json index 36d952d2..be5b046c 100644 --- a/protocol/app-server.schema.json +++ b/protocol/app-server.schema.json @@ -108,7 +108,8 @@ "enum": [ "ask", "read_only", - "full_access" + "full_access", + "dangerous_skip" ] }, "ExecutionPermissionRule": { @@ -2200,41 +2201,82 @@ "type": "object", "additionalProperties": false, "properties": { - "projectId": {"type": "string", "pattern": "^proj_"}, - "name": {"type": "string", "minLength": 1} + "projectId": { + "type": "string", + "pattern": "^proj_" + }, + "name": { + "type": "string", + "minLength": 1 + } }, - "required": ["name"] + "required": [ + "name" + ] }, "McpPresetAddParams": { "type": "object", "additionalProperties": false, "properties": { - "projectId": {"type": "string", "pattern": "^proj_"}, - "presetId": {"type": "string", "minLength": 1}, - "enabled": {"type": "boolean"} + "projectId": { + "type": "string", + "pattern": "^proj_" + }, + "presetId": { + "type": "string", + "minLength": 1 + }, + "enabled": { + "type": "boolean" + } }, - "required": ["presetId"] + "required": [ + "presetId" + ] }, "McpSetEnabledParams": { "type": "object", "additionalProperties": false, "properties": { - "projectId": {"type": "string", "pattern": "^proj_"}, - "name": {"type": "string", "minLength": 1}, - "enabled": {"type": "boolean"} + "projectId": { + "type": "string", + "pattern": "^proj_" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "enabled": { + "type": "boolean" + } }, - "required": ["name", "enabled"] + "required": [ + "name", + "enabled" + ] }, "McpOAuthStartParams": { "type": "object", "additionalProperties": false, "properties": { - "projectId": {"type": "string", "pattern": "^proj_"}, - "name": {"type": "string", "minLength": 1}, - "openBrowser": {"type": "boolean"}, - "resetCredentials": {"type": "boolean"} + "projectId": { + "type": "string", + "pattern": "^proj_" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "openBrowser": { + "type": "boolean" + }, + "resetCredentials": { + "type": "boolean" + } }, - "required": ["name"] + "required": [ + "name" + ] }, "SkillInfo": { "type": "object", @@ -2281,14 +2323,23 @@ }, "originKind": { "type": "string", - "enum": ["local", "bundled", "provider"] + "enum": [ + "local", + "bundled", + "provider" + ] }, "originLabel": { "type": "string" }, "providerKind": { "type": "string", - "enum": ["local", "executor", "orchestrator", "custom"] + "enum": [ + "local", + "executor", + "orchestrator", + "custom" + ] }, "providerId": { "type": "string" @@ -2332,22 +2383,40 @@ ] }, "displayName": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "shortDescription": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "iconSmall": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "iconLarge": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "brandColor": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "defaultPrompt": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "allowImplicitInvocation": { "type": "boolean" @@ -2439,14 +2508,23 @@ }, "originKind": { "type": "string", - "enum": ["local", "bundled", "provider"] + "enum": [ + "local", + "bundled", + "provider" + ] }, "originLabel": { "type": "string" }, "providerKind": { "type": "string", - "enum": ["local", "executor", "orchestrator", "custom"] + "enum": [ + "local", + "executor", + "orchestrator", + "custom" + ] }, "providerId": { "type": "string" @@ -2490,22 +2568,40 @@ ] }, "displayName": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "shortDescription": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "iconSmall": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "iconLarge": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "brandColor": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "defaultPrompt": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "allowImplicitInvocation": { "type": "boolean" @@ -2581,7 +2677,10 @@ "pattern": "^sha256:" }, "authoringSkillId": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "pattern": "^sk_[0-9a-f]{24}$" } }, @@ -2601,7 +2700,9 @@ "pattern": "^plg_[0-9a-f]{24}$" } }, - "required": ["pluginId"] + "required": [ + "pluginId" + ] }, "PluginAddParams": { "type": "object", @@ -2612,7 +2713,9 @@ "minLength": 1 } }, - "required": ["path"] + "required": [ + "path" + ] }, "PluginSetEnabledParams": { "type": "object", @@ -2626,25 +2729,54 @@ "type": "boolean" } }, - "required": ["pluginId", "enabled"] + "required": [ + "pluginId", + "enabled" + ] }, "PluginDiagnostic": { "type": "object", "additionalProperties": false, "properties": { - "code": {"type": "string", "minLength": 1}, + "code": { + "type": "string", + "minLength": 1 + }, "severity": { "type": "string", - "enum": ["warning", "error"] + "enum": [ + "warning", + "error" + ] + }, + "message": { + "type": "string" }, - "message": {"type": "string"}, "component": { - "type": ["string", "null"], - "enum": ["skills", "mcp", null] + "type": [ + "string", + "null" + ], + "enum": [ + "skills", + "mcp", + null + ] }, - "resource": {"type": ["string", "null"]} + "resource": { + "type": [ + "string", + "null" + ] + } }, - "required": ["code", "severity", "message", "component", "resource"] + "required": [ + "code", + "severity", + "message", + "component", + "resource" + ] }, "PluginComponent": { "type": "object", @@ -2652,57 +2784,121 @@ "properties": { "kind": { "type": "string", - "enum": ["skills", "mcp"] + "enum": [ + "skills", + "mcp" + ] }, "status": { "type": "string", - "enum": ["ready", "unsupported", "invalid"] + "enum": [ + "ready", + "unsupported", + "invalid" + ] + }, + "resource": { + "type": [ + "string", + "null" + ] + }, + "itemCount": { + "type": [ + "integer", + "null" + ], + "minimum": 0 }, - "resource": {"type": ["string", "null"]}, - "itemCount": {"type": ["integer", "null"], "minimum": 0}, "diagnostics": { "type": "array", - "items": {"$ref": "#/$defs/PluginDiagnostic"} + "items": { + "$ref": "#/$defs/PluginDiagnostic" + } } }, - "required": ["kind", "status", "resource", "itemCount", "diagnostics"] + "required": [ + "kind", + "status", + "resource", + "itemCount", + "diagnostics" + ] }, "PluginInfo": { "type": "object", "additionalProperties": false, "properties": { - "id": {"type": "string"}, - "name": {"type": "string"}, - "version": {"type": ["string", "null"]}, - "description": {"type": "string"}, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, "status": { "type": "string", - "enum": ["active", "disabled", "invalid"] + "enum": [ + "active", + "disabled", + "invalid" + ] + }, + "enabled": { + "type": "boolean" + }, + "source": { + "const": "linked-directory" + }, + "path": { + "type": "string" }, - "enabled": {"type": "boolean"}, - "source": {"const": "linked-directory"}, - "path": {"type": "string"}, "schema": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "enum": [ "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", null ] }, - "manifestPath": {"type": "string"}, + "manifestPath": { + "type": "string" + }, "manifestRevision": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "pattern": "^sha256:" }, "components": { "type": "array", - "items": {"$ref": "#/$defs/PluginComponent"} + "items": { + "$ref": "#/$defs/PluginComponent" + } }, "diagnostics": { "type": "array", - "items": {"$ref": "#/$defs/PluginDiagnostic"} + "items": { + "$ref": "#/$defs/PluginDiagnostic" + } }, - "error": {"type": ["string", "null"]} + "error": { + "type": [ + "string", + "null" + ] + } }, "required": [ "id", @@ -2727,18 +2923,26 @@ "properties": { "plugins": { "type": "array", - "items": {"$ref": "#/$defs/PluginInfo"} + "items": { + "$ref": "#/$defs/PluginInfo" + } }, "diagnostics": { "type": "array", - "items": {"$ref": "#/$defs/PluginDiagnostic"} + "items": { + "$ref": "#/$defs/PluginDiagnostic" + } }, "revision": { "type": "string", "pattern": "^sha256:" } }, - "required": ["plugins", "diagnostics", "revision"] + "required": [ + "plugins", + "diagnostics", + "revision" + ] }, "HookInfo": { "type": "object", @@ -2847,8 +3051,14 @@ ] }, "auth": { - "type": ["string", "null"], - "enum": ["oauth", null] + "type": [ + "string", + "null" + ], + "enum": [ + "oauth", + null + ] }, "enabled": { "type": "boolean" @@ -2908,11 +3118,15 @@ }, "requiredEnvKeys": { "type": "array", - "items": {"type": "string"} + "items": { + "type": "string" + } }, "missingEnvKeys": { "type": "array", - "items": {"type": "string"} + "items": { + "type": "string" + } }, "credentialEnvKeys": { "type": "array", @@ -2949,16 +3163,38 @@ }, "authState": { "type": "string", - "enum": ["not_required", "login_required", "authorizing", "authenticated"] + "enum": [ + "not_required", + "login_required", + "authorizing", + "authenticated" + ] }, "runtimeState": { "type": "string", - "enum": ["stopped", "connecting", "tested", "connected", "failed"] + "enum": [ + "stopped", + "connecting", + "tested", + "connected", + "failed" + ] + }, + "runtimeMessage": { + "type": "string" + }, + "toolCount": { + "type": "integer", + "minimum": 0 + }, + "resourceCount": { + "type": "integer", + "minimum": 0 }, - "runtimeMessage": {"type": "string"}, - "toolCount": {"type": "integer", "minimum": 0}, - "resourceCount": {"type": "integer", "minimum": 0}, - "promptCount": {"type": "integer", "minimum": 0} + "promptCount": { + "type": "integer", + "minimum": 0 + } }, "required": [ "id", @@ -3026,75 +3262,205 @@ "type": "object", "additionalProperties": false, "properties": { - "id": {"type": "string"}, - "displayName": {"type": "string"}, - "category": {"type": "string"}, - "description": {"type": "string"}, - "docsUrl": {"type": "string"}, - "transport": {"type": "string", "enum": ["stdio", "sse", "streamableHttp"]}, - "auth": {"type": ["string", "null"], "enum": ["oauth", null]}, - "requires": {"type": "string"}, - "note": {"type": "string"}, - "requiredEnvironment": {"type": "array", "items": {"type": "string"}}, - "missingEnvironment": {"type": "array", "items": {"type": "string"}}, - "configured": {"type": "boolean"} + "id": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "docsUrl": { + "type": "string" + }, + "transport": { + "type": "string", + "enum": [ + "stdio", + "sse", + "streamableHttp" + ] + }, + "auth": { + "type": [ + "string", + "null" + ], + "enum": [ + "oauth", + null + ] + }, + "requires": { + "type": "string" + }, + "note": { + "type": "string" + }, + "requiredEnvironment": { + "type": "array", + "items": { + "type": "string" + } + }, + "missingEnvironment": { + "type": "array", + "items": { + "type": "string" + } + }, + "configured": { + "type": "boolean" + } }, "required": [ - "id", "displayName", "category", "description", "docsUrl", - "transport", "auth", "requires", "note", "requiredEnvironment", - "missingEnvironment", "configured" + "id", + "displayName", + "category", + "description", + "docsUrl", + "transport", + "auth", + "requires", + "note", + "requiredEnvironment", + "missingEnvironment", + "configured" ] }, "McpPresetInventory": { "type": "object", "additionalProperties": false, "properties": { - "presets": {"type": "array", "items": {"$ref": "#/$defs/McpPresetInfo"}}, - "source": {"type": "string"}, - "sourceRevision": {"type": "string"} + "presets": { + "type": "array", + "items": { + "$ref": "#/$defs/McpPresetInfo" + } + }, + "source": { + "type": "string" + }, + "sourceRevision": { + "type": "string" + } }, - "required": ["presets", "source", "sourceRevision"] + "required": [ + "presets", + "source", + "sourceRevision" + ] }, "McpProbeResult": { "type": "object", "additionalProperties": false, "properties": { - "serverId": {"type": "string"}, - "name": {"type": "string"}, - "ok": {"type": "boolean"}, - "transport": {"type": "string", "enum": ["stdio", "sse", "streamableHttp"]}, - "toolCount": {"type": "integer", "minimum": 0}, - "resourceCount": {"type": "integer", "minimum": 0}, - "promptCount": {"type": "integer", "minimum": 0}, - "elapsedSeconds": {"type": "number", "minimum": 0}, - "error": {"type": ["string", "null"]} + "serverId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ok": { + "type": "boolean" + }, + "transport": { + "type": "string", + "enum": [ + "stdio", + "sse", + "streamableHttp" + ] + }, + "toolCount": { + "type": "integer", + "minimum": 0 + }, + "resourceCount": { + "type": "integer", + "minimum": 0 + }, + "promptCount": { + "type": "integer", + "minimum": 0 + }, + "elapsedSeconds": { + "type": "number", + "minimum": 0 + }, + "error": { + "type": [ + "string", + "null" + ] + } }, "required": [ - "serverId", "name", "ok", "transport", "toolCount", "resourceCount", - "promptCount", "elapsedSeconds", "error" + "serverId", + "name", + "ok", + "transport", + "toolCount", + "resourceCount", + "promptCount", + "elapsedSeconds", + "error" ] }, "McpOAuthFlow": { "type": "object", "additionalProperties": false, "properties": { - "flowId": {"type": "string"}, - "serverId": {"type": "string"}, - "name": {"type": "string"}, + "flowId": { + "type": "string" + }, + "serverId": { + "type": "string" + }, + "name": { + "type": "string" + }, "status": { "type": "string", "enum": [ - "starting", "authorization_required", "connecting", "authenticated", - "failed", "cancelled", "logged_out" + "starting", + "authorization_required", + "connecting", + "authenticated", + "failed", + "cancelled", + "logged_out" ] }, - "authorizationUrl": {"type": ["string", "null"]}, - "expiresInSeconds": {"type": "integer", "minimum": 0}, - "error": {"type": ["string", "null"]} + "authorizationUrl": { + "type": [ + "string", + "null" + ] + }, + "expiresInSeconds": { + "type": "integer", + "minimum": 0 + }, + "error": { + "type": [ + "string", + "null" + ] + } }, "required": [ - "flowId", "serverId", "name", "status", "authorizationUrl", - "expiresInSeconds", "error" + "flowId", + "serverId", + "name", + "status", + "authorizationUrl", + "expiresInSeconds", + "error" ] }, "DiagnosticsCheck": { @@ -5155,7 +5521,11 @@ }, "trust": { "type": "string", - "enum": ["system", "user", "project"] + "enum": [ + "system", + "user", + "project" + ] }, "name": { "type": "string" @@ -5164,13 +5534,19 @@ "type": "string" }, "tools": { - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "items": { "type": "string" } }, "broken": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } }, "required": [ @@ -5228,7 +5604,10 @@ "additionalProperties": false, "properties": { "agentPreset": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } }, "required": [ @@ -5244,7 +5623,10 @@ "minLength": 1 }, "agentPreset": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } }, "required": [ @@ -5257,7 +5639,10 @@ "additionalProperties": false, "properties": { "agentPreset": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] } }, "required": [ @@ -5835,10 +6220,17 @@ "type": "object", "additionalProperties": false, "properties": { - "removed": {"type": "boolean"}, - "plugin": {"$ref": "#/$defs/PluginInfo"} + "removed": { + "type": "boolean" + }, + "plugin": { + "$ref": "#/$defs/PluginInfo" + } }, - "required": ["removed", "plugin"] + "required": [ + "removed", + "plugin" + ] }, "hooks/list": { "type": "object", @@ -5893,14 +6285,26 @@ "mcp/oauth/cancel": { "type": "object", "additionalProperties": false, - "properties": {"cancelled": {"type": "boolean"}}, - "required": ["cancelled"] + "properties": { + "cancelled": { + "type": "boolean" + } + }, + "required": [ + "cancelled" + ] }, "mcp/oauth/logout": { "type": "object", "additionalProperties": false, - "properties": {"removed": {"type": "boolean"}}, - "required": ["removed"] + "properties": { + "removed": { + "type": "boolean" + } + }, + "required": [ + "removed" + ] }, "diagnostics/read": { "type": "object", diff --git a/tests/contract/test_protocol_schema.py b/tests/contract/test_protocol_schema.py index a23fc784..02076b1a 100644 --- a/tests/contract/test_protocol_schema.py +++ b/tests/contract/test_protocol_schema.py @@ -179,6 +179,7 @@ def test_protocol_exposes_session_access_and_immutable_security_profile() -> Non "ask", "read_only", "full_access", + "dangerous_skip", ] assert definitions["Thread"]["properties"]["accessPresetOverride"] == { "description": ( @@ -251,7 +252,7 @@ def test_protocol_exposes_session_access_and_immutable_security_profile() -> Non generated = GENERATED_TYPES.read_text(encoding="utf-8") assert ( 'export type ExecutionAccessPreset = "ask" | "read_only" | ' - '"full_access";' in generated + '"full_access" | "dangerous_skip";' in generated ) assert "accessPresetOverride: ExecutionAccessPreset | null;" in generated assert "executionSecurityProfile?: ExecutionSecurityProfile | null;" in generated diff --git a/tests/persistence/test_session_execution_security_migration.py b/tests/persistence/test_session_execution_security_migration.py index cbd307ac..36d70c77 100644 --- a/tests/persistence/test_session_execution_security_migration.py +++ b/tests/persistence/test_session_execution_security_migration.py @@ -196,3 +196,68 @@ def test_v14_session_execution_security_migration_is_reversible( ).execution_security_profile == _fail_closed_legacy_profile( ExecutionPermissionMode.FULL_AUTO, ) + + +def test_v17_widens_preset_override_check_to_dangerous_skip( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + database = Database(tmp_path / "state.sqlite3") + database.initialize(target_version=16) + thread, _ = _seed_v13_session(database, workspace) + + with database.read() as connection: + # v16 CHECK rejects the new value. + with pytest.raises(sqlite3.IntegrityError): + connection.execute( + "UPDATE threads SET access_preset_override = 'dangerous_skip' " + "WHERE id = ?", + (thread.id,), + ) + + migrate(connection, 17) + assert current_version(connection) == 17 + connection.execute( + "UPDATE threads SET access_preset_override = 'dangerous_skip' " + "WHERE id = ?", + (thread.id,), + ) + assert ThreadRepository(connection).get( + thread.id + ).access_preset_override == ExecutionAccessPreset.DANGEROUS_SKIP + # Unknown values are still rejected after the widen. + with pytest.raises(sqlite3.IntegrityError): + connection.execute( + "UPDATE threads SET access_preset_override = 'unknown' WHERE id = ?", + (thread.id,), + ) + + +def test_v17_downgrade_clears_dangerous_skip_values(tmp_path: Path) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + database = Database(tmp_path / "state.sqlite3") + database.initialize(target_version=16) + thread, _ = _seed_v13_session(database, workspace) + + with database.read() as connection: + migrate(connection, 17) + connection.execute( + "UPDATE threads SET access_preset_override = 'dangerous_skip' " + "WHERE id = ?", + (thread.id,), + ) + + migrate(connection, 16) + assert current_version(connection) == 16 + # The downgrade backup keeps only non-dangerous values, so the + # dangerous override is cleared rather than left violating v16 CHECK. + assert ( + ThreadRepository(connection).get(thread.id).access_preset_override is None + ) + + migrate(connection, 17) + assert ( + ThreadRepository(connection).get(thread.id).access_preset_override is None + ) diff --git a/tests/persistence/test_web_access_migration.py b/tests/persistence/test_web_access_migration.py index 8a69fef7..c1126eb4 100644 --- a/tests/persistence/test_web_access_migration.py +++ b/tests/persistence/test_web_access_migration.py @@ -63,7 +63,9 @@ def test_fresh_v16_schema_has_no_web_search_policy_columns(tmp_path: Path) -> No database.initialize() with database.read() as connection: - assert current_version(connection) == 16 + # v16 dropped the web-search policy columns; later migrations (v17+) + # must keep them gone on a fresh schema. + assert current_version(connection) >= 16 assert "web_search_mode_override" not in _column_names(connection, "threads") assert "web_access_policy_json" not in _column_names(connection, "turns") From 8371c79a63e085b6717ec2176fc243109968cd53 Mon Sep 17 00:00:00 2001 From: raymondginger Date: Sun, 30 Aug 2026 16:56:49 +0800 Subject: [PATCH 3/4] style: ruff format test_session_execution_security_migration.py --- .../test_session_execution_security_migration.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/persistence/test_session_execution_security_migration.py b/tests/persistence/test_session_execution_security_migration.py index 36d70c77..36372b09 100644 --- a/tests/persistence/test_session_execution_security_migration.py +++ b/tests/persistence/test_session_execution_security_migration.py @@ -219,13 +219,13 @@ def test_v17_widens_preset_override_check_to_dangerous_skip( migrate(connection, 17) assert current_version(connection) == 17 connection.execute( - "UPDATE threads SET access_preset_override = 'dangerous_skip' " - "WHERE id = ?", + "UPDATE threads SET access_preset_override = 'dangerous_skip' WHERE id = ?", (thread.id,), ) - assert ThreadRepository(connection).get( - thread.id - ).access_preset_override == ExecutionAccessPreset.DANGEROUS_SKIP + assert ( + ThreadRepository(connection).get(thread.id).access_preset_override + == ExecutionAccessPreset.DANGEROUS_SKIP + ) # Unknown values are still rejected after the widen. with pytest.raises(sqlite3.IntegrityError): connection.execute( @@ -244,8 +244,7 @@ def test_v17_downgrade_clears_dangerous_skip_values(tmp_path: Path) -> None: with database.read() as connection: migrate(connection, 17) connection.execute( - "UPDATE threads SET access_preset_override = 'dangerous_skip' " - "WHERE id = ?", + "UPDATE threads SET access_preset_override = 'dangerous_skip' WHERE id = ?", (thread.id,), ) From dd822f8f28e5a214f0d7b4377a8bc9bf180f1ef5 Mon Sep 17 00:00:00 2001 From: raymondginger Date: Tue, 1 Sep 2026 15:01:09 +0800 Subject: [PATCH 4/4] fix: ruff format config.py access_preset line (exceeds 88 char) - Wraps the long type annotation added in 8371c79a - Fixes lint-and-format PR check failure (#199) --- core/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/config.py b/core/config.py index ead66092..a34e9444 100644 --- a/core/config.py +++ b/core/config.py @@ -272,7 +272,9 @@ class SecurityConfig(_Base): sandbox atomically; the environment gate remains for legacy callers. """ - access_preset: Literal["ask", "read_only", "full_access", "dangerous_skip"] | None = None + access_preset: ( + Literal["ask", "read_only", "full_access", "dangerous_skip"] | None + ) = None permission_mode: str = "full_auto" permissions: dict[str, Any] = Field(default_factory=dict) sandbox: bool = True