mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
1. Relative parent traversal in shell commands (HIGH) validateShellCommand only scanned absolute path tokens, so commands like `cat ../mateclaw/CLAUDE.md`, `cd .. && cat foo`, or `ln -sf ../bar breakout` had no absolute path to trip the check. From a workspace cwd that's a real escape — `..` segments resolve against the JVM cwd at file-tool time and reach anywhere the user can read. Add a second pass: any token containing `..` as a path segment is resolved against the workspace root via root.resolve(token). normalize(); reject when the result falls outside. In-workspace traversal like `subdir/../sibling` normalizes back inside and passes. Identifiers without slashes (e.g. version strings with `1.2..3`) are not treated as paths. 2. Shell validation and process working directory used different context sources (MEDIUM) execute_shell_command validated with the explicit ToolContext, but buildShellProcess called WorkspacePathGuard.getWorkingDirectory() (no-arg), which only sees the ThreadLocal fallback. Today the ToolExecutionExecutor sets both so the discrepancy is latent, but a future direct Spring AI invocation passing only ToolContext would validate against one basePath and exec against another. Thread ctx through buildShellProcess and call getWorkingDirectory(ctx) so validation and execution agree on a single source of truth. 3. Absolute agent override could disable workspace scoping (MEDIUM) resolveAgentBasePath accepted an absolute override verbatim, even when it pointed outside the workspace root. An admin (or any account with agent-edit permission) could set workspaceBasePath="/" or another team's repo and bypass workspace boundaries entirely. When a workspace has its own basePath, require absolute overrides to sit underneath it. The caller in build() catches the rejection, logs WARN, and falls back to the workspace basePath so chat stays available rather than crashing agent construction. When the workspace has no basePath there's no boundary to enforce, so legacy behavior is preserved. Test coverage: WorkspacePathGuardShellTest grows from 17 to 23 (six new cases for `cd ..`, relative parent traversal, relative symlink escape, deeper traversal, in-workspace normalization, and the identifier false-positive guard). AgentGraphBuilderBasePathResolutionTest grows from 7 to 10 (three new cases for in-workspace absolute, outside-workspace absolute rejection, and no-workspace legacy behavior). All 45 sandbox-area tests pass with no regressions.
123 lines
5.5 KiB
Java
123 lines
5.5 KiB
Java
package vip.mate.agent;
|
|
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.condition.DisabledOnOs;
|
|
import org.junit.jupiter.api.condition.EnabledOnOs;
|
|
import org.junit.jupiter.api.condition.OS;
|
|
|
|
import java.nio.file.Paths;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
|
/**
|
|
* Verifies the agent-vs-workspace basePath precedence rules used by
|
|
* {@link AgentGraphBuilder#resolveAgentBasePath(String, String)}.
|
|
*
|
|
* <p>The UI advertises agent-level paths as "relative to the workspace root",
|
|
* so a relative agent override must compose with the workspace basePath rather
|
|
* than fall through to the JVM working directory.
|
|
*/
|
|
class AgentGraphBuilderBasePathResolutionTest {
|
|
|
|
@Test
|
|
@DisplayName("Both null/blank → null (no working directory configured)")
|
|
void noOverrideNoWorkspace_returnsNull() {
|
|
assertNull(AgentGraphBuilder.resolveAgentBasePath(null, null));
|
|
assertNull(AgentGraphBuilder.resolveAgentBasePath("", ""));
|
|
assertNull(AgentGraphBuilder.resolveAgentBasePath(" ", null));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("No agent override → workspace basePath inherited verbatim")
|
|
void noOverride_inheritsWorkspace() {
|
|
assertEquals("/srv/ws-root",
|
|
AgentGraphBuilder.resolveAgentBasePath(null, "/srv/ws-root"));
|
|
assertEquals("/srv/ws-root",
|
|
AgentGraphBuilder.resolveAgentBasePath("", "/srv/ws-root"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Agent override is absolute and inside workspace → used verbatim")
|
|
@DisabledOnOs(OS.WINDOWS)
|
|
void absoluteOverride_insideWs_usedAsIs_unix() {
|
|
assertEquals("/srv/ws-root/agents/code-review",
|
|
AgentGraphBuilder.resolveAgentBasePath("/srv/ws-root/agents/code-review", "/srv/ws-root"));
|
|
assertEquals("/opt/agents/code-review",
|
|
AgentGraphBuilder.resolveAgentBasePath("/opt/agents/code-review", null));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Agent override is absolute (Windows) and inside workspace → used verbatim")
|
|
@EnabledOnOs(OS.WINDOWS)
|
|
void absoluteOverride_insideWs_usedAsIs_windows() {
|
|
assertEquals("C:\\ws-root\\agents\\code-review",
|
|
AgentGraphBuilder.resolveAgentBasePath("C:\\ws-root\\agents\\code-review", "C:\\ws-root"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Relative agent override + workspace basePath → resolved under workspace")
|
|
void relativeOverride_resolvedUnderWorkspace() {
|
|
String expected = Paths.get("/srv/ws-root").resolve("projects/code-review").toString();
|
|
assertEquals(expected,
|
|
AgentGraphBuilder.resolveAgentBasePath("projects/code-review", "/srv/ws-root"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Relative agent override with no workspace → used verbatim (legacy fallback)")
|
|
void relativeOverride_noWorkspace_usedAsIs() {
|
|
assertEquals("projects/code-review",
|
|
AgentGraphBuilder.resolveAgentBasePath("projects/code-review", null));
|
|
assertEquals("projects/code-review",
|
|
AgentGraphBuilder.resolveAgentBasePath("projects/code-review", ""));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Blank workspace basePath treated like null when override is relative")
|
|
void relativeOverride_blankWorkspace_usedAsIs() {
|
|
assertEquals("agent-dir",
|
|
AgentGraphBuilder.resolveAgentBasePath("agent-dir", " "));
|
|
}
|
|
|
|
// ==================== Absolute override scoped to workspace root ====================
|
|
|
|
@Test
|
|
@DisplayName("Absolute override inside workspace root → allowed verbatim")
|
|
@DisabledOnOs(OS.WINDOWS)
|
|
void absoluteOverride_insideWorkspace_allowed() {
|
|
// /srv/ws-root/agents/code-review starts with /srv/ws-root → fine.
|
|
assertEquals("/srv/ws-root/agents/code-review",
|
|
AgentGraphBuilder.resolveAgentBasePath("/srv/ws-root/agents/code-review", "/srv/ws-root"));
|
|
// Identical to workspace root → trivially allowed.
|
|
assertEquals("/srv/ws-root",
|
|
AgentGraphBuilder.resolveAgentBasePath("/srv/ws-root", "/srv/ws-root"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Absolute override outside workspace root → rejected (workspace-scoping bypass)")
|
|
@DisabledOnOs(OS.WINDOWS)
|
|
void absoluteOverride_outsideWorkspace_rejected() {
|
|
// A less-trusted admin could set / or another repo and bypass scoping —
|
|
// reject it so the override stays inside the team workspace.
|
|
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () ->
|
|
AgentGraphBuilder.resolveAgentBasePath("/etc", "/srv/ws-root"));
|
|
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () ->
|
|
AgentGraphBuilder.resolveAgentBasePath("/", "/srv/ws-root"));
|
|
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () ->
|
|
AgentGraphBuilder.resolveAgentBasePath("/Users/admin/other-project",
|
|
"/srv/ws-root"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Absolute override with no workspace basePath → used verbatim (legacy)")
|
|
@DisabledOnOs(OS.WINDOWS)
|
|
void absoluteOverride_noWorkspace_allowed() {
|
|
// No workspace boundary to enforce — fall back to legacy behavior.
|
|
assertEquals("/Users/admin/scratch",
|
|
AgentGraphBuilder.resolveAgentBasePath("/Users/admin/scratch", null));
|
|
assertEquals("/Users/admin/scratch",
|
|
AgentGraphBuilder.resolveAgentBasePath("/Users/admin/scratch", ""));
|
|
}
|
|
}
|