Developer Guide¶
This guide covers setting up a development environment, building, testing, and extending AgentLens.
Table of Contents¶
- Prerequisites
- Getting Started
- Project Structure
- Makefile Targets
- Building
- Testing
- Linting
- Git Hooks
- Writing Plugins
- Frontend Development
- Docker
- Helm Chart
- CI/CD
- Code Style
Prerequisites¶
- Go 1.26.1 (with CGO enabled for SQLite)
- Bun 1.3+ (install via bun.sh)
- golangci-lint (install via
make tools) - Docker (for container builds and scanning)
- Helm 3+ (for chart development)
Getting Started¶
# Clone the repository
git clone https://github.com/PawelHaracz/agentlens
cd agentlens
# Install Go dependencies
make deps
# Install frontend dependencies
make web-install
# Build everything
make web-build
make build
# Run the server
make run
The server starts on http://localhost:8080.
Project Structure¶
agentlens/
├── cmd/agentlens/ # Application entrypoint (main.go)
├── internal/ # Private application packages
│ ├── api/ # REST API handlers and router
│ ├── config/ # Configuration loading and env overrides
│ ├── discovery/ # Discovery manager and source orchestration
│ ├── health/ # Health checker logic
│ ├── kernel/ # Microkernel core, plugin interfaces, plugin manager
│ ├── model/ # Domain model types (CatalogEntry, Skill, etc.)
│ ├── server/ # HTTP server lifecycle management
│ ├── service/ # Shared services (CardFetcher for URL import)
│ └── store/ # SQLite store, migrations, query builders
├── plugins/ # Plugin implementations
│ ├── enterprise/ # License-gated plugins (SSO, RBAC, audit, PostgreSQL)
│ ├── health/ # Health checker plugin
│ ├── parsers/ # Protocol card parsers (A2A, MCP)
│ │ ├── a2a/
│ │ └── mcp/
│ └── sources/ # Discovery source plugins
│ ├── k8s/ # Kubernetes service discovery
│ └── static/ # Static config source
├── web/ # React + Vite + shadcn/ui frontend
│ └── src/
│ ├── components/ # UI components (CatalogList, EntryDetail, RegisterAgentDialog, etc.)
│ │ └── ui/ # shadcn/ui base components
│ ├── api.ts # API client functions
│ ├── types.ts # TypeScript type definitions
│ └── App.tsx # Root application component
├── deploy/helm/agentlens/ # Helm chart
├── examples/ # Docker Compose + mock agents
├── docs/ # Documentation
├── .github/workflows/ # CI and code scanning workflows
├── Dockerfile # Multi-stage Docker build
├── Makefile # Build automation
└── go.mod # Go module definition
Makefile Targets¶
Run make help to see all available targets:
| Target | Description |
|---|---|
make all |
Format, lint, test, and build |
make build |
Build the Go binary (CGO enabled for SQLite) |
make test |
Run all Go tests |
make test-coverage |
Run tests with coverage report |
make test-coverage-html |
Generate HTML coverage report |
make test-race |
Run tests with Go race detector |
make lint |
Run golangci-lint |
make vet |
Run go vet |
make format |
Format Go source files |
make run |
Build and run the server |
make clean |
Remove build artifacts |
make deps |
Download and tidy Go dependencies |
make tools |
Install golangci-lint, arch-go, lefthook |
make hooks |
Install git hooks via lefthook (run once after cloning) |
make web-install |
Install frontend dependencies (bun) |
make web-build |
Build the frontend |
make web-lint |
TypeScript type check |
make docker-build |
Build the Docker image locally |
make docker-scan |
Scan Docker image with Trivy |
make helm-lint |
Lint and validate the Helm chart |
Building¶
Go Backend¶
make build
This compiles the binary to bin/agentlens with CGO_ENABLED=1 (required for the SQLite driver).
Frontend¶
make web-install # Install bun dependencies
make web-build # Build production bundle
The frontend builds to web/dist/ and is embedded into the Go binary via embed.FS.
Docker Image¶
make docker-build
This runs a multi-stage Docker build: 1. Stage 1 (Node.js) — builds the frontend 2. Stage 2 (Go) — builds the backend binary 3. Stage 3 (Alpine) — minimal runtime image
Testing¶
Run All Tests¶
make test
With Coverage¶
make test-coverage # Print coverage to terminal
make test-coverage-html # Open coverage.html in browser
With Race Detector¶
make test-race
Test Files¶
Tests live alongside the code they test (Go convention):
internal/api/handlers_test.gointernal/api/validate_handler_test.gointernal/api/register_handler_test.gointernal/api/import_handler_test.gointernal/api/auth_handlers_test.gointernal/api/user_handlers_test.gointernal/api/role_handlers_test.gointernal/api/settings_handlers_test.gointernal/config/config_test.gointernal/discovery/a2a_test.go,mcp_test.go,k8s_test.go,manager_test.gointernal/health/checker_test.gointernal/store/sqlite_test.go,user_store_test.go,role_store_test.go,settings_store_test.goplugins/parsers/a2a/validation_test.go
API Handler Test Pattern¶
API handler tests use a shared testRouter helper (in internal/api/test_helpers_test.go) that wires a full in-memory stack. The helper creates a real kernel.Core, registers A2A and MCP parsers, and passes the kernel to api.RouterDeps:
database, _ := db.OpenMemory()
// ... run migrations ...
catalogStore := store.NewSQLStore(database)
core := kernel.NewCore(catalogStore, nil, slog.Default(), kernel.LicenseInfo{})
a2aParser := a2aplugin.New()
_ = a2aParser.Init(core)
core.RegisterParser(a2aParser)
mcpParser := mcpplugin.New()
_ = mcpParser.Init(core)
core.RegisterParser(mcpParser)
router := api.NewRouter(api.RouterDeps{
Kernel: core,
UserStore: userStore,
RoleStore: roleStore,
SettingsStore: settingsStore,
JWTService: jwtService,
})
Tests that exercise the import endpoint (POST /api/v1/catalog/import) inject a stub service.Fetcher via RouterDeps.CardFetcher to avoid real outbound HTTP.
Linting¶
Go¶
make lint # golangci-lint
make vet # go vet
make format # gofmt
Frontend (TypeScript)¶
make web-lint # tsc --noEmit
Git Hooks¶
AgentLens uses lefthook to manage git hooks checked into the repo. Hooks provide a local quality gate that catches issues before they reach CI.
What each hook does¶
| Hook | When | Jobs |
|---|---|---|
| pre-commit | On every git commit |
gofmt check (parallel), golangci-lint (parallel), TypeScript type-check (parallel) |
| commit-msg | After pre-commit | commitlint validates conventional commit format |
| pre-push | On git push |
make test + make web-test (parallel), then make arch-test |
Install hooks (once per clone)¶
make hooks
Run this once after cloning. It installs the lefthook binary and activates all hooks. It is not run automatically by make all to avoid surprising contributors.
Note:
make hooksinstalls lefthook viago install, which places it in$(go env GOPATH)/bin. Ensure that directory is in yourPATH(standard Go setup). If hooks appear to silently skip, add$(go env GOPATH)/binto yourPATH.
Verify hooks are installed¶
ls .git/hooks/pre-commit .git/hooks/commit-msg .git/hooks/pre-push
Expected: all three files exist.
Commit message format¶
Hooks enforce Conventional Commits:
<type>[(scope)]: <subject>
Allowed types: feat, fix, chore, docs, refactor, test, ci, build, perf, revert
Examples:
feat: add kubernetes source plugin
feat(api): expose capabilities endpoint
fix(auth): correct lockout timer reset
chore: bump go version to 1.26.1
Skipping hooks¶
git commit --no-verify -m "wip: ..." # skip pre-commit + commit-msg
git push --no-verify # skip pre-push
LEFTHOOK=0 git commit ... # disable lefthook entirely
Writing Plugins¶
AgentLens is designed to be extended through plugins. All plugins implement the kernel.Plugin interface.
API Handler and Router Wiring¶
The API layer depends on kernel.Kernel, not on the store directly. NewHandler(k kernel.Kernel) obtains the catalog store via k.Store() and looks up parsers via k.Parser(protocol). The RouterDeps struct follows the same pattern — it carries a Kernel field rather than a raw store:
// RouterDeps holds all dependencies for the router.
type RouterDeps struct {
Kernel kernel.Kernel // required; provides store + parser lookup
UserStore *store.UserStore
RoleStore *store.RoleStore
SettingsStore *store.SettingsStore
JWTService *auth.JWTService
// CardFetcher is optional. When nil, a default CardFetcher with SSRF
// protection is used (for POST /api/v1/catalog/import).
CardFetcher service.Fetcher
}
In main.go the core kernel is wired after all plugins have been initialised:
core := kernel.NewCore(catalogStore, cfg, logger, lic)
// ... register and init plugins ...
router := api.NewRouter(api.RouterDeps{
Kernel: core,
UserStore: userStore,
RoleStore: roleStore,
SettingsStore: settingsStore,
JWTService: jwtService,
})
Creating a Parser Plugin¶
A parser plugin converts protocol-specific card JSON into an AgentType (Product Archetype pattern). The ParserPlugin interface requires both Parse and Validate methods.
Parse returns *model.AgentType populated with the protocol, endpoint, version, provider, capabilities, and raw definition. The handler is responsible for wrapping the AgentType in a CatalogEntry (display name, status, source, validity) before persisting.
package myplugin
import (
"context"
"encoding/json"
"fmt"
"github.com/PawelHaracz/agentlens/internal/kernel"
"github.com/PawelHaracz/agentlens/internal/model"
)
type MyParser struct {
log *slog.Logger
}
func New() *MyParser { return &MyParser{} }
func (p *MyParser) Name() string { return "my-protocol-parser" }
func (p *MyParser) Version() string { return "0.1.0" }
func (p *MyParser) Type() kernel.PluginType { return kernel.PluginTypeParser }
func (p *MyParser) Protocol() model.Protocol { return "my-protocol" }
func (p *MyParser) CardPath() string { return "/.well-known/my-card.json" }
func (p *MyParser) Init(k kernel.Kernel) error {
p.log = k.Logger().With("plugin", p.Name())
return nil
}
func (p *MyParser) Start(ctx context.Context) error { return nil }
func (p *MyParser) Stop(ctx context.Context) error { return nil }
// Validate checks the raw card JSON for spec compliance without persisting anything.
// Called by the validate endpoint and the import endpoint before Parse.
func (p *MyParser) Validate(raw []byte) kernel.ValidationResult {
// Inspect the raw JSON and return structured diagnostics.
return kernel.ValidationResult{
Valid: true,
SpecVersion: "1.0",
Errors: nil,
Warnings: nil,
Preview: map[string]any{"display_name": "My Agent"},
}
}
// Parse converts a raw card JSON blob into an AgentType.
// The returned AgentType has Provider and Capabilities populated.
// Source (push, k8s, config) is a catalog concern — the handler adds it.
func (p *MyParser) Parse(raw []byte) (*model.AgentType, error) {
var card struct {
Name string `json:"name"`
Endpoint string `json:"endpoint"`
Version string `json:"version"`
}
if err := json.Unmarshal(raw, &card); err != nil {
return nil, fmt.Errorf("parsing my-protocol card: %w", err)
}
if card.Name == "" {
return nil, fmt.Errorf("my-protocol card missing required field: name")
}
return &model.AgentType{
Protocol: "my-protocol",
Endpoint: card.Endpoint,
Version: card.Version,
RawDefinition: raw,
AgentKey: model.ComputeAgentKey("my-protocol", card.Endpoint),
}, nil
}
Register in cmd/agentlens/main.go:
pm.Register(myplugin.New())
Creating a Source Plugin¶
A source plugin discovers agents from a specific location and returns AgentType values. The discovery manager wraps each into a CatalogEntry.
package mysource
import (
"context"
"github.com/PawelHaracz/agentlens/internal/kernel"
"github.com/PawelHaracz/agentlens/internal/model"
)
type MySource struct {
kernel kernel.Kernel
}
func New() *MySource { return &MySource{} }
func (s *MySource) Name() string { return "my-source" }
func (s *MySource) Version() string { return "0.1.0" }
func (s *MySource) Type() kernel.PluginType { return kernel.PluginTypeSource }
func (s *MySource) Init(k kernel.Kernel) error {
s.kernel = k
return nil
}
func (s *MySource) Start(ctx context.Context) error { return nil }
func (s *MySource) Stop(ctx context.Context) error { return nil }
func (s *MySource) Discover(ctx context.Context) ([]*model.AgentType, error) {
// Discover agents from your source.
// Use s.kernel.Parser(protocol).Parse(raw) to convert card JSON into AgentType.
return agentTypes, nil
}
Enterprise Plugins (License-Gated)¶
Return kernel.ErrLicenseRequired from Init() to gate a plugin behind an enterprise license:
func (p *MyPlugin) Init(k kernel.Kernel) error {
if !k.License().IsEnterprise() {
return kernel.ErrLicenseRequired
}
// Initialize plugin
return nil
}
The plugin manager will log a warning and skip the plugin — it will not be started or stopped.
Frontend Development¶
Development Server¶
cd web
bun run dev
This starts the Vite dev server with hot module replacement. The frontend proxies API requests to http://localhost:8080.
Component Library¶
The UI uses shadcn/ui components built on Radix UI + Tailwind CSS:
Badge— protocol and status badgesButton— action buttonsCard— entry detail cardsTable— catalog list tableInput— search inputSelect— filter dropdownsScrollArea— scrollable containersSeparator— visual dividersSkeleton— loading placeholders
Adding UI Components¶
Components are in web/src/components/. The shadcn/ui base components are in web/src/components/ui/.
TypeScript Types¶
All API types are defined in web/src/types.ts. These mirror the flat JSON response shape produced by CatalogEntry.MarshalJSON():
CatalogEntry— flat catalog entry type (merges AgentType + CatalogEntry fields)Capability— polymorphic agent capability withkind,name,description,propertiesProtocol,Status,SourceType— enum typesProvider,Validity— nested typesStats,ListFilter— API-specific types
The capabilities field on CatalogEntry replaces the old skills field. Each capability has a kind discriminator (e.g. a2a.skill, mcp.tool) and a properties object with protocol-specific fields.
Docker¶
Build Image¶
make docker-build
Scan for Vulnerabilities¶
make docker-scan
Requires Trivy installed locally. In CI, scanning runs automatically via the code-scanning workflow.
Helm Chart¶
The Helm chart is in deploy/helm/agentlens/.
Lint the Chart¶
make helm-lint
Template Preview¶
helm template agentlens deploy/helm/agentlens --debug
Key Values¶
See deploy/helm/agentlens/values.yaml for all configurable values. Key settings:
| Value | Default | Description |
|---|---|---|
replicaCount |
1 |
Number of replicas |
image.repository |
ghcr.io/pawelharacz/agentlens |
Container image |
service.port |
80 |
Service port |
env.AGENTLENS_KUBERNETES_ENABLED |
true |
Enable K8s discovery |
persistence.enabled |
true |
Enable persistent storage |
persistence.size |
1Gi |
Storage size |
CI/CD¶
CI Workflow (.github/workflows/ci.yml)¶
Triggered on PRs to main:
| Job | Description |
|---|---|
| Lint | go vet + golangci-lint + TypeScript type check |
| Test | Go tests with coverage, uploads coverage.out artifact |
| Build | Full frontend + backend build (gates on lint + test) |
Code Scanning (.github/workflows/code-scanning.yml)¶
Triggered on PRs, push to main, and weekly schedule:
| Job | Description |
|---|---|
| CodeQL | Static analysis for Go and JavaScript/TypeScript |
| govulncheck | Go dependency vulnerability scanning |
| Docker Scan | Trivy vulnerability scan of the Docker image |
| Helm Lint | Helm chart linting and template validation |
Code Style¶
- Go: Follow standard Go conventions. Run
make formatbefore committing. - Frontend: TypeScript with strict mode. Use shadcn/ui patterns for UI components.
- Commits: Use Conventional Commits format:
feat:,fix:,refactor:,docs:,test:,ci:. - Testing: Place tests alongside source files (
_test.go). Usetestifyfor assertions. - Logging: Use
slogstructured logging. Includecomponentandpluginfields.