NexusLinkNexusLink Docs

management/agentnetwork — domain layer + synth pipeline

Risk level: High — central business logic + budget enforcement + the source of every middleware-chain change the proxy executes. Backward-compat impact: Additive within the agent-network surface; one behavioural difference for opted-out accounts in parser capture (the capture flag is stamped explicitly false instead of being absent — see capture-pointer semantics below). Non-agent-network proxy services are untouched (the synth chain only ships on agent-net-svc-* targets).

Module boundary

management/server/agentnetwork owns every agent-network entity (providers, policies, guardrails, account budget rules, per-account settings, consumption rows) and translates them into the in-memory *rpservice.Service that the reverse-proxy controller turns into proto.ProxyMappings and pushes to clusters. It is the only writer of the agent-network middleware chain.

Inside the package: manager.go is the CRUD + permissions-gated facade; synthesizer.go walks settings + providers + policies + guardrails and emits the per-account service plus every middleware's JSON config; policyselect.go runs per-request attribution (min-wins account ceiling, then "drain bigger pool first"); reconcile.go diffs successive synth outputs and emits precise Create/Update/Delete proxy-mapping updates plus a peer-map refresh. labelgen/ mints DNS-safe subdomain labels; catalog/ is the static provider catalogue; types/ carries gorm entity structs. The _realstack_test.go files in the parent management/server/ directory exercise the manager + network-map controller end-to-end with no mocks.

Files

Path Role
agentnetwork/manager.go Manager interface + CRUD + permission gates + bootstrap-settings + reconcile trigger
agentnetwork/synthesizer.go Settings/policy → wire-format synthesis; sole writer of the proxy middleware chain
agentnetwork/synthesizer_pricing.go buildCostMeterConfigJSON — default table + per-provider prices → cost_meter config
agentnetwork/pricing/defaults.go Default pricing table derived from the catalog + supplementals; DefaultTable, LookupDefault, wire Entry
agentnetwork/pricing/override.go LoadFile/StartReloader for AgentNetwork.PricingDefaultsFile (mtime poll, merge over compiled-in base)
agentnetwork/pricing/{exampleyaml,gen}.go Generates defaults_llm_pricing.example.yaml from the compiled-in table (golden-tested)
agentnetwork/policyselect.go Per-request policy attribution + account-budget ceiling (min-wins)
agentnetwork/reconcile.go Per-account synth diff vs in-memory cache → Create/Update/Delete
agentnetwork/catalog/catalog.go Static provider catalogue (auth headers, identity-injection shapes)
agentnetwork/labelgen/{labelgen,words}.go DNS-safe subdomain picker + curated wordlist
agentnetwork/types/provider.go Provider entity + APIKey + Models + ExtraValues + SessionKeys
agentnetwork/types/policy.go Policy entity + PolicyLimits (token + budget)
agentnetwork/types/guardrail.go Guardrail entity (ModelAllowlist, PromptCapture)
agentnetwork/types/budgetrule.go AccountBudgetRule (reuses PolicyLimits)
agentnetwork/types/settings.go Per-account Settings (Cluster, Subdomain, 3 toggles)
agentnetwork/types/consumption.go Consumption row + WindowStart aligner
agentnetwork/{synthesizer,policyselect,reconcile,wire_shape}_*test.go See test coverage table
agentnetwork/types/consumption_test.go WindowStart alignment proofs
agentnetwork/labelgen/labelgen_test.go Deterministic picks + exhaustion + fallback
management/server/agentnetwork_realstack_test.go No-mock provider CRUD → network-map fan-out
management/server/agentnetwork_budgetrule_realstack_test.go No-mock budget-rule CRUD + settings preserve-immutable

Architecture & flow

Synthesis (settings/policy → wire format)

flowchart TD
    A[Mutation: provider/policy/guardrail/settings] --> B[managerImpl.reconcile accountID]
    B --> C{proxyController nil?}
    C -- yes --> D[accountManager.UpdateAccountPeers only]
    C -- no --> E[SynthesizeServices]
    E --> F[loadSettings — NotFound returns ok=false, no synth]
    F --> G[filterEnabledProviders sorted by CreatedAt]
    G --> H[filterEnabledPolicies]
    H --> I[backfillProviderSessionKeys if missing]
    I --> J[indexProviderGroups: providerID -> sorted source groups]
    J --> K[buildRouterConfigJSON drops orphan providers]
    J --> L[buildIdentityInjectConfigJSON per catalog entry]
    J --> K2[buildCostMeterConfigJSON: default table + per-provider prices]
    K2 --> P
    H --> M[mergeGuardrails: union allowlist, OR redact]
    M --> N[applyAccountCollectionControls account toggle = SOLE capture control]
    N --> O[marshalGuardrailConfig]
    K --> P[buildMiddlewareChain 8 middleware entries]
    L --> P
    O --> P
    P --> Q[buildAccountService: AccessGroups=union source groups, noop.invalid target]
    Q --> R[reconcile.diffMappings vs cache]
    R --> S[SendServiceUpdateToCluster CREATE/MODIFY/REMOVE]
    R --> T[accountManager.UpdateAccountPeers — fans synth ACLs into network map]

LLM pricing (management is the sole authority)

The proxy carries no price list. Management synthesizes the entire pricing table and ships it inside cost_meter's ConfigJSON, so a price change reaches the proxies as an ordinary mapping push — the chain rebuild installs a fresh table and there is nothing to reload on the proxy side.

flowchart TD
    A[catalog.All — PricingSurfaces x Models] --> B[buildDefaultTable + supplementalDefaults]
    B --> C{AgentNetwork.PricingDefaultsFile}
    C -- absent --> D[compiled-in table serves]
    C -- loaded --> E[LoadFile: merge file entries WHOLE over compiled base]
    E --> F[mergedTable atomic.Pointer]
    D --> G[DefaultTable]
    F --> G
    G --> H[buildCostMeterConfigJSON — pricing.defaults]
    I[types.Provider.Models operator prices] --> J[normalizePricingModelID<br/>bedrock ARN/region/version, vertex @version]
    J --> K[materializeEntry: default entry as base,<br/>operator input/output verbatim,<br/>cache pointers only when non-nil]
    K --> L[pricing.providers keyed by provider record ID]
    H --> M[cost_meter ConfigJSON]
    L --> M
    G --> N[GET /catalog — applyDefaultPricing prefills dashboard rows]
    O[StartReloader: mtime poll every ReloadInterval 1m] --> E

Two tiers, resolved per request on the proxy (synthesizer_pricing.go:22-35):

Same orphan rule as the router: a provider no enabled policy authorises is unreachable, so its prices aren't shipped. Model ids are normalized with the same functions the request parser uses (NormalizeBedrockModel / NormalizeVertexModel), which is what makes the per-record lookup key compare equal to the llm.model the proxy meters. Post-normalization duplicates resolve first-occurrence-wins, matching the routing dedup order.

AgentNetwork.PricingDefaultsFile (config.go:190-207) lets an operator replace default rates without a rebuild. Schema is surface → model → rates (input_per_1k, output_per_1k, and optional cached_input_per_1k / cache_read_per_1k / cache_creation_per_1k). Semantics:

The live table feeds both consumers, which is what keeps them consistent: the synthesizer (what proxies actually bill with) and GET /api/agent-network/catalog via applyDefaultPricing (what the dashboard's model-row prices prefill with). defaults_llm_pricing.example.yaml is generated from the compiled-in table (go generate ./management/internals/modules/agentnetwork/pricing) and golden-tested, so operators start from a file matching the built-in rates exactly.

Budget rule resolution (min-wins, group+user bound)

flowchart TD
    A[SelectPolicyForRequest in] --> B[checkAccountBudget — runs FIRST, independent of policies]
    B --> C[GetAccountAgentNetworkBudgetRules]
    C --> D{for each enabled rule}
    D --> E{budgetRuleApplies?}
    E -- no --> D
    E -- yes --> F[attrGroup = lowestIntersect TargetGroups, in.GroupIDs]
    F --> G{Token cap enabled?}
    G -- yes --> H[evalTokenCap user dim + group dim]
    H --> I{exhausted?}
    I -- yes --> J[DENY: llm_account.token_cap_exceeded - STOP]
    I -- no --> K{Budget cap enabled?}
    G -- no --> K
    K -- yes --> L[evalBudgetCap user dim + group dim]
    L --> M{exhausted?}
    M -- yes --> N[DENY: llm_account.budget_cap_exceeded - STOP]
    M -- no --> D
    K -- no --> D
    D --> O[All rules passed -> fall through to per-policy selection]

Key invariant: rules are checked sequentially and ANY exhausted rule denies (all-must-pass / min-wins). Untargeted rules (len(TargetGroups)==0 && len(TargetUsers)==0) apply to every caller (policyselect.go:393).

Policy selection (per-peer, per-request)

flowchart TD
    A[Account-budget gate passed] --> B[GetAccountAgentNetworkPolicies]
    B --> C[filterApplicablePolicies enabled + provider match + group intersect]
    C --> D{candidates empty?}
    D -- yes --> E[Allow, empty SelectedPolicyID]
    D -- no --> F[scoreCandidates -> scoreOne per policy]
    F --> G[scoreOne: attrGroup + window]
    G --> H{any cap exhausted?}
    H -- yes --> I[Drop policy; record last deny code]
    H -- no --> K[Keep as live candidate]
    F --> L{live candidates exist?}
    L -- no --> M[Deny with last exhaustion code]
    L -- yes --> N[Sort: uncapped wins -> larger group token -> group budget -> user token -> user budget -> oldest CreatedAt]
    N --> O[winner = scored 0]
    O --> P[Allow + SelectedPolicyID + AttributionGroupID + WindowSeconds]

End-to-end: a mutation calls managerImpl.reconcile(ctx, accountID) (manager.go:205,239,...). Reconcile defers an accountManager.UpdateAccountPeers so the network-map controller re-runs and injectAllProxyPolicies picks up the new access groups; with a proxyController wired, it re-synthesizes the service, diffs against reconcileCache[accountID] (guarded by reconcileMu), and emits proto mappings to the cluster derived from the mapping's domain (reconcile.go:120). Synthesis is stateless and idempotent. Sole persistent side effect: backfillProviderSessionKeys (synthesizer.go:249) mints ed25519 keys on legacy provider rows and writes them back.

At request time the path is independent: the proxy calls SelectPolicyForRequest (policyselect.go:56); account-budget ceiling first, then per-policy scoring. Token + budget caps share evalTokenCap / evalBudgetCap — same primitive for account rules and policy limits, label differentiates the deny reason. After a served request, RecordAccountBudgetUsage (policyselect.go:415) fans deltas to every applicable rule's distinct (dim_kind, dim_id, window) tuple, deduplicating to prevent double-count when two rules share target+window.

Public contracts

Slot Idx ID ConfigJSON shape CanMutate
on_request 0 llm_request_parser {"capture_prompt": <bool>, "redact_pii"?: true}
on_request 1 llm_router {"providers":[{id, models[], upstream_*, auth_header_*, allowed_group_ids[]}]} true
on_request 2 llm_limit_check {}
on_request 3 llm_identity_inject {"providers":[{provider_id, header_pair?, json_metadata?, extra_headers?}]} true
on_request 4 llm_guardrail {"provider_allowlists"?: {providerID: []model}, "prompt_capture":{enabled,redact_pii}}
on_response 5 llm_limit_record {} (runs LAST at runtime)
on_response 6 cost_meter {"pricing":{"defaults":{surface:{model:rates}},"providers"?:{providerRecordID:{model:rates}}}} — rates are {input_per_1k, output_per_1k, cached_input_per_1k?, cache_read_per_1k?, cache_creation_per_1k?}
on_response 7 llm_response_parser {"capture_completion": <bool>, "redact_pii"?: true}
- Synthesized service shape (synthesizer.go:739): Mode=HTTP, Private=true, Domain=<subdomain>.<cluster>, AccessGroups=unionSourceGroups(enabledPolicies), one TargetTypeCluster target with Host=noop.invalid:443 (router rewrites per request), Options.{DirectUpstream,AgentNetwork}=true, DisableAccessLog=!settings.EnableLogCollection, CaptureMax{Req,Resp}Bytes=1<<20, CaptureContentTypes=["application/json","text/event-stream"].

Invariants

Things to scrutinize

Correctness

Security

Concurrency

Backward compatibility

Performance

Observability

Test coverage

Test file Locks down
synthesizer_test.go Mock-store: HappyPath (8-mw chain ordering, {"capture_prompt":false} baseline); No{Settings,Providers}; Disabled{Provider,Policy}_NoService; RouterConfigOrdering; PolicyCheckConfig_UnionsSourceGroups; OrphanProvider_HasEmptyAllowedGroups; identity-inject for LiteLLM / Bifrost (overrides + partial disable) / Cloudflare / Portkey / Vercel / OpenRouter / generic non-customizable; GuardrailMerge_AllowlistUnion_LimitsRestrictive; BackfillsMissingSessionKeys; HTTPUpstream_KeepsExplicitPort; UpstreamURLPath_FlowsToRouter; UnknownProviderID_FailsClosed; EmptyAPIKey_FailsClosed.
synthesizer_realstore_test.go Real-sqlite: SurvivesStatusToggle reproduces the disable/re-enable 403 regression; Reconcile_RealStore_PushesPrivateAfterStatusToggle extends through reconcile push.
synthesizer_guardrail_realstore_test.go PromptCaptureAccountIsSoleControl; PromptCaptureFlowsWhenAccountOptsIn; AccountRedactWithoutGuardrailRedact; NoGuardrail_CaptureOff.
synthesizer_log_collection_realstore_test.go LogCollection{Off_SuppressesAccessLog,On_PermitsAccessLog} — verifies DisableAccessLog propagation through ToProtoMapping.
synthesizer_parser_redact_realstore_test.go Capture-pointer regression suite: ParserConfigsCarryRedactPii; ParserConfigsSuppressCaptureWhenLogCollectionOnly (log=on/prompt=off ⇒ both capture flags false); ParserConfigsOmitRedactPiiWhenOff.
synthesizer_pricing_test.go BuildCostMeterConfig_{BedrockModelNormalization,CacheRateNilVsZero,OrphanAndGatewayProviders} — the per-record tier's three load-bearing rules: keys normalized like the parser's, nil cache pointer inherits vs explicit 0 bills at input rate, and orphan / gateway (empty Models) providers ship no per-record entry.
pricing/defaults_test.go DefaultTable_{CoversEveryCatalogModel,NoConflictingContributions,AllRatesFiniteNonNegative,PinnedRates}; LookupDefault_SurfaceOrder. Catalog-derived coverage + rate sanity are structural, not curated.
pricing/override_test.go LoadFile_{MergesOverCompiledDefaults,MissingPath,RejectsInvalid}; Reload_LifeCycle (mtime detect, parse error keeps previous, delete reverts to built-ins); ExampleYAML_InSyncWithBuiltins golden.
policyselect_test.go Mock-store: NoApplicablePolicies; AllowWithLowestGroupAttribution; LargerPoolWinsAcrossUsageLevels; StaysOnLargerPoolAfterPartialDrain; FallsThroughToSmallerPoolWhenLargerExhausted; TiebreakBy{LargerGroupPool,CreatedAt}; DeniesWhenAllExhausted; UncappedPolicyAlwaysWinsAgainstCapped; DisabledPolicyIgnored; StoreErrorPropagates; RejectsEmptyAccount; SharesGroupCounterAcrossPolicies; AntiFallThroughOnLowestGroup; BudgetOnlyExhaustionDenies; BudgetTighterThanTokenWins.
policyselect_realstore_test.go Real-sqlite regression guard: NoApplicablePolicies; AllowAndLowestGroupAttribution; LargerPoolWins_FallsThroughWhenExhausted; BudgetCapDenies; GroupCounterSharedAcrossPolicies; DisabledPolicyIgnored.
policyselect_account_realstore_test.go Account budget rules: AccountCeilingBindsEvenWithUncappedPolicy (min-wins); AccountGroupCeiling; AccountTargetUsersBindsOnlyThatUser; AccountRuleRecordsToOwnWindow.
reconcile_test.go FirstSynth_EmitsCreate; NoChange_EmitsNothingExtra (re-push as Modified — verify desired); PolicyRemoved_EmitsDelete; NilProxyController_NoOp; EmptyAccountID_NoOp; ClusterFromMapping.
wire_shape_test.go TestSynthesizedService_WireShape — proto-shape lockdown via ToProtoMapping. Catches "service not matching" (mapping reaches proxy but no SNI/HTTP route). Asserts ID, Domain, Mode, AuthToken, Private, Auth.Oidc=false, one path / + https://noop.invalid/, 8 middlewares with correct slot enums, router config auth_header_value="Bearer sk-test-key".
labelgen/labelgen_test.go PickUnique_{DeterministicWithSeededRng,AvoidsTakenWordsWhenMostAreReserved,FallsBackWhenAllReserved}; UniqueWords_DropsDuplicates.
types/consumption_test.go WindowStart_{AlignedToUnixEpoch,WithinWindowConverges,AcrossWindowsDiverges,DifferentWindowsHaveDifferentBuckets,SubMinuteAndMinuteAlignment,ZeroWindowReturnsInputUTC}. Bucket alignment so multi-node reads converge.
agentnetwork_realstack_test.go ProviderCRUD_FansOutToProxyAndClientPeers — no-mock end-to-end through real account manager + network-map + agentnetwork: provider create propagates the updated map to both proxy peer and client peer with the synth DNS surface.
agentnetwork_budgetrule_realstack_test.go BudgetRuleCRUD_RealManager; UpdateSettings_PreservesImmutableAndTogglesCollection.

Known limitations / explicit non-goals

Cross-references