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:
+25
@@ -20,6 +20,7 @@ import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.RefinementIterationEvent
|
||||
import com.correx.core.events.events.RetrySalvageDecidedEvent
|
||||
import com.correx.core.events.events.SalvageDecision
|
||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
import com.correx.core.events.orchestration.OrchestrationState
|
||||
@@ -469,6 +470,30 @@ class DefaultSessionOrchestrator(
|
||||
emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Records free-form operator steering for a session that is mid-stage — independent of any
|
||||
* approval gate. Unlike [submitApprovalDecision]/[submitClarification] this never completes a
|
||||
* pending deferred: the running stage is not paused waiting on it. It only appends a
|
||||
* [com.correx.core.events.events.SteeringNoteAddedEvent], which [buildSteeringNoteEntries] folds
|
||||
* into context as an advisory `steeringNote` entry on the *next* context turn (invariants #3/#7 —
|
||||
* advisory only, never a state mutation). Reuses the existing SteeringNoteAddedEvent/fold rather
|
||||
* than a parallel event type, since that mechanism already implements exactly this semantics
|
||||
* (see TalkieFacade's STEERING chat mode, which emits the same event).
|
||||
*/
|
||||
suspend fun submitSteering(sessionId: SessionId, text: String) {
|
||||
if (text.isBlank()) return
|
||||
val currentStageId = runCatching { orchestrationRepository.getState(sessionId).currentStageId }
|
||||
.getOrNull()
|
||||
emit(
|
||||
sessionId,
|
||||
SteeringNoteAddedEvent(
|
||||
sessionId = sessionId,
|
||||
content = text,
|
||||
stageId = currentStageId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun executeMove(
|
||||
ctx: EnrichedExecutionContext,
|
||||
decision: TransitionDecision.Move,
|
||||
|
||||
Reference in New Issue
Block a user