refactor(crypt): complete AX v0.8.0 polish pass

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-26 17:32:21 +00:00
parent f46cd04e2f
commit 53d7d59a9d
42 changed files with 527 additions and 354 deletions

View file

@ -40,11 +40,14 @@ import (
coreerr "dappco.re/go/core/log"
)
// Default durations for challenge and session lifetimes.
const (
// DefaultChallengeTTL is the default lifetime for a generated challenge.
// Usage: pass DefaultChallengeTTL into WithChallengeTTL(...) to keep the package default.
DefaultChallengeTTL = 5 * time.Minute
DefaultSessionTTL = 24 * time.Hour
nonceBytes = 32
// DefaultSessionTTL is the default lifetime for an authenticated session.
// Usage: pass DefaultSessionTTL into WithSessionTTL(...) to keep the package default.
DefaultSessionTTL = 24 * time.Hour
nonceBytes = 32
)
// protectedUsers lists usernames that cannot be deleted.
@ -55,6 +58,7 @@ var protectedUsers = map[string]bool{
}
// User represents a registered user with PGP credentials.
// Usage: use User with the other exported helpers in this package.
type User struct {
PublicKey string `json:"public_key"`
KeyID string `json:"key_id"`
@ -65,6 +69,7 @@ type User struct {
}
// Challenge is a PGP-encrypted nonce sent to a client during authentication.
// Usage: use Challenge with the other exported helpers in this package.
type Challenge struct {
Nonce []byte `json:"nonce"`
Encrypted string `json:"encrypted"` // PGP-encrypted nonce (armored)
@ -72,6 +77,7 @@ type Challenge struct {
}
// Session represents an authenticated session.
// Usage: use Session with the other exported helpers in this package.
type Session struct {
Token string `json:"token"`
UserID string `json:"user_id"`
@ -80,6 +86,7 @@ type Session struct {
// Revocation records the details of a revoked user key.
// Stored as JSON in the user's .rev file, replacing the legacy placeholder.
// Usage: use Revocation with the other exported helpers in this package.
type Revocation struct {
UserID string `json:"user_id"`
Reason string `json:"reason"`
@ -87,9 +94,11 @@ type Revocation struct {
}
// Option configures an Authenticator.
// Usage: use Option with the other exported helpers in this package.
type Option func(*Authenticator)
// WithChallengeTTL sets the lifetime of a challenge before it expires.
// Usage: pass WithChallengeTTL(...) into the related constructor to adjust the default behaviour.
func WithChallengeTTL(d time.Duration) Option {
return func(a *Authenticator) {
a.challengeTTL = d
@ -97,6 +106,7 @@ func WithChallengeTTL(d time.Duration) Option {
}
// WithSessionTTL sets the lifetime of a session before it expires.
// Usage: pass WithSessionTTL(...) into the related constructor to adjust the default behaviour.
func WithSessionTTL(d time.Duration) Option {
return func(a *Authenticator) {
a.sessionTTL = d
@ -105,6 +115,7 @@ func WithSessionTTL(d time.Duration) Option {
// WithSessionStore sets the SessionStore implementation.
// If not provided, an in-memory store is used (sessions lost on restart).
// Usage: pass WithSessionStore(...) into the related constructor to adjust the default behaviour.
func WithSessionStore(s SessionStore) Option {
return func(a *Authenticator) {
a.store = s
@ -120,6 +131,7 @@ func WithSessionStore(s SessionStore) Option {
// An optional HardwareKey can be provided via WithHardwareKey for
// hardware-backed cryptographic operations (PKCS#11, YubiKey, etc.).
// See auth/hardware.go for the interface definition and integration points.
// Usage: create an Authenticator with New(...) and then call Register, Login, or CreateChallenge.
type Authenticator struct {
medium io.Medium
store SessionStore
@ -133,6 +145,7 @@ type Authenticator struct {
// New creates an Authenticator that persists user data via the given Medium.
// By default, sessions are stored in memory. Use WithSessionStore to provide
// a persistent implementation (e.g. SQLiteSessionStore).
// Usage: call New(...) to create a ready-to-use value.
func New(m io.Medium, opts ...Option) *Authenticator {
a := &Authenticator{
medium: m,
@ -159,6 +172,7 @@ func userPath(userID, ext string) string {
// produce a userID, generates a PGP keypair (protected by the given password),
// and persists the public key, private key, revocation placeholder, password
// hash (Argon2id), and encrypted metadata via the Medium.
// Usage: call Register(...) during the package's normal workflow.
func (a *Authenticator) Register(username, password string) (*User, error) {
const op = "auth.Register"
@ -237,6 +251,7 @@ func (a *Authenticator) Register(username, password string) (*User, error) {
// CreateChallenge generates a cryptographic challenge for the given user.
// A random nonce is created and encrypted with the user's PGP public key.
// The client must decrypt the nonce and sign it to prove key ownership.
// Usage: call CreateChallenge(...) during the package's normal workflow.
func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) {
const op = "auth.CreateChallenge"
@ -279,6 +294,7 @@ func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) {
// ValidateResponse verifies a signed nonce from the client. The client must
// have decrypted the challenge nonce and signed it with their private key.
// On success, a new session is created and returned.
// Usage: call ValidateResponse(...) during the package's normal workflow.
func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Session, error) {
const op = "auth.ValidateResponse"
@ -313,6 +329,7 @@ func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Se
}
// ValidateSession checks whether a token maps to a valid, non-expired session.
// Usage: call ValidateSession(...) during the package's normal workflow.
func (a *Authenticator) ValidateSession(token string) (*Session, error) {
const op = "auth.ValidateSession"
@ -330,6 +347,7 @@ func (a *Authenticator) ValidateSession(token string) (*Session, error) {
}
// RefreshSession extends the expiry of an existing valid session.
// Usage: call RefreshSession(...) during the package's normal workflow.
func (a *Authenticator) RefreshSession(token string) (*Session, error) {
const op = "auth.RefreshSession"
@ -351,6 +369,7 @@ func (a *Authenticator) RefreshSession(token string) (*Session, error) {
}
// RevokeSession removes a session, invalidating the token immediately.
// Usage: call RevokeSession(...) during the package's normal workflow.
func (a *Authenticator) RevokeSession(token string) error {
const op = "auth.RevokeSession"
@ -363,6 +382,7 @@ func (a *Authenticator) RevokeSession(token string) error {
// DeleteUser removes a user and all associated keys from storage.
// The "server" user is protected and cannot be deleted (mirroring the
// original TypeScript implementation's safeguard).
// Usage: call DeleteUser(...) during the package's normal workflow.
func (a *Authenticator) DeleteUser(userID string) error {
const op = "auth.DeleteUser"
@ -403,6 +423,8 @@ func (a *Authenticator) DeleteUser(userID string) error {
// - Otherwise, falls back to legacy .lthn file with LTHN hash verification.
// On successful legacy login, the password is re-hashed with Argon2id and
// a .hash file is written (transparent migration).
//
// Usage: call Login(...) for password-based flows when challenge-response is not required.
func (a *Authenticator) Login(userID, password string) (*Session, error) {
const op = "auth.Login"
@ -455,6 +477,7 @@ func (a *Authenticator) Login(userID, password string) (*Session, error) {
// all existing sessions. The caller must provide the current password
// (oldPassword) to decrypt existing metadata and the new password (newPassword)
// to protect the new keypair.
// Usage: call RotateKeyPair(...) during the package's normal workflow.
func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) (*User, error) {
const op = "auth.RotateKeyPair"
@ -539,6 +562,7 @@ func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) (
// RevokeKey marks a user's key as revoked. It verifies the password first,
// writes a JSON revocation record to the .rev file (replacing the placeholder),
// and invalidates all sessions for the user.
// Usage: call RevokeKey(...) during the package's normal workflow.
func (a *Authenticator) RevokeKey(userID, password, reason string) error {
const op = "auth.RevokeKey"
@ -576,6 +600,7 @@ func (a *Authenticator) RevokeKey(userID, password, reason string) error {
// IsRevoked checks whether a user's key has been revoked by inspecting the
// .rev file. Returns true only if the file contains valid revocation JSON
// (not the legacy "REVOCATION_PLACEHOLDER" string).
// Usage: call IsRevoked(...) during the package's normal workflow.
func (a *Authenticator) IsRevoked(userID string) bool {
content, err := a.medium.Read(userPath(userID, ".rev"))
if err != nil {
@ -601,6 +626,7 @@ func (a *Authenticator) IsRevoked(userID string) bool {
// WriteChallengeFile writes an encrypted challenge to a file for air-gapped
// (courier) transport. The challenge is created and then its encrypted nonce
// is written to the specified path on the Medium.
// Usage: call WriteChallengeFile(...) during the package's normal workflow.
func (a *Authenticator) WriteChallengeFile(userID, path string) error {
const op = "auth.WriteChallengeFile"
@ -625,6 +651,7 @@ func (a *Authenticator) WriteChallengeFile(userID, path string) error {
// ReadResponseFile reads a signed response from a file and validates it,
// completing the air-gapped authentication flow. The file must contain the
// raw PGP signature bytes (armored).
// Usage: call ReadResponseFile(...) during the package's normal workflow.
func (a *Authenticator) ReadResponseFile(userID, path string) (*Session, error) {
const op = "auth.ReadResponseFile"
@ -698,6 +725,7 @@ func (a *Authenticator) createSession(userID string) (*Session, error) {
// StartCleanup runs a background goroutine that periodically removes expired
// sessions from the store. It stops when the context is cancelled.
// Usage: call StartCleanup(...) during the package's normal workflow.
func (a *Authenticator) StartCleanup(ctx context.Context, interval time.Duration) {
go func() {
ticker := time.NewTicker(interval)

View file

@ -23,7 +23,7 @@ func newTestAuth(opts ...Option) (*Authenticator, *io.MockMedium) {
// --- Register ---
func TestRegister_Good(t *testing.T) {
func TestAuth_Register_Good(t *testing.T) {
a, m := newTestAuth()
user, err := a.Register("alice", "hunter2")
@ -48,7 +48,7 @@ func TestRegister_Good(t *testing.T) {
assert.False(t, user.Created.IsZero())
}
func TestRegister_Bad(t *testing.T) {
func TestAuth_Register_Bad(t *testing.T) {
a, _ := newTestAuth()
// Register first time succeeds
@ -61,7 +61,7 @@ func TestRegister_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "user already exists")
}
func TestRegister_Ugly(t *testing.T) {
func TestAuth_Register_Ugly(t *testing.T) {
a, _ := newTestAuth()
// Empty username/password should still work (PGP allows it)
@ -72,7 +72,7 @@ func TestRegister_Ugly(t *testing.T) {
// --- CreateChallenge ---
func TestCreateChallenge_Good(t *testing.T) {
func TestAuth_CreateChallenge_Good(t *testing.T) {
a, _ := newTestAuth()
user, err := a.Register("charlie", "pass")
@ -87,7 +87,7 @@ func TestCreateChallenge_Good(t *testing.T) {
assert.True(t, challenge.ExpiresAt.After(time.Now()))
}
func TestCreateChallenge_Bad(t *testing.T) {
func TestAuth_CreateChallenge_Bad(t *testing.T) {
a, _ := newTestAuth()
// Challenge for non-existent user
@ -96,7 +96,7 @@ func TestCreateChallenge_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "user not found")
}
func TestCreateChallenge_Ugly(t *testing.T) {
func TestAuth_CreateChallenge_Ugly(t *testing.T) {
a, _ := newTestAuth()
// Empty userID
@ -106,7 +106,7 @@ func TestCreateChallenge_Ugly(t *testing.T) {
// --- ValidateResponse (full challenge-response flow) ---
func TestValidateResponse_Good(t *testing.T) {
func TestAuth_ValidateResponse_Good(t *testing.T) {
a, m := newTestAuth()
// Register user
@ -140,7 +140,7 @@ func TestValidateResponse_Good(t *testing.T) {
assert.True(t, session.ExpiresAt.After(time.Now()))
}
func TestValidateResponse_Bad(t *testing.T) {
func TestAuth_ValidateResponse_Bad(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("eve", "pass")
@ -153,7 +153,7 @@ func TestValidateResponse_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "no pending challenge")
}
func TestValidateResponse_Ugly(t *testing.T) {
func TestAuth_ValidateResponse_Ugly(t *testing.T) {
a, m := newTestAuth(WithChallengeTTL(1 * time.Millisecond))
_, err := a.Register("frank", "pass")
@ -180,7 +180,7 @@ func TestValidateResponse_Ugly(t *testing.T) {
// --- ValidateSession ---
func TestValidateSession_Good(t *testing.T) {
func TestAuth_ValidateSession_Good(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("grace", "pass")
@ -196,7 +196,7 @@ func TestValidateSession_Good(t *testing.T) {
assert.Equal(t, userID, validated.UserID)
}
func TestValidateSession_Bad(t *testing.T) {
func TestAuth_ValidateSession_Bad(t *testing.T) {
a, _ := newTestAuth()
_, err := a.ValidateSession("nonexistent-token")
@ -204,7 +204,7 @@ func TestValidateSession_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "session not found")
}
func TestValidateSession_Ugly(t *testing.T) {
func TestAuth_ValidateSession_Ugly(t *testing.T) {
a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond))
_, err := a.Register("heidi", "pass")
@ -223,7 +223,7 @@ func TestValidateSession_Ugly(t *testing.T) {
// --- RefreshSession ---
func TestRefreshSession_Good(t *testing.T) {
func TestAuth_RefreshSession_Good(t *testing.T) {
a, _ := newTestAuth(WithSessionTTL(1 * time.Hour))
_, err := a.Register("ivan", "pass")
@ -243,7 +243,7 @@ func TestRefreshSession_Good(t *testing.T) {
assert.True(t, refreshed.ExpiresAt.After(originalExpiry))
}
func TestRefreshSession_Bad(t *testing.T) {
func TestAuth_RefreshSession_Bad(t *testing.T) {
a, _ := newTestAuth()
_, err := a.RefreshSession("nonexistent-token")
@ -251,7 +251,7 @@ func TestRefreshSession_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "session not found")
}
func TestRefreshSession_Ugly(t *testing.T) {
func TestAuth_RefreshSession_Ugly(t *testing.T) {
a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond))
_, err := a.Register("judy", "pass")
@ -270,7 +270,7 @@ func TestRefreshSession_Ugly(t *testing.T) {
// --- RevokeSession ---
func TestRevokeSession_Good(t *testing.T) {
func TestAuth_RevokeSession_Good(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("karl", "pass")
@ -288,7 +288,7 @@ func TestRevokeSession_Good(t *testing.T) {
assert.Error(t, err)
}
func TestRevokeSession_Bad(t *testing.T) {
func TestAuth_RevokeSession_Bad(t *testing.T) {
a, _ := newTestAuth()
err := a.RevokeSession("nonexistent-token")
@ -296,7 +296,7 @@ func TestRevokeSession_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "session not found")
}
func TestRevokeSession_Ugly(t *testing.T) {
func TestAuth_RevokeSession_Ugly(t *testing.T) {
a, _ := newTestAuth()
// Revoke empty token
@ -306,7 +306,7 @@ func TestRevokeSession_Ugly(t *testing.T) {
// --- DeleteUser ---
func TestDeleteUser_Good(t *testing.T) {
func TestAuth_DeleteUser_Good(t *testing.T) {
a, m := newTestAuth()
_, err := a.Register("larry", "pass")
@ -334,7 +334,7 @@ func TestDeleteUser_Good(t *testing.T) {
assert.Contains(t, err.Error(), "session not found")
}
func TestDeleteUser_Bad(t *testing.T) {
func TestAuth_DeleteUser_Bad(t *testing.T) {
a, _ := newTestAuth()
// Protected user "server" cannot be deleted
@ -343,7 +343,7 @@ func TestDeleteUser_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "cannot delete protected user")
}
func TestDeleteUser_Ugly(t *testing.T) {
func TestAuth_DeleteUser_Ugly(t *testing.T) {
a, _ := newTestAuth()
// Non-existent user
@ -354,7 +354,7 @@ func TestDeleteUser_Ugly(t *testing.T) {
// --- Login ---
func TestLogin_Good(t *testing.T) {
func TestAuth_Login_Good(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("mallory", "secret")
@ -370,7 +370,7 @@ func TestLogin_Good(t *testing.T) {
assert.True(t, session.ExpiresAt.After(time.Now()))
}
func TestLogin_Bad(t *testing.T) {
func TestAuth_Login_Bad(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("nancy", "correct-password")
@ -383,7 +383,7 @@ func TestLogin_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "invalid password")
}
func TestLogin_Ugly(t *testing.T) {
func TestAuth_Login_Ugly(t *testing.T) {
a, _ := newTestAuth()
// Login for non-existent user
@ -394,7 +394,7 @@ func TestLogin_Ugly(t *testing.T) {
// --- WriteChallengeFile / ReadResponseFile (Air-Gapped) ---
func TestAirGappedFlow_Good(t *testing.T) {
func TestAuth_AirGappedFlow_Good(t *testing.T) {
a, m := newTestAuth()
_, err := a.Register("oscar", "airgap-pass")
@ -439,7 +439,7 @@ func TestAirGappedFlow_Good(t *testing.T) {
assert.Equal(t, userID, session.UserID)
}
func TestWriteChallengeFile_Bad(t *testing.T) {
func TestAuth_WriteChallengeFile_Bad(t *testing.T) {
a, _ := newTestAuth()
// Challenge for non-existent user
@ -447,7 +447,7 @@ func TestWriteChallengeFile_Bad(t *testing.T) {
assert.Error(t, err)
}
func TestReadResponseFile_Bad(t *testing.T) {
func TestAuth_ReadResponseFile_Bad(t *testing.T) {
a, _ := newTestAuth()
// Response file does not exist
@ -455,7 +455,7 @@ func TestReadResponseFile_Bad(t *testing.T) {
assert.Error(t, err)
}
func TestReadResponseFile_Ugly(t *testing.T) {
func TestAuth_ReadResponseFile_Ugly(t *testing.T) {
a, m := newTestAuth()
_, err := a.Register("peggy", "pass")
@ -477,13 +477,13 @@ func TestReadResponseFile_Ugly(t *testing.T) {
// --- Options ---
func TestWithChallengeTTL_Good(t *testing.T) {
func TestAuth_WithChallengeTTL_Good(t *testing.T) {
ttl := 30 * time.Second
a, _ := newTestAuth(WithChallengeTTL(ttl))
assert.Equal(t, ttl, a.challengeTTL)
}
func TestWithSessionTTL_Good(t *testing.T) {
func TestAuth_WithSessionTTL_Good(t *testing.T) {
ttl := 2 * time.Hour
a, _ := newTestAuth(WithSessionTTL(ttl))
assert.Equal(t, ttl, a.sessionTTL)
@ -491,7 +491,7 @@ func TestWithSessionTTL_Good(t *testing.T) {
// --- Full Round-Trip (Online Flow) ---
func TestFullRoundTrip_Good(t *testing.T) {
func TestAuth_FullRoundTrip_Good(t *testing.T) {
a, m := newTestAuth()
// 1. Register
@ -541,7 +541,7 @@ func TestFullRoundTrip_Good(t *testing.T) {
// --- Concurrent Access ---
func TestConcurrentSessions_Good(t *testing.T) {
func TestAuth_ConcurrentSessions_Good(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("ruth", "pass")
@ -579,9 +579,9 @@ func TestConcurrentSessions_Good(t *testing.T) {
// --- Phase 0 Additions ---
// TestConcurrentSessionCreation_Good verifies that 10 goroutines creating
// TestAuth_ConcurrentSessionCreation_Good verifies that 10 goroutines creating
// sessions simultaneously do not produce data races or errors.
func TestConcurrentSessionCreation_Good(t *testing.T) {
func TestAuth_ConcurrentSessionCreation_Good(t *testing.T) {
a, _ := newTestAuth()
// Register 10 distinct users to avoid contention on a single user record
@ -619,9 +619,9 @@ func TestConcurrentSessionCreation_Good(t *testing.T) {
}
}
// TestSessionTokenUniqueness_Good generates 1000 session tokens and verifies
// TestAuth_SessionTokenUniqueness_Good generates 1000 session tokens and verifies
// no collisions without paying the full login hash-verification cost each time.
func TestSessionTokenUniqueness_Good(t *testing.T) {
func TestAuth_SessionTokenUniqueness_Good(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("uniqueness-test", "pass")
@ -645,9 +645,9 @@ func TestSessionTokenUniqueness_Good(t *testing.T) {
assert.Len(t, tokens, n, "all 1000 tokens should be unique")
}
// TestChallengeExpiryBoundary_Ugly tests validation right at the 5-minute boundary.
// TestAuth_ChallengeExpiryBoundary_Ugly tests validation right at the 5-minute boundary.
// The challenge should still be valid just before expiry and rejected after.
func TestChallengeExpiryBoundary_Ugly(t *testing.T) {
func TestAuth_ChallengeExpiryBoundary_Ugly(t *testing.T) {
// Use a very short TTL to test the boundary without sleeping 5 minutes
ttl := 50 * time.Millisecond
a, m := newTestAuth(WithChallengeTTL(ttl))
@ -691,9 +691,9 @@ func TestChallengeExpiryBoundary_Ugly(t *testing.T) {
assert.Contains(t, err.Error(), "challenge expired")
}
// TestEmptyPasswordRegistration_Good verifies that empty password registration works.
// TestAuth_EmptyPasswordRegistration_Good verifies that empty password registration works.
// PGP key is generated unencrypted in this case.
func TestEmptyPasswordRegistration_Good(t *testing.T) {
func TestAuth_EmptyPasswordRegistration_Good(t *testing.T) {
a, m := newTestAuth()
user, err := a.Register("no-password-user", "")
@ -730,8 +730,8 @@ func TestEmptyPasswordRegistration_Good(t *testing.T) {
assert.NotNil(t, crSession)
}
// TestVeryLongUsername_Ugly verifies behaviour with a 10K character username.
func TestVeryLongUsername_Ugly(t *testing.T) {
// TestAuth_VeryLongUsername_Ugly verifies behaviour with a 10K character username.
func TestAuth_VeryLongUsername_Ugly(t *testing.T) {
a, _ := newTestAuth()
longName := core.NewBuilder()
@ -753,8 +753,8 @@ func TestVeryLongUsername_Ugly(t *testing.T) {
assert.NotNil(t, session)
}
// TestUnicodeUsernamePassword_Good verifies registration and login with Unicode characters.
func TestUnicodeUsernamePassword_Good(t *testing.T) {
// TestAuth_UnicodeUsernamePassword_Good verifies registration and login with Unicode characters.
func TestAuth_UnicodeUsernamePassword_Good(t *testing.T) {
a, _ := newTestAuth()
// Japanese + emoji + Chinese + Arabic
@ -777,9 +777,9 @@ func TestUnicodeUsernamePassword_Good(t *testing.T) {
assert.Error(t, err)
}
// TestAirGappedRoundTrip_Good tests the full air-gapped flow:
// TestAuth_AirGappedRoundTrip_Good tests the full air-gapped flow:
// WriteChallengeFile -> client signs offline -> ReadResponseFile
func TestAirGappedRoundTrip_Good(t *testing.T) {
func TestAuth_AirGappedRoundTrip_Good(t *testing.T) {
a, m := newTestAuth()
_, err := a.Register("airgap-roundtrip", "courier-pass")
@ -832,8 +832,8 @@ func TestAirGappedRoundTrip_Good(t *testing.T) {
assert.Equal(t, session.Token, validated.Token)
}
// TestRefreshExpiredSession_Bad verifies that refreshing an already-expired session fails.
func TestRefreshExpiredSession_Bad(t *testing.T) {
// TestAuth_RefreshExpiredSession_Bad verifies that refreshing an already-expired session fails.
func TestAuth_RefreshExpiredSession_Bad(t *testing.T) {
a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond))
_, err := a.Register("expired-refresh", "pass")
@ -859,8 +859,8 @@ func TestRefreshExpiredSession_Bad(t *testing.T) {
// --- Phase 2: Password Hash Migration ---
// TestRegisterArgon2id_Good verifies that new registrations use Argon2id format.
func TestRegisterArgon2id_Good(t *testing.T) {
// TestAuth_RegisterArgon2id_Good verifies that new registrations use Argon2id format.
func TestAuth_RegisterArgon2id_Good(t *testing.T) {
a, m := newTestAuth()
user, err := a.Register("argon2-user", "strong-pass")
@ -881,8 +881,8 @@ func TestRegisterArgon2id_Good(t *testing.T) {
assert.True(t, core.HasPrefix(user.PasswordHash, "$argon2id$"))
}
// TestLoginArgon2id_Good verifies login works with Argon2id hashed password.
func TestLoginArgon2id_Good(t *testing.T) {
// TestAuth_LoginArgon2id_Good verifies login works with Argon2id hashed password.
func TestAuth_LoginArgon2id_Good(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("login-argon2", "my-password")
@ -895,8 +895,8 @@ func TestLoginArgon2id_Good(t *testing.T) {
assert.NotEmpty(t, session.Token)
}
// TestLoginArgon2id_Bad verifies wrong password fails with Argon2id hash.
func TestLoginArgon2id_Bad(t *testing.T) {
// TestAuth_LoginArgon2id_Bad verifies wrong password fails with Argon2id hash.
func TestAuth_LoginArgon2id_Bad(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("login-argon2-bad", "correct")
@ -908,9 +908,9 @@ func TestLoginArgon2id_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "invalid password")
}
// TestLegacyLTHNMigration_Good verifies that a user registered with the legacy
// TestAuth_LegacyLTHNMigration_Good verifies that a user registered with the legacy
// LTHN hash format is transparently migrated to Argon2id on successful login.
func TestLegacyLTHNMigration_Good(t *testing.T) {
func TestAuth_LegacyLTHNMigration_Good(t *testing.T) {
m := io.NewMockMedium()
a := New(m)
@ -950,8 +950,8 @@ func TestLegacyLTHNMigration_Good(t *testing.T) {
assert.NotEmpty(t, session2.Token)
}
// TestLegacyLTHNLogin_Bad verifies wrong password fails for legacy LTHN users.
func TestLegacyLTHNLogin_Bad(t *testing.T) {
// TestAuth_LegacyLTHNLogin_Bad verifies wrong password fails for legacy LTHN users.
func TestAuth_LegacyLTHNLogin_Bad(t *testing.T) {
m := io.NewMockMedium()
a := New(m)
@ -976,9 +976,9 @@ func TestLegacyLTHNLogin_Bad(t *testing.T) {
// --- Phase 2: Key Rotation ---
// TestRotateKeyPair_Good verifies the full key rotation flow:
// TestAuth_RotateKeyPair_Good verifies the full key rotation flow:
// register -> login -> rotate -> verify old key can't decrypt -> verify new key works -> sessions invalidated.
func TestRotateKeyPair_Good(t *testing.T) {
func TestAuth_RotateKeyPair_Good(t *testing.T) {
a, m := newTestAuth()
// Register and login
@ -1032,8 +1032,8 @@ func TestRotateKeyPair_Good(t *testing.T) {
assert.True(t, core.HasPrefix(meta.PasswordHash, "$argon2id$"))
}
// TestRotateKeyPair_Bad verifies that rotation fails with wrong old password.
func TestRotateKeyPair_Bad(t *testing.T) {
// TestAuth_RotateKeyPair_Bad verifies that rotation fails with wrong old password.
func TestAuth_RotateKeyPair_Bad(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("rotate-bad", "correct-pass")
@ -1046,8 +1046,8 @@ func TestRotateKeyPair_Bad(t *testing.T) {
assert.Contains(t, err.Error(), "failed to decrypt metadata")
}
// TestRotateKeyPair_Ugly verifies rotation for non-existent user.
func TestRotateKeyPair_Ugly(t *testing.T) {
// TestAuth_RotateKeyPair_Ugly verifies rotation for non-existent user.
func TestAuth_RotateKeyPair_Ugly(t *testing.T) {
a, _ := newTestAuth()
_, err := a.RotateKeyPair("nonexistent-user-id", "old", "new")
@ -1055,9 +1055,9 @@ func TestRotateKeyPair_Ugly(t *testing.T) {
assert.Contains(t, err.Error(), "user not found")
}
// TestRotateKeyPair_OldKeyCannotDecrypt_Good verifies old private key
// TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good verifies old private key
// cannot decrypt metadata after rotation.
func TestRotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) {
func TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) {
a, m := newTestAuth()
_, err := a.Register("rotate-crypto", "pass-a")
@ -1081,9 +1081,9 @@ func TestRotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) {
// --- Phase 2: Key Revocation ---
// TestRevokeKey_Good verifies the full revocation flow:
// TestAuth_RevokeKey_Good verifies the full revocation flow:
// register -> login -> revoke -> login fails -> challenge fails -> sessions invalidated.
func TestRevokeKey_Good(t *testing.T) {
func TestAuth_RevokeKey_Good(t *testing.T) {
a, m := newTestAuth()
_, err := a.Register("revoke-user", "pass")
@ -1131,8 +1131,8 @@ func TestRevokeKey_Good(t *testing.T) {
assert.Error(t, err)
}
// TestRevokeKey_Bad verifies revocation fails with wrong password.
func TestRevokeKey_Bad(t *testing.T) {
// TestAuth_RevokeKey_Bad verifies revocation fails with wrong password.
func TestAuth_RevokeKey_Bad(t *testing.T) {
a, _ := newTestAuth()
_, err := a.Register("revoke-bad", "correct")
@ -1147,8 +1147,8 @@ func TestRevokeKey_Bad(t *testing.T) {
assert.False(t, a.IsRevoked(userID))
}
// TestRevokeKey_Ugly verifies revocation for non-existent user.
func TestRevokeKey_Ugly(t *testing.T) {
// TestAuth_RevokeKey_Ugly verifies revocation for non-existent user.
func TestAuth_RevokeKey_Ugly(t *testing.T) {
a, _ := newTestAuth()
err := a.RevokeKey("nonexistent-user-id", "pass", "reason")
@ -1156,9 +1156,9 @@ func TestRevokeKey_Ugly(t *testing.T) {
assert.Contains(t, err.Error(), "user not found")
}
// TestIsRevoked_Placeholder_Good verifies that the legacy placeholder is not
// TestAuth_IsRevoked_Placeholder_Good verifies that the legacy placeholder is not
// treated as a valid revocation.
func TestIsRevoked_Placeholder_Good(t *testing.T) {
func TestAuth_IsRevoked_Placeholder_Good(t *testing.T) {
a, m := newTestAuth()
_, err := a.Register("placeholder-user", "pass")
@ -1174,16 +1174,16 @@ func TestIsRevoked_Placeholder_Good(t *testing.T) {
assert.False(t, a.IsRevoked(userID))
}
// TestIsRevoked_NoRevFile_Good verifies that a missing .rev file returns false.
func TestIsRevoked_NoRevFile_Good(t *testing.T) {
// TestAuth_IsRevoked_NoRevFile_Good verifies that a missing .rev file returns false.
func TestAuth_IsRevoked_NoRevFile_Good(t *testing.T) {
a, _ := newTestAuth()
assert.False(t, a.IsRevoked("completely-nonexistent"))
}
// TestRevokeKey_LegacyUser_Good verifies revocation works for a legacy user
// TestAuth_RevokeKey_LegacyUser_Good verifies revocation works for a legacy user
// with only a .lthn hash file (no .hash file).
func TestRevokeKey_LegacyUser_Good(t *testing.T) {
func TestAuth_RevokeKey_LegacyUser_Good(t *testing.T) {
m := io.NewMockMedium()
a := New(m)

View file

@ -17,6 +17,7 @@ package auth
// similar tamper-resistant devices.
//
// All methods must be safe for concurrent use.
// Usage: implement HardwareKey and pass it to WithHardwareKey(...) to wire hardware-backed auth into New(...).
type HardwareKey interface {
// Sign produces a cryptographic signature over the given data using the
// hardware-stored private key. The signature format depends on the
@ -44,6 +45,7 @@ type HardwareKey interface {
//
// This is a forward-looking option — integration points are documented in
// auth.go but not yet wired up.
// Usage: pass WithHardwareKey(...) into New(...) to enable a HardwareKey implementation.
func WithHardwareKey(hk HardwareKey) Option {
return func(a *Authenticator) {
a.hardwareKey = hk

View file

@ -9,9 +9,11 @@ import (
)
// ErrSessionNotFound is returned when a session token is not found.
// Usage: compare returned errors against ErrSessionNotFound when branching on failures.
var ErrSessionNotFound = coreerr.E("auth", "session not found", nil)
// SessionStore abstracts session persistence.
// Usage: use SessionStore with the other exported helpers in this package.
type SessionStore interface {
Get(token string) (*Session, error)
Set(session *Session) error
@ -21,12 +23,14 @@ type SessionStore interface {
}
// MemorySessionStore is an in-memory SessionStore backed by a map.
// Usage: use MemorySessionStore with the other exported helpers in this package.
type MemorySessionStore struct {
mu sync.RWMutex
sessions map[string]*Session
}
// NewMemorySessionStore creates a new in-memory session store.
// Usage: call NewMemorySessionStore(...) to create a ready-to-use value.
func NewMemorySessionStore() *MemorySessionStore {
return &MemorySessionStore{
sessions: make(map[string]*Session),
@ -34,6 +38,7 @@ func NewMemorySessionStore() *MemorySessionStore {
}
// Get retrieves a session by token.
// Usage: call Get(...) during the package's normal workflow.
func (m *MemorySessionStore) Get(token string) (*Session, error) {
m.mu.RLock()
defer m.mu.RUnlock()
@ -49,6 +54,7 @@ func (m *MemorySessionStore) Get(token string) (*Session, error) {
}
// Set stores a session, keyed by its token.
// Usage: call Set(...) during the package's normal workflow.
func (m *MemorySessionStore) Set(session *Session) error {
m.mu.Lock()
defer m.mu.Unlock()
@ -60,6 +66,7 @@ func (m *MemorySessionStore) Set(session *Session) error {
}
// Delete removes a session by token.
// Usage: call Delete(...) during the package's normal workflow.
func (m *MemorySessionStore) Delete(token string) error {
m.mu.Lock()
defer m.mu.Unlock()
@ -73,6 +80,7 @@ func (m *MemorySessionStore) Delete(token string) error {
}
// DeleteByUser removes all sessions belonging to the given user.
// Usage: call DeleteByUser(...) during the package's normal workflow.
func (m *MemorySessionStore) DeleteByUser(userID string) error {
m.mu.Lock()
defer m.mu.Unlock()
@ -84,6 +92,7 @@ func (m *MemorySessionStore) DeleteByUser(userID string) error {
}
// Cleanup removes all expired sessions and returns the count removed.
// Usage: call Cleanup(...) during the package's normal workflow.
func (m *MemorySessionStore) Cleanup() (int, error) {
m.mu.Lock()
defer m.mu.Unlock()

View file

@ -12,6 +12,7 @@ const sessionGroup = "sessions"
// SQLiteSessionStore is a SessionStore backed by core/store (SQLite KV).
// A mutex serialises all operations because SQLite is single-writer.
// Usage: use SQLiteSessionStore with the other exported helpers in this package.
type SQLiteSessionStore struct {
mu sync.Mutex
store *store.Store
@ -19,6 +20,7 @@ type SQLiteSessionStore struct {
// NewSQLiteSessionStore creates a new SQLite-backed session store.
// Use ":memory:" for testing or a file path for persistent storage.
// Usage: call NewSQLiteSessionStore(...) to create a ready-to-use value.
func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) {
s, err := store.New(dbPath)
if err != nil {
@ -28,6 +30,7 @@ func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) {
}
// Get retrieves a session by token from SQLite.
// Usage: call Get(...) during the package's normal workflow.
func (s *SQLiteSessionStore) Get(token string) (*Session, error) {
s.mu.Lock()
defer s.mu.Unlock()
@ -50,6 +53,7 @@ func (s *SQLiteSessionStore) Get(token string) (*Session, error) {
}
// Set stores a session in SQLite, keyed by its token.
// Usage: call Set(...) during the package's normal workflow.
func (s *SQLiteSessionStore) Set(session *Session) error {
s.mu.Lock()
defer s.mu.Unlock()
@ -63,6 +67,7 @@ func (s *SQLiteSessionStore) Set(session *Session) error {
}
// Delete removes a session by token from SQLite.
// Usage: call Delete(...) during the package's normal workflow.
func (s *SQLiteSessionStore) Delete(token string) error {
s.mu.Lock()
defer s.mu.Unlock()
@ -79,6 +84,7 @@ func (s *SQLiteSessionStore) Delete(token string) error {
}
// DeleteByUser removes all sessions belonging to the given user.
// Usage: call DeleteByUser(...) during the package's normal workflow.
func (s *SQLiteSessionStore) DeleteByUser(userID string) error {
s.mu.Lock()
defer s.mu.Unlock()
@ -104,6 +110,7 @@ func (s *SQLiteSessionStore) DeleteByUser(userID string) error {
}
// Cleanup removes all expired sessions and returns the count removed.
// Usage: call Cleanup(...) during the package's normal workflow.
func (s *SQLiteSessionStore) Cleanup() (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
@ -132,6 +139,7 @@ func (s *SQLiteSessionStore) Cleanup() (int, error) {
}
// Close closes the underlying SQLite store.
// Usage: call Close(...) during the package's normal workflow.
func (s *SQLiteSessionStore) Close() error {
s.mu.Lock()
defer s.mu.Unlock()

View file

@ -16,7 +16,7 @@ import (
// --- MemorySessionStore ---
func TestMemorySessionStore_GetSetDelete_Good(t *testing.T) {
func TestSessionStore_MemorySessionStore_GetSetDelete_Good(t *testing.T) {
store := NewMemorySessionStore()
session := &Session{
@ -44,21 +44,21 @@ func TestMemorySessionStore_GetSetDelete_Good(t *testing.T) {
assert.ErrorIs(t, err, ErrSessionNotFound)
}
func TestMemorySessionStore_GetNotFound_Bad(t *testing.T) {
func TestSessionStore_MemorySessionStore_GetNotFound_Bad(t *testing.T) {
store := NewMemorySessionStore()
_, err := store.Get("nonexistent-token")
assert.ErrorIs(t, err, ErrSessionNotFound)
}
func TestMemorySessionStore_DeleteNotFound_Bad(t *testing.T) {
func TestSessionStore_MemorySessionStore_DeleteNotFound_Bad(t *testing.T) {
store := NewMemorySessionStore()
err := store.Delete("nonexistent-token")
assert.ErrorIs(t, err, ErrSessionNotFound)
}
func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) {
func TestSessionStore_MemorySessionStore_DeleteByUser_Good(t *testing.T) {
store := NewMemorySessionStore()
// Create sessions for two users
@ -94,7 +94,7 @@ func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) {
assert.Equal(t, "user-b", got.UserID)
}
func TestMemorySessionStore_Cleanup_Good(t *testing.T) {
func TestSessionStore_MemorySessionStore_Cleanup_Good(t *testing.T) {
store := NewMemorySessionStore()
// Create expired and valid sessions
@ -134,7 +134,7 @@ func TestMemorySessionStore_Cleanup_Good(t *testing.T) {
assert.ErrorIs(t, err, ErrSessionNotFound)
}
func TestMemorySessionStore_Concurrent_Good(t *testing.T) {
func TestSessionStore_MemorySessionStore_Concurrent_Good(t *testing.T) {
store := NewMemorySessionStore()
const n = 20
@ -164,7 +164,7 @@ func TestMemorySessionStore_Concurrent_Good(t *testing.T) {
// --- SQLiteSessionStore ---
func TestSQLiteSessionStore_GetSetDelete_Good(t *testing.T) {
func TestSessionStore_SQLiteSessionStore_GetSetDelete_Good(t *testing.T) {
store, err := NewSQLiteSessionStore(":memory:")
require.NoError(t, err)
defer store.Close()
@ -194,7 +194,7 @@ func TestSQLiteSessionStore_GetSetDelete_Good(t *testing.T) {
assert.ErrorIs(t, err, ErrSessionNotFound)
}
func TestSQLiteSessionStore_GetNotFound_Bad(t *testing.T) {
func TestSessionStore_SQLiteSessionStore_GetNotFound_Bad(t *testing.T) {
store, err := NewSQLiteSessionStore(":memory:")
require.NoError(t, err)
defer store.Close()
@ -203,7 +203,7 @@ func TestSQLiteSessionStore_GetNotFound_Bad(t *testing.T) {
assert.ErrorIs(t, err, ErrSessionNotFound)
}
func TestSQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) {
func TestSessionStore_SQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) {
store, err := NewSQLiteSessionStore(":memory:")
require.NoError(t, err)
defer store.Close()
@ -212,7 +212,7 @@ func TestSQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) {
assert.ErrorIs(t, err, ErrSessionNotFound)
}
func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) {
func TestSessionStore_SQLiteSessionStore_DeleteByUser_Good(t *testing.T) {
store, err := NewSQLiteSessionStore(":memory:")
require.NoError(t, err)
defer store.Close()
@ -250,7 +250,7 @@ func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) {
assert.Equal(t, "user-b", got.UserID)
}
func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) {
func TestSessionStore_SQLiteSessionStore_Cleanup_Good(t *testing.T) {
store, err := NewSQLiteSessionStore(":memory:")
require.NoError(t, err)
defer store.Close()
@ -292,7 +292,7 @@ func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) {
assert.ErrorIs(t, err, ErrSessionNotFound)
}
func TestSQLiteSessionStore_Persistence_Good(t *testing.T) {
func TestSessionStore_SQLiteSessionStore_Persistence_Good(t *testing.T) {
dir := t.TempDir()
dbPath := core.Path(dir, "sessions.db")
@ -323,7 +323,7 @@ func TestSQLiteSessionStore_Persistence_Good(t *testing.T) {
assert.Equal(t, "persist-token", got.Token)
}
func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) {
func TestSessionStore_SQLiteSessionStore_Concurrent_Good(t *testing.T) {
// Use a temp file — :memory: SQLite has concurrency limitations
dbPath := core.Path(t.TempDir(), "concurrent.db")
store, err := NewSQLiteSessionStore(dbPath)
@ -359,7 +359,7 @@ func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) {
// --- Authenticator with SessionStore ---
func TestAuthenticator_WithSessionStore_Good(t *testing.T) {
func TestSessionStore_Authenticator_WithSessionStore_Good(t *testing.T) {
sqliteStore, err := NewSQLiteSessionStore(":memory:")
require.NoError(t, err)
defer sqliteStore.Close()
@ -398,7 +398,7 @@ func TestAuthenticator_WithSessionStore_Good(t *testing.T) {
assert.Contains(t, err.Error(), "session not found")
}
func TestAuthenticator_DefaultStore_Good(t *testing.T) {
func TestSessionStore_Authenticator_DefaultStore_Good(t *testing.T) {
m := io.NewMockMedium()
a := New(m)
@ -407,7 +407,7 @@ func TestAuthenticator_DefaultStore_Good(t *testing.T) {
assert.True(t, ok, "default store should be MemorySessionStore")
}
func TestAuthenticator_StartCleanup_Good(t *testing.T) {
func TestSessionStore_Authenticator_StartCleanup_Good(t *testing.T) {
m := io.NewMockMedium()
a := New(m, WithSessionTTL(1*time.Millisecond))
@ -436,7 +436,7 @@ func TestAuthenticator_StartCleanup_Good(t *testing.T) {
assert.Contains(t, err.Error(), "session not found")
}
func TestAuthenticator_StartCleanup_CancelStops_Good(t *testing.T) {
func TestSessionStore_Authenticator_StartCleanup_CancelStops_Good(t *testing.T) {
m := io.NewMockMedium()
a := New(m)
@ -448,7 +448,7 @@ func TestAuthenticator_StartCleanup_CancelStops_Good(t *testing.T) {
time.Sleep(50 * time.Millisecond)
}
func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) {
func TestSessionStore_SQLiteSessionStore_UpdateExisting_Good(t *testing.T) {
store, err := NewSQLiteSessionStore(":memory:")
require.NoError(t, err)
defer store.Close()
@ -476,7 +476,7 @@ func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) {
"updated session should have later expiry")
}
func TestSQLiteSessionStore_TempFile_Good(t *testing.T) {
func TestSessionStore_SQLiteSessionStore_TempFile_Good(t *testing.T) {
// Verify we can use a real temp file (not :memory:)
tmpFile := core.Path(t.TempDir(), "go-crypt-test-session-store.db")

View file

@ -7,6 +7,7 @@ func init() {
}
// AddCryptCommands registers the 'crypt' command group and all subcommands.
// Usage: call AddCryptCommands(...) during the package's normal workflow.
func AddCryptCommands(root *cli.Command) {
cryptCmd := &cli.Command{
Use: "crypt",

View file

@ -56,6 +56,7 @@ func initTestFlags() {
}
// AddTestCommands registers the 'test' command and all subcommands.
// Usage: call AddTestCommands(...) during the package's normal workflow.
func AddTestCommands(root *cli.Command) {
initTestFlags()
root.AddCommand(testCmd)

View file

@ -6,19 +6,19 @@ import (
"github.com/stretchr/testify/assert"
)
func TestShortenPackageName_Good(t *testing.T) {
func TestOutput_ShortenPackageName_Good(t *testing.T) {
assert.Equal(t, "pkg/foo", shortenPackageName("dappco.re/go/core/pkg/foo"))
assert.Equal(t, "cli-php", shortenPackageName("example.com/org/cli-php"))
assert.Equal(t, "bar", shortenPackageName("github.com/other/bar"))
}
func TestFormatCoverage_Good(t *testing.T) {
func TestOutput_FormatCoverage_Good(t *testing.T) {
assert.Contains(t, formatCoverage(85.0), "85.0%")
assert.Contains(t, formatCoverage(65.0), "65.0%")
assert.Contains(t, formatCoverage(25.0), "25.0%")
}
func TestParseTestOutput_Good(t *testing.T) {
func TestOutput_ParseTestOutput_Good(t *testing.T) {
output := `ok dappco.re/go/core/pkg/foo 0.100s coverage: 50.0% of statements
FAIL dappco.re/go/core/pkg/bar
? dappco.re/go/core/pkg/baz [no test files]
@ -33,7 +33,7 @@ FAIL dappco.re/go/core/pkg/bar
assert.Equal(t, 50.0, results.packages[0].coverage)
}
func TestPrintCoverageSummary_Good_LongPackageNames(t *testing.T) {
func TestOutput_PrintCoverageSummary_Good_LongPackageNames(t *testing.T) {
// This tests the bug fix for long package names causing negative Repeat count
results := testResults{
packages: []packageCoverage{

View file

@ -11,6 +11,7 @@ import (
)
// Encrypt encrypts data using ChaCha20-Poly1305.
// Usage: call Encrypt(...) during the package's normal workflow.
func Encrypt(plaintext []byte, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
@ -26,6 +27,7 @@ func Encrypt(plaintext []byte, key []byte) ([]byte, error) {
}
// Decrypt decrypts data using ChaCha20-Poly1305.
// Usage: call Decrypt(...) during the package's normal workflow.
func Decrypt(ciphertext []byte, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {

View file

@ -15,7 +15,7 @@ func (r *mockReader) Read(p []byte) (n int, err error) {
return 0, core.NewError("read error")
}
func TestEncryptDecrypt_Good(t *testing.T) {
func TestChachapoly_EncryptDecrypt_Good(t *testing.T) {
key := make([]byte, 32)
for i := range key {
key[i] = 1
@ -31,14 +31,14 @@ func TestEncryptDecrypt_Good(t *testing.T) {
assert.Equal(t, plaintext, decrypted)
}
func TestEncrypt_Bad_InvalidKeySize(t *testing.T) {
func TestChachapoly_Encrypt_Bad_InvalidKeySize(t *testing.T) {
key := make([]byte, 16) // Wrong size
plaintext := []byte("test")
_, err := Encrypt(plaintext, key)
assert.Error(t, err)
}
func TestDecrypt_Bad_WrongKey(t *testing.T) {
func TestChachapoly_Decrypt_Bad_WrongKey(t *testing.T) {
key1 := make([]byte, 32)
key2 := make([]byte, 32)
key2[0] = 1 // Different key
@ -51,7 +51,7 @@ func TestDecrypt_Bad_WrongKey(t *testing.T) {
assert.Error(t, err) // Should fail authentication
}
func TestDecrypt_Bad_TamperedCiphertext(t *testing.T) {
func TestChachapoly_Decrypt_Bad_TamperedCiphertext(t *testing.T) {
key := make([]byte, 32)
plaintext := []byte("secret")
ciphertext, err := Encrypt(plaintext, key)
@ -64,7 +64,7 @@ func TestDecrypt_Bad_TamperedCiphertext(t *testing.T) {
assert.Error(t, err)
}
func TestEncrypt_Good_EmptyPlaintext(t *testing.T) {
func TestChachapoly_Encrypt_Good_EmptyPlaintext(t *testing.T) {
key := make([]byte, 32)
plaintext := []byte("")
ciphertext, err := Encrypt(plaintext, key)
@ -76,7 +76,7 @@ func TestEncrypt_Good_EmptyPlaintext(t *testing.T) {
assert.Equal(t, plaintext, decrypted)
}
func TestDecrypt_Bad_ShortCiphertext(t *testing.T) {
func TestChachapoly_Decrypt_Bad_ShortCiphertext(t *testing.T) {
key := make([]byte, 32)
shortCiphertext := []byte("short")
@ -85,7 +85,7 @@ func TestDecrypt_Bad_ShortCiphertext(t *testing.T) {
assert.Contains(t, err.Error(), "too short")
}
func TestCiphertextDiffersFromPlaintext_Good(t *testing.T) {
func TestChachapoly_CiphertextDiffersFromPlaintext_Good(t *testing.T) {
key := make([]byte, 32)
plaintext := []byte("Hello, world!")
ciphertext, err := Encrypt(plaintext, key)
@ -93,7 +93,7 @@ func TestCiphertextDiffersFromPlaintext_Good(t *testing.T) {
assert.NotEqual(t, plaintext, ciphertext)
}
func TestEncrypt_Bad_NonceError(t *testing.T) {
func TestChachapoly_Encrypt_Bad_NonceError(t *testing.T) {
key := make([]byte, 32)
plaintext := []byte("test")
@ -106,7 +106,7 @@ func TestEncrypt_Bad_NonceError(t *testing.T) {
assert.Error(t, err)
}
func TestDecrypt_Bad_InvalidKeySize(t *testing.T) {
func TestChachapoly_Decrypt_Bad_InvalidKeySize(t *testing.T) {
key := make([]byte, 16) // Wrong size
ciphertext := []byte("test")
_, err := Decrypt(ciphertext, key)

View file

@ -11,6 +11,7 @@ import (
)
// SHA256File computes the SHA-256 checksum of a file and returns it as a hex string.
// Usage: call SHA256File(...) during the package's normal workflow.
func SHA256File(path string) (string, error) {
openResult := (&core.Fs{}).New("/").Open(path)
if !openResult.OK {
@ -29,6 +30,7 @@ func SHA256File(path string) (string, error) {
}
// SHA512File computes the SHA-512 checksum of a file and returns it as a hex string.
// Usage: call SHA512File(...) during the package's normal workflow.
func SHA512File(path string) (string, error) {
openResult := (&core.Fs{}).New("/").Open(path)
if !openResult.OK {
@ -47,12 +49,14 @@ func SHA512File(path string) (string, error) {
}
// SHA256Sum computes the SHA-256 checksum of data and returns it as a hex string.
// Usage: call SHA256Sum(...) during the package's normal workflow.
func SHA256Sum(data []byte) string {
h := sha256.Sum256(data)
return hex.EncodeToString(h[:])
}
// SHA512Sum computes the SHA-512 checksum of data and returns it as a hex string.
// Usage: call SHA512Sum(...) during the package's normal workflow.
func SHA512Sum(data []byte) string {
h := sha512.Sum512(data)
return hex.EncodeToString(h[:])

View file

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestSHA256Sum_Good(t *testing.T) {
func TestChecksum_SHA256Sum_Good(t *testing.T) {
data := []byte("hello")
expected := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
@ -16,7 +16,7 @@ func TestSHA256Sum_Good(t *testing.T) {
assert.Equal(t, expected, result)
}
func TestSHA512Sum_Good(t *testing.T) {
func TestChecksum_SHA512Sum_Good(t *testing.T) {
data := []byte("hello")
expected := "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
@ -26,8 +26,8 @@ func TestSHA512Sum_Good(t *testing.T) {
// --- Phase 0 Additions ---
// TestSHA256FileEmpty_Good verifies checksum of an empty file.
func TestSHA256FileEmpty_Good(t *testing.T) {
// TestChecksum_SHA256FileEmpty_Good verifies checksum of an empty file.
func TestChecksum_SHA256FileEmpty_Good(t *testing.T) {
tmpDir := t.TempDir()
emptyFile := core.Path(tmpDir, "empty.bin")
writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644)
@ -39,8 +39,8 @@ func TestSHA256FileEmpty_Good(t *testing.T) {
assert.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hash)
}
// TestSHA512FileEmpty_Good verifies SHA-512 checksum of an empty file.
func TestSHA512FileEmpty_Good(t *testing.T) {
// TestChecksum_SHA512FileEmpty_Good verifies SHA-512 checksum of an empty file.
func TestChecksum_SHA512FileEmpty_Good(t *testing.T) {
tmpDir := t.TempDir()
emptyFile := core.Path(tmpDir, "empty.bin")
writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644)
@ -51,22 +51,22 @@ func TestSHA512FileEmpty_Good(t *testing.T) {
assert.Equal(t, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", hash)
}
// TestSHA256FileNonExistent_Bad verifies error on non-existent file.
func TestSHA256FileNonExistent_Bad(t *testing.T) {
// TestChecksum_SHA256FileNonExistent_Bad verifies error on non-existent file.
func TestChecksum_SHA256FileNonExistent_Bad(t *testing.T) {
_, err := SHA256File("/nonexistent/path/to/file.bin")
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to open file")
}
// TestSHA512FileNonExistent_Bad verifies error on non-existent file.
func TestSHA512FileNonExistent_Bad(t *testing.T) {
// TestChecksum_SHA512FileNonExistent_Bad verifies error on non-existent file.
func TestChecksum_SHA512FileNonExistent_Bad(t *testing.T) {
_, err := SHA512File("/nonexistent/path/to/file.bin")
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to open file")
}
// TestSHA256FileWithContent_Good verifies checksum of a file with known content.
func TestSHA256FileWithContent_Good(t *testing.T) {
// TestChecksum_SHA256FileWithContent_Good verifies checksum of a file with known content.
func TestChecksum_SHA256FileWithContent_Good(t *testing.T) {
tmpDir := t.TempDir()
testFile := core.Path(tmpDir, "test.txt")
writeResult := (&core.Fs{}).New("/").WriteMode(testFile, "hello", 0o644)

View file

@ -7,6 +7,7 @@ import (
// Encrypt encrypts data with a passphrase using ChaCha20-Poly1305.
// A random salt is generated and prepended to the output.
// Format: salt (16 bytes) + nonce (24 bytes) + ciphertext.
// Usage: call Encrypt(...) during the package's normal workflow.
func Encrypt(plaintext, passphrase []byte) ([]byte, error) {
salt, err := generateSalt(argon2SaltLen)
if err != nil {
@ -29,6 +30,7 @@ func Encrypt(plaintext, passphrase []byte) ([]byte, error) {
// Decrypt decrypts data encrypted with Encrypt.
// Expects format: salt (16 bytes) + nonce (24 bytes) + ciphertext.
// Usage: call Decrypt(...) during the package's normal workflow.
func Decrypt(ciphertext, passphrase []byte) ([]byte, error) {
if len(ciphertext) < argon2SaltLen {
return nil, coreerr.E("crypt.Decrypt", "ciphertext too short", nil)
@ -50,6 +52,7 @@ func Decrypt(ciphertext, passphrase []byte) ([]byte, error) {
// EncryptAES encrypts data using AES-256-GCM with a passphrase.
// A random salt is generated and prepended to the output.
// Format: salt (16 bytes) + nonce (12 bytes) + ciphertext.
// Usage: call EncryptAES(...) during the package's normal workflow.
func EncryptAES(plaintext, passphrase []byte) ([]byte, error) {
salt, err := generateSalt(argon2SaltLen)
if err != nil {
@ -71,6 +74,7 @@ func EncryptAES(plaintext, passphrase []byte) ([]byte, error) {
// DecryptAES decrypts data encrypted with EncryptAES.
// Expects format: salt (16 bytes) + nonce (12 bytes) + ciphertext.
// Usage: call DecryptAES(...) during the package's normal workflow.
func DecryptAES(ciphertext, passphrase []byte) ([]byte, error) {
if len(ciphertext) < argon2SaltLen {
return nil, coreerr.E("crypt.DecryptAES", "ciphertext too short", nil)

View file

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestEncryptDecrypt_Good(t *testing.T) {
func TestCrypt_EncryptDecrypt_Good(t *testing.T) {
plaintext := []byte("hello, world!")
passphrase := []byte("correct-horse-battery-staple")
@ -21,7 +21,7 @@ func TestEncryptDecrypt_Good(t *testing.T) {
assert.Equal(t, plaintext, decrypted)
}
func TestEncryptDecrypt_Bad(t *testing.T) {
func TestCrypt_EncryptDecrypt_Bad(t *testing.T) {
plaintext := []byte("secret data")
passphrase := []byte("correct-passphrase")
wrongPassphrase := []byte("wrong-passphrase")
@ -33,7 +33,7 @@ func TestEncryptDecrypt_Bad(t *testing.T) {
assert.Error(t, err)
}
func TestEncryptDecryptAES_Good(t *testing.T) {
func TestCrypt_EncryptDecryptAES_Good(t *testing.T) {
plaintext := []byte("hello, AES world!")
passphrase := []byte("my-secure-passphrase")
@ -48,8 +48,8 @@ func TestEncryptDecryptAES_Good(t *testing.T) {
// --- Phase 0 Additions ---
// TestWrongPassphraseDecrypt_Bad verifies wrong passphrase returns error, not corrupt data.
func TestWrongPassphraseDecrypt_Bad(t *testing.T) {
// TestCrypt_WrongPassphraseDecrypt_Bad verifies wrong passphrase returns error, not corrupt data.
func TestCrypt_WrongPassphraseDecrypt_Bad(t *testing.T) {
plaintext := []byte("sensitive payload")
passphrase := []byte("correct-passphrase")
wrongPassphrase := []byte("wrong-passphrase")
@ -70,8 +70,8 @@ func TestWrongPassphraseDecrypt_Bad(t *testing.T) {
assert.Nil(t, decryptedAES, "wrong passphrase must not return partial data (AES)")
}
// TestEmptyPlaintextRoundTrip_Good verifies encrypt/decrypt of empty plaintext.
func TestEmptyPlaintextRoundTrip_Good(t *testing.T) {
// TestCrypt_EmptyPlaintextRoundTrip_Good verifies encrypt/decrypt of empty plaintext.
func TestCrypt_EmptyPlaintextRoundTrip_Good(t *testing.T) {
passphrase := []byte("test-passphrase")
// ChaCha20
@ -93,8 +93,8 @@ func TestEmptyPlaintextRoundTrip_Good(t *testing.T) {
assert.Empty(t, decryptedAES)
}
// TestLargePlaintextRoundTrip_Good verifies encrypt/decrypt of a 1MB payload.
func TestLargePlaintextRoundTrip_Good(t *testing.T) {
// TestCrypt_LargePlaintextRoundTrip_Good verifies encrypt/decrypt of a 1MB payload.
func TestCrypt_LargePlaintextRoundTrip_Good(t *testing.T) {
passphrase := []byte("large-payload-passphrase")
plaintext := bytes.Repeat([]byte("X"), 1024*1024) // 1MB
@ -116,8 +116,8 @@ func TestLargePlaintextRoundTrip_Good(t *testing.T) {
assert.Equal(t, plaintext, decryptedAES)
}
// TestDecryptCiphertextTooShort_Ugly verifies short ciphertext is rejected.
func TestDecryptCiphertextTooShort_Ugly(t *testing.T) {
// TestCrypt_DecryptCiphertextTooShort_Ugly verifies short ciphertext is rejected.
func TestCrypt_DecryptCiphertextTooShort_Ugly(t *testing.T) {
_, err := Decrypt([]byte("short"), []byte("pass"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "too short")

View file

@ -14,6 +14,7 @@ import (
// HashPassword hashes a password using Argon2id with default parameters.
// Returns a string in the format: $argon2id$v=19$m=65536,t=3,p=4$<base64salt>$<base64hash>
// Usage: call HashPassword(...) during the package's normal workflow.
func HashPassword(password string) (string, error) {
salt, err := generateSalt(argon2SaltLen)
if err != nil {
@ -34,6 +35,7 @@ func HashPassword(password string) (string, error) {
// VerifyPassword verifies a password against an Argon2id hash string.
// The hash must be in the format produced by HashPassword.
// Usage: call VerifyPassword(...) during the package's normal workflow.
func VerifyPassword(password, hash string) (bool, error) {
parts := core.Split(hash, "$")
if len(parts) != 6 {
@ -116,6 +118,7 @@ func parsePrefixedUint32(input, prefix string) (uint32, error) {
// HashBcrypt hashes a password using bcrypt with the given cost.
// Cost must be between bcrypt.MinCost and bcrypt.MaxCost.
// Usage: call HashBcrypt(...) during the package's normal workflow.
func HashBcrypt(password string, cost int) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), cost)
if err != nil {
@ -125,6 +128,7 @@ func HashBcrypt(password string, cost int) (string, error) {
}
// VerifyBcrypt verifies a password against a bcrypt hash.
// Usage: call VerifyBcrypt(...) during the package's normal workflow.
func VerifyBcrypt(password, hash string) (bool, error) {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
if err == bcrypt.ErrMismatchedHashAndPassword {

View file

@ -7,7 +7,7 @@ import (
"golang.org/x/crypto/bcrypt"
)
func TestHashPassword_Good(t *testing.T) {
func TestHash_HashPassword_Good(t *testing.T) {
password := "my-secure-password"
hash, err := HashPassword(password)
@ -20,7 +20,7 @@ func TestHashPassword_Good(t *testing.T) {
assert.True(t, match)
}
func TestVerifyPassword_Bad(t *testing.T) {
func TestHash_VerifyPassword_Bad(t *testing.T) {
password := "my-secure-password"
wrongPassword := "wrong-password"
@ -32,7 +32,7 @@ func TestVerifyPassword_Bad(t *testing.T) {
assert.False(t, match)
}
func TestHashBcrypt_Good(t *testing.T) {
func TestHash_HashBcrypt_Good(t *testing.T) {
password := "bcrypt-test-password"
hash, err := HashBcrypt(password, bcrypt.DefaultCost)

View file

@ -8,6 +8,7 @@ import (
)
// HMACSHA256 computes the HMAC-SHA256 of a message using the given key.
// Usage: call HMACSHA256(...) during the package's normal workflow.
func HMACSHA256(message, key []byte) []byte {
mac := hmac.New(sha256.New, key)
mac.Write(message)
@ -15,6 +16,7 @@ func HMACSHA256(message, key []byte) []byte {
}
// HMACSHA512 computes the HMAC-SHA512 of a message using the given key.
// Usage: call HMACSHA512(...) during the package's normal workflow.
func HMACSHA512(message, key []byte) []byte {
mac := hmac.New(sha512.New, key)
mac.Write(message)
@ -23,6 +25,7 @@ func HMACSHA512(message, key []byte) []byte {
// VerifyHMAC verifies an HMAC using constant-time comparison.
// hashFunc should be sha256.New, sha512.New, etc.
// Usage: call VerifyHMAC(...) during the package's normal workflow.
func VerifyHMAC(message, key, mac []byte, hashFunc func() hash.Hash) bool {
expected := hmac.New(hashFunc, key)
expected.Write(message)

View file

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestHMACSHA256_Good(t *testing.T) {
func TestHMAC_HMACSHA256_Good(t *testing.T) {
// RFC 4231 Test Case 2
key := []byte("Jefe")
message := []byte("what do ya want for nothing?")
@ -18,7 +18,7 @@ func TestHMACSHA256_Good(t *testing.T) {
assert.Equal(t, expected, hex.EncodeToString(mac))
}
func TestVerifyHMAC_Good(t *testing.T) {
func TestHMAC_VerifyHMAC_Good(t *testing.T) {
key := []byte("secret-key")
message := []byte("test message")
@ -28,7 +28,7 @@ func TestVerifyHMAC_Good(t *testing.T) {
assert.True(t, valid)
}
func TestVerifyHMAC_Bad(t *testing.T) {
func TestHMAC_VerifyHMAC_Bad(t *testing.T) {
key := []byte("secret-key")
message := []byte("test message")
tampered := []byte("tampered message")

View file

@ -25,12 +25,14 @@ const (
// DeriveKey derives a key from a passphrase using Argon2id with default parameters.
// The salt must be argon2SaltLen bytes. keyLen specifies the desired key length.
// Usage: call DeriveKey(...) during the package's normal workflow.
func DeriveKey(passphrase, salt []byte, keyLen uint32) []byte {
return argon2.IDKey(passphrase, salt, argon2Time, argon2Memory, argon2Parallelism, keyLen)
}
// DeriveKeyScrypt derives a key from a passphrase using scrypt.
// Uses recommended parameters: N=32768, r=8, p=1.
// Usage: call DeriveKeyScrypt(...) during the package's normal workflow.
func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) {
key, err := scrypt.Key(passphrase, salt, 32768, 8, 1, keyLen)
if err != nil {
@ -42,6 +44,7 @@ func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) {
// HKDF derives a key using HKDF-SHA256.
// secret is the input keying material, salt is optional (can be nil),
// info is optional context, and keyLen is the desired output length.
// Usage: call HKDF(...) during the package's normal workflow.
func HKDF(secret, salt, info []byte, keyLen int) ([]byte, error) {
reader := hkdf.New(sha256.New, secret, salt, info)
key := make([]byte, keyLen)

View file

@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestDeriveKey_Good(t *testing.T) {
func TestKDF_DeriveKey_Good(t *testing.T) {
passphrase := []byte("test-passphrase")
salt := []byte("1234567890123456") // 16 bytes
@ -21,7 +21,7 @@ func TestDeriveKey_Good(t *testing.T) {
assert.NotEqual(t, key1, key3)
}
func TestDeriveKeyScrypt_Good(t *testing.T) {
func TestKDF_DeriveKeyScrypt_Good(t *testing.T) {
passphrase := []byte("test-passphrase")
salt := []byte("1234567890123456")
@ -35,7 +35,7 @@ func TestDeriveKeyScrypt_Good(t *testing.T) {
assert.Equal(t, key, key2)
}
func TestHKDF_Good(t *testing.T) {
func TestKDF_HKDF_Good(t *testing.T) {
secret := []byte("input-keying-material")
salt := []byte("optional-salt")
info := []byte("context-info")
@ -57,8 +57,8 @@ func TestHKDF_Good(t *testing.T) {
// --- Phase 0 Additions ---
// TestKeyDerivationDeterminism_Good verifies same passphrase + salt always yields same key.
func TestKeyDerivationDeterminism_Good(t *testing.T) {
// TestKDF_KeyDerivationDeterminism_Good verifies same passphrase + salt always yields same key.
func TestKDF_KeyDerivationDeterminism_Good(t *testing.T) {
passphrase := []byte("determinism-test-passphrase")
salt := []byte("1234567890123456") // 16 bytes
@ -82,8 +82,8 @@ func TestKeyDerivationDeterminism_Good(t *testing.T) {
assert.Equal(t, scryptKey1, scryptKey2, "scrypt must also be deterministic")
}
// TestHKDFDifferentInfoStrings_Good verifies different info strings produce different keys.
func TestHKDFDifferentInfoStrings_Good(t *testing.T) {
// TestKDF_HKDFDifferentInfoStrings_Good verifies different info strings produce different keys.
func TestKDF_HKDFDifferentInfoStrings_Good(t *testing.T) {
secret := []byte("shared-secret-material")
salt := []byte("common-salt")
@ -114,8 +114,8 @@ func TestHKDFDifferentInfoStrings_Good(t *testing.T) {
}
}
// TestHKDFNilSalt_Good verifies HKDF works with nil salt.
func TestHKDFNilSalt_Good(t *testing.T) {
// TestKDF_HKDFNilSalt_Good verifies HKDF works with nil salt.
func TestKDF_HKDFNilSalt_Good(t *testing.T) {
secret := []byte("input-keying-material")
info := []byte("context")

View file

@ -42,11 +42,13 @@ var keyMap = map[rune]rune{
// SetKeyMap replaces the default character substitution map.
// Use this to customize the quasi-salt derivation for specific applications.
// Changes affect all subsequent Hash and Verify calls.
// Usage: call SetKeyMap(...) during the package's normal workflow.
func SetKeyMap(newKeyMap map[rune]rune) {
keyMap = newKeyMap
}
// GetKeyMap returns the current character substitution map.
// Usage: call GetKeyMap(...) during the package's normal workflow.
func GetKeyMap() map[rune]rune {
return keyMap
}
@ -61,6 +63,7 @@ func GetKeyMap() map[rune]rune {
//
// The same input always produces the same hash, enabling verification
// without storing a separate salt value.
// Usage: call Hash(...) when you need a deterministic content-style digest rather than a password hash.
func Hash(input string) string {
salt := createSalt(input)
hash := sha256.Sum256([]byte(input + salt))
@ -89,6 +92,7 @@ func createSalt(input string) string {
// Verify checks if an input string produces the given hash.
// Returns true if Hash(input) equals the provided hash value.
// Uses constant-time comparison to prevent timing attacks.
// Usage: call Verify(...) during the package's normal workflow.
func Verify(input string, hash string) bool {
computed := Hash(input)
return subtle.ConstantTimeCompare([]byte(computed), []byte(hash)) == 1

View file

@ -7,36 +7,36 @@ import (
"github.com/stretchr/testify/assert"
)
func TestHash_Good(t *testing.T) {
func TestLTHN_Hash_Good(t *testing.T) {
hash := Hash("hello")
assert.NotEmpty(t, hash)
}
func TestVerify_Good(t *testing.T) {
func TestLTHN_Verify_Good(t *testing.T) {
hash := Hash("hello")
assert.True(t, Verify("hello", hash))
}
func TestVerify_Bad(t *testing.T) {
func TestLTHN_Verify_Bad(t *testing.T) {
hash := Hash("hello")
assert.False(t, Verify("world", hash))
}
func TestCreateSalt_Good(t *testing.T) {
func TestLTHN_CreateSalt_Good(t *testing.T) {
// "hello" reversed: "olleh" -> "0113h"
expected := "0113h"
actual := createSalt("hello")
assert.Equal(t, expected, actual, "Salt should be correctly created for 'hello'")
}
func TestCreateSalt_Bad(t *testing.T) {
func TestLTHN_CreateSalt_Bad(t *testing.T) {
// Test with an empty string
expected := ""
actual := createSalt("")
assert.Equal(t, expected, actual, "Salt for an empty string should be empty")
}
func TestCreateSalt_Ugly(t *testing.T) {
func TestLTHN_CreateSalt_Ugly(t *testing.T) {
// Test with characters not in the keyMap
input := "world123"
// "world123" reversed: "321dlrow" -> "e2ld1r0w"
@ -54,7 +54,7 @@ func TestCreateSalt_Ugly(t *testing.T) {
var testKeyMapMu sync.Mutex
func TestSetKeyMap_Good(t *testing.T) {
func TestLTHN_SetKeyMap_Good(t *testing.T) {
testKeyMapMu.Lock()
originalKeyMap := GetKeyMap()
t.Cleanup(func() {

View file

@ -14,17 +14,20 @@ import (
)
// Service provides OpenPGP cryptographic operations.
// Usage: use Service with the other exported helpers in this package.
type Service struct {
core *framework.Core
}
// New creates a new OpenPGP service instance.
// Usage: call New(...) to create a ready-to-use value.
func New(c *framework.Core) (any, error) {
return &Service{core: c}, nil
}
// CreateKeyPair generates a new RSA-4096 PGP keypair.
// Returns the armored private key string.
// Usage: call CreateKeyPair(...) during the package's normal workflow.
func (s *Service) CreateKeyPair(name, passphrase string) (string, error) {
config := &packet.Config{
Algorithm: packet.PubKeyAlgoRSA,
@ -100,6 +103,7 @@ func serializeEntity(w goio.Writer, e *openpgp.Entity) error {
// EncryptPGP encrypts data for a recipient identified by their public key (armored string in recipientPath).
// The encrypted data is written to the provided writer and also returned as an armored string.
// Usage: call EncryptPGP(...) during the package's normal workflow.
func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) {
entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(recipientPath))
if err != nil {
@ -135,6 +139,7 @@ func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opt
}
// DecryptPGP decrypts a PGP message using the provided armored private key and passphrase.
// Usage: call DecryptPGP(...) during the package's normal workflow.
func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) {
entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(privateKey))
if err != nil {
@ -173,6 +178,7 @@ func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any
}
// HandleIPCEvents handles PGP-related IPC messages.
// Usage: call HandleIPCEvents(...) during the package's normal workflow.
func (s *Service) HandleIPCEvents(c *framework.Core, msg framework.Message) error {
switch m := msg.(type) {
case map[string]any:

View file

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestCreateKeyPair_Good(t *testing.T) {
func TestService_CreateKeyPair_Good(t *testing.T) {
c := framework.New()
s := &Service{core: c}
@ -19,7 +19,7 @@ func TestCreateKeyPair_Good(t *testing.T) {
assert.Contains(t, privKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----")
}
func TestEncryptDecrypt_Good(t *testing.T) {
func TestService_EncryptDecrypt_Good(t *testing.T) {
c := framework.New()
s := &Service{core: c}

View file

@ -16,6 +16,7 @@ import (
)
// KeyPair holds armored PGP public and private keys.
// Usage: use KeyPair with the other exported helpers in this package.
type KeyPair struct {
PublicKey string
PrivateKey string
@ -24,6 +25,7 @@ type KeyPair struct {
// CreateKeyPair generates a new PGP key pair for the given identity.
// If password is non-empty, the private key is encrypted with it.
// Returns a KeyPair with armored public and private keys.
// Usage: call CreateKeyPair(...) during the package's normal workflow.
func CreateKeyPair(name, email, password string) (*KeyPair, error) {
const op = "pgp.CreateKeyPair"
@ -116,6 +118,7 @@ func serializeEncryptedEntity(w io.Writer, e *openpgp.Entity) error {
// Encrypt encrypts data for the recipient identified by their armored public key.
// Returns the encrypted data as armored PGP output.
// Usage: call Encrypt(...) during the package's normal workflow.
func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) {
const op = "pgp.Encrypt"
@ -149,6 +152,7 @@ func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) {
// Decrypt decrypts armored PGP data using the given armored private key.
// If the private key is encrypted, the password is used to decrypt it first.
// Usage: call Decrypt(...) during the package's normal workflow.
func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) {
const op = "pgp.Decrypt"
@ -193,6 +197,7 @@ func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) {
// Sign creates an armored detached signature for the given data using
// the armored private key. If the key is encrypted, the password is used
// to decrypt it first.
// Usage: call Sign(...) during the package's normal workflow.
func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) {
const op = "pgp.Sign"
@ -224,6 +229,7 @@ func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) {
// Verify verifies an armored detached signature against the given data
// and armored public key. Returns nil if the signature is valid.
// Usage: call Verify(...) during the package's normal workflow.
func Verify(data, signature []byte, publicKeyArmor string) error {
const op = "pgp.Verify"

View file

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestCreateKeyPair_Good(t *testing.T) {
func TestPGP_CreateKeyPair_Good(t *testing.T) {
kp, err := CreateKeyPair("Test User", "test@example.com", "")
require.NoError(t, err)
require.NotNil(t, kp)
@ -15,7 +15,7 @@ func TestCreateKeyPair_Good(t *testing.T) {
assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----")
}
func TestCreateKeyPair_Bad(t *testing.T) {
func TestPGP_CreateKeyPair_Bad(t *testing.T) {
// Empty name still works (openpgp allows it), but test with password
kp, err := CreateKeyPair("Secure User", "secure@example.com", "strong-password")
require.NoError(t, err)
@ -24,14 +24,14 @@ func TestCreateKeyPair_Bad(t *testing.T) {
assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----")
}
func TestCreateKeyPair_Ugly(t *testing.T) {
func TestPGP_CreateKeyPair_Ugly(t *testing.T) {
// Minimal identity
kp, err := CreateKeyPair("", "", "")
require.NoError(t, err)
require.NotNil(t, kp)
}
func TestEncryptDecrypt_Good(t *testing.T) {
func TestPGP_EncryptDecrypt_Good(t *testing.T) {
kp, err := CreateKeyPair("Test User", "test@example.com", "")
require.NoError(t, err)
@ -46,7 +46,7 @@ func TestEncryptDecrypt_Good(t *testing.T) {
assert.Equal(t, plaintext, decrypted)
}
func TestEncryptDecrypt_Bad(t *testing.T) {
func TestPGP_EncryptDecrypt_Bad(t *testing.T) {
kp1, err := CreateKeyPair("User One", "one@example.com", "")
require.NoError(t, err)
kp2, err := CreateKeyPair("User Two", "two@example.com", "")
@ -61,7 +61,7 @@ func TestEncryptDecrypt_Bad(t *testing.T) {
assert.Error(t, err)
}
func TestEncryptDecrypt_Ugly(t *testing.T) {
func TestPGP_EncryptDecrypt_Ugly(t *testing.T) {
// Invalid public key for encryption
_, err := Encrypt([]byte("data"), "not-a-pgp-key")
assert.Error(t, err)
@ -71,7 +71,7 @@ func TestEncryptDecrypt_Ugly(t *testing.T) {
assert.Error(t, err)
}
func TestEncryptDecryptWithPassword_Good(t *testing.T) {
func TestPGP_EncryptDecryptWithPassword_Good(t *testing.T) {
password := "my-secret-passphrase"
kp, err := CreateKeyPair("Secure User", "secure@example.com", password)
require.NoError(t, err)
@ -85,7 +85,7 @@ func TestEncryptDecryptWithPassword_Good(t *testing.T) {
assert.Equal(t, plaintext, decrypted)
}
func TestSignVerify_Good(t *testing.T) {
func TestPGP_SignVerify_Good(t *testing.T) {
kp, err := CreateKeyPair("Signer", "signer@example.com", "")
require.NoError(t, err)
@ -99,7 +99,7 @@ func TestSignVerify_Good(t *testing.T) {
assert.NoError(t, err)
}
func TestSignVerify_Bad(t *testing.T) {
func TestPGP_SignVerify_Bad(t *testing.T) {
kp, err := CreateKeyPair("Signer", "signer@example.com", "")
require.NoError(t, err)
@ -112,7 +112,7 @@ func TestSignVerify_Bad(t *testing.T) {
assert.Error(t, err)
}
func TestSignVerify_Ugly(t *testing.T) {
func TestPGP_SignVerify_Ugly(t *testing.T) {
// Invalid key for signing
_, err := Sign([]byte("data"), "not-a-key", "")
assert.Error(t, err)
@ -129,7 +129,7 @@ func TestSignVerify_Ugly(t *testing.T) {
assert.Error(t, err)
}
func TestSignVerifyWithPassword_Good(t *testing.T) {
func TestPGP_SignVerifyWithPassword_Good(t *testing.T) {
password := "signing-password"
kp, err := CreateKeyPair("Signer", "signer@example.com", password)
require.NoError(t, err)
@ -142,7 +142,7 @@ func TestSignVerifyWithPassword_Good(t *testing.T) {
assert.NoError(t, err)
}
func TestFullRoundTrip_Good(t *testing.T) {
func TestPGP_FullRoundTrip_Good(t *testing.T) {
// Generate keys, encrypt, decrypt, sign, and verify - full round trip
kp, err := CreateKeyPair("Full Test", "full@example.com", "")
require.NoError(t, err)

View file

@ -12,14 +12,17 @@ import (
)
// Service provides RSA functionality.
// Usage: use Service with the other exported helpers in this package.
type Service struct{}
// NewService creates and returns a new Service instance for performing RSA-related operations.
// Usage: call NewService(...) to create a ready-to-use value.
func NewService() *Service {
return &Service{}
}
// GenerateKeyPair creates a new RSA key pair.
// Usage: call GenerateKeyPair(...) during the package's normal workflow.
func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err error) {
const op = "rsa.GenerateKeyPair"
@ -50,6 +53,7 @@ func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err e
}
// Encrypt encrypts data with a public key.
// Usage: call Encrypt(...) during the package's normal workflow.
func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) {
const op = "rsa.Encrypt"
@ -77,6 +81,7 @@ func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) {
}
// Decrypt decrypts data with a private key.
// Usage: call Decrypt(...) during the package's normal workflow.
func (s *Service) Decrypt(privateKey, ciphertext, label []byte) ([]byte, error) {
const op = "rsa.Decrypt"

View file

@ -19,7 +19,7 @@ func (r *mockReader) Read(p []byte) (n int, err error) {
return 0, core.NewError("read error")
}
func TestRSA_Good(t *testing.T) {
func TestRSA_RSA_Good(t *testing.T) {
s := NewService()
// Generate a new key pair
@ -37,7 +37,7 @@ func TestRSA_Good(t *testing.T) {
assert.Equal(t, message, plaintext)
}
func TestRSA_Bad(t *testing.T) {
func TestRSA_RSA_Bad(t *testing.T) {
s := NewService()
// Decrypt with wrong key
@ -56,7 +56,7 @@ func TestRSA_Bad(t *testing.T) {
assert.Error(t, err)
}
func TestRSA_Ugly(t *testing.T) {
func TestRSA_RSA_Ugly(t *testing.T) {
s := NewService()
// Malformed keys and messages

View file

@ -13,6 +13,7 @@ import (
// ChaCha20Encrypt encrypts plaintext using ChaCha20-Poly1305.
// The key must be 32 bytes. The nonce is randomly generated and prepended
// to the ciphertext.
// Usage: call ChaCha20Encrypt(...) during the package's normal workflow.
func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
@ -30,6 +31,7 @@ func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) {
// ChaCha20Decrypt decrypts ciphertext encrypted with ChaCha20Encrypt.
// The key must be 32 bytes. Expects the nonce prepended to the ciphertext.
// Usage: call ChaCha20Decrypt(...) during the package's normal workflow.
func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
@ -53,6 +55,7 @@ func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) {
// AESGCMEncrypt encrypts plaintext using AES-256-GCM.
// The key must be 32 bytes. The nonce is randomly generated and prepended
// to the ciphertext.
// Usage: call AESGCMEncrypt(...) during the package's normal workflow.
func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
@ -75,6 +78,7 @@ func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) {
// AESGCMDecrypt decrypts ciphertext encrypted with AESGCMEncrypt.
// The key must be 32 bytes. Expects the nonce prepended to the ciphertext.
// Usage: call AESGCMDecrypt(...) during the package's normal workflow.
func AESGCMDecrypt(ciphertext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {

View file

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestChaCha20_Good(t *testing.T) {
func TestSymmetric_ChaCha20_Good(t *testing.T) {
key := make([]byte, 32)
_, err := rand.Read(key)
assert.NoError(t, err)
@ -23,7 +23,7 @@ func TestChaCha20_Good(t *testing.T) {
assert.Equal(t, plaintext, decrypted)
}
func TestChaCha20_Bad(t *testing.T) {
func TestSymmetric_ChaCha20_Bad(t *testing.T) {
key := make([]byte, 32)
wrongKey := make([]byte, 32)
_, _ = rand.Read(key)
@ -38,7 +38,7 @@ func TestChaCha20_Bad(t *testing.T) {
assert.Error(t, err)
}
func TestAESGCM_Good(t *testing.T) {
func TestSymmetric_AESGCM_Good(t *testing.T) {
key := make([]byte, 32)
_, err := rand.Read(key)
assert.NoError(t, err)
@ -56,8 +56,8 @@ func TestAESGCM_Good(t *testing.T) {
// --- Phase 0 Additions ---
// TestAESGCM_Bad_WrongKey verifies wrong key returns error, not corrupt data.
func TestAESGCM_Bad_WrongKey(t *testing.T) {
// TestSymmetric_AESGCM_Bad_WrongKey verifies wrong key returns error, not corrupt data.
func TestSymmetric_AESGCM_Bad_WrongKey(t *testing.T) {
key := make([]byte, 32)
wrongKey := make([]byte, 32)
_, _ = rand.Read(key)
@ -72,8 +72,8 @@ func TestAESGCM_Bad_WrongKey(t *testing.T) {
assert.Nil(t, decrypted, "wrong key must not return partial data")
}
// TestChaCha20EmptyPlaintext_Good verifies empty plaintext round-trip at low level.
func TestChaCha20EmptyPlaintext_Good(t *testing.T) {
// TestSymmetric_ChaCha20EmptyPlaintext_Good verifies empty plaintext round-trip at low level.
func TestSymmetric_ChaCha20EmptyPlaintext_Good(t *testing.T) {
key := make([]byte, 32)
_, err := rand.Read(key)
assert.NoError(t, err)
@ -87,8 +87,8 @@ func TestChaCha20EmptyPlaintext_Good(t *testing.T) {
assert.Empty(t, decrypted)
}
// TestAESGCMEmptyPlaintext_Good verifies empty plaintext round-trip at low level.
func TestAESGCMEmptyPlaintext_Good(t *testing.T) {
// TestSymmetric_AESGCMEmptyPlaintext_Good verifies empty plaintext round-trip at low level.
func TestSymmetric_AESGCMEmptyPlaintext_Good(t *testing.T) {
key := make([]byte, 32)
_, err := rand.Read(key)
assert.NoError(t, err)
@ -102,8 +102,8 @@ func TestAESGCMEmptyPlaintext_Good(t *testing.T) {
assert.Empty(t, decrypted)
}
// TestChaCha20LargePayload_Good verifies 1MB encrypt/decrypt round-trip.
func TestChaCha20LargePayload_Good(t *testing.T) {
// TestSymmetric_ChaCha20LargePayload_Good verifies 1MB encrypt/decrypt round-trip.
func TestSymmetric_ChaCha20LargePayload_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
@ -120,8 +120,8 @@ func TestChaCha20LargePayload_Good(t *testing.T) {
assert.Equal(t, plaintext, decrypted)
}
// TestAESGCMLargePayload_Good verifies 1MB encrypt/decrypt round-trip.
func TestAESGCMLargePayload_Good(t *testing.T) {
// TestSymmetric_AESGCMLargePayload_Good verifies 1MB encrypt/decrypt round-trip.
func TestSymmetric_AESGCMLargePayload_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)

View file

@ -10,18 +10,23 @@ import (
)
// ApprovalStatus represents the state of an approval request.
// Usage: use ApprovalStatus with the other exported helpers in this package.
type ApprovalStatus int
const (
// ApprovalPending means the request is awaiting review.
// Usage: compare or pass ApprovalPending when using the related package APIs.
ApprovalPending ApprovalStatus = iota
// ApprovalApproved means the request was approved.
// Usage: compare or pass ApprovalApproved when using the related package APIs.
ApprovalApproved
// ApprovalDenied means the request was denied.
// Usage: compare or pass ApprovalDenied when using the related package APIs.
ApprovalDenied
)
// String returns the human-readable name of the approval status.
// Usage: call String(...) during the package's normal workflow.
func (s ApprovalStatus) String() string {
switch s {
case ApprovalPending:
@ -36,6 +41,7 @@ func (s ApprovalStatus) String() string {
}
// ApprovalRequest represents a queued capability approval request.
// Usage: use ApprovalRequest with the other exported helpers in this package.
type ApprovalRequest struct {
// ID is the unique identifier for this request.
ID string
@ -58,6 +64,7 @@ type ApprovalRequest struct {
}
// ApprovalQueue manages pending approval requests for NeedsApproval decisions.
// Usage: use ApprovalQueue with the other exported helpers in this package.
type ApprovalQueue struct {
mu sync.RWMutex
requests map[string]*ApprovalRequest
@ -65,6 +72,7 @@ type ApprovalQueue struct {
}
// NewApprovalQueue creates an empty approval queue.
// Usage: call NewApprovalQueue(...) to create a ready-to-use value.
func NewApprovalQueue() *ApprovalQueue {
return &ApprovalQueue{
requests: make(map[string]*ApprovalRequest),
@ -73,6 +81,7 @@ func NewApprovalQueue() *ApprovalQueue {
// Submit creates a new approval request and returns its ID.
// Returns an error if the agent name or capability is empty.
// Usage: call Submit(...) during the package's normal workflow.
func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (string, error) {
if agent == "" {
return "", coreerr.E("trust.ApprovalQueue.Submit", "agent name is required", nil)
@ -101,6 +110,7 @@ func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (strin
// Approve marks a pending request as approved. Returns an error if the
// request is not found or is not in pending status.
// Usage: call Approve(...) during the package's normal workflow.
func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) error {
q.mu.Lock()
defer q.mu.Unlock()
@ -122,6 +132,7 @@ func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) err
// Deny marks a pending request as denied. Returns an error if the
// request is not found or is not in pending status.
// Usage: call Deny(...) during the package's normal workflow.
func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error {
q.mu.Lock()
defer q.mu.Unlock()
@ -142,6 +153,7 @@ func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error
}
// Get returns the approval request with the given ID, or nil if not found.
// Usage: call Get(...) during the package's normal workflow.
func (q *ApprovalQueue) Get(id string) *ApprovalRequest {
q.mu.RLock()
defer q.mu.RUnlock()
@ -156,6 +168,7 @@ func (q *ApprovalQueue) Get(id string) *ApprovalRequest {
}
// Pending returns all requests with ApprovalPending status.
// Usage: call Pending(...) during the package's normal workflow.
func (q *ApprovalQueue) Pending() []ApprovalRequest {
q.mu.RLock()
defer q.mu.RUnlock()
@ -170,6 +183,7 @@ func (q *ApprovalQueue) Pending() []ApprovalRequest {
}
// PendingSeq returns an iterator over all requests with ApprovalPending status.
// Usage: call PendingSeq(...) during the package's normal workflow.
func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] {
return func(yield func(ApprovalRequest) bool) {
q.mu.RLock()
@ -186,6 +200,7 @@ func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] {
}
// Len returns the total number of requests in the queue.
// Usage: call Len(...) during the package's normal workflow.
func (q *ApprovalQueue) Len() int {
q.mu.RLock()
defer q.mu.RUnlock()

View file

@ -11,19 +11,19 @@ import (
// --- ApprovalStatus ---
func TestApprovalStatusString_Good(t *testing.T) {
func TestApproval_ApprovalStatusString_Good(t *testing.T) {
assert.Equal(t, "pending", ApprovalPending.String())
assert.Equal(t, "approved", ApprovalApproved.String())
assert.Equal(t, "denied", ApprovalDenied.String())
}
func TestApprovalStatusString_Bad_Unknown(t *testing.T) {
func TestApproval_ApprovalStatusString_Bad_Unknown(t *testing.T) {
assert.Contains(t, ApprovalStatus(99).String(), "unknown")
}
// --- Submit ---
func TestApprovalSubmit_Good(t *testing.T) {
func TestApproval_ApprovalSubmit_Good(t *testing.T) {
q := NewApprovalQueue()
id, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, err)
@ -31,7 +31,7 @@ func TestApprovalSubmit_Good(t *testing.T) {
assert.Equal(t, 1, q.Len())
}
func TestApprovalSubmit_Good_MultipleRequests(t *testing.T) {
func TestApproval_ApprovalSubmit_Good_MultipleRequests(t *testing.T) {
q := NewApprovalQueue()
id1, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, err)
@ -42,7 +42,7 @@ func TestApprovalSubmit_Good_MultipleRequests(t *testing.T) {
assert.Equal(t, 2, q.Len())
}
func TestApprovalSubmit_Good_EmptyRepo(t *testing.T) {
func TestApproval_ApprovalSubmit_Good_EmptyRepo(t *testing.T) {
q := NewApprovalQueue()
id, err := q.Submit("Clotho", CapMergePR, "")
require.NoError(t, err)
@ -53,14 +53,14 @@ func TestApprovalSubmit_Good_EmptyRepo(t *testing.T) {
assert.Empty(t, req.Repo)
}
func TestApprovalSubmit_Bad_EmptyAgent(t *testing.T) {
func TestApproval_ApprovalSubmit_Bad_EmptyAgent(t *testing.T) {
q := NewApprovalQueue()
_, err := q.Submit("", CapMergePR, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "agent name is required")
}
func TestApprovalSubmit_Bad_EmptyCapability(t *testing.T) {
func TestApproval_ApprovalSubmit_Bad_EmptyCapability(t *testing.T) {
q := NewApprovalQueue()
_, err := q.Submit("Clotho", "", "")
assert.Error(t, err)
@ -69,7 +69,7 @@ func TestApprovalSubmit_Bad_EmptyCapability(t *testing.T) {
// --- Get ---
func TestApprovalGet_Good(t *testing.T) {
func TestApproval_ApprovalGet_Good(t *testing.T) {
q := NewApprovalQueue()
id, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, err)
@ -85,7 +85,7 @@ func TestApprovalGet_Good(t *testing.T) {
assert.True(t, req.ReviewedAt.IsZero())
}
func TestApprovalGet_Good_ReturnsSnapshot(t *testing.T) {
func TestApproval_ApprovalGet_Good_ReturnsSnapshot(t *testing.T) {
q := NewApprovalQueue()
id, err := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, err)
@ -99,14 +99,14 @@ func TestApprovalGet_Good_ReturnsSnapshot(t *testing.T) {
assert.Equal(t, ApprovalPending, original.Status)
}
func TestApprovalGet_Bad_NotFound(t *testing.T) {
func TestApproval_ApprovalGet_Bad_NotFound(t *testing.T) {
q := NewApprovalQueue()
assert.Nil(t, q.Get("nonexistent"))
}
// --- Approve ---
func TestApprovalApprove_Good(t *testing.T) {
func TestApproval_ApprovalApprove_Good(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
@ -121,14 +121,14 @@ func TestApprovalApprove_Good(t *testing.T) {
assert.False(t, req.ReviewedAt.IsZero())
}
func TestApprovalApprove_Bad_NotFound(t *testing.T) {
func TestApproval_ApprovalApprove_Bad_NotFound(t *testing.T) {
q := NewApprovalQueue()
err := q.Approve("nonexistent", "admin", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestApprovalApprove_Bad_AlreadyApproved(t *testing.T) {
func TestApproval_ApprovalApprove_Bad_AlreadyApproved(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, q.Approve(id, "admin", ""))
@ -138,7 +138,7 @@ func TestApprovalApprove_Bad_AlreadyApproved(t *testing.T) {
assert.Contains(t, err.Error(), "already approved")
}
func TestApprovalApprove_Bad_AlreadyDenied(t *testing.T) {
func TestApproval_ApprovalApprove_Bad_AlreadyDenied(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, q.Deny(id, "admin", "nope"))
@ -150,7 +150,7 @@ func TestApprovalApprove_Bad_AlreadyDenied(t *testing.T) {
// --- Deny ---
func TestApprovalDeny_Good(t *testing.T) {
func TestApproval_ApprovalDeny_Good(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
@ -165,14 +165,14 @@ func TestApprovalDeny_Good(t *testing.T) {
assert.False(t, req.ReviewedAt.IsZero())
}
func TestApprovalDeny_Bad_NotFound(t *testing.T) {
func TestApproval_ApprovalDeny_Bad_NotFound(t *testing.T) {
q := NewApprovalQueue()
err := q.Deny("nonexistent", "admin", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestApprovalDeny_Bad_AlreadyDenied(t *testing.T) {
func TestApproval_ApprovalDeny_Bad_AlreadyDenied(t *testing.T) {
q := NewApprovalQueue()
id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core")
require.NoError(t, q.Deny(id, "admin", ""))
@ -184,7 +184,7 @@ func TestApprovalDeny_Bad_AlreadyDenied(t *testing.T) {
// --- Pending ---
func TestApprovalPending_Good(t *testing.T) {
func TestApproval_ApprovalPending_Good(t *testing.T) {
q := NewApprovalQueue()
q.Submit("Clotho", CapMergePR, "host-uk/core")
q.Submit("Hypnos", CapMergePR, "host-uk/docs")
@ -196,12 +196,12 @@ func TestApprovalPending_Good(t *testing.T) {
assert.Len(t, pending, 2)
}
func TestApprovalPending_Good_Empty(t *testing.T) {
func TestApproval_ApprovalPending_Good_Empty(t *testing.T) {
q := NewApprovalQueue()
assert.Empty(t, q.Pending())
}
func TestApprovalPendingSeq_Good(t *testing.T) {
func TestApproval_ApprovalPendingSeq_Good(t *testing.T) {
q := NewApprovalQueue()
q.Submit("Clotho", CapMergePR, "host-uk/core")
q.Submit("Hypnos", CapMergePR, "host-uk/docs")
@ -219,7 +219,7 @@ func TestApprovalPendingSeq_Good(t *testing.T) {
// --- Concurrent operations ---
func TestApprovalConcurrent_Good(t *testing.T) {
func TestApproval_ApprovalConcurrent_Good(t *testing.T) {
q := NewApprovalQueue()
const n = 10
@ -270,7 +270,7 @@ func TestApprovalConcurrent_Good(t *testing.T) {
// --- Integration: PolicyEngine + ApprovalQueue ---
func TestApprovalWorkflow_Good_EndToEnd(t *testing.T) {
func TestApproval_ApprovalWorkflow_Good_EndToEnd(t *testing.T) {
pe := newTestEngine(t)
q := NewApprovalQueue()
@ -293,7 +293,7 @@ func TestApprovalWorkflow_Good_EndToEnd(t *testing.T) {
assert.Equal(t, "Virgil", req.ReviewedBy)
}
func TestApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) {
func TestApproval_ApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) {
pe := newTestEngine(t)
q := NewApprovalQueue()

View file

@ -11,6 +11,7 @@ import (
)
// AuditEntry records a single policy evaluation for compliance.
// Usage: use AuditEntry with the other exported helpers in this package.
type AuditEntry struct {
// Timestamp is when the evaluation occurred.
Timestamp time.Time `json:"timestamp"`
@ -27,6 +28,7 @@ type AuditEntry struct {
}
// MarshalJSON implements custom JSON encoding for Decision.
// Usage: call MarshalJSON(...) during the package's normal workflow.
func (d Decision) MarshalJSON() ([]byte, error) {
result := core.JSONMarshal(d.String())
if !result.OK {
@ -37,6 +39,7 @@ func (d Decision) MarshalJSON() ([]byte, error) {
}
// UnmarshalJSON implements custom JSON decoding for Decision.
// Usage: call UnmarshalJSON(...) during the package's normal workflow.
func (d *Decision) UnmarshalJSON(data []byte) error {
var s string
result := core.JSONUnmarshal(data, &s)
@ -58,6 +61,7 @@ func (d *Decision) UnmarshalJSON(data []byte) error {
}
// AuditLog is an append-only log of policy evaluations.
// Usage: use AuditLog with the other exported helpers in this package.
type AuditLog struct {
mu sync.Mutex
entries []AuditEntry
@ -66,6 +70,7 @@ type AuditLog struct {
// NewAuditLog creates an in-memory audit log. If a writer is provided,
// each entry is also written as a JSON line to that writer (append-only).
// Usage: call NewAuditLog(...) to create a ready-to-use value.
func NewAuditLog(w io.Writer) *AuditLog {
return &AuditLog{
writer: w,
@ -73,6 +78,7 @@ func NewAuditLog(w io.Writer) *AuditLog {
}
// Record appends an evaluation result to the audit log.
// Usage: call Record(...) during the package's normal workflow.
func (l *AuditLog) Record(result EvalResult, repo string) error {
entry := AuditEntry{
Timestamp: time.Now(),
@ -104,6 +110,7 @@ func (l *AuditLog) Record(result EvalResult, repo string) error {
}
// Entries returns a snapshot of all audit entries.
// Usage: call Entries(...) during the package's normal workflow.
func (l *AuditLog) Entries() []AuditEntry {
l.mu.Lock()
defer l.mu.Unlock()
@ -114,6 +121,7 @@ func (l *AuditLog) Entries() []AuditEntry {
}
// EntriesSeq returns an iterator over all audit entries.
// Usage: call EntriesSeq(...) during the package's normal workflow.
func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] {
return func(yield func(AuditEntry) bool) {
l.mu.Lock()
@ -128,6 +136,7 @@ func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] {
}
// Len returns the number of entries in the log.
// Usage: call Len(...) during the package's normal workflow.
func (l *AuditLog) Len() int {
l.mu.Lock()
defer l.mu.Unlock()
@ -135,6 +144,7 @@ func (l *AuditLog) Len() int {
}
// EntriesFor returns all audit entries for a specific agent.
// Usage: call EntriesFor(...) during the package's normal workflow.
func (l *AuditLog) EntriesFor(agent string) []AuditEntry {
l.mu.Lock()
defer l.mu.Unlock()
@ -149,6 +159,7 @@ func (l *AuditLog) EntriesFor(agent string) []AuditEntry {
}
// EntriesForSeq returns an iterator over audit entries for a specific agent.
// Usage: call EntriesForSeq(...) during the package's normal workflow.
func (l *AuditLog) EntriesForSeq(agent string) iter.Seq[AuditEntry] {
return func(yield func(AuditEntry) bool) {
l.mu.Lock()

View file

@ -12,7 +12,7 @@ import (
// --- AuditLog basic ---
func TestAuditRecord_Good(t *testing.T) {
func TestAudit_AuditRecord_Good(t *testing.T) {
log := NewAuditLog(nil)
result := EvalResult{
@ -26,7 +26,7 @@ func TestAuditRecord_Good(t *testing.T) {
assert.Equal(t, 1, log.Len())
}
func TestAuditRecord_Good_EntryFields(t *testing.T) {
func TestAudit_AuditRecord_Good_EntryFields(t *testing.T) {
log := NewAuditLog(nil)
result := EvalResult{
@ -50,7 +50,7 @@ func TestAuditRecord_Good_EntryFields(t *testing.T) {
assert.False(t, e.Timestamp.IsZero())
}
func TestAuditRecord_Good_NoRepo(t *testing.T) {
func TestAudit_AuditRecord_Good_NoRepo(t *testing.T) {
log := NewAuditLog(nil)
result := EvalResult{
Decision: Allow,
@ -66,7 +66,7 @@ func TestAuditRecord_Good_NoRepo(t *testing.T) {
assert.Empty(t, entries[0].Repo)
}
func TestAuditEntries_Good_Snapshot(t *testing.T) {
func TestAudit_AuditEntries_Good_Snapshot(t *testing.T) {
log := NewAuditLog(nil)
log.Record(EvalResult{Agent: "A", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
@ -78,12 +78,12 @@ func TestAuditEntries_Good_Snapshot(t *testing.T) {
assert.Equal(t, "A", log.Entries()[0].Agent)
}
func TestAuditEntries_Good_Empty(t *testing.T) {
func TestAudit_AuditEntries_Good_Empty(t *testing.T) {
log := NewAuditLog(nil)
assert.Empty(t, log.Entries())
}
func TestAuditEntries_Good_AppendOnly(t *testing.T) {
func TestAudit_AuditEntries_Good_AppendOnly(t *testing.T) {
log := NewAuditLog(nil)
for i := range 5 {
@ -99,7 +99,7 @@ func TestAuditEntries_Good_AppendOnly(t *testing.T) {
// --- EntriesFor ---
func TestAuditEntriesFor_Good(t *testing.T) {
func TestAudit_AuditEntriesFor_Good(t *testing.T) {
log := NewAuditLog(nil)
log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
@ -121,7 +121,7 @@ func TestAuditEntriesFor_Good(t *testing.T) {
assert.Equal(t, 2, count)
}
func TestAuditEntriesSeq_Good(t *testing.T) {
func TestAudit_AuditEntriesSeq_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"}, "")
@ -133,7 +133,7 @@ func TestAuditEntriesSeq_Good(t *testing.T) {
assert.Equal(t, 2, count)
}
func TestAuditEntriesFor_Bad_NotFound(t *testing.T) {
func TestAudit_AuditEntriesFor_Bad_NotFound(t *testing.T) {
log := NewAuditLog(nil)
log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
@ -142,7 +142,7 @@ func TestAuditEntriesFor_Bad_NotFound(t *testing.T) {
// --- Writer output ---
func TestAuditRecord_Good_WritesToWriter(t *testing.T) {
func TestAudit_AuditRecord_Good_WritesToWriter(t *testing.T) {
buf := core.NewBuilder()
log := NewAuditLog(buf)
@ -168,7 +168,7 @@ func TestAuditRecord_Good_WritesToWriter(t *testing.T) {
assert.Equal(t, "host-uk/core", entry.Repo)
}
func TestAuditRecord_Good_MultipleLines(t *testing.T) {
func TestAudit_AuditRecord_Good_MultipleLines(t *testing.T) {
buf := core.NewBuilder()
log := NewAuditLog(buf)
@ -192,7 +192,7 @@ func TestAuditRecord_Good_MultipleLines(t *testing.T) {
}
}
func TestAuditRecord_Bad_WriterError(t *testing.T) {
func TestAudit_AuditRecord_Bad_WriterError(t *testing.T) {
log := NewAuditLog(&failWriter{})
result := EvalResult{
@ -218,7 +218,7 @@ func (f *failWriter) Write(_ []byte) (int, error) {
// --- Decision JSON marshalling ---
func TestDecisionJSON_Good_RoundTrip(t *testing.T) {
func TestAudit_DecisionJSON_Good_RoundTrip(t *testing.T) {
decisions := []Decision{Deny, Allow, NeedsApproval}
expected := []string{`"deny"`, `"allow"`, `"needs_approval"`}
@ -234,7 +234,7 @@ func TestDecisionJSON_Good_RoundTrip(t *testing.T) {
}
}
func TestDecisionJSON_Bad_UnknownString(t *testing.T) {
func TestAudit_DecisionJSON_Bad_UnknownString(t *testing.T) {
var d Decision
result := core.JSONUnmarshal([]byte(`"invalid"`), &d)
err, _ := result.Value.(error)
@ -242,7 +242,7 @@ func TestDecisionJSON_Bad_UnknownString(t *testing.T) {
assert.Contains(t, err.Error(), "unknown decision")
}
func TestDecisionJSON_Bad_NonString(t *testing.T) {
func TestAudit_DecisionJSON_Bad_NonString(t *testing.T) {
var d Decision
result := core.JSONUnmarshal([]byte(`42`), &d)
err, _ := result.Value.(error)
@ -251,7 +251,7 @@ func TestDecisionJSON_Bad_NonString(t *testing.T) {
// --- Concurrent audit logging ---
func TestAuditConcurrent_Good(t *testing.T) {
func TestAudit_AuditConcurrent_Good(t *testing.T) {
buf := core.NewBuilder()
log := NewAuditLog(buf)
@ -277,7 +277,7 @@ func TestAuditConcurrent_Good(t *testing.T) {
// --- Integration: PolicyEngine + AuditLog ---
func TestAuditPolicyIntegration_Good(t *testing.T) {
func TestAudit_AuditPolicyIntegration_Good(t *testing.T) {
buf := core.NewBuilder()
log := NewAuditLog(buf)
pe := newTestEngine(t)

View file

@ -8,6 +8,7 @@ import (
)
// PolicyConfig is the JSON-serialisable representation of a trust policy.
// Usage: use PolicyConfig with the other exported helpers in this package.
type PolicyConfig struct {
Tier int `json:"tier"`
Allowed []string `json:"allowed"`
@ -16,11 +17,13 @@ type PolicyConfig struct {
}
// PoliciesConfig is the top-level configuration containing all tier policies.
// Usage: use PoliciesConfig with the other exported helpers in this package.
type PoliciesConfig struct {
Policies []PolicyConfig `json:"policies"`
}
// LoadPoliciesFromFile reads a JSON file and returns parsed policies.
// Usage: call LoadPoliciesFromFile(...) during the package's normal workflow.
func LoadPoliciesFromFile(path string) ([]Policy, error) {
openResult := (&core.Fs{}).New("/").Open(path)
if !openResult.OK {
@ -31,6 +34,7 @@ func LoadPoliciesFromFile(path string) ([]Policy, error) {
}
// LoadPolicies reads JSON from a reader and returns parsed policies.
// Usage: call LoadPolicies(...) during the package's normal workflow.
func LoadPolicies(r io.Reader) ([]Policy, error) {
readResult := core.ReadAll(r)
if !readResult.OK {
@ -76,6 +80,7 @@ func convertPolicies(cfg PoliciesConfig) ([]Policy, error) {
// ApplyPolicies loads policies from a reader and sets them on the engine,
// replacing any existing policies for the same tiers.
// Usage: call ApplyPolicies(...) during the package's normal workflow.
func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error {
policies, err := LoadPolicies(r)
if err != nil {
@ -90,6 +95,7 @@ func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error {
}
// ApplyPoliciesFromFile loads policies from a JSON file and sets them on the engine.
// Usage: call ApplyPoliciesFromFile(...) during the package's normal workflow.
func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error {
openResult := (&core.Fs{}).New("/").Open(path)
if !openResult.OK {
@ -100,6 +106,7 @@ func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error {
}
// ExportPolicies serialises the current policies as JSON to the given writer.
// Usage: call ExportPolicies(...) during the package's normal workflow.
func (pe *PolicyEngine) ExportPolicies(w io.Writer) error {
var cfg PoliciesConfig
for _, tier := range []Tier{TierUntrusted, TierVerified, TierFull} {

View file

@ -30,13 +30,13 @@ const validPolicyJSON = `{
// --- LoadPolicies ---
func TestLoadPolicies_Good(t *testing.T) {
func TestConfig_LoadPolicies_Good(t *testing.T) {
policies, err := LoadPolicies(core.NewReader(validPolicyJSON))
require.NoError(t, err)
assert.Len(t, policies, 3)
}
func TestLoadPolicies_Good_FieldMapping(t *testing.T) {
func TestConfig_LoadPolicies_Good_FieldMapping(t *testing.T) {
policies, err := LoadPolicies(core.NewReader(validPolicyJSON))
require.NoError(t, err)
@ -60,33 +60,33 @@ func TestLoadPolicies_Good_FieldMapping(t *testing.T) {
assert.Len(t, policies[2].Denied, 2)
}
func TestLoadPolicies_Good_EmptyPolicies(t *testing.T) {
func TestConfig_LoadPolicies_Good_EmptyPolicies(t *testing.T) {
input := `{"policies": []}`
policies, err := LoadPolicies(core.NewReader(input))
require.NoError(t, err)
assert.Empty(t, policies)
}
func TestLoadPolicies_Bad_InvalidJSON(t *testing.T) {
func TestConfig_LoadPolicies_Bad_InvalidJSON(t *testing.T) {
_, err := LoadPolicies(core.NewReader(`{invalid`))
assert.Error(t, err)
}
func TestLoadPolicies_Bad_InvalidTier(t *testing.T) {
func TestConfig_LoadPolicies_Bad_InvalidTier(t *testing.T) {
input := `{"policies": [{"tier": 0, "allowed": ["repo.push"]}]}`
_, err := LoadPolicies(core.NewReader(input))
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid tier")
}
func TestLoadPolicies_Bad_TierTooHigh(t *testing.T) {
func TestConfig_LoadPolicies_Bad_TierTooHigh(t *testing.T) {
input := `{"policies": [{"tier": 99, "allowed": ["repo.push"]}]}`
_, err := LoadPolicies(core.NewReader(input))
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid tier")
}
func TestLoadPolicies_Bad_UnknownField(t *testing.T) {
func TestConfig_LoadPolicies_Bad_UnknownField(t *testing.T) {
input := `{"policies": [{"tier": 1, "allowed": ["repo.push"], "bogus": true}]}`
_, err := LoadPolicies(core.NewReader(input))
assert.Error(t, err, "DisallowUnknownFields should reject unknown fields")
@ -94,7 +94,7 @@ func TestLoadPolicies_Bad_UnknownField(t *testing.T) {
// --- LoadPoliciesFromFile ---
func TestLoadPoliciesFromFile_Good(t *testing.T) {
func TestConfig_LoadPoliciesFromFile_Good(t *testing.T) {
dir := t.TempDir()
path := core.Path(dir, "policies.json")
writePolicyFile(t, path, validPolicyJSON)
@ -104,14 +104,14 @@ func TestLoadPoliciesFromFile_Good(t *testing.T) {
assert.Len(t, policies, 3)
}
func TestLoadPoliciesFromFile_Bad_NotFound(t *testing.T) {
func TestConfig_LoadPoliciesFromFile_Bad_NotFound(t *testing.T) {
_, err := LoadPoliciesFromFile("/nonexistent/path/policies.json")
assert.Error(t, err)
}
// --- ApplyPolicies ---
func TestApplyPolicies_Good(t *testing.T) {
func TestConfig_ApplyPolicies_Good(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{Name: "TestAgent", Tier: TierVerified}))
pe := NewPolicyEngine(r)
@ -135,7 +135,7 @@ func TestApplyPolicies_Good(t *testing.T) {
assert.Equal(t, Allow, result.Decision)
}
func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) {
func TestConfig_ApplyPolicies_Bad_InvalidJSON(t *testing.T) {
r := NewRegistry()
pe := NewPolicyEngine(r)
@ -143,7 +143,7 @@ func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) {
assert.Error(t, err)
}
func TestApplyPolicies_Bad_InvalidTier(t *testing.T) {
func TestConfig_ApplyPolicies_Bad_InvalidTier(t *testing.T) {
r := NewRegistry()
pe := NewPolicyEngine(r)
@ -154,7 +154,7 @@ func TestApplyPolicies_Bad_InvalidTier(t *testing.T) {
// --- ApplyPoliciesFromFile ---
func TestApplyPoliciesFromFile_Good(t *testing.T) {
func TestConfig_ApplyPoliciesFromFile_Good(t *testing.T) {
dir := t.TempDir()
path := core.Path(dir, "policies.json")
writePolicyFile(t, path, validPolicyJSON)
@ -172,7 +172,7 @@ func TestApplyPoliciesFromFile_Good(t *testing.T) {
assert.Len(t, p.Allowed, 3)
}
func TestApplyPoliciesFromFile_Bad_NotFound(t *testing.T) {
func TestConfig_ApplyPoliciesFromFile_Bad_NotFound(t *testing.T) {
r := NewRegistry()
pe := NewPolicyEngine(r)
err := pe.ApplyPoliciesFromFile("/nonexistent/policies.json")
@ -181,7 +181,7 @@ func TestApplyPoliciesFromFile_Bad_NotFound(t *testing.T) {
// --- ExportPolicies ---
func TestExportPolicies_Good(t *testing.T) {
func TestConfig_ExportPolicies_Good(t *testing.T) {
r := NewRegistry()
pe := NewPolicyEngine(r) // loads defaults
@ -196,7 +196,7 @@ func TestExportPolicies_Good(t *testing.T) {
assert.Len(t, cfg.Policies, 3)
}
func TestExportPolicies_Good_RoundTrip(t *testing.T) {
func TestConfig_ExportPolicies_Good_RoundTrip(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{Name: "A", Tier: TierFull}))
pe := NewPolicyEngine(r)
@ -232,26 +232,26 @@ func writePolicyFile(t *testing.T, path, content string) {
// --- Helper conversion ---
func TestToCapabilities_Good(t *testing.T) {
func TestConfig_ToCapabilities_Good(t *testing.T) {
caps := toCapabilities([]string{"repo.push", "pr.merge"})
assert.Len(t, caps, 2)
assert.Equal(t, CapPushRepo, caps[0])
assert.Equal(t, CapMergePR, caps[1])
}
func TestToCapabilities_Good_Empty(t *testing.T) {
func TestConfig_ToCapabilities_Good_Empty(t *testing.T) {
assert.Nil(t, toCapabilities(nil))
assert.Nil(t, toCapabilities([]string{}))
}
func TestFromCapabilities_Good(t *testing.T) {
func TestConfig_FromCapabilities_Good(t *testing.T) {
ss := fromCapabilities([]Capability{CapPushRepo, CapMergePR})
assert.Len(t, ss, 2)
assert.Equal(t, "repo.push", ss[0])
assert.Equal(t, "pr.merge", ss[1])
}
func TestFromCapabilities_Good_Empty(t *testing.T) {
func TestConfig_FromCapabilities_Good_Empty(t *testing.T) {
assert.Nil(t, fromCapabilities(nil))
assert.Nil(t, fromCapabilities([]Capability{}))
}

View file

@ -8,6 +8,7 @@ import (
)
// Policy defines the access rules for a given trust tier.
// Usage: use Policy with the other exported helpers in this package.
type Policy struct {
// Tier is the trust level this policy applies to.
Tier Tier
@ -20,24 +21,30 @@ type Policy struct {
}
// PolicyEngine evaluates capability requests against registered policies.
// Usage: use PolicyEngine with the other exported helpers in this package.
type PolicyEngine struct {
registry *Registry
policies map[Tier]*Policy
}
// Decision is the result of a policy evaluation.
// Usage: use Decision with the other exported helpers in this package.
type Decision int
const (
// Deny means the action is not permitted.
// Usage: compare or pass Deny when using the related package APIs.
Deny Decision = iota
// Allow means the action is permitted.
// Usage: compare or pass Allow when using the related package APIs.
Allow
// NeedsApproval means the action requires human or higher-tier approval.
// Usage: compare or pass NeedsApproval when using the related package APIs.
NeedsApproval
)
// String returns the human-readable name of the decision.
// Usage: call String(...) during the package's normal workflow.
func (d Decision) String() string {
switch d {
case Deny:
@ -52,6 +59,7 @@ func (d Decision) String() string {
}
// EvalResult contains the outcome of a capability evaluation.
// Usage: use EvalResult with the other exported helpers in this package.
type EvalResult struct {
Decision Decision
Agent string
@ -60,6 +68,7 @@ type EvalResult struct {
}
// NewPolicyEngine creates a policy engine with the given registry and default policies.
// Usage: call NewPolicyEngine(...) to create a ready-to-use value.
func NewPolicyEngine(registry *Registry) *PolicyEngine {
pe := &PolicyEngine{
registry: registry,
@ -72,6 +81,7 @@ func NewPolicyEngine(registry *Registry) *PolicyEngine {
// Evaluate checks whether the named agent can perform the given capability.
// If the agent has scoped repos and the capability is repo-scoped, the repo
// parameter is checked against the agent's allowed repos.
// Usage: call Evaluate(...) during the package's normal workflow.
func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) EvalResult {
agent := pe.registry.Get(agentName)
if agent == nil {
@ -145,6 +155,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string)
}
// SetPolicy replaces the policy for a given tier.
// Usage: call SetPolicy(...) during the package's normal workflow.
func (pe *PolicyEngine) SetPolicy(p Policy) error {
if !p.Tier.Valid() {
return coreerr.E("trust.SetPolicy", core.Sprintf("invalid tier %d", p.Tier), nil)
@ -154,6 +165,7 @@ func (pe *PolicyEngine) SetPolicy(p Policy) error {
}
// GetPolicy returns the policy for a tier, or nil if none is set.
// Usage: call GetPolicy(...) during the package's normal workflow.
func (pe *PolicyEngine) GetPolicy(t Tier) *Policy {
return pe.policies[t]
}

View file

@ -29,19 +29,19 @@ func newTestEngine(t *testing.T) *PolicyEngine {
// --- Decision ---
func TestDecisionString_Good(t *testing.T) {
func TestPolicy_DecisionString_Good(t *testing.T) {
assert.Equal(t, "deny", Deny.String())
assert.Equal(t, "allow", Allow.String())
assert.Equal(t, "needs_approval", NeedsApproval.String())
}
func TestDecisionString_Bad_Unknown(t *testing.T) {
func TestPolicy_DecisionString_Bad_Unknown(t *testing.T) {
assert.Contains(t, Decision(99).String(), "unknown")
}
// --- Tier 3 (Full Trust) ---
func TestEvaluate_Good_Tier3CanDoAnything(t *testing.T) {
func TestPolicy_Evaluate_Good_Tier3CanDoAnything(t *testing.T) {
pe := newTestEngine(t)
caps := []Capability{
@ -57,56 +57,56 @@ func TestEvaluate_Good_Tier3CanDoAnything(t *testing.T) {
// --- Tier 2 (Verified) ---
func TestEvaluate_Good_Tier2CanCreatePR(t *testing.T) {
func TestPolicy_Evaluate_Good_Tier2CanCreatePR(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Clotho", CapCreatePR, "host-uk/core")
assert.Equal(t, Allow, result.Decision)
}
func TestEvaluate_Good_Tier2CanPushToScopedRepo(t *testing.T) {
func TestPolicy_Evaluate_Good_Tier2CanPushToScopedRepo(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/core")
assert.Equal(t, Allow, result.Decision)
}
func TestEvaluate_Good_Tier2NeedsApprovalToMerge(t *testing.T) {
func TestPolicy_Evaluate_Good_Tier2NeedsApprovalToMerge(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Clotho", CapMergePR, "host-uk/core")
assert.Equal(t, NeedsApproval, result.Decision)
}
func TestEvaluate_Good_Tier2CanCreateIssue(t *testing.T) {
func TestPolicy_Evaluate_Good_Tier2CanCreateIssue(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Clotho", CapCreateIssue, "")
assert.Equal(t, Allow, result.Decision)
}
func TestEvaluate_Bad_Tier2CannotAccessWorkspace(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier2CannotAccessWorkspace(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Clotho", CapAccessWorkspace, "")
assert.Equal(t, Deny, result.Decision)
}
func TestEvaluate_Bad_Tier2CannotModifyFlows(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier2CannotModifyFlows(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Clotho", CapModifyFlows, "")
assert.Equal(t, Deny, result.Decision)
}
func TestEvaluate_Bad_Tier2CannotRunPrivileged(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier2CannotRunPrivileged(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Clotho", CapRunPrivileged, "")
assert.Equal(t, Deny, result.Decision)
}
func TestEvaluate_Bad_Tier2CannotPushToUnscopedRepo(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier2CannotPushToUnscopedRepo(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/secret-repo")
assert.Equal(t, Deny, result.Decision)
assert.Contains(t, result.Reason, "does not have access")
}
func TestEvaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) {
pe := newTestEngine(t)
// Push without specifying a repo should be denied for scoped agents.
result := pe.Evaluate("Clotho", CapPushRepo, "")
@ -115,43 +115,43 @@ func TestEvaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) {
// --- Tier 1 (Untrusted) ---
func TestEvaluate_Good_Tier1CanCreatePR(t *testing.T) {
func TestPolicy_Evaluate_Good_Tier1CanCreatePR(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("BugSETI-001", CapCreatePR, "")
assert.Equal(t, Allow, result.Decision)
}
func TestEvaluate_Good_Tier1CanCommentIssue(t *testing.T) {
func TestPolicy_Evaluate_Good_Tier1CanCommentIssue(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("BugSETI-001", CapCommentIssue, "")
assert.Equal(t, Allow, result.Decision)
}
func TestEvaluate_Bad_Tier1CannotPush(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier1CannotPush(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("BugSETI-001", CapPushRepo, "")
assert.Equal(t, Deny, result.Decision)
}
func TestEvaluate_Bad_Tier1CannotMerge(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier1CannotMerge(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("BugSETI-001", CapMergePR, "")
assert.Equal(t, Deny, result.Decision)
}
func TestEvaluate_Bad_Tier1CannotCreateIssue(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier1CannotCreateIssue(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("BugSETI-001", CapCreateIssue, "")
assert.Equal(t, Deny, result.Decision)
}
func TestEvaluate_Bad_Tier1CannotReadSecrets(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier1CannotReadSecrets(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("BugSETI-001", CapReadSecrets, "")
assert.Equal(t, Deny, result.Decision)
}
func TestEvaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("BugSETI-001", CapRunPrivileged, "")
assert.Equal(t, Deny, result.Decision)
@ -159,14 +159,14 @@ func TestEvaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) {
// --- Edge cases ---
func TestEvaluate_Bad_UnknownAgent(t *testing.T) {
func TestPolicy_Evaluate_Bad_UnknownAgent(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Unknown", CapCreatePR, "")
assert.Equal(t, Deny, result.Decision)
assert.Contains(t, result.Reason, "not registered")
}
func TestEvaluate_Good_EvalResultFields(t *testing.T) {
func TestPolicy_Evaluate_Good_EvalResultFields(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Athena", CapPushRepo, "")
assert.Equal(t, "Athena", result.Agent)
@ -176,7 +176,7 @@ func TestEvaluate_Good_EvalResultFields(t *testing.T) {
// --- SetPolicy ---
func TestSetPolicy_Good(t *testing.T) {
func TestPolicy_SetPolicy_Good(t *testing.T) {
pe := newTestEngine(t)
err := pe.SetPolicy(Policy{
Tier: TierVerified,
@ -189,64 +189,64 @@ func TestSetPolicy_Good(t *testing.T) {
assert.Equal(t, Allow, result.Decision)
}
func TestSetPolicy_Bad_InvalidTier(t *testing.T) {
func TestPolicy_SetPolicy_Bad_InvalidTier(t *testing.T) {
pe := newTestEngine(t)
err := pe.SetPolicy(Policy{Tier: Tier(0)})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid tier")
}
func TestGetPolicy_Good(t *testing.T) {
func TestPolicy_GetPolicy_Good(t *testing.T) {
pe := newTestEngine(t)
p := pe.GetPolicy(TierFull)
require.NotNil(t, p)
assert.Equal(t, TierFull, p.Tier)
}
func TestGetPolicy_Bad_NotFound(t *testing.T) {
func TestPolicy_GetPolicy_Bad_NotFound(t *testing.T) {
pe := newTestEngine(t)
assert.Nil(t, pe.GetPolicy(Tier(99)))
}
// --- isRepoScoped / repoAllowed helpers ---
func TestIsRepoScoped_Good(t *testing.T) {
func TestPolicy_IsRepoScoped_Good(t *testing.T) {
assert.True(t, isRepoScoped(CapPushRepo))
assert.True(t, isRepoScoped(CapCreatePR))
assert.True(t, isRepoScoped(CapMergePR))
assert.True(t, isRepoScoped(CapReadSecrets))
}
func TestIsRepoScoped_Bad_NotScoped(t *testing.T) {
func TestPolicy_IsRepoScoped_Bad_NotScoped(t *testing.T) {
assert.False(t, isRepoScoped(CapRunPrivileged))
assert.False(t, isRepoScoped(CapAccessWorkspace))
assert.False(t, isRepoScoped(CapModifyFlows))
}
func TestRepoAllowed_Good(t *testing.T) {
func TestPolicy_RepoAllowed_Good(t *testing.T) {
scoped := []string{"host-uk/core", "host-uk/docs"}
assert.True(t, repoAllowed(scoped, "host-uk/core"))
assert.True(t, repoAllowed(scoped, "host-uk/docs"))
}
func TestRepoAllowed_Bad_NotInScope(t *testing.T) {
func TestPolicy_RepoAllowed_Bad_NotInScope(t *testing.T) {
scoped := []string{"host-uk/core"}
assert.False(t, repoAllowed(scoped, "host-uk/secret"))
}
func TestRepoAllowed_Bad_EmptyRepo(t *testing.T) {
func TestPolicy_RepoAllowed_Bad_EmptyRepo(t *testing.T) {
scoped := []string{"host-uk/core"}
assert.False(t, repoAllowed(scoped, ""))
}
func TestRepoAllowed_Bad_EmptyScope(t *testing.T) {
func TestPolicy_RepoAllowed_Bad_EmptyScope(t *testing.T) {
assert.False(t, repoAllowed(nil, "host-uk/core"))
assert.False(t, repoAllowed([]string{}, "host-uk/core"))
}
// --- Tier 3 ignores repo scoping ---
func TestEvaluate_Good_Tier3IgnoresRepoScope(t *testing.T) {
func TestPolicy_Evaluate_Good_Tier3IgnoresRepoScope(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{
Name: "Virgil",
@ -261,7 +261,7 @@ func TestEvaluate_Good_Tier3IgnoresRepoScope(t *testing.T) {
// --- Default rate limits ---
func TestDefaultRateLimit_Good(t *testing.T) {
func TestPolicy_DefaultRateLimit_Good(t *testing.T) {
assert.Equal(t, 10, defaultRateLimit(TierUntrusted))
assert.Equal(t, 60, defaultRateLimit(TierVerified))
assert.Equal(t, 0, defaultRateLimit(TierFull))
@ -270,11 +270,11 @@ func TestDefaultRateLimit_Good(t *testing.T) {
// --- Phase 0 Additions ---
// TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll verifies that a Tier 2
// TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll verifies that a Tier 2
// agent with empty ScopedRepos is treated as "unrestricted" for repo-scoped
// capabilities. NOTE: This is a potential security concern documented in
// FINDINGS.md — empty ScopedRepos bypasses the repo scope check entirely.
func TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) {
func TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{
Name: "Hypnos",
@ -301,9 +301,9 @@ func TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) {
assert.Equal(t, Allow, result.Decision)
}
// TestEvaluate_Bad_CapabilityNotInAnyList verifies that a capability not in
// TestPolicy_Evaluate_Bad_CapabilityNotInAnyList verifies that a capability not in
// allowed, denied, or requires_approval lists defaults to deny.
func TestEvaluate_Bad_CapabilityNotInAnyList(t *testing.T) {
func TestPolicy_Evaluate_Bad_CapabilityNotInAnyList(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{
Name: "TestAgent",
@ -325,9 +325,9 @@ func TestEvaluate_Bad_CapabilityNotInAnyList(t *testing.T) {
assert.Contains(t, result.Reason, "not granted")
}
// TestEvaluate_Bad_UnknownCapability verifies that a completely invented
// TestPolicy_Evaluate_Bad_UnknownCapability verifies that a completely invented
// capability string is denied.
func TestEvaluate_Bad_UnknownCapability(t *testing.T) {
func TestPolicy_Evaluate_Bad_UnknownCapability(t *testing.T) {
pe := newTestEngine(t)
result := pe.Evaluate("Athena", Capability("nonexistent.capability"), "")
@ -335,9 +335,9 @@ func TestEvaluate_Bad_UnknownCapability(t *testing.T) {
assert.Contains(t, result.Reason, "not granted")
}
// TestConcurrentEvaluate_Good verifies that concurrent policy evaluations
// TestPolicy_ConcurrentEvaluate_Good verifies that concurrent policy evaluations
// with 10 goroutines do not race.
func TestConcurrentEvaluate_Good(t *testing.T) {
func TestPolicy_ConcurrentEvaluate_Good(t *testing.T) {
pe := newTestEngine(t)
const n = 10
@ -360,10 +360,10 @@ func TestConcurrentEvaluate_Good(t *testing.T) {
wg.Wait()
}
// TestEvaluate_Bad_Tier2ScopedReposWithEmptyRepoParam verifies that
// TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam verifies that
// a scoped agent requesting a repo-scoped capability without specifying
// the repo is denied.
func TestEvaluate_Bad_Tier2ScopedReposWithEmptyRepoParam(t *testing.T) {
func TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam(t *testing.T) {
pe := newTestEngine(t)
// Clotho has ScopedRepos but passes empty repo

View file

@ -9,100 +9,100 @@ import (
// --- matchScope ---
func TestMatchScope_Good_ExactMatch(t *testing.T) {
func TestScope_MatchScope_Good_ExactMatch(t *testing.T) {
assert.True(t, matchScope("host-uk/core", "host-uk/core"))
}
func TestMatchScope_Good_SingleWildcard(t *testing.T) {
func TestScope_MatchScope_Good_SingleWildcard(t *testing.T) {
assert.True(t, matchScope("core/*", "core/php"))
assert.True(t, matchScope("core/*", "core/go-crypt"))
assert.True(t, matchScope("host-uk/*", "host-uk/core"))
}
func TestMatchScope_Good_RecursiveWildcard(t *testing.T) {
func TestScope_MatchScope_Good_RecursiveWildcard(t *testing.T) {
assert.True(t, matchScope("core/**", "core/php"))
assert.True(t, matchScope("core/**", "core/php/sub"))
assert.True(t, matchScope("core/**", "core/a/b/c"))
}
func TestMatchScope_Bad_ExactMismatch(t *testing.T) {
func TestScope_MatchScope_Bad_ExactMismatch(t *testing.T) {
assert.False(t, matchScope("host-uk/core", "host-uk/docs"))
}
func TestMatchScope_Bad_SingleWildcardNoNested(t *testing.T) {
func TestScope_MatchScope_Bad_SingleWildcardNoNested(t *testing.T) {
// "core/*" should NOT match "core/php/sub" — only single level.
assert.False(t, matchScope("core/*", "core/php/sub"))
assert.False(t, matchScope("core/*", "core/a/b"))
}
func TestMatchScope_Bad_SingleWildcardNoPrefix(t *testing.T) {
func TestScope_MatchScope_Bad_SingleWildcardNoPrefix(t *testing.T) {
// "core/*" should NOT match "other/php".
assert.False(t, matchScope("core/*", "other/php"))
}
func TestMatchScope_Bad_RecursiveWildcardNoPrefix(t *testing.T) {
func TestScope_MatchScope_Bad_RecursiveWildcardNoPrefix(t *testing.T) {
assert.False(t, matchScope("core/**", "other/php"))
}
func TestMatchScope_Bad_EmptyRepo(t *testing.T) {
func TestScope_MatchScope_Bad_EmptyRepo(t *testing.T) {
assert.False(t, matchScope("core/*", ""))
}
func TestMatchScope_Bad_WildcardInMiddle(t *testing.T) {
func TestScope_MatchScope_Bad_WildcardInMiddle(t *testing.T) {
// Wildcard not at the end — should not match.
assert.False(t, matchScope("core/*/sub", "core/php/sub"))
}
func TestMatchScope_Bad_WildcardOnlyPrefix(t *testing.T) {
func TestScope_MatchScope_Bad_WildcardOnlyPrefix(t *testing.T) {
// "core/*" should not match the prefix itself.
assert.False(t, matchScope("core/*", "core"))
assert.False(t, matchScope("core/*", "core/"))
}
func TestMatchScope_Good_RecursiveWildcardSingleLevel(t *testing.T) {
func TestScope_MatchScope_Good_RecursiveWildcardSingleLevel(t *testing.T) {
// "core/**" should also match single-level children.
assert.True(t, matchScope("core/**", "core/php"))
}
func TestMatchScope_Bad_RecursiveWildcardPrefixOnly(t *testing.T) {
func TestScope_MatchScope_Bad_RecursiveWildcardPrefixOnly(t *testing.T) {
assert.False(t, matchScope("core/**", "core"))
assert.False(t, matchScope("core/**", "corefoo"))
}
// --- repoAllowed with wildcards ---
func TestRepoAllowedWildcard_Good(t *testing.T) {
func TestScope_RepoAllowedWildcard_Good(t *testing.T) {
scoped := []string{"core/*", "host-uk/docs"}
assert.True(t, repoAllowed(scoped, "core/php"))
assert.True(t, repoAllowed(scoped, "core/go-crypt"))
assert.True(t, repoAllowed(scoped, "host-uk/docs"))
}
func TestRepoAllowedWildcard_Good_Recursive(t *testing.T) {
func TestScope_RepoAllowedWildcard_Good_Recursive(t *testing.T) {
scoped := []string{"core/**"}
assert.True(t, repoAllowed(scoped, "core/php"))
assert.True(t, repoAllowed(scoped, "core/php/sub"))
}
func TestRepoAllowedWildcard_Bad_NoMatch(t *testing.T) {
func TestScope_RepoAllowedWildcard_Bad_NoMatch(t *testing.T) {
scoped := []string{"core/*"}
assert.False(t, repoAllowed(scoped, "other/repo"))
assert.False(t, repoAllowed(scoped, "core/php/sub"))
}
func TestRepoAllowedWildcard_Bad_EmptyRepo(t *testing.T) {
func TestScope_RepoAllowedWildcard_Bad_EmptyRepo(t *testing.T) {
scoped := []string{"core/*"}
assert.False(t, repoAllowed(scoped, ""))
}
func TestRepoAllowedWildcard_Bad_EmptyScope(t *testing.T) {
func TestScope_RepoAllowedWildcard_Bad_EmptyScope(t *testing.T) {
assert.False(t, repoAllowed(nil, "core/php"))
assert.False(t, repoAllowed([]string{}, "core/php"))
}
// --- Integration: PolicyEngine with wildcard scopes ---
func TestEvaluateWildcardScope_Good_SingleLevel(t *testing.T) {
func TestScope_EvaluateWildcardScope_Good_SingleLevel(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{
Name: "WildAgent",
@ -118,7 +118,7 @@ func TestEvaluateWildcardScope_Good_SingleLevel(t *testing.T) {
assert.Equal(t, Allow, result.Decision)
}
func TestEvaluateWildcardScope_Bad_OutOfScope(t *testing.T) {
func TestScope_EvaluateWildcardScope_Bad_OutOfScope(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{
Name: "WildAgent",
@ -132,7 +132,7 @@ func TestEvaluateWildcardScope_Bad_OutOfScope(t *testing.T) {
assert.Contains(t, result.Reason, "does not have access")
}
func TestEvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) {
func TestScope_EvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{
Name: "WildAgent",
@ -145,7 +145,7 @@ func TestEvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) {
assert.Equal(t, Deny, result.Decision)
}
func TestEvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) {
func TestScope_EvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{
Name: "DeepAgent",
@ -158,7 +158,7 @@ func TestEvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) {
assert.Equal(t, Allow, result.Decision)
}
func TestEvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) {
func TestScope_EvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{
Name: "MixedAgent",
@ -180,7 +180,7 @@ func TestEvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) {
assert.Equal(t, Deny, result.Decision)
}
func TestEvaluateWildcardScope_Good_ReadSecretsScoped(t *testing.T) {
func TestScope_EvaluateWildcardScope_Good_ReadSecretsScoped(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{
Name: "ScopedSecrets",

View file

@ -20,18 +20,23 @@ import (
)
// Tier represents an agent's trust level in the system.
// Usage: use Tier with the other exported helpers in this package.
type Tier int
const (
// TierUntrusted is for external/community agents with minimal access.
// Usage: compare or pass TierUntrusted when using the related package APIs.
TierUntrusted Tier = 1
// TierVerified is for partner agents with scoped access.
// Usage: compare or pass TierVerified when using the related package APIs.
TierVerified Tier = 2
// TierFull is for internal agents with full access.
// Usage: compare or pass TierFull when using the related package APIs.
TierFull Tier = 3
)
// String returns the human-readable name of the tier.
// Usage: call String(...) during the package's normal workflow.
func (t Tier) String() string {
switch t {
case TierUntrusted:
@ -46,26 +51,47 @@ func (t Tier) String() string {
}
// Valid returns true if the tier is a recognised trust level.
// Usage: call Valid(...) during the package's normal workflow.
func (t Tier) Valid() bool {
return t >= TierUntrusted && t <= TierFull
}
// Capability represents a specific action an agent can perform.
// Usage: use Capability with the other exported helpers in this package.
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 allows pushing commits to a repository.
// Usage: pass CapPushRepo to PolicyEngine.Evaluate or include it in a Policy.
CapPushRepo Capability = "repo.push"
// CapMergePR allows merging a pull request.
// Usage: pass CapMergePR to PolicyEngine.Evaluate or include it in a Policy.
CapMergePR Capability = "pr.merge"
// CapCreatePR allows creating a pull request.
// Usage: pass CapCreatePR to PolicyEngine.Evaluate or include it in a Policy.
CapCreatePR Capability = "pr.create"
// CapCreateIssue allows creating an issue.
// Usage: pass CapCreateIssue to PolicyEngine.Evaluate or include it in a Policy.
CapCreateIssue Capability = "issue.create"
// CapCommentIssue allows commenting on an issue.
// Usage: pass CapCommentIssue to PolicyEngine.Evaluate or include it in a Policy.
CapCommentIssue Capability = "issue.comment"
// CapReadSecrets allows reading secret material.
// Usage: pass CapReadSecrets to PolicyEngine.Evaluate or include it in a Policy.
CapReadSecrets Capability = "secrets.read"
// CapRunPrivileged allows running privileged commands.
// Usage: pass CapRunPrivileged to PolicyEngine.Evaluate or include it in a Policy.
CapRunPrivileged Capability = "cmd.privileged"
// CapAccessWorkspace allows accessing the workspace filesystem.
// Usage: pass CapAccessWorkspace to PolicyEngine.Evaluate or include it in a Policy.
CapAccessWorkspace Capability = "workspace.access"
CapModifyFlows Capability = "flows.modify"
// CapModifyFlows allows modifying workflow definitions.
// Usage: pass CapModifyFlows to PolicyEngine.Evaluate or include it in a Policy.
CapModifyFlows Capability = "flows.modify"
)
// Agent represents an agent identity in the trust system.
// Usage: use Agent with the other exported helpers in this package.
type Agent struct {
// Name is the unique identifier for the agent (e.g., "Athena", "Clotho").
Name string
@ -83,12 +109,14 @@ type Agent struct {
}
// Registry manages agent identities and their trust tiers.
// Usage: use Registry with the other exported helpers in this package.
type Registry struct {
mu sync.RWMutex
agents map[string]*Agent
}
// NewRegistry creates an empty agent registry.
// Usage: call NewRegistry(...) to create a ready-to-use value.
func NewRegistry() *Registry {
return &Registry{
agents: make(map[string]*Agent),
@ -97,6 +125,7 @@ func NewRegistry() *Registry {
// Register adds or updates an agent in the registry.
// Returns an error if the agent name is empty or the tier is invalid.
// Usage: call Register(...) during the package's normal workflow.
func (r *Registry) Register(agent Agent) error {
if agent.Name == "" {
return coreerr.E("trust.Register", "agent name is required", nil)
@ -118,6 +147,7 @@ func (r *Registry) Register(agent Agent) error {
}
// Get returns the agent with the given name, or nil if not found.
// Usage: call Get(...) during the package's normal workflow.
func (r *Registry) Get(name string) *Agent {
r.mu.RLock()
defer r.mu.RUnlock()
@ -125,6 +155,7 @@ func (r *Registry) Get(name string) *Agent {
}
// Remove deletes an agent from the registry.
// Usage: call Remove(...) during the package's normal workflow.
func (r *Registry) Remove(name string) bool {
r.mu.Lock()
defer r.mu.Unlock()
@ -136,6 +167,7 @@ func (r *Registry) Remove(name string) bool {
}
// List returns all registered agents. The returned slice is a snapshot.
// Usage: call List(...) during the package's normal workflow.
func (r *Registry) List() []Agent {
r.mu.RLock()
defer r.mu.RUnlock()
@ -147,6 +179,7 @@ func (r *Registry) List() []Agent {
}
// ListSeq returns an iterator over all registered agents.
// Usage: call ListSeq(...) during the package's normal workflow.
func (r *Registry) ListSeq() iter.Seq[Agent] {
return func(yield func(Agent) bool) {
r.mu.RLock()
@ -160,6 +193,7 @@ func (r *Registry) ListSeq() iter.Seq[Agent] {
}
// Len returns the number of registered agents.
// Usage: call Len(...) during the package's normal workflow.
func (r *Registry) Len() int {
r.mu.RLock()
defer r.mu.RUnlock()

View file

@ -12,23 +12,23 @@ import (
// --- Tier ---
func TestTierString_Good(t *testing.T) {
func TestTrust_TierString_Good(t *testing.T) {
assert.Equal(t, "untrusted", TierUntrusted.String())
assert.Equal(t, "verified", TierVerified.String())
assert.Equal(t, "full", TierFull.String())
}
func TestTierString_Bad_Unknown(t *testing.T) {
func TestTrust_TierString_Bad_Unknown(t *testing.T) {
assert.Contains(t, Tier(99).String(), "unknown")
}
func TestTierValid_Good(t *testing.T) {
func TestTrust_TierValid_Good(t *testing.T) {
assert.True(t, TierUntrusted.Valid())
assert.True(t, TierVerified.Valid())
assert.True(t, TierFull.Valid())
}
func TestTierValid_Bad(t *testing.T) {
func TestTrust_TierValid_Bad(t *testing.T) {
assert.False(t, Tier(0).Valid())
assert.False(t, Tier(4).Valid())
assert.False(t, Tier(-1).Valid())
@ -36,14 +36,14 @@ func TestTierValid_Bad(t *testing.T) {
// --- Registry ---
func TestRegistryRegister_Good(t *testing.T) {
func TestTrust_RegistryRegister_Good(t *testing.T) {
r := NewRegistry()
err := r.Register(Agent{Name: "Athena", Tier: TierFull})
require.NoError(t, err)
assert.Equal(t, 1, r.Len())
}
func TestRegistryRegister_Good_SetsDefaults(t *testing.T) {
func TestTrust_RegistryRegister_Good_SetsDefaults(t *testing.T) {
r := NewRegistry()
err := r.Register(Agent{Name: "Athena", Tier: TierFull})
require.NoError(t, err)
@ -54,7 +54,7 @@ func TestRegistryRegister_Good_SetsDefaults(t *testing.T) {
assert.False(t, a.CreatedAt.IsZero())
}
func TestRegistryRegister_Good_TierDefaults(t *testing.T) {
func TestTrust_RegistryRegister_Good_TierDefaults(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{Name: "A", Tier: TierUntrusted}))
require.NoError(t, r.Register(Agent{Name: "B", Tier: TierVerified}))
@ -65,14 +65,14 @@ func TestRegistryRegister_Good_TierDefaults(t *testing.T) {
assert.Equal(t, 0, r.Get("C").RateLimit)
}
func TestRegistryRegister_Good_PreservesExplicitRateLimit(t *testing.T) {
func TestTrust_RegistryRegister_Good_PreservesExplicitRateLimit(t *testing.T) {
r := NewRegistry()
err := r.Register(Agent{Name: "Custom", Tier: TierVerified, RateLimit: 30})
require.NoError(t, err)
assert.Equal(t, 30, r.Get("Custom").RateLimit)
}
func TestRegistryRegister_Good_Update(t *testing.T) {
func TestTrust_RegistryRegister_Good_Update(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierVerified}))
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
@ -81,21 +81,21 @@ func TestRegistryRegister_Good_Update(t *testing.T) {
assert.Equal(t, TierFull, r.Get("Athena").Tier)
}
func TestRegistryRegister_Bad_EmptyName(t *testing.T) {
func TestTrust_RegistryRegister_Bad_EmptyName(t *testing.T) {
r := NewRegistry()
err := r.Register(Agent{Tier: TierFull})
assert.Error(t, err)
assert.Contains(t, err.Error(), "name is required")
}
func TestRegistryRegister_Bad_InvalidTier(t *testing.T) {
func TestTrust_RegistryRegister_Bad_InvalidTier(t *testing.T) {
r := NewRegistry()
err := r.Register(Agent{Name: "Bad", Tier: Tier(0)})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid tier")
}
func TestRegistryGet_Good(t *testing.T) {
func TestTrust_RegistryGet_Good(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
a := r.Get("Athena")
@ -103,24 +103,24 @@ func TestRegistryGet_Good(t *testing.T) {
assert.Equal(t, "Athena", a.Name)
}
func TestRegistryGet_Bad_NotFound(t *testing.T) {
func TestTrust_RegistryGet_Bad_NotFound(t *testing.T) {
r := NewRegistry()
assert.Nil(t, r.Get("nonexistent"))
}
func TestRegistryRemove_Good(t *testing.T) {
func TestTrust_RegistryRemove_Good(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
assert.True(t, r.Remove("Athena"))
assert.Equal(t, 0, r.Len())
}
func TestRegistryRemove_Bad_NotFound(t *testing.T) {
func TestTrust_RegistryRemove_Bad_NotFound(t *testing.T) {
r := NewRegistry()
assert.False(t, r.Remove("nonexistent"))
}
func TestRegistryList_Good(t *testing.T) {
func TestTrust_RegistryList_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}))
@ -136,12 +136,12 @@ func TestRegistryList_Good(t *testing.T) {
assert.True(t, names["Clotho"])
}
func TestRegistryList_Good_Empty(t *testing.T) {
func TestTrust_RegistryList_Good_Empty(t *testing.T) {
r := NewRegistry()
assert.Empty(t, r.List())
}
func TestRegistryList_Good_Snapshot(t *testing.T) {
func TestTrust_RegistryList_Good_Snapshot(t *testing.T) {
r := NewRegistry()
require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull}))
agents := r.List()
@ -151,7 +151,7 @@ func TestRegistryList_Good_Snapshot(t *testing.T) {
assert.Equal(t, TierFull, r.Get("Athena").Tier)
}
func TestRegistryListSeq_Good(t *testing.T) {
func TestTrust_RegistryListSeq_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}))
@ -169,7 +169,7 @@ func TestRegistryListSeq_Good(t *testing.T) {
// --- Agent ---
func TestAgentTokenExpiry_Good(t *testing.T) {
func TestTrust_AgentTokenExpiry_Good(t *testing.T) {
agent := Agent{
Name: "Test",
Tier: TierVerified,
@ -183,9 +183,9 @@ func TestAgentTokenExpiry_Good(t *testing.T) {
// --- Phase 0 Additions ---
// TestConcurrentRegistryOperations_Good verifies that Register/Get/Remove
// TestTrust_ConcurrentRegistryOperations_Good verifies that Register/Get/Remove
// from 10 goroutines do not race.
func TestConcurrentRegistryOperations_Good(t *testing.T) {
func TestTrust_ConcurrentRegistryOperations_Good(t *testing.T) {
r := NewRegistry()
const n = 10
@ -224,24 +224,24 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) {
// No panic or data race = success (run with -race flag)
}
// TestRegisterTierZero_Bad verifies that Tier 0 is rejected.
func TestRegisterTierZero_Bad(t *testing.T) {
// TestTrust_RegisterTierZero_Bad verifies that Tier 0 is rejected.
func TestTrust_RegisterTierZero_Bad(t *testing.T) {
r := NewRegistry()
err := r.Register(Agent{Name: "InvalidTierAgent", Tier: Tier(0)})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid tier")
}
// TestRegisterNegativeTier_Bad verifies that negative tiers are rejected.
func TestRegisterNegativeTier_Bad(t *testing.T) {
// TestTrust_RegisterNegativeTier_Bad verifies that negative tiers are rejected.
func TestTrust_RegisterNegativeTier_Bad(t *testing.T) {
r := NewRegistry()
err := r.Register(Agent{Name: "NegativeTier", Tier: Tier(-1)})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid tier")
}
// TestTokenExpiryBoundary_Good verifies token expiry checking.
func TestTokenExpiryBoundary_Good(t *testing.T) {
// TestTrust_TokenExpiryBoundary_Good verifies token expiry checking.
func TestTrust_TokenExpiryBoundary_Good(t *testing.T) {
// Token that expires in the future — should be valid
futureAgent := Agent{
Name: "FutureAgent",
@ -256,8 +256,8 @@ func TestTokenExpiryBoundary_Good(t *testing.T) {
"token should now be expired")
}
// TestTokenExpiryZeroValue_Ugly verifies zero-value TokenExpiresAt behaviour.
func TestTokenExpiryZeroValue_Ugly(t *testing.T) {
// TestTrust_TokenExpiryZeroValue_Ugly verifies zero-value TokenExpiresAt behaviour.
func TestTrust_TokenExpiryZeroValue_Ugly(t *testing.T) {
agent := Agent{
Name: "ZeroExpiry",
Tier: TierVerified,
@ -274,8 +274,8 @@ func TestTokenExpiryZeroValue_Ugly(t *testing.T) {
"zero-value token expiry should be in the past")
}
// TestConcurrentListDuringMutations_Good verifies List is safe during writes.
func TestConcurrentListDuringMutations_Good(t *testing.T) {
// TestTrust_ConcurrentListDuringMutations_Good verifies List is safe during writes.
func TestTrust_ConcurrentListDuringMutations_Good(t *testing.T) {
r := NewRegistry()
// Pre-populate