* [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.
12 KiB
management/store — persistence for agent-network entities
Risk level: Medium — six brand-new tables behind AutoMigrate, one upsert-counter table that runs on the request hot path, and one column carrying an encrypted secret. Backward-compat impact: Additive (six new tables created by AutoMigrate; the
Storeinterface gains 23 methods, but no existing column/index is touched).
Module boundary
This module is the persistence layer for the Agent Network feature. Everything the management server stores about LLM proxying — providers, policies, guardrails, the per-account settings row, a usage-counter table written on every proxied LLM request, and the account-budget rules — flows through the methods added to store.Store. The module owns six tables, six entity types from management/server/agentnetwork/types, and a single hot-path upsert (IncrementAgentNetworkConsumption) consumed by the proxy fleet.
Out of scope here: the catalog of provider definitions (compiled-in, no DB), the synthesizer/manager built on top of these CRUDs (covered in 21-management-agentnetwork.md), and the HTTP handlers that translate API requests into Save/Delete calls.
Files
| Path | Role |
|---|---|
management/server/store/sql_store_agentnetwork.go |
gorm implementations of all 23 store methods |
management/server/store/sql_store_agentnetwork_budgetrule_test.go |
round-trip + account-scoping coverage against a real sqlite store |
management/server/store/sql_store.go |
one import, six entities appended to the AutoMigrate slice (sql_store.go:40, sql_store.go:141-142) |
management/server/store/store.go |
23 methods added to the Store interface (store.go:328-354) |
management/server/store/store_mock_agentnetwork.go |
mockgen output for the new interface surface |
Tables added / migrations
All six tables are created by db.AutoMigrate invoked from NewSqlStore at sql_store.go:133-143. There is no hand-rolled SQL migration script — the schema is whatever GORM derives from the struct tags.
agent_network_providers—Provider.TableName()at provider.go:76. PKid, index onaccount_id, named indexidx_agent_network_provideronprovider_id. Carries an at-rest-encryptedapi_keyand ed25519session_private_key(provider.go:35,56).extra_valuesandmodelsare JSON blobs (serializer:json).agent_network_policies—Policy.TableName()at policy.go:70. PKid, index onaccount_id. JSON columns:source_groups,destination_provider_ids,guardrail_ids,limits.agent_network_guardrails—Guardrail.TableName()at guardrail.go:41. PKid, index onaccount_id. JSONchecks.agent_network_settings—Settings.TableName()at settings.go:33. PKaccount_id(one row per account), named indexidx_agent_network_settings_cluster_subdomainonsubdomainonly — the index name implies a composite, but only one column is tagged.agent_network_consumption—Consumption.TableName()at consumption.go:46. Composite PK across(account_id, dim_kind, dim_id, window_seconds, window_start_utc)— the same tuple the upsert keys on.agent_network_budget_rules—AccountBudgetRule.TableName()at budgetrule.go:35. PKid, index onaccount_id. JSONtarget_groups,target_users,limits.
CRUD surface added
Provider, Policy, Guardrail, BudgetRule follow the same pattern: Get<Kind>ByID, GetAccount<Kind> (list), Save<Kind> (upsert), Delete<Kind>, with account-scoping enforced by the existing accountAndIDQueryCondition / accountIDCondition constants (sql_store.go:59-62). Provider additionally exposes GetAllAgentNetworkProviders (cross-account, used by the synthesizer). Settings exposes Get/GetByCluster/Save (no delete — one row per account, created on first save). Consumption exposes the upsert Increment, a point Get, and a cross-window List.
Architecture & flow
flowchart LR
handlers["HTTP handlers<br/>(management/server/agentnetwork)"] -->|Save/Delete| iface["Store interface<br/>store.go:328-354"]
manager["agentnetwork.Manager"] -->|Get*| iface
synth["synthesizer<br/>(global)"] -->|GetAllAgentNetworkProviders| iface
proxy["proxy fleet<br/>(hot path)"] -->|IncrementAgentNetworkConsumption| iface
iface --> sql["SqlStore methods<br/>sql_store_agentnetwork.go"]
iface -.gomock.-> mock["MockStore<br/>store_mock_agentnetwork.go"]
sql --> gorm["gorm.DB"]
gorm --> tables[("6 tables<br/>agent_network_*")]
sql --> enc["crypt.FieldEncrypt<br/>(provider only)"]
Reads decrypt provider secrets in-place; writes do provider.Copy().EncryptSensitiveData(...) before db.Save so the caller's in-memory object keeps the plaintext api_key (sql_store_agentnetwork.go:88-102). Every list/get takes a LockingStrength and applies clause.Locking{Strength: ...} when non-None — matching the rest of the store. The upsert path uses clause.OnConflict with gorm.Expr server-side increments so concurrent proxy nodes converge without read-modify-write races (sql_store_agentnetwork.go:321-335).
Invariants enforced at the store layer
- Account scoping. Every entity-by-ID method keys on
account_id = ? and id = ?; no cross-tenant leak path through the API is reachable as long as callers always pass the auth'daccountID(sql_store_agentnetwork.go:70,141,201,429). - NotFound mapping.
gorm.ErrRecordNotFoundis translated to typedstatus.NewAgentNetwork*NotFoundError;Delete*returns NotFound whenRowsAffected == 0(sql_store_agentnetwork.go:111-113,171-173,231-233,461-463). - Provider secret encryption at rest.
SaveAgentNetworkProvideralways encrypts before persist;Get*always decrypts after read. The plaintextapi_keynever reaches the DB through this layer (sql_store_agentnetwork.go:31,54,80,90). - Consumption monotonicity. The upsert only ever issues
col = col + ?for the three counter columns — no decrement path exists (sql_store_agentnetwork.go:330-332). - Window alignment is the caller's responsibility. The store stamps
WindowStartUTCas-passed; alignment to epoch happens intypes.WindowStartat consumption.go:51-58. - Settings has no Delete. Intentional — one row per account, created on first save; the row sticks around for the account lifetime.
Things to scrutinize
Correctness
SaveAgentNetworkProvidersaves the copy (sql_store_agentnetwork.go:95). The caller's in-memory pointer therefore keeps plaintextapi_keyand anyCreatedAt/UpdatedAtgorm autofills land on the copy, not the original. Callers that need synced timestamps must re-fetch.IncrementAgentNetworkConsumption'sCreateprovides initial counter values (TokensInput: tokensIn, etc.) in the row, and on conflict the assignments add the same deltas to the existing values. The insert-vs-update arithmetic is consistent. Cross-check that no engine in use (sqlite, postgres, mysql) silently rejects theOnConflictclause — GORM emits engine-specific SQL butON DUPLICATE KEY UPDATE(mysql) vsON CONFLICT (...)(sqlite/postgres) need their unique constraint to match the composite PK onagent_network_consumption; it does, by construction.IncrementAgentNetworkConsumptionwritesupdated_at: time.Now().UTC()literally inside the assignments map (sql_store_agentnetwork.go:333) — fine, but it's a Go-side timestamp captured at call time, not a DB-sidenow(). Acceptable for an audit field.GetAgentNetworkConsumptionreturns a zero-valued non-nil row onErrRecordNotFound(sql_store_agentnetwork.go:364-371). Document or rename — a typed sentinel error would be more orthodox; callers must know not to error-check.
Concurrency / transactions
- Hot-path
IncrementAgentNetworkConsumptionruns outside any explicit transaction; concurrency safety relies entirely on the DB serialising theON CONFLICTupsert against the composite PK. This is correct for postgres and mysql; for sqlite it serialises behind the single writer. SaveAgentNetworkSettingsis a blind upsert with no version/etag — concurrent writes from two operators last-write-wins on the collection-toggle flags (settings.go:23-25). Acceptable for admin-curated state but worth flagging.Save*Providerusesdb.Saveon a struct with a PK already set — GORM emits UPDATE or INSERT based on row existence. No upsert clause is attached, so a race between two creates with the same generatedxid(vanishingly unlikely) would surface as a PK violation.
Migration safety
- All six tables ride
AutoMigrate(sql_store.go:141-142). AutoMigrate is additive: new columns get added, but it never drops columns nor narrows types. Threeboolcolumns onagent_network_settings(EnableLogCollection,EnablePromptCollection,RedactPii) default to false at the GORM/DDL layer for existing rows; the test at sql_store_agentnetwork_budgetrule_test.go:83-112 locks that down on a fresh sqlite. Verify postgres/mysql produce the same default. - The named index
idx_agent_network_settings_cluster_subdomainon settings.go:15 is declared on onlysubdomain. Either the cluster column also needsgorm:"index:idx_agent_network_settings_cluster_subdomain"to make it composite, or the name is misleading. - The named index
idx_agent_network_provideronProvider.ProviderID(provider.go:30) is not unique and not scoped to account — two providers in the same account with the sameprovider_idare permitted at the DB layer; uniqueness, if any, must live above the store.
Backward compatibility
- Net additive. No removed methods, no renamed columns, no schema change to existing tables. Existing deployments running a prior binary continue to work; the first boot of the new binary creates the six tables.
- The
Storeinterface grows by 23 methods (store.go:330-354); any non-mock external implementer ofstore.Storewill fail to compile. The repo only hasSqlStore+MockStore, both updated.
Performance (indexes, N+1)
- All by-account list queries hit the
idx_account_idper-table index. No N+1: list methods return the full slice in one query. GetAgentNetworkSettingsByCluster(sql_store_agentnetwork.go:263-277) does a tablescan oncluster— no index. Tolerable for the bootstrap label generator (one-shot at provisioning) but worth noting if the call moves onto a hot path.ListAgentNetworkConsumptionreturns every row ever recorded for the account (sql_store_agentnetwork.go:382-400) — unbounded growth, noLIMIT, no time filter. With one row per (dim, window) per request burst, this table grows fastest of the six; a retention job + a paginated list method are obvious follow-ups.
Test coverage
| Test file | Locks down |
|---|---|
sql_store_agentnetwork_budgetrule_test.go::TestAgentNetworkBudgetRule_RealStore_RoundTrip |
full save → reload of AccountBudgetRule including the JSON-serialised PolicyLimits, target slices, double-delete returns NotFound (lines 18-59) |
sql_store_agentnetwork_budgetrule_test.go::TestAgentNetworkBudgetRule_RealStore_ScopedByAccount |
cross-account isolation for budget rules (lines 63-78) |
sql_store_agentnetwork_budgetrule_test.go::TestAgentNetworkSettings_RealStore_CollectionTogglesRoundTrip |
collection toggles default off, survive save/reload at the set values (lines 83-112) |
Gap: there is no store-level test for providers (encryption round-trip), policies, guardrails, or IncrementAgentNetworkConsumption (concurrent upsert, window-key uniqueness). The consumption upsert is the most performance-sensitive method in this module and the only one without a real-sqlite test.
Known limitations / explicit non-goals
- No retention / GC for
agent_network_consumption. - No
DeleteforSettings(one row per account, cleared with the account). - No DB-engine-specific tuning — the same struct tags drive sqlite, mysql, postgres.
- Provider
extra_valuesandmodelsare JSON blobs; querying inside them is not supported by design. GetAgentNetworkConsumption"not-found = zero row" contract is convenient but unconventional.
Cross-references
- Upstream: shared/api, management/agentnetwork
- End-to-end flow: ../01-end-to-end-flows.md
- Top-level: ../00-overview.md