From ee58e790a0b26ccc76cb1a958077a2067b960d77 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 23 Feb 2026 05:47:04 +0000 Subject: [PATCH] feat: modernise to Go 1.26 iterators and stdlib helpers Add iter.Seq iterators for trust registry (ListSeq), audit log (EventsSeq, QuerySeq), and approval store (PendingSeq). Use slices.DeleteFunc in session store, slices.SortFunc in testcmd, range-over-int in benchmarks. Co-Authored-By: Gemini Co-Authored-By: Virgil --- auth/session_store.go | 16 ++++++++-------- cmd/testcmd/cmd_output.go | 7 ++++--- crypt/bench_test.go | 14 +++++++------- go.sum | 3 +++ trust/approval.go | 17 +++++++++++++++++ trust/approval_test.go | 16 ++++++++++++++++ trust/audit.go | 31 +++++++++++++++++++++++++++++++ trust/audit_test.go | 20 ++++++++++++++++++++ trust/bench_test.go | 6 +++--- trust/trust.go | 30 ++++++++++++++++++++++-------- trust/trust_test.go | 16 ++++++++++++++++ 11 files changed, 147 insertions(+), 29 deletions(-) diff --git a/auth/session_store.go b/auth/session_store.go index 955f3fc..12811bb 100644 --- a/auth/session_store.go +++ b/auth/session_store.go @@ -2,6 +2,7 @@ package auth import ( "errors" + "maps" "sync" "time" ) @@ -75,11 +76,9 @@ func (m *MemorySessionStore) DeleteByUser(userID string) error { m.mu.Lock() defer m.mu.Unlock() - for token, session := range m.sessions { - if session.UserID == userID { - delete(m.sessions, token) - } - } + maps.DeleteFunc(m.sessions, func(token string, session *Session) bool { + return session.UserID == userID + }) return nil } @@ -90,11 +89,12 @@ func (m *MemorySessionStore) Cleanup() (int, error) { now := time.Now() count := 0 - for token, session := range m.sessions { + maps.DeleteFunc(m.sessions, func(token string, session *Session) bool { if now.After(session.ExpiresAt) { - delete(m.sessions, token) count++ + return true } - } + return false + }) return count, nil } diff --git a/cmd/testcmd/cmd_output.go b/cmd/testcmd/cmd_output.go index 450cf2b..ab137a1 100644 --- a/cmd/testcmd/cmd_output.go +++ b/cmd/testcmd/cmd_output.go @@ -2,10 +2,11 @@ package testcmd import ( "bufio" + "cmp" "fmt" "path/filepath" "regexp" - "sort" + "slices" "strconv" "strings" @@ -119,8 +120,8 @@ func printCoverageSummary(results testResults) { fmt.Printf("\n %s\n", testHeaderStyle.Render(i18n.T("cmd.test.coverage_by_package"))) // Sort packages by name - sort.Slice(results.packages, func(i, j int) bool { - return results.packages[i].name < results.packages[j].name + slices.SortFunc(results.packages, func(a, b packageCoverage) int { + return cmp.Compare(a.name, b.name) }) // Find max package name length for alignment diff --git a/crypt/bench_test.go b/crypt/bench_test.go index c9c294f..6b2a97b 100644 --- a/crypt/bench_test.go +++ b/crypt/bench_test.go @@ -13,7 +13,7 @@ func BenchmarkArgon2Derive(b *testing.B) { _, _ = rand.Read(salt) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _ = DeriveKey(passphrase, salt, argon2KeyLen) } } @@ -27,7 +27,7 @@ func BenchmarkChaCha20Encrypt_1KB(b *testing.B) { b.ResetTimer() b.SetBytes(1024) - for i := 0; i < b.N; i++ { + for range b.N { _, _ = ChaCha20Encrypt(plaintext, key) } } @@ -41,7 +41,7 @@ func BenchmarkChaCha20Encrypt_1MB(b *testing.B) { b.ResetTimer() b.SetBytes(1024 * 1024) - for i := 0; i < b.N; i++ { + for range b.N { _, _ = ChaCha20Encrypt(plaintext, key) } } @@ -55,7 +55,7 @@ func BenchmarkAESGCMEncrypt_1KB(b *testing.B) { b.ResetTimer() b.SetBytes(1024) - for i := 0; i < b.N; i++ { + for range b.N { _, _ = AESGCMEncrypt(plaintext, key) } } @@ -69,7 +69,7 @@ func BenchmarkAESGCMEncrypt_1MB(b *testing.B) { b.ResetTimer() b.SetBytes(1024 * 1024) - for i := 0; i < b.N; i++ { + for range b.N { _, _ = AESGCMEncrypt(plaintext, key) } } @@ -83,7 +83,7 @@ func BenchmarkHMACSHA256_1KB(b *testing.B) { b.ResetTimer() b.SetBytes(1024) - for i := 0; i < b.N; i++ { + for range b.N { _ = HMACSHA256(message, key) } } @@ -97,7 +97,7 @@ func BenchmarkVerifyHMACSHA256(b *testing.B) { mac := HMACSHA256(message, key) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _ = VerifyHMAC(message, key, mac, sha256.New) } } diff --git a/go.sum b/go.sum index e682685..98c22bf 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,9 @@ forge.lthn.ai/core/cli v0.0.1 h1:nqpc4Tv8a4H/ERei+/71DVQxkCFU8HPFJP4120qPXgk= +forge.lthn.ai/core/cli v0.0.1/go.mod h1:xa3Nqw3sUtYYJ1k+1jYul18tgs6sBevCUsGsHJI1hHA= forge.lthn.ai/core/go v0.0.1 h1:ubk4nmkA3treOUNgPS28wKd1jB6cUlEQUV7jDdGa3zM= +forge.lthn.ai/core/go v0.0.1/go.mod h1:59YsnuMaAGQUxIhX68oK2/HnhQJEPWL1iEZhDTrNCbY= forge.lthn.ai/core/go-store v0.1.0 h1:ONO4NfnFVey2QOE5JAZp5dQPI2pxRCHWAtQ+oYFJgGE= +forge.lthn.ai/core/go-store v0.1.0/go.mod h1:FpUlLEX/ebyoxpk96F7ktr0vYvmFtC5Rpi9fi88UVqw= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/trust/approval.go b/trust/approval.go index a2afd55..0f5608a 100644 --- a/trust/approval.go +++ b/trust/approval.go @@ -2,6 +2,7 @@ package trust import ( "fmt" + "iter" "sync" "time" ) @@ -166,6 +167,22 @@ func (q *ApprovalQueue) Pending() []ApprovalRequest { return out } +// PendingSeq returns an iterator over all requests with ApprovalPending status. +func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] { + return func(yield func(ApprovalRequest) bool) { + q.mu.RLock() + defer q.mu.RUnlock() + + for _, req := range q.requests { + if req.Status == ApprovalPending { + if !yield(*req) { + return + } + } + } + } +} + // Len returns the total number of requests in the queue. func (q *ApprovalQueue) Len() int { q.mu.RLock() diff --git a/trust/approval_test.go b/trust/approval_test.go index fca9f08..23fe6f2 100644 --- a/trust/approval_test.go +++ b/trust/approval_test.go @@ -201,6 +201,22 @@ func TestApprovalPending_Good_Empty(t *testing.T) { assert.Empty(t, q.Pending()) } +func TestApprovalPendingSeq_Good(t *testing.T) { + q := NewApprovalQueue() + q.Submit("Clotho", CapMergePR, "host-uk/core") + q.Submit("Hypnos", CapMergePR, "host-uk/docs") + + id3, _ := q.Submit("Darbs", CapMergePR, "host-uk/tools") + q.Approve(id3, "admin", "") + + count := 0 + for req := range q.PendingSeq() { + assert.Equal(t, ApprovalPending, req.Status) + count++ + } + assert.Equal(t, 2, count) +} + // --- Concurrent operations --- func TestApprovalConcurrent_Good(t *testing.T) { diff --git a/trust/audit.go b/trust/audit.go index acb6920..5b371f4 100644 --- a/trust/audit.go +++ b/trust/audit.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "iter" "sync" "time" ) @@ -103,6 +104,20 @@ func (l *AuditLog) Entries() []AuditEntry { return out } +// EntriesSeq returns an iterator over all audit entries. +func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] { + return func(yield func(AuditEntry) bool) { + l.mu.Lock() + defer l.mu.Unlock() + + for _, e := range l.entries { + if !yield(e) { + return + } + } + } +} + // Len returns the number of entries in the log. func (l *AuditLog) Len() int { l.mu.Lock() @@ -123,3 +138,19 @@ func (l *AuditLog) EntriesFor(agent string) []AuditEntry { } return out } + +// EntriesForSeq returns an iterator over audit entries for a specific agent. +func (l *AuditLog) EntriesForSeq(agent string) iter.Seq[AuditEntry] { + return func(yield func(AuditEntry) bool) { + l.mu.Lock() + defer l.mu.Unlock() + + for _, e := range l.entries { + if e.Agent == agent { + if !yield(e) { + return + } + } + } + } +} diff --git a/trust/audit_test.go b/trust/audit_test.go index c2ed686..2b459ed 100644 --- a/trust/audit_test.go +++ b/trust/audit_test.go @@ -114,6 +114,26 @@ func TestAuditEntriesFor_Good(t *testing.T) { for _, e := range athenaEntries { assert.Equal(t, "Athena", e.Agent) } + + // Test iterator version + count := 0 + for e := range log.EntriesForSeq("Athena") { + assert.Equal(t, "Athena", e.Agent) + count++ + } + assert.Equal(t, 2, count) +} + +func TestAuditEntriesSeq_Good(t *testing.T) { + log := NewAuditLog(nil) + log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") + log.Record(EvalResult{Agent: "Clotho", Cap: CapCreatePR, Decision: Allow, Reason: "ok"}, "") + + count := 0 + for range log.EntriesSeq() { + count++ + } + assert.Equal(t, 2, count) } func TestAuditEntriesFor_Bad_NotFound(t *testing.T) { diff --git a/trust/bench_test.go b/trust/bench_test.go index 53655a0..6314567 100644 --- a/trust/bench_test.go +++ b/trust/bench_test.go @@ -31,7 +31,7 @@ func BenchmarkPolicyEvaluate(b *testing.B) { } b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := range b.N { agentName := fmt.Sprintf("agent-%d", i%100) cap := caps[i%len(caps)] _ = pe.Evaluate(agentName, cap, "host-uk/core") @@ -49,7 +49,7 @@ func BenchmarkRegistryGet(b *testing.B) { } b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := range b.N { name := fmt.Sprintf("agent-%d", i%100) _ = r.Get(name) } @@ -60,7 +60,7 @@ func BenchmarkRegistryRegister(b *testing.B) { r := NewRegistry() b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := range b.N { _ = r.Register(Agent{ Name: fmt.Sprintf("bench-agent-%d", i), Tier: TierVerified, diff --git a/trust/trust.go b/trust/trust.go index d5c0636..3b2a28d 100644 --- a/trust/trust.go +++ b/trust/trust.go @@ -12,6 +12,7 @@ package trust import ( "fmt" + "iter" "sync" "time" ) @@ -51,15 +52,15 @@ func (t Tier) Valid() bool { type Capability string const ( - CapPushRepo Capability = "repo.push" - CapMergePR Capability = "pr.merge" - CapCreatePR Capability = "pr.create" - CapCreateIssue Capability = "issue.create" - CapCommentIssue Capability = "issue.comment" - CapReadSecrets Capability = "secrets.read" - CapRunPrivileged Capability = "cmd.privileged" + CapPushRepo Capability = "repo.push" + CapMergePR Capability = "pr.merge" + CapCreatePR Capability = "pr.create" + CapCreateIssue Capability = "issue.create" + CapCommentIssue Capability = "issue.comment" + CapReadSecrets Capability = "secrets.read" + CapRunPrivileged Capability = "cmd.privileged" CapAccessWorkspace Capability = "workspace.access" - CapModifyFlows Capability = "flows.modify" + CapModifyFlows Capability = "flows.modify" ) // Agent represents an agent identity in the trust system. @@ -143,6 +144,19 @@ func (r *Registry) List() []Agent { return out } +// ListSeq returns an iterator over all registered agents. +func (r *Registry) ListSeq() iter.Seq[Agent] { + return func(yield func(Agent) bool) { + r.mu.RLock() + defer r.mu.RUnlock() + for _, a := range r.agents { + if !yield(*a) { + return + } + } + } +} + // Len returns the number of registered agents. func (r *Registry) Len() int { r.mu.RLock() diff --git a/trust/trust_test.go b/trust/trust_test.go index 3fa1822..69a5369 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -151,6 +151,22 @@ func TestRegistryList_Good_Snapshot(t *testing.T) { assert.Equal(t, TierFull, r.Get("Athena").Tier) } +func TestRegistryListSeq_Good(t *testing.T) { + r := NewRegistry() + require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) + require.NoError(t, r.Register(Agent{Name: "Clotho", Tier: TierVerified})) + + count := 0 + names := make(map[string]bool) + for a := range r.ListSeq() { + names[a.Name] = true + count++ + } + assert.Equal(t, 2, count) + assert.True(t, names["Athena"]) + assert.True(t, names["Clotho"]) +} + // --- Agent --- func TestAgentTokenExpiry(t *testing.T) {