Skip to content

Give a duplicated coworker the endpoint it was copied from - #328

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
zopeVaibhav:fix/duplicate-repoints-the-copy-at-the-managed-bot
Sep 3, 2026
Merged

Give a duplicated coworker the endpoint it was copied from#328
davidmckayv merged 2 commits into
CopilotKit:mainfrom
zopeVaibhav:fix/duplicate-repoints-the-copy-at-the-managed-bot

Conversation

@zopeVaibhav

Copy link
Copy Markdown
Contributor

Closes #327.

What this changes

Duplicating a coworker now gives back a copy that runs where the original ran.

duplicate() wrote the managed Bot's address into every copy without reading the source, so duplicating a coworker hosted at its own AG-UI endpoint produced one answering from a different process under the same name, title and role. The copy's builtIn flipped to true as a result, and the connection section hides the endpoint entirely once that is set, so the swap was invisible on the one screen somebody would check.

The managed address is the fallback create() already applies to a coworker given no endpoint. This makes duplicate() treat it the same way rather than as a rule, which fixes three things at once:

  • A copy of a coworker with its own endpoint keeps that endpoint.
  • A copy of an endpoint-less coworker still runs on the managed Bot, as its source did.
  • Duplicate works on a deployment with no managed Bot when the source brought its own endpoint. It refused there before, telling the person to give the coworker an endpoint it already had.

The endpoint alone is inherited, never the rest of the configuration. auth is a reference into the vault, so copying it would leave two coworkers sharing one credential, where rotating either one's key silently changes the other's. A copy starts with no key and fails closed at its endpoint until somebody gives it one.

Not changed here: avatarSeed is copied from the source and is pinned by an existing test, so a copy still renders the source's avatar. That is worth deciding on its own rather than inside this change.

Where it runs

  • New state that outlives a request? None. One INSERT gains a value read from the row being copied, inside the transaction that already reads it.
  • What happens on the second replica? No change. duplicate() reads its source and writes its copy in one transaction, and the value it now writes comes from that transaction's own read rather than from process configuration. Two replicas duplicating the same source produce two copies with distinct minted ids, exactly as before.
  • Anything serialised? Nothing new. The existing transaction is unchanged, and the added read is of the row it already selected.
  • Anything fanned out to a browser? No. The agent list is refetched by the client after a create or duplicate as it was.
  • New listener, port, or schedule? None.

Boundary and audit

  • Every acting call still goes through the gateway: unchanged, no acting call is added or moved.
  • New refusals and new failures each write a row: no new refusal is introduced. ManagedAgentUnavailableError still fires, on a strictly smaller set of cases (no managed Bot and no endpoint on the source), and is mapped by the same mapStoreError path as before.
  • Nothing new is trusted from the client that the server can resolve itself: the endpoint is read from the stored source row, not accepted from the request.

Changelog

  • Added under Unreleased.

Proof

Five tests added to server/tests/agent-profile-store.integration.test.ts, which exercises the real store against Postgres.

Run against the unfixed code first, two fail and three pass. The two that fail are the proof; the three that pass pin behaviour this change must not break.

(fail) copies the source's own endpoint rather than repointing the copy at the managed Bot
  Expected: "https://hosted.example.test/ag-ui"
  Received: "https://managed.example.test/ag-ui"

(fail) duplicates a coworker with its own endpoint on a deployment with no managed Bot
  ManagedAgentUnavailableError: This deployment has no managed Bot. Give the coworker its own AG-UI endpoint.

 18 pass, 2 fail

After the change:

 20 pass, 0 fail

The three that passed both before and after:

  • an endpoint-less source still gets the managed Bot,
  • the source's stored key is not carried onto the copy (asserted on the copy's hasAuth and on authFromConfiguration over its stored row),
  • duplicate still refuses when there is no managed Bot and no endpoint on the source to inherit.

Verified end to end against a running server as well, not only in tests. A coworker created at http://localhost:4201/ag-ui duplicates to a copy at http://localhost:4201/ag-ui with builtIn: false; an endpoint-less coworker duplicates to the managed address with builtIn: true; and a second server started with both MANAGED_AGENT_* variables unset returns 201 for the first case where it returned 400 before.

Full server suite run with the local dev server and worker stopped, since they contend on the same database and make the lease tests flap. The set of failing tests is identical with and without this change (six, all pre-existing, in routine-sweep.integration.test.ts and agent-handoff-*.integration.test.ts); compared by test name rather than by count. typecheck, lint and format are clean.

@davidmckayv davidmckayv 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.

Validated locally + CI verify green; CHANGELOG-only rebase.

@davidmckayv
davidmckayv merged commit d00f65c into CopilotKit:main Sep 3, 2026
davidmckayv added a commit that referenced this pull request Sep 4, 2026
…355)

#328 fixed the half of duplicate that a coworker with its own endpoint
saw. This is the other half.

A coworker that runs on this deployment's own Bot has no endpoint. Its
configuration is `{ systemPrompt }`, and that prompt is the whole of what
makes it that coworker. Duplicate rebuilt every copy from `source.endpoint`
alone and wrote `type: "remote_ag_ui"` flat, so for one of these the
endpoint read came back null, the copy fell through to the managed Bot,
and the prompt was dropped on the floor.

What comes back is the failure this repository already has a paragraph
about. The copy looks identical on every screen and its entire instruction
becomes standingRoleMessage - see the note above that function in
copilot.ts, which names the compliance Bot that answered a filing question
with invented thresholds because one sentence of role description was all
that reached it. The default tenant package ships two built_in coworkers,
General Assistant and Knowledge, and Knowledge's prompt is a careful
do-not-fabricate instruction with the reasoning written out beside it in
the package. Duplicate it and you get a coworker with the name, the title,
the avatar, and none of that.

The type is carried now, not only the configuration. A copy written as
remote_ag_ui also could not be granted handoff for the rest of its life:
agentRunsHere and botsReachableFrom both key on agents.type == "built_in",
so the original could hand work on and its copy silently could not.

And a built_in source no longer needs a managed Bot to fall back to. On a
deployment with none, copying one used to be refused with advice to give
the coworker an AG-UI endpoint it was never supposed to have - the same
wrong refusal #328 removed for a coworker that brought its own endpoint.

The decision is a pure function so it can be tested without a database.
The stored row is read inside the transaction and after the access check,
rather than widening AgentProfile: the DTO every surface receives should
not start carrying a Bot's instructions. `auth` is still deliberately not
copied.

Co-authored-by: kevin9327 <kevin9327@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: David McKay <david@copilotkit.ai>
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.

Duplicating a coworker gives back a copy that runs somewhere else

2 participants