From eea8000d9a3e47b4f9b398f6378772e700887bbb Mon Sep 17 00:00:00 2001 From: raymondginger Date: Wed, 19 Aug 2026 22:09:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20Claude=20Code=20lessons=20?= =?UTF-8?q?=E2=80=94=20instruction=20file=20exclusion=20+=20explicit=20dan?= =?UTF-8?q?gerous=20preset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 借鉴 Claude Code 两处机制 (2026-08-19): 1. core/harness/memory.py — 指令文件排除模式 - DEEPCODE_INSTRUCTION_EXCLUDES env: 逗号分隔 glob (如 **/code/CLAUDE.md,**/vendor/**) - 命中的 AGENTS.md/DEEPCODE.md/CLAUDE.md 跳过注入, 避免 monorepo 子目录/ 第三方代码指令污染主提示词 - 自实现 glob→regex: ** 匹配任意层级(可选前缀 (?:.*/)?), * / ? 不跨路径 分隔符; vendorized ≠ vendor/ 前缀同名不误匹配 - 非法模式忽略不阻断加载 2. core/domain/execution_security.py — 显式危险预设 - ExecutionAccessPreset.DANGEROUS_SKIP="dangerous_skip", 对齐 Claude Code --allow-dangerously-skip-permissions; 与 FULL_ACCESS 同强度但名字自带 危险警示, 供日志/UI 明确区分 3. tests/test_memory.py — 4 个新用例 (glob 正反例/集成/非法模式) --- core/domain/execution_security.py | 18 ++++++++- core/harness/memory.py | 63 ++++++++++++++++++++++++++++++- tests/test_memory.py | 34 +++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) 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/core/harness/memory.py b/core/harness/memory.py index 64a4fd55..53a5d7e6 100644 --- a/core/harness/memory.py +++ b/core/harness/memory.py @@ -23,6 +23,8 @@ from __future__ import annotations +import os +import re from pathlib import Path from typing import Any @@ -41,6 +43,65 @@ _REMINDER_OPEN = "" _REMINDER_CLOSE = "" _REMINDER_CLOSE_ESCAPED = "</system-reminder>" +# 借鉴 Claude Code 的 CLAUDE.md 排除模式 (ignore 配置): 逗号分隔的 glob 模式 +# (如 "**/code/CLAUDE.md,**/vendor/**")。命中的指令文件跳过不注入 —— 避免 +# monorepo 子目录/第三方代码的指令污染主提示词。 +_INSTRUCTION_EXCLUDE_ENV = "DEEPCODE_INSTRUCTION_EXCLUDES" +_EXCLUDE_RE_CACHE: dict[str, Any] = {} # pattern -> compiled regex + + +def _glob_to_re(pattern: str): + """glob → regex: ** 匹配任意层级 (含零层), * / ? 不跨路径分隔符。""" + compiled = _EXCLUDE_RE_CACHE.get(pattern) + if compiled is not None: + return compiled + parts = [] + i, n = 0, len(pattern) + while i < n: + c = pattern[i] + if c == "*": + if i + 1 < n and pattern[i + 1] == "*": + if i + 2 < n and pattern[i + 2] in "/\\": + # **/ → (?:.*/)? : 任意层级前缀(可选)。不能用 .* —— 那会让 + # "**/vendor/**" 误匹配 "vendorized/..." 这类前缀同名的路径。 + parts.append(r"(?:.*/)?") + i += 3 + else: + # 尾部 ** → 任意剩余(含层级) + parts.append(".*") + i += 2 + else: + parts.append(r"[^/\\]*") + i += 1 + elif c == "?": + parts.append(r"[^/\\]") + i += 1 + else: + parts.append(re.escape(c)) + i += 1 + compiled = re.compile("^" + "".join(parts) + "$", re.IGNORECASE) + _EXCLUDE_RE_CACHE[pattern] = compiled + return compiled + + +def _instruction_excluded(candidate: Path) -> bool: + """Whether the candidate instruction file is excluded by pattern. + + 逗号分隔 glob, 如 ``**/code/CLAUDE.md,**/vendor/**``; 用正斜杠规范化 + 路径后匹配, 兼容 Windows 反斜杠路径。非法模式被忽略, 不阻断加载。 + """ + patterns = [p.strip() for p in + os.environ.get(_INSTRUCTION_EXCLUDE_ENV, "").split(",") if p.strip()] + if not patterns: + return False + cand = str(candidate).replace("\\", "/") + for pat in patterns: + try: + if _glob_to_re(pat.replace("\\", "/")).match(cand): + return True + except (re.error, ValueError): + continue # 非法 glob 模式忽略, 不阻断加载 + return False def memory_dir(workspace: str | Path) -> Path: @@ -135,7 +196,7 @@ def project_instructions(workspace: str | Path) -> str: for directory in search_dirs: for name in _PROJECT_FILES: candidate = directory / name - if candidate.is_file(): + if candidate.is_file() and not _instruction_excluded(candidate): try: body = candidate.read_text( encoding="utf-8", errors="replace" diff --git a/tests/test_memory.py b/tests/test_memory.py index c1562ee1..3ed0ff34 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -11,8 +11,10 @@ sys.path.insert(0, str(ROOT)) from core.harness.memory import ( # noqa: E402 + _INSTRUCTION_EXCLUDE_ENV, _MAX_INJECT_CHARS, MemoryTool, + _instruction_excluded, memory_dir, project_instructions, system_preamble, @@ -36,6 +38,38 @@ def test_project_instructions_prefers_agents_md(tmp_path): assert "spaces" not in out # AGENTS.md wins over CLAUDE.md +def test_instruction_excluded_matches_globs(monkeypatch): + monkeypatch.setenv( + _INSTRUCTION_EXCLUDE_ENV, "**/code/CLAUDE.md,**/vendor/**" + ) + # 匹配: 任意层级前缀 + 目录段精确匹配 + assert _instruction_excluded(Path("repo/code/CLAUDE.md")) + assert _instruction_excluded(Path("repo/vendor/x/AGENTS.md")) + assert _instruction_excluded(Path("repo/vendor/AGENTS.md")) + assert _instruction_excluded(Path("code/CLAUDE.md")) # 零层前缀 + # 反例: 前缀同名目录不误匹配 (vendorized ≠ vendor/) + assert not _instruction_excluded(Path("repo/CLAUDE.md")) + assert not _instruction_excluded(Path("repo/vendorized/AGENTS.md")) + assert not _instruction_excluded(Path("repo/vendorized/x/CLAUDE.md")) + + +def test_project_instructions_skips_excluded_file(tmp_path, monkeypatch): + repo = tmp_path / "repo" + (repo / ".git").mkdir(parents=True) + (repo / "code").mkdir() + (repo / "CLAUDE.md").write_text("root instructions") + (repo / "code" / "CLAUDE.md").write_text("subdir instructions") + monkeypatch.setenv(_INSTRUCTION_EXCLUDE_ENV, "**/code/CLAUDE.md") + out = project_instructions(repo / "code") + assert "root instructions" in out + assert "subdir instructions" not in out + + +def test_instruction_excluded_ignores_invalid_patterns(monkeypatch): + monkeypatch.setenv(_INSTRUCTION_EXCLUDE_ENV, "**/[code/CLAUDE.md") # 非法 glob + assert not _instruction_excluded(Path("code/CLAUDE.md")) + + def test_project_instructions_absent(tmp_path): assert project_instructions(tmp_path) == ""