package ect import ( "errors" "sync" "time" ) // LedgerEntry represents a single ECT record in the audit ledger (Section 9.3). type LedgerEntry struct { LedgerSequence int64 `json:"ledger_sequence"` TaskID string `json:"task_id"` AgentID string `json:"agent_id"` Action string `json:"action"` Parents []string `json:"parents"` ECTJWS string `json:"ect_jws"` SignatureVerified bool `json:"signature_verified"` VerificationTime time.Time `json:"verification_timestamp"` StoredTime time.Time `json:"stored_timestamp"` } // Ledger is the audit ledger interface per Section 9. Append-only; lookup by tid. type Ledger interface { // Append records a verified ECT. Returns the new ledger sequence number or error. Append(ectJWS string, payload *Payload) (seq int64, err error) // GetByTid returns the payload for the given task ID, or nil. GetByTid(tid string) *Payload // Contains returns true if (tid, wid) exists. wid may be empty for global scope. Contains(tid, wid string) bool // ECTStore implementation for DAG validation ECTStore } // MemoryLedger is an in-memory, append-only ECT store implementing Ledger and ECTStore. type MemoryLedger struct { mu sync.RWMutex seq int64 byTid map[string]*Payload bySeq []LedgerEntry entries []LedgerEntry // full entries for audit } // NewMemoryLedger creates an empty in-memory ledger. func NewMemoryLedger() *MemoryLedger { return &MemoryLedger{ byTid: make(map[string]*Payload), bySeq: make([]LedgerEntry, 0), } } // Append implements Ledger. func (m *MemoryLedger) Append(ectJWS string, payload *Payload) (int64, error) { if payload == nil { return 0, nil } m.mu.Lock() defer m.mu.Unlock() // Uniqueness: jti (task id) must not already exist (scoped by wid when present) per spec wid := payload.Wid if m.containsLocked(payload.Jti, wid) { return 0, ErrTaskIDExists } m.seq++ entry := LedgerEntry{ LedgerSequence: m.seq, TaskID: payload.Jti, // task id = jti per spec AgentID: payload.Iss, Action: payload.ExecAct, Parents: append([]string(nil), payload.Par...), ECTJWS: ectJWS, SignatureVerified: true, VerificationTime: time.Now().UTC(), StoredTime: time.Now().UTC(), } m.byTid[payload.Jti] = payload m.bySeq = append(m.bySeq, entry) m.entries = append(m.entries, entry) return m.seq, nil } // GetByTid implements ECTStore and Ledger. func (m *MemoryLedger) GetByTid(tid string) *Payload { m.mu.RLock() defer m.mu.RUnlock() return m.byTid[tid] } // Contains implements ECTStore and Ledger. func (m *MemoryLedger) Contains(tid, wid string) bool { m.mu.RLock() defer m.mu.RUnlock() return m.containsLocked(tid, wid) } func (m *MemoryLedger) containsLocked(tid, wid string) bool { p, ok := m.byTid[tid] if !ok { return false } if wid == "" { return true } return p.Wid == wid } // ErrTaskIDExists is returned when appending an ECT whose jti already exists. var ErrTaskIDExists = errors.New("ect: task ID (jti) already exists in ledger")