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,9 +2,12 @@ package com.correx.core.tools.contract
import com.correx.core.approvals.Tier import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolRequest import com.correx.core.events.events.ToolRequest
import kotlinx.serialization.json.JsonObject
interface Tool { interface Tool {
val name: String val name: String
val description: String
val parametersSchema: JsonObject
val tier: Tier val tier: Tier
val requiredCapabilities: Set<ToolCapability> val requiredCapabilities: Set<ToolCapability>
fun validateRequest(request: ToolRequest): ValidationResult fun validateRequest(request: ToolRequest): ValidationResult
@@ -5,6 +5,8 @@ import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId import com.correx.core.events.types.ToolInvocationId
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertIs import kotlin.test.assertIs
@@ -13,6 +15,8 @@ class ToolContractTest {
private val stubTool = object : Tool { private val stubTool = object : Tool {
override val name: String = "stub" override val name: String = "stub"
override val description: String = "Stub tool for testing"
override val parametersSchema: JsonObject = buildJsonObject {}
override val tier: Tier = Tier.T0 override val tier: Tier = Tier.T0
override val requiredCapabilities: Set<ToolCapability> = emptySet() override val requiredCapabilities: Set<ToolCapability> = emptySet()
override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid
+2
View File
@@ -10,4 +10,6 @@ dependencies {
implementation project(":core:approvals") implementation project(":core:approvals")
implementation project(":core:sessions") implementation project(":core:sessions")
implementation project(":infrastructure:tools:filesystem") 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:tools')
implementation project(':core:events') implementation project(':core:events')
implementation project(':core:approvals') 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.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
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
@@ -26,6 +28,8 @@ class FileEditTool(
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_edit" 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 tier: Tier = Tier.T3
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE) 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.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
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
@@ -22,6 +24,8 @@ class FileReadTool(
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_read" 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 tier: Tier = Tier.T1
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_READ) override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_READ)
@@ -1,6 +1,11 @@
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.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.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
@@ -12,6 +17,14 @@ 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.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.io.IOException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.InvalidPathException import java.nio.file.InvalidPathException
@@ -20,10 +33,32 @@ 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,
) : Tool, FileAffectingTool, ToolExecutor { ) : Tool, FileAffectingTool, ToolExecutor {
private companion object {
val log = 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"
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 tier: Tier = Tier.T2
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE) override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_WRITE)
@@ -107,7 +142,7 @@ class FileWriteTool(
} }
} }
private fun performOperation( private suspend fun performOperation(
operation: String, operation: String,
path: Path, path: Path,
pathString: String, pathString: String,
@@ -116,6 +151,7 @@ class FileWriteTool(
"write" -> { "write" -> {
val content = request.parameters["content"] as String val content = request.parameters["content"] as String
Files.writeString(path, content) Files.writeString(path, content)
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",
@@ -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( private fun handleExecutionException(
e: Throwable, e: Throwable,
invocationId: ToolInvocationId, invocationId: ToolInvocationId,
@@ -1,5 +1,7 @@
package com.correx.infrastructure.tools 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.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
@@ -15,7 +17,13 @@ data class ToolConfig(
) )
data class FileReadConfig(val enabled: Boolean = false, val allowedPaths: Set<Path> = emptySet()) 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 FileEditConfig(val enabled: Boolean = false, val allowedPaths: Set<Path> = emptySet())
data class ShellConfig(val enabled: Boolean = false, val allowedExecutables: Set<String> = 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) { if (fileWrite.enabled) {
add(FileWriteTool( add(FileWriteTool(
allowedPaths = fileWrite.allowedPaths allowedPaths = fileWrite.allowedPaths,
artifactStore = fileWrite.artifactStore,
materializingWriter = fileWrite.materializingWriter,
sandboxRoot = fileWrite.sandboxRoot,
)) ))
} }
if (fileEdit.enabled) { if (fileEdit.enabled) {
@@ -12,12 +12,16 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
class ShellTool( class ShellTool(
private val allowedExecutables: Set<String> = emptySet(), private val allowedExecutables: Set<String> = emptySet(),
private val timeoutMs: Long = 30_000L, private val timeoutMs: Long = 30_000L,
) : Tool, ToolExecutor { ) : Tool, ToolExecutor {
override val name: String = "shell" 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 tier: Tier = Tier.T2
override val requiredCapabilities: Set<ToolCapability> = override val requiredCapabilities: Set<ToolCapability> =
setOf(ToolCapability.SHELL_EXEC, ToolCapability.PROCESS_SPAWN) setOf(ToolCapability.SHELL_EXEC, ToolCapability.PROCESS_SPAWN)
@@ -5,9 +5,13 @@ import com.correx.core.events.events.ToolRequest
import com.correx.core.tools.contract.Tool import com.correx.core.tools.contract.Tool
import com.correx.core.tools.contract.ToolCapability import com.correx.core.tools.contract.ToolCapability
import com.correx.core.tools.contract.ValidationResult import com.correx.core.tools.contract.ValidationResult
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
class EchoTool : Tool { class EchoTool : Tool {
override val name: String = "echo" override val name: String = "echo"
override val description: String = "Echo tool for testing"
override val parametersSchema: JsonObject = buildJsonObject {}
override val tier: Tier = Tier.T0 override val tier: Tier = Tier.T0
override val requiredCapabilities: Set<ToolCapability> = emptySet() override val requiredCapabilities: Set<ToolCapability> = emptySet()
override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid
@@ -5,9 +5,13 @@ import com.correx.core.events.events.ToolRequest
import com.correx.core.tools.contract.Tool import com.correx.core.tools.contract.Tool
import com.correx.core.tools.contract.ToolCapability import com.correx.core.tools.contract.ToolCapability
import com.correx.core.tools.contract.ValidationResult import com.correx.core.tools.contract.ValidationResult
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
class FailingTool : Tool { class FailingTool : Tool {
override val name: String = "failing" override val name: String = "failing"
override val description: String = "Failing tool for testing"
override val parametersSchema: JsonObject = buildJsonObject {}
override val tier: Tier = Tier.T2 override val tier: Tier = Tier.T2
override val requiredCapabilities: Set<ToolCapability> = emptySet() override val requiredCapabilities: Set<ToolCapability> = emptySet()
override fun validateRequest(request: ToolRequest): ValidationResult = override fun validateRequest(request: ToolRequest): ValidationResult =
@@ -5,9 +5,13 @@ import com.correx.core.events.events.ToolRequest
import com.correx.core.tools.contract.Tool import com.correx.core.tools.contract.Tool
import com.correx.core.tools.contract.ToolCapability import com.correx.core.tools.contract.ToolCapability
import com.correx.core.tools.contract.ValidationResult import com.correx.core.tools.contract.ValidationResult
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
class RejectedTool : Tool { class RejectedTool : Tool {
override val name: String = "rejected" override val name: String = "rejected"
override val description: String = "Rejected tool for testing"
override val parametersSchema: JsonObject = buildJsonObject {}
override val tier: Tier = Tier.T4 override val tier: Tier = Tier.T4
override val requiredCapabilities: Set<ToolCapability> = emptySet() override val requiredCapabilities: Set<ToolCapability> = emptySet()
override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid