Require independent evidence for verified findings

This commit is contained in:
rcourtman
2026-07-12 23:15:54 +01:00
parent 7d772acff3
commit a63b3eae2b
12 changed files with 92 additions and 14 deletions
@@ -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.
@@ -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.
@@ -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.
@@ -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,
+31 -1
View File
@@ -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})
@@ -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)
}
}
@@ -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)
}
}
@@ -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 {
+11
View File
@@ -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")
}
}
+5 -5
View File
@@ -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")
}
}
+4 -1
View File
@@ -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:
@@ -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)