Persist Patrol proposal evidence in action origins

This commit is contained in:
rcourtman 2026-07-21 23:57:43 +01:00
parent b63fc0a23d
commit b8e78fa9c2
5 changed files with 29 additions and 4 deletions

View file

@ -938,6 +938,13 @@ must not be projected into the provider child process. Any future design that
distributes subscription CLI state or authentication through an agent requires
a separate agent-lifecycle contract change and verification.
Patrol investigation evidence copied by
`internal/api/patrol_action_broker.go` into an action's broker-owned origin is
API/AI audit provenance only. Tool-use evidence IDs do not identify an agent,
grant an agent scope, authorize a command channel, or replace live agent
readiness and dispatch admission. Any agent-mediated action still crosses the
existing lifecycle identity, authorization, and receipt boundaries.
First-run security discovery under shared `internal/api/` is likewise an
adjacent API/security boundary, not an agent-lifecycle discovery surface.
Unauthenticated `/api/security/status` responses must stay on the public tier

View file

@ -1955,7 +1955,8 @@ a new API state machine, queue contract, or verification-accounting field.
`aicontracts.OrchestratorActionBroker` contract as a tenant-bound proposal
adapter. It stamps the fixed `pulse_patrol` requestedBy actor
and a broker-owned `ActionOrigin` (surface/finding/investigation/proposal
IDs) through the service's internal `PlanWithOptions`; the public plan
IDs plus the orchestrator-observed investigation evidence IDs) through the
service's internal `PlanWithOptions`; the public plan
endpoint keeps calling plain `Plan`, so an HTTP request body can never
claim a first-party origin. Enterprise still has no decision or execution
method. After planning, core may auto-authorize only when the capability's
@ -1990,7 +1991,12 @@ a new API state machine, queue contract, or verification-accounting field.
can reconcile decisions and outcomes onto the correct tenant. A
Patrol proposal must carry both its finding and investigation IDs
before anything persists; identity-less proposals are refused so a
governed action can never lose deterministic correlation.
governed action can never lose deterministic correlation. Enterprise binds
proposal evidence from the bounded, non-terminal tool-use events it
observed for that run; the broker copies those IDs into `ActionOrigin`, and
the lifecycle normalizes and de-duplicates them before persistence. Model
output cannot mint this audit provenance, and the IDs grant no planning,
approval, or execution authority.
Authority proof: `TestContract_PatrolActionBrokerKeepsPolicyExecutionCoreOwned` in
`internal/api/contract_test.go`.
The proposal enters that broker from the investigation-only

View file

@ -147,6 +147,13 @@ governed tool and verification boundaries. CLI authentication state must not be
included in recovery exports, snapshots, or support bundles, and must not be
interpreted as recovery readiness.
Patrol investigation evidence copied by
`internal/api/patrol_action_broker.go` into broker-owned action origin is
correlation provenance, not storage or recovery proof. A tool-use evidence ID
must not be reinterpreted as backup freshness, protection coverage, restore
readiness, or verification truth; those claims continue to require their
domain-owned typed evidence and deterministic verification paths.
Unified Agent durable-operation-receipt metadata is likewise an internal
monitoring and agent-lifecycle boundary. The exact current protocol may be a
fail-closed prerequisite when unified resources derive the bounded

View file

@ -167,6 +167,7 @@ func (b *patrolActionBroker) Submit(ctx context.Context, proposal aicontracts.Ac
FindingID: proposal.FindingID,
InvestigationID: proposal.InvestigationID,
ProposalID: proposal.ProposalID,
EvidenceIDs: append([]string(nil), proposal.EvidenceIDs...),
},
PolicyFactors: policyFactors,
})

View file

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
@ -231,7 +232,9 @@ func TestPatrolActionBrokerSubmitPlansThroughCanonicalLifecycle(t *testing.T) {
h, executor := newPatrolBrokerTestHandlers(t, unified.ApprovalAdmin)
broker := NewPatrolActionBroker("default", h)
disposition, err := broker.Submit(context.Background(), patrolTestProposal())
proposal := patrolTestProposal()
proposal.EvidenceIDs = []string{" evidence-2 ", "evidence-1", "evidence-1", ""}
disposition, err := broker.Submit(context.Background(), proposal)
if err != nil {
t.Fatalf("Submit: %v", err)
}
@ -263,7 +266,8 @@ func TestPatrolActionBrokerSubmitPlansThroughCanonicalLifecycle(t *testing.T) {
record.Origin.Surface != patrolActionOriginSurface ||
record.Origin.FindingID != "finding-1" ||
record.Origin.InvestigationID != "inv-1" ||
record.Origin.ProposalID != "prop-1" {
record.Origin.ProposalID != "prop-1" ||
fmt.Sprint(record.Origin.EvidenceIDs) != "[evidence-1 evidence-2]" {
t.Fatalf("audit origin = %#v", record.Origin)
}
}