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: 4 additions & 0 deletions apps/daemon/internal/localworkspace/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ func (b *Binding) loadCapabilitySnapshot(root *os.Root, input agentcapabilities.
return agentcapabilities.Manifest{}, agentcapabilities.ErrInvalid
}
manifest, err := agentcapabilities.Load(root)
if err == nil && !completed && manifest.Identity != identity {
// A preparation restored from an identical earlier Session carries that Session's identity.
err = agentcapabilities.Rebind(root, &manifest, input, identity)
}
if err != nil || agentcapabilities.ValidateSelection(manifest, input, identity) != nil {
return agentcapabilities.Manifest{}, agentcapabilities.ErrInvalid
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,20 @@ func TestPreparationFreezesLocalContentsAcrossReconnect(t *testing.T) {
if err != nil || string(fresh[len(fresh)-6:]) != "second" {
t.Fatal("new Session did not capture new source", err)
}
// Reusing a snapshot under another identity or selection cannot start native work.
reconnect.stateKey = uuid.NewString()
changed := configured
changed.AgentStateKey = reconnect.stateKey
if _, err = reconnect.Prepare(t.Context(), changed); err == nil {
t.Fatal("foreign snapshot accepted")
}
changed = configured
// Reusing a snapshot under another selection cannot start native work, even
// for another Session.
local := *configured.LocalEnvironment
local.CapabilitySources = &agentcapabilities.Input{}
changed := configured
changed.LocalEnvironment = &local
if _, err = b.Prepare(t.Context(), changed); err == nil {
t.Fatal("changed selection accepted")
}
reconnect.stateKey = uuid.NewString()
changed.AgentStateKey = reconnect.stateKey
if _, err = reconnect.Prepare(t.Context(), changed); err == nil {
t.Fatal("foreign snapshot accepted")
}
}

func TestPreparationUsesOperatorSourcesAndLeavesFailuresInert(t *testing.T) {
Expand Down
37 changes: 37 additions & 0 deletions apps/daemon/internal/localworkspace/capability_rebind_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package localworkspace

import (
"testing"

"github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities"
"github.com/google/uuid"
)

func TestFinalizeRebindsIdenticalRestoredInstallation(t *testing.T) {
first, _ := testBinding(t)
source := t.TempDir()
writeSourceSkill(t, source, "restored")
input := agentcapabilities.Input{Directories: []string{source}}
root, unlock, err := first.openCapabilities(t.Context(), false)
if err != nil {
t.Fatal(err)
}
defer unlock()
if _, err = first.loadCapabilitySnapshot(root, input, first.capabilityIdentity(), false); err != nil {
t.Fatal(err)
}
// The next Session's directory holds the files restored from the first.
next, err := NewWithCapabilityDirectory(uuid.NewString(), uuid.NewString(), first.workspace, first.capabilityRoot)
if err != nil {
t.Fatal(err)
}
if _, err = next.loadCapabilitySnapshot(root, agentcapabilities.Input{}, next.capabilityIdentity(), false); err == nil {
t.Fatal("different selection accepted")
}
if _, err = next.loadCapabilitySnapshot(root, input, next.capabilityIdentity(), false); err != nil {
t.Fatal(err)
}
if manifest, err := agentcapabilities.Load(root); err != nil || manifest.Identity != next.capabilityIdentity() {
t.Fatal("identity not rebound", err)
}
}
4 changes: 4 additions & 0 deletions contracts/agents-api/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ The Session applies the Template first, then its own fields. Composition happens

For comparison, an inline `openai_hosted` Session without a Template treats omitted and null `network` as enabled.

### Preparation reuse

Core may start a hosted Environment from the result of an earlier preparation in the same Project whose effective configuration, after Template composition, is identical, for up to 24 hours after that preparation. Each Session still gets its own copy. Setup commands and package installation then do not run again for this Session. Only `/environment` is reused (see the workspace filesystem scope); HOME and other compute-local changes are not. Setup must not create per-Session unique values, perform external side effects that must happen per Session, or fetch credentials that expire within 24 hours; configurations that need this must differ, for example in `env` or `files`. A Provider that does not declare the [preparation cache](../../docs/sandbox-provider.md#explicit-operation-contracts) always prepares in full.

### Restricted network

`restricted` requires 1–100 exact ASCII hostnames in `allowed_domains`; subdomains and redirect targets need their own entries. Wildcards, URL or port syntax, IP literals, Unicode and trailing dots are rejected. Reads return the supplied spelling, order and duplicates; comparison uses a separate lowercase, deduplicated copy.
Expand Down
4 changes: 3 additions & 1 deletion contracts/agents-api/node-generation-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ Each operation carries its own arguments and returns the following result on suc
| `suspend` | `Suspend` | `suspend` | `state` |
| `resume` | `Resume` | `resume` | `state` |
| `delete_snapshot` | `DeleteSnapshot` | `snapshot` | None |
| `restore_preparation` | `RestorePreparation` | `key` | `restored` |
| `save_preparation` | `SavePreparation` | `key` | None |

A request whose `connection_id`, `owner_epoch` or `sequence` does not match closes the connection. A malformed request gets an `invalid` response. The node runs the request on the provider of the named `deployment_generation` and answers `unconfirmed` when it cannot. Core sends `create` and a `resume` that is not observe-only only to a generation that is ready on that node, and keeps at most 32 requests pending per connection.

The budget is relative: the node anchors `timeout_ms` to its own clock on receipt and consumes it while the request waits in its queue, so the hosts' clocks need not agree. Core still bounds its own wait. A full node queue closes the connection.

The `response` frame carries `id` and `connection_id`. A successful response carries the result named in the operation table, with no result field for `kill`, `kill_compute` or `delete_snapshot`. A failed response carries an `error_code`:
The `response` frame carries `id` and `connection_id`. A successful response carries the result named in the operation table, with no result field for `kill`, `kill_compute`, `delete_snapshot` or `save_preparation`. A failed response carries an `error_code`:

| `error_code` | Meaning |
| --- | --- |
Expand Down
6 changes: 5 additions & 1 deletion contracts/agents-api/zh/environments.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Environment 与模板"
source: contracts/agents-api/environments.md
source_hash: 3408e2c811042732d35227150533cf12a85cfcff8a5118d7eb62a5d68549cc8b
source_hash: f50217dd1fc465626ac7135a46b69cea0ee40fc14fd30141e23813018192cb5c
---

Environment 是 Session 的执行资源,包括 Harness 运行所在的机器、工作区以及已完成准备的能力。Session 通过其 `environment` 配置创建 Environment;不存在独立的 create 调用。Environment 模板是 Session 创建时解析的可复用准备配置。本契约涵盖这两类资源、两种放置方式、输入接纳、能力准备、Skills、Plugins 和 MCP 连接来源。
Expand Down Expand Up @@ -318,6 +318,10 @@ Session 会先应用模板,然后应用自身字段。组合仅在加密 Sessi

作为比较,不带模板的内联 `openai_hosted` Session 会将省略或为 null 的 `network` 视为启用。

### 准备复用 {#preparation-reuse}

Core 可以用同一 Project 中较早一次准备的结果启动托管 Environment,前提是两者在 Template 组合之后的有效配置完全相同,且距该次准备不超过 24 小时。每个 Session 仍然得到自己的副本。此时设置命令和包安装不会为该 Session 再次运行。只复用 `/environment`(见工作区文件系统范围),HOME 和其他与计算实例本地相关的改动不会复用。设置过程不得产生每个 Session 唯一的值,不得执行必须按 Session 发生的外部副作用,也不得获取 24 小时内就会过期的凭据;需要这些行为的配置必须有所不同,例如在 `env` 或 `files` 上。未声明[准备缓存](../../../docs/zh/sandbox-provider.md#explicit-operation-contracts)的提供商始终完整准备。

### 受限网络 {#restricted-network}

`restricted` 要求在 `allowed_domains` 中提供 1–100 个精确 ASCII 主机名;子域名和重定向目标需要各自的条目。通配符、URL 或端口语法、IP 字面量、Unicode 和尾随点都会被拒绝。读取操作会返回提供的拼写、顺序和重复项;比较操作使用单独的小写去重副本。
Expand Down
6 changes: 4 additions & 2 deletions 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: 21fb07f531d4087de59618e5f34211d5dc9674efe97530501170950e8a76f6f2
source_hash: a8135ed519bdc897237cef7f69c78dde638ab76877c67f096b1e5aef9f28b80b
---

沙箱节点协议连接 Core 与 Docker 或 microsandbox 主机:主机针对其本地提供商执行提供商操作,并报告就绪状态、主机测量值及其持有的部署代次。
Expand Down Expand Up @@ -87,12 +87,14 @@ Core 发送包含以下内容的 `request` 帧:
| `suspend` | `Suspend` | `suspend` | `state` |
| `resume` | `Resume` | `resume` | `state` |
| `delete_snapshot` | `DeleteSnapshot` | `snapshot` | 无 |
| `restore_preparation` | `RestorePreparation` | `key` | `restored` |
| `save_preparation` | `SavePreparation` | `key` | 无 |

只要 `connection_id`、`owner_epoch` 或 `sequence` 中任一值不匹配,请求就会关闭连接。格式错误的请求会得到 `invalid` 响应。节点在所指 `deployment_generation` 的提供商上运行请求,无法运行时回复 `unconfirmed`。Core 仅向节点上已就绪的代次发送 `create` 和非 observe-only 的 `resume`,并且每条连接最多保留 32 个待处理请求。

预算采用相对计时:节点收到请求时以自己的时钟为基准锚定 `timeout_ms`,并在请求排队等待期间持续消耗该预算,因此各主机的时钟无需保持一致。Core 仍会限制自身等待时长。节点队列已满时会关闭连接。

`response` 帧包含 `id` 和 `connection_id`。成功响应携带操作表中指定的结果;对于 `kill`、`kill_compute` 或 `delete_snapshot`,响应不含结果字段。失败响应携带一个 `error_code`:
`response` 帧包含 `id` 和 `connection_id`。成功响应携带操作表中指定的结果;对于 `kill`、`kill_compute`、`delete_snapshot` 或 `save_preparation`,响应不含结果字段。失败响应携带一个 `error_code`:

| `error_code` | 含义 |
| --- | --- |
Expand Down
3 changes: 3 additions & 0 deletions docs/sandbox-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,12 @@ Every provider returns a complete `ProviderOperations()` declaration with one en
| `Create`, `GetInfo`, `Renew`, `Kill`, `RunCommand` | Supported | Allocation lifecycle and bounded commands |
| `Observe` | Explicit decision | Ownership-checked read-only observation of one allocation |
| `Initial`, `NewCompute`, `GetCompute`, `Suspend`, `Resume`, `ResumeCompute`, `KillCompute`, `DeleteSnapshot`, `RunCommandCompute` | The same decision for every checkpoint method; supported only by a `nodes` registration | Exact compute incarnations, capture and restore, retained-source resume and cleanup |
| `RestorePreparation`, `SavePreparation` | The same decision for both | Reuse of a finished Environment preparation |

`Initial` and `NewCompute` construct compute references without allocating, `ResumeCompute` thaws only the same resident instance after an aborted pause, and `RunCommandCompute` runs a bounded command in one exact compute incarnation. Core uses `RunCommandCompute` to wake a parked daemon after a restore ([`runtime_compute_wake.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/execution/runtime_compute_wake.go)).

`SavePreparation(reference, key)` stores what preparation wrote under `/environment` (workspace, packages and initialization, never Runtime state) in the allocation's initial compute after a complete preparation, and `RestorePreparation(reference, key)` puts a stored one back and returns `false` when it holds no valid entry. The `key` is opaque: Core derives it from the Project and everything that reaches preparation, never from the Environment, Session, Harness or Agent, and the Provider never interprets it. The Provider owns storage, scoping to its Runtime image, expiry and eviction. An entry is valid for 24 hours after its save, and a restore must not use an expired one. Core calls `RestorePreparation` after the Runtime connects and, on a hit, sends only the `finalize` operation before completing initialization. On a miss it prepares in full and calls `SavePreparation`; a save error is logged and never fails the Session. A Provider that declares neither always prepares in full. The microsandbox adapter keeps one archive per key in a private directory under the Runtime home.

Each declaration entry is `state: supported` with no reason, or `state: unsupported` with an authored reason code. Missing, zero, unknown or unsafe entries fail validation. Adding a method to `SandboxProvider` requires an explicit decision and implementation in every adapter; never supply a base type or generate blanket unsupported implementations.

An unsupported method returns `providercontract.UnsupportedError` before any native I/O. The error names the exact operation and a safe code, never a native message, resource identity, endpoint or credential. An empty result, a nil error, `Unavailable` or an unknown mutation outcome never stands in for unsupported, and the five required methods can never return it.
Expand Down
5 changes: 4 additions & 1 deletion docs/zh/sandbox-provider.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "沙箱提供商协议"
source: docs/sandbox-provider.md
source_hash: 19dcc4268e7d7ed547c1de470ba1bf0f771a35e909a4f869daaeefeddffeea7c
source_hash: 95b02d2c1b46117e603ebadd5fa948c92624c3eb59d50cc402eb5a3907e69378
---

沙箱提供商协议连接 Core 与为 Core 管理的(`openai_hosted`)Environment 提供计算资源的 adapter,并由该 adapter 运行启动 Runtime daemon 的有界引导流程。本页面向 adapter 作者:说明提供商必须实现什么,以及可以依赖 Core 做什么。Docker、microsandbox 和 E2B 实现了该协议。
Expand Down Expand Up @@ -65,9 +65,12 @@ Docker 等没有原生可续期租约的 backend 仍遵守 Core 的 hosted expir
| `Create`、`GetInfo`、`Renew`、`Kill`、`RunCommand` | 支持 | Allocation 生命周期与有界命令 |
| `Observe` | 明确决定 | 检查所有权、只读地观测一个 allocation |
| `Initial`、`NewCompute`、`GetCompute`、`Suspend`、`Resume`、`ResumeCompute`、`KillCompute`、`DeleteSnapshot`、`RunCommandCompute` | 所有 checkpoint 方法决定一致;仅 `nodes` 注册可以支持 | 精确计算实例、捕获与恢复、保留源恢复和清理 |
| `RestorePreparation`、`SavePreparation` | 两者决定一致 | 复用已完成的 Environment 准备结果 |

`Initial` 和 `NewCompute` 构造 compute reference,不分配资源;`ResumeCompute` 在暂停中止后仅解冻同一驻留实例;`RunCommandCompute` 在一个精确 compute incarnation 中运行有界命令。Core 使用 `RunCommandCompute` 在恢复后唤醒 parked daemon([`runtime_compute_wake.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/execution/runtime_compute_wake.go))。

`SavePreparation(reference, key)` 在准备完整完成后保存该分配初始 compute 中准备写入 `/environment` 的内容(workspace、packages 和 initialization,从不包括 Runtime 状态),`RestorePreparation(reference, key)` 把已保存的内容放回,没有有效条目时返回 `false`。`key` 是不透明的:Core 由 Project 和所有进入准备的内容推导它,从不使用 Environment、Session、Harness 或 Agent,提供商也从不解释它。存储、按 Runtime 镜像划分范围、过期和淘汰由提供商负责。条目在保存后 24 小时内有效,恢复不得使用已过期的条目。Core 在 Runtime 连接后调用 `RestorePreparation`,命中时只发送 `finalize` 操作即完成初始化。未命中时完整准备并调用 `SavePreparation`;保存错误只记录日志,不会使 Session 失败。两者都未声明的提供商始终完整准备。microsandbox 适配器在 Runtime home 下的私有目录中为每个 key 保存一个归档。

每个声明项为不带 reason 的 `state: supported`,或带 authored reason code 的 `state: unsupported`。缺失、零值、未知或不安全项都会验证失败。给 `SandboxProvider` 添加方法时,必须在每个 adapter 中明确决定并实现;不提供 base type,也不生成笼统的不支持实现。

不支持的方法在任何原生 I/O 前返回 `providercontract.UnsupportedError`。错误指明精确操作和安全 code,不包含原生消息、资源身份、endpoint 或凭据。空结果、nil error、`Unavailable` 或未知 mutation 结果都不能代替 unsupported,五项必需方法不能返回 unsupported。
Expand Down
15 changes: 15 additions & 0 deletions internal/agentcapabilities/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ func Finalize(installed *os.Root, input Input, identity Identity, resolve Direct
return ErrInvalid
}
}
return writeManifest(installed, manifest)
}

// Rebind gives an installation restored from an earlier preparation the identity
// of this Session. The earlier preparation must have had the identical selection.
func Rebind(installed *os.Root, manifest *Manifest, input Input, identity Identity) error {
digest, err := selectionHash(input)
if err != nil || validateIdentity(identity) != nil || manifest.Version != 1 || manifest.SelectionSHA256 != digest {
return ErrInvalid
}
manifest.Identity = identity
return writeManifest(installed, *manifest)
}

func writeManifest(installed *os.Root, manifest Manifest) error {
body, err := json.Marshal(manifest)
if err != nil || len(body) > 256<<10 || writeFile(installed, ManifestName+".tmp", body, 0400) != nil || installed.Rename(ManifestName+".tmp", ManifestName) != nil {
return ErrInvalid
Expand Down
22 changes: 22 additions & 0 deletions services/core/cmd/server/managed_generation_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,25 @@ func (p *generationRouter) ResumeCompute(ctx context.Context, r sandbox.Referenc
defer done()
return v.ResumeCompute(ctx, r, c)
}
func (p *generationRouter) RestorePreparation(ctx context.Context, r sandbox.Reference, key string) (bool, error) {
if err := providercontract.Require(p, "RestorePreparation"); err != nil {
return false, err
}
v, done, err := p.route(ctx, r)
if err != nil {
return false, err
}
defer done()
return v.RestorePreparation(ctx, r, key)
}
func (p *generationRouter) SavePreparation(ctx context.Context, r sandbox.Reference, key string) error {
if err := providercontract.Require(p, "SavePreparation"); err != nil {
return err
}
v, done, err := p.route(ctx, r)
if err != nil {
return err
}
defer done()
return v.SavePreparation(ctx, r, key)
}
Loading
Loading