feat(kernel): enforce stage allowedTools at tool-call dispatch
A stage's allowedTools only shaped which tool definitions were offered to the model; the dispatch path resolved returned tool calls straight from the full registry and executed them without checking stage membership. A hallucinated, jailbroken, or edited-transcript tool call for any registered tool would run unchecked (violating invariant #5 / policy-is-absolute). dispatchToolCalls now rejects any tool call whose name is not in the stage's allowedTools (STAGE_COMPLETE_TOOL exempt), emitting ToolExecutionRejectedEvent and feeding an error ContextEntry back to the model instead of executing. Empty allowedTools means deny-all domain tools, consistent with offering only STAGE_COMPLETE_TOOL at inference time.
This commit is contained in:
+36
@@ -406,6 +406,42 @@ abstract class SessionOrchestrator(
|
||||
request = request,
|
||||
),
|
||||
)
|
||||
if (toolCall.function.name != STAGE_COMPLETE_TOOL &&
|
||||
toolCall.function.name !in stageConfig.allowedTools
|
||||
) {
|
||||
val denyReason = "tool '${toolCall.function.name}' is not permitted in stage '${stageId.value}'"
|
||||
emit(
|
||||
sessionId,
|
||||
ToolExecutionRejectedEvent(
|
||||
invocationId = invocationId,
|
||||
sessionId = sessionId,
|
||||
toolName = toolCall.function.name,
|
||||
tier = tier,
|
||||
reason = denyReason,
|
||||
),
|
||||
)
|
||||
val sourceId = toolCall.id ?: invocationId.value
|
||||
return@flatMap listOf(
|
||||
ContextEntry(
|
||||
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||
layer = ContextLayer.L2,
|
||||
sourceType = "assistantToolCall",
|
||||
sourceId = sourceId,
|
||||
content = Json.encodeToString(ToolCallRequest.serializer(), toolCall),
|
||||
tokenEstimate = estimateTokens(toolCall.function.arguments),
|
||||
role = EntryRole.ASSISTANT,
|
||||
),
|
||||
ContextEntry(
|
||||
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||
layer = ContextLayer.L2,
|
||||
sourceType = "toolResult",
|
||||
sourceId = sourceId,
|
||||
content = "ERROR: $denyReason",
|
||||
tokenEstimate = estimateTokens(denyReason),
|
||||
role = EntryRole.TOOL,
|
||||
),
|
||||
)
|
||||
}
|
||||
val plane2Risk: RiskSummary? = runPlane2Assessment(
|
||||
sessionId, stageId, invocationId, toolCall.function.name, request, tool, effectives,
|
||||
)?.let { assessment ->
|
||||
|
||||
Reference in New Issue
Block a user