package main import ( "context" "encoding/json" "errors" "io" "log" "net" "net/http" "orchestra/internal/admin" "orchestra/internal/authz" "orchestra/internal/delivery" "orchestra/internal/domain" "orchestra/internal/federation" "orchestra/internal/herdr" "orchestra/internal/operations" "orchestra/internal/orchestrator" "orchestra/internal/provider" "orchestra/internal/registry" "orchestra/internal/router" "orchestra/internal/store" "os" "path/filepath" "strconv" "strings" "time" ) func id() string { return domain.NewID() } const defaultHerdrPort = "9245" func herdrAddress(rr registry.Registry, h registry.Herdr) string { if h.Address != "" { return h.Address } m, ok := rr.Machine(h.MachineID) if !ok { return "" } host, _, err := net.SplitHostPort(m.Address) if err != nil { return m.Address } return net.JoinHostPort(host, defaultHerdrPort) } func main() { dir := os.Getenv("ORCHESTRA_DATA") if dir == "" { dir = "./data" } s, err := store.Open(dir) if err != nil { log.Fatal(err) } var rr registry.Registry var rt *router.Router var coordinator *orchestrator.Coordinator if config := os.Getenv("ORCHESTRA_CONFIG"); config != "" { if rr, err = registry.Load(config); err != nil { log.Fatalf("load orchestra config: %v", err) } rt = &router.Router{Store: s, Registry: rr, Reachability: registry.TCPReachability{}, Timeout: time.Second, Retry: router.RetryPolicy{MaxAttempts: 3, Backoff: time.Minute}} limits := map[string]router.QuotaWindowLimits{} for _, h := range rr.Herdrs() { w := router.QuotaWindowLimits{FiveHour: h.QuotaLimit5h, Weekly: h.QuotaLimitWeekly} if w.Weekly <= 0 && h.QuotaLimit > 0 { // Back-compat: the old single-window field meant weekly. w.Weekly = h.QuotaLimit } if w.FiveHour > 0 || w.Weekly > 0 { limits[h.ID] = w } } if len(limits) > 0 { rt.Availability = router.QuotaAvailability{Store: s, Limits: limits} } if repo, root := os.Getenv("ORCHESTRA_REPO"), os.Getenv("ORCHESTRA_WORKTREE_ROOT"); repo != "" && root != "" { adapters := map[string]herdr.Adapter{} for _, h := range rr.Herdrs() { address := herdrAddress(rr, h) if address == "" { continue } client := herdr.New(address) protocol := h.Protocol if protocol == "" { protocol = os.Getenv("ORCHESTRA_HERDR_PROTOCOL") } if err := client.CheckProtocol(context.Background(), protocol); err != nil { log.Printf("herdr %s unavailable: %v", h.ID, err) continue } switch h.Harness { case "claude": adapters[h.ID] = herdr.Claude(client, 200000, s) case "opencode": adapters[h.ID] = herdr.OpenCode(client, 200000, s) case "codex", "": adapters[h.ID] = herdr.Codex(client, 200000, s) default: log.Printf("herdr %s has unsupported harness %q", h.ID, h.Harness) } } projectRepos := map[string]orchestrator.ProjectRepo{} for _, p := range rr.Projects() { if p.Repo != "" && p.WorktreeRoot != "" { projectRepos[p.ID] = orchestrator.ProjectRepo{Repo: p.Repo, WorktreeRoot: p.WorktreeRoot} } } worktrees := orchestrator.PerProjectGitWorktrees{ Projects: projectRepos, Default: orchestrator.GitWorktrees{Root: root, Repo: repo}, } coordinator = &orchestrator.Coordinator{Store: s, StatePath: filepath.Join(dir, "runtime-sessions.json"), Worktrees: worktrees, Adapters: orchestrator.AdapterFactory{Herdrs: adapters}} rt.OnLease = func(e domain.Event) error { return coordinator.Start(context.Background(), e) } hard := 0.75 if v, parseErr := strconv.ParseFloat(os.Getenv("ORCHESTRA_OCCUPANCY_HARD"), 64); parseErr == nil && v > 0 && v < 1 { hard = v } if v, parseErr := strconv.ParseFloat(os.Getenv("ORCHESTRA_OCCUPANCY_SOFT"), 64); parseErr == nil && v > 0 && v < 1 { coordinator.Soft = v } go func() { if monitorErr := coordinator.Monitor(context.Background(), hard, 30*time.Second); monitorErr != nil { log.Printf("orchestrator monitor: %v", monitorErr) } }() } } mux := http.NewServeMux() workers := &federation.Registry{AdmitToken: os.Getenv("ORCHESTRA_FEDERATION_ADMIT_TOKEN")} workers.OnOffline = func(w federation.Worker) { for _, t := range s.Tasks() { if t.State == domain.StateLeased && t.Lease != nil && t.Lease.HarnessID == w.ID { p, _ := json.Marshal(map[string]any{"reason": "worker_offline", "harness_id": w.ID}) e := domain.Event{ID: id(), Type: "TaskReleased", TaskID: t.ID, Version: t.Version + 1, Payload: p, Surface: string(authz.System)} if err := s.Append(e); err == nil && rt != nil { _, _ = rt.HandleEvent(e) } } } } providerHealth := map[string]*provider.Supervisor{} surface := func(r *http.Request) authz.Surface { v := authz.ParseSurface(r.Header.Get("X-Orchestra-Surface")) if v == "" || v == authz.System { // System means "the plane itself, in-process" and is always // FullControl with no token gate (AUDIT.md B8) — it must never // be constructible from an HTTP request, or any LAN caller // declaring this header gets unauthenticated full control. return authz.Web } return v } mux.HandleFunc("/v1/tasks", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { json.NewEncoder(w).Encode(s.Tasks()) return } if r.Method != "POST" { http.Error(w, "method not allowed", 405) return } var p map[string]any if json.NewDecoder(r.Body).Decode(&p) != nil { http.Error(w, "invalid json", 400) return } b, _ := json.Marshal(p) e := domain.Event{ID: id(), Type: "TaskCreated", TaskID: id(), Version: 1, Payload: b, Surface: string(surface(r))} if err := s.Append(e); err != nil { if errors.Is(err, domain.ErrDuplicate) { // (source, external_id) was already ingested. Nothing was // appended; report the existing task idempotently rather // than fabricating a 201 with an unrelated event (S6). source, _ := p["source"].(string) externalID, _ := p["external_id"].(string) if t, ok := s.TaskBySource(source, externalID); ok { w.WriteHeader(200) json.NewEncoder(w).Encode(t) return } } http.Error(w, err.Error(), 400) return } if rt != nil { if _, err := rt.HandleEvent(e); err != nil { log.Printf("route task: %v", err) } } w.WriteHeader(201) json.NewEncoder(w).Encode(e) }) mux.HandleFunc("/v1/events", func(w http.ResponseWriter, r *http.Request) { var n uint64 if x, err := strconv.ParseUint(r.URL.Query().Get("since"), 10, 64); err == nil { n = x } json.NewEncoder(w).Encode(s.Events(n)) }) mux.HandleFunc("/v1/handoffs", func(w http.ResponseWriter, r *http.Request) { out := make([]domain.Event, 0) for _, e := range s.Events(0) { if e.Type == "TaskReleased" { out = append(out, e) } } json.NewEncoder(w).Encode(out) }) mux.HandleFunc("/v1/quotas", func(w http.ResponseWriter, r *http.Request) { out := make([]domain.Event, 0) for _, e := range s.Events(0) { if e.Type == "QuotaReported" { out = append(out, e) } } json.NewEncoder(w).Encode(out) }) mux.HandleFunc("/v1/artifacts", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Reports and handoffs are content-addressed evidence. Keep uploads // bounded because event payloads only carry their resulting hash. r.Body = http.MaxBytesReader(w, r.Body, 4<<20) b, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "artifact too large or unreadable", http.StatusRequestEntityTooLarge) return } if len(b) == 0 { http.Error(w, "artifact is empty", http.StatusBadRequest) return } ref, err := s.PutArtifact(b) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{"ref": ref}) }) // /v1/harness/complete is the automatic TaskCompleted producer (AUDIT.md // B3): a harness-side hook posts here when the agent has declared the // task done (see deploy/hooks/orchestra-stop.sh), not on every turn // boundary. It reads the transcript locally to build an honest receipt — // same session-file assumption as CLIAdapter.Occupancy — rather than // trusting a self-reported number. harnessToken := os.Getenv("ORCHESTRA_HARNESS_TOKEN") mux.HandleFunc("/v1/harness/complete", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if harnessToken != "" && r.Header.Get("Authorization") != "Bearer "+harnessToken { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var p struct { TaskID string `json:"task_id"` Harness string `json:"harness"` TranscriptPath string `json:"transcript_path"` Report string `json:"report"` } if json.NewDecoder(r.Body).Decode(&p) != nil || p.TaskID == "" || p.Report == "" || p.TranscriptPath == "" { http.Error(w, "task_id, transcript_path, and report are required", http.StatusBadRequest) return } t, ok := s.Task(p.TaskID) if !ok { http.Error(w, "task not found", http.StatusNotFound) return } // Harness-specific session-state readers (AUDIT.md "Codex/opencode // completion producers"). Same local-filesystem assumption B3 already // made for Claude: the caller supplies the path to its own session // state (transcript / rollout / message file), never a herdr pane id. var usage herdr.Usage var err error switch p.Harness { case "codex": usage, err = herdr.CodexUsage(p.TranscriptPath) case "opencode": usage, err = herdr.OpenCodeUsage(p.TranscriptPath) case "", "claude": usage, err = herdr.ClaudeUsage(p.TranscriptPath) default: http.Error(w, "unknown harness: "+p.Harness, http.StatusBadRequest) return } if err != nil { http.Error(w, "reading transcript: "+err.Error(), http.StatusBadRequest) return } ref, err := s.PutArtifact([]byte(p.Report)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } payload, _ := json.Marshal(map[string]any{ "report_ref": ref, "receipt": map[string]any{ "input_tokens": usage.Input, "cache_read_tokens": usage.CacheRead, "cache_write_tokens": usage.CacheWrite, "output_tokens": usage.Output, "numerator": usage.Numerator(), }, }) e := domain.Event{ID: id(), Type: "TaskCompleted", TaskID: p.TaskID, Version: t.Version + 1, Payload: payload, Surface: string(authz.System)} if err := s.Append(e); err != nil { http.Error(w, err.Error(), http.StatusConflict) return } if rt != nil { if _, routeErr := rt.HandleEvent(e); routeErr != nil { log.Printf("route task: %v", routeErr) } } // B7 (AUDIT.md): QuotaReported has no other producer, so router's // 5h/weekly availability filter and the brief's quota_consumed are // permanently zero without this. Fed by the same per-harness usage // read as the receipt above (spec §7.2 — "same per-harness session // state as §5.2.1"); harness_id comes from the lease this completion // closes out, before it's released. if t.Lease != nil && t.Lease.HarnessID != "" { qp, _ := json.Marshal(map[string]any{ "harness_id": t.Lease.HarnessID, "consumed": float64(usage.Numerator()), }) qe := domain.Event{ID: id(), Type: "QuotaReported", TaskID: "system", Payload: qp, Surface: string(authz.System)} if err := s.Append(qe); err != nil { log.Printf("quota report: %v", err) } } json.NewEncoder(w).Encode(e) }) // /v1/harness/turn is the unified turn-decision endpoint (AUDIT.md Phase // 2 items 1-2): the Face-B stop hook posts here on every ordinary turn // boundary (report marker absent — /v1/harness/complete covers task // completion separately) and gets back exactly one of continue / // prepare_handoff / rotate_now / refuse, per spec §5.3. This replaces // what would otherwise be separate ad-hoc marker-file conventions per // decision. mux.HandleFunc("/v1/harness/turn", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if harnessToken != "" && r.Header.Get("Authorization") != "Bearer "+harnessToken { http.Error(w, "unauthorized", http.StatusUnauthorized) return } if coordinator == nil { http.Error(w, "coordinator not configured", http.StatusServiceUnavailable) return } var p struct { TaskID string `json:"task_id"` } if json.NewDecoder(r.Body).Decode(&p) != nil || p.TaskID == "" { http.Error(w, "task_id is required", http.StatusBadRequest) return } decision, err := coordinator.TurnDecision(r.Context(), p.TaskID) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } json.NewEncoder(w).Encode(map[string]string{"decision": decision}) }) mux.HandleFunc("/v1/brief", func(w http.ResponseWriter, r *http.Request) { to := time.Now().UTC() from := to.Add(-12 * time.Hour) if v, parseErr := time.Parse(time.RFC3339, r.URL.Query().Get("from")); parseErr == nil { from = v } if v, parseErr := time.Parse(time.RFC3339, r.URL.Query().Get("to")); parseErr == nil { to = v } git := map[string]operations.GitSync{} for _, p := range rr.Projects() { if p.Repo == "" { continue } git[p.ID] = operations.GitState(p.Repo) } if len(git) == 0 { if repo := os.Getenv("ORCHESTRA_REPO"); repo != "" { git["default"] = operations.GitState(repo) } } json.NewEncoder(w).Encode(operations.BuildBrief(s.Events(0), from, to, git)) }) standup := func() (domain.Event, error) { return operations.GenerateStandupAdvisory(s, time.Now().UTC()) } mux.HandleFunc("/v1/standup", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { json.NewEncoder(w).Encode(operations.StandupItems(s.Tasks())) return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", 405) return } if err := authz.AuthorizeEvent(surface(r), "StandupAdvisory"); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } e, err := standup() if err != nil { http.Error(w, err.Error(), 400) return } json.NewEncoder(w).Encode(e) }) mux.HandleFunc("/v1/standup/approve", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", 405) return } if err := authz.AuthorizeEvent(surface(r), "ApprovalGranted"); err != nil { http.Error(w, err.Error(), 403) return } var p struct { AdvisoryID string `json:"advisory_id"` } if json.NewDecoder(r.Body).Decode(&p) != nil || p.AdvisoryID == "" { http.Error(w, "advisory_id required", 400) return } b, _ := json.Marshal(map[string]any{"subject_ref": p.AdvisoryID}) e := domain.Event{ID: id(), Type: "ApprovalGranted", TaskID: "system", Version: 0, Payload: b, Surface: string(surface(r))} if err := s.Append(e); err != nil { http.Error(w, err.Error(), 400) return } json.NewEncoder(w).Encode(e) }) mux.HandleFunc("/v1/standup/apply", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", 405) return } if err := authz.AuthorizeEvent(surface(r), "TaskAmended"); err != nil { http.Error(w, err.Error(), 403) return } var p struct { AdvisoryID string `json:"advisory_id"` } if json.NewDecoder(r.Body).Decode(&p) != nil || p.AdvisoryID == "" { http.Error(w, "advisory_id required", 400) return } out, err := operations.ApplyAdvisory(s, p.AdvisoryID) if err != nil { http.Error(w, err.Error(), 400) return } json.NewEncoder(w).Encode(out) }) adminServer := &admin.Server{Store: s, RouterReady: rt != nil, Providers: providerHealth, Probes: map[string]admin.ProbeFunc{ "router": func() (bool, string) { return rt != nil, "configured router" }, "gitea": func() (bool, string) { configured := os.Getenv("ORCHESTRA_GITEA_URL") != "" || os.Getenv("ORCHESTRA_GITEA_CONFIG") != "" if !configured { return true, "configured provider" } for name := range providerHealth { if name == "gitea" || strings.HasPrefix(name, "gitea:") { return true, "configured provider" } } return false, "configured provider" }, "jsonl": func() (bool, string) { return os.Getenv("ORCHESTRA_JSONL") == "" || providerHealth["jsonl"] != nil, "configured provider" }, }} mux.HandleFunc("/metrics", adminServer.Metrics) mux.HandleFunc("/v1/events/subscribe", adminServer.Subscribe) mux.HandleFunc("/v1/admin/diagnostics", adminServer.Diagnostics) mux.HandleFunc("/v1/tasks/", func(w http.ResponseWriter, r *http.Request) { parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") if r.Method == http.MethodGet && coordinator != nil && len(parts) == 4 { taskID, view := parts[2], parts[3] if view == "health" { if h, ok := coordinator.MonitorHealth().Sessions[taskID]; ok { json.NewEncoder(w).Encode(h) return } http.Error(w, "session health not found", 404) return } if view == "capture" { body, err := coordinator.Capture(r.Context(), taskID, r.URL.Query().Get("source")) if err != nil { http.Error(w, err.Error(), 404) return } json.NewEncoder(w).Encode(map[string]string{"task_id": taskID, "source": r.URL.Query().Get("source"), "text": body}) return } } if len(parts) < 4 || len(parts) > 5 || r.Method != "POST" { http.Error(w, "not found", http.StatusNotFound) return } taskID, action := parts[2], parts[3] if action == "approval" { if err := authz.AuthorizeEvent(surface(r), map[bool]string{true: "ApprovalRequested", false: "ApprovalGranted"}[len(parts) == 4]); err != nil && len(parts) == 4 { http.Error(w, err.Error(), http.StatusForbidden) return } if len(parts) == 5 { typ := "ApprovalGranted" if parts[4] == "deny" { typ = "ApprovalDenied" } if err := authz.AuthorizeEvent(surface(r), typ); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if parts[4] != "grant" && parts[4] != "deny" { http.Error(w, "unknown approval action", 404) return } t, ok := s.Task(taskID) if !ok { http.Error(w, "task not found", 404) return } by := r.Header.Get("X-Orchestra-Actor") if by == "" { by = "surface" } b, _ := json.Marshal(map[string]any{"subject_ref": taskID, "by": by}) e := domain.Event{ID: id(), Type: typ, TaskID: taskID, Version: t.Version + 1, Payload: b, Surface: string(surface(r))} if err := s.Append(e); err != nil { http.Error(w, err.Error(), 409) return } json.NewEncoder(w).Encode(e) return } var p struct { Options []any `json:"options"` } if json.NewDecoder(r.Body).Decode(&p) != nil || len(p.Options) == 0 { http.Error(w, "options required", 400) return } if _, ok := s.Task(taskID); !ok { http.Error(w, "task not found", 404) return } b, _ := json.Marshal(map[string]any{"subject_ref": taskID, "options": p.Options}) t, _ := s.Task(taskID) e := domain.Event{ID: id(), Type: "ApprovalRequested", TaskID: taskID, Version: t.Version + 1, Payload: b, Surface: string(surface(r))} if err := s.Append(e); err != nil { http.Error(w, err.Error(), 400) return } json.NewEncoder(w).Encode(e) return } var e domain.Event var err error actionTypes := map[string]string{"lease": "TaskLeased", "release": "TaskReleased", "complete": "TaskCompleted", "block": "TaskBlocked"} if typ, known := actionTypes[action]; known { if err := authz.AuthorizeEvent(surface(r), typ); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } } switch action { case "lease": var p struct { HarnessID string `json:"harness_id"` TTLSeconds int `json:"ttl_seconds"` } if json.NewDecoder(r.Body).Decode(&p) != nil || p.HarnessID == "" { http.Error(w, "harness_id required", 400) return } if p.TTLSeconds <= 0 { p.TTLSeconds = 1800 } e, err = s.Lease(taskID, p.HarnessID, time.Duration(p.TTLSeconds)*time.Second) case "release", "complete", "block": t, ok := s.Task(taskID) if !ok { http.Error(w, "task not found", 404) return } types := map[string]string{"release": "TaskReleased", "complete": "TaskCompleted", "block": "TaskBlocked"} var p map[string]any if r.Body == nil || json.NewDecoder(r.Body).Decode(&p) != nil || p == nil { http.Error(w, "invalid lifecycle payload", http.StatusBadRequest) return } if action == "release" && p["reason"] == nil && p["handoff_ref"] == nil { http.Error(w, "reason or handoff_ref required", http.StatusBadRequest) return } if action == "block" && p["blocker"] == nil { http.Error(w, "blocker required", http.StatusBadRequest) return } if action == "complete" && p["report_ref"] == nil { http.Error(w, "report_ref required", http.StatusBadRequest) return } ePayload, _ := json.Marshal(p) e = domain.Event{ID: id(), Type: types[action], TaskID: taskID, Version: t.Version + 1, Payload: ePayload, Surface: string(surface(r))} err = s.Append(e) default: http.Error(w, "unknown action", 404) return } if err != nil { http.Error(w, err.Error(), 409) return } if rt != nil { if _, routeErr := rt.HandleEvent(e); routeErr != nil { log.Printf("route task: %v", routeErr) } } json.NewEncoder(w).Encode(e) }) if rt != nil { go func() { ticker := time.NewTicker(time.Second) defer ticker.Stop() for range ticker.C { // When a coordinator is running, its own Monitor loop (every // 30s) already calls Store.ExpireLeases and, critically, // kills the herdr session for whatever it expires (S9: two // independent expiry loops raced on the same reclaim, and // since this one runs every second vs. the coordinator's // 30s, it almost always won — meaning the coordinator's // ExpireLeases call saw nothing left to expire and its // session-kill path never ran, silently orphaning panes // past their lease TTL). Calling ExpireLeases here too would // just resurrect that race, so leave reclaim to the // coordinator and only keep retrying pending assignment. if coordinator != nil { if _, err := rt.AssignPending(); err != nil { log.Printf("route expired task: %v", err) } continue } if _, err := s.ExpireLeases(time.Now()); err != nil { log.Printf("expire leases: %v", err) } else if _, err := rt.AssignPending(); err != nil { log.Printf("route expired task: %v", err) } } }() } mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok\n")) }) mux.HandleFunc("/readyz", adminServer.Readiness) mux.HandleFunc("/v1/providers/health", func(w http.ResponseWriter, r *http.Request) { out := map[string]provider.Health{} for name, sup := range providerHealth { out[name] = sup.Health() } json.NewEncoder(w).Encode(out) }) mux.HandleFunc("/v1/orchestrator/health", func(w http.ResponseWriter, r *http.Request) { if coordinator == nil { http.Error(w, "orchestrator unavailable", http.StatusServiceUnavailable) return } json.NewEncoder(w).Encode(coordinator.MonitorHealth()) }) mux.HandleFunc("/v1/federation/workers", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { json.NewEncoder(w).Encode(workers.Snapshot()) return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", 405) return } var registration struct { federation.Worker Token string `json:"token"` } if json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(®istration) != nil { http.Error(w, "invalid worker", 400) return } worker := registration.Worker worker.Token = registration.Token if worker.Token == "" { http.Error(w, "token required", 400) return } admitToken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if err := workers.Register(worker, admitToken); err != nil { status := 400 if err == federation.ErrUnauthorized { status = 401 } http.Error(w, err.Error(), status) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(worker) }) workerAuth := func(r *http.Request) (string, error) { wid := r.Header.Get("X-Orchestra-Worker") tok := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if err := workers.Authenticate(wid, tok); err != nil { return "", err } return wid, nil } mux.HandleFunc("/v1/federation/events", func(w http.ResponseWriter, r *http.Request) { wid, err := workerAuth(r) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } if r.Method != http.MethodGet { http.Error(w, "method not allowed", 405) return } cursor, _ := workers.Cursor(wid) if q := r.URL.Query().Get("since"); q != "" { cursor, _ = strconv.ParseUint(q, 10, 64) } events := s.Events(cursor) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{"cursor": cursor, "events": events}) }) mux.HandleFunc("/v1/federation/events/ack", func(w http.ResponseWriter, r *http.Request) { wid, err := workerAuth(r) if err != nil { http.Error(w, err.Error(), 401) return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", 405) return } var body struct { Cursor uint64 `json:"cursor"` } if json.NewDecoder(r.Body).Decode(&body) != nil { http.Error(w, "invalid cursor", 400) return } if err := workers.Ack(wid, body.Cursor); err != nil { http.Error(w, err.Error(), 409) return } w.WriteHeader(http.StatusNoContent) }) mux.HandleFunc("/v1/federation/workers/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || (!strings.HasSuffix(r.URL.Path, "/heartbeat") && !strings.HasSuffix(r.URL.Path, "/claim") && !strings.HasSuffix(r.URL.Path, "/handoff")) { http.Error(w, "not found", 404) return } parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") if len(parts) != 5 { http.Error(w, "not found", 404) return } if _, err := workerAuth(r); err != nil { http.Error(w, err.Error(), 401) return } if strings.HasSuffix(r.URL.Path, "/heartbeat") { if err := workers.Heartbeat(parts[3]); err != nil { http.Error(w, err.Error(), 404) return } w.WriteHeader(http.StatusNoContent) return } var b struct { TaskID string `json:"task_id"` TTLSeconds int `json:"ttl_seconds"` HandoffRef string `json:"handoff_ref"` AnchorSHA string `json:"anchor_sha"` } if json.NewDecoder(r.Body).Decode(&b) != nil || b.TaskID == "" { http.Error(w, "invalid lease body", 400) return } t, ok := s.Task(b.TaskID) if !ok { http.Error(w, "task not found", 404) return } if strings.HasSuffix(r.URL.Path, "/claim") { if b.TTLSeconds <= 0 { b.TTLSeconds = 1800 } e, err := s.Lease(b.TaskID, parts[3], time.Duration(b.TTLSeconds)*time.Second) if err != nil { http.Error(w, err.Error(), 409) return } json.NewEncoder(w).Encode(e) return } if t.State != domain.StateLeased || t.Lease == nil || t.Lease.HarnessID != parts[3] { http.Error(w, "lease not owned", 409) return } if b.HandoffRef == "" { http.Error(w, "handoff_ref required", 400) return } if len(b.AnchorSHA) != 40 { http.Error(w, "anchor_sha required", 400) return } p, _ := json.Marshal(map[string]any{"handoff_ref": b.HandoffRef, "harness_id": parts[3], "anchor_sha": b.AnchorSHA}) e := domain.Event{ID: id(), Type: "TaskReleased", TaskID: b.TaskID, Version: t.Version + 1, Payload: p, Surface: string(authz.System)} if err := s.Append(e); err != nil { http.Error(w, err.Error(), 409) return } if rt != nil { _, _ = rt.HandleEvent(e) } json.NewEncoder(w).Encode(e) }) var giteaSources []provider.GiteaSourceConfig if path := os.Getenv("ORCHESTRA_GITEA_CONFIG"); path != "" { cfgs, err := provider.LoadGiteaConfigs(path) if err != nil { log.Fatalf("load gitea config: %v", err) } giteaSources = cfgs } else if base := os.Getenv("ORCHESTRA_GITEA_URL"); base != "" { // Legacy single-repo configuration: project defaults to the repo // name, matching the historical (pre-multi-source) behavior. giteaSources = []provider.GiteaSourceConfig{{ Project: os.Getenv("ORCHESTRA_GITEA_REPO"), BaseURL: base, Owner: os.Getenv("ORCHESTRA_GITEA_OWNER"), Repo: os.Getenv("ORCHESTRA_GITEA_REPO"), Token: os.Getenv("ORCHESTRA_GITEA_TOKEN"), WebhookSecret: os.Getenv("ORCHESTRA_GITEA_WEBHOOK_SECRET"), }} } if len(giteaSources) > 0 { reflectors := map[string]provider.Gitea{} for _, c := range giteaSources { g := provider.Gitea{BaseURL: c.BaseURL, Token: c.Token, WebhookSecret: c.WebhookSecret, Owner: c.Owner, Repo: c.Repo, Project: c.Project} reflectors[g.SourceName()] = g } reflecting := provider.ReflectingSink{Sink: s, Tasks: s, Reflector: provider.MultiGitea{Sources: reflectors}} for _, c := range giteaSources { g := provider.Gitea{BaseURL: c.BaseURL, Token: c.Token, WebhookSecret: c.WebhookSecret, Owner: c.Owner, Repo: c.Repo, Project: c.Project} name := "gitea:" + c.Project webhookPath := "/v1/providers/gitea/webhook/" + c.Project if len(giteaSources) == 1 && os.Getenv("ORCHESTRA_GITEA_CONFIG") == "" { // Preserve the legacy unprefixed webhook path when running // the single-source (env-var) configuration, so existing // Gitea webhook configs don't need to be re-pointed. webhookPath = "/v1/providers/gitea/webhook" name = "gitea" } mux.Handle(webhookPath, g.WebhookHandler(reflecting)) sup := &provider.Supervisor{Name: name, Run: func(ctx context.Context) error { pollCtx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() _, err := g.Poll(pollCtx, reflecting) if err == nil { select { case <-ctx.Done(): return ctx.Err() case <-time.After(time.Minute): } } return err }} sup.Start(context.Background()) providerHealth[name] = sup } } if path := os.Getenv("ORCHESTRA_JSONL"); path != "" { sup := &provider.Supervisor{Name: "jsonl", Run: func(ctx context.Context) error { return (provider.JSONLWatcher{Path: path, Interval: time.Second, Provider: provider.JSONL{Source: "jsonl"}}).Run(ctx, s) }} sup.Start(context.Background()) providerHealth["jsonl"] = sup } var senders []delivery.Sender if token, chat := os.Getenv("ORCHESTRA_TELEGRAM_BOT_TOKEN"), os.Getenv("ORCHESTRA_TELEGRAM_CHAT_ID"); token != "" && chat != "" { senders = append(senders, delivery.Telegram{Token: token, ChatID: chat}) } if topic := os.Getenv("ORCHESTRA_NTFY_TOPIC"); topic != "" { senders = append(senders, delivery.Ntfy{Topic: topic, Token: os.Getenv("ORCHESTRA_NTFY_TOKEN"), URL: os.Getenv("ORCHESTRA_NTFY_URL")}) } if len(senders) > 0 { cursorPath := filepath.Join(dir, "delivery-cursor") var startCursor uint64 if b, err := os.ReadFile(cursorPath); err == nil { if v, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64); err == nil { startCursor = v } } go func() { fanout := &delivery.Fanout{ Senders: senders, Cursor: startCursor, SaveCursor: func(cursor uint64) { if err := os.WriteFile(cursorPath, []byte(strconv.FormatUint(cursor, 10)), 0o644); err != nil { log.Printf("delivery cursor persist: %v", err) } }, } err := fanout.Run(context.Background(), s.Events) if err != nil { log.Printf("delivery fanout: %v", err) } }() } go func() { lastDay := "" t := time.NewTicker(time.Minute) defer t.Stop() for now := range t.C { utc := now.UTC() day := utc.Format("2006-01-02") if utc.Hour() == 3 && day != lastDay { if _, err := standup(); err != nil { log.Printf("standup advisory: %v", err) } else { lastDay = day } } } }() if rt != nil { if _, err := rt.AssignPending(); err != nil { log.Printf("startup task assignment: %v", err) } } port := os.Getenv("ORCHESTRA_PORT") if port == "" { port = "9145" } log.Println("orchestra listening on :" + port) tokens := map[authz.Surface]string{ authz.TUI: os.Getenv("ORCHESTRA_TUI_TOKEN"), authz.Web: os.Getenv("ORCHESTRA_WEB_TOKEN"), authz.MCP: os.Getenv("ORCHESTRA_MCP_TOKEN"), authz.Maven: os.Getenv("ORCHESTRA_MAVEN_TOKEN"), authz.Telegram: os.Getenv("ORCHESTRA_TELEGRAM_TOKEN"), authz.Ntfy: os.Getenv("ORCHESTRA_NTFY_TOKEN"), } log.Fatal(http.ListenAndServe(":"+port, authz.HTTP(tokens, mux))) } func auth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { surface := strings.ToLower(r.Header.Get("X-Orchestra-Surface")) if surface == "telegram" || surface == "ntfy" { if r.Method != "GET" { http.Error(w, "notify-only surface", 403) return } } next.ServeHTTP(w, r) }) }