Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions deploy/distribution/AgentHost.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ USER root

# Sandbox tools are served by oac-sandbox-io; the caller supplies its bootstrap.
FROM base AS sandbox
# Login shells keep the process environment's package search path.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates bash git python3 python3-pip ripgrep \
&& rm -rf /var/lib/apt/lists/* \
&& sed -i 's|^if \[ "$(id -u)" -eq 0 \]; then$|if [ -n "${PATH:-}" ] \&\& /usr/bin/printenv PATH >/dev/null; then\n :\nelif [ "$(/usr/bin/id -u)" -eq 0 ]; then|' /etc/profile \
&& mkdir -p /environment/workspace /environment/initialization /environment/packages /workspace /home/runtime \
&& chown 1000:1000 /environment/initialization /environment/packages
COPY --chmod=0555 oac-sandbox-io /usr/local/bin/
Expand Down
2 changes: 1 addition & 1 deletion docs/maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export CODEX_CLI_DIR=/absolute/path/to/package MCODE_HARNESS_BUILD_DIR=/absolute
bash scripts/build-agent-host-images.sh
```

The script builds `OAC_AGENT_HOST_IMAGE` (default `oac-agent-host:dev`) and `OAC_SANDBOX_IMAGE` (default `oac-sandbox:dev`). Additional arguments, such as `--label`, go to both Docker builds. The agent-host image contains `oac-daemon`, `oac-process-shim` and each Harness under `/opt/oac/harnesses`; `/opt/oac/harnesses.json` owns their activation paths. The sandbox image contains system tools and `oac-sandbox-io`, which runs as UID/GID 1000, with no Harness or daemon. The distribution verifies its Sandbox I/O executable and ships this image through the node artifact's existing `runtime` slot. E2B templates extract Sandbox I/O from this same image. [Qualify the view](../contracts/agents-api/harness-onboarding.md#qualify-the-view) runs both images; [Agent-host container](./configuration.md#agent-host-container) owns the host requirements.
The script builds `OAC_AGENT_HOST_IMAGE` (default `oac-agent-host:dev`) and `OAC_SANDBOX_IMAGE` (default `oac-sandbox:dev`). Additional arguments, such as `--label`, go to both Docker builds. The agent-host image contains `oac-daemon`, `oac-process-shim` and each Harness under `/opt/oac/harnesses`; `/opt/oac/harnesses.json` owns their activation paths. The sandbox image contains system tools and `oac-sandbox-io`, which runs as UID/GID 1000, with no Harness or daemon. The distribution verifies its Sandbox I/O executable and ships this image through the node artifact's existing `runtime` slot. The sandbox's default login profile preserves a supplied nonempty `PATH`, so the [tool environment](../contracts/agents-api/environments.md#explicit-local-tool-environment) keeps its package commands available. Empty or unset paths retain the image's defaults; profile.d scripts and user startup files can still explicitly change them. Third-party sandbox images own their shell startup configuration. E2B templates extract Sandbox I/O and this profile from the same image. [Qualify the view](../contracts/agents-api/harness-onboarding.md#qualify-the-view) runs both images; [Agent-host container](./configuration.md#agent-host-container) owns the host requirements.

**E2B helper.**

Expand Down
4 changes: 2 additions & 2 deletions docs/zh/maintainers.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "构建并发布 OpenAgentCore"
source: docs/maintainers.md
source_hash: b69ab5a1de19ef81942658187d93dfca14c325c3824e7379e88899f3e3cebf61
source_hash: a6d2d1e9846b1f22a9566048fd3821eee601010aea572c6a402436800c44491b
---

本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请使用 [安装指南](getting-started/install.md)。安装器代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装器](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。
Expand Down Expand Up @@ -70,7 +70,7 @@ export CODEX_CLI_DIR=/absolute/path/to/package MCODE_HARNESS_BUILD_DIR=/absolute
bash scripts/build-agent-host-images.sh
```

脚本构建 `OAC_AGENT_HOST_IMAGE`(默认 `oac-agent-host:dev`)和 `OAC_SANDBOX_IMAGE`(默认 `oac-sandbox:dev`)。附加参数(例如 `--label`)传给两次 Docker 构建。agent-host 镜像包含 `oac-daemon`、`oac-process-shim`,以及 `/opt/oac/harnesses` 下的各 Harness;`/opt/oac/harnesses.json` 拥有其激活路径。沙箱镜像包含系统工具和以 UID/GID 1000 运行的 `oac-sandbox-io`,不含 Harness 或 daemon。分发包校验 Sandbox I/O 可执行文件,并通过节点构件已有的 `runtime` 槽位分发此镜像。E2B 模板从同一镜像提取 Sandbox I/O。[认定视图资格](../../contracts/agents-api/zh/harness-onboarding.md#qualify-the-view)运行两个镜像;[Agent-host 容器](configuration.md#agent-host-container)拥有宿主需求。
脚本构建 `OAC_AGENT_HOST_IMAGE`(默认 `oac-agent-host:dev`)和 `OAC_SANDBOX_IMAGE`(默认 `oac-sandbox:dev`)。附加参数(例如 `--label`)传给两次 Docker 构建。agent-host 镜像包含 `oac-daemon`、`oac-process-shim`,以及 `/opt/oac/harnesses` 下的各 Harness;`/opt/oac/harnesses.json` 拥有其激活路径。沙箱镜像包含系统工具和以 UID/GID 1000 运行的 `oac-sandbox-io`,不含 Harness 或 daemon。分发包校验 Sandbox I/O 可执行文件,并通过节点构件已有的 `runtime` 槽位分发此镜像。沙箱的默认登录 profile 保留传入的非空 `PATH`,使[工具环境](../../contracts/agents-api/zh/environments.md#explicit-local-tool-environment)中的包命令仍然可用。路径为空或未设置时保留镜像的默认值;profile.d 脚本和用户启动文件仍可显式修改路径。第三方沙箱镜像自行负责其 shell 启动配置。E2B 模板从同一镜像提取 Sandbox I/O 和此 profile。[认定视图资格](../../contracts/agents-api/zh/harness-onboarding.md#qualify-the-view)运行两个镜像;[Agent-host 容器](configuration.md#agent-host-container)拥有宿主需求。

**E2B 辅助程序。**

Expand Down
6 changes: 6 additions & 0 deletions scripts/core-distribution-manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ def verify_runtime(image, sandbox_io):
actual = dict(reversed(line.split(None, 1)) for line in output.splitlines())
if actual.get(guest_path) != sha256(sandbox_io):
raise ValueError("Sandbox image does not match the committed build: " + guest_path)
tool_path = "/environment/packages/npm/bin:/environment/packages/python/bin:/usr/local/bin:/usr/bin:/bin"
subprocess.run(
["docker", "run", "--rm", "--label", "io.oac.build=distribution-verify", "--network", "none",
"--user", "1000:1000", "--env", "PATH=" + tool_path, "--entrypoint", "/bin/bash", image,
"-lc", 'test "$PATH" = "$1"', "oac-login-check", tool_path], check=True
)


def extract_runtime(archive, destination):
Expand Down
5 changes: 4 additions & 1 deletion scripts/core-distribution-manifest.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,15 @@ def test_sandbox_executable_must_match_build_input(self):
path = "/usr/local/bin/oac-sandbox-io"
for digest in (distribution.sha256(executable), "0" * 64):
with self.subTest(digest=digest), mock.patch.object(distribution, "verify_image"), \
mock.patch.object(distribution.subprocess, "check_output", return_value=digest + " " + path + "\n"):
mock.patch.object(distribution.subprocess, "check_output", return_value=digest + " " + path + "\n"), \
mock.patch.object(distribution.subprocess, "run") as login:
if digest == "0" * 64:
with self.assertRaisesRegex(ValueError, "Sandbox image does not match"):
distribution.verify_runtime("sha256:" + "a" * 64, executable)
login.assert_not_called()
else:
distribution.verify_runtime("sha256:" + "a" * 64, executable)
login.assert_called_once()

def test_mcode_payload_rejects_stale_companion_at_the_same_version(self):
repository = pathlib.Path(__file__).resolve().parent.parent
Expand Down
2 changes: 1 addition & 1 deletion services/core/deploy/e2b/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ python -m venv "$HOME/.oac/build/e2b-sdk"
--output "$HOME/.oac/build/e2b-template.json"
```

[`build-template.py`](build-template.py) requires a `sha256:` image ID and a Linux amd64 image built from the `sandbox` target of [`AgentHost.Dockerfile`](../../../../deploy/distribution/AgentHost.Dockerfile). It copies only `/usr/local/bin/oac-sandbox-io` onto a digest-pinned `node:22.23.1-bookworm-slim` base with `ca-certificates`, `bash`, `git`, `python3`, `python3-pip`, `ripgrep` and `util-linux`. It installs [`managed_init.py`](managed_init.py) read-only under `/opt/oac-e2b`, makes UID/GID 1000 (`runtime`, home `/home/runtime`) the template user and builds with 2 vCPUs and 2048 MiB. The archive's `usr`, `usr/local` and `usr/local/bin` ancestors have traversable modes; the executable's mode, private build contexts, key inputs and the output's umask stay unchanged. Harnesses run on the separate agent host.
[`build-template.py`](build-template.py) requires a `sha256:` image ID and a Linux amd64 image built from the `sandbox` target of [`AgentHost.Dockerfile`](../../../../deploy/distribution/AgentHost.Dockerfile). It copies `/usr/local/bin/oac-sandbox-io` and the qualified image's `/etc/profile` onto a digest-pinned `node:22.23.1-bookworm-slim` base with `ca-certificates`, `bash`, `git`, `python3`, `python3-pip`, `ripgrep` and `util-linux`. It installs [`managed_init.py`](managed_init.py) read-only under `/opt/oac-e2b`, makes UID/GID 1000 (`runtime`, home `/home/runtime`) the template user and builds with 2 vCPUs and 2048 MiB. The archive's `etc`, `usr`, `usr/local` and `usr/local/bin` ancestors have traversable modes; the executable's mode, private build contexts, key inputs and the output's umask stay unchanged. Harnesses run on the separate agent host.


The output file records `template` (the immutable `templateID:build_UUID`), the source `image`, the packaged `runtime_sha256` and the `base`. Use that exact `template` value. Install system dependencies into the image; sandbox processes run as UID/GID 1000. No E2B account key or model credential belongs in a build, template environment, metadata, command argument or log.
Expand Down
14 changes: 7 additions & 7 deletions services/core/deploy/e2b/build-template.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@
tree.mkdir()
# These public ancestors are synthesized, not extracted from the sandbox.
# Keep their archive modes independent of the caller's private umask.
for parent in ['usr', 'usr/local', 'usr/local/bin']:
for parent in ['usr', 'usr/local', 'usr/local/bin', 'etc']:
directory = tree / parent
directory.mkdir()
directory.chmod(0o755)
container = subprocess.check_output(['docker', 'create', '--label', 'io.oac.build=e2b-template', args.image], text=True).strip()
try:
with tempfile.TemporaryFile() as copied:
path = '/usr/local/bin/oac-sandbox-io'
subprocess.run(['docker', 'cp', container + ':' + path, '-'], stdout=copied, check=True)
copied.seek(0)
with tarfile.open(fileobj=copied) as archive:
archive.extractall(tree / 'usr/local/bin', filter='tar')
for path in ['/usr/local/bin/oac-sandbox-io', '/etc/profile']:
with tempfile.TemporaryFile() as copied:
subprocess.run(['docker', 'cp', container + ':' + path, '-'], stdout=copied, check=True)
copied.seek(0)
with tarfile.open(fileobj=copied) as archive:
archive.extractall(tree / Path(path).parent.relative_to('/'), filter='tar')
finally:
subprocess.run(['docker', 'rm', container], check=True, stdout=subprocess.DEVNULL)
bundle = context / 'runtime.tar.gz'
Expand Down
26 changes: 13 additions & 13 deletions services/core/deploy/e2b/build_template_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def test_public_parents_and_runtime_modes_under_both_umasks(self):
key.write_text('fixture-only')
key.chmod(0o600)
output = root / 'private/result.json'
source_modes = {'oac-sandbox-io': 0o555}
sources = {'usr/local/bin/oac-sandbox-io': (0o555, b'fixture'),
'etc/profile': (0o644, b'# qualified image profile\nexport PATH\n')}
image = {'Architecture': 'amd64', 'Os': 'linux', 'Id': 'sha256:fixture',
'Config': {'Env': ['HOME=/home/runtime']}}

Expand All @@ -38,15 +39,13 @@ def run(argv, **kwargs):
return SimpleNamespace(returncode=0)
self.assertEqual(argv[:2], ['docker', 'cp'])
self.assertEqual(argv[-1], '-')
name = argv[2].split(':', 1)[1].rsplit('/', 1)[-1]
name = argv[2].split(':', 1)[1].lstrip('/')
mode, data = sources[name]
with tarfile.open(fileobj=kwargs['stdout'], mode='w') as archive:
for path, mode in source_modes.items():
if path != name and not path.startswith(name + '/'):
continue
entry = tarfile.TarInfo(path)
entry.mode = mode
entry.size = 7
archive.addfile(entry, io.BytesIO(b'fixture'))
entry = tarfile.TarInfo(Path(name).name)
entry.mode = mode
entry.size = len(data)
archive.addfile(entry, io.BytesIO(data))
return SimpleNamespace(returncode=0)

template = Mock()
Expand All @@ -59,11 +58,12 @@ def build(instance, **kwargs):
self.assertEqual(stat.S_IMODE(context.stat().st_mode), 0o700)
with tarfile.open(context / 'runtime.tar.gz') as archive:
modes = {m.name: stat.S_IMODE(m.mode) for m in archive.getmembers()}
for parent in ('usr', 'usr/local', 'usr/local/bin'):
for path, (mode, data) in sources.items():
self.assertEqual(modes[path], mode)
self.assertEqual(archive.extractfile(path).read(), data)
for parent in ('usr', 'usr/local', 'usr/local/bin', 'etc'):
self.assertEqual(modes[parent], 0o755)
for path, mode in source_modes.items():
self.assertEqual(modes['usr/local/bin/' + path], mode)
self.assertEqual(set(modes), {'usr', 'usr/local', 'usr/local/bin', 'usr/local/bin/oac-sandbox-io'})
self.assertEqual(set(modes), {'usr', 'usr/local', 'usr/local/bin', 'etc', *sources})
self.assertEqual(stat.S_IMODE((context / 'runtime.tar.gz').stat().st_mode), 0o666 & ~mask)
self.assertEqual(stat.S_IMODE(key.stat().st_mode), 0o600)
projection = 'helper_contract_generated.py'
Expand Down
Loading