Why: Project audit cannot use owner-wide logs or current conversation project state to establish trusted attribution. Evidence: RED cases were observed locally; go test ./pkg/extension ./internal/store ./internal/app -count=1 passed on this tree (SELF). Impact: Add optional X2 read and audit.read callback, immutable project/name snapshots, safe owner-only projection, and explicit unsupported coverage. Cross-member publication remains pending. Refs: Stage 5 W9; Stage 6 X2 public extension request
104 lines
3.5 KiB
Go
104 lines
3.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
|
|
"agent-system-backend/pkg/extension"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
// projectAuditSlot binds the audit.read policy to its registering domain plugin.
|
|
// A different plugin cannot borrow that policy through its own Host handle.
|
|
type projectAuditSlot struct {
|
|
mu sync.RWMutex
|
|
holder string
|
|
check extension.ProjectAuditAuthorizer
|
|
}
|
|
|
|
func (s *projectAuditSlot) register(pluginID string, check extension.ProjectAuditAuthorizer) error {
|
|
if pluginID == "" || check == nil {
|
|
return extension.ErrProjectAuditUnsupported
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.check != nil {
|
|
return errors.New("extension: project audit authorizer already registered")
|
|
}
|
|
s.holder, s.check = pluginID, check
|
|
return nil
|
|
}
|
|
|
|
func (r compiledRegistry) RegisterProjectAuditAuthorizer(extension.ProjectAuditAuthorizer) error {
|
|
return errors.New("extension: declarative plugins cannot register project audit authorizers")
|
|
}
|
|
|
|
func (r pluginRegistry) RegisterProjectAuditAuthorizer(check extension.ProjectAuditAuthorizer) error {
|
|
if r.caps.projectAudit == nil {
|
|
return extension.ErrProjectAuditUnsupported
|
|
}
|
|
return r.caps.projectAudit.register(r.pluginID, check)
|
|
}
|
|
|
|
func (r recordRegistry) RegisterProjectAuditAuthorizer(check extension.ProjectAuditAuthorizer) error {
|
|
reg, ok := r.Registry.(extension.ProjectAuditRegistry)
|
|
if !ok {
|
|
return extension.ErrProjectAuditUnsupported
|
|
}
|
|
return reg.RegisterProjectAuditAuthorizer(check)
|
|
}
|
|
|
|
var _ extension.ProjectAuditHost = inprocHost{}
|
|
|
|
func (h inprocHost) QueryProjectAudit(ctx context.Context, q extension.ProjectAuditQuery) (extension.ProjectAuditPage, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return extension.ProjectAuditPage{}, err
|
|
}
|
|
if h.actor == "" || h.actor != h.owner || q.ProjectID == "" {
|
|
return extension.ProjectAuditPage{}, extension.ErrProjectAuditDenied
|
|
}
|
|
if h.caps.projectAudit == nil || h.caps.st == nil {
|
|
return extension.ProjectAuditPage{}, extension.ErrProjectAuditUnsupported
|
|
}
|
|
slot := h.caps.projectAudit
|
|
slot.mu.RLock()
|
|
holder, check := slot.holder, slot.check
|
|
slot.mu.RUnlock()
|
|
if holder != h.pluginID || check == nil {
|
|
return extension.ProjectAuditPage{}, extension.ErrProjectAuditUnsupported
|
|
}
|
|
ok, err := check(h.actor, core.ID(q.ProjectID))
|
|
if err != nil || !ok {
|
|
return extension.ProjectAuditPage{}, extension.ErrProjectAuditDenied
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return extension.ProjectAuditPage{}, err
|
|
}
|
|
rows, cutoff, next, err := h.caps.st.ListProjectCommandAudits(h.owner, string(q.ProjectID), q.BeforeOffset, q.CutoffOffset, q.Limit)
|
|
if err != nil {
|
|
return extension.ProjectAuditPage{}, err
|
|
}
|
|
page := extension.ProjectAuditPage{
|
|
Receipts: make([]extension.HostAuditReceipt, 0, len(rows)),
|
|
NextOffset: next, CutoffOffset: cutoff,
|
|
Coverage: extension.ProjectAuditCoverage{
|
|
CommandAudit: "actor-owned-project-bound-only", CrossMember: "unsupported", LegacyProjectBinding: "not-recorded", RunRelations: "unsupported", ModelReceipts: "unsupported",
|
|
BackgroundReceipts: "unsupported", ResourceBodies: "unsupported",
|
|
},
|
|
}
|
|
for _, row := range rows {
|
|
nameStatus := "not-recorded"
|
|
if row.ActorName != "" {
|
|
nameStatus = "recorded"
|
|
}
|
|
page.Receipts = append(page.Receipts, extension.HostAuditReceipt{
|
|
SourceOffset: row.Offset, Time: row.Envelope.Time, ActorID: row.Envelope.Actor,
|
|
ActorName: row.ActorName, ActorNameStatus: nameStatus,
|
|
CommandID: row.Envelope.CommandID, State: string(row.Envelope.State),
|
|
OperationID: row.Envelope.OperationID, ConversationID: row.Envelope.ConversationID,
|
|
})
|
|
}
|
|
return page, nil
|
|
}
|