mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Clamp Patrol finding chat handoffs to approval mode
This commit is contained in:
@@ -1099,6 +1099,11 @@ execution mode is AI/API-owned. Request-scoped `AutonomousMode:false` and
|
||||
`RequireCommandApproval:true` on `/api/ai/investigate-alert` are Assistant
|
||||
action-governance facts, not agent install readiness, command reachability, or
|
||||
fleet-control capability signals.
|
||||
Patrol finding chat handoffs follow the same ownership split: when
|
||||
`/api/ai/chat` resolves a `finding_id` into model-only Patrol briefing,
|
||||
resource, or action context, the backend-enforced `autonomous_mode:false`
|
||||
clamp is Assistant action-governance, not agent readiness, fleet command
|
||||
reachability, or enrollment health.
|
||||
That same shared `internal/api/` dependency also now assumes SSO test and
|
||||
metadata-preview routes fail closed on validated outbound URL handling.
|
||||
Lifecycle-adjacent setup and hosted bootstrap surfaces may depend on those
|
||||
|
||||
@@ -264,6 +264,13 @@ runtime cost control, and shared AI transport surfaces.
|
||||
Proposed-fix command text must stay out of both the persisted chat message
|
||||
and the model-only handoff context, and command payloads remain
|
||||
approval-context data, not conversational copy.
|
||||
`/api/ai/chat` must also clamp Patrol finding handoffs that resolve to
|
||||
model-only briefing, resource, or action context to approval-required mode
|
||||
by forcing the request-local autonomous-mode override to false, even when a
|
||||
caller supplied `autonomous_mode:true`. That clamp belongs to the
|
||||
backend/API execution boundary, does not mutate the user's persistent AI
|
||||
control setting, and prevents product-originated Patrol action context from
|
||||
becoming silent command authority.
|
||||
The Assistant drawer may also render an attached context briefing for that
|
||||
handoff, but the briefing is runtime context visibility only: it must not
|
||||
mutate chat control settings, execute tools, or reveal raw command payloads.
|
||||
|
||||
@@ -1831,6 +1831,13 @@ overrides. Dashboard Pulse Brief and other scoped handoffs may include
|
||||
execution for that exchange, but the transport must treat the field as a
|
||||
request override only and must not mutate the user's persistent AI control
|
||||
setting.
|
||||
Patrol finding handoffs are stricter than ordinary chat requests: when
|
||||
`finding_id` resolves to model-only Patrol briefing, resource, or action
|
||||
context, `internal/api/ai_handler.go` must clamp the request-local autonomous
|
||||
mode to false even if the caller supplied `autonomous_mode:true`. That
|
||||
server-side clamp is part of the public API contract because the frontend
|
||||
handoff setting is only advisory unless the backend preserves the
|
||||
approval-required boundary.
|
||||
That same backend API boundary now also owns the negative space around
|
||||
assistant control. Wiring native TrueNAS app actions into
|
||||
`internal/api/router.go`, `internal/api/ai_handler.go`, or adjacent backend
|
||||
|
||||
@@ -398,6 +398,11 @@ bypass the API fail-closed execution gate.
|
||||
`RequireCommandApproval:true` are AI action-governance constraints, not
|
||||
storage/recovery restore approval, recovery freshness, or storage diagnostic
|
||||
payload semantics.
|
||||
Patrol finding chat handoff execution controls in `internal/api/ai_handler.go`
|
||||
follow the same boundary: backend-forced `autonomous_mode:false` for
|
||||
`finding_id` handoffs with model-only Patrol briefing, resource, or action
|
||||
context is Assistant action-governance, not a storage/recovery approval,
|
||||
recovery freshness, or restore-command signal.
|
||||
That same adjacent boundary also keeps the retired Patrol quickstart
|
||||
contract out of storage/recovery ownership: shared AI handlers no longer
|
||||
expose active quickstart credit, token, or hosted-model provider state, and
|
||||
|
||||
@@ -783,6 +783,17 @@ type ChatRequest struct {
|
||||
AutonomousMode *bool `json:"autonomous_mode,omitempty"`
|
||||
}
|
||||
|
||||
func chatAutonomousModeForFindingHandoff(requested *bool, findingID, handoffContext string, handoffResources []chat.HandoffResource, handoffActions []chat.HandoffAction) *bool {
|
||||
if strings.TrimSpace(findingID) == "" {
|
||||
return requested
|
||||
}
|
||||
if strings.TrimSpace(handoffContext) == "" && len(handoffResources) == 0 && len(handoffActions) == 0 {
|
||||
return requested
|
||||
}
|
||||
approvalRequired := false
|
||||
return &approvalRequired
|
||||
}
|
||||
|
||||
const findingChatContextListLimit = 5
|
||||
|
||||
type unifiedFindingLookup interface {
|
||||
@@ -2152,7 +2163,7 @@ func (h *AIHandler) HandleChat(w http.ResponseWriter, r *http.Request) {
|
||||
HandoffContext: handoffContext,
|
||||
HandoffResources: handoffResources,
|
||||
HandoffActions: handoffActions,
|
||||
AutonomousMode: req.AutonomousMode,
|
||||
AutonomousMode: chatAutonomousModeForFindingHandoff(req.AutonomousMode, findingID, handoffContext, handoffResources, handoffActions),
|
||||
}, func(event chat.StreamEvent) {
|
||||
if event.Type == "done" {
|
||||
serviceSentDone = true
|
||||
|
||||
@@ -727,6 +727,9 @@ func TestHandleChat_IncludesInvestigationRecordContext(t *testing.T) {
|
||||
reqArg := args.Get(1).(chat.ExecuteRequest)
|
||||
assert.Equal(t, "finding-123", reqArg.FindingID)
|
||||
assert.Equal(t, "What happened?", reqArg.Prompt)
|
||||
if assert.NotNil(t, reqArg.AutonomousMode) {
|
||||
assert.False(t, *reqArg.AutonomousMode)
|
||||
}
|
||||
assert.Contains(t, reqArg.HandoffContext, "[Operator Briefing]")
|
||||
assert.Contains(t, reqArg.HandoffContext, "Briefing Source: Pulse Patrol structured finding")
|
||||
assert.Contains(t, reqArg.HandoffContext, "Finding: High CPU usage (critical, performance, active)")
|
||||
@@ -809,7 +812,7 @@ func TestHandleChat_IncludesInvestigationRecordContext(t *testing.T) {
|
||||
assert.NotContains(t, fmt.Sprintf("%#v", reqArg.HandoffActions), "systemctl restart workload.service")
|
||||
})
|
||||
|
||||
body := `{"prompt":"What happened?","finding_id":"finding-123"}`
|
||||
body := `{"prompt":"What happened?","finding_id":"finding-123","autonomous_mode":true}`
|
||||
req := httptest.NewRequest("POST", "/api/ai/chat", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ func TestContract_AssistantFindingContextUsesModelOnlyHandoff(t *testing.T) {
|
||||
"handoffContext = buildUnifiedFindingChatContext(f, store, handoffActions)",
|
||||
"handoffResources = buildUnifiedFindingHandoffResources(f, store)",
|
||||
"livePatrolApprovalForFinding(f.ID, orgID)",
|
||||
"func chatAutonomousModeForFindingHandoff(requested *bool, findingID, handoffContext string, handoffResources []chat.HandoffResource, handoffActions []chat.HandoffAction) *bool",
|
||||
"AutonomousMode: chatAutonomousModeForFindingHandoff(req.AutonomousMode, findingID, handoffContext, handoffResources, handoffActions)",
|
||||
`appendChatContextLine(&b, "Finding Status", unifiedFindingChatStatus(f, time.Now()))`,
|
||||
`appendChatContextLine(&b, "Finding Detected At", f.DetectedAt.Format(time.RFC3339))`,
|
||||
`appendChatContextLine(&b, "Finding Last Seen At", f.LastSeenAt.Format(time.RFC3339))`,
|
||||
|
||||
Reference in New Issue
Block a user