superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,488 @@
1
+ """
2
+ Enhanced Permission System - Rule-Based Access Control.
3
+
4
+ Provides granular control over what operations agents can perform:
5
+ - File access patterns with wildcards
6
+ - Tool-specific permissions
7
+ - Directory-scoped rules
8
+ - Allow/Deny/Ask actions
9
+ - Optimized for SuperQode's multi-agent QE workflow
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import fnmatch
15
+ import json
16
+ import re
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime
19
+ from enum import Enum
20
+ from pathlib import Path
21
+ from typing import Any, Dict, List, Optional, Set, Tuple
22
+ import hashlib
23
+
24
+
25
+ class PermissionAction(Enum):
26
+ """Action to take when permission rule matches."""
27
+
28
+ ALLOW = "allow" # Automatically allow
29
+ DENY = "deny" # Automatically deny
30
+ ASK = "ask" # Ask user for permission
31
+ ALLOW_SESSION = "allow_session" # Allow for this session only
32
+
33
+
34
+ class PermissionScope(Enum):
35
+ """Scope of the permission rule."""
36
+
37
+ FILE_READ = "file_read"
38
+ FILE_WRITE = "file_write"
39
+ FILE_DELETE = "file_delete"
40
+ DIRECTORY_CREATE = "directory_create"
41
+ DIRECTORY_DELETE = "directory_delete"
42
+ SHELL_EXECUTE = "shell_execute"
43
+ NETWORK_ACCESS = "network_access"
44
+ TOOL_CALL = "tool_call"
45
+
46
+
47
+ @dataclass
48
+ class PermissionRule:
49
+ """A single permission rule.
50
+
51
+ Rules are matched in order of specificity:
52
+ 1. Exact path matches
53
+ 2. Glob patterns
54
+ 3. Directory prefixes
55
+ 4. Default rules
56
+ """
57
+
58
+ pattern: str # Glob pattern or exact path
59
+ scope: PermissionScope # What operation this applies to
60
+ action: PermissionAction # What to do when matched
61
+ priority: int = 0 # Higher = checked first
62
+ reason: str = "" # Explanation for the rule
63
+ expires_at: Optional[datetime] = None # Optional expiration
64
+ created_by: str = "" # Who created this rule
65
+
66
+ @property
67
+ def is_expired(self) -> bool:
68
+ """Check if rule has expired."""
69
+ if self.expires_at is None:
70
+ return False
71
+ return datetime.now() > self.expires_at
72
+
73
+ def matches(self, path: str, scope: PermissionScope) -> bool:
74
+ """Check if this rule matches the given path and scope."""
75
+ if self.scope != scope:
76
+ return False
77
+
78
+ if self.is_expired:
79
+ return False
80
+
81
+ # Exact match
82
+ if self.pattern == path:
83
+ return True
84
+
85
+ # Glob pattern
86
+ if fnmatch.fnmatch(path, self.pattern):
87
+ return True
88
+
89
+ # Directory prefix (pattern ends with /**)
90
+ if self.pattern.endswith("/**"):
91
+ prefix = self.pattern[:-3]
92
+ if path.startswith(prefix):
93
+ return True
94
+
95
+ return False
96
+
97
+ def to_dict(self) -> dict:
98
+ return {
99
+ "pattern": self.pattern,
100
+ "scope": self.scope.value,
101
+ "action": self.action.value,
102
+ "priority": self.priority,
103
+ "reason": self.reason,
104
+ "expires_at": self.expires_at.isoformat() if self.expires_at else None,
105
+ "created_by": self.created_by,
106
+ }
107
+
108
+ @classmethod
109
+ def from_dict(cls, data: dict) -> "PermissionRule":
110
+ return cls(
111
+ pattern=data["pattern"],
112
+ scope=PermissionScope(data["scope"]),
113
+ action=PermissionAction(data["action"]),
114
+ priority=data.get("priority", 0),
115
+ reason=data.get("reason", ""),
116
+ expires_at=datetime.fromisoformat(data["expires_at"])
117
+ if data.get("expires_at")
118
+ else None,
119
+ created_by=data.get("created_by", ""),
120
+ )
121
+
122
+
123
+ @dataclass
124
+ class PermissionRequest:
125
+ """A request for permission."""
126
+
127
+ id: str
128
+ scope: PermissionScope
129
+ path: str
130
+ tool_name: Optional[str] = None
131
+ tool_args: Optional[Dict[str, Any]] = None
132
+ agent_name: str = ""
133
+ timestamp: datetime = field(default_factory=datetime.now)
134
+ context: str = "" # Why this permission is needed
135
+
136
+ @property
137
+ def display_name(self) -> str:
138
+ """Get human-readable name for the request."""
139
+ if self.tool_name:
140
+ return f"{self.tool_name}: {self.path}"
141
+ return f"{self.scope.value}: {self.path}"
142
+
143
+
144
+ @dataclass
145
+ class PermissionDecision:
146
+ """A recorded permission decision."""
147
+
148
+ request_id: str
149
+ action: PermissionAction
150
+ decided_by: str # "user", "rule:pattern", "default"
151
+ timestamp: datetime = field(default_factory=datetime.now)
152
+ rule_pattern: Optional[str] = None
153
+
154
+
155
+ class PermissionManager:
156
+ """
157
+ Manages permission rules and decisions.
158
+
159
+ Provides rule-based access control with wildcard support,
160
+ session-scoped permissions, and decision history.
161
+
162
+ Usage:
163
+ manager = PermissionManager()
164
+
165
+ # Add rules
166
+ manager.add_rule(PermissionRule(
167
+ pattern="src/**",
168
+ scope=PermissionScope.FILE_WRITE,
169
+ action=PermissionAction.ALLOW,
170
+ ))
171
+ manager.add_rule(PermissionRule(
172
+ pattern=".env*",
173
+ scope=PermissionScope.FILE_READ,
174
+ action=PermissionAction.DENY,
175
+ reason="Sensitive environment files",
176
+ ))
177
+
178
+ # Check permission
179
+ result = manager.check_permission(
180
+ PermissionRequest(
181
+ id="req-1",
182
+ scope=PermissionScope.FILE_WRITE,
183
+ path="src/main.py",
184
+ )
185
+ )
186
+
187
+ if result.action == PermissionAction.ASK:
188
+ # Ask user and record decision
189
+ manager.record_decision(...)
190
+ """
191
+
192
+ # Default rules (lowest priority)
193
+ DEFAULT_RULES = [
194
+ # Deny sensitive files by default
195
+ PermissionRule(
196
+ "**/.env*", PermissionScope.FILE_READ, PermissionAction.DENY, -100, "Environment files"
197
+ ),
198
+ PermissionRule(
199
+ "**/*.pem", PermissionScope.FILE_READ, PermissionAction.DENY, -100, "Private keys"
200
+ ),
201
+ PermissionRule(
202
+ "**/*.key", PermissionScope.FILE_READ, PermissionAction.DENY, -100, "Private keys"
203
+ ),
204
+ PermissionRule(
205
+ "**/credentials*", PermissionScope.FILE_READ, PermissionAction.DENY, -100, "Credentials"
206
+ ),
207
+ PermissionRule(
208
+ "**/secrets*", PermissionScope.FILE_READ, PermissionAction.DENY, -100, "Secrets"
209
+ ),
210
+ # Deny dangerous directories
211
+ PermissionRule(
212
+ "**/.git/**", PermissionScope.FILE_WRITE, PermissionAction.DENY, -100, "Git internals"
213
+ ),
214
+ PermissionRule(
215
+ "**/node_modules/**",
216
+ PermissionScope.FILE_WRITE,
217
+ PermissionAction.DENY,
218
+ -100,
219
+ "Dependencies",
220
+ ),
221
+ # Allow reading most source files
222
+ PermissionRule(
223
+ "**/*.py", PermissionScope.FILE_READ, PermissionAction.ALLOW, -50, "Python source"
224
+ ),
225
+ PermissionRule(
226
+ "**/*.js", PermissionScope.FILE_READ, PermissionAction.ALLOW, -50, "JavaScript source"
227
+ ),
228
+ PermissionRule(
229
+ "**/*.ts", PermissionScope.FILE_READ, PermissionAction.ALLOW, -50, "TypeScript source"
230
+ ),
231
+ PermissionRule(
232
+ "**/*.go", PermissionScope.FILE_READ, PermissionAction.ALLOW, -50, "Go source"
233
+ ),
234
+ PermissionRule(
235
+ "**/*.rs", PermissionScope.FILE_READ, PermissionAction.ALLOW, -50, "Rust source"
236
+ ),
237
+ # Default to ask for everything else
238
+ PermissionRule(
239
+ "**", PermissionScope.FILE_WRITE, PermissionAction.ASK, -1000, "Default write"
240
+ ),
241
+ PermissionRule(
242
+ "**", PermissionScope.FILE_DELETE, PermissionAction.ASK, -1000, "Default delete"
243
+ ),
244
+ PermissionRule(
245
+ "**", PermissionScope.SHELL_EXECUTE, PermissionAction.ASK, -1000, "Default shell"
246
+ ),
247
+ ]
248
+
249
+ def __init__(
250
+ self,
251
+ rules_file: Optional[Path] = None,
252
+ include_defaults: bool = True,
253
+ ):
254
+ self._rules: List[PermissionRule] = []
255
+ self._session_rules: List[PermissionRule] = [] # Session-only rules
256
+ self._decisions: List[PermissionDecision] = []
257
+ self._rules_file = rules_file
258
+
259
+ # Load default rules
260
+ if include_defaults:
261
+ self._rules.extend(self.DEFAULT_RULES)
262
+
263
+ # Load saved rules
264
+ if rules_file and rules_file.exists():
265
+ self._load_rules()
266
+
267
+ def add_rule(
268
+ self,
269
+ rule: PermissionRule,
270
+ session_only: bool = False,
271
+ ) -> None:
272
+ """Add a permission rule.
273
+
274
+ Args:
275
+ rule: The rule to add
276
+ session_only: If True, rule is cleared at session end
277
+ """
278
+ if session_only:
279
+ self._session_rules.append(rule)
280
+ else:
281
+ self._rules.append(rule)
282
+
283
+ # Sort by priority (highest first)
284
+ self._rules.sort(key=lambda r: r.priority, reverse=True)
285
+ self._session_rules.sort(key=lambda r: r.priority, reverse=True)
286
+
287
+ def remove_rule(self, pattern: str, scope: PermissionScope) -> bool:
288
+ """Remove a rule by pattern and scope."""
289
+ for rules_list in [self._rules, self._session_rules]:
290
+ for rule in rules_list[:]:
291
+ if rule.pattern == pattern and rule.scope == scope:
292
+ rules_list.remove(rule)
293
+ return True
294
+ return False
295
+
296
+ def check_permission(self, request: PermissionRequest) -> PermissionDecision:
297
+ """
298
+ Check if a permission request should be allowed.
299
+
300
+ Returns a decision based on matching rules.
301
+ """
302
+ # Check session rules first (highest priority)
303
+ for rule in self._session_rules:
304
+ if rule.matches(request.path, request.scope):
305
+ return PermissionDecision(
306
+ request_id=request.id,
307
+ action=rule.action,
308
+ decided_by=f"session_rule:{rule.pattern}",
309
+ rule_pattern=rule.pattern,
310
+ )
311
+
312
+ # Check persistent rules
313
+ for rule in self._rules:
314
+ if rule.matches(request.path, request.scope):
315
+ return PermissionDecision(
316
+ request_id=request.id,
317
+ action=rule.action,
318
+ decided_by=f"rule:{rule.pattern}",
319
+ rule_pattern=rule.pattern,
320
+ )
321
+
322
+ # Default: ask
323
+ return PermissionDecision(
324
+ request_id=request.id,
325
+ action=PermissionAction.ASK,
326
+ decided_by="default",
327
+ )
328
+
329
+ def record_decision(
330
+ self,
331
+ request: PermissionRequest,
332
+ action: PermissionAction,
333
+ create_rule: bool = False,
334
+ rule_scope: str = "exact", # "exact", "directory", "extension"
335
+ ) -> None:
336
+ """
337
+ Record a permission decision (from user).
338
+
339
+ Args:
340
+ request: The original request
341
+ action: The action taken
342
+ create_rule: If True, create a rule for future requests
343
+ rule_scope: How broad to make the rule
344
+ """
345
+ decision = PermissionDecision(
346
+ request_id=request.id,
347
+ action=action,
348
+ decided_by="user",
349
+ )
350
+ self._decisions.append(decision)
351
+
352
+ # Create rule for future requests if requested
353
+ if create_rule and action in (PermissionAction.ALLOW, PermissionAction.DENY):
354
+ pattern = self._create_pattern(request.path, rule_scope)
355
+
356
+ rule = PermissionRule(
357
+ pattern=pattern,
358
+ scope=request.scope,
359
+ action=action,
360
+ priority=100, # User rules have high priority
361
+ created_by="user",
362
+ )
363
+
364
+ # Session-scoped for ALLOW_SESSION
365
+ session_only = action == PermissionAction.ALLOW_SESSION
366
+ self.add_rule(rule, session_only=session_only)
367
+
368
+ def _create_pattern(self, path: str, scope: str) -> str:
369
+ """Create a pattern from a path based on scope."""
370
+ if scope == "exact":
371
+ return path
372
+ elif scope == "directory":
373
+ # Match all files in the same directory
374
+ dir_path = str(Path(path).parent)
375
+ return f"{dir_path}/**"
376
+ elif scope == "extension":
377
+ # Match all files with same extension
378
+ ext = Path(path).suffix
379
+ return f"**/*{ext}"
380
+ else:
381
+ return path
382
+
383
+ def allow_all(
384
+ self,
385
+ pattern: str,
386
+ scope: PermissionScope,
387
+ session_only: bool = True,
388
+ ) -> None:
389
+ """Add an allow-all rule for a pattern."""
390
+ self.add_rule(
391
+ PermissionRule(
392
+ pattern=pattern,
393
+ scope=scope,
394
+ action=PermissionAction.ALLOW,
395
+ priority=200, # High priority
396
+ created_by="user",
397
+ ),
398
+ session_only=session_only,
399
+ )
400
+
401
+ def deny_all(
402
+ self,
403
+ pattern: str,
404
+ scope: PermissionScope,
405
+ session_only: bool = False,
406
+ ) -> None:
407
+ """Add a deny-all rule for a pattern."""
408
+ self.add_rule(
409
+ PermissionRule(
410
+ pattern=pattern,
411
+ scope=scope,
412
+ action=PermissionAction.DENY,
413
+ priority=200, # High priority
414
+ created_by="user",
415
+ ),
416
+ session_only=session_only,
417
+ )
418
+
419
+ def clear_session_rules(self) -> None:
420
+ """Clear all session-only rules."""
421
+ self._session_rules.clear()
422
+
423
+ def get_rules(self, include_session: bool = True) -> List[PermissionRule]:
424
+ """Get all rules."""
425
+ rules = list(self._rules)
426
+ if include_session:
427
+ rules.extend(self._session_rules)
428
+ rules.sort(key=lambda r: r.priority, reverse=True)
429
+ return rules
430
+
431
+ def get_decisions(self, limit: int = 100) -> List[PermissionDecision]:
432
+ """Get recent decisions."""
433
+ return self._decisions[-limit:]
434
+
435
+ def _load_rules(self) -> None:
436
+ """Load rules from file."""
437
+ if not self._rules_file or not self._rules_file.exists():
438
+ return
439
+
440
+ try:
441
+ data = json.loads(self._rules_file.read_text())
442
+ for rule_data in data.get("rules", []):
443
+ rule = PermissionRule.from_dict(rule_data)
444
+ if not rule.is_expired:
445
+ self._rules.append(rule)
446
+
447
+ self._rules.sort(key=lambda r: r.priority, reverse=True)
448
+ except (json.JSONDecodeError, KeyError):
449
+ pass
450
+
451
+ def save_rules(self) -> None:
452
+ """Save rules to file."""
453
+ if not self._rules_file:
454
+ return
455
+
456
+ # Only save non-default, non-session rules
457
+ user_rules = [r for r in self._rules if r.created_by == "user" and not r.is_expired]
458
+
459
+ data = {
460
+ "rules": [r.to_dict() for r in user_rules],
461
+ }
462
+
463
+ self._rules_file.parent.mkdir(parents=True, exist_ok=True)
464
+ self._rules_file.write_text(json.dumps(data, indent=2))
465
+
466
+
467
+ def create_permission_request(
468
+ scope: PermissionScope,
469
+ path: str,
470
+ tool_name: Optional[str] = None,
471
+ tool_args: Optional[Dict[str, Any]] = None,
472
+ agent_name: str = "",
473
+ context: str = "",
474
+ ) -> PermissionRequest:
475
+ """Convenience function to create a permission request."""
476
+ request_id = hashlib.sha256(
477
+ f"{scope.value}:{path}:{datetime.now().isoformat()}".encode()
478
+ ).hexdigest()[:12]
479
+
480
+ return PermissionRequest(
481
+ id=f"req-{request_id}",
482
+ scope=scope,
483
+ path=path,
484
+ tool_name=tool_name,
485
+ tool_args=tool_args,
486
+ agent_name=agent_name,
487
+ context=context,
488
+ )