- ect library: create, verify, DAG validation, ledger interface - In-memory ledger and ECTStore for full ledger mode - Test vectors and unit tests; two-agent demo (cmd/demo) - README: document refimpl scope and usage Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
3.0 KiB
Go
108 lines
3.0 KiB
Go
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: tid must not already exist (scoped by wid when present)
|
|
wid := payload.Wid
|
|
if m.containsLocked(payload.Tid, wid) {
|
|
return 0, ErrTaskIDExists
|
|
}
|
|
m.seq++
|
|
entry := LedgerEntry{
|
|
LedgerSequence: m.seq,
|
|
TaskID: payload.Tid,
|
|
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.Tid] = 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 tid already exists.
|
|
var ErrTaskIDExists = errors.New("ect: task ID already exists in ledger")
|