Feature 2.5 — A2A Security Schemes: Parsing, Storage, and Dashboard Display¶
⚠ SUPERSEDED BY ADR-001
The design in this document (Hybrid: capability rows +
SecurityStorePlugin+security_detailstable) was revised during implementation to comply with ADR-001 (Product Archetype Principles).Implemented design (authoritative): - Security data is stored exclusively in the
capabilitiestable as two capability kinds:a2a.security_schemeanda2a.security_requirement. - NoSecurityStorePlugin, nosecurity_detailstable, no new plugins. -auth_summaryandsecurity_detailare computed at serialization time from capabilities inbuildAuthSummary()/buildSecurityDetail()ininternal/model/agent.go.See
docs/architecture.md(Security Capabilities and Computed Views section) for the authoritative architecture description.
Tier: 2 — SHOULD HAVE Effort: M (3-4 days) Date: 2026-04-10
Goal¶
When a platform engineer discovers an agent via the catalog or the Capabilities tab, the immediate next question is: "How do I authenticate to this thing?" Today the answer is "go read the agent's documentation" — which is exactly the kind of friction AgentLens exists to eliminate.
This feature parses the securitySchemes and securityRequirements blocks from A2A Agent Cards (spec v0.3 and v1.0), stores them via the existing capability system and a new SecurityStorePlugin, exposes them via the API, and renders a clear "Authentication" section in the agent detail view that tells the integrator everything they need to connect.
Why¶
- Every A2A Agent Card can declare how clients must authenticate: API keys, Bearer tokens, OAuth 2.0 flows, OIDC, or mTLS. Today AgentLens stores only a minimal summary (
A2ASecurityScheme{Type, Method, Name}) as non-discoverable capability rows. - Without security scheme display, every integration starts with manual doc diving. Multiply that by 50 agents in a catalog and it becomes a real bottleneck for platform teams.
- Competing registries (a2aregistry.org, Apicurio) do not surface auth requirements in a structured, actionable way. This is a clear UX win.
- Feature 2.3 (Capability-based Discovery) just landed — users are now finding agents by capability. The natural follow-up is "I found the right agent, now show me how to connect."
Decisions Made During Design¶
| Decision | Choice | Rationale |
|---|---|---|
| Rich security detail storage | Hybrid: capability rows + SecurityStorePlugin | a2a.security_scheme capability rows provide indexing/summary (type, method, name). A new SecurityStorePlugin stores the rich structured detail (OAuth flows, scopes, URLs). This follows the same pattern as raw cards: capability table for discovery, plugin for full data. |
| SecurityStorePlugin scope | Protocol-agnostic — works for A2A, MCP, A2UI | The plugin interface accepts any protocol. A2A is the first consumer. MCP and A2UI parsers can use the same plugin when their specs define security. |
| SecurityStorePlugin default impl | Built-in SQLite-compatible OSS implementation in plugins/securitystore/ |
Ships a security_details table with JSON TEXT storage, same as cardstore. Enterprise can swap in a Postgres-optimized version. |
securityRequirements storage |
New non-discoverable capability kind (a2a.security_requirement) |
Stores top-level security requirements as capability rows in the existing capabilities table. Consistent with the pattern used for a2a.extension, a2a.interface, a2a.signature. |
| Per-skill security requirements | Separate a2a.security_requirement capability rows per skill |
Per-skill security overrides are stored as distinct capability rows, not embedded in A2ASkill properties. Keeps capability rows normalized and queryable. |
| A2A spec version handling | Support both v0.3 array + v1.0 named map | Current parser handles securitySchemes as []fullSecurity (v0.3 array format). A2A v1.0 uses a named map map[string]SecurityScheme. Parser detects format and handles both. v0.3 entries get auto-generated names from type field. |
authSummary in list response |
Computed at handler level from a2a.security_scheme capability rows |
No new column, no migration. Handler iterates the agent's capabilities to build a lightweight summary. |
| CatalogEntry changes | None | Security data belongs to AgentType (Product Archetype). CatalogEntry never directly holds capabilities. No new columns on catalog_entries. |
| Domain model location | Enrich existing A2ASecurityScheme + new A2ASecurityRequirement in internal/model/a2a_capabilities.go |
No new security.go file in model. Security types are A2A capability types — they belong with the other A2A capabilities. |
| Deprecated OAuth flows | Parse but never display | Implicit and Password flows are parsed for completeness, marked Deprecated = true in the model. Frontend omits them entirely — no strikethrough, no warning. |
| Migration approach | Go migration function in internal/db/migrations.go |
Not SQL files. The project uses Go migration functions registered in AllMigrations(). New migration creates the security_details table if the SecurityStorePlugin doesn't handle its own schema (following cardstore pattern, the plugin does its own AutoMigrate). |
Scope (in)¶
- Enrich
A2ASecuritySchememodel with full A2A v1.0 security scheme fields (OAuth flows, scopes, URLs, API key details, OIDC, mTLS) - New
A2ASecurityRequirementcapability kind (a2a.security_requirement, non-discoverable) - New
SecurityStorePluginkernel interface — protocol-agnostic, stores rich structured security detail - Built-in
plugins/securitystore/implementation withsecurity_detailstable - A2A parser extension: parse both v0.3 array and v1.0 named map
securitySchemes, parsesecurityRequirements[] - Per-skill
securityRequirementsparsing into separate capability rows - API:
authSummaryderived field inGET /api/v1/cataloglist response - API: rich security detail in
GET /api/v1/catalog/{id}detail response - Dashboard: "Authentication" section in agent detail view with scheme cards, connection recipe, copy buttons
- Per-skill security display on skill rows in agent detail
- Catalog list: small auth indicator badge per row
Scope (out)¶
- AgentLens performing OAuth flows on behalf of the user (AgentLens is a registry, not a gateway)
- Token storage, secret management, or credential vaulting
- Validating that the declared auth endpoint is reachable (health probe already covers endpoint liveness)
- MCP server auth parsing (MCP spec handles auth differently — separate backlog item)
- JWS signature verification of the Agent Card itself (
signatures[]— Tier 3) - Modifying or overriding the security config declared by the agent (read-only display)
- New Postgres
JSONBcolumn —TEXTfor dialect symmetry, same as features 1.6 and 2.2
Backend Changes¶
1. Enrich A2ASecurityScheme capability model¶
File: internal/model/a2a_capabilities.go
The existing A2ASecurityScheme carries only Type, Method, Name. Enrich it with fields for all five A2A v1.0 scheme types:
// A2ASecurityScheme represents a security scheme for A2A communication.
// kind: "a2a.security_scheme"
//
// This is a union type discriminated by Type. Only fields relevant to the
// scheme type are populated. Consumers switch on Type to determine which
// fields to read.
type A2ASecurityScheme struct {
// Common
SchemeName string `json:"scheme_name"` // key from Agent Card's securitySchemes map
Type string `json:"type"` // "apiKey" | "http" | "oauth2" | "openIdConnect" | "mutualTls"
Description string `json:"description,omitempty"`
// apiKey
APIKeyLocation string `json:"api_key_location,omitempty"` // "header" | "query" | "cookie"
APIKeyName string `json:"api_key_name,omitempty"` // e.g. "X-API-Key"
// http
HTTPScheme string `json:"http_scheme,omitempty"` // "Bearer" | "Basic" | "Digest"
BearerFormat string `json:"bearer_format,omitempty"` // e.g. "JWT"
// oauth2
OAuthFlows []A2AOAuthFlow `json:"oauth_flows,omitempty"`
OAuth2MetadataURL string `json:"oauth2_metadata_url,omitempty"`
// openIdConnect
OpenIDConnectURL string `json:"openid_connect_url,omitempty"`
// Backward compat (v0.3 parser used these)
Method string `json:"method,omitempty"`
Name string `json:"name,omitempty"`
}
New supporting type:
// A2AOAuthFlow represents a single OAuth 2.0 flow variant.
type A2AOAuthFlow struct {
FlowType string `json:"flow_type"` // "authorizationCode" | "clientCredentials" | "deviceCode" | "implicit" | "password"
AuthorizationURL string `json:"authorization_url,omitempty"`
TokenURL string `json:"token_url,omitempty"`
RefreshURL string `json:"refresh_url,omitempty"`
DeviceAuthURL string `json:"device_auth_url,omitempty"` // deviceCode flow only
Scopes map[string]string `json:"scopes,omitempty"` // scope -> description
Deprecated bool `json:"deprecated,omitempty"` // true for implicit/password
}
The enriched A2ASecurityScheme continues to implement Capability via Kind() string and Validate() error. The Validate() method checks that Type is non-empty.
Capability name column mapping: The store's capabilityToRow extracts the name column from the capability's JSON "name" field. For backward compatibility, the existing Name field is kept and populated: for v1.0, Name = SchemeName (the map key); for v0.3, Name retains the existing v0.3 value. The SchemeName field is the canonical key for referencing schemes in SecurityRequirement. The Name field is what capabilityToRow uses for the name DB column. Both are set by the parser.
2. New A2ASecurityRequirement capability kind¶
File: internal/model/a2a_capabilities.go
// A2ASecurityRequirement declares which scheme(s) a client MUST use.
// kind: "a2a.security_requirement"
//
// Map key = scheme name (matching a key in SecuritySchemes).
// Map value = list of required scopes (empty = no specific scopes).
// Multiple A2ASecurityRequirement entries on an AgentType are OR'd:
// the client must satisfy at least one complete entry.
type A2ASecurityRequirement struct {
Schemes map[string][]string `json:"schemes"`
SkillRef string `json:"skill_ref,omitempty"` // non-empty = per-skill override
}
func (a *A2ASecurityRequirement) Kind() string { return "a2a.security_requirement" }
func (a *A2ASecurityRequirement) Validate() error {
if len(a.Schemes) == 0 {
return errors.New("a2a.security_requirement: schemes must not be empty")
}
return nil
}
Register in init():
RegisterCapability("a2a.security_requirement", func() Capability { return &A2ASecurityRequirement{} }, false)
SkillRef links per-skill requirements to their parent skill. When empty, the requirement applies to the entire agent. When non-empty, it contains the skill name and overrides the top-level requirements for that specific skill.
3. New SecurityStorePlugin kernel interface¶
File: internal/kernel/plugin.go
Add new plugin type constant:
PluginTypeSecurityStore PluginType = "securitystore"
Add the specialized interface:
// SecurityStorePlugin persists structured security detail keyed by AgentTypeID.
// Protocol-agnostic — works for A2A, MCP, and A2UI.
type SecurityStorePlugin interface {
Plugin
StoreSecurityDetail(ctx context.Context, agentTypeID string, detail *model.SecurityDetail) error
GetSecurityDetail(ctx context.Context, agentTypeID string) (*model.SecurityDetail, error)
}
File: internal/kernel/kernel.go — add field and accessor:
type Core struct {
// ...existing fields...
securityStore SecurityStorePlugin
}
// SecurityStore returns the registered security store plugin, or nil if not loaded.
func (c *Core) SecurityStore() SecurityStorePlugin { return c.securityStore }
File: internal/kernel/plugin.go — extend Kernel interface:
type Kernel interface {
// ...existing methods...
SecurityStore() SecurityStorePlugin
}
File: internal/kernel/core_registry.go:
// RegisterSecurityStore registers the security store plugin.
func (c *Core) RegisterSecurityStore(p SecurityStorePlugin) {
c.securityStore = p
}
File: internal/kernel/plugin_manager.go — add auto-registration in InitAll():
// Register security store plugins with the kernel
if ssp, ok := p.(SecurityStorePlugin); ok {
pm.core.RegisterSecurityStore(ssp)
}
4. SecurityDetail transport model¶
File: internal/model/security_detail.go
// SecurityDetail holds the full structured security metadata for an AgentType.
// It is a pure transport struct returned by SecurityStorePlugin — NOT a GORM
// model and has no database tags. Persistence is handled by the plugin's
// internal row struct.
type SecurityDetail struct {
AgentTypeID string `json:"agent_type_id"`
Protocol string `json:"protocol"`
// Full structured security schemes keyed by scheme name.
// For A2A v0.3 (array format), keys are auto-generated from type field.
Schemes map[string]SecuritySchemeDetail `json:"schemes"`
// Top-level security requirements.
// Multiple entries are OR'd (client must satisfy at least one).
// Schemes within a single entry are AND'd.
Requirements []SecurityRequirementDetail `json:"requirements,omitempty"`
// Per-skill security overrides. Key = skill name.
SkillRequirements map[string][]SecurityRequirementDetail `json:"skill_requirements,omitempty"`
}
// SecuritySchemeDetail is the rich representation of one security scheme.
// Protocol-agnostic wrapper — the fields cover A2A scheme types.
// Future MCP/A2UI security will extend this or use the same fields.
type SecuritySchemeDetail struct {
Type string `json:"type"`
Description string `json:"description,omitempty"`
// apiKey
APIKeyLocation string `json:"api_key_location,omitempty"`
APIKeyName string `json:"api_key_name,omitempty"`
// http
HTTPScheme string `json:"http_scheme,omitempty"`
BearerFormat string `json:"bearer_format,omitempty"`
// oauth2
OAuthFlows []OAuthFlowDetail `json:"oauth_flows,omitempty"`
OAuth2MetadataURL string `json:"oauth2_metadata_url,omitempty"`
// openIdConnect
OpenIDConnectURL string `json:"openid_connect_url,omitempty"`
// mutualTls — no additional fields
}
// OAuthFlowDetail holds one OAuth 2.0 flow variant.
type OAuthFlowDetail struct {
FlowType string `json:"flow_type"`
AuthorizationURL string `json:"authorization_url,omitempty"`
TokenURL string `json:"token_url,omitempty"`
RefreshURL string `json:"refresh_url,omitempty"`
DeviceAuthURL string `json:"device_auth_url,omitempty"`
Scopes map[string]string `json:"scopes,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
}
// SecurityRequirementDetail declares which schemes a client must satisfy.
type SecurityRequirementDetail struct {
Schemes map[string][]string `json:"schemes"`
}
Notes:
- SecurityDetail is a pure transport struct like model.RawCard. NOT a GORM model.
- SecuritySchemeDetail and OAuthFlowDetail mirror A2ASecurityScheme/A2AOAuthFlow fields but live in a protocol-agnostic wrapper. This avoids coupling the plugin interface to A2A-specific types.
- Why two similar types? A2ASecurityScheme is a Capability implementation — it participates in the capability registry, has Kind()/Validate(), and is serialized into capability rows with name/description/properties columns. SecuritySchemeDetail is a flat data struct stored as a single JSON blob by the plugin. The parser builds both from the same source data. The capability is for indexing; the detail is for display.
- The plugin stores/retrieves SecurityDetail as a single JSON blob per AgentTypeID.
5. Built-in SecurityStore plugin¶
File: plugins/securitystore/plugin.go
Follows the exact cardstore plugin pattern:
package securitystore
// Plugin implements kernel.SecurityStorePlugin.
type Plugin struct {
database *db.DB
}
// securityDetailRow is the GORM model for the security_details table.
type securityDetailRow struct {
AgentTypeID string `gorm:"primaryKey;type:text"`
Data string `gorm:"not null;type:text;default:'{}'"` // JSON TEXT
UpdatedAt time.Time `gorm:"not null"`
}
func (securityDetailRow) TableName() string { return "security_details" }
Methods:
- New(database *db.DB) *Plugin
- Name() string — returns "security-store"
- Version() string — returns "1.0.0"
- Type() kernel.PluginType — returns kernel.PluginTypeSecurityStore
- Init(_ kernel.Kernel) error — calls MigrateSchema (AutoMigrate, idempotent)
- Start, Stop — no-ops
- StoreSecurityDetail(ctx, agentTypeID, detail) — marshals SecurityDetail to JSON, upserts by PK using GORM clause.OnConflict
- GetSecurityDetail(ctx, agentTypeID) — reads row, unmarshals JSON into *model.SecurityDetail, returns gorm.ErrRecordNotFound if missing
File: plugins/securitystore/migrate.go
func (p *Plugin) MigrateSchema(_ context.Context) error {
return p.database.AutoMigrate(&securityDetailRow{})
}
6. A2A parser extension¶
File: plugins/parsers/a2a/validation.go
Extend fullCard to support v1.0 named map format. The parser must detect whether securitySchemes is an array (v0.3) or a map (v1.0):
type fullCard struct {
// ...existing fields...
SecuritySchemes json.RawMessage `json:"securitySchemes,omitempty"` // array (v0.3) OR map (v1.0)
SecurityRequirements []json.RawMessage `json:"securityRequirements,omitempty"` // v1.0
}
Remove the existing typed []fullSecurity for SecuritySchemes — use json.RawMessage for deferred parsing.
File: plugins/parsers/a2a/a2a.go
The Parse method extension:
- After
buildA2AMetaCaps(&card), call newbuildSecurityCaps(&card): - Try to unmarshal
card.SecuritySchemesasmap[string]json.RawMessage(v1.0). If that fails, try[]fullSecurity(v0.3). - For v1.0: iterate the map, use the key as
SchemeName, peek at"type"field, unmarshal into enrichedA2ASecuritySchemewith all fields populated. - For v0.3: iterate the array, auto-generate
SchemeNamefromType(e.g.,"apiKey"->"apiKeyAuth","http"->"httpAuth"). - For OAuth2 flows: if
flowTypeis"implicit"or"password", setDeprecated = trueand log a warning. - For unknown scheme types: log warning, store with
TypeandDescriptiononly — do not error. -
Append each
*model.A2ASecuritySchemeto capabilities. -
Call new
buildSecurityRequirements(&card): - Parse
card.SecurityRequirementsarray. - Each entry is
map[string][]string(scheme name -> required scopes). - Verify each referenced scheme name exists in the parsed schemes (warn if not, do not error).
-
Create
*model.A2ASecurityRequirementwithSkillRef = ""(top-level). -
Parse per-skill
securityRequirements: - Extend
fullSkillto includeSecurityRequirements []json.RawMessage. -
For each skill with non-empty security requirements, create
*model.A2ASecurityRequiremententries withSkillRef = skill.Name. -
If the kernel has a
SecurityStorePluginregistered, build amodel.SecurityDetailfrom the parsed data and callStoreSecurityDetail. The parser gets access to the kernel viaInit(k kernel.Kernel): - Add a
kernel kernel.Kernelfield to the A2A parser'sPluginstruct (currently it has onlyinitialized bool). - In
Init(k kernel.Kernel), storep.kernel = k. Parseis a pure function today — it cannot call the plugin directly. Instead, the caller (discovery manager / handler) is responsible for callingSecurityStore().StoreSecurityDetail()after a successful parse, same way the discovery manager callsCardStore().StoreCard()after parsing. The parser returns theSecurityDetailas a new field onAgentType:
type AgentType struct {
// ...existing fields...
SecurityDetail *SecurityDetail `json:"-" gorm:"-"` // transient, not persisted by GORM
}
The discovery manager and the registration handler check for agentType.SecurityDetail != nil and call kernel.SecurityStore().StoreSecurityDetail() if available — following the exact pattern used for RawBytes and CardStore().StoreCard().
7. API changes — detail endpoint¶
File: internal/api/handlers.go
Extend GetEntry handler to include security detail in the response:
- After fetching the
CatalogEntry, check ifkernel.SecurityStore()is non-nil. - If available, call
GetSecurityDetail(ctx, entry.AgentTypeID). - If detail exists, add
securityDetailto the response viacatalogEntryJSON. - If no detail (not found or plugin not registered), omit the field.
Extend catalogEntryJSON in internal/model/agent.go:
type catalogEntryJSON struct {
// ...existing fields...
SecurityDetail *SecurityDetail `json:"security_detail,omitempty"`
}
Important: The MarshalJSON() method on CatalogEntry calls toCatalogEntryJSON() which today has no access to external data like SecurityDetail. The handler must NOT use MarshalJSON() for the detail endpoint. Instead, the handler should:
1. Call entry.SyncFromDB()
2. Call entry.toCatalogEntryJSON() — but this is unexported. Two options:
a. Export it as ToCatalogEntryJSON() and add a SecurityDetail field to catalogEntryJSON.
b. Build the response DTO directly in the handler, composing catalogEntryJSON + security_detail.
Option (a) is cleaner — export the method and extend the DTO. The handler passes SecurityDetail into the exported method. For the list endpoint, it passes nil (omitted from JSON). Existing callers that use json.Marshal(entry) (which calls MarshalJSON) continue to work — they just don't include security detail.
8. API changes — list endpoint authSummary¶
File: internal/api/handlers.go
Extend ListCatalog handler to compute authSummary for each entry:
- After loading entries (which already includes capabilities via
loadCapabilities), iterate each entry'sAgentType.Capabilities. - Filter for
a2a.security_schemekind capabilities. - Build
authSummaryfrom the capability data: types: array of"<type>:<subtype>"strings (e.g.,"http:Bearer","apiKey").label: human-readable join of type names (max 40 chars). Zero schemes ->"Open (no auth)".required:trueif anya2a.security_requirementcapabilities exist,falseotherwise.
Add authSummary to catalogEntryJSON:
type catalogEntryJSON struct {
// ...existing fields...
AuthSummary *authSummaryJSON `json:"auth_summary,omitempty"`
}
type authSummaryJSON struct {
Types []string `json:"types"`
Label string `json:"label"`
Required bool `json:"required"`
}
The authSummary is computed at the handler level, not stored. It is a derived field populated only in the list response.
9. Wiring in main.go¶
File: cmd/agentlens/main.go
import securitystorePlugin "github.com/PawelHaracz/agentlens/plugins/securitystore"
// After cardstorePlugin registration:
pm.Register(securitystorePlugin.New(database))
The PluginManager.InitAll() auto-registers it via the SecurityStorePlugin type assertion (step 3 above).
Frontend Changes¶
Agent detail view — "Authentication" section¶
Add a new section in the agent detail view, positioned below the health section and above protocol extensions. Use shadcn Card as the container.
Scheme cards¶
Each declared security scheme rendered as a shadcn Card inside the section. Content varies by type:
HTTP Auth (type: "http"):
- Badge: scheme value capitalized — "Bearer JWT", "Basic Auth", "Digest Auth"
- Icon: lucide-react KeyRound
- Body: description if present
- If Bearer + JWT: note "Expects a JWT in the Authorization header"
API Key (type: "apiKey"):
- Badge: "API Key"
- Icon: lucide-react Key
- Body: description if present
- Detail line: Location: {location} / Name: {name}
- Copy button: copies the header string {name}: <your-key>
OAuth 2.0 (type: "oauth2"):
- Badge: "OAuth 2.0"
- Icon: lucide-react ShieldCheck
- Sub-badges for each non-deprecated flow: "Authorization Code", "Client Credentials", "Device Code"
- URLs rendered as clickable links (open in new tab): Authorization URL, Token URL, Device Authorization URL
- If oauth2MetadataUrl is present: prominent link "OAuth 2.0 Metadata" opening in new tab
- Scopes table: two columns Scope | Description, only if scopes are non-empty
- Deprecated flows (Implicit, Password): not rendered at all
OpenID Connect (type: "openIdConnect"):
- Badge: "OIDC"
- Icon: lucide-react Fingerprint
- Clickable link: "OpenID Connect Discovery" pointing to openIdConnectUrl
Mutual TLS (type: "mutualTls"):
- Badge: "mTLS"
- Icon: lucide-react Lock
- Default text: "This agent requires mutual TLS. Configure your client certificate before connecting."
Security requirements banner¶
If requirements is non-empty, render a highlighted box above the scheme cards:
- Title: "Required to connect"
- For each requirement entry: list scheme names (linked to scheme card via anchor) and their required scopes
- Multiple entries: show "Any of the following combinations:" (entries are OR'd per A2A spec, schemes within entry are AND'd)
Connection recipe¶
Below scheme cards, a generated curl example using placeholder values:
- Endpoint URL from the agent's first a2a.interface capability
- Auth headers derived from the first securityRequirement entry
- Render in code block with Copy button
No auth declared state¶
- A2A entries with empty security: shadcn
Alertwithvariant="default": "This agent does not declare any authentication requirements." - MCP entries: shadcn
Alert: "MCP servers declare authentication at the transport level, not in the server card."
Per-skill security in agent detail¶
On skill rows, if a skill has per-skill securityRequirements (detected via a2a.security_requirement capabilities with matching SkillRef), show a small inline badge:
- "Custom auth" with shadcn HoverCard showing per-skill requirements
- If different from top-level: "Overrides agent-level auth for this skill"
Catalog list view — auth badge¶
In the catalog list table, add a compact auth indicator using authSummary:
- Column position: after spec version badge, before last seen
- Content: compact badge from authSummary.label, truncated to 25 chars with tooltip
- Color: variant="outline" (neutral)
- No schemes: "Open" badge with variant="secondary"
Capabilities tab — auth indicator¶
On the Capability detail view (feature 2.3), add "Auth" column to agent rows showing the same authSummary.label badge.
New frontend files¶
web/src/routes/catalog/detail/
AuthenticationSection.tsx (main section component)
SchemeCard.tsx (renders one SecuritySchemeDetail)
SecurityRequirementsBanner.tsx ("Required to connect" box)
ConnectionRecipe.tsx (generated curl example)
PerSkillAuthBadge.tsx (inline badge for skill-level security)
web/src/components/
AuthBadge.tsx (compact badge for list views, reused in catalog + capabilities)
web/src/lib/
securityUtils.ts (buildAuthSummary, generateCurlRecipe, formatScopesLabel)
Tests¶
Backend — parser¶
Table-driven tests in plugins/parsers/a2a/. Add fixture files in testdata/:
- v10_bearer_only.json — one
httpscheme with Bearer + JWT, one security requirement. Expected:A2ASecuritySchemewith enriched fields,A2ASecurityRequirementcapability created. - v10_apikey_header.json — one
apiKeyscheme in header. Expected:APIKeyLocation = "header",APIKeyNamepopulated. - v10_oauth2_authcode.json — OAuth2 with AuthorizationCode flow,
oauth2MetadataUrl, scopes. Expected: flow parsed, URLs present, scopes map populated. - v10_oauth2_deprecated_flows.json — OAuth2 with Implicit + Password + ClientCredentials. Expected: Implicit and Password marked
Deprecated = true, ClientCredentials normal. - v10_oauth2_device_code.json — OAuth2 with DeviceCode flow. Expected:
DeviceAuthURLpopulated. - v10_oidc.json — OpenIdConnect scheme. Expected:
OpenIDConnectURLpopulated. - v10_mtls.json — MutualTLS scheme with no extra fields. Expected: type set, no error.
- v10_multiple_schemes.json — Three schemes (Bearer + APIKey + OAuth2), two security requirements. Expected: all three parsed as capabilities, both requirements as
a2a.security_requirementcapabilities. - v10_skill_security.json — Top-level Bearer + one skill with its own APIKey requirement. Expected: skill-level
A2ASecurityRequirementwithSkillRef = skill.Name. - v10_no_security.json — No
securitySchemesfield. Expected: no security capabilities, no error. - v10_unknown_scheme_type.json — Scheme with
"type": "customAuth". Expected: parsed with type and description only, log warning. - v03_security_array.json — v0.3 format
securitySchemesas array. Expected: auto-generatedSchemeName, backward-compatible parsing.
Backend — SecurityStorePlugin¶
- Round-trip: store
SecurityDetail, read back, verify JSON equality (SQLite) - Missing entry:
GetSecurityDetailreturnsgorm.ErrRecordNotFound - Upsert: store twice for same
agentTypeID, verify latest data returned - Empty schemes map: valid state, no error
Backend — handler¶
GET /catalog/{id}includessecurity_detailwhen SecurityStorePlugin has data- Same endpoint omits
security_detailwhen no security data stored GET /cataloglist includesauth_summarywith correcttypes,label,requiredauth_summary.labelfor no schemes ="Open (no auth)"auth_summary.requiredreflects presence ofa2a.security_requirementcapabilities
Frontend — unit¶
SchemeCardrenders correct content for each of 5 scheme types (snapshot tests)ConnectionRecipegenerates correct curl for Bearer + APIKey combinationAuthBadgerenders "Open" whenrequired == falsebuildAuthSummaryutility: multiple schemes -> joined label, zero schemes -> "Open (no auth)"PerSkillAuthBadgeshows "Custom auth" only when skill has own requirements
E2E — Playwright¶
- Seed an A2A agent with Bearer + APIKey schemes and two security requirements
- Navigate to agent detail -> "Authentication" section visible
- Verify two scheme cards rendered with correct badges
- Verify "Required to connect" banner shows requirement
- Verify connection recipe curl present with Copy button
- Copy button works (clipboard API assertion)
- Seed an A2A agent with no security schemes -> verify informational alert
- Seed an MCP entry -> verify MCP-specific alert
- On catalog list -> verify auth badge column shows "Bearer JWT + API Key" for first agent, "Open" for second
- Seed agent with per-skill security -> verify skill row shows "Custom auth" badge with hover card
Acceptance Criteria¶
- All 5 A2A v1.0 security scheme types parse correctly from both v0.3 array and v1.0 named map formats
- Deprecated OAuth flows (Implicit, Password) are parsed but never displayed in the UI
- Unknown scheme types do not cause parse failure — stored with type + description, warning logged
SecurityStorePlugininterface added to kernel, with built-in SQLite implementation- Parser stores rich security detail via
SecurityStorePluginafter parsing GET /catalog/{id}returnssecurity_detailas structured JSON when availableGET /cataloglist includesauth_summaryfor each entry (computed at handler level)- Agent detail shows "Authentication" section with per-scheme cards, badges, and clickable URLs
- Connection recipe curl is generated and copyable
- Per-skill security requirements are displayed when present and differ from agent-level
- Entries with no declared security show appropriate informational alert (different for A2A vs MCP)
- Catalog list and Capabilities detail both show compact auth badges
a2a.security_requirementregistered as non-discoverable capability kind- Per-skill security requirements stored as separate
a2a.security_requirementcapability rows withSkillRef - At least 12 parser fixture files with table-driven tests, all green
- SecurityStorePlugin round-trip tests pass
- Playwright E2E green on CI
docs/api.mdupdated for new response fieldsdocs/end-user-guide.mdupdated for Authentication section
Known Traps¶
- Do NOT rename
CatalogEntrytoAgent. Seven features in. The streak must continue. - Do NOT add columns to
catalog_entriesfor security data. Security belongs toAgentType(Product Archetype). TheSecurityStorePluginstores detail keyed byAgentTypeID. - Do NOT implement a
SecuritySchemeGo interface with per-type structs. Go interface dispatch makes JSON round-tripping painful. Use a single struct with aTypediscriminator and type-specific fields — the union pattern used for all existing A2A capabilities. - Do NOT display deprecated OAuth flows. Parse them, mark
Deprecated = true, but frontend never renders Implicit or Password flows. - Do NOT validate that auth endpoints are reachable. This feature displays declared config. Endpoint reachability is the health prober's job (feature 1.2).
- Do NOT add security parsing to the MCP parser. MCP handles auth differently. Leave MCP entries with empty security capabilities. Dashboard handles this gracefully.
- Do NOT require
securitySchemesto be present. OPTIONAL in the A2A spec. Many agents are publicly accessible. - Do NOT invent a
SecurityLevelenum (high/medium/low). No basis for ranking. Show what the agent declares, do not editorialize. - Do NOT put full
securitySchemesin the list response. Use the lightweightauthSummaryderived field. - Do NOT use
localStorageto cache auth tokens or secrets. AgentLens is a registry, not an authenticator. - Do NOT use SQL files for migrations. The project uses Go migration functions in
internal/db/migrations.go. The SecurityStore plugin handles its own schema viaAutoMigrate(following cardstore pattern). - Do NOT use raw HTML for scheme cards. Use shadcn
Card,Badge,HoverCard,Tooltip,Alert. - Do NOT couple
SecurityStorePluginto A2A. The interface usesmodel.SecurityDetail, notmodel.A2ASecurityScheme. MCP and A2UI parsers will use the same plugin. - Do NOT violate arch-go function limits: max 80 lines per function, 5 params, 3 return values, 10 public functions per file.
- Do NOT bypass the capability system.
A2ASecuritySchemecapabilities provide indexing.SecurityStorePluginprovides rich detail. Both are needed — one does not replace the other.
Execution Order (suggested commit sequence)¶
- Model: Enrich
A2ASecuritySchemewith full fields ina2a_capabilities.go. AddA2AOAuthFlow. AddA2ASecurityRequirement+init()registration. Compiles, zero logic. - Model: Add
SecurityDetail,SecuritySchemeDetail,OAuthFlowDetail,SecurityRequirementDetailtointernal/model/security_detail.go. Add transientSecurityDetail *SecurityDetailfield toAgentType(json:"-" gorm:"-"). Pure transport structs. - Kernel: Add
PluginTypeSecurityStore,SecurityStorePlugininterface,Core.securityStorefield,SecurityStore()accessor,RegisterSecurityStore(). ExtendInitAll()with type assertion. - Plugin:
plugins/securitystore/— plugin struct, GORM model,MigrateSchema,StoreSecurityDetail,GetSecurityDetail. Plugin tests. - Wiring: Register
securitystorePlugin.New(database)incmd/agentlens/main.go. - Parser: Refactor
fullCard.SecuritySchemesfrom[]fullSecuritytojson.RawMessage. AddSecurityRequirementsfield. Implement dual-format parsing (v0.3 array + v1.0 map). AddbuildSecurityCaps()andbuildSecurityRequirements(). Parser stores toSecurityStorePluginif available. All 12 fixture files + table-driven tests. - Parser: Per-skill
securityRequirementsparsing. ExtendfullSkill, createA2ASecurityRequirementwithSkillRef. Tests. - Handler: Extend
GetEntryto includesecurity_detailfrom SecurityStore. Handler tests. - Handler: Add
authSummarytoListCatalogresponse. Utility function + tests. - Frontend utility:
securityUtils.ts—buildAuthSummary,generateCurlRecipe,formatScopesLabel. Unit tests. - Frontend:
SchemeCard+SecurityRequirementsBanner+ConnectionRecipecomponents. Snapshot tests. - Frontend:
AuthenticationSectioncomposed from above, wired into agent detail view. - Frontend:
AuthBadgein catalog list (reusesauthSummary). - Frontend:
AuthBadgein Capabilities detail view agent rows. - Frontend:
PerSkillAuthBadgeon skill rows in agent detail. - Frontend: Empty states — "no auth declared" alerts for A2A and MCP.
- E2E tests in Playwright.
- Docs: Update
docs/api.md,docs/end-user-guide.md.
Each step is its own commit.
Architectural Notes¶
Product Archetype alignment¶
Security data follows the Product Archetype separation:
- AgentType (= ProductType) owns capabilities including a2a.security_scheme and a2a.security_requirement.
- CatalogEntry wraps AgentType with catalog concerns. It never directly holds security data.
- The SecurityStorePlugin keys data by AgentTypeID, not CatalogEntry.ID.
- The authSummary in the list response is derived at the handler level from the agent's capabilities — no storage on CatalogEntry.
Microkernel alignment¶
The SecurityStorePlugin follows the exact pattern established by CardStorePlugin:
| Aspect | CardStorePlugin | SecurityStorePlugin |
|---|---|---|
| Plugin type | cardstore |
securitystore |
| Transport model | model.RawCard |
model.SecurityDetail |
| Internal GORM model | rawCardRow |
securityDetailRow |
| Table | raw_cards |
security_details |
| Key | AgentTypeID |
AgentTypeID |
| Storage format | []byte (blob) |
string (JSON text) |
| Migration | AutoMigrate in Init() |
AutoMigrate in Init() |
| Wiring | main.go registration |
main.go registration |
| Auto-registration | plugin_manager.go type assertion |
plugin_manager.go type assertion |
Capability system alignment¶
Security data participates in the capability system at two levels:
-
Indexing level —
a2a.security_schemeanda2a.security_requirementcapability rows in thecapabilitiestable. These provide the data forauthSummarycomputation and allow SQL queries like "which agents require OAuth?" without parsing JSON. -
Detail level —
SecurityStorePluginstores the full structured data with OAuth flows, scopes, URLs. This provides the data for the "Authentication" section in the detail view.
The two levels serve different consumers and never conflict. Capability rows are written by the store during Create/Update (existing flow). SecurityStore is written by the parser after parsing (new flow, parallel to CardStore).
Layer boundaries (arch-go)¶
| New code | Layer | Dependencies |
|---|---|---|
internal/model/a2a_capabilities.go (enriched) |
Foundation | None |
internal/model/security_detail.go |
Foundation | None |
internal/kernel/plugin.go (SecurityStorePlugin) |
Core | Foundation |
internal/kernel/kernel.go (accessor) |
Core | Foundation |
internal/kernel/core_registry.go (register) |
Core | Foundation |
internal/kernel/plugin_manager.go (assertion) |
Core | Foundation |
plugins/securitystore/ |
Plugins | Kernel + Foundation |
plugins/parsers/a2a/ (extended) |
Plugins | Kernel + Foundation |
internal/api/handlers.go (extended) |
API | Core + Infrastructure |
Out-of-band Notes¶
- The connection recipe is deliberately a template, not a working command. Using
<token>and<key>placeholders makes it clear the user must supply real credentials. securityRequirementsentries are OR'd per the A2A spec (client must satisfy at least one complete entry). Within a single entry, schemes are AND'd. The UI must communicate this clearly.- The auth badge in the list view is intentionally compact — a summary signal, not the full story.
- After 2.5, the remaining Tier 2 features are 2.1 (OpenTelemetry) and 2.4 (Helm chart production-ready). Both are infra/DevOps focused.
- Future Tier 3 work on JWS signature verification will build on the parsing infrastructure added here.
- The
SecurityStorePluginis designed to be protocol-agnostic. When MCP defines its own security model, the same plugin and transport types can be reused — just a different parser populates them.