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 @@ * *

Filesystem topology

* - *

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}. * *

Model wiring (priority order)

* @@ -113,6 +111,9 @@ public class BuilderConfig { @Value("${builder.workspace:${claw.workspace:}}") private String workspaceDir; + @Value("${builder.workspace-store.fs-spec:" + "${claw.workspace-store.fs-spec:auto}}") + private String workspaceFilesystemMode; + // ----------------------------------------------------------------- // Model bean — only created when an api-key is set AND no other // Model bean is already present in the context. The conditional @@ -168,21 +169,23 @@ public BaseStore baseStore(DataSource dataSource) { * Assembles the {@link BuilderBootstrap}, loading agent config from {@code agentscope.json} and * starting the {@link ChatUiChannel} for per-user isolated sessions. * - *

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 modelOpt, ToolEventBus toolEventBus, BaseStore baseStore, - Optional sessionOpt) + Optional sessionOpt, + Optional sandboxFilesystemSpecOpt) throws IOException { Path cwd = resolveCwd(); ensureAgentscopeConfig(); @@ -208,32 +211,27 @@ public BuilderBootstrap builderBootstrap( AgentStateStore.class.getName()); } - // RemoteFilesystemSpec requires a distributed AgentStateStore; when the effective store is - // local (InMemory/JsonFile), use LocalFilesystemSpec instead so the harness won't reject - // the topology at build time. - boolean localStore = isLocalStateStore(stateStore); - if (localStore) { - log.info( - "Effective AgentStateStore is local ({}); using LocalFilesystemSpec.", - stateStore.getClass().getSimpleName()); - } else { - log.info( - "Effective AgentStateStore is distributed ({}); using RemoteFilesystemSpec.", - stateStore.getClass().getSimpleName()); - } + FilesystemMode filesystemMode = resolveFilesystemMode(workspaceFilesystemMode, stateStore); + SandboxFilesystemSpec sandboxFilesystemSpec = + filesystemMode == FilesystemMode.SANDBOX + ? sandboxFilesystemSpecOpt.orElseThrow( + () -> + new IllegalStateException( + "builder.workspace-store.fs-spec=sandbox requires " + + "builder.sandbox.enabled=true or a " + + "SandboxFilesystemSpec bean")) + : null; + + log.info( + "Using {} filesystem mode with AgentStateStore {}.", + filesystemMode.configValue(), + stateStore.getClass().getSimpleName()); builder.configureAllAgents( b -> { b.middleware(new ToolNotificationMiddleware(toolEventBus)); b.stateStore(stateStore); - if (localStore) { - b.filesystem(new LocalFilesystemSpec().isolationScope(IsolationScope.USER)); - } else { - b.filesystem( - new RemoteFilesystemSpec(baseStore) - .isolationScope(IsolationScope.USER) - .addSharedPrefix("activity/")); - } + configureFilesystem(b, filesystemMode, baseStore, sandboxFilesystemSpec); }); BuilderBootstrap bootstrap = builder.build(); @@ -295,6 +293,65 @@ private static boolean isLocalStateStore(AgentStateStore store) { return store instanceof InMemoryAgentStateStore || store instanceof JsonFileAgentStateStore; } + static FilesystemMode resolveFilesystemMode(String configuredMode, AgentStateStore stateStore) { + FilesystemMode mode = FilesystemMode.parse(configuredMode); + if (mode == FilesystemMode.AUTO) { + return isLocalStateStore(stateStore) ? FilesystemMode.LOCAL : FilesystemMode.REMOTE; + } + if (mode == FilesystemMode.REMOTE && isLocalStateStore(stateStore)) { + throw new IllegalStateException( + "builder.workspace-store.fs-spec=remote requires a distributed " + + "AgentStateStore; found " + + stateStore.getClass().getSimpleName()); + } + return mode; + } + + static void configureFilesystem( + io.agentscope.harness.agent.HarnessAgent.Builder builder, + FilesystemMode mode, + BaseStore baseStore, + SandboxFilesystemSpec sandboxFilesystemSpec) { + switch (mode) { + case LOCAL -> + builder.filesystem( + new LocalFilesystemSpec().isolationScope(IsolationScope.USER)); + case SANDBOX -> builder.filesystem(sandboxFilesystemSpec); + case REMOTE -> + builder.filesystem( + new RemoteFilesystemSpec(baseStore) + .isolationScope(IsolationScope.USER) + .addSharedPrefix("activity/")); + case AUTO -> + throw new IllegalStateException( + "AUTO filesystem mode must be resolved before configuring agents"); + } + } + + enum FilesystemMode { + AUTO, + LOCAL, + SANDBOX, + REMOTE; + + static FilesystemMode parse(String raw) { + String value = raw == null || raw.isBlank() ? "auto" : raw.trim(); + try { + return valueOf(value.toUpperCase(java.util.Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Unknown builder.workspace-store.fs-spec value '" + + raw + + "'. Expected one of auto, local, sandbox, remote.", + e); + } + } + + String configValue() { + return name().toLowerCase(java.util.Locale.ROOT); + } + } + private Path resolveCwd() { if (workspaceDir != null && !workspaceDir.isBlank()) { return Paths.get(workspaceDir).toAbsolutePath().normalize(); diff --git a/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderSandboxConfig.java b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderSandboxConfig.java new file mode 100644 index 0000000000..6a75ae337a --- /dev/null +++ b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderSandboxConfig.java @@ -0,0 +1,141 @@ +/* + * 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 io.agentscope.harness.agent.IsolationScope; +import io.agentscope.harness.agent.filesystem.spec.SandboxFilesystemSpec; +import io.agentscope.harness.agent.sandbox.impl.docker.DockerFilesystemSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Optional sandbox filesystem wiring for the agentscope-builder deployable. When {@code + * builder.sandbox.enabled=true}, this configuration contributes: + * + *

+ * + *

The bean is consumed by {@link BuilderConfig#builderBootstrap} which applies it to every + * agent via {@code configureAllAgents}. + * + *

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 roots = parseRoots(projectionRoots); + + DockerFilesystemSpec spec = new DockerFilesystemSpec(); + spec.image(image).workspaceRoot(workspaceRoot); + spec.workspaceProjectionRoots(roots); + spec.isolationScope(scope); + + if (network != null && !network.isBlank()) { + spec.network(network); + } + if (cpuCount > 0) { + spec.cpuCount(cpuCount); + } + if (memoryBytes > 0) { + spec.memorySizeBytes(memoryBytes); + } + + log.info( + "Sandbox enabled: image={}, network={}, workspaceRoot={}, isolation={}," + + " projection={}, cpuCount={}, memoryBytes={}", + image, + network, + workspaceRoot, + scope, + roots, + cpuCount, + memoryBytes); + return spec; + } + + private static IsolationScope parseScope(String raw) { + if (raw == null || raw.isBlank()) return IsolationScope.SESSION; + try { + return IsolationScope.valueOf(raw.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Unknown builder.sandbox.isolation value '" + + raw + + "'. Expected one of SESSION, USER, AGENT, GLOBAL.", + e); + } + } + + private static List parseRoots(String csv) { + if (csv == null || csv.isBlank()) return List.of(); + List out = new ArrayList<>(); + for (String token : Arrays.asList(csv.split(","))) { + String t = token.trim(); + if (!t.isEmpty()) out.add(t); + } + return out; + } +} diff --git a/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/workspace/BuilderWorkspaceConfig.java b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/workspace/BuilderWorkspaceConfig.java index 7876b2fb2b..f6ee10d4bc 100644 --- a/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/workspace/BuilderWorkspaceConfig.java +++ b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/workspace/BuilderWorkspaceConfig.java @@ -25,11 +25,11 @@ /** * Spring configuration for shared workspace path resolution. * - *

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 Date: Sun, 12 Jul 2026 13:30:46 +0800 Subject: [PATCH 2/3] Potential fix for pull request finding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @Value 不拼接字符串 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../java/io/agentscope/builder/web/config/BuilderConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f7cefa6c9a..81d16a87aa 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 @@ -111,7 +111,7 @@ public class BuilderConfig { @Value("${builder.workspace:${claw.workspace:}}") private String workspaceDir; - @Value("${builder.workspace-store.fs-spec:" + "${claw.workspace-store.fs-spec:auto}}") + @Value("${builder.workspace-store.fs-spec:${claw.workspace-store.fs-spec:auto}}") private String workspaceFilesystemMode; // ----------------------------------------------------------------- From 9442d04116a0c981bed74692488af6479e91e3f4 Mon Sep 17 00:00:00 2001 From: BobSong <2434005232@qq.com> Date: Sun, 12 Jul 2026 13:35:11 +0800 Subject: [PATCH 3/3] fix(builder): address sandbox config review --- .../builder/web/config/BuilderSandboxConfig.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderSandboxConfig.java b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderSandboxConfig.java index 6a75ae337a..bf331e7286 100644 --- a/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderSandboxConfig.java +++ b/agentscope-examples/agents/agentscope-builder/src/main/java/io/agentscope/builder/web/config/BuilderSandboxConfig.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -82,6 +83,14 @@ public class BuilderSandboxConfig { @Value("${builder.sandbox.memory-bytes:0}") private long memoryBytes; + /** + * Creates the default Docker-backed sandbox filesystem specification. + * + *

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 '"