fix(kernel+infra): tool approval gate and executor cleanup
- SessionOrchestrator: wire T2/T3/T4 approval gate before tool execution; emit OrchestrationPausedEvent/ApprovalRequestedEvent, await decision, return rejection as ERROR context entry so LLM sees the denial; propagate tool ERROR entries as StageExecutionResult.Failure - SandboxedToolExecutor: remove dead code and simplify - InfrastructureModule: minor wiring cleanup - LlamaCppInferenceProvider / build.gradle: related build fixes
This commit is contained in:
@@ -17,8 +17,6 @@ dependencies {
|
||||
implementation project(':core:context')
|
||||
implementation project(':infrastructure:inference:commons')
|
||||
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0")
|
||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-cio:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
|
||||
|
||||
+24
-1
@@ -25,7 +25,9 @@ import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
private const val DEFAULT_REQUEST_TIMEOUT_MS = 60_000L
|
||||
|
||||
@@ -42,7 +44,23 @@ private fun defaultHttpClient(): HttpClient = HttpClient(CIO) {
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = DEFAULT_REQUEST_TIMEOUT_MS
|
||||
}
|
||||
install(ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
encodeDefaults = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
private val log = LoggerFactory.getLogger(LlamaCppInferenceProvider::class.java)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "MagicNumber")
|
||||
class LlamaCppInferenceProvider(
|
||||
@@ -80,12 +98,17 @@ class LlamaCppInferenceProvider(
|
||||
tools = request.tools.takeIf { it.isNotEmpty() },
|
||||
)
|
||||
|
||||
val encoded = json.encodeToString(body)
|
||||
log.debug("sending request to llm: {}", encoded)
|
||||
|
||||
val response = httpClient.post("$baseUrl/v1/chat/completions") {
|
||||
contentType(ContentType.Application.Json)
|
||||
accept(ContentType.Application.Json)
|
||||
setBody(body)
|
||||
setBody(encoded)
|
||||
}.body<ChatCompletionResponse>()
|
||||
|
||||
log.debug("got response from llm: {}", response)
|
||||
|
||||
val message = response.choices.first().message
|
||||
val finishReason = when (response.choices.first().finishReason.lowercase()) {
|
||||
"length" -> FinishReason.Length
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.correx.infrastructure
|
||||
|
||||
import com.correx.core.approvals.domain.ApprovalEngine
|
||||
import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.events.EventDispatcher
|
||||
import com.correx.core.events.stores.EventStore
|
||||
@@ -110,15 +109,11 @@ object InfrastructureModule {
|
||||
|
||||
fun createToolExecutor(
|
||||
registry: ToolRegistry,
|
||||
approvalEngine: ApprovalEngine,
|
||||
eventStore: EventStore,
|
||||
eventDispatcher: EventDispatcher,
|
||||
workDir: Path,
|
||||
): ToolExecutor = SandboxedToolExecutor(
|
||||
delegate = DispatchingToolExecutor(registry),
|
||||
registry = registry,
|
||||
approvalEngine = approvalEngine,
|
||||
eventStore = eventStore,
|
||||
eventDispatcher = eventDispatcher,
|
||||
workDir = workDir,
|
||||
)
|
||||
|
||||
+1
-82
@@ -1,28 +1,15 @@
|
||||
package com.correx.infrastructure.tools
|
||||
|
||||
import com.correx.core.approvals.Tier
|
||||
import com.correx.core.approvals.domain.ApprovalEngine
|
||||
import com.correx.core.approvals.model.ApprovalContext
|
||||
import com.correx.core.approvals.model.ApprovalGrant
|
||||
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
||||
import com.correx.core.approvals.model.DomainApprovalRequest
|
||||
import com.correx.core.events.EventDispatcher
|
||||
import com.correx.core.events.events.ApprovalGrantCreatedEvent
|
||||
import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
||||
import com.correx.core.events.events.ToolExecutionFailedEvent
|
||||
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
||||
import com.correx.core.events.events.ToolExecutionStartedEvent
|
||||
import com.correx.core.events.events.ToolReceipt
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.ApprovalRequestId
|
||||
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.events.types.ValidationReportId
|
||||
import com.correx.core.sessions.ApprovalMode
|
||||
import com.correx.core.tools.contract.FileAffectingTool
|
||||
import com.correx.core.tools.contract.Tool
|
||||
import com.correx.core.tools.contract.ToolExecutor
|
||||
@@ -41,8 +28,6 @@ import java.util.*
|
||||
class SandboxedToolExecutor(
|
||||
private val delegate: ToolExecutor,
|
||||
private val registry: ToolRegistry,
|
||||
private val approvalEngine: ApprovalEngine,
|
||||
private val eventStore: EventStore,
|
||||
private val eventDispatcher: EventDispatcher,
|
||||
private val workDir: Path = Path.of("/tmp/correx-sandbox"),
|
||||
) : ToolExecutor {
|
||||
@@ -61,26 +46,7 @@ class SandboxedToolExecutor(
|
||||
recoverable = false,
|
||||
)
|
||||
|
||||
// 2. approval check for T2-T4
|
||||
val requiresApproval = when (tool.tier) {
|
||||
Tier.T0, Tier.T1 -> false
|
||||
Tier.T2, Tier.T3, Tier.T4 -> true
|
||||
}
|
||||
|
||||
if (requiresApproval) {
|
||||
val sessionEvents = eventStore.read(sessionId)
|
||||
val decision = evaluateApproval(tool, sessionId, request.stageId, sessionEvents)
|
||||
if (!decision.isApproved) {
|
||||
emitRejected(sessionId, invocationId, toolName, tool.tier, decision.reason ?: "approval denied")
|
||||
return@withContext ToolResult.Failure(
|
||||
invocationId = invocationId,
|
||||
reason = decision.reason ?: "approval denied",
|
||||
recoverable = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. emit started
|
||||
// 2. emit started
|
||||
emitStarted(sessionId, invocationId, toolName)
|
||||
|
||||
// 4. create working dir
|
||||
@@ -121,45 +87,6 @@ class SandboxedToolExecutor(
|
||||
}
|
||||
}
|
||||
|
||||
// --- approval ---
|
||||
|
||||
private fun evaluateApproval(
|
||||
tool: Tool,
|
||||
sessionId: SessionId,
|
||||
stageId: StageId,
|
||||
sessionEvents: List<StoredEvent>,
|
||||
) = approvalEngine.evaluate(
|
||||
request = DomainApprovalRequest(
|
||||
id = ApprovalRequestId(UUID.randomUUID().toString()),
|
||||
tier = tool.tier,
|
||||
validationReportId = ValidationReportId(UUID.randomUUID().toString()),
|
||||
riskSummaryId = null,
|
||||
timestamp = Clock.System.now(),
|
||||
causationId = null,
|
||||
correlationId = null,
|
||||
),
|
||||
context = ApprovalContext(
|
||||
identity = ApprovalScopeIdentity(sessionId, stageId, null),
|
||||
mode = ApprovalMode.PROMPT,
|
||||
),
|
||||
grants = extractGrants(sessionEvents),
|
||||
now = Clock.System.now(),
|
||||
)
|
||||
|
||||
private fun extractGrants(events: List<StoredEvent>): List<ApprovalGrant> =
|
||||
events
|
||||
.mapNotNull { it.payload as? ApprovalGrantCreatedEvent }
|
||||
.map { e ->
|
||||
ApprovalGrant(
|
||||
id = e.grantId,
|
||||
scope = e.scope,
|
||||
permittedTiers = e.permittedTiers,
|
||||
reason = e.reason,
|
||||
timestamp = Clock.System.now(),
|
||||
expiresAt = e.expiresAt,
|
||||
)
|
||||
}
|
||||
|
||||
// --- backup/restore ---
|
||||
|
||||
/**
|
||||
@@ -209,14 +136,6 @@ class SandboxedToolExecutor(
|
||||
|
||||
// --- event emission ---
|
||||
|
||||
private suspend fun emitRejected(
|
||||
sessionId: SessionId,
|
||||
invocationId: ToolInvocationId,
|
||||
toolName: String,
|
||||
tier: Tier,
|
||||
reason: String,
|
||||
) = emit(sessionId, ToolExecutionRejectedEvent(invocationId, sessionId, toolName, tier, reason))
|
||||
|
||||
private suspend fun emitStarted(
|
||||
sessionId: SessionId,
|
||||
invocationId: ToolInvocationId,
|
||||
|
||||
Reference in New Issue
Block a user