package app import ( "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/server" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/settings" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/store" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/pkg/extension" core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git" "bytes" "context" "encoding/json" "errors" "net/http/httptest" "testing" ) func completeLifecycle() extension.ProjectLifecycle { return extension.ProjectLifecycle{ List: func(context.Context, extension.Host) ([]core.Project, error) { return nil, nil }, Create: func(context.Context, extension.Host, string) (core.Project, error) { return core.Project{}, nil }, Rename: func(context.Context, extension.Host, string, string) (core.Project, error) { return core.Project{}, nil }, Archive: func(context.Context, extension.Host, string) error { return nil }, Work: func(context.Context, string, core.ID) (bool, error) { return true, nil }, } } func TestProjectLifecycleOptionalRegistration(t *testing.T) { slot := &projectLifecycleSlot{} reg := pluginRegistry{pluginID: "rdm", caps: hostcaps{projects: slot}} if err := reg.RegisterProjectLifecycle(extension.ProjectLifecycle{}); err == nil { t.Fatal("incomplete registration accepted") } if err := reg.RegisterProjectLifecycle(completeLifecycle()); err != nil { t.Fatal(err) } if err := reg.RegisterProjectLifecycle(completeLifecycle()); err == nil { t.Fatal("duplicate registration accepted") } if err := (compiledRegistry{}).RegisterProjectLifecycle(completeLifecycle()); err == nil { t.Fatal("declarative callback accepted") } rr := recordRegistry{Registry: pluginRegistry{pluginID: "other", caps: hostcaps{projects: &projectLifecycleSlot{}}}} optional, ok := any(rr).(extension.ProjectLifecycleRegistry) if !ok { t.Fatal("record registry hides optional registration") } if err := optional.RegisterProjectLifecycle(completeLifecycle()); err != nil { t.Fatal(err) } } func TestProjectHostMetadataDoesNotGrantMutation(t *testing.T) { st, err := store.OpenSQLite(t.TempDir() + "/t.db") if err != nil { t.Fatal(err) } defer st.Close() alice := inprocHost{owner: "alice", caps: hostcaps{st: st}} p, err := alice.EnsureProject("stable", "Shared") if err != nil { t.Fatal(err) } auth := &projectAuthz{} auth.set("rdm", func(owner string, id core.ID) (bool, error) { return owner == "bob" && id == core.ID(p.ID), nil }) bob := inprocHost{owner: "bob", caps: hostcaps{st: st, authz: auth}} if _, err := bob.ProjectMetadata(p.ID); err != nil { t.Fatal(err) } if _, err := bob.OwnedProject(p.ID); !errors.Is(err, store.ErrNotFound) { t.Fatalf("owned lookup=%v", err) } if _, err := bob.RenameOwnedProject(p.ID, "Other"); !errors.Is(err, store.ErrNotFound) { t.Fatalf("shared rename=%v", err) } if _, err := bob.EnsureProject(p.ID, p.Name); !errors.Is(err, store.ErrNotFound) { t.Fatalf("shared ensure=%v", err) } auth.setV2("rdm", func(q extension.ResourceAuthzQuery) (bool, error) { return false, nil }) if _, err := bob.ProjectMetadata(p.ID); !errors.Is(err, store.ErrNotFound) { t.Fatalf("V2 denial ignored=%v", err) } } func TestProjectHostMetadataMasksSharedSecretsWithoutMutation(t *testing.T) { st, err := store.OpenSQLite(t.TempDir() + "/t.db") if err != nil { t.Fatal(err) } defer st.Close() p, err := st.CreateProject("alice", "Shared") if err != nil { t.Fatal(err) } raw := map[string]any{"models": map[string]any{"chat": map[string]any{"apiKey": "fixture-nested-secret"}}, "models.embedding.apiKey": "fixture-flat-secret", "legacy.token": "fixture-legacy-secret", "plugins.private": map[string]any{"credential": "fixture-schema-secret"}, "display": map[string]any{"caption": "Shared configuration"}, "rdm.projectId": "canonical", "rdm.state": "ready"} if err := st.UpdateProjectSettings("alice", p.ID, raw); err != nil { t.Fatal(err) } svc := settings.NewService(st) svc.SetExtraSchema([]core.SettingDesc{{Key: "plugins.private.credential", Secret: true}}) slot := &projectLifecycleSlot{} auth := &projectAuthz{} caps := hostcaps{st: st, set: svc, projects: slot, authz: auth} provider := completeLifecycle() provider.List = func(_ context.Context, h extension.Host) ([]core.Project, error) { project, err := h.(extension.ProjectHost).ProjectMetadata(p.ID) return []core.Project{project}, err } if err := (pluginRegistry{pluginID: "rdm", caps: caps}).RegisterProjectLifecycle(provider); err != nil { t.Fatal(err) } auth.setV2("rdm", func(q extension.ResourceAuthzQuery) (bool, error) { return q.Actor == "bob" && q.ResourceType == "project" && q.ResourceID == p.ID, nil }) for _, actor := range []string{"bob", "alice"} { h := inprocHost{owner: actor, caps: caps} view, err := h.ProjectMetadata(p.ID) if err != nil { t.Fatal(err) } encoded, _ := json.Marshal(view.Settings) for _, value := range []string{"fixture-nested-secret", "fixture-flat-secret", "fixture-legacy-secret", "fixture-schema-secret"} { if bytes.Contains(encoded, []byte(value)) { t.Fatalf("%s metadata exposed a fixture credential", actor) } } if view.Settings["rdm.projectId"] != "canonical" || view.Settings["rdm.state"] != "ready" || view.Settings["display"].(map[string]any)["caption"] != "Shared configuration" { t.Fatal("allowed service/configuration metadata changed") } } // Exercise the real registry -> actor-scoped Host -> public HTTP list chain. srv := server.New(st, nil, nil, nil, svc, nil) srv.Auth = server.NewAuthenticator(st, true) slot.wire(srv, caps) key, err := st.CreateAPIKey("bob", "test") if err != nil { t.Fatal(err) } request := httptest.NewRequest("GET", "/api/projects", nil) request.Header.Set("Authorization", "Bearer "+key) response := httptest.NewRecorder() srv.Handler().ServeHTTP(response, request) if response.Code != 200 { t.Fatalf("member list=%d", response.Code) } for _, value := range []string{"fixture-nested-secret", "fixture-flat-secret", "fixture-legacy-secret", "fixture-schema-secret"} { if bytes.Contains(response.Body.Bytes(), []byte(value)) { t.Fatal("actor-scoped lifecycle list exposed a fixture credential") } } if !bytes.Contains(response.Body.Bytes(), []byte(`"rdm.projectId":"canonical"`)) { t.Fatal("actor-scoped list lost canonical metadata") } owned, err := (inprocHost{owner: "alice", caps: caps}).OwnedProject(p.ID) if err != nil { t.Fatal(err) } before, _ := json.Marshal(raw) after, _ := json.Marshal(owned.Settings) if !bytes.Equal(before, after) { t.Fatal("metadata read mutated persisted owner settings") } }