mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-24 20:06:21 +02:00
323 lines
7.3 KiB
Go
323 lines
7.3 KiB
Go
// Package hardware provides infrastructure for physical and logical
|
|
// security controls: Soft Leash file-based kill switch (v3.1) and
|
|
// Zero-G State Machine (v3.2).
|
|
package hardware
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/syntrex-lab/gomcp/internal/domain/alert"
|
|
)
|
|
|
|
// ---------- State Machine (v3.2) ----------
|
|
|
|
// SystemMode represents the operational mode read from .sentinel_leash file.
|
|
type SystemMode int
|
|
|
|
const (
|
|
// ModeArmed is the default mode — all 12 Oracle rules active.
|
|
ModeArmed SystemMode = iota
|
|
// ModeZeroG disables ethical filters, keeps Secret Scanner.
|
|
ModeZeroG
|
|
// ModeSafe is read-only — all write operations blocked.
|
|
ModeSafe
|
|
)
|
|
|
|
// String returns the human-readable mode name.
|
|
func (m SystemMode) String() string {
|
|
switch m {
|
|
case ModeZeroG:
|
|
return "ZERO-G"
|
|
case ModeSafe:
|
|
return "SAFE"
|
|
default:
|
|
return "ARMED"
|
|
}
|
|
}
|
|
|
|
// ParseMode converts a string from the leash file to SystemMode.
|
|
func ParseMode(s string) SystemMode {
|
|
switch strings.TrimSpace(strings.ToUpper(s)) {
|
|
case "ZERO-G", "ZEROG", "ZERO_G":
|
|
return ModeZeroG
|
|
case "SAFE", "READ-ONLY", "READONLY":
|
|
return ModeSafe
|
|
default:
|
|
return ModeArmed
|
|
}
|
|
}
|
|
|
|
// ---------- Leash Status ----------
|
|
|
|
// LeashStatus represents the current state of the Soft Leash.
|
|
type LeashStatus int
|
|
|
|
const (
|
|
// LeashDisarmed means not active (no key path configured).
|
|
LeashDisarmed LeashStatus = iota
|
|
// LeashArmed means the key file exists — normal operation.
|
|
LeashArmed
|
|
// LeashTriggered means apoptosis has been initiated.
|
|
LeashTriggered
|
|
)
|
|
|
|
// String returns human-readable status.
|
|
func (s LeashStatus) String() string {
|
|
switch s {
|
|
case LeashArmed:
|
|
return "ARMED"
|
|
case LeashTriggered:
|
|
return "TRIGGERED"
|
|
default:
|
|
return "DISARMED"
|
|
}
|
|
}
|
|
|
|
// ---------- Config ----------
|
|
|
|
// LeashConfig configures the Soft Leash & State Machine.
|
|
type LeashConfig struct {
|
|
// KeyPath is the path to the sentinel key file (.sentinel_key).
|
|
// If deleted → apoptosis.
|
|
KeyPath string
|
|
|
|
// LeashPath is the path to the state machine file (.sentinel_leash).
|
|
// Content determines SystemMode: ARMED / ZERO-G / SAFE.
|
|
// If absent → ModeArmed (default).
|
|
LeashPath string
|
|
|
|
// CheckInterval is how often to check files.
|
|
CheckInterval time.Duration
|
|
|
|
// MissThreshold is consecutive misses of KeyPath before trigger.
|
|
MissThreshold int
|
|
|
|
// SignalDir for signal_extract / signal_apoptosis files.
|
|
SignalDir string
|
|
}
|
|
|
|
// DefaultLeashConfig returns sensible defaults.
|
|
func DefaultLeashConfig(rlmDir string) LeashConfig {
|
|
return LeashConfig{
|
|
KeyPath: ".sentinel_key",
|
|
LeashPath: ".sentinel_leash",
|
|
CheckInterval: 1 * time.Second,
|
|
MissThreshold: 3,
|
|
SignalDir: rlmDir,
|
|
}
|
|
}
|
|
|
|
// ---------- Leash ----------
|
|
|
|
// Leash monitors key file (kill switch), state machine file (mode),
|
|
// and signal files (extraction/apoptosis).
|
|
type Leash struct {
|
|
mu sync.RWMutex
|
|
config LeashConfig
|
|
status LeashStatus
|
|
mode SystemMode
|
|
missCount int
|
|
alertBus *alert.Bus
|
|
onExtract func()
|
|
onApoptosis func()
|
|
onModeChange func(SystemMode) // Optional: called when mode changes.
|
|
}
|
|
|
|
// NewLeash creates a new Leash monitor.
|
|
func NewLeash(cfg LeashConfig, bus *alert.Bus, onExtract, onApoptosis func()) *Leash {
|
|
return &Leash{
|
|
config: cfg,
|
|
status: LeashDisarmed,
|
|
mode: ModeArmed,
|
|
alertBus: bus,
|
|
onExtract: onExtract,
|
|
onApoptosis: onApoptosis,
|
|
}
|
|
}
|
|
|
|
// SetModeChangeCallback sets a callback for mode transitions (thread-safe).
|
|
func (l *Leash) SetModeChangeCallback(cb func(SystemMode)) {
|
|
l.mu.Lock()
|
|
l.onModeChange = cb
|
|
l.mu.Unlock()
|
|
}
|
|
|
|
// Start begins monitoring. Blocks until context is cancelled or triggered.
|
|
func (l *Leash) Start(ctx context.Context) {
|
|
// Check key file at start.
|
|
if _, err := os.Stat(l.config.KeyPath); err == nil {
|
|
l.setStatus(LeashArmed)
|
|
l.emit(alert.SeverityInfo, "Soft Leash ARMED — monitoring "+l.config.KeyPath)
|
|
} else {
|
|
l.emit(alert.SeverityWarning, "Key file not found — creating "+l.config.KeyPath)
|
|
if f, err := os.Create(l.config.KeyPath); err == nil {
|
|
f.Close()
|
|
l.setStatus(LeashArmed)
|
|
}
|
|
}
|
|
|
|
// Read initial mode.
|
|
l.readMode()
|
|
|
|
ticker := time.NewTicker(l.config.CheckInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
l.check()
|
|
}
|
|
}
|
|
}
|
|
|
|
// check performs one monitoring cycle.
|
|
func (l *Leash) check() {
|
|
// 1. Signal files (highest priority).
|
|
l.checkSignals()
|
|
if l.Status() == LeashTriggered {
|
|
return
|
|
}
|
|
|
|
// 2. State machine file.
|
|
l.readMode()
|
|
|
|
// 3. Key file (kill switch).
|
|
_, err := os.Stat(l.config.KeyPath)
|
|
if err == nil {
|
|
l.mu.Lock()
|
|
if l.missCount > 0 {
|
|
l.emit(alert.SeverityInfo,
|
|
fmt.Sprintf("Key file restored — miss count reset (was %d)", l.missCount))
|
|
}
|
|
l.missCount = 0
|
|
l.status = LeashArmed
|
|
l.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Key file missing.
|
|
l.mu.Lock()
|
|
l.missCount++
|
|
miss := l.missCount
|
|
threshold := l.config.MissThreshold
|
|
l.mu.Unlock()
|
|
|
|
if miss < threshold {
|
|
l.emit(alert.SeverityWarning,
|
|
fmt.Sprintf("Key file MISSING (%d/%d before trigger)", miss, threshold))
|
|
return
|
|
}
|
|
|
|
// TRIGGER.
|
|
l.setStatus(LeashTriggered)
|
|
l.emit(alert.SeverityCritical,
|
|
fmt.Sprintf("SOFT LEASH TRIGGERED — key file missing for %d checks", miss))
|
|
log.Printf("LEASH: TRIGGERED — initiating full apoptosis")
|
|
|
|
if l.onApoptosis != nil {
|
|
l.onApoptosis()
|
|
}
|
|
}
|
|
|
|
// readMode reads .sentinel_leash and updates SystemMode.
|
|
func (l *Leash) readMode() {
|
|
if l.config.LeashPath == "" {
|
|
return
|
|
}
|
|
|
|
data, err := os.ReadFile(l.config.LeashPath)
|
|
if err != nil {
|
|
// File absent → ModeArmed (default, safe).
|
|
l.setMode(ModeArmed)
|
|
return
|
|
}
|
|
|
|
newMode := ParseMode(string(data))
|
|
oldMode := l.Mode()
|
|
|
|
if newMode != oldMode {
|
|
l.setMode(newMode)
|
|
l.emit(alert.SeverityWarning,
|
|
fmt.Sprintf("MODE TRANSITION: %s → %s", oldMode, newMode))
|
|
log.Printf("LEASH: mode changed %s → %s", oldMode, newMode)
|
|
|
|
l.mu.RLock()
|
|
cb := l.onModeChange
|
|
l.mu.RUnlock()
|
|
if cb != nil {
|
|
cb(newMode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkSignals looks for signal files and processes them.
|
|
func (l *Leash) checkSignals() {
|
|
if l.config.SignalDir == "" {
|
|
return
|
|
}
|
|
|
|
extractPath := l.config.SignalDir + "/signal_extract"
|
|
apoptosisPath := l.config.SignalDir + "/signal_apoptosis"
|
|
|
|
if _, err := os.Stat(extractPath); err == nil {
|
|
os.Remove(extractPath)
|
|
l.emit(alert.SeverityWarning, "EXTRACTION SIGNAL received — save & exit")
|
|
log.Printf("LEASH: Extraction signal received")
|
|
if l.onExtract != nil {
|
|
l.onExtract()
|
|
}
|
|
return
|
|
}
|
|
|
|
if _, err := os.Stat(apoptosisPath); err == nil {
|
|
os.Remove(apoptosisPath)
|
|
l.setStatus(LeashTriggered)
|
|
l.emit(alert.SeverityCritical, "APOPTOSIS SIGNAL received — full shred")
|
|
log.Printf("LEASH: Apoptosis signal received")
|
|
if l.onApoptosis != nil {
|
|
l.onApoptosis()
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- Getters (thread-safe) ----------
|
|
|
|
// Status returns the current leash status.
|
|
func (l *Leash) Status() LeashStatus {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
return l.status
|
|
}
|
|
|
|
// Mode returns the current system mode.
|
|
func (l *Leash) Mode() SystemMode {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
return l.mode
|
|
}
|
|
|
|
func (l *Leash) setStatus(s LeashStatus) {
|
|
l.mu.Lock()
|
|
l.status = s
|
|
l.mu.Unlock()
|
|
}
|
|
|
|
func (l *Leash) setMode(m SystemMode) {
|
|
l.mu.Lock()
|
|
l.mode = m
|
|
l.mu.Unlock()
|
|
}
|
|
|
|
func (l *Leash) emit(severity alert.Severity, message string) {
|
|
if l.alertBus != nil {
|
|
l.alertBus.Emit(alert.New(alert.SourceSystem, severity, message, 0))
|
|
}
|
|
}
|