diff --git a/core/events/src/main/kotlin/com/correx/core/events/events/ToolEvents.kt b/core/events/src/main/kotlin/com/correx/core/events/events/ToolEvents.kt index b6a22f1f..66c33901 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/events/ToolEvents.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/events/ToolEvents.kt @@ -4,6 +4,7 @@ import com.correx.core.approvals.Tier import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId import com.correx.core.events.types.ToolInvocationId +import com.correx.core.tools.contract.ToolCapability import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -52,6 +53,10 @@ data class ToolInvocationRequestedEvent( val toolName: String, val tier: Tier, val request: ToolRequest, + // The tool's declared capabilities, recorded at emission so replay classifies the call by what + // it could actually do (e.g. FILE_READ/FILE_WRITE) without re-deriving from the live registry. + // Defaulted for backward compatibility: events written before this field deserialize as empty. + val capabilities: Set = emptySet(), ) : EventPayload @Serializable diff --git a/core/events/src/main/kotlin/com/correx/core/tools/contract/ToolCapability.kt b/core/events/src/main/kotlin/com/correx/core/tools/contract/ToolCapability.kt new file mode 100644 index 00000000..1474960b --- /dev/null +++ b/core/events/src/main/kotlin/com/correx/core/tools/contract/ToolCapability.kt @@ -0,0 +1,21 @@ +package com.correx.core.tools.contract + +import kotlinx.serialization.Serializable + +/** + * What a tool is allowed to do, independent of its name. Plane-2 (`core:toolintent`) rules dispatch + * on these — never on tool names — and they are recorded on + * [com.correx.core.events.events.ToolInvocationRequestedEvent] at emission, so replay classifies a + * call by the capability it actually had rather than re-deriving it from the live registry. + * + * Lives in `core:events` (the lowest module) so events can carry it; `core:tools` and everything + * above import it from here — the package name is unchanged, so the move is import-transparent. + */ +@Serializable +enum class ToolCapability { + FILE_READ, + FILE_WRITE, + NETWORK_ACCESS, + SHELL_EXEC, + PROCESS_SPAWN, +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index 624829e2..9d685bdf 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt @@ -648,6 +648,7 @@ abstract class SessionOrchestrator( toolName = toolCall.function.name, tier = tier, request = request, + capabilities = tool?.requiredCapabilities ?: emptySet(), ), ) if (toolCall.function.name != STAGE_COMPLETE_TOOL && diff --git a/core/toolintent/src/main/kotlin/com/correx/core/toolintent/ReadFilesProjection.kt b/core/toolintent/src/main/kotlin/com/correx/core/toolintent/ReadFilesProjection.kt index 5c3af946..16ee828f 100644 --- a/core/toolintent/src/main/kotlin/com/correx/core/toolintent/ReadFilesProjection.kt +++ b/core/toolintent/src/main/kotlin/com/correx/core/toolintent/ReadFilesProjection.kt @@ -6,6 +6,7 @@ import com.correx.core.events.events.ToolInvocationRequestedEvent import com.correx.core.events.types.SessionId import com.correx.core.sessions.projections.Projection import com.correx.core.toolintent.rules.candidatePathStrings +import com.correx.core.tools.contract.ToolCapability /** Reads still awaiting their completion event, plus the paths confirmed read so far. */ data class ReadFilesState( @@ -14,11 +15,12 @@ data class ReadFilesState( ) /** - * Folds one session's tool events into the set of file paths it has successfully read. A `file_read` - * counts only once its [ToolExecutionCompletedEvent] lands — a read that failed (file not found) - * never enters the set, so it can't satisfy [com.correx.core.toolintent.rules.ReadBeforeWriteRule]. - * Paths are kept as the raw arguments; the rule resolves both sides through the probe, so - * `./Foo.kt` read and `Foo.kt` written still match. The same projection precedent as + * Folds one session's tool events into the set of file paths it has successfully read. A call counts + * as a read by its recorded [ToolCapability.FILE_READ] capability (not its tool name), and only once + * its [ToolExecutionCompletedEvent] lands — a read that failed (file not found) never enters the + * set, so it can't satisfy [com.correx.core.toolintent.rules.ReadBeforeWriteRule]. Paths are kept as + * the raw arguments; the rule resolves both sides through the probe, so `./Foo.kt` read and + * `Foo.kt` written still match. Same projection precedent as * [com.correx.core.sessions.projections.EgressAllowlistProjection]. */ class ReadFilesProjection(private val sessionId: SessionId) : Projection { @@ -33,7 +35,7 @@ class ReadFilesProjection(private val sessionId: SessionId) : Projection