langchain-agentx-python 1.3.0__py3-none-any.whl → 1.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. langchain_agentx/__init__.py +1 -1
  2. langchain_agentx/loop/graph/factory.py +20 -11
  3. langchain_agentx/memory/instruction/read_state_seeder.py +78 -0
  4. langchain_agentx/memory/instruction/runtime.py +35 -11
  5. langchain_agentx/plugin/loader.py +2 -1
  6. langchain_agentx/plugin/registries.py +30 -3
  7. langchain_agentx/session/agent_session.py +1 -1
  8. langchain_agentx/session/conversation_session.py +1 -1
  9. langchain_agentx/tool_runtime/__init__.py +6 -1
  10. langchain_agentx/tool_runtime/agent_home_bypass.py +231 -0
  11. langchain_agentx/tool_runtime/inner_permission_chain.py +8 -1
  12. langchain_agentx/tool_runtime/loop_loader.py +32 -5
  13. langchain_agentx/tool_runtime/path_safety.py +22 -3
  14. langchain_agentx/tool_runtime/policy.py +64 -8
  15. langchain_agentx/tool_runtime/read_ignore_patterns.py +61 -12
  16. langchain_agentx/tool_runtime/read_permission.py +41 -7
  17. langchain_agentx/tool_runtime/session_store.py +7 -0
  18. langchain_agentx/tools/edit/staleness.py +6 -61
  19. langchain_agentx/tools/edit/tool.py +4 -0
  20. langchain_agentx/tools/edit/validator.py +15 -6
  21. langchain_agentx/tools/glob/tool.py +4 -0
  22. langchain_agentx/tools/grep/tool.py +4 -0
  23. langchain_agentx/tools/read/tool.py +5 -0
  24. langchain_agentx/tools/shared/__init__.py +6 -0
  25. langchain_agentx/tools/shared/read_state_guard.py +55 -0
  26. langchain_agentx/tools/shared/staleness.py +70 -0
  27. langchain_agentx/tools/skill/dynamic_catalog.py +98 -0
  28. langchain_agentx/tools/skill/dynamic_discoverer.py +88 -0
  29. langchain_agentx/tools/skill/file_discovery.py +40 -0
  30. langchain_agentx/tools/skill/guards.py +44 -0
  31. langchain_agentx/tools/skill/loader.py +14 -2
  32. langchain_agentx/tools/skill/tool.py +15 -5
  33. langchain_agentx/tools/write/read_state_validator.py +69 -0
  34. langchain_agentx/tools/write/tool.py +73 -10
  35. langchain_agentx/tools/write/validator.py +6 -1
  36. langchain_agentx/utils/agent_settings.py +87 -3
  37. langchain_agentx/utils/claude_temp_paths.py +45 -5
  38. langchain_agentx/utils/file_mtime.py +27 -0
  39. langchain_agentx/utils/git/__init__.py +24 -0
  40. langchain_agentx/utils/git/gitignore.py +134 -0
  41. langchain_agentx/utils/permissions/__init__.py +5 -0
  42. langchain_agentx/utils/permissions/filesystem.py +58 -0
  43. {langchain_agentx_python-1.3.0.dist-info → langchain_agentx_python-1.3.2.dist-info}/METADATA +1 -1
  44. {langchain_agentx_python-1.3.0.dist-info → langchain_agentx_python-1.3.2.dist-info}/RECORD +47 -32
  45. {langchain_agentx_python-1.3.0.dist-info → langchain_agentx_python-1.3.2.dist-info}/LICENSE +0 -0
  46. {langchain_agentx_python-1.3.0.dist-info → langchain_agentx_python-1.3.2.dist-info}/WHEEL +0 -0
  47. {langchain_agentx_python-1.3.0.dist-info → langchain_agentx_python-1.3.2.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,7 @@ from langchain_agentx import create_loop_agent
11
11
  ```
12
12
  """
13
13
 
14
- __version__ = "1.3.0"
14
+ __version__ = "1.3.2"
15
15
 
16
16
  from .loop import ( # noqa: F401
17
17
  create_loop_agent,
@@ -502,19 +502,26 @@ class LoopGraphBuilder:
502
502
  from ...memory.session import make_session_memory_section
503
503
 
504
504
  self._dynamic_sections.append(make_session_memory_section(session_summary_path))
505
+ instruction_bootstrap = InstructionMemoryBootstrap(
506
+ workspace_root=Path(self._workspace_root),
507
+ agent_home_segment=self._agent_home_segment,
508
+ managed_rules=self._config.managed_rules,
509
+ claude_md_excludes=self._config.claude_md_excludes,
510
+ active_files_provider=(
511
+ CallbackActiveFilesProvider(self._services.active_files_getter)
512
+ if self._services.active_files_getter is not None
513
+ else None
514
+ ),
515
+ )
516
+ loaded_instruction_rules = instruction_bootstrap.load_rule_set()
505
517
  self._dynamic_sections.extend(
506
- InstructionMemoryBootstrap(
507
- workspace_root=Path(self._workspace_root),
508
- agent_home_segment=self._agent_home_segment,
509
- managed_rules=self._config.managed_rules,
510
- claude_md_excludes=self._config.claude_md_excludes,
511
- active_files_provider=(
512
- CallbackActiveFilesProvider(self._services.active_files_getter)
513
- if self._services.active_files_getter is not None
514
- else None
515
- ),
516
- ).build_sections()
518
+ instruction_bootstrap.build_sections(loaded_instruction_rules)
517
519
  )
520
+ if session_store is not None:
521
+ instruction_bootstrap.seed_injected_read_state(
522
+ session_store,
523
+ loaded_instruction_rules,
524
+ )
518
525
  self._dynamic_sections.extend(
519
526
  MemoryPromptBootstrap(
520
527
  workspace_root=Path(self._workspace_root),
@@ -772,6 +779,8 @@ class LoopGraphBuilder:
772
779
  session_store=session_store,
773
780
  schema_cache_namespace=schema_cache_namespace,
774
781
  )
782
+ if self._loop_loader is not None:
783
+ self._loop_loader.refresh_search_ignore_patterns(session_store=session_store)
775
784
  if self._runtime_tools and self._loop_loader is not None:
776
785
  self._tool_prompt_ctx = self._build_tool_prompt_context(
777
786
  session_store=session_store,
@@ -0,0 +1,78 @@
1
+ """
2
+ memory/instruction/read_state_seeder.py — 指令注入 read-state 播种
3
+
4
+ 职责:
5
+ 将 Layer 1 规则文件注入 prompt 时同步写入 ``AgentSessionStore.file_read_state``;
6
+ 注入内容与磁盘不一致时标记 ``is_partial_view``(对齐 CC attachments memoryFilesToAttachments)。
7
+
8
+ 链路位置:
9
+ LoopGraphBuilder 初始化 → InstructionMemoryBootstrap.seed_injected_read_state()。
10
+
11
+ 当前裁剪范围:
12
+ 仅 Layer 1 ``LoadedRuleSet`` 文件路径;不含 memdir prefetch / session memory。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import time
18
+ from pathlib import Path
19
+ from typing import Iterable
20
+
21
+ from langchain_agentx.tool_runtime.session_store import AgentSessionStore
22
+ from langchain_agentx.utils.file_mtime import FileModificationTimeReader
23
+
24
+ from .loader import strip_html_comments
25
+ from .types import LoadedRuleSet, RuleFile
26
+
27
+
28
+ class InstructionReadStateSeeder:
29
+ """指令规则注入时播种 last_read(partial view 语义)。"""
30
+
31
+ def __init__(self, *, mtime_reader: FileModificationTimeReader | None = None) -> None:
32
+ self._mtime_reader = mtime_reader or FileModificationTimeReader()
33
+
34
+ def seed(self, session_store: AgentSessionStore, loaded: LoadedRuleSet) -> None:
35
+ for path, injected_content in self._iter_path_content_pairs(loaded):
36
+ self._seed_path(session_store, path, injected_content)
37
+
38
+ def _iter_path_content_pairs(self, loaded: LoadedRuleSet) -> Iterable[tuple[Path, str]]:
39
+ if loaded.user is not None and loaded.user.path is not None:
40
+ yield loaded.user.path, loaded.user.content
41
+ for rule_file in loaded.project_files:
42
+ if rule_file.path is not None:
43
+ yield rule_file.path, rule_file.content
44
+ if loaded.local is not None and loaded.local.path is not None:
45
+ yield loaded.local.path, loaded.local.content
46
+ for rule in loaded.conditional_rules:
47
+ yield rule.path, rule.content
48
+
49
+ def _seed_path(
50
+ self,
51
+ session_store: AgentSessionStore,
52
+ file_path: Path,
53
+ injected_content: str,
54
+ ) -> None:
55
+ path = str(file_path.resolve())
56
+ if session_store.has_been_read(path):
57
+ return
58
+ try:
59
+ raw_disk = file_path.read_text(encoding="utf-8")
60
+ except OSError:
61
+ return
62
+
63
+ injected_view = strip_html_comments(injected_content)
64
+ content_differs = injected_view != raw_disk
65
+ mtime_ms = self._mtime_reader.read_ms(path)
66
+ entry: dict[str, object] = {
67
+ "tool_call_id": None,
68
+ "ts": time.time(),
69
+ "offset": None,
70
+ "limit": None,
71
+ "mtime_ms": mtime_ms,
72
+ "line_start": None,
73
+ "line_end": None,
74
+ "content": raw_disk if content_differs else injected_view,
75
+ }
76
+ if content_differs:
77
+ entry["is_partial_view"] = True
78
+ session_store.upsert_last_read_from_bridge_entry(path, entry)
@@ -15,13 +15,15 @@ from __future__ import annotations
15
15
  import logging
16
16
  from collections.abc import Callable, Sequence
17
17
  from pathlib import Path
18
+ from typing import Any
18
19
 
19
20
  from langchain_agentx.loop.prompt import SystemPromptSection
20
21
  from langchain_agentx.workspace import resolve_agent_state_config
21
22
 
22
23
  from .loader import load_rule_set
24
+ from .read_state_seeder import InstructionReadStateSeeder
23
25
  from .sections import make_instruction_sections
24
- from .types import InstructionMemoryConfig
26
+ from .types import InstructionMemoryConfig, LoadedRuleSet
25
27
 
26
28
  logger = logging.getLogger(__name__)
27
29
 
@@ -43,31 +45,53 @@ class InstructionMemoryBootstrap:
43
45
  self._include_depth_limit = include_depth_limit
44
46
  self._claude_md_excludes = tuple(claude_md_excludes)
45
47
  self._active_files_provider = active_files_provider or NullActiveFilesProvider()
48
+ self._read_state_seeder = InstructionReadStateSeeder()
46
49
 
47
- def build_sections(self) -> list[SystemPromptSection]:
50
+ def _build_config(self) -> InstructionMemoryConfig:
48
51
  workspace_cfg = resolve_agent_state_config(
49
52
  workspace_root=self._workspace_root,
50
53
  agent_home_segment=self._agent_home_segment,
51
54
  )
52
- loaded_rules = load_rule_set(
53
- InstructionMemoryConfig(
54
- workspace_cfg=workspace_cfg,
55
- managed_rules=self._managed_rules,
56
- load_user_layer=True,
57
- include_depth_limit=self._include_depth_limit,
58
- claude_md_excludes=self._claude_md_excludes,
59
- )
55
+ return InstructionMemoryConfig(
56
+ workspace_cfg=workspace_cfg,
57
+ managed_rules=self._managed_rules,
58
+ load_user_layer=True,
59
+ include_depth_limit=self._include_depth_limit,
60
+ claude_md_excludes=self._claude_md_excludes,
60
61
  )
62
+
63
+ def load_rule_set(self) -> LoadedRuleSet:
64
+ loaded_rules = load_rule_set(self._build_config())
61
65
  if loaded_rules.external_include_warnings:
62
66
  logger.warning(
63
67
  "instruction_memory: external @path includes detected outside workspace: %s",
64
68
  ", ".join(loaded_rules.external_include_warnings),
65
69
  )
70
+ return loaded_rules
71
+
72
+ def build_sections(
73
+ self,
74
+ loaded_rules: LoadedRuleSet | None = None,
75
+ ) -> list[SystemPromptSection]:
76
+ rules = loaded_rules if loaded_rules is not None else self.load_rule_set()
66
77
  return make_instruction_sections(
67
- loaded_rules,
78
+ rules,
68
79
  active_files_getter=self._active_files_provider.get_active_files,
69
80
  )
70
81
 
82
+ def seed_injected_read_state(
83
+ self,
84
+ session_store: Any,
85
+ loaded_rules: LoadedRuleSet | None = None,
86
+ ) -> None:
87
+ """注入 Layer 1 规则后播种 file_read_state(CC readFileState 对齐)。"""
88
+ from langchain_agentx.tool_runtime.session_store import AgentSessionStore
89
+
90
+ if not isinstance(session_store, AgentSessionStore):
91
+ return
92
+ rules = loaded_rules if loaded_rules is not None else self.load_rule_set()
93
+ self._read_state_seeder.seed(session_store, rules)
94
+
71
95
 
72
96
  class ActiveFilesProvider:
73
97
  """active_files 提供器接口。"""
@@ -245,8 +245,9 @@ class PluginLoader:
245
245
 
246
246
  async def _register_skills(self, plugins: list[LoadedPlugin]) -> list[PluginError]:
247
247
  errors: list[PluginError] = []
248
+ cwd = self._cfg.workspace_root
248
249
  for plugin in plugins:
249
- errors.extend(self._skill_registry.register(plugin))
250
+ errors.extend(self._skill_registry.register(plugin, cwd=cwd))
250
251
  return errors
251
252
 
252
253
  async def _register_commands(self, plugins: list[LoadedPlugin]) -> list[PluginError]:
@@ -13,11 +13,16 @@ plugin/registries.py — Plugin 能力注册表(Skill/Command/Agent)。
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import logging
16
17
  from dataclasses import dataclass
17
18
  from pathlib import Path
18
19
 
20
+ from langchain_agentx.tools.skill.guards import SkillsGitignoreGuard
21
+
19
22
  from .types import ComponentLoadFailedError, LoadedPlugin, PluginError
20
23
 
24
+ logger = logging.getLogger(__name__)
25
+
21
26
 
22
27
  @dataclass(frozen=True)
23
28
  class SkillEntry:
@@ -43,15 +48,37 @@ class AgentDefinitionEntry:
43
48
 
44
49
 
45
50
  class SkillRegistry:
46
- def __init__(self) -> None:
51
+ def __init__(
52
+ self,
53
+ *,
54
+ workspace_root: str | Path | None = None,
55
+ gitignore_guard: SkillsGitignoreGuard | None = None,
56
+ ) -> None:
47
57
  self._skills: dict[str, SkillEntry] = {}
48
58
  self._by_plugin: dict[str, list[str]] = {}
49
-
50
- def register(self, plugin: LoadedPlugin) -> list[PluginError]:
59
+ self._workspace_root = (
60
+ Path(workspace_root).resolve() if workspace_root is not None else None
61
+ )
62
+ self._gitignore_guard = gitignore_guard or SkillsGitignoreGuard()
63
+
64
+ def register(
65
+ self,
66
+ plugin: LoadedPlugin,
67
+ *,
68
+ cwd: str | Path | None = None,
69
+ ) -> list[PluginError]:
51
70
  if plugin.skills_dir is None or not plugin.skills_dir.exists():
52
71
  self._by_plugin[plugin.source] = []
53
72
  return []
54
73
 
74
+ work_cwd = cwd if cwd is not None else self._workspace_root
75
+ if work_cwd is not None and self._gitignore_guard.should_skip_skills_dir(
76
+ plugin.skills_dir,
77
+ cwd=work_cwd,
78
+ ):
79
+ self._by_plugin[plugin.source] = []
80
+ return []
81
+
55
82
  errors: list[PluginError] = []
56
83
  registered: list[str] = []
57
84
  for skill_dir in sorted(plugin.skills_dir.iterdir()):
@@ -97,7 +97,7 @@ class AgentSession:
97
97
  resource_root=workspace_cfg.workspace_root,
98
98
  agent_home_segment=workspace_cfg.agent_home_segment,
99
99
  )
100
- self._skill_registry = SkillRegistry()
100
+ self._skill_registry = SkillRegistry(workspace_root=workspace_cfg.workspace_root)
101
101
  self._command_registry = PluginCommandRegistry()
102
102
  self._agent_registry = AgentRegistry()
103
103
  self._runtime_command_registry = CommandRegistry()
@@ -71,7 +71,7 @@ class ConversationSession:
71
71
  self._container_type = "conversation"
72
72
  self._session_memory_manager: Any | None = None
73
73
  self._capabilities = capabilities
74
- self._skill_registry = SkillRegistry()
74
+ self._skill_registry = SkillRegistry(workspace_root=workspace_cfg.workspace_root)
75
75
  self._command_registry = PluginCommandRegistry()
76
76
  self._agent_registry = AgentRegistry()
77
77
  self._runtime_command_registry = CommandRegistry()
@@ -44,7 +44,11 @@ from .permission_snapshot import (
44
44
  )
45
45
  from .path_safety import PathSafetyConfig, PathSafetyEvaluator
46
46
  from .policy import DefaultPolicyEngine, PolicyEngine, ToolPolicyConfig
47
- from .read_ignore_patterns import ReadDenyPatternCollector, normalize_read_deny_pattern
47
+ from .read_ignore_patterns import (
48
+ ReadDenyPatternCollector,
49
+ SearchIgnorePatternCollector,
50
+ normalize_read_deny_pattern,
51
+ )
48
52
  from .policy_decorator import PolicyEngineDecorator
49
53
  from .prompt import (
50
54
  CliInteractivePolicyEngine,
@@ -124,6 +128,7 @@ __all__ = [
124
128
  "PathSafetyConfig",
125
129
  "PathSafetyEvaluator",
126
130
  "ReadDenyPatternCollector",
131
+ "SearchIgnorePatternCollector",
127
132
  "normalize_read_deny_pattern",
128
133
  # prompt (L3)
129
134
  "PermissionPromptHandler",
@@ -0,0 +1,231 @@
1
+ """
2
+ agent_home_bypass.py — session 级 agent_home 写 bypass(CC filesystem step 1.6)
3
+
4
+ 职责:
5
+ 仅 session 源 Edit allow 规则可绕过 path_safety 对 ``{segment}/`` 的 dangerous ask;
6
+ 对齐 CC ``checkWritePermissionForTool`` 1.6 与 ``CLAUDE_FOLDER_PERMISSION_PATTERN``。
7
+
8
+ 链路位置:
9
+ ``DefaultPolicyEngine.evaluate_write_path_policy`` — L2/internal 之后、scratchpad allow、
10
+ ``PathSafetyEvaluator`` 之前。
11
+
12
+ 当前裁剪范围:
13
+ 仅 ``Edit`` 工具;模式 ``/{segment}/**``、``~/{segment}/**``、
14
+ ``/{segment}/skills/{name}/**``;不含 ``getClaudeSkillScope`` 建议生成(UI 层)。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import fnmatch
20
+ import os
21
+ import re
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ from langchain_agentx.tool_runtime.models import AuthorizationDecision
27
+ from langchain_agentx.tool_runtime.permission_decision import (
28
+ attach_decision_reason,
29
+ make_rule_decision_reason,
30
+ )
31
+ from langchain_agentx.tool_runtime.permission_rules import (
32
+ PermissionRule,
33
+ PermissionRuleBehavior,
34
+ PermissionRuleSource,
35
+ PermissionRuleValue,
36
+ build_permission_rule_registry,
37
+ permission_rule_value_from_string,
38
+ )
39
+ from langchain_agentx.tool_runtime.tool_name_constants import Edit
40
+
41
+ if TYPE_CHECKING:
42
+ from .policy import ToolPolicyConfig
43
+
44
+ _SKILL_GLOB_META = re.compile(r"[*?[\]]")
45
+ _POLICY_ID = "agent_home_session_bypass"
46
+
47
+
48
+ def normalize_agent_home_segment(agent_home_segment: str | None) -> str:
49
+ seg = (agent_home_segment or ".langchain_agentx").strip()
50
+ if not seg.startswith("."):
51
+ seg = f".{seg}"
52
+ return seg
53
+
54
+
55
+ def project_agent_home_folder_pattern(segment: str) -> str:
56
+ """对齐 CC ``CLAUDE_FOLDER_PERMISSION_PATTERN``(参数化 segment)。"""
57
+ return f"/{segment}/**"
58
+
59
+
60
+ def global_agent_home_folder_pattern(segment: str) -> str:
61
+ """对齐 CC ``GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN``。"""
62
+ return f"~/{segment}/**"
63
+
64
+
65
+ def is_valid_agent_home_session_allow_pattern(
66
+ rule_content: str,
67
+ *,
68
+ agent_home_segment: str,
69
+ ) -> bool:
70
+ """
71
+ CC step 1.6 scope check:拒绝 ``..`` / 非 ``/**`` 结尾;窄化 skills 子路径。
72
+ """
73
+ content = str(rule_content).strip()
74
+ if not content or ".." in content or not content.endswith("/**"):
75
+ return False
76
+
77
+ segment = normalize_agent_home_segment(agent_home_segment)
78
+ folder_base = project_agent_home_folder_pattern(segment)[:-2] # ``/.segment/``
79
+ global_base = global_agent_home_folder_pattern(segment)[:-2] # ``~/.segment/``
80
+
81
+ if not (content.startswith(folder_base) or content.startswith(global_base)):
82
+ return False
83
+
84
+ if content in (project_agent_home_folder_pattern(segment), global_agent_home_folder_pattern(segment)):
85
+ return True
86
+
87
+ for base in (folder_base, global_base):
88
+ skills_prefix = f"{base}skills/"
89
+ if content.startswith(skills_prefix):
90
+ skill_part = content[len(skills_prefix) : -3]
91
+ if not skill_part or skill_part == "." or ".." in skill_part:
92
+ return False
93
+ if _SKILL_GLOB_META.search(skill_part):
94
+ return False
95
+ return content == f"{skills_prefix}{skill_part}/**"
96
+
97
+ return False
98
+
99
+
100
+ def _is_under(path: Path, base: Path) -> bool:
101
+ try:
102
+ path.resolve().relative_to(base.resolve())
103
+ return True
104
+ except ValueError:
105
+ return False
106
+
107
+
108
+ def path_matches_agent_home_allow_pattern(
109
+ real_path: str,
110
+ rule_content: str,
111
+ *,
112
+ workspace_root: str | Path | None,
113
+ agent_home_segment: str,
114
+ ) -> bool:
115
+ """路径是否命中 agent_home allow 规则(目录语义 + fnmatch 补充)。"""
116
+ content = str(rule_content).strip()
117
+ segment = normalize_agent_home_segment(agent_home_segment)
118
+ path = Path(real_path).resolve()
119
+
120
+ if content == project_agent_home_folder_pattern(segment):
121
+ if workspace_root is not None:
122
+ project_base = Path(workspace_root).resolve() / segment
123
+ if _is_under(path, project_base):
124
+ return True
125
+ return _is_under(path, Path.home().resolve() / segment)
126
+
127
+ if content == global_agent_home_folder_pattern(segment):
128
+ return _is_under(path, Path.home().resolve() / segment)
129
+
130
+ folder_base = f"/{segment}/skills/"
131
+ global_skills_base = f"~/{segment}/skills/"
132
+ skill_name: str | None = None
133
+ if content.startswith(folder_base) and content.endswith("/**"):
134
+ skill_name = content[len(folder_base) : -3]
135
+ elif content.startswith(global_skills_base) and content.endswith("/**"):
136
+ skill_name = content[len(global_skills_base) : -3]
137
+
138
+ if skill_name:
139
+ if workspace_root is not None:
140
+ skill_dir = Path(workspace_root).resolve() / segment / "skills" / skill_name
141
+ if _is_under(path, skill_dir):
142
+ return True
143
+ skill_dir = Path.home().resolve() / segment / "skills" / skill_name
144
+ return _is_under(path, skill_dir)
145
+
146
+ candidates = [real_path.replace("\\", "/")]
147
+ if workspace_root is not None:
148
+ try:
149
+ rel = os.path.relpath(real_path, str(workspace_root)).replace("\\", "/")
150
+ if not rel.startswith(".."):
151
+ candidates.append(f"/{rel}")
152
+ candidates.append(rel)
153
+ except ValueError:
154
+ pass
155
+ return any(fnmatch.fnmatch(c, content) for c in candidates)
156
+
157
+
158
+ @dataclass
159
+ class AgentHomeSessionBypassEvaluator:
160
+ """session Edit allow → agent_home path_safety bypass(CC step 1.6)。"""
161
+
162
+ def evaluate(
163
+ self,
164
+ real_path: str,
165
+ *,
166
+ tool_name: str,
167
+ policy_config: ToolPolicyConfig | None,
168
+ session_store: Any | None = None,
169
+ workspace_root: str | Path | None = None,
170
+ agent_home_segment: str | None = None,
171
+ ) -> AuthorizationDecision | None:
172
+ if tool_name != Edit:
173
+ return None
174
+
175
+ segment = normalize_agent_home_segment(agent_home_segment)
176
+ registry = build_permission_rule_registry(
177
+ policy_config=policy_config,
178
+ session_store=session_store,
179
+ workspace_root=workspace_root,
180
+ agent_home_segment=segment,
181
+ )
182
+
183
+ session_rules = registry.allow.get(PermissionRuleSource.SESSION.value, ())
184
+ for rule_string in session_rules:
185
+ try:
186
+ value = permission_rule_value_from_string(rule_string)
187
+ except ValueError:
188
+ continue
189
+ if value.tool_name != Edit or value.rule_content is None:
190
+ continue
191
+ content = value.rule_content
192
+ if not is_valid_agent_home_session_allow_pattern(
193
+ content,
194
+ agent_home_segment=segment,
195
+ ):
196
+ continue
197
+ if not path_matches_agent_home_allow_pattern(
198
+ real_path,
199
+ content,
200
+ workspace_root=workspace_root,
201
+ agent_home_segment=segment,
202
+ ):
203
+ continue
204
+ rule = PermissionRule(
205
+ source=PermissionRuleSource.SESSION,
206
+ rule_behavior=PermissionRuleBehavior.ALLOW,
207
+ rule_value=PermissionRuleValue(
208
+ tool_name=Edit,
209
+ rule_content=content,
210
+ ),
211
+ )
212
+ decision = AuthorizationDecision(
213
+ behavior="allow",
214
+ policy_id=_POLICY_ID,
215
+ message="Write allowed by session agent_home permission rule.",
216
+ )
217
+ return attach_decision_reason(
218
+ decision,
219
+ make_rule_decision_reason(rule, rule_kind="always_allow_tool"),
220
+ )
221
+ return None
222
+
223
+
224
+ __all__ = [
225
+ "AgentHomeSessionBypassEvaluator",
226
+ "global_agent_home_folder_pattern",
227
+ "is_valid_agent_home_session_allow_pattern",
228
+ "normalize_agent_home_segment",
229
+ "path_matches_agent_home_allow_pattern",
230
+ "project_agent_home_folder_pattern",
231
+ ]
@@ -383,6 +383,7 @@ class InnerPermissionChain:
383
383
  wr = ctx.workspace_root
384
384
  if wr is None and self._engine._config.project_memory_layout is not None:
385
385
  wr = str(self._engine._config.project_memory_layout.project_root)
386
+ session_id, session_cwd = self._engine._session_context(ctx)
386
387
  decision = self._engine._read_authorizer.authorize(
387
388
  real_path,
388
389
  tool_name=tool_name,
@@ -390,6 +391,8 @@ class InnerPermissionChain:
390
391
  session_store=ctx.session_store,
391
392
  workspace_root=wr,
392
393
  agent_home_segment=ctx.agent_home_segment,
394
+ session_id=session_id,
395
+ session_cwd=session_cwd,
393
396
  )
394
397
  return PathPolicyResult(decision=decision)
395
398
 
@@ -401,7 +404,11 @@ class InnerPermissionChain:
401
404
  ),
402
405
  )
403
406
 
404
- return self._engine.evaluate_write_path_policy(real_path)
407
+ return self._engine.evaluate_write_path_policy(
408
+ real_path,
409
+ tool_name=tool_name,
410
+ ctx=ctx,
411
+ )
405
412
 
406
413
  @staticmethod
407
414
  def _segment_5_passthrough_promoted_ask(
@@ -19,7 +19,7 @@ from typing import Any
19
19
 
20
20
  from .path_safety import PathSafetyConfig
21
21
  from .process_loader import ProcessLoader
22
- from .read_ignore_patterns import ReadDenyPatternCollector
22
+ from .read_ignore_patterns import SearchIgnorePatternCollector
23
23
  from .registry import RuntimeToolRegistry
24
24
  from .policy import DefaultPolicyEngine, ToolPolicyConfig
25
25
 
@@ -76,7 +76,7 @@ class LoopLoader:
76
76
  from langchain_agentx.tools.bash import BashRuntimeTool
77
77
  from langchain_agentx.tools.edit import EditRuntimeTool
78
78
  from langchain_agentx.tools.edit.settings_validator import SettingsValidator
79
- from langchain_agentx.tools.edit.staleness import StalenessChecker
79
+ from langchain_agentx.tools.shared.staleness import StalenessChecker
80
80
  from langchain_agentx.tools.edit.validator import EditStringValidator
81
81
  from langchain_agentx.tools.glob import GlobRuntimeTool
82
82
  from langchain_agentx.tools.grep import GrepRuntimeTool
@@ -124,7 +124,12 @@ class LoopLoader:
124
124
  )
125
125
 
126
126
  self.register(ReadRuntimeTool(state_bridge=shared_bridge))
127
- self.register(WriteRuntimeTool(state_bridge=shared_bridge))
127
+ self.register(
128
+ WriteRuntimeTool(
129
+ state_bridge=shared_bridge,
130
+ staleness_checker=edit_staleness,
131
+ )
132
+ )
128
133
  self.register(
129
134
  EditRuntimeTool(
130
135
  state_bridge=shared_bridge,
@@ -176,12 +181,34 @@ class LoopLoader:
176
181
 
177
182
  return self
178
183
 
179
- def _collect_search_ignore_patterns(self) -> list[str]:
184
+ def _collect_search_ignore_patterns(
185
+ self,
186
+ *,
187
+ session_store: Any | None = None,
188
+ ) -> list[str]:
180
189
  cfg = self._process.policy_config
181
190
  policy = self._process.policy
182
191
  if cfg is None and isinstance(policy, DefaultPolicyEngine):
183
192
  cfg = policy._config
184
- return ReadDenyPatternCollector().collect(cfg)
193
+ return SearchIgnorePatternCollector().collect(
194
+ cfg,
195
+ session_store=session_store,
196
+ workspace_root=self._workspace_root,
197
+ agent_home_segment=self._agent_home,
198
+ )
199
+
200
+ def refresh_search_ignore_patterns(
201
+ self,
202
+ *,
203
+ session_store: Any | None = None,
204
+ ) -> list[str]:
205
+ """热刷新 Grep/Glob 的 ``ignore_patterns``(对齐 CC settings 变更后搜索隐藏)。"""
206
+ patterns = self._collect_search_ignore_patterns(session_store=session_store)
207
+ for tool in self._registry.list():
208
+ setter = getattr(tool, "set_ignore_patterns", None)
209
+ if callable(setter):
210
+ setter(patterns)
211
+ return patterns
185
212
 
186
213
  def _inject_project_memory_policy(self, workspace_cfg: Any) -> None:
187
214
  """Per-loop 注入 CC projects 树读写根与 path_safety layout。"""