mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-18 10:57:19 +02:00
claude_desktop: add subscription-only Codex MCP delegation (#2911)
* 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>
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
|
||||
## 1.36 (27-07-2026)
|
||||
|
||||
- Add optional OpenAI Codex CLI support, so a Claude session in this add-on can delegate work to ChatGPT Codex. Three parts: an `install_codex_cli` switch, a browserless way to activate a ChatGPT subscription on it, and an MCP registration that makes Codex callable as a tool from Claude.
|
||||
- **Install (`install_codex_cli`, default off).** Codex is deliberately *not* baked into the image: the Linux release binary is ~310 MB extracted, which is not worth carrying in every installation for an off-by-default option, and updating it would then require an add-on rebuild. The new `81-codex_cli.sh` cont-init script downloads the pinned static-musl release (`ARG`/`ENV CODEX_VERSION`, currently `0.145.0`) into `/data/codex/bin` and symlinks it onto `PATH`. `/data` is the add-on's own persistent volume — independent of the configurable `data_location` — so the ~113 MB download happens once, not per boot and not again after an add-on update unless the pinned release changes. The install prefix is outside `$HOME` on purpose: the managed-MCP merge in `82-claude_tools.sh` treats any server `command` under `$HOME` as user-installed and refuses to manage it, so an add-on-owned binary living there could never be updated or removed by the add-on. The download fails open like the Claude Desktop update check — an offline boot, a GitHub outage or a bad pin logs a warning and leaves any existing binary in place instead of aborting startup, and the new binary is validated by actually running `--version` from the staging directory before it replaces the current one, so a truncated download can never overwrite a working install. Staging deliberately happens under `/data` rather than the default `/tmp`: `/tmp` in this image is a RAM-backed tmpfs, so holding the 110 MB archive plus the 310 MB binary there during boot is a real risk on a small Home Assistant host, and it is mounted `noexec`, which would make the validation step impossible anyway. Staging on the destination filesystem also turns the final move into an atomic rename instead of a second 310 MB copy. Disabling the option is non-destructive: the binary and the completed sign-in are kept, so a disable/re-enable cycle needs neither another download nor another login.
|
||||
- **Subscription activation (`codex-login`).** Codex's default sign-in serves an OAuth callback on `localhost:1455` and expects a local browser, which cannot work here — the image ships no browser, and the add-on is reached through Home Assistant ingress. The new `/usr/local/bin/codex-login` helper runs `codex login --device-auth` instead, the flow OpenAI documents for headless machines: it prints a verification URL and a short-lived one-time code to approve on any other device, then polls until done. It can be run from the desktop's xterm (openbox right-click menu), from a Claude Code session, or from the container console, and it drops to the `abc` runtime user first so `~/.codex/auth.json` is not created root-owned — the same failure mode `83-github_cli.sh` documents for `~/.gitconfig`. Output is line-buffered so the URL and code appear immediately even without a TTY. If a session is already authenticated the helper reports the account and exits rather than starting a second flow. Credentials live in the persistent home, so the sign-in survives restarts and updates.
|
||||
- **MCP bridge.** `codex mcp-server` exposes Codex itself as a stdio MCP server (tools `codex` and `codex-reply`, verified by driving the 0.145.0 binary over stdio), which is registered through the *existing* managed-MCP merge in `82-claude_tools.sh` rather than a second copy of that logic — so it inherits the merge's idempotence, its refusal to clobber a user-customized entry, its removal-when-disabled behaviour, and the `0600` re-tightening of the token-bearing config files. Registration covers both Claude Code (`~/.claude.json`) and Claude Desktop, so every session type gets the tool. A managed `CLAUDE.md` block tells Claude what Codex is and when a second agent is worth the round-trip, since registering a server without guidance rarely gets it used — the same gap the Headroom block exists to close.
|
||||
- **Sandbox (`codex_sandbox_mode`, default `danger-full-access`).** Codex normally confines itself with a Linux OS sandbox (Landlock, or its bundled bubblewrap), which is unreliable inside a Home Assistant add-on container — and the container is already the security boundary. The default therefore bypasses Codex's own sandbox; `read-only` and `workspace-write` remain selectable. The chosen mode is applied in two places that are kept in sync: as root-level `-c` config overrides in the MCP registration (Codex forwards those to the MCP server, and they must precede the subcommand), and as a managed block at the top of `~/.codex/config.toml` so plain terminal `codex`/`codex exec` runs behave the same. That block is written at position 0 rather than appended, because a bare TOML key placed after a `[table]` header would silently become a key *of that table*. `approval_policy` is always `never` — an MCP- or cron-driven run has nobody to answer a prompt. Claude Code's own permission prompts still gate every `mcp__codex__*` call unless `permission_mode` is `bypass`.
|
||||
- `84-claude_runtime_ownership.sh` now also reconciles `~/.codex`, and `claude-tools-doctor.sh` reports the Codex binary, the installed-versus-pinned version, the two new switches, and the login status (which prints the auth mode, never the token).
|
||||
|
||||
## 1.35 (23-07-2026)
|
||||
|
||||
- Fix recurring "For your security, sign in again to keep using Claude." and the Claude app's dispatch tab showing this desktop as offline when opened from mobile first. Root cause (confirmed from `~/.config/Claude/logs/main.log` on a live install): the app launches with `--password-store=gnome-libsecret`, forcing Electron's libsecret/Secret-Service backend, but `gnome-keyring` was removed from the image in a previous commit because it prompted for a keyring password on first boot and blocked the app from launching. With the flag still forcing libsecret and no keyring daemon running, `safeStorage.isEncryptionAvailable()` is `false` — logs showed `session will not persist; app secrets fall back to plaintext` and `cannot store allowlist cache`. The un-persisted session then goes stale, failing the elevated-access OAuth check (`session_stale_relogin`) that the cowork/dispatch bridge needs, so the bridge is "parked until re-login" — which is what the Claude app surfaces as the desktop being offline, until a fresh sign-in (only completable from a computer, see `SIGN_IN.md` Problem A) un-parks it. `rootfs/defaults/autostart` now launches with `--password-store=basic` instead: Electron's built-in fixed-key store needs no daemon and never prompts, and persists under `$HOME/.config/Claude` (`/data/data`, persistent), so the session survives restarts and dispatch stays online regardless of which device connects first. A passwordless keyring was considered and rejected — it would live in the same persistent volume as the ciphertext, adding no real protection in this single-user self-hosted setup. `Dockerfile`'s stale comment (still describing gnome-keyring as installed) is corrected; the package stays removed.
|
||||
|
||||
@@ -94,7 +94,7 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
|
||||
# cannot alter executables elsewhere in the image.
|
||||
COPY rootfs/ /
|
||||
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" \) -print -exec chmod +x {} \; && \
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" -o -name "codex-login" \) -print -exec chmod +x {} \; && \
|
||||
chmod +x /usr/local/bin/claude
|
||||
|
||||
# Uses /bin for compatibility purposes
|
||||
|
||||
@@ -78,6 +78,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
- Custom script support through the repository standard `claude_desktop.sh`.
|
||||
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
|
||||
available as an opt-in plugin.
|
||||
- Optional OpenAI Codex CLI, authenticated exclusively with a ChatGPT
|
||||
subscription and reachable from Claude through the native Codex MCP server.
|
||||
- Optional Home Assistant MCP bridge so Claude can query and control Home
|
||||
Assistant.
|
||||
- Independent hourly savings reports for Headroom, RTK, and TokenSave.
|
||||
@@ -105,6 +107,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
|
||||
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
|
||||
| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. |
|
||||
| `install_codex_cli` | `false` | Install the latest stable OpenAI Codex CLI at startup and register its native MCP server so Claude can delegate work to ChatGPT Codex. |
|
||||
| `codex_sandbox_mode` | `workspace-write` | Filesystem scope Codex runs with: `read-only`, `workspace-write`, or `danger-full-access`. |
|
||||
| `enable_tools_health_report` | `true` | Write independent Headroom, RTK, and TokenSave gains to the add-on log hourly. |
|
||||
| `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. |
|
||||
| `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. |
|
||||
@@ -195,6 +199,77 @@ The dashboard is disabled externally by default. To expose it:
|
||||
The dashboard is unauthenticated. Do not publish this port to the public
|
||||
internet.
|
||||
|
||||
## Codex CLI
|
||||
|
||||
Setting `install_codex_cli: true` adds OpenAI's Codex CLI alongside Claude and
|
||||
registers `codex mcp-server` in both Claude Code and Claude Desktop. A Claude
|
||||
session can therefore delegate a task to ChatGPT Codex and read its result back
|
||||
through MCP.
|
||||
|
||||
Codex is not baked into the image because its Linux binary is large and the
|
||||
feature is off by default. At each startup, the add-on resolves the latest
|
||||
stable upstream release. It downloads the architecture-specific binary into
|
||||
persistent `/data/codex/bin` only when the installed release is missing or
|
||||
outdated, verifies the GitHub-published SHA-256 digest before extraction or
|
||||
execution, validates the staged binary with `--version`, and replaces the
|
||||
existing binary atomically. If release metadata or the download is unavailable,
|
||||
startup continues and a previously working installation is retained.
|
||||
|
||||
### Signing in with a ChatGPT subscription
|
||||
|
||||
The add-on has no browser, so use the bundled device-code helper:
|
||||
|
||||
```bash
|
||||
codex-login
|
||||
```
|
||||
|
||||
Run it from the desktop's xterm, a Claude Code session, or the container
|
||||
console. It prints a verification URL and one-time code that you approve on
|
||||
another device. Credentials are stored in the runtime user's persistent
|
||||
`~/.codex/auth.json`, so the sign-in survives restarts and add-on updates.
|
||||
|
||||
This integration is deliberately **subscription-only**. The managed launcher
|
||||
removes any inherited `OPENAI_API_KEY` and starts every Codex command—including
|
||||
`codex mcp-server`—with:
|
||||
|
||||
```toml
|
||||
forced_login_method = "chatgpt"
|
||||
cli_auth_credentials_store = "file"
|
||||
```
|
||||
|
||||
The launcher also removes caller-provided overrides for those two keys before
|
||||
starting Codex. The same values are maintained in `~/.codex/config.toml`.
|
||||
Consequently, the MCP server uses the ChatGPT Codex entitlement and cannot
|
||||
silently fall back to usage-based OpenAI API-key billing.
|
||||
|
||||
### Using Codex from Claude
|
||||
|
||||
Claude receives two native MCP tools:
|
||||
|
||||
- `mcp__codex__codex` starts a task. Pass a self-contained `prompt` and set
|
||||
`cwd` to the repository Codex should inspect. The result includes a
|
||||
`threadId`.
|
||||
- `mcp__codex__codex-reply` continues the same Codex thread with its
|
||||
`threadId`.
|
||||
|
||||
The add-on also installs managed Claude guidance recommending Codex for
|
||||
independent review, a second diagnosis, or a competing implementation rather
|
||||
than routine lookups. Codex consumption counts against the signed-in ChatGPT
|
||||
plan's Codex allowance.
|
||||
|
||||
### Sandbox scope
|
||||
|
||||
`codex_sandbox_mode` defaults to `workspace-write`, allowing implementation
|
||||
inside the supplied repository without granting unrestricted access to every
|
||||
mounted path. Select `read-only` for review-only delegation. Use
|
||||
`danger-full-access` only as an explicit fallback when Codex's nested Linux
|
||||
sandbox is unavailable in the Home Assistant add-on container and the mounted
|
||||
paths are trusted.
|
||||
|
||||
`approval_policy` is always `never`, because an MCP-driven Codex process has no
|
||||
interactive operator to answer a prompt. Claude Code's own permissions still
|
||||
gate the `mcp__codex__*` call unless `permission_mode` is `bypass`.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Run the following inside the add-on through a custom script or container console:
|
||||
@@ -206,8 +281,8 @@ claude-tools-doctor.sh
|
||||
The report checks the tool binaries, configuration switches, configured and
|
||||
effective runtime identities, redacted MCP registrations, Claude hooks,
|
||||
permission mode, Headroom health, TokenSave indexes, routing, and recorded
|
||||
savings. It never prints MCP environment values because the Home Assistant MCP
|
||||
entry can contain a long-lived token.
|
||||
savings. It never prints MCP environment values or raw Codex authentication
|
||||
status because either can contain credentials or masked credential fragments.
|
||||
|
||||
The hourly report can also be invoked manually:
|
||||
|
||||
@@ -288,6 +363,8 @@ Persistent state is stored in the configured `data_location` (default
|
||||
shared home
|
||||
- TokenSave repository indexes: `.tokensave/` inside each explicitly configured
|
||||
project
|
||||
- Codex authentication and configuration: `~/.codex`; the verified executable
|
||||
and subscription-only launcher live in persistent `/data/codex/bin`
|
||||
|
||||
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
|
||||
`$HOME/.cache`.
|
||||
|
||||
@@ -59,6 +59,8 @@ options:
|
||||
headroom_auto_compress: true
|
||||
headroom_wrap_claude_code: true
|
||||
install_caveman: false
|
||||
install_codex_cli: false
|
||||
codex_sandbox_mode: workspace-write
|
||||
install_github_cli: true
|
||||
install_headroom: true
|
||||
install_rtk: true
|
||||
@@ -107,6 +109,8 @@ schema:
|
||||
headroom_auto_compress: bool?
|
||||
headroom_wrap_claude_code: bool
|
||||
install_caveman: bool
|
||||
install_codex_cli: bool
|
||||
codex_sandbox_mode: list(read-only|workspace-write|danger-full-access)
|
||||
install_github_cli: bool
|
||||
install_headroom: bool
|
||||
install_rtk: bool
|
||||
@@ -118,5 +122,5 @@ slug: claude_desktop
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.35"
|
||||
version: "1.36"
|
||||
video: true
|
||||
|
||||
321
claude_desktop/rootfs/etc/cont-init.d/81-codex_cli.sh
Executable file
321
claude_desktop/rootfs/etc/cont-init.d/81-codex_cli.sh
Executable file
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# OpenAI Codex CLI, installed on demand rather than baked into the image: the Linux release
|
||||
# binary is large and the option is off by default. Runs before 82-claude_tools.sh so the binary
|
||||
# exists when that script registers the `codex` MCP server.
|
||||
#
|
||||
# The install prefix is /data/codex, NOT $HOME/.codex/bin: /data is persistent regardless of the
|
||||
# configurable data_location, and the managed MCP merge treats commands under $HOME as
|
||||
# user-installed. Codex state (auth.json, config.toml) remains in the runtime user's home.
|
||||
CODEX_ROOT="/data/codex"
|
||||
CODEX_PREFIX="${CODEX_ROOT}/bin"
|
||||
CODEX_BIN="${CODEX_PREFIX}/codex"
|
||||
CODEX_REAL="${CODEX_PREFIX}/codex-real"
|
||||
CODEX_STAMP="${CODEX_PREFIX}/.version"
|
||||
CODEX_LINK="/usr/local/bin/codex"
|
||||
CODEX_RELEASE_API="https://api.github.com/repos/openai/codex/releases/latest"
|
||||
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
bashio::log.warning "Unable to resolve the abc runtime home; using /data/data"
|
||||
RUNTIME_HOME="/data/data"
|
||||
fi
|
||||
|
||||
run_as_runtime_user() {
|
||||
s6-setuidgid abc env HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" "$@"
|
||||
}
|
||||
|
||||
if ! bashio::config.true 'install_codex_cli'; then
|
||||
# Non-destructive: preserve the binary and completed ChatGPT sign-in for a later re-enable.
|
||||
# 82-claude_tools.sh removes only the MCP registration and managed guidance.
|
||||
bashio::log.info "Codex CLI disabled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64) CODEX_TARGET="x86_64-unknown-linux-musl" ;;
|
||||
aarch64 | arm64) CODEX_TARGET="aarch64-unknown-linux-musl" ;;
|
||||
*)
|
||||
bashio::log.warning "Codex CLI has no release binary for $(uname -m); skipping"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
CODEX_ASSET="codex-${CODEX_TARGET}.tar.gz"
|
||||
mkdir -p "$CODEX_PREFIX"
|
||||
|
||||
# Migrate the PR's earlier direct-binary layout to the enforced wrapper layout without another
|
||||
# download. The real binary is kept separately; `codex` becomes a small launcher that always
|
||||
# forces ChatGPT subscription authentication and removes any inherited API key.
|
||||
if [ ! -x "$CODEX_REAL" ] \
|
||||
&& [ -x "$CODEX_BIN" ] \
|
||||
&& run_as_runtime_user "$CODEX_BIN" --version > /dev/null 2>&1; then
|
||||
mv -f "$CODEX_BIN" "$CODEX_REAL"
|
||||
fi
|
||||
|
||||
# Resolve the latest stable release and its GitHub-published SHA-256 digest on every boot. This
|
||||
# follows upstream updates without pinning a version, while downloading the large asset only when
|
||||
# the installed version changes. A metadata outage never replaces or removes a working binary.
|
||||
codex_tmp="$(mktemp -d -p "$CODEX_ROOT")"
|
||||
cleanup() {
|
||||
rm -rf "$codex_tmp"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
release_metadata="${codex_tmp}/release.json"
|
||||
release_info=""
|
||||
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 30 \
|
||||
-o "$release_metadata" "$CODEX_RELEASE_API"; then
|
||||
release_info="$(
|
||||
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" <<'PY' 2> /dev/null || true
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
metadata = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
tag = metadata.get("tag_name", "")
|
||||
if not isinstance(tag, str) or not tag.startswith("rust-v"):
|
||||
raise SystemExit("unexpected release tag")
|
||||
version = tag.removeprefix("rust-v")
|
||||
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)(?:\.[0-9]+){0,2})?", version):
|
||||
raise SystemExit("unexpected release version")
|
||||
|
||||
asset_name = os.environ["CODEX_ASSET"]
|
||||
asset = next(
|
||||
(
|
||||
item
|
||||
for item in metadata.get("assets", [])
|
||||
if isinstance(item, dict) and item.get("name") == asset_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if asset is None:
|
||||
raise SystemExit("release asset missing")
|
||||
digest = asset.get("digest", "")
|
||||
if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-fA-F]{64}", digest):
|
||||
raise SystemExit("release asset has no valid SHA-256 digest")
|
||||
url = asset.get("browser_download_url", "")
|
||||
if not isinstance(url, str) or not url.startswith("https://github.com/openai/codex/releases/download/"):
|
||||
raise SystemExit("unexpected release asset URL")
|
||||
|
||||
print(f"{version}\t{digest.removeprefix('sha256:').lower()}\t{url}")
|
||||
PY
|
||||
)"
|
||||
fi
|
||||
|
||||
if [ -z "$release_info" ]; then
|
||||
if [ -x "$CODEX_REAL" ] && run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
|
||||
bashio::log.warning "Unable to resolve the latest verified Codex release; keeping the existing install"
|
||||
else
|
||||
bashio::log.warning "Unable to resolve the latest verified Codex release; Codex is unavailable this boot"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
IFS=$'\t' read -r CODEX_WANTED CODEX_SHA256 CODEX_URL <<< "$release_info"
|
||||
|
||||
if [ -x "$CODEX_REAL" ] \
|
||||
&& [ "$(cat "$CODEX_STAMP" 2> /dev/null || true)" = "$CODEX_WANTED" ] \
|
||||
&& run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
|
||||
bashio::log.info "Codex CLI ${CODEX_WANTED} already installed (latest stable)"
|
||||
else
|
||||
bashio::log.info "Installing latest stable Codex CLI ${CODEX_WANTED} (${CODEX_TARGET}); this is a large one-time download"
|
||||
archive="${codex_tmp}/${CODEX_ASSET}"
|
||||
extracted="${codex_tmp}/codex-${CODEX_TARGET}"
|
||||
|
||||
# Fail open for add-on startup but fail closed for the candidate binary: its official
|
||||
# release digest must match before extraction or execution, and replacement happens only
|
||||
# after the staged binary successfully runs.
|
||||
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 600 \
|
||||
-o "$archive" "$CODEX_URL" \
|
||||
&& printf '%s %s\n' "$CODEX_SHA256" "$archive" | sha256sum -c - > /dev/null \
|
||||
&& tar -xzf "$archive" -C "$codex_tmp" \
|
||||
&& [ -f "$extracted" ] \
|
||||
&& chmod 0755 "$extracted" \
|
||||
&& run_as_runtime_user "$extracted" --version > /dev/null 2>&1 \
|
||||
&& mv -f "$extracted" "$CODEX_REAL"; then
|
||||
printf '%s' "$CODEX_WANTED" > "$CODEX_STAMP"
|
||||
bashio::log.info "Codex CLI installed: $("$CODEX_REAL" --version 2> /dev/null || echo unknown)"
|
||||
elif [ -x "$CODEX_REAL" ]; then
|
||||
bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; keeping the existing install"
|
||||
else
|
||||
bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; Codex is unavailable this boot"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$CODEX_REAL" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Every Codex entry point, including the MCP server launched by Claude, goes through this wrapper.
|
||||
# This is an execution-time guarantee in addition to the managed config below: API-key billing
|
||||
# cannot be selected even if an API key is present in the surrounding environment. Caller-provided
|
||||
# overrides for the two authentication guards are stripped before the forced root-level overrides
|
||||
# are inserted; root-level -c flags must precede Codex subcommands such as `mcp-server`.
|
||||
{
|
||||
printf '#!/usr/bin/env bash\n'
|
||||
printf 'CODEX_REAL=%q\n' "$CODEX_REAL"
|
||||
cat <<'SH'
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
echo "codex: unable to resolve the abc runtime home" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
is_managed_override() {
|
||||
local assignment="$1"
|
||||
local key="${assignment%%=*}"
|
||||
key="${key//[[:space:]]/}"
|
||||
case "$key" in
|
||||
forced_login_method | cli_auth_credentials_store) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
filtered_args=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-c | --config)
|
||||
if [ "$#" -lt 2 ]; then
|
||||
filtered_args+=("$1")
|
||||
shift
|
||||
continue
|
||||
fi
|
||||
if is_managed_override "$2"; then
|
||||
shift 2
|
||||
continue
|
||||
fi
|
||||
filtered_args+=("$1" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--config=*)
|
||||
assignment="${1#--config=}"
|
||||
if ! is_managed_override "$assignment"; then
|
||||
filtered_args+=("$1")
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-c*)
|
||||
assignment="${1#-c}"
|
||||
if ! is_managed_override "$assignment"; then
|
||||
filtered_args+=("$1")
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
filtered_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
forced_args=(
|
||||
-c 'forced_login_method="chatgpt"'
|
||||
-c 'cli_auth_credentials_store="file"'
|
||||
)
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
exec s6-setuidgid abc env -u OPENAI_API_KEY \
|
||||
HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" \
|
||||
"$CODEX_REAL" "${forced_args[@]}" "${filtered_args[@]}"
|
||||
fi
|
||||
|
||||
unset OPENAI_API_KEY
|
||||
export HOME="$RUNTIME_HOME"
|
||||
export CODEX_HOME="$RUNTIME_HOME/.codex"
|
||||
exec "$CODEX_REAL" "${forced_args[@]}" "${filtered_args[@]}"
|
||||
SH
|
||||
} > "$CODEX_BIN"
|
||||
chmod 0755 "$CODEX_BIN"
|
||||
|
||||
chown -R -- "$(id -u abc):$(id -g abc)" "$CODEX_ROOT" \
|
||||
|| bashio::log.warning "Unable to set ownership on ${CODEX_ROOT}"
|
||||
ln -sfn "$CODEX_BIN" "$CODEX_LINK"
|
||||
|
||||
# Manage the root-level defaults used by terminal Codex and by `codex mcp-server`.
|
||||
#
|
||||
# `forced_login_method = "chatgpt"` makes subscription authentication the only permitted login
|
||||
# method, so an inherited OPENAI_API_KEY cannot silently switch this integration to API billing.
|
||||
# File storage is explicit because the container has no supported OS keyring.
|
||||
#
|
||||
# The managed block must be first: a bare TOML key after a [table] header belongs to that table.
|
||||
# Existing top-level definitions of the managed keys are removed before insertion; retaining them
|
||||
# would create duplicate keys and make the entire Codex configuration invalid.
|
||||
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'workspace-write')"
|
||||
run_as_runtime_user mkdir -p "$RUNTIME_HOME/.codex"
|
||||
CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" RUNTIME_HOME="$RUNTIME_HOME" \
|
||||
run_as_runtime_user python3 - <<'PY' \
|
||||
|| bashio::log.warning "Unable to update the managed Codex configuration block"
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
BEGIN = "# BEGIN managed by claude_desktop addon"
|
||||
END = "# END managed by claude_desktop addon"
|
||||
MANAGED_KEYS = {
|
||||
"sandbox_mode",
|
||||
"approval_policy",
|
||||
"forced_login_method",
|
||||
"cli_auth_credentials_store",
|
||||
}
|
||||
|
||||
block = "\n".join(
|
||||
[
|
||||
BEGIN,
|
||||
"# Managed defaults for terminal and MCP-driven Codex runs.",
|
||||
f'sandbox_mode = "{os.environ["CODEX_SANDBOX_MODE"]}"',
|
||||
'approval_policy = "never"',
|
||||
"# Require ChatGPT subscription OAuth; do not fall back to API-key billing.",
|
||||
'forced_login_method = "chatgpt"',
|
||||
"# This container has no supported OS keyring; keep OAuth credentials in auth.json.",
|
||||
'cli_auth_credentials_store = "file"',
|
||||
END,
|
||||
]
|
||||
)
|
||||
|
||||
path = Path(os.environ["RUNTIME_HOME"]) / ".codex" / "config.toml"
|
||||
original = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
rest = re.sub(
|
||||
rf"{re.escape(BEGIN)}.*?{re.escape(END)}\n?",
|
||||
"",
|
||||
original,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
table_header = re.compile(r"^\s*\[\[?.+?\]\]?\s*(?:#.*)?$")
|
||||
assignment = re.compile(
|
||||
r'''^\s*(?P<key>[A-Za-z0-9_-]+|"[^"]+"|'[^']+')\s*='''
|
||||
)
|
||||
kept = []
|
||||
at_top_level = True
|
||||
for line in rest.splitlines(keepends=True):
|
||||
if at_top_level and table_header.match(line):
|
||||
at_top_level = False
|
||||
match = assignment.match(line) if at_top_level else None
|
||||
if match:
|
||||
key = match.group("key")
|
||||
if key[:1] in {'"', "'"}:
|
||||
key = key[1:-1]
|
||||
if key in MANAGED_KEYS:
|
||||
continue
|
||||
kept.append(line)
|
||||
|
||||
remainder = "".join(kept).lstrip("\n")
|
||||
new = block + "\n" + (("\n" + remainder) if remainder else "")
|
||||
tomllib.loads(new)
|
||||
if new != original:
|
||||
path.write_text(new, encoding="utf-8")
|
||||
path.chmod(0o600)
|
||||
PY
|
||||
|
||||
if [ -f "$RUNTIME_HOME/.codex/auth.json" ]; then
|
||||
bashio::log.info "Codex CLI is signed in with stored ChatGPT credentials"
|
||||
else
|
||||
bashio::log.info "Codex CLI is not signed in yet; run 'codex-login' to activate your ChatGPT subscription"
|
||||
fi
|
||||
@@ -293,6 +293,20 @@ if $TOKENSAVE_ENABLED; then
|
||||
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
||||
fi
|
||||
|
||||
# Codex CLI is installed by 81-codex_cli.sh into /data/codex/bin — deliberately outside $HOME,
|
||||
# because is_managed() below treats any command under $HOME as user-installed.
|
||||
CODEX_BIN="/data/codex/bin/codex"
|
||||
CODEX_ENABLED=false
|
||||
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'workspace-write')"
|
||||
if bashio::config.true 'install_codex_cli'; then
|
||||
if [ -x "$CODEX_BIN" ]; then
|
||||
CODEX_ENABLED=true
|
||||
bashio::log.info "codex $("$CODEX_BIN" --version 2> /dev/null || true) available; registering the codex MCP server (sandbox: ${CODEX_SANDBOX_MODE})"
|
||||
else
|
||||
bashio::log.warning "codex is not available"
|
||||
fi
|
||||
fi
|
||||
|
||||
HA_MCP_ENABLED=false
|
||||
HA_MCP_URL=""
|
||||
HA_MCP_TOKEN=""
|
||||
@@ -314,6 +328,7 @@ fi
|
||||
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
|
||||
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||
@@ -326,6 +341,7 @@ MANAGED_BASENAMES = {
|
||||
"headroom": "headroom",
|
||||
"tokensave": "tokensave",
|
||||
"homeassistant": "mcp-proxy",
|
||||
"codex": "codex",
|
||||
}
|
||||
|
||||
desired = {}
|
||||
@@ -341,6 +357,25 @@ if os.environ["HEADROOM_ENABLED"] == "true":
|
||||
}
|
||||
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
||||
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
|
||||
if os.environ["CODEX_ENABLED"] == "true":
|
||||
# `codex mcp-server` exposes Codex itself as an stdio MCP server (tools: codex, codex-reply),
|
||||
# which is what lets a Claude session hand a task to ChatGPT Codex. The sandbox/approval
|
||||
# policy is pinned with root-level `-c` overrides, which Codex forwards to the MCP server;
|
||||
# they must precede the subcommand. approval_policy is always "never" because an MCP-driven
|
||||
# run has nobody to answer a prompt. The sandbox defaults to workspace-write; users can opt
|
||||
# into danger-full-access explicitly if the nested sandbox is unavailable in their container.
|
||||
# 81-codex_cli.sh writes the same values into ~/.codex/config.toml so plain terminal `codex`
|
||||
# runs behave identically.
|
||||
desired["codex"] = {
|
||||
"command": os.environ["CODEX_BIN"],
|
||||
"args": [
|
||||
"-c",
|
||||
f'sandbox_mode="{os.environ["CODEX_SANDBOX_MODE"]}"',
|
||||
"-c",
|
||||
'approval_policy="never"',
|
||||
"mcp-server",
|
||||
],
|
||||
}
|
||||
if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
# Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp;
|
||||
# mcp-proxy defaults to SSE, so the transport flags are required.
|
||||
@@ -531,6 +566,32 @@ else
|
||||
manage_claude_md_block ha-api-helper remove
|
||||
fi
|
||||
|
||||
# Registering the MCP server is not enough on its own: without guidance the model rarely reaches
|
||||
# for a second agent, the same gap the Headroom block above exists to close.
|
||||
if $CODEX_ENABLED; then
|
||||
manage_claude_md_block codex add <<'MD'
|
||||
## Delegating to ChatGPT Codex
|
||||
|
||||
The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT
|
||||
subscription. It is a genuinely independent second agent — a different model family, reading the
|
||||
files itself — not a search tool. It is slow and costs the user's ChatGPT quota, so use it when a
|
||||
second opinion is worth minutes, not for routine lookups.
|
||||
|
||||
Good uses: an independent review of a design or a risky change before it lands; a second
|
||||
diagnosis of a bug you have a theory about but cannot confirm; a competing implementation of a
|
||||
self-contained piece you can then compare against your own.
|
||||
|
||||
Call `mcp__codex__codex` with `prompt` and always set `cwd` to the repository being discussed —
|
||||
Codex reads the files itself, so it needs the right working directory and enough context in the
|
||||
prompt to act without seeing this conversation. Continue an exchange with
|
||||
`mcp__codex__codex-reply` (note the hyphen) using the `threadId` it returned, rather than
|
||||
starting a fresh `codex` call. Treat its answers as a peer's opinion: verify claims about this
|
||||
codebase before acting on them.
|
||||
MD
|
||||
else
|
||||
manage_claude_md_block codex remove
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
if command -v rtk &> /dev/null; then
|
||||
bashio::log.info "Configuring rtk Claude Code integration"
|
||||
|
||||
@@ -2,14 +2,22 @@
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
|
||||
# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
|
||||
# root). Reconcile ownership with that identity after all Claude configuration writes are
|
||||
# complete, as a safety net in case any intermediate step re-owned a managed path.
|
||||
# Earlier configuration scripts intentionally run as root. Reconcile the paths written by those
|
||||
# scripts with the final abc runtime identity and its configured persistent home.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
|
||||
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
bashio::log.warning "Unable to resolve the abc runtime home; skipping runtime ownership reconciliation"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for managed_path in \
|
||||
"$RUNTIME_HOME/.claude" \
|
||||
"$RUNTIME_HOME/.claude.json" \
|
||||
"$RUNTIME_HOME/.config/Claude" \
|
||||
"$RUNTIME_HOME/.codex"; do
|
||||
if [ -e "$managed_path" ]; then
|
||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \
|
||||
|| bashio::log.warning "Unable to set effective runtime ownership on $managed_path"
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
|
||||
# printing MCP environment values (which may contain the Home Assistant access token).
|
||||
# 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 git gh rg jq shellcheck yamllint hadolint actionlint; do
|
||||
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"
|
||||
@@ -22,13 +27,14 @@ for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck
|
||||
done
|
||||
|
||||
section "Configured switches"
|
||||
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do
|
||||
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
|
||||
@@ -39,11 +45,12 @@ if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
||||
fi
|
||||
|
||||
section "Claude Code permission state"
|
||||
python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
@@ -59,13 +66,15 @@ else:
|
||||
PY
|
||||
|
||||
section "MCP registrations (environment values redacted)"
|
||||
python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
home = Path(os.environ["RUNTIME_HOME"])
|
||||
paths = [
|
||||
Path.home() / ".claude.json",
|
||||
Path.home() / ".config/Claude/claude_desktop_config.json",
|
||||
home / ".claude.json",
|
||||
home / ".config/Claude/claude_desktop_config.json",
|
||||
]
|
||||
for path in paths:
|
||||
print(path)
|
||||
@@ -95,11 +104,12 @@ for path in paths:
|
||||
PY
|
||||
|
||||
section "Claude Code hooks"
|
||||
python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
@@ -148,18 +158,17 @@ 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: feeding the
|
||||
# loop straight from `< <(bashio::config ...)` yields an empty list under errexit.
|
||||
# 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="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
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="$HOME" tokensave status "$repo_root" --short || true
|
||||
s6-setuidgid abc env HOME="$RUNTIME_HOME" tokensave status "$repo_root" --short || true
|
||||
else
|
||||
echo "${repo_root}: NOT INITIALIZED"
|
||||
fi
|
||||
@@ -168,6 +177,45 @@ 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)"
|
||||
|
||||
48
claude_desktop/rootfs/usr/local/bin/codex-login
Executable file
48
claude_desktop/rootfs/usr/local/bin/codex-login
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Activate a ChatGPT subscription on the Codex CLI from inside the add-on.
|
||||
#
|
||||
# The add-on ships no browser, so the normal `codex login` flow cannot complete here.
|
||||
# `codex login --device-auth` prints a verification URL and one-time code that can be
|
||||
# approved on another device. Credentials persist in the abc runtime user's home.
|
||||
# shellcheck shell=bash
|
||||
set -o pipefail
|
||||
|
||||
CODEX_BIN="/data/codex/bin/codex"
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
echo "codex-login: unable to resolve the abc runtime home" >&2
|
||||
exit 1
|
||||
fi
|
||||
export HOME="$RUNTIME_HOME"
|
||||
export CODEX_HOME="$RUNTIME_HOME/.codex"
|
||||
|
||||
if ! bashio::config.true 'install_codex_cli'; then
|
||||
echo "codex-login: the install_codex_cli add-on option is disabled" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$CODEX_BIN" ]; then
|
||||
echo "codex-login: ${CODEX_BIN} is not installed; enable install_codex_cli and restart the add-on" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Everything runs as abc so auth.json is readable by the same account that launches the MCP
|
||||
# server. Explicit HOME handling also makes container-console calls safe when the caller is root.
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
exec s6-setuidgid abc env -u OPENAI_API_KEY HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" "$0" "$@"
|
||||
fi
|
||||
|
||||
# The managed Codex config forces `chatgpt` authentication and file credential storage. An
|
||||
# inherited API key is removed as defense in depth so this helper cannot activate API billing.
|
||||
unset OPENAI_API_KEY
|
||||
|
||||
# Branch on the exit code, not the text: "Not logged in" also contains "logged in".
|
||||
if "$CODEX_BIN" login status; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "codex-login: starting ChatGPT subscription device-code sign-in."
|
||||
echo "codex-login: open the URL below on any device and enter the displayed code."
|
||||
|
||||
# Line-buffered so the URL and code appear while Codex is still polling in non-TTY callers.
|
||||
exec stdbuf -oL -eL "$CODEX_BIN" login --device-auth "$@"
|
||||
Reference in New Issue
Block a user