Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 6 additions & 2 deletions companion/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,16 @@ const ALLOWED: ReadonlyArray<{ method: string; path: RegExp }> = [
{ method: "POST", path: /^\/api\/routine-runs\/[\w-]+\/(?:cancel|seen)$/ },

// Multi-account Composio management exposes opaque ids and aliases only.
// Revocation stays on the host: the account DELETE route is deliberately
// absent — a paired client can see and add accounts, never remove one.
// Account-level removal is allowed: the handler still proves the account
// belongs to the host's own user before revoking, and a paired client can
// already add accounts — connectable but not disconnectable is the bug
// being fixed here. The whole-service DELETE stays denied: it belongs to
// the host.
{ method: "GET", path: /^\/api\/connectors\/catalog$/ },
{ method: "GET", path: /^\/api\/connectors\/connected$/ },
{ method: "GET", path: /^\/api\/connectors$/ },
{ method: "POST", path: /^\/api\/connectors\/[\w-]+\/authorize$/ },
{ method: "DELETE", path: /^\/api\/connectors\/[\w-]+\/accounts\/[\w-]+$/ },
// Inline connector cards are scoped by bot, transcript message, and
// thread. They expose the same opaque OAuth authorization already allowed
// above, then only poll, resume, or dismiss that exact pending card.
Expand Down
8 changes: 5 additions & 3 deletions companion/test/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ describe("what the app may do", () => {
["GET", "/api/connectors/connected"],
["GET", "/api/connectors"],
["POST", "/api/connectors/slack/authorize"],
["DELETE", "/api/connectors/slack/accounts/ca_123"],
["GET", "/api/bots/bot_123/connector-cards/msg_2/status"],
["POST", "/api/bots/bot_123/connector-cards/msg_2/authorize"],
["POST", "/api/bots/bot_123/connector-cards/msg_2/resume"],
Expand Down Expand Up @@ -219,9 +220,10 @@ describe("what it may not", () => {
expect(allowed("POST", "/api/routine-runs/run_1/retry")).toBe(false);
expect(allowed("DELETE", "/api/connectors/slack")).toBe(false);
expect(allowed("GET", "/api/connectors/connected/all")).toBe(false);
// revocation is a host-only affordance: a paired client can list and add
// accounts but the account DELETE route is deliberately not allowed
expect(allowed("DELETE", "/api/connectors/slack/accounts/ca_123")).toBe(false);
// per-account removal is allowed (the server proves ownership before
// revoking); removing the whole service binding stays host-only
expect(allowed("DELETE", "/api/connectors/slack/accounts/ca_123")).toBe(true);
expect(allowed("DELETE", "/api/connectors/slack/accounts/../gmail")).toBe(false);
expect(allowed("POST", "/api/bots/bot_123/secret-cards/msg_2/provided")).toBe(false);
expect(allowed("PATCH", "/api/groups/room-1")).toBe(false);
});
Expand Down
8 changes: 0 additions & 8 deletions src/components/PluginsPanel.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { describe, expect, it } from "vitest";

import {
connectedAppsMayDisconnect,
connectedInventoryCopy,
connectorActionLabel,
disconnectAccountConfirmation,
Expand All @@ -13,13 +12,6 @@ import {
} from "./PluginsPanel";
import { managedConnectorUnavailableReason } from "../../shared/connector-availability";

describe("connected-app remote permissions", () => {
it("allows pairing and status remotely but keeps revocation on the host", () => {
expect(connectedAppsMayDisconnect(false)).toBe(true);
expect(connectedAppsMayDisconnect(true)).toBe(false);
});
});

describe("connected-app status races", () => {
it("offers status recovery for a pending authorization whose URL was lost on remount", () => {
for (const hasAccounts of [false, true]) {
Expand Down
37 changes: 15 additions & 22 deletions src/components/PluginsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,6 @@ export function disconnectAccountConfirmation(
return t("connectors.disconnectConfirm", { identity, service });
}

export function connectedAppsMayDisconnect(remoteClient: boolean): boolean {
return !remoteClient;
}

export function requiresAccountAlias(message: string) {
return /account alias.*existing connection.*not replaced/i.test(message);
}
Expand Down Expand Up @@ -213,7 +209,6 @@ function ServiceIcon({ card }: { card: ToolkitCard }) {
export function PluginsPanel() {
const { state, dispatch } = useStore();
const remoteClient = window.ogb?.remoteClient?.active === true;
const mayDisconnect = connectedAppsMayDisconnect(remoteClient);
const dialogRef = useRef<HTMLDivElement>(null);
const surface = state.pluginsSurface;
const [cards, setCards] = useState<ToolkitCard[] | null>(null);
Expand Down Expand Up @@ -743,23 +738,21 @@ export function PluginsPanel() {
{account.alias ? `${account.id} · ` : ""}{account.status.toLowerCase()}
</div>
</div>
{mayDisconnect && (
<button
type="button"
disabled={busy}
onClick={() => {
if (!window.confirm(disconnectAccountConfirmation(card.label, account))) return;
disconnectAccount(card.slug, account.id);
}}
className="rounded-md px-2 py-1 text-[11px] text-ink-secondary transition-colors hover:bg-danger/10 hover:text-danger disabled:opacity-40"
aria-label={t("connectors.disconnectAria", {
account: account.alias || account.id,
service: card.label,
})}
>
{t("connectors.disconnect")}
</button>
)}
<button
type="button"
disabled={busy}
onClick={() => {
if (!window.confirm(disconnectAccountConfirmation(card.label, account))) return;
disconnectAccount(card.slug, account.id);
}}
className="rounded-md px-2 py-1 text-[11px] text-ink-secondary transition-colors hover:bg-danger/10 hover:text-danger disabled:opacity-40"
aria-label={t("connectors.disconnectAria", {
account: account.alias || account.id,
service: card.label,
})}
>
{t("connectors.disconnect")}
</button>
</div>
);
})}
Expand Down
Loading