Skip to content

copy agent#2934

Open
dimaMachina wants to merge 73 commits into
mainfrom
prd-5932
Open

copy agent#2934
dimaMachina wants to merge 73 commits into
mainfrom
prd-5932

Conversation

@dimaMachina

@dimaMachina dimaMachina commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator

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𝕏

@pullfrog pullfrog Bot left a comment

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.

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.

@claude claude Bot left a comment

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.

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

itoqa Bot commented Apr 7, 2026

Copy link
Copy Markdown

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
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically closed due to inactivity.

If you'd like to continue working on this, please:

  1. Create a new branch from the latest main
  2. Cherry-pick your commits or rebase your changes
  3. Open a new pull request

Thank you for your understanding!

@github-actions github-actions Bot closed this Apr 25, 2026
@github-actions github-actions Bot deleted the prd-5932 branch April 25, 2026 00:44
@dimaMachina dimaMachina restored the prd-5932 branch June 5, 2026 10:44
@dimaMachina dimaMachina reopened this Jun 5, 2026
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Your PR has been mirrored into Inkeep's internal monorepo, where review and merge happen. Your commit attribution is preserved as @dimaMachina.

What happens next:

  • An Inkeep maintainer will review the internal mirror PR.
  • Reviewer comments are not automatically mirrored back to this thread. If you don't hear from us within a few business days, please comment here to nudge — that's the right thing to do.
  • Once merged internally, the change will sync back to this repository and this PR will close automatically (don't be alarmed when it closes — that's how it lands).

For Inkeep maintainers (link goes to a private repo and is not accessible to external contributors): https://github.com/inkeep/agents-private/pull/1650

See the repository's CONTRIBUTING.md for more context on how public PRs flow. This comment will be updated as the bridge state changes.

@dimaMachina dimaMachina marked this pull request as draft June 5, 2026 10:46
@dimaMachina dimaMachina marked this pull request as ready for review June 5, 2026 11:19

@pullfrog pullfrog Bot left a comment

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.

Caution

This PR opens a cross-project data-read path for API-key callers: the new import endpoint never authorizes the body-supplied sourceProjectId, so a project-scoped key can read any other project's full agent definition in the same tenant. See the inline comment on agent.ts.

Reviewed changes — adds same-project agent duplication (POST /agents/{agentId}/duplicate) and cross-project import (POST /agents/import) across agents-core, the manage API, and the dashboard UI.

  • Cross-project import serviceimportFullAgentServerSide (agentImport.ts) loads a source agent's project-scoped dependencies (tools, external agents, data/artifact components, functions, skills), recreates any missing ones in the target inside a transaction, errors on differing-config conflicts, and emits credential_missing warnings.
  • Same-project duplicationduplicateFullAgentServerSide (agentDuplicate.ts) clones a full agent within one project via buildCopiedAgentDefinition.
  • Portability helpersagentPortability.ts strips non-portable root keys (tools, externalAgents, teamAgents, functions, triggers), normalizes transfer/delegate targets, and collects referenced dependency IDs.
  • Routes + authz — both endpoints require edit on the target; import additionally checks canViewProject on the source, with a test-environment branch that uses a single DB client and skips the source authz check.
  • Validation schemasDuplicateAgentRequestSchema, ImportAgentRequestSchema, ImportAgentWarningSchema, ImportAgentResponseSchema.
  • Dashboard UIduplicate-agent-section.tsx (radio of duplicate vs. cross-project import), import section component, server actions, API client functions, and shared Field/combobox components.
  • Tests + docs — unit + route tests for both flows, plus a "Copying an Agent" docs section.

⚠️ Production cross-project read path and source-project authorization are untested

The import route handler branches on process.env.ENVIRONMENT === 'test'. In tests it calls importFullAgentServerSide(db, db) — the same DB client for source and target — and the source-project canViewProject check is wrapped in the same ENVIRONMENT !== 'test' guard. The production branch that actually reads the source project from its Doltgres main-branch ref (withRef + getProjectMainResolvedRef) and enforces source authorization is therefore exercised by no test at all.

  • The single-DB shortcut means tests never validate that sourceDb reads from a different ref than targetDb — the core mechanism of cross-project import.
  • The only coverage of the source authz check is a unit test that calls importAgentHandler directly with ENVIRONMENT forced to development; the route-level happy-path test uses an agent with no project-scoped dependencies, so ensureReferencedDependenciesInTargetProject is never driven through the HTTP layer.
Technical details
# Production cross-project read path and source authorization are untested

## Affected sites
- `agents-api/src/domains/manage/routes/agent.ts:437-456``ENVIRONMENT === 'test'` selects `importFullAgentServerSide(db, db)`; production `withRef`/`getProjectMainResolvedRef` branch is never hit in CI.
- `agents-api/src/domains/manage/routes/agent.ts:77-103` — source `canViewProject` check is skipped entirely under `ENVIRONMENT === 'test'`.
- `agents-api/src/__tests__/manage/routes/crud/agent.test.ts` — route-level import happy path uses a minimal agent (`canUse: []`, no deps), so dependency recreation is not covered via the route.
- `agents-api/src/__tests__/manage/data/agentImport.test.ts` — conflict-detection coverage exists only for the tool case; `externalAgent`, `dataComponent`, `artifactComponent`, `function`, and `skill` conflict branches are untested.

## Required outcome
- Cross-project reads via distinct source/target refs are exercised by at least one test (or the test-only single-DB branch is justified as faithful to production).
- The source-project authorization decision (allow + deny, including the `apikey:` path) is covered.
- Conflict detection is covered for resource types beyond tools, or the shared helper is tested once generically.

## Open questions for the human
- Is the `ENVIRONMENT === 'test'` branch in production route code a deliberate seam, or should the source-DB acquisition be injected so tests can cover the real dual-ref path without env branching?

ℹ️ Nitpicks

  • Both new test files define inline logger mocks (agentDuplicate.test.ts:34, and the equivalent in agentImport.test.ts); CLAUDE.md mandates createMockLoggerModule() from @inkeep/agents-core/test-utils to avoid drift when the logger interface changes.

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

});
}

if (userId !== 'system' && !userId.startsWith('apikey:')) {

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.

🚨 apikey: and system callers skip the source-project access check, but sourceProjectId arrives in the request body, not the path. The upstream API-key validation (manageAuth.ts:247) only checks the key against the path project — the import target — so the source project is never authorized for these callers. A project-scoped API key with edit on the target can read any other project's full agent definition in the same tenant: tool configs, function executeCode, skill content, data-component props, and external-agent URLs.

Technical details
# API-key callers can read any source project via import

## Affected sites
- `agents-api/src/domains/manage/routes/agent.ts:88``if (userId !== 'system' && !userId.startsWith('apikey:'))` skips `canViewProject` on `sourceProjectId` for key/system callers.
- `agents-api/src/middleware/manageAuth.ts:247` — API-key project validation compares the key's `projectId` to the *path* project (`extractProjectIdFromPath`), i.e. the import target, never the body's `sourceProjectId`.
- `agents-api/src/middleware/projectAccess.ts:57` — target `requireProjectPermission('edit')` also bypasses for key/system, so nothing else covers the source.
- `packages/agents-core/src/data-access/manage/agentImport.ts:286-291` — the `credential_missing` warning echoes the source project's `credentialReferenceId` back to the caller, compounding the cross-project leak.

## Required outcome
- A caller authenticated by a project-scoped API key must not be able to read a `sourceProjectId` it has no access to. Either authorize the source for key callers (e.g. require the key's project to equal `sourceProjectId`) or reject cross-project import for API-key auth entirely.

## Open questions for the human
- Should API-key callers be permitted to perform cross-project import at all, given keys are single-project scoped? If not, the cleanest fix is to deny the key/system bypass for the *source* check specifically.

if (
!areNormalizedValuesEqual(
normalizeTool(params.sourceTool, credentialReferenceId),
normalizeTool(existingTool, existingTool.credentialReferenceId ?? null)

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.

⚠️ Credential references are project-scoped and intentionally non-portable here (the flow emits credential_missing warnings and imports tools "disconnected"). But normalizeTool folds credentialReferenceId into the equality check: when the same tool id already exists in the target with its own credential connected, the source side resolves to null (credential missing in target) while the existing side carries a real id, so this comparison throws a spurious 409. In practice almost any pre-existing credentialed tool will collide on import.

Technical details
# Tool conflict false-positive on project-scoped credentials

## Affected sites
- `packages/agents-core/src/data-access/manage/agentImport.ts:70-79``normalizeTool` includes `credentialReferenceId`.
- `packages/agents-core/src/data-access/manage/agentImport.ts:316-327` — conflict check compares `normalizeTool(sourceTool, <resolved, often null>)` against `normalizeTool(existingTool, existingTool.credentialReferenceId)`.

## Required outcome
- An identically-configured tool that already exists in the target should be reused (not 409) regardless of credential linkage, consistent with the documented "credentials are not copied" behavior.

## Suggested approach
- Exclude `credentialReferenceId` from the tool/external-agent equality fingerprint, or compare credential linkage only when both sides resolve to a present credential. Same reasoning applies to `normalizeExternalAgent`.


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.

ℹ️ This const tenantId shadows the path-param tenantId destructured at line 67. The authz check below uses this context-derived value, while the value returned from the handler (and used for the actual import) is the path-param one. Upstream tenantAccess middleware guarantees the two match today, so this is a latent footgun rather than a live bug — reuse the outer binding instead of re-declaring it.

@github-actions github-actions Bot deleted a comment from claude Bot Jun 5, 2026
@itoqa

itoqa Bot commented Jun 5, 2026

Copy link
Copy Markdown

Ito Diff Report ❌

Tested: 73a5738adf2c14
18 test cases ran this commit: 17 passed ✅, 1 failed ❌.
↪️ Carried forward from prior run (not retested this commit): 16 passing.

Across 18 end-to-end and code-verified scenarios, 17 passed and 1 failed, showing that import/duplicate behavior is mostly correct: divergent config and duplicate-ID cases return deterministic 409s, missing dependencies are created, credential logic preserves shared links or nulls missing references with aggregated warnings, sanitizer rules remove non-portable and trigger metadata while normalizing relation payloads, authorization guards correctly mask unauthorized source-project access and return 401 without user context, and UI duplicate/import flows stayed stable under rapid submits and late project switches.
The key finding is a Medium-severity production bug introduced by this PR in POST /manage/tenants/{tenantId}/projects/{projectId}/agents/import, where concurrent requests using the same newAgentId can hit a race and return HTTP 500 instead of deterministic 409 because the current check-then-create path and unique-constraint error mapping do not cover all duplicate-key error shapes.

❌ Failures (1)
Origin Category Severity Summary Screenshot
🆕 New Import 🟠 Medium Concurrent imports with the same new agent ID can return 500 instead of deterministic 409 conflict. IMPORT-5
🟠 Concurrent import conflict returns server error
  • What failed: One request succeeds while the competing request can surface as HTTP 500 internal_server_error instead of the expected deterministic 409 conflict response.
  • Impact: Concurrent import attempts can produce an unexpected server error that breaks predictable API behavior. Clients cannot reliably treat the losing request as a standard conflict path.
  • Steps to reproduce:
    1. Prepare a source agent and target project.
    2. Submit two import requests at the same time with identical sourceProjectId, sourceAgentId, and newAgentId.
    3. Observe that one request can fail as HTTP 500 instead of deterministic 409 conflict.
  • Stub / mock context: Concurrent imports were intentionally fired with identical payloads, and requests used local test-mode authentication bypass rather than full end-user auth to isolate import-path behavior.
  • Code analysis: The import transaction checks agent existence before create, leaving a race window for concurrent transactions; fallback conflict mapping only handles limited unique-error shapes, so some duplicate-key outcomes escape as 500.
  • Why this is likely a bug: Production import code does not guarantee deterministic conflict handling for concurrent duplicate IDs, and the observed 500 aligns with the identified race and partial error-shape mapping.

Relevant code:

packages/agents-core/src/data-access/manage/agentImport.ts (lines 675-685)

return await targetDb.transaction(async (tx) => {
        const existingTargetAgent = await getAgentById(tx)({
          scopes: { tenantId, projectId: targetProjectId, agentId: newAgentId },
        });

        if (existingTargetAgent) {
          throw createApiError({
            code: 'conflict',
            message: `An agent with ID '${newAgentId}' already exists`,
          });

packages/agents-core/src/data-access/manage/agentImport.ts (lines 711-714)

} catch (error) {
      if (!(error instanceof HTTPException)) {
        throwIfUniqueConstraintError(error, `An agent with ID '${newAgentId}' already exists`);
      }

packages/agents-core/src/utils/error.ts (lines 373-382)

export function isUniqueConstraintError(error: unknown): boolean {
  const err = error as
    | { cause?: { code?: string; message?: string }; message?: string }
    | null
    | undefined;
  return (
    err?.cause?.code === '23505' ||
    !!err?.cause?.message?.includes('1062') ||
    !!err?.message?.includes('already exists')
  );
}
✅ Verified Passing (17)
Category Summary Screenshot
Credential Import returned credential_missing warnings for tool and externalAgent, and imported dependency credential links were nulled. CREDENTIAL-1
Credential Import completed with no warnings and retained cred-shared links for tool and external-agent dependencies. CREDENTIAL-2
Credential Import warning aggregation included both credential_missing:tool and credential_missing:externalAgent. CREDENTIAL-3
Credential Import correctly failed with 409 conflict for divergent existing external-agent config and did not create the target agent. CREDENTIAL-4
Duplicate Rapid double-submit produced one created agent and a stable single outcome. DUPLICATE-3
Duplicate Final target-project selection was honored at submit and routed to the matching project. DUPLICATE-4
Import Import correctly rejected a conflicting preexisting dependency with a 409 and created no new agent. IMPORT-1
Import Import successfully created missing dependencies and the new agent in a single flow. IMPORT-2
Import Import correctly rejected team-agent delegation dependencies and created no target agent. IMPORT-3
Import Import correctly rejected duplicate target agent ID with deterministic 409 behavior. IMPORT-4
Permissions Unauthorized source-project visibility is correctly masked as not found. PERMISSIONS-1
Permissions Missing user context correctly returns unauthorized before import logic runs. PERMISSIONS-2
Permissions Cross-project import succeeds in development and lands on the imported agent page. PERMISSIONS-3
Permissions Differential probing behavior matches intended same-project vs unauthorized-source handling. PERMISSIONS-4
Sanitizer Duplicate sanitization behavior is correct after code-level verification and targeted re-check. SANITIZER-1
Sanitizer Duplicated agent correctly excludes webhook and scheduled trigger semantics. SANITIZER-2
Sanitizer Duplicated relation payload preserves links without exposing relation IDs. SANITIZER-3
↪️ Inherited from Prior Run (16)

Tests that passed in the prior run. c3 judged them unaffected by the diff and did not retest.

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

View Full Run


Tell us how we did: Give Ito Feedback

@github-actions github-actions Bot removed the stale label Jun 6, 2026
@claude

claude Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


PR Review in Progress

Starting comprehensive PR review for agent duplicate & cross-project import feature.

  • Phase 1: Analyze PR context and load core workflow
  • Phase 2: Identify reviewer dispatch needs
  • Phase 3: Dispatch specialized reviewers
  • Phase 4: Consolidate and deduplicate findings
  • Phase 5: Add inline comments to pending review
  • Phase 6: Submit final review with summary

@dimaMachina dimaMachina requested a review from omar-inkeep June 8, 2026 17:14
@itoqa

itoqa Bot commented Jun 8, 2026

Copy link
Copy Markdown

Ito Diff Report ❌

Tested: adf2c142c88ca9
14 test cases ran this commit: 12 passed ✅, 2 failed ❌.
↪️ Carried forward from prior run (not retested this commit): 17 passing.

Across 14 test cases, 12 passed and 2 failed, with successful verification of conversation filtering and read consistency, model-resolution and recovery behavior, MCP prompt rendering rules, and end-to-end project model create/edit workflows including rapid-submit deduplication. The two important remaining medium-severity defects are an agent import race where the losing concurrent request can return 500 internal_server_error instead of a deterministic 409 conflict, and a stale/deleted appId path in GET /run/v1/conversations that returns 401 Invalid Token instead of the expected App not found (404/not_found) response.

❌ Failures (2)
Origin Category Severity Summary Screenshot
🆕 New Conversation 🟠 Medium Requests with a deleted appId returned 401 Invalid Token instead of an App not found response. CONVERSATION-2
🔻 Still broken (verified) Import 🟠 Medium Concurrent import loser can surface as internal server error instead of conflict IMPORT-5
🟠 Stale app identifier returns wrong auth error
  • What failed: The API responds with 401 Invalid Token instead of returning an App not found not-found error for the stale app path.
  • Impact: Clients on this path receive the wrong error class, so they cannot reliably distinguish invalid credentials from a deleted app configuration. This can break expected recovery logic and user messaging for app-scoped conversation history.
  • Steps to reproduce:
    1. Create an app and issue an anonymous session token for it.
    2. Delete that app.
    3. Call GET /run/v1/conversations with the stale x-inkeep-app-id and bearer token.
  • Stub / mock context: A dedicated local integration scenario seeded tenant, project, app, and conversation fixtures before calling the Run API; no route interception, stubs, or bypass edits were used in the final verification.
  • Code analysis: I reviewed the conversations route and auth middleware. The route explicitly throws not_found for missing app records, but app-scoped requests are intercepted in auth first; when the app lookup fails there, middleware falls through to a generic 401, preventing the route-level not-found response.
  • Why this is likely a bug: The production code has conflicting stale-app handling paths, and the auth middleware currently overrides the route contract with a generic 401 response.

Relevant code:

agents-api/src/middleware/runAuth.ts (lines 654-657)

const app = await getAppById(runDbClient)(appIdHeader);
if (!app) {
  return { authResult: null, failureMessage: 'App not found' };
}

agents-api/src/middleware/runAuth.ts (lines 1003-1007)

if (reqData.appId && !subAgentId) {
  if (!apiKey) {
    return { authResult: null, failureMessage: 'Bearer token required for app credential auth' };
  }
  return tryAppCredentialAuth(reqData);
}

agents-api/src/middleware/runAuth.ts (lines 1114-1121)

if (!attempt.authResult) {
  logger.error(
    { failureMessage: attempt.failureMessage },
    'API key authentication error - no valid auth method found'
  );
  throw new HTTPException(401, {
    message: 'Invalid Token',
  });
}

agents-api/src/domains/run/routes/conversations.ts (lines 270-280)

if (appId) {
  const appRecord = await getAppByIdForProject(runDbClient)({
    scopes: { tenantId, projectId },
    id: appId,
  });

  if (!appRecord) {
    throw createApiError({
      code: 'not_found',
      message: 'App not found',
    });
  }
}
🟠 Concurrent agent import returns 500 instead of conflict
  • What failed: One request succeeded, but the loser returned internal_server_error (500) instead of a deterministic conflict (409) response when both requests targeted the same new agent ID.
  • Impact: Concurrent users or retries can receive a generic 500 for a known duplicate-ID conflict path, which makes the import workflow look unstable and harder to recover from correctly. The data write stayed bounded in this run, but error classification is incorrect on a high-value import operation.
  • Steps to reproduce:
    1. Create a source project with an agent and a separate target project in the same tenant.
    2. Send two import requests concurrently with the same sourceProjectId, sourceAgentId, and newAgentId.
    3. Wait for both requests to finish and compare the HTTP responses for winner and loser requests.
  • Stub / mock context: The run used a local non-production test session and temporary UI test-mode wiring, but the import race itself was exercised against the real local import endpoint and database behavior without route-level stubbing of the import API responses.
  • Code analysis: In packages/agents-core/src/data-access/manage/agentImport.ts, duplicate detection is split between an optimistic pre-check inside the transaction and a catch-based unique-constraint mapper. In packages/agents-core/src/utils/error.ts, unique violation detection only checks error.cause.code, selected message fragments, and fallback text, so duplicate-key errors that surface on a different shape can bypass conflict mapping and bubble as 500.
  • Why this is likely a bug: The import path is designed to map duplicate-ID races to conflict, but the current unique-constraint detection misses some real duplicate-key error shapes, allowing a 500 to leak instead.

Relevant code:

packages/agents-core/src/data-access/manage/agentImport.ts (lines 675-714)

return await targetDb.transaction(async (tx) => {
  const existingTargetAgent = await getAgentById(tx)({
    scopes: { tenantId, projectId: targetProjectId, agentId: newAgentId },
  });

  if (existingTargetAgent) {
    throw createApiError({
      code: 'conflict',
      message: `An agent with ID '${newAgentId}' already exists`,
    });
  }

  const importedAgent = await createFullAgentServerSide(tx, logger)(
    targetProjectScopes,
    importedAgentDefinition
  );

  return {
    data: importedAgent as ImportAgentResponse['data'],
    warnings,
  };
});
} catch (error) {
  if (!(error instanceof HTTPException)) {
    throwIfUniqueConstraintError(error, `An agent with ID '${newAgentId}' already exists`);
  }
  throw error;
}

packages/agents-core/src/utils/error.ts (lines 373-382)

export function isUniqueConstraintError(error: unknown): boolean {
  const err = error as
    | { cause?: { code?: string; message?: string }; message?: string }
    | null
    | undefined;
  return (
    err?.cause?.code === '23505' ||
    !!err?.cause?.message?.includes('1062') ||
    !!err?.message?.includes('already exists')
  );
}
✅ Verified Passing (12)
Category Summary Screenshot
Conversation The endpoint returned only the default-agent conversations for the authenticated end user. CONVERSATION-1
Conversation Concurrent defaultAgentId updates kept each response internally consistent with no mixed conversation slices. CONVERSATION-3
Model Chat execution correctly stops with a clear base-model-required error when no base model is configured at any level. MODEL-1
Model Sub-agent base override works while structured output and summarizer continue inheriting from higher-level model settings. MODEL-2
Model Runtime behavior recovers cleanly after correcting model configuration, with no code evidence of stale failed-run coupling. MODEL-3
Project New project form showed Claude Sonnet 4.5 for base/structured output and Gemini 3.1 Flash Lite for summarizer by default. PROJECT-1
Project Edit form preserved the existing non-default summarizer value instead of replacing it with the create default. N/A
Project Creating a project with default models redirected to the new agents page and saved the expected default model set. N/A
Project Updating summarizer in edit mode stayed in the settings flow and persisted the selected override after reload. N/A
Project Rapid create submission resulted in one new project row with no duplicate project creation. N/A
Tools Empty MCP groups were correctly excluded and the no-tools sentinel was shown. TOOLS-1
Tools Instructions-only MCP groups with zero tools were fully suppressed in the rendered prompt. TOOLS-2
↪️ Inherited from Prior Run (17)

Tests that passed in the prior run. c3 judged them unaffected by the diff and did not retest.

Category Summary Screenshot
Credential Import returned credential_missing warnings for tool and externalAgent, and imported dependency credential links were nulled. N/A
Credential Import completed with no warnings and retained cred-shared links for tool and external-agent dependencies. N/A
Credential Import warning aggregation included both credential_missing:tool and credential_missing:externalAgent. N/A
Credential Import correctly failed with 409 conflict for divergent existing external-agent config and did not create the target agent. N/A
Duplicate Rapid double-submit produced one created agent and a stable single outcome. N/A
Duplicate Final target-project selection was honored at submit and routed to the matching project. N/A
Import Import correctly rejected a conflicting preexisting dependency with a 409 and created no new agent. N/A
Import Import successfully created missing dependencies and the new agent in a single flow. N/A
Import Import correctly rejected team-agent delegation dependencies and created no target agent. N/A
Import Import correctly rejected duplicate target agent ID with deterministic 409 behavior. N/A
Permissions Unauthorized source-project visibility is correctly masked as not found. N/A
Permissions Missing user context correctly returns unauthorized before import logic runs. N/A
Permissions Cross-project import succeeds in development and lands on the imported agent page. N/A
Permissions Differential probing behavior matches intended same-project vs unauthorized-source handling. N/A
Sanitizer Duplicate sanitization behavior is correct after code-level verification and targeted re-check. N/A
Sanitizer Duplicated agent correctly excludes webhook and scheduled trigger semantics. N/A
Sanitizer Duplicated relation payload preserves links without exposing relation IDs. N/A

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 Jun 16, 2026
@omar-inkeep

Copy link
Copy Markdown
Contributor

@dimaMachina can you look at the "TOCTOU" issue that has been flagged in the PR comments?

@github-actions github-actions Bot removed the stale label Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants