aury-agent 0.0.4__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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""Registry-based permission system for human-in-the-loop approval.
|
|
2
|
+
|
|
3
|
+
Design:
|
|
4
|
+
1. PermissionChecker - Protocol for permission type handlers
|
|
5
|
+
2. PermissionRegistry - Register checkers by type
|
|
6
|
+
3. Unified rules format: "type:pattern" -> "allow|ask|deny"
|
|
7
|
+
4. Tools declare their permission requirements via PermissionSpec
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import fnmatch
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Literal, Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
from ..core.event_bus import EventBus, Events
|
|
18
|
+
from ..core.types.session import generate_id
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# Exceptions
|
|
23
|
+
# =============================================================================
|
|
24
|
+
|
|
25
|
+
class RejectedError(Exception):
|
|
26
|
+
"""Raised when permission is rejected."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
reason: str,
|
|
31
|
+
session_id: str | None = None,
|
|
32
|
+
permission_id: str | None = None,
|
|
33
|
+
metadata: dict[str, Any] | None = None,
|
|
34
|
+
):
|
|
35
|
+
super().__init__(reason)
|
|
36
|
+
self.reason = reason
|
|
37
|
+
self.session_id = session_id
|
|
38
|
+
self.permission_id = permission_id
|
|
39
|
+
self.metadata = metadata or {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SkippedError(Exception):
|
|
43
|
+
"""Raised when permission is skipped."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# =============================================================================
|
|
48
|
+
# Response Types
|
|
49
|
+
# =============================================================================
|
|
50
|
+
|
|
51
|
+
class HumanResponse(Enum):
|
|
52
|
+
"""Human response options for permission requests."""
|
|
53
|
+
APPROVE_ONCE = "approve_once"
|
|
54
|
+
APPROVE_ALWAYS = "approve_always"
|
|
55
|
+
REJECT = "reject"
|
|
56
|
+
EDIT = "edit"
|
|
57
|
+
SKIP = "skip"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
Action = Literal["allow", "ask", "deny"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# =============================================================================
|
|
64
|
+
# Permission Checker Protocol
|
|
65
|
+
# =============================================================================
|
|
66
|
+
|
|
67
|
+
@runtime_checkable
|
|
68
|
+
class PermissionChecker(Protocol):
|
|
69
|
+
"""Protocol for permission type handlers.
|
|
70
|
+
|
|
71
|
+
Each checker handles one permission type (e.g., "shell", "file", "code").
|
|
72
|
+
Tools declare which checker type they use.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def type(self) -> str:
|
|
77
|
+
"""Permission type identifier (e.g., 'shell', 'file', 'code')."""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def get_pattern(self, args: dict[str, Any]) -> str:
|
|
81
|
+
"""Extract pattern from tool arguments for rule matching.
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
shell: command string
|
|
85
|
+
file: file path
|
|
86
|
+
code: language or "*"
|
|
87
|
+
"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
def get_ask_message(self, args: dict[str, Any]) -> str:
|
|
91
|
+
"""Generate human-readable message for permission request."""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# =============================================================================
|
|
96
|
+
# Built-in Checkers
|
|
97
|
+
# =============================================================================
|
|
98
|
+
|
|
99
|
+
class ShellPermissionChecker:
|
|
100
|
+
"""Permission checker for shell command execution."""
|
|
101
|
+
|
|
102
|
+
type = "shell"
|
|
103
|
+
|
|
104
|
+
def __init__(self, command_arg: str = "command"):
|
|
105
|
+
self.command_arg = command_arg
|
|
106
|
+
|
|
107
|
+
def get_pattern(self, args: dict[str, Any]) -> str:
|
|
108
|
+
return args.get(self.command_arg, "")
|
|
109
|
+
|
|
110
|
+
def get_ask_message(self, args: dict[str, Any]) -> str:
|
|
111
|
+
cmd = self.get_pattern(args)
|
|
112
|
+
return f"Execute shell command: {cmd}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class FilePermissionChecker:
|
|
116
|
+
"""Permission checker for file operations."""
|
|
117
|
+
|
|
118
|
+
type = "file"
|
|
119
|
+
|
|
120
|
+
def __init__(self, path_arg: str = "path", op_arg: str = "operation"):
|
|
121
|
+
self.path_arg = path_arg
|
|
122
|
+
self.op_arg = op_arg
|
|
123
|
+
|
|
124
|
+
def get_pattern(self, args: dict[str, Any]) -> str:
|
|
125
|
+
op = args.get(self.op_arg, "read")
|
|
126
|
+
path = args.get(self.path_arg, "")
|
|
127
|
+
return f"{op}:{path}"
|
|
128
|
+
|
|
129
|
+
def get_ask_message(self, args: dict[str, Any]) -> str:
|
|
130
|
+
op = args.get(self.op_arg, "read")
|
|
131
|
+
path = args.get(self.path_arg, "")
|
|
132
|
+
return f"File {op}: {path}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class CodePermissionChecker:
|
|
136
|
+
"""Permission checker for code execution."""
|
|
137
|
+
|
|
138
|
+
type = "code"
|
|
139
|
+
|
|
140
|
+
def __init__(self, language_arg: str = "language"):
|
|
141
|
+
self.language_arg = language_arg
|
|
142
|
+
|
|
143
|
+
def get_pattern(self, args: dict[str, Any]) -> str:
|
|
144
|
+
return args.get(self.language_arg, "*")
|
|
145
|
+
|
|
146
|
+
def get_ask_message(self, args: dict[str, Any]) -> str:
|
|
147
|
+
lang = args.get(self.language_arg, "unknown")
|
|
148
|
+
code = args.get("code", "")[:100]
|
|
149
|
+
return f"Execute {lang} code: {code}..."
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class GenericPermissionChecker:
|
|
153
|
+
"""Generic permission checker for custom tools."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, type: str, pattern_args: list[str] | None = None):
|
|
156
|
+
self._type = type
|
|
157
|
+
self.pattern_args = pattern_args or []
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def type(self) -> str:
|
|
161
|
+
return self._type
|
|
162
|
+
|
|
163
|
+
def get_pattern(self, args: dict[str, Any]) -> str:
|
|
164
|
+
if not self.pattern_args:
|
|
165
|
+
return "*"
|
|
166
|
+
parts = [str(args.get(k, "")) for k in self.pattern_args]
|
|
167
|
+
return ":".join(parts) if parts else "*"
|
|
168
|
+
|
|
169
|
+
def get_ask_message(self, args: dict[str, Any]) -> str:
|
|
170
|
+
return f"Execute {self._type}: {self.get_pattern(args)}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# =============================================================================
|
|
174
|
+
# Permission Registry
|
|
175
|
+
# =============================================================================
|
|
176
|
+
|
|
177
|
+
class PermissionRegistry:
|
|
178
|
+
"""Registry for permission checkers.
|
|
179
|
+
|
|
180
|
+
Usage:
|
|
181
|
+
# Register checker
|
|
182
|
+
PermissionRegistry.register(ShellPermissionChecker())
|
|
183
|
+
|
|
184
|
+
# Get checker
|
|
185
|
+
checker = PermissionRegistry.get("shell")
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
_checkers: dict[str, PermissionChecker] = {}
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def register(cls, checker: PermissionChecker) -> None:
|
|
192
|
+
"""Register a permission checker."""
|
|
193
|
+
cls._checkers[checker.type] = checker
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def get(cls, type: str) -> PermissionChecker | None:
|
|
197
|
+
"""Get checker by type."""
|
|
198
|
+
return cls._checkers.get(type)
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def get_or_create(cls, type: str, **kwargs) -> PermissionChecker:
|
|
202
|
+
"""Get existing checker or create generic one."""
|
|
203
|
+
if type in cls._checkers:
|
|
204
|
+
return cls._checkers[type]
|
|
205
|
+
return GenericPermissionChecker(type, **kwargs)
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def list_types(cls) -> list[str]:
|
|
209
|
+
"""List all registered types."""
|
|
210
|
+
return list(cls._checkers.keys())
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def clear(cls) -> None:
|
|
214
|
+
"""Clear all registered checkers (for testing)."""
|
|
215
|
+
cls._checkers.clear()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# Register built-in checkers
|
|
219
|
+
PermissionRegistry.register(ShellPermissionChecker())
|
|
220
|
+
PermissionRegistry.register(FilePermissionChecker())
|
|
221
|
+
PermissionRegistry.register(CodePermissionChecker())
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# =============================================================================
|
|
225
|
+
# Permission Spec (for tools to declare requirements)
|
|
226
|
+
# =============================================================================
|
|
227
|
+
|
|
228
|
+
@dataclass
|
|
229
|
+
class PermissionSpec:
|
|
230
|
+
"""Permission specification for tools.
|
|
231
|
+
|
|
232
|
+
Tools declare this to specify their permission requirements.
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
class BashTool(BaseTool):
|
|
236
|
+
permission = PermissionSpec(
|
|
237
|
+
type="shell",
|
|
238
|
+
pattern_args=["command"],
|
|
239
|
+
)
|
|
240
|
+
"""
|
|
241
|
+
type: str
|
|
242
|
+
pattern_args: list[str] = field(default_factory=list)
|
|
243
|
+
custom_checker: PermissionChecker | None = None
|
|
244
|
+
|
|
245
|
+
def get_checker(self) -> PermissionChecker:
|
|
246
|
+
"""Get the checker for this spec."""
|
|
247
|
+
if self.custom_checker:
|
|
248
|
+
return self.custom_checker
|
|
249
|
+
return PermissionRegistry.get_or_create(
|
|
250
|
+
self.type,
|
|
251
|
+
pattern_args=self.pattern_args
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# =============================================================================
|
|
256
|
+
# Rules Configuration
|
|
257
|
+
# =============================================================================
|
|
258
|
+
|
|
259
|
+
@dataclass
|
|
260
|
+
class PermissionRules:
|
|
261
|
+
"""Permission rules configuration.
|
|
262
|
+
|
|
263
|
+
Unified format: "type:pattern" -> "allow|ask|deny"
|
|
264
|
+
|
|
265
|
+
Pattern matching uses fnmatch (shell-style wildcards).
|
|
266
|
+
Rules are evaluated in order, first match wins.
|
|
267
|
+
|
|
268
|
+
Example:
|
|
269
|
+
rules = PermissionRules({
|
|
270
|
+
"shell:rm -rf *": "deny",
|
|
271
|
+
"shell:sudo *": "ask",
|
|
272
|
+
"shell:*": "allow",
|
|
273
|
+
"file:write:/etc/*": "deny",
|
|
274
|
+
"file:*": "allow",
|
|
275
|
+
"code:*": "deny",
|
|
276
|
+
"*:*": "ask", # Default
|
|
277
|
+
})
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
rules: dict[str, Action] = field(default_factory=dict)
|
|
281
|
+
default_action: Action = "ask"
|
|
282
|
+
|
|
283
|
+
def get_action(self, type: str, pattern: str) -> Action:
|
|
284
|
+
"""Get action for type:pattern.
|
|
285
|
+
|
|
286
|
+
Evaluates rules in order, returns first match.
|
|
287
|
+
"""
|
|
288
|
+
full_pattern = f"{type}:{pattern}"
|
|
289
|
+
|
|
290
|
+
for rule_pattern, action in self.rules.items():
|
|
291
|
+
if fnmatch.fnmatch(full_pattern, rule_pattern):
|
|
292
|
+
return action
|
|
293
|
+
|
|
294
|
+
return self.default_action
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def allow_all(cls) -> "PermissionRules":
|
|
298
|
+
"""Create rules that allow everything."""
|
|
299
|
+
return cls(rules={"*:*": "allow"}, default_action="allow")
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def deny_all(cls) -> "PermissionRules":
|
|
303
|
+
"""Create rules that deny everything."""
|
|
304
|
+
return cls(rules={"*:*": "deny"}, default_action="deny")
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def ask_all(cls) -> "PermissionRules":
|
|
308
|
+
"""Create rules that ask for everything."""
|
|
309
|
+
return cls(rules={}, default_action="ask")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# =============================================================================
|
|
313
|
+
# Pending Permission
|
|
314
|
+
# =============================================================================
|
|
315
|
+
|
|
316
|
+
@dataclass
|
|
317
|
+
class PendingPermission:
|
|
318
|
+
"""A pending permission request awaiting human response."""
|
|
319
|
+
id: str
|
|
320
|
+
type: str
|
|
321
|
+
pattern: str
|
|
322
|
+
session_id: str
|
|
323
|
+
invocation_id: str
|
|
324
|
+
block_id: str
|
|
325
|
+
call_id: str | None
|
|
326
|
+
message: str
|
|
327
|
+
metadata: dict[str, Any]
|
|
328
|
+
future: asyncio.Future[dict[str, Any] | None]
|
|
329
|
+
created_at: float = field(default_factory=lambda: asyncio.get_event_loop().time())
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# =============================================================================
|
|
333
|
+
# Permission Manager
|
|
334
|
+
# =============================================================================
|
|
335
|
+
|
|
336
|
+
class Permission:
|
|
337
|
+
"""Permission manager for human-in-the-loop approval.
|
|
338
|
+
|
|
339
|
+
Usage:
|
|
340
|
+
permission = Permission(bus, rules)
|
|
341
|
+
|
|
342
|
+
# Check permission (blocks if needs approval)
|
|
343
|
+
await permission.check(
|
|
344
|
+
type="shell",
|
|
345
|
+
args={"command": "rm -rf /tmp/*"},
|
|
346
|
+
session_id="...",
|
|
347
|
+
...
|
|
348
|
+
)
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
def __init__(
|
|
352
|
+
self,
|
|
353
|
+
bus: EventBus,
|
|
354
|
+
rules: PermissionRules | None = None,
|
|
355
|
+
):
|
|
356
|
+
self.bus = bus
|
|
357
|
+
self.rules = rules or PermissionRules()
|
|
358
|
+
|
|
359
|
+
# Pending requests
|
|
360
|
+
self._pending: dict[str, PendingPermission] = {}
|
|
361
|
+
|
|
362
|
+
# Approved patterns: session_id -> set of "type:pattern"
|
|
363
|
+
self._approved: dict[str, set[str]] = {}
|
|
364
|
+
|
|
365
|
+
self._lock = asyncio.Lock()
|
|
366
|
+
|
|
367
|
+
async def check(
|
|
368
|
+
self,
|
|
369
|
+
type: str,
|
|
370
|
+
args: dict[str, Any],
|
|
371
|
+
session_id: str,
|
|
372
|
+
invocation_id: str,
|
|
373
|
+
block_id: str,
|
|
374
|
+
call_id: str | None = None,
|
|
375
|
+
spec: PermissionSpec | None = None,
|
|
376
|
+
metadata: dict[str, Any] | None = None,
|
|
377
|
+
) -> dict[str, Any] | None:
|
|
378
|
+
"""Check permission and wait for approval if needed.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
type: Permission type (shell, file, code, etc.)
|
|
382
|
+
args: Tool arguments
|
|
383
|
+
session_id: Current session ID
|
|
384
|
+
invocation_id: Current invocation ID
|
|
385
|
+
block_id: Current block ID
|
|
386
|
+
call_id: Tool call ID
|
|
387
|
+
spec: Optional PermissionSpec (uses registry if not provided)
|
|
388
|
+
metadata: Additional context
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Edited args if user chose EDIT, otherwise None
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
RejectedError: If permission was denied
|
|
395
|
+
SkippedError: If user chose to skip
|
|
396
|
+
"""
|
|
397
|
+
metadata = metadata or {}
|
|
398
|
+
|
|
399
|
+
# Get checker
|
|
400
|
+
if spec:
|
|
401
|
+
checker = spec.get_checker()
|
|
402
|
+
else:
|
|
403
|
+
checker = PermissionRegistry.get_or_create(type)
|
|
404
|
+
|
|
405
|
+
# Extract pattern
|
|
406
|
+
pattern = checker.get_pattern(args)
|
|
407
|
+
|
|
408
|
+
# 1. Check rules
|
|
409
|
+
action = self.rules.get_action(type, pattern)
|
|
410
|
+
|
|
411
|
+
if action == "allow":
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
if action == "deny":
|
|
415
|
+
raise RejectedError(
|
|
416
|
+
f"Permission denied by rules: {type}:{pattern}",
|
|
417
|
+
session_id=session_id,
|
|
418
|
+
metadata=metadata,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# 2. Check session approvals
|
|
422
|
+
if self._is_approved(session_id, type, pattern):
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
# 3. Request approval
|
|
426
|
+
message = checker.get_ask_message(args)
|
|
427
|
+
|
|
428
|
+
return await self._request_permission(
|
|
429
|
+
type=type,
|
|
430
|
+
pattern=pattern,
|
|
431
|
+
session_id=session_id,
|
|
432
|
+
invocation_id=invocation_id,
|
|
433
|
+
block_id=block_id,
|
|
434
|
+
call_id=call_id,
|
|
435
|
+
message=message,
|
|
436
|
+
metadata={**metadata, "args": args},
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
async def check_with_spec(
|
|
440
|
+
self,
|
|
441
|
+
spec: PermissionSpec,
|
|
442
|
+
args: dict[str, Any],
|
|
443
|
+
session_id: str,
|
|
444
|
+
invocation_id: str,
|
|
445
|
+
block_id: str,
|
|
446
|
+
call_id: str | None = None,
|
|
447
|
+
metadata: dict[str, Any] | None = None,
|
|
448
|
+
) -> dict[str, Any] | None:
|
|
449
|
+
"""Check permission using a PermissionSpec."""
|
|
450
|
+
return await self.check(
|
|
451
|
+
type=spec.type,
|
|
452
|
+
args=args,
|
|
453
|
+
session_id=session_id,
|
|
454
|
+
invocation_id=invocation_id,
|
|
455
|
+
block_id=block_id,
|
|
456
|
+
call_id=call_id,
|
|
457
|
+
spec=spec,
|
|
458
|
+
metadata=metadata,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
async def _request_permission(
|
|
462
|
+
self,
|
|
463
|
+
type: str,
|
|
464
|
+
pattern: str,
|
|
465
|
+
session_id: str,
|
|
466
|
+
invocation_id: str,
|
|
467
|
+
block_id: str,
|
|
468
|
+
call_id: str | None,
|
|
469
|
+
message: str,
|
|
470
|
+
metadata: dict[str, Any],
|
|
471
|
+
) -> dict[str, Any] | None:
|
|
472
|
+
"""Create and wait for a permission request."""
|
|
473
|
+
permission_id = generate_id("perm")
|
|
474
|
+
future: asyncio.Future[dict[str, Any] | None] = asyncio.Future()
|
|
475
|
+
|
|
476
|
+
pending = PendingPermission(
|
|
477
|
+
id=permission_id,
|
|
478
|
+
type=type,
|
|
479
|
+
pattern=pattern,
|
|
480
|
+
session_id=session_id,
|
|
481
|
+
invocation_id=invocation_id,
|
|
482
|
+
block_id=block_id,
|
|
483
|
+
call_id=call_id,
|
|
484
|
+
message=message,
|
|
485
|
+
metadata=metadata,
|
|
486
|
+
future=future,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
async with self._lock:
|
|
490
|
+
self._pending[permission_id] = pending
|
|
491
|
+
|
|
492
|
+
# Publish event
|
|
493
|
+
await self.bus.publish(Events.PERMISSION_REQUESTED, {
|
|
494
|
+
"permission_id": permission_id,
|
|
495
|
+
"type": type,
|
|
496
|
+
"pattern": pattern,
|
|
497
|
+
"message": message,
|
|
498
|
+
"session_id": session_id,
|
|
499
|
+
"invocation_id": invocation_id,
|
|
500
|
+
"block_id": block_id,
|
|
501
|
+
"call_id": call_id,
|
|
502
|
+
"metadata": metadata,
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
return await future
|
|
507
|
+
finally:
|
|
508
|
+
async with self._lock:
|
|
509
|
+
self._pending.pop(permission_id, None)
|
|
510
|
+
|
|
511
|
+
def respond(
|
|
512
|
+
self,
|
|
513
|
+
permission_id: str,
|
|
514
|
+
response: HumanResponse,
|
|
515
|
+
edited_args: dict[str, Any] | None = None,
|
|
516
|
+
) -> None:
|
|
517
|
+
"""Respond to a permission request."""
|
|
518
|
+
if permission_id not in self._pending:
|
|
519
|
+
raise ValueError(f"Unknown permission request: {permission_id}")
|
|
520
|
+
|
|
521
|
+
pending = self._pending[permission_id]
|
|
522
|
+
|
|
523
|
+
match response:
|
|
524
|
+
case HumanResponse.APPROVE_ONCE:
|
|
525
|
+
pending.future.set_result(None)
|
|
526
|
+
|
|
527
|
+
case HumanResponse.APPROVE_ALWAYS:
|
|
528
|
+
self._add_approved(pending.session_id, pending.type, pending.pattern)
|
|
529
|
+
pending.future.set_result(None)
|
|
530
|
+
|
|
531
|
+
case HumanResponse.REJECT:
|
|
532
|
+
pending.future.set_exception(RejectedError(
|
|
533
|
+
"User rejected permission",
|
|
534
|
+
session_id=pending.session_id,
|
|
535
|
+
permission_id=permission_id,
|
|
536
|
+
metadata=pending.metadata,
|
|
537
|
+
))
|
|
538
|
+
|
|
539
|
+
case HumanResponse.EDIT:
|
|
540
|
+
pending.future.set_result(edited_args)
|
|
541
|
+
|
|
542
|
+
case HumanResponse.SKIP:
|
|
543
|
+
pending.future.set_exception(SkippedError())
|
|
544
|
+
|
|
545
|
+
# Publish resolution
|
|
546
|
+
asyncio.create_task(self.bus.publish(Events.PERMISSION_RESOLVED, {
|
|
547
|
+
"permission_id": permission_id,
|
|
548
|
+
"response": response.value,
|
|
549
|
+
"session_id": pending.session_id,
|
|
550
|
+
}))
|
|
551
|
+
|
|
552
|
+
def _is_approved(self, session_id: str, type: str, pattern: str) -> bool:
|
|
553
|
+
"""Check if type:pattern is already approved."""
|
|
554
|
+
if session_id not in self._approved:
|
|
555
|
+
return False
|
|
556
|
+
|
|
557
|
+
full = f"{type}:{pattern}"
|
|
558
|
+
for approved in self._approved[session_id]:
|
|
559
|
+
if fnmatch.fnmatch(full, approved):
|
|
560
|
+
return True
|
|
561
|
+
|
|
562
|
+
return False
|
|
563
|
+
|
|
564
|
+
def _add_approved(self, session_id: str, type: str, pattern: str) -> None:
|
|
565
|
+
"""Add to approved set."""
|
|
566
|
+
if session_id not in self._approved:
|
|
567
|
+
self._approved[session_id] = set()
|
|
568
|
+
self._approved[session_id].add(f"{type}:{pattern}")
|
|
569
|
+
|
|
570
|
+
def clear_session(self, session_id: str) -> None:
|
|
571
|
+
"""Clear all approvals for a session."""
|
|
572
|
+
self._approved.pop(session_id, None)
|
|
573
|
+
|
|
574
|
+
def get_pending(self, session_id: str | None = None) -> list[PendingPermission]:
|
|
575
|
+
"""Get pending permission requests."""
|
|
576
|
+
if session_id:
|
|
577
|
+
return [p for p in self._pending.values() if p.session_id == session_id]
|
|
578
|
+
return list(self._pending.values())
|
|
579
|
+
|
|
580
|
+
def cancel_all(self, session_id: str | None = None) -> int:
|
|
581
|
+
"""Cancel all pending permissions."""
|
|
582
|
+
cancelled = 0
|
|
583
|
+
for pending in self.get_pending(session_id):
|
|
584
|
+
if not pending.future.done():
|
|
585
|
+
pending.future.set_exception(SkippedError())
|
|
586
|
+
cancelled += 1
|
|
587
|
+
return cancelled
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# =============================================================================
|
|
591
|
+
# Exports
|
|
592
|
+
# =============================================================================
|
|
593
|
+
|
|
594
|
+
__all__ = [
|
|
595
|
+
# Exceptions
|
|
596
|
+
"RejectedError",
|
|
597
|
+
"SkippedError",
|
|
598
|
+
# Types
|
|
599
|
+
"HumanResponse",
|
|
600
|
+
"Action",
|
|
601
|
+
# Checker protocol
|
|
602
|
+
"PermissionChecker",
|
|
603
|
+
# Built-in checkers
|
|
604
|
+
"ShellPermissionChecker",
|
|
605
|
+
"FilePermissionChecker",
|
|
606
|
+
"CodePermissionChecker",
|
|
607
|
+
"GenericPermissionChecker",
|
|
608
|
+
# Registry
|
|
609
|
+
"PermissionRegistry",
|
|
610
|
+
# Spec
|
|
611
|
+
"PermissionSpec",
|
|
612
|
+
# Rules
|
|
613
|
+
"PermissionRules",
|
|
614
|
+
# Manager
|
|
615
|
+
"PendingPermission",
|
|
616
|
+
"Permission",
|
|
617
|
+
]
|