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
4 changes: 2 additions & 2 deletions docs/maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This guide is for maintainers who build and publish OpenAgentCore. To install Co

## Build a distribution

A distribution is a matched set of release assets built from one commit: the control archive (the installer, the `oac` command, and the Core, Web, ingress and PostgreSQL images), the Runtime image and node artifacts as separate files, and the native installers.
A distribution is a matched set of release assets built from one commit: the Runtime image and node artifacts as separate files, and the native installers. The build also writes a control archive per architecture (the installer, the `oac` command, and the Core, Web, ingress and PostgreSQL images). The publisher reads the images and the native catalog from it; nothing installs from it, so it is not a release asset.

Core, Web and ingress images are published as verified Linux amd64/arm64 indexes. The arm64 control archive contains those three images; Node, hosted Runtime and offline payloads use Linux amd64. Release builders use QEMU for ARM image steps, including the E2B helper. Host `oac` binaries are built from the same command for Linux amd64/arm64, macOS amd64/arm64 and Windows amd64; launchers only select, verify and invoke them. Each version index is checked against its platform archives before floating tags move.

Expand Down Expand Up @@ -53,7 +53,7 @@ node scripts/build-native-catalog.mjs INPUT_DIR OUTPUT_DIR
export OAC_NATIVE_INSTALLER_BUILD_DIR=OUTPUT_DIR
```

Each platform has a small base archive `<os>-<arch>.tar.gz` (`oac-daemon` and `bundle.json`, which lists every component with the SHA-256 of each file) and one archive per component, `<os>-<arch>-node` and `<os>-<arch>-<harness>` (MiniMax Code is not built for Windows). `build-native-installer.mjs` writes them. Core serves these artifacts by name and knows nothing of their content; `oac-daemon install` downloads the components it needs.
Each platform has a small base archive `<os>-<arch>.tar.gz` (`oac-daemon` and `bundle.json`, which lists every component with the SHA-256 of each file) and one archive per component, `<os>-<arch>-node` and `<os>-<arch>-<harness>` (MiniMax Code is not built for Windows). `build-native-installer.mjs` writes them and leaves out files that only build or install tooling: the CI runner's global pnpm in `node`, and the compiler inputs of `better-sqlite3` in `minimax`. Core serves these artifacts by name and knows nothing of their content; `oac-daemon install` downloads the components it needs.

The catalog records the commit, the Runtime protocol version, each artifact's SHA-256 and, with a release base, its versioned URL. The control archive and Core image carry only `native-installers/catalog.json`; the archives become separate `oac-native-<commit>-<artifact>.tar.gz` release assets, and the offline archive holds a copy of every Linux amd64 archive outside the Core image, which its `install.py` copies into the installation for Core to serve. Core redirects the other platforms to their release assets; a catalog without a release base has no such URL, so its offline archive holds every archive. Without a catalog, Sessions report the install command as unavailable, and the release workflow refuses to publish.

Expand Down
6 changes: 3 additions & 3 deletions docs/zh/maintainers.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
title: "构建并发布 OpenAgentCore"
source: docs/maintainers.md
source_hash: 6f1b4534b4b473c47175560c24ca6e1d1fe2ff4c5e764f204d1022939e4178b3
source_hash: 0b605c6f5df332a2eb9831d3026b4bcde4ae6d7a2e2cb7bd8b3ce3a21704a7db
---

本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请按 [安装 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)。

## 构建分发包 {#build-a-distribution}

分发包是从同一个提交构建的一组相互匹配的发布资源:控制归档(安装程序、`oac` 命令,以及 Core、Web、ingress 和 PostgreSQL 镜像)、作为独立文件的 Runtime 镜像和节点构件,以及原生安装程序。
分发包是从同一个提交构建的一组相互匹配的发布资源:作为独立文件的 Runtime 镜像和节点构件,以及原生安装程序。构建还会为每种架构生成一个控制归档(安装程序、`oac` 命令,以及 Core、Web、ingress 和 PostgreSQL 镜像)。发布器从中读取镜像和原生目录;没有任何安装流程使用它,因此它不是发布资源。

Core、Web 和 ingress 镜像发布为经过校验的 Linux amd64/arm64 多架构索引。arm64 控制归档包含这三个镜像;Node、托管 Runtime 和离线包使用 Linux amd64。发行构建使用 QEMU 执行 ARM 镜像步骤,包括 E2B helper。主机 `oac` 从同一份实现构建为 Linux amd64/arm64、macOS amd64/arm64 和 Windows amd64 二进制;启动脚本只选择、校验并运行它们。所有版本索引校验通过后才更新浮动标签。

Expand Down Expand Up @@ -55,7 +55,7 @@ node scripts/build-native-catalog.mjs INPUT_DIR OUTPUT_DIR
export OAC_NATIVE_INSTALLER_BUILD_DIR=OUTPUT_DIR
```

每个平台有一个小型基础归档 `<os>-<arch>.tar.gz`(`oac-daemon` 和 `bundle.json`,后者列出每个组件及其每个文件的 SHA-256),以及每个组件一个归档,即 `<os>-<arch>-node` 和 `<os>-<arch>-<harness>`(MiniMax Code 不为 Windows 构建)。`build-native-installer.mjs` 生成它们。Core 按名称提供这些制品,不了解其内容;`oac-daemon install` 下载所需的组件。
每个平台有一个小型基础归档 `<os>-<arch>.tar.gz`(`oac-daemon` 和 `bundle.json`,后者列出每个组件及其每个文件的 SHA-256),以及每个组件一个归档,即 `<os>-<arch>-node` 和 `<os>-<arch>-<harness>`(MiniMax Code 不为 Windows 构建)。`build-native-installer.mjs` 生成它们,并略去仅用于构建或安装的文件:`node` 中 CI 运行器全局安装的 pnpm,以及 `minimax` 中 `better-sqlite3` 的编译输入。Core 按名称提供这些制品,不了解其内容;`oac-daemon install` 下载所需的组件。

该目录会记录提交、Runtime 协议版本、每个制品的 SHA-256,并在提供发布基址时记录其带版本号的 URL。控制归档和 Core 镜像仅携带 `native-installers/catalog.json`;这些归档会成为独立的 `oac-native-<commit>-<artifact>.tar.gz` 发布资源,离线归档则会在 Core 镜像之外保留所有 Linux amd64 归档的一份副本,由其 `install.py` 复制到安装目录中供 Core 提供下载。其他平台由 Core 重定向到对应的发布资源;没有发布基址的目录没有这类 URL,因此其离线归档会保留所有归档。如果没有目录,Sessions 会将安装命令报告为不可用,并且发布工作流会拒绝发布。

Expand Down
13 changes: 11 additions & 2 deletions scripts/build-native-installer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ async function outputRealPath(path) {

// Flatten contained links, including directory links used by pnpm exports. Every
// traversal checks its resolved target; active ancestors detect directory cycles.
export async function copyComponent(source, destination) {
export async function copyComponent(source, destination, omit = []) {
const root = await realpath(source);
if (!(await stat(root)).isDirectory()) throw new Error('Component source must be a directory');
const files = {};
async function visit(path, target, ancestors) {
if (omit.includes(relative(destination, target).split(sep).join('/'))) return;
const resolved = await realpath(path);
if (!inside(root, resolved)) throw new Error('Component link escapes its source');
const info = await stat(resolved);
Expand Down Expand Up @@ -66,6 +67,14 @@ function probe(command, args, cwd, env) {
if (result.error || result.status !== 0) throw new Error('Native component compatibility probe failed');
return result.stdout.trim();
}
// Files that only build or install tooling and that no Harness or the daemon loads at run time.
// pnpm is the CI runner's global install, not part of the Node distribution. better-sqlite3 loads only
// build/Release/better_sqlite3.node, with SQLite compiled in; deps/ and src/ are its compiler inputs.
// Names cover the Unix and Windows layouts.
const buildOnly = {
node: ['bin/pnpm', 'bin/pnpx', 'lib/node_modules/pnpm', 'node_modules/pnpm', 'pnpm', 'pnpm.cmd', 'pnpm.ps1', 'pnpx', 'pnpx.cmd', 'pnpx.ps1'],
minimax: ['native/node_modules/better-sqlite3/deps', 'native/node_modules/better-sqlite3/src', 'native/node_modules/better-sqlite3/binding.gyp'],
};
const json = async path => JSON.parse(await readFile(path, 'utf8'));
const requireFile = async path => { if (!(await stat(path)).isFile()) throw new Error('Required component file is missing'); };

Expand Down Expand Up @@ -160,7 +169,7 @@ export async function buildBundle(options) {
await copyFile(options.daemon, daemon); await chmod(daemon, 0o755);
await mkdir(join(staging, 'components'));
const components = {};
for (const name of ['node', ...names]) components[name] = { version: pins[name], files: await copyComponent(options[name], join(staging, 'components', name)) };
for (const name of ['node', ...names]) components[name] = { version: pins[name], files: await copyComponent(options[name], join(staging, 'components', name), buildOnly[name]) };
await prepareNodeEntrypoints(join(staging, 'components', 'node'), components.node.files);
if (components.minimax) {
const component = join(staging, 'components', 'minimax');
Expand Down
12 changes: 12 additions & 0 deletions scripts/build-native-installer.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ test('component preserves bytes and executable metadata with contained links fla
assert.deepEqual(Object.keys(files).sort(), ['lib/native', 'linked/native']);
});

test('component leaves out the named build-only paths', async t => {
const { source, output } = await fixture(t);
await mkdir(join(source, 'pkg/deps'), { recursive: true });
await mkdir(join(source, 'pkg/build'), { recursive: true });
await writeFile(join(source, 'pkg/deps/sqlite3.c'), 'source');
await writeFile(join(source, 'pkg/binding.gyp'), '{}');
await writeFile(join(source, 'pkg/build/addon.node'), 'binary');
const files = await copyComponent(source, output, ['pkg/deps', 'pkg/binding.gyp']);
assert.deepEqual(Object.keys(files), ['pkg/build/addon.node']);
await assert.rejects(lstat(join(output, 'pkg/deps')));
});

test('component rejects escaping directory links', async t => {
const { root, source, output } = await fixture(t);
await mkdir(join(root, 'outside')); await writeFile(join(root, 'outside/secret'), 'not bundled');
Expand Down
4 changes: 4 additions & 0 deletions scripts/publish-core-release.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ def publish(assets, repository, revision, tag, mode):
files = sorted(assets.iterdir())
if not files or any(p.is_symlink() or not p.is_file() for p in files):
raise ValueError("Expected a nonempty directory containing only regular asset files")
# The control archives carry the Core, Web and ingress images and the native
# catalog from the build to this script. Nothing installs from them, so they are not release assets.
control = {f"oac-{revision}-linux-{arch}.tar.gz{suffix}" for arch in ("amd64", "arm64") for suffix in ("", ".sha256")}
# The builder validates the manifest and Runtime assets. Verify archives again
# after the Actions artifact transfer between jobs.
stem = "oac-" + revision + "-linux-amd64"
Expand Down Expand Up @@ -296,6 +299,7 @@ def publish(assets, repository, revision, tag, mode):
endpoint = "releases/" + str(release_id)
# Keep every operation bound to the ID returned by creation. No tag lookup,
# overwrite, deletion or automatic retry can select another release.
files = [p for p in files if p.name not in control]
expected = {p.name: p.stat().st_size for p in files}
def upload(path):
print(f"Uploading {path.name} ({expected[path.name]} bytes)", flush=True)
Expand Down
8 changes: 5 additions & 3 deletions scripts/publish-core-release.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def test_draft_publishes_images_and_stays_unpublished(self):
self.publish(tag="build-" + self.revision, mode="draft")
self.images.assert_called_once()
self.assertTrue(self.release["draft"])
self.assertEqual(len(self.release["assets"]), 28)
self.assertEqual(len(self.release["assets"]), 24)

def test_missing_native_asset_refuses_release_creation(self):
(self.assets / f"oac-native-{self.revision}-windows-amd64-codex.tar.gz").unlink()
Expand Down Expand Up @@ -142,7 +142,7 @@ def response(repo, endpoint, *args):
return result
if endpoint == "releases/7":
self.assertEqual(active, 0)
self.assertIn(len(self.release["assets"]), (26, 28))
self.assertIn(len(self.release["assets"]), (22, 24))
return self.response(repo, endpoint, *args)
self.api.side_effect = response
self.publish()
Expand All @@ -153,8 +153,10 @@ def test_version_tag_publishes_complete_fixed_id(self):
self.publish()
self.assertFalse(self.release["draft"])
self.assertFalse(self.release["prerelease"])
self.assertEqual(len(self.release["assets"]), 28)
self.assertEqual(len(self.release["assets"]), 24)
self.assertEqual({a["name"] for a in self.release["assets"] if a["name"].endswith(".yaml")}, {"compose.yaml"})
self.assertFalse([a for a in self.release["assets"] if "-linux-amd64.tar.gz" in a["name"] and a["name"].startswith("oac-" + self.revision)
or "-linux-arm64.tar.gz" in a["name"] and a["name"].startswith("oac-" + self.revision)])
self.assertEqual(self.api.call_args.args[1:],
("releases/7", "--method", "PATCH", "-F", "draft=false"))

Expand Down
Loading