From 409c56d9c5d6b804e2784de65a79f2fe3336ecc6 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Thu, 8 Oct 2026 17:27:05 +0000 Subject: [PATCH] Initialize the E2B state directory --- docs/configuration.md | 2 +- docs/zh/configuration.md | 4 +- .../execution/managed_setup_preflight_test.go | 11 ++- .../sandbox/e2b/installed_paths_test.go | 76 +++++++++++++++++++ .../core/internal/sandbox/e2b/provider.go | 4 + services/core/tools/e2b-provider/README.md | 2 +- 6 files changed, 91 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c503b1563..494905097 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 | diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 3fd22bfe3..2faa0ead2 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -1,7 +1,7 @@ --- title: "配置参考" source: docs/configuration.md -source_hash: 93f76a4b608e08bb4c4a9f215b2c9802015d9c0782c19403e1f31ba0ea5e3633 +source_hash: 50bb9c230b1f4a8b0531932cc0ba6bb41fdd465ab5594d6164e78d2bd3481995 --- Core 安装的每项设置都恰好只有一个归属位置,分属以下三类: @@ -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 | diff --git a/services/core/internal/execution/managed_setup_preflight_test.go b/services/core/internal/execution/managed_setup_preflight_test.go index 79dfefa9d..cf29c2b12 100644 --- a/services/core/internal/execution/managed_setup_preflight_test.go +++ b/services/core/internal/execution/managed_setup_preflight_test.go @@ -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" @@ -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) } diff --git a/services/core/internal/sandbox/e2b/installed_paths_test.go b/services/core/internal/sandbox/e2b/installed_paths_test.go index 5012e0d97..43b29042e 100644 --- a/services/core/internal/sandbox/e2b/installed_paths_test.go +++ b/services/core/internal/sandbox/e2b/installed_paths_test.go @@ -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) { @@ -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) diff --git a/services/core/internal/sandbox/e2b/provider.go b/services/core/internal/sandbox/e2b/provider.go index 1ac5231d7..2643c979e 100644 --- a/services/core/internal/sandbox/e2b/provider.go +++ b/services/core/internal/sandbox/e2b/provider.go @@ -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 diff --git a/services/core/tools/e2b-provider/README.md b/services/core/tools/e2b-provider/README.md index 7cbdc888a..09ee0c0f6 100644 --- a/services/core/tools/e2b-provider/README.md +++ b/services/core/tools/e2b-provider/README.md @@ -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.