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:
+36
-10
@@ -50,23 +50,49 @@ object JsonSchemaValidator {
|
||||
|
||||
private fun checkProperty(key: String, property: JsonSchemaProperty, element: JsonElement): List<String> {
|
||||
val typeError = checkType(property.type, element)
|
||||
val array = element as? JsonArray
|
||||
return when {
|
||||
typeError != null -> listOf("property '$key': $typeError")
|
||||
property.type != "array" || array == null -> emptyList()
|
||||
else -> buildList {
|
||||
if (array.size < property.minItems) {
|
||||
add("property '$key': expected at least ${property.minItems} item(s), got ${array.size}")
|
||||
}
|
||||
property.items?.let { itemSchema ->
|
||||
array.forEachIndexed { i, el ->
|
||||
checkType(itemSchema.type, el)?.let { add("property '$key'[$i]: $it") }
|
||||
}
|
||||
property.type == "array" -> checkArray(key, property, element as JsonArray)
|
||||
property.type == "object" && property.properties.isNotEmpty() ->
|
||||
validateProperty(property, element as JsonObject).map { "property '$key': $it" }
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkArray(key: String, property: JsonSchemaProperty, array: JsonArray): List<String> = buildList {
|
||||
if (array.size < property.minItems) {
|
||||
add("property '$key': expected at least ${property.minItems} item(s), got ${array.size}")
|
||||
}
|
||||
property.items?.let { itemSchema ->
|
||||
array.forEachIndexed { i, el ->
|
||||
val itemTypeError = checkType(itemSchema.type, el)
|
||||
when {
|
||||
itemTypeError != null -> add("property '$key'[$i]: $itemTypeError")
|
||||
itemSchema.type == "object" && itemSchema.properties.isNotEmpty() ->
|
||||
validateProperty(itemSchema, el as JsonObject).forEach { add("property '$key'[$i]: $it") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate a nested object against a [JsonSchemaProperty] carrying its own shape — mirrors
|
||||
// [validateObject] but keyed off the property's fields (they diverge only in defaults).
|
||||
private fun validateProperty(property: JsonSchemaProperty, value: JsonObject): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
property.required.filterNot { it in value }.forEach { errors += "missing required property '$it'" }
|
||||
for ((key, element) in value) {
|
||||
val nested = property.properties[key]
|
||||
if (nested == null) {
|
||||
if (!property.additionalProperties && property.properties.isNotEmpty()) {
|
||||
errors += "unknown property '$key'"
|
||||
}
|
||||
} else {
|
||||
errors += checkProperty(key, nested, element)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
private fun checkType(type: String, value: JsonElement): String? {
|
||||
val ok = when (type) {
|
||||
"string" -> value is JsonPrimitive && value.isString
|
||||
|
||||
Reference in New Issue
Block a user