feat: enforce event authorization and readiness
This commit is contained in:
+34
-4
@@ -38,6 +38,13 @@ func main() {
|
|||||||
rt = &router.Router{Store: s, Registry: rr, Reachability: registry.TCPReachability{}, Timeout: time.Second, Retry: router.RetryPolicy{MaxAttempts: 3, Backoff: time.Minute}}
|
rt = &router.Router{Store: s, Registry: rr, Reachability: registry.TCPReachability{}, Timeout: time.Second, Retry: router.RetryPolicy{MaxAttempts: 3, Backoff: time.Minute}}
|
||||||
}
|
}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
surface := func(r *http.Request) authz.Surface {
|
||||||
|
v := authz.ParseSurface(r.Header.Get("X-Orchestra-Surface"))
|
||||||
|
if v == "" {
|
||||||
|
return authz.Web
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
mux.HandleFunc("/v1/tasks", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/v1/tasks", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == "GET" {
|
if r.Method == "GET" {
|
||||||
json.NewEncoder(w).Encode(s.Tasks())
|
json.NewEncoder(w).Encode(s.Tasks())
|
||||||
@@ -104,7 +111,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
taskID, action := parts[2], parts[3]
|
taskID, action := parts[2], parts[3]
|
||||||
if action == "approval" {
|
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 {
|
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" {
|
if parts[4] != "grant" && parts[4] != "deny" {
|
||||||
http.Error(w, "unknown approval action", 404)
|
http.Error(w, "unknown approval action", 404)
|
||||||
return
|
return
|
||||||
@@ -118,10 +137,6 @@ func main() {
|
|||||||
if by == "" {
|
if by == "" {
|
||||||
by = "surface"
|
by = "surface"
|
||||||
}
|
}
|
||||||
typ := "ApprovalGranted"
|
|
||||||
if parts[4] == "deny" {
|
|
||||||
typ = "ApprovalDenied"
|
|
||||||
}
|
|
||||||
b, _ := json.Marshal(map[string]any{"subject_ref": taskID, "by": by})
|
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}
|
e := domain.Event{ID: id(), Type: typ, TaskID: taskID, Version: t.Version + 1, Payload: b}
|
||||||
if err := s.Append(e); err != nil {
|
if err := s.Append(e); err != nil {
|
||||||
@@ -154,6 +169,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
var e domain.Event
|
var e domain.Event
|
||||||
var err error
|
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 {
|
switch action {
|
||||||
case "lease":
|
case "lease":
|
||||||
var p struct {
|
var p struct {
|
||||||
@@ -230,6 +252,14 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok\n")) })
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok\n")) })
|
||||||
|
mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
checks := map[string]any{"store": true, "router": rt != nil, "gitea": os.Getenv("ORCHESTRA_GITEA_URL") != "", "jsonl": os.Getenv("ORCHESTRA_JSONL") != ""}
|
||||||
|
ready := rt != nil || (os.Getenv("ORCHESTRA_CONFIG") == "")
|
||||||
|
if !ready {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{"ready": ready, "checks": checks})
|
||||||
|
})
|
||||||
if base := os.Getenv("ORCHESTRA_GITEA_URL"); base != "" {
|
if base := os.Getenv("ORCHESTRA_GITEA_URL"); base != "" {
|
||||||
g := provider.Gitea{BaseURL: base, Token: os.Getenv("ORCHESTRA_GITEA_TOKEN"), WebhookSecret: os.Getenv("ORCHESTRA_GITEA_WEBHOOK_SECRET"), Owner: os.Getenv("ORCHESTRA_GITEA_OWNER"), Repo: os.Getenv("ORCHESTRA_GITEA_REPO")}
|
g := provider.Gitea{BaseURL: base, Token: os.Getenv("ORCHESTRA_GITEA_TOKEN"), WebhookSecret: os.Getenv("ORCHESTRA_GITEA_WEBHOOK_SECRET"), Owner: os.Getenv("ORCHESTRA_GITEA_OWNER"), Repo: os.Getenv("ORCHESTRA_GITEA_REPO")}
|
||||||
mux.Handle("/v1/providers/gitea/webhook", g.WebhookHandler(s))
|
mux.Handle("/v1/providers/gitea/webhook", g.WebhookHandler(s))
|
||||||
|
|||||||
+3
-1
@@ -15,7 +15,7 @@ Updated: 2026-07-26
|
|||||||
- **Snapshots are written but never loaded or used for replay acceleration.** Startup always replays the complete event log.
|
- **Snapshots are written but never loaded or used for replay acceleration.** Startup always replays the complete event log.
|
||||||
- **Task creation projection is incomplete.** `parent`, `due`, `inherent_priority`, and `estimate` are defined in the domain model but are not projected from `TaskCreated` payloads.
|
- **Task creation projection is incomplete.** `parent`, `due`, `inherent_priority`, and `estimate` are defined in the domain model but are not projected from `TaskCreated` payloads.
|
||||||
- **Occupancy support is incomplete relative to the spec.** Native readers exist, but Codex active-session discovery, opencode server/SSE plus fallback, and coordinator monitoring are not implemented.
|
- **Occupancy support is incomplete relative to the spec.** Native readers exist, but Codex active-session discovery, opencode server/SSE plus fallback, and coordinator monitoring are not implemented.
|
||||||
- **Authorization is only partially enforced.** HTTP method restrictions exist, but handlers do not consistently call `AuthorizeEvent`; an absent surface defaults to full-control Web.
|
- **Authorization is mostly enforced at HTTP ingress.** Lifecycle and approval handlers now call `AuthorizeEvent`; an absent surface still defaults to full-control Web, and non-HTTP/event-bus integrations remain unwired.
|
||||||
|
|
||||||
The first pass closed the store/API defects (lifecycle defaults, CAS content verification, validated replay, snapshot loading, and projection of task metadata) and added optional Gitea webhook/poll wiring. The remaining server-side gaps are below.
|
The first pass closed the store/API defects (lifecycle defaults, CAS content verification, validated replay, snapshot loading, and projection of task metadata) and added optional Gitea webhook/poll wiring. The remaining server-side gaps are below.
|
||||||
|
|
||||||
@@ -31,6 +31,8 @@ The first pass closed the store/API defects (lifecycle defaults, CAS content ver
|
|||||||
- **Federated worker behavior is not complete.** Machine affinity filtering is implemented, but there is no worker registration/heartbeat protocol, remote event transport, cross-machine worktree coordination, or server-side synchronization status beyond local git inspection.
|
- **Federated worker behavior is not complete.** Machine affinity filtering is implemented, but there is no worker registration/heartbeat protocol, remote event transport, cross-machine worktree coordination, or server-side synchronization status beyond local git inspection.
|
||||||
- **The server API is narrower than the spec.** There are no explicit task amendment/report/handoff upload endpoints, event subscription/streaming endpoint, health/readiness detail for providers and herdrs, or administrative endpoints for project/machine/herdr status.
|
- **The server API is narrower than the spec.** There are no explicit task amendment/report/handoff upload endpoints, event subscription/streaming endpoint, health/readiness detail for providers and herdrs, or administrative endpoints for project/machine/herdr status.
|
||||||
|
|
||||||
|
The latest pass now applies `AuthorizeEvent` to lifecycle and approval writes and adds `/readyz` with router/provider configuration checks. Readiness is configuration-level only; it does not yet probe herdr/provider health.
|
||||||
|
|
||||||
Recommended order:
|
Recommended order:
|
||||||
|
|
||||||
1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.
|
1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.
|
||||||
|
|||||||
Reference in New Issue
Block a user