Skip to content

Require a token on every call from the server to a Bot - #52

Merged
davidmckayv merged 3 commits into
CopilotKit:mainfrom
zopeVaibhav:fix/agent-bot-auth-token
Aug 21, 2026
Merged

Require a token on every call from the server to a Bot#52
davidmckayv merged 3 commits into
CopilotKit:mainfrom
zopeVaibhav:fix/agent-bot-auth-token

Conversation

@zopeVaibhav

Copy link
Copy Markdown
Contributor

The problem

agent-bot and agent-langgraph serve POST /ag-ui without any authentication. Any process that can reach the Bot's port issues an AG-UI run against the deployment's model credential; docker-compose.yml:162 binds ${BOT_PORT:-4200}:4200 on 0.0.0.0, so on the shipped configuration that is any process on the host's network and any container on the compose network. MANAGED_AGENT_TOKEN is not read anywhere in either Bot, and server/src/agents/runtime-agents.ts attaches no authentication header to its own calls, so the boundary is absent on both sides rather than misconfigured. agent-computer and supervisor both require a shared secret at boot and validate it on every request; the two Bots did not.

This is the opposite direction to #34, which added per-agent callback tokens for the Bot→server path. That change does not cover requests from the server into a Bot's /ag-ui, which is what this PR closes.

Closes #50.

The approach

A single header, x-openbot-agent-token, on every call from the server to a managed Bot. A new shared/agent-authorisation.ts holds a constant-time matchesToken and a hasManagedAgentToken request helper, kept in shared/ so the two Bots and the server all read the same rule rather than three subtly different ones. Both Bots refuse to start when MANAGED_AGENT_TOKEN is unset, no silent-open dev fallback because the process holds a model credential and an unauthenticated port on it is worth failing loudly for, and reject /ag-ui with 401 when the header is missing or wrong. GET /health stays open so an orchestrator can check readiness without holding the token.

The server side matches. server/src/config.ts requires MANAGED_AGENT_TOKEN at boot alongside the other secrets. server/src/agents/runtime-agents.ts attaches the header only on the agent whose endpoint equals config.managedAgentAgUiUrl, so customer-owned AG-UI endpoints never see it and a token intended for the built-in Bot is not leaked to a third party's server on the first customer registration. docker-compose.yml passes MANAGED_AGENT_TOKEN through to both Bots; .env.example, README.md, and docs/configuration.md document the new required variable.

What is not covered

  • Only the wire between the server and the managed Bots. Customer-registered AG-UI endpoints authenticate through their own write-only header, unchanged. This PR does not touch that path.
  • Not a rotation mechanism. Changing the token is a redeploy of the server plus a compose recreate of the two Bots. Fine at this scale, not for a mid-flight rotation.
  • agent-computer and supervisor were already token-guarded and are unchanged.
  • The header name is not part of AG-UI. It is an OpenBot-specific header; anyone forking a Bot needs to add the check.

Merge notes

  • New required env var: MANAGED_AGENT_TOKEN. Existing deployments that upgrade without setting it get a loud refuse-to-start on both the server and each Bot rather than a silent regression.
  • Config type gains a required managedAgentToken: string; loadConfig test fixtures are updated.
  • createRuntimeAgentLoader gains an optional trailing managedAgent parameter; existing callers that pass undefined keep their old behaviour but issue no header, so a self-hosted deployment that runs a custom Bot in place of the managed one needs to opt in.

Verification

All gates from a clean tree at c81d9b1.

  • bun run format:check: no fixes

  • bun run lint: 25 pre-existing noTemplateCurlyInString warnings, none new

  • bun run typecheck: app, server, worker all exit 0

  • bun run test: 686 pass, 5 skip, 0 fail across 79 files, 1701 assertions

  • bun run build: exit 0

  • Live against a rebuilt openbot-agent-bot container (docker compose up -d --build --force-recreate agent-bot):

    request result
    POST /ag-ui, no headers 401 Unauthorized.
    POST /ag-ui, x-openbot-agent-token: wrong 401
    POST /ag-ui, x-openbot-agent-token: $MANAGED_AGENT_TOKEN 200, RUN_STARTED
    GET /health 200 (unchanged)

New tests: shared/agent-authorisation.test.ts (2). Existing tests updated: server/tests/config.test.ts (required-token assertion), server/tests/runtime-agents.integration.test.ts (managed endpoint receives header, customer endpoint does not).

@davidmckayv

Copy link
Copy Markdown
Contributor

Reviewed and driven in a browser. The approach is right and the details that matter are correct: constant-time comparison rather than ===, both Bots refusing to start without the token, and — the subtle one — the token sent only when the endpoint matches MANAGED_AGENT_AG_UI_URL, so a customer-registered agent never receives the deployment's secret.

Checked it against current practice too. mTLS/SPIFFE is the 2026 recommendation for service-to-service, but SPIRE is an optional add-on here for the supervisor and computers and appears nowhere in server/src. A shared secret validated per request is exactly what agent-computer and supervisor already do, so this makes the two Bots consistent with the rest of the deployment rather than introducing a second pattern.

I pushed two commits on top rather than asking you to, since a release is going out.

The quick start could not start. MANAGED_AGENT_TOKEN is required() and ships empty in .env.example, so cp .env.example .env && bash scripts/start.sh gave MANAGED_AGENT_TOKEN must be configured. start.sh now generates one and writes it back to .env. Generated rather than defaulted to a fixed string like its two neighbours, because those reach services on loopback and a well-known token from a public repository would be no boundary at all.

The ports stayed on every interface, which your description names as the reason this mattered. agent-computer was already on 127.0.0.1, so the Bots were the exception. They are bound now, so somebody has to be on the machine before the token is even what stops them.

Verified rather than assumed: from an empty token the script writes 44 characters and one line; the Bots then publish on 127.0.0.1 only, answer 401 with no token and with a wrong one, 200 with the right one, and a Bot replies normally through the product in the browser. Before this, that first case was a 200 spending the deployment's model credential.

Thank you for finding this one.

zopeVaibhav and others added 3 commits August 20, 2026 20:44
Two things the token boundary needs to be usable and to be worth having.

`MANAGED_AGENT_TOKEN` is required and ships empty in `.env.example`, so a fresh
clone doing `cp .env.example .env && bash scripts/start.sh` refused to start
with "MANAGED_AGENT_TOKEN must be configured". That is step four of the quick
start, and the first thing a stranger does. `start.sh` already supplies the two
neighbouring secrets; this one is generated and written back to `.env` rather
than defaulted to a fixed string, because a well-known token from a public
repository is no boundary at all, and because the server and the Bot are
separate processes that have to agree on it across restarts.

The Bot ports were published on every interface, which this PR's own
description names as the reason the hole mattered. `agent-computer` was already
bound to loopback, so the two Bots were the exception rather than the rule.
Binding them means somebody has to be on the machine before the token is even
the thing standing in their way.

Driven rather than reasoned about: from an empty token, the script generates 44
characters and writes one line back; the Bots then publish on 127.0.0.1 only,
answer 401 with no token and with a wrong one, and 200 with the right one.
`MANAGED_AGENT_TOKEN` is required, so the container refuses to start without it
and the check waited 150 seconds for an answer that was never coming.

The job could not have been updated in the original commit: this branch predates
the check, and the workflow that ran came from the merge commit rather than from
here. Rebasing onto main brings it into view.

Reproduced with the job's own command: answers on /api/capabilities in four
seconds, nothing respawning after fifteen.
@davidmckayv
davidmckayv force-pushed the fix/agent-bot-auth-token branch from 141f591 to e6e3ccb Compare August 21, 2026 03:48
@davidmckayv
davidmckayv merged commit d90ff63 into CopilotKit:main Aug 21, 2026
7 checks passed
@zopeVaibhav

Copy link
Copy Markdown
Contributor Author

Thanks for the review and the two follow-up commits.

Generating the token rather than defaulting to a fixed string is the right call. I had considered a placeholder for start.sh and did not settle on one, and it did not click that its two loopback neighbours already generate theirs. Now all three follow the same pattern. Binding the Bots to 127.0.0.1 is the better half of that story: the token stops being the only thing standing between the port and the network.

The SPIFFE/mTLS note is useful context. If the SPIRE work later reaches into server/src, this shared-secret path is a small surface to migrate.

Glad it landed before the release.

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.

agent-bot and agent-langgraph accept unauthenticated /ag-ui runs

2 participants