-
Notifications
You must be signed in to change notification settings - Fork 12
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.
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.
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).
-
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.
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.
-
Security rules have been updated to accommodate these features.
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
deploymentStatusto be "not_deployed" (for clones/imports) andisPublicto 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
isPublicfield. Owners cannot directly modify deployment-related fields through this rule. -
Agent Runs:
create,read,listoperations on agent runs consider if the parent agent is public, in addition to ownership and user permissions (canRunAgent).
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:
- Fetch the agent's document from Firestore using
agentDocId. - If
agent.isPublic === true, allow the query if theadkUserId(user making the query) hascanRunAgentpermission from their user profile. - If
agent.isPublic === false, allow the query only ifadkUserId === agent.userId(owner) AND they havecanRunAgentpermission. - Admins should generally be allowed, subject to overall function design.
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
permissionsfield, 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.jscan correctly evaluatecurrentUser.permissions.isAuthorized.
-
UI: A "Clone" button is available on the
AgentPage.js. -
Logic (
AgentPage.js -> handleCloneAgent):- The current agent's data is copied.
- The name is modified by appending "_Copy".
- Sensitive/instance-specific fields are reset or nulled:
-
isPublicis set tofalse. -
deploymentStatus,vertexAiResourceName,lastDeployedAt, etc., are reset/nulled. -
litellm_api_key(and for child agents) is nulled.
-
-
maxLoopsis handled explicitly: set to a valid number forLoopAgent(defaulting to 3 if invalid) ornullfor other agent types to prevent Firestore errors withundefinedvalues. - The
createAgentInFirestoreservice function is called with the current user's UID and the prepared cloned data. - Upon successful creation, the user is navigated to the edit page of the new clone.
-
UI: A Switch component on
AgentPage.jsallows toggling the public status. -
Logic (
AgentPage.js -> handleTogglePublic):- Authorization check: Only the agent owner or an admin can perform this action.
- The
isPublicfield of the agent document in Firestore is updated to the new status. - The local component state for the agent is updated to reflect the change.
-
Display:
-
AgentPage.js: Shows a "Public" chip ifagent.isPublicis true. -
AgentListItem.js: Shows a "Public" chip.
-
-
UI: An "Export" button on
AgentPage.js. -
Logic (
AgentPage.js -> handleExportAgent):- The current agent's data is retrieved.
- Fields unsuitable for export are removed:
id,userId,createdAt,updatedAt,deploymentStatus,vertexAiResourceName,lastDeployedAt,lastDeploymentAttemptAt,deploymentError,isPublic. - API keys (
litellm_api_keyfor parent and children) are explicitly nulled. - The cleaned data is stringified into JSON.
- A blob is created and downloaded via a temporary anchor link, with the filename
<agent_name_sanitized>.json.
-
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):- The user selects a JSON file.
- The file content is read and parsed as JSON.
- Basic validation checks for required fields like
nameandagentType. - The imported data is sanitized:
-
id,userId,isPublic, timestamps, deployment info are removed or will be set anew. - API keys (
litellm_api_keyfor parent and children) are explicitly nulled.
-
-
createAgentInFirestoreis called with the current user's UID and the sanitized agent data. TheisImportflag is passed to this service function, which ensures certain fields are correctly initialized for an imported agent. - The new agent defaults to
isPublic: falseanddeploymentStatus: "not_deployed". - User is navigated to the edit page of the newly imported agent.
-
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.
- Fetches agents using two separate calls:
-
AgentListItem.js:- Displays a "Public" chip if
agent.isPublicis 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.
- Displays a "Public" chip if
-
createAgentInFirestore(userId, agentData, isImport = false):- Now takes an optional
isImportflag. - Sets
isPublic: falseand resets deployment fields for all new/imported agents. - Ensures
userIdis explicitly set as the owner.
- Now takes an optional
-
getMyAgents(userId): Fetches agents whereuserIdmatches the provided ID. (Renamed fromgetUserAgents). -
getPublicAgents(currentUserId): Fetches agents whereisPublic == trueanduserId != currentUserId. -
updateAgentInFirestore(agentId, updatedData): Used for general updates, including togglingisPublic. -
ensureUserProfile(authUser): Modified to assign default permissions to new users or existing users missing thepermissionsfield, ensuring basic app access for non-admins.
-
"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 createrule, particularly conditions arounddeploymentStatus,userId, andisPublic. -
For Updates (e.g., Make Public): Check the
/agents/{agentId}allow updaterule, ensuring the user has rights to modify the specific fields being changed (e.g., only owner/admin forisPublic). -
For Reads/Lists: Ensure
allow getandallow listrules correctly evaluateisPublicand ownership.
-
"Unsupported field value: undefined":
- Firestore does not accept
undefinedas a field value. Ensure all properties in objects being saved to Firestore are either actual values or explicitlynull. - This was specifically addressed for the
maxLoopsfield during the clone operation.
- Firestore does not accept
-
Non-admin login issues: If non-admins are redirected to
/unauthorizedimmediately after login, verify:-
ensureUserProfileis correctly setting default permissions (especiallyisAuthorized: true). -
ProtectedRoute.jsis correctly checkingcurrentUser.permissions.isAuthorized. - 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.