mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-02 04:51:29 +02:00
Merge branch 'main' into 0.75.0-branch
# Conflicts: # .github/workflows/golang-test-darwin.yml # .github/workflows/golang-test-linux.yml # .github/workflows/golangci-lint.yml # client/internal/connect.go # client/internal/peer/status.go # client/server/server_test.go # client/ui/client_ui.go # go.mod # go.sum
This commit is contained in:
@@ -55,6 +55,14 @@ type GrpcClient struct {
|
||||
connStateCallback ConnStateNotifier
|
||||
connStateCallbackLock sync.RWMutex
|
||||
serverURL string
|
||||
|
||||
// syncStreamErr holds the last Sync stream error, or nil while the stream
|
||||
// is established and healthy. GetServerKey succeeds even when the peer
|
||||
// cannot sync (e.g. the server returns "settings not found"), so the
|
||||
// health probe must consult this to avoid reporting a healthy management
|
||||
// connection while the Sync stream keeps failing.
|
||||
syncStreamMu sync.RWMutex
|
||||
syncStreamErr error
|
||||
}
|
||||
|
||||
type ExposeRequest struct {
|
||||
@@ -364,6 +372,8 @@ func (c *GrpcClient) handleSyncStream(ctx context.Context, serverPubKey wgtypes.
|
||||
stream, err := c.connectToSyncStream(ctx, serverPubKey, sysInfo)
|
||||
if err != nil {
|
||||
log.Debugf("failed to open Management Service stream: %s", err)
|
||||
c.notifyDisconnected(err)
|
||||
c.setSyncStreamDisconnected(err)
|
||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||
return backoff.Permanent(err) // unrecoverable error, propagate to the upper layer
|
||||
}
|
||||
@@ -372,11 +382,13 @@ func (c *GrpcClient) handleSyncStream(ctx context.Context, serverPubKey wgtypes.
|
||||
|
||||
log.Infof("connected to the Management Service stream")
|
||||
c.notifyConnected()
|
||||
c.setSyncStreamConnected()
|
||||
|
||||
// blocking until error
|
||||
err = c.receiveUpdatesEvents(stream, serverPubKey, msgHandler)
|
||||
if err != nil {
|
||||
c.notifyDisconnected(err)
|
||||
c.setSyncStreamDisconnected(err)
|
||||
if ctx.Err() != nil {
|
||||
log.Debugf("management connection context has been canceled, this usually indicates shutdown")
|
||||
return nil
|
||||
@@ -524,12 +536,19 @@ func (c *GrpcClient) IsHealthy() bool {
|
||||
ctx, cancel := context.WithTimeout(c.ctx, healthCheckTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := c.realClient.GetServerKey(ctx, &proto.Empty{})
|
||||
_, err := c.realClient.IsHealthy(ctx, &proto.Empty{})
|
||||
if err != nil {
|
||||
c.notifyDisconnected(err)
|
||||
log.Warnf("health check returned: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if syncErr := c.syncStreamError(); syncErr != nil {
|
||||
c.notifyDisconnected(syncErr)
|
||||
log.Warnf("management transport is up but the Sync stream is unhealthy: %s", syncErr)
|
||||
return false
|
||||
}
|
||||
|
||||
c.notifyConnected()
|
||||
return true
|
||||
}
|
||||
@@ -759,6 +778,24 @@ func (c *GrpcClient) SyncMeta(sysInfo *system.Info) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GrpcClient) setSyncStreamConnected() {
|
||||
c.syncStreamMu.Lock()
|
||||
defer c.syncStreamMu.Unlock()
|
||||
c.syncStreamErr = nil
|
||||
}
|
||||
|
||||
func (c *GrpcClient) setSyncStreamDisconnected(err error) {
|
||||
c.syncStreamMu.Lock()
|
||||
defer c.syncStreamMu.Unlock()
|
||||
c.syncStreamErr = err
|
||||
}
|
||||
|
||||
func (c *GrpcClient) syncStreamError() error {
|
||||
c.syncStreamMu.RLock()
|
||||
defer c.syncStreamMu.RUnlock()
|
||||
return c.syncStreamErr
|
||||
}
|
||||
|
||||
func (c *GrpcClient) notifyDisconnected(err error) {
|
||||
c.connStateCallbackLock.RLock()
|
||||
defer c.connStateCallbackLock.RUnlock()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,45 @@ func (e AccessRestrictionsCrowdsecMode) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for AgentNetworkCatalogProviderKind.
|
||||
const (
|
||||
AgentNetworkCatalogProviderKindCustom AgentNetworkCatalogProviderKind = "custom"
|
||||
AgentNetworkCatalogProviderKindGateway AgentNetworkCatalogProviderKind = "gateway"
|
||||
AgentNetworkCatalogProviderKindProvider AgentNetworkCatalogProviderKind = "provider"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the AgentNetworkCatalogProviderKind enum.
|
||||
func (e AgentNetworkCatalogProviderKind) Valid() bool {
|
||||
switch e {
|
||||
case AgentNetworkCatalogProviderKindCustom:
|
||||
return true
|
||||
case AgentNetworkCatalogProviderKindGateway:
|
||||
return true
|
||||
case AgentNetworkCatalogProviderKindProvider:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for AgentNetworkConsumptionDimensionKind.
|
||||
const (
|
||||
AgentNetworkConsumptionDimensionKindGroup AgentNetworkConsumptionDimensionKind = "group"
|
||||
AgentNetworkConsumptionDimensionKindUser AgentNetworkConsumptionDimensionKind = "user"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the AgentNetworkConsumptionDimensionKind enum.
|
||||
func (e AgentNetworkConsumptionDimensionKind) Valid() bool {
|
||||
switch e {
|
||||
case AgentNetworkConsumptionDimensionKindGroup:
|
||||
return true
|
||||
case AgentNetworkConsumptionDimensionKindUser:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for CreateAzureIntegrationRequestHost.
|
||||
const (
|
||||
CreateAzureIntegrationRequestHostMicrosoftCom CreateAzureIntegrationRequestHost = "microsoft.com"
|
||||
@@ -1163,6 +1202,141 @@ func (e WorkloadType) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for GetApiAgentNetworkAccessLogSessionsParamsSortBy.
|
||||
const (
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortByCostUsd GetApiAgentNetworkAccessLogSessionsParamsSortBy = "cost_usd"
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortByDecision GetApiAgentNetworkAccessLogSessionsParamsSortBy = "decision"
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortByDuration GetApiAgentNetworkAccessLogSessionsParamsSortBy = "duration"
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortByRequestCount GetApiAgentNetworkAccessLogSessionsParamsSortBy = "request_count"
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortByStartedAt GetApiAgentNetworkAccessLogSessionsParamsSortBy = "started_at"
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortByStatusCode GetApiAgentNetworkAccessLogSessionsParamsSortBy = "status_code"
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortByTimestamp GetApiAgentNetworkAccessLogSessionsParamsSortBy = "timestamp"
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortByTotalTokens GetApiAgentNetworkAccessLogSessionsParamsSortBy = "total_tokens"
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortByUserId GetApiAgentNetworkAccessLogSessionsParamsSortBy = "user_id"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the GetApiAgentNetworkAccessLogSessionsParamsSortBy enum.
|
||||
func (e GetApiAgentNetworkAccessLogSessionsParamsSortBy) Valid() bool {
|
||||
switch e {
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortByCostUsd:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortByDecision:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortByDuration:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortByRequestCount:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortByStartedAt:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortByStatusCode:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortByTimestamp:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortByTotalTokens:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortByUserId:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for GetApiAgentNetworkAccessLogSessionsParamsSortOrder.
|
||||
const (
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortOrderAsc GetApiAgentNetworkAccessLogSessionsParamsSortOrder = "asc"
|
||||
GetApiAgentNetworkAccessLogSessionsParamsSortOrderDesc GetApiAgentNetworkAccessLogSessionsParamsSortOrder = "desc"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the GetApiAgentNetworkAccessLogSessionsParamsSortOrder enum.
|
||||
func (e GetApiAgentNetworkAccessLogSessionsParamsSortOrder) Valid() bool {
|
||||
switch e {
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortOrderAsc:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogSessionsParamsSortOrderDesc:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for GetApiAgentNetworkAccessLogsParamsSortBy.
|
||||
const (
|
||||
GetApiAgentNetworkAccessLogsParamsSortByCostUsd GetApiAgentNetworkAccessLogsParamsSortBy = "cost_usd"
|
||||
GetApiAgentNetworkAccessLogsParamsSortByDecision GetApiAgentNetworkAccessLogsParamsSortBy = "decision"
|
||||
GetApiAgentNetworkAccessLogsParamsSortByDuration GetApiAgentNetworkAccessLogsParamsSortBy = "duration"
|
||||
GetApiAgentNetworkAccessLogsParamsSortByModel GetApiAgentNetworkAccessLogsParamsSortBy = "model"
|
||||
GetApiAgentNetworkAccessLogsParamsSortByProvider GetApiAgentNetworkAccessLogsParamsSortBy = "provider"
|
||||
GetApiAgentNetworkAccessLogsParamsSortByStatusCode GetApiAgentNetworkAccessLogsParamsSortBy = "status_code"
|
||||
GetApiAgentNetworkAccessLogsParamsSortByTimestamp GetApiAgentNetworkAccessLogsParamsSortBy = "timestamp"
|
||||
GetApiAgentNetworkAccessLogsParamsSortByTotalTokens GetApiAgentNetworkAccessLogsParamsSortBy = "total_tokens"
|
||||
GetApiAgentNetworkAccessLogsParamsSortByUserId GetApiAgentNetworkAccessLogsParamsSortBy = "user_id"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the GetApiAgentNetworkAccessLogsParamsSortBy enum.
|
||||
func (e GetApiAgentNetworkAccessLogsParamsSortBy) Valid() bool {
|
||||
switch e {
|
||||
case GetApiAgentNetworkAccessLogsParamsSortByCostUsd:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogsParamsSortByDecision:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogsParamsSortByDuration:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogsParamsSortByModel:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogsParamsSortByProvider:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogsParamsSortByStatusCode:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogsParamsSortByTimestamp:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogsParamsSortByTotalTokens:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogsParamsSortByUserId:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for GetApiAgentNetworkAccessLogsParamsSortOrder.
|
||||
const (
|
||||
GetApiAgentNetworkAccessLogsParamsSortOrderAsc GetApiAgentNetworkAccessLogsParamsSortOrder = "asc"
|
||||
GetApiAgentNetworkAccessLogsParamsSortOrderDesc GetApiAgentNetworkAccessLogsParamsSortOrder = "desc"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the GetApiAgentNetworkAccessLogsParamsSortOrder enum.
|
||||
func (e GetApiAgentNetworkAccessLogsParamsSortOrder) Valid() bool {
|
||||
switch e {
|
||||
case GetApiAgentNetworkAccessLogsParamsSortOrderAsc:
|
||||
return true
|
||||
case GetApiAgentNetworkAccessLogsParamsSortOrderDesc:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for GetApiAgentNetworkUsageOverviewParamsGranularity.
|
||||
const (
|
||||
GetApiAgentNetworkUsageOverviewParamsGranularityDay GetApiAgentNetworkUsageOverviewParamsGranularity = "day"
|
||||
GetApiAgentNetworkUsageOverviewParamsGranularityMonth GetApiAgentNetworkUsageOverviewParamsGranularity = "month"
|
||||
GetApiAgentNetworkUsageOverviewParamsGranularityWeek GetApiAgentNetworkUsageOverviewParamsGranularity = "week"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the GetApiAgentNetworkUsageOverviewParamsGranularity enum.
|
||||
func (e GetApiAgentNetworkUsageOverviewParamsGranularity) Valid() bool {
|
||||
switch e {
|
||||
case GetApiAgentNetworkUsageOverviewParamsGranularityDay:
|
||||
return true
|
||||
case GetApiAgentNetworkUsageOverviewParamsGranularityMonth:
|
||||
return true
|
||||
case GetApiAgentNetworkUsageOverviewParamsGranularityWeek:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for GetApiEventsNetworkTrafficParamsType.
|
||||
const (
|
||||
GetApiEventsNetworkTrafficParamsTypeTYPEDROP GetApiEventsNetworkTrafficParamsType = "TYPE_DROP"
|
||||
@@ -1510,6 +1684,9 @@ type AccountSettings struct {
|
||||
// LocalMfaEnabled Enables or disables TOTP multi-factor authentication for local users. Only applicable when the embedded identity provider is enabled.
|
||||
LocalMfaEnabled *bool `json:"local_mfa_enabled,omitempty"`
|
||||
|
||||
// MetricsPushEnabled Enables or disables client metrics push for all peers in the account
|
||||
MetricsPushEnabled *bool `json:"metrics_push_enabled,omitempty"`
|
||||
|
||||
// NetworkRange Allows to define a custom network range for the account in CIDR format
|
||||
NetworkRange *string `json:"network_range,omitempty"`
|
||||
|
||||
@@ -1541,6 +1718,633 @@ type AccountSettings struct {
|
||||
RoutingPeerDnsResolutionEnabled *bool `json:"routing_peer_dns_resolution_enabled,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkAccessLog One per-request agent-network (LLM) access log entry with flattened, queryable LLM dimensions.
|
||||
type AgentNetworkAccessLog struct {
|
||||
// CostUsd Estimated USD cost of the request.
|
||||
CostUsd float64 `json:"cost_usd"`
|
||||
|
||||
// Decision Policy decision for the request (e.g. allow, deny).
|
||||
Decision *string `json:"decision,omitempty"`
|
||||
|
||||
// DenyReason Raw deny reason code when the request was blocked (e.g. llm_policy.token_cap_exceeded).
|
||||
DenyReason *string `json:"deny_reason,omitempty"`
|
||||
|
||||
// DurationMs Duration of the request in milliseconds.
|
||||
DurationMs int `json:"duration_ms"`
|
||||
|
||||
// GroupIds NetBird group ids that authorised the request (the caller's groups intersected with the policy's source groups).
|
||||
GroupIds *[]string `json:"group_ids,omitempty"`
|
||||
|
||||
// Host Upstream host the request was routed to. Empty when log collection is disabled.
|
||||
Host *string `json:"host,omitempty"`
|
||||
|
||||
// Id Unique identifier for the access log entry.
|
||||
Id string `json:"id"`
|
||||
|
||||
// InputTokens Input (prompt) tokens consumed.
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
|
||||
// Method HTTP method of the request.
|
||||
Method *string `json:"method,omitempty"`
|
||||
|
||||
// Model Requested LLM model.
|
||||
Model *string `json:"model,omitempty"`
|
||||
|
||||
// OutputTokens Output (completion) tokens produced.
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
|
||||
// Path Request path. Empty when log collection is disabled.
|
||||
Path *string `json:"path,omitempty"`
|
||||
|
||||
// Provider LLM provider vendor (e.g. openai, anthropic).
|
||||
Provider *string `json:"provider,omitempty"`
|
||||
|
||||
// RequestPrompt Captured request prompt. Present only when prompt collection is enabled.
|
||||
RequestPrompt *string `json:"request_prompt,omitempty"`
|
||||
|
||||
// ResolvedProviderId NetBird agent-network provider id that served the request.
|
||||
ResolvedProviderId *string `json:"resolved_provider_id,omitempty"`
|
||||
|
||||
// ResponseCompletion Captured response completion. Present only when prompt collection is enabled.
|
||||
ResponseCompletion *string `json:"response_completion,omitempty"`
|
||||
|
||||
// SelectedPolicyId Agent-network policy id that authorised (or denied) the request.
|
||||
SelectedPolicyId *string `json:"selected_policy_id,omitempty"`
|
||||
|
||||
// ServiceId ID of the synthesised agent-network service that handled the request.
|
||||
ServiceId string `json:"service_id"`
|
||||
|
||||
// SessionId Conversation / coding-session identifier that groups related requests. Sourced from the client's session marker (e.g. OpenAI Codex client_metadata.session_id, Claude Code metadata.user_id). Empty for clients that send none.
|
||||
SessionId *string `json:"session_id,omitempty"`
|
||||
|
||||
// SourceIp Source IP of the request. Empty when log collection is disabled.
|
||||
SourceIp *string `json:"source_ip,omitempty"`
|
||||
|
||||
// StatusCode HTTP status code returned upstream.
|
||||
StatusCode int `json:"status_code"`
|
||||
|
||||
// Stream Whether the request was a streaming completion.
|
||||
Stream *bool `json:"stream,omitempty"`
|
||||
|
||||
// Timestamp Timestamp when the request was made.
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
||||
// TotalTokens Total tokens consumed.
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
|
||||
// UserId NetBird user id of the authenticated caller, if applicable.
|
||||
UserId *string `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkAccessLogSession A session-grouped view of agent-network access logs — all requests sharing a session id (or a single session-less request) folded into one summary plus its ordered entries.
|
||||
type AgentNetworkAccessLogSession struct {
|
||||
// CostUsd Total estimated USD cost across the session.
|
||||
CostUsd float64 `json:"cost_usd"`
|
||||
|
||||
// Decision Session decision — "deny" if any request was denied, otherwise "allow".
|
||||
Decision string `json:"decision"`
|
||||
|
||||
// EndedAt Timestamp of the session's latest request.
|
||||
EndedAt time.Time `json:"ended_at"`
|
||||
|
||||
// Entries The session's access-log entries, oldest first.
|
||||
Entries []AgentNetworkAccessLog `json:"entries"`
|
||||
|
||||
// GroupIds Union of the authorising group ids across the session's entries.
|
||||
GroupIds *[]string `json:"group_ids,omitempty"`
|
||||
|
||||
// InputTokens Total input (prompt) tokens across the session.
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
|
||||
// Models Distinct models seen in the session.
|
||||
Models *[]string `json:"models,omitempty"`
|
||||
|
||||
// OutputTokens Total output (completion) tokens across the session.
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
|
||||
// Providers Distinct LLM provider vendors seen in the session.
|
||||
Providers *[]string `json:"providers,omitempty"`
|
||||
|
||||
// RequestCount Number of requests in the session.
|
||||
RequestCount int `json:"request_count"`
|
||||
|
||||
// SessionId Conversation / coding-session identifier shared by the entries. Empty for a session-less (singleton) request grouped on its own id.
|
||||
SessionId *string `json:"session_id,omitempty"`
|
||||
|
||||
// StartedAt Timestamp of the session's earliest request.
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
|
||||
// TotalTokens Total tokens across the session.
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
|
||||
// UserId NetBird user id of the session's caller.
|
||||
UserId *string `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkAccessLogSessionsResponse defines model for AgentNetworkAccessLogSessionsResponse.
|
||||
type AgentNetworkAccessLogSessionsResponse struct {
|
||||
// Data List of session-grouped agent-network access logs.
|
||||
Data []AgentNetworkAccessLogSession `json:"data"`
|
||||
|
||||
// Page Current page number.
|
||||
Page int `json:"page"`
|
||||
|
||||
// PageSize Number of sessions per page.
|
||||
PageSize int `json:"page_size"`
|
||||
|
||||
// TotalPages Total number of pages available.
|
||||
TotalPages int `json:"total_pages"`
|
||||
|
||||
// TotalRecords Total number of sessions matching the filter.
|
||||
TotalRecords int `json:"total_records"`
|
||||
}
|
||||
|
||||
// AgentNetworkAccessLogsResponse defines model for AgentNetworkAccessLogsResponse.
|
||||
type AgentNetworkAccessLogsResponse struct {
|
||||
// Data List of agent-network access log entries.
|
||||
Data []AgentNetworkAccessLog `json:"data"`
|
||||
|
||||
// Page Current page number.
|
||||
Page int `json:"page"`
|
||||
|
||||
// PageSize Number of items per page.
|
||||
PageSize int `json:"page_size"`
|
||||
|
||||
// TotalPages Total number of pages available.
|
||||
TotalPages int `json:"total_pages"`
|
||||
|
||||
// TotalRecords Total number of log records matching the filter.
|
||||
TotalRecords int `json:"total_records"`
|
||||
}
|
||||
|
||||
// AgentNetworkBudgetRule Account-level budget rule. A limit-only rule bound to groups and/or users that applies across all policies as a min-wins ceiling. Empty targets means it applies to every caller.
|
||||
type AgentNetworkBudgetRule struct {
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
|
||||
// Enabled Whether the rule is enforced.
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Id Budget rule ID.
|
||||
Id string `json:"id"`
|
||||
|
||||
// Limits Token and budget caps attached directly to the policy. These compose with any guardrail-level checks.
|
||||
Limits AgentNetworkPolicyLimits `json:"limits"`
|
||||
|
||||
// Name Display name for the budget rule.
|
||||
Name string `json:"name"`
|
||||
|
||||
// TargetGroups NetBird group ids the rule binds. Empty plus empty target_users means account-wide.
|
||||
TargetGroups []string `json:"target_groups"`
|
||||
|
||||
// TargetUsers NetBird user ids the rule binds directly.
|
||||
TargetUsers []string `json:"target_users"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkBudgetRuleRequest defines model for AgentNetworkBudgetRuleRequest.
|
||||
type AgentNetworkBudgetRuleRequest struct {
|
||||
// Enabled Whether the rule is enforced. Defaults to true on create.
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
|
||||
// Limits Token and budget caps attached directly to the policy. These compose with any guardrail-level checks.
|
||||
Limits AgentNetworkPolicyLimits `json:"limits"`
|
||||
|
||||
// Name Display name for the budget rule.
|
||||
Name string `json:"name"`
|
||||
|
||||
// TargetGroups NetBird group ids the rule binds. Empty plus empty target_users means account-wide.
|
||||
TargetGroups *[]string `json:"target_groups,omitempty"`
|
||||
|
||||
// TargetUsers NetBird user ids the rule binds directly.
|
||||
TargetUsers *[]string `json:"target_users,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkCatalogExtraHeader One optional per-provider routing/config header surfaced on the dashboard. Operator-typed value lives on the provider record's `extra_values` map keyed by `name`. UI copy (input label, helper line, tooltip) is owned by the dashboard, keyed by `name`.
|
||||
type AgentNetworkCatalogExtraHeader struct {
|
||||
// Name Wire header name the proxy stamps with the operator-typed value.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// AgentNetworkCatalogHeaderPairInjection HeaderPair identity-injection shape — separate per-dimension headers (LiteLLM-style, Bifrost).
|
||||
type AgentNetworkCatalogHeaderPairInjection struct {
|
||||
// Customizable When true, the wire header names are operator-overridable per provider record (Bifrost). When false, the catalog values are authoritative (LiteLLM and similar gateways with a fixed wire protocol).
|
||||
Customizable bool `json:"customizable"`
|
||||
|
||||
// EndUserIdHeader Wire header name for the caller's display identity. Default placeholder when `customizable` is true.
|
||||
EndUserIdHeader string `json:"end_user_id_header"`
|
||||
|
||||
// TagsHeader Wire header name for the caller's groups CSV. Default placeholder when `customizable` is true.
|
||||
TagsHeader string `json:"tags_header"`
|
||||
}
|
||||
|
||||
// AgentNetworkCatalogIdentityInjection Catalog-declared identity-injection shape. Present when this provider supports stamping the caller's NetBird identity onto upstream requests. Exactly one of `header_pair` or `json_metadata` is set per provider entry. The dashboard reads the `customizable` flag on whichever shape is present to decide whether to surface the labels as editable inputs (true → editable with the catalog values shown as placeholders; false → fixed and read-only).
|
||||
type AgentNetworkCatalogIdentityInjection struct {
|
||||
// HeaderPair HeaderPair identity-injection shape — separate per-dimension headers (LiteLLM-style, Bifrost).
|
||||
HeaderPair *AgentNetworkCatalogHeaderPairInjection `json:"header_pair,omitempty"`
|
||||
|
||||
// JsonMetadata JSONMetadata identity-injection shape — one wire header carrying a JSON object whose keys label each dimension (Portkey-style, Cloudflare AI Gateway).
|
||||
JsonMetadata *AgentNetworkCatalogJSONMetadataInjection `json:"json_metadata,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkCatalogJSONMetadataInjection JSONMetadata identity-injection shape — one wire header carrying a JSON object whose keys label each dimension (Portkey-style, Cloudflare AI Gateway).
|
||||
type AgentNetworkCatalogJSONMetadataInjection struct {
|
||||
// Customizable When true, the JSON keys are operator-overridable per provider record (Cloudflare). The wire header itself stays catalog-owned. When false, the catalog values are authoritative (Portkey and similar gateways with a fixed JSON schema).
|
||||
Customizable bool `json:"customizable"`
|
||||
|
||||
// GroupsKey JSON key for the caller's groups CSV. Default placeholder when `customizable` is true.
|
||||
GroupsKey string `json:"groups_key"`
|
||||
|
||||
// Header Wire header name carrying the JSON metadata payload. Catalog-owned (not customizable per provider record).
|
||||
Header string `json:"header"`
|
||||
|
||||
// UserKey JSON key for the caller's display identity. Default placeholder when `customizable` is true.
|
||||
UserKey string `json:"user_key"`
|
||||
}
|
||||
|
||||
// AgentNetworkCatalogModel defines model for AgentNetworkCatalogModel.
|
||||
type AgentNetworkCatalogModel struct {
|
||||
// ContextWindow Maximum context window in tokens.
|
||||
ContextWindow int `json:"context_window"`
|
||||
|
||||
// Id Catalog model identifier as exposed by the upstream provider.
|
||||
Id string `json:"id"`
|
||||
|
||||
// InputPer1k Input token price per 1k tokens, in USD.
|
||||
InputPer1k float64 `json:"input_per_1k"`
|
||||
|
||||
// Label Human-friendly model name for the dashboard.
|
||||
Label string `json:"label"`
|
||||
|
||||
// OutputPer1k Output token price per 1k tokens, in USD.
|
||||
OutputPer1k float64 `json:"output_per_1k"`
|
||||
}
|
||||
|
||||
// AgentNetworkCatalogProvider defines model for AgentNetworkCatalogProvider.
|
||||
type AgentNetworkCatalogProvider struct {
|
||||
// AuthHeaderTemplate Template the proxy uses to inject the API key (the literal string ${API_KEY} is replaced at request time).
|
||||
AuthHeaderTemplate string `json:"auth_header_template"`
|
||||
|
||||
// BrandColor Hex brand color used to render the provider badge in the dashboard.
|
||||
BrandColor string `json:"brand_color"`
|
||||
|
||||
// DefaultContentType Default Content-Type for upstream requests.
|
||||
DefaultContentType string `json:"default_content_type"`
|
||||
|
||||
// DefaultHost Default upstream host suggested when adding a provider of this type.
|
||||
DefaultHost string `json:"default_host"`
|
||||
|
||||
// Description Short description shown in the provider picker.
|
||||
Description string `json:"description"`
|
||||
|
||||
// ExtraHeaders Catalog-declared list of optional per-provider routing/config headers the proxy stamps on every upstream request. Each entry surfaces an input on the dashboard's provider modal (one per item, labeled with `label`). Operators fill any subset; values land on the provider record's `extra_values` map keyed by `name`. Used by gateways like Portkey for `x-portkey-config: pc-...` (saved-config id resolving upstream provider + virtual key).
|
||||
ExtraHeaders *[]AgentNetworkCatalogExtraHeader `json:"extra_headers,omitempty"`
|
||||
|
||||
// Id Catalog provider identifier (referenced by AgentNetworkProvider.provider_id).
|
||||
Id string `json:"id"`
|
||||
|
||||
// IdentityInjection Catalog-declared identity-injection shape. Present when this provider supports stamping the caller's NetBird identity onto upstream requests. Exactly one of `header_pair` or `json_metadata` is set per provider entry. The dashboard reads the `customizable` flag on whichever shape is present to decide whether to surface the labels as editable inputs (true → editable with the catalog values shown as placeholders; false → fixed and read-only).
|
||||
IdentityInjection *AgentNetworkCatalogIdentityInjection `json:"identity_injection,omitempty"`
|
||||
|
||||
// Kind Presentation grouping for the provider Select on the dashboard.
|
||||
// "provider" — first-party vendor API (OpenAI, Anthropic, …); the upstream is the model itself.
|
||||
// "gateway" — routing/aggregation layer in front of multiple providers (LiteLLM, Portkey, …); typically pairs with NetBird identity stamping.
|
||||
// "custom" — generic OpenAI-compatible self-hosted endpoint catch-all.
|
||||
Kind AgentNetworkCatalogProviderKind `json:"kind"`
|
||||
|
||||
// Models Catalog models available for this provider.
|
||||
Models []AgentNetworkCatalogModel `json:"models"`
|
||||
|
||||
// Name Display name for the provider.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// AgentNetworkCatalogProviderKind Presentation grouping for the provider Select on the dashboard.
|
||||
// "provider" — first-party vendor API (OpenAI, Anthropic, …); the upstream is the model itself.
|
||||
// "gateway" — routing/aggregation layer in front of multiple providers (LiteLLM, Portkey, …); typically pairs with NetBird identity stamping.
|
||||
// "custom" — generic OpenAI-compatible self-hosted endpoint catch-all.
|
||||
type AgentNetworkCatalogProviderKind string
|
||||
|
||||
// AgentNetworkConsumption One per-(dimension, window) consumption counter row. The proxy ticks one row per dimension on every served LLM request; the dashboard reads this listing to surface live counter growth.
|
||||
type AgentNetworkConsumption struct {
|
||||
// CostUsd Total USD spend booked against this dimension for the window.
|
||||
CostUsd float64 `json:"cost_usd"`
|
||||
|
||||
// DimensionId NetBird user id (when `dimension_kind=user`) or NetBird group id (when `dimension_kind=group`).
|
||||
DimensionId string `json:"dimension_id"`
|
||||
|
||||
// DimensionKind Whether this row counts a single end user or a single source group across every member.
|
||||
DimensionKind AgentNetworkConsumptionDimensionKind `json:"dimension_kind"`
|
||||
|
||||
// TokensInput Total input tokens consumed within the window.
|
||||
TokensInput int64 `json:"tokens_input"`
|
||||
|
||||
// TokensOutput Total output tokens consumed within the window.
|
||||
TokensOutput int64 `json:"tokens_output"`
|
||||
|
||||
// UpdatedAt Timestamp of the last increment recorded for this row.
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
|
||||
// WindowSeconds Length of the aligned window this counter covers, in seconds. Distinct window lengths produce independent counters even on the same dimension.
|
||||
WindowSeconds int64 `json:"window_seconds"`
|
||||
|
||||
// WindowStartUtc UTC start of the aligned window this counter covers. Aligned to the unix epoch so every node computes the same boundary.
|
||||
WindowStartUtc time.Time `json:"window_start_utc"`
|
||||
}
|
||||
|
||||
// AgentNetworkConsumptionDimensionKind Whether this row counts a single end user or a single source group across every member.
|
||||
type AgentNetworkConsumptionDimensionKind string
|
||||
|
||||
// AgentNetworkGuardrail defines model for AgentNetworkGuardrail.
|
||||
type AgentNetworkGuardrail struct {
|
||||
// Checks Guardrail check parameters. Each entry has an `enabled` flag plus per-check configuration; disabled entries are inert.
|
||||
Checks AgentNetworkGuardrailChecks `json:"checks"`
|
||||
|
||||
// CreatedAt Timestamp when the guardrail was created.
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
|
||||
// Description Optional human-readable description.
|
||||
Description string `json:"description"`
|
||||
|
||||
// Id Guardrail ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// Name Display name for the guardrail.
|
||||
Name string `json:"name"`
|
||||
|
||||
// UpdatedAt Timestamp when the guardrail was last updated.
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkGuardrailChecks Guardrail check parameters. Each entry has an `enabled` flag plus per-check configuration; disabled entries are inert.
|
||||
type AgentNetworkGuardrailChecks struct {
|
||||
ModelAllowlist struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Models Allowed catalog model ids. Requests for any other model are denied.
|
||||
Models []string `json:"models"`
|
||||
} `json:"model_allowlist"`
|
||||
PromptCapture struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
RedactPii bool `json:"redact_pii"`
|
||||
} `json:"prompt_capture"`
|
||||
}
|
||||
|
||||
// AgentNetworkGuardrailRequest defines model for AgentNetworkGuardrailRequest.
|
||||
type AgentNetworkGuardrailRequest struct {
|
||||
// Checks Guardrail check parameters. Each entry has an `enabled` flag plus per-check configuration; disabled entries are inert.
|
||||
Checks AgentNetworkGuardrailChecks `json:"checks"`
|
||||
|
||||
// Description Optional human-readable description.
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
||||
// Name Display name for the guardrail.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// AgentNetworkPolicy defines model for AgentNetworkPolicy.
|
||||
type AgentNetworkPolicy struct {
|
||||
// CreatedAt Timestamp when the policy was created.
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
|
||||
// Description Optional human-readable description.
|
||||
Description string `json:"description"`
|
||||
|
||||
// DestinationProviderIds Agent Network provider ids (returned by the providers API) the source groups can reach.
|
||||
DestinationProviderIds []string `json:"destination_provider_ids"`
|
||||
|
||||
// Enabled Whether the policy is enabled.
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// GuardrailIds Agent Network guardrail ids attached to this policy.
|
||||
GuardrailIds []string `json:"guardrail_ids"`
|
||||
|
||||
// Id Policy ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// Limits Token and budget caps attached directly to the policy. These compose with any guardrail-level checks.
|
||||
Limits AgentNetworkPolicyLimits `json:"limits"`
|
||||
|
||||
// Name Display name for the policy.
|
||||
Name string `json:"name"`
|
||||
|
||||
// SourceGroups NetBird group ids whose members are allowed to call the destination providers.
|
||||
SourceGroups []string `json:"source_groups"`
|
||||
|
||||
// UpdatedAt Timestamp when the policy was last updated.
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkPolicyBudgetLimit Per-policy USD spend cap. `group_cap_usd` is applied to each source group independently — every group in the policy's `source_groups` gets its own bucket of this size. `user_cap_usd` is applied independently to each individual user. Caps reset to zero at the start of each window.
|
||||
type AgentNetworkPolicyBudgetLimit struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// GroupCapUsd USD allowed per source group within the window (each group has its own bucket of this size). 0 means uncapped.
|
||||
GroupCapUsd float64 `json:"group_cap_usd"`
|
||||
|
||||
// UserCapUsd USD allowed per individual user within the window. 0 means uncapped.
|
||||
UserCapUsd float64 `json:"user_cap_usd"`
|
||||
|
||||
// WindowSeconds Reset frequency in seconds. Caps reset at the start of each window. Minimum 60 (one minute) when the limit is enabled.
|
||||
WindowSeconds int64 `json:"window_seconds"`
|
||||
}
|
||||
|
||||
// AgentNetworkPolicyLimits Token and budget caps attached directly to the policy. These compose with any guardrail-level checks.
|
||||
type AgentNetworkPolicyLimits struct {
|
||||
// BudgetLimit Per-policy USD spend cap. `group_cap_usd` is applied to each source group independently — every group in the policy's `source_groups` gets its own bucket of this size. `user_cap_usd` is applied independently to each individual user. Caps reset to zero at the start of each window.
|
||||
BudgetLimit AgentNetworkPolicyBudgetLimit `json:"budget_limit"`
|
||||
|
||||
// TokenLimit Per-policy token cap. `group_cap` is applied to each source group independently — every group in the policy's `source_groups` gets its own bucket of this size. `user_cap` is applied independently to each individual user. Caps reset to zero at the start of each window.
|
||||
TokenLimit AgentNetworkPolicyTokenLimit `json:"token_limit"`
|
||||
}
|
||||
|
||||
// AgentNetworkPolicyRequest defines model for AgentNetworkPolicyRequest.
|
||||
type AgentNetworkPolicyRequest struct {
|
||||
// Description Optional human-readable description.
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
||||
// DestinationProviderIds Agent Network provider ids the source groups can reach.
|
||||
DestinationProviderIds []string `json:"destination_provider_ids"`
|
||||
|
||||
// Enabled Whether the policy is enabled. Defaults to true on create.
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
|
||||
// GuardrailIds Agent Network guardrail ids to attach to this policy.
|
||||
GuardrailIds *[]string `json:"guardrail_ids,omitempty"`
|
||||
|
||||
// Limits Token and budget caps attached directly to the policy. These compose with any guardrail-level checks.
|
||||
Limits *AgentNetworkPolicyLimits `json:"limits,omitempty"`
|
||||
|
||||
// Name Display name for the policy.
|
||||
Name string `json:"name"`
|
||||
|
||||
// SourceGroups NetBird group ids whose members are allowed to call the destination providers.
|
||||
SourceGroups []string `json:"source_groups"`
|
||||
}
|
||||
|
||||
// AgentNetworkPolicyTokenLimit Per-policy token cap. `group_cap` is applied to each source group independently — every group in the policy's `source_groups` gets its own bucket of this size. `user_cap` is applied independently to each individual user. Caps reset to zero at the start of each window.
|
||||
type AgentNetworkPolicyTokenLimit struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// GroupCap Tokens allowed per source group within the window (each group has its own bucket of this size). 0 means uncapped.
|
||||
GroupCap int64 `json:"group_cap"`
|
||||
|
||||
// UserCap Tokens allowed per individual user within the window. 0 means uncapped.
|
||||
UserCap int64 `json:"user_cap"`
|
||||
|
||||
// WindowSeconds Reset frequency in seconds. The cap counter resets to zero at the start of each window. Minimum 60 (one minute) when the limit is enabled.
|
||||
WindowSeconds int64 `json:"window_seconds"`
|
||||
}
|
||||
|
||||
// AgentNetworkProvider defines model for AgentNetworkProvider.
|
||||
type AgentNetworkProvider struct {
|
||||
// CreatedAt Timestamp when the provider was created.
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
|
||||
// Enabled Whether the provider is enabled.
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// ExtraValues Operator-typed values for catalog-declared extra headers. Keys are wire header names (e.g. `x-portkey-config`); values are the strings the proxy stamps on every upstream request to this provider. Catalog (AgentNetworkCatalogProvider.extra_headers) declares which keys are accepted; values not declared by the catalog are ignored at synth time. Empty / missing values mean no header stamped.
|
||||
ExtraValues *map[string]string `json:"extra_values,omitempty"`
|
||||
|
||||
// Id Provider ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// IdentityHeaderGroups Wire header name the proxy stamps with the caller's NetBird groups as a comma-separated list (sorted) when the catalog entry's HeaderPair is `customizable`. Empty disables stamping for this dimension. Same per-catalog semantics as `identity_header_user_id`.
|
||||
IdentityHeaderGroups *string `json:"identity_header_groups,omitempty"`
|
||||
|
||||
// IdentityHeaderUserId Wire header name the proxy stamps with the caller's display identity (user email or peer name) when the catalog entry's HeaderPair is `customizable`. Empty disables stamping for this dimension. Ignored when the catalog entry has a fixed HeaderPair (e.g. LiteLLM, Portkey). Used today by Bifrost: typical values are `x-bf-lh-netbird_user_id` (always-on log metadata) or `x-bf-dim-netbird_user_id` (Prometheus / OTEL — requires the label to be pre-declared in the gateway's `client.prometheus_labels` config).
|
||||
IdentityHeaderUserId *string `json:"identity_header_user_id,omitempty"`
|
||||
|
||||
// Models Models exposed through this endpoint, with the operator's per-1k input/output prices. Empty means all catalog models are allowed at catalog prices.
|
||||
Models []AgentNetworkProviderModel `json:"models"`
|
||||
|
||||
// Name Display name shown in the dashboard.
|
||||
Name string `json:"name"`
|
||||
|
||||
// ProviderId Catalog identifier for the upstream AI provider (e.g. openai_api, anthropic_api, azure_openai_api, bedrock_api, vertex_ai_api, mistral_api, custom).
|
||||
ProviderId string `json:"provider_id"`
|
||||
|
||||
// SkipTlsVerification Whether upstream TLS certificate verification is skipped when the proxy dials this provider's URL. Intended for self-hosted / internal gateways behind a private or self-signed certificate.
|
||||
SkipTlsVerification bool `json:"skip_tls_verification"`
|
||||
|
||||
// UpdatedAt Timestamp when the provider was last updated.
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
|
||||
// UpstreamUrl Full upstream URL (with scheme) that NetBird forwards traffic to.
|
||||
UpstreamUrl string `json:"upstream_url"`
|
||||
}
|
||||
|
||||
// AgentNetworkProviderModel A model exposed by the provider, with the operator's per-1k input/output prices in USD.
|
||||
type AgentNetworkProviderModel struct {
|
||||
// Id Model identifier (e.g. "gpt-4o-mini").
|
||||
Id string `json:"id"`
|
||||
|
||||
// InputPer1k Cost per 1k input tokens, in USD.
|
||||
InputPer1k float64 `json:"input_per_1k"`
|
||||
|
||||
// OutputPer1k Cost per 1k output tokens, in USD.
|
||||
OutputPer1k float64 `json:"output_per_1k"`
|
||||
}
|
||||
|
||||
// AgentNetworkProviderRequest defines model for AgentNetworkProviderRequest.
|
||||
type AgentNetworkProviderRequest struct {
|
||||
// ApiKey Upstream provider API key. Sealed at rest on the management server and never returned in responses. Required on create; optional on update (omit to keep the existing key).
|
||||
ApiKey *string `json:"api_key,omitempty"`
|
||||
|
||||
// BootstrapCluster Proxy cluster used to bootstrap the per-account agent-network endpoint when the first provider is created. Ignored on subsequent creates and on updates because the cluster is pinned on the account-level Settings row.
|
||||
BootstrapCluster *string `json:"bootstrap_cluster,omitempty"`
|
||||
|
||||
// Enabled Whether the provider is enabled. Defaults to true on create.
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
|
||||
// ExtraValues Operator-typed values for catalog-declared extra headers (see AgentNetworkProvider.extra_values). When present on a request, the whole map replaces the stored values. Empty strings drop the corresponding key.
|
||||
ExtraValues *map[string]string `json:"extra_values,omitempty"`
|
||||
|
||||
// IdentityHeaderGroups Wire header name for the caller's groups CSV. See AgentNetworkProvider.identity_header_groups. Same omit / empty semantics as `identity_header_user_id`.
|
||||
IdentityHeaderGroups *string `json:"identity_header_groups,omitempty"`
|
||||
|
||||
// IdentityHeaderUserId Wire header name for the caller's display identity. See AgentNetworkProvider.identity_header_user_id. When omitted on a request, the stored value is left unchanged; pass an empty string explicitly to clear it (which disables stamping for this dimension).
|
||||
IdentityHeaderUserId *string `json:"identity_header_user_id,omitempty"`
|
||||
|
||||
// Models Models exposed through this endpoint, with the operator's per-1k input/output prices. Empty means all catalog models are allowed at catalog prices.
|
||||
Models *[]AgentNetworkProviderModel `json:"models,omitempty"`
|
||||
|
||||
// Name Display name for the provider.
|
||||
Name string `json:"name"`
|
||||
|
||||
// ProviderId Catalog identifier for the upstream AI provider (e.g. openai_api, anthropic_api, azure_openai_api, bedrock_api, vertex_ai_api, mistral_api, custom).
|
||||
ProviderId string `json:"provider_id"`
|
||||
|
||||
// SkipTlsVerification Skip upstream TLS certificate verification when the proxy dials this provider's URL. For self-hosted / internal gateways behind a private or self-signed certificate. Defaults to false. When omitted on update, the stored value is left unchanged.
|
||||
SkipTlsVerification *bool `json:"skip_tls_verification,omitempty"`
|
||||
|
||||
// UpstreamUrl Full upstream URL (with scheme) that NetBird forwards traffic to.
|
||||
UpstreamUrl string `json:"upstream_url"`
|
||||
}
|
||||
|
||||
// AgentNetworkSettings Per-account Agent Network gateway settings. One row per account; cluster and subdomain are auto-assigned on first provider create and immutable thereafter.
|
||||
type AgentNetworkSettings struct {
|
||||
// AccessLogRetentionDays Days to retain full access-log rows; older rows are swept. 0 or less means keep indefinitely. Usage records are retained independently.
|
||||
AccessLogRetentionDays *int `json:"access_log_retention_days,omitempty"`
|
||||
|
||||
// Cluster Address of the NetBird proxy cluster fronting this account's agent-network endpoint.
|
||||
Cluster string `json:"cluster"`
|
||||
|
||||
// CreatedAt Timestamp when the settings row was created.
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
|
||||
// EnableLogCollection Whether per-request access-log entries are collected for this account's agent-network traffic.
|
||||
EnableLogCollection bool `json:"enable_log_collection"`
|
||||
|
||||
// EnablePromptCollection Master switch for request/response prompt capture. Capture runs only when this is on AND a policy guardrail also enables it.
|
||||
EnablePromptCollection bool `json:"enable_prompt_collection"`
|
||||
|
||||
// Endpoint Bare hostname agents call for this account, computed as `<subdomain>.<cluster>`.
|
||||
Endpoint string `json:"endpoint"`
|
||||
|
||||
// RedactPii Whether captured prompts have PII redacted. Effective redaction is the OR of this and any policy guardrail's redact setting.
|
||||
RedactPii bool `json:"redact_pii"`
|
||||
|
||||
// Subdomain Auto-generated DNS-safe label that prefixes the cluster to form the agent-network endpoint.
|
||||
Subdomain string `json:"subdomain"`
|
||||
|
||||
// UpdatedAt Timestamp when the settings row was last updated.
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// AgentNetworkSettingsRequest Mutable account-level Agent Network settings. Cluster and subdomain are immutable and not accepted here.
|
||||
type AgentNetworkSettingsRequest struct {
|
||||
// AccessLogRetentionDays Days to retain full access-log rows; older rows are swept. 0 or less means keep indefinitely.
|
||||
AccessLogRetentionDays *int `json:"access_log_retention_days,omitempty"`
|
||||
|
||||
// EnableLogCollection Whether per-request access-log entries are collected for this account's agent-network traffic.
|
||||
EnableLogCollection bool `json:"enable_log_collection"`
|
||||
|
||||
// EnablePromptCollection Master switch for request/response prompt capture.
|
||||
EnablePromptCollection bool `json:"enable_prompt_collection"`
|
||||
|
||||
// RedactPii Whether captured prompts have PII redacted.
|
||||
RedactPii bool `json:"redact_pii"`
|
||||
}
|
||||
|
||||
// AgentNetworkUsageBucket One aggregated agent-network usage time bucket (UTC). The bucket width is set by the request's granularity.
|
||||
type AgentNetworkUsageBucket struct {
|
||||
// CostUsd Total estimated USD spend in the bucket.
|
||||
CostUsd float64 `json:"cost_usd"`
|
||||
|
||||
// InputTokens Total input (prompt) tokens in the bucket.
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
|
||||
// OutputTokens Total output (completion) tokens in the bucket.
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
|
||||
// PeriodStart Start of the bucket in YYYY-MM-DD (UTC) — the day, the week start (Monday), or the month start, depending on granularity.
|
||||
PeriodStart string `json:"period_start"`
|
||||
|
||||
// TotalTokens Total tokens in the bucket.
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
}
|
||||
|
||||
// AvailablePorts defines model for AvailablePorts.
|
||||
type AvailablePorts struct {
|
||||
// Tcp Number of available TCP ports left on the ingress peer
|
||||
@@ -4892,6 +5696,138 @@ type bearerAuthContextKey string
|
||||
// tokenAuthContextKey is the context key for TokenAuth security scheme
|
||||
type tokenAuthContextKey string
|
||||
|
||||
// GetApiAgentNetworkAccessLogSessionsParams defines parameters for GetApiAgentNetworkAccessLogSessions.
|
||||
type GetApiAgentNetworkAccessLogSessionsParams struct {
|
||||
// Page Page number for pagination (1-indexed).
|
||||
Page *int `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PageSize Number of sessions per page (max 100).
|
||||
PageSize *int `form:"page_size,omitempty" json:"page_size,omitempty"`
|
||||
|
||||
// SortBy Session-level field to sort by. "timestamp" is the session's last activity, "started_at" its first.
|
||||
SortBy *GetApiAgentNetworkAccessLogSessionsParamsSortBy `form:"sort_by,omitempty" json:"sort_by,omitempty"`
|
||||
|
||||
// SortOrder Sort order (ascending or descending).
|
||||
SortOrder *GetApiAgentNetworkAccessLogSessionsParamsSortOrder `form:"sort_order,omitempty" json:"sort_order,omitempty"`
|
||||
|
||||
// Search General search across log ID, host, path, model, and user email/name.
|
||||
Search *string `form:"search,omitempty" json:"search,omitempty"`
|
||||
|
||||
// UserId Filter by authenticated user ID.
|
||||
UserId *string `form:"user_id,omitempty" json:"user_id,omitempty"`
|
||||
|
||||
// SessionId Filter to a single conversation / coding session id.
|
||||
SessionId *string `form:"session_id,omitempty" json:"session_id,omitempty"`
|
||||
|
||||
// GroupId Filter by authorising group id. Repeat for multiple (matches any).
|
||||
GroupId *[]string `form:"group_id,omitempty" json:"group_id,omitempty"`
|
||||
|
||||
// ProviderId Filter by resolved provider id. Repeat for multiple (matches any).
|
||||
ProviderId *[]string `form:"provider_id,omitempty" json:"provider_id,omitempty"`
|
||||
|
||||
// Model Filter by model. Repeat for multiple (matches any).
|
||||
Model *[]string `form:"model,omitempty" json:"model,omitempty"`
|
||||
|
||||
// Decision Filter by policy decision (e.g. allow, deny).
|
||||
Decision *string `form:"decision,omitempty" json:"decision,omitempty"`
|
||||
|
||||
// Path Filter by request path prefix (matches entries whose path starts with this value).
|
||||
Path *string `form:"path,omitempty" json:"path,omitempty"`
|
||||
|
||||
// StartDate Filter by timestamp >= start_date (RFC3339 format).
|
||||
StartDate *time.Time `form:"start_date,omitempty" json:"start_date,omitempty"`
|
||||
|
||||
// EndDate Filter by timestamp <= end_date (RFC3339 format).
|
||||
EndDate *time.Time `form:"end_date,omitempty" json:"end_date,omitempty"`
|
||||
}
|
||||
|
||||
// GetApiAgentNetworkAccessLogSessionsParamsSortBy defines parameters for GetApiAgentNetworkAccessLogSessions.
|
||||
type GetApiAgentNetworkAccessLogSessionsParamsSortBy string
|
||||
|
||||
// GetApiAgentNetworkAccessLogSessionsParamsSortOrder defines parameters for GetApiAgentNetworkAccessLogSessions.
|
||||
type GetApiAgentNetworkAccessLogSessionsParamsSortOrder string
|
||||
|
||||
// GetApiAgentNetworkAccessLogsParams defines parameters for GetApiAgentNetworkAccessLogs.
|
||||
type GetApiAgentNetworkAccessLogsParams struct {
|
||||
// Page Page number for pagination (1-indexed).
|
||||
Page *int `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PageSize Number of items per page (max 100).
|
||||
PageSize *int `form:"page_size,omitempty" json:"page_size,omitempty"`
|
||||
|
||||
// SortBy Field to sort by.
|
||||
SortBy *GetApiAgentNetworkAccessLogsParamsSortBy `form:"sort_by,omitempty" json:"sort_by,omitempty"`
|
||||
|
||||
// SortOrder Sort order (ascending or descending).
|
||||
SortOrder *GetApiAgentNetworkAccessLogsParamsSortOrder `form:"sort_order,omitempty" json:"sort_order,omitempty"`
|
||||
|
||||
// Search General search across log ID, host, path, model, and user email/name.
|
||||
Search *string `form:"search,omitempty" json:"search,omitempty"`
|
||||
|
||||
// UserId Filter by authenticated user ID.
|
||||
UserId *string `form:"user_id,omitempty" json:"user_id,omitempty"`
|
||||
|
||||
// SessionId Filter to a single conversation / coding session id (groups all requests of one session).
|
||||
SessionId *string `form:"session_id,omitempty" json:"session_id,omitempty"`
|
||||
|
||||
// GroupId Filter by authorising group id. Repeat for multiple (matches any).
|
||||
GroupId *[]string `form:"group_id,omitempty" json:"group_id,omitempty"`
|
||||
|
||||
// ProviderId Filter by resolved provider id. Repeat for multiple (matches any).
|
||||
ProviderId *[]string `form:"provider_id,omitempty" json:"provider_id,omitempty"`
|
||||
|
||||
// Model Filter by model. Repeat for multiple (matches any).
|
||||
Model *[]string `form:"model,omitempty" json:"model,omitempty"`
|
||||
|
||||
// Decision Filter by policy decision (e.g. allow, deny).
|
||||
Decision *string `form:"decision,omitempty" json:"decision,omitempty"`
|
||||
|
||||
// Path Filter by request path prefix (matches entries whose path starts with this value).
|
||||
Path *string `form:"path,omitempty" json:"path,omitempty"`
|
||||
|
||||
// StartDate Filter by timestamp >= start_date (RFC3339 format).
|
||||
StartDate *time.Time `form:"start_date,omitempty" json:"start_date,omitempty"`
|
||||
|
||||
// EndDate Filter by timestamp <= end_date (RFC3339 format).
|
||||
EndDate *time.Time `form:"end_date,omitempty" json:"end_date,omitempty"`
|
||||
}
|
||||
|
||||
// GetApiAgentNetworkAccessLogsParamsSortBy defines parameters for GetApiAgentNetworkAccessLogs.
|
||||
type GetApiAgentNetworkAccessLogsParamsSortBy string
|
||||
|
||||
// GetApiAgentNetworkAccessLogsParamsSortOrder defines parameters for GetApiAgentNetworkAccessLogs.
|
||||
type GetApiAgentNetworkAccessLogsParamsSortOrder string
|
||||
|
||||
// GetApiAgentNetworkUsageOverviewParams defines parameters for GetApiAgentNetworkUsageOverview.
|
||||
type GetApiAgentNetworkUsageOverviewParams struct {
|
||||
// Granularity Time bucket width. Defaults to day.
|
||||
Granularity *GetApiAgentNetworkUsageOverviewParamsGranularity `form:"granularity,omitempty" json:"granularity,omitempty"`
|
||||
|
||||
// StartDate Filter by timestamp >= start_date (RFC3339 format).
|
||||
StartDate *time.Time `form:"start_date,omitempty" json:"start_date,omitempty"`
|
||||
|
||||
// EndDate Filter by timestamp <= end_date (RFC3339 format).
|
||||
EndDate *time.Time `form:"end_date,omitempty" json:"end_date,omitempty"`
|
||||
|
||||
// UserId Filter by user ID.
|
||||
UserId *string `form:"user_id,omitempty" json:"user_id,omitempty"`
|
||||
|
||||
// SessionId Filter to a single conversation / coding session id.
|
||||
SessionId *string `form:"session_id,omitempty" json:"session_id,omitempty"`
|
||||
|
||||
// GroupId Filter by authorising group id. Repeat for multiple (matches any).
|
||||
GroupId *[]string `form:"group_id,omitempty" json:"group_id,omitempty"`
|
||||
|
||||
// ProviderId Filter by resolved provider id. Repeat for multiple (matches any).
|
||||
ProviderId *[]string `form:"provider_id,omitempty" json:"provider_id,omitempty"`
|
||||
|
||||
// Model Filter by model. Repeat for multiple (matches any).
|
||||
Model *[]string `form:"model,omitempty" json:"model,omitempty"`
|
||||
}
|
||||
|
||||
// GetApiAgentNetworkUsageOverviewParamsGranularity defines parameters for GetApiAgentNetworkUsageOverview.
|
||||
type GetApiAgentNetworkUsageOverviewParamsGranularity string
|
||||
|
||||
// GetApiEventsNetworkTrafficParams defines parameters for GetApiEventsNetworkTraffic.
|
||||
type GetApiEventsNetworkTrafficParams struct {
|
||||
// Page Page number
|
||||
@@ -5090,6 +6026,33 @@ type GetApiUsersParams struct {
|
||||
// PutApiAccountsAccountIdJSONRequestBody defines body for PutApiAccountsAccountId for application/json ContentType.
|
||||
type PutApiAccountsAccountIdJSONRequestBody = AccountRequest
|
||||
|
||||
// PostApiAgentNetworkBudgetRulesJSONRequestBody defines body for PostApiAgentNetworkBudgetRules for application/json ContentType.
|
||||
type PostApiAgentNetworkBudgetRulesJSONRequestBody = AgentNetworkBudgetRuleRequest
|
||||
|
||||
// PutApiAgentNetworkBudgetRulesRuleIdJSONRequestBody defines body for PutApiAgentNetworkBudgetRulesRuleId for application/json ContentType.
|
||||
type PutApiAgentNetworkBudgetRulesRuleIdJSONRequestBody = AgentNetworkBudgetRuleRequest
|
||||
|
||||
// PostApiAgentNetworkGuardrailsJSONRequestBody defines body for PostApiAgentNetworkGuardrails for application/json ContentType.
|
||||
type PostApiAgentNetworkGuardrailsJSONRequestBody = AgentNetworkGuardrailRequest
|
||||
|
||||
// PutApiAgentNetworkGuardrailsGuardrailIdJSONRequestBody defines body for PutApiAgentNetworkGuardrailsGuardrailId for application/json ContentType.
|
||||
type PutApiAgentNetworkGuardrailsGuardrailIdJSONRequestBody = AgentNetworkGuardrailRequest
|
||||
|
||||
// PostApiAgentNetworkPoliciesJSONRequestBody defines body for PostApiAgentNetworkPolicies for application/json ContentType.
|
||||
type PostApiAgentNetworkPoliciesJSONRequestBody = AgentNetworkPolicyRequest
|
||||
|
||||
// PutApiAgentNetworkPoliciesPolicyIdJSONRequestBody defines body for PutApiAgentNetworkPoliciesPolicyId for application/json ContentType.
|
||||
type PutApiAgentNetworkPoliciesPolicyIdJSONRequestBody = AgentNetworkPolicyRequest
|
||||
|
||||
// PostApiAgentNetworkProvidersJSONRequestBody defines body for PostApiAgentNetworkProviders for application/json ContentType.
|
||||
type PostApiAgentNetworkProvidersJSONRequestBody = AgentNetworkProviderRequest
|
||||
|
||||
// PutApiAgentNetworkProvidersProviderIdJSONRequestBody defines body for PutApiAgentNetworkProvidersProviderId for application/json ContentType.
|
||||
type PutApiAgentNetworkProvidersProviderIdJSONRequestBody = AgentNetworkProviderRequest
|
||||
|
||||
// PutApiAgentNetworkSettingsJSONRequestBody defines body for PutApiAgentNetworkSettings for application/json ContentType.
|
||||
type PutApiAgentNetworkSettingsJSONRequestBody = AgentNetworkSettingsRequest
|
||||
|
||||
// PostApiDnsNameserversJSONRequestBody defines body for PostApiDnsNameservers for application/json ContentType.
|
||||
type PostApiDnsNameserversJSONRequestBody = NameserverGroupRequest
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -312,6 +312,8 @@ message NetbirdConfig {
|
||||
RelayConfig relay = 4;
|
||||
|
||||
FlowConfig flow = 5;
|
||||
|
||||
MetricsConfig metrics = 6;
|
||||
}
|
||||
|
||||
// HostConfig describes connection properties of some server (e.g. STUN, Signal, Management)
|
||||
@@ -350,6 +352,10 @@ message FlowConfig {
|
||||
bool dnsCollection = 8;
|
||||
}
|
||||
|
||||
message MetricsConfig {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
// JWTConfig represents JWT authentication configuration for validating tokens.
|
||||
message JWTConfig {
|
||||
string issuer = 1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,18 @@ service ProxyService {
|
||||
// issue a session cookie without redirecting through the OIDC flow.
|
||||
// Mirrors ValidateSession's response shape.
|
||||
rpc ValidateTunnelPeer(ValidateTunnelPeerRequest) returns (ValidateTunnelPeerResponse);
|
||||
|
||||
// CheckLLMPolicyLimits is the pre-flight RPC the proxy calls before each
|
||||
// LLM request. Management runs the per-policy headroom selection across
|
||||
// every policy authorising the caller's user / groups for the resolved
|
||||
// provider and returns the chosen attribution policy + group, or a deny
|
||||
// when no applicable policy has headroom > 0.
|
||||
rpc CheckLLMPolicyLimits(CheckLLMPolicyLimitsRequest) returns (CheckLLMPolicyLimitsResponse);
|
||||
|
||||
// RecordLLMUsage is the post-flight RPC the proxy calls after the upstream
|
||||
// returns. Increments the per-(dimension, window) counters for the
|
||||
// attribution policy chosen by CheckLLMPolicyLimits.
|
||||
rpc RecordLLMUsage(RecordLLMUsageRequest) returns (RecordLLMUsageResponse);
|
||||
}
|
||||
|
||||
// ProxyCapabilities describes what a proxy can handle.
|
||||
@@ -107,6 +119,59 @@ message PathTargetOptions {
|
||||
// reachable without WireGuard (public APIs, LAN services, localhost
|
||||
// sidecars). Defaults to false — embedded client is the standard path.
|
||||
bool direct_upstream = 7;
|
||||
// Proxy clamps to [0, proxy-wide max (1 MiB)] at apply time. Agent-network
|
||||
// synthesized targets only; private services leave these zero.
|
||||
int64 capture_max_request_bytes = 8;
|
||||
// Proxy clamps to [0, proxy-wide max (1 MiB)] at apply time.
|
||||
int64 capture_max_response_bytes = 9;
|
||||
// Content types eligible for body capture (e.g. "application/json").
|
||||
repeated string capture_content_types = 10;
|
||||
// Per-target middleware configurations populated by the agent-network
|
||||
// synthesizer. Validated and clamped by the proxy at apply time.
|
||||
repeated MiddlewareConfig middlewares = 11;
|
||||
// When true, the proxy stamps agent_network=true on access-log entries
|
||||
// for this target so management routes them to the agent-network log
|
||||
// surface.
|
||||
bool agent_network = 12;
|
||||
// When true, the proxy suppresses the per-request access-log emission for
|
||||
// this target. Defaults false to preserve existing access-log behavior for
|
||||
// every non-agent-network target. The agent-network synth target sets this
|
||||
// true only when the account's EnableLogCollection toggle is off.
|
||||
bool disable_access_log = 13;
|
||||
}
|
||||
|
||||
// MiddlewareSlot identifies where in the request lifecycle a middleware
|
||||
// runs. Mirrors proxy/internal/middleware.Slot.
|
||||
enum MiddlewareSlot {
|
||||
MIDDLEWARE_SLOT_UNSPECIFIED = 0;
|
||||
MIDDLEWARE_SLOT_ON_REQUEST = 1;
|
||||
MIDDLEWARE_SLOT_ON_RESPONSE = 2;
|
||||
MIDDLEWARE_SLOT_TERMINAL = 3;
|
||||
}
|
||||
|
||||
// MiddlewareConfig is the per-target configuration for a single middleware.
|
||||
// The proxy validates every incoming MiddlewareConfig at apply time:
|
||||
// unknown ids are rejected, timeout is clamped to [10ms, 5s], and the
|
||||
// declared slot must match the registered middleware's slot.
|
||||
message MiddlewareConfig {
|
||||
// Middleware id; must match the proxy-local compiled-in registry.
|
||||
string id = 1;
|
||||
bool enabled = 2;
|
||||
MiddlewareSlot slot = 3;
|
||||
// Free-form JSON unmarshalled by the middleware factory into its own typed
|
||||
// config struct. Empty / null / {} are valid (zero-value config).
|
||||
bytes config_json = 4;
|
||||
enum FailMode {
|
||||
FAIL_OPEN = 0;
|
||||
FAIL_CLOSED = 1;
|
||||
}
|
||||
FailMode fail_mode = 5;
|
||||
// Clamped to [10ms, 5s] at apply time; zero → 500ms default.
|
||||
google.protobuf.Duration timeout = 6;
|
||||
// When true, the middleware may mutate request headers or body (subject to
|
||||
// policy). Honoured only when the implementation also declares
|
||||
// MutationsSupported.
|
||||
bool can_mutate = 7;
|
||||
}
|
||||
|
||||
message PathMapping {
|
||||
@@ -190,6 +255,10 @@ message AccessLog {
|
||||
string protocol = 16;
|
||||
// Extra key-value metadata for the access log entry (e.g. crowdsec_verdict, scenario).
|
||||
map<string, string> metadata = 17;
|
||||
// When true, the entry was emitted by an agent-network synth service.
|
||||
// Management routes these to the agent-network access-log surface instead
|
||||
// of the standard service log.
|
||||
bool agent_network = 18;
|
||||
}
|
||||
|
||||
message AuthenticateRequest {
|
||||
@@ -376,3 +445,59 @@ message SyncMappingsResponse {
|
||||
bool initial_sync_complete = 2;
|
||||
}
|
||||
|
||||
// CheckLLMPolicyLimitsRequest carries the resolved caller identity and the
|
||||
// upstream provider already chosen by llm_router. Management computes which
|
||||
// policies authorise the request, picks the one with the most remaining
|
||||
// headroom, and returns the attribution decision.
|
||||
message CheckLLMPolicyLimitsRequest {
|
||||
// account_id is the netbird account the request belongs to.
|
||||
string account_id = 1;
|
||||
// user_id is the netbird user id of the caller. May be empty when the
|
||||
// principal is a tunnel-peer that isn't bound to a user; group membership
|
||||
// still gates the request in that case.
|
||||
string user_id = 2;
|
||||
// group_ids is the caller's full group membership at request time.
|
||||
repeated string group_ids = 3;
|
||||
// provider_id is the agent-network provider record id chosen by llm_router.
|
||||
string provider_id = 4;
|
||||
// model is the upstream model identifier extracted from the request body.
|
||||
string model = 5;
|
||||
}
|
||||
|
||||
// CheckLLMPolicyLimitsResponse is management's allow-or-deny decision for a
|
||||
// pre-flight check.
|
||||
message CheckLLMPolicyLimitsResponse {
|
||||
// decision is "allow" or "deny".
|
||||
string decision = 1;
|
||||
// selected_policy_id names the policy that paid for this request.
|
||||
string selected_policy_id = 2;
|
||||
// attribution_group_id is the source group the request booked against.
|
||||
string attribution_group_id = 3;
|
||||
// window_seconds is the cap window length the selected policy uses.
|
||||
int64 window_seconds = 4;
|
||||
// deny_code is set on decision="deny" with a stable label.
|
||||
string deny_code = 5;
|
||||
// deny_reason is a short human-readable explanation paired with deny_code.
|
||||
string deny_reason = 6;
|
||||
}
|
||||
|
||||
// RecordLLMUsageRequest is the post-flight increment the proxy posts after
|
||||
// the upstream call. Counters are keyed on (account, dimension, window).
|
||||
message RecordLLMUsageRequest {
|
||||
string account_id = 1;
|
||||
string user_id = 2;
|
||||
// group_id is the selected policy's attribution group, recorded against the
|
||||
// policy window (window_seconds).
|
||||
string group_id = 3;
|
||||
int64 window_seconds = 4;
|
||||
int64 tokens_input = 5;
|
||||
int64 tokens_output = 6;
|
||||
double cost_usd = 7;
|
||||
// group_ids is the caller's full group membership, used to fan the same
|
||||
// usage out to every applicable account-level budget rule's own window.
|
||||
repeated string group_ids = 8;
|
||||
}
|
||||
|
||||
message RecordLLMUsageResponse {
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,16 @@ type ProxyServiceClient interface {
|
||||
// issue a session cookie without redirecting through the OIDC flow.
|
||||
// Mirrors ValidateSession's response shape.
|
||||
ValidateTunnelPeer(ctx context.Context, in *ValidateTunnelPeerRequest, opts ...grpc.CallOption) (*ValidateTunnelPeerResponse, error)
|
||||
// CheckLLMPolicyLimits is the pre-flight RPC the proxy calls before each
|
||||
// LLM request. Management runs the per-policy headroom selection across
|
||||
// every policy authorising the caller's user / groups for the resolved
|
||||
// provider and returns the chosen attribution policy + group, or a deny
|
||||
// when no applicable policy has headroom > 0.
|
||||
CheckLLMPolicyLimits(ctx context.Context, in *CheckLLMPolicyLimitsRequest, opts ...grpc.CallOption) (*CheckLLMPolicyLimitsResponse, error)
|
||||
// RecordLLMUsage is the post-flight RPC the proxy calls after the upstream
|
||||
// returns. Increments the per-(dimension, window) counters for the
|
||||
// attribution policy chosen by CheckLLMPolicyLimits.
|
||||
RecordLLMUsage(ctx context.Context, in *RecordLLMUsageRequest, opts ...grpc.CallOption) (*RecordLLMUsageResponse, error)
|
||||
}
|
||||
|
||||
type proxyServiceClient struct {
|
||||
@@ -179,6 +189,24 @@ func (c *proxyServiceClient) ValidateTunnelPeer(ctx context.Context, in *Validat
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *proxyServiceClient) CheckLLMPolicyLimits(ctx context.Context, in *CheckLLMPolicyLimitsRequest, opts ...grpc.CallOption) (*CheckLLMPolicyLimitsResponse, error) {
|
||||
out := new(CheckLLMPolicyLimitsResponse)
|
||||
err := c.cc.Invoke(ctx, "/management.ProxyService/CheckLLMPolicyLimits", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *proxyServiceClient) RecordLLMUsage(ctx context.Context, in *RecordLLMUsageRequest, opts ...grpc.CallOption) (*RecordLLMUsageResponse, error) {
|
||||
out := new(RecordLLMUsageResponse)
|
||||
err := c.cc.Invoke(ctx, "/management.ProxyService/RecordLLMUsage", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ProxyServiceServer is the server API for ProxyService service.
|
||||
// All implementations must embed UnimplementedProxyServiceServer
|
||||
// for forward compatibility
|
||||
@@ -208,6 +236,16 @@ type ProxyServiceServer interface {
|
||||
// issue a session cookie without redirecting through the OIDC flow.
|
||||
// Mirrors ValidateSession's response shape.
|
||||
ValidateTunnelPeer(context.Context, *ValidateTunnelPeerRequest) (*ValidateTunnelPeerResponse, error)
|
||||
// CheckLLMPolicyLimits is the pre-flight RPC the proxy calls before each
|
||||
// LLM request. Management runs the per-policy headroom selection across
|
||||
// every policy authorising the caller's user / groups for the resolved
|
||||
// provider and returns the chosen attribution policy + group, or a deny
|
||||
// when no applicable policy has headroom > 0.
|
||||
CheckLLMPolicyLimits(context.Context, *CheckLLMPolicyLimitsRequest) (*CheckLLMPolicyLimitsResponse, error)
|
||||
// RecordLLMUsage is the post-flight RPC the proxy calls after the upstream
|
||||
// returns. Increments the per-(dimension, window) counters for the
|
||||
// attribution policy chosen by CheckLLMPolicyLimits.
|
||||
RecordLLMUsage(context.Context, *RecordLLMUsageRequest) (*RecordLLMUsageResponse, error)
|
||||
mustEmbedUnimplementedProxyServiceServer()
|
||||
}
|
||||
|
||||
@@ -242,6 +280,12 @@ func (UnimplementedProxyServiceServer) ValidateSession(context.Context, *Validat
|
||||
func (UnimplementedProxyServiceServer) ValidateTunnelPeer(context.Context, *ValidateTunnelPeerRequest) (*ValidateTunnelPeerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ValidateTunnelPeer not implemented")
|
||||
}
|
||||
func (UnimplementedProxyServiceServer) CheckLLMPolicyLimits(context.Context, *CheckLLMPolicyLimitsRequest) (*CheckLLMPolicyLimitsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckLLMPolicyLimits not implemented")
|
||||
}
|
||||
func (UnimplementedProxyServiceServer) RecordLLMUsage(context.Context, *RecordLLMUsageRequest) (*RecordLLMUsageResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RecordLLMUsage not implemented")
|
||||
}
|
||||
func (UnimplementedProxyServiceServer) mustEmbedUnimplementedProxyServiceServer() {}
|
||||
|
||||
// UnsafeProxyServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
@@ -428,6 +472,42 @@ func _ProxyService_ValidateTunnelPeer_Handler(srv interface{}, ctx context.Conte
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ProxyService_CheckLLMPolicyLimits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CheckLLMPolicyLimitsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ProxyServiceServer).CheckLLMPolicyLimits(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/management.ProxyService/CheckLLMPolicyLimits",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ProxyServiceServer).CheckLLMPolicyLimits(ctx, req.(*CheckLLMPolicyLimitsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ProxyService_RecordLLMUsage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RecordLLMUsageRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ProxyServiceServer).RecordLLMUsage(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/management.ProxyService/RecordLLMUsage",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ProxyServiceServer).RecordLLMUsage(ctx, req.(*RecordLLMUsageRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ProxyService_ServiceDesc is the grpc.ServiceDesc for ProxyService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -463,6 +543,14 @@ var ProxyService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ValidateTunnelPeer",
|
||||
Handler: _ProxyService_ValidateTunnelPeer_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CheckLLMPolicyLimits",
|
||||
Handler: _ProxyService_CheckLLMPolicyLimits_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RecordLLMUsage",
|
||||
Handler: _ProxyService_RecordLLMUsage_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -48,6 +48,10 @@ type Type int32
|
||||
var (
|
||||
ErrExtraSettingsNotFound = errors.New("extra settings not found")
|
||||
ErrPeerAlreadyLoggedIn = errors.New("peer with the same public key is already logged in")
|
||||
|
||||
// ErrNoAuthMethodProvided is returned when a peer login attempt carries neither a
|
||||
// setup key nor an SSO token. Match it with errors.Is.
|
||||
ErrNoAuthMethodProvided = Errorf(Unauthenticated, "no peer auth method provided, please use a setup key or interactive SSO login")
|
||||
)
|
||||
|
||||
// Error is an internal error
|
||||
@@ -66,6 +70,16 @@ func (e *Error) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// Is reports whether target is an *Error with the same type and message,
|
||||
// enabling matching with errors.Is against sentinel errors.
|
||||
func (e *Error) Is(target error) bool {
|
||||
var t *Error
|
||||
if !errors.As(target, &t) {
|
||||
return false
|
||||
}
|
||||
return e.ErrorType == t.ErrorType && e.Message == t.Message
|
||||
}
|
||||
|
||||
// Errorf returns Error(ErrorType, fmt.Sprintf(format, a...)).
|
||||
func Errorf(errorType Type, format string, a ...interface{}) error {
|
||||
return &Error{
|
||||
@@ -205,6 +219,26 @@ func NewNetworkResourceNotFoundError(resourceID string) error {
|
||||
return Errorf(NotFound, "network resource: %s not found", resourceID)
|
||||
}
|
||||
|
||||
// NewAgentNetworkProviderNotFoundError creates a new Error with NotFound type for a missing Agent Network provider.
|
||||
func NewAgentNetworkProviderNotFoundError(providerID string) error {
|
||||
return Errorf(NotFound, "agent network provider: %s not found", providerID)
|
||||
}
|
||||
|
||||
// NewAgentNetworkPolicyNotFoundError creates a new Error with NotFound type for a missing Agent Network policy.
|
||||
func NewAgentNetworkPolicyNotFoundError(policyID string) error {
|
||||
return Errorf(NotFound, "agent network policy: %s not found", policyID)
|
||||
}
|
||||
|
||||
// NewAgentNetworkGuardrailNotFoundError creates a new Error with NotFound type for a missing Agent Network guardrail.
|
||||
func NewAgentNetworkGuardrailNotFoundError(guardrailID string) error {
|
||||
return Errorf(NotFound, "agent network guardrail: %s not found", guardrailID)
|
||||
}
|
||||
|
||||
// NewAgentNetworkBudgetRuleNotFoundError creates a new Error with NotFound type for a missing Agent Network budget rule.
|
||||
func NewAgentNetworkBudgetRuleNotFoundError(ruleID string) error {
|
||||
return Errorf(NotFound, "agent network budget rule: %s not found", ruleID)
|
||||
}
|
||||
|
||||
// NewPermissionDeniedError creates a new Error with PermissionDenied type for a permission denied error.
|
||||
func NewPermissionDeniedError() error {
|
||||
return Errorf(PermissionDenied, "permission denied")
|
||||
|
||||
@@ -33,7 +33,7 @@ type Client interface {
|
||||
Receive(ctx context.Context, msgHandler func(msg *proto.Message) error) error
|
||||
Ready() bool
|
||||
IsHealthy() bool
|
||||
WaitStreamConnected()
|
||||
WaitStreamConnected(context.Context)
|
||||
SendToStream(msg *proto.EncryptedMessage) error
|
||||
Send(msg *proto.Message) error
|
||||
SetOnReconnectedListener(func())
|
||||
|
||||
@@ -65,7 +65,10 @@ var _ = Describe("GrpcClient", func() {
|
||||
return
|
||||
}
|
||||
}()
|
||||
clientA.WaitStreamConnected()
|
||||
ctxA, cancelA := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelA()
|
||||
clientA.WaitStreamConnected(ctxA)
|
||||
Expect(clientA.StreamConnected()).To(BeTrue())
|
||||
|
||||
// connect PeerB to Signal
|
||||
keyB, _ := wgtypes.GenerateKey()
|
||||
@@ -91,7 +94,10 @@ var _ = Describe("GrpcClient", func() {
|
||||
}
|
||||
}()
|
||||
|
||||
clientB.WaitStreamConnected()
|
||||
ctxB, cancelB := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelB()
|
||||
clientB.WaitStreamConnected(ctxB)
|
||||
Expect(clientB.StreamConnected()).To(BeTrue())
|
||||
|
||||
// PeerA initiates ping-pong
|
||||
err := clientA.Send(&sigProto.Message{
|
||||
@@ -129,8 +135,10 @@ var _ = Describe("GrpcClient", func() {
|
||||
return
|
||||
}
|
||||
}()
|
||||
client.WaitStreamConnected()
|
||||
Expect(client).NotTo(BeNil())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
client.WaitStreamConnected(ctx)
|
||||
Expect(client.StreamConnected()).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -78,6 +78,14 @@ type GrpcClient struct {
|
||||
// transport-alive but no longer delivering messages. It is the source of
|
||||
// truth IsHealthy reads, and is cleared once any frame is received again.
|
||||
receiveStalled atomic.Bool
|
||||
// receiveHandoffBlocked is set while the receive loop is parked handing a
|
||||
// message to a busy decryption worker. The loop stops calling Recv (and
|
||||
// markReceived) in that window, so the stream looks silent though it is
|
||||
// healthy. The watchdog reads this to avoid misreading self-inflicted
|
||||
// receive backpressure as a dead stream: reconnecting cannot help, since the
|
||||
// new stream feeds the same worker, and only triggers a reconnect storm.
|
||||
receiveHandoffBlocked atomic.Bool
|
||||
watchdogWg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewClient creates a new Signal client
|
||||
@@ -193,10 +201,18 @@ func (c *GrpcClient) Receive(ctx context.Context, msgHandler func(msg *proto.Mes
|
||||
// Guard the receive direction: the transport can stay healthy while the
|
||||
// server stops delivering messages. The watchdog reconnects via cancelStream.
|
||||
c.markReceived()
|
||||
go c.watchReceiveStream(streamCtx, cancelStream)
|
||||
c.watchdogWg.Add(1)
|
||||
go func() {
|
||||
defer c.watchdogWg.Done()
|
||||
c.watchReceiveStream(streamCtx, cancelStream)
|
||||
}()
|
||||
|
||||
// start receiving messages from the Signal stream (from other peers through signal)
|
||||
err = c.receive(stream)
|
||||
|
||||
cancelStream()
|
||||
c.watchdogWg.Wait()
|
||||
|
||||
if err != nil {
|
||||
// Check the parent context, not streamCtx: a watchdog-triggered
|
||||
// cancelStream must reconnect, only a parent cancel is shutdown.
|
||||
@@ -246,15 +262,6 @@ func (c *GrpcClient) notifyStreamConnected() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GrpcClient) getStreamStatusChan() <-chan struct{} {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
if c.connectedCh == nil {
|
||||
c.connectedCh = make(chan struct{})
|
||||
}
|
||||
return c.connectedCh
|
||||
}
|
||||
|
||||
func (c *GrpcClient) connect(ctx context.Context, key string) (proto.SignalExchange_ConnectStreamClient, error) {
|
||||
c.stream = nil
|
||||
|
||||
@@ -310,14 +317,24 @@ func (c *GrpcClient) IsHealthy() bool {
|
||||
}
|
||||
|
||||
// WaitStreamConnected waits until the client is connected to the Signal stream
|
||||
func (c *GrpcClient) WaitStreamConnected() {
|
||||
|
||||
func (c *GrpcClient) WaitStreamConnected(ctx context.Context) {
|
||||
// Check the status and obtain the wait channel atomically: otherwise
|
||||
// notifyStreamConnected could flip the status and close/clear the channel
|
||||
// between the check and the channel creation, leaving us waiting forever on
|
||||
// a stale channel.
|
||||
c.mux.Lock()
|
||||
if c.status == StreamConnected {
|
||||
c.mux.Unlock()
|
||||
return
|
||||
}
|
||||
if c.connectedCh == nil {
|
||||
c.connectedCh = make(chan struct{})
|
||||
}
|
||||
ch := c.connectedCh
|
||||
c.mux.Unlock()
|
||||
|
||||
ch := c.getStreamStatusChan()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-c.ctx.Done():
|
||||
case <-ch:
|
||||
}
|
||||
@@ -392,7 +409,12 @@ func (c *GrpcClient) encryptMessage(msg *proto.Message) (*proto.EncryptedMessage
|
||||
|
||||
// Send sends a message to the remote Peer through the Signal Exchange.
|
||||
func (c *GrpcClient) Send(msg *proto.Message) error {
|
||||
return c.send(c.ctx, msg)
|
||||
}
|
||||
|
||||
// send delivers a message deriving per-attempt timeouts from parentCtx, so a
|
||||
// caller can abort an in-flight send by cancelling that context.
|
||||
func (c *GrpcClient) send(parentCtx context.Context, msg *proto.Message) error {
|
||||
if !c.Ready() {
|
||||
return fmt.Errorf("no connection to signal")
|
||||
}
|
||||
@@ -408,7 +430,7 @@ func (c *GrpcClient) Send(msg *proto.Message) error {
|
||||
if attempt > 1 {
|
||||
attemptTimeout = time.Duration(attempt) * 5 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(c.ctx, attemptTimeout)
|
||||
ctx, cancel := context.WithTimeout(parentCtx, attemptTimeout)
|
||||
|
||||
_, err = c.realClient.Send(ctx, encryptedMessage)
|
||||
|
||||
@@ -438,6 +460,16 @@ func (c *GrpcClient) idleSinceReceive() time.Duration {
|
||||
return time.Since(time.Unix(0, c.lastReceived.Load()))
|
||||
}
|
||||
|
||||
// receiveAlive reports whether the receive stream shows liveness: it delivered a
|
||||
// frame within the inactivity threshold, or the receive loop is currently parked
|
||||
// handing a message to a busy decryption worker. In the latter case the loop has
|
||||
// stopped calling Recv, so the stream looks silent while being healthy, and
|
||||
// reconnecting would not help, so the watchdog must treat it as alive.
|
||||
func (c *GrpcClient) receiveAlive() bool {
|
||||
return c.idleSinceReceive() < receiveInactivityThreshold ||
|
||||
c.receiveHandoffBlocked.Load()
|
||||
}
|
||||
|
||||
// watchReceiveStream guards against a receive stream that is transport-alive but
|
||||
// no longer delivering messages. While the stream is idle past
|
||||
// receiveInactivityThreshold it sends a self-addressed probe that the Signal
|
||||
@@ -454,7 +486,7 @@ func (c *GrpcClient) watchReceiveStream(ctx context.Context, cancelStream contex
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if c.idleSinceReceive() < receiveInactivityThreshold {
|
||||
if c.receiveAlive() {
|
||||
probeSentAt = time.Time{}
|
||||
continue
|
||||
}
|
||||
@@ -468,7 +500,7 @@ func (c *GrpcClient) watchReceiveStream(ctx context.Context, cancelStream contex
|
||||
}
|
||||
|
||||
if probeSentAt.IsZero() {
|
||||
if err := c.sendReceiveProbe(); err != nil {
|
||||
if err := c.sendReceiveProbe(ctx); err != nil {
|
||||
log.Debugf("failed to send signal receive probe: %v", err)
|
||||
}
|
||||
probeSentAt = time.Now()
|
||||
@@ -477,11 +509,13 @@ func (c *GrpcClient) watchReceiveStream(ctx context.Context, cancelStream contex
|
||||
}
|
||||
}
|
||||
|
||||
// sendReceiveProbe sends a self-addressed heartbeat. The Signal server routes it
|
||||
// back to this client, exercising the exact receive path the watchdog guards.
|
||||
func (c *GrpcClient) sendReceiveProbe() error {
|
||||
// sendReceiveProbe sends a self-addressed heartbeat bound to ctx, so cancelStream
|
||||
// aborts an in-flight probe instead of leaving the watchdog blocked on send timeouts.
|
||||
// The Signal server routes it back to this client, exercising the exact receive
|
||||
// path the watchdog guards.
|
||||
func (c *GrpcClient) sendReceiveProbe(ctx context.Context) error {
|
||||
self := c.key.PublicKey().String()
|
||||
return c.Send(&proto.Message{
|
||||
return c.send(ctx, &proto.Message{
|
||||
Key: self,
|
||||
RemoteKey: self,
|
||||
Body: &proto.Body{Type: proto.Body_HEARTBEAT},
|
||||
@@ -516,9 +550,17 @@ func (c *GrpcClient) receive(stream proto.SignalExchange_ConnectStreamClient) er
|
||||
continue
|
||||
}
|
||||
|
||||
// The handoff blocks while the worker is busy, which parks this loop and
|
||||
// stops Recv. Flag it so the watchdog does not read the resulting silence
|
||||
// as a dead stream.
|
||||
c.receiveHandoffBlocked.Store(true)
|
||||
if err := c.decryptionWorker.AddMsg(c.ctx, msg); err != nil {
|
||||
log.Errorf("failed to add message to decryption worker: %v", err)
|
||||
}
|
||||
// Refresh liveness before clearing the flag so the window between here and
|
||||
// the next Recv does not read a stale timestamp as a dead stream.
|
||||
c.markReceived()
|
||||
c.receiveHandoffBlocked.Store(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ func (sm *MockClient) Ready() bool {
|
||||
return sm.ReadyFunc()
|
||||
}
|
||||
|
||||
func (sm *MockClient) WaitStreamConnected() {
|
||||
func (sm *MockClient) WaitStreamConnected(context.Context) {
|
||||
if sm.WaitStreamConnectedFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -65,7 +66,7 @@ func TestReceiveProbeRoundTrips(t *testing.T) {
|
||||
|
||||
streamReady := make(chan struct{})
|
||||
go func() {
|
||||
client.WaitStreamConnected()
|
||||
client.WaitStreamConnected(ctx)
|
||||
close(streamReady)
|
||||
}()
|
||||
select {
|
||||
@@ -74,7 +75,7 @@ func TestReceiveProbeRoundTrips(t *testing.T) {
|
||||
t.Fatal("signal stream did not connect within timeout")
|
||||
}
|
||||
|
||||
require.NoError(t, client.sendReceiveProbe())
|
||||
require.NoError(t, client.sendReceiveProbe(ctx))
|
||||
|
||||
select {
|
||||
case <-received:
|
||||
@@ -82,3 +83,96 @@ func TestReceiveProbeRoundTrips(t *testing.T) {
|
||||
t.Fatal("self-addressed heartbeat did not round-trip back through the signal server")
|
||||
}
|
||||
}
|
||||
|
||||
// TestReceiveAliveTreatsHandoffBlockAsLiveness reproduces the false positive
|
||||
// where a busy decryption worker parks the receive loop on the worker handoff,
|
||||
// so Recv (and markReceived) stops firing even though the stream is healthy.
|
||||
// With the receive stream silent past the inactivity threshold but the loop
|
||||
// blocked on handoff, the watchdog must consider the stream alive rather than
|
||||
// tear it down (reconnecting feeds the same worker and would not help).
|
||||
func TestReceiveAliveTreatsHandoffBlockAsLiveness(t *testing.T) {
|
||||
c := &GrpcClient{}
|
||||
|
||||
// Receive stream silent and the loop not blocked on handoff: genuinely stalled.
|
||||
c.lastReceived.Store(time.Now().Add(-2 * receiveInactivityThreshold).UnixNano())
|
||||
require.False(t, c.receiveAlive(), "silent stream with the receive loop idle must be treated as stalled")
|
||||
|
||||
// Receive stream silent but the loop is parked handing a message to a busy
|
||||
// worker: self-inflicted backpressure, not a dead stream, must not tear down.
|
||||
c.receiveHandoffBlocked.Store(true)
|
||||
require.True(t, c.receiveAlive(), "a receive loop blocked on worker handoff must keep the stream alive")
|
||||
|
||||
// Handoff drained, loop back to reading, a frame just arrived: alive via the receive path.
|
||||
c.receiveHandoffBlocked.Store(false)
|
||||
c.markReceived()
|
||||
require.True(t, c.receiveAlive(), "a freshly received frame must keep the stream alive")
|
||||
}
|
||||
|
||||
// fakeRecvStream feeds the receive loop frames from a channel and reports EOF
|
||||
// once the channel is closed. Only Recv is exercised by the loop.
|
||||
type fakeRecvStream struct {
|
||||
sigProto.SignalExchange_ConnectStreamClient
|
||||
frames chan *sigProto.EncryptedMessage
|
||||
}
|
||||
|
||||
func (s *fakeRecvStream) Recv() (*sigProto.EncryptedMessage, error) {
|
||||
msg, ok := <-s.frames
|
||||
if !ok {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// TestReceiveLoopRefreshesLivenessAfterBlockedHandoff drives the real receive
|
||||
// loop into a handoff that blocks past the inactivity threshold, then checks the
|
||||
// window after the handoff drains but before the next Recv. The loop must have
|
||||
// refreshed the timestamp on unblocking, otherwise that window reads the stale
|
||||
// pre-handoff timestamp as a dead stream and the watchdog tears down a healthy
|
||||
// connection.
|
||||
func TestReceiveLoopRefreshesLivenessAfterBlockedHandoff(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
c := &GrpcClient{ctx: ctx}
|
||||
|
||||
handling := make(chan struct{}, 8)
|
||||
gate := make(chan struct{})
|
||||
decrypt := func(*sigProto.EncryptedMessage) (*sigProto.Message, error) { return &sigProto.Message{}, nil }
|
||||
handler := func(*sigProto.Message) error {
|
||||
handling <- struct{}{}
|
||||
<-gate
|
||||
return nil
|
||||
}
|
||||
c.decryptionWorker = NewWorker(decrypt, handler)
|
||||
workerCtx, workerCancel := context.WithCancel(context.Background())
|
||||
go c.decryptionWorker.Work(workerCtx)
|
||||
t.Cleanup(workerCancel)
|
||||
|
||||
frames := make(chan *sigProto.EncryptedMessage)
|
||||
t.Cleanup(func() { close(frames) })
|
||||
go func() { _ = c.receive(&fakeRecvStream{frames: frames}) }()
|
||||
|
||||
// First frame: the worker drains it and parks in the blocking handler.
|
||||
frames <- &sigProto.EncryptedMessage{}
|
||||
<-handling
|
||||
// Second frame fills the worker's single-slot pool.
|
||||
frames <- &sigProto.EncryptedMessage{}
|
||||
// Third frame: the pool is full, so the loop parks on the handoff.
|
||||
frames <- &sigProto.EncryptedMessage{}
|
||||
|
||||
require.Eventually(t, c.receiveHandoffBlocked.Load, time.Second, time.Millisecond,
|
||||
"receive loop should park on the worker handoff")
|
||||
|
||||
// Simulate the handoff having blocked past the inactivity threshold.
|
||||
c.lastReceived.Store(time.Now().Add(-2 * receiveInactivityThreshold).UnixNano())
|
||||
require.True(t, c.receiveAlive(), "a loop parked on the handoff must stay alive")
|
||||
|
||||
// Drain the worker so the handoff returns and the loop resumes reading.
|
||||
close(gate)
|
||||
|
||||
// Once the handoff clears, the loop is parked on the next Recv with no frame
|
||||
// pending. The stream must still read as alive in that window.
|
||||
require.Eventually(t, func() bool { return !c.receiveHandoffBlocked.Load() }, time.Second, time.Millisecond,
|
||||
"handoff should drain once the worker is released")
|
||||
require.True(t, c.receiveAlive(),
|
||||
"the loop must refresh liveness when the handoff drains, before the next Recv")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user