Skip to content

Duplicating a coworker gives back a copy that runs somewhere else #327

Description

@zopeVaibhav

Duplicate is the quickest way to make a variant of a coworker that already works. The copy it gives back does not run where the original ran: it is silently repointed at the deployment's managed Bot, while keeping the name, title, role and avatar of the coworker it was copied from.

What it looks like

A coworker hosted at its own AG-UI endpoint, and the copy Duplicate returns for it:

source ->  {"endpoint": "http://localhost:4201/ag-ui", "builtIn": false}
copy   ->  {"endpoint": "http://localhost:4200/ag-ui", "builtIn": true}

Same name, same title, same role description, same avatar seed. A different process answers it.

The copy's builtIn flips to true, and that is what makes this hard to notice. The connection section stops showing an endpoint at all once builtIn is set, and says instead:

Runs on this deployment's own Bot. Nothing to connect and nothing to authenticate: its tool calls are covered by the deployment's own credential.

So the one screen that would reveal the swap hides itself precisely because the swap happened.

There is a third consequence on a deployment with no managed Bot, which is what .env.example ships by default. Duplicate refuses outright, even for a coworker whose own endpoint is sitting in the row being copied:

HTTP 400
{"error":"This deployment has no managed Bot. Give the coworker its own AG-UI endpoint."}

The advice does not apply. That coworker already has an endpoint, and Duplicate offers no way to supply one.

Why it happens

duplicate() never reads the source's configuration. It writes the managed address unconditionally, and requires that address before the source row has even been looked at: server/src/agents/profile-store.ts:440-448.

if (!managedConfiguration) {
  throw new ManagedAgentUnavailableError();
}
...
configuration: managedConfiguration,

create() uses that same managed address as a fallback for a coworker given no endpoint, which is right. duplicate() applies it as a rule rather than a fallback, which is where the three behaviours above come from.

Reproduction

  1. Configure MANAGED_AGENT_AG_UI_URL and MANAGED_AGENT_TOKEN, and start the server.
  2. POST /api/agents with "endpoint": "http://localhost:4201/ag-ui".
  3. POST /api/agents/<id>/duplicate.
  4. Read the copy's endpoint. It is the managed address from step 1, not 4201.
  5. For the refusal: restart with both MANAGED_AGENT_* variables unset, and repeat step 3 against the coworker from step 2.

Why it matters

Duplicating a working coworker to make a variant of it is the ordinary path, and it is the one the button exists for. What comes back looks like a copy on every screen that lists it, and answers from a different Bot. Somebody debugging why the copy behaves unlike its original will not find the reason on the copy's own connection screen, because that screen has stopped mentioning the endpoint.

What a fix probably has to do

Inherit the source's endpoint, and treat the managed address as the fallback it already is in create(): use it only when the source had no endpoint of its own. The availability check then belongs after the source is read, since a source with its own endpoint needs nothing to fall back to.

The endpoint alone, and not the rest of the configuration. A stored key is a reference into the vault, so copying the configuration wholesale would leave two coworkers sharing one credential, where rotating either one's key silently changes the other's. A copy should start with no key.

One consequence worth naming rather than folding in: avatarSeed is copied from the source and is covered by an existing test, so two coworkers already render the same avatar. Making copies genuinely identical in every other respect makes that easier to hit. Worth deciding separately.

Severity

Not urgent, and nothing is lost or corrupted. It is wrong quietly rather than loudly, which is the part that costs time: the copy is misconfigured from birth and the screen that would say so is the screen the misconfiguration silences.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions