[management] Rework Agent Network endpoint identity and settings bootstrap (#7085)

Store the per-account gateway endpoint as {domain, proxy_address} with a
global unique index on the full hostname; dedicated = (domain ==
proxy_address). Bootstrap becomes an explicit POST carrying exactly one
of proxy_address (server allocates an adjective-noun label beneath it)
or endpoint (claimed verbatim, address-first); provider create loses its
bootstrap side effect. PUT is a full replace with every field required —
the immutable identity fields must be echoed unchanged and a mismatch is
rejected with 422. A guarded DELETE releases the endpoint: refused with
412 while providers exist or a proxy is actively serving the endpoint
hostname (matched case-insensitively); re-creating bootstraps fresh. A
self-addressed pin excludes its address from the account's cluster allow
list, and the live mapping update path now addresses the serving proxy
from the synthesized service. Existing rows are migrated on all three
store engines.
This commit is contained in:
Brad Ison
2026-08-10 19:06:55 +02:00
committed by GitHub
parent e8671a811d
commit ebfdf7d7b8
46 changed files with 2392 additions and 688 deletions
@@ -68,7 +68,10 @@ func TestAgentNetwork_BudgetRuleCRUD_RealManager(t *testing.T) {
// TestAgentNetwork_UpdateSettings_PreservesImmutableAndTogglesCollection is the
// GC-1 guard for UpdateSettings: it must apply the collection toggles while
// preserving the immutable Cluster/Subdomain pinned at bootstrap.
// preserving the immutable Domain/ProxyAddress assigned at bootstrap. The
// request echoes the identity fields back — the PUT convention every other
// endpoint follows — and a request echoing anything else is rejected outright
// rather than quietly ignored.
func TestAgentNetwork_UpdateSettings_PreservesImmutableAndTogglesCollection(t *testing.T) {
am, _, err := createManager(t)
require.NoError(t, err, "createManager must succeed")
@@ -84,7 +87,14 @@ func TestAgentNetwork_UpdateSettings_PreservesImmutableAndTogglesCollection(t *t
mgr := agentnetwork.NewManager(am.Store, permissions.NewManager(am.Store), am, nil)
// Creating a provider bootstraps the settings row (cluster + subdomain).
// Bootstrap is an explicit settings create; providers have no settings
// side effects anymore.
before, err := mgr.CreateSettings(ctx, adminUserID, agenttypes.DefaultSettings(accountID), clusterAddr, "")
require.NoError(t, err, "CreateSettings must bootstrap the row")
require.Equal(t, clusterAddr, before.ProxyAddress, "proxy address pinned at bootstrap")
require.NotEmpty(t, before.Domain, "endpoint allocated at bootstrap")
assert.False(t, before.EnablePromptCollection, "prompt collection defaults off")
_, err = mgr.CreateProvider(ctx, adminUserID, &agenttypes.Provider{
AccountID: accountID,
ProviderID: "openai_api",
@@ -93,43 +103,64 @@ func TestAgentNetwork_UpdateSettings_PreservesImmutableAndTogglesCollection(t *t
APIKey: "sk-test",
Enabled: true,
Models: []agenttypes.ProviderModel{{ID: "gpt-5.4"}},
}, clusterAddr)
require.NoError(t, err, "CreateProvider must bootstrap settings")
before, err := mgr.GetSettings(ctx, accountID, adminUserID)
require.NoError(t, err, "GetSettings must succeed after bootstrap")
require.Equal(t, clusterAddr, before.Cluster, "cluster pinned at bootstrap")
require.NotEmpty(t, before.Subdomain, "subdomain pinned at bootstrap")
assert.False(t, before.EnablePromptCollection, "prompt collection defaults off")
// A cluster different from the one pinned at bootstrap must be rejected
// outright — never silently swapped or ignored.
_, err = mgr.UpdateSettings(ctx, adminUserID, &agenttypes.Settings{
AccountID: accountID,
Cluster: "attacker.cluster",
EnableLogCollection: true,
})
require.Error(t, err, "UpdateSettings with a mismatched cluster must fail")
require.NoError(t, err, "CreateProvider must succeed")
// Flipping the toggles works with the pinned cluster echoed back (and
// with it omitted); the subdomain is never taken from the request.
// Flipping the toggles works when the request echoes the assigned
// identity. Retention is echoed too: UpdateSettings takes it verbatim, so
// omitting it would zero the account's retention.
updated, err := mgr.UpdateSettings(ctx, adminUserID, &agenttypes.Settings{
AccountID: accountID,
Cluster: clusterAddr,
Subdomain: "evil",
Domain: before.Domain,
ProxyAddress: before.ProxyAddress,
EnableLogCollection: true,
EnablePromptCollection: true,
RedactPii: true,
AccessLogRetentionDays: before.AccessLogRetentionDays,
})
require.NoError(t, err, "UpdateSettings must succeed")
assert.Equal(t, before.Cluster, updated.Cluster, "cluster is immutable and must be preserved")
assert.Equal(t, before.Subdomain, updated.Subdomain, "subdomain is immutable and must be preserved")
assert.Equal(t, before.Domain, updated.Domain, "domain is immutable and must be preserved")
assert.Equal(t, before.ProxyAddress, updated.ProxyAddress, "proxy address is immutable and must be preserved")
assert.True(t, updated.EnableLogCollection, "log collection toggle must apply")
assert.True(t, updated.EnablePromptCollection, "prompt collection toggle must apply")
assert.True(t, updated.RedactPii, "redact toggle must apply")
assert.Equal(t, before.AccessLogRetentionDays, updated.AccessLogRetentionDays, "echoed retention must survive")
// Neither identity field can be smuggled into the row: a hand-rolled
// Settings value carrying a different endpoint or proxy address is
// rejected, not silently ignored.
for _, tc := range []struct {
name string
domain string
proxyAddress string
}{
{name: "foreign endpoint", domain: "evil.example.com", proxyAddress: before.ProxyAddress},
{name: "foreign proxy address", domain: before.Domain, proxyAddress: "attacker.cluster"},
{name: "empty identity echo", domain: "", proxyAddress: ""},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := mgr.UpdateSettings(ctx, adminUserID, &agenttypes.Settings{
AccountID: accountID,
Domain: tc.domain,
ProxyAddress: tc.proxyAddress,
EnableLogCollection: false,
EnablePromptCollection: false,
RedactPii: false,
AccessLogRetentionDays: before.AccessLogRetentionDays,
})
assert.Error(t, err, "a mismatched identity echo must be rejected")
assert.ErrorContains(t, err, "immutable", "the rejection must name the immutability rule")
})
}
// The rejected updates left the row exactly as the accepted one wrote it.
afterRejects, err := mgr.GetSettings(ctx, accountID, adminUserID)
require.NoError(t, err, "GetSettings must succeed")
assert.True(t, afterRejects.EnablePromptCollection, "a rejected update must not roll back the accepted toggles")
reloaded, err := am.Store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
assert.Equal(t, before.Cluster, reloaded.Cluster, "persisted cluster unchanged")
assert.Equal(t, before.Domain, reloaded.Domain, "persisted domain unchanged")
assert.Equal(t, before.ProxyAddress, reloaded.ProxyAddress, "persisted proxy address unchanged")
assert.True(t, reloaded.EnablePromptCollection, "persisted prompt collection toggled on")
}