From d9e4b76df80cf592e0a4f44eb77f66f311d066c7 Mon Sep 17 00:00:00 2001 From: Nathanial Acosta Date: Mon, 13 Jul 2026 11:53:03 -0400 Subject: [PATCH 1/6] Add agent project support --- pkg/resources/agent_projects.go | 6 +- pkg/resources/interfaces.go | 1 + pkg/services/agent_projects.go | 31 +++++++ pkg/services/agent_projects_test.go | 40 +++++++++ pkg/services/interfaces.go | 3 +- .../2023.2/agent-projects/create.success.json | 31 +++++++ .../2023.2/agent-projects/delete.success.json | 7 ++ .../2023.2/agent-projects/export.success.json | 38 ++++++++ .../2023.2/agent-projects/get.success.json | 38 ++++++++ .../2023.2/agent-projects/getall.success.json | 86 +++++++++++++++++++ .../2023.2/agent-projects/import.success.json | 37 ++++++++ 11 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 pkg/services/testdata/2023.2/agent-projects/create.success.json create mode 100644 pkg/services/testdata/2023.2/agent-projects/delete.success.json create mode 100644 pkg/services/testdata/2023.2/agent-projects/export.success.json create mode 100644 pkg/services/testdata/2023.2/agent-projects/get.success.json create mode 100644 pkg/services/testdata/2023.2/agent-projects/getall.success.json create mode 100644 pkg/services/testdata/2023.2/agent-projects/import.success.json diff --git a/pkg/resources/agent_projects.go b/pkg/resources/agent_projects.go index 2e59e23..843efc2 100644 --- a/pkg/resources/agent_projects.go +++ b/pkg/resources/agent_projects.go @@ -49,8 +49,12 @@ func (r *AgentProjectResource) Import(bundle services.AgentProjectBundle, confli return r.service.Import(bundle, conflictMode) } +// Create creates a new agent project with the specified name and description. +func (r *AgentProjectResource) Create(name string, description string) (*services.AgentProject, error) { + return r.service.Create(name, description) +} + // Delete removes an agent project by its identifier. -// This is a pass-through to the service layer for pure API access. func (r *AgentProjectResource) Delete(id string) error { return r.service.Delete(id) } diff --git a/pkg/resources/interfaces.go b/pkg/resources/interfaces.go index fa2381c..d85b622 100644 --- a/pkg/resources/interfaces.go +++ b/pkg/resources/interfaces.go @@ -11,6 +11,7 @@ type AgentProjectResourcer interface { GetAll() ([]services.AgentProject, error) Get(id string) (*services.AgentProject, error) GetByName(name string) (*services.AgentProject, error) + Create(name string, description string) (*services.AgentProject, error) Export(id string) (*services.AgentProjectBundle, error) Import(bundle services.AgentProjectBundle, conflictMode string) (*services.AgentProjectBundle, error) AddMembers(projectId string, members []services.AgentProjectMember) error diff --git a/pkg/services/agent_projects.go b/pkg/services/agent_projects.go index 190da01..9cb4f99 100644 --- a/pkg/services/agent_projects.go +++ b/pkg/services/agent_projects.go @@ -287,3 +287,34 @@ func (svc *AgentProjectService) Import(bundle AgentProjectBundle, conflictMode s return &res.Data, nil } + +// Create creates a new agent project with the specified name and description. +func (svc *AgentProjectService) Create(name string, description string) (*AgentProject, error) { + logging.Trace() + + if name == "" { + return nil, fmt.Errorf("agent project name cannot be empty") + } + + body := map[string]interface{}{ + "name": name, + "description": description, + } + + type createResponse struct { + Message string `json:"message"` + Data AgentProject `json:"data"` + } + + var res createResponse + + if err := svc.PostRequest(&Request{ + uri: agentProjectsBasePath, + body: body, + expectedStatusCode: http.StatusOK, + }, &res); err != nil { + return nil, fmt.Errorf("failed to create agent project '%s': %w", name, err) + } + + return &res.Data, nil +} diff --git a/pkg/services/agent_projects_test.go b/pkg/services/agent_projects_test.go index 8468a26..2851f97 100644 --- a/pkg/services/agent_projects_test.go +++ b/pkg/services/agent_projects_test.go @@ -17,6 +17,7 @@ import ( var ( agentProjectsGetAllSuccess = "agent-project-service/getall.success.json" agentProjectsGetSuccess = "agent-project-service/get.success.json" + agentProjectsCreateSuccess = "agent-projects/create.success.json" agentProjectsExportSuccess = "agent-project-service/export.success.json" agentProjectsImportSuccess = "agent-project-service/import.success.json" ) @@ -223,3 +224,42 @@ func TestAgentProjectService_Import_Error(t *testing.T) { assert.NotNil(t, err) assert.Nil(t, res) } + +func TestAgentProjectService_Create(t *testing.T) { + svc := setupAgentProjectService() + defer testlib.Teardown() + + for _, suite := range fixtureSuites { + response := testlib.Fixture(filepath.Join(fixtureRoot, suite, agentProjectsCreateSuccess)) + testlib.AddPostResponseToMux("/agent-project-service/projects", response, http.StatusOK) + + res, err := svc.Create("Test Agent Project", "A test agent project") + + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, "Test Agent Project", res.Name) + } +} + +func TestAgentProjectService_Create_Error(t *testing.T) { + svc := setupAgentProjectService() + defer testlib.Teardown() + + testlib.AddPostErrorToMux("/agent-project-service/projects", "", 0) + + res, err := svc.Create("Test Agent Project", "A test agent project") + + assert.NotNil(t, err) + assert.Nil(t, res) +} + +func TestAgentProjectService_Create_EmptyName(t *testing.T) { + svc := setupAgentProjectService() + defer testlib.Teardown() + + res, err := svc.Create("", "A test agent project") + + assert.NotNil(t, err) + assert.Nil(t, res) + assert.Contains(t, err.Error(), "name cannot be empty") +} diff --git a/pkg/services/interfaces.go b/pkg/services/interfaces.go index 3c9fce5..65bfa7a 100644 --- a/pkg/services/interfaces.go +++ b/pkg/services/interfaces.go @@ -277,11 +277,12 @@ type TriggerServicer interface { } // AgentProjectServicer defines operations for managing agent projects. -// It handles retrieval and import/export of agent project bundles. +// It handles retrieval, CRUD, and import/export of agent project bundles. type AgentProjectServicer interface { GetAll() ([]AgentProject, error) Get(id string) (*AgentProject, error) GetByName(name string) (*AgentProject, error) + Create(name string, description string) (*AgentProject, error) Export(id string) (*AgentProjectBundle, error) Import(bundle AgentProjectBundle, conflictMode string) (*AgentProjectBundle, error) UpdateProject(projectId string, data map[string]interface{}) error diff --git a/pkg/services/testdata/2023.2/agent-projects/create.success.json b/pkg/services/testdata/2023.2/agent-projects/create.success.json new file mode 100644 index 0000000..e5d51f5 --- /dev/null +++ b/pkg/services/testdata/2023.2/agent-projects/create.success.json @@ -0,0 +1,31 @@ +{ + "message": "success", + "data": { + "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "iid": 1, + "name": "Test Agent Project", + "description": "A test agent project", + "components": [], + "created": "2026-01-01T00:00:00.000Z", + "createdBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "lastUpdated": "2026-01-01T00:00:00.000Z", + "lastUpdatedBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "members": [ + { + "type": "account", + "role": "owner", + "reference": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + } + ] + } +} diff --git a/pkg/services/testdata/2023.2/agent-projects/delete.success.json b/pkg/services/testdata/2023.2/agent-projects/delete.success.json new file mode 100644 index 0000000..3d8192c --- /dev/null +++ b/pkg/services/testdata/2023.2/agent-projects/delete.success.json @@ -0,0 +1,7 @@ +{ + "message": "success", + "data": { + "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "name": "Test Agent Project" + } +} diff --git a/pkg/services/testdata/2023.2/agent-projects/export.success.json b/pkg/services/testdata/2023.2/agent-projects/export.success.json new file mode 100644 index 0000000..553f3ea --- /dev/null +++ b/pkg/services/testdata/2023.2/agent-projects/export.success.json @@ -0,0 +1,38 @@ +{ + "message": "success", + "data": { + "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "name": "Test Agent Project", + "description": "A test agent project", + "agentProjectBundleVersion": 1, + "created": "2026-01-01T00:00:00.000Z", + "createdBy": { + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "agents": [ + { + "_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "name": "Test Agent", + "description": "A test agent", + "instructions": "Do something useful", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": [], + "properties": {} + }, + "created": "2026-01-01T00:00:00.000Z", + "createdBy": { + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "provider": { + "profileName": "anthropic", + "modelName": "claude-sonnet-4-6" + }, + "tools": [] + } + ] + } +} diff --git a/pkg/services/testdata/2023.2/agent-projects/get.success.json b/pkg/services/testdata/2023.2/agent-projects/get.success.json new file mode 100644 index 0000000..cfd400c --- /dev/null +++ b/pkg/services/testdata/2023.2/agent-projects/get.success.json @@ -0,0 +1,38 @@ +{ + "message": "success", + "data": { + "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "iid": 1, + "name": "Test Agent Project", + "description": "A test agent project", + "components": [ + { + "type": "agent", + "iid": 1, + "reference": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "folder": "/" + } + ], + "created": "2026-01-01T00:00:00.000Z", + "createdBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "lastUpdated": "2026-01-01T00:00:00.000Z", + "lastUpdatedBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "members": [ + { + "type": "account", + "role": "owner", + "reference": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + } + ] + } +} diff --git a/pkg/services/testdata/2023.2/agent-projects/getall.success.json b/pkg/services/testdata/2023.2/agent-projects/getall.success.json new file mode 100644 index 0000000..13fcb5f --- /dev/null +++ b/pkg/services/testdata/2023.2/agent-projects/getall.success.json @@ -0,0 +1,86 @@ +{ + "message": "success", + "data": { + "items": [ + { + "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "iid": 1, + "name": "Test Agent Project", + "description": "A test agent project", + "components": [ + { + "type": "agent", + "iid": 1, + "reference": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "folder": "/" + } + ], + "created": "2026-01-01T00:00:00.000Z", + "createdBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "lastUpdated": "2026-01-01T00:00:00.000Z", + "lastUpdatedBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "members": [ + { + "type": "account", + "role": "owner", + "reference": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + } + ] + }, + { + "_id": "dddddddd-dddd-dddd-dddd-dddddddddddd", + "iid": 2, + "name": "Another Agent Project", + "description": "Another test agent project", + "components": [ + { + "type": "agent", + "iid": 1, + "reference": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", + "folder": "/" + } + ], + "created": "2026-01-02T00:00:00.000Z", + "createdBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "lastUpdated": "2026-01-02T00:00:00.000Z", + "lastUpdatedBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "members": [ + { + "type": "account", + "role": "owner", + "reference": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + } + ] + } + ], + "metadata": { + "skip": 0, + "end": 2, + "count": 2, + "limit": 50, + "total": 2, + "previous": null, + "next": null + } + } +} diff --git a/pkg/services/testdata/2023.2/agent-projects/import.success.json b/pkg/services/testdata/2023.2/agent-projects/import.success.json new file mode 100644 index 0000000..3ac4587 --- /dev/null +++ b/pkg/services/testdata/2023.2/agent-projects/import.success.json @@ -0,0 +1,37 @@ +{ + "message": "success", + "data": { + "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "iid": 1, + "name": "Test Agent Project", + "description": "A test agent project", + "agentProjectBundleVersion": 1, + "components": [ + { + "type": "agent", + "reference": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + ], + "created": "2026-01-01T00:00:00.000Z", + "createdBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "lastUpdated": "2026-01-01T00:00:00.000Z", + "lastUpdatedBy": { + "_id": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + }, + "members": [ + { + "type": "account", + "role": "owner", + "reference": "cccccccccccccccccccccccc", + "username": "admin@itential.com", + "provenance": "Local AAA" + } + ] + } +} From fd659c3fc62ade716e215c24af188195705388dc Mon Sep 17 00:00:00 2001 From: Nathanial Acosta Date: Mon, 13 Jul 2026 14:46:24 -0400 Subject: [PATCH 2/6] Finish adding agent-project support --- internal/cli/commands.go | 1 + internal/flags/agent_projects.go | 18 ++ internal/handlers/agent_projects.go | 2 + internal/handlers/descriptors.go | 1 + .../handlers/descriptors/agent_projects.yaml | 81 +++++++++ internal/handlers/handlers.go | 1 + internal/runners/agent_projects.go | 171 ++++++++++++++++++ 7 files changed, 275 insertions(+) diff --git a/internal/cli/commands.go b/internal/cli/commands.go index 44465eb..f444377 100644 --- a/internal/cli/commands.go +++ b/internal/cli/commands.go @@ -87,6 +87,7 @@ func makeChildCommand(root RootCommand, desc map[string]cmdutils.Descriptor) *co cmd.AddGroup( &cobra.Group{ID: "admin-essentials", Title: "Admin Essentials Commands:"}, &cobra.Group{ID: "automation-studio", Title: "Automation Studio Commands:"}, + &cobra.Group{ID: "agent-projects", Title: "Agent Project Commands:"}, &cobra.Group{ID: "configuration-manager", Title: "Configuration Manager Commands:"}, &cobra.Group{ID: "operations-manager", Title: "Operations Manager Commands:"}, &cobra.Group{ID: "lifecycle-manager", Title: "Lifecycle Manager Commands:"}, diff --git a/internal/flags/agent_projects.go b/internal/flags/agent_projects.go index e843ad5..f1807d3 100644 --- a/internal/flags/agent_projects.go +++ b/internal/flags/agent_projects.go @@ -6,6 +6,15 @@ package flags import "github.com/spf13/cobra" +// AgentProjectCreateOptions holds options for the `create agent-project` command. +type AgentProjectCreateOptions struct { + Description string +} + +func (o *AgentProjectCreateOptions) Flags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Description, "description", o.Description, "Description for the agent project (optional)") +} + // AgentProjectImportOptions holds options for the `import agent-project` command. type AgentProjectImportOptions struct { Members []string @@ -23,3 +32,12 @@ func (o *AgentProjectImportOptions) Flags(cmd *cobra.Command) { type AgentProjectExportOptions struct{} func (o *AgentProjectExportOptions) Flags(_ *cobra.Command) {} + +// AgentProjectCopyOptions holds options for the `copy agent-project` command. +type AgentProjectCopyOptions struct { + Members []string +} + +func (o *AgentProjectCopyOptions) Flags(cmd *cobra.Command) { + cmd.Flags().StringArrayVar(&o.Members, "member", o.Members, "Configure one or more agent project members") +} diff --git a/internal/handlers/agent_projects.go b/internal/handlers/agent_projects.go index 0621f27..c5a01fc 100644 --- a/internal/handlers/agent_projects.go +++ b/internal/handlers/agent_projects.go @@ -14,8 +14,10 @@ func NewAgentProjectHandler(rt *Runtime, desc Descriptors) AssetHandler { runners.NewAgentProjectRunner(rt.GetClient(), rt.GetConfig()), desc[agentProjectsDescriptor], &AssetHandlerFlags{ + Create: &flags.AgentProjectCreateOptions{}, Import: &flags.AgentProjectImportOptions{}, Export: &flags.AgentProjectExportOptions{}, + Copy: &flags.AgentProjectCopyOptions{}, }, ) } diff --git a/internal/handlers/descriptors.go b/internal/handlers/descriptors.go index f67eda6..354cdad 100644 --- a/internal/handlers/descriptors.go +++ b/internal/handlers/descriptors.go @@ -41,6 +41,7 @@ const ( transformationsDescriptor = "transformations" jsonformsDescriptor = "jsonforms" projectsDescriptor = "projects" + agentProjectsDescriptor = "agent_projects" analyticTemplatesDescriptor = "analytic_templates" templatesDescriptor = "templates" diff --git a/internal/handlers/descriptors/agent_projects.yaml b/internal/handlers/descriptors/agent_projects.yaml index f349dc3..0a4815f 100644 --- a/internal/handlers/descriptors/agent_projects.yaml +++ b/internal/handlers/descriptors/agent_projects.yaml @@ -33,6 +33,87 @@ describe: # Display details about an agent project called `my-agents` $ ipctl describe agent-project "my-agents" +create: + use: agent-project + group: agent-projects + + description: | + Create a new agent project. + + The `create agent-project` command will create a new empty agent project + with the specified name on the server. An optional description can be + provided via the `--description` flag. If an agent project with the same + name already exists, this command will return an error. + + example: | + # Create a new agent project + $ ipctl create agent-project "my agent project" + + # Create a new agent project with a description + $ ipctl create agent-project "my agent project" --description "A project for my agents" + +delete: + use: agent-project + group: agent-projects + + description: | + Delete an agent project. + + Use the `delete agent-project` command to delete an existing agent project + from the server. If the specified agent project does not exist on the + server, this command will return an error. + + example: | + # Delete an agent project named `my agent project` + $ ipctl delete agent-project "my agent project" + +copy: + use: agent-project --from --to + group: agent-projects + + description: | + Copy an agent project from one server to another. + + The copy command can be used to copy an agent project from one Itential + Platform server to another. + + The `name` argument specifies the agent project name to copy from the + source server to the target. If it does not exist on the source server, + the command will return an error. + + The command has two required options, `--from` and `--to`. The `--from` + option specifies the name of the ipctl profile that points to the server + to copy from. The `--to` option specifies the name of the ipctl profile + that points to the server to copy to. + + When the copy operation is performed, the destination server is first + checked to see if the agent project already exists. If it does exist, + the copy operation will return an error unless `--replace` is specified. + + All member privileges are removed from the project on copy. Use the + `--member` option to set permissions on the destination server. + + example: | + # Copy an agent project from `ipctl-dev` to `ipctl-staging` + $ ipctl copy agent-project "my agent project" --from ipctl-dev --to ipctl-staging + + # Copy and set the group `admins` as owner + $ ipctl copy agent-project "my agent project" --from ipctl-dev --to ipctl-staging --member type=group,name=admins,access=owner + +clear: + use: agent-projects + group: agent-projects + + description: | + Delete all configured agent projects. + + The `clear agent-projects` command will delete all configured agent projects + from the server. This is a destructive command and cannot be undone. + + example: | + # Delete all agent projects from the server + $ ipctl clear agent-projects + import: use: agent-project group: flow-agent diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index fe757ca..7b44920 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -105,6 +105,7 @@ func NewHandler(rt *Runtime) Handler { handlerInstances := []any{ // Automation Studio handlers NewProjectHandler(rt, descriptors), + NewAgentProjectHandler(rt, descriptors), NewWorkflowHandler(rt, descriptors), NewTransformationHandler(rt, descriptors), NewJsonFormHandler(rt, descriptors), diff --git a/internal/runners/agent_projects.go b/internal/runners/agent_projects.go index 183e601..fb8ffc0 100644 --- a/internal/runners/agent_projects.go +++ b/internal/runners/agent_projects.go @@ -86,6 +86,177 @@ func (r *AgentProjectRunner) Describe(in Request) (*Response, error) { }, nil } +////////////////////////////////////////////////////////////////////////////// +// Writer Interface +// + +// Create creates a new agent project with the given name and an optional description. +func (r *AgentProjectRunner) Create(in Request) (*Response, error) { + logging.Trace() + + name := in.Args[0] + description := in.Options.(*flags.AgentProjectCreateOptions).Description + + existing, _ := r.resource.GetByName(name) + if existing != nil { + return nil, fmt.Errorf("agent project %q already exists", name) + } + + project, err := r.resource.Create(name, description) + if err != nil { + return nil, err + } + + return &Response{ + Text: fmt.Sprintf("Successfully created agent project `%s` (%s)", project.Name, project.Id), + Object: project, + }, nil +} + +// Delete removes an agent project by name. +func (r *AgentProjectRunner) Delete(in Request) (*Response, error) { + logging.Trace() + + project, err := r.resource.GetByName(in.Args[0]) + if err != nil { + return nil, err + } + + if err := r.resource.Delete(project.Id); err != nil { + return nil, err + } + + return &Response{ + Text: fmt.Sprintf("Successfully deleted agent project `%s` (%s)", project.Name, project.Id), + }, nil +} + +// Clear removes all agent projects from the platform. +func (r *AgentProjectRunner) Clear(in Request) (*Response, error) { + logging.Trace() + + projects, err := r.resource.GetAll() + if err != nil { + return nil, err + } + + for _, p := range projects { + if err := r.resource.Delete(p.Id); err != nil { + logging.Debug("failed to delete agent project `%s` (%s)", p.Name, p.Id) + return nil, err + } + } + + return &Response{ + Text: fmt.Sprintf("Deleted %v agent project(s)", len(projects)), + }, nil +} + +////////////////////////////////////////////////////////////////////////////// +// Copier Interface +// + +// Copy copies an agent project from the current profile to a destination profile. +func (r *AgentProjectRunner) Copy(in Request) (*Response, error) { + logging.Trace() + + res, err := Copy(CopyRequest{Request: in, Type: "agent project"}, r) + if err != nil { + return nil, err + } + + dstClient, cancel, err := NewClient(in.Common.(*flags.AssetCopyCommon).To, r.config) + if err != nil { + return nil, err + } + defer cancel() + + accounts := services.NewAccountService(dstClient) + groups := services.NewGroupService(dstClient) + userSettings := services.NewUserSettingsService(dstClient) + + activeUser, err := userSettings.Get() + if err != nil { + return nil, fmt.Errorf("failed to get active user: %w", err) + } + + members, err := r.buildAgentProjectMembers( + in.Options.(*flags.AgentProjectCopyOptions).Members, + activeUser.Username, + accounts, + groups, + ) + if err != nil { + return nil, err + } + + if len(members) > 0 { + projectRes := resources.NewAgentProjectResource(services.NewAgentProjectService(dstClient)) + bundle := res.CopyToData.(services.AgentProjectBundle) + if err := projectRes.AddMembers(bundle.Id, members); err != nil { + return nil, fmt.Errorf("failed to add members: %w", err) + } + } + + return &Response{ + Text: fmt.Sprintf("Successfully copied agent project `%s` from `%s` to `%s`", res.Name, res.From, res.To), + }, nil +} + +// CopyFrom exports an agent project bundle from the specified profile. +func (r *AgentProjectRunner) CopyFrom(profile, name string) (any, error) { + logging.Trace() + + c, cancel, err := NewClient(profile, r.config) + if err != nil { + return nil, err + } + defer cancel() + + projectRes := resources.NewAgentProjectResource(services.NewAgentProjectService(c)) + + project, err := projectRes.GetByName(name) + if err != nil { + return nil, err + } + + bundle, err := projectRes.Export(project.Id) + if err != nil { + return nil, err + } + + return *bundle, nil +} + +// CopyTo imports an agent project bundle to the specified profile. +func (r *AgentProjectRunner) CopyTo(profile string, in any, replace bool) (any, error) { + logging.Trace() + + c, cancel, err := NewClient(profile, r.config) + if err != nil { + return nil, err + } + defer cancel() + + projectRes := resources.NewAgentProjectResource(services.NewAgentProjectService(c)) + + bundle, ok := in.(services.AgentProjectBundle) + if !ok { + return nil, fmt.Errorf("expected services.AgentProjectBundle, got %T", in) + } + + if exists, _ := projectRes.GetByName(bundle.Name); exists != nil { + if !replace { + return nil, fmt.Errorf("agent project %q exists on the destination server, use --replace to overwrite", bundle.Name) + } + if err := projectRes.Delete(exists.Id); err != nil { + return nil, fmt.Errorf("failed to delete existing agent project: %w", err) + } + } + + return projectRes.Import(bundle, "keep-both") +} + ////////////////////////////////////////////////////////////////////////////// // Importer Interface // From a58d7a344eb328f84065570f1709b13385db2d76 Mon Sep 17 00:00:00 2001 From: Nathanial Acosta Date: Tue, 14 Jul 2026 10:28:53 -0400 Subject: [PATCH 3/6] Add missing agent-project flag tests --- internal/flags/agent_projects_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/flags/agent_projects_test.go b/internal/flags/agent_projects_test.go index 288e0dc..1137fc7 100644 --- a/internal/flags/agent_projects_test.go +++ b/internal/flags/agent_projects_test.go @@ -11,6 +11,10 @@ import ( "github.com/stretchr/testify/assert" ) +func TestAgentProjectCreateOptions_Flags(t *testing.T) { + checkFlags(t, &AgentProjectCreateOptions{}, []string{"description"}) +} + func TestAgentProjectImportOptions_Flags(t *testing.T) { checkFlags(t, &AgentProjectImportOptions{}, []string{"member"}) } @@ -21,3 +25,7 @@ func TestAgentProjectExportOptions_Flags(t *testing.T) { opts.Flags(cmd) assert.NotNil(t, opts) } + +func TestAgentProjectCopyOptions_Flags(t *testing.T) { + checkFlags(t, &AgentProjectCopyOptions{}, []string{"member"}) +} From 022f22888231a67617b718db842c320c32978bc3 Mon Sep 17 00:00:00 2001 From: Nathanial Acosta Date: Wed, 15 Jul 2026 15:21:16 -0400 Subject: [PATCH 4/6] fix: remove duplicate agentProjectsDescriptor constant Co-Authored-By: Claude Fable 5 --- internal/handlers/descriptors.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/handlers/descriptors.go b/internal/handlers/descriptors.go index 354cdad..34d5526 100644 --- a/internal/handlers/descriptors.go +++ b/internal/handlers/descriptors.go @@ -50,8 +50,6 @@ const ( configurationParsersDescriptor = "configuration_parsers" gctreesDescriptor = "gctrees" - agentProjectsDescriptor = "agent_projects" - serverDescriptor = "server" localAAADescriptor = "localaaa" From 924e4debc2d6811840a98c20c881ccbea8d2f78a Mon Sep 17 00:00:00 2001 From: Nathanial Acosta Date: Wed, 15 Jul 2026 15:57:37 -0400 Subject: [PATCH 5/6] chore: remove unused agent-project test fixtures Co-Authored-By: Claude Fable 5 --- .../2023.2/agent-projects/delete.success.json | 7 -- .../2023.2/agent-projects/export.success.json | 38 -------- .../2023.2/agent-projects/get.success.json | 38 -------- .../2023.2/agent-projects/getall.success.json | 86 ------------------- .../2023.2/agent-projects/import.success.json | 37 -------- 5 files changed, 206 deletions(-) delete mode 100644 pkg/services/testdata/2023.2/agent-projects/delete.success.json delete mode 100644 pkg/services/testdata/2023.2/agent-projects/export.success.json delete mode 100644 pkg/services/testdata/2023.2/agent-projects/get.success.json delete mode 100644 pkg/services/testdata/2023.2/agent-projects/getall.success.json delete mode 100644 pkg/services/testdata/2023.2/agent-projects/import.success.json diff --git a/pkg/services/testdata/2023.2/agent-projects/delete.success.json b/pkg/services/testdata/2023.2/agent-projects/delete.success.json deleted file mode 100644 index 3d8192c..0000000 --- a/pkg/services/testdata/2023.2/agent-projects/delete.success.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "message": "success", - "data": { - "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", - "name": "Test Agent Project" - } -} diff --git a/pkg/services/testdata/2023.2/agent-projects/export.success.json b/pkg/services/testdata/2023.2/agent-projects/export.success.json deleted file mode 100644 index 553f3ea..0000000 --- a/pkg/services/testdata/2023.2/agent-projects/export.success.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "message": "success", - "data": { - "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", - "name": "Test Agent Project", - "description": "A test agent project", - "agentProjectBundleVersion": 1, - "created": "2026-01-01T00:00:00.000Z", - "createdBy": { - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "agents": [ - { - "_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", - "name": "Test Agent", - "description": "A test agent", - "instructions": "Do something useful", - "inputSchema": { - "type": "object", - "additionalProperties": false, - "required": [], - "properties": {} - }, - "created": "2026-01-01T00:00:00.000Z", - "createdBy": { - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "provider": { - "profileName": "anthropic", - "modelName": "claude-sonnet-4-6" - }, - "tools": [] - } - ] - } -} diff --git a/pkg/services/testdata/2023.2/agent-projects/get.success.json b/pkg/services/testdata/2023.2/agent-projects/get.success.json deleted file mode 100644 index cfd400c..0000000 --- a/pkg/services/testdata/2023.2/agent-projects/get.success.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "message": "success", - "data": { - "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", - "iid": 1, - "name": "Test Agent Project", - "description": "A test agent project", - "components": [ - { - "type": "agent", - "iid": 1, - "reference": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", - "folder": "/" - } - ], - "created": "2026-01-01T00:00:00.000Z", - "createdBy": { - "_id": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "lastUpdated": "2026-01-01T00:00:00.000Z", - "lastUpdatedBy": { - "_id": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "members": [ - { - "type": "account", - "role": "owner", - "reference": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - } - ] - } -} diff --git a/pkg/services/testdata/2023.2/agent-projects/getall.success.json b/pkg/services/testdata/2023.2/agent-projects/getall.success.json deleted file mode 100644 index 13fcb5f..0000000 --- a/pkg/services/testdata/2023.2/agent-projects/getall.success.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "message": "success", - "data": { - "items": [ - { - "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", - "iid": 1, - "name": "Test Agent Project", - "description": "A test agent project", - "components": [ - { - "type": "agent", - "iid": 1, - "reference": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", - "folder": "/" - } - ], - "created": "2026-01-01T00:00:00.000Z", - "createdBy": { - "_id": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "lastUpdated": "2026-01-01T00:00:00.000Z", - "lastUpdatedBy": { - "_id": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "members": [ - { - "type": "account", - "role": "owner", - "reference": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - } - ] - }, - { - "_id": "dddddddd-dddd-dddd-dddd-dddddddddddd", - "iid": 2, - "name": "Another Agent Project", - "description": "Another test agent project", - "components": [ - { - "type": "agent", - "iid": 1, - "reference": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", - "folder": "/" - } - ], - "created": "2026-01-02T00:00:00.000Z", - "createdBy": { - "_id": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "lastUpdated": "2026-01-02T00:00:00.000Z", - "lastUpdatedBy": { - "_id": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "members": [ - { - "type": "account", - "role": "owner", - "reference": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - } - ] - } - ], - "metadata": { - "skip": 0, - "end": 2, - "count": 2, - "limit": 50, - "total": 2, - "previous": null, - "next": null - } - } -} diff --git a/pkg/services/testdata/2023.2/agent-projects/import.success.json b/pkg/services/testdata/2023.2/agent-projects/import.success.json deleted file mode 100644 index 3ac4587..0000000 --- a/pkg/services/testdata/2023.2/agent-projects/import.success.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "message": "success", - "data": { - "_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", - "iid": 1, - "name": "Test Agent Project", - "description": "A test agent project", - "agentProjectBundleVersion": 1, - "components": [ - { - "type": "agent", - "reference": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" - } - ], - "created": "2026-01-01T00:00:00.000Z", - "createdBy": { - "_id": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "lastUpdated": "2026-01-01T00:00:00.000Z", - "lastUpdatedBy": { - "_id": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - }, - "members": [ - { - "type": "account", - "role": "owner", - "reference": "cccccccccccccccccccccccc", - "username": "admin@itential.com", - "provenance": "Local AAA" - } - ] - } -} From 3acf4d8fd9a6fecb9874a528d8ea7ffc3f2b119e Mon Sep 17 00:00:00 2001 From: Nathanial Acosta Date: Thu, 16 Jul 2026 12:34:48 -0400 Subject: [PATCH 6/6] fix: remove duplicate agent project handler registration and consolidate fixtures Removes the duplicate NewAgentProjectHandler registration under Automation Studio handlers (already registered under Flow Agent handlers by PR #209). Moves create.success.json fixture into agent-project-service/ alongside existing fixtures and removes the now-empty agent-projects/ directory. Co-Authored-By: Claude Fable 5 --- internal/handlers/handlers.go | 1 - pkg/services/agent_projects_test.go | 2 +- .../create.success.json | 0 3 files changed, 1 insertion(+), 2 deletions(-) rename pkg/services/testdata/2023.2/{agent-projects => agent-project-service}/create.success.json (100%) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 7b44920..fe757ca 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -105,7 +105,6 @@ func NewHandler(rt *Runtime) Handler { handlerInstances := []any{ // Automation Studio handlers NewProjectHandler(rt, descriptors), - NewAgentProjectHandler(rt, descriptors), NewWorkflowHandler(rt, descriptors), NewTransformationHandler(rt, descriptors), NewJsonFormHandler(rt, descriptors), diff --git a/pkg/services/agent_projects_test.go b/pkg/services/agent_projects_test.go index 2851f97..0610170 100644 --- a/pkg/services/agent_projects_test.go +++ b/pkg/services/agent_projects_test.go @@ -17,7 +17,7 @@ import ( var ( agentProjectsGetAllSuccess = "agent-project-service/getall.success.json" agentProjectsGetSuccess = "agent-project-service/get.success.json" - agentProjectsCreateSuccess = "agent-projects/create.success.json" + agentProjectsCreateSuccess = "agent-project-service/create.success.json" agentProjectsExportSuccess = "agent-project-service/export.success.json" agentProjectsImportSuccess = "agent-project-service/import.success.json" ) diff --git a/pkg/services/testdata/2023.2/agent-projects/create.success.json b/pkg/services/testdata/2023.2/agent-project-service/create.success.json similarity index 100% rename from pkg/services/testdata/2023.2/agent-projects/create.success.json rename to pkg/services/testdata/2023.2/agent-project-service/create.success.json