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
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 @@ -123,3 +123,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 6; 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: 187637e03b594296f75883bf67949c430f1b3978f7a059677979243d46177fca
source_hash: 31b83635cc8052ac32e0743f14e25724f1f64d9b77e6df765543b87b72e9bf2a
---

沙箱节点在其主机上运行 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 @@ -125,3 +125,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 和节点使用协议版本 6;发布此 Core 版本前需升级配套节点。
17 changes: 17 additions & 0 deletions deploy/kubernetes/BETA_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,20 @@ Validation passed focused gateway/execution/daemon regressions and race checks,
## 2026-10-06 — Sandbox template change audit

The administrative deployment update records authenticated administrator provenance before the execution owner changes the selection. This allows the template selection and its audit entry to commit together. The change adds no schema migration, SQL, Runtime wire or native dependency changes. The combined Runtime template built at `298957f7e6a2a707e2b01b75523764b4e1ff5ab5` remains compatible with this Core API correction.

## 2026-10-06 — Session-selected Runtime Harness

| Item | Value |
| --- | --- |
| Fork beta baseline | `f6125c99e712ba5156956025a0c68cfd284a9db5` |
| Feature branch | `codex/selected-runtime-harness` |
| Feature commit | `ee047f4dc946ab3ad6974e7ba29437c519641d05` |
| Schema migrations / DDL / SQL changes | None |
| Runtime bootstrap | Version 2, with the owning Session’s immutable `harness` |
| Provider helper protocols | E2B and microsandbox version 3 |
| Node wire protocol | Version 6 |
| Core–Runtime wire / native dependency pins | Unchanged |

A combined image retains all packaged Harnesses, while each managed Runtime discovers and registers only the Session-selected Harness. Unknown or unavailable selections fail without substituting another implementation. Self-hosted installations retain their installed Harness set. Codex version probes still validate the executable and expose secret-safe process spawn/wait timing.

Publish matching Core, provider helpers and a newly built Runtime template together. Node deployments require matching protocol-version-6 nodes. Existing allocations retain their bootstrap and Runtime; qualification must use a fresh allocation. [Runtime bootstrap](../../docs/runtime-bootstrap.md) owns the startup contract.
2 changes: 2 additions & 0 deletions docs/getting-started/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,5 @@ Use the `environment input reserved` log to connect the submitting HTTP trace to
`control_ready_ms` starts after scheduling, Runtime readiness and configuration assembly. `start_control_ms` describes the control acknowledgement. `input_to_first_text_ms` starts immediately before execution delivery, includes start-control time and is not model-only TTFT. The admission, control and first-text intervals overlap; do not sum them as independent durations. A successful provider call describes that operation, not completed daemon connection or a completed Turn. Model request start, response headers, first model text and retries remain unavailable unless the selected native adapter provides those observations. Logs contain correlation IDs and finite status categories, not input content, credentials or raw provider errors.

Daemon startup logs record `runtime process starting` with its build version and `runtime startup stage` for each Harness discovery, bootstrap and transport dial attempt. Durations use a local monotonic clock and omit credentials and raw errors. Discovery runs before transport registration. Compare daemon timestamps with Core registration and capability observations to separate startup from periodic observation and scheduling. These daemon records require a Runtime built with this instrumentation; updating Core alone does not update an existing Runtime template. A managed startup receipt confirms process spawn, not connection or capability readiness.

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
4 changes: 3 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: ee5e9211cc09dbf5a4bfba412c2c97af0aefcb7f9d2d5ffd6c0666729fbe2809
source_hash: 06de77447d1647b7ab546bd41bc2c4c042abdb47053d20b0a4bdf25465bca2f7
---

安装运维人员负责 Core 主机、存储和可用性。节点主机运行各自的服务;参阅[节点](nodes.md)。设置见[配置参考](../configuration.md)。
Expand Down Expand Up @@ -208,3 +208,5 @@ Web 使用 Core 密钥让管理员登录,检查每个请求来源,并用保
`runtime transport registered` 记录认证后的 WebSocket 注册,`runtime capability snapshot observed` 记录合法能力声明变化及设备 ID、能力数量。连接注册本身不代表执行就绪。包含可用 Harness 的能力变化会发出合并后的调度提示;Worker 仍检查所有权、容量、生命周期和能力要求,轮询负责兜底。

Daemon 启动日志记录 `runtime process starting` 和构建版本,`runtime startup stage` 记录各 Harness 探测、bootstrap 和每次连接尝试的本地单调时钟耗时,不输出凭据或原始错误。探测发生在连接注册之前。结合这些边界与已观察到的持久化连接时间,区分启动、周期观察和调度等待。Daemon 观测要求 Runtime 使用带有这些埋点的构建;仅升级 Core 不会升级现有 Runtime 模板。托管启动回执只确认进程已启动,不确认连接或能力就绪。

Codex CLI 可用性探测为 `process_spawn` 和 `process_wait` 输出 `runtime version probe` 记录,只包含耗时和结果。等待区间包含可执行文件加载和版本命令,两者都不是模型执行。
Loading