From a63b3eae2b5a62ee6803bfb4eee8fadbbba8e449 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 12 Jul 2026 23:15:54 +0100 Subject: [PATCH] Require independent evidence for verified findings --- .../v6/internal/subsystems/agent-lifecycle.md | 9 ++++++ .../v6/internal/subsystems/ai-runtime.md | 8 +++++ .../v6/internal/subsystems/api-contracts.md | 10 ++++++ .../internal/subsystems/storage-recovery.md | 7 ++++ .../api/action_result_v2_contract_test.go | 32 ++++++++++++++++++- ..._handlers_investigation_additional_test.go | 4 +-- internal/api/apt_workflow_integration_test.go | 4 +-- .../api/apt_workflows_colima_real_lab_test.go | 4 +-- internal/api/contract_test.go | 11 +++++++ internal/api/patrol_action_broker_test.go | 10 +++--- internal/api/patrol_action_reconciliation.go | 5 ++- .../patrol_autonomy_colima_real_lab_test.go | 2 +- 12 files changed, 92 insertions(+), 14 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index e20f777d4..d411f8a56 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -4480,3 +4480,12 @@ capabilities and readiness. The fake-only code/test floor for claims 16 and 17 is implemented; both claims and workflow scorecards remain below operational completion until browser proof, disposable Debian/Ubuntu lab proof, and Task 12 certification are complete. + +RG-06 and RG-09 keep the executing agent's fresh typed readback classified as +`agent_attested`. A separate Colima control-plane observation proves the lab +fixture changed, but it does not enter the authenticated product action result +and therefore cannot upgrade the product finding to `fix_verified`. Until a +distinct-trust-domain observation is ingested into `ActionResultV2`, those +findings remain `fix_verification_unknown` and unresolved. The Docker restart +journey, which does ingest its direct daemon observation as independent +evidence, remains the positive `fix_verified` control. diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 2ef1f3e9d..6c6fbd99d 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -5905,3 +5905,11 @@ to-reconciliation code/test floor only. Claims 16/17 and both APT scorecards remain below operational completion because browser workflow evidence, disposable Debian/Ubuntu real-lab evidence, and Task 12 final-SHA certification remain explicit residuals; no tier-5 or tier-6 evidence is claimed here. + +`fix_verified` is reserved for a canonical action result whose successful +execution has a confirmed independent postcondition. Agent-attested +confirmation remains useful evidence but projects to +`fix_verification_unknown`; investigation and finding records remain unresolved +and terminal push copy stays explicitly inconclusive. This keeps legacy +single-outcome consumers conservative while `ActionDisposition.ActionResultV2` +retains both truth axes and the evidence source. diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index f350c4b5c..a4cfabb62 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -7543,3 +7543,13 @@ workflow. Action list/detail continues to carry policy provenance, `ActionResultV2`, durable dispatch attempt, and receipt fields unchanged. Unit guards pin those projections and verify that APT presentation reads the shared `ActionAuditRecord`/`ActionResultV2` types directly. + +Patrol finding reconciliation treats `fix_verified` as an independently +confirmed postcondition, not a synonym for agent readback success. It requires +canonical execution `succeeded`, verification `confirmed`, and +`evidenceClass=independent`; normalization already requires bounded durable +evidence from a trust domain distinct from the executor. Confirmed +`agent_attested` truth remains visible in `ActionResultV2` and its compatibility +verification projection, but the finding maps to `fix_verification_unknown` +and remains unresolved. Malformed digests, missing evidence, and false +independence fail closed through canonical result normalization. diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 84c367bb8..24c0b7640 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -2079,6 +2079,13 @@ while storage detail drawers and filter controls must route summary series IDs, source tones, and disk metrics through the shared storage helpers instead of reconstructing them from local table state. +Storage and recovery consumers must not treat confirmed agent-attested action +readback as independently verified restoration. The canonical result retains +its confirmation and evidence class, while the legacy Patrol finding outcome +remains `fix_verification_unknown` unless bounded distinct-trust-domain +evidence is present. Only independently confirmed successful execution may +auto-resolve a finding as `fix_verified`. + Task 09 package-cache cleanup remains a bounded typed action over the fixed APT cache provider and requires fresh canonical reclaimable-byte and containing- filesystem pressure evidence. It accepts no path, package selector, diff --git a/internal/api/action_result_v2_contract_test.go b/internal/api/action_result_v2_contract_test.go index 1ba56c96d..a4cb4acce 100644 --- a/internal/api/action_result_v2_contract_test.go +++ b/internal/api/action_result_v2_contract_test.go @@ -85,7 +85,7 @@ func TestActionResultV2EventFindingContextTelemetryProjectionMatrix(t *testing.T expectedOutcome := aicontracts.OutcomeFixVerificationUnknown if execution == unified.ActionExecutionFailed || execution == unified.ActionExecutionNotRun { expectedOutcome = aicontracts.OutcomeFixFailed - } else if execution == unified.ActionExecutionSucceeded && verification == unified.ActionVerificationConfirmed { + } else if execution == unified.ActionExecutionSucceeded && verification == unified.ActionVerificationConfirmed && class == unified.ActionEvidenceIndependent { expectedOutcome = aicontracts.OutcomeFixVerified } else if execution == unified.ActionExecutionSucceeded && verification == unified.ActionVerificationContradicted { expectedOutcome = aicontracts.OutcomeFixVerificationFailed @@ -114,6 +114,36 @@ func TestActionResultV2EventFindingContextTelemetryProjectionMatrix(t *testing.T } } +func TestPatrolOutcomeRequiresIndependentEvidenceForFixVerified(t *testing.T) { + agentAttested := apiActionResultTruth(t, unified.ActionExecutionSucceeded, unified.ActionVerificationConfirmed, unified.ActionEvidenceAgentAttested) + independent := apiActionResultTruth(t, unified.ActionExecutionSucceeded, unified.ActionVerificationConfirmed, unified.ActionEvidenceIndependent) + falseIndependent := independent + falseIndependent.Verification.Evidence = append([]unified.ActionEvidence(nil), independent.Verification.Evidence...) + falseIndependent.Verification.Evidence[0].ObserverTrustDomain = falseIndependent.Verification.Evidence[0].ExecutorTrustDomain + badDigest := independent + badDigest.Verification.Evidence = append([]unified.ActionEvidence(nil), independent.Verification.Evidence...) + badDigest.Verification.Evidence[0].Digest = "sha256:" + strings.Repeat("0", 64) + + tests := []struct { + name string + truth unified.ActionResultV2 + want aicontracts.InvestigationOutcome + }{ + {name: "agent_attested", truth: agentAttested, want: aicontracts.OutcomeFixVerificationUnknown}, + {name: "independent", truth: independent, want: aicontracts.OutcomeFixVerified}, + {name: "false_independence", truth: falseIndependent, want: aicontracts.OutcomeFixVerificationUnknown}, + {name: "bad_digest", truth: badDigest, want: aicontracts.OutcomeFixVerificationUnknown}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + record := unified.ActionAuditRecord{State: unified.ActionStateCompleted, Result: &unified.ExecutionResult{ActionResultV2: &test.truth}} + if got := patrolOutcomeForActionAudit(record); got != test.want { + t.Fatalf("outcome=%q want=%q truth=%#v", got, test.want, test.truth) + } + }) + } +} + func TestActionResultV2JSONContractSnapshot(t *testing.T) { truth := apiActionResultTruth(t, unified.ActionExecutionSucceeded, unified.ActionVerificationConfirmed, unified.ActionEvidenceIndependent) payload, err := json.Marshal(AgentEventActionCompletedPayload{ActionID: "action-1", State: "completed", ActionResultV2: &truth}) diff --git a/internal/api/ai_handlers_investigation_additional_test.go b/internal/api/ai_handlers_investigation_additional_test.go index d39629d70..f29210061 100644 --- a/internal/api/ai_handlers_investigation_additional_test.go +++ b/internal/api/ai_handlers_investigation_additional_test.go @@ -253,8 +253,8 @@ func TestPatrolActionReconciliationCannotRegressFromOutOfOrderCallback(t *testin if got.Action == nil || got.Action.ActionID != "act-1" || got.Action.State != string(unifiedresources.ActionStateCompleted) { t.Fatalf("reconciled action = %#v, want authoritative completed action", got.Action) } - if got.Outcome != aicontracts.OutcomeFixVerified { - t.Fatalf("outcome = %q, want %q", got.Outcome, aicontracts.OutcomeFixVerified) + if got.Outcome != aicontracts.OutcomeFixVerificationUnknown { + t.Fatalf("outcome = %q, want %q", got.Outcome, aicontracts.OutcomeFixVerificationUnknown) } } diff --git a/internal/api/apt_workflow_integration_test.go b/internal/api/apt_workflow_integration_test.go index 97ad87055..afd72f05c 100644 --- a/internal/api/apt_workflow_integration_test.go +++ b/internal/api/apt_workflow_integration_test.go @@ -289,7 +289,7 @@ func runAPTWorkflowFindingJourney(t *testing.T, resources *ResourceHandlers, fin t.Fatalf("terminal action truth=%#v", audit.Result) } reconciled := patrol.GetFindings().Get(finding.ID) - if reconciled == nil || reconciled.ResolvedAt == nil || reconciled.InvestigationOutcome != string(aicontracts.OutcomeFixVerified) { + if reconciled == nil || reconciled.ResolvedAt != nil || reconciled.InvestigationOutcome != string(aicontracts.OutcomeFixVerificationUnknown) { t.Fatalf("reconciled finding=%#v", reconciled) } var notification relay.PushNotificationPayload @@ -371,7 +371,7 @@ func runAPTWorkflowReceiptRecoveryJourney(t *testing.T, resource unified.Resourc t.Fatalf("requests=%d queries=%d; recovery must query once without resend", requests, queries) } reconciled := patrol.GetFindings().Get(finding.ID) - if reconciled == nil || reconciled.ResolvedAt == nil || reconciled.InvestigationOutcome != string(aicontracts.OutcomeFixVerified) { + if reconciled == nil || reconciled.ResolvedAt != nil || reconciled.InvestigationOutcome != string(aicontracts.OutcomeFixVerificationUnknown) { t.Fatalf("reconciled finding=%#v", reconciled) } } diff --git a/internal/api/apt_workflows_colima_real_lab_test.go b/internal/api/apt_workflows_colima_real_lab_test.go index de470e9ea..7421df6a0 100644 --- a/internal/api/apt_workflows_colima_real_lab_test.go +++ b/internal/api/apt_workflows_colima_real_lab_test.go @@ -372,7 +372,7 @@ func runRG09PositiveWorkflow(t *testing.T, dataPath string, resource unified.Res t.Fatalf("dispatch receipt found=%v err=%v receipt=%#v", found, err, receipt) } reconciled := patrol.GetFindings().Get(finding.ID) - if reconciled == nil || reconciled.ResolvedAt == nil || reconciled.InvestigationOutcome != string(aicontracts.OutcomeFixVerified) { + if reconciled == nil || reconciled.ResolvedAt != nil || reconciled.InvestigationOutcome != string(aicontracts.OutcomeFixVerificationUnknown) { t.Fatalf("finding reconciliation=%#v", reconciled) } projection, err := json.Marshal(struct { @@ -387,7 +387,7 @@ func runRG09PositiveWorkflow(t *testing.T, dataPath string, resource unified.Res t.Fatalf("raw APT detail %q escaped audit/finding projection: %s", forbidden, projection) } } - return rg09WorkflowProof{ActionID: audit.ID, AttemptID: attempt.ID, DispatchCount: attempt.DispatchCount, EvidenceClass: truth.Verification.EvidenceClass, FindingID: finding.ID, FindingOutcome: reconciled.InvestigationOutcome, Resolved: true} + return rg09WorkflowProof{ActionID: audit.ID, AttemptID: attempt.ID, DispatchCount: attempt.DispatchCount, EvidenceClass: truth.Verification.EvidenceClass, FindingID: finding.ID, FindingOutcome: reconciled.InvestigationOutcome, Resolved: false} } func rg09UpdateDriftBarrier(t *testing.T, server *rg09Server, agentID, containerID, expectedHash, distro string) map[string]any { diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index 5f9608cac..bbf129f85 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -20852,3 +20852,14 @@ func TestContract_PatrolActionBrokerKeepsPolicyExecutionCoreOwned(t *testing.T) } } } + +func TestContract_PatrolFixVerifiedRequiresIndependentEvidence(t *testing.T) { + source, err := os.ReadFile("patrol_action_reconciliation.go") + if err != nil { + t.Fatalf("read patrol_action_reconciliation.go: %v", err) + } + pattern := regexp.MustCompile(`(?s)case unifiedresources\.ActionVerificationConfirmed:\s+if truth\.Verification\.EvidenceClass == unifiedresources\.ActionEvidenceIndependent \{\s+return aicontracts\.OutcomeFixVerified\s+\}\s+return aicontracts\.OutcomeFixVerificationUnknown`) + if !pattern.Match(source) { + t.Fatal("fix_verified must require canonical independent verification; agent-attested confirmation must remain verification unknown") + } +} diff --git a/internal/api/patrol_action_broker_test.go b/internal/api/patrol_action_broker_test.go index 133289bd3..e04b17264 100644 --- a/internal/api/patrol_action_broker_test.go +++ b/internal/api/patrol_action_broker_test.go @@ -628,11 +628,11 @@ func TestPatrolTypedActionJourneyDetectPlanApproveExecuteVerifyAndReconcile(t *t if completed.Action == nil || completed.Action.State != string(unified.ActionStateCompleted) { t.Fatalf("completed investigation action = %#v", completed.Action) } - if completed.Outcome != aicontracts.OutcomeFixVerified { - t.Fatalf("completed outcome = %q, want verified", completed.Outcome) + if completed.Outcome != aicontracts.OutcomeFixVerificationUnknown { + t.Fatalf("completed outcome = %q, want verification unknown", completed.Outcome) } updatedFinding := patrol.GetFindings().Get(finding.ID) - if updatedFinding == nil || updatedFinding.InvestigationOutcome != string(aicontracts.OutcomeFixVerified) || updatedFinding.ResolvedAt == nil { + if updatedFinding == nil || updatedFinding.InvestigationOutcome != string(aicontracts.OutcomeFixVerificationUnknown) || updatedFinding.ResolvedAt != nil { t.Fatalf("reconciled finding = %#v", updatedFinding) } store, err := resources.getStore("default") @@ -648,10 +648,10 @@ func TestPatrolTypedActionJourneyDetectPlanApproveExecuteVerifyAndReconcile(t *t } select { case push := <-pushes: - if push.ActionType != relay.PushActionViewFixResult || push.ActionID != finding.ID || push.Body != "Action completed and verified" { + if push.ActionType != relay.PushActionViewFixResult || push.ActionID != finding.ID || push.Body != "Action completed; verification was inconclusive" { t.Fatalf("terminal push = %#v", push) } default: - t.Fatal("verified lifecycle did not publish a terminal mobile notification") + t.Fatal("agent-attested lifecycle did not publish an honest terminal mobile notification") } } diff --git a/internal/api/patrol_action_reconciliation.go b/internal/api/patrol_action_reconciliation.go index e01edae54..4d2c2ef70 100644 --- a/internal/api/patrol_action_reconciliation.go +++ b/internal/api/patrol_action_reconciliation.go @@ -158,7 +158,10 @@ func patrolOutcomeForActionAudit(audit unifiedresources.ActionAuditRecord) aicon } switch truth.Verification.Status { case unifiedresources.ActionVerificationConfirmed: - return aicontracts.OutcomeFixVerified + if truth.Verification.EvidenceClass == unifiedresources.ActionEvidenceIndependent { + return aicontracts.OutcomeFixVerified + } + return aicontracts.OutcomeFixVerificationUnknown case unifiedresources.ActionVerificationContradicted: return aicontracts.OutcomeFixVerificationFailed default: diff --git a/internal/api/patrol_autonomy_colima_real_lab_test.go b/internal/api/patrol_autonomy_colima_real_lab_test.go index b58f04921..719ac7316 100644 --- a/internal/api/patrol_autonomy_colima_real_lab_test.go +++ b/internal/api/patrol_autonomy_colima_real_lab_test.go @@ -312,7 +312,7 @@ reportReady: t.Fatalf("positive authority binding audit=%#v", audit) } reconciled := patrol.GetFindings().Get(positiveFinding.ID) - if reconciled == nil || reconciled.ResolvedAt == nil || reconciled.InvestigationOutcome != string(aicontracts.OutcomeFixVerified) { + if reconciled == nil || reconciled.ResolvedAt != nil || reconciled.InvestigationOutcome != string(aicontracts.OutcomeFixVerificationUnknown) { t.Fatalf("positive finding reconciliation=%#v", reconciled) } cacheAfterPositive := rg06CacheState(t, containerID)