This document provides guidelines for both AI coding assistants (Claude, Copilot, Cursor, etc.) and human developers working on the AgentRegistry codebase.
AgentRegistry is a centralized registry for securely curating, discovering, deploying, and managing agentic infrastructure including MCP (Model Context Protocol) servers, agents, and skills.
Tech Stack:
- Backend/CLI: Go 1.25+
- Database: PostgreSQL with pgvector (accessed via pgx)
- Frontend: Next.js 14 (App Router) with Tailwind CSS
- CLI Framework: Cobra
- API Framework: Huma (OpenAPI)
agentregistry/
├── cmd/ # Entry points only - minimal code
│ ├── cli/ # CLI entry point
│ └── server/ # Server entry point
├── pkg/ # Public, reusable packages
├── internal/ # Private implementation
│ ├── registry/ # Core registry implementation
│ │ ├── api/ # HTTP handlers
│ │ ├── database/ # Database layer (pgx)
│ │ ├── service/ # Business logic
│ │ └── ...
│ ├── cli/ # CLI command implementations
│ ├── mcp/ # MCP protocol handling
│ └── daemon/ # Daemon orchestration
├── ui/ # Next.js frontend
└── docker/ # Container configurations
- cmd/ - Entry points only. Delegate immediately to pkg/ or internal/
- pkg/ - Public APIs for external consumption and reusability
- internal/ - Implementation details, not importable by external packages
- internal/registry/database/ - ONLY place that accesses the database directly
- internal/registry/service/ - Business logic, receives database interface via constructor
Deployment/platform code should be organized by clear ownership, not by vague helper layers:
internal/registry/platforms/<platform>/owns platform behavior - local and kubernetes packages should contain their adapter plus the concrete platform-specific materialization/apply/discovery logic they needinternal/registry/platforms/utils/is for narrowly shared deployment utilities only - use it for adapter-shared materialization helpers, validation, name generation, and request parsing that are truly cross-platforminternal/registry/platforms/types/is for shared contracts only - keep shared schemas and DTO-style platform types here, not behavior-heavy logicinternal/registry/api/handlers/is transport only - HTTP handlers should parse requests, call services/adapters, and map errors; they should not own deployment/platform behaviorinternal/registry/registry_app.gois the composition root - wire concrete platform adapters here explicitly instead of hiding registration/factory behavior in handler packages
Avoid introducing broad "translator" layers or catch-all shared packages when the code really belongs to one concrete platform. Prefer concentrated platform packages with small, explicit shared utilities.
The database MUST only be accessed by:
- The database layer (
internal/registry/database/) - The authorizer component
No other component should have direct database access. All data operations must go through the appropriate interfaces.
// CORRECT: Service receives database interface
type RegistryService struct {
db DatabaseInterface
}
func NewRegistryService(db DatabaseInterface) *RegistryService {
return &RegistryService{db: db}
}
// INCORRECT: Service creates its own database connection
type RegistryService struct {
db *pgxpool.Pool // Direct database access - DO NOT DO THIS
}Authz is enforced at the database layer by default — every store method calls s.authz.Check(...) before the query. Services don't normally need to invoke authz themselves; the DB call fails with auth.ErrForbidden when a caller lacks permission.
When to gate at the API or service layer instead: only when the operation doesn't reach the DB with a check. Current cases:
- External platform calls with no downstream DB write — e.g.
UndeployDeploymentandCancelDeploymenthit adapters before any DB update, so the gate has to fire in the service before the adapter call. - Admin-scope handlers with no per-resource authz — e.g.
POST /v0/embeddings/indexis gated onIsRegistryAdminin the handler.
List operations intentionally skip per-row authz checks. The DB's List* methods return what matches the SQL filter; they do not invoke authz.Check per row. The AuthzProvider interface only gates single-resource operations (Check, IsRegistryAdmin) — it has no row-filter hook. Per-row visibility filtering for Lists would require a custom database.Store implementation wired in at the composition root (registry_app.go), either joining against a permissions table in SQL or calling authz.Check per row.
Prefer DB-layer gates. If you add an API-layer gate, document the reason in the handler or service comment.
See docs/auth/authz-matrix.md for the per-endpoint permission table.
Every significant component must define an interface for its dependencies. This enables:
- Unit testing with mocks
- Loose coupling between packages
- Clear contract definitions
// Define interfaces for dependencies
type AgentRepository interface {
GetAgent(ctx context.Context, id string) (*Agent, error)
ListAgents(ctx context.Context, opts ListOptions) ([]Agent, error)
CreateAgent(ctx context.Context, agent *Agent) error
}
// Implementation receives interface, not concrete type
type AgentService struct {
repo AgentRepository
}Each package and file should have one clear purpose. Signs of mixed responsibilities:
- Files over 500 lines
- Packages importing many unrelated dependencies
- Functions doing multiple unrelated things
Split large components into focused units.
Use standard Go error patterns with wrapping:
import (
"errors"
"fmt"
)
// Wrap errors with context
func (s *Service) GetAgent(ctx context.Context, id string) (*Agent, error) {
agent, err := s.repo.GetAgent(ctx, id)
if err != nil {
return nil, fmt.Errorf("getting agent %s: %w", id, err)
}
return agent, nil
}
// Check error types
if errors.Is(err, ErrNotFound) {
// handle not found
}
// Define sentinel errors for domain-specific cases
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)Consistency requirements:
- Always wrap errors with
fmt.Errorf("context: %w", err) - Use lowercase error messages (they may be wrapped)
- Define sentinel errors for cases that callers need to check
Use the github.com/agentregistry-dev/agentregistry/pkg/logging package for structured logging. Loggers should be package scoped in most cases, but the global logger can be directly used via the slog package if necessary. logging.New keeps track of slog.LevelVar to allow log levels to be changed at runtime via the /logging HTTP endpoint, so calling logging.New* within a re-entrant function will leak memory and should be avoided. If logging.New* is invoked within a re-entrant function, the tracked leveler should be explicitly deleted by a call to logging.DeleteLeveler("component-name") before the function returns.
import (
"log/slog"
"github.com/agentregistry-dev/agentregistry/pkg/logging"
)
var logger = logging.New("my-component")
// package scoped logger
logger.Info("agent created",
"agent_id", agent.ID,
"name", agent.Name,
)
// global logger
slog.Error("failed to create agent",
"error", err,
"agent_name", name,
)The codebase needs significantly more test coverage. When adding or modifying code:
- Write unit tests with mocks for business logic
- Write table-driven tests for functions with multiple cases
- Write integration tests for database and API operations
func TestValidateAgentName(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"valid name", "my-agent", false},
{"empty name", "", true},
{"too long", strings.Repeat("a", 256), true},
{"special chars", "my@agent", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateAgentName(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateAgentName(%q) error = %v, wantErr %v",
tt.input, err, tt.wantErr)
}
})
}
}type mockAgentRepo struct {
agents map[string]*Agent
}
func (m *mockAgentRepo) GetAgent(ctx context.Context, id string) (*Agent, error) {
if agent, ok := m.agents[id]; ok {
return agent, nil
}
return nil, ErrNotFound
}
func TestAgentService_GetAgent(t *testing.T) {
repo := &mockAgentRepo{
agents: map[string]*Agent{
"agent-1": {ID: "agent-1", Name: "Test Agent"},
},
}
svc := NewAgentService(repo)
agent, err := svc.GetAgent(context.Background(), "agent-1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if agent.Name != "Test Agent" {
t.Errorf("got name %q, want %q", agent.Name, "Test Agent")
}
}# Run unit tests
make test
# Run with coverage
make test-coverage
# Run with HTML coverage report
make test-coverage-report
# Run integration tests
make test-integrationUse manual constructor injection. No DI frameworks.
// Define what you need
type AgentService struct {
repo AgentRepository
logger *slog.Logger
}
// Accept dependencies via constructor
func NewAgentService(repo AgentRepository, logger *slog.Logger) *AgentService {
return &AgentService{
repo: repo,
logger: logger,
}
}
// Wire up in main or initialization code
func main() {
db := database.NewPostgresDB(connString)
logger := slog.Default()
agentRepo := database.NewAgentRepository(db)
agentSvc := service.NewAgentService(agentRepo, logger)
// ...
}Use Cobra for CLI commands. Follow existing patterns in internal/cli/.
var agentCmd = &cobra.Command{
Use: "agent",
Short: "Manage agents",
}
var agentListCmd = &cobra.Command{
Use: "list",
Short: "List all agents",
RunE: func(cmd *cobra.Command, args []string) error {
// Implementation
return nil
},
}
func init() {
agentCmd.AddCommand(agentListCmd)
}Use the printer package (pkg/printer) for user-facing output instead of raw fmt.Printf:
printer.PrintSuccess("Added skill 'my-skill' to agent.yaml")
printer.PrintInfo("Processing...")
printer.PrintError("something went wrong")Use Huma for REST APIs. Huma generates OpenAPI documentation automatically.
// Define input/output types
type GetAgentInput struct {
ID string `path:"id"`
}
type AgentOutput struct {
Body Agent
}
// Register routes
huma.Get(api, "/agents/{id}", func(ctx context.Context, input *GetAgentInput) (*AgentOutput, error) {
agent, err := svc.GetAgent(ctx, input.ID)
if err != nil {
return nil, err
}
return &AgentOutput{Body: *agent}, nil
})Use Next.js App Router patterns with React Server Components where appropriate.
// app/agents/page.tsx
export default async function AgentsPage() {
const agents = await fetchAgents();
return (
<div>
{agents.map(agent => (
<AgentCard key={agent.id} agent={agent} />
))}
</div>
);
}Use shadcn/ui components. Check ui/components/ui/ for available components.
When working with this codebase, AI assistants should:
- Read before writing - Always read existing code before suggesting modifications
- Follow existing patterns - Match the style of surrounding code
- Add tests - Include tests for any new functionality
- Use interfaces - Define interfaces for new dependencies
- Keep changes minimal - Only modify what's necessary for the task
- Check database access - Ensure database operations go through proper layers
- Access database directly - Never add direct database access outside the database layer
- Create god objects - Keep components focused and small
- Skip error handling - Always handle and wrap errors appropriately
- Add unnecessary abstractions - Don't over-engineer solutions
- Ignore existing interfaces - Use defined interfaces, don't bypass them
- Create new files unnecessarily - Prefer editing existing files
- Check if similar functionality exists
- Identify the appropriate layer (api/service/database)
- Define interfaces for new dependencies
- Implement with proper error handling
- Add unit tests with mocks
- Add integration tests if touching database/API
- Update any affected documentation
- Write a failing test that reproduces the bug
- Fix the bug with minimal changes
- Verify the test passes
- Check for similar issues elsewhere
- Don't refactor unrelated code
- Database access only through database layer or authorizer
- New dependencies injected via constructor
- Interfaces defined for mockability
- Errors wrapped with context
- Unit tests with mocks included
- Table-driven tests for multiple cases
- No mixed responsibilities in components
- No hardcoded values that should be configurable
| Task | Command |
|---|---|
| Build CLI | make build-cli |
| Build Server | make build-server |
| Run Unit Tests | make test-unit |
| Run all Tests | make test |
| Run Tests w/ Coverage | make test-coverage |
| Coverage HTML Report | make test-coverage-report |
| Run Linter | make lint |
| Format Code | make fmt |
| Build UI | make build-ui |
| Dev UI | make dev-ui |
| Daemon Start | make daemon-start |
| Daemon Stop | make daemon-stop |
- README.md - Project overview and quick start
- DEVELOPMENT.md - Architecture details
- CONTRIBUTING.md - Contribution guidelines
- docs/auth/authz-matrix.md - AuthZ matrix for HTTP endpoints