| """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_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") |
| |
| 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}", |
| ) |
| |
| 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) |
|
|
|
|
| |
| get_registry().register(ConfigTool()) |
|
|