File size: 9,412 Bytes
359acf6 31d646f 359acf6 31d646f 359acf6 31d646f 359acf6 31d646f 359acf6 31d646f 359acf6 31d646f 359acf6 31d646f 359acf6 31d646f 359acf6 31d646f 359acf6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | """ConfigTool — runtime configuration management for Stack 2.9.
Stores configuration in ~/.stack-2.9/config.json
Operations:
- get : retrieve a configuration value
- set : set a configuration value
- list : list all configuration keys and values
- delete : remove a configuration key
Input schema:
operation : str — one of get, set, list, delete
key : str — configuration key (required for get/set/delete)
value : any — new value (required for set)
"""
from __future__ import annotations
import json
import os
from typing import Any
from .base import BaseTool, ToolResult
from .registry import get_registry
TOOL_NAME = "Config"
DATA_DIR = os.path.expanduser("~/.stack-2.9")
CONFIG_FILE = os.path.join(DATA_DIR, "config.json")
def _load_config() -> dict[str, Any]:
os.makedirs(DATA_DIR, exist_ok=True)
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE) as f:
return json.load(f)
except Exception:
pass
return {}
def _save_config(config: dict[str, Any]) -> None:
os.makedirs(DATA_DIR, exist_ok=True)
with open(CONFIG_FILE, "w") as f:
json.dump(config, f, indent=2, default=str)
# Supported configuration keys and their metadata
SUPPORTED_KEYS: dict[str, dict[str, Any]] = {
"theme": {
"type": "string",
"options": ["light", "dark", "system"],
"default": "system",
"description": "UI theme",
},
"model": {
"type": "string",
"description": "Default model to use",
"default": "",
},
"max_tokens": {
"type": "number",
"description": "Maximum tokens per response",
"default": 4096,
},
"temperature": {
"type": "number",
"description": "Sampling temperature",
"default": 0.7,
},
"verbose": {
"type": "boolean",
"description": "Enable verbose output",
"default": False,
},
"permissions.defaultMode": {
"type": "string",
"options": ["auto", "plan", "bypass"],
"description": "Default permissions mode",
"default": "auto",
},
"tools.enabled": {
"type": "array",
"items": {"type": "string"},
"description": "List of enabled tool names",
"default": [],
},
"tools.disabled": {
"type": "array",
"items": {"type": "string"},
"description": "List of disabled tool names",
"default": [],
},
}
class ConfigTool(BaseTool[dict[str, Any], dict[str, Any]]):
"""Runtime configuration management tool.
Supports get, set, list, and delete operations on the persistent config store.
"""
name = TOOL_NAME
description = "Get, set, list, or delete Stack 2.9 runtime configuration settings."
search_hint = "get or set configuration settings theme model permissions"
@property
def input_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["get", "set", "list", "delete"],
"description": "Configuration operation",
},
"key": {
"type": "string",
"description": "Configuration key (required for get/set/delete)",
},
"value": {
"description": "New value (required for 'set' operation)",
},
},
"required": ["operation"],
}
def validate_input(self, input_data: dict[str, Any]) -> tuple[bool, str | None]:
op = input_data.get("operation")
if op in ("get", "set", "delete") and not input_data.get("key"):
return False, f"Error: 'key' is required for '{op}' operation"
if op == "set" and "value" not in input_data:
return False, "Error: 'value' is required for 'set' operation"
return True, None
def execute(self, input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
op = input_data.get("operation")
key = input_data.get("key")
value = input_data.get("value")
if op == "get":
return self._get(key)
elif op == "set":
return self._set(key, value)
elif op == "list":
return self._list()
elif op == "delete":
return self._delete(key)
else:
return ToolResult(success=False, error=f"Unknown operation: {op}")
def _get(self, key: str | None) -> ToolResult[dict[str, Any]]:
if key is None:
return ToolResult(success=False, error="Key is required for 'get'")
config = _load_config()
current = config.get(key)
meta = SUPPORTED_KEYS.get(key, {})
return ToolResult(
success=True,
data={
"operation": "get",
"key": key,
"value": current if current is not None else meta.get("default"),
"default": meta.get("default"),
},
)
def _set(self, key: str | None, value: Any) -> ToolResult[dict[str, Any]]:
if key is None:
return ToolResult(success=False, error="Key is required for 'set'")
meta = SUPPORTED_KEYS.get(key)
if meta is not None:
expected_type = meta.get("type")
# Type coercion
if expected_type == "boolean":
if isinstance(value, str):
value = value.lower() in ("true", "1", "yes")
elif expected_type == "number":
try:
value = float(value)
except (ValueError, TypeError):
return ToolResult(
success=False,
error=f"Invalid number value for '{key}': {value}",
)
# Validate options
options = meta.get("options")
if options and value not in options:
return ToolResult(
success=False,
error=f"Invalid value '{value}' for '{key}'. Options: {', '.join(options)}",
)
config = _load_config()
previous = config.get(key)
config[key] = value
_save_config(config)
return ToolResult(
success=True,
data={
"operation": "set",
"key": key,
"previousValue": previous,
"newValue": value,
},
)
def _list(self) -> ToolResult[dict[str, Any]]:
config = _load_config()
meta = SUPPORTED_KEYS
items = []
all_keys = sorted(set(list(config.keys()) + list(meta.keys())))
for k in all_keys:
items.append({
"key": k,
"value": config.get(k, meta.get(k, {}).get("default")),
"description": meta.get(k, {}).get("description", ""),
"type": meta.get(k, {}).get("type", "unknown"),
"options": meta.get(k, {}).get("options"),
"is_default": k not in config,
})
return ToolResult(
success=True,
data={
"operation": "list",
"settings": items,
"total": len(items),
},
)
def _delete(self, key: str | None) -> ToolResult[dict[str, Any]]:
if key is None:
return ToolResult(success=False, error="Key is required for 'delete'")
config = _load_config()
if key not in config:
return ToolResult(success=False, error=f"Key '{key}' not found in config")
previous = config.pop(key)
_save_config(config)
return ToolResult(
success=True,
data={
"operation": "delete",
"key": key,
"previousValue": previous,
},
)
def map_result_to_message(self, result: dict, tool_use_id: str | None = None) -> str:
if not result.get("success", True):
return f"Error: {result.get('error')}"
data = result.get("data", {})
op = data.get("operation", "")
if op == "get":
val = data.get("value")
default = data.get("default")
note = f" (default: {default})" if default is not None and val == default else ""
return f"{data['key']} = {json.dumps(val)}{note}"
elif op == "set":
return f"Set {data['key']} = {json.dumps(data['newValue'])}"
elif op == "list":
settings = data.get("settings", [])
if not settings:
return "No configuration settings found."
lines = [f"{data['total']} setting(s):\n"]
for s in settings:
val = json.dumps(s["value"])
note = f" [{s['type']}]" if s["type"] != "unknown" else ""
if s["is_default"]:
note += " (default)"
lines.append(f" {s['key']:30} = {val}{note}")
return "\n".join(lines)
elif op == "delete":
return f"Deleted {data['key']} (was: {json.dumps(data['previousValue'])})"
return str(data)
# Auto-register
get_registry().register(ConfigTool())
|