mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-25 22:33:33 +02:00
* claude_desktop: add optional Codex CLI with device-code login and MCP bridge Adds OpenAI's Codex CLI to the add-on as an opt-in fourth tool, so a Claude session can delegate work to ChatGPT Codex as an independent second agent. Install (install_codex_cli, default off): Codex is deliberately not baked into the image -- its Linux binary is ~310 MB extracted, which is not worth carrying in every installation for an off-by-default option, and updating it would then need an add-on rebuild. A new 81-codex_cli.sh downloads the pinned static-musl release (ENV CODEX_VERSION) into /data/codex/bin instead. That prefix is outside $HOME on purpose: the managed-MCP merge treats any command under $HOME as user-installed and refuses to manage it. Staging happens under /data rather than the default /tmp, which here is a RAM-backed tmpfs mounted noexec -- holding 420 MB there during boot is a risk on a small host, and the binary could not be verified there at all. The download fails open like the Claude Desktop update check and validates the new binary by running it before replacing the old one. Login (codex-login): Codex's default sign-in serves an OAuth callback on localhost:1455 and expects a local browser, which cannot work in this add-on. The helper runs `codex login --device-auth` instead -- the flow OpenAI documents for headless machines -- printing a URL and one-time code to approve elsewhere. It drops to the abc runtime user first so auth.json is not created root-owned. MCP (codex mcp-server): registered through the existing managed-MCP merge rather than a second copy of it, so it inherits that code's idempotence, no-clobber and removal-when-disabled behaviour. A managed CLAUDE.md block explains when a second agent is worth the round-trip. New codex_sandbox_mode (default danger-full-access) is applied both as -c overrides on the MCP command and as a managed block at the top of ~/.codex/config.toml; Codex's own Landlock/bubblewrap sandbox is unreliable inside the container, which is already the security boundary. Verified against the real 0.145.0 binary: tools/list returns `codex` and `codex-reply` (hyphen, not the underscore upstream docs report), an invalid -c sandbox_mode is rejected by name, the installer lifecycle behaves correctly on re-run and on a bad pin, and the device code is flushed within seconds while still polling, which is the non-TTY case that matters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * claude_desktop: harden Codex subscription MCP setup * claude_desktop: use runtime home for Codex login * claude_desktop: reconcile runtime user home ownership * claude_desktop: report verified Codex subscription setup * claude_desktop: track latest Codex at runtime * claude_desktop: document subscription-only Codex MCP * claude_desktop: enforce Codex runtime identity * claude_desktop: persist Codex in runtime home * claude_desktop: prevent Codex auth override bypass * claude_desktop: default Codex to workspace write * claude_desktop: redact Codex authentication diagnostics * claude_desktop: document safer Codex MCP defaults * claude_desktop: validate Codex candidate as runtime user * claude_desktop: align Codex sandbox fallback --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
227 lines
8.0 KiB
Bash
Executable File
227 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/with-contenv bashio
|
|
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
|
|
# printing MCP environment values or authentication material.
|
|
# shellcheck shell=bash
|
|
set +e
|
|
set -o pipefail
|
|
export NO_COLOR=1
|
|
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
|
|
|
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
|
if [ -z "$RUNTIME_HOME" ]; then
|
|
RUNTIME_HOME="/data/data"
|
|
fi
|
|
|
|
section() {
|
|
printf '\n=== %s ===\n' "$1"
|
|
}
|
|
|
|
section "Installed binaries"
|
|
for tool in claude claude-desktop headroom rtk tokensave codex git gh rg jq shellcheck yamllint hadolint actionlint; do
|
|
resolved="$(command -v "$tool" 2> /dev/null || true)"
|
|
if [ -n "$resolved" ]; then
|
|
printf '%-16s %s\n' "$tool" "$resolved"
|
|
else
|
|
printf '%-16s %s\n' "$tool" "MISSING"
|
|
fi
|
|
done
|
|
|
|
section "Configured switches"
|
|
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_codex_cli codex_sandbox_mode install_caveman enable_tools_health_report; do
|
|
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
|
|
done
|
|
|
|
section "Runtime identity"
|
|
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
|
|
printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)"
|
|
printf '%-30s %s\n' "abc runtime home" "$RUNTIME_HOME"
|
|
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
|
|
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
|
if [ "$(id -u abc)" -eq 0 ]; then
|
|
echo "bypass runtime: ERROR - Claude Code will reject bypass permissions while abc is root"
|
|
else
|
|
echo "bypass runtime: OK - Claude Desktop and Cowork run as a non-root UID"
|
|
fi
|
|
fi
|
|
|
|
section "Claude Code permission state"
|
|
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
|
|
try:
|
|
data = json.loads(path.read_text())
|
|
except FileNotFoundError:
|
|
print("settings: MISSING")
|
|
except Exception as exc:
|
|
print(f"settings: INVALID: {exc}")
|
|
else:
|
|
permissions = data.get("permissions", {})
|
|
if isinstance(permissions, dict):
|
|
print(f"permissions.defaultMode: {permissions.get('defaultMode', '<upstream default>')}")
|
|
else:
|
|
print("permissions: INVALID")
|
|
PY
|
|
|
|
section "MCP registrations (environment values redacted)"
|
|
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
home = Path(os.environ["RUNTIME_HOME"])
|
|
paths = [
|
|
home / ".claude.json",
|
|
home / ".config/Claude/claude_desktop_config.json",
|
|
]
|
|
for path in paths:
|
|
print(path)
|
|
try:
|
|
data = json.loads(path.read_text())
|
|
except FileNotFoundError:
|
|
print(" MISSING")
|
|
continue
|
|
except Exception as exc:
|
|
print(f" INVALID: {exc}")
|
|
continue
|
|
servers = data.get("mcpServers", {})
|
|
if not isinstance(servers, dict) or not servers:
|
|
print(" no MCP servers")
|
|
continue
|
|
for name, spec in sorted(servers.items()):
|
|
if not isinstance(spec, dict):
|
|
print(f" {name}: invalid entry")
|
|
continue
|
|
command = spec.get("command", "?")
|
|
args = spec.get("args", [])
|
|
server_type = spec.get("type", "")
|
|
suffix = f" type={server_type}" if server_type else ""
|
|
print(f" {name}: {command} {args}{suffix}")
|
|
if spec.get("env"):
|
|
print(" env: <redacted>")
|
|
PY
|
|
|
|
section "Claude Code hooks"
|
|
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
|
|
try:
|
|
data = json.loads(path.read_text())
|
|
except FileNotFoundError:
|
|
print("MISSING")
|
|
raise SystemExit(0)
|
|
except Exception as exc:
|
|
print(f"INVALID: {exc}")
|
|
raise SystemExit(0)
|
|
|
|
hooks = data.get("hooks", {})
|
|
if not isinstance(hooks, dict) or not hooks:
|
|
print("no hooks")
|
|
raise SystemExit(0)
|
|
for event, entries in hooks.items():
|
|
print(event)
|
|
if not isinstance(entries, list):
|
|
print(" invalid entries")
|
|
continue
|
|
for entry in entries:
|
|
matcher = entry.get("matcher", "*") if isinstance(entry, dict) else "?"
|
|
commands = entry.get("hooks", []) if isinstance(entry, dict) else []
|
|
rendered = []
|
|
for command in commands if isinstance(commands, list) else []:
|
|
if isinstance(command, dict):
|
|
rendered.append(" ".join([str(command.get("command", "?")), *map(str, command.get("args", []))]))
|
|
print(f" matcher={matcher}: {', '.join(rendered) or 'no command'}")
|
|
PY
|
|
|
|
section "Headroom"
|
|
if bashio::config.true 'install_headroom'; then
|
|
curl -fsS --max-time 2 http://127.0.0.1:8787/health && echo || echo "proxy health: FAILED"
|
|
headroom mcp status || true
|
|
headroom savings || true
|
|
else
|
|
echo "disabled"
|
|
fi
|
|
|
|
section "RTK"
|
|
if bashio::config.true 'install_rtk'; then
|
|
rtk gain || true
|
|
else
|
|
echo "disabled"
|
|
fi
|
|
|
|
section "TokenSave"
|
|
if bashio::config.true 'install_tokensave'; then
|
|
tokensave doctor --agent claude || true
|
|
tokensave gain --all --range 30d || true
|
|
# Capture before looping — see the matching comment in 82-claude_tools.sh.
|
|
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
|
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
|
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
|
continue
|
|
fi
|
|
repo_root="$(s6-setuidgid abc env HOME="$RUNTIME_HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
|
if [ -z "$repo_root" ]; then
|
|
echo "${configured_path}: not a Git repository"
|
|
elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then
|
|
s6-setuidgid abc env HOME="$RUNTIME_HOME" tokensave status "$repo_root" --short || true
|
|
else
|
|
echo "${repo_root}: NOT INITIALIZED"
|
|
fi
|
|
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
|
else
|
|
echo "disabled"
|
|
fi
|
|
|
|
section "Codex"
|
|
if bashio::config.true 'install_codex_cli'; then
|
|
codex_bin="/data/codex/bin/codex"
|
|
if [ -x "$codex_bin" ]; then
|
|
printf '%-30s %s\n' "installed" "$("$codex_bin" --version 2> /dev/null || echo 'FAILED TO RUN')"
|
|
printf '%-30s %s\n' "installed version stamp" "$(cat /data/codex/bin/.version 2> /dev/null || echo 'MISSING')"
|
|
printf '%-30s %s\n' "release policy" "latest stable, SHA-256 verified"
|
|
printf '%-30s %s\n' "authentication policy" "ChatGPT subscription only"
|
|
|
|
# Never forward raw `login status` output: non-ChatGPT modes can include masked secret
|
|
# fragments. Only print explicitly allow-listed states.
|
|
codex_status="$(
|
|
s6-setuidgid abc env -u OPENAI_API_KEY \
|
|
HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" \
|
|
"$codex_bin" login status 2>&1
|
|
)"
|
|
codex_status_rc=$?
|
|
case "$codex_status" in
|
|
*"Logged in using ChatGPT"*)
|
|
echo "Logged in using ChatGPT"
|
|
;;
|
|
*"Not logged in"*)
|
|
echo "Not logged in; run 'codex-login' to activate a ChatGPT subscription"
|
|
;;
|
|
*)
|
|
if [ "$codex_status_rc" -eq 0 ]; then
|
|
echo "Authenticated with a non-ChatGPT method; run 'codex-login' to enforce subscription authentication"
|
|
else
|
|
echo "Unable to determine Codex login status safely; run 'codex-login'"
|
|
fi
|
|
;;
|
|
esac
|
|
else
|
|
echo "enabled but ${codex_bin} is MISSING (download failed or add-on not yet restarted)"
|
|
fi
|
|
else
|
|
echo "disabled"
|
|
fi
|
|
|
|
section "Claude routing"
|
|
printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)"
|
|
printf 'real claude: %s\n' "$([ -x /usr/bin/claude ] && echo /usr/bin/claude || echo MISSING)"
|
|
if bashio::config.true 'headroom_wrap_claude_code'; then
|
|
echo "PATH-based Claude Code launches are configured for Headroom wrapping."
|
|
else
|
|
echo "Claude Code Headroom wrapping is disabled; Headroom remains available through MCP."
|
|
fi
|