feat: role-based tool access, confirmation gates, and new orchestrator tools

- auth_utils: get_user_role() reads role from auth.json (admin|user, default user)
- manage_passwords: new `role` command to promote/demote users (admin-only by convention)
- tools/__init__: TOOL_ROLES map, CONFIRM_REQUIRED set, get_tools_for_role(),
  get_openai_tools_for_role() — both orchestrators now filter tools by caller's role
- tools/system: cortex_restart (detached subprocess, 5s delay), cortex_logs (admin-only)
- tools/web: http_fetch — direct URL fetch, distinct from web_search
- tools/files: file_list (directory listing), file_write (restricted paths, admin-only)
- tools/notify: nc_talk_send — proactive outbound via notification.py
- orchestrator_engine + openai_orchestrator: user_role param; CONFIRM_REQUIRED tools
  return a confirmation-request result instead of executing — loop breaks after Claude
  asks user to confirm in a follow-up message
- home/scott/auth.json: role set to admin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-29 19:23:53 -04:00
parent 1603ad5124
commit 334e7f0dea
10 changed files with 581 additions and 87 deletions

View File

@@ -18,7 +18,7 @@ from datetime import datetime, timezone
from fastapi import APIRouter
from pydantic import BaseModel
from auth_utils import get_user_gemini_key
from auth_utils import get_user_gemini_key, get_user_role
from config import settings
from context_loader import load_context
from persona import set_context, validate as validate_persona
@@ -163,6 +163,8 @@ async def _run_job(job_id: str, req: OrchestrateRequest, user: str) -> None:
# Choose engine based on the orchestrator role in the model registry
orch_model = model_registry.get_model_for_role(user, "orchestrator")
user_role = get_user_role(user)
if orch_model and orch_model.get("type") == "local_openai":
result = await openai_orchestrator.run(
task=req.task,
@@ -170,6 +172,7 @@ async def _run_job(job_id: str, req: OrchestrateRequest, user: str) -> None:
session_messages=session_messages,
model_cfg=orch_model,
respond_with_final=req.respond_with_claude,
user_role=user_role,
)
else:
# Use the API key embedded in the resolved model config (V2 registry with
@@ -186,6 +189,7 @@ async def _run_job(job_id: str, req: OrchestrateRequest, user: str) -> None:
gemini_api_key=gemini_key,
model_name=orch_model.get("model_name") if orch_model else None,
response_role=req.chat_role,
user_role=user_role,
)
# Save the turn to the session store so it survives a page refresh