Skip to content
Open
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
19 changes: 18 additions & 1 deletion apps/daemon/internal/agent/codex/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"context"
"errors"
"fmt"
obslog "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log"
"os/exec"
"strings"
"time"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/binpath"
)
Expand Down Expand Up @@ -42,7 +44,22 @@ func CheckCLIAvailable(ctx context.Context, binary string) (string, error) {
cmd := exec.CommandContext(ctx, binary, "--version")
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
spawnAt := time.Now()
err := cmd.Start()
status := "ok"
if err != nil {
status = "error"
}
obslog.Info(ctx, "runtime version probe", "harness_kind", "codex", "stage", "process_spawn", "duration_ms", float64(time.Since(spawnAt))/float64(time.Millisecond), "status", status)
if err == nil {
waitAt := time.Now()
err = cmd.Wait()
if err != nil {
status = "error"
}
obslog.Info(ctx, "runtime version probe", "harness_kind", "codex", "stage", "process_wait", "duration_ms", float64(time.Since(waitAt))/float64(time.Millisecond), "status", status)
}
if err != nil {
msg := strings.TrimSpace(stderr.String())
if msg == "" {
msg = err.Error()
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/cli/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func runConnect(ctx *runContext, args []string) error {
if *serverURL != "" || *token != "" || *deviceName != "" || *remote != "" || *environment != "" || *credentialFile != "" || fs.NArg() != 0 {
return errors.New("connect: bootstrap input cannot be combined with enrollment or pairing options")
}
bootstrapped, err = bootstrapProfile(*bootstrapFile)
bootstrapped, err = bootstrapProfile(*bootstrapFile, ctx)
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion apps/daemon/internal/cli/connect_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

// The launch file is the sole credential source for this connection. Reopening
// it on process restart neither pairs again nor overwrites an auth profile.
func bootstrapProfile(path string) (*auth.Profile, error) {
func bootstrapProfile(path string, rc *runContext) (*auth.Profile, error) {
raw, err := runtimefs.ReadPrivatePath(path, runtimebootstrap.MaxBytes)
if err != nil {
return nil, errors.New("connect: Runtime bootstrap file unavailable")
Expand All @@ -18,5 +18,6 @@ func bootstrapProfile(path string) (*auth.Profile, error) {
if err != nil {
return nil, err
}
rc.installedKinds = map[string]bool{input.Harness: true}
return &auth.Profile{ServerURL: input.CoreURL, RuntimeID: input.DeviceID, RunnerCredential: input.Credential}, nil
}
12 changes: 8 additions & 4 deletions apps/daemon/internal/cli/connect_bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestBootstrapConnectionDoesNotReadOrOverwritePrivateProfile(t *testing.T) {
if err := auth.Save("default", prior); err != nil {
t.Fatal(err)
}
input := runtimebootstrap.Connection{Version: runtimebootstrap.Version, CoreURL: "https://core.example/api/v1", DeviceID: "da912024-1543-4242-a2c1-5f4f7ebbc6c7", Credential: "bootstrap-secret"}
input := runtimebootstrap.Connection{Version: runtimebootstrap.Version, CoreURL: "https://core.example/api/v1", DeviceID: "da912024-1543-4242-a2c1-5f4f7ebbc6c7", Credential: "bootstrap-secret", Harness: "codex"}
raw, err := input.Marshal()
if err != nil {
t.Fatal(err)
Expand All @@ -28,7 +28,11 @@ func TestBootstrapConnectionDoesNotReadOrOverwritePrivateProfile(t *testing.T) {
t.Fatal(err)
}
for range 2 {
p, err := bootstrapProfile(path)
rc := &runContext{}
p, err := bootstrapProfile(path, rc)
if !rc.installedKinds["codex"] || len(rc.installedKinds) != 1 {
t.Fatal("bootstrap did not scope discovery")
}
if err != nil || p.ServerURL != input.CoreURL || p.RuntimeID != input.DeviceID || p.RunnerCredential != input.Credential {
t.Fatal("failed bootstrap/restart", err)
}
Expand All @@ -40,13 +44,13 @@ func TestBootstrapConnectionDoesNotReadOrOverwritePrivateProfile(t *testing.T) {
if err = os.WriteFile(path, []byte("bootstrap-secret"), 0600); err != nil {
t.Fatal(err)
}
if _, err = bootstrapProfile(path); err == nil || strings.Contains(err.Error(), input.Credential) {
if _, err = bootstrapProfile(path, &runContext{}); err == nil || strings.Contains(err.Error(), input.Credential) {
t.Fatal("invalid input fell back or leaked")
}
if err = os.Remove(path); err != nil {
t.Fatal(err)
}
if _, err = bootstrapProfile(path); err == nil {
if _, err = bootstrapProfile(path, &runContext{}); err == nil {
t.Fatal("missing input fell back to private auth")
}
}
Expand Down
20 changes: 20 additions & 0 deletions apps/daemon/internal/cli/native_discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,23 @@ func TestDiscoveryUnavailableAndCancelled(t *testing.T) {
t.Fatal("unconfigured adapter retained")
}
}

func TestSelectedHarnessUnavailableDoesNotProbeOthers(t *testing.T) {
declarations := append([]agent.Declaration(nil), harnessDeclarations...)
for i := range declarations {
declarations[i].Discover = func(_ context.Context, _ agent.DiscoveryOptions, info proto.SupportedAgentKind) *agent.Runtime {
if info.Kind != "codex" {
t.Fatalf("unselected Harness was probed: %s", info.Kind)
}
return &agent.Runtime{Info: info}
}
}
rc := &runContext{stdout: io.Discard, stderr: io.Discard, installedKinds: map[string]bool{"codex": true}}
if _, err := discoverAgentCLIs(t.Context(), rc, "default", declarations); err == nil {
t.Fatal("unavailable selection was accepted")
}
rc.installedKinds = map[string]bool{"unknown": true}
if _, err := discoverAgentCLIs(t.Context(), rc, "default", declarations); err == nil {
t.Fatal("unknown selection fell back")
}
}
2 changes: 2 additions & 0 deletions contracts/agents-api/node-generation-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,5 @@ An interrupted download repairs only missing bytes at the original paths. When c
The diagnostic codes are authored in `services/core/internal/sandbox/node_diagnostic.go`. The shared `services/core/internal/sandbox/testdata/node-diagnostics.json` fixture checks the Go mapping, OpenAPI source annotations and generated enums, and the TypeScript client declaration. Web uses the client normalizer and checks localized messages for every declared code. Update these projections with a code change; unknown codes normalize to `provider_unavailable`.

Preparation diagnostics keep fixed typed causes. Only artifact transfer, checksum or release-provenance failures report `runtime_download_failed`; the private preparer signals that class through its exit category, without Core or the node parsing stderr. Provider, ownership, cancellation and unclassified failures keep their typed code or `provider_unavailable`. No raw provider text crosses the protocol.

Creation carries the Session-selected `Bootstrap.Harness` into Runtime bootstrap version 2. Core and nodes use protocol version 5; upgrade matching nodes before publishing this Core release.
4 changes: 3 additions & 1 deletion contracts/agents-api/zh/node-generation-protocol.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "沙箱节点协议"
source: contracts/agents-api/node-generation-protocol.md
source_hash: 1ee43dfcdd0eec0806ea3bc8a4c1227e10bd8ac5e69486505cb113a98e3f548a
source_hash: 39ffd8649b19d3bd0a12a224b650ccd14ffaa73eadbe9646075b698cb7bffa74
---

沙箱节点在其主机上运行 Docker 或 microsandbox Provider,并通过一个 WebSocket 与 Core 相连。Core 通过该连接发送 Provider 操作;节点针对本地 Provider 执行这些操作,并报告就绪状态、主机测量值及其持有的部署代次。Core 始终是唯一的生命周期所有者:节点绝不重试变更操作或调度工作。帧和校验器位于 [`services/core/internal/sandbox/node`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/node)(`wire.go`、`generation_wire.go`);节点用于注册和读取配置的 HTTP 路由位于[机器连接 API](machine-api.md#node-routes)。
Expand Down Expand Up @@ -124,3 +124,5 @@ Runtime 字节缺失时,绝不将固定的放置实例迁移到当前 Runtime
诊断代码编写于 `services/core/internal/sandbox/node_diagnostic.go`。共享的 `services/core/internal/sandbox/testdata/node-diagnostics.json` 测试夹具检查 Go 映射、OpenAPI 源注释和生成的枚举,以及 TypeScript 客户端声明。Web 使用客户端规范化器,并检查每个已声明代码的本地化消息。代码变更时要同步更新这些投影;未知代码会规范化为 `provider_unavailable`。

准备诊断使用固定的类型化原因。只有制品传输、校验和或版本来源验证失败才会报告 `runtime_download_failed`;私有准备器通过退出类别指示这一类失败,Core 和节点都不解析 stderr。Provider 故障、所有权故障、取消和未分类故障保留其类型化代码,或使用 `provider_unavailable`。协议中不会传输任何 Provider 原始文本。

创建操作通过 `Bootstrap.Harness` 将会话选择传递到 Runtime 启动协议版本 2。Core 和节点使用协议版本 5;发布此 Core 版本前需升级配套节点。
4 changes: 4 additions & 0 deletions docs/getting-started/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,7 @@ Mutating `oac` commands hold `.oac.lock`. If another command holds it, retry aft
Web signs administrators in with the Core key, checks the origin of every request, and forwards signed-in `/core/v1` requests to Core with the Core key, which stays on the server. It forwards `/v1` and `/api/v1` to Core unchanged, with the caller's own credential, serves only the non-secret node payload at `/node-install/`, and has no Docker or KVM access. Machine routes under `/api/v1` use their own enrollment and connection credentials. No service receives a Docker socket.

Sandboxes are the isolation boundary ([Runtime and outer isolation](../concepts.md#runtime-and-outer-isolation)). Docker sandboxes share the node's kernel, and a Docker node is [root-equivalent](./nodes.md#what-the-installer-sets-up) on its host; microsandbox gives each sandbox a microVM with an explicit [network policy](./nodes.md#what-the-installer-sets-up). Core itself has no Docker socket or KVM access.

## Runtime availability probes

Codex CLI availability emits `runtime version probe` records for `process_spawn` and `process_wait`, with duration and outcome only. The wait interval includes executable loading and the version command; neither interval is model execution.
3 changes: 3 additions & 0 deletions docs/runtime-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ oac-daemon connect --bootstrap-file /home/runtime/runtime-bootstrap.json
| `version` | The exact bootstrap version, `runtimebootstrap.Version` |
| `core_url` | HTTP(S) machine API base ending in `/api/v1`, without credentials, query or fragment |
| `device_id` | Canonical nonzero UUID of the daemon identity Core issued |
| `harness` | The immutable Session Harness identifier; only this Harness is probed and registered by a managed Runtime |
| `credential` | Nonempty daemon credential Core issued, without whitespace or NUL |

The decoder rejects unknown, duplicate, missing and case-aliased fields, other versions and documents larger than `runtimebootstrap.MaxBytes` (16 KiB). Errors never include submitted values. A missing or malformed file fails before the daemon connects.
Expand All @@ -27,6 +28,8 @@ The file is the only authentication input for this launch: the daemon refuses to

The provider creates the account, mounts and workspace, delivers this file, sets the Runtime's resource and Environment binding settings, and starts the daemon as the unprivileged Runtime account. Docker writes the file into the Runtime's owned home volume; microsandbox and E2B deliver it before launching the same command.

Core derives `harness` from the Session that owns the allocation. A combined image can contain several Harnesses, but its managed Runtime discovers only this selection; an unknown, missing or unavailable selection fails without probing another Harness. The bootstrap version is 2. Upgrade Core, its provider helpers and newly launched Runtime images together; retained allocations keep their existing bootstrap and Runtime. Self-hosted installations continue to discover their installed Harness set.

The Runtime validates the input and owns authentication and connection. A successful launch proves only the handoff: an authenticated connection, prepared capabilities and execution readiness are separate observations under the [Core–Runtime protocol](./runtime-protocol.md), and the [Sandbox Provider guide](./sandbox-provider.md#four-distinct-readiness-facts) lists what each one proves.

Self-hosted executors and operator-provisioned devices get their daemon identity in other ways; the [machine connection API](../contracts/agents-api/machine-api.md#credentials) lists every credential source. All of them enter the same Runtime execution loop.
Expand Down
2 changes: 2 additions & 0 deletions docs/sandbox-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ Node readiness binds to the exact generation, the current connection and the own

### Allocation lifecycle

Core includes the owning Session’s immutable Harness in `Bootstrap.Harness`; every provider projects it into the [Runtime bootstrap](./runtime-bootstrap.md) without choosing an implementation.

The allocation, its dedicated daemon credential digest and the exact Session binding commit atomically before `Create`, under the execution lease and the Session lock. Only a fresh allocation receipt permits `Create`; retries and a Core restart observe the same reference without replaying it or rotating the credential. An allocation is private compute ownership, separate from public Environment connection and native readiness; adapters qualify bootstrap completion, and Core never infers it from an engine or provider name.

With a configured provider, the Worker scans committed pending hosted Environments that have no allocation, which covers idle Session creation and recovery after an interruption between commit and bootstrap; an existing allocation never re-enters that path. The scan is bounded and serialized by the lifecycle owner and needs no caller action. An initial reservation without a Turn leaves its Session idle, and a daemon connection is never treated as native readiness. The same scan publishes authenticated connection observations with durable generations, after verifying the exact Session and device binding and a settled bootstrap.
Expand Down
6 changes: 5 additions & 1 deletion docs/zh/getting-started/operations.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "管理你的安装"
source: docs/getting-started/operations.md
source_hash: 5ac3e57f943b8bb3fadbf9cda0a72399317ad101416ec28ca4037eea07703a82
source_hash: 2f458a8b7689fb5b6b343ceeb4efe0f3aef8627ee820239c9134644db83106e1
---

安装运维人员负责 Core 主机、存储和可用性。节点主机运行各自的服务;参阅[节点](nodes.md)。设置见[配置参考](../configuration.md)。
Expand Down Expand Up @@ -199,3 +199,7 @@ cd && rm -rf ~/.oac/core
Web 使用 Core 密钥让管理员登录,检查每个请求来源,并用保留在服务器上的 Core 密钥将已登录的 `/core/v1` 请求转发到 Core。它把 `/v1` 和 `/api/v1` 原样转发给 Core,使用调用方自己的凭据;Web 仅在 `/node-install/` 提供不含密钥的节点文件,没有 Docker 或 KVM 访问权限。`/api/v1` 机器路由使用独立注册和连接凭据。没有服务持有 Docker 套接字。

沙箱是隔离边界([Runtime 与外层隔离](../concepts.md#runtime-and-outer-isolation))。Docker 沙箱共享节点内核,Docker 节点在主机上[等同于 root 权限](nodes.md#what-the-installer-sets-up);microsandbox 为每个沙箱提供具有显式[网络策略](nodes.md#what-the-installer-sets-up)的 microVM。Core 自身无 Docker 套接字或 KVM 访问权限。

## Runtime 可用性探测 {#runtime-availability-probes}

Codex CLI 可用性探测为 `process_spawn` 和 `process_wait` 输出 `runtime version probe` 记录,只包含耗时和结果。等待区间包含可执行文件加载和版本命令,两者都不是模型执行。
5 changes: 4 additions & 1 deletion docs/zh/runtime-bootstrap.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Runtime 引导"
source: docs/runtime-bootstrap.md
source_hash: ba7de54c4533b7aa4dc66e3e04aa3687d964faef7c2158740b930e4758567fc1
source_hash: 4cf78e3ff92f4e6e04d66373494f085ddc54aabb3ac234121c4209520909d8e8
---

Sandbox Provider 通过交付一个引导文件来启动托管 Runtime。本文负责 Provider 到 Runtime 的启动输入。类型与验证器位于 [`internal/runtimebootstrap`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/runtimebootstrap/bootstrap.go);Go provider 使用 [`runtime_bootstrap.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/runtime_bootstrap.go) 中的 `sandbox.Bootstrap.RuntimeConnection()` 构造输入,SDK helper 原样转发序列化对象。provider 不读取或写入 Runtime 的私有认证存储。
Expand All @@ -19,6 +19,7 @@ oac-daemon connect --bootstrap-file /home/runtime/runtime-bootstrap.json
| `version` | 精确的引导版本 `runtimebootstrap.Version` |
| `core_url` | 以 `/api/v1` 结尾的 HTTP(S) 机器 API 基址,不含凭据、查询或片段 |
| `device_id` | Core 签发的 daemon 身份的规范非零 UUID |
| `harness` | 会话固定选择的 Harness 标识;托管 Runtime 只探测并注册此 Harness |
| `credential` | Core 签发的非空 daemon 凭据,不含空白或 NUL |

解码器拒绝未知、重复、缺失和大小写别名字段,拒绝其他版本及超过 `runtimebootstrap.MaxBytes`(16 KiB)的文档。错误不包含提交的值。文件缺失或格式错误时,daemon 在连接前失败。
Expand All @@ -29,6 +30,8 @@ oac-daemon connect --bootstrap-file /home/runtime/runtime-bootstrap.json

provider 创建账户、挂载和工作区,交付该文件,设置 Runtime 的资源与 Environment 绑定配置,然后以无特权 Runtime 账户启动 daemon。Docker 将文件写入 Runtime 拥有的 home volume;microsandbox 和 E2B 在启动同一命令之前交付文件。

Core 从分配所属会话派生 `harness`。组合镜像可以包含多个 Harness,但托管 Runtime 只探测所选 Harness;未知、缺失或不可用的选择会失败,不探测其他 Harness。启动协议版本为 2,Core、提供商辅助程序和新启动的 Runtime 镜像需配套升级;已有分配保留原启动文件和 Runtime。自托管安装仍探测已安装的 Harness 集合。

Runtime 验证输入,并负责认证与连接。启动成功仅证明交付完成:经过认证的连接、已准备的能力和执行就绪是 [Core–Runtime 协议](runtime-protocol.md) 下的独立观测;[Sandbox Provider 指南](sandbox-provider.md#four-distinct-readiness-facts) 列出各自证明的事实。

自托管 executor 和运维人员供应的设备通过其他方式获取 daemon 身份;[机器连接 API](../../contracts/agents-api/zh/machine-api.md#credentials) 列出所有凭据来源。它们都进入同一 Runtime 执行循环。
Expand Down
4 changes: 3 additions & 1 deletion docs/zh/sandbox-provider.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "添加 Sandbox Provider"
source: docs/sandbox-provider.md
source_hash: 36ed532c778ec8c1c4c596a011a37613ed2aa0e6ffdf20854ea30ef9a8e0c953
source_hash: 72c540033b6529f64ec0dd03a7969eeec3723d64fa74723589eaaf2d806bec82
---

**Sandbox Provider** 为 Core 管理的 Environment 提供 Runtime daemon 运行所需的外层计算资源,以及启动 daemon 的有界引导流程。本指南说明如何添加 Provider,并作为 Core 驱动 Provider 的参考。接口为 [`SandboxProvider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/sandbox_provider.go)。
Expand Down Expand Up @@ -162,6 +162,8 @@ Node readiness 绑定到精确 generation、当前连接和 owner epoch。持久

### Allocation 生命周期 {#allocation-lifecycle}

Core 在 `Bootstrap.Harness` 中传递所属会话固定选择的 Harness;所有提供商都将其投影到 [Runtime 启动输入](./runtime-bootstrap.md),不自行选择实现。

allocation、专用 daemon credential digest 和精确 Session binding 在 `Create` 前、execution lease 与 Session lock 下原子提交。只有新 allocation receipt 允许 `Create`;重试和 Core 重启观察同一 reference,不重放或轮换凭据。allocation 是私有计算资源所有权,与公开 Environment connection 和原生 readiness 独立;adapter 验证 bootstrap completion,Core 不从 engine 或 provider name 推断。

配置 provider 后,Worker 扫描已提交且没有 allocation 的 pending hosted Environment,涵盖空闲 Session 创建以及 commit 与 bootstrap 之间中断后的恢复;已有 allocation 不重新进入此路径。scan 有界,由 lifecycle owner 串行化,不需要调用方操作。没有 Turn 的初始预约让 Session 保持空闲,daemon 连接不被当作原生 readiness。同一 scan 在验证精确 Session、device binding 和已结算 bootstrap 后,发布带持久 generation 的认证连接观测。
Expand Down
Loading
Loading