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.
83 lines
2.6 KiB
Go
83 lines
2.6 KiB
Go
package server
|
|
|
|
import (
|
|
"agent-system-backend/internal/store"
|
|
"agent-system-backend/pkg/extension"
|
|
core "agent-system-core"
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
)
|
|
|
|
// SetProjectLifecycle is wired only for a registered in-process provider. The
|
|
// optional host factory constructs a capability scoped to this request's actor.
|
|
func (s *Server) SetProjectLifecycle(p extension.ProjectLifecycle, host func(string) extension.Host) {
|
|
s.projectLifecycle = &p
|
|
s.projectHost = host
|
|
}
|
|
func (s *Server) lifecycleHost(owner string) extension.Host {
|
|
if s.projectHost == nil {
|
|
return nil
|
|
}
|
|
return s.projectHost(owner)
|
|
}
|
|
func (s *Server) projectWork(ctx context.Context, owner, projectID string) error {
|
|
if s.projectLifecycle == nil {
|
|
return nil
|
|
}
|
|
if s.projectLifecycle.Work == nil {
|
|
return store.ErrNotFound
|
|
}
|
|
ok, err := s.projectLifecycle.Work(ctx, owner, core.ID(projectID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return store.ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createPrivateConversation is shared by the API and /new. Only live work
|
|
// authorization grants member creation; the resulting conversation stays owned.
|
|
func (s *Server) createPrivateConversation(ctx context.Context, owner, projectID, title, profile string) (core.Conversation, error) {
|
|
if s.projectLifecycle != nil {
|
|
if err := s.projectWork(ctx, owner, projectID); err != nil {
|
|
return core.Conversation{}, err
|
|
}
|
|
return s.st.CreateConversationAccountChecked(owner, projectID, title, profile, s.Auth == nil || !s.Auth.Enabled, false)
|
|
}
|
|
c, err := s.st.CreateConversationAccountChecked(owner, projectID, title, profile, s.Auth == nil || !s.Auth.Enabled, true)
|
|
if errors.Is(err, store.ErrNotFound) && s.authorizeSharedResource(owner, "read", "project", core.ID(projectID), "", core.ID(projectID)) {
|
|
return s.st.CreateConversationAccountChecked(owner, projectID, title, profile, s.Auth == nil || !s.Auth.Enabled, false)
|
|
}
|
|
return c, err
|
|
}
|
|
func writeProjectErr(w http.ResponseWriter, err error) {
|
|
var coded interface{ StatusCode() int }
|
|
if errors.As(err, &coded) {
|
|
switch code := coded.StatusCode(); code {
|
|
case 400, 403, 404, 409, 503:
|
|
http.Error(w, err.Error(), code)
|
|
return
|
|
}
|
|
}
|
|
writeStoreErr(w, err)
|
|
}
|
|
func writeProjectJSON(w http.ResponseWriter, p any, err error) {
|
|
if err != nil {
|
|
writeProjectErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, p)
|
|
}
|
|
|
|
// publicProjectMetadata also masks provider-returned metadata, since providers
|
|
// may obtain raw owner-management rows before producing a shared public view.
|
|
func (s *Server) publicProjectMetadata(p core.Project) core.Project {
|
|
if s.projectLifecycle != nil {
|
|
p.Settings = s.set.MaskedProjectMetadata(p.Settings)
|
|
}
|
|
return p
|
|
}
|