kairo-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- image-service/main.py +178 -0
- infra/chat/app/main.py +84 -0
- kairo/backend/__init__.py +0 -0
- kairo/backend/api/__init__.py +0 -0
- kairo/backend/api/admin/__init__.py +23 -0
- kairo/backend/api/admin/audit.py +54 -0
- kairo/backend/api/admin/content.py +142 -0
- kairo/backend/api/admin/incidents.py +148 -0
- kairo/backend/api/admin/stats.py +125 -0
- kairo/backend/api/admin/system.py +87 -0
- kairo/backend/api/admin/users.py +279 -0
- kairo/backend/api/agents.py +94 -0
- kairo/backend/api/api_keys.py +85 -0
- kairo/backend/api/auth.py +116 -0
- kairo/backend/api/billing.py +41 -0
- kairo/backend/api/chat.py +72 -0
- kairo/backend/api/conversations.py +125 -0
- kairo/backend/api/device_auth.py +100 -0
- kairo/backend/api/files.py +83 -0
- kairo/backend/api/health.py +36 -0
- kairo/backend/api/images.py +80 -0
- kairo/backend/api/openai_compat.py +225 -0
- kairo/backend/api/projects.py +102 -0
- kairo/backend/api/usage.py +32 -0
- kairo/backend/api/webhooks.py +79 -0
- kairo/backend/app.py +297 -0
- kairo/backend/config.py +179 -0
- kairo/backend/core/__init__.py +0 -0
- kairo/backend/core/admin_auth.py +24 -0
- kairo/backend/core/api_key_auth.py +55 -0
- kairo/backend/core/database.py +28 -0
- kairo/backend/core/dependencies.py +70 -0
- kairo/backend/core/logging.py +23 -0
- kairo/backend/core/rate_limit.py +73 -0
- kairo/backend/core/security.py +29 -0
- kairo/backend/models/__init__.py +19 -0
- kairo/backend/models/agent.py +30 -0
- kairo/backend/models/api_key.py +25 -0
- kairo/backend/models/api_usage.py +29 -0
- kairo/backend/models/audit_log.py +26 -0
- kairo/backend/models/conversation.py +48 -0
- kairo/backend/models/device_code.py +30 -0
- kairo/backend/models/feature_flag.py +21 -0
- kairo/backend/models/image_generation.py +24 -0
- kairo/backend/models/incident.py +28 -0
- kairo/backend/models/project.py +28 -0
- kairo/backend/models/uptime_record.py +24 -0
- kairo/backend/models/usage.py +24 -0
- kairo/backend/models/user.py +49 -0
- kairo/backend/schemas/__init__.py +0 -0
- kairo/backend/schemas/admin/__init__.py +0 -0
- kairo/backend/schemas/admin/audit.py +28 -0
- kairo/backend/schemas/admin/content.py +53 -0
- kairo/backend/schemas/admin/stats.py +77 -0
- kairo/backend/schemas/admin/system.py +44 -0
- kairo/backend/schemas/admin/users.py +48 -0
- kairo/backend/schemas/agent.py +42 -0
- kairo/backend/schemas/api_key.py +30 -0
- kairo/backend/schemas/auth.py +57 -0
- kairo/backend/schemas/chat.py +26 -0
- kairo/backend/schemas/conversation.py +39 -0
- kairo/backend/schemas/device_auth.py +40 -0
- kairo/backend/schemas/image.py +15 -0
- kairo/backend/schemas/openai_compat.py +76 -0
- kairo/backend/schemas/project.py +21 -0
- kairo/backend/schemas/status.py +81 -0
- kairo/backend/schemas/usage.py +15 -0
- kairo/backend/services/__init__.py +0 -0
- kairo/backend/services/admin/__init__.py +0 -0
- kairo/backend/services/admin/audit_service.py +78 -0
- kairo/backend/services/admin/content_service.py +119 -0
- kairo/backend/services/admin/incident_service.py +94 -0
- kairo/backend/services/admin/stats_service.py +281 -0
- kairo/backend/services/admin/system_service.py +126 -0
- kairo/backend/services/admin/user_service.py +157 -0
- kairo/backend/services/agent_service.py +107 -0
- kairo/backend/services/api_key_service.py +66 -0
- kairo/backend/services/api_usage_service.py +126 -0
- kairo/backend/services/auth_service.py +101 -0
- kairo/backend/services/chat_service.py +501 -0
- kairo/backend/services/conversation_service.py +264 -0
- kairo/backend/services/device_auth_service.py +193 -0
- kairo/backend/services/email_service.py +55 -0
- kairo/backend/services/image_service.py +181 -0
- kairo/backend/services/llm_service.py +186 -0
- kairo/backend/services/project_service.py +109 -0
- kairo/backend/services/status_service.py +167 -0
- kairo/backend/services/stripe_service.py +78 -0
- kairo/backend/services/usage_service.py +150 -0
- kairo/backend/services/web_search_service.py +96 -0
- kairo/migrations/env.py +60 -0
- kairo/migrations/versions/001_initial.py +55 -0
- kairo/migrations/versions/002_usage_tracking_and_indexes.py +66 -0
- kairo/migrations/versions/003_username_to_email.py +21 -0
- kairo/migrations/versions/004_add_plans_and_verification.py +67 -0
- kairo/migrations/versions/005_add_projects.py +52 -0
- kairo/migrations/versions/006_add_image_generation.py +63 -0
- kairo/migrations/versions/007_add_admin_portal.py +107 -0
- kairo/migrations/versions/008_add_device_code_auth.py +76 -0
- kairo/migrations/versions/009_add_status_page.py +65 -0
- kairo/tools/extract_claude_data.py +465 -0
- kairo/tools/filter_claude_data.py +303 -0
- kairo/tools/generate_curated_data.py +157 -0
- kairo/tools/mix_training_data.py +295 -0
- kairo_code/__init__.py +3 -0
- kairo_code/agents/__init__.py +25 -0
- kairo_code/agents/architect.py +98 -0
- kairo_code/agents/audit.py +100 -0
- kairo_code/agents/base.py +463 -0
- kairo_code/agents/coder.py +155 -0
- kairo_code/agents/database.py +77 -0
- kairo_code/agents/docs.py +88 -0
- kairo_code/agents/explorer.py +62 -0
- kairo_code/agents/guardian.py +80 -0
- kairo_code/agents/planner.py +66 -0
- kairo_code/agents/reviewer.py +91 -0
- kairo_code/agents/security.py +94 -0
- kairo_code/agents/terraform.py +88 -0
- kairo_code/agents/testing.py +97 -0
- kairo_code/agents/uiux.py +88 -0
- kairo_code/auth.py +232 -0
- kairo_code/config.py +172 -0
- kairo_code/conversation.py +173 -0
- kairo_code/heartbeat.py +63 -0
- kairo_code/llm.py +291 -0
- kairo_code/logging_config.py +156 -0
- kairo_code/main.py +818 -0
- kairo_code/router.py +217 -0
- kairo_code/sandbox.py +248 -0
- kairo_code/settings.py +183 -0
- kairo_code/tools/__init__.py +51 -0
- kairo_code/tools/analysis.py +509 -0
- kairo_code/tools/base.py +417 -0
- kairo_code/tools/code.py +58 -0
- kairo_code/tools/definitions.py +617 -0
- kairo_code/tools/files.py +315 -0
- kairo_code/tools/review.py +390 -0
- kairo_code/tools/search.py +185 -0
- kairo_code/ui.py +418 -0
- kairo_code-0.1.0.dist-info/METADATA +13 -0
- kairo_code-0.1.0.dist-info/RECORD +144 -0
- kairo_code-0.1.0.dist-info/WHEEL +5 -0
- kairo_code-0.1.0.dist-info/entry_points.txt +2 -0
- kairo_code-0.1.0.dist-info/top_level.txt +4 -0
kairo_code/router.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Intent router using small LLM for classification with few-shot examples"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from .llm import LLM
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Intent(Enum):
|
|
11
|
+
SEARCH = "search"
|
|
12
|
+
CODE = "code"
|
|
13
|
+
FILE_READ = "file_read"
|
|
14
|
+
FILE_WRITE = "file_write"
|
|
15
|
+
FILE_LIST = "file_list"
|
|
16
|
+
EXPLORE = "explore"
|
|
17
|
+
PLAN = "plan"
|
|
18
|
+
CHAT = "chat"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class RoutedIntent:
|
|
23
|
+
intent: Intent
|
|
24
|
+
confidence: str # "high", "medium", "low"
|
|
25
|
+
extracted_params: dict
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Few-shot examples for the router
|
|
29
|
+
ROUTER_EXAMPLES = """
|
|
30
|
+
## Examples
|
|
31
|
+
|
|
32
|
+
Input: "What's the weather like today?"
|
|
33
|
+
Output: search
|
|
34
|
+
|
|
35
|
+
Input: "Write a Python function to sort a list"
|
|
36
|
+
Output: code
|
|
37
|
+
|
|
38
|
+
Input: "Show me what's in config.py"
|
|
39
|
+
Output: file_read
|
|
40
|
+
|
|
41
|
+
Input: "Create a new file called utils.py with a helper function"
|
|
42
|
+
Output: file_write
|
|
43
|
+
|
|
44
|
+
Input: "What files are in the src directory?"
|
|
45
|
+
Output: file_list
|
|
46
|
+
|
|
47
|
+
Input: "Analyze this codebase for improvements"
|
|
48
|
+
Output: explore
|
|
49
|
+
|
|
50
|
+
Input: "How should I implement user authentication?"
|
|
51
|
+
Output: plan
|
|
52
|
+
|
|
53
|
+
Input: "What's the capital of France?"
|
|
54
|
+
Output: chat
|
|
55
|
+
|
|
56
|
+
Input: "Search for the latest Python 3.12 features"
|
|
57
|
+
Output: search
|
|
58
|
+
|
|
59
|
+
Input: "Fix the bug in main.py line 42"
|
|
60
|
+
Output: code
|
|
61
|
+
|
|
62
|
+
Input: "Read the README file"
|
|
63
|
+
Output: file_read
|
|
64
|
+
|
|
65
|
+
Input: "Save this code to server.py"
|
|
66
|
+
Output: file_write
|
|
67
|
+
|
|
68
|
+
Input: "Find all Python files in this project"
|
|
69
|
+
Output: file_list
|
|
70
|
+
|
|
71
|
+
Input: "Explore how the API endpoints work"
|
|
72
|
+
Output: explore
|
|
73
|
+
|
|
74
|
+
Input: "Plan out how to refactor the database layer"
|
|
75
|
+
Output: plan
|
|
76
|
+
|
|
77
|
+
Input: "Hello, how are you?"
|
|
78
|
+
Output: chat
|
|
79
|
+
|
|
80
|
+
Input: "Create a tool with a UI that pulls data from an API"
|
|
81
|
+
Output: code
|
|
82
|
+
|
|
83
|
+
Input: "Build me an app that tracks my tasks"
|
|
84
|
+
Output: code
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
ROUTER_SYSTEM_PROMPT = f"""You are a classifier. Classify the user's intent into exactly ONE category.
|
|
88
|
+
|
|
89
|
+
Categories:
|
|
90
|
+
- search: User wants current/real-time information from the web
|
|
91
|
+
- code: User wants to write, modify, fix, or generate code
|
|
92
|
+
- file_read: User wants to see contents of a specific file
|
|
93
|
+
- file_write: User wants to create or save a file
|
|
94
|
+
- file_list: User wants to list or find files
|
|
95
|
+
- explore: User wants to analyze or understand a codebase
|
|
96
|
+
- plan: User wants to plan a complex task before implementing
|
|
97
|
+
- chat: General conversation, questions, or explanations
|
|
98
|
+
|
|
99
|
+
{ROUTER_EXAMPLES}
|
|
100
|
+
|
|
101
|
+
Respond with ONLY the category name, nothing else.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Router:
|
|
106
|
+
"""Routes user input to the appropriate handler using few-shot classification."""
|
|
107
|
+
|
|
108
|
+
INTENT_CATEGORIES = [i.value for i in Intent]
|
|
109
|
+
|
|
110
|
+
def __init__(self, llm: LLM):
|
|
111
|
+
self.llm = llm
|
|
112
|
+
|
|
113
|
+
def route(self, user_input: str) -> RoutedIntent:
|
|
114
|
+
"""
|
|
115
|
+
Classify user input and extract relevant parameters.
|
|
116
|
+
Uses few-shot examples for better accuracy with small models.
|
|
117
|
+
"""
|
|
118
|
+
# First try rule-based classification for common patterns
|
|
119
|
+
rule_based = self._rule_based_classify(user_input)
|
|
120
|
+
if rule_based:
|
|
121
|
+
return rule_based
|
|
122
|
+
|
|
123
|
+
# Fall back to LLM classification
|
|
124
|
+
from .config import Config
|
|
125
|
+
config = Config()
|
|
126
|
+
system = config.router_prompt + "\n" + ROUTER_EXAMPLES + "\nRespond with ONLY the category name, nothing else."
|
|
127
|
+
response = self.llm.generate(
|
|
128
|
+
prompt=f"Input: {user_input}\nOutput:",
|
|
129
|
+
model=self.llm.select_model("router"),
|
|
130
|
+
system=system,
|
|
131
|
+
stream=False,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Parse the response
|
|
135
|
+
intent_str = response.strip().lower()
|
|
136
|
+
|
|
137
|
+
# Find matching intent
|
|
138
|
+
intent = Intent.CHAT # Default
|
|
139
|
+
for cat in self.INTENT_CATEGORIES:
|
|
140
|
+
if cat in intent_str:
|
|
141
|
+
try:
|
|
142
|
+
intent = Intent(cat)
|
|
143
|
+
except ValueError:
|
|
144
|
+
pass
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
# Extract params based on intent
|
|
148
|
+
params = self._extract_params(user_input, intent)
|
|
149
|
+
|
|
150
|
+
return RoutedIntent(
|
|
151
|
+
intent=intent,
|
|
152
|
+
confidence="high" if intent_str in self.INTENT_CATEGORIES else "medium",
|
|
153
|
+
extracted_params=params,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _rule_based_classify(self, user_input: str) -> RoutedIntent | None:
|
|
157
|
+
"""Fast rule-based classification for common patterns."""
|
|
158
|
+
lower = user_input.lower().strip()
|
|
159
|
+
|
|
160
|
+
# Search patterns
|
|
161
|
+
search_keywords = ["weather", "latest", "current", "today's", "news", "price of"]
|
|
162
|
+
if any(kw in lower for kw in search_keywords):
|
|
163
|
+
return RoutedIntent(Intent.SEARCH, "high", {})
|
|
164
|
+
|
|
165
|
+
# File read patterns
|
|
166
|
+
if re.match(r"^(show|read|cat|display|what'?s in|open)\s+.+\.(py|js|ts|json|yaml|yml|md|txt)", lower):
|
|
167
|
+
# Extract filename
|
|
168
|
+
match = re.search(r'[\w./\\-]+\.\w+', user_input)
|
|
169
|
+
path = match.group(0) if match else ""
|
|
170
|
+
return RoutedIntent(Intent.FILE_READ, "high", {"path": path})
|
|
171
|
+
|
|
172
|
+
# File list patterns
|
|
173
|
+
if re.match(r"^(list|find|show|what)\s+(files?|all)", lower):
|
|
174
|
+
return RoutedIntent(Intent.FILE_LIST, "high", {})
|
|
175
|
+
|
|
176
|
+
# Explore patterns
|
|
177
|
+
if any(phrase in lower for phrase in ["explore", "analyze", "understand", "how does", "explain the code"]):
|
|
178
|
+
return RoutedIntent(Intent.EXPLORE, "high", {})
|
|
179
|
+
|
|
180
|
+
# Plan patterns
|
|
181
|
+
if any(phrase in lower for phrase in ["plan", "how should i", "how to implement", "design"]):
|
|
182
|
+
return RoutedIntent(Intent.PLAN, "high", {})
|
|
183
|
+
|
|
184
|
+
# Code patterns
|
|
185
|
+
code_keywords = ["write", "create", "fix", "modify", "add", "implement", "refactor", "update", "change", "build", "make"]
|
|
186
|
+
code_context = ["function", "class", "method", "code", "script", "bug", "error", "tool", "app", "application", "program", "ui", "gui", "api"]
|
|
187
|
+
if any(kw in lower for kw in code_keywords) and any(ctx in lower for ctx in code_context):
|
|
188
|
+
return RoutedIntent(Intent.CODE, "high", {})
|
|
189
|
+
|
|
190
|
+
# Greeting patterns
|
|
191
|
+
greetings = ["hello", "hi", "hey", "good morning", "good afternoon"]
|
|
192
|
+
if any(lower.startswith(g) for g in greetings):
|
|
193
|
+
return RoutedIntent(Intent.CHAT, "high", {})
|
|
194
|
+
|
|
195
|
+
return None # Fall back to LLM
|
|
196
|
+
|
|
197
|
+
def _extract_params(self, user_input: str, intent: Intent) -> dict:
|
|
198
|
+
"""Extract relevant parameters from user input based on intent."""
|
|
199
|
+
params = {}
|
|
200
|
+
|
|
201
|
+
if intent in (Intent.FILE_READ, Intent.FILE_WRITE):
|
|
202
|
+
# Extract file path
|
|
203
|
+
path_match = re.search(r'[\w./\\-]+\.\w+', user_input)
|
|
204
|
+
if path_match:
|
|
205
|
+
params["path"] = path_match.group(0)
|
|
206
|
+
|
|
207
|
+
if intent == Intent.FILE_LIST:
|
|
208
|
+
# Extract glob pattern if present
|
|
209
|
+
pattern_match = re.search(r'\*+\.?\w*', user_input)
|
|
210
|
+
if pattern_match:
|
|
211
|
+
params["pattern"] = pattern_match.group(0)
|
|
212
|
+
|
|
213
|
+
if intent == Intent.SEARCH:
|
|
214
|
+
# The whole input is basically the search query
|
|
215
|
+
params["query"] = user_input
|
|
216
|
+
|
|
217
|
+
return params
|
kairo_code/sandbox.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Sandbox and safety controls for Kairo Code - prevents unauthorized file access"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SandboxConfig:
|
|
11
|
+
"""Configuration for the sandbox."""
|
|
12
|
+
# Root directory for all file operations (usually project dir)
|
|
13
|
+
root_dir: Path
|
|
14
|
+
# Allow operations outside root (dangerous!)
|
|
15
|
+
allow_escape: bool = False
|
|
16
|
+
# Paths that are always blocked (even with allow_escape)
|
|
17
|
+
blocked_paths: tuple = (
|
|
18
|
+
"/etc", "/usr", "/bin", "/sbin", "/boot", "/root",
|
|
19
|
+
"/sys", "/proc", "/dev", "/var/log",
|
|
20
|
+
"~/.ssh", "~/.gnupg", "~/.aws", "~/.config",
|
|
21
|
+
)
|
|
22
|
+
# File extensions that require extra confirmation
|
|
23
|
+
sensitive_extensions: tuple = (
|
|
24
|
+
".env", ".pem", ".key", ".crt", ".p12",
|
|
25
|
+
".password", ".secret", ".credentials",
|
|
26
|
+
)
|
|
27
|
+
# Max file size to write (prevents filling disk)
|
|
28
|
+
max_file_size: int = 10 * 1024 * 1024 # 10MB
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Sandbox:
|
|
32
|
+
"""
|
|
33
|
+
Sandbox for safe file operations.
|
|
34
|
+
|
|
35
|
+
Restricts file access to a root directory and blocks sensitive paths.
|
|
36
|
+
Similar to Claude Code's sandboxing.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: Optional[SandboxConfig] = None):
|
|
40
|
+
if config is None:
|
|
41
|
+
# Default: use current working directory as root
|
|
42
|
+
config = SandboxConfig(root_dir=Path.cwd())
|
|
43
|
+
|
|
44
|
+
self.config = config
|
|
45
|
+
self.root = config.root_dir.resolve()
|
|
46
|
+
|
|
47
|
+
# Expand blocked paths
|
|
48
|
+
self._blocked = set()
|
|
49
|
+
for p in config.blocked_paths:
|
|
50
|
+
expanded = Path(p).expanduser()
|
|
51
|
+
self._blocked.add(str(expanded.resolve()))
|
|
52
|
+
|
|
53
|
+
def validate_path(self, path: str, operation: str = "access") -> tuple[bool, str, Path]:
|
|
54
|
+
"""
|
|
55
|
+
Validate a path for safety.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
path: The path to validate
|
|
59
|
+
operation: The operation being performed (read, write, delete)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
(is_valid, error_message, resolved_path)
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
# Resolve the path
|
|
66
|
+
target = Path(path).expanduser()
|
|
67
|
+
|
|
68
|
+
# Make absolute if relative
|
|
69
|
+
if not target.is_absolute():
|
|
70
|
+
target = self.root / target
|
|
71
|
+
|
|
72
|
+
resolved = target.resolve()
|
|
73
|
+
|
|
74
|
+
# Check if path escapes sandbox
|
|
75
|
+
if not self.config.allow_escape:
|
|
76
|
+
try:
|
|
77
|
+
resolved.relative_to(self.root)
|
|
78
|
+
except ValueError:
|
|
79
|
+
return (
|
|
80
|
+
False,
|
|
81
|
+
f"Path '{path}' is outside project directory. "
|
|
82
|
+
f"Only paths within '{self.root}' are allowed.",
|
|
83
|
+
resolved
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Check blocked paths
|
|
87
|
+
resolved_str = str(resolved)
|
|
88
|
+
for blocked in self._blocked:
|
|
89
|
+
if resolved_str.startswith(blocked):
|
|
90
|
+
return (
|
|
91
|
+
False,
|
|
92
|
+
f"Access to '{path}' is blocked for security reasons.",
|
|
93
|
+
resolved
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Check for path traversal attempts
|
|
97
|
+
if ".." in path:
|
|
98
|
+
# Re-verify after resolution
|
|
99
|
+
try:
|
|
100
|
+
resolved.relative_to(self.root)
|
|
101
|
+
except ValueError:
|
|
102
|
+
return (
|
|
103
|
+
False,
|
|
104
|
+
f"Path traversal detected in '{path}'.",
|
|
105
|
+
resolved
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Check sensitive extensions for write operations
|
|
109
|
+
if operation == "write" and resolved.suffix in self.config.sensitive_extensions:
|
|
110
|
+
return (
|
|
111
|
+
False,
|
|
112
|
+
f"Writing to sensitive file type '{resolved.suffix}' requires manual confirmation.",
|
|
113
|
+
resolved
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return (True, "", resolved)
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
return (False, f"Invalid path: {e}", Path(path))
|
|
120
|
+
|
|
121
|
+
def validate_content(self, content: str, path: str) -> tuple[bool, str]:
|
|
122
|
+
"""
|
|
123
|
+
Validate content being written.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
content: The content to write
|
|
127
|
+
path: The target path
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
(is_valid, error_message)
|
|
131
|
+
"""
|
|
132
|
+
# Check size
|
|
133
|
+
size = len(content.encode('utf-8'))
|
|
134
|
+
if size > self.config.max_file_size:
|
|
135
|
+
return (
|
|
136
|
+
False,
|
|
137
|
+
f"Content too large ({size / 1024 / 1024:.1f}MB). "
|
|
138
|
+
f"Max allowed: {self.config.max_file_size / 1024 / 1024:.0f}MB"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Check for suspicious content patterns
|
|
142
|
+
suspicious_patterns = [
|
|
143
|
+
("#!/", "Script with shebang"),
|
|
144
|
+
("rm -rf", "Destructive command"),
|
|
145
|
+
("sudo ", "Sudo command"),
|
|
146
|
+
("eval(", "Eval statement"),
|
|
147
|
+
("exec(", "Exec statement"),
|
|
148
|
+
("__import__", "Dynamic import"),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
warnings = []
|
|
152
|
+
for pattern, description in suspicious_patterns:
|
|
153
|
+
if pattern in content:
|
|
154
|
+
warnings.append(description)
|
|
155
|
+
|
|
156
|
+
if warnings:
|
|
157
|
+
# Don't block, just warn
|
|
158
|
+
return (True, f"Warning: Content contains: {', '.join(warnings)}")
|
|
159
|
+
|
|
160
|
+
return (True, "")
|
|
161
|
+
|
|
162
|
+
def safe_read(self, path: str) -> str:
|
|
163
|
+
"""Safely read a file with validation."""
|
|
164
|
+
valid, error, resolved = self.validate_path(path, "read")
|
|
165
|
+
if not valid:
|
|
166
|
+
raise PermissionError(error)
|
|
167
|
+
|
|
168
|
+
if not resolved.exists():
|
|
169
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
170
|
+
|
|
171
|
+
if not resolved.is_file():
|
|
172
|
+
raise IsADirectoryError(f"Not a file: {path}")
|
|
173
|
+
|
|
174
|
+
return resolved.read_text()
|
|
175
|
+
|
|
176
|
+
def safe_write(self, path: str, content: str) -> str:
|
|
177
|
+
"""Safely write a file with validation."""
|
|
178
|
+
valid, error, resolved = self.validate_path(path, "write")
|
|
179
|
+
if not valid:
|
|
180
|
+
raise PermissionError(error)
|
|
181
|
+
|
|
182
|
+
valid, warning = self.validate_content(content, path)
|
|
183
|
+
if not valid:
|
|
184
|
+
raise ValueError(warning)
|
|
185
|
+
|
|
186
|
+
# Create parent directories if needed
|
|
187
|
+
resolved.parent.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
|
|
189
|
+
# Write the file
|
|
190
|
+
resolved.write_text(content)
|
|
191
|
+
|
|
192
|
+
line_count = content.count('\n') + 1
|
|
193
|
+
result = f"File written: {resolved.relative_to(self.root)} ({line_count} lines)"
|
|
194
|
+
|
|
195
|
+
if warning:
|
|
196
|
+
result += f"\n{warning}"
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
def safe_delete(self, path: str) -> str:
|
|
201
|
+
"""Safely delete a file with validation."""
|
|
202
|
+
valid, error, resolved = self.validate_path(path, "delete")
|
|
203
|
+
if not valid:
|
|
204
|
+
raise PermissionError(error)
|
|
205
|
+
|
|
206
|
+
if not resolved.exists():
|
|
207
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
208
|
+
|
|
209
|
+
if resolved.is_dir():
|
|
210
|
+
raise IsADirectoryError(f"Cannot delete directory: {path}. Use bash for that.")
|
|
211
|
+
|
|
212
|
+
resolved.unlink()
|
|
213
|
+
return f"Deleted: {resolved.relative_to(self.root)}"
|
|
214
|
+
|
|
215
|
+
def list_allowed_paths(self) -> list[str]:
|
|
216
|
+
"""List paths that are allowed for access."""
|
|
217
|
+
return [str(self.root)]
|
|
218
|
+
|
|
219
|
+
def get_relative_path(self, path: str) -> str:
|
|
220
|
+
"""Get path relative to sandbox root."""
|
|
221
|
+
try:
|
|
222
|
+
resolved = Path(path).resolve()
|
|
223
|
+
return str(resolved.relative_to(self.root))
|
|
224
|
+
except ValueError:
|
|
225
|
+
return path
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# Global sandbox instance (initialized when Kairo Code starts)
|
|
229
|
+
_sandbox: Optional[Sandbox] = None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def init_sandbox(root: Optional[Path] = None, allow_escape: bool = False) -> Sandbox:
|
|
233
|
+
"""Initialize the global sandbox."""
|
|
234
|
+
global _sandbox
|
|
235
|
+
config = SandboxConfig(
|
|
236
|
+
root_dir=root or Path.cwd(),
|
|
237
|
+
allow_escape=allow_escape
|
|
238
|
+
)
|
|
239
|
+
_sandbox = Sandbox(config)
|
|
240
|
+
return _sandbox
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def get_sandbox() -> Sandbox:
|
|
244
|
+
"""Get the global sandbox, initializing if needed."""
|
|
245
|
+
global _sandbox
|
|
246
|
+
if _sandbox is None:
|
|
247
|
+
_sandbox = Sandbox()
|
|
248
|
+
return _sandbox
|
kairo_code/settings.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""User settings management with interactive prompts."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.prompt import Confirm, Prompt
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
# User settings directory
|
|
14
|
+
SETTINGS_DIR = Path.home() / ".kairo_code"
|
|
15
|
+
SETTINGS_FILE = SETTINGS_DIR / "settings.yaml"
|
|
16
|
+
|
|
17
|
+
DEFAULT_SETTINGS = {
|
|
18
|
+
"auto_approve": {
|
|
19
|
+
"enabled": False,
|
|
20
|
+
"write_file": False,
|
|
21
|
+
"bash": False,
|
|
22
|
+
"prompted": False, # Whether we've asked the user yet
|
|
23
|
+
},
|
|
24
|
+
"verbose": False,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_user_settings() -> dict[str, Any]:
|
|
29
|
+
"""Load user settings from ~/.kairo_code/settings.yaml"""
|
|
30
|
+
SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
if not SETTINGS_FILE.exists():
|
|
33
|
+
return DEFAULT_SETTINGS.copy()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with open(SETTINGS_FILE) as f:
|
|
37
|
+
settings = yaml.safe_load(f) or {}
|
|
38
|
+
# Merge with defaults for any missing keys
|
|
39
|
+
merged = DEFAULT_SETTINGS.copy()
|
|
40
|
+
_deep_merge(merged, settings)
|
|
41
|
+
return merged
|
|
42
|
+
except Exception:
|
|
43
|
+
return DEFAULT_SETTINGS.copy()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def save_user_settings(settings: dict[str, Any]) -> None:
|
|
47
|
+
"""Save user settings to ~/.kairo_code/settings.yaml"""
|
|
48
|
+
SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
with open(SETTINGS_FILE, "w") as f:
|
|
51
|
+
yaml.dump(settings, f, default_flow_style=False)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _deep_merge(base: dict, override: dict) -> None:
|
|
55
|
+
"""Recursively merge override into base."""
|
|
56
|
+
for key, value in override.items():
|
|
57
|
+
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
|
58
|
+
_deep_merge(base[key], value)
|
|
59
|
+
else:
|
|
60
|
+
base[key] = value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def prompt_auto_approve_settings(force: bool = False) -> dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
Interactively prompt user for auto-approve settings.
|
|
66
|
+
Only prompts on first run unless force=True.
|
|
67
|
+
Returns the updated settings.
|
|
68
|
+
"""
|
|
69
|
+
settings = load_user_settings()
|
|
70
|
+
|
|
71
|
+
# Skip if already prompted (unless forced)
|
|
72
|
+
if settings["auto_approve"].get("prompted") and not force:
|
|
73
|
+
return settings
|
|
74
|
+
|
|
75
|
+
console.print("\n[bold cyan]═══ Auto-Approve Settings ═══[/]\n")
|
|
76
|
+
console.print("[dim]Kairo Code can auto-approve certain actions to speed up your workflow.[/]")
|
|
77
|
+
console.print("[dim]You can change these settings anytime with /settings[/]\n")
|
|
78
|
+
|
|
79
|
+
# Ask about file writes
|
|
80
|
+
settings["auto_approve"]["write_file"] = Confirm.ask(
|
|
81
|
+
"[yellow]?[/] Auto-approve [bold]file writes[/] (create/modify files without asking)?",
|
|
82
|
+
default=False
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Ask about bash commands
|
|
86
|
+
console.print("\n[dim]⚠ Bash commands can modify your system. Auto-approve with caution.[/]")
|
|
87
|
+
settings["auto_approve"]["bash"] = Confirm.ask(
|
|
88
|
+
"[yellow]?[/] Auto-approve [bold]bash commands[/] (run shell commands without asking)?",
|
|
89
|
+
default=False
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Set enabled if any are true
|
|
93
|
+
settings["auto_approve"]["enabled"] = (
|
|
94
|
+
settings["auto_approve"]["write_file"] or
|
|
95
|
+
settings["auto_approve"]["bash"]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Mark as prompted
|
|
99
|
+
settings["auto_approve"]["prompted"] = True
|
|
100
|
+
|
|
101
|
+
# Save settings
|
|
102
|
+
save_user_settings(settings)
|
|
103
|
+
|
|
104
|
+
# Show summary
|
|
105
|
+
console.print("\n[bold]Settings saved![/]")
|
|
106
|
+
_print_current_settings(settings)
|
|
107
|
+
console.print()
|
|
108
|
+
|
|
109
|
+
return settings
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _print_current_settings(settings: dict[str, Any]) -> None:
|
|
113
|
+
"""Print current auto-approve settings."""
|
|
114
|
+
auto = settings["auto_approve"]
|
|
115
|
+
|
|
116
|
+
write_status = "[green]✓ Yes[/]" if auto.get("write_file") else "[red]✗ No[/]"
|
|
117
|
+
bash_status = "[green]✓ Yes[/]" if auto.get("bash") else "[red]✗ No[/]"
|
|
118
|
+
|
|
119
|
+
console.print(f" • Auto-approve file writes: {write_status}")
|
|
120
|
+
console.print(f" • Auto-approve bash commands: {bash_status}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def show_settings_menu() -> dict[str, Any]:
|
|
124
|
+
"""Show settings menu and allow user to change settings."""
|
|
125
|
+
settings = load_user_settings()
|
|
126
|
+
|
|
127
|
+
console.print("\n[bold cyan]═══ Kairo Code Settings ═══[/]\n")
|
|
128
|
+
console.print("[bold]Current settings:[/]")
|
|
129
|
+
_print_current_settings(settings)
|
|
130
|
+
|
|
131
|
+
console.print("\n[bold]Options:[/]")
|
|
132
|
+
console.print(" [cyan]1[/] - Toggle auto-approve file writes")
|
|
133
|
+
console.print(" [cyan]2[/] - Toggle auto-approve bash commands")
|
|
134
|
+
console.print(" [cyan]3[/] - Enable all auto-approve")
|
|
135
|
+
console.print(" [cyan]4[/] - Disable all auto-approve")
|
|
136
|
+
console.print(" [cyan]q[/] - Back to chat")
|
|
137
|
+
|
|
138
|
+
choice = Prompt.ask("\n[yellow]?[/] Select option", choices=["1", "2", "3", "4", "q"], default="q")
|
|
139
|
+
|
|
140
|
+
if choice == "1":
|
|
141
|
+
settings["auto_approve"]["write_file"] = not settings["auto_approve"]["write_file"]
|
|
142
|
+
console.print(f"[green]File writes auto-approve: {'enabled' if settings['auto_approve']['write_file'] else 'disabled'}[/]")
|
|
143
|
+
elif choice == "2":
|
|
144
|
+
settings["auto_approve"]["bash"] = not settings["auto_approve"]["bash"]
|
|
145
|
+
console.print(f"[green]Bash auto-approve: {'enabled' if settings['auto_approve']['bash'] else 'disabled'}[/]")
|
|
146
|
+
elif choice == "3":
|
|
147
|
+
settings["auto_approve"]["write_file"] = True
|
|
148
|
+
settings["auto_approve"]["bash"] = True
|
|
149
|
+
settings["auto_approve"]["enabled"] = True
|
|
150
|
+
console.print("[green]All auto-approve enabled[/]")
|
|
151
|
+
elif choice == "4":
|
|
152
|
+
settings["auto_approve"]["write_file"] = False
|
|
153
|
+
settings["auto_approve"]["bash"] = False
|
|
154
|
+
settings["auto_approve"]["enabled"] = False
|
|
155
|
+
console.print("[green]All auto-approve disabled[/]")
|
|
156
|
+
|
|
157
|
+
# Update enabled flag
|
|
158
|
+
settings["auto_approve"]["enabled"] = (
|
|
159
|
+
settings["auto_approve"]["write_file"] or
|
|
160
|
+
settings["auto_approve"]["bash"]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
save_user_settings(settings)
|
|
164
|
+
return settings
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def should_auto_approve(tool_name: str, settings: dict[str, Any] | None = None) -> bool:
|
|
168
|
+
"""Check if a tool should be auto-approved based on user settings."""
|
|
169
|
+
if settings is None:
|
|
170
|
+
settings = load_user_settings()
|
|
171
|
+
|
|
172
|
+
auto = settings.get("auto_approve", {})
|
|
173
|
+
|
|
174
|
+
if tool_name == "write_file":
|
|
175
|
+
return auto.get("write_file", False)
|
|
176
|
+
elif tool_name == "bash":
|
|
177
|
+
return auto.get("bash", False)
|
|
178
|
+
|
|
179
|
+
# Read-only tools are always auto-approved
|
|
180
|
+
if tool_name in ("read_file", "list_files", "tree", "search_files", "web_search"):
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
return False
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Kairo Code Tools - File operations, search, and code generation"""
|
|
2
|
+
|
|
3
|
+
from .base import (
|
|
4
|
+
Tool,
|
|
5
|
+
ToolResult,
|
|
6
|
+
ToolRegistry,
|
|
7
|
+
FunctionTool,
|
|
8
|
+
parse_tool_calls,
|
|
9
|
+
format_tool_error,
|
|
10
|
+
ParsedToolCall,
|
|
11
|
+
ToolParseResult,
|
|
12
|
+
)
|
|
13
|
+
from .files import (
|
|
14
|
+
read_file,
|
|
15
|
+
write_file,
|
|
16
|
+
edit_file,
|
|
17
|
+
list_files,
|
|
18
|
+
search_files,
|
|
19
|
+
tree,
|
|
20
|
+
)
|
|
21
|
+
from .search import web_search, format_search_results
|
|
22
|
+
from .code import extract_code_blocks, build_code_prompt, build_explain_prompt
|
|
23
|
+
from .definitions import create_default_registry, create_readonly_registry
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Base classes
|
|
27
|
+
"Tool",
|
|
28
|
+
"ToolResult",
|
|
29
|
+
"ToolRegistry",
|
|
30
|
+
"FunctionTool",
|
|
31
|
+
"parse_tool_calls",
|
|
32
|
+
"format_tool_error",
|
|
33
|
+
"ParsedToolCall",
|
|
34
|
+
"ToolParseResult",
|
|
35
|
+
"create_default_registry",
|
|
36
|
+
"create_readonly_registry",
|
|
37
|
+
# File tools
|
|
38
|
+
"read_file",
|
|
39
|
+
"write_file",
|
|
40
|
+
"edit_file",
|
|
41
|
+
"list_files",
|
|
42
|
+
"search_files",
|
|
43
|
+
"tree",
|
|
44
|
+
# Search
|
|
45
|
+
"web_search",
|
|
46
|
+
"format_search_results",
|
|
47
|
+
# Code
|
|
48
|
+
"extract_code_blocks",
|
|
49
|
+
"build_code_prompt",
|
|
50
|
+
"build_explain_prompt",
|
|
51
|
+
]
|