Skip to content

Give a duplicated Bot in the box the instructions it was copied from - #355

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:duplicate-a-built-in-coworker
Sep 4, 2026
Merged

davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:duplicate-a-built-in-coworker

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

#328 fixed the half of duplicate that a coworker with its own endpoint saw. This is the other half,
and it is the worse one.

A coworker that runs on this deployment's own Bot has no endpoint. Its agents.configuration is
{ systemPrompt } (server/src/tenant-package.ts:374-385), and that prompt is the whole of what
makes it that coworker. duplicate rebuilt the copy from source.endpoint alone and wrote
type: "remote_ag_ui" flat:

const configuration = source.endpoint ? { endpoint: source.endpoint } : managedConfiguration;
...
await transaction.insert(agents).values({ id: duplicateId, name: source.name, type: "remote_ag_ui", configuration });

endpointOf returns null for a built_in row, so the copy fell through to the managed Bot and the
prompt went nowhere.

What comes back is a failure this repository already has a paragraph about. The copy is
remote_ag_ui, so registeredAgentFromRow takes the remote branch and its entire instruction
becomes standingRoleMessage — whose own comment in server/src/copilot.ts:113-119 says exactly
what that costs:

for a remote Bot the standing role is the only instruction there is: role_description is one
sentence somebody wrote about what it is for, and nothing else reaches it. The compliance Bot that
answered a filing question with thresholds and deadlines and no source was a remote-ag-ui agent
whose entire prompt was "Investigate policies, transaction monitoring, and control evidence."

This is not a corner of the product. The default tenant package — examples/fintech, the value
TENANT_PACKAGE_DIR falls back to — ships two built_in coworkers, General Assistant and
Knowledge. Both are visibility: public, so every signed-in person can see them, and Duplicate is
one item in the coworker dialog #317 just added. Knowledge's prompt is a careful do-not-fabricate
instruction, with the reasoning for every clause 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 — and no screen says
so, which is the same "looked identical on every screen and answered from a different process"
#328's changelog describes.

Two more consequences of the type flip, both silent:

  • The copy can never be granted handoff. agentRunsHere (plugins/store.ts:2195-2204) and
    botsReachableFrom (:2360-2372) both key on agents.type == "built_in", so the Handoff section
    reports grantable: false on the copy while the original could hand work on.
  • On a deployment with no managed Bot, duplicating a built_in coworker was refused with
    This deployment has no managed Bot. Give the coworker its own AG-UI endpoint. — advice about an
    endpoint it was never supposed to have. That is the same wrong refusal Give a duplicated coworker the endpoint it was copied from #328 removed for a coworker
    that brought its own endpoint.

The fix. runForDuplicate decides what the copy runs on from what the original ran on: a
built_in source with a prompt stays a built_in with that prompt; a source with an endpoint keeps
that endpoint; only a source with neither falls back to the managed Bot, and null — the case that
raises ManagedAgentUnavailableError — is now reachable only when there is genuinely nothing to run
the copy on.

It is a pure function so it can be tested without a database. The stored row is read inside the same
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 —
two coworkers sharing one vault reference would mean rotating either one's key silently changed the
other's.

Where it runs

  • New state that outlives a request? None. The copy's type and configuration are columns
    on agents, as they already were.
  • What happens on the second replica? Identical. The decision is a pure function of the row
    being copied and the deployment's managed-agent configuration, both of which every replica
    reads the same way.
  • Anything serialised? The extra read is select type, configuration from agents where id = ?
    inside the existing database.transaction, on the same connection, after
    findAccessibleProfile has already taken its read of that agent in the same transaction. It
    cannot see a row the caller may not see, and it cannot see a different row from the profile
    read beside it.
  • Anything fanned out to a browser? No. The route's response shape is unchanged.
  • New listener, port, or schedule? None.

Boundary and audit

  • Every acting call still goes through the gateway: no acting path is touched.
  • New refusals and new failures each write a row: no new refusal. The bot.duplicated row the
    route already writes is unchanged. One refusal becomes less frequent — a built_in source no
    longer raises ManagedAgentUnavailableError on a deployment with no managed Bot — and that
    refusal was never audited on either side of this change.
  • Nothing new is trusted from the client. The type and the prompt come from the stored row, never
    from the request.

Proof

server/tests/duplicate-run.test.ts — 8 tests, no database needed. Fail-before confirmed by
replacing runForDuplicate's body with the previous endpoint-only selection:

error: expect(received).toEqual(expected)

@@ -2,5 +2,5 @@
    "configuration": {
-     "systemPrompt": "Never answer from memory.",
+     "endpoint": "http://managed.invalid/ag-ui",
    },
-   "type": "built_in",
+   "type": "remote_ag_ui",
  }

(fail) what a copy runs on > keeps a Bot-in-the-box's prompt, and stays a Bot in the box
(fail) what a copy runs on > needs no managed Bot to copy one that brought its own prompt

 6 pass
 2 fail

With the fix:

$ bun test server/tests/duplicate-run.test.ts
 8 pass  0 fail

$ bun test server/tests/agent-routes.test.ts server/tests/bot-lifecycle-audit.test.ts
 65 pass  0 fail  118 expect() calls

Gates:

$ bun run format:check   Checked 516 files. No fixes applied.
$ bun run lint           Checked 519 files. No fixes applied.
$ bun run typecheck      app / server / worker: Exited with code 0

server/tests/agent-profile-store.integration.test.ts — where #328's four duplicate assertions live
— needs a live Postgres and fails here with Connection closed, so it was read and not run. It is
the right place for an end-to-end assertion that the copied agents row keeps type: "built_in" and
its systemPrompt; say the word and I will add one there too.

Changelog

  • Duplicating a Bot in the box keeps its instructions, under Unreleased.

CopilotKit#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 CopilotKit#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: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

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

Reviewed diff against current main; resolved CHANGELOG keep-both; validated locally (format + composed test run). CI green.

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.

2 participants