feat(api-service): managed-runtime mode with Claude Platform integration fixes NV-7618#11058
Conversation
Introduces managed agents — users can now point a Novu agent at Claude Platform instead of self-hosting a bridge. Novu stores the Anthropic API key as an Integration, provisions the agent upstream, and proxies all live config (model, system prompt, MCP servers, tools) through a new IAgentRuntimeProvider abstraction. - packages/shared: AGENT_RUNTIME_PROVIDERS catalog, AgentRuntimeCapabilities, IS_MANAGED_AGENT_RUNTIME_ENABLED flag, NotificationChannelTypeEnum, managed-runtime DTOs, ChannelTypeEnum.AGENT_RUNTIME, anthropic providerId - libs/dal: AgentEntity extended with runtime, managedRuntime, soft-delete fields - libs/application-generic: IAgentRuntimeProvider interface, AnthropicAgentRuntimeProvider (@anthropic-ai/sdk), factory, error taxonomy (Unauthorized/Forbidden/NotFound/RateLimited/ServiceUnavailable/...), capabilities-parity test - apps/api: ProvisionManagedAgent / GetAgentRuntimeConfig / UpdateAgentRuntimeConfig use-cases, soft-delete with async Claude cleanup, AgentRuntimeExceptionFilter (stable HTTP codes + Retry-After header), GET /agents/runtime-providers endpoint - apps/dashboard: agent-runtime API client, agent-runtime-errors utility (UI treatment map + TanStack Query retry/backoff), runtime badge, runtime-config section with section-level error panels, auth banner (401), drift modal (409), onboarding runtime chooser + managed setup form Co-authored-by: Cursor <[email protected]>
…okups Follow-up fixes after introducing ChannelTypeEnum.AGENT_RUNTIME: - Add AGENT_RUNTIME entry to all ChannelTypeEnum lookup maps (channels.ts, channel-preferences-form, preferences-item, workflow-preferences, use-integration-list, agent-integrations-tab) - Use NotificationChannelTypeEnum in upsert-workflow.command to keep workflow preferences typed correctly (excludes AGENT_RUNTIME) - Fix TanStack Query v5 keepPreviousData → placeholderData in agent-runtime-config-section - Fix Button ghost variant props in drift modal and managed setup guide Co-authored-by: Cursor <[email protected]>
✅ Deploy Preview for dashboard-v2-novu-staging canceled.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds managed-agent runtime support (Anthropic provider): provider catalog, provider implementation, runtime DTOs, controller endpoints for provider discovery and runtime config, usecases for provisioning/adoption/get/update/delete with rollback, dashboard onboarding/UI, runtime error mapping, and e2e tests. ChangesManaged Agent Runtime Support
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
|
There was a problem hiding this comment.
Actionable comments posted: 14
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/dashboard/src/components/agents/agent-integrations-tab.tsx (1)
33-39:⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoffAdd AGENT_RUNTIME to CHANNEL_GROUP_ORDER to display agent-runtime integrations.
CHANNEL_GROUP_ORDERis used bygroupLinksByChannel(lines 100–105) to determine which channel groups to render. SinceAGENT_RUNTIMEis missing from this array, any integration withchannel === ChannelTypeEnum.AGENT_RUNTIMEwill be collected in the map but never displayed in the UI.🔧 Proposed fix
const CHANNEL_GROUP_ORDER: ChannelTypeEnum[] = [ ChannelTypeEnum.IN_APP, ChannelTypeEnum.CHAT, ChannelTypeEnum.EMAIL, ChannelTypeEnum.PUSH, ChannelTypeEnum.SMS, + ChannelTypeEnum.AGENT_RUNTIME, ];🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/agents/agent-integrations-tab.tsx` around lines 33 - 39, CHANNEL_GROUP_ORDER is missing ChannelTypeEnum.AGENT_RUNTIME so groupLinksByChannel will collect agent-runtime integrations but never render them; update the CHANNEL_GROUP_ORDER array to include ChannelTypeEnum.AGENT_RUNTIME (in the appropriate display order) so agent-runtime channel groups are rendered by groupLinksByChannel.packages/shared/src/types/channel.ts (1)
36-42:⚠️ Potential issue | 🔴 CriticalAdd null check for undefined channel in create-notification-jobs.usecase.ts or filter AGENT_RUNTIME steps.
AGENT_RUNTIMEis now a validStepTypebut is intentionally excluded fromSTEP_TYPE_TO_CHANNEL_TYPE. Increate-notification-jobs.usecase.tsat line 187–188, the map result is used directly without a null check:const channel = STEP_TYPE_TO_CHANNEL_TYPE.get(step.template.type); const providerId = command.templateProviderIds[channel]; // unsafe if channel is undefinedIf an
AGENT_RUNTIMEstep reaches this function,channelwill beundefined, causingproviderIdto beundefined. Either add a null check before line 188, skipAGENT_RUNTIMEsteps in the loop at line 79–85, or addAGENT_RUNTIMEto the map.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/types/channel.ts` around lines 36 - 42, The code uses STEP_TYPE_TO_CHANNEL_TYPE.get(step.template.type) in create-notification-jobs.usecase.ts and then indexes command.templateProviderIds with the resulting channel without guarding against undefined; ensure AGENT_RUNTIME (a valid StepTypeEnum intentionally excluded from the map) cannot reach that usage by either: add a null/undefined check after const channel = STEP_TYPE_TO_CHANNEL_TYPE.get(step.template.type) and skip processing when channel is undefined, or filter out steps with step.template.type === StepTypeEnum.AGENT_RUNTIME earlier (e.g., in the loop that iterates steps) so the providerId lookup (command.templateProviderIds[channel]) is never executed for AGENT_RUNTIME; reference STEP_TYPE_TO_CHANNEL_TYPE, the local variable channel, and command.templateProviderIds when applying the fix.apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts (1)
35-66:⚠️ Potential issue | 🟠 Major | ⚡ Quick winOrphaned local agent if managed provisioning fails.
The agent row is
create()d before provisioning runs. IfprovisionManagedAgentUsecase.execute(...)throws (network error, invalid API key, Anthropic 5xx), the local agent doc remains withruntime: undefinedand the user retrying the create with the sameidentifierwill hit the existing-agentConflictExceptionat line 30, leaving them stuck.Either run create + provision inside a transaction (or compensating delete on failure), or deliberately allow the partial state and let the user repair via update — but the current behavior silently strands users on retry.
const agent = await this.agentRepository.create({ ... }); if (command.runtime === 'managed' && command.managedRuntime) { - await this.provisionManagedAgentUsecase.execute( - Object.assign(new ProvisionManagedAgentCommand(), { - ... - }) - ); + try { + await this.provisionManagedAgentUsecase.execute( + Object.assign(new ProvisionManagedAgentCommand(), { + agentId: agent._id, + ... + }) + ); + } catch (err) { + // Compensate: remove the orphan local agent so the caller can retry + await this.agentRepository.delete({ + _id: agent._id, + _environmentId: command.environmentId, + _organizationId: command.organizationId, + }); + throw err; + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts` around lines 35 - 66, The create flow currently persists the Agent via agentRepository.create before calling provisionManagedAgentUsecase.execute, which can orphan a local agent if provisioning fails; modify create-agent.usecase.ts to perform compensating cleanup: after creating the agent (agentRepository.create) and before returning, wrap the provisionManagedAgentUsecase.execute call in a try/catch and on any error call agentRepository.delete (scoped by _id, _environmentId, _organizationId) to remove the partially-created agent, then rethrow the original error; ensure the cleanup itself is awaited and failures are logged/handled so you don't swallow the original error (references: agentRepository.create, provisionManagedAgentUsecase.execute, ProvisionManagedAgentCommand, agentRepository.delete, agentRepository.findOne).
🟡 Minor comments (8)
apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx-111-111 (1)
111-111:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd blank line before
returnstatement.As per coding guidelines, include a blank line before every
returnstatement.📐 Proposed fix
if (!isManagedEnabled) { + return null; }As per coding guidelines: Include a blank line before every
returnstatement.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx` at line 111, In the ManagedRuntimeSetupGuide component (in apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx) there is no blank line before the return statement; insert a single blank line immediately above the "return null;" statement so the return is separated from preceding code, preserving the file's coding guideline of a blank line before every return.apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx-36-36 (1)
36-36:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInconsistent null-safety handling.
Line 36 uses a non-null assertion (
!), but line 268 uses optional chaining (anthropicProvider?.displayName), suggesting the provider could be undefined. Since the code already has a fallback on line 268, remove the non-null assertion to handle edge cases gracefully.🛡️ Proposed fix
-const anthropicProvider = AGENT_RUNTIME_PROVIDERS.find((p) => p.providerId === 'anthropic')!; +const anthropicProvider = AGENT_RUNTIME_PROVIDERS.find((p) => p.providerId === 'anthropic');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx` at line 36, The code currently forces anthropicProvider non-null with a "!" when finding the provider (const anthropicProvider = AGENT_RUNTIME_PROVIDERS.find((p) => p.providerId === 'anthropic')!), but other code (e.g., anthropicProvider?.displayName) already treats it as potentially undefined; remove the non-null assertion so the variable is typed as possibly undefined and update any usages to handle undefined safely (use optional chaining or a fallback display string) — locate the find call for anthropicProvider and change it to not assert non-null, then ensure all references to anthropicProvider (like displayName) use ?. or a default value.apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx-301-301 (1)
301-301:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd blank line before
returnstatement.As per coding guidelines, include a blank line before every
returnstatement.📐 Proposed fix
const provider = AGENT_RUNTIME_PROVIDERS.find((p) => p.providerId === providerId); - if (!provider) return null; + if (!provider) { + + return null; + }As per coding guidelines: Include a blank line before every
returnstatement.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx` at line 301, The conditional early-return in the ManagedRuntimeSetupGuide component ("if (!provider) return null;") lacks a blank line before the return; update the conditional to place a blank line immediately before the return statement (e.g., convert to a block or separate the return onto its own line) so there is a blank line preceding every return in the ManagedRuntimeSetupGuide component.apps/dashboard/src/components/agents/agent-overview-tab.tsx-68-80 (1)
68-80:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDrift modal recreate/unlink are no-ops — users hit a UX dead-end.
Both callbacks just close the modal, so a user surfaced into this flow because of
AGENT_RUNTIME_DRIFThas no path forward from this UI; the comments acknowledge the work is deferred but the buttons still appear functional. At minimum, disable/hide the recreate and unlink controls (or render a "coming soon" inline note) until the corresponding endpoints land, so users don't repeatedly trigger no-op clicks and assume the system is broken.Want me to wire the
AgentRuntimeDriftModalto render disabled CTAs with a tooltip explaining the action is pending until the recreate/unlink endpoints ship?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/agents/agent-overview-tab.tsx` around lines 68 - 80, The recreate/unlink callbacks on AgentRuntimeDriftModal are no-ops and leave users stuck; update the usage so the modal shows disabled CTAs or a "coming soon" note instead of active buttons: either pass explicit props (e.g., recreateEnabled={false} unlinkEnabled={false} or comingSoonMessage="Recreate/unlink pending backend support") to AgentRuntimeDriftModal, or replace the onRecreate/onUnlink handlers with logic that opens an inline tooltip/alert explaining the action is pending (instead of just calling setShowDriftModal(false)). Modify the AgentRuntimeDriftModal invocation here and ensure the modal component (AgentRuntimeDriftModal) uses those props to render disabled buttons and a tooltip/message so users aren’t offered functional-looking but no-op controls.apps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.ts-34-34 (1)
34-34:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSilent leak path when
managedRuntimeis missing on a managed agent.
isManagedAgentrequires bothruntime === 'managed'AND a truthymanagedRuntime. If a row hasruntime: 'managed'butmanagedRuntimeis somehow absent (provisioning crashed mid-flight, partial restore, manual edit), this branch falls through to the hard-delete path and the local agent is destroyed without ever attempting the external cleanup. Consider explicitly handling that state — e.g., throw a 422/422-equivalent so the caller can retry provisioning, or log+hard-delete deliberately.- const isManagedAgent = agent.runtime === 'managed' && agent.managedRuntime; - - if (isManagedAgent) { + if (agent.runtime === 'managed') { + if (!agent.managedRuntime) { + this.logger.warn( + { agentId: agent._id }, + 'Managed agent missing managedRuntime metadata; performing local-only delete' + ); + } + // Soft-delete: mark as pending external deletion, then clean up provider-side async🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.ts` at line 34, The current check using isManagedAgent (const isManagedAgent = agent.runtime === 'managed' && agent.managedRuntime) silently falls through to hard-delete when runtime === 'managed' but managedRuntime is missing; add an explicit branch that detects agent.runtime === 'managed' && !agent.managedRuntime and handle it deterministically (e.g., throw a 422/unprocessable error or log and choose hard-delete) before using isManagedAgent so callers can retry provisioning or you can record the anomalous state; update the delete logic in delete-agent.usecase.ts to reference agent.runtime and agent.managedRuntime directly to implement this explicit error/decision path.apps/api/src/app/agents/usecases/get-agent-runtime-config/get-agent-runtime-config.usecase.ts-48-49 (1)
48-49:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winValidate
apiKeybefore instantiating the provider.
ProvisionManagedAgentexplicitly throws whenapiKeyis missing (lines 31-33 there), but here a missing/empty key is asserted away with!and silently passed intogetAgentRuntimeProvider, surfacing as an opaque SDK error rather than a clear422. Mirror the validation for consistency.- const decryptedCredentials = decryptCredentials(integration.credentials); - const runtimeProvider = getAgentRuntimeProvider(providerId, decryptedCredentials.apiKey!); + const decryptedCredentials = decryptCredentials(integration.credentials); + if (!decryptedCredentials.apiKey) { + throw new UnprocessableEntityException( + `Runtime integration for agent "${command.identifier}" has no API key configured.` + ); + } + const runtimeProvider = getAgentRuntimeProvider(providerId, decryptedCredentials.apiKey);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/get-agent-runtime-config/get-agent-runtime-config.usecase.ts` around lines 48 - 49, The code calls getAgentRuntimeProvider(providerId, decryptedCredentials.apiKey!) without validating the apiKey; replicate the validation used in ProvisionManagedAgent by checking decryptedCredentials.apiKey (and treating empty string as missing) and throw or return a 422-style error before calling getAgentRuntimeProvider so missing keys produce a clear validation error; update the logic around decryptCredentials and the call-site for getAgentRuntimeProvider to validate apiKey and only pass a non-empty string.apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts-31-33 (1)
31-33:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse
UnprocessableEntityException(notNotFoundException) when the integration lacks an API key.The integration was found; its configuration is invalid. Returning a
404here misleads the dashboard's error-to-UI mapping (which already treats 404 as "not found, please re-create") and conflates two distinct failure modes.-import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; ... - if (!apiKey) { - throw new NotFoundException(`Integration "${command.integrationId}" has no API key configured.`); - } + if (!apiKey) { + throw new UnprocessableEntityException( + `Integration "${command.integrationId}" has no API key configured.` + ); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts` around lines 31 - 33, The code currently throws NotFoundException when an integration exists but has no API key; update the check in provision-managed-agent.usecase.ts to throw UnprocessableEntityException instead (keep the existing message or adjust to indicate invalid configuration), and ensure you import UnprocessableEntityException from `@nestjs/common` and remove NotFoundException if it becomes unused; update any related tests/handlers expecting a 404 to expect a 422 for this validation case.apps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.usecase.ts-47-48 (1)
47-48:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSame
apiKey!non-null assertion as inGetAgentRuntimeConfig.Apply the same validation suggested for the GET use case here so missing credentials surface as a clean 422 instead of a downstream SDK error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.usecase.ts` around lines 47 - 48, The code uses a non-null assertion on integration.credentials.apiKey when calling getAgentRuntimeProvider in update-agent-runtime-config.usecase.ts; mirror the GET use case validation by checking decryptCredentials(integration.credentials) and ensuring apiKey exists before calling getAgentRuntimeProvider(providerId, apiKey). If apiKey is missing, throw/return the same 422 validation error used in GetAgentRuntimeConfig (i.e., produce a clear 422 response rather than letting the SDK error surface) and remove the trailing `!` on apiKey.
🧹 Nitpick comments (8)
apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts (1)
42-44: TODO appropriately marks future work for managed agent execution.The comment clearly defers message-time execution to a future PR. When implementing this, note that the agent entity will need to be fetched earlier in the method (currently fetched at line 149), and the fetch projection must include the
runtimeandmanagedRuntimefields (currently only fetches_id,name,identifierat line 155).Would you like me to open a tracking issue that outlines the implementation prerequisites (early agent fetch, field inclusion, branching logic, runtime provider integration) to make this TODO more actionable for the runtime team?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts` around lines 42 - 44, Update the TODO branch so the code fetches the agent entity earlier in handle-agent-reply.usecase.ts (before routing logic) and include the runtime and managedRuntime fields in the query projection in addition to _id, name, identifier; then add a branch: if (agent.runtime === 'managed') call the runtime provider via IAgentRuntimeProvider.runConversationTurn(externalAgentId, userMessage) (passing the agent.managedRuntime/external id as required) and short-circuit the self-hosted bridge path, otherwise continue current routing; ensure variable names match existing symbols (agent, runtime, managedRuntime, externalAgentId, userMessage, IAgentRuntimeProvider.runConversationTurn).packages/shared/src/consts/providers/agent-runtime-providers.ts (1)
26-42: ⚡ Quick winPrefer immutable exported provider collections.
AGENT_RUNTIME_PROVIDERSandAGENT_RUNTIME_PROVIDER_IDSare currently mutable exports. Making these readonly reduces accidental cross-module mutation risk.Suggested refactor
-export const AGENT_RUNTIME_PROVIDERS: AgentRuntimeProvider[] = [ +export const AGENT_RUNTIME_PROVIDERS: ReadonlyArray<AgentRuntimeProvider> = [ { providerId: AgentRuntimeProviderIdEnum.Anthropic, displayName: 'Claude (Anthropic)', @@ -]; +]; -export const AGENT_RUNTIME_PROVIDER_IDS = new Set(AGENT_RUNTIME_PROVIDERS.map((p) => p.providerId)); +export const AGENT_RUNTIME_PROVIDER_IDS: ReadonlySet<AgentRuntimeProviderIdEnum> = new Set( + AGENT_RUNTIME_PROVIDERS.map((p) => p.providerId) +);As per coding guidelines: “Keep
packages/sharedfree of runtime side-effects since it is imported by both Node.js services and browser bundles.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/consts/providers/agent-runtime-providers.ts` around lines 26 - 42, AGENT_RUNTIME_PROVIDERS and AGENT_RUNTIME_PROVIDER_IDS are exported mutable collections; change them to readonly to prevent cross-module mutation by: update the type of AGENT_RUNTIME_PROVIDERS to ReadonlyArray<AgentRuntimeProvider> (or add the `readonly` modifier) and AGENT_RUNTIME_PROVIDER_IDS to ReadonlySet<AgentRuntimeProviderIdEnum>, and make the runtime values immutable (e.g., use Object.freeze on the array and its objects or construct a ReadonlySet) so both the TypeScript types and the runtime values are immutable; refer to the AGENT_RUNTIME_PROVIDERS and AGENT_RUNTIME_PROVIDER_IDS symbols to locate and apply these changes.libs/application-generic/src/agent-runtimes/agent-runtime-capabilities-parity.spec.ts (1)
13-19: ⚡ Quick winRun this parity check through the real factory and compare the whole capability object.
This helper duplicates provider registration, so
agent-runtime.factory.tscan drift without the test noticing. The per-field assertions also miss newly added capability flags. ReusinggetAgentRuntimeProviderand assertinginstance.capabilitieswithtoEqualcloses both gaps.Suggested refactor
import { AGENT_RUNTIME_PROVIDERS, AgentRuntimeProviderIdEnum } from '@novu/shared'; -import { createAnthropicProvider } from './anthropic/anthropic-agent-runtime.provider'; +import { getAgentRuntimeProvider } from './agent-runtime.factory'; import type { IAgentRuntimeProvider } from './i-agent-runtime-provider'; @@ function getProviderInstance(id: AgentRuntimeProviderIdEnum): IAgentRuntimeProvider { - switch (id) { - case AgentRuntimeProviderIdEnum.Anthropic: - return createAnthropicProvider('test-key'); - default: - throw new Error(`No concrete provider registered for ${id}. Add it to this test.`); - } + return getAgentRuntimeProvider(id, 'test-key'); } @@ - it('capabilities.mcpServers matches the catalog', () => { - expect(instance.capabilities.mcpServers).toBe(catalogEntry.capabilities.mcpServers); - }); - - it('capabilities.tools matches the catalog', () => { - expect(instance.capabilities.tools).toBe(catalogEntry.capabilities.tools); - }); - - it('capabilities.model matches the catalog', () => { - expect(instance.capabilities.model).toBe(catalogEntry.capabilities.model); - }); - - it('capabilities.systemPrompt matches the catalog', () => { - expect(instance.capabilities.systemPrompt).toBe(catalogEntry.capabilities.systemPrompt); + it('capabilities match the catalog', () => { + expect(instance.capabilities).toEqual(catalogEntry.capabilities); });Also applies to: 35-49
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/application-generic/src/agent-runtimes/agent-runtime-capabilities-parity.spec.ts` around lines 13 - 19, The test helper getProviderInstance duplicates provider creation and can drift from the real registry; instead call the real factory function getAgentRuntimeProvider with AgentRuntimeProviderIdEnum.Anthropic to obtain the provider instance, then replace the per-field capability assertions with a single deep equality assertion comparing instance.capabilities to the expected capability object (use toEqual) so the whole capability shape from agent-runtime.factory.ts is validated; update the other occurrence (lines 35-49) similarly to reuse getAgentRuntimeProvider and assert instance.capabilities with toEqual.apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts (1)
37-45: 💤 Low value
validateCredentials+createAgentis two upstream calls where one usually suffices.
createAgentwill surface 401 just as well asvalidateCredentials, and you're paying double rate-limit budget for every managed-agent creation. Consider relying oncreateAgentand mapping its 401 to yourAgentRuntimeUnauthorizedError, dropping the explicit pre-flight unless it has a meaningful side benefit (e.g., probing scopes thatcreateAgentdoesn't reveal).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts` around lines 37 - 45, Remove the redundant pre-flight call to runtimeProvider.validateCredentials and instead rely on runtimeProvider.createAgent to surface authorization failures; catch a 401/unauthorized response from createAgent in the provision flow and map it to AgentRuntimeUnauthorizedError (or rethrow as that type) so we preserve behavior without the extra upstream call; update the logic in provision-managed-agent.usecase.ts around the runtimeProvider.createAgent call (and any surrounding try/catch) to perform this mapping and delete the validateCredentials invocation.apps/dashboard/src/components/agents/agent-runtime-config-section.tsx (1)
35-36: 💤 Low valueType-narrowing nit on the runtime error.
The
RuntimeErrorPanelcastserrortoRecord<string, unknown> | null, then accesseserr?.code. Since the runtime error utility module already exports typed helpers, exposing a smallgetRuntimeErrorCode(error: unknown): stringfrom@/utils/agent-runtime-errorsand reusing it both here and at lines 110-114 would remove the duplicatedas unknown asladder and centralize the contract. Optional.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/agents/agent-runtime-config-section.tsx` around lines 35 - 36, Replace the ad-hoc cast and property access in RuntimeErrorPanel with a dedicated helper: add or re-export getRuntimeErrorCode(error: unknown): string in the agent-runtime-errors util and call it from RuntimeErrorPanel (instead of casting to Record<string, unknown> and reading err?.code) and also use it where similar logic exists around lines 110-114; this centralizes the type-narrowing and ensures you return a default code like 'AGENT_RUNTIME_UNKNOWN' from getRuntimeErrorCode when the error shape is unexpected.apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts (1)
59-66: 💤 Low valueAvoid the extra round-trip; have
ProvisionManagedAgentreturn what's needed.
ProvisionManagedAgent.executealready knows the persistedruntimeandmanagedRuntimevalues it just wrote. Returning them (or the updated doc) avoids this extrafindOne('*'). Optional polish.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts` around lines 59 - 66, The extra findOne('*') round-trip should be removed by having ProvisionManagedAgent.execute return the persisted values (either the updated agent document or at least the runtime and managedRuntime it wrote); update ProvisionManagedAgent.execute's signature to return those values, change the caller in create-agent.usecase.ts (the code that currently calls agentRepository.findOne with _id and '*') to use the returned runtime/managedRuntime or returned updatedAgent instead, and adjust any related types/interfaces (and tests) accordingly so no additional repository fetch is needed.apps/dashboard/src/components/agents/agent-overview-tab.tsx (1)
21-31: ⚡ Quick winExtract runtime mode string literal to a named constant in
packages/shared.The string
'managed'is duplicated across 9+ files (agent-overview-tab.tsx, agent-runtime-config-section.tsx, plus create-agent, delete-agent, get-agent-runtime-config, update-agent-runtime-config, and provision-managed-agent files). Introduce a named constant—either an enum likeAgentRuntimeModealongsideAgentRuntimeProviderIdEnum, or a const likeAGENT_RUNTIME_MODES = { MANAGED: 'managed', SELF_HOSTED: 'self-hosted' }inpackages/shared—to prevent silent typos and enable searchability across the codebase.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/agents/agent-overview-tab.tsx` around lines 21 - 31, The codebase uses the string literal 'managed' in multiple places (e.g., the isManagedRuntime check in agent-overview-tab.tsx) which risks typos and duplication; add a central constant or enum in packages/shared (for example AgentRuntimeMode or AGENT_RUNTIME_MODES with MANAGED and SELF_HOSTED values), export it, and replace all occurrences of the literal 'managed' across components and API modules (agent-overview-tab.tsx, agent-runtime-config-section.tsx, create-agent, delete-agent, get-agent-runtime-config, update-agent-runtime-config, provision-managed-agent) to reference the new constant (update the isManagedRuntime assignment to compare against the exported enum/const instead of the raw string).apps/api/src/app/agents/agents.controller.ts (1)
132-134: ⚡ Quick winAvoid unsafe cast for provider catalog response.
Line 133 uses
as AgentRuntimeProviderResponseDto[], which can hide contract drift between shared catalog shape and DTO shape.💡 Suggested fix
listAgentRuntimeProviders(): AgentRuntimeProviderResponseDto[] { - return AGENT_RUNTIME_PROVIDERS as AgentRuntimeProviderResponseDto[]; + return [...AGENT_RUNTIME_PROVIDERS]; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/agents.controller.ts` around lines 132 - 134, The return uses an unsafe cast of AGENT_RUNTIME_PROVIDERS to AgentRuntimeProviderResponseDto[], which can hide contract drift; update listAgentRuntimeProviders to explicitly map/validate each entry from AGENT_RUNTIME_PROVIDERS into an AgentRuntimeProviderResponseDto (e.g., map over AGENT_RUNTIME_PROVIDERS and construct objects containing only the DTO fields or run a validation/transform such as class-transformer/class-validator or a type guard) so the function (listAgentRuntimeProviders) returns a guaranteed-correct AgentRuntimeProviderResponseDto[] rather than relying on a raw "as" cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/app/agents/usecases/create-agent/create-agent.command.ts`:
- Around line 38-42: The nested DTO validation for managed runtime is broken
because ManagedRuntimeDto is imported as a type-only and `@Type` uses Object;
update the import so ManagedRuntimeDto is a real runtime class import (not
type-only) and change the decorator from `@Type`(() => Object) to `@Type`(() =>
ManagedRuntimeDto) in create-agent.command.ts for proper
class-transformer/validator behavior; then update the controller action in
agents.controller (the method that constructs CreateAgentCommand) to pass
request.body.runtime and request.body.managedRuntime into the CreateAgentCommand
constructor/assignment so the CreateAgentCommand and its handler
(CreateAgentCommand handler lines that read runtime/managedRuntime) receive the
values at runtime.
In `@apps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.ts`:
- Around line 34-55: The current fire-and-forget call to
deleteExternalAgent(agent._id, agent.managedRuntime!, command) after setting
pendingExternalDelete: true leaves external agents orphaned because there is no
retry/reconciler and the logger message incorrectly claims "will retry"; fix by
either (A) enqueueing the deletion into the existing job/queue system so it
survives restarts and gets retries/backoff, (B) adding a scheduled reconciler
that scans agentRepository for pendingExternalDelete: true and re-invokes
deleteExternalAgent, or (C) changing the flow to await deleteExternalAgent and
only hard-delete locally on success; if you cannot implement a durable retry
now, at minimum remove/replace the "will retry" text from the logger.error call
to avoid misleading on-call engineers and ensure pendingExternalDelete is
cleared or handled by one of the above approaches.
In
`@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts`:
- Around line 13-15: The providerId property in ProvisionManagedAgentCommand is
only validated with `@IsNotEmpty`(); add enum validation by decorating providerId
with `@IsEnum`(AgentRuntimeProviderIdEnum) (and import IsEnum from
class-validator) so invalid provider IDs are rejected at DTO validation time;
ensure the AgentRuntimeProviderIdEnum symbol is imported/used and place the
decorator above the providerId field in provision-managed-agent.command.ts.
In
`@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts`:
- Around line 49-83: The update result from agentRepository.update in
provision-managed-agent.usecase.ts must be checked because it returns { matched,
modified } and a matched===0 indicates the local agent record was removed (or
not found) leaving the external agent orphaned; capture the result of
this.update call, and if result.matched === 0 perform the same best-effort
rollback as in the catch (await runtimeProvider.deleteAgent(externalAgentId)),
log the rollback failure with this.logger.error including agentId,
externalAgentId, providerId and rollbackError, and then throw an error (e.g.,
throw new Error or rethrow a descriptive error) to abort the flow so callers
know the update did not apply.
In
`@apps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.usecase.ts`:
- Around line 50-55: The PATCH use case returns the raw provider result from
runtimeProvider.updateConfig(...) cast to AgentRuntimeConfigResponseDto, which
omits the derived capabilities that GetAgentRuntimeConfig adds from
AGENT_RUNTIME_PROVIDERS and breaks consumers; fix by creating a small shared
helper (e.g., withCapabilities(providerId, config)) that looks up
AGENT_RUNTIME_PROVIDERS[providerId] to compute and attach the capabilities, then
call that helper to enrich the result of runtimeProvider.updateConfig(...) in
update-agent-runtime-config.usecase.ts (and refactor GetAgentRuntimeConfig to
use the same withCapabilities helper) so both endpoints return the same shape
including capabilities.
In `@apps/dashboard/src/api/agent-runtime.ts`:
- Around line 28-54: Patch payload currently requires externalId because
PatchAgentRuntimeConfigBody.mcpServers and .tools reference AgentMcpServer and
AgentTool which have externalId mandatory; update the patch types so externalId
is optional by defining new types (e.g., PatchAgentMcpServer and PatchAgentTool)
that mirror AgentMcpServer and AgentTool but mark externalId as optional, then
change PatchAgentRuntimeConfigBody to use PatchAgentMcpServer[] and
PatchAgentTool[]; alternatively use Partial<...> or Omit+optional externalId for
the same effect.
In `@apps/dashboard/src/components/agents/agent-runtime-config-section.tsx`:
- Around line 109-114: The current render body in AgentRuntimeConfigSection
calls onDrift?.() and onUnauthorized?.() directly when error exists, which
triggers parent setters (setShowDriftModal/setShowAuthBanner) during render;
move this logic into a useEffect inside AgentRuntimeConfigSection that watches
the error identity (e.g., error and/or (error as Record<string,unknown>)?.code)
and, when the code equals 'AGENT_RUNTIME_DRIFT' or 'AGENT_RUNTIME_UNAUTHORIZED',
invokes onDrift() or onUnauthorized() respectively; ensure the effect only runs
when the error/code changes to avoid repeated invocations and remove the direct
calls from the render body so AgentOverviewTab updates happen from the effect.
In `@apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx`:
- Around line 108-112: The render is calling onSelfHostedSelected() directly
which can cause setState during render; remove the direct call from the JSX
branch and instead invoke onSelfHostedSelected inside a useEffect that watches
isManagedEnabled (e.g., useEffect(() => { if (!isManagedEnabled)
onSelfHostedSelected(); }, [isManagedEnabled])); keep the return null when
!isManagedEnabled in the render path so the UI still short-circuits, and ensure
the effect only runs when isManagedEnabled transitions to false to avoid
repeated calls.
In
`@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts`:
- Around line 251-258: The sync logic in anthropic-agent-runtime.provider.ts
currently only creates MCP servers/tools when externalId is missing and deletes
when absent, so entries with the same externalId but changed fields (name, url,
authToken, description, type) are skipped; modify the loops that handle desired
servers (the block using (client as any).beta.agents.mcpServers.create) and the
analogous tools loop to detect if currentIds has server.externalId then fetch
the existing record and compare mutable fields, and if any differ call the
provider's update method (e.g., mcpServers.update or tools.update) with the
agentId and the changed fields instead of no-op; ensure authToken is handled
like create (auth_token key) and preserve unchanged fields when issuing the
update.
- Around line 140-143: The current .catch(() => ({ data: [] })) on calls like
(client as any).beta.agents.mcpServers.list(externalAgentId) and (client as
any).beta.agents.tools.list(externalAgentId) hides provider failures by
returning an empty list; remove those swallow-catches so errors propagate (or
replace them with a catch that logs and rethrows) so getConfig/updateConfig see
real failures and don’t diff against fabricated empty baselines. Update the same
pattern at the other occurrences you noted (the similar calls around the
getConfig/updateConfig usages) to either drop the .catch or rethrow after
logging so transient provider errors are not converted into empty data.
- Around line 50-65: The error-normalization block incorrectly uses bracket
notation on APIError.headers (a Headers object) and drops the error's request
identifier; update this to read the request id from the error first
(err.requestId or err.requestID) and fall back to err.headers.get('request-id')
if needed, and replace bracket access with err.headers.get('retry-after') when
calling parseRetryAfter; ensure the requestId value is passed into
AgentRuntimeUnauthorizedError, AgentRuntimeForbiddenError,
AgentRuntimeNotFoundError, and AgentRuntimeRateLimitedError along with
PROVIDER_ID and the parsed retryAfterMs where applicable.
In `@libs/dal/src/repositories/agent/agent.schema.ts`:
- Line 43: The agent schema's soft-delete field "deletedAt" is declared as
Schema.Types.String; change it to a Date type to preserve correct
sorting/filtering and consistent timestamp semantics by updating the "deletedAt"
field in agent.schema.ts to use Schema.Types.Date (optionally nullable/default
null and indexed/sparse if you rely on queries), ensuring any related code that
sets/reads deletedAt (e.g., softDelete methods or repository queries)
writes/reads Date objects or converts strings to Date objects accordingly.
In `@packages/shared/src/dto/agent/managed-runtime.dto.ts`:
- Around line 25-32: The AgentMcpServerDto currently exposes a raw authToken
which can leak provider secrets; remove the authToken field from this shared
read DTO (AgentMcpServerDto) and replace it with a safe read-only indicator such
as hasAuthToken: boolean (or masked metadata) to signal presence without
exposing the secret, and add a separate write/update DTO (e.g.,
AgentMcpServerUpdateDto or AgentMcpServerWriteDto) that includes authToken?:
string for incoming updates only; update any consumers/serializers to use the
read DTO for responses and the write DTO for mutations.
In `@packages/shared/src/types/workflow-channel-preferences.ts`:
- Around line 70-71: The change narrows the exported key type for
WorkflowPreferences.channels to NotificationChannelTypeEnum which is a
public-API-breaking change; restore backward compatibility by keeping
WorkflowPreferences.channels keyed by the original ChannelTypeEnum (or a
compatible union) and introduce NotificationChannelTypeEnum as a narrower,
documented/deprecated helper for consumers to migrate to; update the type
declarations around NotificationChannelTypeEnum and WorkflowPreferences.channels
to preserve the old shape while adding a deprecation comment and migration
guidance so you can ship this as a non-breaking patch (or only make the breaking
narrow change behind a major bump).
---
Outside diff comments:
In `@apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts`:
- Around line 35-66: The create flow currently persists the Agent via
agentRepository.create before calling provisionManagedAgentUsecase.execute,
which can orphan a local agent if provisioning fails; modify
create-agent.usecase.ts to perform compensating cleanup: after creating the
agent (agentRepository.create) and before returning, wrap the
provisionManagedAgentUsecase.execute call in a try/catch and on any error call
agentRepository.delete (scoped by _id, _environmentId, _organizationId) to
remove the partially-created agent, then rethrow the original error; ensure the
cleanup itself is awaited and failures are logged/handled so you don't swallow
the original error (references: agentRepository.create,
provisionManagedAgentUsecase.execute, ProvisionManagedAgentCommand,
agentRepository.delete, agentRepository.findOne).
In `@apps/dashboard/src/components/agents/agent-integrations-tab.tsx`:
- Around line 33-39: CHANNEL_GROUP_ORDER is missing
ChannelTypeEnum.AGENT_RUNTIME so groupLinksByChannel will collect agent-runtime
integrations but never render them; update the CHANNEL_GROUP_ORDER array to
include ChannelTypeEnum.AGENT_RUNTIME (in the appropriate display order) so
agent-runtime channel groups are rendered by groupLinksByChannel.
In `@packages/shared/src/types/channel.ts`:
- Around line 36-42: The code uses
STEP_TYPE_TO_CHANNEL_TYPE.get(step.template.type) in
create-notification-jobs.usecase.ts and then indexes command.templateProviderIds
with the resulting channel without guarding against undefined; ensure
AGENT_RUNTIME (a valid StepTypeEnum intentionally excluded from the map) cannot
reach that usage by either: add a null/undefined check after const channel =
STEP_TYPE_TO_CHANNEL_TYPE.get(step.template.type) and skip processing when
channel is undefined, or filter out steps with step.template.type ===
StepTypeEnum.AGENT_RUNTIME earlier (e.g., in the loop that iterates steps) so
the providerId lookup (command.templateProviderIds[channel]) is never executed
for AGENT_RUNTIME; reference STEP_TYPE_TO_CHANNEL_TYPE, the local variable
channel, and command.templateProviderIds when applying the fix.
---
Minor comments:
In `@apps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.ts`:
- Line 34: The current check using isManagedAgent (const isManagedAgent =
agent.runtime === 'managed' && agent.managedRuntime) silently falls through to
hard-delete when runtime === 'managed' but managedRuntime is missing; add an
explicit branch that detects agent.runtime === 'managed' &&
!agent.managedRuntime and handle it deterministically (e.g., throw a
422/unprocessable error or log and choose hard-delete) before using
isManagedAgent so callers can retry provisioning or you can record the anomalous
state; update the delete logic in delete-agent.usecase.ts to reference
agent.runtime and agent.managedRuntime directly to implement this explicit
error/decision path.
In
`@apps/api/src/app/agents/usecases/get-agent-runtime-config/get-agent-runtime-config.usecase.ts`:
- Around line 48-49: The code calls getAgentRuntimeProvider(providerId,
decryptedCredentials.apiKey!) without validating the apiKey; replicate the
validation used in ProvisionManagedAgent by checking decryptedCredentials.apiKey
(and treating empty string as missing) and throw or return a 422-style error
before calling getAgentRuntimeProvider so missing keys produce a clear
validation error; update the logic around decryptCredentials and the call-site
for getAgentRuntimeProvider to validate apiKey and only pass a non-empty string.
In
`@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts`:
- Around line 31-33: The code currently throws NotFoundException when an
integration exists but has no API key; update the check in
provision-managed-agent.usecase.ts to throw UnprocessableEntityException instead
(keep the existing message or adjust to indicate invalid configuration), and
ensure you import UnprocessableEntityException from `@nestjs/common` and remove
NotFoundException if it becomes unused; update any related tests/handlers
expecting a 404 to expect a 422 for this validation case.
In
`@apps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.usecase.ts`:
- Around line 47-48: The code uses a non-null assertion on
integration.credentials.apiKey when calling getAgentRuntimeProvider in
update-agent-runtime-config.usecase.ts; mirror the GET use case validation by
checking decryptCredentials(integration.credentials) and ensuring apiKey exists
before calling getAgentRuntimeProvider(providerId, apiKey). If apiKey is
missing, throw/return the same 422 validation error used in
GetAgentRuntimeConfig (i.e., produce a clear 422 response rather than letting
the SDK error surface) and remove the trailing `!` on apiKey.
In `@apps/dashboard/src/components/agents/agent-overview-tab.tsx`:
- Around line 68-80: The recreate/unlink callbacks on AgentRuntimeDriftModal are
no-ops and leave users stuck; update the usage so the modal shows disabled CTAs
or a "coming soon" note instead of active buttons: either pass explicit props
(e.g., recreateEnabled={false} unlinkEnabled={false} or
comingSoonMessage="Recreate/unlink pending backend support") to
AgentRuntimeDriftModal, or replace the onRecreate/onUnlink handlers with logic
that opens an inline tooltip/alert explaining the action is pending (instead of
just calling setShowDriftModal(false)). Modify the AgentRuntimeDriftModal
invocation here and ensure the modal component (AgentRuntimeDriftModal) uses
those props to render disabled buttons and a tooltip/message so users aren’t
offered functional-looking but no-op controls.
In `@apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx`:
- Line 111: In the ManagedRuntimeSetupGuide component (in
apps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsx) there
is no blank line before the return statement; insert a single blank line
immediately above the "return null;" statement so the return is separated from
preceding code, preserving the file's coding guideline of a blank line before
every return.
- Line 36: The code currently forces anthropicProvider non-null with a "!" when
finding the provider (const anthropicProvider = AGENT_RUNTIME_PROVIDERS.find((p)
=> p.providerId === 'anthropic')!), but other code (e.g.,
anthropicProvider?.displayName) already treats it as potentially undefined;
remove the non-null assertion so the variable is typed as possibly undefined and
update any usages to handle undefined safely (use optional chaining or a
fallback display string) — locate the find call for anthropicProvider and change
it to not assert non-null, then ensure all references to anthropicProvider (like
displayName) use ?. or a default value.
- Line 301: The conditional early-return in the ManagedRuntimeSetupGuide
component ("if (!provider) return null;") lacks a blank line before the return;
update the conditional to place a blank line immediately before the return
statement (e.g., convert to a block or separate the return onto its own line) so
there is a blank line preceding every return in the ManagedRuntimeSetupGuide
component.
---
Nitpick comments:
In `@apps/api/src/app/agents/agents.controller.ts`:
- Around line 132-134: The return uses an unsafe cast of AGENT_RUNTIME_PROVIDERS
to AgentRuntimeProviderResponseDto[], which can hide contract drift; update
listAgentRuntimeProviders to explicitly map/validate each entry from
AGENT_RUNTIME_PROVIDERS into an AgentRuntimeProviderResponseDto (e.g., map over
AGENT_RUNTIME_PROVIDERS and construct objects containing only the DTO fields or
run a validation/transform such as class-transformer/class-validator or a type
guard) so the function (listAgentRuntimeProviders) returns a guaranteed-correct
AgentRuntimeProviderResponseDto[] rather than relying on a raw "as" cast.
In `@apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts`:
- Around line 59-66: The extra findOne('*') round-trip should be removed by
having ProvisionManagedAgent.execute return the persisted values (either the
updated agent document or at least the runtime and managedRuntime it wrote);
update ProvisionManagedAgent.execute's signature to return those values, change
the caller in create-agent.usecase.ts (the code that currently calls
agentRepository.findOne with _id and '*') to use the returned
runtime/managedRuntime or returned updatedAgent instead, and adjust any related
types/interfaces (and tests) accordingly so no additional repository fetch is
needed.
In
`@apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts`:
- Around line 42-44: Update the TODO branch so the code fetches the agent entity
earlier in handle-agent-reply.usecase.ts (before routing logic) and include the
runtime and managedRuntime fields in the query projection in addition to _id,
name, identifier; then add a branch: if (agent.runtime === 'managed') call the
runtime provider via IAgentRuntimeProvider.runConversationTurn(externalAgentId,
userMessage) (passing the agent.managedRuntime/external id as required) and
short-circuit the self-hosted bridge path, otherwise continue current routing;
ensure variable names match existing symbols (agent, runtime, managedRuntime,
externalAgentId, userMessage, IAgentRuntimeProvider.runConversationTurn).
In
`@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts`:
- Around line 37-45: Remove the redundant pre-flight call to
runtimeProvider.validateCredentials and instead rely on
runtimeProvider.createAgent to surface authorization failures; catch a
401/unauthorized response from createAgent in the provision flow and map it to
AgentRuntimeUnauthorizedError (or rethrow as that type) so we preserve behavior
without the extra upstream call; update the logic in
provision-managed-agent.usecase.ts around the runtimeProvider.createAgent call
(and any surrounding try/catch) to perform this mapping and delete the
validateCredentials invocation.
In `@apps/dashboard/src/components/agents/agent-overview-tab.tsx`:
- Around line 21-31: The codebase uses the string literal 'managed' in multiple
places (e.g., the isManagedRuntime check in agent-overview-tab.tsx) which risks
typos and duplication; add a central constant or enum in packages/shared (for
example AgentRuntimeMode or AGENT_RUNTIME_MODES with MANAGED and SELF_HOSTED
values), export it, and replace all occurrences of the literal 'managed' across
components and API modules (agent-overview-tab.tsx,
agent-runtime-config-section.tsx, create-agent, delete-agent,
get-agent-runtime-config, update-agent-runtime-config, provision-managed-agent)
to reference the new constant (update the isManagedRuntime assignment to compare
against the exported enum/const instead of the raw string).
In `@apps/dashboard/src/components/agents/agent-runtime-config-section.tsx`:
- Around line 35-36: Replace the ad-hoc cast and property access in
RuntimeErrorPanel with a dedicated helper: add or re-export
getRuntimeErrorCode(error: unknown): string in the agent-runtime-errors util and
call it from RuntimeErrorPanel (instead of casting to Record<string, unknown>
and reading err?.code) and also use it where similar logic exists around lines
110-114; this centralizes the type-narrowing and ensures you return a default
code like 'AGENT_RUNTIME_UNKNOWN' from getRuntimeErrorCode when the error shape
is unexpected.
In
`@libs/application-generic/src/agent-runtimes/agent-runtime-capabilities-parity.spec.ts`:
- Around line 13-19: The test helper getProviderInstance duplicates provider
creation and can drift from the real registry; instead call the real factory
function getAgentRuntimeProvider with AgentRuntimeProviderIdEnum.Anthropic to
obtain the provider instance, then replace the per-field capability assertions
with a single deep equality assertion comparing instance.capabilities to the
expected capability object (use toEqual) so the whole capability shape from
agent-runtime.factory.ts is validated; update the other occurrence (lines 35-49)
similarly to reuse getAgentRuntimeProvider and assert instance.capabilities with
toEqual.
In `@packages/shared/src/consts/providers/agent-runtime-providers.ts`:
- Around line 26-42: AGENT_RUNTIME_PROVIDERS and AGENT_RUNTIME_PROVIDER_IDS are
exported mutable collections; change them to readonly to prevent cross-module
mutation by: update the type of AGENT_RUNTIME_PROVIDERS to
ReadonlyArray<AgentRuntimeProvider> (or add the `readonly` modifier) and
AGENT_RUNTIME_PROVIDER_IDS to ReadonlySet<AgentRuntimeProviderIdEnum>, and make
the runtime values immutable (e.g., use Object.freeze on the array and its
objects or construct a ReadonlySet) so both the TypeScript types and the runtime
values are immutable; refer to the AGENT_RUNTIME_PROVIDERS and
AGENT_RUNTIME_PROVIDER_IDS symbols to locate and apply these changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 979f9196-5de8-4ee8-8a48-54bace5436ba
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (59)
apps/api/src/app/agents/agents.controller.tsapps/api/src/app/agents/agents.module.tsapps/api/src/app/agents/dtos/agent-response.dto.tsapps/api/src/app/agents/dtos/agent-runtime-config.dto.tsapps/api/src/app/agents/dtos/create-agent-request.dto.tsapps/api/src/app/agents/dtos/index.tsapps/api/src/app/agents/filters/agent-runtime-exception.filter.tsapps/api/src/app/agents/mappers/agent-response.mapper.tsapps/api/src/app/agents/usecases/create-agent/create-agent.command.tsapps/api/src/app/agents/usecases/create-agent/create-agent.usecase.tsapps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.tsapps/api/src/app/agents/usecases/get-agent-runtime-config/get-agent-runtime-config.command.tsapps/api/src/app/agents/usecases/get-agent-runtime-config/get-agent-runtime-config.usecase.tsapps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.tsapps/api/src/app/agents/usecases/index.tsapps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.tsapps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.tsapps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.command.tsapps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.usecase.tsapps/dashboard/src/api/agent-runtime.tsapps/dashboard/src/api/agents.tsapps/dashboard/src/components/agents/agent-integrations-tab.tsxapps/dashboard/src/components/agents/agent-overview-tab.tsxapps/dashboard/src/components/agents/agent-runtime-auth-banner.tsxapps/dashboard/src/components/agents/agent-runtime-badge.tsxapps/dashboard/src/components/agents/agent-runtime-config-section.tsxapps/dashboard/src/components/agents/agent-runtime-drift-modal.tsxapps/dashboard/src/components/integrations/components/hooks/use-integration-list.tsapps/dashboard/src/components/onboarding/managed-runtime-setup-guide.tsxapps/dashboard/src/components/subscribers/preferences/preferences-item.tsxapps/dashboard/src/components/subscribers/preferences/workflow-preferences.tsxapps/dashboard/src/components/workflow-editor/channel-preferences-form.tsxapps/dashboard/src/pages/agents-setup-page.tsxapps/dashboard/src/utils/agent-runtime-errors.tsapps/dashboard/src/utils/channels.tslibs/application-generic/package.jsonlibs/application-generic/src/agent-runtimes/agent-runtime-capabilities-parity.spec.tslibs/application-generic/src/agent-runtimes/agent-runtime.factory.tslibs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.tslibs/application-generic/src/agent-runtimes/errors.tslibs/application-generic/src/agent-runtimes/i-agent-runtime-provider.tslibs/application-generic/src/agent-runtimes/index.tslibs/application-generic/src/dtos/workflow/workflow-preferences.dto.tslibs/application-generic/src/index.tslibs/application-generic/src/usecases/preview/preview.usecase.tslibs/application-generic/src/usecases/upsert-preferences/upsert-preferences.command.tslibs/application-generic/src/usecases/upsert-workflow/upsert-workflow.command.tslibs/dal/src/repositories/agent/agent.entity.tslibs/dal/src/repositories/agent/agent.schema.tspackages/shared/src/consts/providers/agent-runtime-providers.tspackages/shared/src/consts/providers/index.tspackages/shared/src/dto/agent/index.tspackages/shared/src/dto/agent/managed-runtime.dto.tspackages/shared/src/dto/index.tspackages/shared/src/types/channel.tspackages/shared/src/types/feature-flags.tspackages/shared/src/types/providers.tspackages/shared/src/types/workflow-channel-preferences.tspackages/shared/src/utils/buildWorkflowPreferences.ts
| }, | ||
| externalAgentId: Schema.Types.String, | ||
| }, | ||
| deletedAt: Schema.Types.String, |
There was a problem hiding this comment.
Use a date type for soft-delete timestamps.
deletedAt is currently a string (Line 43). This should be a Date field to keep sort/filter semantics correct and avoid inconsistent timestamp parsing.
Suggested fix
- deletedAt: Schema.Types.String,
+ deletedAt: Schema.Types.Date,As per coding guidelines: “libs/dal/**: Review with focus on data integrity and query performance.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| deletedAt: Schema.Types.String, | |
| deletedAt: Schema.Types.Date, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@libs/dal/src/repositories/agent/agent.schema.ts` at line 43, The agent
schema's soft-delete field "deletedAt" is declared as Schema.Types.String;
change it to a Date type to preserve correct sorting/filtering and consistent
timestamp semantics by updating the "deletedAt" field in agent.schema.ts to use
Schema.Types.Date (optionally nullable/default null and indexed/sparse if you
rely on queries), ensuring any related code that sets/reads deletedAt (e.g.,
softDelete methods or repository queries) writes/reads Date objects or converts
strings to Date objects accordingly.
There was a problem hiding this comment.
deletedAt is no longer in the schema. Soft-delete fields (deletedAt, pendingExternalDelete) were removed; deletion is now synchronous and either-or (Novu only, or Novu + provider via deleteFromProvider query).
There was a problem hiding this comment.
@djabarovgeorge — confirmed, the current schema file no longer contains deletedAt or pendingExternalDelete. The synchronous deleteFromProvider path (calling the provider directly before local cleanup) is a cleaner design anyway — no background job, no partial state to reconcile. The review comment is stale and can be dismissed. Thanks for the clarification!
🧠 Learnings used
Learnt from: ChmaraX
Repo: novuhq/novu PR: 11003
File: packages/framework/src/handler.ts:0-0
Timestamp: 2026-05-06T13:36:38.575Z
Learning: In novuhq/novu, Biome's noUselessUndefinedInitialization rule automatically strips '= undefined' initializers in TypeScript files. When fixing TS2454 'used before assigned' errors, prefer structural fixes over adding '= undefined' (which will be removed and can cause the commit to fail). Do not initialize variables with undefined. Instead, initialize with a concrete value at declaration when possible, or refactor to ensure the value is assigned before use (e.g., via a helper function or inline initialization that yields a valid value). Use guards or explicit control flow to guarantee assignment before first use. This guidance applies broadly to TypeScript files in the repository, including packages/framework/src/handler.ts.
Learnt from: scopsy
Repo: novuhq/novu PR: 11060
File: packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.tsx:55-56
Timestamp: 2026-05-11T05:04:35.896Z
Learning: In PR reviews for TypeScript/TSX code, do not suggest adding blank lines before `return` statements. Follow the repository’s existing formatting conventions and avoid raising review comments about this specific whitespace change (blank line insertion before `return`).
| export type AgentMcpServerDto = { | ||
| /** Provider-assigned ID for the MCP server */ | ||
| externalId: string; | ||
| name: string; | ||
| url: string; | ||
| /** Optional token used to authenticate with the MCP server */ | ||
| authToken?: string; | ||
| }; |
There was a problem hiding this comment.
Avoid exposing raw MCP auth tokens in shared read DTOs.
Including authToken in a cross-layer DTO makes it easy to leak provider secrets to UI/logging paths. Prefer a write-only token field for updates and a read DTO with hasAuthToken/masked metadata only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/shared/src/dto/agent/managed-runtime.dto.ts` around lines 25 - 32,
The AgentMcpServerDto currently exposes a raw authToken which can leak provider
secrets; remove the authToken field from this shared read DTO
(AgentMcpServerDto) and replace it with a safe read-only indicator such as
hasAuthToken: boolean (or masked metadata) to signal presence without exposing
the secret, and add a separate write/update DTO (e.g., AgentMcpServerUpdateDto
or AgentMcpServerWriteDto) that includes authToken?: string for incoming updates
only; update any consumers/serializers to use the read DTO for responses and the
write DTO for mutations.
There was a problem hiding this comment.
💬 Acknowledged — deferred. The read DTO does not currently expose authToken from the provider (Anthropic's getConfig mapping at mapMcpServer only writes externalId, name, url). The optional field on the shared DTO is reserved for future write/update flows. Tracking the split into separate read/write DTOs (hasAuthToken: boolean for reads) as a follow-up to avoid blocking this PR.
There was a problem hiding this comment.
@djabarovgeorge, thanks for the context — that's a meaningful clarification. Since mapMcpServer in the Anthropic getConfig path currently only populates externalId, name, and url, the optional authToken field carries no runtime exposure risk at this time. The concern becomes relevant only once a write/update flow populates it.
Would you like me to open a GitHub issue to track the follow-up work of splitting into separate read (hasAuthToken: boolean) and write (authToken?: string) DTOs? That way it won't get lost between PRs.
🧠 Learnings used
Learnt from: ChmaraX
Repo: novuhq/novu PR: 11003
File: packages/framework/src/handler.ts:0-0
Timestamp: 2026-05-06T13:36:38.575Z
Learning: In novuhq/novu, Biome's noUselessUndefinedInitialization rule automatically strips '= undefined' initializers in TypeScript files. When fixing TS2454 'used before assigned' errors, prefer structural fixes over adding '= undefined' (which will be removed and can cause the commit to fail). Do not initialize variables with undefined. Instead, initialize with a concrete value at declaration when possible, or refactor to ensure the value is assigned before use (e.g., via a helper function or inline initialization that yields a valid value). Use guards or explicit control flow to guarantee assignment before first use. This guidance applies broadly to TypeScript files in the repository, including packages/framework/src/handler.ts.
Learnt from: scopsy
Repo: novuhq/novu PR: 11060
File: packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.tsx:55-56
Timestamp: 2026-05-11T05:04:35.896Z
Learning: In PR reviews for TypeScript/TSX code, do not suggest adding blank lines before `return` statements. Follow the repository’s existing formatting conventions and avoid raising review comments about this specific whitespace change (blank line insertion before `return`).
| type: Schema.Types.ObjectId, | ||
| ref: 'Integration', | ||
| }, | ||
| externalAgentId: Schema.Types.String, |
There was a problem hiding this comment.
I guess we also need here vendor specific fields? Like anthropic specific environmentId, etc?
There was a problem hiding this comment.
💬 Discussion: keeping managedRuntime provider-agnostic by design. The persisted fields (providerId, _integrationId, externalAgentId) are the minimal stable linkage. Anthropic-specific data (environment id, etc.) lives on the Integration's credentials (where we already encrypt-at-rest and rotate). Live config (model, system prompt, MCP servers, tools, skills) is fetched from the provider on-demand via IAgentRuntimeProvider.getConfig — not persisted. This avoids drift and keeps the agent doc lean. If we ever need provider-specific metadata that doesn't fit credentials, we can add an opaque providerMetadata: Record<string, unknown> field then.
- Added support for managed runtime agents, allowing integration with Claude Platform. - Updated `CreateAgentRequestDto` to include `runtime` and `managedRuntime` fields. - Introduced `AgentSkillInputDto` for specifying skills during agent creation. - Enhanced `ProvisionManagedAgentCommand` to accept tools, MCP servers, and skills. - Updated the dashboard to handle new fields in agent creation and management. This update improves the flexibility and capabilities of agent provisioning, enabling users to leverage external skills and tools effectively.
…board integration - Fixed bugs related to agent runtime configuration handling in the dashboard. - Updated the agent-runtime API client to improve error handling and user experience. - Enhanced the runtime-config section with better error panels and notifications. These changes ensure a smoother user experience when managing agent runtimes and improve the overall stability of the dashboard.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/src/components/agents/create-agent-dialog.tsx (1)
229-265:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidation gap:
existingmode skips required-field checks for both API key and external agent ID.When
runtime === 'claude' && mode === 'existing':
apiKeyis never validated (line 229 only checksmode === 'create'), butagents-list.tsxrequiresbody.apiKeyto be truthy to create the integration. If the user leaves it blank, integration creation is silently skipped and the agent is created withmanagedRuntime.integrationId === ''— the backend will reject this with a 4xx and the user will see a generic toast.externalAgentIdis also never validated (line 263 only assigns it if.trim()is truthy). An empty value silently degenerates into the create-new-agent code path, but withbody.runtime = 'managed'already set, so Claude will provision a brand-new agent instead of linking to the existing one — the opposite of what the user intended.Both checks belong in the validation block before the payload is assembled.
🛠️ Suggested fix
- if (runtime === 'claude' && mode === 'create' && !apiKey.trim()) { - nextErrors.apiKey = 'Anthropic API key is required.'; - } + if (runtime === 'claude' && !apiKey.trim()) { + nextErrors.apiKey = 'Anthropic API key is required.'; + } + + if (runtime === 'claude' && mode === 'existing' && !externalAgentId.trim()) { + nextErrors.externalAgentId = 'External agent ID is required when linking an existing agent.'; + }(You'll also need to add
externalAgentId?: stringtoFormErrorsand surface it on the input at lines 468–479.)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/agents/create-agent-dialog.tsx` around lines 229 - 265, The validation currently only checks apiKey when runtime === 'claude' && mode === 'create', which lets mode === 'existing' submit without apiKey or externalAgentId; move/extend validation into the pre-payload block so that when runtime === 'claude' && mode === 'existing' you validate apiKey.trim() and externalAgentId.trim() and add errors to nextErrors (e.g., nextErrors.apiKey and nextErrors.externalAgentId) before calling setErrors/return; ensure CreateAgentDialogSubmitBody population still sets body.apiKey and body.externalAgentId only after validation; also add externalAgentId?: string to the FormErrors type and surface that error on the externalAgentId input so the UI shows the message.
♻️ Duplicate comments (2)
packages/shared/src/dto/agent/managed-runtime.dto.ts (1)
52-59:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift
AgentMcpServerDto.authTokenis still exposed on the read DTO.This concern was raised previously and remains unaddressed in the current code: an
authToken?: stringfield on a DTO that flows into both read and write paths makes it easy for MCP auth tokens to leak through API responses, logs, or analytics events. Split into a read DTO that exposes onlyhasAuthToken: boolean(or masked metadata) and a separate write DTO that acceptsauthToken?: stringfor updates.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/dto/agent/managed-runtime.dto.ts` around lines 52 - 59, AgentMcpServerDto currently exposes authToken and must be split into separate read/write DTOs: keep a read-only shape (e.g., AgentMcpServerReadDto) that replaces authToken with a boolean hasAuthToken (or masked metadata) and a write/update shape (e.g., AgentMcpServerWriteDto) that accepts authToken?: string for incoming requests. Replace usages of AgentMcpServerDto in response/serialization paths with AgentMcpServerReadDto and use AgentMcpServerWriteDto for controllers/services that accept or persist authToken; ensure any mapping code (serializers or constructors that currently reference AgentMcpServerDto.authToken) only sets hasAuthToken on read DTOs and only reads/writes authToken from the write DTO.libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts (1)
55-71:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winStill using bracket notation on
APIError.headers— request-id and Retry-After are being dropped.This was raised in a prior review and remains unresolved.
APIError.headersin@anthropic-ai/sdkis aHeadersinstance, soerr.headers?.['request-id']anderr.headers?.['retry-after']always evaluate toundefined. The SDK also exposeserr.requestIDdirectly. As written:
- The
requestIdarg passed into every error class is alwaysundefined— request-id is never logged or surfaced.parseRetryAfteralways falls through to its60_000default, so 429s lose their actual Retry-After hint and the API/dashboard's Retry-After-aware retry logic gets the wrong value.🩹 Proposed fix
if (err instanceof APIError) { - const requestId = err.headers?.['request-id'] as string | undefined; + const requestId = err.requestID ?? err.headers?.get('request-id') ?? undefined; if (err.status === 401) { throw new AgentRuntimeUnauthorizedError(err.message, PROVIDER_ID, requestId); } if (err.status === 403) { throw new AgentRuntimeForbiddenError(err.message, PROVIDER_ID, requestId); } if (err.status === 404) { throw new AgentRuntimeNotFoundError(err.message, PROVIDER_ID, requestId); } if (err.status === 429) { - const retryAfterMs = parseRetryAfter(err.headers?.['retry-after'] as string | undefined); + const retryAfterMs = parseRetryAfter(err.headers?.get('retry-after') ?? undefined); throw new AgentRuntimeRateLimitedError(err.message, PROVIDER_ID, retryAfterMs, requestId); }`@anthropic-ai/sdk` 0.95.1 APIError headers Headers object requestID property🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts` around lines 55 - 71, The code is reading request-id and retry-after using bracket notation on APIError.headers which is a Headers instance, so change to read requestId from err.requestID (set requestId = err.requestID as string | undefined) and read retry-after via the Headers.get method (call parseRetryAfter(err.headers?.get('retry-after') as string | undefined)); then pass that requestId into the AgentRuntimeUnauthorizedError/AgentRuntimeForbiddenError/AgentRuntimeNotFoundError/AgentRuntimeRateLimitedError constructors (and pass the parsed retryAfterMs to AgentRuntimeRateLimitedError) while keeping PROVIDER_ID unchanged.
🧹 Nitpick comments (6)
libs/application-generic/src/agent-runtimes/i-agent-runtime-provider.ts (1)
10-32: ⚡ Quick winUse
interfaceinstead oftypefor backend type definitions.
CreateAgentInput,CreateAgentResult, andUpdateAgentRuntimeConfigInputshould be declared asinterfaceon the backend to match the rest of the contract (which already usesinterface IAgentRuntimeProvider).♻️ Proposed change
-export type CreateAgentInput = { +export interface CreateAgentInput { name: string; model?: string; systemPrompt?: string; /** Builtin tool type strings, e.g. 'web_search', 'bash' */ tools?: string[]; /** MCP server catalog entries resolved to {name, url} pairs */ mcpServers?: Array<{ name: string; url: string }>; /** Skills to attach to the agent at creation time. Maximum 20. */ skills?: AgentSkillDto[]; -}; +} -export type CreateAgentResult = { +export interface CreateAgentResult { externalAgentId: string; -}; +} -export type UpdateAgentRuntimeConfigInput = { +export interface UpdateAgentRuntimeConfigInput { model?: string; systemPrompt?: string; mcpServers?: AgentMcpServerDto[]; tools?: AgentToolDto[]; skills?: AgentSkillDto[]; -}; +}As per coding guidelines: "On the backend: use
interfacefor type definitions; on the frontend: usetypefor type definitions".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/application-generic/src/agent-runtimes/i-agent-runtime-provider.ts` around lines 10 - 32, Replace the three backend type aliases with interfaces to match the existing contract style: convert CreateAgentInput, CreateAgentResult, and UpdateAgentRuntimeConfigInput from "type" to "interface" declarations, preserving all property names, optional markers, and nested types (e.g., AgentSkillDto, AgentMcpServerDto, AgentToolDto) so consumers of IAgentRuntimeProvider see consistent interface types; ensure JSDoc/comments (like the tools and mcpServers descriptions) remain above the corresponding interface properties.apps/dashboard/src/components/agents/create-agent-dialog.tsx (1)
250-262: 💤 Low valueOptional: extract the hard-coded model and
providerId: 'anthropic'strings.
'claude-opus-4-5'(line 253) and'anthropic'(line 251) are duplicated inagents-list.tsxand the provider catalog. Consider pulling them from the sharedAGENT_RUNTIME_PROVIDERSentry (default model) andAgentRuntimeProviderIdEnum.Anthropicso a model bump only needs to be made in one place.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/agents/create-agent-dialog.tsx` around lines 250 - 262, Replace hard-coded provider and model strings in the create-agent payload with the shared constants: use AgentRuntimeProviderIdEnum.Anthropic (instead of 'anthropic') to set providerId and pull the default model from AGENT_RUNTIME_PROVIDERS (e.g., AGENT_RUNTIME_PROVIDERS[AgentRuntimeProviderIdEnum.Anthropic].defaultModel) instead of 'claude-opus-4-5'; update the block that assigns body.managedRuntime (and any related mapping for skills) to reference these constants so future model/provider changes are centralized.packages/shared/src/consts/providers/claude-skills.ts (1)
64-157: 💤 Low valueOptional: track
CLAUDE_OPENSOURCE_SKILLSas a parked TODO so it doesn't quietly rot.
CLAUDE_OPENSOURCE_SKILLSis defined (~90 lines) but neither exported nor included inCLAUDE_ANTHROPIC_SKILLS, so it ships as dead bytes until the/v1/skillsupload flow lands. Consider either:
- Adding a
// TODO(NV-XXXX):reference next to the comment on line 156 so the link to the follow-up work is grep-able, or- Stripping the constant until the upload feature is implemented and reintroducing it then.
Either approach prevents accidental drift between this list and the upstream
anthropics/skillsrepo.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/consts/providers/claude-skills.ts` around lines 64 - 157, CLAUDE_OPENSOURCE_SKILLS is defined but unused and will rot; either add a grep-able TODO or remove it: locate the CLAUDE_OPENSOURCE_SKILLS constant and either (a) add a comment like "// TODO(NV-XXXX): track open-source skills list until /v1/skills upload is implemented" immediately above or next to the declaration so it’s searchable, or (b) delete the CLAUDE_OPENSOURCE_SKILLS block and reintroduce it when the upload flow exists; ensure CLAUDE_ANTHROPIC_SKILLS (and any reference to CLAUDE_PREBUILT_SKILLS) remains unchanged.apps/dashboard/src/components/agents/agents-list.tsx (1)
95-106: ⚡ Quick winHard-coded integration
namecollides for users with multiple managed agents.Every managed-runtime agent in an environment creates an integration named
"Anthropic (managed agents)". From the integrations list this becomes indistinguishable noise (and there's no link back to which agent the credential belongs to). Consider scoping the name with the agent identifier or name, e.g.:- name: 'Anthropic (managed agents)', + name: `Anthropic — ${body.name}`,That keeps it human-readable and traceable. Same comment applies if/when more managed providers are added.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/agents/agents-list.tsx` around lines 95 - 106, Integration names are being hard-coded to "Anthropic (managed agents)" which collides when multiple managed agents exist; update the createIntegration call (the object passed to createIntegration in agents-list.tsx) to include a unique, human-readable identifier by incorporating the agent's name or id (e.g., append agent.name or agent._id into the name field) so each created integration is traceable back to its agent, and ensure the chosen field (agent.name or agent._id) is available in scope and safely stringified/escaped before concatenation.libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts (1)
116-189: ⚖️ Poor tradeoffRemove unnecessary
as anycasts for typed access tobeta.agentsAPI.The provider casts
client as anyforcreate(line 116),retrieve(line 151), andupdate(line 189), but uses the typed surface forarchive(line 139). The inconsistency indicates the casts are defensive or residual from prototyping. Sincebeta.agents.archive()works without casting, the typed API is accessible for all methods.Removing the
as anycasts will:
- Allow TypeScript to catch payload-shape changes at compile time rather than runtime
- Eliminate implicit
anytypes on method returns (e.g.,agent.model,agent.skills)- Be consistent across all
beta.agentsmethodsVerify that your TypeScript configuration or IDE correctly infers types for
beta.agents.create,retrieve, andupdatein@anthropic-ai/[email protected], then drop the casts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts` around lines 116 - 189, The file uses defensive casts like (client as any).beta.agents.create/retrieve/update while archive uses the typed client; remove the unnecessary "as any" casts and call client.beta.agents.create, client.beta.agents.retrieve and client.beta.agents.update directly so TypeScript can infer the correct request/response types (adjust any downstream usages that assumed implicit any on agent.model/agent.skills to match the SDK types); ensure buildClient returns the correctly typed client so the compiler recognizes beta.agents methods without casting.apps/api/src/app/agents/e2e/managed-agent.e2e.ts (1)
30-42: ⚡ Quick winUse the runtime integration channel in this fixture.
The managed-runtime flow stores Anthropic credentials as an agent-runtime integration, but this setup inserts an
IN_APPintegration. That means the suite will not catch bugs in channel-specific lookup or validation.Suggested fix
- channel: ChannelTypeEnum.IN_APP, + channel: ChannelTypeEnum.AGENT_RUNTIME,Based on PR objectives: the PR adds
ChannelTypeEnum.AGENT_RUNTIMEfor managed runtime integrations.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/e2e/managed-agent.e2e.ts` around lines 30 - 42, The fixture creates an integration with ChannelTypeEnum.IN_APP but the managed-runtime flow expects ChannelTypeEnum.AGENT_RUNTIME; update the integration object in the test (the call to integrationRepository.create where providerId is AgentRuntimeProviderIdEnum.Anthropic and name 'Anthropic Runtime E2E' / identifier `anthropic-runtime-e2e-${Date.now()}`) to set channel: ChannelTypeEnum.AGENT_RUNTIME so the test uses the runtime integration path and validates channel-specific lookup/validation logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/app/agents/agents.controller.ts`:
- Around line 123-134: The GET /agents/runtime-providers route is not
feature-flag gated; update the listAgentRuntimeProviders method to check the
managed-runtime feature flag before returning AGENT_RUNTIME_PROVIDERS and deny
access when the flag is disabled. Inside listAgentRuntimeProviders (referencing
the method name) call the existing feature-flag check utility (e.g.,
managedRuntimeEnabled(), featureFlagService.isEnabled('managed-runtime'), or
equivalent used elsewhere in the codebase) and if it returns false, respond with
an appropriate forbidden/not-found behavior (throw
HttpException/ForbiddenException or return an empty list) instead of returning
AGENT_RUNTIME_PROVIDERS; otherwise continue to return AGENT_RUNTIME_PROVIDERS as
AgentRuntimeProviderResponseDto[].
In `@apps/api/src/app/agents/dtos/agent-runtime-config.dto.ts`:
- Around line 84-96: The AgentRuntimeCapabilitiesDto is missing the new "skills"
capability so the GET /agents/runtime-providers schema is out of sync; add a
boolean skills property decorated with `@ApiProperty`() to the
AgentRuntimeCapabilitiesDto class (and ensure any code that builds or maps
AgentRuntimeCapabilities objects to this DTO includes the skills flag) so
clients can detect provider skills support.
In `@apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts`:
- Around line 35-90: The current isManaged check (const isManaged =
command.runtime === 'managed' && command.managedRuntime) allows a missing
managedRuntime to fall through to the self-hosted create path and silently
persist an invalid agent; change validation in the create logic to explicitly
reject when command.runtime === 'managed' but command.managedRuntime is falsy by
throwing a clear ValidationError (or similar) before calling
agentRepository.create, e.g. in the create-agent use case (around isManaged and
before any repository calls), so that provisionManagedAgentUsecase.execute and
the ProvisionManagedAgentCommand are only used when managedRuntime is present
and the invalid DTO is never persisted.
In `@apps/dashboard/src/components/agents/agents-list.tsx`:
- Around line 90-122: The current createMutation flow calls createIntegration
then createAgent and can leave orphaned integrations if createAgent fails; wrap
the "createIntegration → createAgent" sequence inside a try/catch in
createMutation: after createIntegration succeeds capture
integrationResponse.data._id, call createAgent, and if createAgent throws call
deleteIntegration(integrationResponse.data._id) (log any deletion error but do
not swallow the original error) before rethrowing so the UI shows the original
failure; alternatively, prefer moving this whole flow into a backend
transactional API so createIntegration and createAgent are atomic (references:
createMutation, createIntegration, createAgent, deleteIntegration,
requireEnvironment, integrationResponse.data._id).
In
`@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts`:
- Around line 110-205: The provider currently reads the API key via `(this as
any)._apiKey`; change the class to accept and store the apiKey via constructor
injection (e.g., add a private readonly apiKey: string field and a constructor
AnthropicAgentRuntimeProvider(apiKey: string)), update createAnthropicProvider
to instantiate the class with new AnthropicAgentRuntimeProvider(apiKey) and
remove the ad-hoc cast assignment, and update all usages in methods createAgent,
deleteAgent, getConfig, and updateConfig to call this.buildClient(this.apiKey)
(or pass this.apiKey to the existing buildClient overload) instead of `(this as
any)._apiKey`; keep validateCredentials/buildClient's apiKey parameter behavior
intact.
- Around line 110-132: createAgent is currently wrapped in withRetry which can
double-create upstream agents if the first create succeeds but the response is
lost; either stop using withRetry for createAgent or make the create call
idempotent via the SDK's idempotency key. Fix by changing createAgent to call
(client as any).beta.agents.create(...) directly (remove the withRetry wrapper
around the closure) and let normaliseError(err) propagate the failure, or
alternately pass a stable idempotencyKey to beta.agents.create (e.g., derived
from a provided stable input field or deterministic hash of
input.name+input.model) so retries via withRetry are deduped; update any related
tests and keep deleteAgent/getConfig unchanged (consider same idempotency
approach for updateConfig which uses update).
In `@packages/shared/src/consts/providers/claude-mcp-servers.ts`:
- Around line 24-412: Update the incorrect MCP endpoint URLs in the
CLAUDE_MCP_SERVERS constant: for id 'slack' (Slack) change the url from .../sse
to the official /mcp path, for id 'stripe' (Stripe) use the vendor-recommended
bare MCP domain, for id 'figma' (Figma) change .../sse to /mcp, for id
'airtable' (Airtable) change .../sse to /mcp, and for Atlassian entries (e.g.,
id 'atlassian-rovo' and other atlassian-based entries like 'confluence' and
'jira') update their urls to use the official /v1/sse path; locate and edit
these entries inside the CLAUDE_MCP_SERVERS array to match the vendor docs
before rebuilding and releasing.
---
Outside diff comments:
In `@apps/dashboard/src/components/agents/create-agent-dialog.tsx`:
- Around line 229-265: The validation currently only checks apiKey when runtime
=== 'claude' && mode === 'create', which lets mode === 'existing' submit without
apiKey or externalAgentId; move/extend validation into the pre-payload block so
that when runtime === 'claude' && mode === 'existing' you validate apiKey.trim()
and externalAgentId.trim() and add errors to nextErrors (e.g., nextErrors.apiKey
and nextErrors.externalAgentId) before calling setErrors/return; ensure
CreateAgentDialogSubmitBody population still sets body.apiKey and
body.externalAgentId only after validation; also add externalAgentId?: string to
the FormErrors type and surface that error on the externalAgentId input so the
UI shows the message.
---
Duplicate comments:
In
`@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts`:
- Around line 55-71: The code is reading request-id and retry-after using
bracket notation on APIError.headers which is a Headers instance, so change to
read requestId from err.requestID (set requestId = err.requestID as string |
undefined) and read retry-after via the Headers.get method (call
parseRetryAfter(err.headers?.get('retry-after') as string | undefined)); then
pass that requestId into the
AgentRuntimeUnauthorizedError/AgentRuntimeForbiddenError/AgentRuntimeNotFoundError/AgentRuntimeRateLimitedError
constructors (and pass the parsed retryAfterMs to AgentRuntimeRateLimitedError)
while keeping PROVIDER_ID unchanged.
In `@packages/shared/src/dto/agent/managed-runtime.dto.ts`:
- Around line 52-59: AgentMcpServerDto currently exposes authToken and must be
split into separate read/write DTOs: keep a read-only shape (e.g.,
AgentMcpServerReadDto) that replaces authToken with a boolean hasAuthToken (or
masked metadata) and a write/update shape (e.g., AgentMcpServerWriteDto) that
accepts authToken?: string for incoming requests. Replace usages of
AgentMcpServerDto in response/serialization paths with AgentMcpServerReadDto and
use AgentMcpServerWriteDto for controllers/services that accept or persist
authToken; ensure any mapping code (serializers or constructors that currently
reference AgentMcpServerDto.authToken) only sets hasAuthToken on read DTOs and
only reads/writes authToken from the write DTO.
---
Nitpick comments:
In `@apps/api/src/app/agents/e2e/managed-agent.e2e.ts`:
- Around line 30-42: The fixture creates an integration with
ChannelTypeEnum.IN_APP but the managed-runtime flow expects
ChannelTypeEnum.AGENT_RUNTIME; update the integration object in the test (the
call to integrationRepository.create where providerId is
AgentRuntimeProviderIdEnum.Anthropic and name 'Anthropic Runtime E2E' /
identifier `anthropic-runtime-e2e-${Date.now()}`) to set channel:
ChannelTypeEnum.AGENT_RUNTIME so the test uses the runtime integration path and
validates channel-specific lookup/validation logic.
In `@apps/dashboard/src/components/agents/agents-list.tsx`:
- Around line 95-106: Integration names are being hard-coded to "Anthropic
(managed agents)" which collides when multiple managed agents exist; update the
createIntegration call (the object passed to createIntegration in
agents-list.tsx) to include a unique, human-readable identifier by incorporating
the agent's name or id (e.g., append agent.name or agent._id into the name
field) so each created integration is traceable back to its agent, and ensure
the chosen field (agent.name or agent._id) is available in scope and safely
stringified/escaped before concatenation.
In `@apps/dashboard/src/components/agents/create-agent-dialog.tsx`:
- Around line 250-262: Replace hard-coded provider and model strings in the
create-agent payload with the shared constants: use
AgentRuntimeProviderIdEnum.Anthropic (instead of 'anthropic') to set providerId
and pull the default model from AGENT_RUNTIME_PROVIDERS (e.g.,
AGENT_RUNTIME_PROVIDERS[AgentRuntimeProviderIdEnum.Anthropic].defaultModel)
instead of 'claude-opus-4-5'; update the block that assigns body.managedRuntime
(and any related mapping for skills) to reference these constants so future
model/provider changes are centralized.
In
`@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts`:
- Around line 116-189: The file uses defensive casts like (client as
any).beta.agents.create/retrieve/update while archive uses the typed client;
remove the unnecessary "as any" casts and call client.beta.agents.create,
client.beta.agents.retrieve and client.beta.agents.update directly so TypeScript
can infer the correct request/response types (adjust any downstream usages that
assumed implicit any on agent.model/agent.skills to match the SDK types); ensure
buildClient returns the correctly typed client so the compiler recognizes
beta.agents methods without casting.
In `@libs/application-generic/src/agent-runtimes/i-agent-runtime-provider.ts`:
- Around line 10-32: Replace the three backend type aliases with interfaces to
match the existing contract style: convert CreateAgentInput, CreateAgentResult,
and UpdateAgentRuntimeConfigInput from "type" to "interface" declarations,
preserving all property names, optional markers, and nested types (e.g.,
AgentSkillDto, AgentMcpServerDto, AgentToolDto) so consumers of
IAgentRuntimeProvider see consistent interface types; ensure JSDoc/comments
(like the tools and mcpServers descriptions) remain above the corresponding
interface properties.
In `@packages/shared/src/consts/providers/claude-skills.ts`:
- Around line 64-157: CLAUDE_OPENSOURCE_SKILLS is defined but unused and will
rot; either add a grep-able TODO or remove it: locate the
CLAUDE_OPENSOURCE_SKILLS constant and either (a) add a comment like "//
TODO(NV-XXXX): track open-source skills list until /v1/skills upload is
implemented" immediately above or next to the declaration so it’s searchable, or
(b) delete the CLAUDE_OPENSOURCE_SKILLS block and reintroduce it when the upload
flow exists; ensure CLAUDE_ANTHROPIC_SKILLS (and any reference to
CLAUDE_PREBUILT_SKILLS) remains unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 76778214-727d-43fe-bbf3-8da0246b2ffb
📒 Files selected for processing (17)
apps/api/src/app/agents/agents.controller.tsapps/api/src/app/agents/dtos/agent-runtime-config.dto.tsapps/api/src/app/agents/e2e/managed-agent.e2e.tsapps/api/src/app/agents/usecases/create-agent/create-agent.usecase.tsapps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.tsapps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.tsapps/dashboard/src/api/agents.tsapps/dashboard/src/components/agents/agents-list.tsxapps/dashboard/src/components/agents/create-agent-dialog.tsxlibs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.tslibs/application-generic/src/agent-runtimes/i-agent-runtime-provider.tspackages/shared/src/consts/providers/agent-runtime-providers.tspackages/shared/src/consts/providers/claude-mcp-servers.tspackages/shared/src/consts/providers/claude-skills.tspackages/shared/src/consts/providers/claude-tools.tspackages/shared/src/consts/providers/index.tspackages/shared/src/dto/agent/managed-runtime.dto.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/shared/src/consts/providers/index.ts
- packages/shared/src/consts/providers/agent-runtime-providers.ts
- apps/dashboard/src/api/agents.ts
- apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts
- Added support for adopting existing agents from the provider platform, allowing users to link to an existing agent instead of creating a new one. - Updated `CreateAgentRequestDto` and `ProvisionManagedAgentCommand` to include `externalAgentId` for adoption scenarios. - Enhanced the `CreateAgent` use case to handle temporary placeholders for name and identifier during the adoption process. - Improved error handling for scenarios where the external agent ID does not exist or the API key is invalid. - Updated the dashboard components to accommodate the new adoption flow, including validation for external agent ID input. These changes enhance the flexibility of agent management, enabling users to leverage existing agents seamlessly.
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (3)
apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts (1)
43-44:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDefense-in-depth: still silently downgrades
runtime: 'managed'to a self-hosted insert whenmanagedRuntimeis absent.
isManaged = command.runtime === 'managed' && command.managedRuntimeevaluates tofalsewhenruntime === 'managed'butmanagedRuntimeis missing, so the code falls through to the plainagentRepository.create(...)branch on line 127 and silently persists a self-hosted agent labeled as managed-in-intent. The DTO@ValidateIfshould normally catch this, but untilcreate-agent.command.tsis fixed (@Type(() => Object)and the type-only import still neuter the nested validator), this is reachable. ThrowUnprocessableEntityExceptionexplicitly here.Suggested fix
-import { ConflictException, Injectable } from '@nestjs/common'; +import { ConflictException, Injectable, UnprocessableEntityException } from '@nestjs/common'; @@ - const isManaged = command.runtime === 'managed' && command.managedRuntime; + if (command.runtime === 'managed' && !command.managedRuntime) { + throw new UnprocessableEntityException('managedRuntime is required when runtime is "managed".'); + } + const isManaged = command.runtime === 'managed';As per coding guidelines:
apps/api/**: Check for proper error handling and input validation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts` around lines 43 - 44, The current isManaged calculation can silently downgrade runtime:'managed' when managedRuntime is missing; update the logic in create-agent.usecase.ts to explicitly validate this: if command.runtime === 'managed' and !command.managedRuntime, throw an UnprocessableEntityException with a clear message instead of falling through to agentRepository.create; keep the existing isManaged flag (e.g., const isManaged = command.runtime === 'managed' && !!command.managedRuntime) but add the explicit guard before any repository create calls to prevent persisting a mis-labeled agent.apps/api/src/app/agents/usecases/create-agent/create-agent.command.ts (1)
15-15:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNested DTO validation for
managedRuntimeis still broken.The class is still imported as a type-only symbol on line 15, and
@Type(() => Object)on line 43 does not triggerclass-transformerto instantiateManagedRuntimeDto. As a result@ValidateNested()walks a plain object and the inner@IsEnum/@IsString/etc. decorators onManagedRuntimeDtonever run, so invalidproviderId,integrationId, orexternalAgentIdshapes pass through unchecked.Suggested fix
-import type { ManagedRuntimeDto } from '../../dtos/agent-runtime-config.dto'; +import { ManagedRuntimeDto } from '../../dtos/agent-runtime-config.dto'; @@ `@ValidateIf`((o) => o.runtime === 'managed') `@IsObject`() `@ValidateNested`() - `@Type`(() => Object) + `@Type`(() => ManagedRuntimeDto) managedRuntime?: ManagedRuntimeDto;Also applies to: 40-44
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/create-agent/create-agent.command.ts` at line 15, The nested DTO validation fails because ManagedRuntimeDto is imported as a type-only symbol and the property decorator uses `@Type`(() => Object), so class-transformer never instantiates ManagedRuntimeDto and `@ValidateNested`() walks a plain object; fix by importing ManagedRuntimeDto as a runtime value (remove the type-only import) and change the property decorator for managedRuntime to `@Type`(() => ManagedRuntimeDto) alongside `@ValidateNested`(), so class-transformer will create a ManagedRuntimeDto instance and inner decorators (e.g., `@IsEnum/`@IsString) will run; apply the same change to the other managedRuntime occurrences around the existing decorators.apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts (1)
1-1:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
providerIdstill lacks enum validation.
providerIdis only decorated with@IsNotEmpty(), so any non-empty string (e.g."foo") survives DTO validation and is forwarded togetAgentRuntimeProvider. Add@IsEnum(AgentRuntimeProviderIdEnum)and promote the import from a type-only import so the enum is available at runtime.Suggested fix
-import type { AgentRuntimeProviderIdEnum, AgentSkillDto } from '@novu/shared'; +import { AgentRuntimeProviderIdEnum } from '@novu/shared'; +import type { AgentSkillDto } from '@novu/shared'; import { Type } from 'class-transformer'; -import { IsArray, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; @@ `@IsNotEmpty`() + `@IsEnum`(AgentRuntimeProviderIdEnum) providerId: AgentRuntimeProviderIdEnum;Also applies to: 22-23
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts` at line 1, The providerId field lacks enum validation; change the type-only import of AgentRuntimeProviderIdEnum to a runtime import (remove the "type" modifier) so the enum is available at runtime, and add the class-validator decorator `@IsEnum`(AgentRuntimeProviderIdEnum) to the providerId property (in the DTO used by provision-managed-agent.command and the related DTO on lines 22-23) instead of only `@IsNotEmpty`(), so invalid strings are rejected before calling getAgentRuntimeProvider.
🧹 Nitpick comments (3)
libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts (2)
224-230: 💤 Low value
parseRetryAfterignores the HTTP‑date form ofRetry-After.Per RFC 9110,
Retry-Aftermay be either delta-seconds or an HTTP-date.parseFloatwill returnNaNfor the date form and silently fall back to 60s, which is fine as a safety net but can grossly under- or over-state the actual wait. If you want to honor a 429/503 hint accurately, parse the date branch as well (Date.parse(header) - Date.now()), clamped to a non-negative value.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts` around lines 224 - 230, The parseRetryAfter function currently treats the header only as delta-seconds; update parseRetryAfter(header: string | undefined) to also handle the HTTP-date form: if parseFloat(header) yields NaN, attempt a date parse (const ms = Date.parse(header) - Date.now()), ensure the result is a non-negative integer (Math.max(0, Math.round(ms))), and return that value in milliseconds; if the date parse is invalid fall back to the existing 60_000 ms default. Ensure you reference parseRetryAfter in anthropic-agent-runtime.provider.ts and preserve the original return type and fallback semantics.
306-327: 💤 Low value
buildToolsPayloadalways emits at least one entry, so thelength > 0guards increateAgent/updateConfigare dead code.
buildToolsPayloadunconditionally pushes theagent_toolset_20260401entry on line 315 before considering MCP servers, so its return array is never empty. ThetoolsPayload.length > 0checks at lines 124 and 198 will therefore always be true. This isn't itself broken, but it's misleading and hides the side effect that every create/patch implicitly resets the builtin toolset. Either drop the dead guards or short‑circuitbuildToolsPayloadto return[]when both inputs are absent so the gates become meaningful.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts` around lines 306 - 327, buildToolsPayload currently always pushes the agent_toolset_20260401 entry so callers' guards (toolsPayload.length > 0) in createAgent/updateConfig are dead; fix by short‑circuiting buildToolsPayload to return [] when both toolTypes is null/empty and mcpServers is null/empty (i.e. check if (!toolTypes || toolTypes.length===0) && (!mcpServers || mcpServers.length===0) then return []), otherwise build the payload as before (refer to buildToolsPayload, CLAUDE_BUILTIN_TOOLS, and the callers createAgent/updateConfig).apps/dashboard/src/components/agents/create-agent-dialog.tsx (1)
254-263: ⚡ Quick win
integrationId: ''is sent from the dialog and must be overwritten by the caller.Both submission branches set
managedRuntime.integrationIdto an empty string. The dialog itself never creates an integration, so this implicitly requires the caller (e.g.,agents-list.tsx) to create the integration first and overwriteintegrationId(or strip it) before forwarding to the API. This is easy to break silently if a new caller is added. Consider omittingintegrationIdhere and letting the caller add it, so the type system catches the missing value rather than shipping a placeholder.Also applies to: 273-287
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/agents/create-agent-dialog.tsx` around lines 254 - 263, The dialog currently always sets managedRuntime.integrationId to an empty string in both submission branches (see the body object built when isExistingMode and the other branch around lines 273-287), which forces callers to overwrite it; instead remove integrationId from the dialog payload when it's empty and only include managedRuntime.integrationId when a non-empty value is provided (e.g., check externalIntegrationId or a local integrationId field and conditionally add that property), so callers must explicitly supply an integrationId and the type system will catch missing values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/app/agents/e2e/managed-agent.e2e.ts`:
- Around line 81-83: The test mutates
process.env.IS_CONVERSATIONAL_AGENTS_ENABLED in the before hook without
restoring it; capture the previous value (e.g., const previous =
process.env.IS_CONVERSATIONAL_AGENTS_ENABLED) before setting it in the before()
block and add a matching after() hook that restores
process.env.IS_CONVERSATIONAL_AGENTS_ENABLED = previous (or deletes it if
previous was undefined) to avoid leaking the env var across test files.
- Line 8: The test imports the internal build path module
(AgentRuntimeFactoryModule) which couples the test to package internals and can
prevent stubs from intercepting runtime imports; change the import to the
package public entry (import * as AgentRuntimeFactoryModule from
'@novu/application-generic' or better import { getAgentRuntimeProvider } from
'@novu/application-generic') and update any stubbing/spy usage that references
the old module identity (e.g., the stub around getAgentRuntimeProvider at the
test stub location) so it targets the exported function from the public module
instead of the deep build path.
In `@apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts`:
- Line 87: The current guard if (isAdoptMode && provisionResult.adoptedName)
lets empty/falsy adoptedName skip renaming and leaves "__adopt_pending__" in
Mongo; update the logic in create-agent.usecase.ts around
isAdoptMode/provisionResult.adoptedName to either fail loudly or derive a
deterministic fallback name from externalAgentId: explicitly check for a
non-empty string (e.g., typeof provisionResult.adoptedName === 'string' &&
provisionResult.adoptedName.trim().length > 0), and if empty/undefined in adopt
mode either throw a specific error (so the request fails) or set adoptedName =
`adopted-${externalAgentId.slice(0,8)}` (or similar deterministic scheme) and
use that variable for the document rename/update instead of relying on the
original truthy guard.
In
`@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts`:
- Around line 49-51: The current throw in provision-managed-agent.usecase.ts
uses NotFoundException when decryptCredentials(...) returns no apiKey, which is
misleading; replace the NotFoundException with an UnprocessableEntityException
(or throw a dedicated AGENT_RUNTIME_* error your dashboard understands) in the
branch that checks for apiKey so callers can distinguish "integration exists but
not configured" from a true 404; update any tests or error mappings that expect
NotFoundException from the decryptCredentials -> apiKey check to expect the new
exception.
- Around line 73-77: The mapping for resolvedMcpServers silently substitutes an
empty url when a serverId is not found in CLAUDE_MCP_SERVERS; instead validate
and reject unknown IDs: in the provision-managed-agent use case (where
resolvedMcpServers is computed and later passed to runtimeProvider.createAgent)
detect any serverId with no matching CLAUDE_MCP_SERVERS entry and throw a
BadRequestException (imported from `@nestjs/common`) with a clear message listing
the invalid IDs, or alternatively filter them out and log a warning before
proceeding—do not pass {name: id, url: ''} to runtimeProvider.createAgent.
In `@apps/dashboard/src/components/agents/create-agent-dialog.tsx`:
- Around line 211-213: handleTemplateRotate is a no-op when
AGENT_TEMPLATES.length === 4 because it advances offset by 4; change the
rotation to advance by 1 (i.e., setTemplateOffset(prev => (prev + 1) %
AGENT_TEMPLATES.length)) or alternatively disable/hide the refresh control when
AGENT_TEMPLATES.length <= visibleCount; update the logic around
handleTemplateRotate, setTemplateOffset, and the visibleTemplates slice to use
the chosen approach so clicking the refresh actually rotates the shown
templates.
In
`@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts`:
- Around line 193-202: The PATCH handler currently rebuilds the entire
agent_toolset_20260401 when either patch.tools or patch.mcpServers is present,
and buildToolsPayload(undefined, ...) produces an empty enabledSet which
disables all existing tools; change the logic in
anthropic-agent-runtime.provider so it does not overwrite the side the caller
didn't touch by either (A) fetching current config via getConfig and merging
existing tool entries with the new tool list before calling
buildToolsPayload/updatePayload.tools, or (B) splitting the update into two
independent payload pieces so patch.tools only updates tool-enabled flags and
patch.mcpServers only updates MCP server entries (ensure buildToolsPayload is
only called with explicitly-supplied toolTypes or merged toolTypes, and do not
clear MCP servers when patch.mcpServers is undefined). Ensure references:
patch.tools, patch.mcpServers, buildToolsPayload, enabledSet,
updatePayload.tools, and getConfig are used to locate and implement the
merge/split.
---
Duplicate comments:
In `@apps/api/src/app/agents/usecases/create-agent/create-agent.command.ts`:
- Line 15: The nested DTO validation fails because ManagedRuntimeDto is imported
as a type-only symbol and the property decorator uses `@Type`(() => Object), so
class-transformer never instantiates ManagedRuntimeDto and `@ValidateNested`()
walks a plain object; fix by importing ManagedRuntimeDto as a runtime value
(remove the type-only import) and change the property decorator for
managedRuntime to `@Type`(() => ManagedRuntimeDto) alongside `@ValidateNested`(), so
class-transformer will create a ManagedRuntimeDto instance and inner decorators
(e.g., `@IsEnum/`@IsString) will run; apply the same change to the other
managedRuntime occurrences around the existing decorators.
In `@apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts`:
- Around line 43-44: The current isManaged calculation can silently downgrade
runtime:'managed' when managedRuntime is missing; update the logic in
create-agent.usecase.ts to explicitly validate this: if command.runtime ===
'managed' and !command.managedRuntime, throw an UnprocessableEntityException
with a clear message instead of falling through to agentRepository.create; keep
the existing isManaged flag (e.g., const isManaged = command.runtime ===
'managed' && !!command.managedRuntime) but add the explicit guard before any
repository create calls to prevent persisting a mis-labeled agent.
In
`@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts`:
- Line 1: The providerId field lacks enum validation; change the type-only
import of AgentRuntimeProviderIdEnum to a runtime import (remove the "type"
modifier) so the enum is available at runtime, and add the class-validator
decorator `@IsEnum`(AgentRuntimeProviderIdEnum) to the providerId property (in the
DTO used by provision-managed-agent.command and the related DTO on lines 22-23)
instead of only `@IsNotEmpty`(), so invalid strings are rejected before calling
getAgentRuntimeProvider.
---
Nitpick comments:
In `@apps/dashboard/src/components/agents/create-agent-dialog.tsx`:
- Around line 254-263: The dialog currently always sets
managedRuntime.integrationId to an empty string in both submission branches (see
the body object built when isExistingMode and the other branch around lines
273-287), which forces callers to overwrite it; instead remove integrationId
from the dialog payload when it's empty and only include
managedRuntime.integrationId when a non-empty value is provided (e.g., check
externalIntegrationId or a local integrationId field and conditionally add that
property), so callers must explicitly supply an integrationId and the type
system will catch missing values.
In
`@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts`:
- Around line 224-230: The parseRetryAfter function currently treats the header
only as delta-seconds; update parseRetryAfter(header: string | undefined) to
also handle the HTTP-date form: if parseFloat(header) yields NaN, attempt a date
parse (const ms = Date.parse(header) - Date.now()), ensure the result is a
non-negative integer (Math.max(0, Math.round(ms))), and return that value in
milliseconds; if the date parse is invalid fall back to the existing 60_000 ms
default. Ensure you reference parseRetryAfter in
anthropic-agent-runtime.provider.ts and preserve the original return type and
fallback semantics.
- Around line 306-327: buildToolsPayload currently always pushes the
agent_toolset_20260401 entry so callers' guards (toolsPayload.length > 0) in
createAgent/updateConfig are dead; fix by short‑circuiting buildToolsPayload to
return [] when both toolTypes is null/empty and mcpServers is null/empty (i.e.
check if (!toolTypes || toolTypes.length===0) && (!mcpServers ||
mcpServers.length===0) then return []), otherwise build the payload as before
(refer to buildToolsPayload, CLAUDE_BUILTIN_TOOLS, and the callers
createAgent/updateConfig).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5f639558-7ec5-4548-86da-4f0d2c305cfd
📒 Files selected for processing (12)
apps/api/src/app/agents/dtos/agent-runtime-config.dto.tsapps/api/src/app/agents/dtos/create-agent-request.dto.tsapps/api/src/app/agents/e2e/managed-agent.e2e.tsapps/api/src/app/agents/usecases/create-agent/create-agent.command.tsapps/api/src/app/agents/usecases/create-agent/create-agent.usecase.tsapps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.tsapps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.tsapps/dashboard/src/api/agents.tsapps/dashboard/src/components/agents/agents-list.tsxapps/dashboard/src/components/agents/create-agent-dialog.tsxlibs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.tslibs/application-generic/src/agent-runtimes/i-agent-runtime-provider.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/dashboard/src/api/agents.ts
- apps/dashboard/src/components/agents/agents-list.tsx
- libs/application-generic/src/agent-runtimes/i-agent-runtime-provider.ts
- apps/api/src/app/agents/dtos/agent-runtime-config.dto.ts
| const handleTemplateRotate = () => { | ||
| setTemplateOffset((prev) => (prev + 4) % AGENT_TEMPLATES.length); | ||
| }; |
There was a problem hiding this comment.
Template rotation is a no-op when there are exactly 4 templates.
AGENT_TEMPLATES currently has 4 entries and visibleTemplates already takes slice(templateOffset, templateOffset + 4). (prev + 4) % AGENT_TEMPLATES.length evaluates to (0 + 4) % 4 === 0, so clicking the refresh button never changes the visible set. Either reduce the step (e.g. rotate by 1) or guard the button when the list fits in one page.
♻️ Suggested fix
- const handleTemplateRotate = () => {
- setTemplateOffset((prev) => (prev + 4) % AGENT_TEMPLATES.length);
- };
+ const handleTemplateRotate = () => {
+ if (AGENT_TEMPLATES.length <= 4) return;
+ setTemplateOffset((prev) => (prev + 1) % AGENT_TEMPLATES.length);
+ };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleTemplateRotate = () => { | |
| setTemplateOffset((prev) => (prev + 4) % AGENT_TEMPLATES.length); | |
| }; | |
| const handleTemplateRotate = () => { | |
| if (AGENT_TEMPLATES.length <= 4) return; | |
| setTemplateOffset((prev) => (prev + 1) % AGENT_TEMPLATES.length); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/dashboard/src/components/agents/create-agent-dialog.tsx` around lines
211 - 213, handleTemplateRotate is a no-op when AGENT_TEMPLATES.length === 4
because it advances offset by 4; change the rotation to advance by 1 (i.e.,
setTemplateOffset(prev => (prev + 1) % AGENT_TEMPLATES.length)) or alternatively
disable/hide the refresh control when AGENT_TEMPLATES.length <= visibleCount;
update the logic around handleTemplateRotate, setTemplateOffset, and the
visibleTemplates slice to use the chosen approach so clicking the refresh
actually rotates the shown templates.
There was a problem hiding this comment.
💬 Out of scope for this round. The current dialog has exactly 4 templates so the rotate button is functionally a no-op until we add more. Will either disable the button conditionally or change the step to +1 once we expand the catalog — tracking separately to keep this PR focused on the managed-runtime changes.
There was a problem hiding this comment.
@djabarovgeorge, understood — makes sense to keep the scope tight on the managed-runtime work. Would you like me to open a follow-up GitHub issue to track disabling/hiding the rotate button (or changing the step to +1) when the template catalog is expanded?
🧠 Learnings used
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10922
File: apps/dashboard/src/components/welcome/inbox-connected-guide.tsx:131-135
Timestamp: 2026-04-29T14:47:11.569Z
Learning: In the apps/dashboard codebase, `useTelemetry` is intentionally implemented as a React Query `useMutation` wrapper whose callback uses fire-and-forget semantics (it returns the `mutate` function, effectively `void`). Therefore, `await telemetry(...)` calls are intentional and should NOT be flagged as incorrect/buggy or as needing `mutateAsync`. Do not suggest changing `useTelemetry` to use `mutateAsync`; treat this behavior as an established codebase-wide pattern.
Learnt from: ChmaraX
Repo: novuhq/novu PR: 10930
File: apps/dashboard/src/components/agents/agents-list.tsx:259-290
Timestamp: 2026-04-30T16:37:12.078Z
Learning: In apps/dashboard/src, follow the established tooltip pattern for disabled buttons: render the disabled <Button ... disabled> inside <TooltipTrigger className="cursor-not-allowed">...</TooltipTrigger> and do not require or flag the absence of the `asChild` prop on `TooltipTrigger` when it wraps a disabled `Button`. This convention should be treated as acceptable and not reported as an issue during code review.
Learnt from: scopsy
Repo: novuhq/novu PR: 11060
File: packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.tsx:55-56
Timestamp: 2026-05-11T05:04:35.896Z
Learning: In PR reviews for TypeScript/TSX code, do not suggest adding blank lines before `return` statements. Follow the repository’s existing formatting conventions and avoid raising review comments about this specific whitespace change (blank line insertion before `return`).
- Updated the agent provisioning process to support both `integrationId` and `apiKey`, allowing for auto-creation of integrations and environments when an API key is provided. - Modified `CreateAgentRequestDto`, `ProvisionManagedAgentCommand`, and related use cases to handle the new `apiKey` field, ensuring mutual exclusivity with `integrationId`. - Enhanced error handling to enforce the requirement that exactly one of `integrationId` or `apiKey` must be supplied. - Updated dashboard components to reflect changes in the agent creation flow, simplifying the integration process for users. These improvements streamline the agent management experience, enabling more flexible integration options with the Claude Platform.
- Updated the delete agent API to include an optional `deleteFromProvider` parameter, allowing users to archive agents on the provider side (e.g., Anthropic) when deleting managed-runtime agents. - Modified the `DeleteAgentCommand` to handle the new parameter and updated the corresponding use case to implement the provider deletion logic. - Enhanced the dashboard components to support the new deletion option, including a confirmation dialog that allows users to choose between deleting the agent from Novu only or from both Novu and the provider. These changes improve the flexibility of agent management, enabling users to manage their agents more effectively across platforms.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (8)
packages/shared/src/dto/agent/managed-runtime.dto.ts (1)
57-64:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftDon't expose raw MCP auth tokens in a shared read DTO.
AgentRuntimeConfigDtois a cross-layer response shape, so carryingauthTokenhere makes secret leakage into UI state and logging paths far too easy. Split this into a safe read DTO (hasAuthTokenor masked metadata) and a separate write DTO for updates.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/dto/agent/managed-runtime.dto.ts` around lines 57 - 64, AgentMcpServerDto currently includes a raw authToken which can leak secrets via cross-layer responses; change the public/shared response shape by removing authToken from AgentMcpServerDto and replace it with a safe indicator (e.g., hasAuthToken: boolean or masked metadata) and create a separate write/update DTO (e.g., AgentMcpServerUpdateDto or AgentMcpServerWriteDto) that contains the authToken for create/update flows; update all references/uses of AgentMcpServerDto and any consumers (including where AgentRuntimeConfigDto embeds MCP server info) to use the new safe read DTO for responses and the write DTO for input paths so the secret is never emitted in read payloads or logs.apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts (2)
185-202:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCheck the agent update result before treating provisioning as persisted.
agentRepository.update()can return{ matched: 0 }without throwing. In that case the upstream agent is already created/adopted, but no local document storesexternalAgentId, so the provider resource is orphaned.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts` around lines 185 - 202, The agent update call in provision-managed-agent.usecase.ts uses this.agentRepository.update(...) but doesn't inspect the returned result; because update() can return { matched: 0 } without throwing, ensure you capture the update result, check matched/modified (e.g., const result = await this.agentRepository.update(...)), and if matched === 0 (or modified === 0) treat provisioning as not persisted: either throw an error to abort the flow or perform cleanup by deleting the created provider resource using externalAgentId via the provider client (and/or rollback any created integration), ensuring you pass the same session/options when performing cleanup or throwing so you don't leave an orphaned externalAgentId.
155-159:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject unknown MCP server IDs instead of sending empty URLs upstream.
Falling back to
{ name: serverId, url: '' }turns invalid input into a provider-side failure after provisioning has already started. Validate againstCLAUDE_MCP_SERVERSand fail fast with a clear 400.As per coding guidelines:
apps/api/**: Check for proper error handling and input validation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts` around lines 155 - 159, The current mapping of command.mcpServers creates entries with empty URLs for unknown IDs; instead validate against CLAUDE_MCP_SERVERS and fail fast: in the provision flow where resolvedMcpServers is built (the mapping over command.mcpServers in provision-managed-agent.usecase.ts), for each serverId look up CLAUDE_MCP_SERVERS.find(s => s.id === serverId) and if not found throw a 400 Bad Request (e.g., throw new BadRequestException or HttpException) with a clear message listing the invalid serverId(s); only return the mapped array when all IDs are valid and handle the case where command.mcpServers is undefined/null by leaving resolvedMcpServers undefined or empty as intended.libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts (3)
59-74:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse
Headers.get()when normalizing Anthropic errors.
APIError.headersis aHeadersobject, so bracket access here leavesrequestIdandretry-afterundefined. That breaks request correlation and drops the 429 backoff hint.In `@anthropic-ai/sdk` v0.95.1, is APIError.headers a Headers object, and should request-id / retry-after be read with headers.get(...) instead of bracket notation?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts` around lines 59 - 74, The code assumes APIError.headers is a plain object and uses bracket access for request-id and retry-after, but APIError.headers is a Headers instance; replace bracket access with Headers.get(...) to read values (e.g., use err.headers?.get('request-id') for requestId and err.headers?.get('retry-after') for parseRetryAfter), then pass those retrieved strings into AgentRuntimeUnauthorizedError / AgentRuntimeForbiddenError / AgentRuntimeNotFoundError / AgentRuntimeRateLimitedError (and into parseRetryAfter) so request correlation and 429 backoff hints work correctly in anthropic-agent-runtime.provider.ts where APIError, parseRetryAfter, and the AgentRuntime* error constructors are used.
114-135:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't retry provider-side create calls without idempotency.
withRetrywraps bothbeta.agents.createandbeta.environments.create. If the first write succeeds but the response is lost, the retry can provision a second upstream resource and Novu will persist only the retry result.In `@anthropic-ai/sdk` v0.95.1, do beta.agents.create and beta.environments.create support request-level idempotencyKey, and are retries without it safe for resource creation?Also applies to: 216-233
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts` around lines 114 - 135, The createAgent method is wrapping the non-idempotent upstream call (client.beta.agents.create) inside withRetry which can duplicate resources if the first request succeeded but the response was lost; update createAgent (and the similar beta.environments.create call) to either stop using withRetry for the provider-side create path or, if the Anthropic SDK supports a request-level idempotency key for beta.agents.create / beta.environments.create, pass a stable idempotencyKey (derived from input or a persisted mapping) to the SDK call so retries are safe—locate the calls in createAgent and the environment creation block, remove the retry wrapper or add idempotencyKey handling and ensure normaliseError still runs on unretriable failures.
193-202:⚠️ Potential issue | 🟠 Major | ⚡ Quick winA one-sided PATCH currently clears the untouched tool configuration.
When
patch.mcpServersis set withoutpatch.tools,buildToolsPayload(undefined, ...)disables every builtin tool. The inverse case rebuildsupdatePayload.toolswithout existing MCP toolsets, so they get removed even though the caller didn't patch them.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts` around lines 193 - 202, The PATCH logic currently calls buildToolsPayload with undefined when only one side (patch.tools or patch.mcpServers) is provided, which removes untouched tool configuration; to fix, when computing toolTypes and mcpServers for buildToolsPayload in anthropic-agent-runtime.provider.ts, merge the incoming patch values with the existing runtime's current values instead of passing undefined: derive currentToolTypes = patch.tools ? patch.tools.map(t=>t.name) : existingRuntime.tools?.map(t=>t.name) and currentMcpServers = patch.mcpServers ? patch.mcpServers.map(s=>({name:s.name,url:s.url})) : existingRuntime.mcp_servers?.map(s=>({name:s.name,url:s.url})), then call buildToolsPayload(currentToolTypes, currentMcpServers) and only assign updatePayload.tools if (patch.tools !== undefined || patch.mcpServers !== undefined) && toolsPayload.length > 0 so untouched toolsets are preserved when the caller did not patch them.apps/api/src/app/agents/agents.controller.ts (1)
123-134:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGate
/agents/runtime-providersbehind the managed-runtime flag.This route still returns the provider catalog for any authenticated tenant with
AGENT_READ, so disabled tenants can discover managed-runtime metadata directly via the API.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/agents.controller.ts` around lines 123 - 134, The listAgentRuntimeProviders route currently returns AGENT_RUNTIME_PROVIDERS for any user with AGENT_READ; update listAgentRuntimeProviders to first check the tenant's managed-runtime feature flag (via the request/tenant context or the FeatureFlagService used elsewhere) and if the managed-runtime flag is disabled for that tenant return a ForbiddenException or NotFoundException (matching existing pattern for hiding disabled features) instead of the provider list; keep the RequirePermissions(PermissionsEnum.AGENT_READ) decorator but gate the actual response using the managed-runtime check before returning AGENT_RUNTIME_PROVIDERS.apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts (1)
22-23:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate
providerIdas an enum, not just a non-empty value.
@IsNotEmpty()still accepts arbitrary strings here, so unsupported provider IDs can get through command validation and fail later in the provisioning flow. Add@IsEnum(AgentRuntimeProviderIdEnum).In class-validator 0.14.1, does `@IsNotEmpty`() validate enum membership, or is `@IsEnum`(...) required to reject invalid enum values?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts` around lines 22 - 23, The providerId property uses only `@IsNotEmpty`() so invalid strings can pass validation; update the ProvisionManagedAgentCommand (property providerId) to also use `@IsEnum`(AgentRuntimeProviderIdEnum) so only valid enum members are accepted, and add the corresponding import for IsEnum from class-validator; keep `@IsNotEmpty`() if you want both non-empty and enum checks but ensure IsEnum(AgentRuntimeProviderIdEnum) is present to enforce membership.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/app/agents/e2e/managed-agent.e2e.ts`:
- Around line 32-37: The fixture creates an Anthropic integration with the wrong
channel — change the channel passed to integrationRepository.create from
ChannelTypeEnum.IN_APP to ChannelTypeEnum.AGENT_RUNTIME so the test seeds a
runtime integration that matches managed runtimes; update the call that
constructs the integration (the object passed into integrationRepository.create)
to use ChannelTypeEnum.AGENT_RUNTIME where ChannelTypeEnum.IN_APP is currently
used.
- Around line 86-88: The test suite currently sets
process.env.IS_CONVERSATIONAL_AGENTS_ENABLED but also needs to explicitly enable
the managed runtime flag for the endpoints it exercises; in the before() hook
(the same setup block that sets IS_CONVERSATIONAL_AGENTS_ENABLED) add an
assignment to enable the managed-runtime feature (e.g. set
process.env.IS_MANAGED_RUNTIME_ENABLED = 'true' or whatever the repo's managed
runtime flag is named) so the suite no longer depends on ambient process state
when calling runtime endpoints like /v1/agents/runtime-providers.
In
`@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts`:
- Around line 82-107: The catch block after
runtimeProviderForEnv.createEnvironment() currently just logs and rethrows,
leaving the auto-created environment/integration orphaned; update that catch to
perform rollback: if createdExternalEnvironmentId is set call
runtimeProviderForEnv.deleteEnvironment({ externalEnvironmentId:
createdExternalEnvironmentId }) (using the same runtimeProviderForEnv instance),
and if resolvedIntegrationId is set remove the created integration via
this.integrationRepository.delete (or deleteOne) with the same filter used for
update ({ _id: resolvedIntegrationId, _environmentId: command.environmentId,
_organizationId: command.organizationId }) and pass session if present; wrap
those cleanup calls in their own try/catch to log cleanup errors (use
this.logger.error) but always rethrow the original envError after attempting
rollback.
- Around line 41-43: The current guard in provision-managed-agent.usecase.ts
only rejects when neither command.integrationId nor command.apiKey is provided;
update the validation in the ProvisionManagedAgent use case to enforce XOR by
also rejecting the case when both are present: check if command.integrationId &&
command.apiKey and throw a BadRequestException (e.g., "Provide either
integrationId or apiKey, not both") so the code does not proceed to the apiKey
branch when an integrationId was also supplied.
In `@apps/dashboard/src/components/agents/delete-agent-dialog.tsx`:
- Around line 39-45: The delete scope state (scope) can linger when the parent
closes the dialog by toggling the open prop; add a useEffect that watches the
dialog's open prop and calls setScope('novu-only') whenever open becomes false
so the scope is reset on prop-driven closes (in addition to existing resets in
handleOpenChange), ensuring handleConfirm() won't send stale
deleteFromProvider:true for non-managed agents; reference the scope state,
setScope, handleConfirm, and handleOpenChange in your change.
---
Duplicate comments:
In `@apps/api/src/app/agents/agents.controller.ts`:
- Around line 123-134: The listAgentRuntimeProviders route currently returns
AGENT_RUNTIME_PROVIDERS for any user with AGENT_READ; update
listAgentRuntimeProviders to first check the tenant's managed-runtime feature
flag (via the request/tenant context or the FeatureFlagService used elsewhere)
and if the managed-runtime flag is disabled for that tenant return a
ForbiddenException or NotFoundException (matching existing pattern for hiding
disabled features) instead of the provider list; keep the
RequirePermissions(PermissionsEnum.AGENT_READ) decorator but gate the actual
response using the managed-runtime check before returning
AGENT_RUNTIME_PROVIDERS.
In
`@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.ts`:
- Around line 22-23: The providerId property uses only `@IsNotEmpty`() so invalid
strings can pass validation; update the ProvisionManagedAgentCommand (property
providerId) to also use `@IsEnum`(AgentRuntimeProviderIdEnum) so only valid enum
members are accepted, and add the corresponding import for IsEnum from
class-validator; keep `@IsNotEmpty`() if you want both non-empty and enum checks
but ensure IsEnum(AgentRuntimeProviderIdEnum) is present to enforce membership.
In
`@apps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.ts`:
- Around line 185-202: The agent update call in
provision-managed-agent.usecase.ts uses this.agentRepository.update(...) but
doesn't inspect the returned result; because update() can return { matched: 0 }
without throwing, ensure you capture the update result, check matched/modified
(e.g., const result = await this.agentRepository.update(...)), and if matched
=== 0 (or modified === 0) treat provisioning as not persisted: either throw an
error to abort the flow or perform cleanup by deleting the created provider
resource using externalAgentId via the provider client (and/or rollback any
created integration), ensuring you pass the same session/options when performing
cleanup or throwing so you don't leave an orphaned externalAgentId.
- Around line 155-159: The current mapping of command.mcpServers creates entries
with empty URLs for unknown IDs; instead validate against CLAUDE_MCP_SERVERS and
fail fast: in the provision flow where resolvedMcpServers is built (the mapping
over command.mcpServers in provision-managed-agent.usecase.ts), for each
serverId look up CLAUDE_MCP_SERVERS.find(s => s.id === serverId) and if not
found throw a 400 Bad Request (e.g., throw new BadRequestException or
HttpException) with a clear message listing the invalid serverId(s); only return
the mapped array when all IDs are valid and handle the case where
command.mcpServers is undefined/null by leaving resolvedMcpServers undefined or
empty as intended.
In
`@libs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.ts`:
- Around line 59-74: The code assumes APIError.headers is a plain object and
uses bracket access for request-id and retry-after, but APIError.headers is a
Headers instance; replace bracket access with Headers.get(...) to read values
(e.g., use err.headers?.get('request-id') for requestId and
err.headers?.get('retry-after') for parseRetryAfter), then pass those retrieved
strings into AgentRuntimeUnauthorizedError / AgentRuntimeForbiddenError /
AgentRuntimeNotFoundError / AgentRuntimeRateLimitedError (and into
parseRetryAfter) so request correlation and 429 backoff hints work correctly in
anthropic-agent-runtime.provider.ts where APIError, parseRetryAfter, and the
AgentRuntime* error constructors are used.
- Around line 114-135: The createAgent method is wrapping the non-idempotent
upstream call (client.beta.agents.create) inside withRetry which can duplicate
resources if the first request succeeded but the response was lost; update
createAgent (and the similar beta.environments.create call) to either stop using
withRetry for the provider-side create path or, if the Anthropic SDK supports a
request-level idempotency key for beta.agents.create / beta.environments.create,
pass a stable idempotencyKey (derived from input or a persisted mapping) to the
SDK call so retries are safe—locate the calls in createAgent and the environment
creation block, remove the retry wrapper or add idempotencyKey handling and
ensure normaliseError still runs on unretriable failures.
- Around line 193-202: The PATCH logic currently calls buildToolsPayload with
undefined when only one side (patch.tools or patch.mcpServers) is provided,
which removes untouched tool configuration; to fix, when computing toolTypes and
mcpServers for buildToolsPayload in anthropic-agent-runtime.provider.ts, merge
the incoming patch values with the existing runtime's current values instead of
passing undefined: derive currentToolTypes = patch.tools ?
patch.tools.map(t=>t.name) : existingRuntime.tools?.map(t=>t.name) and
currentMcpServers = patch.mcpServers ?
patch.mcpServers.map(s=>({name:s.name,url:s.url})) :
existingRuntime.mcp_servers?.map(s=>({name:s.name,url:s.url})), then call
buildToolsPayload(currentToolTypes, currentMcpServers) and only assign
updatePayload.tools if (patch.tools !== undefined || patch.mcpServers !==
undefined) && toolsPayload.length > 0 so untouched toolsets are preserved when
the caller did not patch them.
In `@packages/shared/src/dto/agent/managed-runtime.dto.ts`:
- Around line 57-64: AgentMcpServerDto currently includes a raw authToken which
can leak secrets via cross-layer responses; change the public/shared response
shape by removing authToken from AgentMcpServerDto and replace it with a safe
indicator (e.g., hasAuthToken: boolean or masked metadata) and create a separate
write/update DTO (e.g., AgentMcpServerUpdateDto or AgentMcpServerWriteDto) that
contains the authToken for create/update flows; update all references/uses of
AgentMcpServerDto and any consumers (including where AgentRuntimeConfigDto
embeds MCP server info) to use the new safe read DTO for responses and the write
DTO for input paths so the secret is never emitted in read payloads or logs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: eac1a4ce-0e3a-464f-a80e-0c5d52543027
📒 Files selected for processing (17)
apps/api/src/app/agents/agents.controller.tsapps/api/src/app/agents/dtos/agent-runtime-config.dto.tsapps/api/src/app/agents/e2e/managed-agent.e2e.tsapps/api/src/app/agents/usecases/create-agent/create-agent.usecase.tsapps/api/src/app/agents/usecases/delete-agent/delete-agent.command.tsapps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.tsapps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.command.tsapps/api/src/app/agents/usecases/provision-managed-agent/provision-managed-agent.usecase.tsapps/dashboard/src/api/agents.tsapps/dashboard/src/components/agents/agents-list.tsxapps/dashboard/src/components/agents/create-agent-dialog.tsxapps/dashboard/src/components/agents/delete-agent-dialog.tsxapps/dashboard/src/pages/agent-details.tsxlibs/application-generic/src/agent-runtimes/anthropic/anthropic-agent-runtime.provider.tslibs/application-generic/src/agent-runtimes/i-agent-runtime-provider.tspackages/shared/src/dto/agent/managed-runtime.dto.tspackages/shared/src/entities/integration/credential.interface.ts
✅ Files skipped from review due to trivial changes (1)
- packages/shared/src/entities/integration/credential.interface.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/api/src/app/agents/dtos/agent-runtime-config.dto.ts
- apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts
- apps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.ts
- apps/dashboard/src/components/agents/create-agent-dialog.tsx
- Enhanced the agent runtime configuration by adding a `skills` field to the relevant DTOs and commands, allowing for better integration of skills during agent updates and creation. - Updated the `UpdateAgentRuntimeConfigCommand` and `PatchAgentRuntimeConfigRequestDto` to include the new `skills` property, ensuring it is validated and processed correctly. - Modified the `AgentsController` to handle the `skills` input in the agent creation process, improving the overall functionality and flexibility of agent management. These changes facilitate a more comprehensive agent configuration, enabling users to specify skills alongside other runtime parameters.
- Introduced a new `creationSource` field across various agent-related DTOs and commands to track the origin of agent creation within the Novu Dashboard. - Updated `CreateAgentRequestDto`, `AgentResponseDto`, and `CreateAgentCommand` to include the `creationSource` property, ensuring it is optional and validated. - Enhanced the `AgentsController` and related mappers to handle the new field, improving the overall agent management experience. These changes provide better insights into how agents are created, facilitating improved tracking and management.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
libs/dal/src/repositories/agent/agent.schema.ts (2)
31-34: ⚡ Quick winConsider adding a default value for
runtime.The
runtimefield has no default, which means existing agents will haveundefinedruntime. Per the entity documentation, absence is treated as 'self-hosted' for backward compatibility, but this implicit behavior creates ambiguity in queries and application logic.Adding
default: 'self-hosted'would make the data model explicit and simplify query predicates (e.g., finding all self-hosted agents wouldn't need{ $or: [{ runtime: 'self-hosted' }, { runtime: { $exists: false } }] }).📝 Proposed fix to add explicit default
runtime: { type: Schema.Types.String, enum: ['self-hosted', 'managed'], + default: 'self-hosted', },Based on past review comments: scopsy previously asked "Wonder if we should default here?" on this field.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/dal/src/repositories/agent/agent.schema.ts` around lines 31 - 34, The runtime field in the Agent schema currently has type: Schema.Types.String and enum ['self-hosted','managed'] but no default, leaving old documents undefined; update the runtime schema definition (the runtime property in agent.schema.ts) to include default: 'self-hosted' so new records are explicit and queries no longer need to special-case missing runtime values.
60-61: ⚡ Quick winAdd indexes for runtime-based queries.
The schema currently indexes
_environmentIdandidentifier+_environmentId, but there are no indexes on the newruntimeormanagedRuntime.providerIdfields. If the application queries agents by runtime type (e.g., "find all managed agents") or by provider (e.g., "find all Anthropic agents"), these queries will require collection scans.🔍 Proposed indexes for common runtime queries
agentSchema.index({ _environmentId: 1 }); agentSchema.index({ identifier: 1, _environmentId: 1 }, { unique: true }); +agentSchema.index({ runtime: 1, _environmentId: 1 }); +agentSchema.index({ 'managedRuntime.providerId': 1, _environmentId: 1 }, { sparse: true });Note: The
managedRuntime.providerIdindex should be sparse since only managed-runtime agents will have this field.As per coding guidelines: "Review with focus on data integrity and query performance. Check for proper MongoDB indexes on new queries."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@libs/dal/src/repositories/agent/agent.schema.ts` around lines 60 - 61, The schema is missing indexes for runtime-based queries; add indexes via agentSchema.index to cover queries on the runtime field and on managedRuntime.providerId (use a sparse index for managedRuntime.providerId since only managed agents have it) so lookups like "find by runtime" and "find by managedRuntime.providerId" avoid collection scans; update the file to add agentSchema.index({ runtime: 1 }) and agentSchema.index({ "managedRuntime.providerId": 1 }, { sparse: true }) alongside the existing indexes.apps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.ts (1)
70-96: ⚖️ Poor tradeoffTransaction ordering exposes a small inconsistency window.
The provider delete (line 95) executes before the local transaction begins (line 39). If the provider delete succeeds but the local transaction fails (e.g., database unavailable), the external agent is deleted but the local record remains.
While Mongoose transactions can be retried and will eventually succeed, this creates a brief inconsistency where the provider agent is gone but Novu still shows it.
Consider moving the provider delete inside the transaction scope if the runtime provider SDK supports idempotent deletes. This ensures atomicity: both succeed or both fail together. However, this increases transaction duration and may not be worth the complexity if provider SDKs don't handle idempotency well.
await this.agentRepository.withTransaction(async (session) => { // Delete from provider first (may throw and abort transaction) if (agent.runtime === 'managed' && agent.managedRuntime && shouldDeleteFromProvider) { await this.deleteFromProvider(agent.managedRuntime, command); } // Then clean up locally await this.cleanupNovuEmail.cleanupForAgent(agent._id, command.environmentId, command.organizationId, session); // ... rest of transaction });As per coding guidelines: "Check for proper error handling" and "Review with focus on data integrity" (libs/dal patterns).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.ts` around lines 70 - 96, The provider delete currently runs outside the DB transaction and can leave the external agent removed while the local delete fails; move the call to deleteFromProvider(...) into the agentRepository.withTransaction(...) callback before the local cleanup so both operations occur inside the same transaction scope (use the same session passed to cleanupNovuEmail.cleanupForAgent and other repository calls); ensure runtime provider deletes are idempotent or handle delete errors to abort the transaction (references: deleteFromProvider, agentRepository.withTransaction, cleanupNovuEmail.cleanupForAgent, DeleteAgentCommand, managedRuntime).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.ts`:
- Around line 83-91: The current early returns after checking integration and
decryptedCredentials.apiKey in delete-agent.usecase.ts silently skip provider
cleanup and can orphan external agents; update the logic around
decryptCredentials(...) and the call to deleteFromProvider(...) so that either
(Option 1) you throw/return a propagated error to abort the local deletion when
integration or apiKey is missing (fail the whole delete flow), or (Option 2) you
convert the early returns into robust logged warnings that include the agent id
and externalAgentId and ensure deleteFromProvider is attempted only when valid
credentials exist (fail-open); pick one strategy, implement it in the
deleteFromProvider call path and surrounding checks (refer to
decryptCredentials, integration, and deleteFromProvider symbols) and ensure the
function returns/throws or logs appropriately so external cleanup is not
silently skipped.
---
Nitpick comments:
In `@apps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.ts`:
- Around line 70-96: The provider delete currently runs outside the DB
transaction and can leave the external agent removed while the local delete
fails; move the call to deleteFromProvider(...) into the
agentRepository.withTransaction(...) callback before the local cleanup so both
operations occur inside the same transaction scope (use the same session passed
to cleanupNovuEmail.cleanupForAgent and other repository calls); ensure runtime
provider deletes are idempotent or handle delete errors to abort the transaction
(references: deleteFromProvider, agentRepository.withTransaction,
cleanupNovuEmail.cleanupForAgent, DeleteAgentCommand, managedRuntime).
In `@libs/dal/src/repositories/agent/agent.schema.ts`:
- Around line 31-34: The runtime field in the Agent schema currently has type:
Schema.Types.String and enum ['self-hosted','managed'] but no default, leaving
old documents undefined; update the runtime schema definition (the runtime
property in agent.schema.ts) to include default: 'self-hosted' so new records
are explicit and queries no longer need to special-case missing runtime values.
- Around line 60-61: The schema is missing indexes for runtime-based queries;
add indexes via agentSchema.index to cover queries on the runtime field and on
managedRuntime.providerId (use a sparse index for managedRuntime.providerId
since only managed agents have it) so lookups like "find by runtime" and "find
by managedRuntime.providerId" avoid collection scans; update the file to add
agentSchema.index({ runtime: 1 }) and agentSchema.index({
"managedRuntime.providerId": 1 }, { sparse: true }) alongside the existing
indexes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7c43953b-ab53-4ab7-b8a4-9e536472a52f
📒 Files selected for processing (13)
apps/api/src/app/agents/agents.controller.tsapps/api/src/app/agents/dtos/agent-response.dto.tsapps/api/src/app/agents/dtos/agent-runtime-config.dto.tsapps/api/src/app/agents/dtos/create-agent-request.dto.tsapps/api/src/app/agents/mappers/agent-response.mapper.tsapps/api/src/app/agents/usecases/create-agent/create-agent.command.tsapps/api/src/app/agents/usecases/create-agent/create-agent.usecase.tsapps/api/src/app/agents/usecases/delete-agent/delete-agent.usecase.tsapps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.command.tsapps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.usecase.tslibs/dal/src/repositories/agent/agent.entity.tslibs/dal/src/repositories/agent/agent.schema.tspackages/shared/src/dto/agent/managed-runtime.dto.ts
✅ Files skipped from review due to trivial changes (1)
- apps/api/src/app/agents/mappers/agent-response.mapper.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- apps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.command.ts
- apps/api/src/app/agents/dtos/agent-response.dto.ts
- packages/shared/src/dto/agent/managed-runtime.dto.ts
- apps/api/src/app/agents/usecases/update-agent-runtime-config/update-agent-runtime-config.usecase.ts
- apps/api/src/app/agents/dtos/create-agent-request.dto.ts
- apps/api/src/app/agents/dtos/agent-runtime-config.dto.ts
- apps/api/src/app/agents/usecases/create-agent/create-agent.usecase.ts
- apps/api/src/app/agents/usecases/create-agent/create-agent.command.ts
- apps/api/src/app/agents/agents.controller.ts
- Deleted the `GET /v1/agents/runtime-providers` endpoint from the `AgentsController`, streamlining the API by removing unused functionality. - Removed `AgentRuntimeProviderResponseDto` and associated capabilities DTOs from the codebase, as they are no longer needed. - Introduced `AgentSkillInputDto` in the `agent-runtime-config.dto.ts` file to enhance agent skill management, ensuring better integration for future features. These changes simplify the agent management API and prepare the codebase for upcoming enhancements related to agent skills.
…ation - Updated E2E tests to manage environment variables for conversational agents and managed runtime flags, ensuring proper setup and teardown. - Refactored `ProvisionManagedAgentCommand` to enforce the `providerId` as an enum, improving validation. - Enhanced `ProvisionManagedAgent` use case to handle agent provisioning more robustly, including error handling for non-existent agents. - Modified API types for patching agent configurations to allow optional `externalId` fields for MCP servers and tools. - Improved error handling in the `AnthropicAgentRuntimeProvider` for better clarity and response management. These changes improve the reliability and clarity of managed agent operations, enhancing the overall user experience.
- Enhanced `CreateAgentRequestDto` to require `name` and `identifier` fields, improving clarity in agent creation requirements. - Updated validation logic in `CreateAgent` use case to throw `BadRequestException` for missing `name` and `identifier` in non-adopt mode. - Modified API documentation to reflect changes in required fields for agent creation. These updates ensure better validation and error handling during agent provisioning, enhancing the overall user experience.
…ypeEnum in workflow preferences and related use cases - Updated WorkflowPreferencesDto, PreviewUsecase, and various command files to use ChannelTypeEnum instead of NotificationChannelTypeEnum for channel preferences. - Adjusted related types and interfaces to ensure consistency across the application. - These changes enhance type safety and align the codebase with the updated channel type definitions.
…actoryModule directly - Replaced the import of AgentRuntimeFactoryModule from the package barrel with a direct import from the source module to enable proper stubbing in tests. - This change addresses issues with TypeScript's `__exportStar` helper, ensuring that stubbing works as intended across related use cases. These updates enhance the reliability of E2E tests for managed agents by improving the stubbing mechanism.
- Modified the expected URL for the Slack MCP server in the managed agent E2E tests to reflect the correct endpoint. - This change ensures that the tests accurately validate the agent creation process with the updated MCP server configuration.
…NV-7618)
PATCH /agents/:id/runtime/config previously forwarded caller-supplied
{name, url} MCP entries straight to the provider, while provisioning
already restricted IDs to the trusted CLAUDE_MCP_SERVERS catalog. A
caller with AGENT_WRITE could swap in attacker-controlled MCP endpoints
on a managed agent, enabling tool-chain hijack via provider-side calls.
Resolve every incoming MCP entry against the catalog on the update path,
discarding the client-supplied url and using the catalog value. Extract
the policy into a shared utils helper used by both provisioning and
update paths so the rule lives in one place.
Co-authored-by: Cursor <[email protected]>


What changed? Why was the change needed?
What changed
This PR adds managed agent runtimes so Novu agents can run on Anthropic's Claude Platform instead of a self‑hosted bridge. Novu can store Anthropic API keys as Integrations, provision or adopt Claude agents upstream, and proxy live runtime configuration (model, system prompt, MCP servers, tools, skills) via a new IAgentRuntimeProvider abstraction. The change enables provider-backed provisioning/adoption, provider-aware config updates, soft-delete with async upstream cleanup, and stable error mapping for runtime errors (including rate-limit Retry-After).
Affected areas
Key technical decisions
Testing
Adds extensive e2e tests covering provisioning, adoption, apiKey auto-provisioning and rollback, error mappings (drift/unauthorized/bad request/rate limit), and config PATCH behavior, plus a unit test ensuring provider capability parity.
Introduces managed agents — users can now point a Novu agent at Claude Platform instead of self-hosting a bridge server. Novu stores the Anthropic API key as an Integration, provisions the Claude agent upstream, and proxies all live configuration (model, system prompt, MCP servers, tools) through a new
IAgentRuntimeProviderabstraction.Linear: https://linear.app/novu/issue/NV-7618
packages/shared
AGENT_RUNTIME_PROVIDERSstatic catalog withAgentRuntimeCapabilitiesper provider (drives capability-gated UI panels)AgentRuntimeProviderIdEnum(anthropic) +ChannelTypeEnum.AGENT_RUNTIMENotificationChannelTypeEnum(Exclude<ChannelTypeEnum, AGENT_RUNTIME>) — keeps channel-preference types correctIS_MANAGED_AGENT_RUNTIME_ENABLEDfeature flagAgentRuntime,ManagedRuntimeConfigDto,AgentRuntimeConfigDtoDTOslibs/dal
AgentEntityextended withruntime,managedRuntime { providerId, _integrationId, externalAgentId }, and soft-delete fields (deletedAt,pendingExternalDelete)libs/application-generic
IAgentRuntimeProviderinterfaceAnthropicAgentRuntimeProviderusing@anthropic-ai/sdkwith error normalisation and single-retry for transient failuresgetAgentRuntimeProviderfactoryAgentRuntime*Errortaxonomy (Unauthorized / Forbidden / NotFound / RateLimited / Overloaded / ServiceUnavailable / Timeout / Network / BadRequest / Unknown)apps/api
ProvisionManagedAgentuse-case — fetches integration credentials, calls provider, persistsexternalAgentId, rolls back on Mongo failureGetAgentRuntimeConfig/UpdateAgentRuntimeConfiguse-cases proxying live config to/from the providerDELETEextended for soft-delete + async upstream cleanup jobAgentRuntimeExceptionFilter— maps error union to stable HTTP codes +Retry-Afterheader on 429GET /agents/runtime-providers— returns static catalog (gated by feature flag)GET/PATCH /agents/:id/runtime/configendpointsapps/dashboard
src/api/agent-runtime.ts—getAgentRuntimeProviders,getAgentRuntimeConfig,updateAgentRuntimeConfigsrc/utils/agent-runtime-errors.ts— error-code → UI treatment map + TanStack Query retry/backoff (honoursRetry-Afteron 429, no retry on 401/403/400/409, 2 retries on 503)AgentRuntimeBadge— Self-hosted / Managed (provider name) badgeAgentRuntimeConfigSection— live config card with loading skeleton + section-level error panelsAgentRuntimeAuthBanner— sticky page banner on 401, links to integration editAgentRuntimeDriftModal— Recreate / Unlink / Cancel modal forAGENT_RUNTIME_DRIFT(409)AgentOverviewTab— wires all new components; capability-driven panel visibilityManagedRuntimeSetupGuide— onboarding runtime chooser + Anthropic key + model + system prompt formAgentsSetupPage— runtime chooser injected before self-hosted bridge setup, gated byIS_MANAGED_AGENT_RUNTIME_ENABLEDScreenshots
Expand for optional sections
Special notes for your reviewer
handle-agent-reply.usecase.ts) is out of scope — aTODO(managed-agents)comment marks the branch point for a future PR by the runtime team.@anthropic-ai/sdkdependency satisfies theminimumReleaseAge: 1440policy inpnpm-workspace.yaml.Made with Cursor