Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes. Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending. Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete. Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
152 lines
4.7 KiB
Go
152 lines
4.7 KiB
Go
package app
|
|
|
|
import (
|
|
"agent-system-backend/internal/projectscope"
|
|
"agent-system-backend/internal/server"
|
|
"agent-system-backend/internal/store"
|
|
"agent-system-backend/pkg/extension"
|
|
core "agent-system-core"
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
)
|
|
|
|
type projectLifecycleSlot struct {
|
|
mu sync.Mutex
|
|
holder string
|
|
provider *extension.ProjectLifecycle
|
|
}
|
|
|
|
func (r compiledRegistry) RegisterProjectLifecycle(extension.ProjectLifecycle) error {
|
|
return errors.New("extension: declarative plugins cannot register project lifecycle")
|
|
}
|
|
func (r pluginRegistry) RegisterProjectLifecycle(p extension.ProjectLifecycle) error {
|
|
if r.caps.projects == nil || r.pluginID == "" {
|
|
return errors.New("extension: project lifecycle slot unavailable")
|
|
}
|
|
if p.List == nil || p.Create == nil || p.Rename == nil || p.Archive == nil || p.Work == nil {
|
|
return errors.New("extension: project lifecycle requires all callbacks")
|
|
}
|
|
slot := r.caps.projects
|
|
slot.mu.Lock()
|
|
defer slot.mu.Unlock()
|
|
if slot.provider != nil {
|
|
return errors.New("extension: project lifecycle already registered")
|
|
}
|
|
slot.holder, slot.provider = r.pluginID, &p
|
|
return nil
|
|
}
|
|
func (r recordRegistry) RegisterProjectLifecycle(p extension.ProjectLifecycle) error {
|
|
reg, ok := r.Registry.(extension.ProjectLifecycleRegistry)
|
|
if !ok {
|
|
return errors.New("extension: project lifecycle registration unavailable")
|
|
}
|
|
return reg.RegisterProjectLifecycle(p)
|
|
}
|
|
func (slot *projectLifecycleSlot) wire(srv *server.Server, caps hostcaps) {
|
|
slot.mu.Lock()
|
|
defer slot.mu.Unlock()
|
|
if slot.provider == nil {
|
|
return
|
|
}
|
|
holder := slot.holder
|
|
srv.SetProjectLifecycle(*slot.provider, func(owner string) extension.Host {
|
|
return inprocHost{caps: caps, owner: owner, actor: owner, pluginID: holder}
|
|
})
|
|
}
|
|
|
|
func (slot *projectLifecycleSlot) active() bool {
|
|
slot.mu.Lock()
|
|
defer slot.mu.Unlock()
|
|
return slot.provider != nil
|
|
}
|
|
|
|
func (slot *projectLifecycleSlot) work() func(context.Context, string, core.ID) (bool, error) {
|
|
slot.mu.Lock()
|
|
defer slot.mu.Unlock()
|
|
if slot.provider == nil {
|
|
return nil
|
|
}
|
|
return slot.provider.Work
|
|
}
|
|
|
|
func (slot *projectLifecycleSlot) resolve(ctx context.Context, caps hostcaps, owner string, id core.ID) (core.ID, error) {
|
|
slot.mu.Lock()
|
|
if slot.provider == nil {
|
|
slot.mu.Unlock()
|
|
return id, nil
|
|
}
|
|
p, holder := *slot.provider, slot.holder
|
|
slot.mu.Unlock()
|
|
return projectscope.Resolve(ctx, inprocHost{caps: caps, owner: owner, actor: owner, pluginID: holder}, p, id)
|
|
}
|
|
|
|
var _ extension.ProjectHost = inprocHost{}
|
|
|
|
func (h inprocHost) OwnedProjects() ([]core.Project, error) {
|
|
if err := h.requireEffective(); err != nil {
|
|
return nil, err
|
|
}
|
|
ps, err := h.caps.st.ListProjects(h.owner)
|
|
for i := range ps {
|
|
ps[i].Owner = h.owner
|
|
}
|
|
return ps, err
|
|
}
|
|
func (h inprocHost) OwnedProject(id string) (core.Project, error) {
|
|
if err := h.requireEffective(); err != nil {
|
|
return core.Project{}, err
|
|
}
|
|
return h.caps.st.GetProject(h.owner, id)
|
|
}
|
|
func (h inprocHost) ProjectMetadata(id string) (core.Project, error) {
|
|
if err := h.requireEffective(); err != nil {
|
|
return core.Project{}, err
|
|
}
|
|
p, err := h.caps.st.ProjectMetadata(id)
|
|
if err != nil {
|
|
return core.Project{}, err
|
|
}
|
|
if h.caps.projects != nil && h.caps.projects.active() {
|
|
p.Settings = h.caps.set.MaskedProjectMetadata(p.Settings)
|
|
}
|
|
if p.Owner == h.owner {
|
|
return p, nil
|
|
}
|
|
if h.caps.authz == nil {
|
|
return core.Project{}, store.ErrNotFound
|
|
}
|
|
ok, err := h.caps.authz.AuthorizeResource(extension.ResourceAuthzQuery{Actor: h.owner, Action: "read", ResourceType: "project", ResourceID: core.ID(id), Owner: p.Owner, ProjectID: core.ID(id)})
|
|
if err != nil || !ok {
|
|
return core.Project{}, store.ErrNotFound
|
|
}
|
|
return p, nil
|
|
}
|
|
func (h inprocHost) EnsureProject(id, name string) (core.Project, error) {
|
|
if err := h.requireEffective(); err != nil {
|
|
return core.Project{}, err
|
|
}
|
|
if h.actor != "" && h.actor != h.owner {
|
|
return core.Project{}, store.ErrAccountForbidden
|
|
}
|
|
// A creator-scoped Host is the approved I1 projection capability. RDM
|
|
// authorizes a delegated operator before invoking it; cross-database
|
|
// commit fencing remains outside U3's guarantee.
|
|
return h.caps.st.EnsureProjectAccountChecked(h.owner, h.owner, id, name, h.caps.allowImplicitLocal)
|
|
}
|
|
func (h inprocHost) RenameOwnedProject(id, name string) (core.Project, error) {
|
|
if err := h.requireEffective(); err != nil {
|
|
return core.Project{}, err
|
|
}
|
|
if h.actor != "" && h.actor != h.owner {
|
|
return core.Project{}, store.ErrAccountForbidden
|
|
}
|
|
return h.caps.st.RenameOwnedProjectAccountChecked(h.owner, h.owner, id, name, h.caps.allowImplicitLocal)
|
|
}
|
|
func (h inprocHost) ProjectNameAvailable(name, exceptID string) (bool, error) {
|
|
if err := h.requireEffective(); err != nil {
|
|
return false, err
|
|
}
|
|
return h.caps.st.ProjectNameAvailable(name, exceptID)
|
|
}
|