mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-04 16:42:35 +00:00
Pause Google one-click connect pending CASA verification
Gmail/Calendar/Drive show a disabled Coming-soon button; the server refuses the flow too. Manual token connect and already-connected accounts are untouched.
This commit is contained in:
@@ -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 `<name>:account:<id>` 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:<email> 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;
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
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");
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -824,7 +824,21 @@ export function ConnectSetup({
|
||||
)}
|
||||
{c.managed && !c.mcp && !manualOnly && (
|
||||
<div className="space-y-2" data-testid="managed-connect">
|
||||
{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.
|
||||
<>
|
||||
<button className={BTN_ACCENT + " opacity-50"} disabled data-testid="managed-coming-soon">
|
||||
{`Connect ${c.title} with one click`}
|
||||
<span className="ml-2 text-[11px] font-medium px-1.5 py-0.5 rounded-full bg-white/25">
|
||||
Coming soon
|
||||
</span>
|
||||
</button>
|
||||
<div className="text-[11.5px] text-faint">
|
||||
One-click sign-in is coming soon — connect manually below for now:
|
||||
</div>
|
||||
</>
|
||||
) : cloud?.signed_in ? (
|
||||
<button className={BTN_ACCENT} onClick={oneClick} disabled={waiting}>
|
||||
{waiting ? "Check your browser…" : `Connect ${c.title} with one click`}
|
||||
</button>
|
||||
@@ -837,7 +851,7 @@ export function ConnectSetup({
|
||||
// possibly-signed-in user (FB-013); the host keeps polling.
|
||||
<CloudStatusPending />
|
||||
)}
|
||||
{cloud?.signed_in && (
|
||||
{!c.managed_paused && cloud?.signed_in && (
|
||||
<div className="text-[11.5px] text-faint">or connect manually:</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -46,13 +46,19 @@ export function CalendarDetail({ c, cloud, slack: _slack, onChanged }: DetailPro
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={PILL_ACCENT}
|
||||
className={PILL_ACCENT + (c.managed_paused ? " opacity-50" : "")}
|
||||
data-testid="add-account-btn"
|
||||
onClick={addAccount}
|
||||
disabled={busy || !cloud?.signed_in}
|
||||
title={cloud?.signed_in ? "" : "Sign in to OpenWorker Cloud first"}
|
||||
disabled={busy || !cloud?.signed_in || c.managed_paused}
|
||||
title={
|
||||
c.managed_paused
|
||||
? "One-click Google sign-in is coming soon"
|
||||
: cloud?.signed_in
|
||||
? ""
|
||||
: "Sign in to OpenWorker Cloud first"
|
||||
}
|
||||
>
|
||||
{busy ? "Check your browser…" : "+ Add account"}
|
||||
{c.managed_paused ? "+ Add account · Coming soon" : busy ? "Check your browser…" : "+ Add account"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -48,13 +48,19 @@ export function GmailDetail({ c, cloud, slack: _slack, onChanged }: DetailProps)
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={PILL_ACCENT}
|
||||
className={PILL_ACCENT + (c.managed_paused ? " opacity-50" : "")}
|
||||
data-testid="add-account-btn"
|
||||
onClick={addAccount}
|
||||
disabled={busy || !cloud?.signed_in}
|
||||
title={cloud?.signed_in ? "" : "Sign in to OpenWorker Cloud first"}
|
||||
disabled={busy || !cloud?.signed_in || c.managed_paused}
|
||||
title={
|
||||
c.managed_paused
|
||||
? "One-click Google sign-in is coming soon"
|
||||
: cloud?.signed_in
|
||||
? ""
|
||||
: "Sign in to OpenWorker Cloud first"
|
||||
}
|
||||
>
|
||||
{busy ? "Check your browser…" : "+ Add account"}
|
||||
{c.managed_paused ? "+ Add account · Coming soon" : busy ? "Check your browser…" : "+ Add account"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user