Skip to content

AgentLabUI: Agent Cloning, Sharing, and Import Export Features

Trevor Grant edited this page Jun 19, 2025 · 1 revision

AgentLabUI: Agent Cloning, Sharing, and Import/Export Features

This document outlines the implementation of agent cloning, sharing (public/private), and import/export functionalities within AgentLabUI.

1. Overview of Features

1.1. Clone Agent

Users can create an exact duplicate of an existing agent. The clone will:

  • Have "_Copy" appended to its name.
  • Be initially private and undeployed.
  • Retain configurations of the original agent, including child agent references (child agents themselves are not duplicated).
  • Redirect the user to the edit page of the newly cloned agent.

1.2. Share Agent (Public/Private)

Agents can be toggled between:

  • Private: Visible and manageable only by the owner (and admins). This is the default state for new or imported agents.
  • Public: Visible and runnable by any authenticated user on the platform instance. Management (editing, deleting, changing public status) remains restricted to the owner or an admin.
    The dashboard displays "Your Agents" and "Public Agents" (excluding the user's own public agents from the latter list).

1.3. Import/Export Agent

  • Export: Users can download the configuration of an agent as a JSON file (<agent_name>.json). Sensitive information like API keys, user ID, and deployment status are stripped or reset.
  • Import: Users can upload a previously exported agent JSON file to create a new, private, and undeployed agent in their account.

2. Data Model Changes

A new field has been added to the agent documents in the /agents/{agentId} Firestore collection:

  • isPublic: (Boolean)
    • false (default): The agent is private to the owner.
    • true: The agent is public and visible/runnable by all authenticated users.

3. Firestore Security Rules (firestore.rules)

Security rules have been updated to accommodate these features.

3.1. /agents/{agentId} Rules

rules_version = '2';  
service cloud.firestore {  
  match /databases/{database}/documents {  
  
    // --- Users Collection ---  
    match /users/{userId} {  
      function isAdmin() {  
        return request.auth != null && exists(/databases/$(database)/documents/users/$(request.auth.uid)) &&  
               get(/databases/$(database)/documents/users/$(request.auth.uid)).data.permissions.isAdmin == true;  
      }  
      allow get: if request.auth.uid == userId || isAdmin();  
      allow list: if isAdmin();  
      allow create: if request.auth.uid == userId &&  
                       !("permissions" in request.resource.data);  
      allow update: if request.auth.uid == userId &&  
                       request.resource.data.diff(resource.data).affectedKeys().hasOnly(['lastLoginAt', 'email', 'displayName', 'photoURL', 'updatedAt'])  
                    ||  
                    isAdmin() &&  
                       request.resource.data.diff(resource.data).affectedKeys().hasAny(['permissions', 'permissionsLastUpdatedAt']);  
      allow delete: if isAdmin();  
    }  
  
    // --- Agents Collection ---  
    match /agents/{agentId} {  
      function isOwner(docData) {  
        return request.auth.uid == docData.userId;  
      }  
      function isAdmin() {  
        return request.auth != null && exists(/databases/$(database)/documents/users/$(request.auth.uid)) &&  
               get(/databases/$(database)/documents/users/$(request.auth.uid)).data.permissions.isAdmin == true;  
      }  
      function isAgentPublic() {  
        // Check incoming data for updates, existing resource for reads  
        return request.method == 'update' ? request.resource.data.isPublic == true : resource.data.isPublic == true;  
      }  
  
      // Allow create if user is authenticated, setting their own userId,  
      // deploymentStatus is "not_deployed" or not present, and isPublic is boolean.  
      allow create: if request.auth.uid == request.resource.data.userId &&  
                       (request.resource.data.deploymentStatus == "not_deployed" || !("deploymentStatus" in request.resource.data)) &&  
                       (request.resource.data.isPublic == false || request.resource.data.isPublic == true);  
  
      // Owner, admin, or if agent is public can read.  
      allow get: if isOwner(resource.data) || isAdmin() || resource.data.isPublic == true;  
  
      // Admin can list all.  
      // Users can list their own agents OR public agents. Client makes separate queries.  
      allow list: if isAdmin() || (  
                     request.auth.uid != null &&  
                     request.query.resource.__name__[0] == 'projects' && // General sanity check  
                     request.query.filters.size() > 0 && (  
                       // Querying own agents  
                       (request.query.filters[0].fieldPath == "userId" &&  
                        request.query.filters[0].op == "==" &&  
                        request.query.filters[0].value == request.auth.uid)  
                       ||  
                       // Querying public agents  
                       (request.query.filters[0].fieldPath == "isPublic" &&  
                        request.query.filters[0].op == "==" &&  
                        request.query.filters[0].value == true)  
                     )  
                   );  
        
      // Owner can update their agent's config fields including 'isPublic'.  
      // Admin can update agent's config fields, 'isPublic', and deployment fields.  
      // Owner cannot change userId or directly update deployment fields via this rule.  
      allow update: if  
                    (isOwner(resource.data) &&  
                      request.resource.data.userId == resource.data.userId &&   
                      request.resource.data.diff(resource.data).affectedKeys().hasOnly([  
                        'name', 'description', 'agentType', 'instruction', 'tools', 'enableCodeExecution',  
                        'usedCustomRepoUrls', 'selectedProviderId', 'litellm_model_string', 'litellm_api_base',  
                        'litellm_api_key', 'outputKey', 'maxLoops', 'childAgents', 'platform',  
                        'isPublic',   
                        'updatedAt'  
                        // Owner cannot directly change deployment fields via this rule  
                      ]) && (  
                        // Check that deployment related fields are not being changed by owner.  
                        // This complex check is needed because hasOnly would allow them if they are the *only* fields.  
                        // We want to ensure they are *not* part of an update by an owner through this particular rule branch.  
                        !request.resource.data.diff(resource.data).affectedKeys().hasAny([  
                            'deploymentStatus', 'vertexAiResourceName', 'lastDeployedAt',  
                            'lastDeploymentAttemptAt', 'deploymentError'  
                        ]) || ( // OR if these fields are present but not actually changing  
                            request.resource.data.deploymentStatus == resource.data.deploymentStatus &&  
                            request.resource.data.vertexAiResourceName == resource.data.vertexAiResourceName &&  
                            request.resource.data.lastDeployedAt == resource.data.lastDeployedAt &&  
                            request.resource.data.lastDeploymentAttemptAt == resource.data.lastDeploymentAttemptAt &&  
                            request.resource.data.deploymentError == resource.data.deploymentError  
                        )  
                      )  
                    ) ||  
                    (isAdmin() &&  
                      request.resource.data.userId == resource.data.userId && // Admin cannot change userId  
                      request.resource.data.diff(resource.data).affectedKeys().hasAny([ // Admin can update more fields  
                        'name', 'description', 'agentType', 'instruction', 'tools', 'enableCodeExecution',  
                        'usedCustomRepoUrls', 'selectedProviderId', 'litellm_model_string', 'litellm_api_base',  
                        'litellm_api_key', 'outputKey', 'maxLoops', 'childAgents', 'platform',  
                        'isPublic',   
                        'deploymentStatus', 'vertexAiResourceName', 'lastDeployedAt',  
                        'lastDeploymentAttemptAt', 'deploymentError',  
                        'updatedAt'  
                      ])  
                    );  
  
      // Owner or Admin can delete.  
      allow delete: if isOwner(resource.data) || isAdmin();  
    }  
  
    // --- Agent Runs Subcollection ---  
    match /agents/{agentId}/runs/{runId} {  
      function isAgentOwnerForRuns() {  
        let agentDoc = get(/databases/$(database)/documents/agents/$(agentId));  
        return request.auth.uid != null &&  
               exists(/databases/$(database)/documents/agents/$(agentId)) &&  
               agentDoc.data.userId == request.auth.uid;  
      }  
      function isParentAgentPublicForRuns() {  
        let agentDoc = get(/databases/$(database)/documents/agents/$(agentId));  
        return exists(/databases/$(database)/documents/agents/$(agentId)) &&  
               agentDoc.data.isPublic == true;  
      }  
      function canRunAgentPermission() {  
        return request.auth != null &&  
               exists(/databases/$(database)/documents/users/$(request.auth.uid)) &&  
               get(/databases/$(database)/documents/users/$(request.auth.uid)).data.permissions.canRunAgent == true;  
      }  
      function isAdminForRuns() {   
        return request.auth != null && exists(/databases/$(database)/documents/users/$(request.auth.uid)) &&  
               get(/databases/$(database)/documents/users/$(request.auth.uid)).data.permissions.isAdmin == true;  
      }  
  
      // Agent owner OR if agent is public, AND user has 'canRunAgent' permission can create runs. Admin can also create.  
      allow create: if ((isAgentOwnerForRuns() || isParentAgentPublicForRuns()) && canRunAgentPermission()) || isAdminForRuns();  
  
      // Agent owner OR if agent is public can read their agent's runs. Admin can read any.  
      allow read: if isAgentOwnerForRuns() || isParentAgentPublicForRuns() || isAdminForRuns();  
      allow list: if isAgentOwnerForRuns() || isParentAgentPublicForRuns() || isAdminForRuns(); // For listing runs for a specific agent  
    }  
  
    // --- Gofannon Tool Manifest ---  
    match /gofannonToolManifest/{docId} {  
      function isAdmin() { // Ensure this is defined or accessible globally if not re-declared  
        return request.auth != null && exists(/databases/$(database)/documents/users/$(request.auth.uid)) &&  
               get(/databases/$(database)/documents/users/$(request.auth.uid)).data.permissions.isAdmin == true;  
      }  
      allow read: if request.auth != null;  
      allow write: if isAdmin();  
    }  
  }  
}  

Key Rule Changes:

  • Create Agent: Allows deploymentStatus to be "not_deployed" (for clones/imports) and isPublic to be set as a boolean.
  • Get Agent: Allows access if the agent is public (resource.data.isPublic == true).
  • List Agents: Allows querying for agents where isPublic == true.
  • Update Agent: Owners and admins can modify the isPublic field. Owners cannot directly modify deployment-related fields through this rule.
  • Agent Runs: create, read, list operations on agent runs consider if the parent agent is public, in addition to ownership and user permissions (canRunAgent).

4. Backend Firebase Functions (Conceptual)

While specific Firebase Function code was not modified in this iteration, it's crucial that backend functions, especially query_deployed_agent, respect the agent's isPublic status for authorization checks before interacting with deployed resources on platforms like Vertex AI.
The function should:

  1. Fetch the agent's document from Firestore using agentDocId.
  2. If agent.isPublic === true, allow the query if the adkUserId (user making the query) has canRunAgent permission from their user profile.
  3. If agent.isPublic === false, allow the query only if adkUserId === agent.userId (owner) AND they have canRunAgent permission.
  4. Admins should generally be allowed, subject to overall function design.

5. Frontend Implementation Details

5.1. User Permissions and Login (ensureUserProfile)

To ensure non-admin users can log in and use the application according to defined access levels, the ensureUserProfile function in src/services/firebaseService.js was updated:

  • Default Permissions: When a new user profile is created, or an existing user profile is found without a permissions field, a default set of permissions is assigned:
    • isAuthorized: true (allows app access)
    • canRunAgent: true (allows running public agents or owned agents)
    • canCreateAgent: false (creation rights typically granted by an admin)
    • isAdmin: false
  • This ensures that ProtectedRoute.js can correctly evaluate currentUser.permissions.isAuthorized.

5.2. Cloning an Agent

  • UI: A "Clone" button is available on the AgentPage.js.
  • Logic (AgentPage.js -> handleCloneAgent):
    1. The current agent's data is copied.
    2. The name is modified by appending "_Copy".
    3. Sensitive/instance-specific fields are reset or nulled:
      • isPublic is set to false.
      • deploymentStatus, vertexAiResourceName, lastDeployedAt, etc., are reset/nulled.
      • litellm_api_key (and for child agents) is nulled.
    4. maxLoops is handled explicitly: set to a valid number for LoopAgent (defaulting to 3 if invalid) or null for other agent types to prevent Firestore errors with undefined values.
    5. The createAgentInFirestore service function is called with the current user's UID and the prepared cloned data.
    6. Upon successful creation, the user is navigated to the edit page of the new clone.

5.3. Sharing an Agent (Public/Private Toggle)

  • UI: A Switch component on AgentPage.js allows toggling the public status.
  • Logic (AgentPage.js -> handleTogglePublic):
    1. Authorization check: Only the agent owner or an admin can perform this action.
    2. The isPublic field of the agent document in Firestore is updated to the new status.
    3. The local component state for the agent is updated to reflect the change.
  • Display:
    • AgentPage.js: Shows a "Public" chip if agent.isPublic is true.
    • AgentListItem.js: Shows a "Public" chip.

5.4. Exporting an Agent

  • UI: An "Export" button on AgentPage.js.
  • Logic (AgentPage.js -> handleExportAgent):
    1. The current agent's data is retrieved.
    2. Fields unsuitable for export are removed: id, userId, createdAt, updatedAt, deploymentStatus, vertexAiResourceName, lastDeployedAt, lastDeploymentAttemptAt, deploymentError, isPublic.
    3. API keys (litellm_api_key for parent and children) are explicitly nulled.
    4. The cleaned data is stringified into JSON.
    5. A blob is created and downloaded via a temporary anchor link, with the filename <agent_name_sanitized>.json.

5.5. Importing an Agent

  • UI: An "Import Agent from JSON" option in a split button dropdown on DashboardPage.js. This triggers a hidden file input.
  • Logic (DashboardPage.js -> handleFileImport):
    1. The user selects a JSON file.
    2. The file content is read and parsed as JSON.
    3. Basic validation checks for required fields like name and agentType.
    4. The imported data is sanitized:
      • id, userId, isPublic, timestamps, deployment info are removed or will be set anew.
      • API keys (litellm_api_key for parent and children) are explicitly nulled.
    5. createAgentInFirestore is called with the current user's UID and the sanitized agent data. The isImport flag is passed to this service function, which ensures certain fields are correctly initialized for an imported agent.
    6. The new agent defaults to isPublic: false and deploymentStatus: "not_deployed".
    7. User is navigated to the edit page of the newly imported agent.

5.6. Dashboard Display

  • DashboardPage.js:
    • Fetches agents using two separate calls:
      • getMyAgents(currentUser.uid): Fetches agents owned by the current user.
      • getPublicAgents(currentUser.uid): Fetches all public agents, filtering out any owned by the current user (to avoid duplication).
    • Displays "Your Agents" and "Public Agents" in distinct sections using AgentList.
  • AgentListItem.js:
    • Displays a "Public" chip if agent.isPublic is true.
    • Displays the agent owner's UID (or "You" if it's the current user).
    • Edit and Delete buttons are only shown if the current user is the owner or an admin.

6. Key Service Functions (src/services/firebaseService.js)

  • createAgentInFirestore(userId, agentData, isImport = false):
    • Now takes an optional isImport flag.
    • Sets isPublic: false and resets deployment fields for all new/imported agents.
    • Ensures userId is explicitly set as the owner.
  • getMyAgents(userId): Fetches agents where userId matches the provided ID. (Renamed from getUserAgents).
  • getPublicAgents(currentUserId): Fetches agents where isPublic == true and userId != currentUserId.
  • updateAgentInFirestore(agentId, updatedData): Used for general updates, including toggling isPublic.
  • ensureUserProfile(authUser): Modified to assign default permissions to new users or existing users missing the permissions field, ensuring basic app access for non-admins.

7. Troubleshooting Common Issues

  • "FirebaseError: Missing or insufficient permissions":
    • This usually indicates a mismatch between the data being written/read and the Firestore security rules.
    • For Create/Clone: Check the /agents/{agentId} allow create rule, particularly conditions around deploymentStatus, userId, and isPublic.
    • For Updates (e.g., Make Public): Check the /agents/{agentId} allow update rule, ensuring the user has rights to modify the specific fields being changed (e.g., only owner/admin for isPublic).
    • For Reads/Lists: Ensure allow get and allow list rules correctly evaluate isPublic and ownership.
  • "Unsupported field value: undefined":
    • Firestore does not accept undefined as a field value. Ensure all properties in objects being saved to Firestore are either actual values or explicitly null.
    • This was specifically addressed for the maxLoops field during the clone operation.
  • Non-admin login issues: If non-admins are redirected to /unauthorized immediately after login, verify:
    1. ensureUserProfile is correctly setting default permissions (especially isAuthorized: true).
    2. ProtectedRoute.js is correctly checking currentUser.permissions.isAuthorized.
    3. Firestore rules allow users to read their own /users/{userId} document.

This comprehensive set of features enhances collaboration and agent management within AgentLabUI. Remember to thoroughly test all user flows and security rule implications.