Why: Explicit low IDs bypassed the implicit sequence allocator and could commit after a higher audit page cutoff. Evidence: Isolated PostgreSQL RED receipt for cross-page omission; local GREEN for explicit and default writes, trigger drift, isolation, mutation denial, offline copy rollback, retry, and sequence non-rewind. Fixed-HEAD receipts follow. Impact: Add v34 all-writer and append-only guards; reject unsafe strong reads; make command audit storage migration exclusive and atomic with guard restoration and a forward-only sequence. Refs: Approved W9 all-writers cutoff adjustment r1; W9 Backend task card.
162 lines
6.7 KiB
Go
162 lines
6.7 KiB
Go
package extension
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
core "agent-system-core"
|
|
)
|
|
|
|
// ProjectAuditAuthorizer answers the domain's audit.read decision for the
|
|
// authenticated actor and a project identified by trusted host storage.
|
|
// A project ID supplied by a caller is a locator, never an authorization.
|
|
type ProjectAuditAuthorizer func(actor string, projectID core.ID) (bool, error)
|
|
|
|
// ProjectAuditRegistry is optional so older extension registries remain valid.
|
|
// Only an in-process domain plugin can register this callback.
|
|
type ProjectAuditRegistry interface {
|
|
RegisterProjectAuditAuthorizer(ProjectAuditAuthorizer) error
|
|
}
|
|
|
|
var (
|
|
ErrProjectAuditDenied = errors.New("extension: project audit denied")
|
|
ErrProjectAuditUnsupported = errors.New("extension: project audit unsupported")
|
|
ErrProjectAuditLinkConflict = errors.New("extension: project audit link conflict")
|
|
ErrProjectAuditPending = errors.New("extension: committed host audit receipt pending")
|
|
)
|
|
|
|
// ProjectAuditLinkRequest binds one host command receipt to an already
|
|
// committed domain fact. Owner is never caller supplied; the host handle and
|
|
// stored audit row establish it. HostAuditOffset=0 asks the host to resolve a
|
|
// unique committed row by its trusted owner/project/OperationID, which lets an
|
|
// outbox wait for the bus audit commit after the domain handler returns. This
|
|
// first slice supports domain-commit only.
|
|
type ProjectAuditLinkRequest struct {
|
|
ProjectID core.ID `json:"projectId"`
|
|
HostAuditOffset int64 `json:"hostAuditOffset"`
|
|
SourceKind string `json:"sourceKind"`
|
|
SourceID string `json:"sourceId"`
|
|
OperationID string `json:"operationId"`
|
|
ResourceKind string `json:"resourceKind"`
|
|
ResourceID string `json:"resourceId"`
|
|
RunID string `json:"runId,omitempty"`
|
|
DisclosureClass string `json:"disclosureClass"`
|
|
IdempotencyKey string `json:"idempotencyKey"`
|
|
}
|
|
|
|
// ProjectAuditLinkProof contains the caller request plus identity and source
|
|
// fields re-read from trusted host storage. The domain verifier must check its
|
|
// committed source event and operation/resource attribution, not a model claim.
|
|
type ProjectAuditLinkProof struct {
|
|
ProjectAuditLinkRequest
|
|
Owner string
|
|
}
|
|
|
|
type ProjectAuditLinkVerifier func(context.Context, ProjectAuditLinkProof) (bool, error)
|
|
|
|
// ProjectAuditLinkedResource is the stored, immutable link presented to the
|
|
// current domain disclosure policy. A caller ID alone never authorizes it.
|
|
type ProjectAuditLinkedResource struct {
|
|
ProjectID core.ID
|
|
Owner string
|
|
SourceKind string
|
|
SourceID string
|
|
OperationID string
|
|
ResourceKind string
|
|
ResourceID string
|
|
RunID string
|
|
DisclosureClass string
|
|
}
|
|
|
|
type ProjectAuditDisclosure struct {
|
|
Metadata bool
|
|
Body bool
|
|
}
|
|
|
|
type ProjectAuditDisclosureAuthorizer func(context.Context, string, ProjectAuditLinkedResource) (ProjectAuditDisclosure, error)
|
|
|
|
// ProjectAuditPublisherFactory is issued only by successful verifier
|
|
// registration. A domain outbox may retain it and bind an owner for each job;
|
|
// owner and committed-source attribution are checked again when publishing.
|
|
// NewScopedHost(owner, pluginID) cannot mint this capability.
|
|
type ProjectAuditPublisherFactory interface {
|
|
ForOwner(owner string) (ProjectAuditPublisher, error)
|
|
}
|
|
|
|
// ProjectAuditLinkRegistry is optional. Only the registering compiled domain
|
|
// plugin receives a publisher factory; string plugin IDs carry no authority.
|
|
type ProjectAuditLinkRegistry interface {
|
|
RegisterProjectAuditLinkVerifier(ProjectAuditLinkVerifier) (ProjectAuditPublisherFactory, error)
|
|
RegisterProjectAuditDisclosureAuthorizer(ProjectAuditDisclosureAuthorizer) error
|
|
}
|
|
|
|
type ProjectAuditLinkReceipt struct {
|
|
LinkID int64 `json:"linkId"`
|
|
HostAuditOffset int64 `json:"hostAuditOffset"`
|
|
}
|
|
|
|
type ProjectAuditPublisher interface {
|
|
PublishProjectAuditLink(context.Context, ProjectAuditLinkRequest) (ProjectAuditLinkReceipt, error)
|
|
}
|
|
|
|
// ProjectAuditQuery reads one project-bound command-audit slice. Offset is a
|
|
// host source offset, not a cross-domain event sequence. A nonzero BeforeOffset
|
|
// must be paired with the CutoffOffset returned on the first page. PostgreSQL
|
|
// pages are served only while the installed generator, sequence, and all-writer
|
|
// database guards prove a
|
|
// committed prefix; otherwise the host rejects the read instead of claiming a
|
|
// fixed snapshot. See Coverage.CutoffConsistency.
|
|
type ProjectAuditQuery struct {
|
|
ProjectID core.ID `json:"projectId"`
|
|
BeforeOffset int64 `json:"beforeOffset,omitempty"`
|
|
CutoffOffset int64 `json:"cutoffOffset,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
// Linked* select one exact committed link. This avoids enumerating hidden
|
|
// private links or leaking their counts before cursor policy is complete.
|
|
LinkedSourceKind string `json:"linkedSourceKind,omitempty"`
|
|
LinkedSourceID string `json:"linkedSourceId,omitempty"`
|
|
LinkedHostOffset int64 `json:"linkedHostOffset,omitempty"`
|
|
}
|
|
|
|
// HostAuditReceipt is a server-projected receipt, never a raw audit payload.
|
|
// ActorName is the name recorded with this command; old rows have no snapshot.
|
|
type HostAuditReceipt struct {
|
|
SourceOffset int64 `json:"sourceOffset"`
|
|
Time time.Time `json:"time"`
|
|
ActorID string `json:"actorId,omitempty"`
|
|
ActorName string `json:"actorName,omitempty"`
|
|
ActorNameStatus string `json:"actorNameStatus"`
|
|
CommandID string `json:"commandId"`
|
|
State string `json:"state"`
|
|
OperationID string `json:"operationId,omitempty"`
|
|
ConversationID string `json:"conversationId"`
|
|
}
|
|
|
|
// ProjectAuditCoverage states the exact scope of this first X2 slice. Missing
|
|
// host sources and old unbound rows cannot be inferred from owner-wide audit.
|
|
type ProjectAuditCoverage struct {
|
|
CommandAudit string `json:"commandAudit"`
|
|
CutoffConsistency string `json:"cutoffConsistency"`
|
|
CrossMember string `json:"crossMember"`
|
|
LegacyProjectBinding string `json:"legacyProjectBinding"`
|
|
RunRelations string `json:"runRelations"`
|
|
ModelReceipts string `json:"modelReceipts"`
|
|
BackgroundReceipts string `json:"backgroundReceipts"`
|
|
ResourceBodies string `json:"resourceBodies"`
|
|
}
|
|
|
|
type ProjectAuditPage struct {
|
|
Receipts []HostAuditReceipt `json:"receipts"`
|
|
NextOffset int64 `json:"nextOffset,omitempty"`
|
|
CutoffOffset int64 `json:"cutoffOffset"`
|
|
Coverage ProjectAuditCoverage `json:"coverage"`
|
|
}
|
|
|
|
// ProjectAuditHost is an optional, actor-bound read capability. A consumer must
|
|
// treat a failed type assertion as unsupported; an authorized page still reports
|
|
// partial coverage explicitly.
|
|
type ProjectAuditHost interface {
|
|
QueryProjectAudit(context.Context, ProjectAuditQuery) (ProjectAuditPage, error)
|
|
}
|