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) {