diff --git a/coworker/connectors/descriptors.py b/coworker/connectors/descriptors.py index 296dec43..9a6a34ab 100644 --- a/coworker/connectors/descriptors.py +++ b/coworker/connectors/descriptors.py @@ -82,6 +82,10 @@ class ConnectorDescriptor: # is an extra path, never a replacement (local-only open-source flow is # sacred). managed: bool = False + # One-click temporarily unavailable (e.g. Google pending CASA verification): + # the GUI shows a disabled button with a "Coming soon" badge, the server + # refuses begin_managed_connect, and the manual path is unaffected. + managed_paused: bool = False # Multi-account (accounts.py generic layer): the creds field that names an # account (e.g. "project_id"), or "@identity" = the validator's identity # string. Non-empty → profiles live at `:account:` and the @@ -558,6 +562,8 @@ DESCRIPTORS: list[ConnectorDescriptor] = [ ], available=True, managed=True, + # Google OAuth verification (CASA) pending — one-click off until it clears. + managed_paused=True, ), ConnectorDescriptor( name="google_calendar", @@ -582,6 +588,7 @@ DESCRIPTORS: list[ConnectorDescriptor] = [ ], available=True, managed=True, + managed_paused=True, # same Google app as Gmail — paused until CASA clears ), ConnectorDescriptor( name="browser", @@ -1119,12 +1126,13 @@ DESCRIPTORS: list[ConnectorDescriptor] = [ ), ], instructions=[ - "One click connects via OpenWorker Cloud (recommended).", - "Manual: use a Google OAuth access token with Drive readonly scope.", + "Use a Google OAuth access token with Drive readonly scope.", + "Paste the access token below.", ], validate=_validate_google_drive, available=True, managed=True, + managed_paused=True, # same Google app as Gmail — paused until CASA clears # Key each connected account by its Google email (the broker's `account` # field) so multiple Drive accounts list the same way Gmail's do, rather # than by the opaque `sub` that account_field="account_id" would use. diff --git a/coworker/connectors/setup.py b/coworker/connectors/setup.py index 0a987f4a..fe716319 100644 --- a/coworker/connectors/setup.py +++ b/coworker/connectors/setup.py @@ -97,6 +97,7 @@ def connector_list(secrets: SecretStore) -> list[dict[str, Any]]: "experimental": d.experimental, "risk_notice": d.risk_notice, "managed": d.managed, + "managed_paused": d.managed_paused, # Whether THIS profile came from managed OAuth (vs manual paste). "managed_profile": bool(profile.get("managed")), # "relay" for the managed cloud path; empty for manual/token connect. diff --git a/coworker/server/app.py b/coworker/server/app.py index e4322739..cbccc4a7 100644 --- a/coworker/server/app.py +++ b/coworker/server/app.py @@ -968,7 +968,15 @@ def create_app(manager: SessionManager) -> FastAPI: from .. import cloud from ..config import load_config + from ..connectors.descriptors import get_descriptor + d = get_descriptor(name) + if d is not None and d.managed_paused: + # GUI shows the Coming-soon state; this guard covers stale GUIs/API callers. + return { + "ok": False, + "error": f"one-click connect for {d.title} is coming soon — connect manually for now", + } access = str((body or {}).get("access") or "") flow = str((body or {}).get("flow") or "") # github: "" install | "authorize" out = await asyncio.to_thread( diff --git a/surfaces/gui/e2e/fixtures.ts b/surfaces/gui/e2e/fixtures.ts index ad9a1690..f4b1f8b4 100644 --- a/surfaces/gui/e2e/fixtures.ts +++ b/surfaces/gui/e2e/fixtures.ts @@ -399,6 +399,10 @@ export async function mockApi(page: import("@playwright/test").Page) { }); // Gmail — PER-TEST multi-account state (starts disconnected; managed connects add // mailboxes instantly, mirroring the backend's gmail:account: profiles). + // NOTE: the real server currently sends managed_paused: true for the Google trio + // (CASA pending). The fixture keeps gmail UNPAUSED because the cloud-machinery specs + // use its one-click as their subject; the paused UI is covered by google-paused.spec.ts + // via a per-test connectors override. const gmailState = { accounts: [] as { email: string; default: boolean; managed: boolean; scopes: string; needs_reauth: boolean; diff --git a/surfaces/gui/e2e/google-paused.spec.ts b/surfaces/gui/e2e/google-paused.spec.ts new file mode 100644 index 00000000..71230ac8 --- /dev/null +++ b/surfaces/gui/e2e/google-paused.spec.ts @@ -0,0 +1,81 @@ +// Google one-click paused pending CASA verification (owner ask 2026-07-22): the managed +// button parks with a "Coming soon" badge — pre-connect modal AND the connected page's +// add-account — while the manual token path stays fully live. The shared fixture keeps +// gmail unpaused (the cloud-machinery specs use it as their one-click subject), so this +// spec overrides the connectors payload per test, like automations-quickstart does. +import { expect } from "@playwright/test"; +import { test } from "./fixtures"; + +const GMAIL_BASE = { + name: "gmail", + title: "Gmail", + icon: "✉", + blurb: "Search, summarize, draft, and send email.", + about: "Search, summarize, and send over your Gmail.", + access: ["Reads and searches your mail."], + auth: "oauth", + two_way: false, + channels: false, + available: true, + brand_color: "#ea4335", + logo: "gmail", + fields: [ + { key: "access_token", label: "OAuth access token", secret: true, required: true, help: "", placeholder: "" }, + ], + instructions: [], + account: null, + allowed_users: [], + tools: [], + managed: true, + managed_paused: true, + managed_profile: false, +}; + +async function serveGmail(page, extra: Record) { + await page.route("**/v1/connectors", (route) => + route.fulfill({ json: { connectors: [{ ...GMAIL_BASE, connected: false, enabled: false, ...extra }] } }), + ); +} + +async function openConnectors(page) { + await page.goto("/"); + await page.getByTestId("account-row").click(); + await page.getByRole("button", { name: "Connectors", exact: true }).click(); +} + +test("paused one-click: Coming soon badge in the connect modal, manual path alive", async ({ + page, +}) => { + await serveGmail(page, {}); + await openConnectors(page); + await page.getByTestId("connector-gmail").getByRole("button", { name: "Connect", exact: true }).click(); + + const soon = page.getByTestId("managed-coming-soon"); + await expect(soon).toBeVisible(); + await expect(soon).toBeDisabled(); + await expect(soon).toContainText("Coming soon"); + await expect(page.getByText("connect manually below for now")).toBeVisible(); + // The manual token field is still right there. + await expect(page.getByText("OAuth access token")).toBeVisible(); +}); + +test("paused one-click: connected page's add-account is parked too", async ({ page }) => { + await serveGmail(page, { + connected: true, + enabled: true, + account: "rohit@gmail.com", + accounts: [ + { email: "rohit@gmail.com", default: true, managed: true, scopes: "gmail", needs_reauth: false }, + ], + filters: { senders: [], labels: [] }, + }); + await openConnectors(page); + await page.getByTestId("connector-gmail").click(); + await expect(page.getByTestId("gmail-detail")).toBeVisible(); + + const add = page.getByTestId("add-account-btn"); + await expect(add).toBeDisabled(); + await expect(add).toContainText("Coming soon"); + // Existing accounts keep working and stay manageable. + await expect(page.getByTestId("gmail-account-rohit@gmail.com")).toContainText("Default"); +}); diff --git a/surfaces/gui/src/api.ts b/surfaces/gui/src/api.ts index 842d7082..a47fe33f 100644 --- a/surfaces/gui/src/api.ts +++ b/surfaces/gui/src/api.ts @@ -391,6 +391,7 @@ export interface Connector { unauthorized?: ParkedMessage[]; // parked messages from unallowed senders (§19) tools: ConnectorTool[]; managed: boolean; // one-click managed OAuth available (needs cloud sign-in) + managed_paused?: boolean; // one-click temporarily off (e.g. Google CASA pending) — badge "Coming soon" managed_profile: boolean; // current profile came from managed OAuth (vs manual paste) mode?: string; // "relay" for the managed cloud path; "" for manual/token connect workspaces?: SlackWorkspace[]; // Slack only: connected workspaces (managed relay) diff --git a/surfaces/gui/src/components/ManageTabs.tsx b/surfaces/gui/src/components/ManageTabs.tsx index 1d461686..f9ab2992 100644 --- a/surfaces/gui/src/components/ManageTabs.tsx +++ b/surfaces/gui/src/components/ManageTabs.tsx @@ -824,7 +824,21 @@ export function ConnectSetup({ )} {c.managed && !c.mcp && !manualOnly && (
- {cloud?.signed_in ? ( + {c.managed_paused ? ( + // One-click temporarily off (e.g. Google pending CASA verification): + // a visibly-parked button, and the manual path below stays fully live. + <> + +
+ One-click sign-in is coming soon — connect manually below for now: +
+ + ) : cloud?.signed_in ? ( @@ -837,7 +851,7 @@ export function ConnectSetup({ // possibly-signed-in user (FB-013); the host keeps polling. )} - {cloud?.signed_in && ( + {!c.managed_paused && cloud?.signed_in && (
or connect manually:
)}
diff --git a/surfaces/gui/src/components/connectors/CalendarDetail.tsx b/surfaces/gui/src/components/connectors/CalendarDetail.tsx index 13fb7b96..5109a0c4 100644 --- a/surfaces/gui/src/components/connectors/CalendarDetail.tsx +++ b/surfaces/gui/src/components/connectors/CalendarDetail.tsx @@ -46,13 +46,19 @@ export function CalendarDetail({ c, cloud, slack: _slack, onChanged }: DetailPro diff --git a/surfaces/gui/src/components/connectors/GmailDetail.tsx b/surfaces/gui/src/components/connectors/GmailDetail.tsx index a710f152..3fd95752 100644 --- a/surfaces/gui/src/components/connectors/GmailDetail.tsx +++ b/surfaces/gui/src/components/connectors/GmailDetail.tsx @@ -48,13 +48,19 @@ export function GmailDetail({ c, cloud, slack: _slack, onChanged }: DetailProps) diff --git a/tests/test_cloud_server.py b/tests/test_cloud_server.py index d538dd8b..4a4d63a8 100644 --- a/tests/test_cloud_server.py +++ b/tests/test_cloud_server.py @@ -30,7 +30,9 @@ def test_cloud_status_signed_out(client): def test_connect_managed_requires_sign_in(client): - body = client.post("/v1/connectors/gmail/connect-managed").json() + # notion, not gmail: the Google trio is managed_paused (CASA pending) and its + # guard fires before the sign-in check — see test_google_one_click_paused…. + body = client.post("/v1/connectors/notion/connect-managed").json() assert not body["ok"] assert "not signed in" in body["error"] diff --git a/tests/test_server.py b/tests/test_server.py index b1605ea7..7a9c723c 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -751,3 +751,18 @@ def test_always_allow_grants_survive_restart(tmp_path): # "Restart": new manager + engine rebuilt from the persisted record. mgr2 = SessionManager(workspace=None, provider=_shell_turns()) _run_turn(TestClient(create_app(mgr2)), expect_prompts=0) + + +def test_google_one_click_paused_but_manual_alive(tmp_path): + """CASA verification pending: Gmail/Calendar/Drive expose managed_paused (GUI badges + "Coming soon"), the managed-connect route refuses, and the manual fields stay.""" + client = _client(tmp_path, []) + connectors = {c["name"]: c for c in client.get("/v1/connectors").json()["connectors"]} + for name in ("gmail", "google_calendar", "google_drive"): + c = connectors[name] + assert c["managed"] is True and c["managed_paused"] is True + assert c["fields"], f"{name} lost its manual fields" + assert connectors["slack"]["managed_paused"] is False # only Google is paused + + refused = client.post("/v1/connectors/gmail/connect-managed", json={}).json() + assert refused["ok"] is False and "coming soon" in refused["error"]