From 15d1e09c44231cd9aa7286de0e5a23363384fc9f Mon Sep 17 00:00:00 2001 From: kami Date: Mon, 18 May 2026 21:39:36 +0400 Subject: [PATCH] feat(tools): add description and parametersSchema to Tool interface; wire FileWriteTool materialization --- .../com/correx/core/tools/contract/Tool.kt | 3 + .../core/tools/contract/ToolContractTest.kt | 4 ++ infrastructure/tools/build.gradle | 2 + infrastructure/tools/filesystem/build.gradle | 3 + .../tools/filesystem/FileEditTool.kt | 4 ++ .../tools/filesystem/FileReadTool.kt | 4 ++ .../tools/filesystem/FileWriteTool.kt | 57 ++++++++++++++++++- .../correx/infrastructure/tools/ToolConfig.kt | 15 ++++- .../infrastructure/tools/shell/ShellTool.kt | 4 ++ .../correx/testing/fixtures/tools/EchoTool.kt | 4 ++ .../testing/fixtures/tools/FailingTool.kt | 4 ++ .../testing/fixtures/tools/RejectedTool.kt | 4 ++ 12 files changed, 105 insertions(+), 3 deletions(-) diff --git a/core/tools/src/main/kotlin/com/correx/core/tools/contract/Tool.kt b/core/tools/src/main/kotlin/com/correx/core/tools/contract/Tool.kt index 9966c0c1..f8d7621d 100644 --- a/core/tools/src/main/kotlin/com/correx/core/tools/contract/Tool.kt +++ b/core/tools/src/main/kotlin/com/correx/core/tools/contract/Tool.kt @@ -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 fun validateRequest(request: ToolRequest): ValidationResult diff --git a/core/tools/src/test/kotlin/com/correx/core/tools/contract/ToolContractTest.kt b/core/tools/src/test/kotlin/com/correx/core/tools/contract/ToolContractTest.kt index e0a89e7f..87b4a16a2 100644 --- a/core/tools/src/test/kotlin/com/correx/core/tools/contract/ToolContractTest.kt +++ b/core/tools/src/test/kotlin/com/correx/core/tools/contract/ToolContractTest.kt @@ -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 = emptySet() override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid diff --git a/infrastructure/tools/build.gradle b/infrastructure/tools/build.gradle index 47f43e5b..8af013d4 100644 --- a/infrastructure/tools/build.gradle +++ b/infrastructure/tools/build.gradle @@ -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") } diff --git a/infrastructure/tools/filesystem/build.gradle b/infrastructure/tools/filesystem/build.gradle index ec31807c..43c817eb 100644 --- a/infrastructure/tools/filesystem/build.gradle +++ b/infrastructure/tools/filesystem/build.gradle @@ -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' } diff --git a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileEditTool.kt b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileEditTool.kt index 11d33cc0..92f5bc58 100644 --- a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileEditTool.kt +++ b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileEditTool.kt @@ -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 = 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 = setOf(ToolCapability.FILE_WRITE) diff --git a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt index 42257f1e..4d3d0894 100644 --- a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt +++ b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileReadTool.kt @@ -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 = 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 = setOf(ToolCapability.FILE_READ) diff --git a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteTool.kt b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteTool.kt index f9648a2b..6ff23b91 100644 --- a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteTool.kt +++ b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/FileWriteTool.kt @@ -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 = 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 = 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 = 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, diff --git a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt index f2543cf7..d056f960 100644 --- a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt +++ b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/ToolConfig.kt @@ -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 = emptySet()) -data class FileWriteConfig(val enabled: Boolean = false, val allowedPaths: Set = emptySet()) +data class FileWriteConfig( + val enabled: Boolean = false, + val allowedPaths: Set = emptySet(), + val artifactStore: ArtifactStore? = null, + val materializingWriter: MaterializingArtifactWriter? = null, + val sandboxRoot: Path? = null, +) data class FileEditConfig(val enabled: Boolean = false, val allowedPaths: Set = emptySet()) data class ShellConfig(val enabled: Boolean = false, val allowedExecutables: Set = emptySet()) @@ -35,7 +43,10 @@ fun ToolConfig.buildTools(): List = buildList { } if (fileWrite.enabled) { add(FileWriteTool( - allowedPaths = fileWrite.allowedPaths + allowedPaths = fileWrite.allowedPaths, + artifactStore = fileWrite.artifactStore, + materializingWriter = fileWrite.materializingWriter, + sandboxRoot = fileWrite.sandboxRoot, )) } if (fileEdit.enabled) { diff --git a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/shell/ShellTool.kt b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/shell/ShellTool.kt index fbd48907..66f322c3 100644 --- a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/shell/ShellTool.kt +++ b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/shell/ShellTool.kt @@ -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 = 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 = setOf(ToolCapability.SHELL_EXEC, ToolCapability.PROCESS_SPAWN) diff --git a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/EchoTool.kt b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/EchoTool.kt index 6233ee8a..06e98727 100644 --- a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/EchoTool.kt +++ b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/EchoTool.kt @@ -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 = emptySet() override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid diff --git a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/FailingTool.kt b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/FailingTool.kt index fbf9ddbb..0653e701 100644 --- a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/FailingTool.kt +++ b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/FailingTool.kt @@ -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 = emptySet() override fun validateRequest(request: ToolRequest): ValidationResult = diff --git a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/RejectedTool.kt b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/RejectedTool.kt index 8b4d1286..f02f7c97 100644 --- a/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/RejectedTool.kt +++ b/testing/fixtures/src/main/kotlin/com/correx/testing/fixtures/tools/RejectedTool.kt @@ -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 = emptySet() override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid