diff --git a/auth/auth.go b/auth/auth.go index 35db896..cf7417e 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -29,12 +29,10 @@ import ( "context" "crypto/rand" "encoding/hex" - "encoding/json" - "fmt" - "strings" "sync" "time" + core "dappco.re/go/core" "dappco.re/go/core/crypt/crypt" "dappco.re/go/core/crypt/crypt/lthn" "dappco.re/go/core/crypt/crypt/pgp" @@ -218,12 +216,13 @@ func (a *Authenticator) Register(username, password string) (*User, error) { } // Encrypt metadata with the user's public key and store - metaJSON, err := json.Marshal(user) - if err != nil { + metaJSONResult := core.JSONMarshal(user) + if !metaJSONResult.OK { + err, _ := metaJSONResult.Value.(error) return nil, coreerr.E(op, "failed to marshal user metadata", err) } - encMeta, err := pgp.Encrypt(metaJSON, kp.PublicKey) + encMeta, err := pgp.Encrypt(metaJSONResult.Value.([]byte), kp.PublicKey) if err != nil { return nil, coreerr.E(op, "failed to encrypt user metadata", err) } @@ -419,7 +418,7 @@ func (a *Authenticator) Login(userID, password string) (*Session, error) { return nil, coreerr.E(op, "failed to read password hash", err) } - if strings.HasPrefix(storedHash, "$argon2id$") { + if core.HasPrefix(storedHash, "$argon2id$") { valid, err := crypt.VerifyPassword(password, storedHash) if err != nil { return nil, coreerr.E(op, "failed to verify password", err) @@ -482,7 +481,9 @@ func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) ( } var user User - if err := json.Unmarshal(metaJSON, &user); err != nil { + metaResult := core.JSONUnmarshal(metaJSON, &user) + if !metaResult.OK { + err, _ := metaResult.Value.(error) return nil, coreerr.E(op, "failed to unmarshal user metadata", err) } @@ -504,12 +505,13 @@ func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) ( user.PasswordHash = newHash // Re-encrypt metadata with new public key - updatedMeta, err := json.Marshal(&user) - if err != nil { + updatedMetaResult := core.JSONMarshal(&user) + if !updatedMetaResult.OK { + err, _ := updatedMetaResult.Value.(error) return nil, coreerr.E(op, "failed to marshal updated metadata", err) } - encUpdatedMeta, err := pgp.Encrypt(updatedMeta, newKP.PublicKey) + encUpdatedMeta, err := pgp.Encrypt(updatedMetaResult.Value.([]byte), newKP.PublicKey) if err != nil { return nil, coreerr.E(op, "failed to encrypt metadata with new key", err) } @@ -556,11 +558,12 @@ func (a *Authenticator) RevokeKey(userID, password, reason string) error { Reason: reason, RevokedAt: time.Now(), } - revJSON, err := json.Marshal(&rev) - if err != nil { + revJSONResult := core.JSONMarshal(&rev) + if !revJSONResult.OK { + err, _ := revJSONResult.Value.(error) return coreerr.E(op, "failed to marshal revocation record", err) } - if err := a.medium.Write(userPath(userID, ".rev"), string(revJSON)); err != nil { + if err := a.medium.Write(userPath(userID, ".rev"), string(revJSONResult.Value.([]byte))); err != nil { return coreerr.E(op, "failed to write revocation record", err) } @@ -586,7 +589,8 @@ func (a *Authenticator) IsRevoked(userID string) bool { // Attempt to parse as JSON revocation record var rev Revocation - if err := json.Unmarshal([]byte(content), &rev); err != nil { + revResult := core.JSONUnmarshal([]byte(content), &rev) + if !revResult.OK { return false } @@ -605,12 +609,13 @@ func (a *Authenticator) WriteChallengeFile(userID, path string) error { return coreerr.E(op, "failed to create challenge", err) } - data, err := json.Marshal(challenge) - if err != nil { + challengeResult := core.JSONMarshal(challenge) + if !challengeResult.OK { + err, _ := challengeResult.Value.(error) return coreerr.E(op, "failed to marshal challenge", err) } - if err := a.medium.Write(path, string(data)); err != nil { + if err := a.medium.Write(path, string(challengeResult.Value.([]byte))); err != nil { return coreerr.E(op, "failed to write challenge file", err) } @@ -645,7 +650,7 @@ func (a *Authenticator) verifyPassword(userID, password string) error { // Try Argon2id hash first (.hash file) if a.medium.IsFile(userPath(userID, ".hash")) { storedHash, err := a.medium.Read(userPath(userID, ".hash")) - if err == nil && strings.HasPrefix(storedHash, "$argon2id$") { + if err == nil && core.HasPrefix(storedHash, "$argon2id$") { valid, verr := crypt.VerifyPassword(password, storedHash) if verr != nil { return coreerr.E(op, "failed to verify password", nil) @@ -705,11 +710,11 @@ func (a *Authenticator) StartCleanup(ctx context.Context, interval time.Duration case <-ticker.C: count, err := a.store.Cleanup() if err != nil { - fmt.Printf("auth: session cleanup error: %v\n", err) + core.Print(nil, "auth: session cleanup error: %v", err) continue } if count > 0 { - fmt.Printf("auth: cleaned up %d expired session(s)\n", count) + core.Print(nil, "auth: cleaned up %d expired session(s)", count) } } } diff --git a/auth/auth_test.go b/auth/auth_test.go index 5777a8e..97a3c17 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -1,13 +1,11 @@ package auth import ( - "encoding/json" - "fmt" - "strings" "sync" "testing" "time" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -46,7 +44,7 @@ func TestRegister_Good(t *testing.T) { assert.NotEmpty(t, user.PublicKey) assert.Equal(t, userID, user.KeyID) assert.NotEmpty(t, user.Fingerprint) - assert.True(t, strings.HasPrefix(user.PasswordHash, "$argon2id$"), "password hash should be Argon2id format") + assert.True(t, core.HasPrefix(user.PasswordHash, "$argon2id$"), "password hash should be Argon2id format") assert.False(t, user.Created.IsZero()) } @@ -414,8 +412,8 @@ func TestAirGappedFlow_Good(t *testing.T) { require.NoError(t, err) var challenge Challenge - err = json.Unmarshal([]byte(challengeData), &challenge) - require.NoError(t, err) + result := core.JSONUnmarshal([]byte(challengeData), &challenge) + require.Truef(t, result.OK, "failed to unmarshal challenge: %v", result.Value) // Client-side: decrypt nonce and sign it privKey, err := m.Read(userPath(userID, ".key")) @@ -590,7 +588,7 @@ func TestConcurrentSessionCreation_Good(t *testing.T) { const n = 10 userIDs := make([]string, n) for i := range n { - username := fmt.Sprintf("concurrent-user-%d", i) + username := core.Sprintf("concurrent-user-%d", i) _, err := a.Register(username, "pass") require.NoError(t, err) userIDs[i] = lthn.Hash(username) @@ -736,7 +734,11 @@ func TestEmptyPasswordRegistration_Good(t *testing.T) { func TestVeryLongUsername_Ugly(t *testing.T) { a, _ := newTestAuth() - longUsername := strings.Repeat("a", 10000) + longName := core.NewBuilder() + for range 10000 { + longName.WriteString("a") + } + longUsername := longName.String() user, err := a.Register(longUsername, "pass") require.NoError(t, err) require.NotNil(t, user) @@ -795,8 +797,8 @@ func TestAirGappedRoundTrip_Good(t *testing.T) { require.NoError(t, err) var challenge Challenge - err = json.Unmarshal([]byte(challengeData), &challenge) - require.NoError(t, err) + result := core.JSONUnmarshal([]byte(challengeData), &challenge) + require.Truef(t, result.OK, "failed to unmarshal challenge: %v", result.Value) assert.NotEmpty(t, challenge.Encrypted) assert.True(t, challenge.ExpiresAt.After(time.Now())) @@ -870,13 +872,13 @@ func TestRegisterArgon2id_Good(t *testing.T) { assert.True(t, m.IsFile(userPath(userID, ".hash"))) hashContent, err := m.Read(userPath(userID, ".hash")) require.NoError(t, err) - assert.True(t, strings.HasPrefix(hashContent, "$argon2id$"), "stored hash should be Argon2id") + assert.True(t, core.HasPrefix(hashContent, "$argon2id$"), "stored hash should be Argon2id") // .lthn file should NOT exist for new registrations assert.False(t, m.IsFile(userPath(userID, ".lthn"))) // User struct should have Argon2id hash - assert.True(t, strings.HasPrefix(user.PasswordHash, "$argon2id$")) + assert.True(t, core.HasPrefix(user.PasswordHash, "$argon2id$")) } // TestLoginArgon2id_Good verifies login works with Argon2id hashed password. @@ -940,7 +942,7 @@ func TestLegacyLTHNMigration_Good(t *testing.T) { assert.True(t, m.IsFile(userPath(userID, ".hash")), "migration should create .hash file") newHash, err := m.Read(userPath(userID, ".hash")) require.NoError(t, err) - assert.True(t, strings.HasPrefix(newHash, "$argon2id$"), "migrated hash should be Argon2id") + assert.True(t, core.HasPrefix(newHash, "$argon2id$"), "migrated hash should be Argon2id") // Subsequent login should use the new Argon2id hash (not LTHN) session2, err := a.Login(userID, "legacy-pass") @@ -1024,10 +1026,10 @@ func TestRotateKeyPair_Good(t *testing.T) { require.NoError(t, err) var meta User - err = json.Unmarshal(decrypted, &meta) - require.NoError(t, err) + result := core.JSONUnmarshal(decrypted, &meta) + require.Truef(t, result.OK, "failed to unmarshal metadata: %v", result.Value) assert.Equal(t, userID, meta.KeyID) - assert.True(t, strings.HasPrefix(meta.PasswordHash, "$argon2id$")) + assert.True(t, core.HasPrefix(meta.PasswordHash, "$argon2id$")) } // TestRotateKeyPair_Bad verifies that rotation fails with wrong old password. @@ -1108,8 +1110,8 @@ func TestRevokeKey_Good(t *testing.T) { assert.NotEqual(t, "REVOCATION_PLACEHOLDER", revContent) var rev Revocation - err = json.Unmarshal([]byte(revContent), &rev) - require.NoError(t, err) + result := core.JSONUnmarshal([]byte(revContent), &rev) + require.Truef(t, result.OK, "failed to unmarshal revocation: %v", result.Value) assert.Equal(t, userID, rev.UserID) assert.Equal(t, "compromised key material", rev.Reason) assert.False(t, rev.RevokedAt.IsZero()) diff --git a/auth/session_store_sqlite.go b/auth/session_store_sqlite.go index 4340da8..1d61ff2 100644 --- a/auth/session_store_sqlite.go +++ b/auth/session_store_sqlite.go @@ -1,11 +1,10 @@ package auth import ( - "encoding/json" - "errors" "sync" "time" + core "dappco.re/go/core" "dappco.re/go/core/store" ) @@ -35,14 +34,16 @@ func (s *SQLiteSessionStore) Get(token string) (*Session, error) { val, err := s.store.Get(sessionGroup, token) if err != nil { - if errors.Is(err, store.ErrNotFound) { + if core.Is(err, store.ErrNotFound) { return nil, ErrSessionNotFound } return nil, err } var session Session - if err := json.Unmarshal([]byte(val), &session); err != nil { + result := core.JSONUnmarshal([]byte(val), &session) + if !result.OK { + err, _ := result.Value.(error) return nil, err } return &session, nil @@ -53,11 +54,12 @@ func (s *SQLiteSessionStore) Set(session *Session) error { s.mu.Lock() defer s.mu.Unlock() - data, err := json.Marshal(session) - if err != nil { + result := core.JSONMarshal(session) + if !result.OK { + err, _ := result.Value.(error) return err } - return s.store.Set(sessionGroup, session.Token, string(data)) + return s.store.Set(sessionGroup, session.Token, string(result.Value.([]byte))) } // Delete removes a session by token from SQLite. @@ -68,7 +70,7 @@ func (s *SQLiteSessionStore) Delete(token string) error { // Check existence first to return ErrSessionNotFound _, err := s.store.Get(sessionGroup, token) if err != nil { - if errors.Is(err, store.ErrNotFound) { + if core.Is(err, store.ErrNotFound) { return ErrSessionNotFound } return err @@ -88,7 +90,8 @@ func (s *SQLiteSessionStore) DeleteByUser(userID string) error { for token, val := range all { var session Session - if err := json.Unmarshal([]byte(val), &session); err != nil { + result := core.JSONUnmarshal([]byte(val), &session) + if !result.OK { continue // Skip malformed entries } if session.UserID == userID { @@ -114,7 +117,8 @@ func (s *SQLiteSessionStore) Cleanup() (int, error) { count := 0 for token, val := range all { var session Session - if err := json.Unmarshal([]byte(val), &session); err != nil { + result := core.JSONUnmarshal([]byte(val), &session) + if !result.OK { continue // Skip malformed entries } if now.After(session.ExpiresAt) { diff --git a/auth/session_store_test.go b/auth/session_store_test.go index 69fdbfa..977e159 100644 --- a/auth/session_store_test.go +++ b/auth/session_store_test.go @@ -2,13 +2,11 @@ package auth import ( "context" - "fmt" - "os" - "path/filepath" "sync" "testing" "time" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -66,7 +64,7 @@ func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) { // Create sessions for two users for i := range 3 { err := store.Set(&Session{ - Token: fmt.Sprintf("user-a-token-%d", i), + Token: core.Sprintf("user-a-token-%d", i), UserID: "user-a", ExpiresAt: time.Now().Add(1 * time.Hour), }) @@ -86,7 +84,7 @@ func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) { // user-a sessions should be gone for i := range 3 { - _, err := store.Get(fmt.Sprintf("user-a-token-%d", i)) + _, err := store.Get(core.Sprintf("user-a-token-%d", i)) assert.ErrorIs(t, err, ErrSessionNotFound) } @@ -146,11 +144,11 @@ func TestMemorySessionStore_Concurrent_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - token := fmt.Sprintf("concurrent-token-%d", idx) + token := core.Sprintf("concurrent-token-%d", idx) err := store.Set(&Session{ Token: token, - UserID: fmt.Sprintf("user-%d", idx%5), + UserID: core.Sprintf("user-%d", idx%5), ExpiresAt: time.Now().Add(1 * time.Hour), }) assert.NoError(t, err) @@ -222,7 +220,7 @@ func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) { // Create sessions for two users for i := range 3 { err := store.Set(&Session{ - Token: fmt.Sprintf("sqlite-user-a-%d", i), + Token: core.Sprintf("sqlite-user-a-%d", i), UserID: "user-a", ExpiresAt: time.Now().Add(1 * time.Hour), }) @@ -242,7 +240,7 @@ func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) { // user-a sessions should be gone for i := range 3 { - _, err := store.Get(fmt.Sprintf("sqlite-user-a-%d", i)) + _, err := store.Get(core.Sprintf("sqlite-user-a-%d", i)) assert.ErrorIs(t, err, ErrSessionNotFound) } @@ -296,7 +294,7 @@ func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) { func TestSQLiteSessionStore_Persistence_Good(t *testing.T) { dir := t.TempDir() - dbPath := filepath.Join(dir, "sessions.db") + dbPath := core.Path(dir, "sessions.db") // Write a session store1, err := NewSQLiteSessionStore(dbPath) @@ -327,7 +325,7 @@ func TestSQLiteSessionStore_Persistence_Good(t *testing.T) { func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) { // Use a temp file — :memory: SQLite has concurrency limitations - dbPath := filepath.Join(t.TempDir(), "concurrent.db") + dbPath := core.Path(t.TempDir(), "concurrent.db") store, err := NewSQLiteSessionStore(dbPath) require.NoError(t, err) defer store.Close() @@ -339,11 +337,11 @@ func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - token := fmt.Sprintf("sqlite-concurrent-%d", idx) + token := core.Sprintf("sqlite-concurrent-%d", idx) err := store.Set(&Session{ Token: token, - UserID: fmt.Sprintf("user-%d", idx%5), + UserID: core.Sprintf("user-%d", idx%5), ExpiresAt: time.Now().Add(1 * time.Hour), }) assert.NoError(t, err) @@ -480,8 +478,7 @@ func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) { func TestSQLiteSessionStore_TempFile_Good(t *testing.T) { // Verify we can use a real temp file (not :memory:) - tmpFile := filepath.Join(os.TempDir(), "go-crypt-test-session-store.db") - defer os.Remove(tmpFile) + tmpFile := core.Path(t.TempDir(), "go-crypt-test-session-store.db") store, err := NewSQLiteSessionStore(tmpFile) require.NoError(t, err) diff --git a/cmd/crypt/cmd_checksum.go b/cmd/crypt/cmd_checksum.go index 0475fa5..e751f51 100644 --- a/cmd/crypt/cmd_checksum.go +++ b/cmd/crypt/cmd_checksum.go @@ -1,9 +1,7 @@ package crypt import ( - "fmt" - "path/filepath" - + core "dappco.re/go/core" "dappco.re/go/core/crypt/crypt" "forge.lthn.ai/core/cli/pkg/cli" ) @@ -42,12 +40,12 @@ func runChecksum(path string) error { if checksumVerify != "" { if hash == checksumVerify { - cli.Success(fmt.Sprintf("Checksum matches: %s", filepath.Base(path))) + cli.Success(core.Sprintf("Checksum matches: %s", core.PathBase(path))) return nil } - cli.Error(fmt.Sprintf("Checksum mismatch: %s", filepath.Base(path))) - cli.Dim(fmt.Sprintf(" expected: %s", checksumVerify)) - cli.Dim(fmt.Sprintf(" got: %s", hash)) + cli.Error(core.Sprintf("Checksum mismatch: %s", core.PathBase(path))) + cli.Dim(core.Sprintf(" expected: %s", checksumVerify)) + cli.Dim(core.Sprintf(" got: %s", hash)) return cli.Err("checksum verification failed") } @@ -56,6 +54,6 @@ func runChecksum(path string) error { algo = "SHA-512" } - fmt.Printf("%s %s (%s)\n", hash, path, algo) + core.Print(nil, "%s %s (%s)", hash, path, algo) return nil } diff --git a/cmd/crypt/cmd_encrypt.go b/cmd/crypt/cmd_encrypt.go index 709733d..44db894 100644 --- a/cmd/crypt/cmd_encrypt.go +++ b/cmd/crypt/cmd_encrypt.go @@ -1,9 +1,7 @@ package crypt import ( - "fmt" - "strings" - + core "dappco.re/go/core" "dappco.re/go/core/crypt/crypt" coreio "dappco.re/go/core/io" "forge.lthn.ai/core/cli/pkg/cli" @@ -74,7 +72,7 @@ func runEncrypt(path string) error { return cli.Wrap(err, "failed to write encrypted file") } - cli.Success(fmt.Sprintf("Encrypted %s -> %s", path, outPath)) + cli.Success(core.Sprintf("Encrypted %s -> %s", path, outPath)) return nil } @@ -103,7 +101,7 @@ func runDecrypt(path string) error { return cli.Wrap(err, "failed to decrypt") } - outPath := strings.TrimSuffix(path, ".enc") + outPath := core.TrimSuffix(path, ".enc") if outPath == path { outPath = path + ".dec" } @@ -112,6 +110,6 @@ func runDecrypt(path string) error { return cli.Wrap(err, "failed to write decrypted file") } - cli.Success(fmt.Sprintf("Decrypted %s -> %s", path, outPath)) + cli.Success(core.Sprintf("Decrypted %s -> %s", path, outPath)) return nil } diff --git a/cmd/crypt/cmd_hash.go b/cmd/crypt/cmd_hash.go index d8fca7b..ca3cbe2 100644 --- a/cmd/crypt/cmd_hash.go +++ b/cmd/crypt/cmd_hash.go @@ -1,8 +1,7 @@ package crypt import ( - "fmt" - + core "dappco.re/go/core" "dappco.re/go/core/crypt/crypt" "forge.lthn.ai/core/cli/pkg/cli" @@ -39,7 +38,7 @@ func runHash(input string) error { if err != nil { return cli.Wrap(err, "failed to hash password") } - fmt.Println(hash) + core.Println(hash) return nil } @@ -47,7 +46,7 @@ func runHash(input string) error { if err != nil { return cli.Wrap(err, "failed to hash password") } - fmt.Println(hash) + core.Println(hash) return nil } diff --git a/cmd/crypt/cmd_keygen.go b/cmd/crypt/cmd_keygen.go index 025ebf5..0b752ae 100644 --- a/cmd/crypt/cmd_keygen.go +++ b/cmd/crypt/cmd_keygen.go @@ -4,8 +4,8 @@ import ( "crypto/rand" "encoding/base64" "encoding/hex" - "fmt" + core "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" ) @@ -43,12 +43,12 @@ func runKeygen() error { switch { case keygenHex: - fmt.Println(hex.EncodeToString(key)) + core.Println(hex.EncodeToString(key)) case keygenBase64: - fmt.Println(base64.StdEncoding.EncodeToString(key)) + core.Println(base64.StdEncoding.EncodeToString(key)) default: // Default to hex output - fmt.Println(hex.EncodeToString(key)) + core.Println(hex.EncodeToString(key)) } return nil diff --git a/cmd/testcmd/cmd_output.go b/cmd/testcmd/cmd_output.go index f9ae73a..8665215 100644 --- a/cmd/testcmd/cmd_output.go +++ b/cmd/testcmd/cmd_output.go @@ -3,13 +3,11 @@ package testcmd import ( "bufio" "cmp" - "fmt" - "path/filepath" "regexp" "slices" "strconv" - "strings" + core "dappco.re/go/core" "dappco.re/go/core/i18n" ) @@ -40,7 +38,7 @@ func parseTestOutput(output string) testResults { skipPattern := regexp.MustCompile(`^\?\s+(\S+)\s+\[no test files\]`) coverPattern := regexp.MustCompile(`coverage:\s+([\d.]+)%`) - scanner := bufio.NewScanner(strings.NewReader(output)) + scanner := bufio.NewScanner(core.NewReader(output)) for scanner.Scan() { line := scanner.Text() @@ -85,21 +83,32 @@ func printTestSummary(results testResults, showCoverage bool) { // Print pass/fail summary total := results.passed + results.failed if total > 0 { - fmt.Printf(" %s %s", testPassStyle.Render("✓"), i18n.T("i18n.count.passed", results.passed)) + line := core.NewBuilder() + line.WriteString(" ") + line.WriteString(testPassStyle.Render("✓")) + line.WriteString(" ") + line.WriteString(i18n.T("i18n.count.passed", results.passed)) if results.failed > 0 { - fmt.Printf(" %s %s", testFailStyle.Render("✗"), i18n.T("i18n.count.failed", results.failed)) + line.WriteString(" ") + line.WriteString(testFailStyle.Render("✗")) + line.WriteString(" ") + line.WriteString(i18n.T("i18n.count.failed", results.failed)) } if results.skipped > 0 { - fmt.Printf(" %s %s", testSkipStyle.Render("○"), i18n.T("i18n.count.skipped", results.skipped)) + line.WriteString(" ") + line.WriteString(testSkipStyle.Render("○")) + line.WriteString(" ") + line.WriteString(i18n.T("i18n.count.skipped", results.skipped)) } - fmt.Println() + core.Println(line.String()) } // Print failed packages if len(results.failedPkgs) > 0 { - fmt.Printf("\n %s\n", i18n.T("cmd.test.failed_packages")) + core.Println() + core.Println(" " + i18n.T("cmd.test.failed_packages")) for _, pkg := range results.failedPkgs { - fmt.Printf(" %s %s\n", testFailStyle.Render("✗"), pkg) + core.Println(core.Sprintf(" %s %s", testFailStyle.Render("✗"), pkg)) } } @@ -108,7 +117,8 @@ func printTestSummary(results testResults, showCoverage bool) { printCoverageSummary(results) } else if results.covCount > 0 { avgCov := results.totalCov / float64(results.covCount) - fmt.Printf("\n %s %s\n", i18n.Label("coverage"), formatCoverage(avgCov)) + core.Println() + core.Println(core.Sprintf(" %s %s", i18n.Label("coverage"), formatCoverage(avgCov))) } } @@ -117,7 +127,8 @@ func printCoverageSummary(results testResults) { return } - fmt.Printf("\n %s\n", testHeaderStyle.Render(i18n.T("cmd.test.coverage_by_package"))) + core.Println() + core.Println(" " + testHeaderStyle.Render(i18n.T("cmd.test.coverage_by_package"))) // Sort packages by name slices.SortFunc(results.packages, func(a, b packageCoverage) int { @@ -143,8 +154,8 @@ func printCoverageSummary(results testResults) { if padLen < 0 { padLen = 2 } - padding := strings.Repeat(" ", padLen) - fmt.Printf(" %s%s%s\n", name, padding, formatCoverage(pkg.coverage)) + padding := repeatString(" ", padLen) + core.Println(core.Sprintf(" %s%s%s", name, padding, formatCoverage(pkg.coverage))) } // Print average @@ -155,13 +166,14 @@ func printCoverageSummary(results testResults) { if padLen < 0 { padLen = 2 } - padding := strings.Repeat(" ", padLen) - fmt.Printf("\n %s%s%s\n", testHeaderStyle.Render(avgLabel), padding, formatCoverage(avgCov)) + padding := repeatString(" ", padLen) + core.Println() + core.Println(core.Sprintf(" %s%s%s", testHeaderStyle.Render(avgLabel), padding, formatCoverage(avgCov))) } } func formatCoverage(cov float64) string { - s := fmt.Sprintf("%.1f%%", cov) + s := core.Sprintf("%.1f%%", cov) if cov >= 80 { return testCovHighStyle.Render(s) } else if cov >= 50 { @@ -172,41 +184,47 @@ func formatCoverage(cov float64) string { func shortenPackageName(name string) string { const modulePrefix = "dappco.re/go/" - if strings.HasPrefix(name, modulePrefix) { - remainder := strings.TrimPrefix(name, modulePrefix) - // If there's a sub-path (e.g. "go/pkg/foo"), strip the module name - if idx := strings.Index(remainder, "/"); idx >= 0 { - return remainder[idx+1:] + if core.HasPrefix(name, modulePrefix) { + remainder := core.TrimPrefix(name, modulePrefix) + parts := core.SplitN(remainder, "/", 2) + if len(parts) == 2 { + return parts[1] } // Module root (e.g. "cli-php") — return as-is return remainder } - return filepath.Base(name) + return core.PathBase(name) } func printJSONResults(results testResults, exitCode int) { - // Simple JSON output for agents - fmt.Printf("{\n") - fmt.Printf(" \"passed\": %d,\n", results.passed) - fmt.Printf(" \"failed\": %d,\n", results.failed) - fmt.Printf(" \"skipped\": %d,\n", results.skipped) + payload := struct { + Passed int `json:"passed"` + Failed int `json:"failed"` + Skipped int `json:"skipped"` + Coverage float64 `json:"coverage,omitempty"` + ExitCode int `json:"exit_code"` + FailedPackages []string `json:"failed_packages"` + }{ + Passed: results.passed, + Failed: results.failed, + Skipped: results.skipped, + ExitCode: exitCode, + FailedPackages: results.failedPkgs, + } if results.covCount > 0 { - avgCov := results.totalCov / float64(results.covCount) - fmt.Printf(" \"coverage\": %.1f,\n", avgCov) + payload.Coverage = results.totalCov / float64(results.covCount) } - fmt.Printf(" \"exit_code\": %d,\n", exitCode) - if len(results.failedPkgs) > 0 { - fmt.Printf(" \"failed_packages\": [\n") - for i, pkg := range results.failedPkgs { - comma := "," - if i == len(results.failedPkgs)-1 { - comma = "" - } - fmt.Printf(" %q%s\n", pkg, comma) - } - fmt.Printf(" ]\n") - } else { - fmt.Printf(" \"failed_packages\": []\n") - } - fmt.Printf("}\n") + core.Println(core.JSONMarshalString(payload)) +} + +func repeatString(part string, count int) string { + if count <= 0 { + return "" + } + + builder := core.NewBuilder() + for range count { + builder.WriteString(part) + } + return builder.String() } diff --git a/cmd/testcmd/cmd_runner.go b/cmd/testcmd/cmd_runner.go index d952b1b..6195458 100644 --- a/cmd/testcmd/cmd_runner.go +++ b/cmd/testcmd/cmd_runner.go @@ -2,20 +2,31 @@ package testcmd import ( "bufio" - "fmt" - "io" - "os" - "os/exec" + "context" "runtime" - "strings" + "sync" + core "dappco.re/go/core" "dappco.re/go/core/i18n" coreerr "dappco.re/go/core/log" + "dappco.re/go/core/process" +) + +var ( + processInitOnce sync.Once + processInitErr error ) func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error { + processInitOnce.Do(func() { + processInitErr = process.Init(core.New()) + }) + if processInitErr != nil { + return coreerr.E("cmd.test", i18n.T("i18n.fail.run", "tests"), processInitErr) + } + // Detect if we're in a Go project - if _, err := os.Stat("go.mod"); os.IsNotExist(err) { + if !(&core.Fs{}).New("/").Exists("go.mod") { return coreerr.E("cmd.test", i18n.T("cmd.test.error.no_go_mod"), nil) } @@ -47,45 +58,32 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo // Add package pattern args = append(args, pkg) - // Create command - cmd := exec.Command("go", args...) - cmd.Dir, _ = os.Getwd() - - // Set environment to suppress macOS linker warnings - cmd.Env = append(os.Environ(), getMacOSDeploymentTarget()) - if !jsonOutput { - fmt.Printf("%s %s\n", testHeaderStyle.Render(i18n.Label("test")), i18n.ProgressSubject("run", "tests")) - fmt.Printf(" %s %s\n", i18n.Label("package"), testDimStyle.Render(pkg)) + core.Println(core.Sprintf("%s %s", testHeaderStyle.Render(i18n.Label("test")), i18n.ProgressSubject("run", "tests"))) + core.Println(core.Sprintf(" %s %s", i18n.Label("package"), testDimStyle.Render(pkg))) if run != "" { - fmt.Printf(" %s %s\n", i18n.Label("filter"), testDimStyle.Render(run)) + core.Println(core.Sprintf(" %s %s", i18n.Label("filter"), testDimStyle.Render(run))) } - fmt.Println() + core.Println() } - // Capture output for parsing - var stdout, stderr strings.Builder - - if verbose && !jsonOutput { - // Stream output in verbose mode, but also capture for parsing - cmd.Stdout = io.MultiWriter(os.Stdout, &stdout) - cmd.Stderr = io.MultiWriter(os.Stderr, &stderr) - } else { - // Capture output for parsing - cmd.Stdout = &stdout - cmd.Stderr = &stderr + options := process.RunOptions{ + Command: "go", + Args: args, + Dir: core.Env("DIR_CWD"), + } + if target := getMacOSDeploymentTarget(); target != "" { + options.Env = []string{target} } - err := cmd.Run() - exitCode := 0 + proc, err := process.StartWithOptions(context.Background(), options) if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - exitCode = exitErr.ExitCode() - } + return coreerr.E("cmd.test", i18n.T("i18n.fail.run", "tests"), err) } - // Combine stdout and stderr for parsing, filtering linker warnings - combined := filterLinkerWarnings(stdout.String() + "\n" + stderr.String()) + waitErr := proc.Wait() + exitCode := proc.ExitCode + combined := filterLinkerWarnings(proc.Output()) // Parse results results := parseTestOutput(combined) @@ -104,16 +102,23 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo printTestSummary(results, coverage) } else if coverage { // In verbose mode, still show coverage summary at end - fmt.Println() + if combined != "" { + core.Println(combined) + } + core.Println() printCoverageSummary(results) + } else if combined != "" { + core.Println(combined) } if exitCode != 0 { - fmt.Printf("\n%s %s\n", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.tests_failed")) - return coreerr.E("cmd.test", i18n.T("i18n.fail.run", "tests"), nil) + core.Println() + core.Println(core.Sprintf("%s %s", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.tests_failed"))) + return coreerr.E("cmd.test", i18n.T("i18n.fail.run", "tests"), waitErr) } - fmt.Printf("\n%s %s\n", testPassStyle.Render(i18n.T("cli.pass")), i18n.T("common.result.all_passed")) + core.Println() + core.Println(core.Sprintf("%s %s", testPassStyle.Render(i18n.T("cli.pass")), i18n.T("common.result.all_passed"))) return nil } @@ -128,18 +133,18 @@ func getMacOSDeploymentTarget() string { func filterLinkerWarnings(output string) string { // Filter out ld: warning lines that pollute the output var filtered []string - scanner := bufio.NewScanner(strings.NewReader(output)) + scanner := bufio.NewScanner(core.NewReader(output)) for scanner.Scan() { line := scanner.Text() // Skip linker warnings - if strings.HasPrefix(line, "ld: warning:") { + if core.HasPrefix(line, "ld: warning:") { continue } // Skip test binary build comments - if strings.HasPrefix(line, "# ") && strings.HasSuffix(line, ".test") { + if core.HasPrefix(line, "# ") && core.HasSuffix(line, ".test") { continue } filtered = append(filtered, line) } - return strings.Join(filtered, "\n") + return core.Join("\n", filtered...) } diff --git a/crypt/chachapoly/chachapoly.go b/crypt/chachapoly/chachapoly.go index d5db0fa..733feaa 100644 --- a/crypt/chachapoly/chachapoly.go +++ b/crypt/chachapoly/chachapoly.go @@ -2,9 +2,9 @@ package chachapoly import ( "crypto/rand" - "fmt" "io" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "golang.org/x/crypto/chacha20poly1305" @@ -34,7 +34,7 @@ func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { minLen := aead.NonceSize() + aead.Overhead() if len(ciphertext) < minLen { - return nil, coreerr.E("chachapoly.Decrypt", fmt.Sprintf("ciphertext too short: got %d bytes, need at least %d bytes", len(ciphertext), minLen), nil) + return nil, coreerr.E("chachapoly.Decrypt", core.Sprintf("ciphertext too short: got %d bytes, need at least %d bytes", len(ciphertext), minLen), nil) } nonce, ciphertext := ciphertext[:aead.NonceSize()], ciphertext[aead.NonceSize():] diff --git a/crypt/chachapoly/chachapoly_test.go b/crypt/chachapoly/chachapoly_test.go index da5b96c..8e2c548 100644 --- a/crypt/chachapoly/chachapoly_test.go +++ b/crypt/chachapoly/chachapoly_test.go @@ -2,9 +2,9 @@ package chachapoly import ( "crypto/rand" - "errors" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" ) @@ -12,7 +12,7 @@ import ( type mockReader struct{} func (r *mockReader) Read(p []byte) (n int, err error) { - return 0, errors.New("read error") + return 0, core.NewError("read error") } func TestEncryptDecrypt_Good(t *testing.T) { diff --git a/crypt/checksum.go b/crypt/checksum.go index 7f5c7a7..80b8af5 100644 --- a/crypt/checksum.go +++ b/crypt/checksum.go @@ -5,17 +5,19 @@ import ( "crypto/sha512" "encoding/hex" "io" - "os" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) // SHA256File computes the SHA-256 checksum of a file and returns it as a hex string. func SHA256File(path string) (string, error) { - f, err := os.Open(path) - if err != nil { + openResult := (&core.Fs{}).New("/").Open(path) + if !openResult.OK { + err, _ := openResult.Value.(error) return "", coreerr.E("crypt.SHA256File", "failed to open file", err) } + f := openResult.Value.(io.ReadCloser) defer func() { _ = f.Close() }() h := sha256.New() @@ -28,10 +30,12 @@ func SHA256File(path string) (string, error) { // SHA512File computes the SHA-512 checksum of a file and returns it as a hex string. func SHA512File(path string) (string, error) { - f, err := os.Open(path) - if err != nil { + openResult := (&core.Fs{}).New("/").Open(path) + if !openResult.OK { + err, _ := openResult.Value.(error) return "", coreerr.E("crypt.SHA512File", "failed to open file", err) } + f := openResult.Value.(io.ReadCloser) defer func() { _ = f.Close() }() h := sha512.New() diff --git a/crypt/checksum_test.go b/crypt/checksum_test.go index 3c50c1e..03f92bb 100644 --- a/crypt/checksum_test.go +++ b/crypt/checksum_test.go @@ -1,10 +1,9 @@ package crypt import ( - "os" - "path/filepath" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -30,9 +29,9 @@ func TestSHA512Sum_Good(t *testing.T) { // TestSHA256FileEmpty_Good verifies checksum of an empty file. func TestSHA256FileEmpty_Good(t *testing.T) { tmpDir := t.TempDir() - emptyFile := filepath.Join(tmpDir, "empty.bin") - err := os.WriteFile(emptyFile, []byte{}, 0o644) - require.NoError(t, err) + emptyFile := core.Path(tmpDir, "empty.bin") + writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644) + require.Truef(t, writeResult.OK, "failed to write empty test file: %v", writeResult.Value) hash, err := SHA256File(emptyFile) require.NoError(t, err) @@ -43,9 +42,9 @@ func TestSHA256FileEmpty_Good(t *testing.T) { // TestSHA512FileEmpty_Good verifies SHA-512 checksum of an empty file. func TestSHA512FileEmpty_Good(t *testing.T) { tmpDir := t.TempDir() - emptyFile := filepath.Join(tmpDir, "empty.bin") - err := os.WriteFile(emptyFile, []byte{}, 0o644) - require.NoError(t, err) + emptyFile := core.Path(tmpDir, "empty.bin") + writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644) + require.Truef(t, writeResult.OK, "failed to write empty test file: %v", writeResult.Value) hash, err := SHA512File(emptyFile) require.NoError(t, err) @@ -69,9 +68,9 @@ func TestSHA512FileNonExistent_Bad(t *testing.T) { // TestSHA256FileWithContent_Good verifies checksum of a file with known content. func TestSHA256FileWithContent_Good(t *testing.T) { tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "test.txt") - err := os.WriteFile(testFile, []byte("hello"), 0o644) - require.NoError(t, err) + testFile := core.Path(tmpDir, "test.txt") + writeResult := (&core.Fs{}).New("/").WriteMode(testFile, "hello", 0o644) + require.Truef(t, writeResult.OK, "failed to write checksum fixture: %v", writeResult.Value) hash, err := SHA256File(testFile) require.NoError(t, err) diff --git a/crypt/hash.go b/crypt/hash.go index 80b2127..bd08003 100644 --- a/crypt/hash.go +++ b/crypt/hash.go @@ -3,9 +3,9 @@ package crypt import ( "crypto/subtle" "encoding/base64" - "fmt" - "strings" + "strconv" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "golang.org/x/crypto/argon2" @@ -25,7 +25,7 @@ func HashPassword(password string) (string, error) { b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) - encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + encoded := core.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argon2Memory, argon2Time, argon2Parallelism, b64Salt, b64Hash) @@ -35,20 +35,21 @@ 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. func VerifyPassword(password, hash string) (bool, error) { - parts := strings.Split(hash, "$") + parts := core.Split(hash, "$") if len(parts) != 6 { return false, coreerr.E("crypt.VerifyPassword", "invalid hash format", nil) } - var version int - if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil { + version, err := parsePrefixedInt(parts[2], "v=") + if err != nil { return false, coreerr.E("crypt.VerifyPassword", "failed to parse version", err) } + if version != argon2.Version { + return false, coreerr.E("crypt.VerifyPassword", core.Sprintf("unsupported argon2 version %d", version), nil) + } - var memory uint32 - var time uint32 - var parallelism uint8 - if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, ¶llelism); err != nil { + memory, time, parallelism, err := parseArgonParams(parts[3]) + if err != nil { return false, coreerr.E("crypt.VerifyPassword", "failed to parse parameters", err) } @@ -67,6 +68,52 @@ func VerifyPassword(password, hash string) (bool, error) { return subtle.ConstantTimeCompare(computedHash, expectedHash) == 1, nil } +func parseArgonParams(input string) (uint32, uint32, uint8, error) { + fields := core.Split(input, ",") + if len(fields) != 3 { + return 0, 0, 0, core.NewError("invalid argon2 parameters") + } + + memory, err := parsePrefixedUint32(fields[0], "m=") + if err != nil { + return 0, 0, 0, err + } + time, err := parsePrefixedUint32(fields[1], "t=") + if err != nil { + return 0, 0, 0, err + } + parallelismValue, err := parsePrefixedUint32(fields[2], "p=") + if err != nil { + return 0, 0, 0, err + } + + return memory, time, uint8(parallelismValue), nil +} + +func parsePrefixedInt(input, prefix string) (int, error) { + if !core.HasPrefix(input, prefix) { + return 0, core.NewError(core.Sprintf("missing %q prefix", prefix)) + } + + value, err := strconv.Atoi(core.TrimPrefix(input, prefix)) + if err != nil { + return 0, err + } + return value, nil +} + +func parsePrefixedUint32(input, prefix string) (uint32, error) { + if !core.HasPrefix(input, prefix) { + return 0, core.NewError(core.Sprintf("missing %q prefix", prefix)) + } + + value, err := strconv.ParseUint(core.TrimPrefix(input, prefix), 10, 32) + if err != nil { + return 0, err + } + return uint32(value), nil +} + // HashBcrypt hashes a password using bcrypt with the given cost. // Cost must be between bcrypt.MinCost and bcrypt.MaxCost. func HashBcrypt(password string, cost int) (string, error) { diff --git a/crypt/openpgp/service.go b/crypt/openpgp/service.go index cc63e26..5e1f85f 100644 --- a/crypt/openpgp/service.go +++ b/crypt/openpgp/service.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto" goio "io" - "strings" framework "dappco.re/go/core" coreerr "dappco.re/go/core/log" @@ -102,7 +101,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. func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) { - entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(recipientPath)) + entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(recipientPath)) if err != nil { return "", coreerr.E("openpgp.EncryptPGP", "failed to read recipient key", err) } @@ -137,7 +136,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. func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) { - entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(privateKey)) + entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(privateKey)) if err != nil { return "", coreerr.E("openpgp.DecryptPGP", "failed to read private key", err) } @@ -154,7 +153,7 @@ func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any } // Decrypt armored message - block, err := armor.Decode(strings.NewReader(message)) + block, err := armor.Decode(framework.NewReader(message)) if err != nil { return "", coreerr.E("openpgp.DecryptPGP", "failed to decode armored message", err) } diff --git a/crypt/rsa/rsa.go b/crypt/rsa/rsa.go index 93bc3be..a6534e9 100644 --- a/crypt/rsa/rsa.go +++ b/crypt/rsa/rsa.go @@ -6,8 +6,8 @@ import ( "crypto/sha256" "crypto/x509" "encoding/pem" - "fmt" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -24,7 +24,7 @@ func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err e const op = "rsa.GenerateKeyPair" if bits < 2048 { - return nil, nil, coreerr.E(op, fmt.Sprintf("key size too small: %d (minimum 2048)", bits), nil) + return nil, nil, coreerr.E(op, core.Sprintf("key size too small: %d (minimum 2048)", bits), nil) } privKey, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { diff --git a/crypt/rsa/rsa_test.go b/crypt/rsa/rsa_test.go index 7211995..8ee7460 100644 --- a/crypt/rsa/rsa_test.go +++ b/crypt/rsa/rsa_test.go @@ -6,9 +6,9 @@ import ( "crypto/rand" "crypto/x509" "encoding/pem" - "errors" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" ) @@ -16,7 +16,7 @@ import ( type mockReader struct{} func (r *mockReader) Read(p []byte) (n int, err error) { - return 0, errors.New("read error") + return 0, core.NewError("read error") } func TestRSA_Good(t *testing.T) { diff --git a/go.mod b/go.mod index ac2c01f..4a65ef2 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module dappco.re/go/core/crypt go 1.26.0 require ( - dappco.re/go/core v0.5.0 + dappco.re/go/core v0.8.0-alpha.1 dappco.re/go/core/i18n v0.2.0 dappco.re/go/core/io v0.2.0 dappco.re/go/core/log v0.1.0 + dappco.re/go/core/process v0.3.0 dappco.re/go/core/store v0.2.0 forge.lthn.ai/core/cli v0.3.7 github.com/ProtonMail/go-crypto v1.4.0 @@ -48,7 +49,6 @@ require ( github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/mod v0.34.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect diff --git a/go.sum b/go.sum index e5bd4d3..f34e190 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,13 @@ -dappco.re/go/core v0.5.0 h1:P5DJoaCiK5Q+af5UiTdWqUIW4W4qYKzpgGK50thm21U= -dappco.re/go/core v0.5.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= +dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= dappco.re/go/core/i18n v0.2.0 h1:NHzk6RCU93/qVRA3f2jvMr9P1R6FYheR/sHL+TnvKbI= dappco.re/go/core/i18n v0.2.0/go.mod h1:9eSVJXr3OpIGWQvDynfhqcp27xnLMwlYLgsByU+p7ok= dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4= dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E= dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc= dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= +dappco.re/go/core/process v0.3.0 h1:BPF9R79+8ZWe34qCIy/sZy+P4HwbaO95js2oPJL7IqM= +dappco.re/go/core/process v0.3.0/go.mod h1:qwx8kt6x+J9gn7fu8lavuess72Ye9jPBODqDZQ9K0as= dappco.re/go/core/store v0.2.0 h1:MH3R9m3mdr5T3lMWi37ryvTrXzF4xLBTYBGyNZF0p3I= dappco.re/go/core/store v0.2.0/go.mod h1:QQGJiruayjna3nywbf0N2gcO502q/oEkPoSpBpSKbLM= forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg= diff --git a/trust/approval.go b/trust/approval.go index fbf6bbd..e1f0495 100644 --- a/trust/approval.go +++ b/trust/approval.go @@ -1,11 +1,11 @@ package trust import ( - "fmt" "iter" "sync" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -31,7 +31,7 @@ func (s ApprovalStatus) String() string { case ApprovalDenied: return "denied" default: - return fmt.Sprintf("unknown(%d)", int(s)) + return core.Sprintf("unknown(%d)", int(s)) } } @@ -85,7 +85,7 @@ func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (strin defer q.mu.Unlock() q.nextID++ - id := fmt.Sprintf("approval-%d", q.nextID) + id := core.Sprintf("approval-%d", q.nextID) q.requests[id] = &ApprovalRequest{ ID: id, @@ -107,10 +107,10 @@ func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) err req, ok := q.requests[id] if !ok { - return coreerr.E("trust.ApprovalQueue.Approve", fmt.Sprintf("request %q not found", id), nil) + return coreerr.E("trust.ApprovalQueue.Approve", core.Sprintf("request %q not found", id), nil) } if req.Status != ApprovalPending { - return coreerr.E("trust.ApprovalQueue.Approve", fmt.Sprintf("request %q is already %s", id, req.Status), nil) + return coreerr.E("trust.ApprovalQueue.Approve", core.Sprintf("request %q is already %s", id, req.Status), nil) } req.Status = ApprovalApproved @@ -128,10 +128,10 @@ func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error req, ok := q.requests[id] if !ok { - return coreerr.E("trust.ApprovalQueue.Deny", fmt.Sprintf("request %q not found", id), nil) + return coreerr.E("trust.ApprovalQueue.Deny", core.Sprintf("request %q not found", id), nil) } if req.Status != ApprovalPending { - return coreerr.E("trust.ApprovalQueue.Deny", fmt.Sprintf("request %q is already %s", id, req.Status), nil) + return coreerr.E("trust.ApprovalQueue.Deny", core.Sprintf("request %q is already %s", id, req.Status), nil) } req.Status = ApprovalDenied diff --git a/trust/approval_test.go b/trust/approval_test.go index 23fe6f2..6e3c344 100644 --- a/trust/approval_test.go +++ b/trust/approval_test.go @@ -1,10 +1,10 @@ package trust import ( - "fmt" "sync" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -234,7 +234,7 @@ func TestApprovalConcurrent_Good(t *testing.T) { go func(idx int) { defer wg.Done() id, err := q.Submit( - fmt.Sprintf("agent-%d", idx), + core.Sprintf("agent-%d", idx), CapMergePR, "host-uk/core", ) diff --git a/trust/audit.go b/trust/audit.go index a9445f7..06b9032 100644 --- a/trust/audit.go +++ b/trust/audit.go @@ -1,12 +1,12 @@ package trust import ( - "encoding/json" "io" "iter" "sync" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -28,13 +28,20 @@ type AuditEntry struct { // MarshalJSON implements custom JSON encoding for Decision. func (d Decision) MarshalJSON() ([]byte, error) { - return json.Marshal(d.String()) + result := core.JSONMarshal(d.String()) + if !result.OK { + err, _ := result.Value.(error) + return nil, err + } + return result.Value.([]byte), nil } // UnmarshalJSON implements custom JSON decoding for Decision. func (d *Decision) UnmarshalJSON(data []byte) error { var s string - if err := json.Unmarshal(data, &s); err != nil { + result := core.JSONUnmarshal(data, &s) + if !result.OK { + err, _ := result.Value.(error) return err } switch s { @@ -82,11 +89,12 @@ func (l *AuditLog) Record(result EvalResult, repo string) error { l.entries = append(l.entries, entry) if l.writer != nil { - data, err := json.Marshal(entry) - if err != nil { + dataResult := core.JSONMarshal(entry) + if !dataResult.OK { + err, _ := dataResult.Value.(error) return coreerr.E("trust.AuditLog.Record", "marshal failed", err) } - data = append(data, '\n') + data := append(dataResult.Value.([]byte), '\n') if _, err := l.writer.Write(data); err != nil { return coreerr.E("trust.AuditLog.Record", "write failed", err) } diff --git a/trust/audit_test.go b/trust/audit_test.go index 2b459ed..23c9342 100644 --- a/trust/audit_test.go +++ b/trust/audit_test.go @@ -1,14 +1,11 @@ package trust import ( - "bytes" - "encoding/json" - "fmt" "io" - "strings" "sync" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -91,7 +88,7 @@ func TestAuditEntries_Good_AppendOnly(t *testing.T) { for i := range 5 { log.Record(EvalResult{ - Agent: fmt.Sprintf("agent-%d", i), + Agent: core.Sprintf("agent-%d", i), Cap: CapPushRepo, Decision: Allow, Reason: "ok", @@ -146,8 +143,8 @@ func TestAuditEntriesFor_Bad_NotFound(t *testing.T) { // --- Writer output --- func TestAuditRecord_Good_WritesToWriter(t *testing.T) { - var buf bytes.Buffer - log := NewAuditLog(&buf) + buf := core.NewBuilder() + log := NewAuditLog(buf) result := EvalResult{ Decision: Allow, @@ -160,11 +157,11 @@ func TestAuditRecord_Good_WritesToWriter(t *testing.T) { // Should have written a JSON line. output := buf.String() - assert.True(t, strings.HasSuffix(output, "\n")) + assert.True(t, core.HasSuffix(output, "\n")) var entry AuditEntry - err = json.Unmarshal([]byte(output), &entry) - require.NoError(t, err) + decodeResult := core.JSONUnmarshal([]byte(output), &entry) + require.Truef(t, decodeResult.OK, "failed to unmarshal audit entry: %v", decodeResult.Value) assert.Equal(t, "Athena", entry.Agent) assert.Equal(t, CapPushRepo, entry.Cap) assert.Equal(t, Allow, entry.Decision) @@ -172,26 +169,26 @@ func TestAuditRecord_Good_WritesToWriter(t *testing.T) { } func TestAuditRecord_Good_MultipleLines(t *testing.T) { - var buf bytes.Buffer - log := NewAuditLog(&buf) + buf := core.NewBuilder() + log := NewAuditLog(buf) for i := range 3 { log.Record(EvalResult{ - Agent: fmt.Sprintf("agent-%d", i), + Agent: core.Sprintf("agent-%d", i), Cap: CapPushRepo, Decision: Allow, Reason: "ok", }, "") } - lines := strings.Split(strings.TrimSpace(buf.String()), "\n") + lines := core.Split(core.Trim(buf.String()), "\n") assert.Len(t, lines, 3) // Each line should be valid JSON. for _, line := range lines { var entry AuditEntry - err := json.Unmarshal([]byte(line), &entry) - assert.NoError(t, err) + result := core.JSONUnmarshal([]byte(line), &entry) + assert.Truef(t, result.OK, "failed to unmarshal audit line: %v", result.Value) } } @@ -226,35 +223,37 @@ func TestDecisionJSON_Good_RoundTrip(t *testing.T) { expected := []string{`"deny"`, `"allow"`, `"needs_approval"`} for i, d := range decisions { - data, err := json.Marshal(d) - require.NoError(t, err) - assert.Equal(t, expected[i], string(data)) + result := core.JSONMarshal(d) + require.Truef(t, result.OK, "failed to marshal decision: %v", result.Value) + assert.Equal(t, expected[i], string(result.Value.([]byte))) var decoded Decision - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) + decodeResult := core.JSONUnmarshal(result.Value.([]byte), &decoded) + require.Truef(t, decodeResult.OK, "failed to unmarshal decision: %v", decodeResult.Value) assert.Equal(t, d, decoded) } } func TestDecisionJSON_Bad_UnknownString(t *testing.T) { var d Decision - err := json.Unmarshal([]byte(`"invalid"`), &d) + result := core.JSONUnmarshal([]byte(`"invalid"`), &d) + err, _ := result.Value.(error) assert.Error(t, err) assert.Contains(t, err.Error(), "unknown decision") } func TestDecisionJSON_Bad_NonString(t *testing.T) { var d Decision - err := json.Unmarshal([]byte(`42`), &d) + result := core.JSONUnmarshal([]byte(`42`), &d) + err, _ := result.Value.(error) assert.Error(t, err) } // --- Concurrent audit logging --- func TestAuditConcurrent_Good(t *testing.T) { - var buf bytes.Buffer - log := NewAuditLog(&buf) + buf := core.NewBuilder() + log := NewAuditLog(buf) const n = 10 var wg sync.WaitGroup @@ -264,7 +263,7 @@ func TestAuditConcurrent_Good(t *testing.T) { go func(idx int) { defer wg.Done() log.Record(EvalResult{ - Agent: fmt.Sprintf("agent-%d", idx), + Agent: core.Sprintf("agent-%d", idx), Cap: CapPushRepo, Decision: Allow, Reason: "ok", @@ -279,8 +278,8 @@ func TestAuditConcurrent_Good(t *testing.T) { // --- Integration: PolicyEngine + AuditLog --- func TestAuditPolicyIntegration_Good(t *testing.T) { - var buf bytes.Buffer - log := NewAuditLog(&buf) + buf := core.NewBuilder() + log := NewAuditLog(buf) pe := newTestEngine(t) // Evaluate and record diff --git a/trust/bench_test.go b/trust/bench_test.go index 6314567..142236d 100644 --- a/trust/bench_test.go +++ b/trust/bench_test.go @@ -1,8 +1,9 @@ package trust import ( - "fmt" "testing" + + core "dappco.re/go/core" ) // BenchmarkPolicyEvaluate measures policy evaluation across 100 registered agents. @@ -17,7 +18,7 @@ func BenchmarkPolicyEvaluate(b *testing.B) { tier = TierVerified } _ = r.Register(Agent{ - Name: fmt.Sprintf("agent-%d", i), + Name: core.Sprintf("agent-%d", i), Tier: tier, ScopedRepos: []string{"host-uk/core", "host-uk/docs"}, }) @@ -32,7 +33,7 @@ func BenchmarkPolicyEvaluate(b *testing.B) { b.ResetTimer() for i := range b.N { - agentName := fmt.Sprintf("agent-%d", i%100) + agentName := core.Sprintf("agent-%d", i%100) cap := caps[i%len(caps)] _ = pe.Evaluate(agentName, cap, "host-uk/core") } @@ -43,14 +44,14 @@ func BenchmarkRegistryGet(b *testing.B) { r := NewRegistry() for i := range 100 { _ = r.Register(Agent{ - Name: fmt.Sprintf("agent-%d", i), + Name: core.Sprintf("agent-%d", i), Tier: TierVerified, }) } b.ResetTimer() for i := range b.N { - name := fmt.Sprintf("agent-%d", i%100) + name := core.Sprintf("agent-%d", i%100) _ = r.Get(name) } } @@ -62,7 +63,7 @@ func BenchmarkRegistryRegister(b *testing.B) { b.ResetTimer() for i := range b.N { _ = r.Register(Agent{ - Name: fmt.Sprintf("bench-agent-%d", i), + Name: core.Sprintf("bench-agent-%d", i), Tier: TierVerified, }) } diff --git a/trust/config.go b/trust/config.go index 6f9a593..ea2618c 100644 --- a/trust/config.go +++ b/trust/config.go @@ -1,11 +1,9 @@ package trust import ( - "encoding/json" - "fmt" "io" - "os" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -24,20 +22,31 @@ type PoliciesConfig struct { // LoadPoliciesFromFile reads a JSON file and returns parsed policies. func LoadPoliciesFromFile(path string) ([]Policy, error) { - f, err := os.Open(path) - if err != nil { + openResult := (&core.Fs{}).New("/").Open(path) + if !openResult.OK { + err, _ := openResult.Value.(error) return nil, coreerr.E("trust.LoadPoliciesFromFile", "failed to open file", err) } - defer f.Close() - return LoadPolicies(f) + return LoadPolicies(openResult.Value.(io.Reader)) } // LoadPolicies reads JSON from a reader and returns parsed policies. func LoadPolicies(r io.Reader) ([]Policy, error) { + readResult := core.ReadAll(r) + if !readResult.OK { + err, _ := readResult.Value.(error) + return nil, coreerr.E("trust.LoadPolicies", "failed to decode JSON", err) + } + + data := []byte(readResult.Value.(string)) + if err := validatePoliciesJSON(data); err != nil { + return nil, coreerr.E("trust.LoadPolicies", "failed to decode JSON", err) + } + var cfg PoliciesConfig - dec := json.NewDecoder(r) - dec.DisallowUnknownFields() - if err := dec.Decode(&cfg); err != nil { + decodeResult := core.JSONUnmarshal(data, &cfg) + if !decodeResult.OK { + err, _ := decodeResult.Value.(error) return nil, coreerr.E("trust.LoadPolicies", "failed to decode JSON", err) } return convertPolicies(cfg) @@ -50,7 +59,7 @@ func convertPolicies(cfg PoliciesConfig) ([]Policy, error) { for i, pc := range cfg.Policies { tier := Tier(pc.Tier) if !tier.Valid() { - return nil, coreerr.E("trust.LoadPolicies", fmt.Sprintf("invalid tier %d at index %d", pc.Tier, i), nil) + return nil, coreerr.E("trust.LoadPolicies", core.Sprintf("invalid tier %d at index %d", pc.Tier, i), nil) } p := Policy{ @@ -82,12 +91,12 @@ func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error { // ApplyPoliciesFromFile loads policies from a JSON file and sets them on the engine. func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error { - f, err := os.Open(path) - if err != nil { + openResult := (&core.Fs{}).New("/").Open(path) + if !openResult.OK { + err, _ := openResult.Value.(error) return coreerr.E("trust.ApplyPoliciesFromFile", "failed to open file", err) } - defer f.Close() - return pe.ApplyPolicies(f) + return pe.ApplyPolicies(openResult.Value.(io.Reader)) } // ExportPolicies serialises the current policies as JSON to the given writer. @@ -106,14 +115,66 @@ func (pe *PolicyEngine) ExportPolicies(w io.Writer) error { }) } - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - if err := enc.Encode(cfg); err != nil { + dataResult := core.JSONMarshal(cfg) + if !dataResult.OK { + err, _ := dataResult.Value.(error) + return coreerr.E("trust.ExportPolicies", "failed to encode JSON", err) + } + if _, err := w.Write(dataResult.Value.([]byte)); err != nil { return coreerr.E("trust.ExportPolicies", "failed to encode JSON", err) } return nil } +func validatePoliciesJSON(data []byte) error { + var raw map[string]any + + result := core.JSONUnmarshal(data, &raw) + if !result.OK { + err, _ := result.Value.(error) + return err + } + + for key := range raw { + if key != "policies" { + return core.NewError(core.Sprintf("json: unknown field %q", key)) + } + } + + rawPolicies, ok := raw["policies"] + if !ok { + return nil + } + + policies, ok := rawPolicies.([]any) + if !ok { + return nil + } + + for _, rawPolicy := range policies { + fields, ok := rawPolicy.(map[string]any) + if !ok { + continue + } + for key := range fields { + if !isKnownPolicyConfigKey(key) { + return core.NewError(core.Sprintf("json: unknown field %q", key)) + } + } + } + + return nil +} + +func isKnownPolicyConfigKey(key string) bool { + switch key { + case "tier", "allowed", "requires_approval", "denied": + return true + default: + return false + } +} + // toCapabilities converts string slices to Capability slices. func toCapabilities(ss []string) []Capability { if len(ss) == 0 { diff --git a/trust/config_test.go b/trust/config_test.go index ef7f970..72825ba 100644 --- a/trust/config_test.go +++ b/trust/config_test.go @@ -1,13 +1,9 @@ package trust import ( - "bytes" - "encoding/json" - "os" - "path/filepath" - "strings" "testing" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -35,13 +31,13 @@ const validPolicyJSON = `{ // --- LoadPolicies --- func TestLoadPolicies_Good(t *testing.T) { - policies, err := LoadPolicies(strings.NewReader(validPolicyJSON)) + policies, err := LoadPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) assert.Len(t, policies, 3) } func TestLoadPolicies_Good_FieldMapping(t *testing.T) { - policies, err := LoadPolicies(strings.NewReader(validPolicyJSON)) + policies, err := LoadPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) // Tier 3 @@ -66,33 +62,33 @@ func TestLoadPolicies_Good_FieldMapping(t *testing.T) { func TestLoadPolicies_Good_EmptyPolicies(t *testing.T) { input := `{"policies": []}` - policies, err := LoadPolicies(strings.NewReader(input)) + policies, err := LoadPolicies(core.NewReader(input)) require.NoError(t, err) assert.Empty(t, policies) } func TestLoadPolicies_Bad_InvalidJSON(t *testing.T) { - _, err := LoadPolicies(strings.NewReader(`{invalid`)) + _, err := LoadPolicies(core.NewReader(`{invalid`)) assert.Error(t, err) } func TestLoadPolicies_Bad_InvalidTier(t *testing.T) { input := `{"policies": [{"tier": 0, "allowed": ["repo.push"]}]}` - _, err := LoadPolicies(strings.NewReader(input)) + _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } func TestLoadPolicies_Bad_TierTooHigh(t *testing.T) { input := `{"policies": [{"tier": 99, "allowed": ["repo.push"]}]}` - _, err := LoadPolicies(strings.NewReader(input)) + _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } func TestLoadPolicies_Bad_UnknownField(t *testing.T) { input := `{"policies": [{"tier": 1, "allowed": ["repo.push"], "bogus": true}]}` - _, err := LoadPolicies(strings.NewReader(input)) + _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err, "DisallowUnknownFields should reject unknown fields") } @@ -100,9 +96,8 @@ func TestLoadPolicies_Bad_UnknownField(t *testing.T) { func TestLoadPoliciesFromFile_Good(t *testing.T) { dir := t.TempDir() - path := filepath.Join(dir, "policies.json") - err := os.WriteFile(path, []byte(validPolicyJSON), 0644) - require.NoError(t, err) + path := core.Path(dir, "policies.json") + writePolicyFile(t, path, validPolicyJSON) policies, err := LoadPoliciesFromFile(path) require.NoError(t, err) @@ -122,7 +117,7 @@ func TestApplyPolicies_Good(t *testing.T) { pe := NewPolicyEngine(r) // Apply custom policies from JSON - err := pe.ApplyPolicies(strings.NewReader(validPolicyJSON)) + err := pe.ApplyPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) // Verify the Tier 2 policy was replaced @@ -144,7 +139,7 @@ func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) - err := pe.ApplyPolicies(strings.NewReader(`{invalid`)) + err := pe.ApplyPolicies(core.NewReader(`{invalid`)) assert.Error(t, err) } @@ -153,7 +148,7 @@ func TestApplyPolicies_Bad_InvalidTier(t *testing.T) { pe := NewPolicyEngine(r) input := `{"policies": [{"tier": 0, "allowed": ["repo.push"]}]}` - err := pe.ApplyPolicies(strings.NewReader(input)) + err := pe.ApplyPolicies(core.NewReader(input)) assert.Error(t, err) } @@ -161,15 +156,14 @@ func TestApplyPolicies_Bad_InvalidTier(t *testing.T) { func TestApplyPoliciesFromFile_Good(t *testing.T) { dir := t.TempDir() - path := filepath.Join(dir, "policies.json") - err := os.WriteFile(path, []byte(validPolicyJSON), 0644) - require.NoError(t, err) + path := core.Path(dir, "policies.json") + writePolicyFile(t, path, validPolicyJSON) r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "A", Tier: TierFull})) pe := NewPolicyEngine(r) - err = pe.ApplyPoliciesFromFile(path) + err := pe.ApplyPoliciesFromFile(path) require.NoError(t, err) // Verify Tier 3 was replaced — only 3 allowed caps now @@ -191,14 +185,14 @@ func TestExportPolicies_Good(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) // loads defaults - var buf bytes.Buffer - err := pe.ExportPolicies(&buf) + buf := core.NewBuilder() + err := pe.ExportPolicies(buf) require.NoError(t, err) // Output should be valid JSON var cfg PoliciesConfig - err = json.Unmarshal(buf.Bytes(), &cfg) - require.NoError(t, err) + result := core.JSONUnmarshalString(buf.String(), &cfg) + require.Truef(t, result.OK, "failed to unmarshal exported policies: %v", result.Value) assert.Len(t, cfg.Policies, 3) } @@ -208,15 +202,15 @@ func TestExportPolicies_Good_RoundTrip(t *testing.T) { pe := NewPolicyEngine(r) // Export - var buf bytes.Buffer - err := pe.ExportPolicies(&buf) + buf := core.NewBuilder() + err := pe.ExportPolicies(buf) require.NoError(t, err) // Create a new engine and apply the exported policies r2 := NewRegistry() require.NoError(t, r2.Register(Agent{Name: "A", Tier: TierFull})) pe2 := NewPolicyEngine(r2) - err = pe2.ApplyPolicies(strings.NewReader(buf.String())) + err = pe2.ApplyPolicies(core.NewReader(buf.String())) require.NoError(t, err) // Evaluations should produce the same results @@ -229,6 +223,13 @@ func TestExportPolicies_Good_RoundTrip(t *testing.T) { } } +func writePolicyFile(t *testing.T, path, content string) { + t.Helper() + + result := (&core.Fs{}).New("/").WriteMode(path, content, 0o644) + require.Truef(t, result.OK, "failed to write %s: %v", path, result.Value) +} + // --- Helper conversion --- func TestToCapabilities_Good(t *testing.T) { diff --git a/trust/policy.go b/trust/policy.go index 85ac374..bc72760 100644 --- a/trust/policy.go +++ b/trust/policy.go @@ -1,10 +1,9 @@ package trust import ( - "fmt" "slices" - "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -48,7 +47,7 @@ func (d Decision) String() string { case NeedsApproval: return "needs_approval" default: - return fmt.Sprintf("unknown(%d)", int(d)) + return core.Sprintf("unknown(%d)", int(d)) } } @@ -90,7 +89,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Deny, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("no policy for tier %s", agent.Tier), + Reason: core.Sprintf("no policy for tier %s", agent.Tier), } } @@ -100,7 +99,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Deny, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("capability %s is denied for tier %s", cap, agent.Tier), + Reason: core.Sprintf("capability %s is denied for tier %s", cap, agent.Tier), } } @@ -110,7 +109,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: NeedsApproval, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("capability %s requires approval for tier %s", cap, agent.Tier), + Reason: core.Sprintf("capability %s requires approval for tier %s", cap, agent.Tier), } } @@ -124,7 +123,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Deny, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("agent %q does not have access to repo %q", agentName, repo), + Reason: core.Sprintf("agent %q does not have access to repo %q", agentName, repo), } } } @@ -132,7 +131,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Allow, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("capability %s allowed for tier %s", cap, agent.Tier), + Reason: core.Sprintf("capability %s allowed for tier %s", cap, agent.Tier), } } } @@ -141,14 +140,14 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) Decision: Deny, Agent: agentName, Cap: cap, - Reason: fmt.Sprintf("capability %s not granted for tier %s", cap, agent.Tier), + Reason: core.Sprintf("capability %s not granted for tier %s", cap, agent.Tier), } } // SetPolicy replaces the policy for a given tier. func (pe *PolicyEngine) SetPolicy(p Policy) error { if !p.Tier.Valid() { - return coreerr.E("trust.SetPolicy", fmt.Sprintf("invalid tier %d", p.Tier), nil) + return coreerr.E("trust.SetPolicy", core.Sprintf("invalid tier %d", p.Tier), nil) } pe.policies[p.Tier] = &p return nil @@ -218,8 +217,8 @@ func (pe *PolicyEngine) loadDefaults() { // isRepoScoped returns true if the capability is constrained by repo scope. func isRepoScoped(cap Capability) bool { - return strings.HasPrefix(string(cap), "repo.") || - strings.HasPrefix(string(cap), "pr.") || + return core.HasPrefix(string(cap), "repo.") || + core.HasPrefix(string(cap), "pr.") || cap == CapReadSecrets } @@ -248,14 +247,14 @@ func matchScope(pattern, repo string) bool { } // Check for wildcard patterns. - if !strings.Contains(pattern, "*") { + if !core.Contains(pattern, "*") { return false } // "prefix/**" — recursive: matches anything under prefix/. - if strings.HasSuffix(pattern, "/**") { + if core.HasSuffix(pattern, "/**") { prefix := pattern[:len(pattern)-3] // strip "/**" - if !strings.HasPrefix(repo, prefix+"/") { + if !core.HasPrefix(repo, prefix+"/") { return false } // Must have something after the prefix/. @@ -263,14 +262,14 @@ func matchScope(pattern, repo string) bool { } // "prefix/*" — single level: matches prefix/X but not prefix/X/Y. - if strings.HasSuffix(pattern, "/*") { + if core.HasSuffix(pattern, "/*") { prefix := pattern[:len(pattern)-2] // strip "/*" - if !strings.HasPrefix(repo, prefix+"/") { + if !core.HasPrefix(repo, prefix+"/") { return false } remainder := repo[len(prefix)+1:] // Must have a non-empty name, and no further slashes. - return remainder != "" && !strings.Contains(remainder, "/") + return remainder != "" && !core.Contains(remainder, "/") } // Unsupported wildcard position — fall back to no match. diff --git a/trust/trust.go b/trust/trust.go index 3b32de0..8cc205f 100644 --- a/trust/trust.go +++ b/trust/trust.go @@ -11,11 +11,11 @@ package trust import ( - "fmt" "iter" "sync" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" ) @@ -41,7 +41,7 @@ func (t Tier) String() string { case TierFull: return "full" default: - return fmt.Sprintf("unknown(%d)", int(t)) + return core.Sprintf("unknown(%d)", int(t)) } } @@ -102,7 +102,7 @@ func (r *Registry) Register(agent Agent) error { return coreerr.E("trust.Register", "agent name is required", nil) } if !agent.Tier.Valid() { - return coreerr.E("trust.Register", fmt.Sprintf("invalid tier %d for agent %q", agent.Tier, agent.Name), nil) + return coreerr.E("trust.Register", core.Sprintf("invalid tier %d for agent %q", agent.Tier, agent.Name), nil) } if agent.CreatedAt.IsZero() { agent.CreatedAt = time.Now() diff --git a/trust/trust_test.go b/trust/trust_test.go index e323110..303facc 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -1,11 +1,11 @@ package trust import ( - "fmt" "sync" "testing" "time" + core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -196,7 +196,7 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - name := fmt.Sprintf("agent-%d", idx) + name := core.Sprintf("agent-%d", idx) err := r.Register(Agent{Name: name, Tier: TierVerified}) assert.NoError(t, err) }(i) @@ -206,7 +206,7 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - name := fmt.Sprintf("agent-%d", idx) + name := core.Sprintf("agent-%d", idx) _ = r.Get(name) // Just exercise the read path }(i) } @@ -215,7 +215,7 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) { for i := range n { go func(idx int) { defer wg.Done() - name := fmt.Sprintf("agent-%d", idx) + name := core.Sprintf("agent-%d", idx) _ = r.Remove(name) }(i) } @@ -281,7 +281,7 @@ func TestConcurrentListDuringMutations_Good(t *testing.T) { // Pre-populate for i := range 5 { require.NoError(t, r.Register(Agent{ - Name: fmt.Sprintf("base-%d", i), + Name: core.Sprintf("base-%d", i), Tier: TierFull, })) } @@ -302,7 +302,7 @@ func TestConcurrentListDuringMutations_Good(t *testing.T) { for i := range 10 { go func(idx int) { defer wg.Done() - name := fmt.Sprintf("concurrent-%d", idx) + name := core.Sprintf("concurrent-%d", idx) _ = r.Register(Agent{Name: name, Tier: TierUntrusted}) }(i) }