From 6241cf53f07bf5a3ceb263cb838aa1e207dee93d Mon Sep 17 00:00:00 2001 From: BobSong <2434005232@qq.com> Date: Sun, 12 Jul 2026 12:29:38 +0800 Subject: [PATCH 1/3] fix(builder): honor workspace filesystem mode configuration --- .../agents/agentscope-builder/README.md | 16 +- .../agents/agentscope-builder/README_zh.md | 16 +- .../builder/web/audit/AgentActivityStore.java | 10 +- .../builder/web/config/BuilderConfig.java | 129 +++++++++++----- .../web/config/BuilderSandboxConfig.java | 141 ++++++++++++++++++ .../web/workspace/BuilderWorkspaceConfig.java | 10 +- .../src/main/resources/application.yml | 27 +++- .../BuilderConfigFilesystemModeTest.java | 107 +++++++++++++ 8 files changed, 397 insertions(+), 59 deletions(-) create mode 100644 agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderSandboxConfig.java create mode 100644 agentscope-examples/agents/agentscope-builder/src/test/java/io/agentscope/builder/web/config/BuilderConfigFilesystemModeTest.java diff --git a/agentscope-examples/agents/agentscope-builder/README.md b/agentscope-examples/agents/agentscope-builder/README.md index d9dc74fd77..8d33663247 100644 --- a/agentscope-examples/agents/agentscope-builder/README.md +++ b/agentscope-examples/agents/agentscope-builder/README.md @@ -134,9 +134,19 @@ builder: ## Filesystem Modes -Builder supports three filesystem modes that control how per-(user, agent) workspaces are backed. Set via `builder.workspace-store.fs-spec`. +Builder supports three explicit filesystem modes that control how per-(user, agent) workspaces are backed. Set via `builder.workspace-store.fs-spec`. -### Local Mode (default) +### Auto Mode (default) + +```yaml +builder: + workspace-store: + fs-spec: auto +``` + +`auto` preserves the deployment-aware behavior: `InMemoryAgentStateStore` and `JsonFileAgentStateStore` select local mode, while a distributed `AgentStateStore` selects remote mode. Set an explicit mode when you need to override this detection. + +### Local Mode ```yaml builder: @@ -256,7 +266,7 @@ Per-agent sandbox config (`sandbox.mode` / `sandbox.scope`) is metadata stored w | `BUILDER_MODEL_NAME` | `builder.dashscope.model-name` | `qwen-max` | Model name | | `BUILDER_WORKSPACE` | `builder.workspace` | (JVM cwd) | Working directory | | `BUILDER_JWT_SECRET` | `builder.jwt.secret` | (dev default) | JWT signing secret | -| `BUILDER_WORKSPACE_FS_SPEC` | `builder.workspace-store.fs-spec` | `local` | Filesystem mode | +| `BUILDER_WORKSPACE_FS_SPEC` | `builder.workspace-store.fs-spec` | `auto` | Filesystem mode (`auto`, `local`, `sandbox`, or `remote`) | | `BUILDER_SANDBOX_ENABLED` | `builder.sandbox.enabled` | `false` | Enable sandbox | | `BUILDER_SANDBOX_IMAGE` | `builder.sandbox.image` | `agentscope/python-sandbox:py311-slim` | Sandbox Docker image | | `BUILDER_SANDBOX_ISOLATION` | `builder.sandbox.isolation` | `USER` | Sandbox isolation scope | diff --git a/agentscope-examples/agents/agentscope-builder/README_zh.md b/agentscope-examples/agents/agentscope-builder/README_zh.md index d599a4a412..13838a2742 100644 --- a/agentscope-examples/agents/agentscope-builder/README_zh.md +++ b/agentscope-examples/agents/agentscope-builder/README_zh.md @@ -113,9 +113,19 @@ builder: ## 文件系统模式 -Builder 通过 `builder.workspace-store.fs-spec` 切换三种文件系统模式,控制 per-(用户, agent) workspace 的存储后端。 +Builder 通过 `builder.workspace-store.fs-spec` 切换三种显式文件系统模式,控制 per-(用户, agent) workspace 的存储后端。 -### 本地模式(默认) +### Auto 模式(默认) + +```yaml +builder: + workspace-store: + fs-spec: auto +``` + +`auto` 保留按部署拓扑自动选择的行为:使用 `InMemoryAgentStateStore` 或 `JsonFileAgentStateStore` 时选择本地模式;提供分布式 `AgentStateStore` 时选择 Remote 模式。需要覆盖自动判断时,可以显式指定其他模式。 + +### 本地模式 ```yaml builder: @@ -235,7 +245,7 @@ per-agent 的 sandbox 配置(`sandbox.mode` / `sandbox.scope`)当前只是 | `BUILDER_MODEL_NAME` | `builder.dashscope.model-name` | `qwen-max` | 模型名 | | `BUILDER_WORKSPACE` | `builder.workspace` | (JVM cwd) | 工作目录 | | `BUILDER_JWT_SECRET` | `builder.jwt.secret` | (开发默认) | JWT 签名密钥 | -| `BUILDER_WORKSPACE_FS_SPEC` | `builder.workspace-store.fs-spec` | `local` | 文件系统模式 | +| `BUILDER_WORKSPACE_FS_SPEC` | `builder.workspace-store.fs-spec` | `auto` | 文件系统模式(`auto` / `local` / `sandbox` / `remote`) | | `BUILDER_SANDBOX_ENABLED` | `builder.sandbox.enabled` | `false` | 是否启用 sandbox | | `BUILDER_SANDBOX_IMAGE` | `builder.sandbox.image` | `agentscope/python-sandbox:py311-slim` | Sandbox Docker 镜像 | | `BUILDER_SANDBOX_ISOLATION` | `builder.sandbox.isolation` | `USER` | Sandbox 隔离粒度 | diff --git a/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/audit/AgentActivityStore.java b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/audit/AgentActivityStore.java index bbedb1e94c..bfe88d1349 100644 --- a/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/audit/AgentActivityStore.java +++ b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/audit/AgentActivityStore.java @@ -48,11 +48,11 @@ * activity/activity-{timestampMs}.jsonl # rotated chunks (most recent first by name) * * - *
All paths live under the {@code activity/} prefix because the harness composite filesystem - * (see {@link io.agentscope.builder.web.config.BuilderConfig}) registers {@code activity/} as an - * additional shared prefix via {@code RemoteFilesystemSpec.addSharedPrefix("activity/")}. Writes - * therefore land on the shared {@link BaseStore} (visible across - * pods) rather than on per-pod local disk — critical for a multi-tenant deployment. + *
All paths live under the {@code activity/} prefix. In remote mode, the harness composite + * filesystem (see {@link io.agentscope.builder.web.config.BuilderConfig}) registers that path via + * {@code RemoteFilesystemSpec.addSharedPrefix("activity/")}, so writes land on the shared {@link + * BaseStore} and remain visible across pods. Local and sandbox modes keep the activity log in their + * selected workspace backend. * *
Writes go through the per-agent {@link HarnessAgent#workspaceFor(String, String)} view, which * runs in the owner's namespace ({@code IsolationScope.USER}) so the audit trail is keyed to the diff --git a/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderConfig.java b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderConfig.java index efa1056336..f7cefa6c9a 100644 --- a/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderConfig.java +++ b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderConfig.java @@ -29,6 +29,7 @@ import io.agentscope.harness.agent.filesystem.remote.store.BaseStore; import io.agentscope.harness.agent.filesystem.spec.LocalFilesystemSpec; import io.agentscope.harness.agent.filesystem.spec.RemoteFilesystemSpec; +import io.agentscope.harness.agent.filesystem.spec.SandboxFilesystemSpec; import io.agentscope.harness.agent.gateway.channel.ChannelConfig; import io.agentscope.harness.agent.gateway.channel.DmScope; import io.agentscope.harness.agent.gateway.channel.chatui.ChatUiChannel; @@ -60,15 +61,12 @@ * *
Builder is a multi-tenant distributed deployable, so every {@link - * io.agentscope.harness.agent.HarnessAgent} (the global main agent and every user-custom agent) - * runs with a {@link RemoteFilesystemSpec} composite filesystem: the per-pod - * {@code LocalFilesystem} only carries shared/read-only template content, while the {@code - * memory/}, {@code MEMORY.md}, {@code sessions/}, {@code tasks/}, {@code skills/} and {@code - * subagents/} routes are persisted through a {@link BaseStore}-backed {@code RemoteFilesystem} - * shared across pods. The {@link BaseStore} bean is therefore required — operators must - * provide one (e.g. SQLite/JDBC/Redis-backed); a missing bean fails context refresh fast with a - * clear message. + *
The {@code builder.workspace-store.fs-spec} property selects the filesystem used by every + * {@link io.agentscope.harness.agent.HarnessAgent}: {@code local}, {@code sandbox}, or {@code + * remote}. The default {@code auto} mode preserves the topology inferred from the configured + * {@link AgentStateStore}: local state stores use {@link LocalFilesystemSpec}, while distributed + * stores use a {@link RemoteFilesystemSpec} backed by {@link BaseStore}. Sandbox mode uses the + * optional {@link SandboxFilesystemSpec} contributed when {@code builder.sandbox.enabled=true}. * *
Every agent built by the bootstrap is wired with a {@link RemoteFilesystemSpec} (per-user - * isolation scope) backed by the shared {@link BaseStore}, so memory/sessions/tasks/skills/ - * subagents files persist across pods. + *
Every agent built by the bootstrap receives the filesystem selected by {@code
+ * builder.workspace-store.fs-spec}. The default {@code auto} mode uses a local filesystem for
+ * in-process state stores and a remote filesystem for distributed state stores.
*
* @param modelOpt the {@link Model} to use, or empty if none is configured
* @param toolEventBus the shared tool-event bus for real-time SSE streaming of tool calls
- * @param baseStore the shared {@link BaseStore} that backs every agent's {@code
- * RemoteFilesystem} routes; required — operators must provide a bean.
+ * @param baseStore the shared {@link BaseStore} used when remote filesystem mode is selected
+ * @param sessionOpt an optional state store; defaults to {@link InMemoryAgentStateStore}
+ * @param sandboxFilesystemSpecOpt the sandbox filesystem created when sandbox support is enabled
*/
@Bean
public BuilderBootstrap builderBootstrap(
Optional The bean is consumed by {@link BuilderConfig#builderBootstrap} which applies it to every
+ * agent via {@code configureAllAgents}.
+ *
+ * Filesystem composition for each agent now lives entirely inside the harness via {@code
- * HarnessAgent#workspaceFor(String, String)} (driven by the per-agent {@code
- * RemoteFilesystemSpec} wired in {@code BuilderConfig}). The only thing left in this module is the
- * shared on-disk root resolver used by platform side-channels (the per-user marketplaces git-clone
- * cache and per-agent workspace scaffolding).
+ * Filesystem composition for each agent lives inside the harness via {@code
+ * HarnessAgent#workspaceFor(String, String)} and is selected by {@code
+ * builder.workspace-store.fs-spec} in {@code BuilderConfig}. The only thing left in this module is
+ * the shared on-disk root resolver used by platform side-channels (the per-user marketplaces
+ * git-clone cache and per-agent workspace scaffolding).
*/
@Configuration
public class BuilderWorkspaceConfig {
diff --git a/agentscope-examples/agents/agentscope-builder/src/main/resources/application.yml b/agentscope-examples/agents/agentscope-builder/src/main/resources/application.yml
index bdb6a0e0c8..392d87b7d4 100644
--- a/agentscope-examples/agents/agentscope-builder/src/main/resources/application.yml
+++ b/agentscope-examples/agents/agentscope-builder/src/main/resources/application.yml
@@ -78,17 +78,30 @@ builder:
workspace: ${BUILDER_WORKSPACE:${CLAW_WORKSPACE:}}
# ---- Workspace storage topology (multi-tenant filesystem) ----
- # Builder always runs every HarnessAgent on a CompositeFilesystem: a read-only
- # LocalFilesystem over `${builder.workspace}/.agentscope/workspace` (templates,
- # AGENTS.md, default skills/subagents/knowledge shipped on disk) blended with
- # per-(owner, agent) RemoteFilesystem routes for memory/, MEMORY.md, sessions/,
- # tasks/, skills/, subagents/. The remote routes are backed by a `BaseStore`
- # Spring bean — operators MUST provide one (e.g. SqliteBaseStore, a Redis-
- # backed implementation, etc.); a missing bean fails context refresh fast.
+ # `auto` selects LocalFilesystemSpec for local/in-process state stores and
+ # RemoteFilesystemSpec for distributed state stores. Set `local`, `sandbox`,
+ # or `remote` explicitly to override the detected topology. Remote mode uses
+ # the configured BaseStore; sandbox mode requires builder.sandbox.enabled=true.
workspace-store:
+ # auto selects local for in-process state stores and remote for distributed stores.
+ # Set explicitly to local, sandbox, or remote to override the detected topology.
+ fs-spec: ${BUILDER_WORKSPACE_FS_SPEC:auto}
local:
max-file-size-mb: 10
+ # ---- Optional Docker sandbox filesystem ----
+ # Used when builder.workspace-store.fs-spec=sandbox. Keeping enabled=false avoids
+ # creating Docker resources for the default local/remote topology.
+ sandbox:
+ enabled: ${BUILDER_SANDBOX_ENABLED:false}
+ image: ${BUILDER_SANDBOX_IMAGE:agentscope/python-sandbox:py311-slim}
+ network: ${BUILDER_SANDBOX_NETWORK:none}
+ workspace-root: ${BUILDER_SANDBOX_WORKSPACE_ROOT:/workspace}
+ isolation: ${BUILDER_SANDBOX_ISOLATION:USER}
+ projection-roots: ${BUILDER_SANDBOX_PROJECTION_ROOTS:AGENTS.md,skills,subagents,knowledge}
+ cpu-count: ${BUILDER_SANDBOX_CPU_COUNT:0}
+ memory-bytes: ${BUILDER_SANDBOX_MEMORY_BYTES:0}
+
# ---- DashScope model (used when no Model Spring bean is present) ----
# Set DASHSCOPE_API_KEY env var or override api-key below.
dashscope:
diff --git a/agentscope-examples/agents/agentscope-builder/src/test/java/io/agentscope/builder/web/config/BuilderConfigFilesystemModeTest.java b/agentscope-examples/agents/agentscope-builder/src/test/java/io/agentscope/builder/web/config/BuilderConfigFilesystemModeTest.java
new file mode 100644
index 0000000000..2a89816b18
--- /dev/null
+++ b/agentscope-examples/agents/agentscope-builder/src/test/java/io/agentscope/builder/web/config/BuilderConfigFilesystemModeTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.builder.web.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.verify;
+
+import io.agentscope.core.state.AgentStateStore;
+import io.agentscope.core.state.InMemoryAgentStateStore;
+import io.agentscope.harness.agent.HarnessAgent;
+import io.agentscope.harness.agent.filesystem.remote.store.BaseStore;
+import io.agentscope.harness.agent.filesystem.spec.LocalFilesystemSpec;
+import io.agentscope.harness.agent.filesystem.spec.RemoteFilesystemSpec;
+import io.agentscope.harness.agent.filesystem.spec.SandboxFilesystemSpec;
+import org.junit.jupiter.api.Test;
+
+class BuilderConfigFilesystemModeTest {
+
+ @Test
+ void autoModeSelectsLocalFilesystemForInProcessStateStore() {
+ BuilderConfig.FilesystemMode mode =
+ BuilderConfig.resolveFilesystemMode("auto", new InMemoryAgentStateStore());
+
+ assertThat(mode).isEqualTo(BuilderConfig.FilesystemMode.LOCAL);
+ }
+
+ @Test
+ void autoModeSelectsRemoteFilesystemForDistributedStateStore() {
+ AgentStateStore distributedStore = mock(AgentStateStore.class);
+
+ BuilderConfig.FilesystemMode mode =
+ BuilderConfig.resolveFilesystemMode("auto", distributedStore);
+
+ assertThat(mode).isEqualTo(BuilderConfig.FilesystemMode.REMOTE);
+ }
+
+ @Test
+ void rejectsRemoteFilesystemWithLocalStateStore() {
+ assertThatThrownBy(
+ () ->
+ BuilderConfig.resolveFilesystemMode(
+ "remote", new InMemoryAgentStateStore()))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("requires a distributed AgentStateStore");
+ }
+
+ @Test
+ void rejectsUnknownFilesystemMode() {
+ assertThatThrownBy(
+ () ->
+ BuilderConfig.resolveFilesystemMode(
+ "unsupported", new InMemoryAgentStateStore()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("auto, local, sandbox, remote");
+ }
+
+ @Test
+ void configuresLocalFilesystemSpec() {
+ HarnessAgent.Builder builder = mock(HarnessAgent.Builder.class);
+
+ BuilderConfig.configureFilesystem(
+ builder, BuilderConfig.FilesystemMode.LOCAL, mock(BaseStore.class), null);
+
+ verify(builder).filesystem(any(LocalFilesystemSpec.class));
+ }
+
+ @Test
+ void configuresRemoteFilesystemSpec() {
+ HarnessAgent.Builder builder = mock(HarnessAgent.Builder.class);
+
+ BuilderConfig.configureFilesystem(
+ builder, BuilderConfig.FilesystemMode.REMOTE, mock(BaseStore.class), null);
+
+ verify(builder).filesystem(any(RemoteFilesystemSpec.class));
+ }
+
+ @Test
+ void configuresProvidedSandboxFilesystemSpec() {
+ HarnessAgent.Builder builder = mock(HarnessAgent.Builder.class);
+ SandboxFilesystemSpec sandboxFilesystemSpec = mock(SandboxFilesystemSpec.class);
+
+ BuilderConfig.configureFilesystem(
+ builder,
+ BuilderConfig.FilesystemMode.SANDBOX,
+ mock(BaseStore.class),
+ sandboxFilesystemSpec);
+
+ verify(builder).filesystem(same(sandboxFilesystemSpec));
+ }
+}
From 97520f73a8ced4276989210b9b3c585cd4320a5c Mon Sep 17 00:00:00 2001
From: BobSong Applications can provide their own {@link SandboxFilesystemSpec} bean to override this
+ * default implementation.
+ *
+ * @return the sandbox filesystem specification used when sandbox mode is enabled
+ */
@Bean
@ConditionalOnMissingBean(SandboxFilesystemSpec.class)
public SandboxFilesystemSpec sandboxFilesystemSpec() {
@@ -119,7 +128,7 @@ public SandboxFilesystemSpec sandboxFilesystemSpec() {
private static IsolationScope parseScope(String raw) {
if (raw == null || raw.isBlank()) return IsolationScope.SESSION;
try {
- return IsolationScope.valueOf(raw.trim().toUpperCase());
+ return IsolationScope.valueOf(raw.trim().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Unknown builder.sandbox.isolation value '"
+ *
+ *
+ * Configuration
+ *
+ * {@code
+ * builder:
+ * sandbox:
+ * enabled: true # off by default
+ * image: agentscope/python-sandbox:py311-slim
+ * network: none # docker --network value
+ * workspace-root: /workspace # path inside container
+ * isolation: USER # SESSION | USER | AGENT | GLOBAL
+ * projection-roots: AGENTS.md,skills,subagents,knowledge
+ * cpu-count: 1
+ * memory-bytes: 1073741824 # 1 GiB
+ * }
+ */
+@Configuration
+@ConditionalOnProperty(name = "builder.sandbox.enabled", havingValue = "true")
+public class BuilderSandboxConfig {
+
+ private static final Logger log = LoggerFactory.getLogger(BuilderSandboxConfig.class);
+
+ @Value("${builder.sandbox.image:agentscope/python-sandbox:py311-slim}")
+ private String image;
+
+ @Value("${builder.sandbox.network:none}")
+ private String network;
+
+ @Value("${builder.sandbox.workspace-root:/workspace}")
+ private String workspaceRoot;
+
+ @Value("${builder.sandbox.isolation:USER}")
+ private String isolation;
+
+ @Value("${builder.sandbox.projection-roots:AGENTS.md,skills,subagents,knowledge}")
+ private String projectionRoots;
+
+ @Value("${builder.sandbox.cpu-count:0}")
+ private long cpuCount;
+
+ @Value("${builder.sandbox.memory-bytes:0}")
+ private long memoryBytes;
+
+ @Bean
+ @ConditionalOnMissingBean(SandboxFilesystemSpec.class)
+ public SandboxFilesystemSpec sandboxFilesystemSpec() {
+ IsolationScope scope = parseScope(isolation);
+ List