feat(guardrails): steering channel + shell-in-file rule + capability-gap detector
Bundles three operator-reliability guardrails (Vikunja #28/#29/#30) plus the in-flight branch WIP they were built on top of (reasoning_content capture, operator/project profile editor, write-jail workspaceRoot fix) — the tree is interdependent (SessionOrchestrator references reasoningArtifactId from the WIP) and does not compile as separable subsets, so it lands as one commit. Guardrails: - #28 mid-stage steering: ClientMessage.SteerSession -> GlobalStreamHandler -> orchestrator.submitSteering, reusing SteeringNoteAddedEvent + existing context fold (advisory, non-authoritative; invariants #3/#7). Closes the gap where steering typed off an approval gate was silently dropped. - #29 shell-in-file guardrail: ShellInFileContentRule (core:toolintent) blocks a file_write whose content is a bare shell command (e.g. "mkdir -p ..."); FileWriteTool description now advertises auto-mkdir of parent dirs. Basename-allowlist so the extensionless case is caught; scripts/Makefiles/multiline exempt. - #30 pt1 capability-gap detector: deterministic CapabilityGapDetector maps stage intent -> implied ToolCapability, compares to granted tools, emits advisory CapabilityGapDetectedEvent in FreestyleDriver.lockAndRun. Recorded, never fails the gate and never auto-grants (invariants #3/#4/#5). Reflection rung is pt2. Verified: ./gradlew check green (whole tree).
This commit is contained in:
@@ -52,3 +52,24 @@ data class PlanCompileCheckedEvent(
|
||||
/** Compiler error when [passed] is false; null on success. Kept bounded. */
|
||||
val error: String? = null,
|
||||
) : EventPayload
|
||||
|
||||
/**
|
||||
* Capability-gap detector finding (Vikunja #30 part 1): a stage's declared intent implies a tool
|
||||
* capability ([impliedCapability]) it was not granted, per the deterministic verb map in
|
||||
* [com.correx.infrastructure.workflow.CapabilityGapDetector]. [evidence] is the triggering phrase
|
||||
* found in the stage brief.
|
||||
*
|
||||
* Advisory only — recorded alongside the plan-compile gate's other findings but does NOT fail the
|
||||
* gate and does NOT grant the tool itself (invariants #3 LLM-proposes/core-decides, #4
|
||||
* policy-absolute, #5 tiers-declared-not-inferred). A future reflection rung (part 2, an LLM
|
||||
* "are you sure?" pass) consumes these events; this ticket stops at detection + recording.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("CapabilityGapDetected")
|
||||
data class CapabilityGapDetectedEvent(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val impliedCapability: String,
|
||||
val evidence: String,
|
||||
val timestampMs: Long,
|
||||
) : EventPayload
|
||||
|
||||
@@ -30,6 +30,7 @@ data class InferenceCompletedEvent(
|
||||
val tokensUsed: TokenUsage,
|
||||
val latencyMs: Long,
|
||||
val responseArtifactId: ArtifactId,
|
||||
val reasoningArtifactId: ArtifactId? = null,
|
||||
) : EventPayload
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.correx.core.events.events.EgressHostsGrantedEvent
|
||||
import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
||||
import com.correx.core.events.events.PlanCompileCheckedEvent
|
||||
import com.correx.core.events.events.CapabilityGapDetectedEvent
|
||||
import com.correx.core.events.events.ReviewFindingsRaisedEvent
|
||||
import com.correx.core.events.events.ExecutionPlanRejectedEvent
|
||||
import com.correx.core.events.events.HealthDegradedEvent
|
||||
@@ -171,6 +172,7 @@ val eventModule = SerializersModule {
|
||||
subclass(ExecutionPlanLockedEvent::class)
|
||||
subclass(ExecutionPlanRejectedEvent::class)
|
||||
subclass(PlanCompileCheckedEvent::class)
|
||||
subclass(CapabilityGapDetectedEvent::class)
|
||||
subclass(ReviewFindingsRaisedEvent::class)
|
||||
subclass(JournalCompactedEvent::class)
|
||||
subclass(HealthDegradedEvent::class)
|
||||
|
||||
Reference in New Issue
Block a user