Skip to content

fix: let All servers agent keys reach servers added after issue - #8320

Closed
mfbx9da4 wants to merge 1 commit into
mainfrom
fix/all-servers-agent-key-wildcard
Closed

mfbx9da4 wants to merge 1 commit into
mainfrom
fix/all-servers-agent-key-wildcard

Conversation

@mfbx9da4

@mfbx9da4 mfbx9da4 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Why

An agent saved with "All servers" got a key that listed the servers that existed at issue time. A server added later never showed on the agent gateway (/agent-mcp/{agentID}) until someone issued a new key, because the key's stored policy is one of three policies that must all allow. Re-saving the agent did not help.

What changed

Before: the wizard expanded "All servers" into today's servers and dropped any wildcard candidate. Discovery also omitted a wildcard that an owner exclusion overlaps.

After:

  • listDelegableGrants takes carry_exclusions. The wildcard comes back with the overlapping mcp:blocked_connect exclusions beside it, through the existing DelegableGrantsWithExclusions logic.
  • The wizard issues "All servers" keys with the project mcp:connect wildcard plus those exclusions, at delegated grants version 4. When no wildcard is delegable (an owner granted servers one by one), it falls back to the old per-server list.
  • The wrong comment about the agent wildcard carrying new servers is gone.
  • Tests: a server created after the key is issued is listed on the gateway, and an owner-blocked server stays hidden.

Gutternote review steps

No visible UI change. The fix shows on the agent gateway's list_servers, not in the dashboard.


Summary by cubic

Agent keys issued with "All servers" now reach MCP servers added after the key was issued; before, the key only listed servers that existed at the time, so new servers stayed hidden until a new key was issued.

  • The wizard now issues the project wildcard plus any owner-blocked server exclusions, using delegated grants version 4. When no wildcard can be delegated, it falls back to listing servers.
  • Discovery accepts carry_exclusions so the wildcard isn't dropped when an owner exclusion overlaps it.

Written for commit f0af20f. Summary will update on new commits.

View guided diff Turn on auto-fix

@mfbx9da4 mfbx9da4 added the bug Something isn't working label Oct 30, 2026
@changeset-bot

changeset-bot Bot commented Oct 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f0af20f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
dashboard Patch
server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 24 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="server/internal/agentmanagement/delegable_test.go">

<violation number="1" location="server/internal/agentmanagement/delegable_test.go:225">
P3: These `require.Contains` checks would still pass if discovery returned extra or wrongly scoped grants alongside the expected two. Assert the exact set with `require.ElementsMatch`, as the neighboring exclusion test does.</violation>
</file>

<file name="client/dashboard/src/pages/agents/agent-key-discovery.test.ts">

<violation number="1" location="client/dashboard/src/pages/agents/agent-key-discovery.test.ts:248">
P2: The third test passes `[]` (organization-wide reach) but its mock has no wildcard, so it never checks that wildcards from any project are kept. Add a case with `[]` and a `wildcard("project_two")` in the mock, and expect it in the result. That covers the organization-wide branch the wizard uses.</violation>
</file>

<file name="server/internal/mcp/serve_agent_gateway_test.go">

<violation number="1" location="server/internal/mcp/serve_agent_gateway_test.go:537">
P2: This test rebuilds the wizard's issuance by hand (grant filtering, policy version, containment check, direct `CreateAgentAPIKey`) instead of going through the key creation service. If the wizard's wildcard selection or the service's validation regresses, this test still passes. Drive issuance through `keys.Service.createAgentKey` (or the delegable-grants endpoint) so the test covers the code that ships.</violation>
</file>

<file name="server/design/agents/design.go">

<violation number="1" location="server/design/agents/design.go:313">
P3: The new `carry_exclusions` description says "Requesting both on a key", but "both" has no referent, and this endpoint takes no version. Name the pair and say that issuance enforces the version: "Storing the wildcard with its exclusion on one key requires delegated_grants_version 4 or later, enforced at key issuance."</violation>
</file>

<file name="client/dashboard/src/pages/agents/credentials/CredentialsWizard.tsx">

<violation number="1" location="client/dashboard/src/pages/agents/credentials/CredentialsWizard.tsx:1096">
P1: This treats any delegable wildcard as covering the full “All servers” reach. If one project has no delegable wildcard, `listed` is empty and the gap check is skipped, so the key silently omits that project; verify coverage for every project before skipping the per-server check.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

})),
controller.signal,
);
const listed = wildcard.length > 0 ? [] : reach;

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This treats any delegable wildcard as covering the full “All servers” reach. If one project has no delegable wildcard, listed is empty and the gap check is skipped, so the key silently omits that project; verify coverage for every project before skipping the per-server check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At client/dashboard/src/pages/agents/credentials/CredentialsWizard.tsx, line 1096:

<comment>This treats any delegable wildcard as covering the full “All servers” reach. If one project has no delegable wildcard, `listed` is empty and the gap check is skipped, so the key silently omits that project; verify coverage for every project before skipping the per-server check.</comment>

<file context>
@@ -1068,26 +1071,42 @@ export function CredentialsWizard({
-        })),
-        controller.signal,
-      );
+      const listed = wildcard.length > 0 ? [] : reach;
+      const delegable =
+        wildcard.length > 0
</file context>
Fix with cubic

expect(grants).toEqual([wildcard("project_one"), exclusion]);
});

it("is empty when no wildcard can be delegated, so the caller names servers", async () => {

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The third test passes [] (organization-wide reach) but its mock has no wildcard, so it never checks that wildcards from any project are kept. Add a case with [] and a wildcard("project_two") in the mock, and expect it in the result. That covers the organization-wide branch the wizard uses.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At client/dashboard/src/pages/agents/agent-key-discovery.test.ts, line 248:

<comment>The third test passes `[]` (organization-wide reach) but its mock has no wildcard, so it never checks that wildcards from any project are kept. Add a case with `[]` and a `wildcard("project_two")` in the mock, and expect it in the result. That covers the organization-wide branch the wizard uses.</comment>

<file context>
@@ -190,3 +191,70 @@ describe("scoped credential discovery", () => {
+    expect(grants).toEqual([wildcard("project_one"), exclusion]);
+  });
+
+  it("is empty when no wildcard can be delegated, so the caller names servers", async () => {
+    const listDelegableGrants = vi
+      .fn()
</file context>
Fix with cubic

}
}
version := runtimepolicy.IssuedDelegatedPolicyVersion
policy, err := runtimepolicy.NewDelegatedPolicy(version, requested)

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This test rebuilds the wizard's issuance by hand (grant filtering, policy version, containment check, direct CreateAgentAPIKey) instead of going through the key creation service. If the wizard's wildcard selection or the service's validation regresses, this test still passes. Drive issuance through keys.Service.createAgentKey (or the delegable-grants endpoint) so the test covers the code that ships.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At server/internal/mcp/serve_agent_gateway_test.go, line 537:

<comment>This test rebuilds the wizard's issuance by hand (grant filtering, policy version, containment check, direct `CreateAgentAPIKey`) instead of going through the key creation service. If the wizard's wildcard selection or the service's validation regresses, this test still passes. Drive issuance through `keys.Service.createAgentKey` (or the delegable-grants endpoint) so the test covers the code that ships.</comment>

<file context>
@@ -475,6 +477,104 @@ func TestServeAgentGateway_ListsPrivateOnlyMembersOnPrivateIngress(t *testing.T)
+		}
+	}
+	version := runtimepolicy.IssuedDelegatedPolicyVersion
+	policy, err := runtimepolicy.NewDelegatedPolicy(version, requested)
+	require.NoError(t, err)
+	contained, err := runtimepolicy.DelegationContainedFor(authCtx.UserID, policy, agentPolicy, ownerPolicy, ownerPolicy)
</file context>
Fix with cubic

Comment on lines +225 to +226
require.Contains(t, result, &gen.AgentPolicyGrantForm{Scope: string(authz.ScopeMCPConnect), Effect: "allow", Selector: &gen.AgentPolicySelector{ResourceKind: authz.ResourceKindMCP, ResourceID: "*", ProjectID: &projectOne}})
require.Contains(t, result, &gen.AgentPolicyGrantForm{Scope: string(authz.ScopeMCPBlockedConnect), Effect: "allow", Selector: &gen.AgentPolicySelector{ResourceKind: authz.ResourceKindMCP, ResourceID: "blocked-server", ProjectID: &projectOne}})

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: These require.Contains checks would still pass if discovery returned extra or wrongly scoped grants alongside the expected two. Assert the exact set with require.ElementsMatch, as the neighboring exclusion test does.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At server/internal/agentmanagement/delegable_test.go, line 225:

<comment>These `require.Contains` checks would still pass if discovery returned extra or wrongly scoped grants alongside the expected two. Assert the exact set with `require.ElementsMatch`, as the neighboring exclusion test does.</comment>

<file context>
@@ -205,6 +205,27 @@ func TestListDelegableGrantsConservativelyOmitsOverlappingExclusions(t *testing.
+	result, err := f.service.ListDelegableGrants(validatedHumanContext(t, "org-delegable", "owner"), &gen.ListDelegableGrantsPayload{AgentID: f.agentID.String(), CarryExclusions: true})
+	require.NoError(t, err)
+	projectOne := "project-one"
+	require.Contains(t, result, &gen.AgentPolicyGrantForm{Scope: string(authz.ScopeMCPConnect), Effect: "allow", Selector: &gen.AgentPolicySelector{ResourceKind: authz.ResourceKindMCP, ResourceID: "*", ProjectID: &projectOne}})
+	require.Contains(t, result, &gen.AgentPolicyGrantForm{Scope: string(authz.ScopeMCPBlockedConnect), Effect: "allow", Selector: &gen.AgentPolicySelector{ResourceKind: authz.ResourceKindMCP, ResourceID: "blocked-server", ProjectID: &projectOne}})
+}
</file context>
Suggested change
require.Contains(t, result, &gen.AgentPolicyGrantForm{Scope: string(authz.ScopeMCPConnect), Effect: "allow", Selector: &gen.AgentPolicySelector{ResourceKind: authz.ResourceKindMCP, ResourceID: "*", ProjectID: &projectOne}})
require.Contains(t, result, &gen.AgentPolicyGrantForm{Scope: string(authz.ScopeMCPBlockedConnect), Effect: "allow", Selector: &gen.AgentPolicySelector{ResourceKind: authz.ResourceKindMCP, ResourceID: "blocked-server", ProjectID: &projectOne}})
require.ElementsMatch(t, []*gen.AgentPolicyGrantForm{
{Scope: string(authz.ScopeMCPConnect), Effect: "allow", Selector: &gen.AgentPolicySelector{ResourceKind: authz.ResourceKindMCP, ResourceID: "*", ProjectID: &projectOne}},
{Scope: string(authz.ScopeMCPBlockedConnect), Effect: "allow", Selector: &gen.AgentPolicySelector{ResourceKind: authz.ResourceKindMCP, ResourceID: "blocked-server", ProjectID: &projectOne}},
}, result)
Fix with cubic

Extend(AgentIDForm)
Attribute("toolset_id", String, "Optional MCP authorization resource identifier: the toolset ID when present, otherwise the MCP server ID. Narrows discovery to this server and its project before evaluating exclusions.", func() { Format(FormatUUID) })
Attribute("toolset_ids", ArrayOf(String, func() { Format(FormatUUID) }), "Optional MCP authorization resource identifiers, discovered in one request. Each narrows discovery like toolset_id; the result is the union of candidates across all of them.", func() { MaxLength(100) })
Attribute("carry_exclusions", Boolean, "Keep a wildcard candidate that a narrower exclusion overlaps, and return that exclusion beside it as a blocklist-scope grant, instead of omitting the candidate. Requesting both on a key needs delegated grants version 4 or later.", func() { Default(false) })

@cubic-dev-ai cubic-dev-ai Bot Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new carry_exclusions description says "Requesting both on a key", but "both" has no referent, and this endpoint takes no version. Name the pair and say that issuance enforces the version: "Storing the wildcard with its exclusion on one key requires delegated_grants_version 4 or later, enforced at key issuance."

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At server/design/agents/design.go, line 313:

<comment>The new `carry_exclusions` description says "Requesting both on a key", but "both" has no referent, and this endpoint takes no version. Name the pair and say that issuance enforces the version: "Storing the wildcard with its exclusion on one key requires delegated_grants_version 4 or later, enforced at key issuance."</comment>

<file context>
@@ -310,13 +310,15 @@ var _ = Service("agents", func() {
 			Extend(AgentIDForm)
 			Attribute("toolset_id", String, "Optional MCP authorization resource identifier: the toolset ID when present, otherwise the MCP server ID. Narrows discovery to this server and its project before evaluating exclusions.", func() { Format(FormatUUID) })
 			Attribute("toolset_ids", ArrayOf(String, func() { Format(FormatUUID) }), "Optional MCP authorization resource identifiers, discovered in one request. Each narrows discovery like toolset_id; the result is the union of candidates across all of them.", func() { MaxLength(100) })
+			Attribute("carry_exclusions", Boolean, "Keep a wildcard candidate that a narrower exclusion overlaps, and return that exclusion beside it as a blocklist-scope grant, instead of omitting the candidate. Requesting both on a key needs delegated grants version 4 or later.", func() { Default(false) })
 		})
 		Result(ArrayOf(PolicyGrantForm))
</file context>
Suggested change
Attribute("carry_exclusions", Boolean, "Keep a wildcard candidate that a narrower exclusion overlaps, and return that exclusion beside it as a blocklist-scope grant, instead of omitting the candidate. Requesting both on a key needs delegated grants version 4 or later.", func() { Default(false) })
Attribute("carry_exclusions", Boolean, "Keep a wildcard candidate that a narrower exclusion overlaps, and return that exclusion beside it as a blocklist-scope grant, instead of omitting the candidate. Storing the wildcard with its exclusion on one key requires delegated_grants_version 4 or later, enforced at key issuance.", func() { Default(false) })
Fix with cubic

@mfbx9da4

Copy link
Copy Markdown
Contributor Author

Merged into #8315 to keep these agent setup fixes in one PR.

@mfbx9da4 mfbx9da4 closed this Oct 30, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant