Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9ed2854
Take out the Drive connector that answered as the deployment
guidovizoso Aug 20, 2026
868df40
Make a server say whose credential reaches it, and add Google Drive
guidovizoso Aug 20, 2026
9e40149
Say when a tool returned nothing, instead of returning nothing
guidovizoso Aug 20, 2026
0ee45d4
Give one person one credential per server
guidovizoso Aug 20, 2026
0f8e633
Call a user-oauth server as the person asking, or not at all
guidovizoso Aug 20, 2026
3a784f9
Let a person connect their own account, and believe only what we signed
guidovizoso Aug 20, 2026
7dcdf5e
Give the two halves of a connector the two screens they belong on
guidovizoso Aug 20, 2026
87ef07c
Document the one setting a connected account needs
guidovizoso Aug 20, 2026
586ed58
Send somebody back to the app after consenting, not to the API
guidovizoso Aug 20, 2026
bce8761
Merge remote-tracking branch 'origin/main' into guido/knowledge
guidovizoso Aug 20, 2026
61e7ca5
Merge remote-tracking branch 'origin/main' into guido/knowledge
guidovizoso Aug 20, 2026
53571eb
Merge remote-tracking branch 'origin/main' into guido/knowledge
guidovizoso Aug 21, 2026
02a4531
Give every plugin its own page, and two lists instead of three tabs
guidovizoso Aug 21, 2026
6924915
Give each row's icon a tile to sit in
guidovizoso Aug 21, 2026
6a64f9c
Leave one connector in the catalogue, and drop the tile's border
guidovizoso Aug 21, 2026
f53b2be
Make enabling a connector a switch, and keep the tile for third parties
guidovizoso Aug 21, 2026
6594954
Separate the plugin page's sections, and drop its icons
guidovizoso Aug 21, 2026
cbfa301
Move Refresh tools onto the Tools heading
guidovizoso Aug 21, 2026
04000fa
Give connected accounts its own screen, and a page per service
guidovizoso Aug 21, 2026
36600a4
Stop the credential test from eating the deployment's OAuth client
guidovizoso Aug 21, 2026
f67f6ed
Connect with a button that admits it leaves, disconnect from a menu
guidovizoso Aug 21, 2026
9fe4013
Return from consent to the account, not to a list
guidovizoso Aug 21, 2026
13646d7
Say which status a vendor refused with, and stop the suites eating re…
guidovizoso Aug 21, 2026
e7fa826
Keep a 403's reason, and write down what it took to connect Drive
guidovizoso Aug 21, 2026
b197701
Stop the tests deleting real grants, and stop the trail claiming success
guidovizoso Aug 21, 2026
518f80c
Point the Drive troubleshooting at the audit trail
guidovizoso Aug 21, 2026
7b54511
Record the vendor's sentence on a failed tool result, and name the pr…
guidovizoso Aug 21, 2026
d2fcbce
Reach Drive over its GA REST API, behind a swappable transport
guidovizoso Aug 21, 2026
6d0e2f5
Stop Knowledge denying access to files a tool just handed it
guidovizoso Aug 21, 2026
36c4098
Let an administrator set Drive up without leaving the admin screen
guidovizoso Aug 21, 2026
ae56cde
Give each tool its own screen, with a switch per Bot
guidovizoso Aug 21, 2026
7c53587
wip: pre-merge checkpoint
guidovizoso Aug 21, 2026
121d50f
Merge origin/main into guido/knowledge
guidovizoso Aug 21, 2026
5566c70
Merge remote-tracking branch 'origin/main' into guido/knowledge
guidovizoso Aug 21, 2026
5e47b30
Say what this changes for somebody running OpenBot
guidovizoso Aug 21, 2026
ca2445d
Retire a person's connector credentials when they are removed
guidovizoso Aug 21, 2026
ea8d5c2
Restore the client pointer whether or not the suite borrowed a server
guidovizoso Aug 21, 2026
2ea660d
Add the offboarding change and the credential-pointer migration to th…
guidovizoso Aug 21, 2026
25b6bca
Say what the Drive connector needs before it works, and link the doc
guidovizoso Aug 21, 2026
d8adf8f
Merge origin/main into the Drive connector
davidmckayv Aug 21, 2026
9bd0b60
Merge origin/main into the Drive connector
davidmckayv Aug 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .claude/skills/openbot-screen-layout/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ here yet" is a fact, and the section heading already said what the section is fo
4. Give each section one `PageRows` card. Rows go inside it as `Item size="sm"`, with `<Separator />`
between them and none after the last. `PageRows` is a card with dividers, not a stack of cards —
gaps between rows are the wrong shape.
5. Read `admin/connectors.tsx` for the whole pattern end to end, and
`admin/components/$name.tsx` for a screen with two sections and mixed row kinds.
5. Read `admin/plugins/index.tsx` for the whole pattern end to end, and
`admin/plugins/$key.tsx` or `admin/components/$name.tsx` for a screen with several sections and
mixed row kinds.

### Procedure 2: Compose a row

Expand Down Expand Up @@ -205,7 +206,7 @@ to sit a centred element visibly off centre or clip a card's corners against its
- **A row's summary does not change after its dialog edits something**: the summary was captured into
state instead of computed from the query. Derive it on every render.
- **Two screens that should match do not**: one of them drew its own container. Diff the two against
`admin/connectors.tsx` and delete whichever hand-drawn wrapper is not `PageRows`.
`admin/plugins/index.tsx` and delete whichever hand-drawn wrapper is not `PageRows`.
- **The layout genuinely cannot express the screen**: stop and say so rather than bending it
silently. A deviation with a stated reason and a comment is fine; an undocumented fifth way to draw
a card is what this skill exists to prevent.
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ OPENBOT_SINGLE_USER=true
# OKTA_OAUTH_CLIENT_SECRET=
# OKTA_OAUTH_ISSUER=https://example.okta.com/oauth2/default

# Where this deployment is reached from outside. Only needed for connectors that a person connects
# their own account to, such as Google Drive: it builds the redirect URI the vendor sends them back
# to, which has to match what an administrator registered character for character. Defaults to
# BETTER_AUTH_URL, so most deployments never set it; set it where the API is behind a different
# public address than the one sign-in uses. Without either, the Plugins page says so and nobody can
# connect an account.
# OPENBOT_PUBLIC_URL=https://openbot.example.com

# Where the browser app is served from. The OAuth callback lands on the API and has to send the
# person back to a page, and locally those are two different ports: the app is 3010 and the API is
# 3001. Defaults to the first TRUSTED_ORIGINS entry, then to OPENBOT_PUBLIC_URL, which is right for a
# deployment serving both from one origin.
# OPENBOT_APP_URL=http://localhost:3010
TRUSTED_ORIGINS=http://localhost:3010

# CopilotKit Intelligence. Required: the server refuses to start without all four because
Expand Down
69 changes: 69 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ digit. The same rule container and volume names have always followed. A deployme
`AUDIT_RETENTION_DAYS` is new and unset, which keeps the audit trail forever, as before. Set it to a
whole number of days to have old rows removed.

**An MCP server pointed at a credential that no longer exists loses the pointer.** `mcp_servers`
now names its credential with a real foreign key, where the column was `text` against a `uuid`
primary key with nothing checking it — so a deployment is allowed to be holding a pointer to a vault
row that was deleted underneath it, and the screens read as though the server were still configured.
The migration clears those before adding the key, because it cannot add it otherwise. If this
happens, that connector correctly reports having no credential and an administrator registers it
again; nothing else is affected, and a deployment with no such pointer sees nothing.

**The old Google Drive connector is gone, and it is not the new one renamed.** It configured a
service account with domain impersonation and had the worker sync documents into a local pgvector
index guarded by our own ACL rows, so every person got the same answer computed from what one
credential could see, and revoking somebody's access left a cached copy of their documents behind.
`/admin/connectors` and its two screens, the connector catalogue and admin service, the sync
persistence and the worker's connector runner have all been removed. A deployment that was syncing
this way stops syncing and should enable the new connector at `/admin/plugins/google-drive`, where
each person connects their own account.

`knowledge.yaml` is still parsed and still refused when malformed, because it is part of the
deployment-package contract. Its `sources:` are now read by nothing.
`MANAGED_AGENT_AG_UI_URL` is no longer required to start. The one-container image does not carry a
Bot, so requiring it registered the shipped Risk Analyst against a host that was not there and every
conversation with it failed. Leave it unset for that image. A laptop `scripts/start.sh` still points
Expand Down Expand Up @@ -63,6 +82,46 @@ Sessions survive and nobody signs in again.
and skips routing entirely. If the router is uncertain or unreachable, it falls back to the same
default the composer always used, and says so, rather than misroute or drop.

- **A Bot can answer from Google Drive, as the person asking.** Ask a Bot a question whose answer is
in a document and it answers from the live file rather than from an index, citing a link that opens
it. A Bot granted these tools reads Drive on the asker's own grant, so two people asking the same
question get the answers their own accounts can see, and neither sees the other's documents.
Read-only: the scope requested is `drive.readonly`, so a write is refused by Google before this
deployment has to. Nothing is cached — the refresh token is stored and an access token is minted
per call, so revoking access at Google takes effect on the next one rather than when a cache
expires.

Setting it up takes two people and neither can do the other's half. An administrator registers a
Google Cloud OAuth client and enables the connector at `/admin/plugins/google-drive`; each person
then connects their own account, and there is deliberately no endpoint for an administrator to
connect one on somebody's behalf. The redirect URI has to match what is registered character for
character, and the connector page states the exact string to paste, because a mismatch fails at
Google with a message that never mentions OpenBot. See
[docs/plugins/google-drive.md](docs/plugins/google-drive.md) for the whole setup and for what each
failure means.

**Disconnecting is not built yet.** The account page says so and points at Google's own third-party
access settings, which is what withdraws it today.
- **Each tool a connector offers has its own screen**, at `/admin/plugins/<connector>/tools/<tool>`,
with a switch per Bot. The connector page previously drew a button per Bot inside every tool row,
which is a control per Bot per tool stacked in one list, and grew without bound as Bots were added.
- **Connected accounts**, at `/settings/connected-accounts`. What a Bot may read as you, and the
scope the vendor actually granted rather than the one that was asked for.
- **A tool result that found nothing says so.** An empty result used to reach the model as an empty
string, which reads as "the tool had nothing to say" rather than "there is nothing there" — and a
model closes that gap from memory, which for a knowledge connector is the failure worth preventing.
- **The shipped Knowledge Bot answers from the tools it has.** Its instructions in
`examples/fintech` told it to say no source was connected, which was honest when none could be:
the connector this replaces had been removed and nothing had taken its place. With a connector
granted it became the opposite of honest — the Bot called a tool, was handed a file listing, and
said it had no access anyway. It now reports what its tools return, says so plainly when it has no
tool or a tool reports a problem, and does neither of the two things worth forbidding: answering
from its own memory as though it came from a source, or claiming to lack access to something a tool
has just returned. A deployment with its own tenant package is unaffected.
- **`mcp.call_failed`.** A call this deployment permitted and the vendor did not complete now leaves
a row of its own, carrying the vendor's own sentence. `mcp.call_succeeded` was written before the
network call rather than after, so a call that died at the vendor recorded success and the Admin
page agreed with it.
- **Releases are cut by a workflow, not by hand.** `Create release PR` bumps the version and promotes
`## Unreleased` to a numbered section; merging the pull request it opens is what publishes. Merging
builds and pushes one image to `ghcr.io/copilotkit/openbot`, signs a build provenance attestation
Expand Down Expand Up @@ -125,6 +184,16 @@ Sessions survive and nobody signs in again.
is unavailable never blocks a sign-in.

### Fixed
- **Removing somebody left the credentials they had granted this deployment sitting in the vault.**
Removing them from the People screen ended their sessions and stopped the next sign-in, and left the
refresh token behind, unrevoked. They could not use it — the account comes from a session they no
longer get — but "we removed their access" was not true of the token, which for a connector read as
the person asking is the part that matters. Removing somebody now retires it, and each retirement is
on the audit trail as `mcp.account_disconnected`. Deleting a person's row used to be worse, because
it took the connection record with it and left the credential reachable by nothing at all; those are
found and retired too. This stops the deployment holding a usable secret. It does **not** withdraw
the grant at the vendor, which needs revoking there until disconnect ships, and the audit row says
which of the two happened rather than implying both.
- **The one-container image registered a coworker it could not run.** `MANAGED_AGENT_AG_UI_URL`
defaulted to `localhost:4201` and was required, so Risk Analyst appeared on the roster and every
conversation with it failed. The URL is optional; the package omits that coworker when it is
Expand Down
18 changes: 9 additions & 9 deletions app/src/components/admin/admin-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {
IconBuildingBank,
IconCode,
IconDeviceDesktop,
IconFileText,
IconKey,
IconLayoutGrid,
IconListDetails,
IconPlugConnected,
IconPuzzle,
IconShieldCheck,
IconUsers,
Expand All @@ -29,11 +29,11 @@ const appLinkOptions = { to: "/" } satisfies LinkOptions;
const adminLinkOptions = { to: "/admin" } satisfies LinkOptions;

/**
* The same three groups, in the same order, as the admin index.
* The same four groups, in the same order, as the admin index.
*
* A rail that lists eight things flat asks somebody to know which of them is the one they want. The
* A rail that lists ten things flat asks somebody to know which of them is the one they want. The
* grouping is the only navigation help this screen offers, so it has to agree with the page it
* navigates to — two different orderings of the same eight links is worse than either ordering.
* navigates to — two different orderings of the same ten links is worse than either ordering.
*/
const GROUPS: {
label: string;
Expand All @@ -46,11 +46,6 @@ const GROUPS: {
{
label: "What Bots can reach",
items: [
{
title: "Connectors",
icon: IconPlugConnected,
linkOptions: { to: "/admin/connectors" },
},
{
title: "Credentials",
icon: IconKey,
Expand All @@ -76,6 +71,11 @@ const GROUPS: {
icon: IconPuzzle,
linkOptions: { to: "/admin/plugins" },
},
{
title: "Skills",
icon: IconFileText,
linkOptions: { to: "/admin/skills" },
},
{
title: "UI Components",
icon: IconLayoutGrid,
Expand Down
38 changes: 38 additions & 0 deletions app/src/components/layout/row-mark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type * as React from "react";
import { ItemMedia } from "@/components/ui/item";
import { cn } from "@/lib/utils";

/**
* A row's leading icon, as a tile rather than a bare glyph.
*
* A DELIBERATE DEVIATION from the default row anatomy, which is `ItemMedia variant="icon"` and
* nothing else. Stated here because the layout skill asks for a reason when a screen departs from it.
*
* FOR ONE LIST ONLY: the connectors on `admin/plugins`. Every row there is another company, and the
* tile carries that vendor's own mark — so it is doing work no other row in the app needs, which is
* telling third parties apart at a glance. `variant="icon"` puts a 15px glyph straight against the
* text, and a list of vendors read that way has no fixed left edge for the eye to run down.
*
* Not for a detail page, and not for skills. Those rows are this deployment's own settings and its
* own instructions; there is no third party to identify, so they take the standard media and the
* screens stay consistent with the other eleven that use it.
*
* One component rather than a class literal at every call site, because the whole value is that the
* tiles are identical: copies of `size-9 rounded-lg bg-muted/60` are chances for one of them to
* drift, and a list with one tile a pixel out looks broken rather than varied.
*/
export function RowMark({
className,
...props
}: React.ComponentProps<typeof ItemMedia>) {
return (
<ItemMedia
className={cn(
"size-9 rounded-lg bg-muted/60 text-muted-foreground",
className,
)}
variant="icon"
{...props}
/>
);
}
10 changes: 10 additions & 0 deletions app/src/components/settings/settings-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
IconArrowLeft,
IconLayoutGrid,
IconPlug,
IconSettings,
} from "@tabler/icons-react";
import { Link, type LinkOptions } from "@tanstack/react-router";
Expand Down Expand Up @@ -37,6 +38,15 @@ const ITEMS: {
exact: true,
linkOptions: { to: "/settings" },
},
{
/*
* The same subject as Admin's Plugins, from the other side: there an administrator decides what
* this deployment may reach at all, here you decide what it may reach as you.
*/
title: "Connected accounts",
icon: IconPlug,
linkOptions: { to: "/settings/connected-accounts" },
},
{
/* The same mark Admin gives UI Components. It is the same subject seen from the other side. */
title: "Components gallery",
Expand Down
30 changes: 0 additions & 30 deletions app/src/lib/connectors/mutations.ts

This file was deleted.

26 changes: 0 additions & 26 deletions app/src/lib/connectors/queries.ts

This file was deleted.

Loading