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

@@ -25,7 +25,7 @@ from openai import AsyncOpenAI
from config import settings
from orchestrator_engine import OrchestratorResult
from tools import OPENAI_TOOL_SCHEMAS, call_tool
from tools import OPENAI_TOOL_SCHEMAS, call_tool, get_openai_tools_for_role, CONFIRM_REQUIRED
logger = logging.getLogger(__name__)
@@ -44,6 +44,7 @@ async def run(
session_messages: list[dict] | None = None,
model_cfg: dict | None = None,
respond_with_final: bool = True,
user_role: str = "user",
) -> OrchestratorResult:
"""
Run a tool-enabled task using an OpenAI-compatible API.
@@ -93,6 +94,9 @@ async def run(
)
messages.append({"role": "user", "content": task})
active_tools = get_openai_tools_for_role(user_role)
active_callables: dict | None = None # resolved lazily below
tool_call_log: list[dict] = []
final_response = ""
@@ -103,7 +107,7 @@ async def run(
response = await client.chat.completions.create(
model=model_name,
messages=messages,
tools=OPENAI_TOOL_SCHEMAS,
tools=active_tools,
tool_choice="auto",
)
@@ -130,39 +134,54 @@ async def run(
messages.append(assistant_msg)
if choice.finish_reason == "tool_calls" and msg.tool_calls:
# Execute all tool calls in parallel, then feed results back
tool_tasks = [
_execute_tool(tc.function.name, tc.function.arguments)
for tc in msg.tool_calls
]
results = await asyncio.gather(*tool_tasks, return_exceptions=True)
for tc, result in zip(msg.tool_calls, results):
result_str = (
str(result)
if not isinstance(result, Exception)
else f"Tool error: {result}"
)
logger.info("Tool %s%d chars", tc.function.name, len(result_str))
confirm_requested = False
for tc in msg.tool_calls:
name = tc.function.name
try:
args_parsed = json.loads(tc.function.arguments)
except json.JSONDecodeError:
args_parsed = {"raw": tc.function.arguments}
tool_call_log.append({
"tool": tc.function.name,
"args": args_parsed,
"result": result_str,
})
if name in CONFIRM_REQUIRED:
args_str = json.dumps(args_parsed, indent=2) if args_parsed else "(no arguments)"
result_str = (
f"⚠️ CONFIRMATION REQUIRED ⚠️\n"
f"Tool: {name}\nArguments:\n{args_str}\n\n"
f"Do NOT call this tool again. Tell the user exactly what you were "
f"about to do, explain the potential impact, and ask them to confirm "
f"by sending a follow-up message before you proceed."
)
confirm_requested = True
logger.info("Tool %s blocked — confirmation required", name)
else:
result_str = await _execute_tool(name, tc.function.arguments, user_role)
logger.info("Tool %s%d chars", name, len(result_str))
# Tool result message — tools array must be re-sent on every request
tool_call_log.append({
"tool": name,
"args": args_parsed,
"result": "[awaiting confirmation]" if name in CONFIRM_REQUIRED else result_str,
})
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result_str,
})
if confirm_requested:
# One more model round to produce the confirmation-request message, then stop.
conf_resp = await client.chat.completions.create(
model=model_name,
messages=messages,
tools=active_tools,
tool_choice="none",
)
final_response = conf_resp.choices[0].message.content or (
"This action requires your explicit confirmation before it can proceed."
)
break
else:
# finish_reason == "stop" (or no tool_calls) — model is done
final_response = msg.content or ""
@@ -194,14 +213,16 @@ async def run(
)
async def _execute_tool(name: str, arguments_json: str) -> str:
"""Parse tool arguments and execute, returning a string result."""
async def _execute_tool(name: str, arguments_json: str, user_role: str = "user") -> str:
"""Parse tool arguments and execute with role-filtered callables."""
from tools import get_tools_for_role
_, callables = get_tools_for_role(user_role)
try:
args = json.loads(arguments_json)
except json.JSONDecodeError:
args = {}
try:
return await call_tool(name, args)
return await call_tool(name, args, callables)
except Exception as e:
logger.warning("Tool %s failed: %s", name, e)
return f"Tool error: {e}"