mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a67a23fc1f | ||
|
|
5a5e04ad35 | ||
|
|
598ecb196d | ||
|
|
7c8352822d | ||
|
|
e4f5046ff2 | ||
|
|
d5f61efc1a | ||
|
|
3503141c56 | ||
|
|
a27e4654cf | ||
|
|
b9569f48d5 | ||
|
|
9694bb753d |
No files matched your search
@@ -39,7 +39,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: netbirdio/IronRDP
|
||||
latest: true
|
||||
tag: v0.0.2
|
||||
fileName: "*.ts"
|
||||
out-file-path: 'public/ironrdp-pkg'
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: netbirdio/IronRDP
|
||||
latest: true
|
||||
tag: v0.0.2
|
||||
fileName: "*.js"
|
||||
out-file-path: 'public/ironrdp-pkg'
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: netbirdio/IronRDP
|
||||
latest: true
|
||||
tag: v0.0.2
|
||||
fileName: "ironrdp_web_bg.wasm"
|
||||
out-file-path: 'public/ironrdp-pkg'
|
||||
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
[
|
||||
{
|
||||
"tag": "Preview",
|
||||
"text": "The new NetBird desktop app is here - now available as a 0.75 release candidate.",
|
||||
"link": "https://github.com/netbirdio/netbird/discussions/6483",
|
||||
"tag": "New",
|
||||
"text": "The new NetBird desktop app is here - now available in the v0.75 release.",
|
||||
"link": "https://netbird.io/knowledge-hub/netbird-v0-75-new-desktop-app",
|
||||
"linkText": "Learn more",
|
||||
"variant": "important",
|
||||
"isExternal": true,
|
||||
|
||||
@@ -433,3 +433,53 @@ export async function deleteUserByEmail(page: Page, email: string) {
|
||||
await deleteUserById(page, user.id);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent Network
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type AgentNetworkCatalogProvider = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type AgentNetworkProvider = {
|
||||
id: string;
|
||||
name: string;
|
||||
provider_id: string;
|
||||
};
|
||||
|
||||
/** List the Agent Network provider catalog (server-defined provider types). */
|
||||
export async function listAgentNetworkCatalog(
|
||||
page: Page,
|
||||
): Promise<AgentNetworkCatalogProvider[]> {
|
||||
return apiGet<AgentNetworkCatalogProvider[]>(
|
||||
page,
|
||||
"/agent-network/catalog/providers",
|
||||
);
|
||||
}
|
||||
|
||||
/** List connected Agent Network providers. */
|
||||
export async function listAgentNetworkProviders(
|
||||
page: Page,
|
||||
): Promise<AgentNetworkProvider[]> {
|
||||
return apiGet<AgentNetworkProvider[]>(page, "/agent-network/providers");
|
||||
}
|
||||
|
||||
/** Delete an Agent Network provider by ID. */
|
||||
export async function deleteAgentNetworkProviderById(page: Page, id: string) {
|
||||
await apiDelete(page, `/agent-network/providers/${id}`);
|
||||
}
|
||||
|
||||
/** Delete all Agent Network providers whose name starts with the prefix. */
|
||||
export async function deleteAgentNetworkProvidersByPrefix(
|
||||
page: Page,
|
||||
prefix: string,
|
||||
) {
|
||||
const providers = await listAgentNetworkProviders(page);
|
||||
for (const p of providers) {
|
||||
if (p.name.startsWith(prefix)) {
|
||||
await deleteAgentNetworkProviderById(page, p.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Agent Network Kimi (Moonshot AI) provider spec.
|
||||
*
|
||||
* Walks the Kimi provider lifecycle end to end against the real backend:
|
||||
* pick kimi_api from the catalog (prefilled host, catalog models with
|
||||
* pricing), connect it, then verify the Kimi-gated config surfaces in the
|
||||
* "Configure Your Agent" modal (Kimi CLI tab, Kimi backend option in the
|
||||
* Claude Code tab) that only render when a Kimi provider is connected.
|
||||
*
|
||||
* The kimi_api catalog entry ships with newer management builds. When the
|
||||
* backend under test predates it, the whole suite skips instead of failing —
|
||||
* same trade-off as the provider matrix in netbird's agent-network e2e
|
||||
* harness, which skips per-provider on missing credentials.
|
||||
*
|
||||
* The Agent Network menu is deployment-gated; the test build honors the
|
||||
* localStorage override (see testAgentNetworkOverride in utils/netbird.ts),
|
||||
* set via addInitScript on a dedicated context below.
|
||||
*/
|
||||
import { test, expect, type Browser, type Page } from "@playwright/test";
|
||||
import { loginToApp } from "../helpers/auth";
|
||||
import { generateRandomName } from "../helpers/utils";
|
||||
import {
|
||||
deleteAgentNetworkProvidersByPrefix,
|
||||
listAgentNetworkCatalog,
|
||||
} from "../helpers/api";
|
||||
|
||||
const AGENT_NETWORK_CONFIG_KEY = "netbird-test-agent-network";
|
||||
const KIMI_CATALOG_ID = "kimi_api";
|
||||
const KIMI_CATALOG_NAME = "Kimi (Moonshot AI) API";
|
||||
const PROVIDER_PREFIX = "e2e-kimi-";
|
||||
|
||||
async function newAgentNetworkPage(browser: Browser): Promise<{
|
||||
page: Page;
|
||||
close: () => Promise<void>;
|
||||
}> {
|
||||
const context = await browser.newContext({
|
||||
storageState: "e2e/fixtures/auth/owner.json",
|
||||
});
|
||||
await context.addInitScript(
|
||||
([key, value]) => {
|
||||
try {
|
||||
window.localStorage.setItem(key as string, value as string);
|
||||
} catch (e) {}
|
||||
},
|
||||
[AGENT_NETWORK_CONFIG_KEY, "enabled"],
|
||||
);
|
||||
const page = await context.newPage();
|
||||
await loginToApp(page, "owner");
|
||||
return { page, close: () => context.close() };
|
||||
}
|
||||
|
||||
test.describe.serial("Agent Network Kimi provider @agent-network", () => {
|
||||
test("connect a Kimi provider and see Kimi agent configs", async ({
|
||||
browser,
|
||||
}) => {
|
||||
const { page, close } = await newAgentNetworkPage(browser);
|
||||
try {
|
||||
// Backend support probe: skip the suite on management builds whose
|
||||
// catalog predates kimi_api.
|
||||
const catalog = await listAgentNetworkCatalog(page);
|
||||
test.skip(
|
||||
!catalog.some((c) => c.id === KIMI_CATALOG_ID),
|
||||
`management catalog has no ${KIMI_CATALOG_ID} entry`,
|
||||
);
|
||||
|
||||
await deleteAgentNetworkProvidersByPrefix(page, PROVIDER_PREFIX);
|
||||
|
||||
await page.goto("/agent-network/providers");
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
// ---- connect the provider ----
|
||||
await page
|
||||
.getByRole("button", { name: "Connect Provider" })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
// The provider select defaults to OpenAI API; search the catalog for
|
||||
// the Kimi entry.
|
||||
await page
|
||||
.getByRole("button", { name: /OpenAI API/ })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
await page.getByPlaceholder("Search providers...").fill("kimi");
|
||||
await page.getByText(KIMI_CATALOG_NAME).first().click({ force: true });
|
||||
|
||||
// Catalog default host lands in the upstream URL input.
|
||||
await expect(
|
||||
page.locator('input[value="https://api.moonshot.ai"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByPlaceholder("sk-...")
|
||||
.first()
|
||||
.fill("sk-e2e-kimi-test-key");
|
||||
|
||||
const providerName = generateRandomName(PROVIDER_PREFIX);
|
||||
const nameInput = page.locator(`input[value="${KIMI_CATALOG_NAME}"]`);
|
||||
await nameInput.fill(providerName);
|
||||
|
||||
// ---- models tab: catalog models carry pricing ----
|
||||
await page.getByRole("tab", { name: "Models" }).click({ force: true });
|
||||
await page.getByRole("button", { name: "Add More" }).click({ force: true });
|
||||
await page.getByText("Kimi K3", { exact: false }).first().click({ force: true });
|
||||
// Catalog price for kimi-k3 input tokens pre-fills the price cell.
|
||||
await expect(page.locator('input[value="0.003"]')).toBeVisible();
|
||||
|
||||
const createResponse = page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes("/agent-network/providers") &&
|
||||
resp.request().method() === "POST",
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
await page
|
||||
.getByRole("button", { name: "Connect Provider" })
|
||||
.last()
|
||||
.click({ force: true });
|
||||
expect([200, 201]).toContain((await createResponse).status());
|
||||
|
||||
// Row lands in the providers table.
|
||||
await expect(page.getByText(providerName).first()).toBeVisible();
|
||||
|
||||
// ---- Kimi-gated agent config surfaces ----
|
||||
await page
|
||||
.getByRole("button", { name: "Agent Config" })
|
||||
.click({ force: true });
|
||||
|
||||
// Kimi CLI tab only renders when a kimi_api provider is connected.
|
||||
await expect(page.getByRole("tab", { name: "Kimi CLI" })).toBeVisible();
|
||||
|
||||
// Claude Code tab's backend dropdown offers (and, with Kimi as the only
|
||||
// Anthropic-shaped provider, pre-selects) Kimi — its settings.json
|
||||
// snippet pins every model slot to kimi-k3 and disables tool search,
|
||||
// per Moonshot's Claude Code guide.
|
||||
await expect(page.getByText('"ANTHROPIC_MODEL": "kimi-k3"')).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('"CLAUDE_CODE_SUBAGENT_MODEL": "kimi-k3"'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('"ENABLE_TOOL_SEARCH": "false"'),
|
||||
).toBeVisible();
|
||||
// The base URL carries the /anthropic shape prefix that rides through
|
||||
// the endpoint to Moonshot's Anthropic surface.
|
||||
await expect(
|
||||
page.getByText(/"ANTHROPIC_BASE_URL": "https:\/\/[^"]+\/anthropic"/),
|
||||
).toBeVisible();
|
||||
|
||||
// Kimi CLI tab carries the ~/.kimi/config.toml provider block.
|
||||
await page.getByRole("tab", { name: "Kimi CLI" }).click({ force: true });
|
||||
await expect(page.getByText('default_model = "kimi-k3"')).toBeVisible();
|
||||
|
||||
// Codex has no Kimi variant — Kimi's upstream doesn't support Codex, so
|
||||
// the tab keeps the plain Responses-API config with no backend dropdown.
|
||||
await page.getByRole("tab", { name: "Codex" }).click({ force: true });
|
||||
await expect(page.getByText('wire_api = "responses"')).toBeVisible();
|
||||
await expect(page.getByText('wire_api = "chat"')).not.toBeVisible();
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
// ---- cleanup ----
|
||||
await deleteAgentNetworkProvidersByPrefix(page, PROVIDER_PREFIX);
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { VerticalTabs } from "@components/VerticalTabs";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { Gauge, ScrollText } from "lucide-react";
|
||||
import { ExternalLinkIcon, Gauge, ScrollText, ServerIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
||||
import AgentNetworkIcon from "@/assets/icons/AgentNetworkIcon";
|
||||
import GroupsProvider from "@/contexts/GroupsProvider";
|
||||
import PeersProvider from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { REVERSE_PROXY_CLUSTERS_DOCS_LINK } from "@/interfaces/ReverseProxy";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { useAgentNetworkMode } from "@/modules/agent-network/useAgentNetworkMode";
|
||||
import AgentAccountControlsCard from "@/modules/agent-network/AgentAccountControlsCard";
|
||||
import AgentBudgetRulesTable from "@/modules/agent-network/AgentBudgetRulesTable";
|
||||
import AIProvidersProvider from "@/modules/agent-network/AIProvidersProvider";
|
||||
|
||||
const ClustersTable = lazy(
|
||||
() => import("@/modules/reverse-proxy/clusters/ClustersTable"),
|
||||
);
|
||||
|
||||
const TAB_BUDGET_SETTINGS = "budget-settings";
|
||||
const TAB_LOG_SETTINGS = "log-settings";
|
||||
const TAB_CLUSTERS = "clusters";
|
||||
|
||||
// AgentNetworkConfigurationPage holds the configuration surfaces extracted from
|
||||
// Usage & Logs — global limits and log-collection controls — using the same
|
||||
@@ -27,6 +35,7 @@ const TAB_LOG_SETTINGS = "log-settings";
|
||||
// Settings page so it reads as "settings for the Agent Network".
|
||||
export default function AgentNetworkConfigurationPage() {
|
||||
const { permission } = usePermissions();
|
||||
const { only: agentNetworkOnly } = useAgentNetworkMode();
|
||||
const queryParams = useSearchParams();
|
||||
const queryTab = queryParams.get("tab");
|
||||
const [tab, setTab] = useState(queryTab ?? TAB_BUDGET_SETTINGS);
|
||||
@@ -47,6 +56,10 @@ export default function AgentNetworkConfigurationPage() {
|
||||
<ScrollText size={14} />
|
||||
Log Collection
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value={TAB_CLUSTERS}>
|
||||
<ServerIcon size={14} />
|
||||
Clusters
|
||||
</VerticalTabs.Trigger>
|
||||
</VerticalTabs.List>
|
||||
<RestrictedAccess
|
||||
page={"Configuration"}
|
||||
@@ -82,6 +95,27 @@ export default function AgentNetworkConfigurationPage() {
|
||||
<AgentAccountControlsCard />
|
||||
</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"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</ConfigTabHeader>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ClustersTable />
|
||||
</Suspense>
|
||||
</Tabs.Content>
|
||||
</div>
|
||||
</AIProvidersProvider>
|
||||
</PeersProvider>
|
||||
|
||||
@@ -7,6 +7,8 @@ import useFetchApi from "@utils/api";
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { RDPCertificateModal } from "@/modules/remote-access/rdp/RDPCertificateModal";
|
||||
import { RDPCredentialsModal } from "@/modules/remote-access/rdp/RDPCredentialsModal";
|
||||
import { useRDPQueryParams } from "@/modules/remote-access/rdp/useRDPQueryParams";
|
||||
@@ -138,6 +140,8 @@ function RDPSession({ peer }: Props) {
|
||||
domain: credentials.domain,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
dynamicResize:
|
||||
getOperatingSystem(peer?.os) === OperatingSystem.WINDOWS,
|
||||
});
|
||||
if (result === RDPStatus.CONNECTED) {
|
||||
connected.current = true;
|
||||
@@ -182,7 +186,18 @@ function RDPSession({ peer }: Props) {
|
||||
if (client.error) {
|
||||
sendErrorNotification("NetBird Client Error", client.error);
|
||||
}
|
||||
}, [rdp, client]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rdp.error, client.error]);
|
||||
|
||||
/**
|
||||
* Return to the credentials screen when an established session drops with an
|
||||
* error, so the user can see what happened and retry.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (connected.current && rdp.error) {
|
||||
reset();
|
||||
}
|
||||
}, [rdp.error, reset]);
|
||||
|
||||
/**
|
||||
* Close credentials modal when RDP is connected
|
||||
|
||||
@@ -6,6 +6,26 @@ import { createElement, useMemo } from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { useCountries } from "@/contexts/CountryProvider";
|
||||
|
||||
// browserCountryCode returns the ISO 3166-1 alpha-2 region from the browser's
|
||||
// preferred languages (e.g. "en-US" -> "US"), or undefined when none carries a
|
||||
// region. It reads navigator.languages, so it is client-only.
|
||||
function browserCountryCode(): string | undefined {
|
||||
if (typeof navigator === "undefined") return undefined;
|
||||
const langs = navigator.languages?.length
|
||||
? navigator.languages
|
||||
: [navigator.language];
|
||||
for (const lang of langs) {
|
||||
if (!lang) continue;
|
||||
try {
|
||||
const region = new Intl.Locale(lang).region;
|
||||
if (region) return region.toUpperCase();
|
||||
} catch {
|
||||
// Ignore malformed language tags and try the next one.
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
@@ -13,7 +33,13 @@ type Props = {
|
||||
popoverWidth?: "auto" | "content" | number;
|
||||
truncate?: boolean;
|
||||
};
|
||||
export const CountrySelector = ({ value, onChange, iconSize = 20, popoverWidth, truncate }: Props) => {
|
||||
export const CountrySelector = ({
|
||||
value,
|
||||
onChange,
|
||||
iconSize = 20,
|
||||
popoverWidth,
|
||||
truncate,
|
||||
}: Props) => {
|
||||
const { countries, isLoading } = useCountries();
|
||||
|
||||
const countryList = useMemo(() => {
|
||||
@@ -36,6 +62,20 @@ export const CountrySelector = ({ value, onChange, iconSize = 20, popoverWidth,
|
||||
}) as SelectOption[];
|
||||
}, [countries]);
|
||||
|
||||
// Surface the browser-detected country at the top so the common case is one
|
||||
// click away. Falls back to the original order when detection fails or the
|
||||
// code is not in the list.
|
||||
const orderedList = useMemo(() => {
|
||||
if (!countryList?.length) return countryList;
|
||||
const code = browserCountryCode();
|
||||
if (!code) return countryList;
|
||||
const index = countryList.findIndex((option) => option.value === code);
|
||||
if (index <= 0) return countryList;
|
||||
const reordered = countryList.slice();
|
||||
const [detected] = reordered.splice(index, 1);
|
||||
return [detected, ...reordered];
|
||||
}, [countryList]);
|
||||
|
||||
return (
|
||||
<div className={"block w-full"}>
|
||||
<SelectDropdown
|
||||
@@ -46,7 +86,7 @@ export const CountrySelector = ({ value, onChange, iconSize = 20, popoverWidth,
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
iconSize={iconSize}
|
||||
options={countryList || []}
|
||||
options={orderedList || []}
|
||||
popoverWidth={popoverWidth}
|
||||
truncate={truncate}
|
||||
/>
|
||||
|
||||
@@ -64,9 +64,11 @@ export default function PoliciesProvider({ children }: Props) {
|
||||
return created.id!;
|
||||
};
|
||||
|
||||
const sources = await Promise.all(
|
||||
(rule.sources ?? []).map(resolveGroup),
|
||||
).then((ids) => ids.filter(Boolean) as string[]);
|
||||
const sources = rule.sourceResource
|
||||
? undefined
|
||||
: await Promise.all(
|
||||
(rule.sources ?? []).map(resolveGroup),
|
||||
).then((ids) => ids.filter(Boolean) as string[]);
|
||||
|
||||
const destinations = rule.destinationResource
|
||||
? undefined
|
||||
|
||||
@@ -21,7 +21,7 @@ const ENDPOINT_EXISTS_ERROR_CODES = [401, 403, 405];
|
||||
* activation be picked up without forcing the user to clear storage.
|
||||
*/
|
||||
const LICENSE_CACHE_PREFIX = "netbird-licensed:";
|
||||
const LICENSE_CACHE_TTL_MS = 60 * 60 * 1000;
|
||||
const LICENSE_CACHE_TTL_MS = 60 * 10 * 1000;
|
||||
|
||||
const licenseCacheKey = () => `${LICENSE_CACHE_PREFIX}${loadConfig().apiOrigin}`;
|
||||
|
||||
|
||||
@@ -33,12 +33,20 @@ export const CrowdSecMode = {
|
||||
|
||||
export type CrowdSecMode = (typeof CrowdSecMode)[keyof typeof CrowdSecMode];
|
||||
|
||||
export const AllowMatch = {
|
||||
ALL: "all",
|
||||
ANY: "any",
|
||||
} as const;
|
||||
|
||||
export type AllowMatch = (typeof AllowMatch)[keyof typeof AllowMatch];
|
||||
|
||||
export interface AccessRestrictions {
|
||||
allowed_cidrs?: string[];
|
||||
blocked_cidrs?: string[];
|
||||
allowed_countries?: string[];
|
||||
blocked_countries?: string[];
|
||||
crowdsec_mode?: CrowdSecMode;
|
||||
allow_match?: AllowMatch;
|
||||
}
|
||||
|
||||
export interface ReverseProxyMeta {
|
||||
|
||||
@@ -113,6 +113,8 @@ function upstreamUrlPlaceholder(providerId: AIProviderId): string {
|
||||
return "https://api.portkey.ai";
|
||||
case "vllm":
|
||||
return "https://your-vllm-host:8000";
|
||||
case "kimi_api":
|
||||
return "https://api.moonshot.ai";
|
||||
case "custom":
|
||||
return "https://your-llm-host";
|
||||
default:
|
||||
@@ -217,6 +219,9 @@ export default function AIProviderModal({
|
||||
const [skipTlsVerification, setSkipTlsVerification] = useState<boolean>(
|
||||
provider?.skipTlsVerification ?? false,
|
||||
);
|
||||
const [metadataDisabled, setMetadataDisabled] = useState<boolean>(
|
||||
provider?.metadataDisabled ?? false,
|
||||
);
|
||||
|
||||
const catalog = getById(providerId);
|
||||
// Custom-kind providers (the generic "Custom" entry and named self-hosted
|
||||
@@ -262,7 +267,8 @@ export default function AIProviderModal({
|
||||
providerId === "bifrost" ||
|
||||
providerId === "cloudflare_ai_gateway" ||
|
||||
providerId === "vercel_ai_gateway" ||
|
||||
providerId === "openrouter";
|
||||
providerId === "openrouter" ||
|
||||
providerId === "bedrock_api";
|
||||
|
||||
// If the user flips provider type while viewing the Mappings tab and
|
||||
// the new type doesn't show mappings, snap back to the Provider tab
|
||||
@@ -340,6 +346,7 @@ export default function AIProviderModal({
|
||||
setIdentityHeaderUserId(provider.identityHeaderUserId ?? "");
|
||||
setIdentityHeaderGroups(provider.identityHeaderGroups ?? "");
|
||||
setSkipTlsVerification(provider.skipTlsVerification ?? false);
|
||||
setMetadataDisabled(provider.metadataDisabled ?? false);
|
||||
} else {
|
||||
const fallback = getById("openai_api");
|
||||
setProviderId("openai_api");
|
||||
@@ -354,6 +361,7 @@ export default function AIProviderModal({
|
||||
setIdentityHeaderUserId("");
|
||||
setIdentityHeaderGroups("");
|
||||
setSkipTlsVerification(false);
|
||||
setMetadataDisabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -405,6 +413,7 @@ export default function AIProviderModal({
|
||||
extraValues: sanitizedExtraValues,
|
||||
...identityOverrides,
|
||||
skipTlsVerification: isCustomKind ? skipTlsVerification : false,
|
||||
metadataDisabled,
|
||||
// Only forward the API key when the user actually rotated it
|
||||
...(apiKey && apiKey !== "••••••••" ? { apiKey } : {}),
|
||||
});
|
||||
@@ -420,6 +429,7 @@ export default function AIProviderModal({
|
||||
extraValues: sanitizedExtraValues,
|
||||
...identityOverrides,
|
||||
skipTlsVerification: isCustomKind ? skipTlsVerification : false,
|
||||
metadataDisabled,
|
||||
models,
|
||||
enabled: true,
|
||||
});
|
||||
@@ -609,7 +619,20 @@ export default function AIProviderModal({
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
label={"Upstream URL"}
|
||||
label={
|
||||
providerId === "kimi_api" ? (
|
||||
<>
|
||||
Upstream URL
|
||||
<HelpTooltip
|
||||
content={
|
||||
"Moonshot AI's international platform endpoint. Keep the bare host. Moonshot serves both API shapes with the same key: the path an agent calls rides through to Moonshot, so its base URL picks the shape (Claude Code appends /anthropic; Kimi CLI and OpenAI shaped callers use the bare endpoint). Mainland China accounts use api.moonshot.cn instead."
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
"Upstream URL"
|
||||
)
|
||||
}
|
||||
helpText={upstreamUrlHelpText(providerId)}
|
||||
>
|
||||
<Input
|
||||
@@ -730,7 +753,7 @@ export default function AIProviderModal({
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
customPrefix={<KeyRound size={14} />}
|
||||
placeholder={
|
||||
providerId === "openai_api"
|
||||
providerId === "openai_api" || providerId === "kimi_api"
|
||||
? "sk-..."
|
||||
: providerId === "anthropic_api"
|
||||
? "sk-ant-..."
|
||||
@@ -785,6 +808,23 @@ export default function AIProviderModal({
|
||||
{showMappings && providerId === "litellm_proxy" && (
|
||||
<TabsContent value={"mappings"} className={"pb-8"}>
|
||||
<div className={"px-8 pt-3 flex-col flex gap-4"}>
|
||||
{/* The forwarding toggle sits first: it gates the identity
|
||||
mappings described below, so turning it off makes the fixed
|
||||
mapping that follows moot. */}
|
||||
<FancyToggleSwitch
|
||||
value={!metadataDisabled}
|
||||
onChange={(v) => setMetadataDisabled(!v)}
|
||||
label={
|
||||
<>
|
||||
<ArrowRightLeft size={15} />
|
||||
Forward Identity Metadata
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Stamp the identity mappings below onto LiteLLM requests."
|
||||
}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Label>Identity Mappings</Label>
|
||||
<HelpText className={"mb-0"}>
|
||||
@@ -987,6 +1027,61 @@ export default function AIProviderModal({
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{showMappings && providerId === "bedrock_api" && (
|
||||
<TabsContent value={"mappings"} className={"pb-8"}>
|
||||
<div className={"px-8 pt-3 flex-col flex gap-4"}>
|
||||
{/* The forwarding toggle sits first: it gates the identity
|
||||
metadata described below, so turning it off makes the fixed
|
||||
mapping that follows moot. */}
|
||||
<FancyToggleSwitch
|
||||
value={!metadataDisabled}
|
||||
onChange={(v) => setMetadataDisabled(!v)}
|
||||
label={
|
||||
<>
|
||||
<ArrowRightLeft size={15} />
|
||||
Forward Identity Metadata
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Stamp the identity metadata below onto Bedrock requests."
|
||||
}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Label>Identity Metadata</Label>
|
||||
<HelpText className={"mb-0"}>
|
||||
NetBird stamps the caller's identity into the{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.aws.amazon.com/bedrock/latest/userguide/cost-mgmt-request-metadata.html"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
<code
|
||||
className={
|
||||
"text-xs font-mono bg-nb-gray-900/60 rounded px-1.5 py-0.5"
|
||||
}
|
||||
>
|
||||
X-Amzn-Bedrock-Request-Metadata
|
||||
</code>
|
||||
</InlineLink>{" "}
|
||||
header, so you can break Bedrock spend down by user and
|
||||
group. Client-supplied values are stripped and sanitized.
|
||||
</HelpText>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
"rounded-md overflow-hidden border border-nb-gray-900 bg-nb-gray-920/30"
|
||||
}
|
||||
>
|
||||
<MappingRow header={"user"} sourceLabel={"User Email"} />
|
||||
<MappingRow header={"group"} sourceLabel={"Groups"} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{showMappings && providerId === "vercel_ai_gateway" && (
|
||||
<TabsContent value={"mappings"} className={"pb-8"}>
|
||||
<div className={"px-8 pt-3 flex-col flex gap-4"}>
|
||||
@@ -1164,8 +1259,11 @@ export default function AIProviderModal({
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink href={"https://docs.netbird.io/"} target={"_blank"}>
|
||||
Agent Network
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/agent-network/providers"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Agent Network Providers
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
|
||||
@@ -50,6 +50,9 @@ export type APIProvider = {
|
||||
// Skip TLS certificate verification on upstream requests (custom providers
|
||||
// with self-signed certs). Off by default.
|
||||
skip_tls_verification?: boolean;
|
||||
// Disable identity metadata injection (caller user + authorizing group) on
|
||||
// upstream requests. Off by default (metadata is sent).
|
||||
metadata_disabled?: boolean;
|
||||
enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -67,6 +70,7 @@ export type APIProviderRequest = {
|
||||
identity_header_user_id?: string;
|
||||
identity_header_groups?: string;
|
||||
skip_tls_verification?: boolean;
|
||||
metadata_disabled?: boolean;
|
||||
models: APIProviderModel[];
|
||||
enabled?: boolean;
|
||||
};
|
||||
@@ -84,6 +88,8 @@ export type ProviderConnectInput = {
|
||||
identityHeaderGroups?: string;
|
||||
// Skip upstream TLS verification (custom providers with self-signed certs).
|
||||
skipTlsVerification?: boolean;
|
||||
// Disable identity metadata injection (user + authorizing group). Off by default.
|
||||
metadataDisabled?: boolean;
|
||||
models: ProviderModel[];
|
||||
enabled?: boolean;
|
||||
};
|
||||
@@ -97,6 +103,7 @@ export type ProviderUpdateInput = {
|
||||
identityHeaderUserId?: string;
|
||||
identityHeaderGroups?: string;
|
||||
skipTlsVerification?: boolean;
|
||||
metadataDisabled?: boolean;
|
||||
models?: ProviderModel[];
|
||||
enabled?: boolean;
|
||||
};
|
||||
@@ -159,6 +166,7 @@ function fromAPI(p: APIProvider): AIProvider {
|
||||
identityHeaderUserId: p.identity_header_user_id,
|
||||
identityHeaderGroups: p.identity_header_groups,
|
||||
skipTlsVerification: p.skip_tls_verification ?? false,
|
||||
metadataDisabled: p.metadata_disabled ?? false,
|
||||
status: p.enabled ? "active" : "disabled",
|
||||
models,
|
||||
allowedGroups: [],
|
||||
@@ -201,6 +209,7 @@ function toCreateRequest(input: ProviderConnectInput): APIProviderRequest {
|
||||
identity_header_user_id: input.identityHeaderUserId,
|
||||
identity_header_groups: input.identityHeaderGroups,
|
||||
skip_tls_verification: input.skipTlsVerification,
|
||||
metadata_disabled: input.metadataDisabled,
|
||||
models: toAPIModels(input.models),
|
||||
enabled: input.enabled,
|
||||
};
|
||||
@@ -647,6 +656,8 @@ export default function AIProvidersProvider({ children }: Readonly<Props>) {
|
||||
updates.identityHeaderGroups ?? existing.identity_header_groups,
|
||||
skip_tls_verification:
|
||||
updates.skipTlsVerification ?? existing.skip_tls_verification,
|
||||
metadata_disabled:
|
||||
updates.metadataDisabled ?? existing.metadata_disabled,
|
||||
models: updates.models
|
||||
? toAPIModels(updates.models)
|
||||
: existing.models ?? [],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Code from "@components/Code";
|
||||
import { Modal, ModalContent } from "@components/modal/Modal";
|
||||
import { useAIProviders } from "@/modules/agent-network/AIProvidersProvider";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SmallParagraph from "@components/SmallParagraph";
|
||||
import SquareIcon from "@components/SquareIcon";
|
||||
@@ -56,6 +57,7 @@ export function AgentConnectTabs({
|
||||
listClassName = "px-8",
|
||||
contentClassName = "px-6 py-2",
|
||||
defaultTab = "claude-code",
|
||||
providerIds = [],
|
||||
}: {
|
||||
endpoint: string;
|
||||
listClassName?: string;
|
||||
@@ -64,23 +66,39 @@ export function AgentConnectTabs({
|
||||
// matching tool (e.g. Anthropic → claude-code, OpenAI → curl). Keyed below
|
||||
// so a late-resolving defaultTab still re-initialises the tabs.
|
||||
defaultTab?: string;
|
||||
// Catalog ids of the account's connected providers. Kimi-specific config
|
||||
// surfaces (the Kimi CLI tab and the Kimi variant inside the Claude Code
|
||||
// tab) only render when a kimi_api provider is actually connected —
|
||||
// showing Moonshot setup against an endpoint that can't route to Kimi
|
||||
// would just be a trap.
|
||||
providerIds?: string[];
|
||||
}) {
|
||||
const baseUrl = `https://${endpoint}`;
|
||||
const openaiBase = `${baseUrl}/v1`;
|
||||
const hasKimi = providerIds.includes("kimi_api");
|
||||
const [claudeMode, setClaudeMode] = React.useState<"config" | "shell">(
|
||||
"config",
|
||||
);
|
||||
// Which backend the Claude Code config targets — Anthropic API direct, or
|
||||
// via Vertex AI / Bedrock. Switched in-tab instead of separate tabs.
|
||||
// Which backend the Claude Code config targets — Anthropic API direct,
|
||||
// via Vertex AI / Bedrock, or Kimi (Moonshot AI, whose upstream speaks the
|
||||
// Anthropic Messages API too). Switched in-tab instead of separate tabs.
|
||||
// When Kimi is the only Anthropic-shaped provider connected, it's the only
|
||||
// config that can work, so start there.
|
||||
const kimiOnlyAnthropicShape =
|
||||
hasKimi &&
|
||||
!["anthropic_api", "vertex_ai_api", "bedrock_api"].some((id) =>
|
||||
providerIds.includes(id),
|
||||
);
|
||||
const [claudeProvider, setClaudeProvider] = React.useState<
|
||||
"anthropic" | "vertex" | "bedrock"
|
||||
>("anthropic");
|
||||
"anthropic" | "vertex" | "bedrock" | "kimi"
|
||||
>(kimiOnlyAnthropicShape ? "kimi" : "anthropic");
|
||||
|
||||
return (
|
||||
<Tabs key={defaultTab} defaultValue={defaultTab} className={"mt-2"}>
|
||||
<TabsList justify={"start"} className={listClassName}>
|
||||
<TabsTrigger value={"claude-code"}>Claude Code</TabsTrigger>
|
||||
<TabsTrigger value={"codex"}>Codex</TabsTrigger>
|
||||
{hasKimi && <TabsTrigger value={"kimi-cli"}>Kimi CLI</TabsTrigger>}
|
||||
<TabsTrigger value={"openai-sdk"}>OpenAI SDK</TabsTrigger>
|
||||
<TabsTrigger value={"curl"}>cURL</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -91,15 +109,20 @@ export function AgentConnectTabs({
|
||||
<SelectDropdown
|
||||
value={claudeProvider}
|
||||
onChange={(v) =>
|
||||
setClaudeProvider(v as "anthropic" | "vertex" | "bedrock")
|
||||
setClaudeProvider(
|
||||
v as "anthropic" | "vertex" | "bedrock" | "kimi",
|
||||
)
|
||||
}
|
||||
options={[
|
||||
{ label: "Anthropic API", value: "anthropic" },
|
||||
{ label: "Vertex AI", value: "vertex" },
|
||||
{ label: "Bedrock", value: "bedrock" },
|
||||
...(hasKimi
|
||||
? [{ label: "Kimi (Moonshot AI)", value: "kimi" }]
|
||||
: []),
|
||||
]}
|
||||
showValues={false}
|
||||
className={"w-[160px]"}
|
||||
className={"!w-auto min-w-[160px]"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -176,6 +199,76 @@ export function AgentConnectTabs({
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{claudeProvider === "kimi" && (
|
||||
<>
|
||||
<div className={"flex items-center justify-between gap-3 mb-2"}>
|
||||
<SmallParagraph className={"!mb-0"}>
|
||||
{claudeMode === "config"
|
||||
? "Add to ~/.claude/settings.json:"
|
||||
: "Run in your shell:"}
|
||||
</SmallParagraph>
|
||||
<button
|
||||
type={"button"}
|
||||
onClick={() =>
|
||||
setClaudeMode(claudeMode === "config" ? "shell" : "config")
|
||||
}
|
||||
className={
|
||||
"shrink-0 mr-2 text-[11px] text-white hover:underline underline-offset-2 cursor-pointer"
|
||||
}
|
||||
>
|
||||
{claudeMode === "config" ? "Shell" : "JSON"}
|
||||
</button>
|
||||
</div>
|
||||
<Snippet
|
||||
// Claude Code speaks the Anthropic Messages API, which
|
||||
// Moonshot serves under the /anthropic path prefix. The
|
||||
// prefix goes in the agent's base URL and rides through the
|
||||
// endpoint to the bare https://api.moonshot.ai upstream, so
|
||||
// one Kimi provider serves both API shapes. Every model slot
|
||||
// Claude Code fills on its own (opus/sonnet/haiku tiers,
|
||||
// subagents) is pinned to kimi-k3 so no Claude model names
|
||||
// leak into requests the upstream can't serve, and tool
|
||||
// search is off because Moonshot rejects its tool_reference
|
||||
// blocks — both per Moonshot's Claude Code guide.
|
||||
lines={
|
||||
claudeMode === "config"
|
||||
? [
|
||||
`{`,
|
||||
` "apiKeyHelper": "echo '-'",`,
|
||||
` "env": {`,
|
||||
` "ANTHROPIC_BASE_URL": "${baseUrl}/anthropic",`,
|
||||
` "ANTHROPIC_MODEL": "kimi-k3",`,
|
||||
` "ANTHROPIC_DEFAULT_OPUS_MODEL": "kimi-k3",`,
|
||||
` "ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-k3",`,
|
||||
` "ANTHROPIC_DEFAULT_HAIKU_MODEL": "kimi-k3",`,
|
||||
` "CLAUDE_CODE_SUBAGENT_MODEL": "kimi-k3",`,
|
||||
` "ENABLE_TOOL_SEARCH": "false"`,
|
||||
` }`,
|
||||
`}`,
|
||||
]
|
||||
: [
|
||||
`export ANTHROPIC_BASE_URL=${baseUrl}/anthropic`,
|
||||
`export ANTHROPIC_API_KEY=none`,
|
||||
`export ANTHROPIC_MODEL=kimi-k3`,
|
||||
`export ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k3`,
|
||||
`export ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k3`,
|
||||
`export ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k3`,
|
||||
`export CLAUDE_CODE_SUBAGENT_MODEL=kimi-k3`,
|
||||
`export ENABLE_TOOL_SEARCH=false`,
|
||||
`claude`,
|
||||
]
|
||||
}
|
||||
/>
|
||||
<SmallParagraph className={"mt-3"}>
|
||||
Pairs with a Kimi provider keeping the default upstream URL{" "}
|
||||
<code className={"font-mono"}>https://api.moonshot.ai</code>.
|
||||
The <code className={"font-mono"}>/anthropic</code> suffix in
|
||||
the base URL rides through the endpoint to Moonshot, which
|
||||
serves the Anthropic Messages API under that path.
|
||||
</SmallParagraph>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -195,6 +288,38 @@ export function AgentConnectTabs({
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"kimi-cli"}>
|
||||
<div className={contentClassName} hidden={!hasKimi}>
|
||||
<Snippet
|
||||
// Kimi CLI reads providers from ~/.kimi/config.toml. Unlike
|
||||
// Claude Code, its "anthropic" provider type needs the bare
|
||||
// endpoint — no /anthropic prefix in base_url; api_key is a
|
||||
// placeholder since NetBird injects the real key server-side.
|
||||
caption={"Add to ~/.kimi/config.toml:"}
|
||||
lines={[
|
||||
`default_model = "kimi-k3"`,
|
||||
``,
|
||||
`[providers.netbird]`,
|
||||
`type = "anthropic"`,
|
||||
`base_url = "${baseUrl}"`,
|
||||
`api_key = "-"`,
|
||||
``,
|
||||
`[models.kimi-k3]`,
|
||||
`provider = "netbird"`,
|
||||
`model = "kimi-k3"`,
|
||||
`max_context_size = 1000000`,
|
||||
]}
|
||||
/>
|
||||
<SmallParagraph className={"mt-3"}>
|
||||
Pairs with a Kimi provider keeping the default upstream URL{" "}
|
||||
<code className={"font-mono"}>https://api.moonshot.ai</code>. For
|
||||
the OpenAI shape instead, use{" "}
|
||||
<code className={"font-mono"}>type = "openai_legacy"</code>{" "}
|
||||
with <code className={"font-mono"}>base_url = "{openaiBase}"</code>.
|
||||
</SmallParagraph>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"openai-sdk"}>
|
||||
<div className={contentClassName}>
|
||||
<Snippet
|
||||
@@ -243,6 +368,9 @@ export default function AgentConnectModal({
|
||||
onOpenChange,
|
||||
endpoint,
|
||||
}: Readonly<Props>) {
|
||||
// Rendered inside <AIProvidersProvider> (providers page); the connected
|
||||
// provider ids gate which per-tool config variants the tabs offer.
|
||||
const { providers } = useAIProviders();
|
||||
return (
|
||||
<Modal open={open} onOpenChange={onOpenChange}>
|
||||
<ModalContent maxWidthClass={"max-w-2xl"}>
|
||||
@@ -258,7 +386,10 @@ export default function AgentConnectModal({
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<AgentConnectTabs endpoint={endpoint} />
|
||||
<AgentConnectTabs
|
||||
endpoint={endpoint}
|
||||
providerIds={providers.map((p) => p.providerId)}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -132,6 +132,7 @@ const KNOWN_PROVIDER_IDS: AIProviderId[] = [
|
||||
"bedrock_api",
|
||||
"vertex_ai_api",
|
||||
"mistral_api",
|
||||
"kimi_api",
|
||||
"custom",
|
||||
];
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export type AIProviderId =
|
||||
| "bedrock_api"
|
||||
| "vertex_ai_api"
|
||||
| "mistral_api"
|
||||
| "kimi_api"
|
||||
| "litellm_proxy"
|
||||
| "portkey"
|
||||
| "bifrost"
|
||||
@@ -64,6 +65,9 @@ export type AIProvider = {
|
||||
// Skip TLS certificate verification on upstream requests — for custom
|
||||
// providers using self-signed certs. Off by default.
|
||||
skipTlsVerification?: boolean;
|
||||
// Disable identity metadata injection (caller user + authorizing group) on
|
||||
// upstream requests, e.g. AWS Bedrock cost-allocation. Off by default.
|
||||
metadataDisabled?: boolean;
|
||||
status: AIProviderStatus;
|
||||
models: ProviderModel[];
|
||||
allowedGroups: string[];
|
||||
|
||||
@@ -169,8 +169,11 @@ export default function AgentProvidersTable({
|
||||
learnMore={
|
||||
<>
|
||||
Learn more about
|
||||
<InlineLink href={"https://docs.netbird.io/"} target={"_blank"}>
|
||||
Agent Network
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/agent-network/providers"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Agent Network Providers
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
|
||||
@@ -70,8 +70,8 @@ export function IntegrationCard<T>({
|
||||
)}
|
||||
>
|
||||
<div className={"flex flex-col gap-4"}>
|
||||
<div className={"flex justify-between"}>
|
||||
<div className={"flex gap-4"}>
|
||||
<div className={"flex justify-between gap-3"}>
|
||||
<div className={"flex gap-4 min-w-0"}>
|
||||
<div
|
||||
className={
|
||||
"h-12 w-12 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70"
|
||||
@@ -84,25 +84,24 @@ export function IntegrationCard<T>({
|
||||
priority={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={""}>{name}</h3>
|
||||
<div className={"min-w-0"}>
|
||||
<h3 className={"truncate"}>{name}</h3>
|
||||
<InlineLink
|
||||
href={url.href}
|
||||
target={"_blank"}
|
||||
className={"text-sm font-light"}
|
||||
className={"text-sm font-light min-w-0 max-w-full"}
|
||||
variant={"faded"}
|
||||
>
|
||||
{url.title}
|
||||
<ExternalLinkIcon size={12} />
|
||||
<span className={"truncate"}>{url.title}</span>
|
||||
<ExternalLinkIcon size={12} className={"shrink-0"} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
</div>
|
||||
{!hideSwitch && (
|
||||
<div className={"flex items-center"}>
|
||||
<div className={"flex items-center shrink-0"}>
|
||||
<ToggleSwitch
|
||||
checked={switchState}
|
||||
onCheckedChange={onEnabledChange}
|
||||
className={"grow"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,9 +15,12 @@ type Props = {
|
||||
export const OnboardingAgentConfigure = ({ onBack, onNext }: Props) => {
|
||||
const { settings, providers } = useAIProviders();
|
||||
|
||||
// Open the tab that matches the connected provider: Anthropic speaks the
|
||||
// Claude Code config, everything else is OpenAI-shaped so default to cURL.
|
||||
const defaultTab = providers.some((p) => p.providerId === "anthropic_api")
|
||||
// Open the tab that matches the connected provider: Anthropic and Kimi
|
||||
// (whose upstream also speaks the Anthropic Messages API) get the Claude
|
||||
// Code config, everything else is OpenAI-shaped so default to cURL.
|
||||
const defaultTab = providers.some(
|
||||
(p) => p.providerId === "anthropic_api" || p.providerId === "kimi_api",
|
||||
)
|
||||
? "claude-code"
|
||||
: "curl";
|
||||
|
||||
@@ -42,6 +45,7 @@ export const OnboardingAgentConfigure = ({ onBack, onNext }: Props) => {
|
||||
listClassName={"px-0"}
|
||||
contentClassName={"px-0 py-2"}
|
||||
defaultTab={defaultTab}
|
||||
providerIds={providers.map((p) => p.providerId)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
|
||||
@@ -3,27 +3,31 @@ import { DropdownMenuItem } from "@components/DropdownMenu";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { CircleHelpIcon, MonitorIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { RDPTooltip } from "@/modules/remote-access/rdp/RDPTooltip";
|
||||
import { SSHCredentialsModal } from "@/modules/remote-access/ssh/SSHCredentialsModal";
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
isDropdown?: boolean;
|
||||
};
|
||||
|
||||
const RDP_SUPPORTED_OS = new Set([
|
||||
OperatingSystem.WINDOWS,
|
||||
OperatingSystem.LINUX,
|
||||
OperatingSystem.FREEBSD,
|
||||
]);
|
||||
|
||||
export const RDPButton = ({ peer, isDropdown = false }: Props) => {
|
||||
const [modal, setModal] = useState(false);
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const os = getOperatingSystem(peer?.os);
|
||||
if (!RDP_SUPPORTED_OS.has(os)) return null;
|
||||
|
||||
const disabled = !peer.connected || !permission.peers.update;
|
||||
const hasPermission = permission.peers.update;
|
||||
|
||||
const isWindows = getOperatingSystem(peer?.os) === OperatingSystem.WINDOWS;
|
||||
|
||||
const openRDPPage = () => {
|
||||
window.open(
|
||||
`/peer/rdp?id=${peer.id}`,
|
||||
@@ -33,40 +37,36 @@ export const RDPButton = ({ peer, isDropdown = false }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
isWindows && (
|
||||
<>
|
||||
<div>
|
||||
<RDPTooltip
|
||||
disabled={!disabled}
|
||||
hasPermission={hasPermission}
|
||||
side={isDropdown ? "left" : "top"}
|
||||
<div>
|
||||
<RDPTooltip
|
||||
disabled={!disabled}
|
||||
hasPermission={hasPermission}
|
||||
side={isDropdown ? "left" : "top"}
|
||||
>
|
||||
{isDropdown ? (
|
||||
<DropdownMenuItem
|
||||
onClick={openRDPPage}
|
||||
disabled={disabled}
|
||||
className={"w-full"}
|
||||
>
|
||||
{isDropdown ? (
|
||||
<DropdownMenuItem
|
||||
onClick={openRDPPage}
|
||||
disabled={disabled}
|
||||
className={"w-full"}
|
||||
>
|
||||
<div className={"flex gap-3 items-center w-full"}>
|
||||
<MonitorIcon size={14} className={"shrink-0"} />
|
||||
RDP
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={openRDPPage}
|
||||
disabled={disabled}
|
||||
>
|
||||
<MonitorIcon size={16} />
|
||||
RDP
|
||||
{disabled && <CircleHelpIcon size={12} />}
|
||||
</Button>
|
||||
)}
|
||||
</RDPTooltip>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
<div className={"flex gap-3 items-center w-full"}>
|
||||
<MonitorIcon size={14} className={"shrink-0"} />
|
||||
RDP
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={openRDPPage}
|
||||
disabled={disabled}
|
||||
>
|
||||
<MonitorIcon size={16} />
|
||||
RDP
|
||||
{disabled && <CircleHelpIcon size={12} />}
|
||||
</Button>
|
||||
)}
|
||||
</RDPTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,8 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
|
||||
import ModalHeader from "@components/modal/ModalHeader";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import {
|
||||
ChevronsLeftRightEllipsis,
|
||||
ExternalLinkIcon,
|
||||
@@ -38,7 +40,11 @@ export const RDPCredentialsModal = ({
|
||||
error,
|
||||
loading,
|
||||
}: Props) => {
|
||||
const [username, setUsername] = useState("Administrator");
|
||||
const defaultUsername =
|
||||
getOperatingSystem(peer?.os) === OperatingSystem.WINDOWS
|
||||
? "Administrator"
|
||||
: "root";
|
||||
const [username, setUsername] = useState(defaultUsername);
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const [port, setPort] = useState("3389");
|
||||
|
||||
Loaded 20 of 25 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user