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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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
|
+
)
|