Add L0-L4 security clearance for fleet commands and AI elevation.

Gate manual and AI commands by per-agent clearance, auto-elevate stuck hosts to L4 when AI mode allows, and surface clearance in Access Depth and LOTL timeline.
This commit is contained in:
AetherForge
2026-06-07 02:30:00 -07:00
parent dd612251d1
commit f89ba94cb7
21 changed files with 952 additions and 46 deletions

View File

@@ -0,0 +1,46 @@
package clearance
import (
"testing"
)
func TestL2AgentCannotExecShellWithoutElevation(t *testing.T) {
const agentClearance = L2
cases := []struct {
cmdType string
args map[string]interface{}
}{
{"exec_shell", nil},
{"agent_command", map[string]interface{}{"action": "exec"}},
{"agent_command", map[string]interface{}{"action": "powershell"}},
}
for _, tc := range cases {
if err := EnforceClearance(tc.cmdType, tc.args, agentClearance); err == nil {
t.Fatalf("expected clearance error for %s %+v at L2", tc.cmdType, tc.args)
}
}
if err := EnforceClearance("discover_and_join", nil, agentClearance); err != nil {
t.Fatalf("L2 should allow spread: %v", err)
}
if err := EnforceAction("exec", agentClearance); err == nil {
t.Fatal("L2 should block exec action")
}
if err := EnforceAction("pause", agentClearance); err != nil {
t.Fatalf("L2 should allow pause: %v", err)
}
}
func TestCommandRequiredLevels(t *testing.T) {
if CommandRequiredLevel("restart_mining", nil) != L1 {
t.Fatal("restart_mining should be L1")
}
if CommandRequiredLevel("spread_now", nil) != L2 {
t.Fatal("spread_now should be L2")
}
if CommandRequiredLevel("set_agent_version", nil) != L4 {
t.Fatal("set_agent_version should be L4")
}
}

View File

@@ -0,0 +1,12 @@
package clearance
import "crypto-miner-server/internal/models"
// DefaultClearance returns the baseline clearance for an agent.
// Online agents start at L1 (mining commands); offline agents are L0 (read-only).
func DefaultClearance(agent *models.Agent) int {
if agent != nil && agent.Status == "online" {
return L1
}
return L0
}

View File

@@ -0,0 +1,42 @@
package clearance
import (
"fmt"
"strings"
)
// EventStore persists clearance changes and audit rows.
type EventStore interface {
InsertClearanceEvent(agentID string, fromLevel, toLevel int, reason, source string) error
}
// RequestElevation raises an agent to toLevel when higher than current, logs the event, and returns the new level.
func RequestElevation(store EventStore, agentID string, currentLevel, toLevel int, reason, source string) (int, error) {
agentID = strings.TrimSpace(agentID)
if agentID == "" {
return currentLevel, fmt.Errorf("agent id required")
}
if toLevel < L0 {
toLevel = L0
}
if toLevel > L4 {
toLevel = L4
}
if toLevel <= currentLevel {
return currentLevel, nil
}
reason = strings.TrimSpace(reason)
if reason == "" {
reason = "elevation requested"
}
source = strings.TrimSpace(source)
if source == "" {
source = "system"
}
if store != nil {
if err := store.InsertClearanceEvent(agentID, currentLevel, toLevel, reason, source); err != nil {
return currentLevel, err
}
}
return toLevel, nil
}

View File

@@ -0,0 +1,114 @@
package clearance
import (
"fmt"
"strings"
)
// Security clearance levels (L0L4).
const (
L0 = 0 // stats / read-only
L1 = 1 // mining commands
L2 = 2 // spread
L3 = 3 // shell
L4 = 4 // forge / version
)
// LevelLabel returns the badge string for a clearance level.
func LevelLabel(level int) string {
if level < L0 {
level = L0
}
if level > L4 {
level = L4
}
return fmt.Sprintf("L%d", level)
}
// LevelPermissions describes what each level allows (UI tooltips).
func LevelPermissions(level int) string {
switch level {
case L0:
return "stats and read-only probes"
case L1:
return "mining: pause, resume, restart"
case L2:
return "spread: discover_and_join, spread_now, stage_fetch"
case L3:
return "shell: exec_shell, agent_command"
case L4:
return "forge: set_agent_version, reorder_tiers fleet-wide"
default:
return "unknown clearance"
}
}
// ActionRequiredLevel maps a remote agent action to the minimum clearance level.
func ActionRequiredLevel(action string) int {
switch normalizeKey(action) {
case "pause", "resume", "restart", "restart_mining", "start_mining", "stop":
return L1
case "discover_and_join", "spread_now", "stage_fetch":
return L2
case "exec", "exec_shell", "powershell", "agent_command":
return L3
case "fetch_module", "set_agent_version", "reorder_tiers", "adaptive_strategy_update":
return L4
default:
return L0
}
}
// CommandRequiredLevel maps a fleet AI command type to the minimum clearance level.
func CommandRequiredLevel(cmdType string, args map[string]interface{}) int {
typ := normalizeKey(cmdType)
switch typ {
case "noop", "":
return L0
case "restart_mining":
return L1
case "discover_and_join", "spread_now", "stage_fetch":
return L2
case "agent_command":
if args != nil {
if action, ok := args["action"].(string); ok && action != "" {
return ActionRequiredLevel(action)
}
}
return L3
case "bulk_command":
if args != nil {
if action, ok := args["action"].(string); ok && action != "" {
return ActionRequiredLevel(action)
}
}
return L1
case "set_agent_version", "reorder_tiers":
return L4
default:
return ActionRequiredLevel(typ)
}
}
// EnforceClearance returns an error when agentClearance is below the command requirement.
func EnforceClearance(cmdType string, args map[string]interface{}, agentClearance int) error {
required := CommandRequiredLevel(cmdType, args)
if agentClearance >= required {
return nil
}
return fmt.Errorf("clearance %s insufficient for %s (requires %s)", LevelLabel(agentClearance), cmdType, LevelLabel(required))
}
// EnforceAction is the manual API path for raw agent actions.
func EnforceAction(action string, agentClearance int) error {
required := ActionRequiredLevel(action)
if agentClearance >= required {
return nil
}
return fmt.Errorf("clearance %s insufficient for %s (requires %s)", LevelLabel(agentClearance), action, LevelLabel(required))
}
func normalizeKey(s string) string {
s = strings.TrimSpace(strings.ToLower(s))
return strings.ReplaceAll(s, "-", "_")
}