diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java
index 2a3b8f7b03..e22ad8d91f 100644
--- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java
+++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java
@@ -2431,14 +2431,22 @@ public HarnessAgent build() {
io.agentscope.harness.agent.skill.runtime.ShellPathPolicy.noShell();
}
- inner.middleware(
+ HarnessSkillMiddleware skillMiddleware =
new HarnessSkillMiddleware(
orderedSkillRepos,
agentToolkit,
skillFilter,
visibilityFilter,
stager,
- shellPolicy));
+ shellPolicy);
+ inner.middleware(skillMiddleware);
+
+ // Wire pre-start staging so sandbox projection picks up .skills-cache content
+ // that MarketplaceStager materialises from database-backed repositories.
+ if (sandboxLifecycleMw != null && stager != null) {
+ sandboxLifecycleMw.setBeforeStartCallback(
+ skillMiddleware::prestageMarketplaceSkills);
+ }
} else if (disableDynamicSkills) {
// Suppress core's auto-install so the static SkillBox fallback (constructed
// below by staticSkillBoxFromRepos) remains the only skill source.
diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/HarnessSkillMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/HarnessSkillMiddleware.java
index 5ee8667614..a5ed05073b 100644
--- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/HarnessSkillMiddleware.java
+++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/HarnessSkillMiddleware.java
@@ -144,6 +144,31 @@ public SkillRuntime runtime() {
return runtime;
}
+ /**
+ * Pre-stages marketplace skill resources to {@code .skills-cache/} on the host workspace.
+ * Intended to be called before sandbox start so that workspace projection picks up
+ * the staged content in the same call. Safe to call multiple times — staging is idempotent
+ * (content-hash guarded).
+ *
+ * @param ctx the per-call runtime context
+ */
+ public void prestageMarketplaceSkills(RuntimeContext ctx) {
+ if (stager == null) {
+ return;
+ }
+ if (ctx == null) {
+ ctx = RuntimeContext.empty();
+ }
+ Map merged = mergeRepositories(ctx);
+ if (merged.isEmpty()) {
+ return;
+ }
+ List visible = applyVisibility(merged.values(), ctx);
+ if (!visible.isEmpty()) {
+ stager.stage(visible, sourceNamespaces);
+ }
+ }
+
@Override
public Mono onSystemPrompt(Agent agent, RuntimeContext ctx, String currentPrompt) {
if (ctx == null) {
diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SandboxLifecycleMiddleware.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SandboxLifecycleMiddleware.java
index d754345c79..a33ff6edc5 100644
--- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SandboxLifecycleMiddleware.java
+++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/middleware/SandboxLifecycleMiddleware.java
@@ -22,6 +22,7 @@
import io.agentscope.harness.agent.sandbox.SandboxContext;
import io.agentscope.harness.agent.sandbox.SandboxManager;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -55,6 +56,7 @@ public class SandboxLifecycleMiddleware implements HarnessRuntimeMiddleware {
private final SandboxBackedFilesystem filesystemProxy;
private final AtomicReference currentAcquireResult =
new AtomicReference<>();
+ private volatile Consumer beforeStartCallback;
public SandboxLifecycleMiddleware(
SandboxManager sandboxManager, SandboxBackedFilesystem filesystemProxy) {
@@ -62,6 +64,18 @@ public SandboxLifecycleMiddleware(
this.filesystemProxy = filesystemProxy;
}
+ /**
+ * Registers a callback that runs after the sandbox session is acquired but before
+ * {@link io.agentscope.harness.agent.sandbox.Sandbox#start()} applies workspace projection.
+ * This allows callers to materialise resources on the host workspace (e.g.
+ * {@code .skills-cache/}) so that projection picks them up in the same call.
+ *
+ * @param callback receives the per-call {@link RuntimeContext}; may be {@code null} to clear
+ */
+ public void setBeforeStartCallback(Consumer callback) {
+ this.beforeStartCallback = callback;
+ }
+
/**
* Acquires the sandbox for the current call. Called from
* {@code ReActAgent.beforeAgentExecution()} to ensure the sandbox is available
@@ -78,6 +92,18 @@ public void acquireForCall(RuntimeContext ctx) {
return;
}
try {
+ Consumer cb = beforeStartCallback;
+ if (cb != null) {
+ try {
+ cb.accept(ctx);
+ } catch (Exception e) {
+ log.warn(
+ "[sandbox-mw] beforeStartCallback failed; proceeding with sandbox"
+ + " start: {}",
+ e.getMessage(),
+ e);
+ }
+ }
SandboxAcquireResult result = sandboxManager.acquire(sandboxContext, ctx);
Sandbox sandbox = result.getSandbox();
try {
diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/SandboxLifecycleMiddlewareCallbackTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/SandboxLifecycleMiddlewareCallbackTest.java
new file mode 100644
index 0000000000..ef052147a5
--- /dev/null
+++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/middleware/SandboxLifecycleMiddlewareCallbackTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.harness.agent.middleware;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.agentscope.core.agent.RuntimeContext;
+import io.agentscope.core.skill.AgentSkill;
+import io.agentscope.core.skill.repository.AgentSkillRepository;
+import io.agentscope.core.skill.repository.AgentSkillRepositoryInfo;
+import io.agentscope.core.tool.Toolkit;
+import io.agentscope.harness.agent.skill.runtime.MarketplaceStager;
+import io.agentscope.harness.agent.skill.runtime.ShellPathPolicy;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Verifies that {@link HarnessSkillMiddleware#prestageMarketplaceSkills} materialises
+ * {@code .skills-cache/} on the host workspace so that sandbox projection can pick up
+ * database-backed skill resources before {@code sandbox.start()}.
+ */
+class SandboxLifecycleMiddlewareCallbackTest {
+
+ @TempDir Path tempWorkspace;
+
+ @Test
+ void prestageMarketplaceSkillsMaterialisesSkillsCache() throws IOException {
+ AgentSkill dbSkill =
+ new AgentSkill(
+ "db-tool",
+ "A database-sourced skill",
+ "skill content",
+ Map.of("run.sh", "#!/bin/bash\necho hello"),
+ "test-db");
+
+ StubSkillRepository dbRepo = new StubSkillRepository(List.of(dbSkill), "test-db");
+ MarketplaceStager stager = new MarketplaceStager(tempWorkspace);
+
+ HarnessSkillMiddleware middleware =
+ new HarnessSkillMiddleware(
+ List.of(dbRepo),
+ new Toolkit(),
+ null,
+ null,
+ stager,
+ ShellPathPolicy.noShell());
+
+ Path cacheDir = tempWorkspace.resolve(".skills-cache");
+ assertTrue(Files.notExists(cacheDir), ".skills-cache should not exist before prestage");
+
+ middleware.prestageMarketplaceSkills(RuntimeContext.empty());
+
+ assertTrue(Files.isDirectory(cacheDir), ".skills-cache should be created by prestage");
+ Path stagedScript = cacheDir.resolve("test-db").resolve("db-tool").resolve("run.sh");
+ assertTrue(Files.exists(stagedScript), "run.sh should be staged");
+ String content = Files.readString(stagedScript);
+ assertEquals("#!/bin/bash\necho hello", content);
+ }
+
+ @Test
+ void prestageIsIdempotent() {
+ AgentSkill skill =
+ new AgentSkill(
+ "idempotent-skill", "test", "content", Map.of("data.txt", "hello"), "src");
+
+ StubSkillRepository repo = new StubSkillRepository(List.of(skill), "src");
+ MarketplaceStager stager = new MarketplaceStager(tempWorkspace);
+
+ HarnessSkillMiddleware middleware =
+ new HarnessSkillMiddleware(
+ List.of(repo),
+ new Toolkit(),
+ null,
+ null,
+ stager,
+ ShellPathPolicy.noShell());
+
+ middleware.prestageMarketplaceSkills(RuntimeContext.empty());
+ middleware.prestageMarketplaceSkills(RuntimeContext.empty());
+
+ Path staged = tempWorkspace.resolve(".skills-cache/src/idempotent-skill/data.txt");
+ assertTrue(Files.exists(staged));
+ }
+
+ @Test
+ void prestageWithNullStagerIsNoOp() {
+ HarnessSkillMiddleware middleware =
+ new HarnessSkillMiddleware(
+ List.of(), new Toolkit(), null, null, null, ShellPathPolicy.noShell());
+
+ middleware.prestageMarketplaceSkills(RuntimeContext.empty());
+ assertTrue(Files.notExists(tempWorkspace.resolve(".skills-cache")));
+ }
+
+ @Test
+ void prestageCalledViaCallbackMaterialisesCache() {
+ AgentSkill skill =
+ new AgentSkill(
+ "callback-skill",
+ "test",
+ "content",
+ Map.of("tool.py", "print('hi')"),
+ "cb-src");
+
+ StubSkillRepository repo = new StubSkillRepository(List.of(skill), "cb-src");
+ MarketplaceStager stager = new MarketplaceStager(tempWorkspace);
+
+ HarnessSkillMiddleware skillMw =
+ new HarnessSkillMiddleware(
+ List.of(repo),
+ new Toolkit(),
+ null,
+ null,
+ stager,
+ ShellPathPolicy.noShell());
+
+ // Simulate the callback wiring done by HarnessAgent.Builder.build()
+ java.util.function.Consumer callback = skillMw::prestageMarketplaceSkills;
+ callback.accept(RuntimeContext.empty());
+
+ Path staged = tempWorkspace.resolve(".skills-cache/cb-src/callback-skill/tool.py");
+ assertTrue(
+ Files.exists(staged),
+ ".skills-cache should be populated by the callback before sandbox.start()");
+ }
+
+ /** Minimal repository stub that returns a fixed skill list. */
+ private static final class StubSkillRepository implements AgentSkillRepository {
+
+ private final List skills;
+ private final String source;
+
+ StubSkillRepository(List skills, String source) {
+ this.skills = skills;
+ this.source = source;
+ }
+
+ @Override
+ public AgentSkill getSkill(String name) {
+ return skills.stream().filter(s -> s.getName().equals(name)).findFirst().orElse(null);
+ }
+
+ @Override
+ public List getAllSkillNames() {
+ return skills.stream().map(AgentSkill::getName).toList();
+ }
+
+ @Override
+ public List getAllSkills() {
+ return skills;
+ }
+
+ @Override
+ public boolean save(List skills, boolean force) {
+ return false;
+ }
+
+ @Override
+ public boolean delete(String skillName) {
+ return false;
+ }
+
+ @Override
+ public boolean skillExists(String skillName) {
+ return skills.stream().anyMatch(s -> s.getName().equals(skillName));
+ }
+
+ @Override
+ public AgentSkillRepositoryInfo getRepositoryInfo() {
+ return new AgentSkillRepositoryInfo(source, "", false);
+ }
+
+ @Override
+ public String getSource() {
+ return source;
+ }
+
+ @Override
+ public void setWriteable(boolean writeable) {}
+
+ @Override
+ public boolean isWriteable() {
+ return false;
+ }
+ }
+}