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" ) // 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() }