Skip to content

copy agent#2934

Open
dimaMachina wants to merge 70 commits intomainfrom
prd-5932
Open

copy agent#2934
dimaMachina wants to merge 70 commits intomainfrom
prd-5932

Conversation

@dimaMachina
Copy link
Copy Markdown
Collaborator

@dimaMachina dimaMachina commented Mar 31, 2026

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

Method Path Description
POST /agents/{agentId}/duplicate Duplicate an agent within the same project
POST /agents/import Import an agent from another project into the current project

Core Logic (agents-core)

  • agentDuplicate.tsduplicateFullAgentServerSide: clones a full agent definition (via buildCopiedAgentDefinition) within the same project. Validates the new ID differs from the source and that no agent with that ID already exists.
  • agentImport.tsimportFullAgentServerSide: 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. Produces credential_missing warnings when imported tools or external agents reference credentials absent in the target project.
  • agentPortability.ts — Shared helpers: buildCopiedAgentDefinition strips non-portable root keys (tools, externalAgents, teamAgents, functions, triggers), normalizes transfer/delegate targets, and re-validates through validateAndTypeAgentData. collectReferencedDependencyIds walks 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

  • Both endpoints require edit permission on the target project.
  • Cross-project import additionally verifies the caller has view access on the source project (via canViewProject).
  • Source project data is read from its main branch ref using withRef / getProjectMainResolvedRef.
  • Team-agent delegations are rejected during cross-project import.

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-import credential_missing warnings.
  • import-agent-section.tsx — Standalone import form component for the cross-project flow with project/agent combobox selectors and auto-prefilled ID/name.
  • Server actions (duplicateAgentAction, importAgentAction) and API client functions added for both flows.
  • New shared Field UI 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 /duplicate and /import endpoints.

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏


if (process.env.ENVIRONMENT !== 'test') {
const userId = c.get('userId');
const tenantId = c.get('tenantId');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const tenantId = c.get('tenantId');
const userId = c.get('userId');
if (!userId || !tenantId) {

Comment on lines +60 to +72
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') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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**.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +533 to +541
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),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +674 to +675
try {
return await targetDb.transaction(async (tx) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DuplicateAgentSectionDuplicateAgentDialog; 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:83 API key users bypass source project authorization — !userId.startsWith('apikey:') skips canViewProject check for API keys
  • 🟠 agentDuplicate.ts:44-54 TOCTOU race condition — existence check and create not wrapped in transaction (Ito test confirms this manifests as intermittent 500s on rapid double-submit)
  • 🟠 tests Missing test coverage for race conditions and dependency conflicts beyond tools
  • 🟡 agentImport.ts:1 HTTPException import breaks data-access layer boundary — DAL should use createApiError, not Hono's HTTPException
  • 🟡 agentImport.ts:148-244 Sequential dependency loading could be parallelized per AGENTS.md performance guidelines

Resolved since prior review:

  • buildWarningSummary duplication — import-agent-section.tsx was 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:

  1. 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.
  2. 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).
  3. 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.

@github-actions github-actions bot deleted a comment from claude bot Apr 7, 2026
@itoqa
Copy link
Copy Markdown

itoqa bot commented Apr 7, 2026

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)
Category Summary Screenshot
Adversarial Duplicate-submit safeguards hold: UI blocks repeat submit while in flight and backend conflict/unique guards prevent duplicate creation. ADV-1
Adversarial Rapid target-project toggle + submit followed the final selection and routed requests/navigation to the correct project path. ADV-2
Adversarial Malicious IDs are rejected with validation errors, and script-like input is rendered as inert text. ADV-3
Adversarial Logged-out users are redirected to login, and unauthenticated duplicate/import API attempts return 401. ADV-4
Adversarial Source-project visibility enforcement returns not-found semantics for hidden source projects without data leakage. ADV-5
Edge Same-project import request was rejected with 400 Bad Request and guidance to use /duplicate. EDGE-1
Edge Duplicate API rejected newAgentId equal to sourceAgentId with the expected 400 bad_request response. EDGE-2
Edge Duplicate and import flows both rejected an existing target ID with expected 409 conflict semantics. EDGE-3
Edge Import of a non-existent source agent returned expected 404 not_found with no target artifact creation. EDGE-4
Edge Degraded-mode concern was reproduced as a test-method limitation; current code intentionally hides Change only when projects query errors and supports same-project copy fallback. EDGE-6
Edge Dialog state reset behaved correctly after cancel, reopen, and refresh; fresh values were submitted and redirected as expected. EDGE-7
Mobile At 390x844, duplicate/import dialog controls stayed usable and submission redirected successfully for a cross-project copy. MOBILE-1
Happy-path Same-project duplicate created dup-happy-001, redirected to the new agent route, and left the source agent intact in the list. ROUTE-1
Happy-path Cross-project copy via target switch redirected to /default/projects/source-project/agents/imp-happy-001 and preserved the original source agent. ROUTE-2
Happy-path Import flow behavior is implemented correctly; warning semantics and delegation guard are covered by source logic and passing targeted tests. ROUTE-3
Unusual Two-tab same-ID conflict resolved cleanly: first submit created the copy and second submit did not create a duplicate. UNUSUAL-1

Commit: 73a5738

View Full Run


Tell us how we did: Give Ito Feedback

@github-actions
Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity.
It will be closed in 7 days if no further activity occurs.

If this PR is still relevant:

  • Rebase it on the latest main branch
  • Add a comment explaining its current status
  • Request a review if it's ready

Thank you for your contributions!

@github-actions github-actions bot added the stale label Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant