Skip to content

fix(harness): fix concurrent sandbox isolation in multi-user singleton deployments#1964

Open
Buktal wants to merge 8 commits into
agentscope-ai:mainfrom
Buktal:fix/sandbox-concurrent-isolation
Open

fix(harness): fix concurrent sandbox isolation in multi-user singleton deployments#1964
Buktal wants to merge 8 commits into
agentscope-ai:mainfrom
Buktal:fix/sandbox-concurrent-isolation

Conversation

@Buktal

@Buktal Buktal commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

AgentScope-Java Version

2.0.0-SNAPSHOT

Description

Fixes two related concurrency bugs in HarnessAgent when used as a singleton serving concurrent sessions (the standard multi-tenant deployment pattern documented in the class Javadoc and official examples).

Bug 1 — SandboxBackedFilesystem cross-session contamination (#1896)

SandboxBackedFilesystem held a single volatile Sandbox sandbox field, and SandboxLifecycleMiddleware held a single AtomicReference<SandboxAcquireResult>. Concurrent calls from different sessions overwrote each other's bindings — User A's tools could execute inside User B's sandbox.

Fix: replace both single-value fields with ConcurrentHashMap<sessionId, ...>. SandboxAware interface updated: setSandbox/getSandbox replaced by bindSandbox(key, sandbox), unbindSandbox(key), and getSandbox(key). The three public methods on SandboxBackedFilesystem already receive RuntimeContext, so the session key is always available at call time.

Bug 2 — WorkspaceMessageBus / WorkspaceAsyncToolRegistry using sandbox filesystem

HarnessAgent.build() constructed both components after replacing filesystem with SandboxBackedFilesystem. This caused their internal RuntimeContext.empty() file operations to call requireSandbox(null), which would throw or contaminate sandbox state.

Fix: capture busFilesystem before the sandbox replacement so bus and registry always use the non-sandbox (local or remote) filesystem. These are process-scoped infrastructure components with their own path-based isolation; they must not go through the sandbox.

Testing

  • SandboxBackedFilesystemTest: updated existing 3 tests to new API; added 3 new tests including a concurrent 8-session isolation test using CountDownLatch
  • CompactionMiddlewareTest: already existed on the feat/compaction-events branch; no changes needed for this fix

Closes #1896

Checklist

  • Code has been formatted with mvn spotless:apply
  • All tests are passing (mvn test)
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated (e.g. links, examples, etc.)
  • Code is ready for review

…n deployments

Two related bugs in HarnessAgent when used as a singleton serving concurrent sessions:

Bug 1: SandboxBackedFilesystem used a single volatile Sandbox field and
SandboxLifecycleMiddleware used a single AtomicReference<SandboxAcquireResult>.
Concurrent sessions overwrote each other's bindings, causing cross-user sandbox
contamination. Fixed by replacing both with ConcurrentHashMap<sessionId, ...>,
keyed by sessionId so each session gets an isolated slot. SandboxAware interface
updated: setSandbox/getSandbox replaced by bindSandbox/unbindSandbox/getSandbox(key).

Bug 2: WorkspaceMessageBus and WorkspaceAsyncToolRegistry were constructed with
the post-sandbox filesystem (SandboxBackedFilesystem) in HarnessAgent.build().
These are process-scoped infrastructure components; routing them through the
sandbox filesystem causes RuntimeContext.empty() calls to fail or contaminate
sandbox state. Fixed by capturing busFilesystem before the sandbox replacement
and using it exclusively for bus/registry construction.

Closes agentscope-ai#1896
@Buktal Buktal requested a review from a team June 29, 2026 13:25
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

@itxaiohanglover itxaiohanglover left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good architectural fix — separating bus/registry filesystem from the sandbox filesystem prevents accidental routing through SandboxBackedFilesystem. The per-session ConcurrentHashMap approach for sandbox bindings looks correct for concurrent isolation. One question: is there a cleanup path for stale session entries when a session ends, or do entries persist for the proxy's lifetime?

Aligns the in-memory sandbox binding key with ReActAgent.slotKey() to
prevent cross-user contamination when different users share the same
sessionId in multi-tenant singleton deployments.

Addresses review feedback from PR agentscope-ai#1964.

@jujn jujn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sandbox is still acquired in HarnessAgent.wrappedCall / wrappedStreamEvents before delegate.call() enters ReActAgent's per-slot serialization gate. If two calls for the same (userId, sessionId) are subscribed concurrently, both can run acquireForCall() first and write the same binding key. The second acquire overwrites the first sandbox/acquire result; the first call may then execute against the second sandbox and its cleanup can persist/release the wrong result, leaving the first sandbox leaked and the second call without a binding.

HarnessAgent.wrappedCall acquired the sandbox in Mono.using's resource
supplier (subscription-time eager), which runs before ReActAgent's
per-session serialization gate. Two concurrent same-session requests
both ran acquireForCall(), the second overwrote the first's binding key
in the ConcurrentHashMap — leaking the first sandbox and failing the
second call with "No active sandbox".

Fix: introduce a per-session async gate in SandboxLifecycleMiddleware
(mirroring AgentBase.serializeOnKey) via serializedCall/serializedFlux.
Same-session acquires now queue non-blocking; the second waits until the
first's release completes before proceeding. Different sessions still
run concurrently.

HarnessAgent.wrappedCall/wrappedStream/wrappedStreamEvents delegate to
the new methods instead of managing Mono.using/Flux.using directly.
@Buktal Buktal requested a review from jujn July 9, 2026 01:49

@jujn jujn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.
Can we quickly fix the conflict ? Thanks.

…t-isolation

# Conflicts:
#	agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SandboxLifecycleMiddleware.java
@Buktal

Buktal commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

LGTM. Can we quickly fix the conflict ? Thanks.

Resolved, thanks!

@Buktal Buktal force-pushed the fix/sandbox-concurrent-isolation branch from c5d726f to 3f8f8ae Compare July 9, 2026 06:17
@chickenlj

Copy link
Copy Markdown
Collaborator

@Buktal I have sent my Dingtalk Id to you email address 1171971708@qq.com, thanks.

@Buktal

Buktal commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@Buktal I have sent my Dingtalk Id to you email address 1171971708@qq.com, thanks.

Got it, thanks

@chickenlj

chickenlj commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

核心修复方向是对的——ConcurrentHashMap 替换 volatile 单字段解决跨 session 污染,busFilesystem 在沙箱替换前捕获让基础设施组件不走沙箱路由。

不过 serializedCall / serializedFlux + sandboxGates 这套同 session 并发序列化门可以先去掉。同一个 session 的并发请求应该在业务入口层就阻止,不需要在 sandbox 层再做一层排队。这部分逻辑比较复杂(Sinks gate chain + conditional remove),加进来增加了理解和维护成本,当前没有实际场景需要它。

建议保持原来 Mono.using / Flux.using 的 acquire-release 模式,只把内部的 acquire/release 改成按 sessionKey 操作就行。

另外一个小点:bindingKey()SandboxBackedFilesystemSandboxLifecycleMiddleware 里各有一份相同实现,如果只改了一边会出 bug,建议提取到一个共享位置。

Buktal added 3 commits July 10, 2026 11:50
…dingKey

- Remove serializedCall/serializedFlux + sandboxGates (same-session
  concurrency serialization gate). Same-session concurrency is guarded at
  the business entry layer, not re-queued in the sandbox layer.
- Restore acquireForCall/releaseForCall with Mono.using/Flux.using
  (aligned with main); internal acquire/release now operate per sessionKey.
- Extract shared SandboxBindingKey to remove duplicated bindingKey impl
  in SandboxBackedFilesystem and SandboxLifecycleMiddleware.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

4 participants