package main import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "strings" "time" "git.cin.su/m/Assistant/adapters/postgres" researchadapter "git.cin.su/m/Assistant/adapters/research" "git.cin.su/m/Assistant/contracts/epistemics" secretcontracts "git.cin.su/m/Assistant/contracts/secrets" "git.cin.su/m/Assistant/modules/epistemics/application" "git.cin.su/m/Assistant/platform/secrets" "git.cin.su/m/Assistant/runtime/health" ) func main() { ctx := context.Background() password, err := resolveDatabasePassword() if err != nil { log.Fatal(err) } repository, err := postgres.OpenEpistemics(ctx, postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("EPISTEMICS_POSTGRES_USER"), Password: password}) if err != nil { log.Fatal(err) } defer repository.Close() mux := http.NewServeMux() mux.Handle("/healthz", health.Handler{Runtime: "research-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1}) mux.Handle("/readyz", health.ReadinessHandler{Runtime: "research-runtime", Probes: []health.Probe{{Name: "epistemics-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}}) mux.HandleFunc("POST /v1/research/evaluate", func(w http.ResponseWriter, r *http.Request) { var request epistemics.ResearchEvaluationRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<20)).Decode(&request); err != nil { http.Error(w, "invalid research evidence", http.StatusBadRequest) return } result, err := application.EvaluateAndPersist(r.Context(), request, repository, time.Now().UnixMilli()) if err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(result) }) connector := researchadapter.RepositoryConnector{Root: os.Getenv("RESEARCH_SOURCE_ROOT")} mux.HandleFunc("POST /v1/research/repository/evaluate", func(w http.ResponseWriter, r *http.Request) { var request epistemics.RepositoryResearchRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10)).Decode(&request); err != nil { http.Error(w, "invalid repository research request", http.StatusBadRequest) return } candidate, err := connector.Collect(r.Context(), request, time.Now().UnixMilli()) if err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } result, err := application.EvaluateAndPersist(r.Context(), epistemics.ResearchEvaluationRequest{Plan: request.Plan, Candidates: []epistemics.ResearchCandidate{candidate}}, repository, time.Now().UnixMilli()) if err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(result) }) webConnector := researchadapter.HTTPConnector{AllowedHosts: allowedHosts(os.Getenv("RESEARCH_HTTP_ALLOWED_HOSTS")), Client: &http.Client{Timeout: 5 * time.Second}} mux.HandleFunc("POST /v1/research/web/evaluate", func(w http.ResponseWriter, r *http.Request) { var request epistemics.WebResearchRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10)).Decode(&request); err != nil { http.Error(w, "invalid web research request", http.StatusBadRequest) return } candidate, err := webConnector.Collect(r.Context(), request, time.Now().UnixMilli()) if err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } result, err := application.EvaluateAndPersist(r.Context(), epistemics.ResearchEvaluationRequest{Plan: request.Plan, Candidates: []epistemics.ResearchCandidate{candidate}}, repository, time.Now().UnixMilli()) if err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(result) }) addr := os.Getenv("LISTEN_ADDR") if addr == "" { addr = ":8087" } log.Printf("research-runtime listening on %s", addr) log.Fatal(http.ListenAndServe(addr, mux)) } func allowedHosts(value string) map[string]struct{} { result := map[string]struct{}{} for _, host := range strings.Split(value, ",") { if host = strings.ToLower(strings.TrimSpace(host)); host != "" { result[host] = struct{}{} } } return result } func resolveDatabasePassword() (string, error) { if _, ok := os.LookupEnv("EPISTEMICS_POSTGRES_PASSWORD"); ok { return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD direct environment secret is forbidden") } path, ref := os.Getenv("EPISTEMICS_POSTGRES_PASSWORD_FILE"), os.Getenv("EPISTEMICS_POSTGRES_CREDENTIAL_REF") if path == "" || ref == "" { return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD_FILE and EPISTEMICS_POSTGRES_CREDENTIAL_REF are required") } root := os.Getenv("SECRET_BACKEND_ROOT") if root == "" { root = "/run/secrets" } resolver, err := secrets.NewFileResolver(root) if err != nil { return "", err } return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path}) }