Report queued tasks the scheduling pass never considers

A task in retry backoff was filtered out before the candidate loop, so it
recorded no rejection at all: queued, apparently assignable, and silent. That is
the exact shape that made F5 take a live session to diagnose. It now reports
"retry backoff until <time>".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 23:42:26 +04:00
parent 0ead6d2d02
commit 4fbf3ac966
18 changed files with 1163 additions and 165 deletions
-27
View File
@@ -1,27 +0,0 @@
// orchestra-password prints a bcrypt hash suitable for ORCHESTRA_WEB_PASSWORD_HASH.
package main
import (
"fmt"
"os"
"syscall"
"golang.org/x/crypto/bcrypt"
"golang.org/x/term"
)
func main() {
fmt.Fprint(os.Stderr, "Password: ")
password, err := term.ReadPassword(int(syscall.Stdin))
fmt.Fprintln(os.Stderr)
if err != nil || len(password) == 0 {
fmt.Fprintln(os.Stderr, "password is required")
os.Exit(1)
}
hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
fmt.Fprintln(os.Stderr, "hash password:", err)
os.Exit(1)
}
fmt.Println(string(hash))
}
+91
View File
@@ -0,0 +1,91 @@
// orchestra-user manages browser-operator accounts in Orchestra's embedded
// credential database. Passwords are read from the terminal and hashed inside
// the database; no reusable hash has to be copied into deployment config.
package main
import (
"flag"
"fmt"
"log"
"orchestra/internal/authn"
"os"
"syscall"
"golang.org/x/term"
)
func usage() {
fmt.Fprintln(os.Stderr, "usage:")
fmt.Fprintln(os.Stderr, " orchestra-user set -data DIR -username NAME")
fmt.Fprintln(os.Stderr, " orchestra-user list -data DIR")
}
func readPassword(prompt string) (string, error) {
fmt.Fprint(os.Stderr, prompt)
value, err := term.ReadPassword(int(syscall.Stdin))
fmt.Fprintln(os.Stderr)
return string(value), err
}
func open(data string) *authn.Store {
if data == "" {
log.Fatal("-data is required")
}
users, err := authn.Open(authn.Path(data))
if err != nil {
log.Fatal(err)
}
return users
}
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "set":
flags := flag.NewFlagSet("set", flag.ExitOnError)
data := flags.String("data", "", "Orchestra data directory")
username := flags.String("username", "", "operator username")
_ = flags.Parse(os.Args[2:])
password, err := readPassword("New password: ")
if err != nil {
log.Fatal(err)
}
confirmation, err := readPassword("Confirm password: ")
if err != nil {
log.Fatal(err)
}
if password != confirmation {
log.Fatal("passwords do not match")
}
users := open(*data)
defer users.Close()
user, created, err := users.SetPassword(*username, password)
if err != nil {
log.Fatal(err)
}
if created {
fmt.Printf("created operator %s\n", user.Username)
} else {
fmt.Printf("updated password for %s\n", user.Username)
}
case "list":
flags := flag.NewFlagSet("list", flag.ExitOnError)
data := flags.String("data", "", "Orchestra data directory")
_ = flags.Parse(os.Args[2:])
users := open(*data)
defer users.Close()
list, err := users.Users()
if err != nil {
log.Fatal(err)
}
for _, user := range list {
fmt.Println(user.Username)
}
default:
usage()
os.Exit(2)
}
}
+38 -46
View File
@@ -11,6 +11,7 @@ import (
"net"
"net/http"
"orchestra/internal/admin"
"orchestra/internal/authn"
"orchestra/internal/authz"
"orchestra/internal/buildinfo"
"orchestra/internal/delivery"
@@ -150,6 +151,37 @@ func main() {
if err != nil {
log.Fatal(err)
}
users, err := authn.Open(authn.Path(dir))
if err != nil {
log.Fatal(err)
}
defer users.Close()
userCount, err := users.Count()
if err != nil {
log.Fatalf("read operator database: %v", err)
}
legacyUsername := os.Getenv("ORCHESTRA_WEB_USERNAME")
legacyPasswordHash := os.Getenv("ORCHESTRA_WEB_PASSWORD_HASH")
if userCount == 0 {
if legacyUsername != "" || legacyPasswordHash != "" {
if legacyUsername == "" || legacyPasswordHash == "" {
log.Fatal("operator database is empty and the legacy web credential is incomplete: both ORCHESTRA_WEB_USERNAME and ORCHESTRA_WEB_PASSWORD_HASH are required for one-time migration")
}
imported, importErr := users.ImportBcrypt(legacyUsername, legacyPasswordHash)
if importErr != nil {
log.Fatalf("migrate legacy web credential: %v", importErr)
}
if imported {
userCount = 1
log.Printf("migrated web operator %q into %s; remove ORCHESTRA_WEB_USERNAME and ORCHESTRA_WEB_PASSWORD_HASH from the deployment environment", legacyUsername, authn.Path(dir))
}
}
if userCount == 0 {
log.Fatalf("operator database is empty: stop Orchestra and run orchestra-user set -data %s -username NAME", dir)
}
} else if legacyUsername != "" || legacyPasswordHash != "" {
log.Printf("operator database already contains %d account(s); legacy ORCHESTRA_WEB_USERNAME and ORCHESTRA_WEB_PASSWORD_HASH are ignored and should be removed", userCount)
}
var rr registry.Registry
var rt *router.Router
var coordinator *orchestrator.Coordinator
@@ -275,53 +307,13 @@ func main() {
}
mux := http.NewServeMux()
// B18: the UI is a full control plane — it can create tasks, release or
// complete them, and inject approval keystrokes into live panes. It has
// one explicit operator identity and is never enabled by a missing env var.
webCredentials := authz.WebCredentials{Username: os.Getenv("ORCHESTRA_WEB_USERNAME"), PasswordHash: os.Getenv("ORCHESTRA_WEB_PASSWORD_HASH")}
if err := webCredentials.Validate(); err != nil {
log.Fatalf("web login configuration: %v", err)
}
// complete them, and inject approval keystrokes into live panes. Browser
// sessions are backed by operator accounts in the embedded auth database;
// no missing environment variable can open this surface.
sessions := &authz.Sessions{}
// A browser login exchanges verified credentials for an HttpOnly cookie.
mux.HandleFunc("/v1/ui/session", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodDelete {
// Expire the browser credential even if it is already absent or stale.
// The client never has access to the HttpOnly value, so this is the
// only reliable way for an operator to end a browser session.
if cookie, err := r.Cookie(authz.SessionCookie); err == nil {
sessions.Revoke(cookie.Value)
}
http.SetCookie(w, &http.Cookie{Name: authz.SessionCookie, Value: "", Path: "/", MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteStrictMode, Secure: os.Getenv("ORCHESTRA_UI_INSECURE_COOKIE") == ""})
w.WriteHeader(http.StatusNoContent)
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body struct {
Username string `json:"username"`
Password string `json:"password"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&body); err != nil || !webCredentials.Authenticate(body.Username, body.Password) {
http.Error(w, "invalid credentials", http.StatusUnauthorized)
return
}
v, err := sessions.Issue()
if err != nil {
http.Error(w, "session unavailable", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: authz.SessionCookie, Value: v, Path: "/",
HttpOnly: true, SameSite: http.SameSiteStrictMode,
Secure: os.Getenv("ORCHESTRA_UI_INSECURE_COOKIE") == "",
MaxAge: int((12 * time.Hour).Seconds()),
})
w.WriteHeader(http.StatusNoContent)
})
browserAuth := authn.HTTP{Users: users, Sessions: sessions, SecureCookie: os.Getenv("ORCHESTRA_UI_INSECURE_COOKIE") == ""}
mux.HandleFunc(authz.SessionPath, browserAuth.Session)
mux.HandleFunc("/v1/ui/account", browserAuth.Account)
// Browser-specific endpoints intentionally present a joined read model;
// raw lifecycle endpoints below remain stable for workers and harnesses.
mux.Handle("/v1/ui/", ui.Server{Store: s, Workers: workers, Coordinator: coordinator, Route: func(e domain.Event) error {