feat(tools): add description and parametersSchema to Tool interface; wire FileWriteTool materialization
This commit is contained in:
@@ -2,9 +2,12 @@ package com.correx.core.tools.contract
|
||||
|
||||
import com.correx.core.approvals.Tier
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
interface Tool {
|
||||
val name: String
|
||||
val description: String
|
||||
val parametersSchema: JsonObject
|
||||
val tier: Tier
|
||||
val requiredCapabilities: Set<ToolCapability>
|
||||
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.StageId
|
||||
import com.correx.core.events.types.ToolInvocationId
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
@@ -13,6 +15,8 @@ class ToolContractTest {
|
||||
|
||||
private val stubTool = object : Tool {
|
||||
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 requiredCapabilities: Set<ToolCapability> = emptySet()
|
||||
override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
+4
@@ -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)
|
||||
|
||||
|
||||
+4
@@ -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)
|
||||
|
||||
|
||||
+56
-1
@@ -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) {
|
||||
|
||||
+4
@@ -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)
|
||||
|
||||
@@ -5,9 +5,13 @@ import com.correx.core.events.events.ToolRequest
|
||||
import com.correx.core.tools.contract.Tool
|
||||
import com.correx.core.tools.contract.ToolCapability
|
||||
import com.correx.core.tools.contract.ValidationResult
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
|
||||
class EchoTool : Tool {
|
||||
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 requiredCapabilities: Set<ToolCapability> = emptySet()
|
||||
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.ToolCapability
|
||||
import com.correx.core.tools.contract.ValidationResult
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
|
||||
class FailingTool : Tool {
|
||||
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 requiredCapabilities: Set<ToolCapability> = emptySet()
|
||||
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.ToolCapability
|
||||
import com.correx.core.tools.contract.ValidationResult
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
|
||||
class RejectedTool : Tool {
|
||||
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 requiredCapabilities: Set<ToolCapability> = emptySet()
|
||||
override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid
|
||||
|
||||
Reference in New Issue
Block a user