Files
netbird/proxy/internal/middleware/keys.go
Maycon Santos b416063bcc [management,proxy] Agent network: per-account LLM gateway (policy, metering, multi-provider) (#6555)
* [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.
2026-06-27 13:41:00 +02:00

87 lines
4.0 KiB
Go

package middleware
// Metadata key namespace constants shared across the built-in
// middlewares. Each domain owns a prefix; middlewares declare their
// per-key allowlist drawn from these constants. Agents implementing
// the G2 middlewares import this file so the dashboard's expanded-row
// viewer and the access-log writer see a stable key surface.
//
// Key shape rules (enforced by the metadata accumulator):
// - Lowercase ASCII letters, digits, dot, underscore, hyphen.
// - At least one dot separating namespace from leaf.
// - Max length: MaxMetadataKeyBytes.
const (
// LLM request-side metadata (emitted by llm_request_parser).
KeyLLMProvider = "llm.provider"
KeyLLMModel = "llm.model"
KeyLLMStream = "llm.stream"
KeyLLMRequestPromptRaw = "llm.request_prompt_raw"
KeyLLMCaptureTruncated = "llm.capture_truncated"
// KeyLLMSessionID groups requests of the same conversation / coding
// session, read from the per-provider session marker in the request
// body. Empty for clients that don't send one.
KeyLLMSessionID = "llm.session_id"
// LLM response-side metadata (emitted by llm_response_parser).
//nolint:gosec // metadata key name, not a credential
KeyLLMInputTokens = "llm.input_tokens"
//nolint:gosec // metadata key name, not a credential
KeyLLMOutputTokens = "llm.output_tokens"
//nolint:gosec // metadata key name, not a credential
KeyLLMTotalTokens = "llm.total_tokens"
// LLM cached-input bucket. For OpenAI it's the SUBSET of input
// tokens that hit the prompt cache (prompt_tokens_details.
// cached_tokens) — billed at the cached_input_per_1k rate when
// configured. For Anthropic it's cache_read_input_tokens, which
// is ADDITIVE to llm.input_tokens — billed at cache_read_per_1k.
// cost_meter switches formula on llm.provider.
//nolint:gosec // metadata key name, not a credential
KeyLLMCachedInputTokens = "llm.cached_input_tokens"
// LLM cache-creation bucket (Anthropic only). ADDITIVE to
// llm.input_tokens; billed at cache_creation_per_1k.
//nolint:gosec // metadata key name, not a credential
KeyLLMCacheCreationTokens = "llm.cache_creation_tokens"
KeyLLMResponseCompletion = "llm.response_completion"
// Guardrail outcomes (emitted by llm_guardrail). The guardrail
// also re-emits llm.request_prompt as a redacted variant of the
// raw prompt and drops llm.request_prompt_raw from the bag.
KeyLLMRequestPrompt = "llm.request_prompt"
KeyLLMPolicyDecision = "llm_policy.decision"
KeyLLMPolicyReason = "llm_policy.reason"
// LLM router routing decision (emitted by llm_router). The router
// stamps the resolved provider id so downstream middlewares and
// the access-log emitter can attribute the request without
// re-parsing the body.
KeyLLMResolvedProviderID = "llm.resolved_provider_id"
// LLM authorising groups for this request (emitted by llm_router
// on the allow path). Carries the comma-separated intersection of
// the caller's UserGroups with the resolved route's
// AllowedGroupIDs — i.e. the groups that actually authorise this
// specific request, NOT every group the peer happens to be in.
// Identity-stamping middlewares use this for per-request tag
// attribution so unrelated group memberships don't leak into
// downstream gateways' spend logs.
KeyLLMAuthorisingGroups = "llm.authorising_groups"
// LLM policy attribution (emitted by llm_limit_check on the allow
// path). Names the policy that paid for this request and the
// dimension counters the post-flight llm_limit_record middleware
// must tick. Empty when no applicable policy has any caps
// configured (catch-all-allow attribution).
KeyLLMSelectedPolicyID = "llm.selected_policy_id"
KeyLLMAttributionGroupID = "llm.attribution_group_id"
KeyLLMAttributionWindowS = "llm.attribution_window_seconds"
// Cost metering (emitted by cost_meter).
KeyCostUSDTotal = "cost.usd_total"
KeyCostSkipped = "cost.skipped"
// Framework-emitted error markers. Use the mw.<id>.* prefix to
// distinguish framework-injected entries from middleware-emitted
// metadata.
KeyFrameworkErrorKindFmt = "mw.%s.error_kind"
)