644 lines
26 KiB
Go
644 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"git.cin.su/m/Assistant/adapters/postgres"
|
|
"git.cin.su/m/Assistant/contracts/capability"
|
|
"git.cin.su/m/Assistant/contracts/identity"
|
|
"git.cin.su/m/Assistant/contracts/interaction"
|
|
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
|
"git.cin.su/m/Assistant/contracts/situation"
|
|
"git.cin.su/m/Assistant/contracts/work"
|
|
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
|
|
identitydomain "git.cin.su/m/Assistant/modules/identity/domain"
|
|
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
|
situationdomain "git.cin.su/m/Assistant/modules/situation/domain"
|
|
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
|
"git.cin.su/m/Assistant/runtime/health"
|
|
"github.com/jackc/pgx/v5"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const maxCreatorCredentialsBytes = 4 << 10
|
|
|
|
func main() {
|
|
addr := os.Getenv("LISTEN_ADDR")
|
|
if addr == "" {
|
|
addr = ":8080"
|
|
}
|
|
bootstrapToken, err := readRequiredSecret("CREATOR_BOOTSTRAP_TOKEN")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
sessionKeyValue, err := readRequiredSecret("CREATOR_SESSION_SIGNING_KEY")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
creatorPassword, err := readRequiredSecret("CREATOR_PASSWORD")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
sessionKey := []byte(sessionKeyValue)
|
|
creatorUsername := os.Getenv("CREATOR_USERNAME")
|
|
repository, err := postgres.OpenSituation(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("POSTGRES_USER"), Password: os.Getenv("POSTGRES_PASSWORD")})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer repository.Close()
|
|
workRepository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: os.Getenv("WORK_POSTGRES_PASSWORD")})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer workRepository.Close()
|
|
capabilityRepository, err := postgres.OpenCapability(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("CAPABILITY_POSTGRES_USER"), Password: os.Getenv("CAPABILITY_POSTGRES_PASSWORD")})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer capabilityRepository.Close()
|
|
identityRepository, err := postgres.OpenIdentity(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("IDENTITY_POSTGRES_USER"), Password: os.Getenv("IDENTITY_POSTGRES_PASSWORD")})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer identityRepository.Close()
|
|
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
interactionUser := os.Getenv("INTERACTION_POSTGRES_USER")
|
|
if interactionUser == "" {
|
|
interactionUser = "interaction_role"
|
|
}
|
|
interactionRepository, err := postgres.OpenInteraction(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: interactionUser, Password: interactionPassword})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer interactionRepository.Close()
|
|
mux := http.NewServeMux()
|
|
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
|
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
|
if err != nil {
|
|
http.Error(w, "creator session denied", http.StatusUnauthorized)
|
|
return identity.CreatorSession{}, false
|
|
}
|
|
return session, true
|
|
}
|
|
resolveFocus := func(r *http.Request) (situation.FocusBinding, error) {
|
|
runtime := situation.RuntimeBinding{SessionID: situation.SessionID(r.URL.Query().Get("session_id")), Workspace: situation.WorkspaceID(r.URL.Query().Get("workspace_id"))}
|
|
bindings, err := repository.ByRuntime(r.Context(), runtime)
|
|
if err != nil {
|
|
return situation.FocusBinding{}, err
|
|
}
|
|
return situationdomain.NewResolver(bindings).Resolve(runtime)
|
|
}
|
|
mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
|
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
|
|
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
|
|
session, ok := authenticateCreator(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(session)
|
|
})
|
|
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
|
|
session, ok := authenticateCreator(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
focus, err := resolveFocus(r)
|
|
if err != nil {
|
|
http.Error(w, "creator focus unavailable", http.StatusConflict)
|
|
return
|
|
}
|
|
if focus.ActorID != session.ActorID {
|
|
http.Error(w, "creator focus denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(focus)
|
|
})
|
|
mux.HandleFunc("GET /v1/creator/tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
session, ok := authenticateCreator(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
focus, err := resolveFocus(r)
|
|
if err != nil || focus.ActorID != session.ActorID {
|
|
http.Error(w, "creator focus denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
target, err := workRepository.Load(r.Context(), work.TaskID(r.PathValue("id")))
|
|
if err != nil {
|
|
http.Error(w, "task not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err := situationdomain.ValidateTaskTarget(focus, target); err != nil {
|
|
http.Error(w, "task outside creator focus", http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(target)
|
|
})
|
|
mux.HandleFunc("GET /v1/creator/capabilities/{id}/health", func(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := authenticateCreator(w, r); !ok {
|
|
return
|
|
}
|
|
status, err := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID(r.PathValue("id")))
|
|
if err != nil {
|
|
http.Error(w, "capability health not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(status)
|
|
})
|
|
mux.HandleFunc("POST /v1/creator/chat/messages", func(w http.ResponseWriter, r *http.Request) {
|
|
session, ok := authenticateCreator(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var request interaction.ChatMessageRequest
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, interactiondomain.MaxHumanUtteranceBytes+1024))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&request); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
|
|
http.Error(w, "invalid chat message", http.StatusBadRequest)
|
|
return
|
|
}
|
|
now := time.Now()
|
|
var conversation interaction.Conversation
|
|
if request.ConversationID == "" {
|
|
conversation, err = interactiondomain.NewConversation(session.ActorID, session.PrincipalID, now)
|
|
if err != nil {
|
|
http.Error(w, "conversation unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
} else {
|
|
conversation, err = interactionRepository.Conversation(r.Context(), request.ConversationID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
http.Error(w, "conversation not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(w, "conversation unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
if conversation.ActorID != session.ActorID || conversation.PrincipalID != session.PrincipalID {
|
|
http.Error(w, "conversation denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
conversation.UpdatedAtUnixMillis = now.UnixMilli()
|
|
}
|
|
utterance, intent, relations, err := interactiondomain.NewHumanUtterance(conversation, request, now)
|
|
if err != nil {
|
|
http.Error(w, "invalid chat message", http.StatusBadRequest)
|
|
return
|
|
}
|
|
projects, err := repository.ListProjects(r.Context())
|
|
if err != nil {
|
|
http.Error(w, "project registry unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
var previousFocus *situation.ActiveProjectFocus
|
|
storedFocus, err := repository.ActiveProjectFocus(r.Context(), session.ActorID, session.PrincipalID)
|
|
if err == nil {
|
|
previousFocus = &storedFocus
|
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
|
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
semantic, err := interactiondomain.InterpretUtterance(utterance, projects, previousFocus, now)
|
|
if err != nil {
|
|
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
|
|
http.Error(w, "save chat message", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Projects, semantic.Relations); err != nil {
|
|
http.Error(w, "save chat semantics", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
activeFocusName := ""
|
|
activeFocusProvenance := situation.ProjectResolutionProvenance("")
|
|
if semantic.ActiveFocus != nil {
|
|
if err := repository.SaveActiveProjectFocus(r.Context(), *semantic.ActiveFocus); err != nil {
|
|
http.Error(w, "save project focus", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
activeFocusName = semantic.ActiveFocus.ProjectDisplayName
|
|
activeFocusProvenance = semantic.ActiveFocus.Provenance
|
|
} else if previousFocus != nil {
|
|
activeFocusName = previousFocus.ProjectDisplayName
|
|
activeFocusProvenance = previousFocus.Provenance
|
|
}
|
|
response := interaction.ChatMessageResponse{
|
|
ConversationID: conversation.ID,
|
|
UtteranceID: utterance.ID,
|
|
SpeechAct: semantic.Interpretation.SpeechAct,
|
|
InterpretationState: utterance.InterpretationState,
|
|
NeedsCognition: semantic.NeedsCognition,
|
|
FallbackText: semantic.Reply,
|
|
ActiveProjectName: activeFocusName,
|
|
ProjectProvenance: activeFocusProvenance,
|
|
Clarification: semantic.Interpretation.Clarification,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(response)
|
|
})
|
|
mux.HandleFunc("GET /v1/principal-bindings/{actor}/{principal}", func(w http.ResponseWriter, r *http.Request) {
|
|
binding, err := identityRepository.LoadBinding(r.Context(), identity.ActorID(r.PathValue("actor")), identity.PrincipalID(r.PathValue("principal")))
|
|
if err != nil {
|
|
http.Error(w, "principal binding not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(binding)
|
|
})
|
|
mux.HandleFunc("GET /v1/tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
target, err := workRepository.Load(r.Context(), work.TaskID(r.PathValue("id")))
|
|
if err != nil {
|
|
http.Error(w, "task not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(target)
|
|
})
|
|
mux.HandleFunc("POST /v1/capabilities/health", func(w http.ResponseWriter, r *http.Request) {
|
|
var status capability.Health
|
|
if err := json.NewDecoder(r.Body).Decode(&status); err != nil {
|
|
http.Error(w, "invalid capability health", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := capabilitydomain.ValidateHealth(status); err != nil {
|
|
http.Error(w, "invalid capability health", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := capabilityRepository.SaveHealth(r.Context(), status); err != nil {
|
|
http.Error(w, "save capability health", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
})
|
|
mux.HandleFunc("GET /v1/capabilities/{id}/health", func(w http.ResponseWriter, r *http.Request) {
|
|
status, err := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID(r.PathValue("id")))
|
|
if err != nil {
|
|
http.Error(w, "capability health not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(status)
|
|
})
|
|
mux.HandleFunc("GET /v1/capability-bindings/{id}/revisions/{revision}", func(w http.ResponseWriter, r *http.Request) {
|
|
revision, err := strconv.ParseInt(r.PathValue("revision"), 10, 64)
|
|
if err != nil || revision < 1 {
|
|
http.Error(w, "invalid binding revision", http.StatusBadRequest)
|
|
return
|
|
}
|
|
binding, err := capabilityRepository.LoadBinding(r.Context(), capability.CapabilityBindingID(r.PathValue("id")), revision)
|
|
if err != nil {
|
|
http.Error(w, "capability binding not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err = capabilitydomain.ValidateBinding(binding); err != nil {
|
|
http.Error(w, "capability binding invalid", http.StatusConflict)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(binding)
|
|
})
|
|
mux.HandleFunc("GET /v1/capability-bindings/active", func(w http.ResponseWriter, r *http.Request) {
|
|
capabilityID := capability.CapabilityID(r.URL.Query().Get("capability_id"))
|
|
ownerID := identity.PrincipalID(r.URL.Query().Get("owner_principal_id"))
|
|
if capabilityID == "" || ownerID == "" {
|
|
http.Error(w, "invalid binding query", http.StatusBadRequest)
|
|
return
|
|
}
|
|
binding, err := capabilityRepository.LoadActiveBinding(r.Context(), capabilityID, ownerID)
|
|
if err != nil {
|
|
http.Error(w, "active capability binding not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err = capabilitydomain.UsableBinding(binding); err != nil {
|
|
http.Error(w, "active capability binding unavailable", http.StatusConflict)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(binding)
|
|
})
|
|
mux.HandleFunc("POST /v1/cognitive-bindings/resolve", func(w http.ResponseWriter, r *http.Request) {
|
|
var requirement capability.CognitiveRequirement
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 16*1024+1))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&requirement); err != nil || requirement.CapabilityID == "" || requirement.OwnerPrincipalID == "" || requirement.InputBytes < 0 || requirement.MaxOutputBytes < 1 {
|
|
http.Error(w, "invalid cognitive requirement", http.StatusBadRequest)
|
|
return
|
|
}
|
|
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
|
|
if err != nil {
|
|
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
selected, err := capabilitydomain.SelectCognitiveBinding(requirement, candidates)
|
|
if err != nil {
|
|
http.Error(w, "no invocable cognitive binding", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(selected)
|
|
})
|
|
mux.HandleFunc("POST /v1/cognitive-registry/discoveries", func(w http.ResponseWriter, r *http.Request) {
|
|
var snapshot capability.CognitiveDiscoverySnapshot
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&snapshot); err != nil {
|
|
http.Error(w, "invalid cognitive discovery", http.StatusBadRequest)
|
|
return
|
|
}
|
|
registration, err := capabilitydomain.BuildDiscoveryRegistration(snapshot)
|
|
if err != nil {
|
|
http.Error(w, "invalid cognitive discovery", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := capabilityRepository.SaveCognitiveDiscovery(r.Context(), registration); err != nil {
|
|
http.Error(w, "save cognitive discovery", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(registration)
|
|
})
|
|
mux.HandleFunc("POST /v1/cognitive-registry/evaluated-capabilities", func(w http.ResponseWriter, r *http.Request) {
|
|
var value capability.EvaluatedCapability
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&value); err != nil || value.ID == "" || value.BindingID == "" || value.BindingRevision < 1 || value.CapabilityID == "" || value.BenchmarkVersion == "" || value.ArtifactID == "" || value.ObservedAtUnixMillis < 1 {
|
|
http.Error(w, "invalid evaluated capability", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := capabilityRepository.SaveEvaluatedCapability(r.Context(), value); err != nil {
|
|
http.Error(w, "save evaluated capability", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
})
|
|
mux.HandleFunc("POST /v1/creator/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
|
|
session, ok := authenticateCreator(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var request capability.CognitiveCanaryPromotionRequest
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&request); err != nil {
|
|
http.Error(w, "invalid canary promotion", http.StatusBadRequest)
|
|
return
|
|
}
|
|
request.RequestedByPrincipalID = session.PrincipalID
|
|
request.CapabilityID = "creator-advisory"
|
|
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
|
|
if err != nil {
|
|
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
var current capability.ResolvedCognitiveBinding
|
|
for _, candidate := range candidates {
|
|
if candidate.Binding.ID == request.BindingID && candidate.Binding.Revision == request.ExpectedBindingRevision {
|
|
current = candidate
|
|
break
|
|
}
|
|
}
|
|
promotion, err := capabilitydomain.BuildCanaryPromotion(request, current)
|
|
if err != nil {
|
|
http.Error(w, "canary promotion precondition failed", http.StatusConflict)
|
|
return
|
|
}
|
|
if err = capabilityRepository.SaveCognitiveCanaryPromotion(r.Context(), promotion); err != nil {
|
|
http.Error(w, "canary promotion conflict", http.StatusConflict)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(promotion)
|
|
})
|
|
mux.HandleFunc("POST /v1/cognitive-registry/performance-evidence", func(w http.ResponseWriter, r *http.Request) {
|
|
var value capability.PerformanceEvidence
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&value); err != nil || value.ID == "" || value.BindingID == "" || value.BindingRevision < 1 || value.ModelDescriptorID == "" || value.ModelRevision < 1 || value.HardwareProfile == "" || value.Runtime == "" || value.BenchmarkVersion == "" || value.ArtifactID == "" || value.ObservedAtUnixMillis < 1 {
|
|
http.Error(w, "invalid performance evidence", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := capabilityRepository.SavePerformanceEvidence(r.Context(), value); err != nil {
|
|
http.Error(w, "save performance evidence", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
})
|
|
mux.HandleFunc("POST /v1/cognitive-usage-observations", func(w http.ResponseWriter, r *http.Request) {
|
|
var observation capability.CognitiveUsageObservation
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 16*1024+1))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&observation); err != nil || observation.ObservationID == "" || observation.RequestID == "" || observation.BindingID == "" || observation.BindingRevision < 1 || observation.ProviderInstanceID == "" || observation.ModelDescriptorID == "" || observation.ObservedAtUnixMillis < 1 || observation.LatencyMillis < 0 || observation.InputUnits < 0 || observation.OutputUnits < 0 || (observation.EstimatedCostMicros != nil && *observation.EstimatedCostMicros < 0) || (observation.ActualCostMicros != nil && *observation.ActualCostMicros < 0) || observation.ResultClass == "" {
|
|
http.Error(w, "invalid cognitive usage observation", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := capabilityRepository.SaveCognitiveUsageObservation(r.Context(), observation); err != nil {
|
|
http.Error(w, "save cognitive usage observation", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
})
|
|
mux.HandleFunc("POST /v1/capabilities/health-projection/rebuild", func(w http.ResponseWriter, r *http.Request) {
|
|
rebuilt, err := capabilityRepository.RebuildHealthProjection(r.Context())
|
|
if err != nil {
|
|
http.Error(w, "rebuild capability health projection", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(struct{ Rebuilt int64 }{rebuilt})
|
|
})
|
|
mux.HandleFunc("POST /v1/focus-bindings", func(w http.ResponseWriter, r *http.Request) {
|
|
var binding situation.FocusBinding
|
|
if err := json.NewDecoder(r.Body).Decode(&binding); err != nil {
|
|
http.Error(w, "invalid focus binding", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := repository.Save(r.Context(), binding); err != nil {
|
|
http.Error(w, "save focus binding", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
})
|
|
mux.HandleFunc("GET /v1/focus", func(w http.ResponseWriter, r *http.Request) {
|
|
focus, err := resolveFocus(r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusConflict)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(focus)
|
|
})
|
|
mux.HandleFunc("POST /v1/tasks", func(w http.ResponseWriter, r *http.Request) {
|
|
var target work.TaskTarget
|
|
if err := json.NewDecoder(r.Body).Decode(&target); err != nil {
|
|
http.Error(w, "invalid task", 400)
|
|
return
|
|
}
|
|
if err := workRepository.Create(r.Context(), target); err != nil {
|
|
http.Error(w, "create task", 500)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
})
|
|
mux.HandleFunc("POST /v1/tasks/{id}/commit", func(w http.ResponseWriter, r *http.Request) {
|
|
var target work.TaskTarget
|
|
if err := json.NewDecoder(r.Body).Decode(&target); err != nil {
|
|
http.Error(w, "invalid task commit", 400)
|
|
return
|
|
}
|
|
target.TaskID = work.TaskID(r.PathValue("id"))
|
|
revision, err := workRepository.Commit(r.Context(), target)
|
|
if err != nil {
|
|
http.Error(w, "task revision conflict", http.StatusConflict)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(struct{ Revision work.TaskRevision }{revision})
|
|
})
|
|
mux.HandleFunc("POST /v1/focus-task-commits", func(w http.ResponseWriter, r *http.Request) {
|
|
var request struct {
|
|
SessionID situation.SessionID
|
|
Workspace situation.WorkspaceID
|
|
Target work.TaskTarget
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
http.Error(w, "invalid guarded commit", http.StatusBadRequest)
|
|
return
|
|
}
|
|
bindings, err := repository.ByRuntime(r.Context(), situation.RuntimeBinding{SessionID: request.SessionID, Workspace: request.Workspace})
|
|
if err != nil {
|
|
http.Error(w, "load focus", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
focus, err := situationdomain.NewResolver(bindings).Resolve(situation.RuntimeBinding{SessionID: request.SessionID, Workspace: request.Workspace})
|
|
if err != nil {
|
|
http.Error(w, "focus unavailable", http.StatusConflict)
|
|
return
|
|
}
|
|
if err := situationdomain.ValidateTaskTarget(focus, request.Target); err != nil {
|
|
http.Error(w, "target outside resolved focus", http.StatusConflict)
|
|
return
|
|
}
|
|
revision, err := workRepository.Commit(r.Context(), request.Target)
|
|
if err != nil {
|
|
http.Error(w, "task revision conflict", http.StatusConflict)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(struct{ Revision work.TaskRevision }{revision})
|
|
})
|
|
log.Printf("core-runtime listening on %s", addr)
|
|
if err := http.ListenAndServe(addr, mux); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword string, sessionKey []byte, now func() time.Time) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
authenticated := identitydomain.BootstrapMatches(bootstrapToken, bearerToken(r))
|
|
if !authenticated {
|
|
var credentials identity.CreatorCredentials
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&credentials); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
|
|
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
authenticated = identitydomain.CreatorCredentialsMatch(creatorUsername, creatorPassword, credentials)
|
|
}
|
|
if !authenticated {
|
|
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
token, session, err := identitydomain.IssueCreatorSession(sessionKey, now(), 8*time.Hour)
|
|
if err != nil {
|
|
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(struct {
|
|
Token string
|
|
Session identity.CreatorSession
|
|
}{token, session})
|
|
}
|
|
}
|
|
|
|
func bearerToken(r *http.Request) string {
|
|
value := r.Header.Get("Authorization")
|
|
if !strings.HasPrefix(value, "Bearer ") {
|
|
return ""
|
|
}
|
|
return strings.TrimPrefix(value, "Bearer ")
|
|
}
|
|
|
|
func readRequiredSecret(name string) (string, error) {
|
|
direct, directSet := os.LookupEnv(name)
|
|
fileName, fileSet := os.LookupEnv(name + "_FILE")
|
|
if directSet && fileSet {
|
|
return "", fmt.Errorf("%s and %s_FILE cannot both be configured", name, name)
|
|
}
|
|
if fileSet {
|
|
if fileName == "" {
|
|
return "", fmt.Errorf("%s_FILE is empty", name)
|
|
}
|
|
contents, err := os.ReadFile(fileName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read %s_FILE: %w", name, err)
|
|
}
|
|
direct = strings.TrimRight(string(contents), "\r\n")
|
|
if strings.ContainsAny(direct, "\r\n") {
|
|
return "", fmt.Errorf("%s_FILE must contain one line", name)
|
|
}
|
|
}
|
|
if direct == "" {
|
|
return "", fmt.Errorf("%s is required", name)
|
|
}
|
|
return direct, nil
|
|
}
|
|
|
|
func readCredentialRefSecret(name, refName string) (string, error) {
|
|
if _, directSet := os.LookupEnv(name); directSet {
|
|
return "", fmt.Errorf("%s direct environment secret is forbidden; use %s_FILE", name, name)
|
|
}
|
|
fileName := os.Getenv(name + "_FILE")
|
|
if fileName == "" {
|
|
return "", fmt.Errorf("%s_FILE is required", name)
|
|
}
|
|
ref := os.Getenv(refName)
|
|
if ref == "" {
|
|
return "", fmt.Errorf("%s is required", refName)
|
|
}
|
|
root := os.Getenv("SECRET_BACKEND_ROOT")
|
|
if root == "" {
|
|
root = "/run/secrets"
|
|
}
|
|
resolver, err := platformsecrets.NewFileResolver(root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resolver.ResolveFile(secretcontracts.SecretFileRef{
|
|
CredentialRef: secretcontracts.CredentialRef(ref),
|
|
Purpose: secretcontracts.SecretPurposeDatabasePassword,
|
|
Consumer: secretcontracts.RuntimeConsumerCore,
|
|
Path: fileName,
|
|
})
|
|
}
|