Conversation
There was a problem hiding this comment.
Solid feature implementation — agent copy (same-project duplicate and cross-project import) with well-structured data access, proper authorization checks, and comprehensive test coverage. A few issues worth addressing before merge, most notably a variable shadowing bug in the import handler and a docs/UI label mismatch.
Claude Opus | 𝕏
|
|
||
| if (process.env.ENVIRONMENT !== 'test') { | ||
| const userId = c.get('userId'); | ||
| const tenantId = c.get('tenantId'); |
There was a problem hiding this comment.
Bug: variable shadowing. const tenantId = c.get('tenantId') shadows the tenantId from c.req.valid('param') on line 61. This means the returned tenantId (line 100) comes from the validated path param, but the canViewProject call on line 84 uses the middleware-injected value. If these ever diverge (e.g. a middleware bug or a crafted request), authorization could silently check the wrong tenant. Remove the inner declaration and reuse the outer tenantId.
| const tenantId = c.get('tenantId'); | |
| const userId = c.get('userId'); | |
| if (!userId || !tenantId) { |
| export const importAgentHandler = async (c: ImportAgentHandlerContext) => { | ||
| const { tenantId, projectId } = c.req.valid('param'); | ||
| const body = c.req.valid('json'); | ||
|
|
||
| if (body.sourceProjectId === projectId) { | ||
| throw createApiError({ | ||
| code: 'bad_request', | ||
| message: | ||
| 'Source and target project must differ. Use /duplicate to copy within the same project.', | ||
| }); | ||
| } | ||
|
|
||
| if (process.env.ENVIRONMENT !== 'test') { |
There was a problem hiding this comment.
Architecture concern: process.env.ENVIRONMENT branching. The import handler uses process.env.ENVIRONMENT !== 'test' in two places — once here to skip auth, and again in the route body to skip Doltgres branching. This pattern isn't used elsewhere in the codebase and creates a production code path that's untestable in integration tests. Consider extracting the canViewProject check into a middleware or injecting a canViewSourceProject callback through the handler context so tests can supply a stub without relying on a global env check.
| 1. Open the agent's menu and select **Duplicate**. | ||
| 2. Choose the **Target project**. The current project is selected by default. | ||
| 3. Change the suggested name and ID. | ||
| 4. Click **Copy agent**. |
There was a problem hiding this comment.
The docs say Click **Copy agent** but the actual button in duplicate-agent-section.tsx reads "Duplicate agent". Update the docs to match the UI label.
| (value) => value || undefined, | ||
| DuplicateAgentRequestSchema.shape.newAgentName | ||
| ), | ||
| }); |
There was a problem hiding this comment.
The DuplicateAgentFormSchema wraps newAgentName in z.preprocess to coerce falsy values to undefined, but DuplicateAgentRequestSchema.shape.newAgentName is already .optional(). Unless there's a specific edge case with empty-string form inputs, the preprocess layer is redundant. If it IS needed (react-hook-form sends "" instead of undefined), a brief inline comment would help future readers understand why.
| await createSkill(params.targetDb)({ | ||
| tenantId: params.targetScopes.tenantId, | ||
| projectId: params.targetScopes.projectId, | ||
| name: params.sourceSkill.name, | ||
| description: params.sourceSkill.description, | ||
| content: params.sourceSkill.content, | ||
| metadata: params.sourceSkill.metadata ?? null, | ||
| files: normalizeSkillFiles(params.sourceSkill.files), | ||
| }); |
There was a problem hiding this comment.
Fragile implicit assumption: createSkill derives id from name (id: data.name), so the imported skill will match the agent's skill references only if sourceSkill.id === sourceSkill.name. This is currently true because createSkill always sets id = name, but this coupling isn't enforced by types and would break silently if skill ID generation changes. Consider passing id: params.sourceSkill.id explicitly if the createSkill signature allows it, or add an assertion sourceSkill.id === sourceSkill.name as a guard.
| try { | ||
| return await targetDb.transaction(async (tx) => { |
There was a problem hiding this comment.
The entire import operation runs inside targetDb.transaction(), which is good for atomicity. However, loadReferencedSourceDependencies (line 668) reads from sourceDb outside the transaction. If a concurrent write mutates the source project between the read and the transactional write, the import could use stale data. This is likely acceptable given the intended workflow (point-in-time copy), but worth documenting as a known trade-off.
| }); | ||
|
|
||
| const { isSubmitting } = form.formState; | ||
| const { data: projects, isError: projectsError } = useProjectsQuery({ |
There was a problem hiding this comment.
Nit: useProjectsQuery destructures data and isError, but projects (from data) is accessed directly without a guard for loading/error state. If the query is still loading, projects could be undefined, and currentProject, otherProjects, etc. would fail. The enabled: isOpen guard helps, but the initial render when isOpen flips to true could briefly hit this. Confirm that useProjectsQuery returns a stable empty array default for data.
There was a problem hiding this comment.
PR Review Summary
(0) New Issues | (5) Pending from Prior Review | Risk: Medium
Delta Review: This review covers changes since the prior automated review (commits
98f297a6..73a57389). The Ito test failure (race condition on rapid double-submit) correlates with the TOCTOU issue flagged previously.
Delta Changes Analysis
The delta consists primarily of a UI architecture refactor that moves the duplicate functionality from a modal within "New Agent" to a dedicated dialog accessible from each agent's context menu:
| File | Change | Assessment |
|---|---|---|
duplicate-agent-section.tsx |
Refactored DuplicateAgentSection → DuplicateAgentDialog; props now include source agent info directly |
✅ Clean — proper state reset in handleOpenChange, React Compiler-compliant |
import-agent-section.tsx |
Deleted | ✅ Consolidation into DuplicateAgentDialog |
new-agent-item.tsx |
Simplified to only "Create blank agent" | ✅ Cleaner UX |
agent-item-menu.tsx |
Added "Duplicate" menu action | ✅ Follows existing patterns |
field.tsx |
Deleted (no longer needed) | ✅ Cleanup |
No new issues introduced in the delta. The refactored dialog properly resets state on close, follows React patterns, and maintains accessibility (DialogTitle + DialogDescription present).
🕐 Pending Recommendations (5)
The following issues from the prior review remain unaddressed:
- 🟠
agent.ts:83API key users bypass source project authorization —!userId.startsWith('apikey:')skipscanViewProjectcheck for API keys - 🟠
agentDuplicate.ts:44-54TOCTOU race condition — existence check and create not wrapped in transaction (Ito test confirms this manifests as intermittent 500s on rapid double-submit) - 🟠
testsMissing test coverage for race conditions and dependency conflicts beyond tools - 🟡
agentImport.ts:1HTTPException import breaks data-access layer boundary — DAL should usecreateApiError, not Hono's HTTPException - 🟡
agentImport.ts:148-244Sequential dependency loading could be parallelized per AGENTS.md performance guidelines
Resolved since prior review:
- ✅
buildWarningSummaryduplication —import-agent-section.tsxwas deleted, eliminating the duplicate
🚫 REQUEST CHANGES
Summary: The delta changes (UI refactor) are clean and well-implemented. However, 5 issues from the prior review remain unaddressed, including 3 Major findings:
- API key authorization bypass — API keys can import from any project in the tenant without explicit view permission. Verify this is intentional and document if so, or add project-level authz.
- TOCTOU race condition — The Ito test confirms rapid concurrent requests can hit intermittent 500s. Wrap existence check + create in a transaction (as done in
agentImport.ts). - Missing test coverage — Add tests for the race condition path and dependency conflict scenarios for non-tool resources.
The Minor items (HTTPException import, sequential loading) can be addressed in follow-up but don't block merge.
Discarded (1)
| Location | Issue | Reason Discarded |
|---|---|---|
duplicate-agent-section.tsx:91 |
State initialization from props without sync | LOW confidence; properly handled by handleOpenChange reset; each dialog instance has stable props |
Reviewers (2)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
pr-review-frontend |
1 | 0 | 0 | 0 | 0 | 0 | 1 |
pr-review-standards |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Total | 1 | 0 | 0 | 0 | 0 | 5 | 1 |
Note: Delta review scoped to changes since 98f297a6. Prior findings carried forward as Pending Recommendations.
Ito Test Report ✅16 test cases ran. 16 passed. Across a unified run of 16 test cases, all passed (16/16) with zero failures, and the duplicate/import workflows behaved consistently with implemented API and UI logic without surfacing a likely production defect. The most important findings were that same-project duplicate and cross-project import (including mobile) redirected correctly while preserving source agents, edge guards returned expected 400/404/409 responses (including same-project import rejection and missing/existing ID handling), dialog degraded-mode and state-reset behavior were intentional/correct, concurrency/race and two-tab conflicts produced only one final copy, and security/authorization checks held with malicious input blocked, unauthenticated access returning 401/login redirect, and hidden source projects protected by not-found semantics. ✅ Passed (16)Commit: Tell us how we did: Give Ito Feedback |
|
This pull request has been automatically marked as stale because it has not had recent activity. If this PR is still relevant:
Thank you for your contributions! |

















Agent Duplicate & Cross-Project Import
Adds two new agent copy flows — same-project duplication and cross-project import — across the API, data access layer, and dashboard UI.
New API Endpoints
POST/agents/{agentId}/duplicatePOST/agents/importCore Logic (
agents-core)agentDuplicate.ts—duplicateFullAgentServerSide: clones a full agent definition (viabuildCopiedAgentDefinition) within the same project. Validates the new ID differs from the source and that no agent with that ID already exists.agentImport.ts—importFullAgentServerSide: copies an agent from a source project into a target project inside a transaction. Loads all referenced project-scoped dependencies (tools, external agents, data components, artifact components, functions, skills) from the source, then ensures each exists in the target — creating them if missing, erroring on conflicts with differing configuration. Normalizes resource shapes for stable equality comparison. Producescredential_missingwarnings when imported tools or external agents reference credentials absent in the target project.agentPortability.ts— Shared helpers:buildCopiedAgentDefinitionstrips non-portable root keys (tools,externalAgents,teamAgents,functions,triggers), normalizes transfer/delegate targets, and re-validates throughvalidateAndTypeAgentData.collectReferencedDependencyIdswalks sub-agents to collect all dependency IDs needed for cross-project import.Validation Schemas
New Zod schemas and inferred types in
agents-core:DuplicateAgentRequestSchema—{ newAgentId, newAgentName? }ImportAgentRequestSchema—{ sourceProjectId, sourceAgentId, newAgentId, newAgentName? }ImportAgentWarningSchema—{ code: 'credential_missing', resourceType, resourceId, credentialReferenceId }ImportAgentResponseSchema—{ data, warnings }Authorization
editpermission on the target project.viewaccess on the source project (viacanViewProject).withRef/getProjectMainResolvedRef.Dashboard UI (
agents-manage-ui)new-agent-item.tsx— "New Agent" dialog now presents a radio group: Create blank agent or Copy existing agent.duplicate-agent-section.tsx— Renders the copy form. When the selected source project matches the current project, calls the duplicate endpoint. When a different project is selected, calls the import endpoint. Displays post-importcredential_missingwarnings.import-agent-section.tsx— Standalone import form component for the cross-project flow with project/agent combobox selectors and auto-prefilled ID/name.duplicateAgentAction,importAgentAction) and API client functions added for both flows.FieldUI components (Field,FieldContent,FieldTitle,FieldDescription,FieldLabel) used in the radio group layout.Documentation
agents-docs/content/visual-builder/sub-agents.mdx— New "Copying an Agent" section documenting both same-project and cross-project copy flows, including behavior around triggers, credentials, and team-agent limitations.Tests
agentDuplicate.test.ts— Unit tests for same-project duplication (success, ID collision, same-ID rejection).agentImport.test.ts— Unit tests for cross-project import covering dependency recreation, conflict detection, credential warnings, team-agent rejection, and same-project guard.agent.test.ts— Route-level CRUD tests for both/duplicateand/importendpoints.Claude Opus| 𝕏