Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
64178c6
Add Agent Network Admin and Usage Viewer roles, gate Agent Network pe…
mlsmaycon Aug 16, 2026
b8462cc
Gate Usage and Clusters tabs by their own permissions
mlsmaycon Aug 16, 2026
6c9c73a
Fall back to a known configuration tab for unknown query values
mlsmaycon Aug 16, 2026
fe96e56
Derive the configuration tab from the URL query
mlsmaycon Aug 16, 2026
eff4321
Derive the usage tab from the URL query
mlsmaycon Aug 16, 2026
3fbeb1e
Group user roles into General and Agent Network tabs
braginini Aug 16, 2026
f75324f
Point agent network learn-more links at their docs pages
braginini Aug 16, 2026
64b76d1
Skip the domains lookup when the role can't read it
braginini Aug 17, 2026
654c4eb
Flatten the role selector and add a caller-scoped My Setup page
mlsmaycon Aug 18, 2026
0ae1aab
Fix delegated-role API errors and reuse the usage overview for My Usage
mlsmaycon Aug 18, 2026
183e59a
Reuse the providers-page agent config on My Setup and self-scope Usag…
mlsmaycon Aug 18, 2026
df2ad79
Hide provider write actions from read-only viewers
mlsmaycon Aug 18, 2026
684da5c
Restore the grouped role selector tabs
mlsmaycon Aug 22, 2026
c20d6ff
Merge remote-tracking branch 'origin/main' into agent-network-roles
mlsmaycon Aug 25, 2026
d2b3ea5
Offer the provider and model filters to self-scoped callers
mlsmaycon Aug 27, 2026
751cbdf
Give the agent config a single home on Connect Agent
braginini Aug 31, 2026
3eb77fc
Simplify Bedrock config
braginini Aug 31, 2026
d091fa6
Inline the agent config on the Connect page
braginini Aug 31, 2026
c11728a
Show a policy's allowed models in the policies table
braginini Aug 31, 2026
5251e46
Point the Kimi e2e config assertions at the Connect Agent page
mlsmaycon Aug 31, 2026
91ed66c
Hand the agent config to every account member
braginini Aug 31, 2026
ec52828
Merge remote-tracking branch 'origin/agent-network-roles' into agent-…
braginini Aug 31, 2026
da1c8a2
Let the surface switch decide the Agent Network section
braginini Aug 31, 2026
7b59034
Merge remote-tracking branch 'origin/main' into agent-network-roles
braginini Aug 31, 2026
78a1053
Collapse the two User types the merge left behind
braginini Aug 31, 2026
ba8b895
Fix SNAPSHOT version display
braginini Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 42 additions & 27 deletions src/app/(dashboard)/agent-network/configuration/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,22 @@ export default function AgentNetworkConfigurationPage() {
const { only: agentNetworkOnly } = useAgentNetworkMode();
const queryParams = useSearchParams();
const queryTab = queryParams.get("tab");
const [tab, setTab] = useState(queryTab ?? TAB_BUDGET_SETTINGS);

// Clusters is a reverse-proxy surface (its table and controls run on the
// services permission), so it stays hidden from roles that only hold
// agent_network.settings, e.g. agent_network_admin.
const canReadClusters = !!permission?.services?.read;
const requestedTab =
queryTab && (queryTab !== TAB_CLUSTERS || canReadClusters)
? queryTab
: TAB_BUDGET_SETTINGS;
const [tab, setTab] = useState(requestedTab);

useEffect(() => {
if (queryTab) setTab(queryTab);
}, [queryTab]);
if (queryTab && (queryTab !== TAB_CLUSTERS || canReadClusters)) {
setTab(queryTab);
}
}, [queryTab, canReadClusters]);

return (
<PageContainer>
Expand All @@ -56,14 +67,16 @@ export default function AgentNetworkConfigurationPage() {
<ScrollText size={14} />
Log Collection
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value={TAB_CLUSTERS}>
<ServerIcon size={14} />
Clusters
</VerticalTabs.Trigger>
{canReadClusters && (
<VerticalTabs.Trigger value={TAB_CLUSTERS}>
<ServerIcon size={14} />
Clusters
</VerticalTabs.Trigger>
)}
</VerticalTabs.List>
<RestrictedAccess
page={"Configuration"}
hasAccess={permission?.services?.read}
hasAccess={permission?.["agent_network.settings"]?.read}
>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<GroupsProvider>
<PeersProvider>
Expand Down Expand Up @@ -96,26 +109,28 @@ export default function AgentNetworkConfigurationPage() {
</Suspense>
</Tabs.Content>

<Tabs.Content value={TAB_CLUSTERS} className={"w-full"}>
<ConfigTabHeader
label={"Clusters"}
href={"/agent-network/configuration?tab=clusters"}
>
{agentNetworkOnly
? "Proxy clusters route your agents' traffic to AI providers and run on your own infrastructure. Add multiple clusters to scale your environment."
: "Proxy clusters route inbound traffic to your services. Shared clusters are run by the platform; account clusters (self-hosted) run on your own infrastructure."}{" "}
<InlineLink
href={REVERSE_PROXY_CLUSTERS_DOCS_LINK}
target={"_blank"}
{canReadClusters && (
<Tabs.Content value={TAB_CLUSTERS} className={"w-full"}>
<ConfigTabHeader
label={"Clusters"}
href={"/agent-network/configuration?tab=clusters"}
>
Learn more
<ExternalLinkIcon size={12} />
</InlineLink>
</ConfigTabHeader>
<Suspense fallback={<SkeletonTable />}>
<ClustersTable />
</Suspense>
</Tabs.Content>
{agentNetworkOnly
? "Proxy clusters route your agents' traffic to AI providers and run on your own infrastructure. Add multiple clusters to scale your environment."
: "Proxy clusters route inbound traffic to your services. Shared clusters are run by the platform; account clusters (self-hosted) run on your own infrastructure."}{" "}
<InlineLink
href={REVERSE_PROXY_CLUSTERS_DOCS_LINK}
target={"_blank"}
>
Learn more
<ExternalLinkIcon size={12} />
</InlineLink>
</ConfigTabHeader>
<Suspense fallback={<SkeletonTable />}>
<ClustersTable />
</Suspense>
</Tabs.Content>
)}
</div>
</AIProvidersProvider>
</PeersProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(dashboard)/agent-network/policies/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function AgentNetworkPoliciesPage() {

<RestrictedAccess
page={"Policies"}
hasAccess={permission?.services?.read}
hasAccess={permission?.["agent_network.policies"]?.read}
>
<AIProvidersProvider>
<Suspense fallback={<SkeletonTable />}>
Expand Down
4 changes: 2 additions & 2 deletions src/app/(dashboard)/agent-network/providers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ export default function AgentNetworkProvidersPage() {
<PageContainer>
{/* Gate the whole surface: AIProvidersProvider and EndpointHeader fetch
agent-network state, so they must not mount for users without
services.read. */}
read on the providers submodule. */}
<RestrictedAccess
page={"Providers"}
hasAccess={permission?.services?.read}
hasAccess={permission?.["agent_network.providers"]?.read}
>
<AIProvidersProvider>
<div className={"p-default py-6"}>
Expand Down
100 changes: 62 additions & 38 deletions src/app/(dashboard)/agent-network/usage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import AIProvidersProvider from "@/modules/agent-network/AIProvidersProvider";
const TAB_USAGE = "usage";
const TAB_ACCESS_LOGS = "access-logs";

const VALID_TABS = new Set([TAB_USAGE, TAB_ACCESS_LOGS]);

// UsageAndLogsPage surfaces the live access log and the spend dashboard.
// Budget rules and log-collection controls live under the separate
// Configuration entry. Providers are mounted once at the top so switching
Expand All @@ -36,8 +34,23 @@ export default function UsageAndLogsPage() {
const router = useRouter();
const pathname = usePathname();

const canReadUsage = !!permission?.["agent_network.usage"]?.read;
const canReadLogs = !!permission?.["agent_network.logs"]?.read;

// Each tab maps to its own permission submodule (usage_viewer, for one,
// reads usage but not the request-level log). Only permitted tabs are
// selectable; a deep link to a forbidden or unknown tab falls back to the
// first permitted one.
const allowedTabs = useMemo(() => {
const tabs = new Set<string>();
if (canReadUsage) tabs.add(TAB_USAGE);
if (canReadLogs) tabs.add(TAB_ACCESS_LOGS);
return tabs;
}, [canReadUsage, canReadLogs]);
const defaultTab = canReadUsage ? TAB_USAGE : TAB_ACCESS_LOGS;

const queryTab = searchParams.get("tab") ?? "";
const initialTab = VALID_TABS.has(queryTab) ? queryTab : TAB_USAGE;
const initialTab = allowedTabs.has(queryTab) ? queryTab : defaultTab;
const [tab, setTab] = useState(initialTab);

// Access-log view mode: flat per-request rows, or grouped by provider session.
Expand All @@ -59,8 +72,8 @@ export default function UsageAndLogsPage() {
// Also reset to the default when ?tab= is removed or invalid, so navigating
// back to the bare URL doesn't leave the previous tab selected.
useEffect(() => {
setTab(VALID_TABS.has(queryTab) ? queryTab : TAB_USAGE);
}, [queryTab]);
setTab(allowedTabs.has(queryTab) ? queryTab : defaultTab);
}, [queryTab, allowedTabs, defaultTab]);
Comment thread
mlsmaycon marked this conversation as resolved.
Outdated

// Reflect the active tab in the URL so it's shareable, like Settings.
const onTabChange = (value: string) => {
Expand Down Expand Up @@ -94,7 +107,10 @@ export default function UsageAndLogsPage() {

<RestrictedAccess
page={"Usage & Logs"}
hasAccess={permission?.services?.read}
hasAccess={
permission?.["agent_network.usage"]?.read ||
permission?.["agent_network.logs"]?.read
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
>
<GroupsProvider>
<PeersProvider>
Expand All @@ -106,41 +122,49 @@ export default function UsageAndLogsPage() {
className={"pt-4 pb-0 mb-0"}
>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={TAB_USAGE}>
<LayoutDashboard size={16} />
Usage
</TabsTrigger>
<TabsTrigger value={TAB_ACCESS_LOGS}>
<ScrollText size={16} />
Access Logs
</TabsTrigger>
{canReadUsage && (
<TabsTrigger value={TAB_USAGE}>
<LayoutDashboard size={16} />
Usage
</TabsTrigger>
)}
{canReadLogs && (
<TabsTrigger value={TAB_ACCESS_LOGS}>
<ScrollText size={16} />
Access Logs
</TabsTrigger>
)}
</TabsList>

<TabsContent value={TAB_USAGE} className={"pb-8"}>
<Suspense fallback={<SkeletonTable />}>
<AgentOverviewPanel />
</Suspense>
</TabsContent>
{canReadUsage && (
<TabsContent value={TAB_USAGE} className={"pb-8"}>
<Suspense fallback={<SkeletonTable />}>
<AgentOverviewPanel />
</Suspense>
</TabsContent>
)}

<TabsContent value={TAB_ACCESS_LOGS} className={"pb-8"}>
<Suspense fallback={<SkeletonTable />}>
<ServerPaginationProvider
key={groupBySession ? "sessions" : "flat"}
url={
groupBySession
? "/agent-network/access-log-sessions"
: "/agent-network/access-logs"
}
defaultPageSize={25}
defaultFilters={defaultAccessLogFilters}
>
<AgentAccessLogTable
grouped={groupBySession}
onGroupedChange={setGroupBySession}
/>
</ServerPaginationProvider>
</Suspense>
</TabsContent>
{canReadLogs && (
<TabsContent value={TAB_ACCESS_LOGS} className={"pb-8"}>
<Suspense fallback={<SkeletonTable />}>
<ServerPaginationProvider
key={groupBySession ? "sessions" : "flat"}
url={
groupBySession
? "/agent-network/access-log-sessions"
: "/agent-network/access-logs"
}
defaultPageSize={25}
defaultFilters={defaultAccessLogFilters}
>
<AgentAccessLogTable
grouped={groupBySession}
onGroupedChange={setGroupBySession}
/>
</ServerPaginationProvider>
</Suspense>
</TabsContent>
)}
</Tabs>
</AIProvidersProvider>
</PeersProvider>
Expand Down
8 changes: 8 additions & 0 deletions src/contexts/PermissionsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ const MODULE_KEYS: Array<keyof Permissions["modules"]> = [
"proxy",
"proxy_configuration",
"services",
"agent_network",
"agent_network.providers",
"agent_network.policies",
"agent_network.guardrails",
"agent_network.budgets",
"agent_network.usage",
"agent_network.logs",
"agent_network.settings",
];

const DENIED: Permission = {
Expand Down
9 changes: 9 additions & 0 deletions src/interfaces/Permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ export interface Permissions {
proxy_configuration: Permission;

services: Permission;

agent_network: Permission;
"agent_network.providers": Permission;
"agent_network.policies": Permission;
"agent_network.guardrails": Permission;
"agent_network.budgets": Permission;
"agent_network.usage": Permission;
"agent_network.logs": Permission;
"agent_network.settings": Permission;
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,6 @@ export enum Role {
BillingAdmin = "billing_admin",
Auditor = "auditor",
NetworkAdmin = "network_admin",
AgentNetworkAdmin = "agent_network_admin",
UsageViewer = "usage_viewer",
}
37 changes: 29 additions & 8 deletions src/layouts/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,38 +197,59 @@ export default function Navigation({
href={"/agent-network/providers"}
collapsible
exactPathMatch={false}
// Parent is visible when at least one child is permitted. All
// Agent Network pages guard on services.read, so the section
// tracks that (plus the feature gating).
visible={agentNetworkEnabled && permission?.services?.read}
// Parent is visible when at least one child is permitted.
// Each page tracks its agent_network submodule, so delegated
// roles (agent_network_admin, usage_viewer) see exactly the
// pages their grants cover.
visible={
agentNetworkEnabled &&
(permission?.["agent_network.providers"]?.read ||
permission?.["agent_network.policies"]?.read ||
permission?.["agent_network.usage"]?.read ||
permission?.["agent_network.logs"]?.read ||
permission?.["agent_network.settings"]?.read)
}
>
<SidebarItem
label="Providers"
isChild
href={"/agent-network/providers"}
exactPathMatch={true}
visible={agentNetworkEnabled && permission?.services?.read}
visible={
agentNetworkEnabled &&
permission?.["agent_network.providers"]?.read
}
/>
<SidebarItem
label="Policies"
isChild
href={"/agent-network/policies"}
exactPathMatch={true}
visible={agentNetworkEnabled && permission?.services?.read}
visible={
agentNetworkEnabled &&
permission?.["agent_network.policies"]?.read
}
/>
<SidebarItem
label="Usage & Logs"
isChild
href={"/agent-network/usage"}
exactPathMatch={true}
visible={agentNetworkEnabled && permission?.services?.read}
visible={
agentNetworkEnabled &&
(permission?.["agent_network.usage"]?.read ||
permission?.["agent_network.logs"]?.read)
}
/>
<SidebarItem
label="Configuration"
isChild
href={"/agent-network/configuration"}
exactPathMatch={true}
visible={agentNetworkEnabled && permission?.services?.read}
visible={
agentNetworkEnabled &&
permission?.["agent_network.settings"]?.read
}
/>
</SidebarItem>

Expand Down
Loading
Loading