Files
Agent-System-Backend/internal/server/project_lifecycle.go
T
weichen adcd8fefd2 Fence U3 account writes and stabilize directory cursors
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.
2026-10-14 07:33:54 +00:00

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
}