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:
@@ -119,6 +119,10 @@ func (m Model) renderOverlay(base string) string {
|
||||
return m.center(m.artifactsModal())
|
||||
case OverlayConfig:
|
||||
return m.center(m.configModal())
|
||||
case OverlayProjectProfile:
|
||||
return m.center(m.projectProfileModal())
|
||||
case OverlayOperatorProfile:
|
||||
return m.center(m.operatorProfileModal())
|
||||
case OverlayStats:
|
||||
return m.center(m.statsModal())
|
||||
case OverlayIdeas:
|
||||
@@ -160,7 +164,11 @@ func (m Model) approvalBandHeight() int {
|
||||
if s := m.session(m.selectedID); s != nil && s.Pending != nil {
|
||||
rows = m.approvalContentRows(s.Pending)
|
||||
if n := len(s.Pending.Rationale); n > 0 {
|
||||
rat = n + 1 // rationale lines + trailing blank
|
||||
textW := m.width - 4 - 2 // box borders/padding + rationale marker
|
||||
for _, r := range s.Pending.Rationale {
|
||||
rat += len(wrapLine(r, textW))
|
||||
}
|
||||
rat++ // trailing blank
|
||||
}
|
||||
}
|
||||
steer := 0
|
||||
@@ -219,7 +227,10 @@ func (m Model) renderApprovalBand(h int) string {
|
||||
innerH := h - 2
|
||||
ratBlock := 0
|
||||
if len(a.Rationale) > 0 {
|
||||
ratBlock = len(a.Rationale) + 1 // rationale lines + trailing blank
|
||||
for _, r := range a.Rationale {
|
||||
ratBlock += len(wrapLine(r, textW-2))
|
||||
}
|
||||
ratBlock++ // trailing blank
|
||||
}
|
||||
steerBlock := 0
|
||||
if m.steering || m.steerBuffer != "" {
|
||||
@@ -233,7 +244,13 @@ func (m Model) renderApprovalBand(h int) string {
|
||||
body = append(body, m.justify(leftHdr, rightHdr, textW, t.P.Bg), "")
|
||||
for _, r := range a.Rationale {
|
||||
marker := lipgloss.NewStyle().Foreground(t.P.Warn).Background(t.P.Bg).Render("▲ ")
|
||||
body = append(body, marker+t.span(truncate(r, textW-2), t.P.Dim))
|
||||
for i, ln := range wrapLine(r, textW-2) {
|
||||
prefix := marker
|
||||
if i > 0 {
|
||||
prefix = " "
|
||||
}
|
||||
body = append(body, prefix+t.span(ln, t.P.Dim))
|
||||
}
|
||||
}
|
||||
if len(a.Rationale) > 0 {
|
||||
body = append(body, "")
|
||||
|
||||
Reference in New Issue
Block a user