diff --git a/management/internals/modules/agentnetwork/setup.go b/management/internals/modules/agentnetwork/agent_config.go similarity index 86% rename from management/internals/modules/agentnetwork/setup.go rename to management/internals/modules/agentnetwork/agent_config.go index 3139a030d..5571fd159 100644 --- a/management/internals/modules/agentnetwork/setup.go +++ b/management/internals/modules/agentnetwork/agent_config.go @@ -9,7 +9,7 @@ import ( "github.com/netbirdio/netbird/management/server/store" ) -// GetSetupForUser returns the Agent Network setup the calling user's +// GetAgentConfigForUser returns the Agent Network setup the calling user's // groups authorize. It deliberately performs no role permission check: // the result is scoped to the caller's own groups, which is strictly // tighter than any role gate, so every authenticated user (any role) may @@ -18,15 +18,15 @@ import ( // session validation resolves them from the same user record's // auto-groups — so this answer and the proxy's verdict are computed from // the same memberships. -func (m *managerImpl) GetSetupForUser(ctx context.Context, accountID, userID string) (*types.EffectiveSetup, error) { +func (m *managerImpl) GetAgentConfigForUser(ctx context.Context, accountID, userID string) (*types.AgentConfig, error) { user, err := m.store.GetUserByUserID(ctx, store.LockingStrengthNone, userID) if err != nil { return nil, fmt.Errorf("get user: %w", err) } - return m.effectiveSetupForGroups(ctx, accountID, user.AutoGroups) + return m.agentConfigForGroups(ctx, accountID, user.AutoGroups) } -// effectiveSetupForGroups computes the effective Agent Network setup for +// agentConfigForGroups computes the effective Agent Network setup for // a set of caller groups: the account endpoint plus, per authorized // provider, the effective model set. It mirrors what the proxy enforces // at request time — the policy filter matches filterApplicablePolicies, @@ -35,12 +35,16 @@ func (m *managerImpl) GetSetupForUser(ctx context.Context, accountID, userID str // the router synthesizer omits them — so the answer never advertises // anything the proxy would refuse. // -// Every "nothing available" shape returns Configured=false rather than -// an error, and "account not set up" is indistinguishable from "caller -// has no access" by design: the response must not leak what exists for -// others. -func (m *managerImpl) effectiveSetupForGroups(ctx context.Context, accountID string, groupIDs []string) (*types.EffectiveSetup, error) { - notConfigured := &types.EffectiveSetup{Providers: []types.EffectiveProvider{}} +// Configured tracks the account, not the caller: once the account has an +// endpoint every member gets it, with Providers empty for those no policy +// covers yet. The dashboard shows each user the same connection config +// regardless of role, and an empty provider list tells them to ask for +// access. Only the account having no Agent Network at all reads as not +// configured. Providers stays caller-scoped either way — the endpoint on +// its own authorizes nothing, and the proxy still refuses every request +// no policy permits. +func (m *managerImpl) agentConfigForGroups(ctx context.Context, accountID string, groupIDs []string) (*types.AgentConfig, error) { + notConfigured := &types.AgentConfig{Providers: []types.AgentConfigProvider{}} settings, err := m.store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, accountID) switch { @@ -58,8 +62,14 @@ func (m *managerImpl) effectiveSetupForGroups(ctx context.Context, accountID str if err != nil { return nil, err } + + out := &types.AgentConfig{ + Configured: true, + Endpoint: "https://" + settings.Endpoint(), + Providers: make([]types.AgentConfigProvider, 0, len(authorized)), + } if len(authorized) == 0 { - return notConfigured, nil + return out, nil } var guardrailsByID map[string]*types.Guardrail @@ -69,19 +79,13 @@ func (m *managerImpl) effectiveSetupForGroups(ctx context.Context, accountID str return nil, err } } - - out := &types.EffectiveSetup{ - Configured: true, - Endpoint: "https://" + settings.Endpoint(), - Providers: make([]types.EffectiveProvider, 0, len(authorized)), - } for _, p := range authorized { allAllowed, models := effectiveModelsForProvider(p, policiesForProvider(applicable, p.ID), guardrailsByID) flavor := "" if entry, ok := catalog.Lookup(p.ProviderID); ok { flavor = entry.ParserID } - out.Providers = append(out.Providers, types.EffectiveProvider{ + out.Providers = append(out.Providers, types.AgentConfigProvider{ Name: p.Name, CatalogID: p.ProviderID, APIFlavor: flavor, @@ -276,8 +280,8 @@ func declaredModelIDs(provider *types.Provider) []string { return out } -// GetSetupForUser on the mock manager reports "not configured" so tests +// GetAgentConfigForUser on the mock manager reports "not configured" so tests // that don't care about setup still compile. -func (*mockManager) GetSetupForUser(_ context.Context, _, _ string) (*types.EffectiveSetup, error) { - return &types.EffectiveSetup{Providers: []types.EffectiveProvider{}}, nil +func (*mockManager) GetAgentConfigForUser(_ context.Context, _, _ string) (*types.AgentConfig, error) { + return &types.AgentConfig{Providers: []types.AgentConfigProvider{}}, nil } diff --git a/management/internals/modules/agentnetwork/setup_realstore_test.go b/management/internals/modules/agentnetwork/agent_config_realstore_test.go similarity index 78% rename from management/internals/modules/agentnetwork/setup_realstore_test.go rename to management/internals/modules/agentnetwork/agent_config_realstore_test.go index 03540a444..9a66e1190 100644 --- a/management/internals/modules/agentnetwork/setup_realstore_test.go +++ b/management/internals/modules/agentnetwork/agent_config_realstore_test.go @@ -23,7 +23,7 @@ import ( // model logic matches policyPermitsModel, and orphan providers are // omitted like the router synthesizer omits them. -func newSetupTestMgr(t *testing.T) (*managerImpl, store.Store) { +func newAgentConfigTestMgr(t *testing.T) (*managerImpl, store.Store) { t.Helper() ctx := context.Background() s, cleanup, err := store.NewTestStoreFromSQL(ctx, "", t.TempDir()) @@ -44,18 +44,18 @@ func newSetupTestGuardrail(id string, models ...string) *types.Guardrail { } } -func TestEffectiveSetup_RealStore_NoSettingsRow(t *testing.T) { - mgr, _ := newSetupTestMgr(t) +func TestAgentConfig_RealStore_NoSettingsRow(t *testing.T) { + mgr, _ := newAgentConfigTestMgr(t) - setup, err := mgr.effectiveSetupForGroups(context.Background(), testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(context.Background(), testAccountID, []string{"grp-eng"}) require.NoError(t, err) assert.False(t, setup.Configured, "account without settings must read as not configured") assert.Empty(t, setup.Endpoint) assert.Empty(t, setup.Providers) } -func TestEffectiveSetup_RealStore_NoApplicablePolicy(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_NoApplicablePolicy(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -63,15 +63,15 @@ func TestEffectiveSetup_RealStore_NoApplicablePolicy(t *testing.T) { require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider)) require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", ""))) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-other"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-other"}) require.NoError(t, err) - assert.False(t, setup.Configured, "caller outside every policy's source groups must read as not configured") - assert.Empty(t, setup.Endpoint, "no-access answer must not leak the endpoint") - assert.Empty(t, setup.Providers) + assert.True(t, setup.Configured, "the account is set up, so every member reads as configured") + assert.Equal(t, "https://"+testEndpoint, setup.Endpoint, "every member gets the same connection config") + assert.Empty(t, setup.Providers, "a caller no policy covers is authorized for nothing") } -func TestEffectiveSetup_RealStore_UnrestrictedPolicyListsDeclaredModels(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_UnrestrictedPolicyListsDeclaredModels(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -79,7 +79,7 @@ func TestEffectiveSetup_RealStore_UnrestrictedPolicyListsDeclaredModels(t *testi require.NoError(t, s.SaveAgentNetworkProvider(ctx, provider)) require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", ""))) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-eng"}) require.NoError(t, err) assert.True(t, setup.Configured) assert.Equal(t, "https://"+testEndpoint, setup.Endpoint) @@ -92,8 +92,8 @@ func TestEffectiveSetup_RealStore_UnrestrictedPolicyListsDeclaredModels(t *testi assert.Equal(t, []string{"gpt-5.4"}, p.Models, "declared models listed as a courtesy") } -func TestEffectiveSetup_RealStore_AllowlistIntersectsDeclaredModels(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_AllowlistIntersectsDeclaredModels(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -106,7 +106,7 @@ func TestEffectiveSetup_RealStore_AllowlistIntersectsDeclaredModels(t *testing.T require.NoError(t, s.SaveAgentNetworkGuardrail(ctx, newSetupTestGuardrail("guard-1", " GPT-5.4 ", "gpt-4.1"))) require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", "guard-1"))) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-eng"}) require.NoError(t, err) require.Len(t, setup.Providers, 1) p := setup.Providers[0] @@ -114,8 +114,8 @@ func TestEffectiveSetup_RealStore_AllowlistIntersectsDeclaredModels(t *testing.T assert.Equal(t, []string{"gpt-5.4"}, p.Models, "allowlist ∩ declared, in declared order and casing") } -func TestEffectiveSetup_RealStore_AllowlistMatchesBedrockDeclaredIDsCanonically(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_AllowlistMatchesBedrockDeclaredIDsCanonically(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -135,7 +135,7 @@ func TestEffectiveSetup_RealStore_AllowlistMatchesBedrockDeclaredIDsCanonically( require.NoError(t, s.SaveAgentNetworkGuardrail(ctx, newSetupTestGuardrail("guard-1", "anthropic.claude-sonnet-4-5"))) require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", "guard-1"))) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-eng"}) require.NoError(t, err) require.Len(t, setup.Providers, 1) p := setup.Providers[0] @@ -144,8 +144,8 @@ func TestEffectiveSetup_RealStore_AllowlistMatchesBedrockDeclaredIDsCanonically( "the allowlisted canonical id must admit the declared region/version form, and only it") } -func TestEffectiveSetup_RealStore_UnrestrictedPolicyWinsOverRestricted(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_UnrestrictedPolicyWinsOverRestricted(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -158,15 +158,15 @@ func TestEffectiveSetup_RealStore_UnrestrictedPolicyWinsOverRestricted(t *testin open.ID = "pol-2" require.NoError(t, s.SaveAgentNetworkPolicy(ctx, open)) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-eng"}) require.NoError(t, err) require.Len(t, setup.Providers, 1) assert.True(t, setup.Providers[0].AllModelsAllowed, "one applicable policy without an allowlist makes the provider unrestricted — the proxy would admit any model through it") } -func TestEffectiveSetup_RealStore_AllowlistUnionAcrossPolicies(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_AllowlistUnionAcrossPolicies(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -181,7 +181,7 @@ func TestEffectiveSetup_RealStore_AllowlistUnionAcrossPolicies(t *testing.T) { p2.ID = "pol-2" require.NoError(t, s.SaveAgentNetworkPolicy(ctx, p2)) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-eng"}) require.NoError(t, err) require.Len(t, setup.Providers, 1) p := setup.Providers[0] @@ -189,8 +189,8 @@ func TestEffectiveSetup_RealStore_AllowlistUnionAcrossPolicies(t *testing.T) { assert.ElementsMatch(t, []string{"gpt-5.4", "gpt-4o"}, p.Models, "union of allowlists across applicable policies") } -func TestEffectiveSetup_RealStore_OrphanAndDisabledProvidersOmitted(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_OrphanAndDisabledProvidersOmitted(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -205,14 +205,14 @@ func TestEffectiveSetup_RealStore_OrphanAndDisabledProvidersOmitted(t *testing.T require.NoError(t, s.SaveAgentNetworkProvider(ctx, disabled)) require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(disabled.ID, "grp-eng", ""))) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-eng"}) require.NoError(t, err) - assert.False(t, setup.Configured, "neither an orphan nor a disabled provider is reachable, so nothing is configured for the caller") - assert.Empty(t, setup.Providers) + assert.True(t, setup.Configured) + assert.Empty(t, setup.Providers, "neither an orphan nor a disabled provider is reachable for the caller") } -func TestEffectiveSetup_RealStore_DisabledPolicyIgnored(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_DisabledPolicyIgnored(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -222,13 +222,14 @@ func TestEffectiveSetup_RealStore_DisabledPolicyIgnored(t *testing.T) { policy.Enabled = false require.NoError(t, s.SaveAgentNetworkPolicy(ctx, policy)) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-eng"}) require.NoError(t, err) - assert.False(t, setup.Configured) + assert.True(t, setup.Configured) + assert.Empty(t, setup.Providers, "a disabled policy authorizes nothing") } -func TestEffectiveSetup_RealStore_UndeclaredModelsUseAllowlistAsIs(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_UndeclaredModelsUseAllowlistAsIs(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -242,7 +243,7 @@ func TestEffectiveSetup_RealStore_UndeclaredModelsUseAllowlistAsIs(t *testing.T) require.NoError(t, s.SaveAgentNetworkGuardrail(ctx, newSetupTestGuardrail("guard-1", "claude-sonnet-4-5"))) require.NoError(t, s.SaveAgentNetworkPolicy(ctx, newSynthTestPolicy(provider.ID, "grp-eng", "guard-1"))) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-eng"}) require.NoError(t, err) require.Len(t, setup.Providers, 1) p := setup.Providers[0] @@ -250,8 +251,8 @@ func TestEffectiveSetup_RealStore_UndeclaredModelsUseAllowlistAsIs(t *testing.T) assert.Equal(t, []string{"claude-sonnet-4-5"}, p.Models) } -func TestEffectiveSetup_RealStore_ProvidersInCreatedAtOrder(t *testing.T) { - mgr, s := newSetupTestMgr(t) +func TestAgentConfig_RealStore_ProvidersInCreatedAtOrder(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -270,19 +271,20 @@ func TestEffectiveSetup_RealStore_ProvidersInCreatedAtOrder(t *testing.T) { policy.DestinationProviderIDs = []string{newer.ID, older.ID} require.NoError(t, s.SaveAgentNetworkPolicy(ctx, policy)) - setup, err := mgr.effectiveSetupForGroups(ctx, testAccountID, []string{"grp-eng"}) + setup, err := mgr.agentConfigForGroups(ctx, testAccountID, []string{"grp-eng"}) require.NoError(t, err) require.Len(t, setup.Providers, 2) assert.Equal(t, "Older", setup.Providers[0].Name) assert.Equal(t, "Newer", setup.Providers[1].Name) } -// TestGetSetupForUser_RealStore pins the self-service entry point: the +// TestGetAgentConfigForUser_RealStore pins the self-service entry point: the // user's group memberships (AutoGroups — the same groups the user's peers -// carry) scope the answer, and users outside every policy get the -// indistinguishable not-configured shape. -func TestGetSetupForUser_RealStore(t *testing.T) { - mgr, s := newSetupTestMgr(t) +// carry) scope the providers, while the account's endpoint reaches every +// member — a user outside every policy gets the config with nothing +// authorized in it. +func TestGetAgentConfigForUser_RealStore(t *testing.T) { + mgr, s := newAgentConfigTestMgr(t) ctx := context.Background() require.NoError(t, s.SaveAgentNetworkSettings(ctx, newSynthTestSettings())) @@ -300,14 +302,16 @@ func TestGetSetupForUser_RealStore(t *testing.T) { Id: "user-out", AccountID: testAccountID, Role: nbtypes.UserRoleUser, AutoGroups: []string{"grp-other"}, })) - setupIn, err := mgr.GetSetupForUser(ctx, testAccountID, "user-in") + setupIn, err := mgr.GetAgentConfigForUser(ctx, testAccountID, "user-in") require.NoError(t, err) assert.True(t, setupIn.Configured) require.Len(t, setupIn.Providers, 1) - setupOut, err := mgr.GetSetupForUser(ctx, testAccountID, "user-out") + setupOut, err := mgr.GetAgentConfigForUser(ctx, testAccountID, "user-out") require.NoError(t, err) - assert.False(t, setupOut.Configured, "user outside the policy's source groups gets the not-configured answer") + assert.True(t, setupOut.Configured, "the account is set up, so the user reads as configured") + assert.Equal(t, "https://"+testEndpoint, setupOut.Endpoint) + assert.Empty(t, setupOut.Providers, "user outside the policy's source groups is authorized for nothing") } // TestGetUsageOverview_RealStore_SelfScoped pins the self-scope fallback: @@ -316,7 +320,7 @@ func TestGetSetupForUser_RealStore(t *testing.T) { // filter for someone else must be overridden, not honored, and never // denied. A caller holding the grant keeps the account-wide view. func TestGetUsageOverview_RealStore_SelfScoped(t *testing.T) { - mgr, s := newSetupTestMgr(t) + mgr, s := newAgentConfigTestMgr(t) mgr.permissionsManager = permissions.NewManager(s) ctx := context.Background() diff --git a/management/internals/modules/agentnetwork/handlers/me_handler.go b/management/internals/modules/agentnetwork/handlers/agent_config_handler.go similarity index 53% rename from management/internals/modules/agentnetwork/handlers/me_handler.go rename to management/internals/modules/agentnetwork/handlers/agent_config_handler.go index 9b05a5031..0d6c45110 100644 --- a/management/internals/modules/agentnetwork/handlers/me_handler.go +++ b/management/internals/modules/agentnetwork/handlers/agent_config_handler.go @@ -11,36 +11,36 @@ import ( "github.com/netbirdio/netbird/shared/management/http/util" ) -// addMeEndpoints registers the self-service "My Agent Network" route. +// addAgentConfigEndpoints registers the self-service agent-config route. // It is available to every authenticated user regardless of role: the -// response is scoped strictly to the caller, which is tighter than any -// role gate could be. The caller's own usage and requests are served by +// providers in the response are scoped strictly to the caller, which is +// tighter than any role gate could be. The caller's own usage and requests are served by // the regular usage/logs endpoints, which self-scope for callers without // the account-wide grants. -func (h *handler) addMeEndpoints(router *mux.Router) { - router.HandleFunc("/agent-network/agent-config", h.getMySetup).Methods("GET", "OPTIONS") +func (h *handler) addAgentConfigEndpoints(router *mux.Router) { + router.HandleFunc("/agent-network/agent-config", h.getAgentConfig).Methods("GET", "OPTIONS") } -func (h *handler) getMySetup(w http.ResponseWriter, r *http.Request) { +func (h *handler) getAgentConfig(w http.ResponseWriter, r *http.Request) { userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { util.WriteError(r.Context(), err, w) return } - setup, err := h.manager.GetSetupForUser(r.Context(), userAuth.AccountId, userAuth.UserId) + setup, err := h.manager.GetAgentConfigForUser(r.Context(), userAuth.AccountId, userAuth.UserId) if err != nil { util.WriteError(r.Context(), err, w) return } - util.WriteJSONObject(r.Context(), w, setupToAPI(setup)) + util.WriteJSONObject(r.Context(), w, agentConfigToAPI(setup)) } -func setupToAPI(setup *types.EffectiveSetup) api.AgentNetworkMeSetup { - providers := make([]api.AgentNetworkMeProvider, 0, len(setup.Providers)) +func agentConfigToAPI(setup *types.AgentConfig) api.AgentNetworkAgentConfig { + providers := make([]api.AgentNetworkAgentConfigProvider, 0, len(setup.Providers)) for _, p := range setup.Providers { - providers = append(providers, api.AgentNetworkMeProvider{ + providers = append(providers, api.AgentNetworkAgentConfigProvider{ Name: p.Name, CatalogId: p.CatalogID, ApiFlavor: p.APIFlavor, @@ -48,7 +48,7 @@ func setupToAPI(setup *types.EffectiveSetup) api.AgentNetworkMeSetup { Models: p.Models, }) } - return api.AgentNetworkMeSetup{ + return api.AgentNetworkAgentConfig{ Configured: setup.Configured, Endpoint: setup.Endpoint, Providers: providers, diff --git a/management/internals/modules/agentnetwork/handlers/providers_handler.go b/management/internals/modules/agentnetwork/handlers/providers_handler.go index b3cd53c58..ef4b93dac 100644 --- a/management/internals/modules/agentnetwork/handlers/providers_handler.go +++ b/management/internals/modules/agentnetwork/handlers/providers_handler.go @@ -46,7 +46,7 @@ func RegisterEndpoints(manager agentnetwork.Manager, router *mux.Router) { h.addConsumptionEndpoints(router) h.addAccessLogEndpoints(router) h.addBudgetRuleEndpoints(router) - h.addMeEndpoints(router) + h.addAgentConfigEndpoints(router) } func (h *handler) getCatalogProviders(w http.ResponseWriter, r *http.Request) { diff --git a/management/internals/modules/agentnetwork/manager.go b/management/internals/modules/agentnetwork/manager.go index f1e119b12..98aca7f5d 100644 --- a/management/internals/modules/agentnetwork/manager.go +++ b/management/internals/modules/agentnetwork/manager.go @@ -86,12 +86,12 @@ type Manager interface { RecordUsage(ctx context.Context, in RecordUsageInput) error SelectPolicyForRequest(ctx context.Context, in PolicySelectionInput) (*PolicySelectionResult, error) - // GetSetupForUser backs the self-service "My Agent Network" setup - // endpoint. Caller-scoped, so it skips the role permission gate; see + // GetAgentConfigForUser backs the self-service agent-config endpoint. + // Caller-scoped, so it skips the role permission gate; see // the implementation. The caller's own usage and requests come // through GetUsageOverview / ListAccessLogs, which self-scope when // the account-wide grant is missing. - GetSetupForUser(ctx context.Context, accountID, userID string) (*types.EffectiveSetup, error) + GetAgentConfigForUser(ctx context.Context, accountID, userID string) (*types.AgentConfig, error) } // PolicySelectionInput is the per-request selection envelope. The diff --git a/management/internals/modules/agentnetwork/provider_redaction_test.go b/management/internals/modules/agentnetwork/provider_redaction_test.go index 246f4db1e..d6a749fb9 100644 --- a/management/internals/modules/agentnetwork/provider_redaction_test.go +++ b/management/internals/modules/agentnetwork/provider_redaction_test.go @@ -103,7 +103,7 @@ func TestGetProvider_RedactsForReadOnlyViewer(t *testing.T) { // other's rows. func newSelfScopeStore(t *testing.T) (*managerImpl, store.Store) { t.Helper() - mgr, s := newSetupTestMgr(t) + mgr, s := newAgentConfigTestMgr(t) mgr.permissionsManager = permissions.NewManager(s) ctx := context.Background() diff --git a/management/internals/modules/agentnetwork/types/setup.go b/management/internals/modules/agentnetwork/types/agent_config.go similarity index 65% rename from management/internals/modules/agentnetwork/types/setup.go rename to management/internals/modules/agentnetwork/types/agent_config.go index e6724f6e9..a154efed3 100644 --- a/management/internals/modules/agentnetwork/types/setup.go +++ b/management/internals/modules/agentnetwork/types/agent_config.go @@ -1,27 +1,29 @@ package types -// EffectiveSetup is the caller-scoped answer to "what may this caller +// AgentConfig is the caller-scoped answer to "what may this caller // use on the Agent Network?" — the account's proxy endpoint plus the // providers and models the caller's groups authorize. It intentionally // carries display metadata only: no keys, no upstream URLs, no policy or // guardrail structure, and no hint of providers the caller cannot reach. -type EffectiveSetup struct { - // Configured is false when the account has no Agent Network set up or - // when nothing is authorized for the caller's groups — the two cases - // are deliberately indistinguishable so the response leaks nothing - // about what exists for others. +type AgentConfig struct { + // Configured is false only when the account has no Agent Network set + // up. A caller no policy covers yet still reads as configured, with an + // empty Providers list: every member gets the same connection config, + // and the empty list is what tells them to ask for access. Configured bool // Endpoint is the account's proxy base URL // ("https://."), reachable over the NetBird tunnel - // only. Empty when Configured is false. + // only. Empty when Configured is false. Handing it to a member the + // policies do not cover authorizes nothing on its own — the proxy + // still refuses every request no policy permits. Endpoint string // Providers lists the providers at least one applicable policy // authorizes for the caller, in the account's created_at order. - Providers []EffectiveProvider + Providers []AgentConfigProvider } -// EffectiveProvider is one authorized provider in an EffectiveSetup. -type EffectiveProvider struct { +// AgentConfigProvider is one authorized provider in an AgentConfig. +type AgentConfigProvider struct { // Name is the operator-assigned label, e.g. "Bedrock prod". Name string // CatalogID names the catalog entry, e.g. "anthropic_api". diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 4cb9129f5..19cb18307 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -5807,27 +5807,27 @@ components: required: - name - checks - AgentNetworkMeSetup: + AgentNetworkAgentConfig: type: object - description: The caller-scoped Agent Network connection info backing the "My Agent Network" self-service view. Available to every authenticated user; the answer is computed from the caller's own groups and carries display metadata only. + description: The caller-scoped Agent Network connection config backing the self-service "Connect your agent" view. Available to every authenticated user; the providers are computed from the caller's own groups and the answer carries display metadata only. properties: configured: type: boolean - description: False when the account has no Agent Network set up or the caller's groups authorize none of it. The two cases are deliberately indistinguishable. + description: False only when the account has no Agent Network set up. A caller no policy covers yet still reads as configured, with an empty providers list. endpoint: type: string - description: The account's Agent Network base URL, reachable over the NetBird tunnel only. Empty when configured is false. + description: The account's Agent Network base URL, reachable over the NetBird tunnel only. Returned to every member of a configured account - it authorizes nothing on its own, since the gateway still refuses every request no policy permits. Empty when configured is false. example: https://calm-otter.proxy.example.com providers: type: array - description: The providers at least one of the caller's policies authorizes, in creation order. + description: The providers at least one of the caller's policies authorizes, in creation order. Empty when no policy covers the caller. items: - $ref: '#/components/schemas/AgentNetworkMeProvider' + $ref: '#/components/schemas/AgentNetworkAgentConfigProvider' required: - configured - endpoint - providers - AgentNetworkMeProvider: + AgentNetworkAgentConfigProvider: type: object description: One provider the caller may use, reduced to what a local tool needs for configuration. properties: @@ -13854,7 +13854,7 @@ paths: "$ref": "#/components/responses/internal_error" /api/agent-network/agent-config: get: - summary: Retrieve the caller's Agent Network setup + summary: Retrieve the caller's Agent Network agent config description: Returns everything the caller needs to configure a local AI tool and nothing more - the account's Agent Network endpoint plus the providers and models the caller's own policies allow. Available to every authenticated user regardless of role; the response never contains provider credentials, policy or guardrail configuration, or providers the caller cannot reach. tags: [ Agent Network ] security: @@ -13862,11 +13862,11 @@ paths: - TokenAuth: [ ] responses: '200': - description: The caller-scoped Agent Network connection info + description: The caller-scoped Agent Network agent config content: application/json: schema: - $ref: '#/components/schemas/AgentNetworkMeSetup' + $ref: '#/components/schemas/AgentNetworkAgentConfig' '401': "$ref": "#/components/responses/requires_authentication" '500': diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 6779cdb8e..1d574c857 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -1931,6 +1931,36 @@ type AgentNetworkAccessLogsResponse struct { TotalRecords int `json:"total_records"` } +// AgentNetworkAgentConfig The caller-scoped Agent Network connection config backing the self-service "Connect your agent" view. Available to every authenticated user; the providers are computed from the caller's own groups and the answer carries display metadata only. +type AgentNetworkAgentConfig struct { + // Configured False only when the account has no Agent Network set up. A caller no policy covers yet still reads as configured, with an empty providers list. + Configured bool `json:"configured"` + + // Endpoint The account's Agent Network base URL, reachable over the NetBird tunnel only. Returned to every member of a configured account - it authorizes nothing on its own, since the gateway still refuses every request no policy permits. Empty when configured is false. + Endpoint string `json:"endpoint"` + + // Providers The providers at least one of the caller's policies authorizes, in creation order. Empty when no policy covers the caller. + Providers []AgentNetworkAgentConfigProvider `json:"providers"` +} + +// AgentNetworkAgentConfigProvider One provider the caller may use, reduced to what a local tool needs for configuration. +type AgentNetworkAgentConfigProvider struct { + // AllModelsAllowed True when no model allowlist restricts this provider for the caller; models then lists the declared or catalog models as a courtesy. + AllModelsAllowed bool `json:"all_models_allowed"` + + // ApiFlavor Request-body shape the provider speaks ("anthropic", "openai"). Empty when the gateway dispatches it by URL path instead. + ApiFlavor string `json:"api_flavor"` + + // CatalogId Catalog entry id naming the provider type. + CatalogId string `json:"catalog_id"` + + // Models The effective model allowlist for the caller (or the declared/catalog models when all_models_allowed is true). + Models []string `json:"models"` + + // Name Operator-assigned provider label. + Name string `json:"name"` +} + // 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"` @@ -2194,36 +2224,6 @@ type AgentNetworkGuardrailRequest struct { Name string `json:"name"` } -// AgentNetworkMeProvider One provider the caller may use, reduced to what a local tool needs for configuration. -type AgentNetworkMeProvider struct { - // AllModelsAllowed True when no model allowlist restricts this provider for the caller; models then lists the declared or catalog models as a courtesy. - AllModelsAllowed bool `json:"all_models_allowed"` - - // ApiFlavor Request-body shape the provider speaks ("anthropic", "openai"). Empty when the gateway dispatches it by URL path instead. - ApiFlavor string `json:"api_flavor"` - - // CatalogId Catalog entry id naming the provider type. - CatalogId string `json:"catalog_id"` - - // Models The effective model allowlist for the caller (or the declared/catalog models when all_models_allowed is true). - Models []string `json:"models"` - - // Name Operator-assigned provider label. - Name string `json:"name"` -} - -// AgentNetworkMeSetup The caller-scoped Agent Network connection info backing the "My Agent Network" self-service view. Available to every authenticated user; the answer is computed from the caller's own groups and carries display metadata only. -type AgentNetworkMeSetup struct { - // Configured False when the account has no Agent Network set up or the caller's groups authorize none of it. The two cases are deliberately indistinguishable. - Configured bool `json:"configured"` - - // Endpoint The account's Agent Network base URL, reachable over the NetBird tunnel only. Empty when configured is false. - Endpoint string `json:"endpoint"` - - // Providers The providers at least one of the caller's policies authorizes, in creation order. - Providers []AgentNetworkMeProvider `json:"providers"` -} - // AgentNetworkModelDiscoveryRequest defines model for AgentNetworkModelDiscoveryRequest. type AgentNetworkModelDiscoveryRequest struct { // ApiKey Credential to query the vendor with, for a provider that has not been saved yet. Mutually exclusive with provider_id.