container: Forward env vars by prefix and source shell config from container-home

Replace hardcoded env var lists with prefix-based forwarding (OPENCODE_*,
ANTHROPIC_*, CLAUDE_*) so new env vars are picked up automatically.

Run tools through bash -l inside the container so that .bashrc, .bash_profile,
and .profile from container-home are sourced. Seed container-home with default
shell config from /etc/skel on first run if the files don't already exist.
This commit is contained in:
Jeena 2026-03-31 04:26:20 +00:00
parent d6f179ec57
commit a3db357b6f
2 changed files with 51 additions and 26 deletions

View file

@ -35,9 +35,16 @@ The container home directory at `$XDG_DATA_HOME/agent-container/container-home/`
## Environment Variables ## Environment Variables
Host environment variables matching the following prefixes are automatically forwarded into the container:
- `OPENCODE_*` -- passed through to OpenCode
- `ANTHROPIC_*`, `CLAUDE_*` -- passed through to Claude Code
This means inline overrides work as expected: `OPENCODE_CONFIG=foo opencode`
For persistent environment variables, add them to `container-home/.bashrc` (or `.bash_profile` / `.profile`). Tools run through `bash -l` inside the container, so standard shell config files are sourced automatically. Default shell config files from `/etc/skel` are seeded into the container home on first run.
- `XDG_DATA_HOME`: Override default data directory (default: `~/.local/share`) - `XDG_DATA_HOME`: Override default data directory (default: `~/.local/share`)
- `ANTHROPIC_API_KEY`: Your Anthropic API key (passed through to Claude Code)
- `ANTHROPIC_BASE_URL`: Override the API base URL (optional, passed through to Claude Code)
## Usage ## Usage

View file

@ -3,6 +3,7 @@
import hashlib import hashlib
import logging import logging
import os import os
import shlex
import signal import signal
import subprocess import subprocess
import sys import sys
@ -69,23 +70,13 @@ class AgentContainer:
self._run_tool( self._run_tool(
"opencode", "opencode",
args, args,
env_vars=[ env_prefixes=["OPENCODE_"],
"OPENCODE_SERVER_PASSWORD",
"OPENCODE_SERVER_USERNAME",
"OPENCODE_CONFIG",
"OPENCODE_CONFIG_DIR",
"OPENCODE_CONFIG_CONTENT",
"OPENCODE_TUI_CONFIG",
],
) )
elif command == "claude": elif command == "claude":
self._run_tool( self._run_tool(
"claude", "claude",
args, args,
env_vars=[ env_prefixes=["ANTHROPIC_", "CLAUDE_"],
"ANTHROPIC_API_KEY",
"ANTHROPIC_BASE_URL",
],
extra_env={"DISABLE_AUTOUPDATER": "1"}, extra_env={"DISABLE_AUTOUPDATER": "1"},
) )
elif command == "update": elif command == "update":
@ -105,7 +96,7 @@ class AgentContainer:
self, self,
tool: str, tool: str,
args: list[str], args: list[str],
env_vars: list[str], env_prefixes: list[str],
extra_env: dict[str, str] | None = None, extra_env: dict[str, str] | None = None,
) -> None: ) -> None:
self.container_home_path.mkdir(parents=True, exist_ok=True) self.container_home_path.mkdir(parents=True, exist_ok=True)
@ -143,27 +134,53 @@ class AgentContainer:
if not self.start_container(): if not self.start_container():
raise RuntimeError("Container failed to start after recreation") raise RuntimeError("Container failed to start after recreation")
# Seed container home with default shell config from /etc/skel
# (only copies files that don't already exist)
self._seed_home()
try: try:
signal.signal(signal.SIGTSTP, signal.SIG_IGN) signal.signal(signal.SIGTSTP, signal.SIG_IGN)
self._exec_tool(tool, args, env_vars, extra_env or {}) self._exec_tool(tool, args, env_prefixes, extra_env or {})
finally: finally:
signal.signal(signal.SIGTSTP, signal.SIG_DFL) signal.signal(signal.SIGTSTP, signal.SIG_DFL)
self.stop_container() self.stop_container()
def _seed_home(self) -> None:
"""Copy default shell config from /etc/skel into the container
home directory, skipping files that already exist."""
subprocess.run(
[
"docker",
"exec",
self.container_name,
"bash",
"-c",
'for f in /etc/skel/.*; do '
'[ -f "$f" ] && [ ! -e "$HOME/$(basename "$f")" ] && '
'cp "$f" "$HOME/"; done',
],
)
def _exec_tool( def _exec_tool(
self, tool: str, args: list[str], env_vars: list[str], extra_env: dict[str, str] self,
tool: str,
args: list[str],
env_prefixes: list[str],
extra_env: dict[str, str],
) -> None: ) -> None:
env_args: list[str] = [] env_args: list[str] = []
# Pass through host environment variables if set # Pass through host environment variables matching any prefix
for var in env_vars: for key, val in os.environ.items():
val = os.environ.get(var) if any(key.startswith(prefix) for prefix in env_prefixes):
if val: env_args += ["-e", f"{key}={val}"]
env_args += ["-e", f"{var}={val}"]
# Set additional environment variables # Set additional environment variables
for var, val in extra_env.items(): for key, val in extra_env.items():
env_args += ["-e", f"{var}={val}"] env_args += ["-e", f"{key}={val}"]
# Build the shell command with proper quoting
cmd_str = " ".join(shlex.quote(a) for a in [tool, *args])
result = subprocess.run( result = subprocess.run(
[ [
@ -174,8 +191,9 @@ class AgentContainer:
"-w", "-w",
str(self.project_path), str(self.project_path),
self.container_name, self.container_name,
tool, "bash",
*args, "-lc",
f"exec {cmd_str}",
], ],
) )
if result.returncode != 0: if result.returncode != 0: