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:
@@ -53,6 +53,8 @@ const (
|
||||
TypeRouterNarration = "router.narration"
|
||||
TypeArtifactList = "artifact.list"
|
||||
TypeConfigSnapshot = "config.snapshot"
|
||||
TypeProjectProfile = "project_profile.snapshot"
|
||||
TypeOperatorProfile = "operator_profile.snapshot"
|
||||
TypeSessionStats = "session.stats"
|
||||
TypeIdeaList = "idea.list"
|
||||
TypeHealthChecks = "health.checks"
|
||||
@@ -139,6 +141,15 @@ type ServerMessage struct {
|
||||
ConfigRestartRequired []string `json:"restartRequired"`
|
||||
ConfigError *string `json:"error"`
|
||||
|
||||
// project_profile.snapshot / operator_profile.snapshot (About/Conventions shared; Commands is
|
||||
// project-only, ApprovalMode/PreferredModels/ProposedAdaptation are operator-only)
|
||||
About string `json:"about"`
|
||||
Conventions []string `json:"conventions"`
|
||||
Commands map[string]string `json:"commands"`
|
||||
ApprovalMode string `json:"approvalMode"`
|
||||
PreferredModels []string `json:"preferredModels"`
|
||||
ProposedAdaptation *string `json:"proposedAdaptation"`
|
||||
|
||||
// session.stats — derived metrics for a session (reply to GetSessionStats)
|
||||
Stats *StatsDto `json:"stats"`
|
||||
|
||||
@@ -334,8 +345,9 @@ type ApprovalDto struct {
|
||||
}
|
||||
|
||||
type StageToolDecl struct {
|
||||
StageID string `json:"stageId"`
|
||||
Tools []ToolDecl `json:"tools"`
|
||||
StageID string `json:"stageId"`
|
||||
Tools []ToolDecl `json:"tools"`
|
||||
TokenBudget *int `json:"tokenBudget"`
|
||||
}
|
||||
|
||||
type ToolDecl struct {
|
||||
@@ -460,6 +472,38 @@ func UpdateConfig(patch map[string]string) []byte {
|
||||
return encode("UpdateConfig", map[string]any{"patch": patch})
|
||||
}
|
||||
|
||||
// GetProjectProfile requests the ProjectProfile bound to a session's workspace (replied to with
|
||||
// a project_profile.snapshot).
|
||||
func GetProjectProfile(sessionID string) []byte {
|
||||
return encode("GetProjectProfile", map[string]any{"sessionId": sessionID})
|
||||
}
|
||||
|
||||
// UpdateProjectProfile overwrites the ProjectProfile at a session's workspace root.
|
||||
func UpdateProjectProfile(sessionID, about string, conventions []string, commands map[string]string) []byte {
|
||||
return encode("UpdateProjectProfile", map[string]any{
|
||||
"sessionId": sessionID,
|
||||
"about": about,
|
||||
"conventions": conventions,
|
||||
"commands": commands,
|
||||
})
|
||||
}
|
||||
|
||||
// GetOperatorProfile requests the operator's OperatorProfile (replied to with an
|
||||
// operator_profile.snapshot). Not session-scoped.
|
||||
func GetOperatorProfile() []byte {
|
||||
return encode("GetOperatorProfile", map[string]any{})
|
||||
}
|
||||
|
||||
// UpdateOperatorProfile overwrites the OperatorProfile.
|
||||
func UpdateOperatorProfile(about, approvalMode string, preferredModels, conventions []string) []byte {
|
||||
return encode("UpdateOperatorProfile", map[string]any{
|
||||
"about": about,
|
||||
"approvalMode": approvalMode,
|
||||
"preferredModels": preferredModels,
|
||||
"conventions": conventions,
|
||||
})
|
||||
}
|
||||
|
||||
// ApprovalResponse answers an approval gate. decision is "APPROVE" or "REJECT".
|
||||
func ApprovalResponse(requestID, decision string, steeringNote *string) []byte {
|
||||
return encode("ApprovalResponse", map[string]any{
|
||||
|
||||
Reference in New Issue
Block a user