feat: add shell_exec tool and fix orchestrator model name resolution
- 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>
This commit is contained in:
@@ -57,6 +57,7 @@ async def run(
|
||||
session_messages: list[dict] | None = None,
|
||||
respond_with_claude: bool = True,
|
||||
gemini_api_key: str | None = None,
|
||||
model_name: str | None = None,
|
||||
) -> OrchestratorResult:
|
||||
"""
|
||||
Run the full orchestration loop for a task.
|
||||
@@ -96,7 +97,7 @@ async def run(
|
||||
|
||||
response = await asyncio.to_thread(
|
||||
client.models.generate_content,
|
||||
model=settings.orchestrator_model,
|
||||
model=model_name or settings.orchestrator_model,
|
||||
contents=contents,
|
||||
config=types.GenerateContentConfig(
|
||||
tools=TOOL_DECLARATIONS,
|
||||
|
||||
@@ -183,6 +183,7 @@ async def _run_job(job_id: str, req: OrchestrateRequest, user: str) -> None:
|
||||
session_messages=session_messages,
|
||||
respond_with_claude=req.respond_with_claude,
|
||||
gemini_api_key=gemini_key,
|
||||
model_name=orch_model.get("model_name") if orch_model else None,
|
||||
)
|
||||
|
||||
# Save the turn to the session store so it survives a page refresh
|
||||
|
||||
@@ -20,7 +20,7 @@ from tools.ae_knowledge import journal_search as _ae_journal_search
|
||||
from tools.ae_knowledge import journal_entry_create as _ae_journal_entry_create
|
||||
from tools.ae_tasks import task_list as _ae_task_list
|
||||
from tools.files import file_read as _file_read
|
||||
from tools.system import claude_allow_dir as _claude_allow_dir
|
||||
from tools.system import claude_allow_dir as _claude_allow_dir, shell_exec as _shell_exec
|
||||
from tools.tasks import task_list as _task_list, task_create as _task_create
|
||||
from tools.tasks import task_update as _task_update, task_complete as _task_complete
|
||||
from tools.cron import (
|
||||
@@ -192,6 +192,7 @@ _CALLABLES: dict[str, callable] = {
|
||||
"ae_task_list": _ae_task_list,
|
||||
"file_read": _file_read,
|
||||
"claude_allow_dir": _claude_allow_dir,
|
||||
"shell_exec": _shell_exec,
|
||||
"task_list": _task_list,
|
||||
"task_create": _task_create,
|
||||
"task_update": _task_update,
|
||||
@@ -236,6 +237,35 @@ _claude_allow_dir_declaration = types.FunctionDeclaration(
|
||||
),
|
||||
)
|
||||
|
||||
_shell_exec_declaration = types.FunctionDeclaration(
|
||||
name="shell_exec",
|
||||
description=(
|
||||
"Execute a shell command on the Cortex host machine and return its output. "
|
||||
"Use for system diagnostics: disk usage (df -h), process status (ps aux), "
|
||||
"directory listings (ls), memory (free -h), uptime, network info, log tails, etc. "
|
||||
"Commands run as the Cortex service user. Timeout enforced (default 30s, max 120s). "
|
||||
"Avoid destructive commands — prefer read-only system queries."
|
||||
),
|
||||
parameters=types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"command": types.Schema(
|
||||
type=types.Type.STRING,
|
||||
description="Shell command to run (e.g. 'df -h', 'ls ~/agents_sync/', 'journalctl --user -u cortex -n 50')",
|
||||
),
|
||||
"working_dir": types.Schema(
|
||||
type=types.Type.STRING,
|
||||
description="Optional working directory (e.g. '~/agents_sync/projects'). Defaults to home directory.",
|
||||
),
|
||||
"timeout": types.Schema(
|
||||
type=types.Type.INTEGER,
|
||||
description="Timeout in seconds (default 30, max 120)",
|
||||
),
|
||||
},
|
||||
required=["command"],
|
||||
),
|
||||
)
|
||||
|
||||
_task_list_declaration = types.FunctionDeclaration(
|
||||
name="task_list",
|
||||
description=(
|
||||
@@ -526,6 +556,7 @@ TOOL_DECLARATIONS = [
|
||||
_ae_task_list_declaration,
|
||||
_file_read_declaration,
|
||||
_claude_allow_dir_declaration,
|
||||
_shell_exec_declaration,
|
||||
_task_list_declaration,
|
||||
_task_create_declaration,
|
||||
_task_update_declaration,
|
||||
|
||||
@@ -6,6 +6,7 @@ These tools affect the host system directly. Use with care.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,3 +43,43 @@ async def claude_allow_dir(path: str, mode: str = "rw") -> str:
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user