113 lines
3.7 KiB
Go
113 lines
3.7 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
|
|
}
|
|
|
|
// projectRead is the read-side member decision (U3). Live work stays the
|
|
// authority for every member who is still active. A member the provider reports
|
|
// as no longer live (RDM state=removed) is not decided by a second membership
|
|
// probe: the decision goes back to the one resource authorization query surface
|
|
// the domain already answers, and only a shared-domain read grant opens the
|
|
// read path. Everything else stays denied and every write keeps projectWork.
|
|
//
|
|
// With no resource authorizer registered (EXT-8b V2 absent) this is exactly
|
|
// projectWork, so un-extended hosts keep their current denial unchanged.
|
|
func (s *Server) projectRead(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 nil
|
|
}
|
|
if projectID != "" && s.ProjectAuthzV2 != nil &&
|
|
s.authorizeSharedResource(owner, "read", "project", core.ID(projectID), "", core.ID(projectID)) {
|
|
return nil
|
|
}
|
|
return store.ErrNotFound
|
|
}
|
|
|
|
// 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
|
|
}
|