Why: Both approved Backend slices share extension surfaces and migration numbers, and current main added account audit v31. Evidence: The isolated probe resolves additive interfaces and v31-v40 ordering; focused and full Go tests passed without PostgreSQL. Impact: Produces a local integration candidate for isolated PostgreSQL and race verification only; shipping branches remain unchanged. Refs: feature/rdm-two-trees-latest-templates W6 W9
239 lines
9.0 KiB
Go
239 lines
9.0 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
agentcommand "agent-command"
|
|
"agent-system-backend/internal/store"
|
|
"agent-system-backend/pkg/extension"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
func TestProjectAuditHostChecksDomainReadAndProjectsStoredReceipts(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "audit.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
for _, owner := range []string{"alice", "bob"} {
|
|
if err := st.EnsureUser(owner, strings.ToUpper(owner)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
a, err := st.CreateProject("alice", "A")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := st.CreateProject("alice", "B")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ac, err := st.CreateConversation("alice", string(a.ID), "A", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bc, err := st.CreateConversation("alice", string(b.ID), "B", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
shared, err := st.CreateConversationInProject("bob", string(a.ID), "A shared", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, item := range []struct{ owner, conversation, command string }{
|
|
{"alice", string(ac.ID), "a.command"},
|
|
{"alice", string(bc.ID), "b.command"},
|
|
{"bob", string(shared.ID), "shared.command"},
|
|
} {
|
|
if err := st.AppendAudit(item.owner, agentcommand.AuditEnvelope{Time: time.Now(), Actor: item.owner, CommandID: item.command, ConversationID: item.conversation, State: agentcommand.StateCommitted, ParamsDigest: "sensitive-digest"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
slot := &projectAuditSlot{}
|
|
caps := hostcaps{st: st, projectAudit: slot}
|
|
reg := pluginRegistry{pluginID: "rdm", caps: caps}
|
|
allowed := true
|
|
if err := reg.RegisterProjectAuditAuthorizer(func(actor string, projectID core.ID) (bool, error) {
|
|
return allowed && actor == "bob" && projectID == a.ID, nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := inprocHost{caps: caps, owner: "bob", actor: "bob", pluginID: "rdm"}
|
|
page, err := h.QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: a.ID, Limit: 10})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(page.Receipts) != 1 || page.Receipts[0].CommandID != "shared.command" {
|
|
t.Fatalf("A page: %+v", page)
|
|
}
|
|
if page.Coverage.CommandAudit != "actor-owned-project-bound-only" || page.Coverage.CrossMember != "unsupported" || page.Coverage.LegacyProjectBinding != "not-recorded" || page.Coverage.RunRelations != "unsupported" || page.Coverage.ModelReceipts != "unsupported" || page.Coverage.CutoffConsistency != "committed-prefix" {
|
|
t.Fatalf("coverage: %+v", page.Coverage)
|
|
}
|
|
if page.Receipts[0].ActorName != "BOB" {
|
|
t.Fatalf("actor names: %+v", page.Receipts)
|
|
}
|
|
raw, err := json.Marshal(page)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(raw), "sensitive-digest") || strings.Contains(string(raw), "paramsDigest") || strings.Contains(string(raw), "payload") {
|
|
t.Fatalf("raw audit payload leaked: %s", raw)
|
|
}
|
|
if _, err := h.QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: b.ID}); !errors.Is(err, extension.ErrProjectAuditDenied) {
|
|
t.Fatalf("project B: %v", err)
|
|
}
|
|
if _, err := (inprocHost{caps: caps, owner: "bob", actor: "bob", pluginID: "other"}).QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: a.ID}); !errors.Is(err, extension.ErrProjectAuditUnsupported) {
|
|
t.Fatalf("other plugin: %v", err)
|
|
}
|
|
if _, err := (inprocHost{caps: caps, owner: "bob", pluginID: "rdm"}).QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: a.ID}); !errors.Is(err, extension.ErrProjectAuditDenied) {
|
|
t.Fatalf("unbound actor: %v", err)
|
|
}
|
|
allowed = false
|
|
if _, err := h.QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: a.ID, BeforeOffset: page.NextOffset, CutoffOffset: page.CutoffOffset}); !errors.Is(err, extension.ErrProjectAuditDenied) {
|
|
t.Fatalf("revoked audit.read: %v", err)
|
|
}
|
|
}
|
|
|
|
// A direct explicit ID bypasses the v39 default allocator. This must never
|
|
// yield a committed-prefix page that permanently skips the late lower row.
|
|
func TestProjectAuditPostgresExplicitLowIDCannotHideAcrossPages(t *testing.T) {
|
|
dsn := os.Getenv("AGENT_PG_TEST_DSN")
|
|
if dsn == "" {
|
|
t.Skip("AGENT_PG_TEST_DSN unset: isolated PostgreSQL check NOT_RUN")
|
|
}
|
|
admin, err := sql.Open("pgx", dsn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { admin.Close() })
|
|
if _, err := admin.Exec(`CREATE EXTENSION IF NOT EXISTS pg_trgm`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schema := "w9_" + strings.ToLower(string(store.NewID("audit")))
|
|
if _, err := admin.Exec(`CREATE SCHEMA ` + schema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { admin.Exec(`DROP SCHEMA IF EXISTS ` + schema + ` CASCADE`) })
|
|
st, err := store.OpenPostgres(store.AppendPGRuntimeParam(dsn, "search_path", schema+",public"), filepath.Join(t.TempDir(), "artifacts"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
if err := st.EnsureUser("alice", "Alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
project, err := st.CreateProject("alice", "A")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conversation, err := st.CreateConversation("alice", string(project.ID), "work", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, command := range []string{"first", "second"} {
|
|
if err := st.AppendAudit("alice", agentcommand.AuditEnvelope{Time: time.Now(), Actor: "alice", CommandID: command, ConversationID: string(conversation.ID), State: agentcommand.StateCommitted}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
insert := `INSERT INTO command_audit(time,command_id,source,entrance,state,operation_id,conversation_id,owner,payload,actor_name,project_id)
|
|
VALUES ($1,$2,'','','committed','',$3,'alice',$4,'Alice',$5) RETURNING id`
|
|
gap, err := st.DB().Begin()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer gap.Rollback()
|
|
var gapID int64
|
|
if err := gap.QueryRow(insert, time.Now().UTC().Format(time.RFC3339Nano), "rolled-back-gap", string(conversation.ID), `{}`, string(project.ID)).Scan(&gapID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gapID != 3 {
|
|
t.Fatalf("rollback gap ID=%d want=3", gapID)
|
|
}
|
|
if err := gap.Rollback(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a, err := st.DB().Begin()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer a.Rollback()
|
|
env := agentcommand.AuditEnvelope{Time: time.Now(), Actor: "alice", CommandID: "explicit-late", ConversationID: string(conversation.ID), State: agentcommand.StateCommitted}
|
|
raw, err := json.Marshal(env)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = a.Exec(`INSERT INTO command_audit(id,time,command_id,source,entrance,state,operation_id,conversation_id,owner,payload,actor_name,project_id)
|
|
VALUES (3,$1,$2,'','','committed','',$3,'alice',$4,'Alice',$5)`, env.Time.UTC().Format(time.RFC3339Nano), env.CommandID, string(conversation.ID), string(raw), string(project.ID))
|
|
if err != nil {
|
|
// Rejecting the online explicit ID is also a safe outcome.
|
|
t.Logf("online explicit low ID rejected: %v", err)
|
|
return
|
|
}
|
|
slot := &projectAuditSlot{}
|
|
caps := hostcaps{st: st, projectAudit: slot}
|
|
reg := pluginRegistry{pluginID: "rdm", caps: caps}
|
|
if err := reg.RegisterProjectAuditAuthorizer(func(actor string, projectID core.ID) (bool, error) {
|
|
return actor == "alice" && projectID == project.ID, nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := inprocHost{caps: caps, owner: "alice", actor: "alice", pluginID: "rdm"}
|
|
committed := make(chan error, 1)
|
|
started := make(chan struct{})
|
|
go func() {
|
|
close(started)
|
|
committed <- st.AppendAudit("alice", agentcommand.AuditEnvelope{Time: time.Now(), Actor: "alice", CommandID: "implicit-high", ConversationID: string(conversation.ID), State: agentcommand.StateCommitted})
|
|
}()
|
|
<-started
|
|
var first extension.ProjectAuditPage
|
|
highCommittedEarly := false
|
|
select {
|
|
case err := <-committed:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
highCommittedEarly = true
|
|
case <-time.After(500 * time.Millisecond):
|
|
}
|
|
if highCommittedEarly {
|
|
first, err = h.QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: project.ID, Limit: 2})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(first.Receipts) != 2 || first.Receipts[0].SourceOffset != 4 || first.Receipts[1].SourceOffset != 2 || first.CutoffOffset != 4 || first.NextOffset != 2 || first.Coverage.CutoffConsistency != "committed-prefix" {
|
|
t.Fatalf("counterexample first page changed: %+v", first)
|
|
}
|
|
}
|
|
if err := a.Commit(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !highCommittedEarly {
|
|
if err := <-committed; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, err = h.QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: project.ID, Limit: 2})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
second, err := h.QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: project.ID, BeforeOffset: first.NextOffset, CutoffOffset: first.CutoffOffset, Limit: 2})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen := map[int64]bool{}
|
|
for _, receipt := range append(first.Receipts, second.Receipts...) {
|
|
seen[receipt.SourceOffset] = true
|
|
}
|
|
if !seen[3] || len(seen) != 4 {
|
|
t.Fatalf("committed-prefix omitted explicit low ID 3: early=%v first=%+v second=%+v", highCommittedEarly, first, second)
|
|
}
|
|
}
|