github CopilotKit/OpenBot v0.0.14

2 hours ago

A tool cannot be granted for an app this deployment has not added

Granting a Bot a connector's tool checked only that the person asking was an administrator, so a
grant naming an app that was never added was stored and then invisible — the page that reports a
grant nothing advertises is built from the connector's own row, and there was none. Adding that app
later put every such grant straight onto its Bots, with nobody having granted anything and nothing in
the trail saying so. The grant is now refused, naming the app. Taking a grant away is unaffected, so
a dead row an administrator can see is still one they can remove, and a tool a connector has stopped
advertising can still be granted: what a vendor lists today is not what somebody decided yesterday.

The LiteLLM Bots keep the chosen provider on a model name that contains a slash

A model name the endpoint publishes with a slash in it — qwen/qwen3-8b, or the docs example
openai/gpt-5.6-terra — was treated as already carrying a provider. LiteLLM then took the first
half as the provider and sent only the rest, so a compatibility endpoint either failed with
LLM Provider NOT provided or received gpt-5.6-terra instead of the namespaced name. The ADK,
Strands, Agno, LlamaIndex and CrewAI Bots now keep the chosen provider in front, and the whole
model name reaches the endpoint.

A Google Drive shortcut is read as the file it points at

Search and recent files return shortcuts as ordinary hits, and reading one by that id was refused
as a binary application/vnd.google-apps.shortcut. A document somebody had starred or filed as a
shortcut — the usual way a shared drive file is kept at hand — could be named and not opened. The
connector now follows the target once and reads that file the same way it would have if search had
returned it directly. A shortcut that names nothing, or another shortcut, is still declined.

An MCP tool that answers in structuredContent is no longer read as empty

A tool that declares an output schema often puts the answer in structuredContent and leaves the
content list empty. That empty list was reported as "nothing was found", so the model filled the
gap from memory while the vendor had answered. The structured object is now read when the content
list had nothing to say. A tool that already sent text is unchanged.
The slash no longer names the provider on those five Bots, so a BOT_MODEL that relied on it to
reach somewhere other than BOT_PROVIDER is now read as part of the model name: bedrock/…,
azure/… and openrouter/… reach the provider BOT_PROVIDER names rather than the one written in
front of the slash. BOT_PROVIDER is the setting for that, and a model name written bare beside it
behaves as it did. The framework Bot agent-langgraph and the Langroid Bot are unchanged, because
neither read the slash that way.

A Bot's computer no longer runs as root on Kubernetes

The image already builds pwuser, chowns /workspace, /profiles and /app to it, and the
all-in-one image drops to it through s6. A computer pod overrides the command to run the browser
process alone, so it never reached s6 and ran as uid 0. It now runs as pwuser in both the modes
this chart runs a computer in, shared and sandbox, with HOME and BUN_INSTALL set the way s6
sets them. BUN_INSTALL moves because its default is root-owned, and bun add in a Bot's shell
would otherwise stop working the moment the pod stopped being root.

Kubernetes only. The Compose and supervisor paths run agent-computer/Dockerfile, which builds
no such user and is still uid 0. That is the rest of the residual #261 named and is not this change.

What it buys, and what it does not. It is not a containment boundary against a Bot's own shell:
the image grants pwuser passwordless sudo for apt-get/apt/dpkg, which escalate to root by design,
and gVisor plus a computer per Bot remain the actual boundary. Nor does it change which Pod
Security Standard the pod meets: runAsNonRoot is a restricted control, not a baseline one,
and restricted also wants allowPrivilegeEscalation: false, capabilities.drop: [ALL] and a
seccomp profile, which are deliberately not set here for the sudo reason above. The pod met
baseline before this and still does not meet restricted after it. What it does buy is the org
policies and admission rules that reject uid 0 outright, that root-owned data stops accumulating
in the volumes, and that a bug yielding a constrained primitive lands as 1001.

Before upgrading an existing release, two things. The kubelet applies fsGroup only where the
volume plugin says it can: the EBS, PD and Azure Disk CSI drivers do, hostPath does not, and
hostPath is what rancher/local-path-provisioner hands out by default, which is the default
StorageClass on k3s. On storage that cannot, the computer now refuses to start and says so, rather
than coming up healthy with a browser profile Chromium silently replaced, which is what it did
before this release. Chown the directory, or set computers.podSecurityContext: null. And an
upgrade run with helm upgrade --reuse-values does not pick up a new key at all, so it keeps
running as root and says nothing; pass the value or drop the flag.

In computers.mode: sandbox, existing Bots keep their old computer. The server copies the pod
template into a Sandbox when it creates one and never updates it, so only Bots created after the
upgrade run as 1001. The server does restart, because the template checksum changes, which makes it
look like the change landed everywhere. The only lever today is reset, which deletes that Bot's
volumes and its logins.

fsGroupChangePolicy: OnRootMismatch is set deliberately. Unset means Always, which walks every
file on every mount; a real Chromium profile is tens of thousands of small ones, and in sandbox
mode that pass would run again on every resume from idle.

Which email domains may sign in, decided by the deployment rather than the provider

Nothing in OpenBot filtered who could sign in. INITIAL_ADMIN_EMAILS decides who is an
administrator once they are in, which is a different question. The providers do not answer it
either: MICROSOFT_OAUTH_TENANT_ID defaults to common, which is any Microsoft account including
personal ones, and Okta has no equivalent setting. So a deployment reachable from the internet
admitted anybody who could complete the flow, as a non-administrator with access to its Bots.

SIGNIN_ALLOWED_EMAIL_DOMAINS, or config.allowedEmailDomains on the chart, names the domains
admitted. It is checked against the address the provider returns, in the same two hooks the removal
deny list uses, so it covers a first sign-in and an account that already exists. A refusal writes a
session.refused audit row naming the reason. Empty means no opinion, so nothing changes for a
deployment that does not set it.

Matching is exact with no wildcards, for the reason AGENT_ENDPOINT_ALLOWED_HOSTS gives:
example.com admits neither sub.example.com nor evil-example.com. Both sides go through the
same IDNA normalisation, so a list may be written @Example.COM. or in punycode and still mean
what it says.

It is a filter, not a boundary, and the deployment is told so. OpenBot does not require a
verified address, and Entra's email claim comes from a directory attribute that the signing-in
tenant's own administrator writes. Two consequences, both now enforced at start-up rather than
documented and hoped for:

  • Naming domains while MICROSOFT_OAUTH_TENANT_ID names no directory is refused. That is
    common, and equally organizations, which Microsoft describes as admitting any work or school
    account in any directory, and consumers. Anybody can create a tenant and write your domain into
    their own user, so the list would refuse the honest and admit the rest while reading as a
    control. Set your directory GUID.
  • A list that names nothing, which SIGNIN_ALLOWED_EMAIL_DOMAINS=@ and a stray . both produce, is
    refused. It is a non-empty list no address can match, and left to run it turns every visitor
    away with nothing said at boot.

A deployment that names no domains and leaves the tenant multi-tenant warns instead of refusing,
because genuinely multi-tenant is a real deployment.

No sign-in cannot be combined with a public address

OPENBOT_SINGLE_USER=true admits every request as one administrator. The flag is how somebody says
they meant that, and it still is. What was missing is the second question: one administrator and no
sign-in is a thing you run where only you can reach it, and nothing checked the address.

It is now refused alongside an OPENBOT_PUBLIC_URL, OPENBOT_APP_URL or TRUSTED_ORIGINS entry
that the public internet routes to, and the refusal names the address it objected to. This is the
rule the chart already applies twice, in validation.yaml, moved to where a deployment that never
goes near Helm is asked it too: .env.example ships the flag on so that a clone runs, and README's
"Deploy it" hands that same .env to docker run, where the only thing separating a laptop from a
server is an address.

Public, not "not loopback", and the difference is most of the deployments this flag has. A home
server at 192.168.1.10, a Tailnet address, a VPN address, openbot.local, a bare hostname with
no dot in it: none of those is loopback and none of them is a stranger's to reach. Refusing them
would refuse the feature's own audience, and the only way out would be to turn off the flag that
describes what the operator is doing. Those run, and warn once at boot that anybody on that network
is the administrator. Loopback is silent. A value that cannot be parsed as a URL counts as public,
because a value nobody could read is not one anybody checked.

Nothing on loopback changes, so the local workflow the flag exists for is untouched, and neither is
the chart's config.singleUser trial mode, which sets no public address. A deployment naming an
external authority through OPENBOT_ORGANIZATION_AUTH_URL is unaffected too: the authority wins
before this is consulted. Explicitly NOT gated on NODE_ENV: the image and the chart both set it
to production for every deployment including that trial, so it says nothing about who can reach
a deployment.

docs/architecture.md, docs/deployment.md and docs/configuration.md all described the old rule
and now describe this one.

The EKS cluster recipe puts the node's IAM role out of a Bot's reach

A Bot has a shell, and a shell reaches whatever its pod reaches, including the instance metadata
service at 169.254.169.254 that hands out the node's IAM role. The NetworkPolicy excepts that
address, but it is off by default and does nothing on EKS until the VPC CNI is told to enforce it,
so the cluster config in the chart README now sets disableIMDSv1 and disablePodIMDS on the node
group, which closes it at the node whatever the CNI is doing.

disablePodIMDS is the line that does the work: it sets the hop limit to 1, and that governs the
IMDSv2 token PUT, so a pod one hop further out than the node cannot get a token. disableIMDSv1 is
written beside it to say so out loud rather than to change anything, because eksctl defaults it to
true and sets httpTokens: required for either flag. Pods on the cluster network are covered; one
running with hostNetwork: true is on the node and is not.

It costs something, and the recipe says so. The EBS CSI driver reads instance metadata from
IMDS, and its own documentation asks for a hop limit of 2 or greater in a containerized environment.
A hop limit of 1 also rules out MutableCSINodeAllocatableCount, which requires IMDS to be the
driver's metadata source. Take these two lines out if you need that.

Nothing in OpenBot wants pod-level IMDS: the chart reaches AWS through IRSA. Documentation only; no
chart template changed, and an existing cluster is unaffected until its node group is recreated.

A large text file says so on the composer before it is sent

The model reads the first 120,000 characters of an attached text file and the rest is dropped. The
part said so to the model and nothing said so to the person attaching it, so a 1MB CSV was answered
from roughly a tenth of itself with nothing on screen to explain the answer. A staged file over that
ceiling now carries "may be cut" beside its size, with the exact number on hover. Warned, never
refused: the whole file is still uploaded and still stored.

Whether a file is text is decided by what the server read in its bytes, not by what the browser
called it, so a file the browser labelled an image and the server read as text is warned about too.
That is the file the old reading would have missed.

ADK and Langroid Bots can call the tools OpenBot supplies

Both answered text prompts on an OpenAI key and neither could reliably use the tools the deployment
supplies them, so a Bot on either harness could talk but could not act. ADK now registers the
documented AGUIToolset. Langroid now sends each run's tool schemas and its complete message
history, tool results included, so a tool's answer reaches the model that asked for it. Each run
keeps its own tool and history state.

Built-in Bots carry a model provider's sign-in failure through as itself

A built-in or LangGraph Bot whose model credential had expired ended the run as a generic failure,
which reads as the Bot being broken. The run now ends with OPENBOT_MODEL_AUTH_REQUIRED and says to
sign in to the model provider again.

The built-in Bot's tool loop also moved to the server, which owns the grants that decide what a tool
may do. The harness is given model input alone, and a tool result is resolved against the call that
produced it, so a Bot can carry a multi-step task across several tool calls rather than losing the
thread after the first.

A deployment can name an organization authority that decides who it admits

OPENBOT_ORGANIZATION_AUTH_URL points at an OpenBot deployment configured with Google, Microsoft or
Okta, which verifies who somebody is and what roles they currently hold. It is separate from any
Intelligence connection, and the authority keeps its own secrets.

Naming one settles the sign-in question by itself: it wins over OPENBOT_SINGLE_USER, so a
deployment carrying a leftover single-user flag admits verified people rather than admitting
everybody as one administrator.

A coworker can be a file of its own

The example package declared every coworker in one agents.yaml, so adding one meant editing a file
somebody else was editing too, and handing somebody a coworker meant handing them a fragment to
paste into the middle of theirs. A package may now also keep a coworker per file in an agents/
directory beside agents.yaml, and both are read. A package that keeps everything in agents.yaml
loads exactly as before. A file holds the coworker on its own or a list under agents:, only
.yaml and .yml are read, and files are read in filename order. Two declarations of the same id
stop the server and both files are named, rather than one quietly winning on the order a directory
was listed in. The directory is in the package checksum, so a coworker added or edited there is a
package change a running deployment notices.

Ten more example coworkers, each doing one job

The example package shipped three coworkers, which is enough to prove the format and not enough to
give anybody ideas, and writing a role_description cold is the part that decides whether a
coworker answers usefully or vaguely. Ten more ship in examples/fintech/agents/, one file each:
reading an expense claim against the policy as written, turning a meeting note into the follow-ups
actually in it, drafting release notes from what shipped, triaging a support ticket, answering a new
starter from the handbook, writing a brief that names what it could not find, writing up an
interview with question, answer and observation kept apart, handing an on-call shift over from the
record, assembling what is known before a renewal decision, and grouping customer feedback into
themes it can cite. Each says what the job is, what the coworker must not do, and what to say when
it cannot find something. They grant nothing: a coworker names skills, a skill names tools, and what
it may call is what an administrator has granted. Delete the ones you do not want.

Built-in and Mastra Bots can run on Anthropic API keys

The example built-in Bots and the Mastra Bot now use the selected model provider instead of
assuming OpenAI. A deployment with BOT_PROVIDER=anthropic, BOT_MODEL=claude-sonnet-4-5 and an
Anthropic key routes built-in model calls, tool selection and Mastra runs through Anthropic's native
API. Source startup no longer waits for the unused OpenAI-only sample when Anthropic is selected.

The Agno Bot answers on an OpenAI key

Picked with an OpenAI key, the Agno Bot failed every run before reaching OpenAI. Agno sends a
temperature and a top_p with each request, and LiteLLM refuses both for gpt-5.5, the model
Compose passes when the setup screen names none, so the run ended in UnsupportedParamsError. A
parameter the model does not take is now dropped instead, the way the LlamaIndex Bot already does
it. The Anthropic key and an OpenAI-compatible endpoint behave as before.

The Pydantic AI Bot starts on an Ollama model named with its tag

Ollama names every model with a tag after a colon, as in llama3.1:8b, and that is the name the
setup screen passes on for an OpenAI-compatible endpoint. The Pydantic AI Bot took any colon in the
model's name to mean the name already carried a provider, so Pydantic AI read llama3.1 as one,
refused it as unknown, and the Bot never started. A model's name now carries a provider only when it
begins with the chosen provider's own, as in anthropic:claude-sonnet-4-5.

The Langroid Bot starts on an Anthropic key

Picked with an Anthropic key, the Langroid Bot exited on startup asking for an OpenAI key. It names
a model from any provider but OpenAI through litellm, which its image did not install, and Langroid
builds an OpenAI client for such a model all the same, from the OPENAI_API_KEY Compose writes
empty when the choice was not OpenAI. The image now installs Langroid's litellm extra and an empty
OpenAI key is treated as none, so the Bot starts and answers with the Anthropic model chosen.

The AG2 Bot answers on an Anthropic key

The AG2 Bot built an OpenAI client whatever the setup screen chose, and read BOT_MODEL but never
BOT_PROVIDER. Picked with an Anthropic key, every run failed asking for an OpenAI key, because
Compose writes that one empty when the choice was Anthropic. It now reaches Anthropic through AG2's
own Anthropic client when that is the provider chosen, and OpenAI or an OpenAI-compatible endpoint
as before otherwise.

The Microsoft Agent Framework Bot starts on an Anthropic key

The Microsoft Agent Framework Bot built an OpenAI client whatever the setup screen chose, and read
BOT_MODEL but never BOT_PROVIDER. Picked with an Anthropic key, it exited on startup asking for
an OpenAI key, because Compose writes that one empty when the choice was Anthropic. It now reaches
Anthropic through Agent Framework's own Anthropic client when that is the provider chosen, and
OpenAI or an OpenAI-compatible endpoint as before otherwise.

Don't miss a new OpenBot release

NewReleases is sending notifications on new releases.