fix(codex): provision dummy user auth state

This commit is contained in:
2026-05-29 03:46:15 -04:00
committed by didericis
parent 62dd7b2aa5
commit a6332b9535
15 changed files with 406 additions and 31 deletions
+6
View File
@@ -286,6 +286,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
intercepted without per-tool reconfiguration."""
self.provision_ca(plan, target)
prompt_path = self.provision_prompt(plan, target)
self.provision_provider_auth(plan, target)
self.provision_skills(plan, target)
self.provision_git(plan, target)
self.provision_supervise(plan, target)
@@ -300,6 +301,11 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
backend overrides to docker-cp the cert in and run
`update-ca-certificates`."""
def provision_provider_auth(self, plan: PlanT, target: str) -> None:
"""Install non-secret provider auth marker files into the agent
home when a provider needs them to select the right auth mode.
The default is no-op."""
@abstractmethod
def provision_prompt(self, plan: PlanT, target: str) -> str | None:
"""Copy the prompt file into the running bottle. Returns the
+4
View File
@@ -29,6 +29,7 @@ from .bottle_plan import DockerBottlePlan
from .provision import ca as _ca
from .provision import git as _git
from .provision import prompt as _prompt
from .provision import provider_auth as _provider_auth
from .provision import skills as _skills
from .provision import supervise as _supervise_prov
@@ -62,6 +63,9 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
def provision_prompt(self, plan: DockerBottlePlan, target: str) -> str | None:
return _prompt.provision_prompt(plan, target)
def provision_provider_auth(self, plan: DockerBottlePlan, target: str) -> None:
_provider_auth.provision_provider_auth(plan, target)
def provision_skills(self, plan: DockerBottlePlan, target: str) -> None:
_skills.provision_skills(plan, target)
+1
View File
@@ -55,6 +55,7 @@ class DockerBottlePlan(BottlePlan):
agent_command: str = "claude"
agent_prompt_mode: PromptMode = "append_file"
agent_provider_template: str = "claude"
codex_auth_file: Path | None = None
def print(self, *, remote_control: bool) -> None:
"""Render the y/N preflight summary to stderr — compact form
+5
View File
@@ -15,6 +15,7 @@ from datetime import datetime, timezone
from pathlib import Path
from ...agent_provider import runtime_for
from ...codex_auth import write_codex_dummy_auth_file
from ...egress import Egress
from ...env import ResolvedEnv, resolve_env
from ...git_gate import GitGate
@@ -155,6 +156,7 @@ def resolve_plan(
agent_dir.mkdir(parents=True, exist_ok=True)
env_file = agent_dir / "agent.env"
prompt_file = agent_dir / "prompt.txt"
codex_auth_file = agent_dir / "codex-auth.json"
prompt_file.write_text("")
prompt_file.chmod(0o600)
@@ -219,6 +221,8 @@ def resolve_plan(
# error reporting) that egress can't gate by auth.
forwarded_env.setdefault("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1")
forwarded_env.setdefault("DISABLE_ERROR_REPORTING", "1")
if provider.forward_host_credentials:
write_codex_dummy_auth_file(codex_auth_file, dict(os.environ))
_write_env_file(resolved, env_file)
prompt_file.write_text(agent.prompt)
@@ -245,6 +249,7 @@ def resolve_plan(
agent_command=provider_runtime.command,
agent_prompt_mode=provider_runtime.prompt_mode,
agent_provider_template=provider.template,
codex_auth_file=codex_auth_file if provider.forward_host_credentials else None,
)
@@ -0,0 +1,43 @@
"""Provision non-secret provider auth markers into a Docker bottle."""
from __future__ import annotations
import os
import subprocess
from ..bottle_plan import DockerBottlePlan
def provision_provider_auth(plan: DockerBottlePlan, target: str) -> None:
"""Copy a dummy Codex auth marker when host credentials are
forwarded through egress.
The file contains no real access or refresh token values; it only
nudges Codex into the same user/device auth branch as the host.
"""
if not plan.codex_auth_file:
return
container_home = os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node")
auth_dir = f"{container_home}/.codex"
auth_path = f"{auth_dir}/auth.json"
subprocess.run(
["docker", "exec", "-u", "0", target, "mkdir", "-p", auth_dir],
stdout=subprocess.DEVNULL,
check=True,
)
subprocess.run(
["docker", "cp", str(plan.codex_auth_file), f"{target}:{auth_path}"],
stdout=subprocess.DEVNULL,
check=True,
)
subprocess.run(
["docker", "exec", "-u", "0", target, "chown", "node:node", auth_path],
stdout=subprocess.DEVNULL,
check=True,
)
subprocess.run(
["docker", "exec", "-u", "0", target, "chmod", "600", auth_path],
stdout=subprocess.DEVNULL,
check=True,
)
@@ -19,6 +19,7 @@ from .bottle_plan import SmolmachinesBottlePlan
from .provision import ca as _ca
from .provision import git as _git
from .provision import prompt as _prompt
from .provision import provider_auth as _provider_auth
from .provision import skills as _skills
from .provision import supervise as _supervise
@@ -61,6 +62,11 @@ class SmolmachinesBottleBackend(
) -> str | None:
return _prompt.provision_prompt(plan, target)
def provision_provider_auth(
self, plan: SmolmachinesBottlePlan, target: str
) -> None:
_provider_auth.provision_provider_auth(plan, target)
def provision_skills(
self, plan: SmolmachinesBottlePlan, target: str
) -> None:
@@ -97,6 +97,7 @@ class SmolmachinesBottlePlan(BottlePlan):
agent_prompt_mode: PromptMode = "append_file"
agent_provider_template: str = "claude"
agent_dockerfile_path: str = ""
codex_auth_file: Path | None = None
def print(self, *, remote_control: bool) -> None:
"""Compact y/N preflight. Same shape as the Docker
@@ -16,6 +16,7 @@ from pathlib import Path
from ...agent_provider import runtime_for
from ...backend import BottleSpec
from ...codex_auth import write_codex_dummy_auth_file
from ...backend.docker.bottle_state import (
BottleMetadata,
agent_state_dir,
@@ -144,9 +145,12 @@ def resolve_plan(
agent_dir = agent_state_dir(slug)
agent_dir.mkdir(parents=True, exist_ok=True)
prompt_file = agent_dir / "prompt.txt"
codex_auth_file = agent_dir / "codex-auth.json"
agent = manifest.agents[spec.agent_name]
prompt_file.write_text(agent.prompt or "")
prompt_file.chmod(0o600)
if provider.forward_host_credentials:
write_codex_dummy_auth_file(codex_auth_file, dict(os.environ))
machine_name = f"bot-bottle-{slug}"
# Stash the agent image ref — `launch.launch` runs the
@@ -182,6 +186,7 @@ def resolve_plan(
agent_prompt_mode=provider_runtime.prompt_mode,
agent_provider_template=provider.template,
agent_dockerfile_path=agent_dockerfile_path,
codex_auth_file=codex_auth_file if provider.forward_host_credentials else None,
)
@@ -0,0 +1,30 @@
"""Provision non-secret provider auth markers into a smolmachines bottle."""
from __future__ import annotations
import os
from .. import smolvm as _smolvm
from ..bottle_plan import SmolmachinesBottlePlan
_DEFAULT_GUEST_HOME = "/home/node"
def provision_provider_auth(plan: SmolmachinesBottlePlan, target: str) -> None:
"""Copy a dummy Codex auth marker when host credentials are
forwarded through egress.
The real host access token remains in the egress bundle env; this
file only selects Codex's user/device auth code path.
"""
if not plan.codex_auth_file:
return
guest_home = os.environ.get("BOT_BOTTLE_GUEST_HOME", _DEFAULT_GUEST_HOME)
auth_dir = f"{guest_home}/.codex"
auth_path = f"{auth_dir}/auth.json"
_smolvm.machine_exec(target, ["mkdir", "-p", auth_dir])
_smolvm.machine_cp(str(plan.codex_auth_file), f"{target}:{auth_path}")
_smolvm.machine_exec(target, ["chown", "node:node", auth_path])
_smolvm.machine_exec(target, ["chmod", "600", auth_path])
+84 -9
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import base64
import json
import os
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path
@@ -37,16 +38,12 @@ def codex_host_access_token(
"Run `codex login --device-auth` on the host or disable "
"agent_provider.forward_host_credentials."
)
try:
raw = json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as e:
die(f"codex host credentials: could not read valid JSON at {path}: {e}")
if not isinstance(raw, dict):
die(f"codex host credentials: {path} must contain a JSON object")
raw = _read_auth_object(path)
if raw.get("auth_mode") != "chatgpt":
auth_mode = raw.get("auth_mode")
if not isinstance(auth_mode, str) or auth_mode == "api_key":
die(
"codex host credentials: host Codex auth is not ChatGPT/device "
"codex host credentials: host Codex auth is not user/device "
"auth. Run `codex login --device-auth` on the host."
)
@@ -72,6 +69,79 @@ def codex_host_access_token(
return access
def codex_dummy_auth_json(
host_env: dict[str, str] | None = None,
*,
now: datetime | None = None,
) -> str:
"""Return a non-secret `auth.json` that keeps Codex in the host's
auth branch while egress owns the real bearer token."""
path = codex_auth_path(host_env)
codex_host_access_token(host_env, now=now)
raw = _read_auth_object(path)
dummy = _redact_codex_auth(deepcopy(raw), now=now)
return json.dumps(dummy, indent=2, sort_keys=True) + "\n"
def write_codex_dummy_auth_file(
path: Path,
host_env: dict[str, str] | None = None,
*,
now: datetime | None = None,
) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(codex_dummy_auth_json(host_env, now=now))
path.chmod(0o600)
def _read_auth_object(path: Path) -> dict:
try:
raw = json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as e:
die(f"codex host credentials: could not read valid JSON at {path}: {e}")
if not isinstance(raw, dict):
die(f"codex host credentials: {path} must contain a JSON object")
return raw
def _dummy_jwt(now: datetime | None = None) -> str:
check_now = now or datetime.now(timezone.utc)
exp = int(check_now.timestamp()) + 3600
def enc(obj: dict) -> str:
raw = json.dumps(obj, separators=(",", ":")).encode()
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
return (
f"{enc({'alg': 'none', 'typ': 'JWT'})}."
f"{enc({'exp': exp, 'sub': 'bot-bottle-placeholder'})}."
"placeholder"
)
def _redact_codex_auth(value: object, *, now: datetime | None = None) -> object:
if isinstance(value, dict):
out: dict[str, object] = {}
for key, inner in value.items():
lower = key.lower()
if lower == "openai_api_key":
out[key] = None
elif lower == "tokens":
out[key] = _redact_codex_auth(inner, now=now)
elif lower in {"access_token", "id_token"}:
out[key] = _dummy_jwt(now)
elif "token" in lower or "secret" in lower or lower.endswith("_key"):
out[key] = "bot-bottle-placeholder"
elif lower in {"account_id", "user_id", "email"}:
out[key] = "bot-bottle-placeholder"
else:
out[key] = _redact_codex_auth(inner, now=now)
return out
if isinstance(value, list):
return [_redact_codex_auth(v, now=now) for v in value]
return value
def _jwt_exp(token: str) -> datetime | None:
parts = token.split(".")
if len(parts) < 2:
@@ -93,4 +163,9 @@ def _b64url_decode(value: str) -> str:
return base64.urlsafe_b64decode(padded.encode("ascii")).decode("utf-8")
__all__ = ["codex_auth_path", "codex_host_access_token"]
__all__ = [
"codex_auth_path",
"codex_dummy_auth_json",
"codex_host_access_token",
"write_codex_dummy_auth_file",
]