From 04e74155cf8dc7a6db28bde728540a938014d65e Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Thu, 14 May 2026 11:10:09 +0300 Subject: [PATCH 01/10] feat(dashboard): open conversation detail in side sheet from agent overview fixes NV-7545 (#11123) Co-authored-by: Cursor Agent --- .../agents/recent-conversations-section.tsx | 57 +++++++++++-------- .../conversation-detail-sheet.tsx | 28 +++++++++ 2 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 apps/dashboard/src/components/conversations/conversation-detail-sheet.tsx diff --git a/apps/dashboard/src/components/agents/recent-conversations-section.tsx b/apps/dashboard/src/components/agents/recent-conversations-section.tsx index 00981631f3e..b6feb6add5e 100644 --- a/apps/dashboard/src/components/agents/recent-conversations-section.tsx +++ b/apps/dashboard/src/components/agents/recent-conversations-section.tsx @@ -1,7 +1,9 @@ +import { useState } from 'react'; import { RiArrowRightLine, RiCheckboxCircleFill, RiRobot2Line } from 'react-icons/ri'; import { Link } from 'react-router-dom'; import type { AgentResponse } from '@/api/agents'; import type { ConversationDto } from '@/api/conversations'; +import { ConversationDetailSheet } from '@/components/conversations/conversation-detail-sheet'; import { ConversationStatusBadge } from '@/components/conversations/conversation-status-badge'; import { ConversationsUpgradeCta } from '@/components/conversations/conversations-upgrade-cta'; import { SubscriberFallbackAvatar } from '@/components/conversations/subscriber-fallback-avatar'; @@ -20,6 +22,7 @@ type RecentConversationsSectionProps = { export function RecentConversationsSection({ agent }: RecentConversationsSectionProps) { const { currentEnvironment } = useEnvironment(); + const [activeConversationId, setActiveConversationId] = useState(null); const conversationsPath = currentEnvironment?.slug ? buildRoute(ROUTES.ACTIVITY_CONVERSATIONS, { environmentSlug: currentEnvironment.slug }) @@ -44,18 +47,31 @@ export function RecentConversationsSection({ agent }: RecentConversationsSection
{!IS_SELF_HOSTED || IS_ENTERPRISE ? ( - + ) : ( )}
+ + { + if (!open) { + setActiveConversationId(null); + } + }} + /> ); } -function RecentConversationsContent({ agent }: { agent: AgentResponse }) { - const { currentEnvironment } = useEnvironment(); +type RecentConversationsContentProps = { + agent: AgentResponse; + onSelectConversation: (conversationId: string) => void; +}; +function RecentConversationsContent({ agent, onSelectConversation }: RecentConversationsContentProps) { const { conversations, isLoading, isError } = useFetchConversations({ limit: RECENT_CONVERSATIONS_DISPLAY_LIMIT, filters: { agentId: agent.identifier }, @@ -89,7 +105,7 @@ function RecentConversationsContent({ agent }: { agent: AgentResponse }) {
    {conversations.map((conversation) => (
  • - +
  • ))}
@@ -98,21 +114,26 @@ function RecentConversationsContent({ agent }: { agent: AgentResponse }) { type RecentConversationItemProps = { conversation: ConversationDto; - environmentSlug: string | undefined; + onSelect: (conversationId: string) => void; }; -function RecentConversationItem({ conversation, environmentSlug }: RecentConversationItemProps) { +function RecentConversationItem({ conversation, onSelect }: RecentConversationItemProps) { const subscriber = getSubscriberLabel(conversation); const subscriberParticipant = (conversation.participants ?? []).find((p) => p.type === 'subscriber'); const subscriberAvatar = subscriberParticipant?.subscriber?.avatar; const isFailed = conversation.status === 'failed'; - const baseClassName = 'flex flex-col gap-1.5 px-3 py-2'; - const interactiveClassName = - 'group transition-colors hover:bg-neutral-50 focus-visible:bg-neutral-50 focus-visible:outline-none'; + const handleSelect = () => onSelect(conversation.identifier); - const content = ( - <> + return ( + ); } diff --git a/apps/dashboard/src/components/conversations/conversation-detail-sheet.tsx b/apps/dashboard/src/components/conversations/conversation-detail-sheet.tsx new file mode 100644 index 00000000000..069da1e6fe2 --- /dev/null +++ b/apps/dashboard/src/components/conversations/conversation-detail-sheet.tsx @@ -0,0 +1,28 @@ +import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/primitives/sheet'; +import { VisuallyHidden } from '@/components/primitives/visually-hidden'; +import { ConversationDetail } from './conversation-detail'; + +type ConversationDetailSheetProps = { + conversationId: string | null; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +}; + +export function ConversationDetailSheet({ conversationId, isOpen, onOpenChange }: ConversationDetailSheetProps) { + return ( + + e.preventDefault()} + > + + Conversation details + Details and timeline for the selected conversation. + + {conversationId ? ( + onOpenChange(false)} /> + ) : null} + + + ); +} From 87fa6ce587d4bd101d5694fbf825efbc2fff8d52 Mon Sep 17 00:00:00 2001 From: "cursor[bot]" <206951365+cursor[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 11:10:24 +0300 Subject: [PATCH 02/10] fix(root): resolve high systeminformation and langsmith vulnerabilities fixes NV-7657 (#11124) Co-authored-by: cursoragent Co-authored-by: Dima Grossman --- package.json | 4 ++-- pnpm-lock.yaml | 33 +++++++++++++++------------------ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 97d1592516e..11a9bd655ad 100644 --- a/package.json +++ b/package.json @@ -175,7 +175,7 @@ "postcss@<8.5.10": "^8.5.10", "proxy-agent@<6.3.1": "^6.3.1", "semver@>=7.0.0 <7.5.2": "^7.5.2", - "systeminformation@<5.31.0": "^5.31.3", + "systeminformation@<5.31.6": "^5.31.6", "tar": "7.5.13", "tar-fs": ">=3.1.1", "tough-cookie@<4.1.3": "^4.1.3", @@ -265,7 +265,7 @@ "path-to-regexp@>=8.0.0 <8.4.0": "^8.4.0", "svelte@<5.53.5": "5.53.5", "@sveltejs/kit@<=2.57.0": "^2.57.1", - "langsmith@<=0.5.18": "^0.5.19", + "langsmith@<0.6.0": "^0.6.3", "devalue@<5.6.4": "5.6.4", "picomatch@<2.3.2": "^2.3.2", "picomatch@>=4.0.0 <4.0.4": "^4.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d17f6ef4bbe..fffdc125b2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,7 +23,7 @@ overrides: postcss@<8.5.10: ^8.5.10 proxy-agent@<6.3.1: ^6.3.1 semver@>=7.0.0 <7.5.2: ^7.5.2 - systeminformation@<5.31.0: ^5.31.3 + systeminformation@<5.31.6: ^5.31.6 tar: 7.5.13 tar-fs: '>=3.1.1' tough-cookie@<4.1.3: ^4.1.3 @@ -113,7 +113,7 @@ overrides: path-to-regexp@>=8.0.0 <8.4.0: ^8.4.0 svelte@<5.53.5: 5.53.5 '@sveltejs/kit@<=2.57.0': ^2.57.1 - langsmith@<=0.5.18: ^0.5.19 + langsmith@<0.6.0: ^0.6.3 devalue@<5.6.4: 5.6.4 picomatch@<2.3.2: ^2.3.2 picomatch@>=4.0.0 <4.0.4: ^4.0.4 @@ -20624,8 +20624,8 @@ packages: resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} engines: {node: '>=16.0.0'} - langsmith@0.5.19: - resolution: {integrity: sha512-5tFoETuFMvGkbPGsINNlIE4Ab86CsPhdPOQZCGwNt/NX0h5NDKQLKOWS/G2XcRUBOQl4mCNbrayUvUTWaIRsCg==} + langsmith@0.6.3: + resolution: {integrity: sha512-pXrQ4/4myQvjFFOAUmt5pWRrLEZR20gzIJD7MNdUH+5/S5nLI4ZRBo/SYKC6coaYj9pYTfQdBIzcs+3kfJ5uDA==} peerDependencies: '@opentelemetry/api': '*' '@opentelemetry/exporter-trace-otlp-proto': '*' @@ -25510,8 +25510,8 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} - systeminformation@5.31.3: - resolution: {integrity: sha512-vX0eeI7oGIr79NLiJRWnK8SyxDjyiNOEanaQnHRNyb5ep8QcpD8QMDvrukdrxV4pV4AKjwUDfaypXnWHMC/65A==} + systeminformation@5.31.6: + resolution: {integrity: sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==} engines: {node: '>=8.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true @@ -33728,7 +33728,7 @@ snapshots: camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.5.19(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@4.78.1(encoding@0.1.13)(zod@4.3.5))(ws@8.20.0) + langsmith: 0.6.3(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@4.78.1(encoding@0.1.13)(zod@4.3.5))(ws@8.20.0) mustache: 4.2.0 p-queue: 6.6.2 p-retry: 4.6.2 @@ -33749,7 +33749,7 @@ snapshots: camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.5.19(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0) + langsmith: 0.6.3(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0) mustache: 4.2.0 p-queue: 6.6.2 uuid: 10.0.0 @@ -33768,7 +33768,7 @@ snapshots: camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.5.19(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@4.3.5))(ws@8.20.0) + langsmith: 0.6.3(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@4.3.5))(ws@8.20.0) mustache: 4.2.0 p-queue: 6.6.2 uuid: 10.0.0 @@ -35400,7 +35400,7 @@ snapshots: '@opentelemetry/host-metrics@0.35.5(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - systeminformation: 5.31.3 + systeminformation: 5.31.6 '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.0)': dependencies: @@ -51037,7 +51037,7 @@ snapshots: '@langchain/core': 1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0) '@langchain/langgraph': 1.1.4(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.1(zod@3.25.20))(zod@3.25.76) '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0)) - langsmith: 0.5.19(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0) + langsmith: 0.6.3(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0) uuid: 10.0.0 zod: 3.25.76 transitivePeerDependencies: @@ -51058,10 +51058,9 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 - langsmith@0.5.19(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@4.78.1(encoding@0.1.13)(zod@4.3.5))(ws@8.20.0): + langsmith@0.6.3(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@4.78.1(encoding@0.1.13)(zod@4.3.5))(ws@8.20.0): dependencies: p-queue: 6.6.2 - uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/exporter-trace-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) @@ -51069,10 +51068,9 @@ snapshots: openai: 4.78.1(encoding@0.1.13)(zod@4.3.5) ws: 8.20.0 - langsmith@0.5.19(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0): + langsmith@0.6.3(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0): dependencies: p-queue: 6.6.2 - uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/exporter-trace-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) @@ -51080,10 +51078,9 @@ snapshots: openai: 6.17.0(ws@8.20.0)(zod@3.25.20) ws: 8.20.0 - langsmith@0.5.19(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@4.3.5))(ws@8.20.0): + langsmith@0.6.3(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@4.3.5))(ws@8.20.0): dependencies: p-queue: 6.6.2 - uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/exporter-trace-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) @@ -56981,7 +56978,7 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - systeminformation@5.31.3: {} + systeminformation@5.31.6: {} table-layout@1.0.2: dependencies: From 6cc3c55b59a44c80345ba00323d868fab434c59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Tymczuk?= Date: Thu, 14 May 2026 10:46:40 +0200 Subject: [PATCH 03/10] chore(dashboard): add agent modal improvements fixes NV-7632 (#11110) --- .../agents/dtos/agent-runtime-config.dto.ts | 7 + .../src/app/agents/e2e/managed-agent.e2e.ts | 137 ++++ .../create-agent/create-agent.usecase.ts | 1 + .../provision-managed-agent.command.ts | 7 + .../provision-managed-agent.usecase.ts | 20 +- apps/dashboard/src/api/agents.ts | 30 +- apps/dashboard/src/api/integrations.ts | 9 +- .../src/components/agents/agents-list.tsx | 103 ++- .../components/agents/create-agent-dialog.tsx | 670 ++++++++++++++---- .../dashboard/src/components/icons/claude.tsx | 10 + .../dashboard/src/components/icons/google.tsx | 22 + .../anthropic-agent-runtime.provider.ts | 16 + .../i-agent-runtime-provider.ts | 10 + 13 files changed, 888 insertions(+), 154 deletions(-) create mode 100644 apps/dashboard/src/components/icons/claude.tsx create mode 100644 apps/dashboard/src/components/icons/google.tsx diff --git a/apps/api/src/app/agents/dtos/agent-runtime-config.dto.ts b/apps/api/src/app/agents/dtos/agent-runtime-config.dto.ts index 335a8ea1fa2..ff20fabe4f7 100644 --- a/apps/api/src/app/agents/dtos/agent-runtime-config.dto.ts +++ b/apps/api/src/app/agents/dtos/agent-runtime-config.dto.ts @@ -148,6 +148,13 @@ export class ManagedRuntimeDto { @IsString() externalAgentId?: string; + @ApiPropertyOptional({ + description: 'ID of an existing environment on the provider platform. When set, Novu adopts the environment.', + }) + @IsOptional() + @IsString() + externalEnvironmentId?: string; + @ApiPropertyOptional() @IsOptional() @IsString() diff --git a/apps/api/src/app/agents/e2e/managed-agent.e2e.ts b/apps/api/src/app/agents/e2e/managed-agent.e2e.ts index 44c763d0ab5..9a840b2e830 100644 --- a/apps/api/src/app/agents/e2e/managed-agent.e2e.ts +++ b/apps/api/src/app/agents/e2e/managed-agent.e2e.ts @@ -21,6 +21,7 @@ const FAKE_EXTERNAL_AGENT_ID = 'ext-agent-e2e-123'; const FAKE_ADOPT_AGENT_ID = 'agent_01XJ5AdoptE2E'; const FAKE_ADOPT_AGENT_NAME = 'My Existing Claude Agent'; const FAKE_EXTERNAL_ENV_ID = 'env_01XJ5FakeEnvE2E'; +const FAKE_NEW_EXTERNAL_ENV_ID = 'env_01XJ5NewEnvE2E'; const agentRepository = new AgentRepository(); const integrationRepository = new IntegrationRepository(); @@ -33,6 +34,7 @@ function buildMockProvider(overrides: Partial> = createAgent: sinon.stub().resolves({ externalAgentId: FAKE_EXTERNAL_AGENT_ID }), deleteAgent: sinon.stub().resolves(), getAgent: sinon.stub().resolves({ externalAgentId: FAKE_ADOPT_AGENT_ID, name: FAKE_ADOPT_AGENT_NAME }), + getEnvironment: sinon.stub().resolves({ id: FAKE_EXTERNAL_ENV_ID, name: 'Default Env' }), getConfig: sinon.stub().resolves({ model: 'claude-3-5-sonnet-20241022', systemPrompt: '', @@ -356,6 +358,141 @@ describe('Managed Agents API #novu-v2', () => { }); }); + // ─── POST /v1/agents — externalEnvironmentId rebinding ───────────────────── + // When the caller supplies a managedRuntime.externalEnvironmentId that + // differs from the integration's stored value, the use-case calls the + // provider's getEnvironment() and persists the *canonical id returned by the + // provider* back into the integration credentials (not the raw input). When + // the input matches the stored value, both the lookup and the update are + // skipped. When the provider rejects the lookup (e.g. unknown env id), no + // mutation happens and the agent creation is rolled back. + describe('POST /v1/agents — externalEnvironmentId rebinding', () => { + it('should persist the canonical id returned by the provider, not the raw input id', async () => { + const integrationId = await createAgentRuntimeIntegration(); + const identifier = `e2e-rebind-env-${Date.now()}`; + createdAgentIdentifiers.push(identifier); + + const inputEnvId = 'my-prod-env'; + mockProvider.getEnvironment.resolves({ id: FAKE_NEW_EXTERNAL_ENV_ID, name: 'Production' }); + + const res = await session.testAgent.post('/v1/agents').send( + managedBody(identifier, integrationId, { + managedRuntime: { + providerId: AgentRuntimeProviderIdEnum.Anthropic, + integrationId, + externalEnvironmentId: inputEnvId, + }, + }) + ); + + expect(res.status).to.equal(201); + expect(mockProvider.getEnvironment.calledOnce, 'getEnvironment should be called once').to.be.true; + expect(mockProvider.getEnvironment.getCall(0).args[0]).to.equal(inputEnvId); + + const integration = await integrationRepository.findOne( + { + _id: integrationId, + _environmentId: session.environment._id, + _organizationId: session.organization._id, + }, + ['credentials'] + ); + + if (!integration) throw new Error('integration should exist after rebinding'); + const decrypted = decryptCredentials(integration.credentials); + + expect(decrypted.externalEnvironmentId, 'stored value should be the provider canonical id').to.equal( + FAKE_NEW_EXTERNAL_ENV_ID + ); + expect(decrypted.externalEnvironmentId, 'stored value should NOT be the raw input id').to.not.equal(inputEnvId); + expect(decrypted.apiKey, 'apiKey must remain intact and decryptable').to.equal(FAKE_API_KEY); + }); + + it('should NOT call getEnvironment and NOT mutate credentials when externalEnvironmentId matches the stored value', async () => { + const integrationId = await createAgentRuntimeIntegration(); + const identifier = `e2e-rebind-noop-${Date.now()}`; + createdAgentIdentifiers.push(identifier); + + const res = await session.testAgent.post('/v1/agents').send( + managedBody(identifier, integrationId, { + managedRuntime: { + providerId: AgentRuntimeProviderIdEnum.Anthropic, + integrationId, + externalEnvironmentId: FAKE_EXTERNAL_ENV_ID, + }, + }) + ); + + expect(res.status).to.equal(201); + expect(mockProvider.getEnvironment.called, 'getEnvironment should NOT be called when env id is unchanged').to.be + .false; + + const integration = await integrationRepository.findOne( + { + _id: integrationId, + _environmentId: session.environment._id, + _organizationId: session.organization._id, + }, + ['credentials'] + ); + + if (!integration) throw new Error('integration should exist for no-op test'); + const decrypted = decryptCredentials(integration.credentials); + expect(decrypted.externalEnvironmentId).to.equal(FAKE_EXTERNAL_ENV_ID); + }); + + it('should return 409 AGENT_RUNTIME_DRIFT and leave credentials untouched when getEnvironment rejects with not found', async () => { + const integrationId = await createAgentRuntimeIntegration(); + const identifier = `e2e-rebind-invalid-${Date.now()}`; + + mockProvider.getEnvironment.rejects( + new AgentRuntimeNotFoundError('Environment not found on provider', AgentRuntimeProviderIdEnum.Anthropic) + ); + + const res = await session.testAgent.post('/v1/agents').send( + managedBody(identifier, integrationId, { + managedRuntime: { + providerId: AgentRuntimeProviderIdEnum.Anthropic, + integrationId, + externalEnvironmentId: 'env_does_not_exist', + }, + }) + ); + + expect(res.status).to.equal(409); + expect(res.body.code).to.equal('AGENT_RUNTIME_DRIFT'); + expect(mockProvider.getEnvironment.calledOnce).to.be.true; + expect(mockProvider.createAgent.called, 'createAgent must not run when env lookup fails').to.be.false; + + const integration = await integrationRepository.findOne( + { + _id: integrationId, + _environmentId: session.environment._id, + _organizationId: session.organization._id, + }, + ['credentials'] + ); + + if (!integration) throw new Error('integration should still exist after env lookup failure'); + const decrypted = decryptCredentials(integration.credentials); + + expect(decrypted.externalEnvironmentId, 'credentials must not be mutated when env lookup fails').to.equal( + FAKE_EXTERNAL_ENV_ID + ); + + const leftover = await agentRepository.findOne( + { + identifier, + _environmentId: session.environment._id, + _organizationId: session.organization._id, + }, + ['_id'] + ); + + expect(leftover, 'agent document should be rolled back when env lookup fails').to.equal(null); + }); + }); + // ─── GET /v1/agents/:identifier/runtime/config ────────────────────────────── describe('GET /v1/agents/:identifier/runtime/config', () => { diff --git a/apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts b/apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts index 8b5ea8d1b8d..4fd37f2968b 100644 --- a/apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts +++ b/apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts @@ -81,6 +81,7 @@ export class CreateAgent { Object.assign(new ProvisionManagedAgentCommand(), { agentId: created._id, name: command.name, + externalEnvironmentId: managedRuntime.externalEnvironmentId, externalAgentId: managedRuntime.externalAgentId, providerId: managedRuntime.providerId, integrationId: managedRuntime.integrationId, diff --git a/apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts b/apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts index 261d918bb20..dbd81b80ba2 100644 --- a/apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts +++ b/apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts @@ -20,6 +20,13 @@ export class ProvisionManagedAgentCommand { @IsString() externalAgentId?: string; + /** + * When set, the usecase adopts this existing provider environment. + */ + @IsOptional() + @IsString() + externalEnvironmentId?: string; + @IsNotEmpty() @IsEnum(AgentRuntimeProviderIdEnum) providerId: AgentRuntimeProviderIdEnum; diff --git a/apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts b/apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts index 02411be680f..ae249e56b4f 100644 --- a/apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts +++ b/apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; -import { decryptCredentials, getAgentRuntimeProvider, PinoLogger } from '@novu/application-generic'; +import { decryptCredentials, encryptCredentials, getAgentRuntimeProvider, PinoLogger } from '@novu/application-generic'; import { AgentRepository, IntegrationRepository } from '@novu/dal'; import type { ClientSession } from 'mongoose'; import { resolveMcpServersById } from '../../utils/resolve-mcp-servers'; @@ -58,6 +58,24 @@ export class ProvisionManagedAgent { const runtimeProvider = getAgentRuntimeProvider(command.providerId, resolvedApiKey); + if (command.externalEnvironmentId && command.externalEnvironmentId !== decryptedCredentials.externalEnvironmentId) { + const providerEnvironment = await runtimeProvider.getEnvironment(command.externalEnvironmentId); + const nextCredentials = encryptCredentials({ + ...decryptedCredentials, + externalEnvironmentId: providerEnvironment.id, + }); + + await this.integrationRepository.update( + { + _id: resolvedIntegrationId, + _environmentId: command.environmentId, + _organizationId: command.organizationId, + }, + { $set: { credentials: nextCredentials } }, + session ? { session } : {} + ); + } + let externalAgentId: string; let adoptedName: string | undefined; diff --git a/apps/dashboard/src/api/agents.ts b/apps/dashboard/src/api/agents.ts index 02417eab10e..4e4e2283aed 100644 --- a/apps/dashboard/src/api/agents.ts +++ b/apps/dashboard/src/api/agents.ts @@ -1,4 +1,11 @@ -import type { ChannelTypeEnum, DirectionEnum, IEnvironment } from '@novu/shared'; +import type { + AgentCreationSourceEnum, + AgentRuntime, + AgentRuntimeProviderIdEnum, + ChannelTypeEnum, + DirectionEnum, + IEnvironment, +} from '@novu/shared'; import { del, get, patch, post } from '@/api/api.client'; /** Root segment for TanStack Query keys; use with {@link getAgentsListQueryKey}. */ @@ -64,11 +71,32 @@ export type ListAgentsResponse = { totalCountCapped: boolean; }; +type AgentSkillInputDto = { + type: 'anthropic' | 'custom'; + skillId: string; + version?: string | null; +}; + +type ManagedRuntimeDto = { + providerId: AgentRuntimeProviderIdEnum; + integrationId: string; + externalAgentId?: string; + externalEnvironmentId?: string; + model?: string; + systemPrompt?: string; + tools?: string[]; + mcpServers?: string[]; + skills?: AgentSkillInputDto[]; +}; + export type CreateAgentBody = { name: string; identifier: string; description?: string; active?: boolean; + runtime?: AgentRuntime; + managedRuntime?: ManagedRuntimeDto; + creationSource?: AgentCreationSourceEnum; }; export type UpdateAgentBody = { diff --git a/apps/dashboard/src/api/integrations.ts b/apps/dashboard/src/api/integrations.ts index d79ff96e5ea..8734ba94bc0 100644 --- a/apps/dashboard/src/api/integrations.ts +++ b/apps/dashboard/src/api/integrations.ts @@ -1,4 +1,4 @@ -import { ChannelTypeEnum, IEnvironment, IIntegration } from '@novu/shared'; +import { ChannelTypeEnum, IEnvironment, IIntegration, IntegrationKindEnum } from '@novu/shared'; import { del, get, post, put } from './api.client'; export type HealthCheckStatus = 'ready' | 'pending' | 'failed'; @@ -13,14 +13,15 @@ export type MsTeamsHealthCheckResult = { export type CreateIntegrationData = { providerId: string; - channel: ChannelTypeEnum; + channel?: ChannelTypeEnum; + kind?: IntegrationKindEnum; credentials: Record; - configurations: Record; + configurations?: Record; name: string; identifier?: string; active: boolean; primary?: boolean; - _environmentId: string; + _environmentId?: string; }; export enum CheckIntegrationResponseEnum { diff --git a/apps/dashboard/src/components/agents/agents-list.tsx b/apps/dashboard/src/components/agents/agents-list.tsx index af518be40b3..ac71d4d8fa0 100644 --- a/apps/dashboard/src/components/agents/agents-list.tsx +++ b/apps/dashboard/src/components/agents/agents-list.tsx @@ -1,4 +1,11 @@ -import { DirectionEnum, EnvironmentTypeEnum, PermissionsEnum } from '@novu/shared'; +import { + AgentRuntimeProviderIdEnum, + CLAUDE_BUILTIN_TOOLS, + DirectionEnum, + EnvironmentTypeEnum, + IntegrationKindEnum, + PermissionsEnum, +} from '@novu/shared'; import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { RiArrowRightSLine, RiRobot2Line } from 'react-icons/ri'; @@ -13,10 +20,11 @@ import { listAgents, } from '@/api/agents'; import { NovuApiError } from '@/api/api.client'; +import { deleteIntegration } from '@/api/integrations'; import { AgentsEmptyTeaser } from '@/components/agents/agents-empty-teaser'; import { AgentsProductionEmptyState } from '@/components/agents/agents-production-empty-state'; import { AgentsTable } from '@/components/agents/agents-table'; -import { CreateAgentDialog } from '@/components/agents/create-agent-dialog'; +import { CreateAgentDialog, CreateAgentForm } from '@/components/agents/create-agent-dialog'; import { DeleteAgentDialog } from '@/components/agents/delete-agent-dialog'; import { ListNoResults } from '@/components/list-no-results'; import { Button } from '@/components/primitives/button'; @@ -26,6 +34,7 @@ import { showErrorToast, showSuccessToast } from '@/components/primitives/sonner import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/primitives/tooltip'; import { requireEnvironment, useEnvironment } from '@/context/environment/hooks'; import { useAgentRoutes } from '@/hooks/use-agent-routes'; +import { useCreateIntegration } from '@/hooks/use-create-integration'; import { useCurrentApp } from '@/hooks/use-current-app'; import { useHasPermission } from '@/hooks/use-has-permission'; import { useTelemetry } from '@/hooks/use-telemetry'; @@ -129,9 +138,7 @@ export function AgentsList() { mutationFn: (body: CreateAgentBody) => createAgent(requireEnvironment(currentEnvironment, 'No environment selected'), body), onSuccess: async (createdAgent) => { - await queryClient.invalidateQueries({ queryKey: [AGENTS_LIST_QUERY_KEY] }); showSuccessToast('Agent created', 'Your agent is ready to use.'); - handleCreateOpenChange(false); track( isDispatchApp @@ -151,6 +158,8 @@ export function AgentsList() { })}${location.search}`; navigate(agentDetailsPath); + queryClient.invalidateQueries({ queryKey: [AGENTS_LIST_QUERY_KEY] }); + setCreateOpen(false); }, onError: (err: Error) => { const message = err instanceof NovuApiError ? err.message : 'Could not create agent.'; @@ -159,6 +168,8 @@ export function AgentsList() { }, }); + const { mutateAsync: createIntegration, isPending: isCreatingIntegration } = useCreateIntegration(); + const deleteMutation = useMutation({ mutationFn: (identifier: string) => deleteAgent(requireEnvironment(currentEnvironment, 'No environment selected'), identifier), @@ -238,10 +249,84 @@ export function AgentsList() { }, []); const handleCreateSubmit = useCallback( - async (body: CreateAgentBody) => { - await createMutation.mutateAsync(body); + async ({ + name, + identifier, + instructions, + apiKey, + externalAgentId, + externalEnvironmentId, + runtime, + isExistingMode, + }: CreateAgentForm) => { + if (runtime === 'scratch') { + const request: CreateAgentBody = { + name, + identifier, + description: instructions, + }; + + await createMutation.mutateAsync(request); + } else if (runtime === 'claude') { + const request: CreateAgentBody = { + name, + identifier, + }; + + let integrationId: string; + + try { + const { data: integration } = await createIntegration({ + active: true, + kind: IntegrationKindEnum.AGENT, + providerId: AgentRuntimeProviderIdEnum.Anthropic, + credentials: { apiKey }, + name, + identifier, + }); + + integrationId = integration._id; + } catch (err) { + const message = err instanceof NovuApiError ? err.message : 'Could not create integration.'; + + showErrorToast(message, 'Create failed'); + + return; + } + + if (isExistingMode) { + request.runtime = 'managed'; + request.managedRuntime = { + integrationId, + providerId: AgentRuntimeProviderIdEnum.Anthropic, + externalAgentId, + externalEnvironmentId, + }; + } else { + request.runtime = 'managed'; + request.managedRuntime = { + integrationId, + providerId: AgentRuntimeProviderIdEnum.Anthropic, + model: 'claude-opus-4-5', + systemPrompt: instructions || undefined, + tools: CLAUDE_BUILTIN_TOOLS.map((tool) => tool.type), + }; + } + + const environment = requireEnvironment(currentEnvironment, 'No environment selected'); + + createMutation.mutate(request, { + onError: async () => { + try { + await deleteIntegration({ id: integrationId, environment }); + } catch { + // Best-effort cleanup; the global onError toast already informed the user. + } + }, + }); + } }, - [createMutation] + [createMutation, createIntegration, currentEnvironment] ); if (!canReadAgents) { @@ -374,9 +459,9 @@ export function AgentsList() { open={createOpen} onOpenChange={handleCreateOpenChange} onSubmit={handleCreateSubmit} - isSubmitting={createMutation.isPending} + isSubmitting={createMutation.isPending || isCreatingIntegration} initialName={memoizedInitialName} - initialDescription={memoizedInitialDescription} + initialInstructions={memoizedInitialDescription} /> word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); -} +const ANTHROPIC_API_KEY_HREF = 'https://console.anthropic.com/settings/keys'; +const CLAUDE_AGENT_ID_HREF = 'https://docs.claude.com/en/api/agents-list'; +const CLAUDE_ENVIRONMENT_ID_HREF = 'https://docs.claude.com/en/api/agents-list'; -type CreateAgentDialogProps = { - open: boolean; - onOpenChange: (open: boolean) => void; - onSubmit: (body: CreateAgentBody) => Promise; - isSubmitting: boolean; - initialName?: string; - initialDescription?: string; +type RuntimeType = 'scratch' | 'claude' | 'vertex'; + +type AgentTemplate = { + label: string; + name: string; + instructions: string; }; +const AGENT_TEMPLATES: AgentTemplate[] = [ + { + label: 'Customer Support', + name: 'Customer Support Agent', + instructions: + 'You are a helpful customer support assistant. Answer questions clearly and concisely, and escalate complex issues when needed.', + }, + { + label: 'DevOps Buddy', + name: 'DevOps Buddy', + instructions: + 'You are a DevOps assistant. Help with CI/CD pipelines, infrastructure troubleshooting, and deployment best practices.', + }, + { + label: 'Code Reviewer', + name: 'Code Reviewer', + instructions: + 'You are a senior code reviewer. Provide constructive feedback on code quality, security, and maintainability.', + }, + { + label: 'Docs Helper', + name: 'Docs Helper', + instructions: + 'You are a documentation assistant. Help users find information, clarify concepts, and cite sources accurately.', + }, +]; + +type CreateAgentMode = 'create' | 'existing'; + type FormErrors = { name?: string; identifier?: string; + apiKey?: string; + externalAgentId?: string; + externalEnvironmentId?: string; }; function RequiredFieldLabel({ htmlFor, children }: { htmlFor: string; children: ReactNode }) { @@ -47,38 +89,99 @@ function RequiredFieldLabel({ htmlFor, children }: { htmlFor: string; children: ); } +type RuntimeCardProps = { + selected: boolean; + onClick: () => void; + disabled?: boolean; + icon: ReactNode; + title: string; + description: string; +}; + +function RuntimeCard({ selected, onClick, disabled, icon, title, description }: RuntimeCardProps) { + return ( + + ); +} + +export type CreateAgentForm = { + name: string; + identifier: string; + instructions: string; + apiKey: string; + runtime: RuntimeType; + isExistingMode: boolean; + externalAgentId?: string; + externalEnvironmentId?: string; +}; + +type CreateAgentDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (body: CreateAgentForm) => Promise; + isSubmitting: boolean; + initialName?: string; + initialInstructions?: string; +}; + export function CreateAgentDialog({ open, onOpenChange, onSubmit, isSubmitting, initialName, - initialDescription, + initialInstructions, }: CreateAgentDialogProps) { const formId = useId(); const nameId = `${formId}-name`; const identifierId = `${formId}-identifier`; - const descriptionId = `${formId}-description`; - - const { currentOrganization } = useAuth(); - - const { namePlaceholder, identifierPlaceholder } = useMemo(() => { - const trimmedOrgName = currentOrganization?.name?.trim() ?? ''; - const displayOrgName = trimmedOrgName ? capitalizeOrgName(trimmedOrgName) : DEFAULT_AGENT_NAME_PLACEHOLDER_ORG; - const slugOrgName = slugify(displayOrgName) || slugify(DEFAULT_AGENT_NAME_PLACEHOLDER_ORG); - - return { - namePlaceholder: `e.g. ${displayOrgName} Copilot`, - identifierPlaceholder: `e.g. ${slugOrgName}-copilot`, - }; - }, [currentOrganization?.name]); + const instructionsId = `${formId}-instructions`; + const apiKeyId = `${formId}-api-key`; + const isManagedEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_MANAGED_AGENT_RUNTIME_ENABLED, false); + const [runtime, setRuntime] = useState('scratch'); + const [mode, setMode] = useState('create'); const [name, setName] = useState(initialName ?? ''); const [identifier, setIdentifier] = useState(initialName ? slugify(initialName) : ''); - const [description, setDescription] = useState(initialDescription ?? ''); + const [instructions, setInstructions] = useState(initialInstructions ?? ''); + const [apiKey, setApiKey] = useState(''); + const [externalAgentId, setExternalAgentId] = useState(''); + const [externalEnvironmentId, setExternalEnvironmentId] = useState(''); const [errors, setErrors] = useState({}); - // Once the user edits the identifier manually, stop auto-syncing it from the name. const [isIdentifierTouched, setIsIdentifierTouched] = useState(false); + const [templateOffset, setTemplateOffset] = useState(0); + const [showSecret, setShowSecret] = useState(false); + + const toggleSecretVisibility = () => { + setShowSecret(!showSecret); + }; + + const visibleTemplates = AGENT_TEMPLATES.slice(templateOffset, templateOffset + 4); // Seed the form fields from initial props when the dialog opens. useEffect(() => { @@ -86,42 +189,65 @@ export function CreateAgentDialog({ setName(initialName ?? ''); setIdentifier(initialName ? slugify(initialName) : ''); - setDescription(initialDescription ?? ''); + setInstructions(initialInstructions ?? ''); setIsIdentifierTouched(false); setErrors({}); - }, [open, initialName, initialDescription]); + }, [open, initialName, initialInstructions]); const reset = () => { + setRuntime('scratch'); + setMode('create'); setName(''); setIdentifier(''); - setDescription(''); + setInstructions(''); + setApiKey(''); + setExternalAgentId(''); + setExternalEnvironmentId(''); setErrors({}); setIsIdentifierTouched(false); + setTemplateOffset(0); }; const handleOpenChange = (next: boolean) => { - if (!next) { - reset(); - } - + if (!next) reset(); onOpenChange(next); }; + const handleTemplateSelect = (template: AgentTemplate) => { + setName(template.name); + if (!isIdentifierTouched) { + setIdentifier(slugify(template.name)); + setErrors((prev) => ({ ...prev, identifier: undefined })); + } + setInstructions(template.instructions); + setErrors((prev) => ({ ...prev, name: undefined })); + }; + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - const trimmedName = name.trim(); - const trimmedIdentifier = identifier.trim(); const nextErrors: FormErrors = {}; + const isExistingMode = runtime === 'claude' && mode === 'existing'; + + if (!isExistingMode) { + const trimmedName = name.trim(); + const trimmedIdentifier = identifier.trim(); - if (!trimmedName) { - nextErrors.name = 'Name is required.'; + if (!trimmedName) nextErrors.name = 'Name is required.'; + + if (!trimmedIdentifier) { + nextErrors.identifier = 'Identifier is required.'; + } else if (!SLUG_IDENTIFIER_REGEX.test(trimmedIdentifier)) { + nextErrors.identifier = slugIdentifierFormatMessage('identifier'); + } } - if (!trimmedIdentifier) { - nextErrors.identifier = 'Identifier is required.'; - } else if (!SLUG_IDENTIFIER_REGEX.test(trimmedIdentifier)) { - nextErrors.identifier = slugIdentifierFormatMessage('identifier'); + if (runtime === 'claude' && !apiKey.trim()) { + nextErrors.apiKey = 'Anthropic API key is required.'; + } + + if (isExistingMode && !externalAgentId.trim()) { + nextErrors.externalAgentId = 'Claude Agent ID is required.'; } if (Object.keys(nextErrors).length > 0) { @@ -132,27 +258,40 @@ export function CreateAgentDialog({ setErrors({}); - const body: CreateAgentBody = { - name: trimmedName, - identifier: trimmedIdentifier, - }; - - const trimmedDescription = description.trim(); - - if (trimmedDescription) { - body.description = trimmedDescription; + const trimmedInstructions = instructions.trim(); + const trimmedName = name.trim(); + const trimmedIdentifier = identifier.trim(); + const trimmedApiKey = apiKey.trim(); + const trimmedExternalAgentId = externalAgentId.trim(); + const trimmedExternalEnvironmentId = externalEnvironmentId.trim(); + + try { + await onSubmit({ + name: trimmedName, + identifier: trimmedIdentifier, + instructions: trimmedInstructions, + apiKey: trimmedApiKey, + runtime, + isExistingMode, + externalAgentId: trimmedExternalAgentId, + externalEnvironmentId: trimmedExternalEnvironmentId, + }); + handleOpenChange(false); + } catch { + // Caller surfaces a toast; keep the dialog open so the user can retry. } - - await onSubmit(body); - handleOpenChange(false); }; + const isClaudeSelected = runtime === 'claude'; + const showManagedOptions = isManagedEnabled; + return ( + {/* Header */}
@@ -168,7 +307,7 @@ export function CreateAgentDialog({ className="text-text-soft hover:text-text-sub inline-flex items-center gap-0.5 underline-offset-2 hover:underline" > Learn more - +
@@ -180,86 +319,339 @@ export function CreateAgentDialog({
+
+
-
-
-
- Agent name - { - const nextName = e.target.value; - setName(nextName); - setErrors((prev) => ({ ...prev, name: undefined })); - - if (!isIdentifierTouched) { - setIdentifier(slugify(nextName)); - setErrors((prev) => ({ ...prev, identifier: undefined })); - } - }} - placeholder={namePlaceholder} - hasError={Boolean(errors.name)} - aria-invalid={errors.name ? true : undefined} - aria-describedby={errors.name ? `${nameId}-error` : undefined} +
+ {/* Runtime selection cards */} +
+ +
+ setRuntime('scratch')} + icon={} + title="Custom Code" + description="Built with LangChain, AI SDK, or your own scaffold" /> - {errors.name ? ( - - ) : null} -
-
- Identifier - { - setIdentifier(e.target.value); - setIsIdentifierTouched(true); - setErrors((prev) => ({ ...prev, identifier: undefined })); - }} - placeholder={identifierPlaceholder} - hasError={Boolean(errors.identifier)} - aria-invalid={errors.identifier ? true : undefined} - aria-describedby={ - errors.identifier ? `${identifierId}-hint ${identifierId}-error` : `${identifierId}-hint` - } + {showManagedOptions && ( + setRuntime('claude')} + icon={} + title="Claude Managed Agent" + description="Agent managed by Claude Managed Agents" + /> + )} + + {}} + disabled + icon={} + title="Google Vertex AI Agent" + description="Agent is managed in Google Vertex AI Agent" /> - - - Used in code and APIs. Must be unique. Letters, numbers, hyphens, underscores, and dots only (no - spaces). - - {errors.identifier ? ( - - ) : null}
+
-
- -