Files
rcourtman f356994869 Extract the transport-independent action lifecycle service
Planning, approval decisions, and execution for typed resource actions
move out of the HTTP handlers in internal/api/actions.go into a new
internal/actionlifecycle.Service owned by api-contracts. The REST
handlers become thin decode/actor/error-mapping adapters over the one
shared service, and ResourceHandlers.ActionLifecycle() exposes the same
service for in-process consumers, so a future Patrol action broker
inherits identical resource lookup, availability checks, plan hashing,
audit persistence, remediation locks, plan-drift revalidation,
execution, and terminal publication instead of loopback HTTP or a
parallel lifecycle.

Behavior is preserved: same status codes, error codes, and audit/
lifecycle persistence ordering, backed by the existing api contract
tests plus new fail-closed proofs for the service itself (unknown
resource/capability, availability refusal, unapproved execution,
remediation lock, plan drift, missing executor, missing store).

Contract text in api-contracts, agent-lifecycle, and storage-recovery
now names the service alongside actions.go and planner.go; the
subsystem registry owns internal/actionlifecycle/ under api-contracts
with a dedicated path policy; the code-standards and contract source
pins follow the moved invariants; and the subsystem_lookup line-number
pin shifts with the api-contracts canonical-files list insertion.

This is the first slice of making the typed action lifecycle the only
autonomous execution route for Patrol, Assistant, and MCP.
2026-07-10 11:06:55 +01:00

84 lines
2.4 KiB
Go

package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
func main() {
listen := flag.String("listen", "127.0.0.1:17655", "HTTP listen address")
agentBinary := flag.String("agent-binary", "", "Windows agent binary to serve")
version := flag.String("version", "", "Version returned by /api/version")
flag.Parse()
if strings.TrimSpace(*agentBinary) == "" || strings.TrimSpace(*version) == "" {
log.Fatal("--agent-binary and --version are required")
}
binaryPath, err := filepath.Abs(*agentBinary)
if err != nil {
log.Fatalf("resolve agent binary: %v", err)
}
binary, err := os.ReadFile(binaryPath)
if err != nil {
log.Fatalf("read agent binary: %v", err)
}
digest := sha256.Sum256(binary)
checksum := hex.EncodeToString(digest[:])
mux := http.NewServeMux()
mux.HandleFunc("/api/version", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"version": *version,
"agentUpdateTargetVersion": *version,
"channel": "stable",
})
})
mux.HandleFunc("/download/pulse-agent", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("arch") != "windows-amd64" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(binary)))
w.Header().Set("X-Checksum-Sha256", checksum)
w.Header().Set("Cache-Control", "no-store")
if r.Method == http.MethodHead {
w.WriteHeader(http.StatusOK)
return
}
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
_, _ = w.Write(binary)
})
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true}`))
})
server := &http.Server{
Addr: *listen,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 5 * time.Minute,
IdleTimeout: 30 * time.Second,
}
log.Printf("Windows lifecycle server listening on %s with %s (%s)", *listen, filepath.Base(binaryPath), *version)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}