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:
Scott Idem
2026-04-28 20:29:46 -04:00
parent 8baab874f1
commit 1cc7988953
5 changed files with 101 additions and 6 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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}"