fix: repair codex resume paths
This commit is contained in:
@@ -47,10 +47,24 @@ _HOME_FOR = {
|
||||
"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]:
|
||||
home = _HOME_FOR.get(user, f"/home/{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():
|
||||
out.append(f"{k}={v}")
|
||||
return out
|
||||
|
||||
@@ -34,6 +34,12 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
_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:
|
||||
@@ -50,7 +56,7 @@ def _prompt_path(guest_home: str) -> str:
|
||||
|
||||
_RUNTIME = AgentProviderRuntime(
|
||||
template="codex",
|
||||
command="codex",
|
||||
command=_CODEX_CLI,
|
||||
image="bot-bottle-codex:latest",
|
||||
prompt_mode="read_prompt_file",
|
||||
bypass_args=("--dangerously-bypass-approvals-and-sandbox",),
|
||||
@@ -145,7 +151,8 @@ class CodexAgentProvider(AgentProvider):
|
||||
"env",
|
||||
f"HOME={guest_home}",
|
||||
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 "
|
||||
"guest, but Codex did not accept it"
|
||||
@@ -267,7 +274,7 @@ class CodexAgentProvider(AgentProvider):
|
||||
return
|
||||
info(f"registering supervise MCP server in agent codex config → {supervise_url}")
|
||||
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)}",
|
||||
user="node",
|
||||
)
|
||||
|
||||
@@ -228,7 +228,7 @@ supervise_gitleaks_allow() {
|
||||
fi
|
||||
|
||||
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 hashlib
|
||||
import json
|
||||
@@ -236,7 +236,10 @@ import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from bot_bottle import supervise as _sv
|
||||
try:
|
||||
import supervise as _sv
|
||||
except ImportError:
|
||||
from bot_bottle import supervise as _sv
|
||||
|
||||
report_path = Path(sys.argv[1])
|
||||
slug = os.environ.get("SUPERVISE_BOTTLE_SLUG", "")
|
||||
@@ -314,10 +317,13 @@ PY
|
||||
echo "git-gate: approve with './cli.py supervise' to continue this push" >&2
|
||||
waited=0
|
||||
while [ "$waited" -lt "$timeout" ]; do
|
||||
status=$(python3 - "$slug" "$proposal_id" <<'PY'
|
||||
status=$(PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}" python3 - "$slug" "$proposal_id" <<'PY'
|
||||
import sys
|
||||
|
||||
from bot_bottle import supervise as _sv
|
||||
try:
|
||||
import supervise as _sv
|
||||
except ImportError:
|
||||
from bot_bottle import supervise as _sv
|
||||
|
||||
slug = sys.argv[1]
|
||||
try:
|
||||
@@ -336,10 +342,13 @@ PY
|
||||
if [ -n "$status" ]; then
|
||||
case "$status" in
|
||||
approved|modified)
|
||||
python3 - "$slug" "$proposal_id" <<'PY' || true
|
||||
PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}" python3 - "$slug" "$proposal_id" <<'PY' || true
|
||||
import sys
|
||||
|
||||
from bot_bottle import supervise as _sv
|
||||
try:
|
||||
import supervise as _sv
|
||||
except ImportError:
|
||||
from bot_bottle import supervise as _sv
|
||||
|
||||
_sv.archive_proposal(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
|
||||
@@ -35,7 +35,10 @@ class TestAgentProviderRuntime(unittest.TestCase):
|
||||
)
|
||||
config = Path(tmp, "codex-config.toml").read_text()
|
||||
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("/tmp/Dockerfile.codex", plan.dockerfile)
|
||||
self.assertEqual(
|
||||
|
||||
@@ -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("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):
|
||||
provision = AgentProvisionPlan(
|
||||
template="codex", command="codex",
|
||||
@@ -301,7 +331,8 @@ class TestCodexSuperviseMcp(unittest.TestCase):
|
||||
script = bottle.exec.call_args.args[0]
|
||||
self.assertEqual("node", bottle.exec.call_args.kwargs.get("user"))
|
||||
self.assertEqual(
|
||||
f"codex mcp add supervise --url {_URL}",
|
||||
"/home/node/.codex/packages/standalone/current/bin/codex "
|
||||
f"mcp add supervise --url {_URL}",
|
||||
script,
|
||||
)
|
||||
|
||||
|
||||
@@ -217,6 +217,16 @@ class TestHookRender(unittest.TestCase):
|
||||
self.assertIn("supervisor approved # 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):
|
||||
hook = git_gate_render_hook()
|
||||
self.assertIn(
|
||||
|
||||
@@ -30,6 +30,19 @@ class TestMacosContainerBottle(unittest.TestCase):
|
||||
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):
|
||||
bottle = MacosContainerBottle(
|
||||
"bot-bottle-dev-abc",
|
||||
|
||||
@@ -79,6 +79,11 @@ class TestClaudeArgvWrapped(unittest.TestCase):
|
||||
"--",
|
||||
"runuser", "-u", "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",
|
||||
],
|
||||
argv,
|
||||
@@ -127,16 +132,24 @@ class TestClaudeArgvWrapped(unittest.TestCase):
|
||||
self.assertIn("HTTPS_PROXY=http://127.0.0.1:1234", 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):
|
||||
# The dashboard's `_build_resume_argv_with_fallback` finds
|
||||
# the `claude` token to split exec-framing from the claude
|
||||
# tail. `runuser -u node --` must sit on the prefix side so
|
||||
# the shell wrap inherits the UID switch.
|
||||
argv = _bottle().agent_argv([])
|
||||
agent_idx = argv.index("claude")
|
||||
runuser_idx = argv.index("runuser")
|
||||
self.assertEqual(
|
||||
["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):
|
||||
|
||||
Reference in New Issue
Block a user