Why: Clock-derived IDs could collide and string-scoped hosts could impersonate a registered audit publisher.
Evidence: Fixed-clock concurrent, ambiguous private offset, forged scoped-host, and registered publisher RED-to-GREEN tests; go test ./... -count=1 and targeted race test passed locally as SELF.
Impact: Generate 192-bit host IDs, reject ambiguous explicit offsets, and issue a publication-only capability at verifier registration.
Refs: Stage 5 W9; approved Stage 6 audit-link design adjustment r1; review of a8fa2fe
116 lines
4.6 KiB
Go
116 lines
4.6 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
agentcommand "agent-command"
|
|
"agent-system-backend/internal/cmdbus"
|
|
"agent-system-backend/internal/store"
|
|
"agent-system-backend/internal/surfacesvc"
|
|
"agent-system-backend/pkg/extension"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
func TestSurfaceButtonDispatchUsesAuthenticatedActor(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "audit.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
if err := st.EnsureUser("alice", "Alice at action"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p, err := st.CreateProject("alice", "A")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c, err := st.CreateConversation("alice", string(p.ID), "work", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bus := cmdbus.New(st, nil, func() time.Time { return time.Now().UTC() })
|
|
if err := bus.RegisterBuiltin(agentcommand.CommandSpec{ID: "demo.submit", Title: "Submit"}, "", func(context.Context, cmdbus.Request) (any, error) { return "ok", nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := dispatchSurfaceAction(context.Background(), bus, surfacesvc.ActionDispatch{CommandID: "demo.submit", Owner: "alice", ConversationID: string(c.ID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.State != "committed" {
|
|
t.Fatalf("state: %s", result.State)
|
|
}
|
|
rows, _, _, err := st.ListProjectCommandAudits("alice", string(p.ID), 0, 0, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 1 || rows[0].Envelope.Actor != "alice" || rows[0].ActorName != "Alice at action" {
|
|
t.Fatalf("button audit actor: %+v", rows)
|
|
}
|
|
}
|
|
|
|
func TestCompiledHandlerReceivesHostOperationID(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "audit.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
p, err := st.CreateProject("alice", "A")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c, err := st.CreateConversation("alice", string(p.ID), "work", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bus := cmdbus.New(st, nil, func() time.Time { return time.Now().UTC() })
|
|
slot := &projectAuditSlot{}
|
|
caps := hostcaps{st: st, projectAudit: slot}
|
|
reg := pluginRegistry{compiledRegistry: compiledRegistry{bus: bus}, pluginID: "rdm", caps: caps}
|
|
var seen string
|
|
if err := reg.AddCommandHandler(json.RawMessage(`{"id":"rdm.confirm","title":"Confirm"}`), func(_ context.Context, _ extension.Host, req extension.Request) (any, error) {
|
|
seen = req.OperationID
|
|
return "domain-committed", nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp := bus.Dispatch(context.Background(), cmdbus.Request{ID: "rdm.confirm", Actor: "alice", Owner: "alice", ConversationID: string(c.ID)})
|
|
rows, _, _, err := st.ListProjectCommandAudits("alice", string(p.ID), 0, 0, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if seen == "" || resp.Receipt.OperationID != seen || len(rows) != 1 || rows[0].Envelope.OperationID != seen {
|
|
t.Fatalf("handler/domain correlation: seen=%q receipt=%+v rows=%+v", seen, resp.Receipt, rows)
|
|
}
|
|
if err := reg.RegisterProjectAuditAuthorizer(func(actor string, project core.ID) (bool, error) { return actor == "bob" && project == p.ID, nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
publisherFactory, err := (recordRegistry{Registry: reg}).RegisterProjectAuditLinkVerifier(func(_ context.Context, proof extension.ProjectAuditLinkProof) (bool, error) {
|
|
return proof.Owner == "alice" && proof.ProjectID == p.ID && proof.OperationID == seen && proof.SourceID == "domain-commit-1" && proof.ResourceID == "doc-1", nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := reg.RegisterProjectAuditDisclosureAuthorizer(func(_ context.Context, actor string, r extension.ProjectAuditLinkedResource) (extension.ProjectAuditDisclosure, error) {
|
|
return extension.ProjectAuditDisclosure{Metadata: actor == "bob" && r.ResourceID == "doc-1"}, nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
publisher, err := publisherFactory.ForOwner("alice")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
linked, err := publisher.PublishProjectAuditLink(context.Background(), extension.ProjectAuditLinkRequest{ProjectID: p.ID, OperationID: seen, SourceKind: "domain-commit", SourceID: "domain-commit-1", ResourceKind: "document", ResourceID: "doc-1", DisclosureClass: "metadata", IdempotencyKey: "outbox-1"})
|
|
if err != nil || linked.HostAuditOffset != rows[0].Offset {
|
|
t.Fatalf("publish by trusted operation: %+v %v", linked, err)
|
|
}
|
|
bob := inprocHost{caps: caps, owner: "bob", actor: "bob", pluginID: "rdm"}
|
|
page, err := bob.QueryProjectAudit(context.Background(), extension.ProjectAuditQuery{ProjectID: p.ID, LinkedSourceKind: "domain-commit", LinkedSourceID: "domain-commit-1", LinkedHostOffset: linked.HostAuditOffset})
|
|
if err != nil || len(page.Receipts) != 1 || page.Receipts[0].OperationID != seen {
|
|
t.Fatalf("linked read: %+v %v", page, err)
|
|
}
|
|
}
|