feat(tools): add description and parametersSchema to Tool interface; wire FileWriteTool materialization

This commit is contained in:
2026-05-18 21:39:36 +04:00
parent 24df3b55b3
commit 15d1e09c44
12 changed files with 105 additions and 3 deletions
+2
View File
@@ -10,4 +10,6 @@ dependencies {
implementation project(":core:approvals")
implementation project(":core:sessions")
implementation project(":infrastructure:tools:filesystem")
implementation project(":core:artifacts")
implementation project(":core:artifacts-store")
}
@@ -8,4 +8,7 @@ dependencies {
implementation project(':core:tools')
implementation project(':core:events')
implementation project(':core:approvals')
implementation project(':core:artifacts')
implementation project(':core:artifacts-store')
implementation 'org.slf4j:slf4j-api:2.0.16'
}
@@ -12,6 +12,8 @@ import com.correx.core.tools.contract.ValidationResult
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
@@ -26,6 +28,8 @@ class FileEditTool(
private val normalizedAllowedPaths: Set<Path> = allowedPaths.map { it.normalize().toAbsolutePath() }.toSet()
override val name: String = "file_edit"
override val description: String = "Edit content in a file at the specified path"
override val parametersSchema: JsonObject = buildJsonObject {}
override val tier: Tier = Tier.T3
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE)
@@ -10,6 +10,8 @@ import com.correx.core.tools.contract.ValidationResult
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
@@ -22,6 +24,8 @@ class FileReadTool(
private val normalizedAllowedPaths: Set<Path> = allowedPaths.map { it.normalize().toAbsolutePath() }.toSet()
override val name: String = "file_read"
override val description: String = "Read content from a file at the specified path"
override val parametersSchema: JsonObject = buildJsonObject {}
override val tier: Tier = Tier.T1
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_READ)
@@ -1,6 +1,11 @@
package com.correx.infrastructure.tools.filesystem
import com.correx.core.approvals.Tier
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.artifacts.MaterializingArtifactWriter
import com.correx.core.artifacts.kind.FileWrittenArtifact
import com.correx.core.artifacts.kind.FileWrittenPayload
import com.correx.core.artifacts.MaterializationResult
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.tools.contract.FileAffectingTool
@@ -12,6 +17,14 @@ 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.LoggerFactory
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
@@ -20,10 +33,32 @@ 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,
) : Tool, FileAffectingTool, ToolExecutor {
private companion object {
val log = LoggerFactory.getLogger(FileWriteTool::class.java)
}
private val normalizedAllowedPaths: Set<Path> = allowedPaths.map { it.normalize().toAbsolutePath() }.toSet()
override val name: String = "file_write"
override val description: String = "Write content to a file at the specified path"
override val parametersSchema: JsonObject = buildJsonObject {
put("type", "object")
putJsonObject("properties") {
putJsonObject("path") {
put("type", "string")
put("description", "Relative path to write to")
}
putJsonObject("content") {
put("type", "string")
put("description", "File content")
}
}
put("required", buildJsonArray { add(JsonPrimitive("path")); add(JsonPrimitive("content")) })
}
override val tier: Tier = Tier.T2
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE)
@@ -107,7 +142,7 @@ class FileWriteTool(
}
}
private fun performOperation(
private suspend fun performOperation(
operation: String,
path: Path,
pathString: String,
@@ -116,6 +151,7 @@ class FileWriteTool(
"write" -> {
val content = request.parameters["content"] as String
Files.writeString(path, content)
storeArtifact(pathString, content, request.parameters["mode"] as? String ?: "0644")
ToolResult.Success(
invocationId = request.invocationId,
output = "File written successfully to $pathString",
@@ -145,6 +181,25 @@ 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,5 +1,7 @@
package com.correx.infrastructure.tools
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.artifacts.MaterializingArtifactWriter
import com.correx.core.tools.contract.Tool
import com.correx.infrastructure.tools.filesystem.FileEditTool
import com.correx.infrastructure.tools.filesystem.FileReadTool
@@ -15,7 +17,13 @@ data class ToolConfig(
)
data class FileReadConfig(val enabled: Boolean = false, val allowedPaths: Set<Path> = emptySet())
data class FileWriteConfig(val enabled: Boolean = false, val allowedPaths: Set<Path> = emptySet())
data class FileWriteConfig(
val enabled: Boolean = false,
val allowedPaths: Set<Path> = emptySet(),
val artifactStore: ArtifactStore? = null,
val materializingWriter: MaterializingArtifactWriter? = null,
val sandboxRoot: Path? = null,
)
data class FileEditConfig(val enabled: Boolean = false, val allowedPaths: Set<Path> = emptySet())
data class ShellConfig(val enabled: Boolean = false, val allowedExecutables: Set<String> = emptySet())
@@ -35,7 +43,10 @@ fun ToolConfig.buildTools(): List<Tool> = buildList {
}
if (fileWrite.enabled) {
add(FileWriteTool(
allowedPaths = fileWrite.allowedPaths
allowedPaths = fileWrite.allowedPaths,
artifactStore = fileWrite.artifactStore,
materializingWriter = fileWrite.materializingWriter,
sandboxRoot = fileWrite.sandboxRoot,
))
}
if (fileEdit.enabled) {
@@ -12,12 +12,16 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
class ShellTool(
private val allowedExecutables: Set<String> = emptySet(),
private val timeoutMs: Long = 30_000L,
) : Tool, ToolExecutor {
override val name: String = "shell"
override val description: String = "Execute a shell command"
override val parametersSchema: JsonObject = buildJsonObject {}
override val tier: Tier = Tier.T2
override val requiredCapabilities: Set<ToolCapability> =
setOf(ToolCapability.SHELL_EXEC, ToolCapability.PROCESS_SPAWN)