langchain-agentx-python 2.2.7__py3-none-any.whl → 2.2.9__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.
@@ -11,7 +11,7 @@ from langchain_agentx import create_loop_agent
11
11
  ```
12
12
  """
13
13
 
14
- __version__ = "2.2.7"
14
+ __version__ = "2.2.9"
15
15
 
16
16
  from .loop import ( # noqa: F401
17
17
  create_loop_agent,
@@ -43,6 +43,7 @@ from .permission_snapshot import (
43
43
  collect_denied_agent_types_from_policy as collect_denied_agent_types,
44
44
  )
45
45
  from .path_safety import PathSafetyConfig, PathSafetyEvaluator
46
+ from .permission_cli_rules import apply_cli_permission_rules, parse_tool_list_from_cli
46
47
  from .policy import DefaultPolicyEngine, PolicyEngine, ToolPolicyConfig
47
48
  from .read_ignore_patterns import (
48
49
  ReadDenyPatternCollector,
@@ -122,6 +123,8 @@ __all__ = [
122
123
  "validation_error_envelope",
123
124
  # policy
124
125
  "ToolPolicyConfig",
126
+ "apply_cli_permission_rules",
127
+ "parse_tool_list_from_cli",
125
128
  "PolicyEngine",
126
129
  "DefaultPolicyEngine",
127
130
  "PolicyEngineDecorator",
@@ -24,6 +24,7 @@ from .models import AuthorizationDecision
24
24
  from .permission_decision import (
25
25
  POLICY_ID_PREFIX_ALWAYS_ASK_TOOL,
26
26
  attach_decision_reason,
27
+ get_decision_reason,
27
28
  make_other_decision_reason,
28
29
  make_rule_decision_reason,
29
30
  )
@@ -40,6 +41,14 @@ if TYPE_CHECKING:
40
41
  from .policy import DefaultPolicyEngine
41
42
 
42
43
 
44
+ def _l1_ask_blocks_tool_level_allow(decision: AuthorizationDecision) -> bool:
45
+ """CC step 1g:safetyCheck 产生的 ask 不得被段 4 tool-level allow 覆盖。"""
46
+ reason = get_decision_reason(decision)
47
+ if reason is None:
48
+ return False
49
+ return reason.get("type") == "safetyCheck"
50
+
51
+
43
52
  @dataclass
44
53
  class PathPolicyResult:
45
54
  decision: AuthorizationDecision | None = None
@@ -80,6 +89,9 @@ class InnerPermissionChain:
80
89
  raise ValueError(f"L1 outcome={l1.outcome!r} requires decision")
81
90
  if tool.requires_user_interaction_for(data, ctx):
82
91
  return l1.decision
92
+ # CC step 1g:safetyCheck ask 不被 tool-level allow(2b)覆盖
93
+ if _l1_ask_blocks_tool_level_allow(l1.decision):
94
+ return l1.decision
83
95
  seg4 = self._segment_4_mode_shortcircuit(tool, data, ctx, tool_flags)
84
96
  if seg4 is not None:
85
97
  return seg4
@@ -0,0 +1,143 @@
1
+ """
2
+ permission_cli_rules.py — CC 对齐 CLI allow/disallow 解析与 ToolPolicyConfig 注入
3
+
4
+ 职责:
5
+ parse_tool_list_from_cli(对齐 CC parseToolListFromCLI);
6
+ apply_cli_permission_rules 写入 allow_rules_by_source / deny_rules_by_source["cliArg"]。
7
+
8
+ 链路位置:
9
+ CLI / new_agent / stream_ui → ToolRuntimeLoader(policy_config=...) → build_permission_rule_registry。
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Sequence
15
+
16
+ from .permission_rules import (
17
+ PermissionRule,
18
+ PermissionRuleBehavior,
19
+ PermissionRuleSource,
20
+ PermissionRuleValue,
21
+ permission_rule_to_string,
22
+ permission_rule_value_from_string,
23
+ )
24
+ from .policy import ToolPolicyConfig
25
+
26
+ _LEGACY_TASK_TOOL = "Task"
27
+ _CANONICAL_AGENT_TOOL = "Agent"
28
+
29
+
30
+ def parse_tool_list_from_cli(tools: Sequence[str]) -> list[str]:
31
+ """对齐 CC permissionSetup.ts parseToolListFromCLI(括号内不切分)。"""
32
+ if not tools:
33
+ return []
34
+ result: list[str] = []
35
+ for tool_string in tools:
36
+ if not tool_string:
37
+ continue
38
+ current = ""
39
+ is_in_parens = False
40
+ for char in tool_string:
41
+ if char == "(":
42
+ is_in_parens = True
43
+ current += char
44
+ elif char == ")":
45
+ is_in_parens = False
46
+ current += char
47
+ elif char == ",":
48
+ if is_in_parens:
49
+ current += char
50
+ else:
51
+ if current.strip():
52
+ result.append(current.strip())
53
+ current = ""
54
+ elif char == " ":
55
+ if is_in_parens:
56
+ current += char
57
+ elif current.strip():
58
+ result.append(current.strip())
59
+ current = ""
60
+ else:
61
+ current += char
62
+ if current.strip():
63
+ result.append(current.strip())
64
+ return result
65
+
66
+
67
+ def canonical_cli_rule_string(rule: str) -> str:
68
+ """对齐 CC allow 列表规范化(permissionRuleValueFromString + ToString;Task→Agent)。"""
69
+ value = permission_rule_value_from_string(rule.strip())
70
+ tool_name = value.tool_name
71
+ if tool_name == _LEGACY_TASK_TOOL:
72
+ tool_name = _CANONICAL_AGENT_TOOL
73
+ normalized = PermissionRule(
74
+ source=PermissionRuleSource.CLI_ARG,
75
+ rule_behavior=PermissionRuleBehavior.ALLOW,
76
+ rule_value=PermissionRuleValue(
77
+ tool_name=tool_name,
78
+ rule_content=value.rule_content,
79
+ ),
80
+ )
81
+ return permission_rule_to_string(normalized)
82
+
83
+
84
+ def apply_cli_permission_rules(
85
+ config: ToolPolicyConfig,
86
+ *,
87
+ allowed: Sequence[str] | None = None,
88
+ disallowed: Sequence[str] | None = None,
89
+ ) -> ToolPolicyConfig:
90
+ """
91
+ 合并 cliArg 规则;不修改 userSettings / projectSettings 等其它 source。
92
+
93
+ ``allowed is None`` / ``disallowed is None`` 表示不触碰对应 cliArg 键;
94
+ 传空序列表示清除该 cliArg 键。
95
+ """
96
+ allow_map = dict(config.allow_rules_by_source)
97
+ deny_map = dict(config.deny_rules_by_source)
98
+
99
+ if allowed is not None:
100
+ if allowed:
101
+ parsed = parse_tool_list_from_cli(allowed)
102
+ allow_map[PermissionRuleSource.CLI_ARG.value] = tuple(
103
+ canonical_cli_rule_string(r) for r in parsed
104
+ )
105
+ else:
106
+ allow_map.pop(PermissionRuleSource.CLI_ARG.value, None)
107
+
108
+ if disallowed is not None:
109
+ if disallowed:
110
+ parsed = parse_tool_list_from_cli(disallowed)
111
+ deny_map[PermissionRuleSource.CLI_ARG.value] = tuple(
112
+ canonical_cli_rule_string(r) for r in parsed
113
+ )
114
+ else:
115
+ deny_map.pop(PermissionRuleSource.CLI_ARG.value, None)
116
+
117
+ return ToolPolicyConfig(
118
+ read_roots=list(config.read_roots),
119
+ write_roots=list(config.write_roots),
120
+ deny_globs=list(config.deny_globs),
121
+ read_deny_globs=list(config.read_deny_globs),
122
+ ask_globs=list(config.ask_globs),
123
+ always_ask_tools=config.always_ask_tools,
124
+ allow_rules_by_source=allow_map,
125
+ deny_rules_by_source=deny_map,
126
+ ask_rules_by_source=dict(config.ask_rules_by_source),
127
+ cli_allow_tools=config.cli_allow_tools,
128
+ flag_settings_path=config.flag_settings_path,
129
+ read_only_mode=config.read_only_mode,
130
+ enable_builtin_deny=config.enable_builtin_deny,
131
+ enable_path_safety=config.enable_path_safety,
132
+ path_safety=config.path_safety,
133
+ auto_mode=config.auto_mode,
134
+ project_memory_layout=config.project_memory_layout,
135
+ denial_tracking=config.denial_tracking,
136
+ )
137
+
138
+
139
+ __all__ = [
140
+ "apply_cli_permission_rules",
141
+ "canonical_cli_rule_string",
142
+ "parse_tool_list_from_cli",
143
+ ]
@@ -54,6 +54,13 @@ class RegistryPermissionMatcher:
54
54
  ) -> AuthorizationDecision | None:
55
55
  return self._match(tool_name, target, PermissionRuleBehavior.ASK)
56
56
 
57
+ def match_allow(
58
+ self,
59
+ tool_name: str,
60
+ target: str,
61
+ ) -> AuthorizationDecision | None:
62
+ return self._match(tool_name, target, PermissionRuleBehavior.ALLOW)
63
+
57
64
  def _match(
58
65
  self,
59
66
  tool_name: str,
@@ -91,7 +98,12 @@ def collect_denied_agent_types(registry: PermissionRuleRegistry) -> frozenset[st
91
98
  def _decision_from_rule(rule: PermissionRule, *, behavior: str) -> AuthorizationDecision:
92
99
  rule_str = permission_rule_to_string(rule)
93
100
  policy_id = f"permission_rule:{rule.source_name}:{rule_str}"
94
- if behavior == "ask":
101
+ if behavior == "allow":
102
+ decision = AuthorizationDecision(
103
+ behavior="allow",
104
+ policy_id=policy_id,
105
+ )
106
+ elif behavior == "ask":
95
107
  decision = AuthorizationDecision(
96
108
  behavior="ask",
97
109
  ask_prompt=(
@@ -106,6 +106,30 @@ class PermissionRule:
106
106
  return s.value if isinstance(s, PermissionRuleSource) else str(s)
107
107
 
108
108
 
109
+ def _content_rule_matches_target(
110
+ *,
111
+ tool_name: str,
112
+ target: str,
113
+ rule_content: str,
114
+ ) -> bool:
115
+ if tool_name == "Bash":
116
+ from langchain_agentx.tools.bash.command_allow_rules import (
117
+ bash_command_matches_allow_rule,
118
+ )
119
+
120
+ return bash_command_matches_allow_rule(target, rule_content)
121
+ if tool_name == "WebFetch":
122
+ from langchain_agentx.tools.webfetch.registry_permission import (
123
+ webfetch_rule_content_matches,
124
+ )
125
+
126
+ return webfetch_rule_content_matches(
127
+ target=target,
128
+ rule_content=rule_content,
129
+ )
130
+ return fnmatch.fnmatch(target, rule_content)
131
+
132
+
109
133
  def permission_rule_value_from_string(rule_string: str) -> PermissionRuleValue:
110
134
  """解析 CC 规则串,如 ``Bash(python:*)`` 或工具级 ``Read``。"""
111
135
  text = rule_string.strip()
@@ -254,7 +278,7 @@ class PermissionRuleRegistry:
254
278
  target: str,
255
279
  behavior: PermissionRuleBehavior,
256
280
  ) -> PermissionRule | None:
257
- """CC Tool(content) 规则:工具名精确匹配 + fnmatch(content, target)。"""
281
+ """CC Tool(content) 规则:工具名精确匹配 + fnmatch / Bash prefix:*。"""
258
282
  tool_key = tool_name.strip()
259
283
  if not tool_key:
260
284
  return None
@@ -264,7 +288,11 @@ class PermissionRuleRegistry:
264
288
  continue
265
289
  if rule.tool_name != tool_key:
266
290
  continue
267
- if fnmatch.fnmatch(target, content):
291
+ if _content_rule_matches_target(
292
+ tool_name=tool_key,
293
+ target=target,
294
+ rule_content=content,
295
+ ):
268
296
  return rule
269
297
  return None
270
298
 
@@ -274,6 +302,8 @@ class PermissionRuleRegistry:
274
302
  behavior: PermissionRuleBehavior,
275
303
  ) -> str | None:
276
304
  """v1 deny_globs/ask_globs 减法:裸 glob / 命令模式 fnmatch。"""
305
+ if behavior is PermissionRuleBehavior.ALLOW:
306
+ return None
277
307
  patterns = (
278
308
  self.legacy_deny_globs
279
309
  if behavior is PermissionRuleBehavior.DENY
@@ -25,12 +25,12 @@ MAX_PREVIEW_SNAPSHOT_CHARS: int = 8192
25
25
  # present():每段 preview / snapshot 写入模型可见文本时的默认截断
26
26
  PRESENT_PREVIEW_SNIPPET_MAX: int = 500
27
27
 
28
- # 全角问号(中文输入法常见);校验要求 ASCII '?'
28
+ # 全角问号(中文输入法常见);仅作 normalize 软转换,不作为 validate 硬拒条件
29
29
  FULLWIDTH_QUESTION_MARK: str = "\uff1f"
30
30
 
31
31
 
32
32
  def normalize_question_text(text: str) -> str:
33
- """将题干末尾全角 ``?`` 规范为 ASCII ``?``(CC / QuestionValidator 一致)。"""
33
+ """将题干末尾全角 ``?`` 规范为 ASCII ``?``(展示一致性;校验不强制末尾问号)。"""
34
34
  if text.endswith(FULLWIDTH_QUESTION_MARK):
35
35
  return text[:-1] + "?"
36
36
  return text
@@ -2,13 +2,14 @@
2
2
  tools/ask_user_question/validators.py — QuestionValidator
3
3
 
4
4
  职责:
5
- 封装单题与整卷语义校验(问号、选项数、label 唯一、multiSelect v2 约束等)。
5
+ 封装单题与整卷语义校验(非空题干、选项数、label 唯一等)。
6
6
 
7
7
  链路位置:
8
8
  AskUserQuestionTool.validate_input 委托本类。
9
9
 
10
10
  当前裁剪范围:
11
- 含 preview 长度(dict 路径,与 Pydantic max_length 双保险);HTML 规则在 HtmlPreviewValidator(v3)。
11
+ 题干末尾问号仅为 schema/prompt 软提示(对齐 CC validateInput,无硬拒);
12
+ preview 长度与 Pydantic 双保险;HTML 规则在 HtmlPreviewValidator。
12
13
 
13
14
  """
14
15
 
@@ -32,11 +33,12 @@ class QuestionValidator:
32
33
  code="MISSING_QUESTION_FIELD",
33
34
  )
34
35
  question_text = question["question"]
35
- if not isinstance(question_text, str) or not question_text.endswith("?"):
36
+ # CC: Zod describe 建议 end with '?';validateInput 不硬拒末尾字符。
37
+ if not isinstance(question_text, str) or not question_text.strip():
36
38
  return ValidationResult(
37
39
  ok=False,
38
- message=f"Question at index {index}: '{question_text}' must end with '?'",
39
- code="QUESTION_NO_QUESTION_MARK",
40
+ message=f"Question at index {index}: 'question' must be a non-empty string",
41
+ code="EMPTY_QUESTION_TEXT",
40
42
  )
41
43
 
42
44
  header = question.get("header", "")
@@ -1,21 +1,31 @@
1
1
  """
2
- tools/bash/command_allow_rules.py — /commit 等 command 级 Bash 白名单匹配。
2
+ tools/bash/command_allow_rules.py — Bash 内容级 allow 匹配(slash command + cliArg registry)。
3
3
 
4
4
  职责:
5
- 将 CC 风格 Bash(git add:*) 规则用于 check_permissions 早放行。
5
+ CC 风格 Bash(git:*) / Bash(python:*) 前缀规则;slash 白名单与 registry cliArg 内容 allow。
6
6
 
7
7
  链路位置:
8
- BashRuntimeTool.check_permissions → try_command_allow_decision。
8
+ BashRuntimeTool._evaluate_bash_l1_chain / _authorize_command_segment。
9
9
  """
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
13
  import fnmatch
14
+ from typing import TYPE_CHECKING
14
15
 
15
16
  from langchain_agentx.tool_runtime.models import AuthorizationDecision, ToolExecutionContext
16
17
  from langchain_agentx.tool_runtime.permission_context import get_command_bash_allow_rules
18
+ from langchain_agentx.tool_runtime.permission_rule_matcher import RegistryPermissionMatcher
19
+ from langchain_agentx.tool_runtime.permission_rules import (
20
+ PermissionRuleBehavior,
21
+ PermissionRuleRegistry,
22
+ build_permission_rule_registry,
23
+ )
17
24
  from langchain_agentx.tools.bash.read_only_validation import BashReadOnlyClassifier
18
25
 
26
+ if TYPE_CHECKING:
27
+ from langchain_agentx.tool_runtime.policy import DefaultPolicyEngine
28
+
19
29
 
20
30
  def bash_command_matches_allow_rule(command: str, rule_content: str) -> bool:
21
31
  """CC prefix:* 与简单 fnmatch 子集。"""
@@ -55,6 +65,69 @@ def try_command_allow_decision(
55
65
  return None
56
66
 
57
67
 
68
+ def cliarg_bash_has_content_allow_without_bare_tool(
69
+ registry: PermissionRuleRegistry,
70
+ ) -> bool:
71
+ """cliArg 仅配置了 Bash(pattern) 而无裸 Bash(CC-CLI-B-05 / B-08 边界)。"""
72
+ has_content = False
73
+ has_bare = False
74
+ for rule in registry.iter_rules(PermissionRuleBehavior.ALLOW):
75
+ if rule.source_name != "cliArg" or rule.tool_name != "Bash":
76
+ continue
77
+ if rule.rule_content is None:
78
+ has_bare = True
79
+ else:
80
+ has_content = True
81
+ return has_content and not has_bare
82
+
83
+
84
+ def try_registry_bash_content_allow_decision(
85
+ command: str,
86
+ ctx: ToolExecutionContext,
87
+ policy_engine: DefaultPolicyEngine | None,
88
+ ) -> AuthorizationDecision | None:
89
+ """cliArg/settings 等内容级 Bash allow(CC bashPermissions prefix allow,path 通过后)。"""
90
+ if policy_engine is None:
91
+ return None
92
+ registry = build_permission_rule_registry(
93
+ policy_config=policy_engine._config,
94
+ session_store=ctx.session_store,
95
+ workspace_root=ctx.workspace_root,
96
+ agent_home_segment=ctx.agent_home_segment,
97
+ )
98
+ matcher = RegistryPermissionMatcher(registry)
99
+ return matcher.match_allow("Bash", command.strip())
100
+
101
+
102
+ def promote_unmatched_cliarg_content_only_allow_to_ask(
103
+ decision: AuthorizationDecision,
104
+ *,
105
+ command: str,
106
+ ctx: ToolExecutionContext,
107
+ policy_engine: DefaultPolicyEngine | None,
108
+ ) -> AuthorizationDecision:
109
+ """roots 内 silent allow → ask(仅 cliArg 有 Bash(pattern) 且无裸 Bash 时)。"""
110
+ if policy_engine is None or decision.behavior != "allow" or decision.policy_id:
111
+ return decision
112
+ registry = build_permission_rule_registry(
113
+ policy_config=policy_engine._config,
114
+ session_store=ctx.session_store,
115
+ workspace_root=ctx.workspace_root,
116
+ agent_home_segment=ctx.agent_home_segment,
117
+ )
118
+ if not cliarg_bash_has_content_allow_without_bare_tool(registry):
119
+ return decision
120
+ return AuthorizationDecision(
121
+ behavior="ask",
122
+ message=decision.message,
123
+ policy_id="write_roots",
124
+ ask_prompt=(
125
+ decision.ask_prompt
126
+ or f"Allow Bash command in workspace?\n{command.strip()}"
127
+ ),
128
+ )
129
+
130
+
58
131
  _READ_ONLY_CLASSIFIER = BashReadOnlyClassifier()
59
132
 
60
133
 
@@ -42,7 +42,12 @@ from .limits import get_bash_limits
42
42
  from .mode_validation import BashPermissionModeValidator
43
43
  from .models import BashToolInput, BashToolOutput
44
44
  from .boundary_check import authorize_bash_boundary
45
- from .command_allow_rules import try_command_allow_decision, try_command_restrict_decision
45
+ from .command_allow_rules import (
46
+ promote_unmatched_cliarg_content_only_allow_to_ask,
47
+ try_command_allow_decision,
48
+ try_command_restrict_decision,
49
+ try_registry_bash_content_allow_decision,
50
+ )
46
51
  from .events import (
47
52
  BashProgressAccumulator,
48
53
  bash_progress_emit_data,
@@ -552,8 +557,15 @@ class BashRuntimeTool(RuntimeTool):
552
557
  (item[1].updated_input for item in decisions if item[1].updated_input),
553
558
  None,
554
559
  )
560
+ _, primary_allow = decisions[0]
555
561
  return self._with_observability(
556
- AuthorizationDecision(behavior="allow", updated_input=updated_input),
562
+ AuthorizationDecision(
563
+ behavior="allow",
564
+ message=primary_allow.message,
565
+ policy_id=primary_allow.policy_id,
566
+ updated_input=updated_input or primary_allow.updated_input,
567
+ metadata=dict(primary_allow.metadata or {}),
568
+ ),
557
569
  trace=trace,
558
570
  trace_context=TraceContext.from_ctx(ctx),
559
571
  )
@@ -977,6 +989,12 @@ class BashRuntimeTool(RuntimeTool):
977
989
  if path_decision.behavior != "allow":
978
990
  return path_decision
979
991
 
992
+ registry_allow = try_registry_bash_content_allow_decision(
993
+ command, ctx, self._policy
994
+ )
995
+ if registry_allow is not None:
996
+ return registry_allow
997
+
980
998
  hardening_decision = self._security_hardener.check_segment(
981
999
  command=command,
982
1000
  analysis=analysis,
@@ -1015,7 +1033,13 @@ class BashRuntimeTool(RuntimeTool):
1015
1033
  if decision.behavior != "allow":
1016
1034
  return decision
1017
1035
 
1018
- return self._authorize_sed_segment(command=command, ctx=ctx, cwd=cwd) or decision
1036
+ final = self._authorize_sed_segment(command=command, ctx=ctx, cwd=cwd) or decision
1037
+ return promote_unmatched_cliarg_content_only_allow_to_ask(
1038
+ final,
1039
+ command=command,
1040
+ ctx=ctx,
1041
+ policy_engine=self._policy,
1042
+ )
1019
1043
 
1020
1044
  def _authorize_sed_segment(
1021
1045
  self,
@@ -0,0 +1,74 @@
1
+ """
2
+ tools/webfetch/registry_permission.py — WebFetch registry 内容规则(CC domain:hostname)
3
+
4
+ 职责:
5
+ 将 URL 转为 CC ``webFetchToolInputToPermissionRuleContent`` 键;在 L1 按 deny→ask→allow 评估。
6
+
7
+ 链路位置:
8
+ WebFetchRuntimeTool.run_l1_permission_check(预批准 host 之后)。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING, Any
14
+ from urllib.parse import urlparse
15
+
16
+ from langchain_agentx.tool_runtime.models import AuthorizationDecision
17
+ from langchain_agentx.tool_runtime.permission_decision import (
18
+ attach_decision_reason,
19
+ make_other_decision_reason,
20
+ )
21
+ from langchain_agentx.tool_runtime.permission_rule_matcher import RegistryPermissionMatcher
22
+ from langchain_agentx.tool_runtime.permission_rules import build_permission_rule_registry
23
+
24
+ if TYPE_CHECKING:
25
+ from langchain_agentx.tool_runtime.models import ToolExecutionContext
26
+
27
+
28
+ def webfetch_permission_rule_content(url: str) -> str:
29
+ """对齐 CC WebFetchTool.ts ``domain:${hostname}``。"""
30
+ text = url.strip()
31
+ if not text:
32
+ return "input:"
33
+ try:
34
+ host = (urlparse(text).hostname or "").lower()
35
+ if host:
36
+ return f"domain:{host}"
37
+ except Exception:
38
+ pass
39
+ return f"input:{text}"
40
+
41
+
42
+ def webfetch_rule_content_matches(*, target: str, rule_content: str) -> bool:
43
+ """WebFetch 内容规则:target 与 rule 均为 ``domain:host`` 或 ``input:…``。"""
44
+ return target.strip().lower() == rule_content.strip().lower()
45
+
46
+
47
+ def evaluate_webfetch_registry_permission(
48
+ url: str,
49
+ ctx: ToolExecutionContext,
50
+ policy_engine: Any,
51
+ ) -> AuthorizationDecision:
52
+ registry = build_permission_rule_registry(
53
+ policy_config=policy_engine._config,
54
+ session_store=ctx.session_store,
55
+ workspace_root=ctx.workspace_root,
56
+ agent_home_segment=ctx.agent_home_segment,
57
+ )
58
+ target = webfetch_permission_rule_content(url)
59
+ matcher = RegistryPermissionMatcher(registry)
60
+ for match_fn in (matcher.match_deny, matcher.match_ask, matcher.match_allow):
61
+ matched = match_fn("WebFetch", target)
62
+ if matched is not None:
63
+ return matched
64
+ decision = AuthorizationDecision(
65
+ behavior="ask",
66
+ ask_prompt=(
67
+ "Claude requested permissions to use WebFetch, but you haven't granted it yet."
68
+ ),
69
+ policy_id="webfetch_default",
70
+ )
71
+ return attach_decision_reason(
72
+ decision,
73
+ make_other_decision_reason("WebFetch default permission prompt"),
74
+ )
@@ -123,7 +123,18 @@ class WebFetchRuntimeTool(RuntimeTool):
123
123
  outcome="allow",
124
124
  decision=AuthorizationDecision(behavior="allow"),
125
125
  )
126
- return L1PermissionResult(outcome="continue")
126
+ if self._policy is None:
127
+ from langchain_agentx.tool_runtime.policy import DefaultPolicyEngine, ToolPolicyConfig
128
+
129
+ engine = DefaultPolicyEngine(ToolPolicyConfig())
130
+ else:
131
+ engine = self._policy
132
+ from langchain_agentx.tools.webfetch.registry_permission import (
133
+ evaluate_webfetch_registry_permission,
134
+ )
135
+
136
+ decision = evaluate_webfetch_registry_permission(url, ctx, engine)
137
+ return L1PermissionResult(outcome=decision.behavior, decision=decision)
127
138
 
128
139
  def invoke(self, data: dict[str, Any], ctx: ToolExecutionContext) -> WebFetchToolOutput:
129
140
  parsed = _as_input(data)
@@ -155,7 +155,23 @@ class WebSearchRuntimeTool(RuntimeTool):
155
155
  outcome="deny",
156
156
  decision=AuthorizationDecision(behavior="deny", message=deny_reason),
157
157
  )
158
- return L1PermissionResult(outcome="continue")
158
+ from langchain_agentx.tool_runtime.permission_decision import (
159
+ attach_decision_reason,
160
+ make_other_decision_reason,
161
+ )
162
+
163
+ decision = AuthorizationDecision(
164
+ behavior="ask",
165
+ ask_prompt="WebSearch requires permission.",
166
+ policy_id="websearch_default",
167
+ )
168
+ return L1PermissionResult(
169
+ outcome="ask",
170
+ decision=attach_decision_reason(
171
+ decision,
172
+ make_other_decision_reason("WebSearch passthrough promoted to ask"),
173
+ ),
174
+ )
159
175
 
160
176
  def invoke(self, data: dict[str, Any], ctx: ToolExecutionContext) -> WebSearchToolOutput:
161
177
  parsed = WebSearchToolInput.model_validate(data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langchain-agentx-python
3
- Version: 2.2.7
3
+ Version: 2.2.9
4
4
  Summary: LangChain/LangGraph-based agent utilities for CodeBaseX.
5
5
  Author-email: GoodMood2008 <GoodMood2008@users.noreply.github.com>
6
6
  License: Apache License
@@ -1,4 +1,4 @@
1
- langchain_agentx/__init__.py,sha256=u163iBLScsjUuLfV2YYL3tProAOl3aRQEK1lyYmWoJA,1614
1
+ langchain_agentx/__init__.py,sha256=sLs2vKbNQCEmhW-uC3NdfDAK1zHUjcNzPxy1y2UPlqg,1614
2
2
  langchain_agentx/command/__init__.py,sha256=Ej260S5uFQcmEtGZQG_NH7BYjHoStrpBLtGUsBTxyZk,631
3
3
  langchain_agentx/command/allowed_tools.py,sha256=TVA0VM8rm98H-QbLK_GRiX5RhEAZFxKawliMi4kk96A,3359
4
4
  langchain_agentx/command/context.py,sha256=DIGOGPBw5UGDBm0WNNJlKx1h_9rCRmcdROv4DT61Qug,731
@@ -389,7 +389,7 @@ langchain_agentx/task_runtime/tasks/trace_cleanup/__init__.py,sha256=riWlkXNpJb3
389
389
  langchain_agentx/task_runtime/tasks/trace_cleanup/bootstrap.py,sha256=xYVabnY0Qe4AnKKXhLIL10h22ZOwtaxRGdQ9ew0T9zo,3399
390
390
  langchain_agentx/task_runtime/tasks/trace_cleanup/executor.py,sha256=qez3WnzBTagYZMc0qA1mk-jvM2dMy7uCrwZ7aWhEJzw,2094
391
391
  langchain_agentx/task_runtime/tasks/trace_cleanup/scheduler.py,sha256=lqWlCqucs26fhr4M9LfgY8I0xuNDxwSLkre6XjAJDhA,6877
392
- langchain_agentx/tool_runtime/__init__.py,sha256=C4ZrS6UWSL65ErfNWMvBuZBnht6ZuGb0PBXwA0Xke74,5144
392
+ langchain_agentx/tool_runtime/__init__.py,sha256=bl_PIAbVXuaPgM-qH4z0mh7vgKBSttoxb-XMxsshles,5297
393
393
  langchain_agentx/tool_runtime/accept_edits_fast_path.py,sha256=UMCOF9Ws7hx3pcYvL-Kq7vzIAGvOGdEfx69ccUDXT-k,2281
394
394
  langchain_agentx/tool_runtime/adapter.py,sha256=rM-Y2zSZ--N-TIvx1HtI1J7ralPU0w4jJuX3bBvIVxM,33847
395
395
  langchain_agentx/tool_runtime/agent_home_bypass.py,sha256=na9xQPgJ02qA3YiJ1F8jN0Uv4uBDPzqRk3lfPPE089g,8010
@@ -404,7 +404,7 @@ langchain_agentx/tool_runtime/denial_tracking_bridge.py,sha256=qssnZ_tzCIrDeJvtx
404
404
  langchain_agentx/tool_runtime/errors.py,sha256=Ed1FMjBdPfr0WG-YwLIm_7jokl9k_DjHFNiewZTeQfg,8883
405
405
  langchain_agentx/tool_runtime/failure_event_emitter.py,sha256=C7c4Gc7Vo_R6_ah2Xxa2eso7l9Iuz_KTgRV0j0B08rA,10467
406
406
  langchain_agentx/tool_runtime/identical_call_cache.py,sha256=v8WWuTHWR5ThujcMO4Ty28mZS_w-JN24267fZ4PHWK8,3865
407
- langchain_agentx/tool_runtime/inner_permission_chain.py,sha256=k0uf-Tvr4NitX9xsSvGki2iuwBn0IZZ0t_rQf49CbnE,17075
407
+ langchain_agentx/tool_runtime/inner_permission_chain.py,sha256=SSr45DuwiuKxoyb3r7DdrRu7YUFzKIWBCmTYhBKFO68,17594
408
408
  langchain_agentx/tool_runtime/limits.py,sha256=imE1IetmyKQM82INR0VkP3qZNM4bjUJuf1CG5BkY0uw,356
409
409
  langchain_agentx/tool_runtime/loader.py,sha256=ZeT-xIB6TDkehqyYWx1gkvI0aKx9_xKDKJhqyIvlAvw,9397
410
410
  langchain_agentx/tool_runtime/loop_loader.py,sha256=C_4T9XxlDmKaTzGv4MO6mJuR-rd6uueb1ZWwEKvvFaE,9087
@@ -414,11 +414,12 @@ langchain_agentx/tool_runtime/models.py,sha256=cO9EgLKPXiktHl_xXtBwLQrCo6mj4tbT4
414
414
  langchain_agentx/tool_runtime/outer_permission_resolver.py,sha256=8uKS148DXxt5SfDIHmbRwee4FchKdlquUZbfWeyNRXw,4184
415
415
  langchain_agentx/tool_runtime/override.py,sha256=D8TRftoeWzqlTuWCGUxNkFY7XzWx_d5oxI_ltNibqPA,10258
416
416
  langchain_agentx/tool_runtime/path_safety.py,sha256=dUrl2SObKmRDwdm9k8veO_Eh0iXw0LPLAMZJGxeKnTE,8548
417
+ langchain_agentx/tool_runtime/permission_cli_rules.py,sha256=fz8cW-jXH3f92EYfO0ShIaBSC0k2Lb6GtLJwCpLiaPk,4799
417
418
  langchain_agentx/tool_runtime/permission_context.py,sha256=ReVV2kjVWAPTsPaYXxRB-dICqualEvi2zzwF8NX_Wkk,6229
418
419
  langchain_agentx/tool_runtime/permission_decision.py,sha256=-dWuC60MV6PYgHHOsSxr6Qad8Mm3qgMuh6nkzM87LO4,10280
419
420
  langchain_agentx/tool_runtime/permission_orchestrator.py,sha256=nwx76k74aAFgo63tfkoi6zUJTj9-10tnQzj03VYqFuw,4637
420
- langchain_agentx/tool_runtime/permission_rule_matcher.py,sha256=n3k3EKC4cd_2RMxprKP4NR0DR5iAig4YtsdRwt7wHws,4384
421
- langchain_agentx/tool_runtime/permission_rules.py,sha256=ev6-Hm3N8YtbJO3SLGpoGMUlKGNE6ocvz6lQBhP0waY,18575
421
+ langchain_agentx/tool_runtime/permission_rule_matcher.py,sha256=z4VesjyvvEry-L2U1VsrJB3SR6wtaG8OOuhvftsvj9U,4725
422
+ langchain_agentx/tool_runtime/permission_rules.py,sha256=Aw4qTckMCpQDZpGeAHkRS4fON3ixWU9B5B9W6D2OlMg,19435
422
423
  langchain_agentx/tool_runtime/permission_settings_loader.py,sha256=Xu6dso0ghh4pb7Puf8nj20nXxjGeQvL4kIGycavwG_o,5624
423
424
  langchain_agentx/tool_runtime/permission_settings_sync.py,sha256=g6TvjWwa7D0kw5rDznPsoMv7xylESPOyWA2vZW4HDm0,4590
424
425
  langchain_agentx/tool_runtime/permission_snapshot.py,sha256=xFoi9fIcZjSw_zrIoyglneM-vJsDOLCEjiVoazr5Ack,1947
@@ -520,12 +521,12 @@ langchain_agentx/tools/agent/registry/__init__.py,sha256=Ts5_OJzTnbiqZDxXqMI1UnF
520
521
  langchain_agentx/tools/agent/registry/config.py,sha256=4bcI0Gq1Kqclc3y6YsDhVSkc8tNTkslQAPqy5ZSgF-Y,1628
521
522
  langchain_agentx/tools/agent/registry/registry.py,sha256=knii6XWFZvPkcqzTK3Y4PX5evGcDmto4SsZ7VzUsPSI,1544
522
523
  langchain_agentx/tools/ask_user_question/__init__.py,sha256=bDHVPc4dHBw___7rbaTxClsbDpQ-jLt5Obj8R2VBLAc,444
523
- langchain_agentx/tools/ask_user_question/constants.py,sha256=vK5QraZBBVj7woW1TF08G-MecrooCa-gl9RPH9CvHKE,1229
524
+ langchain_agentx/tools/ask_user_question/constants.py,sha256=rqlL31BTSNY2spn8f2i-MtKjy3y3QoLFAeqenFmYe6Q,1283
524
525
  langchain_agentx/tools/ask_user_question/html_preview.py,sha256=OwNpsH9qm8iJ-TTbzkMFvG49T4-oNJXJXH4OZ8r55wc,3913
525
526
  langchain_agentx/tools/ask_user_question/models.py,sha256=uLc2UK_v8iMFEL1Zk0mUOBYdcg2HfbaLdH68o8kr7GE,3376
526
527
  langchain_agentx/tools/ask_user_question/prompt.py,sha256=0RdVuUeNzQBzPeVrLcbiBqh1uizFgUANvaKOZpVy_qg,2803
527
528
  langchain_agentx/tools/ask_user_question/tool.py,sha256=DgU7OBAqrJBb77ewQ76okoUFJxTThAiN6PRMPa8_FvU,13467
528
- langchain_agentx/tools/ask_user_question/validators.py,sha256=gUYuuKJNrExWGvhFlmhqqSU8ax7s4EhXMvTEjapHeiY,5974
529
+ langchain_agentx/tools/ask_user_question/validators.py,sha256=Uc88cBzqUp0PNG_Jf2yX2WtKP8hzBsnkMb_KUXh9C4M,6088
529
530
  langchain_agentx/tools/bash/__init__.py,sha256=molPHFDylE2wWG_aQx7fb0ciAx3wWpdXzIixTZV5ne4,214
530
531
  langchain_agentx/tools/bash/ast_security.py,sha256=6DFd_TwT986dbGkKT3UnDIZioBdmi8mbJcYSI-XEtdM,22444
531
532
  langchain_agentx/tools/bash/auto_mode_adapter.py,sha256=7-yb_kGmpytBYkD2Hn5YWPIceo_YN4PatEKj8O7HKsM,4243
@@ -533,7 +534,7 @@ langchain_agentx/tools/bash/backend.py,sha256=E0DMBAENZLUSLvuEh6RubNJ6iCVnx1d2VG
533
534
  langchain_agentx/tools/bash/bash_hardening.py,sha256=KEYKRC7kS_to4mzXuChvCmOIIb4dSUkOb9K1MBDtWDc,29296
534
535
  langchain_agentx/tools/bash/bash_runtime_contract.py,sha256=s8vrvMl7405KLGw2hPFbzlzWohm5JX3c9cImMBgqh8I,1511
535
536
  langchain_agentx/tools/bash/boundary_check.py,sha256=k9Bv-baIAUDE3-c_Brw3VSXXYMv_g3lTM0mabIadnwc,8822
536
- langchain_agentx/tools/bash/command_allow_rules.py,sha256=T8TZeGjb6f7C1KAdkzRG01gjyNpNiRlm65bx0y0Asag,3275
537
+ langchain_agentx/tools/bash/command_allow_rules.py,sha256=-1m-yzTCEBHnomeliejUs-CR9yBr9SqtczKoLZIZKsc,5950
537
538
  langchain_agentx/tools/bash/cwd_reporter.py,sha256=mASbKWLb4MiX4zLKY3w1ocIdfhmwj5KRgX4OW_PcAns,2642
538
539
  langchain_agentx/tools/bash/events.py,sha256=xDpPA3zhne2cnlYTmiiPTsV423-h-Nw-VKDR_rlnbGE,6065
539
540
  langchain_agentx/tools/bash/l1_decision_reason.py,sha256=MOL9HBBpJEgypuTiYdNojQBl8eIsTwRge5CtGeYRJKI,5365
@@ -557,7 +558,7 @@ langchain_agentx/tools/bash/semantics.py,sha256=pkf4URTulAOVL7m04Rq9JJF7z0J2mTEs
557
558
  langchain_agentx/tools/bash/shell_locator.py,sha256=AUnCZS8gmZurCs8rL4y0MMOS9UZimfg5rQ5At38mZKU,6296
558
559
  langchain_agentx/tools/bash/shell_quoting.py,sha256=K4JLVGwU8VpdQzfQB1o3xsq4HoCKHew1pry8hHChcoA,4498
559
560
  langchain_agentx/tools/bash/task_runtime.py,sha256=hikVC9YugtvpLf4nDaPlX_GFem1YQvDRwN0ShRflUYU,3568
560
- langchain_agentx/tools/bash/tool.py,sha256=SsLLQdWNUAvXAQD6p_QwtVbwpAx3eGrzG6d6hUcCHZU,45618
561
+ langchain_agentx/tools/bash/tool.py,sha256=xhuFynhBg4Vcar3WjLoJptlYPoqRnkFiV_lQRi0l6T0,46387
561
562
  langchain_agentx/tools/bash/windows_shell_quoting.py,sha256=0RzLnTKyFrJ536QzqPZvSyUcQWN794XRlAq2RdHeKHo,1615
562
563
  langchain_agentx/tools/edit/__init__.py,sha256=PiiOHUODUzTRQxxPdE7ZMTjd3vfdzm9LEcn1lpVtg8o,203
563
564
  langchain_agentx/tools/edit/backend.py,sha256=JrikYafZDGMQ2JjXRxbnv1VSHVGRUdLmZN__nnqkM9o,3159
@@ -649,8 +650,9 @@ langchain_agentx/tools/webfetch/loader.py,sha256=QCATTqBvQHXiMVuAbGQGQeymVevDfWw
649
650
  langchain_agentx/tools/webfetch/models.py,sha256=e_k3FS_9JLekKsq1auRioLDhUFz4YNUlX3MrvnW5BE0,2561
650
651
  langchain_agentx/tools/webfetch/preapproved.py,sha256=8SjbOZdZla208oml8pzixkA-vvZ9c0kofCQ-3XQCoeQ,4312
651
652
  langchain_agentx/tools/webfetch/prompt.py,sha256=lpiAV9ftjs-M1BS6R5_L_uNAcOPJ5bJ9H-ZE2eq_5OY,3771
653
+ langchain_agentx/tools/webfetch/registry_permission.py,sha256=5jSg_c2TuAqNGMA1_C_pSNRufN2fuOaqTOEcitjaQf8,2535
652
654
  langchain_agentx/tools/webfetch/summary.py,sha256=wk-5C39VoEIj9OyZtw6DOs6GYtdvPVECb80uGgVwI7w,1038
653
- langchain_agentx/tools/webfetch/tool.py,sha256=DYHuXuf1ebpEjh6Vd-8mAbWeBLjpGDXgM3lGk1ES78U,8720
655
+ langchain_agentx/tools/webfetch/tool.py,sha256=4qT0nH2DwC8iPEWzmnZjns8lnynXS7muc-2xCgVlbZY,9199
654
656
  langchain_agentx/tools/websearch/__init__.py,sha256=gOVwLv3X9iLUpnN5lhyLeEiovw-JDmzdqxGg7ufkIMY,950
655
657
  langchain_agentx/tools/websearch/backend.py,sha256=AIv9vh1rwFLlYmSEl-NRTM_LiMHRu5sMiMpE5CksWrg,8871
656
658
  langchain_agentx/tools/websearch/events.py,sha256=iQY_vEdCS1mCYfiB7jFMrGNUbpnVI9gJoA6jaXvNxeA,810
@@ -658,7 +660,7 @@ langchain_agentx/tools/websearch/limits.py,sha256=trZxxFAXHUv2JstYxMIJb7_hIWYkXm
658
660
  langchain_agentx/tools/websearch/loader.py,sha256=Y9FX3BPGe_oBSpqfzDke7XN4QqRjjVSVprnsSd0K_SI,1489
659
661
  langchain_agentx/tools/websearch/models.py,sha256=YkVhxM2CXj2RAn4KtOO_dgxZuvo4viJ0AUdAy06-4hM,4167
660
662
  langchain_agentx/tools/websearch/prompt.py,sha256=Sy2zlAO233WvmpOkJuCrkTCpS6oZIkCS_i2CmhBb_js,3188
661
- langchain_agentx/tools/websearch/tool.py,sha256=ZrVdsAvkle8OuqSEeY1QkIac1yDBolZ6wuVEL1WWFhQ,8878
663
+ langchain_agentx/tools/websearch/tool.py,sha256=KQW_awFJqcSVV-oltjb4QTQGjLqC8RnTCZtgbVXCIZ0,9406
662
664
  langchain_agentx/tools/write/__init__.py,sha256=lsmRlcXRgiN3boyfp3QjqRdhHarl2xJkC_XjN5AvEFk,171
663
665
  langchain_agentx/tools/write/backend.py,sha256=qZ86LKIunfIYLsfVZrwvBiJsjmvJCbNA_PB4ZLUuHFM,2298
664
666
  langchain_agentx/tools/write/display_builder.py,sha256=3pRUuLyOoHzY9iqHMeg8yTVCby5AAFz2Zk3NMOttEbM,3932
@@ -761,8 +763,8 @@ langchain_agentx/workspace/root_grant_manager.py,sha256=W3gy8HTevbERbXqIgRcjzOXO
761
763
  langchain_agentx/workspace/tool_boundary.py,sha256=UDwX6swpSLsx9HNkYuuRRiV5o7ZZG_Bru2Yn0c5kNX8,5724
762
764
  langchain_agentx/workspace/validators.py,sha256=tQt-6TOcL8Fw7Ig5ebA9S7vGWh1rby920eFW6x8Tk9E,1439
763
765
  langchain_agentx/workspace/view.py,sha256=PGasqTaqhlD03SXXazHuw4RHfV681AIdlsYqL70pEjc,4774
764
- langchain_agentx_python-2.2.7.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
765
- langchain_agentx_python-2.2.7.dist-info/METADATA,sha256=hqm9pkU7FcAv244Gd1gPQyI1-A81Xf3uWQmi9YZq-Wg,24250
766
- langchain_agentx_python-2.2.7.dist-info/WHEEL,sha256=51RkbunBAw4BWsgaQWTpPhg4Diwp3c9P5iaLk67Hdtg,92
767
- langchain_agentx_python-2.2.7.dist-info/top_level.txt,sha256=Ge284pniNt8xea0OLk2o9o32GqVpDhOYk20fwE-0xxA,17
768
- langchain_agentx_python-2.2.7.dist-info/RECORD,,
766
+ langchain_agentx_python-2.2.9.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
767
+ langchain_agentx_python-2.2.9.dist-info/METADATA,sha256=MWx8nrJ8lXDQorwBf7tximEwOMScMgBTbNOqshUd2v0,24250
768
+ langchain_agentx_python-2.2.9.dist-info/WHEEL,sha256=51RkbunBAw4BWsgaQWTpPhg4Diwp3c9P5iaLk67Hdtg,92
769
+ langchain_agentx_python-2.2.9.dist-info/top_level.txt,sha256=Ge284pniNt8xea0OLk2o9o32GqVpDhOYk20fwE-0xxA,17
770
+ langchain_agentx_python-2.2.9.dist-info/RECORD,,