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: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ The initialization service generates secrets and the installation ID once, then
| `secrets/core/` | Credential encryption key, installation ID and Core key digest | Core |
| `secrets/web/` | Generated Core sign-in key | Web |
| `secrets/agent-host/` | `identity.json`, the [agent host's identity](#agent-host-container) | Core and the agent host |
| `state/` | Private Provider state, mounted in Core at `/state`. Each adapter owns a subdirectory; E2B uses `e2b/`, with no group or other access | Core |
| `state/` | Private Provider state, mounted in Core at `/state`. Each adapter creates and owns its subdirectory; E2B uses `e2b/`, with no group or other access | Core |
| `agent-host/` | The [agent host's state directory](#agent-host-container) | The agent host; initialization checks whether it is empty |
| `node-payload/` | Verified node installation metadata, mounted read-only at `/opt/oac/node-payload` | Core |

Expand Down
4 changes: 2 additions & 2 deletions docs/zh/configuration.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "配置参考"
source: docs/configuration.md
source_hash: 93f76a4b608e08bb4c4a9f215b2c9802015d9c0782c19403e1f31ba0ea5e3633
source_hash: 50bb9c230b1f4a8b0531932cc0ba6bb41fdd465ab5594d6164e78d2bd3481995
---

Core 安装的每项设置都恰好只有一个归属位置,分属以下三类:
Expand Down Expand Up @@ -116,7 +116,7 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置
| `secrets/core/` | 凭据加密密钥、安装 ID 和 Core 密钥摘要 | Core |
| `secrets/web/` | 生成的 Core 登录密钥 | Web |
| `secrets/agent-host/` | `identity.json`,即 [agent host 的身份](#agent-host-container) | Core 和 agent host |
| `state/` | 私有 Provider 状态,在 Core 中挂载到 `/state`。每个适配器拥有一个子目录;E2B 使用 `e2b/`,不允许组或其他用户访问 | Core |
| `state/` | 私有 Provider 状态,在 Core 中挂载到 `/state`。每个适配器创建并拥有自己的子目录;E2B 使用 `e2b/`,不允许组或其他用户访问 | Core |
| `agent-host/` | [agent host 的状态目录](#agent-host-container) | agent host;初始化时检查它是否为空 |
| `node-payload/` | 已验证的节点安装元数据,只读挂载于 `/opt/oac/node-payload` | Core |

Expand Down
11 changes: 7 additions & 4 deletions services/core/internal/execution/managed_setup_preflight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ else:

func TestE2BCandidateAdoptsTemplateBuildForOmittedResources(t *testing.T) {
state := filepath.Join(t.TempDir(), "e2b")
if err := os.MkdirAll(state, 0700); err != nil {
t.Fatal(err)
}
helper := filepath.Join(t.TempDir(), "provider")
requests := filepath.Join(state, "requests")
script := "#!/bin/sh\ncat >>" + requests + "\necho >>" + requests + "\nprintf '%s' '{\"Version\":1,\"ErrorCode\":\"\",\"DeploymentValid\":true,\"TemplateBuild\":{\"Status\":\"ready\",\"CPUs\":4,\"MemoryMiB\":4096,\"RootDiskMiB\":24063}}'\n"
Expand All @@ -109,8 +106,14 @@ func TestE2BCandidateAdoptsTemplateBuildForOmittedResources(t *testing.T) {
s := &runtimeManager{processPaths: paths, providers: providers.Builtin(), setupInstallationID: id, setups: &fakeDeploymentSetups{t: t}}
selection := deployment.Setup{InstallationID: id, Provider: "e2b", Mode: "direct", UsesCredential: true, Configuration: &e2b.DeploymentConfiguration{APIKey: "synthetic-private-key", Template: "runtime:" + uuid.NewString()}}
candidate, err := s.prepareDeployment(t.Context(), selection)
if err != nil {
t.Fatalf("fresh installation cannot prepare E2B: %v", err)
}
if info, err := os.Lstat(state); err != nil || !info.IsDir() || info.Mode().Perm() != 0700 {
t.Fatalf("adapter did not create private state: %v %v", info, err)
}
disk := int32(24063)
if err != nil || candidate.Selection.Configuration.(*e2b.DeploymentConfiguration).TemplateBuild == nil || candidate.Selection.Configuration.(*e2b.DeploymentConfiguration).TemplateBuild.CPUs != 4 || candidate.Selection.Configuration.(*e2b.DeploymentConfiguration).TemplateBuild.MemoryMiB != 4096 ||
if candidate.Selection.Configuration.(*e2b.DeploymentConfiguration).TemplateBuild == nil || candidate.Selection.Configuration.(*e2b.DeploymentConfiguration).TemplateBuild.CPUs != 4 || candidate.Selection.Configuration.(*e2b.DeploymentConfiguration).TemplateBuild.MemoryMiB != 4096 ||
*candidate.Selection.Configuration.(*e2b.DeploymentConfiguration).TemplateBuild.RootDiskMiB != disk || candidate.Selection.Configuration.(*e2b.DeploymentConfiguration).TemplateBuild.Status != "ready" {
t.Fatalf("validated build was not recorded: %+v %v", candidate.Selection.Configuration.(*e2b.DeploymentConfiguration).TemplateBuild, err)
}
Expand Down
76 changes: 76 additions & 0 deletions services/core/internal/sandbox/e2b/installed_paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"errors"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox"
"github.com/google/uuid"
)

func TestInstalledPathsRejectMissingAndRelativeRoots(t *testing.T) {
Expand All @@ -22,6 +24,80 @@ func TestInstalledPathsRejectMissingAndRelativeRoots(t *testing.T) {
}
}

func TestDirectProviderPreparesOnlyItsPrivateStateDirectory(t *testing.T) {
artifacts := t.TempDir()
binary, _, err := InstalledPaths(sandbox.ProcessPaths{ArtifactRoot: artifacts, StateRoot: t.TempDir()})
if err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Dir(binary), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(binary, []byte("unused helper"), 0700); err != nil {
t.Fatal(err)
}
for _, shape := range []string{"fresh", "receipt", "file", "symlink", "public", "missing_parent"} {
t.Run(shape, func(t *testing.T) {
root := t.TempDir()
state := filepath.Join(root, "e2b")
var err error
switch shape {
case "receipt":
err = os.Mkdir(state, 0700)
if err == nil {
err = os.WriteFile(filepath.Join(state, "receipt.json"), []byte("retained receipt"), 0600)
}
case "file":
err = os.WriteFile(state, []byte("retained file"), 0600)
case "symlink":
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires Windows privileges")
}
err = os.Symlink(t.TempDir(), state)
case "public":
err = os.Mkdir(state, 0700)
if err == nil {
err = os.Chmod(state, 0755)
}
case "missing_parent":
root = filepath.Join(root, "missing")
}
if err != nil {
t.Fatal(err)
}
before, _ := os.Lstat(state)
_, err = BuildDirect(sandbox.DirectConfig{
ProcessPaths: sandbox.ProcessPaths{ArtifactRoot: artifacts, StateRoot: root},
InstallationID: uuid.NewString(),
Selection: sandbox.Selection{Provider: "e2b", Configuration: &DeploymentConfiguration{
APIKey: "synthetic-key", Template: "runtime:" + uuid.NewString(),
}},
})
if (err == nil) != (shape == "fresh" || shape == "receipt") {
t.Fatalf("unexpected construction result: %v", err)
}
after, statErr := os.Lstat(state)
if shape == "fresh" && (statErr != nil || !after.IsDir() || after.Mode().Perm() != 0700) {
t.Fatalf("fresh state is not private: %v %v", after, statErr)
}
if before != nil && (statErr != nil || !os.SameFile(before, after) || before.Mode() != after.Mode()) {
t.Fatal("existing state was replaced or repaired")
}
if shape == "receipt" {
data, err := os.ReadFile(filepath.Join(state, "receipt.json"))
if err != nil || string(data) != "retained receipt" {
t.Fatal("existing receipt changed", err)
}
}
if shape == "missing_parent" {
if _, err := os.Lstat(root); !errors.Is(err, os.ErrNotExist) {
t.Fatal("adapter created the installation root", err)
}
}
})
}
}

func TestConfigurationDiscoveryUsesSuppliedProcessPaths(t *testing.T) {
paths := sandbox.ProcessPaths{ArtifactRoot: t.TempDir(), StateRoot: t.TempDir()}
binary, _, err := InstalledPaths(paths)
Expand Down
4 changes: 4 additions & 0 deletions services/core/internal/sandbox/e2b/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ func newDirect(c sandbox.DirectConfig) (*Provider, error) {
if err != nil {
return nil, err
}
// The installation owns the parent; this adapter owns its private subdirectory.
if err := os.Mkdir(state, 0700); err != nil && !errors.Is(err, os.ErrExist) {
return nil, fmt.Errorf("E2B provider cannot prepare its private state directory: %w", err)
}
// Only a candidate that omitted its resources has none; its validation
// reads them from the template build before the candidate is rebuilt.
var resources *sandbox.Resources
Expand Down
2 changes: 1 addition & 1 deletion services/core/tools/e2b-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The boundary has version 1. The request and credentials arrive on standard input

## Receipts and state directory

`OAC_E2B_STATE_DIR` ([configuration](../../../../docs/configuration.md#appendix-core-environment-without-the-installer)) must already exist, be owned by the helper's user and grant no group or other access. Keep it on durable private storage across Core upgrades and restarts. Its receipts hold SDK connection material, ownership identities and one-shot creation claims; they are not Core execution state. Losing the directory cannot authorize recreation or successful cleanup; never delete receipts after an uncertain call.
The Go adapter creates its private subdirectory under the installation's [Provider state root](../../../../docs/configuration.md#compose-installations) when constructing the Provider. An existing path must be a directory owned by the helper's user with no group or other access; it is never replaced or repaired. Keep it on durable private storage across Core restarts. Its receipts hold SDK connection material, ownership identities and one-shot creation claims; they are not Core execution state. Creating an empty directory restores no receipt: missing receipts still cannot authorize recreation, credential replacement or successful cleanup. Never delete receipts after an uncertain call.

A helper holds its allocation's lock until the SDK operation returns, even after Core's caller times out. Core tracks the helper's actual exit, and a credential change waits for running helpers without killing them. Helper exit never proves that a remote Create settled. Core serializes lifecycle requests per allocation and never replays Create.

Expand Down