Skip to content

feat: Plugin catalog + abctl visibility & validation#449

Merged
huang195 merged 15 commits into
rossoctl:mainfrom
huang195:feat/plugin-catalog
May 29, 2026
Merged

feat: Plugin catalog + abctl visibility & validation#449
huang195 merged 15 commits into
rossoctl:mainfrom
huang195:feat/plugin-catalog

Conversation

@huang195

@huang195 huang195 commented May 29, 2026

Copy link
Copy Markdown
Member

Summary

Adds a plugin catalog to authbridge + abctl, giving operators visibility into the registered-plugin set without reading source. Plus several follow-on fixes that surfaced during testing of the new editor in #448.

Backend (framework + wire)

  • pipeline.PluginCapabilities gains Description — a one-line operator-facing label, populated on every registered plugin (jwt-validation, token-exchange, a2a-parser, mcp-parser, inference-parser, ibac, token-broker).
  • plugins.Catalog() — new accessor returning [{Name, Capabilities}] for every registered plugin. Memoized on first call; constructor contract tightened to "MUST NOT allocate goroutines/connections" with a factory-invariant test that locks the load-bearing claim.
  • /v1/pipeline carries the full metadataRequires/RequiresAny/After/Claims/Description (all omitempty).
  • New GET /v1/plugins — registered-plugin catalog endpoint; gated on WithCatalog Option. Three binaries (proxy, envoy, lite) wire sessionapi.PluginsCatalog identically.

abctl UI

  • P key opens the plugin catalog browser pane — sortable table of every registered plugin with description + dependency declarations. Lazy-fetched, cached for the session, r refreshes.
  • Pipeline pane gains a DEPS column — ✓/✗/blank per plugin row based on whether Requires/RequiresAny/After are satisfied. Footer hint counts plugins with unmet deps.
  • Plugin-detail pane renders Description + per-Requires ✓/✗ with the satisfying upstream's position.
  • Pre-apply validation in the editor — abctl runs Requires/RequiresAny/After/Claims/unknown-name checks against the cached catalog before showing the diff. Issues render as a red banner above the diff in ~50ms instead of waiting through kubelet sync (~60s) for the framework's reload error. Non-blocking — the prompt becomes "apply anyway? (y/N)".

Follow-on fixes that surfaced during testing

  • Parser Skip on empty response body (mcp/a2a/inference) — fixes pairing for JSON-RPC notifications (notifications/initialized) that ack with HTTP 202: parsers now record Skip("no_response_body") so abctl's pair-by-(plugin, method, direction) keying works.
  • Post-splice YAML validation in editor — catches indentation mismatches that produce a subtree that parses standalone but breaks the combined inner YAML, before kubectl apply rather than after a 60s reload roundtrip.
  • Pair-aware skip filtering in events_pane — skips that pair with non-skip rows stay visible (avoids orphan glyphs); continuation rows always render the phase text alongside the tree glyph.
  • reverseproxy cross-conversation session leak — inbound A2A request bucketing was A2A.SessionID || ActiveSession() || Default. The middle fallback silently routed a new chat's first turn into a previous chat's still-alive bucket. Fix mirrors what extproc already does: drop the ActiveSession() fallback; route to DefaultSessionID and rely on the existing rekey-on-response.

Test Plan

  • go test -race ./... across authlib (all packages) and cmd/abctl (all packages) — all green
  • TestCatalog_FactoryInvariant runs across all 6 registered plugins — locks the Capabilities() consistency contract
  • TestInboundSessionID_NoActiveSessionFallback + TestRecordInbound_NewChatDoesntLeakIntoPriorBucket — regression coverage for the session-leak fix
  • TestEditFlow_PostSpliceYAMLError — locks the splice-context YAML validation
  • TestEditFlow_PairedSkipVisibility — pair-aware skip filtering covered via existing tests in tui/
  • Parser tests for all three parsers' OnResponse_EmptyBody now assert the Skip("no_response_body") invocation
  • TestHandlePluginCatalog_* — new endpoint covered (lists + 404 when no provider)
  • Pre-apply validation: TestValidatePipeline_* — happy path, missing Requires, misordered Requires, After misorder, Unknown plugin, Claims conflict, nil-catalog skip
  • Live e2e against IBAC + weather-agent in kagenti dev cluster — /v1/plugins reachable, DEPS column shows ✓ for ibac (mcp-parser upstream), Catalog pane renders 6 plugins, pre-apply validation catches bogus-plugin typo, two consecutive chats no longer cross-contaminate session buckets, paired req/resp visible for notifications/initialized

Pre-PR review

A whole-branch reviewer (Opus) flagged 10 findings; 3 IMPORTANT and 3 NITs addressed in commit 2266bbe:

  1. Catalog memoized to bound constructor side-effect surface; contract tightened
  2. TestCatalog_FactoryInvariant added to lock the consistency claim
  3. Validator's After semantics now match framework's validateRelationships exactly
  4. CatalogEntry uses readsBody (modern) not bodyAccess (deprecated)
  5. previousPane uses explicit paneNone sentinel
  6. tokenbroker description rewritten to be direction-agnostic

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Plugin catalog browser in the TUI (browse, refresh, open with "P") and new Catalog pane
    • Session API endpoints: GET /v1/plugins (plugin catalog) and expanded GET /v1/pipeline (richer capability metadata)
    • Pre-apply pipeline validation showing dependency/claim warnings before apply
    • DEPS column and detailed plugin dependency/status views in TUI; plugin descriptions shown
  • Improvements

    • Stricter session event bucketing and improved request/response pairing/skip visibility (fewer misattributed events)

Review Change Stack

huang195 added 13 commits May 29, 2026 11:36
Operators currently have no way to know what a plugin does without
reading its source or hunting through CLAUDE.md. Add a one-line
Description field to PluginCapabilities and populate it on every
registered plugin. This is the data that abctl's plugin-detail and
catalog panes will surface in subsequent commits.

The godoc on Description also documents the static-metadata
constraint: Capabilities() must return the same value for any
instance produced by a given factory, because the catalog endpoint
walks factory()→Capabilities() to introspect plugins without holding
live instances.

No behavioral change: Description is purely additive metadata.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Surfaces the registered-plugin metadata that abctl needs to render
its catalog browser pane and pre-apply validation. Calls each
factory once with no config — Capabilities() on a freshly-built
instance is the static type-level metadata. The framework's existing
constructors are all cheap (allocate-only) so this is fine; the
godoc on PluginCapabilities documents the constraint going forward.

No new behavior in /v1/* yet — that wiring lands in the next commit.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Two complementary changes:

1. /v1/pipeline now carries Requires/RequiresAny/After/Claims/
   Description on each plugin (omitempty). abctl will use these to
   render the plugin-detail pane and a "deps satisfied" indicator on
   the Pipeline pane in subsequent commits.

2. New GET /v1/plugins lists every registered plugin's metadata,
   not just the ones in the active pipeline. abctl will render this
   in a catalog browser pane so operators can see what's available
   before adding one. The endpoint is gated on a CatalogProvider
   (WithCatalog option); without it the endpoint 404s. The three
   binaries (proxy, envoy, lite) all wire sessionapi.PluginsCatalog
   which adapts plugins.Catalog().

The /v1/pipeline change also fixes a pre-existing wire issue:
BodyAccess now reflects ReadsBody (post-Normalize) so plugins that
declare ReadsBody-only correctly surface as having body access.
Backward-compatible because every plugin that previously set
BodyAccess still does.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Decode the Requires/RequiresAny/After/Claims/Description fields the
server now emits on /v1/pipeline. Add PluginCatalog/PluginCatalogEntry
types and a GetPluginCatalog method for the new /v1/plugins endpoint.

Wire-format only — abctl's UI changes follow.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Two related abctl-side changes consuming the new wire fields:

Plugin-detail pane (showPluginDetail):
- Render Description as a hint line under the plugin name when
  populated.
- New Requires / Requires any of / After (soft) / Claims sections.
  When the plugin is part of an active chain, each Requires entry
  gets a ✓/✗ indicator with the satisfying upstream's position;
  ✗ entries are flagged "NOT in this chain". After entries report
  ✓/✗ for misorder, marking absent ones as soft-OK. RequiresAny
  shows per-alternative satisfaction so operators see which option
  is currently doing the work.

Pipeline pane (rebuildPipelineTable):
- New DEPS column. ✓ when all declared dependencies are met, ✗ when
  any fail, blank when the plugin declares no deps. Prevents a
  "looks fine" misread on plugins with nothing to verify.
- Footer hint counts plugins with unmet deps so a single ✗ in a
  long list doesn't get lost ("· 1 plugin with unmet deps").

The dependency-checking helpers (deps.go) are shared between the
two surfaces; deps_test.go covers Requires/RequiresAny/After
satisfaction logic plus the unmet-count walker.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
New paneCatalog renders /v1/plugins as a table with NAME / REQUIRES /
DESCRIPTION columns. Pressing `P` from any session-view pane opens
the catalog (lazy-loaded on first open, cached for the session;
`r` refreshes from inside the pane). `Enter` on a row drills into
the existing plugin-detail pane, reusing showPluginDetail with a
synthetic PipelinePlugin built from the catalog entry — Direction
and Position are blank, so showPluginDetail elides them gracefully.

`Esc` from the catalog returns to whichever pane invoked it
(previousPane is captured at P-press time). `Esc` from plugin-detail
also routes back to the catalog when the detail was opened from
there, so the back-out chain is symmetric.

Operators can browse the registered plugin set without reading
source — descriptions and dependency declarations come straight from
the framework.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
After the user saves their pipeline edit, abctl now runs the same
Requires/RequiresAny/After/Claims/unknown-name checks the framework
runs at reload-time, BUT against the locally-cached plugin catalog —
result lands in ~50ms instead of waiting through kubelet sync (~60s)
to discover the issue.

ValidatePipeline parses the proposed YAML, walks each direction,
and surfaces structured errors. The diff overlay renders them as a
red banner above the diff, with one bullet per issue:

  ⚠ 1 validation issue — framework reload will reject:
    • [outbound] ibac pos 1: Requires "mcp-parser", but it is not in the outbound chain

The y/N prompt becomes "apply anyway? (y/N)" so it's still possible
to push past the warning — abctl is the fast-feedback layer; the
framework's validateRelationships is the source of truth and will
fire again at reload regardless.

Validation is silently skipped when the catalog isn't loaded
(operator hasn't pressed P yet); a single visit to the Catalog pane
populates m.catalog for the rest of the session.

Tests: validate_test.go covers happy path, missing Requires,
misordered Requires, After misorder, Unknown plugin, Claims
conflict, and nil-catalog skip.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Cover the new abctl-side surfaces in cmd/abctl/README.md:
- Add P/r keybind rows.
- Update the Panes section to list Pipeline, Plugin detail, and
  Catalog (the new browser).
- New "Pre-apply validation" subsection under "Editing the pipeline"
  documents the catalog-backed warnings that land before kubectl
  apply.
- New top-level "Plugin dependencies" section names
  Requires/RequiresAny/After/Claims and lists the three places
  abctl surfaces them.

Cover /v1/pipeline and /v1/plugins in authbridge/CLAUDE.md's
session-API endpoint table — the previous version only listed
the session-related endpoints.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
When a parser's OnResponse runs on a request it had previously parsed
(Extensions.X populated) but the response body is empty — the typical
case for JSON-RPC notifications that ack with HTTP 202 — the parser
was returning Continue without recording any Invocation. abctl's
events table then displayed the response row with `—` for plugin and
action, and the events_pane.go pairing logic (which keys on
plugin+method+direction) couldn't pair the request and response,
leaving both rows orphaned and missing the ┌/└ tree glyphs.

Fix: split the early-return guard. Stay silent only when the request
side never participated (Extensions.X == nil). When the parser DID
process the request but the response is empty, record a Skip with
reason "no_response_body" so abctl can pair the rows.

Applied to all three parsers (mcp, a2a, inference) — same pattern,
same gap. Each gets a strengthened OnResponse_EmptyBody test
asserting the new Skip Invocation contract.

Triggers visibly on `notifications/initialized` (MCP) and
`message/cancel` style fire-and-forget A2A calls. The skip rows are
hidden by default in abctl (action=skip is muted unless `s` is
toggled), but pairing now works regardless.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Pre-apply validation parsed only the user's edited subtree, so an
indentation mismatch between the edit and the splice site produced
a subtree that parses standalone yet breaks the combined inner YAML.
The error wouldn't surface until the framework's hot-reload tried
the spliced doc 60s later, then auto-rollback fired with a stale
"mapping values are not allowed" error pointing at a line operators
can't trace back to their edit.

Splice the edit into the inner YAML and yaml.Unmarshal the result
in the editorExitedMsg handler, before transitioning to the diff
phase. Failures route to editPhaseError immediately with a hint that
the pipeline: subtree must start at column 0.

Regression test: TestEditFlow_PostSpliceYAMLError supplies an edit
with leading whitespace on `pipeline:` — passes the standalone YAML
check, fails the post-splice check.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Two events_pane rendering fixes the empty-body parser-skip work
exposed:

1. Skip rows that pair with a non-skip request were getting hidden
   under the default skip-filter, leaving the request's ┌ glyph
   orphaned. The classic case: mcp-parser/observe on a
   notifications/initialized request paired with mcp-parser/skip/
   no_response_body on the response — the response would vanish from
   the timeline, making a successful exchange read as a missing
   response. New rule: a skip stays visible when its pair partner is
   non-skip; only fully-skip pairs (both ends skip) get hidden.

2. Continuation rows (multiple invocations on one event) were
   rendering only the tree glyph in the PHASE column, blanking
   "req"/"resp". Operators scanning the column couldn't tell which
   side of the lifecycle each plugin's row belonged to. Render
   prefix + shortPhase always — the 6-char budget (outer + inner +
   "resp") still fits.

Together: notifications/initialized exchanges now show both rows
with ┌req/└resp glyphs, and a2a-parser on a multi-plugin inbound
event correctly displays "req" alongside its tree glyph.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Inbound A2A request bucketing was: A2A.SessionID || ActiveSession() ||
DefaultSessionID. The middle fallback was a cross-conversation
contamination vector — a new chat's first turn (empty contextId)
silently inherited the previous chat's session bucket as long as
that bucket was still inside its TTL.

Symptom: every event of a new chat (inbound request + all outbound
MCP/inference work the agent fired) landed in the prior chat's
bucket. Only the response, by then carrying the agent-assigned
contextId, created a new bucket — leaving the old bucket polluted
and the new bucket as a 1-event orphan with just the response.

Extract inboundSessionID() helper mirroring extproc's
inboundSessionID(): trust the A2A-stated contextId when non-empty,
otherwise route to DefaultSessionID. The rekey-on-response path
already migrates Default → contextId once the agent reveals it, so
the new chat lands cleanly in its own bucket.

Applied to all three reverseproxy bucketing sites: inbound request
recording, inbound response recording, recordInboundReject.

New regression tests:
- TestInboundSessionID_NoActiveSessionFallback (table-driven first
  turn vs subsequent turn)
- TestInboundSessionID_NoA2AExtension (auth-only path)
- TestRecordInbound_NewChatDoesntLeakIntoPriorBucket (integration)

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Five findings from the pre-PR review:

1. plugins.Catalog() now memoizes the result on first call. Bounds the
   constructor side-effect surface: even a misbehaving plugin that
   leaks a goroutine in its constructor leaks one process-wide rather
   than per /v1/plugins request. Tightens the constructor contract
   from "should be cheap" to "MUST NOT allocate goroutines /
   connections / file handles" with explicit godoc. Cache is reset by
   resetCatalogCache() for tests.

2. New TestCatalog_FactoryInvariant runs across every registered
   plugin: calls factory twice, asserts Capabilities() is byte-equal
   between the two instances. Locks the load-bearing claim that
   underlies the catalog cache (one snapshot describes every instance).

3. Validator's After semantics now match the framework's
   validateRelationships exactly (rp >= pos, not rp > pos). Previously
   abctl could pass YAML the framework would reject on duplicate
   plugin names.

4. CatalogEntry wire shape uses readsBody (modern) instead of
   bodyAccess (deprecated alias). New endpoint, no migration cost; the
   parallel pipelinePluginView keeps bodyAccess for compat with abctl
   versions shipped pre-catalog.

5. previousPane uses an explicit paneNone = -1 sentinel instead of
   relying on paneNamespaces being the zero value. Future-proofs
   against P opening the catalog from new entry points.

token-broker description tweaked: was "Inbound token broker" but the
plugin can be configured outbound too. Operator-facing labels should
be direction-agnostic.

Tests: full authlib + abctl suites green with -race.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c4743c4b-8f79-41a8-a9cd-43c07861fdd9

📥 Commits

Reviewing files that changed from the base of the PR and between 8d9da0a and a1caecd.

📒 Files selected for processing (6)
  • authbridge/authlib/plugins/registry.go
  • authbridge/authlib/plugins/registry_test.go
  • authbridge/cmd/abctl/README.md
  • authbridge/cmd/abctl/tui/app.go
  • authbridge/cmd/abctl/tui/deps.go
  • authbridge/cmd/abctl/tui/deps_test.go
✅ Files skipped from review due to trivial changes (1)
  • authbridge/cmd/abctl/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • authbridge/cmd/abctl/tui/deps_test.go
  • authbridge/authlib/plugins/registry.go
  • authbridge/cmd/abctl/tui/app.go

📝 Walkthrough

Walkthrough

Adds a memoized plugin catalog and session API surface (/v1/plugins), expands plugin capability metadata, centralizes inbound session bucketing, implements pipeline pre-apply validation, and extends abctl with a Catalog pane, dependency evaluation, and related client/server tests and wiring.

Changes

Plugin Dependency System & Catalog Surface

Layer / File(s) Summary
Inbound session bucketing refactor
authbridge/authlib/listener/reverseproxy/server.go, authbridge/authlib/listener/reverseproxy/server_test.go
Centralizes inbound session bucket selection in inboundSessionID(pctx) and adds tests ensuring first-turn/default behavior and no ActiveSession fallback.
PluginCapabilities.Description & parser OnResponse guards
authbridge/authlib/pipeline/plugin.go, authbridge/authlib/plugins/*/plugin.go, authbridge/authlib/plugins/*/plugin_test.go
Adds PluginCapabilities.Description; refines parser response-phase logic to treat missing request-side extension as no-op and empty response bodies as pctx.Skip("no_response_body"), updating tests to assert Skip invocations where applicable.
Plugin registry catalog & memoization
authbridge/authlib/plugins/registry.go, authbridge/authlib/plugins/registry_test.go
Adds exported CatalogEntry, Catalog() with mutex-protected cache, defensive cloning, resetCatalogCache() for tests, and WarmCatalog() to prime at boot; tests validate inclusion, invariants, and defensive-copy semantics.
Session API adapter & server surface
authbridge/authlib/sessionapi/catalog_adapter.go, authbridge/authlib/sessionapi/server.go, authbridge/authlib/sessionapi/server_test.go
Implements PluginsCatalog() adapter, adds WithCatalog option, registers GET /v1/plugins (404 when provider unset), expands /v1/pipeline view with requires, requiresAny, after, claims, and description, and tests pipeline metadata and catalog endpoints.
abctl API client: plugin catalog & pipeline decoding
authbridge/cmd/abctl/apiclient/client.go, authbridge/cmd/abctl/apiclient/client_test.go
Expands PipelinePlugin wire shape, adds PluginCatalog/PluginCatalogEntry types and GetPluginCatalog method; tests decode pipeline capability metadata and catalog responses.
Pipeline validation system
authbridge/cmd/abctl/edit/validate.go, authbridge/cmd/abctl/edit/validate_test.go
Implements ValidatePipeline and validateChain to report unknown plugins, unsatisfied Requires/RequiresAny, misordered After, and claim conflicts; includes comprehensive tests and fixture catalog.
abctl TUI core: catalog pane and commands
authbridge/cmd/abctl/tui/app.go, authbridge/cmd/abctl/tui/keys.go, authbridge/cmd/abctl/tui/namespaces_pane.go
Adds paneCatalog, model catalog/table state, loadCatalogCmd/catalogLoadedMsg, lazy fetch/refresh (P/r keys), previousPane navigation, and integrates edit splice/validation.
Catalog table & selected entry
authbridge/cmd/abctl/tui/catalog_pane.go, authbridge/cmd/abctl/tui/catalog_pane_test.go
Implements newCatalogTable, rebuildCatalogTable, and selectedCatalogEntry, with tests for rendering, selection mapping, and nil-catalog behavior.
Dependency helpers & tests
authbridge/cmd/abctl/tui/deps.go, authbridge/cmd/abctl/tui/deps_test.go
Adds depCheck and dependency evaluation helpers (requiresStatus, requiresAnyStatus, afterStatus, pluginDepsAllSatisfied, pluginHasAnyDeps, formatDepCheck) and tests covering multiple dependency scenarios and unmet-deps counting.
Pipeline table & plugin detail UI enhancements
authbridge/cmd/abctl/tui/pipeline_pane.go, authbridge/cmd/abctl/tui/plugin_detail_pane.go, authbridge/cmd/abctl/tui/plugin_detail_pane_test.go, authbridge/cmd/abctl/tui/events_pane.go, authbridge/cmd/abctl/tui/edit_overlay.go, authbridge/cmd/abctl/tui/edit_e2e_test.go
Adds DEPS column with ✓/✗ markers, conditional plugin detail sections for dependencies and description, refined events skip-row filtering and phase column, edit diff validation banner with "apply anyway" prompt, and a post-splice YAML error e2e test.
Docs & server wiring
authbridge/CLAUDE.md, authbridge/cmd/abctl/README.md, authbridge/cmd/authbridge-*/main.go
Document new session API endpoints (/v1/pipeline, /v1/plugins), update abctl README for catalog pane, pre-apply validation and keybindings, warm catalog at startup and wire WithCatalog(sessionapi.PluginsCatalog) in binaries.

🎯 4 (Complex) | ⏱️ ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SessionAPI
  participant PluginRegistry
  Client->>SessionAPI: GET /v1/plugins
  alt CatalogProvider set
    SessionAPI->>PluginRegistry: Catalog()
    PluginRegistry-->>SessionAPI: []CatalogEntry
    SessionAPI-->>Client: 200 {plugins: [...]}
  else
    SessionAPI-->>Client: 404
  end
Loading

Possibly Related PRs

Suggested reviewers

  • pdettori

"I'm a rabbit in a tidy code glen,
Catalogs and deps hop in again.
Checks turn green when friends align,
TUI shows the dance in tidy lines.
Hooray, the plugins sing — all set to shine!" 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Plugin catalog + abctl visibility & validation' clearly and concisely describes the main changes: adding a plugin catalog feature and enhancing abctl's visibility and validation capabilities. It is specific, relevant, and directly corresponds to the primary objectives of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
authbridge/cmd/abctl/edit/validate_test.go (1)

10-150: ⚡ Quick win

Add a direction-mismatch validation test case.

Current tests don’t exercise placing a cataloged inbound-only plugin in the outbound chain (or inverse), which is a useful guard for pre-apply validation behavior.

Proposed test sketch
 func validateFixtureCatalog() []apiclient.PluginCatalogEntry {
 	return []apiclient.PluginCatalogEntry{
-		{Name: "jwt-validation", Description: "Inbound JWT"},
+		{Name: "jwt-validation", Direction: "inbound", Description: "Inbound JWT"},
 		{Name: "a2a-parser", Description: "Parser"},
 		{Name: "mcp-parser", Description: "MCP parser"},
 		{Name: "ibac", Description: "IBAC", Requires: []string{"mcp-parser"}, After: []string{"a2a-parser"}},
 		{Name: "claim-a", Claims: []string{"authorization-header"}},
 		{Name: "claim-b", Claims: []string{"authorization-header"}},
 	}
 }
+
+func TestValidatePipeline_DirectionMismatch(t *testing.T) {
+	yaml := `pipeline:
+  outbound:
+    plugins:
+      - name: jwt-validation
+`
+	errs := ValidatePipeline([]byte(yaml), validateFixtureCatalog())
+	found := false
+	for _, e := range errs {
+		if strings.Contains(e.Message, "cannot run in outbound chain") {
+			found = true
+		}
+	}
+	if !found {
+		t.Fatalf("expected direction mismatch error; got %+v", errs)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/cmd/abctl/edit/validate_test.go` around lines 10 - 150, Add a test
that verifies ValidatePipeline rejects a plugin placed in the wrong direction:
extend validateFixtureCatalog to include a plugin with an explicit direction
(e.g., add a PluginCatalogEntry like {Name: "inbound-only", Description:
"Inbound only", Direction: "inbound"}) and add a new test
TestValidatePipeline_DirectionMismatch that places "inbound-only" under outbound
in the YAML, calls ValidatePipeline([]byte(yaml), validateFixtureCatalog()),
asserts an error is returned, and checks the error message mentions the
direction mismatch (e.g., contains "inbound" or "Inbound-only") and references
the PluginName "inbound-only". Ensure you use the existing ValidatePipeline and
validateFixtureCatalog identifiers so the test lives alongside the other
TestValidatePipeline_* cases.
authbridge/cmd/abctl/apiclient/client_test.go (1)

150-180: ⚡ Quick win

Add an explicit 404 contract test for GetPluginCatalog.

GetPluginCatalog documents graceful ErrNotFound behavior, but this path is currently untested.

Proposed test
+func TestGetPluginCatalog_404(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path != "/v1/plugins" {
+			t.Fatalf("wrong path: %q", r.URL.Path)
+		}
+		http.NotFound(w, r)
+	}))
+	defer ts.Close()
+
+	c := New(ts.URL)
+	_, err := c.GetPluginCatalog(context.Background())
+	if !errors.Is(err, ErrNotFound) {
+		t.Fatalf("expected ErrNotFound, got %v", err)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/cmd/abctl/apiclient/client_test.go` around lines 150 - 180, Add a
test that verifies GetPluginCatalog returns the documented not-found error:
create a new httptest.Server (or a sub-case in TestGetPluginCatalog) whose
handler returns a 404 for "/v1/plugins" (with optional empty body), call c :=
New(ts.URL) and then c.GetPluginCatalog(context.Background()), and assert the
returned error matches ErrNotFound (use errors.Is(err, ErrNotFound) if the code
wraps errors). Name the test to indicate the 404 contract (e.g.,
TestGetPluginCatalog_NotFound) and keep the existing happy-path test unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@authbridge/authlib/plugins/registry.go`:
- Around line 124-129: Catalog() currently returns the shared cached slice
(catalogCacheVal) directly which allows callers to mutate the cache; change
Catalog() to return a defensive deep copy: after taking catalogCacheVal into a
local variable, allocate a new slice and copy each element, and for elements
that contain nested slices or mutable fields (e.g., the capabilities/capability
slices inside the Plugin metadata structs) copy those nested slices/structures
as well so the returned value is fully independent of catalogCacheVal; keep
using catalogCacheMu for locking but always return the deep-copied slice instead
of the original catalogCacheVal.

In `@authbridge/cmd/abctl/edit/validate.go`:
- Around line 96-106: The current lookup using byName[p.Name] only checks
existence and can accept plugins whose catalog direction doesn't match the
target; after locating entry := byName[p.Name], verify the catalog's allowed
direction(s) on entry (e.g. compare entry.Direction or entry.Directions) against
the current direction variable and, if they conflict, append a ValidationError
(same fields: Direction: direction, PluginName: p.Name, Position: pos) with a
message saying the plugin is not valid for that direction; keep the existing
continue behavior when reporting the error.

In `@authbridge/cmd/abctl/README.md`:
- Around line 147-150: The fenced code block in README.md (the block showing the
validation warning) lacks a language tag, causing markdownlint MD040 failures;
update the block surrounding the lines that start with "⚠ 1 validation issue —
framework reload will reject:" to use a language tag (e.g., replace ``` with
```text) so the fenced example is explicitly marked as plain text.

In `@authbridge/cmd/abctl/tui/app.go`:
- Around line 233-240: The session-scoped plugin catalog cache (m.catalog)
persists across pod switches and must be cleared when the connection context
changes; update the reconnection/teardown path (e.g., inside backToPodsPane() or
wherever the app resets connection state) to set m.catalog = nil so the next
time the catalog pane opens (catalog pane code that checks m.catalog) it will
fetch /v1/plugins anew for the current pod.

In `@authbridge/cmd/abctl/tui/deps.go`:
- Around line 55-71: The current requiresAnyOK implementation returns true if
any named dependency appears earlier, but we must instead ensure that every name
listed in p.RequiresAny that is present in the chain is positioned earlier than
p; change requiresAnyOK to iterate each name and: if the name is found in chain
and its q.Position >= p.Position, return false; after checking all names return
true (and still return true when none are present). Apply the same corrected
logic to the other occurrence referenced (the similar block at lines 116-134 /
the other helper used by pluginDepsAllSatisfied) so DEPS/footer and
pluginDepsAllSatisfied use the stricter validation semantics.

---

Nitpick comments:
In `@authbridge/cmd/abctl/apiclient/client_test.go`:
- Around line 150-180: Add a test that verifies GetPluginCatalog returns the
documented not-found error: create a new httptest.Server (or a sub-case in
TestGetPluginCatalog) whose handler returns a 404 for "/v1/plugins" (with
optional empty body), call c := New(ts.URL) and then
c.GetPluginCatalog(context.Background()), and assert the returned error matches
ErrNotFound (use errors.Is(err, ErrNotFound) if the code wraps errors). Name the
test to indicate the 404 contract (e.g., TestGetPluginCatalog_NotFound) and keep
the existing happy-path test unchanged.

In `@authbridge/cmd/abctl/edit/validate_test.go`:
- Around line 10-150: Add a test that verifies ValidatePipeline rejects a plugin
placed in the wrong direction: extend validateFixtureCatalog to include a plugin
with an explicit direction (e.g., add a PluginCatalogEntry like {Name:
"inbound-only", Description: "Inbound only", Direction: "inbound"}) and add a
new test TestValidatePipeline_DirectionMismatch that places "inbound-only" under
outbound in the YAML, calls ValidatePipeline([]byte(yaml),
validateFixtureCatalog()), asserts an error is returned, and checks the error
message mentions the direction mismatch (e.g., contains "inbound" or
"Inbound-only") and references the PluginName "inbound-only". Ensure you use the
existing ValidatePipeline and validateFixtureCatalog identifiers so the test
lives alongside the other TestValidatePipeline_* cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 640d0208-0abc-484e-9040-02797357d8ef

📥 Commits

Reviewing files that changed from the base of the PR and between b047b20 and 2266bbe.

📒 Files selected for processing (40)
  • authbridge/CLAUDE.md
  • authbridge/authlib/listener/reverseproxy/server.go
  • authbridge/authlib/listener/reverseproxy/server_test.go
  • authbridge/authlib/pipeline/plugin.go
  • authbridge/authlib/plugins/a2aparser/plugin.go
  • authbridge/authlib/plugins/a2aparser/plugin_test.go
  • authbridge/authlib/plugins/ibac/plugin.go
  • authbridge/authlib/plugins/inferenceparser/plugin.go
  • authbridge/authlib/plugins/inferenceparser/plugin_test.go
  • authbridge/authlib/plugins/jwtvalidation/plugin.go
  • authbridge/authlib/plugins/mcpparser/plugin.go
  • authbridge/authlib/plugins/mcpparser/plugin_test.go
  • authbridge/authlib/plugins/registry.go
  • authbridge/authlib/plugins/registry_test.go
  • authbridge/authlib/plugins/tokenbroker/plugin.go
  • authbridge/authlib/plugins/tokenexchange/plugin.go
  • authbridge/authlib/sessionapi/catalog_adapter.go
  • authbridge/authlib/sessionapi/server.go
  • authbridge/authlib/sessionapi/server_test.go
  • authbridge/cmd/abctl/README.md
  • authbridge/cmd/abctl/apiclient/client.go
  • authbridge/cmd/abctl/apiclient/client_test.go
  • authbridge/cmd/abctl/edit/validate.go
  • authbridge/cmd/abctl/edit/validate_test.go
  • authbridge/cmd/abctl/tui/app.go
  • authbridge/cmd/abctl/tui/catalog_pane.go
  • authbridge/cmd/abctl/tui/catalog_pane_test.go
  • authbridge/cmd/abctl/tui/deps.go
  • authbridge/cmd/abctl/tui/deps_test.go
  • authbridge/cmd/abctl/tui/edit_e2e_test.go
  • authbridge/cmd/abctl/tui/edit_overlay.go
  • authbridge/cmd/abctl/tui/events_pane.go
  • authbridge/cmd/abctl/tui/keys.go
  • authbridge/cmd/abctl/tui/namespaces_pane.go
  • authbridge/cmd/abctl/tui/pipeline_pane.go
  • authbridge/cmd/abctl/tui/plugin_detail_pane.go
  • authbridge/cmd/abctl/tui/plugin_detail_pane_test.go
  • authbridge/cmd/authbridge-envoy/main.go
  • authbridge/cmd/authbridge-lite/main.go
  • authbridge/cmd/authbridge-proxy/main.go

Comment thread authbridge/authlib/plugins/registry.go
Comment on lines +96 to +106
entry, known := byName[p.Name]
if !known {
errs = append(errs, ValidationError{
Direction: direction,
PluginName: p.Name,
Position: pos,
Message: fmt.Sprintf("Unknown plugin %q (not in /v1/plugins catalog)",
p.Name),
})
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate plugin direction against the target chain.

At Line 96, name-only catalog lookup can treat an inbound-only plugin as valid in outbound (and vice versa), so pre-apply validation can miss a real misconfiguration.

Proposed fix
 		entry, known := byName[p.Name]
 		if !known {
 			errs = append(errs, ValidationError{
 				Direction:  direction,
 				PluginName: p.Name,
 				Position:   pos,
 				Message: fmt.Sprintf("Unknown plugin %q (not in /v1/plugins catalog)",
 					p.Name),
 			})
 			continue
 		}
+		if entry.Direction != "" && entry.Direction != direction {
+			errs = append(errs, ValidationError{
+				Direction:  direction,
+				PluginName: p.Name,
+				Position:   pos,
+				Message: fmt.Sprintf("Plugin %q is %s-only, cannot run in %s chain",
+					p.Name, entry.Direction, direction),
+			})
+			continue
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/cmd/abctl/edit/validate.go` around lines 96 - 106, The current
lookup using byName[p.Name] only checks existence and can accept plugins whose
catalog direction doesn't match the target; after locating entry :=
byName[p.Name], verify the catalog's allowed direction(s) on entry (e.g. compare
entry.Direction or entry.Directions) against the current direction variable and,
if they conflict, append a ValidationError (same fields: Direction: direction,
PluginName: p.Name, Position: pos) with a message saying the plugin is not valid
for that direction; keep the existing continue behavior when reporting the
error.

Comment thread authbridge/cmd/abctl/README.md Outdated
Comment thread authbridge/cmd/abctl/tui/app.go
Comment thread authbridge/cmd/abctl/tui/deps.go Outdated
huang195 added 2 commits May 29, 2026 14:18
Three suggestions worth addressing:

1. Eager Catalog() at boot. New plugins.WarmCatalog() helper is
   called in each binary's main() right before sessionapi.New, so any
   factory that violates the constructor contract (panics, allocates
   goroutines/connections/file handles) fails fast at startup rather
   than silently caching a faulty throwaway instance on the first
   /v1/plugins request. Cheap insurance — the result is already
   memoized so this just moves "first call" out of the request hot
   path.

2. Stale-catalog hint on the editor's "Unknown plugin" validation
   error. abctl caches /v1/plugins for the session; a freshly-
   installed plugin in-cluster won't appear until refresh. The
   message now hints at the staleness path:
       Unknown plugin "foo" (not in cached /v1/plugins; catalog may
       be stale, press P then r to refresh)
   The prompt is already non-blocking ("apply anyway? (y/N)"), so
   operators can still proceed; this just saves one round of
   confusion.

3. Document the auth-posture decision on /v1/plugins. The handler
   inherits the no-auth posture of the rest of /v1/*; the catalog
   reveals plugin metadata (names, deps, descriptions) but never
   user content or secrets, so it's fine. Doc-only addition so the
   next maintainer doesn't re-derive the conclusion.

Reviewer's rossoctl#4 was a compliment on the inboundSessionID comment
block — no action needed.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Four findings worth addressing:

1. Catalog() returns a defensive deep copy, not the cached slice.
   Callers can mutate the returned []CatalogEntry (sort/filter) and
   the nested capability []string fields without tainting the cache
   or polluting future /v1/plugins responses. New
   TestCatalog_ReturnsDefensiveCopy locks the contract: mutate first
   call's result, second call still sees originals.

2. RequiresAny semantics in deps.go now match the framework's
   validateRelationships exactly. Was: at-least-one-upstream wins.
   Now: at-least-one-upstream AND every present alternative must be
   earlier (downstream presence is a misorder, even if another
   alternative is upstream). Earlier the Pipeline pane DEPS column
   could show ✓ for chains the framework would reject. New
   TestPluginDepsAllSatisfied_RequiresAnyMisorderOnAlternate locks it.

3. backToPodsPane() now drops m.catalog (and the catalogTbl rows).
   Different pod = different framework instance = potentially
   different plugin versions registered. Without this the next P
   would show the prior pod's catalog with no refresh prompt.

4. README.md fenced code block gets a `text` language tag (markdownlint
   MD040).

Skipped CodeRabbit's "validate plugin direction against the target
chain" suggestion: the catalog deliberately doesn't populate
Direction (see catalog_adapter.go) — plugins are direction-agnostic
at the type level, with positional placement decided by config.
The framework's own validateRelationships doesn't enforce a per-
plugin direction either, so the suggestion would diverge from the
source-of-truth validation it's supposed to mirror.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>

@cwiklik cwiklik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Large but well-structured PR: adds plugin catalog endpoint (GET /v1/plugins) with proper gating via WithCatalog option, fixes a cross-chat session leak (all 3 call sites updated + test locks the invariant), and extends the abctl TUI with catalog browsing and pre-apply validation. Strong test coverage across all new features. No dependency changes, no security concerns.

Areas reviewed: Go (framework, API, CLI, tests), Docs, Security
Commits: 15 commits, all signed-off: yes
CI status: all checks passing

Two minor suggestions:

  1. reverseproxy/server.goinboundSessionID: The comment says it "Mirrors extproc's inboundSessionID." Consider extracting to a shared package (e.g., session.InboundID(pctx)) to prevent future drift between the two implementations.

  2. sessionapi/server.gohandlePluginCatalog: JSON encoding errors are logged at slog.Debug level. Since this is an API endpoint and the client receives a truncated/empty response on failure, slog.Warn might be more appropriate for operational visibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants