Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2779,6 +2779,7 @@ public HarnessAgent build() {
visibilityFilter,
stager,
shellPolicy);
skillMiddleware.isolationScope(fsIsolationScope);
inner.middleware(skillMiddleware);

// Harness owns both the live and frozen repository paths.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.agentscope.core.skill.SkillFilter;
import io.agentscope.core.skill.repository.AgentSkillRepository;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.harness.agent.IsolationScope;
import io.agentscope.harness.agent.skill.LazyResourceCapable;
import io.agentscope.harness.agent.skill.RuntimeContextSkillRepository;
import io.agentscope.harness.agent.skill.SkillResources;
Expand Down Expand Up @@ -81,6 +82,36 @@ public class HarnessSkillMiddleware implements HarnessRuntimeMiddleware {
private final SkillRuntime runtime;
private final Map<AgentSkillRepository, String> sourceNamespaces;
private final Map<String, RepoBound> frozenSkills;
private IsolationScope isolationScope;

/**
* Per-call cache scope, mirroring the identity {@link IsolationScope} already applies to
* runtime data. Calls that share a scope share a {@code .skills-cache} subtree, and only
* those calls can sweep it — which is what makes one call's visible-skill white-list
* authoritative for everything the sweep can reach.
*/
private String scopeKeyFor(RuntimeContext ctx) {
IsolationScope scope = isolationScope != null ? isolationScope : IsolationScope.USER;
return switch (scope) {
case USER -> {
String uid = ctx != null ? ctx.getUserId() : null;
if (uid != null && !uid.isBlank()) {
yield uid;
}
// Mirrors IsolationScope.USER's documented fall back to the session identity.
// null means "no identity to key on" and is distinct from an identity that
// happens to be spelled like the stager's shared bucket.
String sid = ctx != null ? ctx.getSessionId() : null;
yield sid != null && !sid.isBlank() ? sid : null;
}
case SESSION -> {
String sid = ctx != null ? ctx.getSessionId() : null;
yield sid != null && !sid.isBlank() ? sid : null;
}
// The workspace is already per-agent, so these need no further separation.
case AGENT, GLOBAL -> null;
};
}

public HarnessSkillMiddleware(List<AgentSkillRepository> repositories, Toolkit toolkit) {
this(repositories, toolkit, null, null, null, ShellPathPolicy.noShell());
Expand Down Expand Up @@ -169,6 +200,7 @@ private HarnessSkillMiddleware(
this.stager = stager;
this.shellPathPolicy =
shellPathPolicy != null ? shellPathPolicy : ShellPathPolicy.noShell();
this.isolationScope = IsolationScope.USER;
this.runtime = new SkillRuntime();
// Pre-resolve source namespaces once at build time. The compose order is fixed for
// the lifetime of the middleware, so this is safe and avoids repeated work per call.
Expand All @@ -186,6 +218,15 @@ public SkillRuntime runtime() {
return runtime;
}

/**
* Overrides the isolation dimension used to separate {@code .skills-cache} subtrees.
* Defaults to {@link IsolationScope#USER}, matching the default for runtime data.
*/
public HarnessSkillMiddleware isolationScope(IsolationScope scope) {
this.isolationScope = scope;
return this;
}

/** Whether repository enumeration is frozen to the construction-time snapshot. */
public boolean isFrozen() {
return frozenSkills != null;
Expand Down Expand Up @@ -213,7 +254,7 @@ public void prestageMarketplaceSkills(RuntimeContext ctx) {
List<RepoBound> visible = applyVisibility(merged.values(), ctx);
List<RepoBound> enabled = applySkillFilter(visible, effectiveFilter(ctx));
if (!enabled.isEmpty()) {
stager.stage(enabled, sourceNamespaces);
stager.stage(enabled, sourceNamespaces, scopeKeyFor(ctx));
}
}

Expand All @@ -239,7 +280,9 @@ public Mono<String> onSystemPrompt(Agent agent, RuntimeContext ctx, String curre
}

Map<String, StageResult> staged =
stager != null ? stager.stage(enabled, sourceNamespaces) : Map.of();
stager != null
? stager.stage(enabled, sourceNamespaces, scopeKeyFor(ctx))
: Map.of();

List<HarnessSkillEntry> entries = new ArrayList<>(enabled.size());
for (RepoBound bound : enabled) {
Expand Down
Loading
Loading