Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified 14_experiments/telemetry_replay/telemetry_replay
Binary file not shown.
6 changes: 6 additions & 0 deletions PHOENIX_PROBLEMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,9 @@ This document tracks critical gaps between PhoenixOS theory and implementation,
**Problem:** Proving "Why" an autonomous action was taken.
**Expert Assessment:** **Content-Addressable Evidence Ledger**.
**Implementation:** Phoenix Ledger (P0) implemented with SHA-256 hash chaining of `(trace_hash, sdi, policy, action, result, time, confidence, replay, experiment)` tuples.

## 8. Discovered Infrastructure Issues
**Status:** **[NEW]**
- **Issue:** `build_phoenix.sh` lacks execution permissions.
- **Issue:** Test suite fails collection (`ModuleNotFoundError`) due to environment configuration / `PYTHONPATH` issues.
- **Action Required:** Fix script permissions in repository and resolve module resolution path for the test suite.
2 changes: 1 addition & 1 deletion PHOENIX_TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@

## Validation Metrics
- **Build Status:** PASSED (All 9 services).
- **Test Status:** PASSED (Arbiter, Bus, Guard, Ledger, Monitor, Nexus, Sentinel, Trace, Warden).
- **Test Status:** FAILED (Collection Errors: ModuleNotFoundError).
- **Latency:** Guard < 50us (Verified).
- **Storage:** 3-Tier Trace Eviction (Verified).
Empty file modified build_phoenix.sh
100644 → 100755
Empty file.
8 changes: 8 additions & 0 deletions config/warden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"thresholds": {
"safe": 0.3,
"watch": 0.5,
"suspicious": 0.7,
"critical": 0.9
}
}
73 changes: 73 additions & 0 deletions phoenix_os/agents/internal/game/marl/stability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package marl

import (
"sync"
"time"
)

// StabilityController enforces action limits and cooling periods for MARL agents.
type StabilityController struct {
mu sync.RWMutex
actionDebt float64
lastAction time.Time
cooldown time.Duration
maxContainment float64
decayRate float64 // Debt reduction per second
lastDecay time.Time
}

// NewStabilityController initializes a controller with defined limits and decay rate.
func NewStabilityController(cooldown time.Duration, maxContainment float64, decayRate float64) *StabilityController {
now := time.Now()
return &StabilityController{
cooldown: cooldown,
maxContainment: maxContainment,
decayRate: decayRate,
lastDecay: now,
}
}

// applyDecay reduces the action debt based on elapsed time. Must be called with lock held.
func (sc *StabilityController) applyDecay(now time.Time) {
elapsed := now.Sub(sc.lastDecay).Seconds()
if elapsed > 0 {
reduction := elapsed * sc.decayRate
sc.actionDebt -= reduction
if sc.actionDebt < 0 {
sc.actionDebt = 0
}
sc.lastDecay = now
}
}

// TryRecordAction atomically checks if an action is allowed and records it if so.
// It also applies time-based decay to the action debt.
func (sc *StabilityController) TryRecordAction(cost float64, now time.Time) bool {
sc.mu.Lock()
defer sc.mu.Unlock()

sc.applyDecay(now)

// Check Cooldown
if now.Sub(sc.lastAction) < sc.cooldown {
return false
}

// Check Containment Rate (Action Debt)
if sc.actionDebt+cost > sc.maxContainment {
return false
}

sc.actionDebt += cost
sc.lastAction = now
return true
}

// GetActionDebt returns the current action debt (mainly for testing/monitoring).
func (sc *StabilityController) GetActionDebt(now time.Time) float64 {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.applyDecay(now)
return sc.actionDebt
}

60 changes: 60 additions & 0 deletions phoenix_os/agents/internal/game/marl/stability_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package marl

import (
"testing"
"time"
)

func TestStability(t *testing.T) {
// 100ms cooldown, 1.0 max containment, decay 0.0 per sec (for testing strict limits)
sc := NewStabilityController(100*time.Millisecond, 1.0, 0.0)

start := time.Now()

// First action should pass
if !sc.TryRecordAction(0.5, start) {
t.Error("Expected action to be allowed")
}

// Immediate second action should fail due to cooldown
if sc.TryRecordAction(0.1, start.Add(10*time.Millisecond)) {
t.Error("Expected action to be throttled due to cooldown")
}

// Advance time past cooldown
afterCooldown := start.Add(150 * time.Millisecond)

// Should pass if under containment limit
if !sc.TryRecordAction(0.4, afterCooldown) {
t.Error("Expected action to be allowed after cooldown")
}

// Should fail if exceeding limit
if sc.TryRecordAction(0.2, afterCooldown.Add(150*time.Millisecond)) {
t.Error("Expected action to be throttled due to containment limit")
}
}

func TestStabilityDecay(t *testing.T) {
// Decay 1.0 per second
sc := NewStabilityController(100*time.Millisecond, 1.0, 1.0)

start := time.Now()

// Max out containment
sc.TryRecordAction(1.0, start)

// Should fail
if sc.TryRecordAction(0.1, start.Add(150*time.Millisecond)) {
t.Error("Expected action to be throttled due to containment limit")
}

// Advance time by 0.5s, debt should decay by 0.5
afterDecay := start.Add(500 * time.Millisecond)

// Should pass now that debt has decayed
if !sc.TryRecordAction(0.4, afterDecay) {
t.Error("Expected action to be allowed after debt decay")
}
}

31 changes: 31 additions & 0 deletions phoenix_os/agents/internal/swarm/governance/governance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package governance

import (
"sync"
)

// Policy defines the rules for swarm node participation.
type Policy struct {
MinReputation float64
QuorumSize int
}

// SwarmGovernor enforces governance policies on the node network.
type SwarmGovernor struct {
mu sync.RWMutex
Policy Policy
}

// NewSwarmGovernor initializes the governor with a policy.
func NewSwarmGovernor(policy Policy) *SwarmGovernor {
return &SwarmGovernor{
Policy: policy,
}
}

// ValidateProposal checks if a node is authorized to participate in consensus.
func (sg *SwarmGovernor) ValidateProposal(nodeReputation float64) bool {
sg.mu.RLock()
defer sg.mu.RUnlock()
return nodeReputation >= sg.Policy.MinReputation
Comment on lines +27 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce quorum in proposal validation

ValidateProposal only checks MinReputation and never uses Policy.QuorumSize, so proposals can be accepted even when the consensus set is below the configured quorum. In this commit, QuorumSize is defined and set in tests but never read, which means the advertised Byzantine/quorum protection is not actually enforced.

Useful? React with 👍 / 👎.

}
16 changes: 16 additions & 0 deletions phoenix_os/agents/internal/swarm/governance/governance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package governance

import "testing"

func TestGovernance(t *testing.T) {
policy := Policy{MinReputation: 0.7, QuorumSize: 3}
gov := NewSwarmGovernor(policy)

if gov.ValidateProposal(0.8) == false {
t.Error("Proposal should be accepted for compliant reputation")
}

if gov.ValidateProposal(0.5) == true {
t.Error("Proposal should be rejected for low reputation")
}
}
Binary file modified phoenix_os/bus/artifacts/phoenix_bus
Binary file not shown.
Binary file modified phoenix_os/ledger/artifacts/phoenix_ledger
100755 → 100644
Binary file not shown.
Binary file modified phoenix_os/monitor/artifacts/phoenix_monitor
Binary file not shown.
10 changes: 10 additions & 0 deletions phoenix_os/telemetry/serialization/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module phoenix/telemetry/serialization

go 1.25.0

require github.com/segmentio/encoding v0.5.4

require (
github.com/segmentio/asm v1.1.3 // indirect
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 // indirect
)
6 changes: 6 additions & 0 deletions phoenix_os/telemetry/serialization/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 h1:WecRHqgE09JBkh/584XIE6PMz5KKE/vER4izNUi30AQ=
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
11 changes: 11 additions & 0 deletions phoenix_os/telemetry/serialization/optimizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package serialization

import (
"encoding/json"
"github.com/segmentio/encoding/json" // Utilizing high-performance JSON encoder
)

// OptimizedMarshaler replaces standard library JSON encoding with high-perf alternatives.
func OptimizedMarshaler(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
21 changes: 21 additions & 0 deletions phoenix_os/telemetry/serialization/optimizer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package serialization

import (
"testing"
)

type TestStruct struct {
ID int `json:"id"`
Value string `json:"value"`
}

func TestOptimizer(t *testing.T) {
data := TestStruct{ID: 1, Value: "test"}
encoded, err := OptimizedMarshaler(data)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
if string(encoded) == "" {
Comment on lines +12 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Strengthen assertions by validating the actual JSON output and error paths

Asserting only that the output is non-empty is too weak. Please:

  • Assert the exact JSON output for this struct (or decode into a map if field order is unstable) to verify tags and field mapping.
  • Add a negative test using an unsupported type and assert that OptimizedMarshaler returns a non-nil error.

This will better verify correctness and error handling of the marshaler.

Suggested implementation:

import (
	"encoding/json"
	"testing"
)
func TestOptimizer(t *testing.T) {
	data := TestStruct{ID: 1, Value: "test"}
	encoded, err := OptimizedMarshaler(data)
	if err != nil {
		t.Fatalf("Failed to marshal: %v", err)
	}

	if len(encoded) == 0 {
		t.Fatal("Marshaled data is empty")
	}

	var decoded map[string]interface{}
	if err := json.Unmarshal(encoded, &decoded); err != nil {
		t.Fatalf("Failed to unmarshal encoded JSON: %v", err)
	}

	id, ok := decoded["id"].(float64)
	if !ok || id != 1 {
		t.Fatalf("Unexpected id field: %#v", decoded["id"])
	}

	value, ok := decoded["value"].(string)
	if !ok || value != "test" {
		t.Fatalf("Unexpected value field: %#v", decoded["value"])
	}
}

func TestOptimizerUnsupportedType(t *testing.T) {
	type Unsupported struct {
		Ch chan int `json:"ch"`
	}

	data := Unsupported{Ch: make(chan int)}
	encoded, err := OptimizedMarshaler(data)
	if err == nil {
		t.Fatalf("Expected error for unsupported type, got nil (encoded=%q)", string(encoded))
	}
}

t.Error("Marshaled data is empty")
}
}
41 changes: 34 additions & 7 deletions phoenix_os/warden/src/warden.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"encoding/json"
"fmt"
"os"
)

type SystemState uint8
Expand All @@ -14,30 +16,51 @@ const (
StateCompromised SystemState = 4
)

type Config struct {
Thresholds struct {
Safe float64 `json:"safe"`
Watch float64 `json:"watch"`
Suspicious float64 `json:"suspicious"`
Critical float64 `json:"critical"`
} `json:"thresholds"`
}

type Warden struct {
CurrentState SystemState
Throttling float64 // 0.0 (None) to 1.0 (Full Block)
Config Config
}

func NewWarden() *Warden {
func NewWarden(configPath string) (*Warden, error) {
configFile, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}

var config Config
if err := json.Unmarshal(configFile, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}

return &Warden{
CurrentState: StateSafe,
Throttling: 0.0,
}
Config: config,
}, nil
}
Comment on lines +34 to 50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Validate the loaded thresholds to ensure they are sane and ordered.

Given that EvaluateSDI relies on strictly increasing thresholds (Safe < Watch < Suspicious < Critical) and normalized values, a misconfigured JSON (e.g., non-monotonic, equal, zero, or >1 thresholds) could cause incorrect or collapsed state transitions. Please add post-unmarshal validation (e.g., monotonicity check and ensuring all thresholds are in [0,1]) and return an error if the config is invalid.

Suggested change
func NewWarden(configPath string) (*Warden, error) {
configFile, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config Config
if err := json.Unmarshal(configFile, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
return &Warden{
CurrentState: StateSafe,
Throttling: 0.0,
}
Config: config,
}, nil
}
func NewWarden(configPath string) (*Warden, error) {
configFile, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config Config
if err := json.Unmarshal(configFile, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
if err := validateConfig(&config); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return &Warden{
CurrentState: StateSafe,
Throttling: 0.0,
Config: config,
}, nil
}
func validateConfig(config *Config) error {
t := config.Thresholds
// Ensure each threshold is normalized and non-zero: (0, 1]
if t.Safe <= 0 || t.Safe > 1 {
return fmt.Errorf("threshold 'safe' must be in (0, 1], got %f", t.Safe)
}
if t.Watch <= 0 || t.Watch > 1 {
return fmt.Errorf("threshold 'watch' must be in (0, 1], got %f", t.Watch)
}
if t.Suspicious <= 0 || t.Suspicious > 1 {
return fmt.Errorf("threshold 'suspicious' must be in (0, 1], got %f", t.Suspicious)
}
if t.Critical <= 0 || t.Critical > 1 {
return fmt.Errorf("threshold 'critical' must be in (0, 1], got %f", t.Critical)
}
// Ensure strictly increasing thresholds: Safe < Watch < Suspicious < Critical
if !(t.Safe < t.Watch && t.Watch < t.Suspicious && t.Suspicious < t.Critical) {
return fmt.Errorf(
"thresholds must be strictly increasing (safe < watch < suspicious < critical), got safe=%f, watch=%f, suspicious=%f, critical=%f",
t.Safe, t.Watch, t.Suspicious, t.Critical,
)
}
return nil
}


// EvaluateSDI maps the System Disorder Index to a discrete state
func (w *Warden) EvaluateSDI(sdi float64) {
oldState := w.CurrentState

switch {
case sdi < 0.3:
case sdi < w.Config.Thresholds.Safe:
w.CurrentState = StateSafe
case sdi < 0.5:
case sdi < w.Config.Thresholds.Watch:
w.CurrentState = StateWatch
case sdi < 0.7:
case sdi < w.Config.Thresholds.Suspicious:
w.CurrentState = StateSuspicious
case sdi < 0.9:
case sdi < w.Config.Thresholds.Critical:
w.CurrentState = StateCritical
default:
w.CurrentState = StateCompromised
Expand Down Expand Up @@ -71,7 +94,11 @@ func (w *Warden) ApplyAction() {

func main() {
fmt.Println("Phoenix Warden starting with Finite-State Controller...")
warden := NewWarden()
warden, err := NewWarden("../../config/warden.json")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider making the config path configurable rather than hardcoded relative paths.

Hardcoding "../../config/warden.json" couples startup to a specific working directory and layout, which is likely to break under different launch setups (systemd, containers, tests). Prefer a configurable source (e.g., flag, env var, or embedded default with override) so startup doesn’t depend on cwd.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a resolvable default config path

The hard-coded "../../config/warden.json" is resolved from the process working directory, not from the source file location, so normal launches (for example from repo root or as a service) will often fail to find the config and exit at startup. The committed config lives at config/warden.json, so this default path is brittle and frequently incorrect.

Useful? React with 👍 / 👎.

if err != nil {
fmt.Printf("Error initializing Warden: %v\n", err)
os.Exit(1)
}

// Simulate rising threat
warden.EvaluateSDI(0.15) // Safe
Expand Down
28 changes: 25 additions & 3 deletions phoenix_os/warden/src/warden_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
package main

import "testing"
import (
"os"
"testing"
)

func TestWardenFSM(t *testing.T) {
w := NewWarden()

// Create a temporary config file for the test
configContent := `{
"thresholds": {
"safe": 0.3,
"watch": 0.5,
"suspicious": 0.7,
"critical": 0.9
}
}`
configFile := "test_warden.json"
err := os.WriteFile(configFile, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to create temp config file: %v", err)
}
defer os.Remove(configFile)
Comment on lines +18 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Use t.TempDir and unique paths instead of a fixed filename in the working directory

A fixed filename in the working directory can cause tests to race with each other, break under concurrent runs, or fail on read-only directories. Use t.TempDir() and build the path with filepath.Join so each test gets its own temp file and automatic cleanup, instead of relying on os.Remove.

Suggested implementation:

	// Create a config file in a temporary directory for the test
	configContent := `{
		"thresholds": {
			"safe": 0.3,
			"watch": 0.5,
			"suspicious": 0.7,
			"critical": 0.9
		}
	}`

	dir := t.TempDir()
	configFile := filepath.Join(dir, "warden_config.json")

	err := os.WriteFile(configFile, []byte(configContent), 0o644)
	if err != nil {
		t.Fatalf("Failed to create temp config file: %v", err)
	}

	w, err := NewWarden(configFile)
  1. Ensure filepath is imported at the top of warden_test.go:
    • Add path/filepath to the import block, e.g. import "path/filepath", or to the existing grouped imports if present.
  2. If other tests in this file also create temporary config files with fixed names in the working directory, consider updating them similarly to use t.TempDir() and filepath.Join for consistency and to avoid race conditions.


w, err := NewWarden(configFile)
if err != nil {
t.Fatalf("Failed to create Warden with config: %v", err)
}

// Initial State
if w.CurrentState != StateSafe {
t.Errorf("Expected initial state Safe, got %v", w.CurrentState)
Expand Down
Binary file modified tests/__pycache__/test_orchestrator.cpython-313-pytest-9.0.3.pyc
Binary file not shown.
Binary file modified tests/__pycache__/test_service.cpython-313-pytest-9.0.3.pyc
Binary file not shown.
Loading