Skip to content

feat(harness): add deliver_artifact tool for sandboxed agents - #2667

Open
chcodex wants to merge 7 commits into
agentscope-ai:mainfrom
chcodex:fix/artifact-delivery-tool
Open

feat(harness): add deliver_artifact tool for sandboxed agents#2667
chcodex wants to merge 7 commits into
agentscope-ai:mainfrom
chcodex:fix/artifact-delivery-tool

Conversation

@chcodex

@chcodex chcodex commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #2663

Problem

In sandbox mode, WorkspaceContextMiddleware renders a ## Workspace paragraph telling the model to "use the upload/download tools to move files across the boundary". The harness exposes no such AgentToolFilesystemTool only registers read_file/write_file/edit_file/grep_files/glob_files/list_files, and SandboxFileTransfer is an internal mechanism invisible to the LLM. The instruction is a dead reference: an agent that produces an artifact inside a sandbox has no supported way to hand it out.

Change

Introduces a generic, business-agnostic artifact delivery path for sandboxed agents:

  • ArtifactDeliveryTarget SPI — application implements deliver(RuntimeContext, ArtifactDeliveryRequest) -> ArtifactDeliveryResult and owns destination logic (WebDAV, object store, project artifact store, …).
  • deliver_artifact tool — registered via HarnessAgent.Builder.artifactDeliveryTarget(...). It downloads the file bytes from the agent filesystem and delegates transport to the target.
  • Sandbox prompt — when a target is configured, the workspace paragraph tells the model to use deliver_artifact; otherwise it states plainly that there is no mechanism for moving files across the boundary. The old dead reference to "upload/download tools" is gone.
  • Safety/limits:
    • The tool is exposed only to the main agent, not to automatically constructed subagents (they return plain text results for the main agent to deliver).
    • It is suppressed when disableFilesystemTools() is set, keeping file-access isolation intact.
    • fileName must be a plain file name — path separators, . and .. are rejected, and target names are derived from a cleaned basename.
    • ArtifactDeliveryRequest.fileName() is documented as already validated, so targets can resolve it directly.

Tests

  • ArtifactDeliveryToolTest — download/forward, defaults, normalization, failures, conflict advice, and new fileName validation cases.
  • WorkspaceContextMiddlewareSandboxPromptTest — both sandbox prompt branches.
  • HarnessAgentTest — registration, disableFilesystemTools interaction, main-agent-only exposure to subagents, and prompt integration.

…rget SPI

Sandboxed agents can hand produced artifacts to a configured destination
outside the workspace via the generic deliver_artifact tool. The tool
downloads file bytes from the filesystem and delegates transport to a
business-agnostic ArtifactDeliveryTarget. The sandbox workspace prompt
tells the model to use it when configured. The tool is exposed only to
the main agent, honors disableFilesystemTools, and rejects unsafe target
file names.
@chcodex
chcodex force-pushed the fix/artifact-delivery-tool branch from 0ee406d to 6cf458a Compare August 11, 2026 14:06
WorkspaceManager(workspace, filesystem) opens the SQLite-backed
WorkspaceIndex (.index/workspace.db), whose JDBC driver keeps a file
handle open. The test never closed the manager, so JUnit @tempdir
cleanup failed on Windows with DirectoryNotEmptyException, breaking
the build (windows-latest) CI job. Wrap the manager in try-with-resources.
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.56716% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...scope/harness/agent/tool/ArtifactDeliveryTool.java 84.31% 1 Missing and 7 partials ⚠️
...s/agent/middleware/WorkspaceContextMiddleware.java 90.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@AgentScopeJavaBot AgentScopeJavaBot added enhancement New feature or request area/harness agentscope-harness (test/runtime support) area/docs Documentation labels Aug 12, 2026

@AgentScopeJavaBot AgentScopeJavaBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 AI Review

This PR introduces a generic, business-agnostic artifact delivery mechanism for sandboxed agents. It adds an ArtifactDeliveryTarget SPI, a deliver_artifact tool registered only on the main agent, and corresponding sandbox prompt updates. The design is clean: the framework stays transport-agnostic (WebDAV, object store, etc. are implemented by the application), the tool validates fileName against path traversal, and the builder correctly suppresses the tool when disableFilesystemTools is set. Test coverage is solid with 13+ unit tests covering happy paths, input validation, error branches, and subagent isolation. The PR resolves #2663 appropriately.

boolean effectiveForce = Boolean.TRUE.equals(force);

List<FileDownloadResponse> responses =
filesystem.downloadFiles(runtimeContext, List.of(normalized));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[major] No file size guard. downloadFiles() loads the entire file into a byte[] which is then wrapped in ArtifactDeliveryRequest and forwarded to the target. The tool description explicitly mentions "archives" as a use case. A sandboxed agent could produce a very large file and trigger OOM or excessive GC pressure. Consider adding a configurable maxArtifactSizeBytes on the builder and returning an error before the download when the filesystem can report size, or after download if response.content().length exceeds the limit.

* constructed subagents, which return plain text results for the main agent to deliver.
*
* <p>Note: the tool reads files from the agent filesystem, so it is also suppressed when
* {@link #disableFilesystemTools()} is used — combine the two only when you intend the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[nitpick] Self-contradictory Javadoc. The note reads: "it is also suppressed when disableFilesystemTools() is used — combine the two only when you intend the delivery tool to remain available." The second clause implies the tool will remain available when combining both, but the code actually suppresses it. Suggested rewrite to clarify the suppression semantics.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ Verified as addressed in 8b3bd29: Javadoc rewritten to clearly state Combining both leaves the tool unregistered, resolving the self-contradiction.

&& !name.equals("..");
}

private static String basename(String path) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[nitpick] isPlainFileName does not reject NUL bytes. While LLMs rarely produce \0, some target implementations (C-based file APIs, certain object stores) truncate file names at NUL. Adding && name.indexOf('\0') < 0 would close this edge case cheaply.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ Verified as addressed in 8b3bd29: isPlainFileName now includes name.indexOf('\0') < 0 check, exactly as suggested.

@AgentScopeJavaBot AgentScopeJavaBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 AI Review

This PR introduces a generic, business-agnostic artifact delivery mechanism for sandboxed agents. It adds an ArtifactDeliveryTarget SPI, a deliver_artifact tool registered only on the main agent, and corresponding sandbox prompt updates. The design is clean: the framework stays transport-agnostic (WebDAV, object store, etc. are implemented by the application), the tool validates fileName against path traversal, and the builder correctly suppresses the tool when disableFilesystemTools is set. Test coverage is solid with 13+ unit tests covering happy paths, input validation, error branches, and subagent isolation. The PR resolves #2663 appropriately.

boolean effectiveForce = Boolean.TRUE.equals(force);

List<FileDownloadResponse> responses =
filesystem.downloadFiles(runtimeContext, List.of(normalized));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[major] No file size guard. downloadFiles() loads the entire file into a byte[] which is then wrapped in ArtifactDeliveryRequest and forwarded to the target. The tool description explicitly mentions "archives" as a use case. A sandboxed agent could produce a very large file and trigger OOM or excessive GC pressure. Consider adding a configurable maxArtifactSizeBytes on the builder and returning an error before the download when the filesystem can report size, or after download if response.content().length exceeds the limit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review. This concern is acknowledged, but it is not introduced by this PR: AbstractFilesystem.downloadFiles(...) returning List<FileDownloadResponse> with a byte[] content is the framework's existing contract (also used by WorkspaceSkillRepository), and deliver_artifact simply reuses that API without adding new in-memory amplification. A real fix needs a streaming download API at the filesystem layer plus streaming support in sandbox backends — a framework-level design change beyond the scope of this PR. We will track it as a follow-up (streaming artifact delivery); this PR intentionally leaves it unchanged.

* constructed subagents, which return plain text results for the main agent to deliver.
*
* <p>Note: the tool reads files from the agent filesystem, so it is also suppressed when
* {@link #disableFilesystemTools()} is used — combine the two only when you intend the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[nitpick] Self-contradictory Javadoc. The note reads: "it is also suppressed when disableFilesystemTools() is used — combine the two only when you intend the delivery tool to remain available." The second clause implies the tool will remain available when combining both, but the code actually suppresses it. Suggested rewrite to clarify the suppression semantics.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ Verified as addressed in 8b3bd29: Javadoc rewritten to clearly state Combining both leaves the tool unregistered, resolving the self-contradiction.

&& !name.equals("..");
}

private static String basename(String path) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[nitpick] isPlainFileName does not reject NUL bytes. While LLMs rarely produce \0, some target implementations (C-based file APIs, certain object stores) truncate file names at NUL. Adding && name.indexOf('\0') < 0 would close this edge case cheaply.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ Verified as addressed in 8b3bd29: isPlainFileName now includes name.indexOf('\0') < 0 check, exactly as suggested.

- Clarify artifactDeliveryTarget javadoc: combined with disableFilesystemTools
  the tool stays unregistered (was self-contradictory)
- Reject NUL bytes in the target file name in addition to path separators
- Cover the NUL rejection with a unit test
@chcodex

chcodex commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author
image @jujn Actual test results are OK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation area/harness agentscope-harness (test/runtime support) enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Sandbox prompt references non-existent upload/download tools — agents lack an artifact delivery tool

2 participants