- Add shell_exec to orchestrator tool suite (system.py + __init__.py) Runs arbitrary shell commands on the Cortex host with timeout (1–120s), combined stdout/stderr output, optional working_dir, and exit code reporting. Enables system diagnostics (df, ls, ps, journalctl, etc.) from Agent mode. - Fix orchestrator_engine.run() to use model_name from resolved registry entry Previously used settings.orchestrator_model (.env hardcode) regardless of what model was assigned to the orchestrator role. Now accepts model_name param and falls back to settings value only when registry has no model_name. - Update ARCH__FUTURE.md: date, running host, local orchestrator status, model registry V2 progress, added Cortex Mesh concept (section 9) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
2.7 KiB
Python
86 lines
2.7 KiB
Python
"""
|
|
System tools — local machine operations.
|
|
|
|
These tools affect the host system directly. Use with care.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ALLOW_SCRIPT = "/home/scott/.local/bin/claude-allow-dir"
|
|
|
|
|
|
async def claude_allow_dir(path: str, mode: str = "rw") -> str:
|
|
"""Add Read/Edit allow rules to ~/.claude/settings.json for a directory.
|
|
|
|
Calls the claude-allow-dir script, which edits settings.json directly.
|
|
Changes take effect in the next Claude Code session (or after /hooks reload).
|
|
"""
|
|
if mode not in ("r", "w", "rw"):
|
|
return f"Error: mode must be r, w, or rw (got '{mode}')"
|
|
|
|
try:
|
|
proc = await asyncio.create_subprocess_exec(
|
|
"python3", ALLOW_SCRIPT, path, mode,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=10)
|
|
output = stdout.decode().strip()
|
|
err = stderr.decode().strip()
|
|
|
|
if proc.returncode != 0:
|
|
logger.warning("claude-allow-dir failed (rc=%d): %s", proc.returncode, err)
|
|
return f"Failed (exit {proc.returncode}): {err or output}"
|
|
|
|
return output or "Done."
|
|
|
|
except asyncio.TimeoutError:
|
|
return "Error: script timed out"
|
|
except Exception as e:
|
|
logger.error("claude_allow_dir error: %s", e)
|
|
return f"Error: {e}"
|
|
|
|
|
|
async def shell_exec(command: str, working_dir: str | None = None, timeout: int = 30) -> str:
|
|
"""Execute a shell command on the Cortex host and return combined stdout/stderr."""
|
|
timeout = min(max(timeout, 1), 120)
|
|
|
|
cwd = None
|
|
if working_dir:
|
|
cwd = os.path.expanduser(working_dir)
|
|
if not os.path.isdir(cwd):
|
|
return f"Error: working_dir '{working_dir}' does not exist or is not a directory"
|
|
|
|
try:
|
|
proc = await asyncio.create_subprocess_shell(
|
|
command,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
cwd=cwd,
|
|
)
|
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
|
|
out = stdout.decode(errors="replace").strip()
|
|
err = stderr.decode(errors="replace").strip()
|
|
|
|
parts = []
|
|
if out:
|
|
parts.append(out)
|
|
if err:
|
|
parts.append(f"[stderr]\n{err}")
|
|
combined = "\n".join(parts) if parts else "(no output)"
|
|
|
|
if proc.returncode != 0:
|
|
return f"Exit {proc.returncode}:\n{combined}"
|
|
return combined
|
|
|
|
except asyncio.TimeoutError:
|
|
return f"Error: command timed out after {timeout}s"
|
|
except Exception as e:
|
|
logger.error("shell_exec error: %s", e)
|
|
return f"Error: {e}"
|