slice 10B: signed-op job completion (DELETE clear-job) (hub v0.10.0)
Add DELETE /hosts/{id}/jobs/{job_id} (per-host self-scoped, idempotent) so the
agent clears a job after executing or terminally rejecting it. The hub stores
the operator-signed blobs opaquely (no signing key — cannot forge or open);
the agent verifies + executes. Doc 03 §4/§6/§9 updated (operator-signed path
live; 8C wipe completes; 10B done).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case r.Method == http.MethodGet && strings.HasPrefix(path, "/hosts/") && strings.HasSuffix(path, "/jobs"):
|
||||
hostID := strings.TrimSuffix(strings.TrimPrefix(path, "/hosts/"), "/jobs")
|
||||
h.handleGetJobs(w, r, hostID)
|
||||
// Job completion (slice 10B) — per-host-key, self-scoped: DELETE /hosts/{id}/jobs/{job_id}.
|
||||
case r.Method == http.MethodDelete && strings.HasPrefix(path, "/hosts/") && strings.Contains(path, "/jobs/"):
|
||||
rest := strings.TrimPrefix(path, "/hosts/")
|
||||
if i := strings.Index(rest, "/jobs/"); i > 0 {
|
||||
h.handleDeleteJob(w, r, rest[:i], rest[i+len("/jobs/"):])
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
// Admin-set (slice 10A) — global/operator key only; bumps the generation.
|
||||
case r.Method == http.MethodPut && strings.HasPrefix(path, "/admin/hosts/") && strings.HasSuffix(path, "/desired-state"):
|
||||
hostID := strings.TrimSuffix(strings.TrimPrefix(path, "/admin/hosts/"), "/desired-state")
|
||||
@@ -739,6 +747,33 @@ func (h *Handler) handleGetJobs(w http.ResponseWriter, r *http.Request, pathHost
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"jobs": out})
|
||||
}
|
||||
|
||||
// handleDeleteJob clears a processed job from a host's queue (slice 10B). Per-host key,
|
||||
// SELF-SCOPED (a host clears only its own jobs; the global key may clear any). Idempotent.
|
||||
func (h *Handler) handleDeleteJob(w http.ResponseWriter, r *http.Request, pathHostID, jobID string) {
|
||||
authHostID, _, isGlobal, ok := h.checkAuthHost(r)
|
||||
if !ok {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if pathHostID == "" || jobID == "" {
|
||||
http.Error(w, "Missing host_id or job_id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !isGlobal && authHostID != pathHostID {
|
||||
http.Error(w, "Forbidden: host_id mismatch", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := h.store.DeleteSignedJob(pathHostID, jobID); err != nil {
|
||||
h.logger.Printf("[ERROR] delete job %s for %s: %v", jobID, pathHostID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.logger.Printf("[INFO] host %s cleared signed-op job %s (executed or rejected)", pathHostID, jobID)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
}
|
||||
|
||||
// handleAdminSetDesiredState sets a host's desired-state (slice 10A). GLOBAL/operator key ONLY —
|
||||
// a per-host key cannot author its own intent. The body is the desired-state JSON (opaque to the
|
||||
// hub: it stores + serves bytes, never validates/interprets the schema — the agent/CLI owns it).
|
||||
|
||||
Reference in New Issue
Block a user