Why: X3 must share one Backend surface with the X1 context and X2 audit candidates while current main now owns account migrations 32-34.
Evidence: go test -p 1 ./... and go build ./... passed on the merged tree; isolated PostgreSQL project audit, migration, and work-context tests passed against a temporary instance. Default parallel go test ./... had only the unrelated sandbox orphan timing test fail; its isolated ten-run replay passed.
Impact: Preserve account migrations 32-34, place X1/X2 migrations at 35-43, and retain account-effective write guards alongside project audit/context behavior. X3 observer remains a contract only.
Refs: TT-C01-TT-C04, TT-A01-TT-A04, TT-N03, TT-R01; combination probe d9b00c2
421 lines
20 KiB
Go
421 lines
20 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"agent-system-backend/internal/settings"
|
|
"agent-system-backend/internal/store"
|
|
"agent-system-backend/pkg/extension"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
type testProjectContextProvider struct {
|
|
resolve func(extension.ProjectContextRequest) (extension.ProjectContextBundle, error)
|
|
read func(extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error)
|
|
}
|
|
|
|
type testSourceRecheckProvider struct {
|
|
testProjectContextProvider
|
|
called *bool
|
|
}
|
|
|
|
func (p testSourceRecheckProvider) Recheck(ctx context.Context, req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
if _, ok := ctx.Deadline(); !ok || req.Scope.PluginID != "rdm" || req.Scope.Actor != "alice" || req.Scope.ProjectID != "p1" || req.Scope.Goal != "" {
|
|
return extension.ProjectContextSourceRecheckResult{}, fmt.Errorf("untrusted or unbounded recheck scope")
|
|
}
|
|
*p.called = true
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.Scope.ProjectID}}, nil
|
|
}
|
|
|
|
func TestProjectSourceRecheckUsesSameRegisteredProviderAndDeadline(t *testing.T) {
|
|
called := false
|
|
slot := &projectContextSlot{}
|
|
provider := testSourceRecheckProvider{called: &called}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: hostcaps{contexts: slot}}).RegisterProjectContextProvider(provider); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := slot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1"}}); err != nil || !called {
|
|
t.Fatalf("registered source rechecker unavailable: called=%v err=%v", called, err)
|
|
}
|
|
if _, err := slot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1", Goal: "OLD_RESTRICTED_BODY"}}); err == nil {
|
|
t.Fatal("raw Goal entered source recheck provider")
|
|
}
|
|
cancelled, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := slot.recheck(cancelled, extension.ProjectContextSourceRecheckRequest{Scope: extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1"}}); err == nil {
|
|
t.Fatal("cancelled source recheck returned authorized context")
|
|
}
|
|
legacy := &projectContextSlot{}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: hostcaps{contexts: legacy}}).RegisterProjectContextProvider(testProjectContextProvider{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := legacy.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1"}}); err == nil {
|
|
t.Fatal("provider without optional rechecker silently accepted old sources")
|
|
}
|
|
}
|
|
|
|
type deadlineContextProvider struct{ sawDeadline *bool }
|
|
|
|
func (p deadlineContextProvider) Resolve(ctx context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
_, *p.sawDeadline = ctx.Deadline()
|
|
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.ProjectID}, nil
|
|
}
|
|
func (p deadlineContextProvider) ReadEvidence(ctx context.Context, req extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error) {
|
|
_, *p.sawDeadline = ctx.Deadline()
|
|
return extension.ProjectEvidence{Reference: req.Reference, Text: "detail"}, nil
|
|
}
|
|
|
|
func TestProjectContextProviderCallsHaveBoundedDeadline(t *testing.T) {
|
|
seen := false
|
|
slot := &projectContextSlot{}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: hostcaps{contexts: slot}}).RegisterProjectContextProvider(deadlineContextProvider{sawDeadline: &seen}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := slot.resolve(context.Background(), extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1"}); err != nil || !seen {
|
|
t.Fatalf("Resolve missing bounded context: seen=%v err=%v", seen, err)
|
|
}
|
|
seen = false
|
|
if _, err := slot.readEvidence(context.Background(), extension.ProjectEvidenceRequest{Scope: extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1", BudgetChars: 100}, Reference: extension.ProjectEvidenceRef{ID: "r1"}}); err != nil || !seen {
|
|
t.Fatalf("ReadEvidence missing bounded context: seen=%v err=%v", seen, err)
|
|
}
|
|
}
|
|
|
|
func (p testProjectContextProvider) Resolve(_ context.Context, req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
return p.resolve(req)
|
|
}
|
|
|
|
func (p testProjectContextProvider) ReadEvidence(_ context.Context, req extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error) {
|
|
if p.read != nil {
|
|
return p.read(req)
|
|
}
|
|
return extension.ProjectEvidence{}, nil
|
|
}
|
|
|
|
func TestProjectContextRegistryOptionalAndSingleOwner(t *testing.T) {
|
|
slot := &projectContextSlot{}
|
|
reg := recordRegistry{Registry: pluginRegistry{pluginID: "rdm", caps: hostcaps{contexts: slot}}}
|
|
optional, ok := any(reg).(extension.ProjectContextRegistry)
|
|
if !ok {
|
|
t.Fatal("record registry hides optional project context registration")
|
|
}
|
|
provider := testProjectContextProvider{resolve: func(extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
return extension.ProjectContextBundle{Status: extension.ProjectContextReady}, nil
|
|
}}
|
|
if err := optional.RegisterProjectContextProvider(provider); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := optional.RegisterProjectContextProvider(provider); err == nil {
|
|
t.Fatal("duplicate provider replaced the first provider")
|
|
}
|
|
if err := (compiledRegistry{}).RegisterProjectContextProvider(provider); err == nil {
|
|
t.Fatal("declarative provider accepted")
|
|
}
|
|
}
|
|
|
|
func TestRequiredProjectContextFailsWhenProviderNotAssembled(t *testing.T) {
|
|
slot := &projectContextSlot{}
|
|
if err := requireProjectContextProvider(true, slot); err == nil {
|
|
t.Fatal("required project context silently degraded")
|
|
}
|
|
if err := requireProjectContextProvider(false, slot); err != nil {
|
|
t.Fatalf("optional legacy path blocked: %v", err)
|
|
}
|
|
provider := testProjectContextProvider{resolve: func(extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
return extension.ProjectContextBundle{}, nil
|
|
}}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: hostcaps{contexts: slot}}).RegisterProjectContextProvider(provider); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := requireProjectContextProvider(true, slot); err != nil {
|
|
t.Fatalf("registered provider rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCompleteForWorkUsesStoredScopeAndActualInputManifest(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
if err := st.EnsureUser("alice", "Alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p, err := st.CreateProject("alice", "Project")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := st.CreateProject("alice", "Other")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conv, err := st.CreateConversation("alice", p.ID, "Work", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sent string
|
|
modelCalls := 0
|
|
model := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
modelCalls++
|
|
var payload struct {
|
|
Messages []struct {
|
|
Content string `json:"content"`
|
|
} `json:"messages"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
t.Error(err)
|
|
}
|
|
for _, m := range payload.Messages {
|
|
sent += m.Content
|
|
}
|
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"done"}}]}`))
|
|
}))
|
|
defer model.Close()
|
|
set := settings.NewService(st)
|
|
set.SetSystemOverride(map[string]any{"models": map[string]any{"chat": map[string]any{"baseUrl": model.URL, "model": "test-model"}}})
|
|
projects := &projectLifecycleSlot{}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: hostcaps{projects: projects}}).RegisterProjectLifecycle(completeLifecycle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
contexts := &projectContextSlot{}
|
|
called := 0
|
|
oversized := false
|
|
revokeDuringResolve := false
|
|
revokeDocDuringResolve := false
|
|
authEpoch := "duty-1"
|
|
provider := testProjectContextProvider{resolve: func(req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
called++
|
|
if req.Actor != "alice" || req.ProjectID != core.ID(p.ID) || req.ConversationID != core.ID(conv.ID) || req.PluginID != "rdm" {
|
|
t.Errorf("untrusted scope: %+v", req)
|
|
}
|
|
item := extension.ProjectContextItem{SourceID: "decision-1", Revision: "r3", Hash: "source-hash", Text: "Authorized summary", Required: true}
|
|
if oversized {
|
|
item.Text = strings.Repeat("x", workContextBudgetChars+1)
|
|
}
|
|
if revokeDuringResolve {
|
|
projects.mu.Lock()
|
|
projects.provider.Work = func(context.Context, string, core.ID) (bool, error) { return false, nil }
|
|
projects.mu.Unlock()
|
|
}
|
|
epoch := authEpoch
|
|
if revokeDocDuringResolve {
|
|
authEpoch = "duty-2"
|
|
revokeDocDuringResolve = false
|
|
}
|
|
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: core.ID(p.ID), Version: "v7", AuthorizationEpoch: epoch, Items: []extension.ProjectContextItem{item}}, nil
|
|
}}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: hostcaps{contexts: contexts}}).RegisterProjectContextProvider(provider); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := inprocHost{caps: hostcaps{st: st, set: set, projects: projects, contexts: contexts}, pluginID: "rdm", owner: "alice", actor: "alice", conversationID: conv.ID, hostProjectID: p.ID}
|
|
work := any(h).(extension.WorkCompletionHost)
|
|
result, err := work.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Draft report"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Text != "done" || !strings.Contains(sent, "Authorized summary") || !strings.Contains(sent, "Draft report") || result.Manifest.InputSHA256 == "" || len(result.Manifest.Included) != 1 {
|
|
t.Fatalf("input or manifest mismatch: result=%+v sent=%q", result, sent)
|
|
}
|
|
if want := fmt.Sprintf("%x", sha256.Sum256([]byte(sent))); result.Manifest.InputSHA256 != want {
|
|
t.Fatalf("manifest input hash %q differs from actual request content hash %q", result.Manifest.InputSHA256, want)
|
|
}
|
|
if got, want := result.Manifest.Included[0].InputSHA256, fmt.Sprintf("%x", sha256.Sum256([]byte("Authorized summary"))); got != want || result.Manifest.Included[0].SourceHash != "source-hash" {
|
|
t.Fatalf("manifest does not distinguish source hash and actual text hash: %+v", result.Manifest.Included[0])
|
|
}
|
|
if called != 2 {
|
|
t.Fatalf("provider called %d times, want initial and pre-send reads", called)
|
|
}
|
|
stored, err := st.ListContextManifests("alice", string(conv.ID))
|
|
if err != nil || len(stored) != 2 || stored[0].Delivery != "attempted" || stored[1].Delivery != "responded" || stored[0].AttemptID != stored[1].AttemptID || stored[1].InputSHA256 != result.Manifest.InputSHA256 {
|
|
t.Fatalf("work completion manifest not persisted: got=%+v err=%v", stored, err)
|
|
}
|
|
h.hostProjectID = other.ID
|
|
if _, err := h.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Draft report"}); err == nil || called != 2 {
|
|
t.Fatal("forged project scope reached provider")
|
|
}
|
|
h.hostProjectID = p.ID
|
|
h.conversationID = ""
|
|
if _, err := h.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Draft report"}); err == nil || called != 2 {
|
|
t.Fatal("scoped host without dispatch scope reached provider")
|
|
}
|
|
h.conversationID = conv.ID
|
|
projects.mu.Lock()
|
|
projects.provider.Work = func(context.Context, string, core.ID) (bool, error) { return false, nil }
|
|
projects.mu.Unlock()
|
|
if _, err := h.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Draft report"}); err == nil || called != 2 {
|
|
t.Fatal("revoked project work reached provider")
|
|
}
|
|
projects.mu.Lock()
|
|
projects.provider.Work = func(context.Context, string, core.ID) (bool, error) { return true, nil }
|
|
projects.mu.Unlock()
|
|
oversized = true
|
|
if _, err := h.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Draft report"}); err == nil || modelCalls != 1 {
|
|
t.Fatal("required context was dropped and model still called")
|
|
}
|
|
oversized = false
|
|
revokeDuringResolve = true
|
|
if _, err := h.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Draft report"}); err == nil || modelCalls != 1 {
|
|
t.Fatal("revocation during context resolution reached model")
|
|
}
|
|
projects.mu.Lock()
|
|
projects.provider.Work = func(context.Context, string, core.ID) (bool, error) { return true, nil }
|
|
projects.mu.Unlock()
|
|
revokeDuringResolve = false
|
|
revokeDocDuringResolve = true
|
|
if _, err := h.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Draft report"}); err == nil || modelCalls != 1 {
|
|
t.Fatal("document duty revoked while project membership remains reached model")
|
|
}
|
|
taskHost := any(h).(extension.WorkTaskHost)
|
|
taskID, err := taskHost.TaskCreateForWork("compose", "Draft report")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scoped := inprocHost{caps: h.caps, pluginID: "rdm", owner: "alice"}
|
|
if _, err := scoped.TaskCreateForWork("compose", "Forged"); err == nil {
|
|
t.Fatal("scoped host created a work task without dispatch scope")
|
|
}
|
|
if _, err := scoped.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Resume report", TaskID: taskID}); err != nil || modelCalls != 2 {
|
|
t.Fatalf("persisted plugin task did not restore trusted work scope: err=%v calls=%d", err, modelCalls)
|
|
}
|
|
otherPlugin := inprocHost{caps: h.caps, pluginID: "other", owner: "alice"}
|
|
if _, err := otherPlugin.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Steal task", TaskID: taskID}); err == nil || modelCalls != 2 {
|
|
t.Fatal("other plugin reused task binding")
|
|
}
|
|
otherOwner := inprocHost{caps: h.caps, pluginID: "rdm", owner: "bob"}
|
|
if _, err := otherOwner.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Steal task", TaskID: taskID}); err == nil || modelCalls != 2 {
|
|
t.Fatal("other owner reused task binding")
|
|
}
|
|
legacyTaskID, err := h.TaskCreate("legacy", "Unbound", string(conv.ID))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := scoped.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Old task", TaskID: legacyTaskID}); err == nil || modelCalls != 2 {
|
|
t.Fatal("old unbound task was treated as trusted work scope")
|
|
}
|
|
if _, err := st.DB().Exec(`UPDATE conversations SET project_id=? WHERE id=?`, other.ID, conv.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := scoped.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Moved project", TaskID: taskID}); err == nil || modelCalls != 2 {
|
|
t.Fatal("task silently followed a conversation to another project")
|
|
}
|
|
}
|
|
|
|
func TestWorkContextBudgetKeepsRequiredEvidenceFirst(t *testing.T) {
|
|
bundle := extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "project-1", Items: []extension.ProjectContextItem{
|
|
{SourceID: "optional", Revision: "r1", Hash: "h1", Text: "12345678"},
|
|
{SourceID: "required", Revision: "r1", Hash: "h2", Text: "must keep", Required: true},
|
|
}}
|
|
prompt, manifest, err := assembleWorkCompletion("goal", bundle, 140)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(prompt, "must keep") || strings.Contains(prompt, "12345678") {
|
|
t.Fatalf("required evidence lost to optional evidence: %q", prompt)
|
|
}
|
|
if len([]rune(prompt)) > 140 {
|
|
t.Fatalf("final prompt exceeded the budget: %d", len([]rune(prompt)))
|
|
}
|
|
if len(manifest.Included) != 1 || manifest.Included[0].SourceID != "required" || len(manifest.Dropped) != 1 || manifest.Dropped[0].SourceID != "optional" {
|
|
t.Fatalf("manifest does not match selected input: %+v", manifest)
|
|
}
|
|
}
|
|
|
|
func TestCompleteForWorkReadsOnlyCurrentBundleEvidence(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "evidence.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
p, err := st.CreateProject("alice", "P")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conv, err := st.CreateConversation("alice", p.ID, "C", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sent []string
|
|
model := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Messages []struct {
|
|
Content string `json:"content"`
|
|
} `json:"messages"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
var input string
|
|
for _, m := range body.Messages {
|
|
input += m.Content
|
|
}
|
|
sent = append(sent, input)
|
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"done"}}]}`))
|
|
}))
|
|
defer model.Close()
|
|
set := settings.NewService(st)
|
|
set.SetSystemOverride(map[string]any{"models": map[string]any{"chat": map[string]any{"baseUrl": model.URL, "model": "test-model"}}})
|
|
projects := &projectLifecycleSlot{}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: hostcaps{projects: projects}}).RegisterProjectLifecycle(completeLifecycle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
contexts := &projectContextSlot{}
|
|
ref := extension.ProjectEvidenceRef{ID: "ref-1", SourceID: "doc-1", Revision: "r2", Hash: "source-hash"}
|
|
readCalls := 0
|
|
readText, authEpoch := "current restricted detail", "duty-1"
|
|
provider := testProjectContextProvider{
|
|
resolve: func(req extension.ProjectContextRequest) (extension.ProjectContextBundle, error) {
|
|
return extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.ProjectID, AuthorizationEpoch: authEpoch, PublicationEpoch: "pub-1", Evidence: []extension.ProjectEvidenceRef{ref}}, nil
|
|
},
|
|
read: func(req extension.ProjectEvidenceRequest) (extension.ProjectEvidence, error) {
|
|
readCalls++
|
|
if req.Reference != ref || req.Scope.ProjectID != core.ID(p.ID) {
|
|
t.Errorf("untrusted evidence scope: %+v", req)
|
|
}
|
|
return extension.ProjectEvidence{Reference: req.Reference, Text: readText}, nil
|
|
},
|
|
}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: hostcaps{contexts: contexts}}).RegisterProjectContextProvider(provider); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := inprocHost{caps: hostcaps{st: st, set: set, projects: projects, contexts: contexts}, pluginID: "rdm", owner: "alice", actor: "alice", conversationID: conv.ID, hostProjectID: p.ID}
|
|
result, err := h.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Write", EvidenceRefs: []string{"ref-1"}})
|
|
if err != nil || len(sent) != 1 || !strings.Contains(sent[0], "current restricted detail") || len(result.Manifest.ReadEvidence) != 1 || readCalls != 2 {
|
|
t.Fatalf("requested evidence did not reach model under reauthorization: err=%v sent=%q manifest=%+v reads=%d", err, sent, result.Manifest, readCalls)
|
|
}
|
|
if _, err := h.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Write", EvidenceRefs: []string{"arbitrary-path"}}); err == nil || readCalls != 2 {
|
|
t.Fatal("arbitrary evidence reference reached provider")
|
|
}
|
|
readText, authEpoch = "new public detail", "duty-2"
|
|
if _, err := h.CompleteForWork(context.Background(), extension.WorkCompletionRequest{Goal: "Write", EvidenceRefs: []string{"ref-1"}}); err != nil || len(sent) != 2 || strings.Contains(sent[1], "current restricted detail") || !strings.Contains(sent[1], "new public detail") {
|
|
t.Fatalf("same Host reused old evidence after duty change: err=%v sent=%q", err, sent)
|
|
}
|
|
}
|
|
|
|
func TestPluginAuditCannotForgeOrCorruptContextManifest(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "manifest.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
good := extension.ProjectContextManifest{ProjectID: "p1", AttemptID: "attempt-1", Delivery: "attempted", InputSHA256: "sha-good"}
|
|
if err := st.AppendContextManifest("alice", "run-1", good); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := inprocHost{caps: hostcaps{st: st}, pluginID: "malicious", owner: "alice"}
|
|
if err := h.Audit("model.context_manifest", "run-1", `{"Delivery":"responded","InputSHA256":"forged"}`); err == nil {
|
|
t.Fatal("plugin forged a trusted model manifest action")
|
|
}
|
|
if err := st.AppendAuditEvent(store.AuditEvent{Owner: "alice", Action: "model.context_manifest", Target: "run-1", Evidence: "not-json"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := st.ListContextManifests("alice", "run-1")
|
|
if err != nil || len(got) != 1 || got[0].InputSHA256 != "sha-good" {
|
|
t.Fatalf("untrusted audit row corrupted manifest reader: got=%+v err=%v", got, err)
|
|
}
|
|
}
|