[PR #6905] [MERGED] [management, proxy] scope agent-network model allowlist per policy/group and provider #30015

Closed
opened 2026-08-05 08:09:41 -04:00 by saavagebueno · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/netbirdio/netbird/pull/6905
Author: @mlsmaycon
Created: 7/26/2026
Status: Merged
Merged: 7/27/2026
Merged by: @braginini

Base: mainHead: agent-network-per-policy-model-allowlist


📝 Commits (10+)

  • c48347c [e2e] guardrail blocks the unselected model for path-routed providers
  • 66e48bc agentnetwork: scope model allowlist per policy/group and provider
  • 886d5a6 ci: temporarily run agent-network e2e on push to this branch
  • 9266453 agentnetwork: drop dead MergedGuardrails token/budget/retention fields
  • 255c87c agentnetwork: tighten comment blocks to <=250 chars
  • 244ca3f agentnetwork: cover within-policy allowlist-guardrail union
  • a620b49 agentnetwork: fix staticcheck S1016 in marshalGuardrailConfig
  • 692257a agentnetwork: cover enabled-but-empty allowlist denies all
  • ef174b1 agentnetwork: fold in extra unit tests from review
  • bc0b9b0 Merge branch 'main' into agent-network-per-policy-model-allowlist

📊 Changes

25 files changed (+2148 additions, -154 deletions)

View changed files

📝 docs/agent-networks/01-end-to-end-flows.md (+16 -1)
📝 docs/agent-networks/modules/21-management-agentnetwork.md (+1 -1)
📝 docs/agent-networks/modules/31-proxy-middleware-builtin.md (+1 -1)
e2e/agentnetwork/guardrail_block_test.go (+209 -0)
e2e/agentnetwork/guardrail_groupswitch_test.go (+205 -0)
e2e/agentnetwork/guardrail_multipolicy_test.go (+201 -0)
e2e/agentnetwork/guardrail_pergroup_providers_test.go (+422 -0)
📝 e2e/harness/proxy.go (+11 -1)
📝 management/internals/modules/agentnetwork/manager.go (+6 -1)
📝 management/internals/modules/agentnetwork/policyselect.go (+108 -0)
management/internals/modules/agentnetwork/policyselect_model_test.go (+329 -0)
📝 management/internals/modules/agentnetwork/synthesizer.go (+97 -85)
📝 management/internals/modules/agentnetwork/synthesizer_guardrail_realstore_test.go (+3 -3)
management/internals/modules/agentnetwork/synthesizer_provider_allowlist_test.go (+95 -0)
📝 management/internals/modules/agentnetwork/synthesizer_test.go (+6 -2)
📝 management/internals/shared/grpc/proxy.go (+1 -0)
management/internals/shared/grpc/proxy_llm_policy_limits_test.go (+138 -0)
📝 proxy/internal/auth/tunnel_cache.go (+34 -5)
📝 proxy/internal/auth/tunnel_cache_test.go (+29 -0)
📝 proxy/internal/middleware/builtin/llm_guardrail/factory.go (+24 -13)

...and 5 more files

📄 Description

Describe your changes

The model-allowlist guardrail was merged into one account-wide union and enforced flat on every request, ignoring which policy/group/provider authorised it. With multiple policies — especially a mix of guardrailed and un-guardrailed ones — this caused:

  • false-allow: a model allowlisted for one group/provider leaked to any caller; and
  • false-deny: an un-guardrailed policy (intended unrestricted) was blocked by another policy's allowlist.

Enforcement is now policy/group-aware, mirroring llm_limit_check:

  • Management (SelectPolicyForRequest) is authoritative. It uses the request model (already carried in CheckLLMPolicyLimitsRequest.model, previously ignored) to keep only applicable policies whose guardrails permit the model; no allowlist-enabled guardrail = unrestricted. Denies llm_policy.model_blocked when policies govern the (provider, groups) but none permits the model.
  • Proxy llm_guardrail becomes a per-provider fail-closed backstop. The synthesiser emits an allowlist only for providers every authorising policy restricts; the middleware keys off the resolved provider id and keeps unknown-model fail-closed.

No proto change (the field already existed and was already sent).

N/A — from a customer report that a policy allowlist "has no effect" with multiple policies.

Stack

Stacked on e2e-guardrail-blocks-unselected-model.

Checklist

  • Is it a bug fix
  • Created tests that fail without the change

Note: changes enforcement behaviour (the fix) and uses an existing gRPC field; no wire/schema change.

Documentation

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Summary by CodeRabbit

  • New Features

    • Model allowlists are now scoped per provider, preventing restrictions from leaking across providers, groups, or policies.
    • Requests with unknown models are blocked when restrictions apply, using clearer model-specific denial messages.
    • Allowlist changes follow group membership updates after the tunnel cache refreshes.
    • Tunnel cache duration can now be configured through an environment setting.
  • Documentation

    • Updated configuration and request-flow documentation to describe provider-specific allowlists and fail-closed behavior.
  • Tests

    • Added coverage for multi-provider, multi-group, policy, normalization, and cache-refresh scenarios.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/netbirdio/netbird/pull/6905 **Author:** [@mlsmaycon](https://github.com/mlsmaycon) **Created:** 7/26/2026 **Status:** ✅ Merged **Merged:** 7/27/2026 **Merged by:** [@braginini](https://github.com/braginini) **Base:** `main` ← **Head:** `agent-network-per-policy-model-allowlist` --- ### 📝 Commits (10+) - [`c48347c`](https://github.com/netbirdio/netbird/commit/c48347c51e193d484a5a7e656b7623b938e91057) [e2e] guardrail blocks the unselected model for path-routed providers - [`66e48bc`](https://github.com/netbirdio/netbird/commit/66e48bc933aae53bb2e49a79f9614bf604e2b13e) agentnetwork: scope model allowlist per policy/group and provider - [`886d5a6`](https://github.com/netbirdio/netbird/commit/886d5a654b6423a3799a2d5c5946809c8badb0a6) ci: temporarily run agent-network e2e on push to this branch - [`9266453`](https://github.com/netbirdio/netbird/commit/92664532c5f6f6d298aba3bd386a48bcb548f762) agentnetwork: drop dead MergedGuardrails token/budget/retention fields - [`255c87c`](https://github.com/netbirdio/netbird/commit/255c87cc97709b1cf57bd39f1525e623dc274e9d) agentnetwork: tighten comment blocks to <=250 chars - [`244ca3f`](https://github.com/netbirdio/netbird/commit/244ca3f671e134293d9ad066a9a36c86f4799b94) agentnetwork: cover within-policy allowlist-guardrail union - [`a620b49`](https://github.com/netbirdio/netbird/commit/a620b497def53126ad578e7e680ae515bb3363c4) agentnetwork: fix staticcheck S1016 in marshalGuardrailConfig - [`692257a`](https://github.com/netbirdio/netbird/commit/692257a3ebd2a491c37265a767fe2eb046273849) agentnetwork: cover enabled-but-empty allowlist denies all - [`ef174b1`](https://github.com/netbirdio/netbird/commit/ef174b19e51678e14b2dac8663025e15acedd45d) agentnetwork: fold in extra unit tests from review - [`bc0b9b0`](https://github.com/netbirdio/netbird/commit/bc0b9b053dd8ef95c00ccc42cca153e3299448c4) Merge branch 'main' into agent-network-per-policy-model-allowlist ### 📊 Changes **25 files changed** (+2148 additions, -154 deletions) <details> <summary>View changed files</summary> 📝 `docs/agent-networks/01-end-to-end-flows.md` (+16 -1) 📝 `docs/agent-networks/modules/21-management-agentnetwork.md` (+1 -1) 📝 `docs/agent-networks/modules/31-proxy-middleware-builtin.md` (+1 -1) ➕ `e2e/agentnetwork/guardrail_block_test.go` (+209 -0) ➕ `e2e/agentnetwork/guardrail_groupswitch_test.go` (+205 -0) ➕ `e2e/agentnetwork/guardrail_multipolicy_test.go` (+201 -0) ➕ `e2e/agentnetwork/guardrail_pergroup_providers_test.go` (+422 -0) 📝 `e2e/harness/proxy.go` (+11 -1) 📝 `management/internals/modules/agentnetwork/manager.go` (+6 -1) 📝 `management/internals/modules/agentnetwork/policyselect.go` (+108 -0) ➕ `management/internals/modules/agentnetwork/policyselect_model_test.go` (+329 -0) 📝 `management/internals/modules/agentnetwork/synthesizer.go` (+97 -85) 📝 `management/internals/modules/agentnetwork/synthesizer_guardrail_realstore_test.go` (+3 -3) ➕ `management/internals/modules/agentnetwork/synthesizer_provider_allowlist_test.go` (+95 -0) 📝 `management/internals/modules/agentnetwork/synthesizer_test.go` (+6 -2) 📝 `management/internals/shared/grpc/proxy.go` (+1 -0) ➕ `management/internals/shared/grpc/proxy_llm_policy_limits_test.go` (+138 -0) 📝 `proxy/internal/auth/tunnel_cache.go` (+34 -5) 📝 `proxy/internal/auth/tunnel_cache_test.go` (+29 -0) 📝 `proxy/internal/middleware/builtin/llm_guardrail/factory.go` (+24 -13) _...and 5 more files_ </details> ### 📄 Description ## Describe your changes The model-allowlist guardrail was merged into one account-wide union and enforced flat on every request, ignoring which policy/group/provider authorised it. With multiple policies — especially a mix of guardrailed and un-guardrailed ones — this caused: - **false-allow**: a model allowlisted for one group/provider leaked to any caller; and - **false-deny**: an un-guardrailed policy (intended unrestricted) was blocked by another policy's allowlist. Enforcement is now policy/group-aware, mirroring `llm_limit_check`: - **Management (`SelectPolicyForRequest`) is authoritative.** It uses the request model (already carried in `CheckLLMPolicyLimitsRequest.model`, previously ignored) to keep only applicable policies whose guardrails permit the model; no allowlist-enabled guardrail = unrestricted. Denies `llm_policy.model_blocked` when policies govern the (provider, groups) but none permits the model. - **Proxy `llm_guardrail` becomes a per-provider fail-closed backstop.** The synthesiser emits an allowlist only for providers every authorising policy restricts; the middleware keys off the resolved provider id and keeps unknown-model fail-closed. No proto change (the field already existed and was already sent). ## Issue ticket number and link N/A — from a customer report that a policy allowlist "has no effect" with multiple policies. ## Stack Stacked on `e2e-guardrail-blocks-unselected-model`. ### Checklist - [x] Is it a bug fix - [x] Created tests that fail without the change Note: changes enforcement behaviour (the fix) and uses an existing gRPC field; no wire/schema change. ## Documentation - [ ] I added/updated documentation for this change - [x] Documentation is **not needed** for this change (explain why) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Model allowlists are now scoped per provider, preventing restrictions from leaking across providers, groups, or policies. * Requests with unknown models are blocked when restrictions apply, using clearer model-specific denial messages. * Allowlist changes follow group membership updates after the tunnel cache refreshes. * Tunnel cache duration can now be configured through an environment setting. * **Documentation** * Updated configuration and request-flow documentation to describe provider-specific allowlists and fail-closed behavior. * **Tests** * Added coverage for multi-provider, multi-group, policy, normalization, and cache-refresh scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
saavagebueno added the pull-request label 2026-08-05 08:09:41 -04:00
Sign in to join this conversation.
No Label pull-request
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#30015