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.PathContainmentRule
import com.correx.infrastructure.InfrastructureModule
import com.correx.infrastructure.artifactscas.DefaultMaterializingArtifactWriter
import com.correx.infrastructure.inference.DefaultProviderRegistry
import com.correx.infrastructure.inference.FirstAvailableRoutingStrategy
import com.correx.infrastructure.inference.llama.cpp.LlamaCppInferenceProvider
@@ -92,10 +91,13 @@ fun main() {
?: toolsConfig.workingDir.takeIf { it.isNotEmpty() }?.let { Path.of(it) }
?: Path.of("").toAbsolutePath()
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(
buildToolConfig(
artifactStore,
sandboxRoot,
workspaceRoot,
workingDir,
shellAllowedExecutables,
toolsConfig,
@@ -110,10 +112,6 @@ fun main() {
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 workspacePolicy = WorkspacePolicy(workspaceRoot, privilegedLocations)
val toolCallAssessor = ToolCallAssessor(
@@ -297,36 +295,39 @@ private fun buildRepositories(
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(
artifactStore: ArtifactStore,
sandboxRoot: Path,
workspaceRoot: Path,
workingDir: Path,
shellAllowedExecutables: Set<String>,
toolsConfig: com.correx.core.config.ToolsConfig,
): ToolConfig = ToolConfig(
shell = ShellConfig(
enabled = toolsConfig.shellEnabled,
allowedExecutables = shellAllowedExecutables,
workingDir = workingDir,
),
fileRead = FileReadConfig(
enabled = toolsConfig.fileReadEnabled,
allowedPaths = setOf(sandboxRoot, workingDir),
),
fileWrite = FileWriteConfig(
allowedPaths = setOf(sandboxRoot, workingDir),
enabled = toolsConfig.fileWriteEnabled,
artifactStore = artifactStore,
materializingWriter = DefaultMaterializingArtifactWriter(),
sandboxRoot = sandboxRoot,
workingDir = workingDir,
),
fileEdit = FileEditConfig(
enabled = toolsConfig.fileEditEnabled,
allowedPaths = setOf(sandboxRoot, workingDir),
workingDir = workingDir,
),
)
): ToolConfig {
val allowed = setOf(workspaceRoot, workingDir)
return ToolConfig(
shell = ShellConfig(
enabled = toolsConfig.shellEnabled,
allowedExecutables = shellAllowedExecutables,
workingDir = workingDir,
),
fileRead = FileReadConfig(
enabled = toolsConfig.fileReadEnabled,
allowedPaths = allowed,
),
fileWrite = FileWriteConfig(
allowedPaths = allowed,
enabled = toolsConfig.fileWriteEnabled,
workingDir = workingDir,
),
fileEdit = FileEditConfig(
enabled = toolsConfig.fileEditEnabled,
allowedPaths = allowed,
workingDir = workingDir,
),
)
}
private fun DefaultProviderRegistry.asServerRegistry(): ProviderRegistry = object : ProviderRegistry {
override fun listAll() = this@asServerRegistry.listAll()
@@ -1,11 +1,6 @@
package com.correx.infrastructure.tools.filesystem
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.types.ToolInvocationId
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.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
@@ -34,16 +26,9 @@ import java.nio.file.Paths
class FileWriteTool(
allowedPaths: Set<Path> = emptySet(),
private val artifactStore: ArtifactStore? = null,
private val materializingWriter: MaterializingArtifactWriter? = null,
private val sandboxRoot: Path? = null,
private val workingDir: Path? = null,
) : 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()
override val name: String = "file_write"
@@ -174,7 +159,6 @@ class FileWriteTool(
withContext(Dispatchers.IO) {
AtomicFileWriter.write(path, content.toByteArray(Charsets.UTF_8))
}
storeArtifact(pathString, content, request.parameters["mode"] as? String ?: "0644")
ToolResult.Success(
invocationId = request.invocationId,
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(
e: Throwable,
invocationId: ToolInvocationId,
@@ -1,7 +1,5 @@
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.infrastructure.tools.filesystem.FileEditTool
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(
val enabled: Boolean = false,
val allowedPaths: Set<Path> = emptySet(),
val artifactStore: ArtifactStore? = null,
val materializingWriter: MaterializingArtifactWriter? = null,
val sandboxRoot: Path? = null,
val workingDir: Path? = null,
)
@@ -61,9 +56,6 @@ fun ToolConfig.buildTools(): List<Tool> = buildList {
add(
FileWriteTool(
allowedPaths = fileWrite.allowedPaths,
artifactStore = fileWrite.artifactStore,
materializingWriter = fileWrite.materializingWriter,
sandboxRoot = fileWrite.sandboxRoot,
workingDir = fileWrite.workingDir,
),
)