refactor: retire sandbox dual-write; file tools write in-place into workspace

This commit is contained in:
2026-05-31 10:39:58 +04:00
parent 3c90ac4aef
commit 95e79bffb8
3 changed files with 34 additions and 77 deletions
@@ -44,7 +44,6 @@ import com.correx.core.toolintent.rules.ExecInterpreterRule
import com.correx.core.toolintent.rules.NetworkHostRule import com.correx.core.toolintent.rules.NetworkHostRule
import com.correx.core.toolintent.rules.PathContainmentRule import com.correx.core.toolintent.rules.PathContainmentRule
import com.correx.infrastructure.InfrastructureModule import com.correx.infrastructure.InfrastructureModule
import com.correx.infrastructure.artifactscas.DefaultMaterializingArtifactWriter
import com.correx.infrastructure.inference.DefaultProviderRegistry import com.correx.infrastructure.inference.DefaultProviderRegistry
import com.correx.infrastructure.inference.FirstAvailableRoutingStrategy import com.correx.infrastructure.inference.FirstAvailableRoutingStrategy
import com.correx.infrastructure.inference.llama.cpp.LlamaCppInferenceProvider import com.correx.infrastructure.inference.llama.cpp.LlamaCppInferenceProvider
@@ -92,10 +91,13 @@ fun main() {
?: toolsConfig.workingDir.takeIf { it.isNotEmpty() }?.let { Path.of(it) } ?: toolsConfig.workingDir.takeIf { it.isNotEmpty() }?.let { Path.of(it) }
?: Path.of("").toAbsolutePath() ?: Path.of("").toAbsolutePath()
val shellAllowedExecutables = toolsConfig.shellAllowedExecutables.toSet() val shellAllowedExecutables = toolsConfig.shellAllowedExecutables.toSet()
val workspaceRoot = System.getenv("CORREX_WORKSPACE_ROOT")
?.let { Path.of(it) }
?: toolsConfig.workspaceRoot.takeIf { it.isNotEmpty() }?.let { Path.of(it) }
?: workingDir
val toolRegistry = InfrastructureModule.createToolRegistry( val toolRegistry = InfrastructureModule.createToolRegistry(
buildToolConfig( buildToolConfig(
artifactStore, workspaceRoot,
sandboxRoot,
workingDir, workingDir,
shellAllowedExecutables, shellAllowedExecutables,
toolsConfig, toolsConfig,
@@ -110,10 +112,6 @@ fun main() {
logToolInfo(sandboxRoot, workingDir, shellAllowedExecutables, toolRegistry) logToolInfo(sandboxRoot, workingDir, shellAllowedExecutables, toolRegistry)
val workspaceRoot = System.getenv("CORREX_WORKSPACE_ROOT")
?.let { Path.of(it) }
?: toolsConfig.workspaceRoot.takeIf { it.isNotEmpty() }?.let { Path.of(it) }
?: workingDir
val privilegedLocations = toolsConfig.privilegedLocations.map { Path.of(it) } val privilegedLocations = toolsConfig.privilegedLocations.map { Path.of(it) }
val workspacePolicy = WorkspacePolicy(workspaceRoot, privilegedLocations) val workspacePolicy = WorkspacePolicy(workspaceRoot, privilegedLocations)
val toolCallAssessor = ToolCallAssessor( val toolCallAssessor = ToolCallAssessor(
@@ -297,36 +295,39 @@ private fun buildRepositories(
approvalRepository = InfrastructureModule.createApprovalRepository(eventStore), approvalRepository = InfrastructureModule.createApprovalRepository(eventStore),
) )
// File tools write IN PLACE into the workspace (cwd / allow-listed dirs). The old
// sandboxRoot dual-write is retired; reversibility is event-sourced (FileWrittenEvent +
// CAS pre/post images, captured by SandboxedToolExecutor). sandboxRoot survives only as
// the executor's scratch workDir, never as a write destination.
private fun buildToolConfig( private fun buildToolConfig(
artifactStore: ArtifactStore, workspaceRoot: Path,
sandboxRoot: Path,
workingDir: Path, workingDir: Path,
shellAllowedExecutables: Set<String>, shellAllowedExecutables: Set<String>,
toolsConfig: com.correx.core.config.ToolsConfig, toolsConfig: com.correx.core.config.ToolsConfig,
): ToolConfig = ToolConfig( ): ToolConfig {
shell = ShellConfig( val allowed = setOf(workspaceRoot, workingDir)
enabled = toolsConfig.shellEnabled, return ToolConfig(
allowedExecutables = shellAllowedExecutables, shell = ShellConfig(
workingDir = workingDir, enabled = toolsConfig.shellEnabled,
), allowedExecutables = shellAllowedExecutables,
fileRead = FileReadConfig( workingDir = workingDir,
enabled = toolsConfig.fileReadEnabled, ),
allowedPaths = setOf(sandboxRoot, workingDir), fileRead = FileReadConfig(
), enabled = toolsConfig.fileReadEnabled,
fileWrite = FileWriteConfig( allowedPaths = allowed,
allowedPaths = setOf(sandboxRoot, workingDir), ),
enabled = toolsConfig.fileWriteEnabled, fileWrite = FileWriteConfig(
artifactStore = artifactStore, allowedPaths = allowed,
materializingWriter = DefaultMaterializingArtifactWriter(), enabled = toolsConfig.fileWriteEnabled,
sandboxRoot = sandboxRoot, workingDir = workingDir,
workingDir = workingDir, ),
), fileEdit = FileEditConfig(
fileEdit = FileEditConfig( enabled = toolsConfig.fileEditEnabled,
enabled = toolsConfig.fileEditEnabled, allowedPaths = allowed,
allowedPaths = setOf(sandboxRoot, workingDir), workingDir = workingDir,
workingDir = workingDir, ),
), )
) }
private fun DefaultProviderRegistry.asServerRegistry(): ProviderRegistry = object : ProviderRegistry { private fun DefaultProviderRegistry.asServerRegistry(): ProviderRegistry = object : ProviderRegistry {
override fun listAll() = this@asServerRegistry.listAll() override fun listAll() = this@asServerRegistry.listAll()
@@ -1,11 +1,6 @@
package com.correx.infrastructure.tools.filesystem package com.correx.infrastructure.tools.filesystem
import com.correx.core.approvals.Tier import com.correx.core.approvals.Tier
import com.correx.core.artifacts.MaterializationResult
import com.correx.core.artifacts.MaterializingArtifactWriter
import com.correx.core.artifacts.kind.FileWrittenArtifact
import com.correx.core.artifacts.kind.FileWrittenPayload
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.events.ToolRequest import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.ToolInvocationId import com.correx.core.events.types.ToolInvocationId
import com.correx.core.tools.contract.FileAffectingTool import com.correx.core.tools.contract.FileAffectingTool
@@ -17,15 +12,12 @@ import com.correx.core.tools.contract.ValidationResult
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.InvalidPathException import java.nio.file.InvalidPathException
@@ -34,16 +26,9 @@ import java.nio.file.Paths
class FileWriteTool( class FileWriteTool(
allowedPaths: Set<Path> = emptySet(), allowedPaths: Set<Path> = emptySet(),
private val artifactStore: ArtifactStore? = null,
private val materializingWriter: MaterializingArtifactWriter? = null,
private val sandboxRoot: Path? = null,
private val workingDir: Path? = null, private val workingDir: Path? = null,
) : Tool, FileAffectingTool, ToolExecutor { ) : Tool, FileAffectingTool, ToolExecutor {
private companion object {
val log: Logger = LoggerFactory.getLogger(FileWriteTool::class.java)
}
private val normalizedAllowedPaths: Set<Path> = allowedPaths.map { it.normalize().toAbsolutePath() }.toSet() private val normalizedAllowedPaths: Set<Path> = allowedPaths.map { it.normalize().toAbsolutePath() }.toSet()
override val name: String = "file_write" override val name: String = "file_write"
@@ -174,7 +159,6 @@ class FileWriteTool(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
AtomicFileWriter.write(path, content.toByteArray(Charsets.UTF_8)) AtomicFileWriter.write(path, content.toByteArray(Charsets.UTF_8))
} }
storeArtifact(pathString, content, request.parameters["mode"] as? String ?: "0644")
ToolResult.Success( ToolResult.Success(
invocationId = request.invocationId, invocationId = request.invocationId,
output = "File written successfully to $pathString", output = "File written successfully to $pathString",
@@ -206,26 +190,6 @@ class FileWriteTool(
) )
} }
@Suppress("ReturnCount")
private suspend fun storeArtifact(pathString: String, content: String, mode: String) {
val writer = materializingWriter ?: return
val store = artifactStore ?: return
val root = sandboxRoot ?: return
val payload = FileWrittenPayload(path = pathString, content = content, mode = mode)
when (val result = writer.materialize(payload, root)) {
is MaterializationResult.Success -> {
val canonicalBytes = Json.encodeToString(
FileWrittenArtifact.serializer(),
result.artifact,
).toByteArray(Charsets.UTF_8)
store.put(canonicalBytes)
}
is MaterializationResult.Failure ->
log.warn("[FileWriteTool] artifact materialization failed for {}: {}", pathString, result.reason)
}
}
private fun handleExecutionException( private fun handleExecutionException(
e: Throwable, e: Throwable,
invocationId: ToolInvocationId, invocationId: ToolInvocationId,
@@ -1,7 +1,5 @@
package com.correx.infrastructure.tools package com.correx.infrastructure.tools
import com.correx.core.artifacts.MaterializingArtifactWriter
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.tools.contract.Tool import com.correx.core.tools.contract.Tool
import com.correx.infrastructure.tools.filesystem.FileEditTool import com.correx.infrastructure.tools.filesystem.FileEditTool
import com.correx.infrastructure.tools.filesystem.FileReadTool import com.correx.infrastructure.tools.filesystem.FileReadTool
@@ -20,9 +18,6 @@ data class FileReadConfig(val enabled: Boolean = false, val allowedPaths: Set<Pa
data class FileWriteConfig( data class FileWriteConfig(
val enabled: Boolean = false, val enabled: Boolean = false,
val allowedPaths: Set<Path> = emptySet(), val allowedPaths: Set<Path> = emptySet(),
val artifactStore: ArtifactStore? = null,
val materializingWriter: MaterializingArtifactWriter? = null,
val sandboxRoot: Path? = null,
val workingDir: Path? = null, val workingDir: Path? = null,
) )
@@ -61,9 +56,6 @@ fun ToolConfig.buildTools(): List<Tool> = buildList {
add( add(
FileWriteTool( FileWriteTool(
allowedPaths = fileWrite.allowedPaths, allowedPaths = fileWrite.allowedPaths,
artifactStore = fileWrite.artifactStore,
materializingWriter = fileWrite.materializingWriter,
sandboxRoot = fileWrite.sandboxRoot,
workingDir = fileWrite.workingDir, workingDir = fileWrite.workingDir,
), ),
) )