Why: Shared project reads previously doubled as write permission, project mutations bypassed the domain lifecycle, and coarse retrieval shared private resources. Evidence: SELF red-before-implementation logs cover projection, lifecycle, private work, resource filtering, canonical retrieval scope, publication source and namespace. SELF go test ./..., isolated PostgreSQL go test ./internal/store -count=1, and CGO_ENABLED=0 go build ./cmd/agent-system passed; task owner owns replay/CI. Impact: Add optional actor-scoped projection/lifecycle capabilities; registered providers control public lifecycle and live work. Preserve owned conversation history and ordinary unregistered behavior. Authorize knowledge by stored ID/source, map current host scopes through trusted service metadata, and filter artifact lists. Refs: Dev project-member-access implementation plan r1 Task 1; approved design r0.1; 2026-10-13 trusted publication Source hardening ruling
74 lines
2.2 KiB
Go
74 lines
2.2 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.CreateConversationInProject(owner, projectID, title, profile)
|
|
}
|
|
c, err := s.st.CreateConversation(owner, projectID, title, profile)
|
|
if errors.Is(err, store.ErrNotFound) && s.authorizeSharedResource(owner, "read", "project", core.ID(projectID), "", core.ID(projectID)) {
|
|
return s.st.CreateConversationInProject(owner, projectID, title, profile)
|
|
}
|
|
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)
|
|
}
|