feat(workspace): WorkspaceResolver trust pipeline + allowed_workspace_roots

Resolve a client-supplied workspace dir safely: canonicalize via toRealPath,
reject privileged locations, clamp to [tools] allowed_workspace_roots (permissive
when unset, logged), fall back to the boot default on any rejection. PathJail made
non-internal so the server can reuse its symlink-safe containment. (Axis 2 Phase A, task 4.)
This commit is contained in:
2026-06-02 19:57:03 +04:00
parent d405f9d2c1
commit 7d7e524756
6 changed files with 226 additions and 1 deletions
@@ -331,6 +331,7 @@ object ConfigLoader {
.ifEmpty { ToolsConfig.DEFAULT_INTERPRETERS },
networkAllowedHosts = asStringList(toolsSection["network_allowed_hosts"]),
networkDeniedHosts = asStringList(toolsSection["network_denied_hosts"]),
allowedWorkspaceRoots = asStringList(toolsSection["allowed_workspace_roots"]),
)
val providers = providersList.mapNotNull { providerMap ->
@@ -61,6 +61,7 @@ data class ToolsConfig(
val interpreterExecutables: List<String> = DEFAULT_INTERPRETERS,
val networkAllowedHosts: List<String> = emptyList(),
val networkDeniedHosts: List<String> = emptyList(),
val allowedWorkspaceRoots: List<String> = emptyList(),
) {
companion object {
val DEFAULT_PRIVILEGED_LOCATIONS: List<String> = listOf(
@@ -12,6 +12,7 @@ class ConfigLoaderTest {
assertEquals("dark", config.tui.theme)
assertEquals(5, config.tui.sessionListLimit)
assertEquals("human", config.cli.defaultOutput)
assertEquals(emptyList<String>(), config.tools.allowedWorkspaceRoots)
}
@Test
@@ -398,4 +399,20 @@ class ConfigLoaderTest {
assertEquals("/models/local.gguf", result.models[0].modelPath)
assertEquals(8192, result.models[0].contextSize)
}
@Test
fun `parseToml parses tools allowed_workspace_roots`() {
val toml = """
[tools]
allowed_workspace_roots = ["/home/user/projects", "/tmp/work"]
""".trimIndent()
val parseTomlMethod = ConfigLoader::class.java.getDeclaredMethod("parseToml", String::class.java)
parseTomlMethod.isAccessible = true
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
assertEquals(2, result.tools.allowedWorkspaceRoots.size)
assertEquals("/home/user/projects", result.tools.allowedWorkspaceRoots[0])
assertEquals("/tmp/work", result.tools.allowedWorkspaceRoots[1])
}
}