fix: repair codex resume paths
lint / lint (push) Successful in 1m55s
test / unit (push) Successful in 57s
test / integration (push) Successful in 19s
test / coverage (push) Successful in 1m6s
Update Quality Badges / update-badges (push) Successful in 1m1s

This commit is contained in:
2026-07-08 12:38:15 -04:00
parent 1085a2280d
commit 76fdefded3
8 changed files with 113 additions and 13 deletions
+14
View File
@@ -47,10 +47,24 @@ _HOME_FOR = {
"root": "/root", "root": "/root",
} }
_DEFAULT_PATH_FOR = {
# Committed smolmachine snapshots are rebuilt from a rootfs tarball and
# lose Docker image ENV metadata. Restore the provider CLI path here so
# resumed Codex bottles can still find the per-user install.
"node": (
"/home/node/.local/bin:"
"/home/node/.codex/packages/standalone/current/bin:"
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
),
"root": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
}
def _env_assignments_for(user: str, env: Mapping[str, str]) -> list[str]: def _env_assignments_for(user: str, env: Mapping[str, str]) -> list[str]:
home = _HOME_FOR.get(user, f"/home/{user}") home = _HOME_FOR.get(user, f"/home/{user}")
out = [f"HOME={home}", f"USER={user}"] out = [f"HOME={home}", f"USER={user}"]
if "PATH" not in env:
out.append(f"PATH={_DEFAULT_PATH_FOR.get(user, _DEFAULT_PATH_FOR['root'])}")
for k, v in env.items(): for k, v in env.items():
out.append(f"{k}={v}") out.append(f"{k}={v}")
return out return out
+10 -3
View File
@@ -34,6 +34,12 @@ if TYPE_CHECKING:
_SUPERVISE_MCP_NAME = "supervise" _SUPERVISE_MCP_NAME = "supervise"
_CODEX_CLI = "/home/node/.codex/packages/standalone/current/bin/codex"
_CODEX_CLI_PATH = (
"/home/node/.local/bin:"
"/home/node/.codex/packages/standalone/current/bin:"
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
)
def _skills_dir(guest_home: str) -> str: def _skills_dir(guest_home: str) -> str:
@@ -50,7 +56,7 @@ def _prompt_path(guest_home: str) -> str:
_RUNTIME = AgentProviderRuntime( _RUNTIME = AgentProviderRuntime(
template="codex", template="codex",
command="codex", command=_CODEX_CLI,
image="bot-bottle-codex:latest", image="bot-bottle-codex:latest",
prompt_mode="read_prompt_file", prompt_mode="read_prompt_file",
bypass_args=("--dangerously-bypass-approvals-and-sandbox",), bypass_args=("--dangerously-bypass-approvals-and-sandbox",),
@@ -145,7 +151,8 @@ class CodexAgentProvider(AgentProvider):
"env", "env",
f"HOME={guest_home}", f"HOME={guest_home}",
f"CODEX_HOME={auth_dir}", f"CODEX_HOME={auth_dir}",
"codex", "login", "status", f"PATH={_CODEX_CLI_PATH}",
_CODEX_CLI, "login", "status",
), ( ), (
"codex host credentials: dummy auth was copied into the " "codex host credentials: dummy auth was copied into the "
"guest, but Codex did not accept it" "guest, but Codex did not accept it"
@@ -267,7 +274,7 @@ class CodexAgentProvider(AgentProvider):
return return
info(f"registering supervise MCP server in agent codex config → {supervise_url}") info(f"registering supervise MCP server in agent codex config → {supervise_url}")
r = bottle.exec( r = bottle.exec(
f"codex mcp add {_SUPERVISE_MCP_NAME} --url " f"{shlex.quote(_CODEX_CLI)} mcp add {_SUPERVISE_MCP_NAME} --url "
f"{shlex.quote(supervise_url)}", f"{shlex.quote(supervise_url)}",
user="node", user="node",
) )
+12 -3
View File
@@ -228,7 +228,7 @@ supervise_gitleaks_allow() {
fi fi
proposal_id=$( proposal_id=$(
GITLEAKS_ALLOW_REF="$ref" python3 - "$report_file" <<'PY' PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}" GITLEAKS_ALLOW_REF="$ref" python3 - "$report_file" <<'PY'
import datetime import datetime
import hashlib import hashlib
import json import json
@@ -236,6 +236,9 @@ import os
import sys import sys
from pathlib import Path from pathlib import Path
try:
import supervise as _sv
except ImportError:
from bot_bottle import supervise as _sv from bot_bottle import supervise as _sv
report_path = Path(sys.argv[1]) report_path = Path(sys.argv[1])
@@ -314,9 +317,12 @@ PY
echo "git-gate: approve with './cli.py supervise' to continue this push" >&2 echo "git-gate: approve with './cli.py supervise' to continue this push" >&2
waited=0 waited=0
while [ "$waited" -lt "$timeout" ]; do while [ "$waited" -lt "$timeout" ]; do
status=$(python3 - "$slug" "$proposal_id" <<'PY' status=$(PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}" python3 - "$slug" "$proposal_id" <<'PY'
import sys import sys
try:
import supervise as _sv
except ImportError:
from bot_bottle import supervise as _sv from bot_bottle import supervise as _sv
slug = sys.argv[1] slug = sys.argv[1]
@@ -336,9 +342,12 @@ PY
if [ -n "$status" ]; then if [ -n "$status" ]; then
case "$status" in case "$status" in
approved|modified) approved|modified)
python3 - "$slug" "$proposal_id" <<'PY' || true PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}" python3 - "$slug" "$proposal_id" <<'PY' || true
import sys import sys
try:
import supervise as _sv
except ImportError:
from bot_bottle import supervise as _sv from bot_bottle import supervise as _sv
_sv.archive_proposal(sys.argv[1], sys.argv[2]) _sv.archive_proposal(sys.argv[1], sys.argv[2])
+4 -1
View File
@@ -35,7 +35,10 @@ class TestAgentProviderRuntime(unittest.TestCase):
) )
config = Path(tmp, "codex-config.toml").read_text() config = Path(tmp, "codex-config.toml").read_text()
self.assertEqual("codex", plan.template) self.assertEqual("codex", plan.template)
self.assertEqual("codex", plan.command) self.assertEqual(
"/home/node/.codex/packages/standalone/current/bin/codex",
plan.command,
)
self.assertEqual("read_prompt_file", plan.prompt_mode) self.assertEqual("read_prompt_file", plan.prompt_mode)
self.assertEqual("/tmp/Dockerfile.codex", plan.dockerfile) self.assertEqual("/tmp/Dockerfile.codex", plan.dockerfile)
self.assertEqual( self.assertEqual(
+32 -1
View File
@@ -261,6 +261,36 @@ class TestCodexProvision(unittest.TestCase):
self.assertTrue(any("find" in s and "-delete" in s for s in scripts)) self.assertTrue(any("find" in s and "-delete" in s for s in scripts))
self.assertTrue(any("runuser" in s and "codex login status" in s for s in scripts)) self.assertTrue(any("runuser" in s and "codex login status" in s for s in scripts))
def test_forwarded_credentials_verify_sets_codex_path(self):
with tempfile.TemporaryDirectory(prefix="bb-codex-auth.") as tmp:
state_dir = Path(tmp)
prompt_file = state_dir / "prompt.txt"
prompt_file.write_text("")
with patch(
"bot_bottle.contrib.codex.agent_provider.codex_host_access_token",
return_value="token",
), patch(
"bot_bottle.contrib.codex.agent_provider.write_codex_dummy_auth_file",
):
provision = CodexAgentProvider().provision_plan(
dockerfile="",
state_dir=state_dir,
instance_name="bot-bottle-demo-abc12",
prompt_file=prompt_file,
forward_host_credentials=True,
host_env={},
)
verify_argv = provision.verify[0].argv
self.assertTrue(any(
item.startswith("PATH=/home/node/.local/bin:")
for item in verify_argv
))
self.assertIn(
"/home/node/.codex/packages/standalone/current/bin/codex",
verify_argv,
)
def test_dies_when_dir_creation_fails(self): def test_dies_when_dir_creation_fails(self):
provision = AgentProvisionPlan( provision = AgentProvisionPlan(
template="codex", command="codex", template="codex", command="codex",
@@ -301,7 +331,8 @@ class TestCodexSuperviseMcp(unittest.TestCase):
script = bottle.exec.call_args.args[0] script = bottle.exec.call_args.args[0]
self.assertEqual("node", bottle.exec.call_args.kwargs.get("user")) self.assertEqual("node", bottle.exec.call_args.kwargs.get("user"))
self.assertEqual( self.assertEqual(
f"codex mcp add supervise --url {_URL}", "/home/node/.codex/packages/standalone/current/bin/codex "
f"mcp add supervise --url {_URL}",
script, script,
) )
+10
View File
@@ -217,6 +217,16 @@ class TestHookRender(unittest.TestCase):
self.assertIn("supervisor approved # gitleaks:allow", hook) self.assertIn("supervisor approved # gitleaks:allow", hook)
self.assertIn("supervisor rejected # gitleaks:allow", hook) self.assertIn("supervisor rejected # gitleaks:allow", hook)
def test_inline_gitleaks_allow_python_imports_work_in_sidecar_layout(self):
hook = git_gate_render_hook()
# The sidecar image copies supervise.py flat under /app, while
# host-side tests import it through the bot_bottle package.
# Hooks execute from the bare repo directory, so the embedded
# Python must include /app and support both import layouts.
self.assertIn('PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}"', hook)
self.assertIn("import supervise as _sv", hook)
self.assertIn("from bot_bottle import supervise as _sv", hook)
def test_inline_gitleaks_allow_fails_closed_without_supervisor(self): def test_inline_gitleaks_allow_fails_closed_without_supervisor(self):
hook = git_gate_render_hook() hook = git_gate_render_hook()
self.assertIn( self.assertIn(
+13
View File
@@ -30,6 +30,19 @@ class TestMacosContainerBottle(unittest.TestCase):
argv, argv,
) )
def test_agent_argv_accepts_absolute_provider_command(self):
command = "/home/node/.codex/packages/standalone/current/bin/codex"
bottle = MacosContainerBottle(
"bot-bottle-dev-abc",
lambda: None,
None,
agent_command=command,
)
with patch.dict(bottle_mod.os.environ, {}, clear=True):
argv = bottle.agent_argv(["run"])
self.assertIn(command, argv)
self.assertNotIn("codex", argv)
def test_agent_argv_includes_workdir(self): def test_agent_argv_includes_workdir(self):
bottle = MacosContainerBottle( bottle = MacosContainerBottle(
"bot-bottle-dev-abc", "bot-bottle-dev-abc",
+15 -2
View File
@@ -79,6 +79,11 @@ class TestClaudeArgvWrapped(unittest.TestCase):
"--", "--",
"runuser", "-u", "node", "--", "runuser", "-u", "node", "--",
"env", "HOME=/home/node", "USER=node", "env", "HOME=/home/node", "USER=node",
(
"PATH=/home/node/.local/bin:"
"/home/node/.codex/packages/standalone/current/bin:"
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
),
"claude", "claude",
], ],
argv, argv,
@@ -127,16 +132,24 @@ class TestClaudeArgvWrapped(unittest.TestCase):
self.assertIn("HTTPS_PROXY=http://127.0.0.1:1234", argv) self.assertIn("HTTPS_PROXY=http://127.0.0.1:1234", argv)
self.assertIn("NO_PROXY=localhost", argv) self.assertIn("NO_PROXY=localhost", argv)
def test_guest_env_path_overrides_default_path(self):
argv = _unwrap(_bottle(None, PATH="/custom/bin").agent_argv([]))
self.assertIn("PATH=/custom/bin", argv)
self.assertFalse(any(
item.startswith("PATH=/home/node/.local/bin")
for item in argv
))
def test_runuser_switch_precedes_claude(self): def test_runuser_switch_precedes_claude(self):
# The dashboard's `_build_resume_argv_with_fallback` finds # The dashboard's `_build_resume_argv_with_fallback` finds
# the `claude` token to split exec-framing from the claude # the `claude` token to split exec-framing from the claude
# tail. `runuser -u node --` must sit on the prefix side so # tail. `runuser -u node --` must sit on the prefix side so
# the shell wrap inherits the UID switch. # the shell wrap inherits the UID switch.
argv = _bottle().agent_argv([]) argv = _bottle().agent_argv([])
agent_idx = argv.index("claude") runuser_idx = argv.index("runuser")
self.assertEqual( self.assertEqual(
["runuser", "-u", "node", "--", "env"], ["runuser", "-u", "node", "--", "env"],
argv[agent_idx - 7:agent_idx - 2], argv[runuser_idx:runuser_idx + 5],
) )
def test_pi_provider_appends_system_prompt_without_print_mode(self): def test_pi_provider_appends_system_prompt_without_print_mode(self):