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:
@@ -0,0 +1,48 @@
|
||||
package com.correx.core.config
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
/**
|
||||
* Serializes an [OperatorProfile] back to TOML text that [ProfileLoader] reads identically,
|
||||
* mirroring [ProjectProfileWriter]'s regenerate-the-file, skip-empty-sections approach.
|
||||
*/
|
||||
object OperatorProfileWriter {
|
||||
|
||||
/** Renders [profile] as TOML. String values escape `\` and `"`; empty sections are skipped. */
|
||||
fun serialize(profile: OperatorProfile): String {
|
||||
val b = StringBuilder()
|
||||
|
||||
if (profile.about.isNotBlank()) {
|
||||
b.append("about = ").append(str(profile.about)).append('\n')
|
||||
}
|
||||
|
||||
val prefs = profile.preferences
|
||||
val hasPrefs = prefs.approvalMode.isNotBlank() || prefs.preferredModels.isNotEmpty() || prefs.conventions.isNotEmpty()
|
||||
if (hasPrefs) {
|
||||
if (b.isNotEmpty()) b.append('\n')
|
||||
b.append("[preferences]\n")
|
||||
if (prefs.approvalMode.isNotBlank()) {
|
||||
b.append("approval_mode = ").append(str(prefs.approvalMode)).append('\n')
|
||||
}
|
||||
if (prefs.preferredModels.isNotEmpty()) {
|
||||
b.append("preferred_models = ").append(list(prefs.preferredModels)).append('\n')
|
||||
}
|
||||
if (prefs.conventions.isNotEmpty()) {
|
||||
b.append("conventions = ").append(list(prefs.conventions)).append('\n')
|
||||
}
|
||||
}
|
||||
|
||||
return b.toString()
|
||||
}
|
||||
|
||||
/** Writes [profile] to `<homeDir>/.config/correx/profile.toml`, creating parent dirs if missing. */
|
||||
fun write(profile: OperatorProfile, homeDir: String = System.getProperty("user.home")) {
|
||||
val path = ProfileLoader.profilePath(homeDir)
|
||||
Files.createDirectories(path.parent)
|
||||
Files.writeString(path, serialize(profile))
|
||||
}
|
||||
|
||||
private fun str(v: String): String = "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
|
||||
|
||||
private fun list(v: List<String>): String = "[" + v.joinToString(", ") { str(it) } + "]"
|
||||
}
|
||||
Reference in New Issue
Block a user