fix: enable Codex sandbox in Docker
This commit is contained in:
+7
-2
@@ -24,11 +24,13 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PATH=/opt/agentci/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --yes --no-install-recommends adduser ca-certificates git libstdc++6 \
|
||||
&& apt-get install --yes --no-install-recommends \
|
||||
adduser bubblewrap ca-certificates git libstdc++6 \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& /usr/sbin/adduser --disabled-password --gecos "" --uid 10001 agentci \
|
||||
&& mkdir -p /opt/agentci /var/lib/agentci /var/lib/codex /etc/codex /run/agentci \
|
||||
&& chown agentci:agentci /run/agentci
|
||||
&& chown agentci:agentci /run/agentci \
|
||||
&& chmod u+s /usr/bin/bwrap
|
||||
|
||||
COPY --from=uv /uv /uvx /usr/local/bin/
|
||||
COPY --from=tea /bin/tea /usr/local/bin/tea
|
||||
@@ -36,6 +38,9 @@ COPY --from=codex /usr/local/bin/node /usr/local/bin/node
|
||||
COPY --from=codex /usr/local/lib/node_modules/@openai/codex /usr/local/lib/node_modules/@openai/codex
|
||||
COPY --from=codex /usr/local/lib/node_modules/@colbymchenry /usr/local/lib/node_modules/@colbymchenry
|
||||
RUN ln -s /usr/local/lib/node_modules/@openai/codex/bin/codex.js /usr/local/bin/codex \
|
||||
&& ln -s \
|
||||
/usr/local/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/bin/codex \
|
||||
/usr/local/bin/codex-linux-sandbox \
|
||||
&& ln -s /usr/local/lib/node_modules/@colbymchenry/codegraph/npm-shim.js \
|
||||
/usr/local/bin/codegraph
|
||||
|
||||
|
||||
@@ -76,7 +76,10 @@ loopback destinations remain blocked. Codex's interactive Git trust check is
|
||||
skipped because every turn runs non-interactively against a service-owned clone;
|
||||
the configured filesystem and network permissions still apply. Git credentials
|
||||
exist only in the service-owned clone/push subprocess and are not inherited by
|
||||
Codex turns.
|
||||
Codex turns. The image and Compose capability/security settings let the non-root
|
||||
service create Codex's nested `bwrap` sandbox. They follow Codex's secure
|
||||
devcontainer pattern instead of making the service container privileged. Do not
|
||||
remove Codex's configured filesystem and network restrictions.
|
||||
|
||||
## State and recovery
|
||||
|
||||
|
||||
@@ -7,6 +7,18 @@ services:
|
||||
CODEGRAPH_VERSION: ${CODEGRAPH_VERSION:-1.3.1}
|
||||
TEA_VERSION: ${TEA_VERSION:-0.14.2}
|
||||
restart: unless-stopped
|
||||
# Codex applies its own bwrap sandbox inside this otherwise unprivileged container.
|
||||
cap_add:
|
||||
- SYS_ADMIN
|
||||
- SYS_CHROOT
|
||||
- SETUID
|
||||
- SETGID
|
||||
- SYS_PTRACE
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
security_opt:
|
||||
- seccomp=unconfined
|
||||
- apparmor=unconfined
|
||||
environment:
|
||||
AGENTCI_GITEA_URL: ${AGENTCI_GITEA_URL:-http://gitea:3000}
|
||||
AGENTCI_BOT_USERNAME: ${AGENTCI_BOT_USERNAME:-agentci}
|
||||
|
||||
@@ -76,7 +76,7 @@ class CodexClient:
|
||||
*self._turn_args(model, reasoning, permission, schema_name),
|
||||
"-",
|
||||
]
|
||||
session_id, result = await self._invoke(args, prompt, result_type)
|
||||
session_id, result = await self._invoke(args, prompt, result_type, workspace=workspace)
|
||||
if not session_id:
|
||||
raise CodexError("Codex did not emit a thread.started event")
|
||||
return session_id, result
|
||||
@@ -104,7 +104,7 @@ class CodexClient:
|
||||
session_id,
|
||||
"-",
|
||||
]
|
||||
_, result = await self._invoke(args, prompt, result_type)
|
||||
_, result = await self._invoke(args, prompt, result_type, workspace=workspace)
|
||||
return result
|
||||
|
||||
def _turn_args(
|
||||
@@ -127,7 +127,7 @@ class CodexClient:
|
||||
]
|
||||
|
||||
async def _invoke(
|
||||
self, args: list[str], prompt: str, result_type: type[T]
|
||||
self, args: list[str], prompt: str, result_type: type[T], *, workspace: Path
|
||||
) -> tuple[str | None, T]:
|
||||
started = monotonic()
|
||||
operation = "codex.resume" if "resume" in args else "codex.start"
|
||||
@@ -141,6 +141,7 @@ class CodexClient:
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*args,
|
||||
cwd=workspace,
|
||||
env=self._environment(),
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import asyncio
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from agentci.adapters.codex import CodexClient, _session_id
|
||||
from agentci.domain.models import AgentResult
|
||||
|
||||
|
||||
def test_enables_codegraph_in_shared_codex_config() -> None:
|
||||
@@ -65,3 +67,44 @@ def test_configures_research_agent_and_optional_context7_key(tmp_path) -> None:
|
||||
assert 'url = "https://mcp.grep.app"' in agent
|
||||
assert "ctx7-secret" not in agent
|
||||
assert client._environment()["CONTEXT7_API_KEY"] == "ctx7-secret"
|
||||
|
||||
|
||||
async def test_invokes_codex_from_workflow_workspace(tmp_path, monkeypatch) -> None:
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class Process:
|
||||
returncode = 0
|
||||
|
||||
async def communicate(self, prompt: bytes) -> tuple[bytes, bytes]:
|
||||
assert prompt == b"prompt"
|
||||
return b'{"type":"thread.started","thread_id":"thread"}', b""
|
||||
|
||||
async def create_subprocess_exec(*args, **kwargs):
|
||||
captured["cwd"] = kwargs["cwd"]
|
||||
output = Path(args[args.index("--output-last-message") + 1])
|
||||
output.write_text( # noqa: ASYNC240 - tiny test-owned result file
|
||||
'{"summary_markdown":"summary","tests":[]}'
|
||||
)
|
||||
return Process()
|
||||
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_exec", create_subprocess_exec)
|
||||
client = CodexClient(
|
||||
codex_home=tmp_path / "codex",
|
||||
schemas_dir=tmp_path / "schemas",
|
||||
timeout_seconds=60,
|
||||
research_model="gpt-5.6-luna",
|
||||
research_reasoning="high",
|
||||
context7_api_key=None,
|
||||
)
|
||||
|
||||
_, result = await client._invoke(
|
||||
["codex", "exec", "-"],
|
||||
"prompt",
|
||||
AgentResult,
|
||||
workspace=workspace,
|
||||
)
|
||||
|
||||
assert captured["cwd"] == workspace
|
||||
assert result.summary_markdown == "summary"
|
||||
|
||||
Reference in New Issue
Block a user