Stack-2-9-finetuned / src /tools /config_tool.py
walidsobhie-code
Fix tool parameter signatures and improve consistency
359acf6
"""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())