Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Operators can remove machines from Crucible and dashboard rosters with honest messaging that deletion is registry-only; bulk select, oath ledger entries, and Go/Vitest coverage included.
176 lines
4.6 KiB
Go
176 lines
4.6 KiB
Go
package db
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
// Oath action types — immutable accountability rows.
|
|
const (
|
|
OathSpreadTierEscalation = "spread_tier_escalation"
|
|
OathGraft = "graft"
|
|
OathForkMerge = "fork_merge"
|
|
OathStrainCardPlay = "strain_card_play"
|
|
OathCourtL4Decision = "court_l4_decision"
|
|
OathSpreadAttempt = "spread_attempt"
|
|
OathSpreadDiscoveredHost = "spread_discovered_host"
|
|
OathStrainHospice = "strain_hospice"
|
|
OathReconScan = "recon_scan"
|
|
OathAgentRemoved = "agent_removed"
|
|
)
|
|
|
|
// Oath outcomes.
|
|
const (
|
|
OathOutcomeSuccess = "success"
|
|
OathOutcomeFail = "fail"
|
|
OathOutcomePending = "pending"
|
|
)
|
|
|
|
// OathLedgerEntry is one immutable operator / AI council accountability row.
|
|
type OathLedgerEntry struct {
|
|
ID int64 `json:"id"`
|
|
Timestamp string `json:"timestamp"`
|
|
Actor string `json:"actor"`
|
|
ActionType string `json:"action_type"`
|
|
AgentID string `json:"agent_id"`
|
|
Strain string `json:"strain"`
|
|
WhyHash string `json:"why_hash"`
|
|
Outcome string `json:"outcome"`
|
|
PayloadJSON json.RawMessage `json:"payload_json"`
|
|
}
|
|
|
|
func (d *Database) ensureOathLedgerTable() error {
|
|
_, err := d.Exec(`CREATE TABLE IF NOT EXISTS oath_ledger (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
actor TEXT NOT NULL DEFAULT '',
|
|
action_type TEXT NOT NULL,
|
|
agent_id TEXT NOT NULL DEFAULT '',
|
|
strain TEXT NOT NULL DEFAULT '',
|
|
why_hash TEXT NOT NULL DEFAULT '',
|
|
outcome TEXT NOT NULL DEFAULT 'pending',
|
|
payload_json TEXT NOT NULL DEFAULT '{}'
|
|
)`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_oath_ledger_ts ON oath_ledger(timestamp)`)
|
|
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_oath_ledger_action ON oath_ledger(action_type)`)
|
|
return nil
|
|
}
|
|
|
|
// HashWhyJSON returns SHA256 hex of JSON-encoded source evidence (autopsy / court transcript).
|
|
func HashWhyJSON(source interface{}) string {
|
|
if source == nil {
|
|
return ""
|
|
}
|
|
raw, err := json.Marshal(source)
|
|
if err != nil {
|
|
raw = []byte("{}")
|
|
}
|
|
sum := sha256.Sum256(raw)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func marshalOathPayload(payload interface{}) []byte {
|
|
if payload == nil {
|
|
return []byte("{}")
|
|
}
|
|
switch v := payload.(type) {
|
|
case json.RawMessage:
|
|
if len(v) == 0 {
|
|
return []byte("{}")
|
|
}
|
|
return v
|
|
case map[string]interface{}:
|
|
raw, err := json.Marshal(v)
|
|
if err != nil {
|
|
return []byte("{}")
|
|
}
|
|
return raw
|
|
default:
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return []byte("{}")
|
|
}
|
|
return raw
|
|
}
|
|
}
|
|
|
|
// InsertOathLedger appends one immutable accountability row.
|
|
func (d *Database) InsertOathLedger(actor, actionType, agentID, strain, whyHash, outcome string, payload interface{}) (*OathLedgerEntry, error) {
|
|
if d == nil {
|
|
return nil, nil
|
|
}
|
|
if err := d.ensureOathLedgerTable(); err != nil {
|
|
return nil, err
|
|
}
|
|
if outcome == "" {
|
|
outcome = OathOutcomePending
|
|
}
|
|
payloadJSON := marshalOathPayload(payload)
|
|
ts := time.Now().UTC()
|
|
res, err := d.Exec(
|
|
`INSERT INTO oath_ledger (timestamp, actor, action_type, agent_id, strain, why_hash, outcome, payload_json)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
ts, actor, actionType, agentID, strain, whyHash, outcome, string(payloadJSON),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
return &OathLedgerEntry{
|
|
ID: id,
|
|
Timestamp: ts.Format(time.RFC3339),
|
|
Actor: actor,
|
|
ActionType: actionType,
|
|
AgentID: agentID,
|
|
Strain: strain,
|
|
WhyHash: whyHash,
|
|
Outcome: outcome,
|
|
PayloadJSON: json.RawMessage(payloadJSON),
|
|
}, nil
|
|
}
|
|
|
|
// ListOathLedger returns recent rows newest-first.
|
|
func (d *Database) ListOathLedger(limit int) ([]OathLedgerEntry, error) {
|
|
if d == nil {
|
|
return nil, nil
|
|
}
|
|
if err := d.ensureOathLedgerTable(); err != nil {
|
|
return nil, err
|
|
}
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
if limit > 500 {
|
|
limit = 500
|
|
}
|
|
rows, err := d.Query(
|
|
`SELECT id, timestamp, actor, action_type, agent_id, strain, why_hash, outcome, payload_json
|
|
FROM oath_ledger ORDER BY id DESC LIMIT ?`,
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []OathLedgerEntry
|
|
for rows.Next() {
|
|
var e OathLedgerEntry
|
|
var ts time.Time
|
|
var payloadStr string
|
|
if err := rows.Scan(&e.ID, &ts, &e.Actor, &e.ActionType, &e.AgentID, &e.Strain, &e.WhyHash, &e.Outcome, &payloadStr); err != nil {
|
|
return nil, err
|
|
}
|
|
e.Timestamp = ts.UTC().Format(time.RFC3339)
|
|
if payloadStr != "" {
|
|
e.PayloadJSON = json.RawMessage(payloadStr)
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out, rows.Err()
|
|
}
|