* [agent-network] Shared proto, OpenAPI schema, and generated types * [agent-network] Management: store, manager, synthesizer, policy engine, provider catalog, HTTP/gRPC API Adds the account-scoped agent-network module: provider/policy/budget CRUD and store, the reverse-proxy service synthesizer, policy selection + limit enforcement, the provider catalog (incl. Vertex AI and AWS Bedrock entries), and the management HTTP + proxy gRPC surfaces. * [management] Fix agent-network proxy-peer fan-out on affected-peer recompute The affected-peers resolver loaded only persisted reverse-proxy services, but agent-network services are synthesized on demand and never persisted. As a result the embedded proxy peer was never folded into the affected set when a client's group changed, so the proxy received no network-map update for a newly authorised client and rejected its handshake until a full resync (restart). loadProxyServices now merges the synthesized agent-network services (injected via a registration hook to avoid an import cycle), so proxy peers learn newly authorised clients immediately. * [proxy] Reverse-proxy middleware framework, chain, and request plumbing The per-target middleware chain (slots, dispatcher, mutation gate, metadata merger), body capture, access-log terminal sink, and the proxy wiring that builds + runs chains for synthesized agent-network services. * [proxy] LLM parsers, pricing, and builtin middlewares (OpenAI, Anthropic, Vertex AI, AWS Bedrock) Request/response parsers and SSE/event-stream metering, the embedded pricing table, and the builtin middleware set: request parser, router, policy limit-check/record, cost meter, guardrail, identity inject, response parser. Includes the path-routed providers — Google Vertex AI (keyfile:: service-account OAuth minting) and AWS Bedrock (bearer auth, invoke/converse/streaming, optional /bedrock prefix) — plus the Models allowlist and unmeterable-publisher deny. * [proxy] IPv6 in-place apply and TCP accept-loop hardening on netstack listeners * [agent-network] End-to-end test suite, module docs, and deployment preset * [agent-network] Fix codespell typos and exclude false positives - labelgen word pool: vermillion -> vermilion, racoon -> raccoon. - codespell ignore list: add flate (Go compress/flate package), recordin (a test-local identifier), and unparseable (a valid alternative spelling used consistently across identifiers + a metadata-value constant). * [management] Set LastSeen on injected proxy peer in realstack test (MySQL strict-mode) The injected embedded proxy peer had a PeerStatus with a zero LastSeen, which serializes to '0000-00-00' and is rejected by MySQL in strict mode (SQLite tolerates it). Set LastSeen to a valid time so SaveAccount succeeds on both engines. * [agent-network] Remove e2e shell-script suite from this branch The end-to-end shell scripts under scripts/e2e/ are maintained in a separate testing suite and are not part of this change set. * [agent-network] Polish module docs: remove internal review scaffolding, fix links, verify diagrams Strip PR-review framing, commit references, absolute paths, and stale internal references from the agent-network module docs; fix broken relative links; verify all diagrams against the current architecture. Remove the internal AI-reviewer prompt file. * [management] Refine session expiration handling to support 3-state encoding for SSO deadlines * [agent-network] Relocate agentnetwork package to internals/modules Move management/server/agentnetwork (and its catalog/, labelgen/, types/ subpackages) to management/internals/modules/agentnetwork, alongside the reverse-proxy module, and rewrite all importers. Pure relocation: package names, the synthesizer + affectedpeers registration hook, and store access (shared store.Store) are unchanged, so no import cycle is introduced (affectedpeers still depends only on the agentnetwork/types leaf). * [agent-network] Co-locate HTTP handlers in the module (RegisterEndpoints) Move the agent-network HTTP handlers from server/http/handlers/agentnetwork into the module at internals/modules/agentnetwork/handlers (package handlers) and rename the entrypoint AddEndpoints -> RegisterEndpoints, matching the reverse-proxy module convention. Wiring in http/handler.go updated accordingly.
20 KiB
dashboard — UI for agent-networks
This module documents code that lives in the dashboard repo (under
src/modules/agent-network/ and src/app/(dashboard)/agent-network/), not
in this repo. It is co-located here so backend readers see the full picture.
Risk level: Medium. The new surface is isolated under
src/modules/agent-network/andsrc/app/(dashboard)/agent-network/, but it also reshapes the sidebar, splits/peers, renamesreverse-proxy/clusters→self-hosted-proxies, and overlays the Control Center graph. Regressions here would be cross-cutting. Backward-compat impact: Additive on the API side. Breaking on URL/navigation:/peersredirects to/peers/devices(src/app/(dashboard)/peers/page.tsx:7-15),/reverse-proxy/clusterswas renamed to/reverse-proxy/self-hosted-proxies, the sidebar lost Access Control / Networks / Reverse Proxy / DNS / standalone Guardrails / Consumption / Activity (Navigation.tsx:165-171 — routes still resolve via URL), and the standalone/agent-network/{access-log,consumption,global-controls}routes are gone in favor of/agent-network/observability.
Module boundary
The dashboard is the only place an operator interacts with agent-networks: provider catalog, configured providers, policies, guardrails, account-level budget rules, account settings (collection / redaction toggles), per-request access log, and consumption rollups all render, paginate, and edit here. Data flows in via SWR (useFetchApi) keyed by REST URL. One big context provider (src/modules/agent-network/AIProvidersProvider.tsx) aggregates five resources (providers, policies, guardrails, budget rules, settings) plus the proxy access-log stream filtered to agent_network=true, and exposes add* / update* / toggle* / delete* mutators that call through useApiCall and re-mutate() SWR. Pages mount the provider once at the top and compose presentational tables and modals beneath. The control-center page additionally fetches /agent-network/{providers,policies} directly (control-center/page.tsx:123-130) to overlay graph nodes.
What the UI delivers
- AI Observability page with four tabs: Access Logs, Budget Dashboard, Budget Settings, Log Settings (replaces the standalone access-log, consumption, and global-controls routes).
- Providers page: provider catalog + connect/edit wizard with per-vendor copy (LiteLLM, Portkey, Bifrost, Cloudflare, Vercel, OpenRouter, custom).
- Policies page: group → provider authorization with per-policy Limits (minute-granular windows) + guardrail attach.
- Guardrails page: reusable model-allowlist + prompt-capture sets.
- Account controls: Log Collection / Prompt Collection / Redact PII toggles.
- Budget rules: account-level rules reusing the policy Limits UI.
- Control Center overlay: provider + agent-policy nodes on the graph.
- Navigation + peers reshaping: peers split into Devices / Agents,
reverse-proxy/clustersrenamed toself-hosted-proxies, sidebar repackaged for agent-network focus.
Surface added
New pages
| Route | Purpose | Backing module(s) |
|---|---|---|
/agent-network |
Redirect to /agent-network/providers |
page.tsx:7-15 |
/agent-network/providers |
List + connect providers; header surfaces per-account base URL | providers/page.tsx + AgentProvidersTable + AIProviderModal |
/agent-network/policies |
Group → Provider authorization with per-policy Limits + Guardrail attach | policies/page.tsx + AgentPoliciesTable + AgentPolicyModal |
/agent-network/guardrails |
Reusable guardrail sets (model allowlist + prompt capture) | guardrails/page.tsx + AgentGuardrailsTable + AgentGuardrailModal |
/agent-network/observability |
Tabs: Access Logs / Budget Dashboard / Budget Settings / Log Settings | observability/page.tsx |
/peers/devices, /peers/agents |
Split of /peers, shared via PeersListView keyed by kind |
peers/{devices,agents}/page.tsx |
/reverse-proxy/self-hosted-proxies |
Renamed from clusters |
self-hosted-proxies/page.tsx |
Removed in favor of /agent-network/observability: /agent-network/access-log, /agent-network/consumption, /agent-network/global-controls.
New modules under src/modules/agent-network
| File | Role |
|---|---|
| AIProvidersProvider.tsx (~1158 LOC) | Aggregates every agent-network resource via SWR; normalises snake↔camel; exposes mutators; holds wizard-open state |
| AIProviderModal.tsx (~1268 LOC) | Connect / edit provider wizard with per-vendor copy (Bifrost, Portkey, LiteLLM, Cloudflare, Vercel, OpenRouter, custom) |
| AIProviderLogo + useProviderCatalog | Catalog-driven brand swatch + SWR hook over /agent-network/catalog/providers |
| AgentPoliciesTable + AgentPolicyModal + AgentPolicyGuardrailsTab + AgentPolicyLimitsTab | Policies; modal has 3 tabs (Rule, Limits, Guardrails) |
| AgentGuardrailsTable + AgentGuardrailModal + AgentGuardrailBrowseModal + AgentGuardrailChecksCell | Guardrails CRUD + attach-from-policy |
| AgentBudgetRulesTable + AgentBudgetRuleModal | Account-level budget rules; modal reuses AgentPolicyLimitsTab verbatim |
| AgentAccountControlsCard | Three account-wide toggles (Log Collection / Prompt Collection / Redact PII) |
| AgentAccessLogTable + AgentAccessLogExpandedRow | Access log on /events/proxy?agent_network=true |
| AgentConsumptionPanel + AgentConsumptionTable | Token + cost panel: charts + counter table |
| table/AgentProvidersTable + AgentProviderActionCell | Providers table + per-row actions |
| data/mockData.ts | Domain types and a few residual MOCK_* constants (see scrutinize) |
Touched non-agent-network areas
- control-center: agent-network overlay (provider + agent-policy nodes); removed the All Networks dropdown; hid the Networks tab in FlowSelector (FlowSelector.tsx:9-14 — enum value kept so
?tab=networksstill type-checks); wrappedControlCenterViewinAIProvidersProvider(page.tsx:73-83);agentPolicyNodeclicks routed to a separate state slot (page.tsx:1871-1874). New node renderers: nodes/ProviderNode.tsx, nodes/AgentPolicyNode.tsx (registered at utils/nodes.ts:21-22). - peers: Split into Devices and Agents sub-routes; shared via
PeersListViewkeyed bykind(PeersListView.tsx:24-95). New compact-toolbarUserFilterSelector(users/UserFilterSelector.tsx). - reverse-proxy: Folder rename
clusters/→self-hosted-proxies/; deletedClustersFeaturesCell.tsx,ClusterTypeIndicator.tsx; new ReverseProxyClusterTargetSelector for cluster target type; Private toggle on target modal; body-capture knobs removed; new ReverseProxyEventExpandedRow. - events:
ReverseProxyEventsUserCellrewritten with user + peer fallback (ReverseProxyEventsUserCell.tsx:14-21), shared with the access-log table. - navigation: Full repackaging in Navigation.tsx — Agent Network items flattened (no collapsible parent), distinct icons per item; Access Control, Networks, Reverse Proxy, DNS, standalone Guardrails, Consumption, Activity removed (still URL-reachable, per lines 165-171).
Architecture & flow
Page → Provider → Table/Modal hierarchy
graph TD
Nav[Navigation.tsx]
Nav --> ProvidersPage[/agent-network/providers/]
Nav --> PoliciesPage[/agent-network/policies/]
Nav --> GuardrailsPage[/agent-network/guardrails/]
Nav --> ObsPage[/agent-network/observability/]
ProvidersPage --> AIPP1[AIProvidersProvider]
PoliciesPage --> AIPP2[AIProvidersProvider]
GuardrailsPage --> AIPP3[AIProvidersProvider]
ObsPage --> AIPP4[AIProvidersProvider]
ObsPage -.wraps.-> GroupsProvider
ObsPage -.wraps.-> PeersProvider
AIPP1 --> ProvTable[AgentProvidersTable]
ProvTable --> ProvModal[AIProviderModal]
AIPP2 --> PolTable[AgentPoliciesTable]
PolTable --> PolModal[AgentPolicyModal]
PolModal --> PolGuardTab[AgentPolicyGuardrailsTab]
PolModal --> PolLimitsTab[AgentPolicyLimitsTab]
PolGuardTab --> GuardBrowse[AgentGuardrailBrowseModal]
PolGuardTab --> GuardModal[AgentGuardrailModal]
AIPP3 --> GuardTable[AgentGuardrailsTable]
GuardTable --> GuardModal
AIPP4 --> Tabs[Tabs]
Tabs --> AccessLog[AgentAccessLogTable]
Tabs --> Consumption[AgentConsumptionPanel]
Tabs --> BudgetRules[AgentBudgetRulesTable]
Tabs --> AccountCtl[AgentAccountControlsCard]
BudgetRules --> BudgetModal[AgentBudgetRuleModal]
BudgetModal -.reuses.-> PolLimitsTab
AI Observability tab page
graph LR
Page[AIObservabilityPage] --> RA[RestrictedAccess<br/>permission.services.read]
RA --> GP[GroupsProvider]
GP --> PP[PeersProvider]
PP --> AIP[AIProvidersProvider]
AIP --> Tabs[Tabs / TabsList]
Tabs --> T1[Access Logs<br/>AgentAccessLogTable]
Tabs --> T2[Budget Dashboard<br/>AgentConsumptionPanel]
Tabs --> T3[Budget Settings<br/>AgentBudgetRulesTable]
Tabs --> T4[Log Settings<br/>AgentAccountControlsCard]
T1 -.GET.-> EP[/events/proxy?agent_network=true/]
T2 -.GET poll 5s.-> CONS[/agent-network/consumption/]
T3 -.GET/PUT.-> BR[/agent-network/budget-rules/]
T4 -.GET/PUT.-> ST[/agent-network/settings/]
Data fetch path
graph TD
Page[Page component] --> Prov[AIProvidersProvider]
Prov -->|useFetchApi| SWR[(SWR cache<br/>key = URL)]
SWR -.GET.-> P[/agent-network/providers/]
SWR -.GET.-> POL[/agent-network/policies/]
SWR -.GET.-> G[/agent-network/guardrails/]
SWR -.GET.-> BR[/agent-network/budget-rules/]
SWR -.GET ignoreError.-> ST[/agent-network/settings/]
SWR -.GET.-> CAT[/agent-network/catalog/providers/]
SWR -.GET pageSize=100.-> EVT[/events/proxy agent_network=true/]
Prov --> Mut[useApiCall.post/put/del]
Mut -.on success.-> MutateSWR[SWR mutate keys]
Prov --> Children[Tables / Modals via useAIProviders]
Every list view reaches management through SWR over /api/agent-network/*. The provider context maps snake-case payloads to camelCase domain types (fromAPI, policyFromAPI, guardrailFromAPI, budgetRuleFromAPI, settingsFromAPI, accessLogFromAPI — AIProvidersProvider.tsx:138-562) and back via matching *ToRequest adaptors. The access log piggy-backs on /events/proxy with agent_network=true&page_size=100 (line 707-709) and decodes LLM-specific fields from per-event metadata. Group IDs on events are resolved to current names through the surrounding GroupsProvider catalog (lines 515-521, 717-731) — no extra round trip. Mutators run *ToRequest, await useApiCall.post/put/del, call SWR mutate(), then notify. Errors caught and surfaced via notify — no exceptions escape into render. The Connect Provider modal's open state lives in the provider itself (isWizardOpen at lines 732-735) so the providers-page empty-state CTA and the table's + button share one modal. Control-center re-fetches /agent-network/{providers,policies} directly on top of AIProvidersProvider — SWR de-dupes but the code path is harder to reason about.
Public contracts consumed
GET/POST /api/agent-network/providers,PUT/DELETE /:idGET/POST /api/agent-network/policies,PUT/DELETE /:idGET/POST /api/agent-network/guardrails,PUT/DELETE /:idGET/POST /api/agent-network/budget-rules,PUT/DELETE /:idGET/PUT /api/agent-network/settings(ignoreError-tolerant; 404 = not yet bootstrapped — auto-bootstrap on first provider create viabootstrap_clusterfield — AIProvidersProvider.tsx:737-760)GET /api/agent-network/catalog/providers(read-only declarative; backend owns vendor list, IDs, brand colors, models, extra_headers, identity_injection — useProviderCatalog.ts:6-95)GET /api/agent-network/consumption(polled every 5s on Budget Dashboard — ConsumptionPanel.tsx:53,65-71)GET /api/events/proxy?agent_network=true&page_size=100(shared with Proxy Events)permission?.services?.readgates every agent-network route via RestrictedAccess.
AIProviderId is a closed union in dashboard types (data/mockData.ts:8-21) but the converter tolerates anything the backend ships — unknown ids fall through to "custom" (AIProvidersProvider.tsx:497-506). Catalog values are pure read-through: anything declared in extra_headers renders in the modal automatically, copy keyed by header name (EXTRA_HEADER_UI in AIProviderModal.tsx:61-89), labeled-fallback for unknown ones.
Invariants
- Provider context wrap order on user-attribution pages:
GroupsProvider > PeersProvider > AIProvidersProvider(observability/page.tsx:87-89). Reverse it and access-log group resolution silently drops names. - Every agent-network route checks
permission?.services?.readviaRestrictedAccess(observability/page.tsx:85, providers/page.tsx:184, policies/page.tsx:53, guardrails/page.tsx:55). - Modal
key={open ? 1 : 0}pattern is used to force unmount/remount on close so internaluseStateresets between edits (AgentBudgetRuleModal.tsx:60, AgentPolicyModal.tsx:66). Removing this would leak prior-row state into a new-row session. mockData.tsis the canonical home for ALL agent-network domain types;MOCK_*constants must never reach a production code path. One leak remains (below).
Things to scrutinize
Correctness
- Tab-state URL hand-off is one-way. observability/page.tsx:53-58 reads
?tab=on mount (despite the file comment at line 28 saying URL hand-off is future) butsetTabdoes NOT push back, so reload preserves the chosen tab only if it came in via the link. Inconsistent with control-center (page.tsx:1817-1831). - Provider overlay runs only in
applySingleGroupView/applyPeerView(control-center/page.tsx:557, 1159-1166). User view does NOT show providers — if agent-network is a primary lens, that's a gap. - Two useEffects race to invalidate the control-center layout. page.tsx:1655-1657 drops
layoutInitializedwhenagentPolicies/agentProvidersarrive; the main effect (1786-1799) also lists them as deps. Functional but fragile — watch for flash-of-empty-graph. updateProvider/updatePolicy/updateBudgetRuleuse??onenabled(AIProvidersProvider.tsx:784, 859, 1018). Toggle paths are safe; any caller sendingenabled: falsethinking "leave it off" getsexisting.enabledinstead. Audit modal callers.- Form validation in modals is minimal. Window-seconds picker — mockData.ts:209-215 documents "minimum 60 — one minute" but there is no matching UI guard in PolicyLimitsTab; the backend validator is the enforcement point.
Security
- No client-side enforcement claims — every cap, allowlist, and toggle is display + edit; proxy is the source of truth for deny decisions (AccessLogTable.tsx:177-191 renders backend-emitted
denyReasonas-is). - Prompt display is gated by what the backend stamps. When
enable_prompt_collectionis OFF the proxy must not put prompt/completion into event metadata; the dashboard renders whatever it gets verbatim (AccessLogTable lines 532-534, AccessLogExpandedRow.tsx:42-57). No UI filter on top of backend collection switches. - Account Controls disables
Redact PIIwhenPrompt Collectionis off (AgentAccountControlsCard.tsx:122) and clears it on off-transition (line 100), but relies on backend to enforce the same gate at write — confirm PUT handler rejectsredact_pii=true && enable_prompt_collection=false. - Bifrost identity-header overrides: empty-string vs nil semantics documented in AIProvidersProvider.tsx:772-781 ("omitted = preserve, empty = explicit clear"). Mishandling could leak group attribution to a header the operator thought disabled. Focused read of Bifrost code path in AIProviderModal.tsx recommended.
Accessibility
- Observability TabsList (observability/page.tsx:96-113) uses the shared Tabs component — should inherit Radix roving-tabindex. All four TabsTriggers carry only icon + text, no
aria-label; fine because text is visible. - Modal focus traps are inherited from the shared Modal; agent-network modals don't override them. Quick keyboard pass recommended.
EndpointBadgeCopy button (providers/page.tsx:66-76) has anaria-label, good.
Performance
AgentConsumptionPanelpolls/agent-network/consumptionevery 5s (ConsumptionPanel.tsx:53,70). Tab switches unmount the panel, so the poll stops — verify in network panel.AgentAccessLogTableis hard-capped at 100 rows viapage_size=100(AIProvidersProvider.tsx:707-709). Server-side pagination is future work; high-traffic tenants miss everything past row 100 — known limitation.- Observability page mounts providers ONCE at page level (observability/page.tsx:87-89); tab switches keep SWR cache hot. Moving the provider mount inside
TabsContentwould re-fetch the access log on every switch.
Visual consistency
- The observability tab style mirrors peers/page.tsx. Outer Tabs
pt-4 pb-0 mb-0, TabsListpx-8(observability/page.tsx:94-96) — confirm chrome height matches so the page doesn't visually jump. - Sidebar:
Boxesfor Providers,AccessControlIconfor Policies,TelescopeIconfor AI Observability (Navigation.tsx:113,120,133). ReusingAccessControlIconmakes Policies look identical to the (now hidden) Access Control item — if Access Control ever comes back, they collide. AgentNetworkIconis used in breadcrumbs on every agent-network page but NOT in the sidebar (per-page icons instead). Deliberate departure — record so it doesn't get reverted.
Test coverage
- Cypress: One file (
cypress/e2e/test.cy.ts) covering only the install-page copy-to-clipboard flow. NOTHING covers agent-network UI. - Component / unit tests:
src/utils/version.test.tsis the only.test.*file in the repo. The agent-network modules ship without component tests. - Data-cy hooks exist on key controls:
save-account-controls(AgentAccountControlsCard.tsx:71),enable-log-collection,enable-prompt-collection,redact-pii, plus existingdata-cy={policy.name}/data-cy={provider.name}on ActiveInactiveRow. Sufficient hooks for Cypress flows; none written yet. - Tooling gap (pre-existing):
npm run lint(next lint) is broken in Next 16 — thelintsubcommand was removed from the Next CLI in 16.x, so the dashboard effectively has no working lint gate. The fix is to add either a flat-configeslint .script or wire ESLint via an expliciteslint-config-nextinvocation.
Known limitations / explicit non-goals
data/mockData.tsstill containsMOCK_GROUPS,MOCK_PROVIDERS,MOCK_PEERS. OnlyMOCK_GROUPSis referenced from production — AgentPoliciesTable.tsx:45,76 uses it as a name-lookup fallback when a policy references a group ID the real GroupsProvider doesn't know about.MOCK_PROVIDERS/MOCK_PEERSare unreferenced; safe to delete. The file is/* eslint-disable */so dead-code warnings don't flag them.- Tab-state URL hand-off on observability page is one-way (read-only).
- Access log hard-capped at 100 rows; no server-side pagination.
- No optimistic updates. All mutations are round-trip; failures rollback via SWR revalidation.
FlowView.NETWORKSretained but hidden from FlowSelector (FlowSelector.tsx:9-14). Old?tab=networkslinks still route to the hidden view becauseapplyNetworksViewstill runs.- Redirects are not query-preserving —
router.replace("/peers/devices")(peers/page.tsx:13) strips any incoming filter params. - Control-center cross-fetches
/agent-network/{providers,policies}directly on top ofAIProvidersProvider. Could be collapsed. - Sidebar permanently hides Access Control, Networks, Reverse Proxy, standalone Guardrails, DNS, Activity, Consumption. Routes still resolve via URL (Navigation.tsx:165-171); intentional.
Cross-references
- Upstream API contracts: shared/api
- Backend persistence: management/store
- Backend handler wiring: management/handlers + wiring
- End-to-end flow narrative: ../01-end-to-end-flows.md
- Top-level overview: ../00-overview.md