langchain-agentx-python 2.2.9__py3-none-any.whl → 2.3.0__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.
- langchain_agentx/__init__.py +1 -1
- langchain_agentx/tool_runtime/inner_permission_chain.py +7 -6
- langchain_agentx/tool_runtime/permission_rules.py +9 -0
- langchain_agentx/tools/bash/bash_registry_content.py +106 -0
- langchain_agentx/tools/bash/command_allow_rules.py +54 -13
- langchain_agentx/tools/bash/shell_rule_matching.py +143 -0
- langchain_agentx/tools/bash/tool.py +11 -1
- langchain_agentx/tools/skill/registry_permission.py +99 -0
- langchain_agentx/tools/skill/tool.py +18 -0
- langchain_agentx/tools/webfetch/registry_permission.py +37 -2
- {langchain_agentx_python-2.2.9.dist-info → langchain_agentx_python-2.3.0.dist-info}/METADATA +1 -1
- {langchain_agentx_python-2.2.9.dist-info → langchain_agentx_python-2.3.0.dist-info}/RECORD +15 -12
- {langchain_agentx_python-2.2.9.dist-info → langchain_agentx_python-2.3.0.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.2.9.dist-info → langchain_agentx_python-2.3.0.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.2.9.dist-info → langchain_agentx_python-2.3.0.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -209,12 +209,13 @@ class InnerPermissionChain:
|
|
|
209
209
|
command = data.get("command")
|
|
210
210
|
if not isinstance(command, str) or not command.strip():
|
|
211
211
|
return None
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
from langchain_agentx.tools.bash.bash_registry_content import (
|
|
213
|
+
evaluate_bash_inner_content_rules,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
content_decision = evaluate_bash_inner_content_rules(command, registry)
|
|
217
|
+
if content_decision is not None:
|
|
218
|
+
return content_decision
|
|
218
219
|
return None
|
|
219
220
|
|
|
220
221
|
path = self._engine._extract_path(data)
|
|
@@ -127,6 +127,15 @@ def _content_rule_matches_target(
|
|
|
127
127
|
target=target,
|
|
128
128
|
rule_content=rule_content,
|
|
129
129
|
)
|
|
130
|
+
if tool_name == "Skill":
|
|
131
|
+
from langchain_agentx.tools.skill.registry_permission import (
|
|
132
|
+
skill_rule_content_matches,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return skill_rule_content_matches(
|
|
136
|
+
skill_name=target,
|
|
137
|
+
rule_content=rule_content,
|
|
138
|
+
)
|
|
130
139
|
return fnmatch.fnmatch(target, rule_content)
|
|
131
140
|
|
|
132
141
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/bash_registry_content.py — Bash cliArg 内容 deny/ask(compound 段级 · Phase D)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
Inner 段 1 对 Bash 按 AST permission_segments 评估内容级 deny/ask(对齐 CC compound 子命令优先 deny)。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
InnerPermissionChain._segment_1_2_global(Bash 分支)。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from langchain_agentx.tool_runtime.models import AuthorizationDecision
|
|
14
|
+
from langchain_agentx.tool_runtime.permission_rule_matcher import (
|
|
15
|
+
RegistryPermissionMatcher,
|
|
16
|
+
_decision_from_legacy_glob,
|
|
17
|
+
_decision_from_rule,
|
|
18
|
+
)
|
|
19
|
+
from langchain_agentx.tool_runtime.permission_rules import (
|
|
20
|
+
PermissionRuleBehavior,
|
|
21
|
+
PermissionRuleRegistry,
|
|
22
|
+
)
|
|
23
|
+
from langchain_agentx.tools.bash.shell_rule_matching import match_shell_permission_rule
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def bash_permission_segments(command: str) -> list[str]:
|
|
27
|
+
from langchain_agentx.tools.bash.ast_security import BashAstAnalyzer
|
|
28
|
+
|
|
29
|
+
analysis = BashAstAnalyzer().analyze(command)
|
|
30
|
+
segments = [s.strip() for s in analysis.permission_segments if s and s.strip()]
|
|
31
|
+
return segments or [command.strip()]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _segment_content_decision(
|
|
35
|
+
segment: str,
|
|
36
|
+
registry: PermissionRuleRegistry,
|
|
37
|
+
behavior: PermissionRuleBehavior,
|
|
38
|
+
) -> AuthorizationDecision | None:
|
|
39
|
+
matcher = RegistryPermissionMatcher(registry)
|
|
40
|
+
if behavior is PermissionRuleBehavior.DENY:
|
|
41
|
+
direct = matcher.match_deny("Bash", segment)
|
|
42
|
+
else:
|
|
43
|
+
direct = matcher.match_ask("Bash", segment)
|
|
44
|
+
if direct is not None:
|
|
45
|
+
return direct
|
|
46
|
+
|
|
47
|
+
legacy = registry.find_legacy_glob_match(segment, behavior)
|
|
48
|
+
if legacy is not None:
|
|
49
|
+
return _decision_from_legacy_glob(legacy, behavior=behavior.value)
|
|
50
|
+
|
|
51
|
+
for rule in registry.iter_rules(behavior):
|
|
52
|
+
if rule.tool_name != "Bash" or not rule.rule_content:
|
|
53
|
+
continue
|
|
54
|
+
if match_shell_permission_rule(rule.rule_content, segment, match_mode="prefix"):
|
|
55
|
+
return _decision_from_rule(rule, behavior=behavior.value)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def evaluate_bash_inner_content_rules(
|
|
60
|
+
command: str,
|
|
61
|
+
registry: PermissionRuleRegistry,
|
|
62
|
+
) -> AuthorizationDecision | None:
|
|
63
|
+
"""CC compound:段级 deny 优先 → full deny/ask → 段级 ask。"""
|
|
64
|
+
cmd = command.strip()
|
|
65
|
+
if not cmd:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
matcher = RegistryPermissionMatcher(registry)
|
|
69
|
+
full_deny = matcher.match_deny("Bash", cmd)
|
|
70
|
+
if full_deny is not None:
|
|
71
|
+
return full_deny
|
|
72
|
+
|
|
73
|
+
segments = bash_permission_segments(cmd)
|
|
74
|
+
if len(segments) > 1:
|
|
75
|
+
for segment in segments:
|
|
76
|
+
seg_deny = _segment_content_decision(
|
|
77
|
+
segment, registry, PermissionRuleBehavior.DENY
|
|
78
|
+
)
|
|
79
|
+
if seg_deny is not None:
|
|
80
|
+
return seg_deny
|
|
81
|
+
|
|
82
|
+
full_ask = matcher.match_ask("Bash", cmd)
|
|
83
|
+
if full_ask is not None:
|
|
84
|
+
return full_ask
|
|
85
|
+
|
|
86
|
+
if len(segments) > 1:
|
|
87
|
+
first_ask: AuthorizationDecision | None = None
|
|
88
|
+
for segment in segments:
|
|
89
|
+
seg_ask = _segment_content_decision(
|
|
90
|
+
segment, registry, PermissionRuleBehavior.ASK
|
|
91
|
+
)
|
|
92
|
+
if seg_ask is not None and first_ask is None:
|
|
93
|
+
first_ask = seg_ask
|
|
94
|
+
if first_ask is not None:
|
|
95
|
+
return first_ask
|
|
96
|
+
|
|
97
|
+
if len(segments) == 1:
|
|
98
|
+
return _segment_content_decision(
|
|
99
|
+
segments[0], registry, PermissionRuleBehavior.DENY
|
|
100
|
+
) or _segment_content_decision(
|
|
101
|
+
segments[0], registry, PermissionRuleBehavior.ASK
|
|
102
|
+
)
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = ["bash_permission_segments", "evaluate_bash_inner_content_rules"]
|
|
@@ -10,9 +10,10 @@ tools/bash/command_allow_rules.py — Bash 内容级 allow 匹配(slash comman
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
import fnmatch
|
|
14
13
|
from typing import TYPE_CHECKING
|
|
15
14
|
|
|
15
|
+
from langchain_agentx.tools.bash.shell_rule_matching import match_shell_permission_rule
|
|
16
|
+
|
|
16
17
|
from langchain_agentx.tool_runtime.models import AuthorizationDecision, ToolExecutionContext
|
|
17
18
|
from langchain_agentx.tool_runtime.permission_context import get_command_bash_allow_rules
|
|
18
19
|
from langchain_agentx.tool_runtime.permission_rule_matcher import RegistryPermissionMatcher
|
|
@@ -28,22 +29,17 @@ if TYPE_CHECKING:
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def bash_command_matches_allow_rule(command: str, rule_content: str) -> bool:
|
|
31
|
-
"""CC prefix
|
|
32
|
+
"""CC shellRuleMatching(prefix / wildcard / exact)。"""
|
|
32
33
|
cmd = command.strip()
|
|
33
34
|
pattern = rule_content.strip()
|
|
34
35
|
if not cmd or not pattern:
|
|
35
36
|
return False
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if "*" in pattern or "?" in pattern or "[" in pattern:
|
|
44
|
-
return fnmatch.fnmatch(cmd, pattern)
|
|
45
|
-
|
|
46
|
-
return cmd == pattern or cmd.startswith(f"{pattern} ")
|
|
37
|
+
return match_shell_permission_rule(
|
|
38
|
+
pattern,
|
|
39
|
+
cmd,
|
|
40
|
+
match_mode="prefix",
|
|
41
|
+
is_compound=False,
|
|
42
|
+
)
|
|
47
43
|
|
|
48
44
|
|
|
49
45
|
def try_command_allow_decision(
|
|
@@ -99,6 +95,51 @@ def try_registry_bash_content_allow_decision(
|
|
|
99
95
|
return matcher.match_allow("Bash", command.strip())
|
|
100
96
|
|
|
101
97
|
|
|
98
|
+
def registry_has_cliarg_bash_allow(registry: PermissionRuleRegistry) -> bool:
|
|
99
|
+
for rule in registry.iter_rules(PermissionRuleBehavior.ALLOW):
|
|
100
|
+
if rule.source_name == "cliArg" and rule.tool_name == "Bash":
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def promote_unscoped_bash_passthrough_to_ask(
|
|
106
|
+
decision: AuthorizationDecision,
|
|
107
|
+
*,
|
|
108
|
+
command: str,
|
|
109
|
+
ctx: ToolExecutionContext,
|
|
110
|
+
policy_engine: DefaultPolicyEngine | None,
|
|
111
|
+
) -> AuthorizationDecision:
|
|
112
|
+
"""无 cliArg Bash allow 时,write_roots 静默 allow → ask(对齐 CC passthrough)。"""
|
|
113
|
+
if policy_engine is None or decision.behavior != "allow":
|
|
114
|
+
return decision
|
|
115
|
+
if _READ_ONLY_CLASSIFIER.classify(
|
|
116
|
+
command.strip(),
|
|
117
|
+
shell_ops_already_resolved=True,
|
|
118
|
+
).is_read_only:
|
|
119
|
+
return decision
|
|
120
|
+
if decision.policy_id != "write_roots":
|
|
121
|
+
return decision
|
|
122
|
+
registry = build_permission_rule_registry(
|
|
123
|
+
policy_config=policy_engine._config,
|
|
124
|
+
session_store=ctx.session_store,
|
|
125
|
+
workspace_root=ctx.workspace_root,
|
|
126
|
+
agent_home_segment=ctx.agent_home_segment,
|
|
127
|
+
)
|
|
128
|
+
if registry_has_cliarg_bash_allow(registry):
|
|
129
|
+
return decision
|
|
130
|
+
if cliarg_bash_has_content_allow_without_bare_tool(registry):
|
|
131
|
+
return decision
|
|
132
|
+
return AuthorizationDecision(
|
|
133
|
+
behavior="ask",
|
|
134
|
+
message=decision.message,
|
|
135
|
+
policy_id="bash_passthrough",
|
|
136
|
+
ask_prompt=(
|
|
137
|
+
decision.ask_prompt
|
|
138
|
+
or f"Allow Bash command?\n{command.strip()}"
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
102
143
|
def promote_unmatched_cliarg_content_only_allow_to_ask(
|
|
103
144
|
decision: AuthorizationDecision,
|
|
104
145
|
*,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/shell_rule_matching.py — CC shellRuleMatching.ts 对齐(Phase D)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
prefix :*、wildcard(含 \\* / \\\\ 转义)、``git *`` 尾 wildcard 可选参数语义。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
command_allow_rules.bash_command_matches_permission_rule。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
_ESCAPED_STAR = "\x00ESCAPED_STAR\x00"
|
|
18
|
+
_ESCAPED_BACKSLASH = "\x00ESCAPED_BACKSLASH\x00"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def permission_rule_extract_prefix(permission_rule: str) -> str | None:
|
|
22
|
+
m = re.match(r"^(.+):\*$", permission_rule.strip())
|
|
23
|
+
return m.group(1) if m else None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def has_wildcards(pattern: str) -> bool:
|
|
27
|
+
text = pattern.strip()
|
|
28
|
+
if text.endswith(":*"):
|
|
29
|
+
return False
|
|
30
|
+
i = 0
|
|
31
|
+
while i < len(text):
|
|
32
|
+
if text[i] == "*":
|
|
33
|
+
backslashes = 0
|
|
34
|
+
j = i - 1
|
|
35
|
+
while j >= 0 and text[j] == "\\":
|
|
36
|
+
backslashes += 1
|
|
37
|
+
j -= 1
|
|
38
|
+
if backslashes % 2 == 0:
|
|
39
|
+
return True
|
|
40
|
+
i += 1
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def match_wildcard_pattern(
|
|
45
|
+
pattern: str,
|
|
46
|
+
command: str,
|
|
47
|
+
*,
|
|
48
|
+
case_insensitive: bool = False,
|
|
49
|
+
) -> bool:
|
|
50
|
+
"""对齐 CC ``matchWildcardPattern``(含 trailing `` *`` → optional args)。"""
|
|
51
|
+
trimmed = pattern.strip()
|
|
52
|
+
processed = ""
|
|
53
|
+
i = 0
|
|
54
|
+
while i < len(trimmed):
|
|
55
|
+
ch = trimmed[i]
|
|
56
|
+
if ch == "\\" and i + 1 < len(trimmed):
|
|
57
|
+
nxt = trimmed[i + 1]
|
|
58
|
+
if nxt == "*":
|
|
59
|
+
processed += _ESCAPED_STAR
|
|
60
|
+
i += 2
|
|
61
|
+
continue
|
|
62
|
+
if nxt == "\\":
|
|
63
|
+
processed += _ESCAPED_BACKSLASH
|
|
64
|
+
i += 2
|
|
65
|
+
continue
|
|
66
|
+
processed += ch
|
|
67
|
+
i += 1
|
|
68
|
+
|
|
69
|
+
escaped = re.sub(r"[.+?^${}()|\[\]\\'\"]", r"\\\g<0>", processed)
|
|
70
|
+
with_wildcards = escaped.replace("*", ".*")
|
|
71
|
+
regex_body = with_wildcards.replace(_ESCAPED_STAR, r"\*").replace(
|
|
72
|
+
_ESCAPED_BACKSLASH, r"\\"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
unescaped_stars = processed.count("*")
|
|
76
|
+
if regex_body.endswith(" .*") and unescaped_stars == 1:
|
|
77
|
+
regex_body = regex_body[:-3] + "( .*)?"
|
|
78
|
+
|
|
79
|
+
flags = re.DOTALL
|
|
80
|
+
if case_insensitive:
|
|
81
|
+
flags |= re.IGNORECASE
|
|
82
|
+
return re.fullmatch(f"^{regex_body}$", command, flags=flags) is not None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class ShellPermissionRule:
|
|
87
|
+
kind: Literal["exact", "prefix", "wildcard"]
|
|
88
|
+
value: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_permission_rule(rule_content: str) -> ShellPermissionRule:
|
|
92
|
+
text = rule_content.strip()
|
|
93
|
+
prefix = permission_rule_extract_prefix(text)
|
|
94
|
+
if prefix is not None:
|
|
95
|
+
return ShellPermissionRule(kind="prefix", value=prefix)
|
|
96
|
+
if has_wildcards(text):
|
|
97
|
+
return ShellPermissionRule(kind="wildcard", value=text)
|
|
98
|
+
return ShellPermissionRule(kind="exact", value=text)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def match_shell_permission_rule(
|
|
102
|
+
rule_content: str,
|
|
103
|
+
command: str,
|
|
104
|
+
*,
|
|
105
|
+
match_mode: Literal["exact", "prefix"] = "prefix",
|
|
106
|
+
is_compound: bool = False,
|
|
107
|
+
) -> bool:
|
|
108
|
+
"""单条 Bash 内容规则对单段 command 的匹配(CC ``bashPermissionRule`` 分支)。"""
|
|
109
|
+
cmd = command.strip()
|
|
110
|
+
if not cmd:
|
|
111
|
+
return False
|
|
112
|
+
parsed = parse_permission_rule(rule_content)
|
|
113
|
+
|
|
114
|
+
if parsed.kind == "exact":
|
|
115
|
+
return parsed.value == cmd
|
|
116
|
+
|
|
117
|
+
if parsed.kind == "prefix":
|
|
118
|
+
if match_mode == "exact":
|
|
119
|
+
return parsed.value == cmd
|
|
120
|
+
if is_compound:
|
|
121
|
+
return False
|
|
122
|
+
prefix = parsed.value
|
|
123
|
+
if cmd == prefix:
|
|
124
|
+
return True
|
|
125
|
+
if cmd.startswith(prefix + " "):
|
|
126
|
+
return True
|
|
127
|
+
xargs_prefix = "xargs " + prefix
|
|
128
|
+
if cmd == xargs_prefix or cmd.startswith(xargs_prefix + " "):
|
|
129
|
+
return True
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
if match_mode == "exact":
|
|
133
|
+
return False
|
|
134
|
+
if is_compound:
|
|
135
|
+
return False
|
|
136
|
+
return match_wildcard_pattern(parsed.value, cmd)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = [
|
|
140
|
+
"match_shell_permission_rule",
|
|
141
|
+
"match_wildcard_pattern",
|
|
142
|
+
"parse_permission_rule",
|
|
143
|
+
]
|
|
@@ -1034,7 +1034,17 @@ class BashRuntimeTool(RuntimeTool):
|
|
|
1034
1034
|
return decision
|
|
1035
1035
|
|
|
1036
1036
|
final = self._authorize_sed_segment(command=command, ctx=ctx, cwd=cwd) or decision
|
|
1037
|
-
|
|
1037
|
+
final = promote_unmatched_cliarg_content_only_allow_to_ask(
|
|
1038
|
+
final,
|
|
1039
|
+
command=command,
|
|
1040
|
+
ctx=ctx,
|
|
1041
|
+
policy_engine=self._policy,
|
|
1042
|
+
)
|
|
1043
|
+
from langchain_agentx.tools.bash.command_allow_rules import (
|
|
1044
|
+
promote_unscoped_bash_passthrough_to_ask,
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
return promote_unscoped_bash_passthrough_to_ask(
|
|
1038
1048
|
final,
|
|
1039
1049
|
command=command,
|
|
1040
1050
|
ctx=ctx,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/skill/registry_permission.py — Skill 内容级 deny/allow(CC SkillTool.checkPermissions · Phase D)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
Skill(name) / Skill(prefix:*) 与 registry 规则匹配。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
SkillRuntimeTool.run_l1_permission_check(frontmatter 约束之前)。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from langchain_agentx.tool_runtime.models import AuthorizationDecision
|
|
16
|
+
from langchain_agentx.tool_runtime.permission_decision import (
|
|
17
|
+
attach_decision_reason,
|
|
18
|
+
make_other_decision_reason,
|
|
19
|
+
)
|
|
20
|
+
from langchain_agentx.tool_runtime.permission_rule_matcher import RegistryPermissionMatcher
|
|
21
|
+
from langchain_agentx.tool_runtime.permission_rules import build_permission_rule_registry
|
|
22
|
+
from langchain_agentx.tools.bash.shell_rule_matching import match_shell_permission_rule
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from langchain_agentx.tool_runtime.models import ToolExecutionContext
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def skill_permission_rule_content(skill_name: str) -> str:
|
|
29
|
+
text = skill_name.strip()
|
|
30
|
+
if text.startswith("/"):
|
|
31
|
+
text = text[1:]
|
|
32
|
+
return text
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def skill_rule_content_matches(*, skill_name: str, rule_content: str) -> bool:
|
|
36
|
+
"""对齐 CC ``ruleMatches``:精确或 ``prefix:*``。"""
|
|
37
|
+
command_name = skill_permission_rule_content(skill_name)
|
|
38
|
+
normalized_rule = rule_content.strip()
|
|
39
|
+
if normalized_rule.startswith("/"):
|
|
40
|
+
normalized_rule = normalized_rule[1:]
|
|
41
|
+
if normalized_rule == command_name:
|
|
42
|
+
return True
|
|
43
|
+
if normalized_rule.endswith(":*"):
|
|
44
|
+
prefix = normalized_rule[:-2]
|
|
45
|
+
return command_name.startswith(prefix)
|
|
46
|
+
return match_shell_permission_rule(
|
|
47
|
+
normalized_rule, command_name, match_mode="prefix"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def evaluate_skill_registry_permission(
|
|
52
|
+
skill_name: str,
|
|
53
|
+
ctx: ToolExecutionContext,
|
|
54
|
+
policy_engine: Any,
|
|
55
|
+
) -> AuthorizationDecision | None:
|
|
56
|
+
if policy_engine is None:
|
|
57
|
+
return None
|
|
58
|
+
registry = build_permission_rule_registry(
|
|
59
|
+
policy_config=policy_engine._config,
|
|
60
|
+
session_store=ctx.session_store,
|
|
61
|
+
workspace_root=ctx.workspace_root,
|
|
62
|
+
agent_home_segment=ctx.agent_home_segment,
|
|
63
|
+
)
|
|
64
|
+
target = skill_permission_rule_content(skill_name)
|
|
65
|
+
matcher = RegistryPermissionMatcher(registry)
|
|
66
|
+
for match_fn in (matcher.match_deny, matcher.match_ask, matcher.match_allow):
|
|
67
|
+
matched = match_fn("Skill", target)
|
|
68
|
+
if matched is not None:
|
|
69
|
+
return matched
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def skill_default_ask_decision(skill_name: str) -> AuthorizationDecision:
|
|
74
|
+
target = skill_permission_rule_content(skill_name)
|
|
75
|
+
suggestions = (
|
|
76
|
+
{
|
|
77
|
+
"type": "addRules",
|
|
78
|
+
"destination": "localSettings",
|
|
79
|
+
"behavior": "allow",
|
|
80
|
+
"rules": [{"toolName": "Skill", "ruleContent": target}],
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
decision = AuthorizationDecision(
|
|
84
|
+
behavior="ask",
|
|
85
|
+
ask_prompt=f"Allow skill {target}?",
|
|
86
|
+
policy_id="skill_default",
|
|
87
|
+
metadata={"suggestions": suggestions},
|
|
88
|
+
)
|
|
89
|
+
return attach_decision_reason(
|
|
90
|
+
decision,
|
|
91
|
+
make_other_decision_reason("Skill default permission prompt"),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
__all__ = [
|
|
96
|
+
"evaluate_skill_registry_permission",
|
|
97
|
+
"skill_default_ask_decision",
|
|
98
|
+
"skill_rule_content_matches",
|
|
99
|
+
]
|
|
@@ -187,6 +187,24 @@ class SkillRuntimeTool(RuntimeTool):
|
|
|
187
187
|
),
|
|
188
188
|
)
|
|
189
189
|
|
|
190
|
+
from langchain_agentx.tools.skill.registry_permission import (
|
|
191
|
+
evaluate_skill_registry_permission,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
registry_decision = evaluate_skill_registry_permission(
|
|
195
|
+
inp.skill_name,
|
|
196
|
+
ctx,
|
|
197
|
+
self._policy,
|
|
198
|
+
)
|
|
199
|
+
if registry_decision is not None:
|
|
200
|
+
if registry_decision.behavior == "allow":
|
|
201
|
+
pass
|
|
202
|
+
else:
|
|
203
|
+
return L1PermissionResult(
|
|
204
|
+
outcome=registry_decision.behavior,
|
|
205
|
+
decision=registry_decision,
|
|
206
|
+
)
|
|
207
|
+
|
|
190
208
|
local = self._constraint_evaluator.evaluate(cmd.frontmatter)
|
|
191
209
|
if local.behavior != "allow":
|
|
192
210
|
return L1PermissionResult(outcome=local.behavior, decision=local)
|
|
@@ -40,8 +40,32 @@ def webfetch_permission_rule_content(url: str) -> str:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def webfetch_rule_content_matches(*, target: str, rule_content: str) -> bool:
|
|
43
|
-
"""WebFetch
|
|
44
|
-
|
|
43
|
+
"""WebFetch 内容规则:``domain:host`` 精确或 ``domain:*.example.com`` wildcard(Phase D)。"""
|
|
44
|
+
t = target.strip().lower()
|
|
45
|
+
r = rule_content.strip().lower()
|
|
46
|
+
if t == r:
|
|
47
|
+
return True
|
|
48
|
+
if not r.startswith("domain:") or not t.startswith("domain:"):
|
|
49
|
+
return False
|
|
50
|
+
host_pattern = r[len("domain:") :]
|
|
51
|
+
host_target = t[len("domain:") :]
|
|
52
|
+
if "*" not in host_pattern and "?" not in host_pattern:
|
|
53
|
+
return False
|
|
54
|
+
from langchain_agentx.tools.bash.shell_rule_matching import match_wildcard_pattern
|
|
55
|
+
|
|
56
|
+
return match_wildcard_pattern(host_pattern, host_target)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def webfetch_allow_suggestions(rule_content: str) -> tuple[dict[str, object], ...]:
|
|
60
|
+
"""对齐 CC WebFetchTool ``buildSuggestions``(L3 ``metadata.suggestions``)。"""
|
|
61
|
+
return (
|
|
62
|
+
{
|
|
63
|
+
"type": "addRules",
|
|
64
|
+
"destination": "localSettings",
|
|
65
|
+
"behavior": "allow",
|
|
66
|
+
"rules": [{"toolName": "WebFetch", "ruleContent": rule_content}],
|
|
67
|
+
},
|
|
68
|
+
)
|
|
45
69
|
|
|
46
70
|
|
|
47
71
|
def evaluate_webfetch_registry_permission(
|
|
@@ -60,6 +84,16 @@ def evaluate_webfetch_registry_permission(
|
|
|
60
84
|
for match_fn in (matcher.match_deny, matcher.match_ask, matcher.match_allow):
|
|
61
85
|
matched = match_fn("WebFetch", target)
|
|
62
86
|
if matched is not None:
|
|
87
|
+
if matched.behavior == "ask":
|
|
88
|
+
meta = dict(matched.metadata or {})
|
|
89
|
+
meta.setdefault("suggestions", webfetch_allow_suggestions(target))
|
|
90
|
+
return AuthorizationDecision(
|
|
91
|
+
behavior=matched.behavior,
|
|
92
|
+
message=matched.message,
|
|
93
|
+
policy_id=matched.policy_id,
|
|
94
|
+
ask_prompt=matched.ask_prompt,
|
|
95
|
+
metadata=meta,
|
|
96
|
+
)
|
|
63
97
|
return matched
|
|
64
98
|
decision = AuthorizationDecision(
|
|
65
99
|
behavior="ask",
|
|
@@ -67,6 +101,7 @@ def evaluate_webfetch_registry_permission(
|
|
|
67
101
|
"Claude requested permissions to use WebFetch, but you haven't granted it yet."
|
|
68
102
|
),
|
|
69
103
|
policy_id="webfetch_default",
|
|
104
|
+
metadata={"suggestions": webfetch_allow_suggestions(target)},
|
|
70
105
|
)
|
|
71
106
|
return attach_decision_reason(
|
|
72
107
|
decision,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
langchain_agentx/__init__.py,sha256=
|
|
1
|
+
langchain_agentx/__init__.py,sha256=8oCd_P48WoQZB9jQROU6rc2POQ3qA6K1LHWG1xtaSPo,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
|
|
@@ -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=
|
|
407
|
+
langchain_agentx/tool_runtime/inner_permission_chain.py,sha256=50wYXRu7Hi-rvDiI2QxSLC4uBAsJwG3Vp-v3AutLY4E,17629
|
|
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
|
|
@@ -419,7 +419,7 @@ langchain_agentx/tool_runtime/permission_context.py,sha256=ReVV2kjVWAPTsPaYXxRB-
|
|
|
419
419
|
langchain_agentx/tool_runtime/permission_decision.py,sha256=-dWuC60MV6PYgHHOsSxr6Qad8Mm3qgMuh6nkzM87LO4,10280
|
|
420
420
|
langchain_agentx/tool_runtime/permission_orchestrator.py,sha256=nwx76k74aAFgo63tfkoi6zUJTj9-10tnQzj03VYqFuw,4637
|
|
421
421
|
langchain_agentx/tool_runtime/permission_rule_matcher.py,sha256=z4VesjyvvEry-L2U1VsrJB3SR6wtaG8OOuhvftsvj9U,4725
|
|
422
|
-
langchain_agentx/tool_runtime/permission_rules.py,sha256=
|
|
422
|
+
langchain_agentx/tool_runtime/permission_rules.py,sha256=2N66xdR9khrzRY_1OCKsv05oAwWyf45e546NUwC_7hg,19709
|
|
423
423
|
langchain_agentx/tool_runtime/permission_settings_loader.py,sha256=Xu6dso0ghh4pb7Puf8nj20nXxjGeQvL4kIGycavwG_o,5624
|
|
424
424
|
langchain_agentx/tool_runtime/permission_settings_sync.py,sha256=g6TvjWwa7D0kw5rDznPsoMv7xylESPOyWA2vZW4HDm0,4590
|
|
425
425
|
langchain_agentx/tool_runtime/permission_snapshot.py,sha256=xFoi9fIcZjSw_zrIoyglneM-vJsDOLCEjiVoazr5Ack,1947
|
|
@@ -532,9 +532,10 @@ langchain_agentx/tools/bash/ast_security.py,sha256=6DFd_TwT986dbGkKT3UnDIZioBdmi
|
|
|
532
532
|
langchain_agentx/tools/bash/auto_mode_adapter.py,sha256=7-yb_kGmpytBYkD2Hn5YWPIceo_YN4PatEKj8O7HKsM,4243
|
|
533
533
|
langchain_agentx/tools/bash/backend.py,sha256=E0DMBAENZLUSLvuEh6RubNJ6iCVnx1d2VGsQzVuLA5k,65361
|
|
534
534
|
langchain_agentx/tools/bash/bash_hardening.py,sha256=KEYKRC7kS_to4mzXuChvCmOIIb4dSUkOb9K1MBDtWDc,29296
|
|
535
|
+
langchain_agentx/tools/bash/bash_registry_content.py,sha256=YHE3PwuLBeFrEux4xNaDZjJpYtUSMFPLYr6A7g-fFxk,3569
|
|
535
536
|
langchain_agentx/tools/bash/bash_runtime_contract.py,sha256=s8vrvMl7405KLGw2hPFbzlzWohm5JX3c9cImMBgqh8I,1511
|
|
536
537
|
langchain_agentx/tools/bash/boundary_check.py,sha256=k9Bv-baIAUDE3-c_Brw3VSXXYMv_g3lTM0mabIadnwc,8822
|
|
537
|
-
langchain_agentx/tools/bash/command_allow_rules.py,sha256
|
|
538
|
+
langchain_agentx/tools/bash/command_allow_rules.py,sha256=RYW1M9QpJa2HuvOeU9dnmoiNhmExKA1Sgv5jPWDd_Ag,7357
|
|
538
539
|
langchain_agentx/tools/bash/cwd_reporter.py,sha256=mASbKWLb4MiX4zLKY3w1ocIdfhmwj5KRgX4OW_PcAns,2642
|
|
539
540
|
langchain_agentx/tools/bash/events.py,sha256=xDpPA3zhne2cnlYTmiiPTsV423-h-Nw-VKDR_rlnbGE,6065
|
|
540
541
|
langchain_agentx/tools/bash/l1_decision_reason.py,sha256=MOL9HBBpJEgypuTiYdNojQBl8eIsTwRge5CtGeYRJKI,5365
|
|
@@ -557,8 +558,9 @@ langchain_agentx/tools/bash/semantic_matrix.py,sha256=stvsudu8mGjS1JJCawMAUKabt-
|
|
|
557
558
|
langchain_agentx/tools/bash/semantics.py,sha256=pkf4URTulAOVL7m04Rq9JJF7z0J2mTEsGWrjyoY9r6o,5769
|
|
558
559
|
langchain_agentx/tools/bash/shell_locator.py,sha256=AUnCZS8gmZurCs8rL4y0MMOS9UZimfg5rQ5At38mZKU,6296
|
|
559
560
|
langchain_agentx/tools/bash/shell_quoting.py,sha256=K4JLVGwU8VpdQzfQB1o3xsq4HoCKHew1pry8hHChcoA,4498
|
|
561
|
+
langchain_agentx/tools/bash/shell_rule_matching.py,sha256=uYEKg2C5M1r0eh3L4b5NCML1Gj2UBdIXeL_u2A6_RyM,3981
|
|
560
562
|
langchain_agentx/tools/bash/task_runtime.py,sha256=hikVC9YugtvpLf4nDaPlX_GFem1YQvDRwN0ShRflUYU,3568
|
|
561
|
-
langchain_agentx/tools/bash/tool.py,sha256=
|
|
563
|
+
langchain_agentx/tools/bash/tool.py,sha256=O8wDLMsPM9ryx_BfKo_SaGtlyRb7UyUxDAmOEgUgIek,46699
|
|
562
564
|
langchain_agentx/tools/bash/windows_shell_quoting.py,sha256=0RzLnTKyFrJ536QzqPZvSyUcQWN794XRlAq2RdHeKHo,1615
|
|
563
565
|
langchain_agentx/tools/edit/__init__.py,sha256=PiiOHUODUzTRQxxPdE7ZMTjd3vfdzm9LEcn1lpVtg8o,203
|
|
564
566
|
langchain_agentx/tools/edit/backend.py,sha256=JrikYafZDGMQ2JjXRxbnv1VSHVGRUdLmZN__nnqkM9o,3159
|
|
@@ -618,9 +620,10 @@ langchain_agentx/tools/skill/paths_matcher.py,sha256=J4SOBThray_72mrYiQUfJ5Icu6T
|
|
|
618
620
|
langchain_agentx/tools/skill/plugin_loader.py,sha256=qT0VcIWkKFmRGGH8-e_f4y1XIpjY71TVbnZwnu2M9_M,1191
|
|
619
621
|
langchain_agentx/tools/skill/policy.py,sha256=SFlW627gyo_-wyIppAqoytBmrLPlSGw-mbR00gjOJQY,1541
|
|
620
622
|
langchain_agentx/tools/skill/prompt.py,sha256=tvSqvCGroVdloveiWIYxIPQmpESqzPAs5ZJztvHBdOI,1782
|
|
623
|
+
langchain_agentx/tools/skill/registry_permission.py,sha256=ns_S6IBOdacrZcgHzCOj9rjlPOIecavF5hW-pHmalcY,3258
|
|
621
624
|
langchain_agentx/tools/skill/repository_factory.py,sha256=7zDoPDZgxoR03CkmKdx9b0aUXwaH8E5LtBYQPGF26rA,1222
|
|
622
625
|
langchain_agentx/tools/skill/source_paths.py,sha256=7qhErxCyDTBiGZc41i_EqMZi6aueCdY4kvMJ03g6z4s,2996
|
|
623
|
-
langchain_agentx/tools/skill/tool.py,sha256=
|
|
626
|
+
langchain_agentx/tools/skill/tool.py,sha256=vhiwjeyST9BhsMZmKk8-Vej3JId0AxTPrQ5ebFvguG4,19711
|
|
624
627
|
langchain_agentx/tools/skill/bundled/__init__.py,sha256=gaCH_mROpv0lIgLjin1VIFu-8hbP2nY1PHoF7vZt2Ns,4516
|
|
625
628
|
langchain_agentx/tools/skill/bundled/prompts.py,sha256=gcaTnNRlWfHNPgO3EfZxU9vMYbOapMrAduvgaVc-_Fc,4838
|
|
626
629
|
langchain_agentx/tools/skill/bundled/registry.py,sha256=oxvvFeeUzM-qVPtayVaQp5tfRiMazyln2-rCM86AYa8,6274
|
|
@@ -650,7 +653,7 @@ langchain_agentx/tools/webfetch/loader.py,sha256=QCATTqBvQHXiMVuAbGQGQeymVevDfWw
|
|
|
650
653
|
langchain_agentx/tools/webfetch/models.py,sha256=e_k3FS_9JLekKsq1auRioLDhUFz4YNUlX3MrvnW5BE0,2561
|
|
651
654
|
langchain_agentx/tools/webfetch/preapproved.py,sha256=8SjbOZdZla208oml8pzixkA-vvZ9c0kofCQ-3XQCoeQ,4312
|
|
652
655
|
langchain_agentx/tools/webfetch/prompt.py,sha256=lpiAV9ftjs-M1BS6R5_L_uNAcOPJ5bJ9H-ZE2eq_5OY,3771
|
|
653
|
-
langchain_agentx/tools/webfetch/registry_permission.py,sha256=
|
|
656
|
+
langchain_agentx/tools/webfetch/registry_permission.py,sha256=lkJx0XhY6XOeYgM3uAsFiFEtWawE5mZMl8IyuQchYSo,3924
|
|
654
657
|
langchain_agentx/tools/webfetch/summary.py,sha256=wk-5C39VoEIj9OyZtw6DOs6GYtdvPVECb80uGgVwI7w,1038
|
|
655
658
|
langchain_agentx/tools/webfetch/tool.py,sha256=4qT0nH2DwC8iPEWzmnZjns8lnynXS7muc-2xCgVlbZY,9199
|
|
656
659
|
langchain_agentx/tools/websearch/__init__.py,sha256=gOVwLv3X9iLUpnN5lhyLeEiovw-JDmzdqxGg7ufkIMY,950
|
|
@@ -763,8 +766,8 @@ langchain_agentx/workspace/root_grant_manager.py,sha256=W3gy8HTevbERbXqIgRcjzOXO
|
|
|
763
766
|
langchain_agentx/workspace/tool_boundary.py,sha256=UDwX6swpSLsx9HNkYuuRRiV5o7ZZG_Bru2Yn0c5kNX8,5724
|
|
764
767
|
langchain_agentx/workspace/validators.py,sha256=tQt-6TOcL8Fw7Ig5ebA9S7vGWh1rby920eFW6x8Tk9E,1439
|
|
765
768
|
langchain_agentx/workspace/view.py,sha256=PGasqTaqhlD03SXXazHuw4RHfV681AIdlsYqL70pEjc,4774
|
|
766
|
-
langchain_agentx_python-2.
|
|
767
|
-
langchain_agentx_python-2.
|
|
768
|
-
langchain_agentx_python-2.
|
|
769
|
-
langchain_agentx_python-2.
|
|
770
|
-
langchain_agentx_python-2.
|
|
769
|
+
langchain_agentx_python-2.3.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
770
|
+
langchain_agentx_python-2.3.0.dist-info/METADATA,sha256=PGoShL7qCuKn4cToEl22oCzy1TD3bu1o7uYqnN9rpOk,24250
|
|
771
|
+
langchain_agentx_python-2.3.0.dist-info/WHEEL,sha256=51RkbunBAw4BWsgaQWTpPhg4Diwp3c9P5iaLk67Hdtg,92
|
|
772
|
+
langchain_agentx_python-2.3.0.dist-info/top_level.txt,sha256=Ge284pniNt8xea0OLk2o9o32GqVpDhOYk20fwE-0xxA,17
|
|
773
|
+
langchain_agentx_python-2.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{langchain_agentx_python-2.2.9.dist-info → langchain_agentx_python-2.3.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|