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
|
PATH=/opt/agentci/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
|
||||||
RUN apt-get update \
|
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/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& /usr/sbin/adduser --disabled-password --gecos "" --uid 10001 agentci \
|
&& /usr/sbin/adduser --disabled-password --gecos "" --uid 10001 agentci \
|
||||||
&& mkdir -p /opt/agentci /var/lib/agentci /var/lib/codex /etc/codex /run/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=uv /uv /uvx /usr/local/bin/
|
||||||
COPY --from=tea /bin/tea /usr/local/bin/tea
|
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/@openai/codex /usr/local/lib/node_modules/@openai/codex
|
||||||
COPY --from=codex /usr/local/lib/node_modules/@colbymchenry /usr/local/lib/node_modules/@colbymchenry
|
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 \
|
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 \
|
&& ln -s /usr/local/lib/node_modules/@colbymchenry/codegraph/npm-shim.js \
|
||||||
/usr/local/bin/codegraph
|
/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;
|
skipped because every turn runs non-interactively against a service-owned clone;
|
||||||
the configured filesystem and network permissions still apply. Git credentials
|
the configured filesystem and network permissions still apply. Git credentials
|
||||||
exist only in the service-owned clone/push subprocess and are not inherited by
|
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
|
## State and recovery
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ services:
|
|||||||
CODEGRAPH_VERSION: ${CODEGRAPH_VERSION:-1.3.1}
|
CODEGRAPH_VERSION: ${CODEGRAPH_VERSION:-1.3.1}
|
||||||
TEA_VERSION: ${TEA_VERSION:-0.14.2}
|
TEA_VERSION: ${TEA_VERSION:-0.14.2}
|
||||||
restart: unless-stopped
|
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:
|
environment:
|
||||||
AGENTCI_GITEA_URL: ${AGENTCI_GITEA_URL:-http://gitea:3000}
|
AGENTCI_GITEA_URL: ${AGENTCI_GITEA_URL:-http://gitea:3000}
|
||||||
AGENTCI_BOT_USERNAME: ${AGENTCI_BOT_USERNAME:-agentci}
|
AGENTCI_BOT_USERNAME: ${AGENTCI_BOT_USERNAME:-agentci}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class CodexClient:
|
|||||||
*self._turn_args(model, reasoning, permission, schema_name),
|
*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:
|
if not session_id:
|
||||||
raise CodexError("Codex did not emit a thread.started event")
|
raise CodexError("Codex did not emit a thread.started event")
|
||||||
return session_id, result
|
return session_id, result
|
||||||
@@ -104,7 +104,7 @@ class CodexClient:
|
|||||||
session_id,
|
session_id,
|
||||||
"-",
|
"-",
|
||||||
]
|
]
|
||||||
_, result = await self._invoke(args, prompt, result_type)
|
_, result = await self._invoke(args, prompt, result_type, workspace=workspace)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _turn_args(
|
def _turn_args(
|
||||||
@@ -127,7 +127,7 @@ class CodexClient:
|
|||||||
]
|
]
|
||||||
|
|
||||||
async def _invoke(
|
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]:
|
) -> tuple[str | None, T]:
|
||||||
started = monotonic()
|
started = monotonic()
|
||||||
operation = "codex.resume" if "resume" in args else "codex.start"
|
operation = "codex.resume" if "resume" in args else "codex.start"
|
||||||
@@ -141,6 +141,7 @@ class CodexClient:
|
|||||||
try:
|
try:
|
||||||
process = await asyncio.create_subprocess_exec(
|
process = await asyncio.create_subprocess_exec(
|
||||||
*args,
|
*args,
|
||||||
|
cwd=workspace,
|
||||||
env=self._environment(),
|
env=self._environment(),
|
||||||
stdin=asyncio.subprocess.PIPE,
|
stdin=asyncio.subprocess.PIPE,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import asyncio
|
||||||
import tomllib
|
import tomllib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from agentci.adapters.codex import CodexClient, _session_id
|
from agentci.adapters.codex import CodexClient, _session_id
|
||||||
|
from agentci.domain.models import AgentResult
|
||||||
|
|
||||||
|
|
||||||
def test_enables_codegraph_in_shared_codex_config() -> None:
|
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 'url = "https://mcp.grep.app"' in agent
|
||||||
assert "ctx7-secret" not in agent
|
assert "ctx7-secret" not in agent
|
||||||
assert client._environment()["CONTEXT7_API_KEY"] == "ctx7-secret"
|
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