feat: run claude as non-root for bypassPermissions + replace MCP menu with live tools view

This commit is contained in:
Zac Gaetano 2026-04-05 12:26:37 -04:00
parent 004b1efdc6
commit 318e7f01ad

View file

@ -231,6 +231,7 @@ class ClaudeProcessManager:
self._broadcast_queues: List[asyncio.Queue] = []
self._is_ready = False
self._status = "not_started" # not_started, starting, ready, dead
self._available_tools: List[dict] = [] # populated from system/init event
@property
def status(self):
@ -249,17 +250,18 @@ class ClaudeProcessManager:
self._status = "starting"
logger.info("Starting Claude interactive process...")
# Run as non-root user so --permission-mode bypassPermissions is allowed
# (Claude CLI refuses bypassPermissions when running as root)
env = os.environ.copy()
env["HOME"] = "/home/claudeuser"
# Build command: interactive claude with stream-json I/O
# --permission-mode bypassPermissions allows the agent to use all tools
# (Bash, file ops, MCP servers) without interactive confirmation prompts
cmd = [
"claude",
"--output-format", "stream-json",
"--input-format", "stream-json",
"--verbose",
"--permission-mode", "bypassPermissions",
"su", "-s", "/bin/bash", "claudeuser", "-c",
"claude --output-format stream-json --input-format stream-json "
"--verbose --permission-mode bypassPermissions"
]
try:
@ -359,7 +361,8 @@ class ClaudeProcessManager:
if event_type == "system" and event.get("subtype") == "init":
self._current_session_id = event.get("session_id")
self._is_ready = True
logger.info(f"Claude session initialized: {self._current_session_id}")
self._available_tools = event.get("tools", [])
logger.info(f"Claude session initialized: {self._current_session_id}, tools: {len(self._available_tools)}")
elif event_type == "result":
# End of a response turn
@ -636,6 +639,29 @@ async def claude_status():
}
@app.get("/api/claude/tools")
async def claude_tools():
"""Return the tools available in the current Claude session (from system/init)."""
tools = claude_mgr._available_tools
# Group by MCP server prefix (e.g. "mcp__truenas__list_pools" -> "truenas")
servers: dict = {}
builtin = []
for t in tools:
name = t.get("name", "")
if name.startswith("mcp__"):
parts = name.split("__")
server = parts[1] if len(parts) > 1 else "unknown"
servers.setdefault(server, []).append(name)
else:
builtin.append(name)
return {
"total": len(tools),
"mcp_servers": {k: len(v) for k, v in servers.items()},
"builtin_tools": builtin,
"raw": tools,
}
# -------- Chat --------
@app.get("/api/chat/sessions")