agentscope-ai / agentscope-ai/agentscope-java

[Bug]:SandboxBackedFilesystem 并发多用户不安全 &&框架后台任务用 RuntimeContext.empty() 写文件系统,破坏多用户路由

Open
#1,896 0 comments 0 reactions 0 assignees View on GitHub
area/core/tool bug
Dominant language
Java
Stars
5.6k
Forks
1.3k
Avg merge
4d 12h
Merged PRs (30d)
77

Description

---
Bug 1:SandboxBackedFilesystem 并发多用户不安全

Title: SandboxBackedFilesystem is not safe for concurrent multi-user agents (single volatile field)

## Problem

`SandboxBackedFilesystem` uses a single `volatile Sandbox sandbox` field injected per-call
via `setSandbox()`. This design implicitly assumes one agent instance serves one call at a time.

In a multi-tenant setup (one singleton `HarnessAgent` serving multiple concurrent users),
concurrent calls overwrite each other's sandbox field, causing cross-user sandbox contamination:

- User A's call sets `sandbox = sandboxA`
- User B's call sets `sandbox = sandboxB`
- User A's tool execution now runs inside sandboxB

## Expected Behavior

`getSandbox()` should route to the correct sandbox based on the current `RuntimeContext`
(userId / sessionId), not from a shared mutable field.

## Workaround (current)

We had to implement a custom `RoutingFilesystem` that overrides `getSandbox()` to look up
a per-user `SandboxPool` via `RuntimeContext`. This is hundreds of lines of boilerplate that
reimplements what `IsolationScope.USER` promises to provide.

## Environment

- agentscope-harness: v2.0.0-RC4
- IsolationScope.USER is documented as supported, but the implementation still uses
the single volatile field pattern

## Proposed Fix

Make `getSandbox()` accept a `RuntimeContext` parameter (or use a ThreadLocal bridge)
and route to the correct sandbox slot based on userId/sessionId, consistent with what
`IsolationScope` declares.

---
Bug 2:框架后台任务用 RuntimeContext.empty() 写文件系统,破坏多用户路由

Title: Framework background tasks (ConversationCompactor, WorkspaceMessageBus, etc.)
use RuntimeContext.empty(), making per-user filesystem routing impossible

## Problem

Framework-internal components run asynchronously with `RuntimeContext.empty()` (no userId,
no sessionId):

- `ConversationCompactor` — compacts session history
- `WorkspaceMessageBus`
- `AsyncToolRegistry`
- `SubagentsMiddleware`

In the framework's single-sandbox model this is fine — there's only one sandbox per call.
But in a multi-user setup where filesystem routing depends on `RuntimeContext.userId`,
empty context cannot be routed to the correct user's sandbox.

## Impact

These writes fall through to the host-local filesystem (pod-local), which:
1. Makes pods stateful — session files are lost on pod restart
2. Triggers spurious workspace re-syncs into sandbox on every Compactor write
3. Produces noisy "local workspace changed, re-syncing" logs on every turn

## Root Cause

Background tasks are scheduled on Reactor's `boundedElastic` scheduler without carrying
user context. `InheritableThreadLocal` doesn't propagate through thread pool reuse.

## Proposed Fix

Options (in order of preference):
1. Have the framework propagate session context to background tasks via
`Schedulers.onScheduleHook()` or Reactor `Context`
2. Expose a hook for background task context injection
3. Document that background tasks require a "system sandbox" slot and make the routing
explicit rather than silently falling back to host FS

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.