fix: repair codex resume paths
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,7 +236,10 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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])
|
report_path = Path(sys.argv[1])
|
||||||
slug = os.environ.get("SUPERVISE_BOTTLE_SLUG", "")
|
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
|
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
|
||||||
|
|
||||||
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]
|
slug = sys.argv[1]
|
||||||
try:
|
try:
|
||||||
@@ -336,10 +342,13 @@ 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
|
||||||
|
|
||||||
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])
|
_sv.archive_proposal(sys.argv[1], sys.argv[2])
|
||||||
PY
|
PY
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user