Skip to content
Open
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 @@ -80,14 +80,21 @@ public void delete(SandboxIsolationKey key) throws IOException {
* {@link AgentStateStore} 2-arg slot model. The userId column is always {@code null} because
* sandbox state is conceptually agent-scoped, not user-scoped: USER/AGENT/GLOBAL scopes are
* encoded into the sessionId prefix rather than the userId slot.
*
* <p>Uses {@code :} rather than {@code /} as the segment separator: SQL-backed
* {@link AgentStateStore} implementations validate the sessionId and reject {@code /} and
* {@code \} as path separators (e.g. {@code PostgresAgentStateStore}/{@code MysqlAgentStateStore}
* {@code validateSessionId}), so a {@code /}-delimited slot id makes sandbox state persistence
* throw {@code IllegalArgumentException} on those stores under any isolation scope. {@code :} is
* the same separator {@code MysqlAgentStateStore#slotId} already relies on for that reason.
*/
private String slotSessionId(SandboxIsolationKey key) {
IsolationScope scope = key.getScope();
return switch (scope) {
case SESSION -> "sandbox/session/" + key.getValue();
case USER -> "sandbox/user/" + agentId + "/" + key.getValue();
case AGENT -> "sandbox/agent/" + agentId;
case GLOBAL -> "sandbox/global";
case SESSION -> "sandbox:session:" + key.getValue();
case USER -> "sandbox:user:" + agentId + ":" + key.getValue();
case AGENT -> "sandbox:agent:" + agentId;
case GLOBAL -> "sandbox:global";
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.state.InMemoryAgentStateStore;
import io.agentscope.core.state.State;
import io.agentscope.harness.agent.IsolationScope;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -71,6 +73,28 @@ void agentAndGlobalScope_doNotInterfere() throws Exception {
assertEquals("{\"scope\":\"global\"}", store.load(globalKey).orElseThrow());
}

@Test
void slotSessionId_hasNoPathSeparators_soSqlStoresAccept_allScopes() throws Exception {
// SQL-backed AgentStateStore impls (Postgres/MySQL) reject '/' and '\' in the sessionId
// via validateSessionId. A store whose slot ids contain path separators makes sandbox
// state persistence throw on those backends for every isolation scope (issue #2327).
SessionSandboxStateStore sqlLikeStore =
new SessionSandboxStateStore(new PathSeparatorRejectingStore(), AGENT_ID);

for (SandboxIsolationKey key :
new SandboxIsolationKey[] {
isolationKey(IsolationScope.SESSION, "sess-001"),
isolationKey(IsolationScope.USER, "user-123"),
isolationKey(IsolationScope.AGENT, AGENT_ID),
isolationKey(IsolationScope.GLOBAL, SandboxIsolationKey.GLOBAL_VALUE)
}) {
sqlLikeStore.save(key, JSON);
assertEquals(JSON, sqlLikeStore.load(key).orElseThrow());
sqlLikeStore.delete(key);
assertFalse(sqlLikeStore.load(key).isPresent());
}
}

@Test
void deleteUsesTombstone_evenWhenSessionDeleteUnsupported() throws Exception {
SessionSandboxStateStore redisLikeStore =
Expand Down Expand Up @@ -106,4 +130,37 @@ public void delete(String userId, String sessionId, String key) {
// no-op
}
}

/**
* Mirrors the {@code validateSessionId} contract of the SQL-backed stores
* ({@code PostgresAgentStateStore}/{@code MysqlAgentStateStore}): the sessionId must not contain
* a path separator. Lets the round-trip test fail exactly the way those backends do.
*/
private static final class PathSeparatorRejectingStore extends InMemoryAgentStateStore {
private static void validate(String sessionId) {
if (sessionId != null && (sessionId.contains("/") || sessionId.contains("\\"))) {
throw new IllegalArgumentException(
"AgentStateStore ID cannot contain path separators: " + sessionId);
}
}

@Override
public void save(String userId, String sessionId, String key, State value) {
validate(sessionId);
super.save(userId, sessionId, key, value);
}

@Override
public <T extends State> Optional<T> get(
String userId, String sessionId, String key, Class<T> type) {
validate(sessionId);
return super.get(userId, sessionId, key, type);
}

@Override
public void delete(String userId, String sessionId, String key) {
validate(sessionId);
super.delete(userId, sessionId, key);
}
}
}
Loading