feat: add MissingToolRule semantic validation

Flags when a workflow stage's allowedTools references a tool name that
is not registered in the tool registry (config drift), surfaced as a
non-blocking WARNING.

- Add ValidationContext.availableTools (nullable; null = registry
  unavailable, skip the check)
- MissingToolRule emits one MISSING_TOOL warning per (stage, tool) pair,
  deterministically ordered
- Populate availableTools from the ToolRegistry at the orchestrator
  construction site; wire the rule into the prod SemanticValidator
This commit is contained in:
2026-05-30 13:18:43 +04:00
parent ee3f9b8aaa
commit e8372e0643
5 changed files with 145 additions and 1 deletions
@@ -304,7 +304,11 @@ abstract class SessionOrchestrator(
is InferenceResult.Failed -> StageExecutionResult.Failure(inferenceResult.reason, retryable = true)
is InferenceResult.Success -> {
if (isCancelled(sessionId)) return StageExecutionResult.Failure("CANCELLED", retryable = false)
val validationCtx = ValidationContext(graph = graph, sessionState = session.state)
val validationCtx = ValidationContext(
graph = graph,
sessionState = session.state,
availableTools = toolRegistry?.all()?.map { it.name }?.toSet()
)
when (val outcome = mapValidationOutcome(sessionId, stageId, validationCtx)) {
is StageExecutionResult.Success -> {
emitToolArtifacts(sessionId, stageId, stageConfig)
@@ -10,4 +10,5 @@ data class ValidationContext(
val detectedCycles: List<DetectedCycle> = emptyList(),
val cyclePolicies: Set<CyclePolicyBinding> = emptySet(),
val sessionState: SessionState? = null,
val availableTools: Set<String>? = null,
)
@@ -0,0 +1,34 @@
package com.correx.core.validation.semantic.rules
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationLocation
import com.correx.core.validation.model.ValidationSeverity
import com.correx.core.validation.semantic.SemanticRule
class MissingToolRule : SemanticRule {
override fun validate(context: ValidationContext): List<ValidationIssue> {
val available = context.availableTools ?: return emptyList()
val issues = mutableListOf<ValidationIssue>()
for (stageId in context.graph.stageIds.sortedBy { it.value }) {
val stageConfig = context.graph.stages[stageId] ?: continue
for (toolName in stageConfig.allowedTools.sorted()) {
if (toolName !in available) {
issues.add(
ValidationIssue(
code = "MISSING_TOOL",
message = "Stage '${stageId.value}' allows unregistered tool '$toolName'",
severity = ValidationSeverity.WARNING,
location = ValidationLocation.Graph(stageId = stageId)
)
)
}
}
}
return issues
}
}