Skip to content
Open
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
17 changes: 17 additions & 0 deletions agentscope-examples/agents/agentscope-builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ Agents run directly on the host with `LocalFilesystemWithShell`. Each user's wor

**When to use:** Single-node deployments, local development, trusted environments.

### Shared read-only workspace directories

Local and remote filesystem modes can expose selected directories from the physical agent
workspace root as a shared baseline for every tenant:

```yaml
builder:
workspace-store:
shared-read-prefixes: docs/,knowledge-base/
```

The equivalent environment variable is
`BUILDER_WORKSPACE_SHARED_READ_PREFIXES=docs/,knowledge-base/`. Reads fall back to the shared
directory (for example `~/.agentscope/builder/workspace/docs/`), while tenant writes use
copy-on-write and remain in that tenant's namespace. Shared source files are never modified by an
agent write.

### Sandbox Mode

```yaml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public final class BuilderBootstrap {
private final Map<String, HarnessAgent> agents;
private final AgentscopeConfig loadedConfig;
private final List<Channel> registeredChannels;
private final List<Consumer<HarnessAgent.Builder>> globalConfigurators;
private final HarnessGateway gateway;
private final ChannelManager channelManager;

Expand All @@ -119,6 +120,7 @@ private BuilderBootstrap(
Map<String, HarnessAgent> agents,
AgentscopeConfig loadedConfig,
List<Channel> registeredChannels,
List<Consumer<HarnessAgent.Builder>> globalConfigurators,
HarnessGateway gateway,
ChannelManager channelManager) {
this.cwd = Objects.requireNonNull(cwd, "cwd");
Expand All @@ -128,6 +130,8 @@ private BuilderBootstrap(
this.loadedConfig = loadedConfig != null ? loadedConfig : new AgentscopeConfig();
this.registeredChannels =
registeredChannels != null ? List.copyOf(registeredChannels) : List.of();
this.globalConfigurators =
globalConfigurators != null ? List.copyOf(globalConfigurators) : List.of();
this.gateway = gateway;
this.channelManager = channelManager;
}
Expand Down Expand Up @@ -234,6 +238,25 @@ public Path configPath() {
return configPath;
}

/**
* Applies the cross-cutting configuration registered through {@link
* Builder#configureAllAgents(Consumer)} to an agent created after bootstrap startup.
*
* <p>Dynamic agents must use this method so filesystem, state-store, middleware, and similar
* application-wide settings remain consistent with agents loaded during {@link Builder#build()}.
* Call it after applying the dynamic agent's own fields and toolkit, but before {@link
* HarnessAgent.Builder#build()}; this mirrors startup ordering and gives global configurators
* the same final override semantics for both startup and dynamically created agents.
*
* @param builder fully initialized dynamic builder awaiting global configuration and build
*/
public void configureDynamicAgent(HarnessAgent.Builder builder) {
Objects.requireNonNull(builder, "builder");
for (Consumer<HarnessAgent.Builder> configurator : globalConfigurators) {
configurator.accept(builder);
}
}

List<Channel> registeredChannels() {
return registeredChannels;
}
Expand Down Expand Up @@ -646,6 +669,7 @@ public BuilderBootstrap build() throws IOException {
Map.copyOf(built),
fileConfig,
resolvedChannels,
globalConfigurators,
gateway,
channelMgr);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ public class AgentCatalogService {
private final BuilderBootstrap builderBootstrap;
private final UserAgentDefinitionStore store;
private final Model model;
private final io.agentscope.builder.web.toolbus.ToolEventBus toolEventBus;
private final TemplateRegistry templateRegistry;
private final SharedWorkspacePaths sharedWorkspacePaths;
private final UserStore userStore;
Expand All @@ -99,15 +98,13 @@ public AgentCatalogService(
BuilderBootstrap builderBootstrap,
UserAgentDefinitionStore store,
Optional<Model> modelOpt,
io.agentscope.builder.web.toolbus.ToolEventBus toolEventBus,
TemplateRegistry templateRegistry,
SharedWorkspacePaths sharedWorkspacePaths,
UserStore userStore,
AgentAclService aclService) {
this.builderBootstrap = builderBootstrap;
this.store = store;
this.model = modelOpt.orElse(null);
this.toolEventBus = toolEventBus;
this.templateRegistry = templateRegistry;
this.sharedWorkspacePaths = sharedWorkspacePaths;
this.userStore = userStore;
Expand Down Expand Up @@ -860,9 +857,10 @@ private String buildAndRegisterUca(String userId, UserAgentDefinitionStore.Store
builderBootstrap.channelManager()));
b.toolkit(ucaToolkit);

// Inject ToolNotificationMiddleware so user-custom agents also publish tool-call events.
b.middleware(
new io.agentscope.builder.web.toolbus.ToolNotificationMiddleware(toolEventBus));
// Keep this call after per-agent fields/toolkit setup and before build(). This matches the
// startup build order, so global configurators intentionally get the same final say over
// the filesystem spec (including shared read prefixes), state store, and middleware.
builderBootstrap.configureDynamicAgent(b);

HarnessAgent agent = b.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import javax.sql.DataSource;
import org.slf4j.Logger;
Expand Down Expand Up @@ -113,6 +114,9 @@ public class BuilderConfig {
@Value("${builder.workspace:${claw.workspace:}}")
private String workspaceDir;

@Value("${builder.workspace-store.shared-read-prefixes:}")
private List<String> sharedReadPrefixes;

// -----------------------------------------------------------------
// Model bean — only created when an api-key is set AND no other
// Model bean is already present in the context. The conditional
Expand Down Expand Up @@ -227,12 +231,17 @@ public BuilderBootstrap builderBootstrap(
b.middleware(new ToolNotificationMiddleware(toolEventBus));
b.stateStore(stateStore);
if (localStore) {
b.filesystem(new LocalFilesystemSpec().isolationScope(IsolationScope.USER));
LocalFilesystemSpec filesystemSpec =
new LocalFilesystemSpec().isolationScope(IsolationScope.USER);
sharedReadPrefixes.forEach(filesystemSpec::addSharedPrefix);
b.filesystem(filesystemSpec);
} else {
b.filesystem(
RemoteFilesystemSpec filesystemSpec =
new RemoteFilesystemSpec(baseStore)
.isolationScope(IsolationScope.USER)
.addSharedPrefix("activity/"));
.addSharedPrefix("activity/");
sharedReadPrefixes.forEach(filesystemSpec::addSharedPrefix);
b.filesystem(filesystemSpec);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,15 @@ 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.
# Agents use LocalFilesystemSpec with a local AgentStateStore, or
# RemoteFilesystemSpec when a distributed AgentStateStore is supplied. Both
# topologies honor shared-read-prefixes with the same copy-on-write semantics.
workspace-store:
local:
max-file-size-mb: 10
# Comma-separated workspace-relative directories exposed to every tenant as a
# shared read-only baseline. Tenant writes are copy-on-write and remain isolated.
shared-read-prefixes: ${BUILDER_WORKSPACE_SHARED_READ_PREFIXES:docs}

# ---- DashScope model (used when no Model Spring bean is present) ----
# Set DASHSCOPE_API_KEY env var or override api-key below.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,26 @@
*/
package io.agentscope.builder;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import io.agentscope.builder.runtime.BuilderBootstrap;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.Model;
import io.agentscope.harness.agent.HarnessAgent;
import io.agentscope.harness.agent.IsolationScope;
import io.agentscope.harness.agent.filesystem.spec.LocalFilesystemSpec;
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;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -103,6 +109,45 @@ void multiAgent_defaultRoutesToMain() throws Exception {
assertTrue(reply.getTextContent().contains("from-main"));
}

@Test
void dynamicAgentUsesGlobalFilesystemConfigurationForItsOwnWorkspace() throws Exception {
Path customWorkspace = tempDir.resolve("custom-agent-workspace");
Files.createDirectories(customWorkspace.resolve("docs"));
Files.writeString(customWorkspace.resolve("docs/guide.md"), "custom shared guide");

Model model = stubModel("dynamic-agent-reply");
BuilderBootstrap bootstrap =
BuilderBootstrap.builder()
.skipConfigFile(true)
.cwd(tempDir)
.model(model)
.configureAgent("main", b -> b.name("main"))
.configureAllAgents(
b ->
b.filesystem(
new LocalFilesystemSpec()
.isolationScope(IsolationScope.USER)
.addSharedPrefix("docs")))
.mainAgent("main")
.build();

HarnessAgent.Builder dynamicBuilder =
HarnessAgent.builder()
.agentId("dynamic")
.name("dynamic")
.model(model)
.workspace(customWorkspace);
bootstrap.configureDynamicAgent(dynamicBuilder);
HarnessAgent dynamicAgent = dynamicBuilder.build();

String content =
dynamicAgent
.workspaceFor("alice", null)
.readManagedWorkspaceFileUtf8(
RuntimeContext.builder().userId("alice").build(), "docs/guide.md");
assertEquals("custom shared guide", content);
}

private static Model stubModel(String assistantText) {
Model model = mock(Model.class);
when(model.getModelName()).thenReturn("stub-model");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import io.agentscope.builder.web.auth.UserStore.UserRecord;
import io.agentscope.builder.web.share.AgentAclService;
import io.agentscope.builder.web.template.TemplateRegistry;
import io.agentscope.builder.web.toolbus.ToolEventBus;
import io.agentscope.builder.web.workspace.SharedWorkspacePaths;
import io.agentscope.harness.agent.HarnessAgent;
import java.util.List;
Expand Down Expand Up @@ -107,7 +106,6 @@ void constructor_installsResolverOnGateway() {
bootstrap,
mock(UserAgentDefinitionStore.class),
Optional.empty(),
mock(ToolEventBus.class),
mock(TemplateRegistry.class),
mock(SharedWorkspacePaths.class),
mock(UserStore.class),
Expand Down Expand Up @@ -142,7 +140,6 @@ private static AgentCatalogService newService(
bootstrap,
store,
Optional.empty(),
mock(ToolEventBus.class),
mock(TemplateRegistry.class),
mock(SharedWorkspacePaths.class),
userStore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@

import io.agentscope.harness.agent.filesystem.remote.RemoteFilesystem;
import io.agentscope.harness.agent.filesystem.remote.store.NamespaceFactory;
import io.agentscope.harness.agent.filesystem.spec.LocalFilesystemSpec;
import io.agentscope.harness.agent.filesystem.spec.RemoteFilesystemSpec;
import java.util.List;

/**
* Controls how agent state is isolated and shared across calls.
*
* <p>This enum is the canonical isolation-scope definition used by both the sandbox filesystem
* backend ({@link io.agentscope.harness.agent.sandbox.SandboxContext}) and the remote filesystem
* backend ({@link RemoteFilesystemSpec}).
* <p>This enum is the canonical isolation-scope definition used by the sandbox filesystem backend
* ({@link io.agentscope.harness.agent.sandbox.SandboxContext}), the remote filesystem backend
* ({@link RemoteFilesystemSpec}), and local shared-prefix overlays ({@link LocalFilesystemSpec}).
*
* <p><b>Sandbox semantics</b>: the scope determines which key is used when persisting and loading
* {@code _sandbox.json} state. Calls that resolve to the <em>same</em> scope key will
Expand All @@ -37,6 +38,11 @@
* key-value store. Different scopes produce different namespace prefixes, controlling which calls
* share the same view of stored files.
*
* <p><b>Local shared-prefix semantics</b>: the scope determines the local workspace namespace used
* for copy-on-write overrides of directories registered through {@link
* LocalFilesystemSpec#addSharedPrefix(String)}. The un-namespaced directory remains the shared,
* read-only baseline.
*
* <p>Scope selection:
* <ul>
* <li>{@link #USER} – shared across all sessions of the same user; the default. When
Expand Down
Loading
Loading