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.
Files changed (149) hide show
  1. aury/__init__.py +2 -0
  2. aury/agents/__init__.py +55 -0
  3. aury/agents/a2a/__init__.py +168 -0
  4. aury/agents/backends/__init__.py +196 -0
  5. aury/agents/backends/artifact/__init__.py +9 -0
  6. aury/agents/backends/artifact/memory.py +130 -0
  7. aury/agents/backends/artifact/types.py +133 -0
  8. aury/agents/backends/code/__init__.py +65 -0
  9. aury/agents/backends/file/__init__.py +11 -0
  10. aury/agents/backends/file/local.py +66 -0
  11. aury/agents/backends/file/types.py +40 -0
  12. aury/agents/backends/invocation/__init__.py +8 -0
  13. aury/agents/backends/invocation/memory.py +81 -0
  14. aury/agents/backends/invocation/types.py +110 -0
  15. aury/agents/backends/memory/__init__.py +8 -0
  16. aury/agents/backends/memory/memory.py +179 -0
  17. aury/agents/backends/memory/types.py +136 -0
  18. aury/agents/backends/message/__init__.py +9 -0
  19. aury/agents/backends/message/memory.py +122 -0
  20. aury/agents/backends/message/types.py +124 -0
  21. aury/agents/backends/sandbox.py +275 -0
  22. aury/agents/backends/session/__init__.py +8 -0
  23. aury/agents/backends/session/memory.py +93 -0
  24. aury/agents/backends/session/types.py +124 -0
  25. aury/agents/backends/shell/__init__.py +11 -0
  26. aury/agents/backends/shell/local.py +110 -0
  27. aury/agents/backends/shell/types.py +55 -0
  28. aury/agents/backends/shell.py +209 -0
  29. aury/agents/backends/snapshot/__init__.py +19 -0
  30. aury/agents/backends/snapshot/git.py +95 -0
  31. aury/agents/backends/snapshot/hybrid.py +125 -0
  32. aury/agents/backends/snapshot/memory.py +86 -0
  33. aury/agents/backends/snapshot/types.py +59 -0
  34. aury/agents/backends/state/__init__.py +29 -0
  35. aury/agents/backends/state/composite.py +49 -0
  36. aury/agents/backends/state/file.py +57 -0
  37. aury/agents/backends/state/memory.py +52 -0
  38. aury/agents/backends/state/sqlite.py +262 -0
  39. aury/agents/backends/state/types.py +178 -0
  40. aury/agents/backends/subagent/__init__.py +165 -0
  41. aury/agents/cli/__init__.py +41 -0
  42. aury/agents/cli/chat.py +239 -0
  43. aury/agents/cli/config.py +236 -0
  44. aury/agents/cli/extensions.py +460 -0
  45. aury/agents/cli/main.py +189 -0
  46. aury/agents/cli/session.py +337 -0
  47. aury/agents/cli/workflow.py +276 -0
  48. aury/agents/context_providers/__init__.py +66 -0
  49. aury/agents/context_providers/artifact.py +299 -0
  50. aury/agents/context_providers/base.py +177 -0
  51. aury/agents/context_providers/memory.py +70 -0
  52. aury/agents/context_providers/message.py +130 -0
  53. aury/agents/context_providers/skill.py +50 -0
  54. aury/agents/context_providers/subagent.py +46 -0
  55. aury/agents/context_providers/tool.py +68 -0
  56. aury/agents/core/__init__.py +83 -0
  57. aury/agents/core/base.py +573 -0
  58. aury/agents/core/context.py +797 -0
  59. aury/agents/core/context_builder.py +303 -0
  60. aury/agents/core/event_bus/__init__.py +15 -0
  61. aury/agents/core/event_bus/bus.py +203 -0
  62. aury/agents/core/factory.py +169 -0
  63. aury/agents/core/isolator.py +97 -0
  64. aury/agents/core/logging.py +95 -0
  65. aury/agents/core/parallel.py +194 -0
  66. aury/agents/core/runner.py +139 -0
  67. aury/agents/core/services/__init__.py +5 -0
  68. aury/agents/core/services/file_session.py +144 -0
  69. aury/agents/core/services/message.py +53 -0
  70. aury/agents/core/services/session.py +53 -0
  71. aury/agents/core/signals.py +109 -0
  72. aury/agents/core/state.py +363 -0
  73. aury/agents/core/types/__init__.py +107 -0
  74. aury/agents/core/types/action.py +176 -0
  75. aury/agents/core/types/artifact.py +135 -0
  76. aury/agents/core/types/block.py +736 -0
  77. aury/agents/core/types/message.py +350 -0
  78. aury/agents/core/types/recall.py +144 -0
  79. aury/agents/core/types/session.py +257 -0
  80. aury/agents/core/types/subagent.py +154 -0
  81. aury/agents/core/types/tool.py +205 -0
  82. aury/agents/eval/__init__.py +331 -0
  83. aury/agents/hitl/__init__.py +57 -0
  84. aury/agents/hitl/ask_user.py +242 -0
  85. aury/agents/hitl/compaction.py +230 -0
  86. aury/agents/hitl/exceptions.py +87 -0
  87. aury/agents/hitl/permission.py +617 -0
  88. aury/agents/hitl/revert.py +216 -0
  89. aury/agents/llm/__init__.py +31 -0
  90. aury/agents/llm/adapter.py +367 -0
  91. aury/agents/llm/openai.py +294 -0
  92. aury/agents/llm/provider.py +476 -0
  93. aury/agents/mcp/__init__.py +153 -0
  94. aury/agents/memory/__init__.py +46 -0
  95. aury/agents/memory/compaction.py +394 -0
  96. aury/agents/memory/manager.py +465 -0
  97. aury/agents/memory/processor.py +177 -0
  98. aury/agents/memory/store.py +187 -0
  99. aury/agents/memory/types.py +137 -0
  100. aury/agents/messages/__init__.py +40 -0
  101. aury/agents/messages/config.py +47 -0
  102. aury/agents/messages/raw_store.py +224 -0
  103. aury/agents/messages/store.py +118 -0
  104. aury/agents/messages/types.py +88 -0
  105. aury/agents/middleware/__init__.py +31 -0
  106. aury/agents/middleware/base.py +341 -0
  107. aury/agents/middleware/chain.py +342 -0
  108. aury/agents/middleware/message.py +129 -0
  109. aury/agents/middleware/message_container.py +126 -0
  110. aury/agents/middleware/raw_message.py +153 -0
  111. aury/agents/middleware/truncation.py +139 -0
  112. aury/agents/middleware/types.py +81 -0
  113. aury/agents/plugin.py +162 -0
  114. aury/agents/react/__init__.py +4 -0
  115. aury/agents/react/agent.py +1923 -0
  116. aury/agents/sandbox/__init__.py +23 -0
  117. aury/agents/sandbox/local.py +239 -0
  118. aury/agents/sandbox/remote.py +200 -0
  119. aury/agents/sandbox/types.py +115 -0
  120. aury/agents/skill/__init__.py +16 -0
  121. aury/agents/skill/loader.py +180 -0
  122. aury/agents/skill/types.py +83 -0
  123. aury/agents/tool/__init__.py +39 -0
  124. aury/agents/tool/builtin/__init__.py +23 -0
  125. aury/agents/tool/builtin/ask_user.py +155 -0
  126. aury/agents/tool/builtin/bash.py +107 -0
  127. aury/agents/tool/builtin/delegate.py +726 -0
  128. aury/agents/tool/builtin/edit.py +121 -0
  129. aury/agents/tool/builtin/plan.py +277 -0
  130. aury/agents/tool/builtin/read.py +91 -0
  131. aury/agents/tool/builtin/thinking.py +111 -0
  132. aury/agents/tool/builtin/yield_result.py +130 -0
  133. aury/agents/tool/decorator.py +252 -0
  134. aury/agents/tool/set.py +204 -0
  135. aury/agents/usage/__init__.py +12 -0
  136. aury/agents/usage/tracker.py +236 -0
  137. aury/agents/workflow/__init__.py +85 -0
  138. aury/agents/workflow/adapter.py +268 -0
  139. aury/agents/workflow/dag.py +116 -0
  140. aury/agents/workflow/dsl.py +575 -0
  141. aury/agents/workflow/executor.py +659 -0
  142. aury/agents/workflow/expression.py +136 -0
  143. aury/agents/workflow/parser.py +182 -0
  144. aury/agents/workflow/state.py +145 -0
  145. aury/agents/workflow/types.py +86 -0
  146. aury_agent-0.0.4.dist-info/METADATA +90 -0
  147. aury_agent-0.0.4.dist-info/RECORD +149 -0
  148. aury_agent-0.0.4.dist-info/WHEEL +4 -0
  149. 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
+ ]