fix(codex): trust bottle workspace on launch
This commit is contained in:
@@ -3,29 +3,72 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from ..bottle_plan import DockerBottlePlan
|
from ..bottle_plan import DockerBottlePlan
|
||||||
|
|
||||||
|
|
||||||
def provision_provider_auth(plan: DockerBottlePlan, target: str) -> None:
|
_CODEX_WORKSPACE = "/home/node/workspace"
|
||||||
"""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
|
|
||||||
|
def provision_provider_auth(plan: DockerBottlePlan, target: str) -> None:
|
||||||
|
"""Prepare Codex home state inside a Docker bottle.
|
||||||
|
|
||||||
|
Every Codex bottle gets a minimal config.toml that trusts the
|
||||||
|
in-container workspace path. When host credentials are forwarded,
|
||||||
|
auth.json contains no real access or refresh token values; it only
|
||||||
nudges Codex into the same user/device auth branch as the host.
|
nudges Codex into the same user/device auth branch as the host.
|
||||||
"""
|
"""
|
||||||
if not plan.codex_auth_file:
|
if plan.agent_provider_template != "codex":
|
||||||
return
|
return
|
||||||
container_home = os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node")
|
container_home = os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node")
|
||||||
auth_dir = f"{container_home}/.codex"
|
auth_dir = f"{container_home}/.codex"
|
||||||
auth_path = f"{auth_dir}/auth.json"
|
|
||||||
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["docker", "exec", "-u", "0", target, "mkdir", "-p", auth_dir],
|
["docker", "exec", "-u", "0", target, "mkdir", "-p", auth_dir],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "exec", "-u", "0", target, "chown", "node:node", auth_dir],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "exec", "-u", "0", target, "chmod", "700", auth_dir],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
config_path = f"{auth_dir}/config.toml"
|
||||||
|
config = (
|
||||||
|
f'[projects."{_CODEX_WORKSPACE}"]\n'
|
||||||
|
'trust_level = "trusted"\n'
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"docker", "exec", "-u", "0", target,
|
||||||
|
"sh", "-c",
|
||||||
|
f"printf %s {shlex.quote(config)} > {shlex.quote(config_path)}",
|
||||||
|
],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "exec", "-u", "0", target, "chown", "node:node", config_path],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "exec", "-u", "0", target, "chmod", "600", config_path],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not plan.codex_auth_file:
|
||||||
|
return
|
||||||
|
|
||||||
|
auth_path = f"{auth_dir}/auth.json"
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["docker", "cp", str(plan.codex_auth_file), f"{target}:{auth_path}"],
|
["docker", "cp", str(plan.codex_auth_file), f"{target}:{auth_path}"],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
|
|
||||||
from ....log import die
|
from ....log import die
|
||||||
from .. import smolvm as _smolvm
|
from .. import smolvm as _smolvm
|
||||||
@@ -10,16 +11,18 @@ from ..bottle_plan import SmolmachinesBottlePlan
|
|||||||
|
|
||||||
|
|
||||||
_DEFAULT_GUEST_HOME = "/home/node"
|
_DEFAULT_GUEST_HOME = "/home/node"
|
||||||
|
_CODEX_WORKSPACE = "/home/node/workspace"
|
||||||
|
|
||||||
|
|
||||||
def provision_provider_auth(plan: SmolmachinesBottlePlan, target: str) -> None:
|
def provision_provider_auth(plan: SmolmachinesBottlePlan, target: str) -> None:
|
||||||
"""Copy a dummy Codex auth marker when host credentials are
|
"""Prepare Codex home state inside the smolmachine.
|
||||||
forwarded through egress.
|
|
||||||
|
|
||||||
The real host access token remains in the egress bundle env; this
|
Every Codex bottle gets a minimal config.toml that trusts the
|
||||||
file only selects Codex's user/device auth code path.
|
in-guest workspace path. When host credentials are forwarded, the
|
||||||
|
real host access token remains in the egress bundle env; auth.json
|
||||||
|
only selects Codex's user/device auth code path.
|
||||||
"""
|
"""
|
||||||
if not plan.codex_auth_file:
|
if plan.agent_provider_template != "codex":
|
||||||
return
|
return
|
||||||
guest_home = os.environ.get("BOT_BOTTLE_GUEST_HOME", _DEFAULT_GUEST_HOME)
|
guest_home = os.environ.get("BOT_BOTTLE_GUEST_HOME", _DEFAULT_GUEST_HOME)
|
||||||
auth_dir = plan.guest_env.get("CODEX_HOME", f"{guest_home}/.codex")
|
auth_dir = plan.guest_env.get("CODEX_HOME", f"{guest_home}/.codex")
|
||||||
@@ -65,6 +68,29 @@ def provision_provider_auth(plan: SmolmachinesBottlePlan, target: str) -> None:
|
|||||||
detail = f": {detail}"
|
detail = f": {detail}"
|
||||||
die(f"codex host credentials: could not reset runtime db files{detail}")
|
die(f"codex host credentials: could not reset runtime db files{detail}")
|
||||||
|
|
||||||
|
config_path = f"{auth_dir}/config.toml"
|
||||||
|
config = (
|
||||||
|
f'[projects."{_CODEX_WORKSPACE}"]\n'
|
||||||
|
'trust_level = "trusted"\n'
|
||||||
|
)
|
||||||
|
result = _smolvm.machine_exec(
|
||||||
|
target,
|
||||||
|
[
|
||||||
|
"sh", "-c",
|
||||||
|
f"printf %s {shlex.quote(config)} > {shlex.quote(config_path)}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
detail = (result.stderr or result.stdout).strip()
|
||||||
|
if detail:
|
||||||
|
detail = f": {detail}"
|
||||||
|
die(f"codex host credentials: could not write {config_path}{detail}")
|
||||||
|
_smolvm.machine_exec(target, ["chown", "node:node", config_path])
|
||||||
|
_smolvm.machine_exec(target, ["chmod", "600", config_path])
|
||||||
|
|
||||||
|
if not plan.codex_auth_file:
|
||||||
|
return
|
||||||
|
|
||||||
auth_path = f"{auth_dir}/auth.json"
|
auth_path = f"{auth_dir}/auth.json"
|
||||||
_smolvm.machine_cp(str(plan.codex_auth_file), f"{target}:{auth_path}")
|
_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, ["chown", "node:node", auth_path])
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ from bot_bottle.manifest import Manifest
|
|||||||
from bot_bottle.pipelock import PipelockProxyPlan
|
from bot_bottle.pipelock import PipelockProxyPlan
|
||||||
|
|
||||||
|
|
||||||
def _plan(*, codex_auth_file: Path | None = None) -> DockerBottlePlan:
|
def _plan(
|
||||||
|
*,
|
||||||
|
codex_auth_file: Path | None = None,
|
||||||
|
agent_provider_template: str = "codex",
|
||||||
|
) -> DockerBottlePlan:
|
||||||
manifest = Manifest.from_json_obj({
|
manifest = Manifest.from_json_obj({
|
||||||
"bottles": {"dev": {"agent_provider": {"template": "codex"}}},
|
"bottles": {"dev": {"agent_provider": {"template": "codex"}}},
|
||||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||||
@@ -58,18 +62,46 @@ def _plan(*, codex_auth_file: Path | None = None) -> DockerBottlePlan:
|
|||||||
supervise_plan=None,
|
supervise_plan=None,
|
||||||
use_runsc=False,
|
use_runsc=False,
|
||||||
agent_command="codex",
|
agent_command="codex",
|
||||||
agent_provider_template="codex",
|
agent_provider_template=agent_provider_template,
|
||||||
codex_auth_file=codex_auth_file,
|
codex_auth_file=codex_auth_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestProvisionProviderAuth(unittest.TestCase):
|
class TestProvisionProviderAuth(unittest.TestCase):
|
||||||
def test_noop_without_codex_auth_file(self):
|
def test_noop_for_non_codex_provider(self):
|
||||||
|
with patch.object(_provider_auth.subprocess, "run") as run:
|
||||||
|
_provider_auth.provision_provider_auth(
|
||||||
|
_plan(agent_provider_template="claude"), "bot-bottle-demo-abc12",
|
||||||
|
)
|
||||||
|
self.assertEqual(0, run.call_count)
|
||||||
|
|
||||||
|
def test_codex_provider_trusts_workspace_without_auth_file(self):
|
||||||
with patch.object(_provider_auth.subprocess, "run") as run:
|
with patch.object(_provider_auth.subprocess, "run") as run:
|
||||||
_provider_auth.provision_provider_auth(
|
_provider_auth.provision_provider_auth(
|
||||||
_plan(), "bot-bottle-demo-abc12",
|
_plan(), "bot-bottle-demo-abc12",
|
||||||
)
|
)
|
||||||
self.assertEqual(0, run.call_count)
|
argvs = [call.args[0] for call in run.call_args_list]
|
||||||
|
self.assertIn(
|
||||||
|
["docker", "exec", "-u", "0", "bot-bottle-demo-abc12",
|
||||||
|
"mkdir", "-p", "/home/node/.codex"],
|
||||||
|
argvs,
|
||||||
|
)
|
||||||
|
trust_config = next(
|
||||||
|
a for a in argvs
|
||||||
|
if a[:6] == ["docker", "exec", "-u", "0", "bot-bottle-demo-abc12", "sh"]
|
||||||
|
)
|
||||||
|
self.assertIn('[projects."/home/node/workspace"]', trust_config[-1])
|
||||||
|
self.assertIn('trust_level = "trusted"', trust_config[-1])
|
||||||
|
self.assertIn(
|
||||||
|
["docker", "exec", "-u", "0", "bot-bottle-demo-abc12",
|
||||||
|
"chown", "node:node", "/home/node/.codex/config.toml"],
|
||||||
|
argvs,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
["docker", "exec", "-u", "0", "bot-bottle-demo-abc12",
|
||||||
|
"chmod", "600", "/home/node/.codex/config.toml"],
|
||||||
|
argvs,
|
||||||
|
)
|
||||||
|
|
||||||
def test_copies_dummy_auth_json_to_codex_home(self):
|
def test_copies_dummy_auth_json_to_codex_home(self):
|
||||||
with patch.object(_provider_auth.subprocess, "run") as run:
|
with patch.object(_provider_auth.subprocess, "run") as run:
|
||||||
@@ -83,6 +115,16 @@ class TestProvisionProviderAuth(unittest.TestCase):
|
|||||||
"mkdir", "-p", "/home/node/.codex"],
|
"mkdir", "-p", "/home/node/.codex"],
|
||||||
argvs,
|
argvs,
|
||||||
)
|
)
|
||||||
|
self.assertIn(
|
||||||
|
["docker", "exec", "-u", "0", "bot-bottle-demo-abc12",
|
||||||
|
"chown", "node:node", "/home/node/.codex"],
|
||||||
|
argvs,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
["docker", "exec", "-u", "0", "bot-bottle-demo-abc12",
|
||||||
|
"chmod", "700", "/home/node/.codex"],
|
||||||
|
argvs,
|
||||||
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
["docker", "cp", "/tmp/codex-auth.json",
|
["docker", "cp", "/tmp/codex-auth.json",
|
||||||
"bot-bottle-demo-abc12:/home/node/.codex/auth.json"],
|
"bot-bottle-demo-abc12:/home/node/.codex/auth.json"],
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ def _plan(
|
|||||||
agent_git_gate_host: str = "127.0.0.1:55555",
|
agent_git_gate_host: str = "127.0.0.1:55555",
|
||||||
agent_supervise_url: str = "http://127.0.0.1:55556/",
|
agent_supervise_url: str = "http://127.0.0.1:55556/",
|
||||||
codex_auth_file: Path | None = None,
|
codex_auth_file: Path | None = None,
|
||||||
|
agent_provider_template: str = "claude",
|
||||||
guest_env: dict[str, str] | None = None,
|
guest_env: dict[str, str] | None = None,
|
||||||
) -> SmolmachinesBottlePlan:
|
) -> SmolmachinesBottlePlan:
|
||||||
bottle_json: dict = {}
|
bottle_json: dict = {}
|
||||||
@@ -133,6 +134,7 @@ def _plan(
|
|||||||
agent_git_gate_host=agent_git_gate_host,
|
agent_git_gate_host=agent_git_gate_host,
|
||||||
agent_supervise_url=agent_supervise_url,
|
agent_supervise_url=agent_supervise_url,
|
||||||
codex_auth_file=codex_auth_file,
|
codex_auth_file=codex_auth_file,
|
||||||
|
agent_provider_template=agent_provider_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -204,19 +206,45 @@ class TestProvisionProviderAuth(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_noop_without_codex_auth_file(self):
|
def test_noop_for_non_codex_provider(self):
|
||||||
cp_p, ex_p = self._patch()
|
cp_p, ex_p = self._patch()
|
||||||
with cp_p as cp, ex_p as ex:
|
with cp_p as cp, ex_p as ex:
|
||||||
_provider_auth.provision_provider_auth(_plan(), "bot-bottle-demo-abc12")
|
_provider_auth.provision_provider_auth(_plan(), "bot-bottle-demo-abc12")
|
||||||
self.assertEqual(0, cp.call_count)
|
self.assertEqual(0, cp.call_count)
|
||||||
self.assertEqual(0, ex.call_count)
|
self.assertEqual(0, ex.call_count)
|
||||||
|
|
||||||
|
def test_codex_provider_trusts_workspace_without_auth_file(self):
|
||||||
|
cp_p, ex_p = self._patch()
|
||||||
|
with cp_p as cp, ex_p as ex:
|
||||||
|
ex.return_value = SmolvmRunResult(0, "", "")
|
||||||
|
_provider_auth.provision_provider_auth(
|
||||||
|
_plan(agent_provider_template="codex"),
|
||||||
|
"bot-bottle-demo-abc12",
|
||||||
|
)
|
||||||
|
self.assertEqual(0, cp.call_count)
|
||||||
|
argv_seen = [call.args[1] for call in ex.call_args_list]
|
||||||
|
self.assertIn(["mkdir", "-p", "/home/node/.codex"], argv_seen)
|
||||||
|
trust_config = next(
|
||||||
|
a for a in argv_seen
|
||||||
|
if a[:2] == ["sh", "-c"] and "config.toml" in a[2]
|
||||||
|
)
|
||||||
|
self.assertIn('[projects."/home/node/workspace"]', trust_config[2])
|
||||||
|
self.assertIn('trust_level = "trusted"', trust_config[2])
|
||||||
|
self.assertIn(
|
||||||
|
["chown", "node:node", "/home/node/.codex/config.toml"],
|
||||||
|
argv_seen,
|
||||||
|
)
|
||||||
|
self.assertIn(["chmod", "600", "/home/node/.codex/config.toml"], argv_seen)
|
||||||
|
|
||||||
def test_copies_dummy_auth_json_to_codex_home(self):
|
def test_copies_dummy_auth_json_to_codex_home(self):
|
||||||
cp_p, ex_p = self._patch()
|
cp_p, ex_p = self._patch()
|
||||||
with cp_p as cp, ex_p as ex:
|
with cp_p as cp, ex_p as ex:
|
||||||
ex.return_value = SmolvmRunResult(0, "Logged in using ChatGPT\n", "")
|
ex.return_value = SmolvmRunResult(0, "Logged in using ChatGPT\n", "")
|
||||||
_provider_auth.provision_provider_auth(
|
_provider_auth.provision_provider_auth(
|
||||||
_plan(codex_auth_file=Path("/tmp/codex-auth.json")),
|
_plan(
|
||||||
|
agent_provider_template="codex",
|
||||||
|
codex_auth_file=Path("/tmp/codex-auth.json"),
|
||||||
|
),
|
||||||
"bot-bottle-demo-abc12",
|
"bot-bottle-demo-abc12",
|
||||||
)
|
)
|
||||||
cp.assert_called_once_with(
|
cp.assert_called_once_with(
|
||||||
@@ -269,6 +297,7 @@ class TestProvisionProviderAuth(unittest.TestCase):
|
|||||||
ex.return_value = SmolvmRunResult(0, "Logged in using ChatGPT\n", "")
|
ex.return_value = SmolvmRunResult(0, "Logged in using ChatGPT\n", "")
|
||||||
_provider_auth.provision_provider_auth(
|
_provider_auth.provision_provider_auth(
|
||||||
_plan(
|
_plan(
|
||||||
|
agent_provider_template="codex",
|
||||||
codex_auth_file=Path("/tmp/codex-auth.json"),
|
codex_auth_file=Path("/tmp/codex-auth.json"),
|
||||||
guest_env={"CODEX_HOME": "/run/codex-home"},
|
guest_env={"CODEX_HOME": "/run/codex-home"},
|
||||||
),
|
),
|
||||||
@@ -297,6 +326,7 @@ class TestProvisionProviderAuth(unittest.TestCase):
|
|||||||
with self.assertRaises(SystemExit):
|
with self.assertRaises(SystemExit):
|
||||||
_provider_auth.provision_provider_auth(
|
_provider_auth.provision_provider_auth(
|
||||||
_plan(
|
_plan(
|
||||||
|
agent_provider_template="codex",
|
||||||
codex_auth_file=Path("/tmp/codex-auth.json"),
|
codex_auth_file=Path("/tmp/codex-auth.json"),
|
||||||
),
|
),
|
||||||
"bot-bottle-demo-abc12",
|
"bot-bottle-demo-abc12",
|
||||||
@@ -313,6 +343,9 @@ class TestProvisionProviderAuth(unittest.TestCase):
|
|||||||
SmolvmRunResult(0, "", ""), # chown CODEX_HOME
|
SmolvmRunResult(0, "", ""), # chown CODEX_HOME
|
||||||
SmolvmRunResult(0, "", ""), # chmod CODEX_HOME
|
SmolvmRunResult(0, "", ""), # chmod CODEX_HOME
|
||||||
SmolvmRunResult(0, "", ""), # reset runtime db files
|
SmolvmRunResult(0, "", ""), # reset runtime db files
|
||||||
|
SmolvmRunResult(0, "", ""), # write config.toml
|
||||||
|
SmolvmRunResult(0, "", ""), # chown config.toml
|
||||||
|
SmolvmRunResult(0, "", ""), # chmod config.toml
|
||||||
SmolvmRunResult(0, "", ""), # chown auth.json
|
SmolvmRunResult(0, "", ""), # chown auth.json
|
||||||
SmolvmRunResult(0, "", ""), # chmod auth.json
|
SmolvmRunResult(0, "", ""), # chmod auth.json
|
||||||
SmolvmRunResult(1, "Not logged in\n", ""), # login status
|
SmolvmRunResult(1, "Not logged in\n", ""), # login status
|
||||||
@@ -320,6 +353,7 @@ class TestProvisionProviderAuth(unittest.TestCase):
|
|||||||
with self.assertRaises(SystemExit):
|
with self.assertRaises(SystemExit):
|
||||||
_provider_auth.provision_provider_auth(
|
_provider_auth.provision_provider_auth(
|
||||||
_plan(
|
_plan(
|
||||||
|
agent_provider_template="codex",
|
||||||
codex_auth_file=Path("/tmp/codex-auth.json"),
|
codex_auth_file=Path("/tmp/codex-auth.json"),
|
||||||
),
|
),
|
||||||
"bot-bottle-demo-abc12",
|
"bot-bottle-demo-abc12",
|
||||||
|
|||||||
Reference in New Issue
Block a user