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
|
# is an extra path, never a replacement (local-only open-source flow is
|
||||||
# sacred).
|
# sacred).
|
||||||
managed: bool = False
|
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
|
# Multi-account (accounts.py generic layer): the creds field that names an
|
||||||
# account (e.g. "project_id"), or "@identity" = the validator's identity
|
# account (e.g. "project_id"), or "@identity" = the validator's identity
|
||||||
# string. Non-empty → profiles live at `<name>:account:<id>` and the
|
# string. Non-empty → profiles live at `<name>:account:<id>` and the
|
||||||
@@ -558,6 +562,8 @@ DESCRIPTORS: list[ConnectorDescriptor] = [
|
|||||||
],
|
],
|
||||||
available=True,
|
available=True,
|
||||||
managed=True,
|
managed=True,
|
||||||
|
# Google OAuth verification (CASA) pending — one-click off until it clears.
|
||||||
|
managed_paused=True,
|
||||||
),
|
),
|
||||||
ConnectorDescriptor(
|
ConnectorDescriptor(
|
||||||
name="google_calendar",
|
name="google_calendar",
|
||||||
@@ -582,6 +588,7 @@ DESCRIPTORS: list[ConnectorDescriptor] = [
|
|||||||
],
|
],
|
||||||
available=True,
|
available=True,
|
||||||
managed=True,
|
managed=True,
|
||||||
|
managed_paused=True, # same Google app as Gmail — paused until CASA clears
|
||||||
),
|
),
|
||||||
ConnectorDescriptor(
|
ConnectorDescriptor(
|
||||||
name="browser",
|
name="browser",
|
||||||
@@ -1119,12 +1126,13 @@ DESCRIPTORS: list[ConnectorDescriptor] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
instructions=[
|
instructions=[
|
||||||
"One click connects via OpenWorker Cloud (recommended).",
|
"Use a Google OAuth access token with Drive readonly scope.",
|
||||||
"Manual: use a Google OAuth access token with Drive readonly scope.",
|
"Paste the access token below.",
|
||||||
],
|
],
|
||||||
validate=_validate_google_drive,
|
validate=_validate_google_drive,
|
||||||
available=True,
|
available=True,
|
||||||
managed=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`
|
# 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
|
# 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.
|
# 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,
|
"experimental": d.experimental,
|
||||||
"risk_notice": d.risk_notice,
|
"risk_notice": d.risk_notice,
|
||||||
"managed": d.managed,
|
"managed": d.managed,
|
||||||
|
"managed_paused": d.managed_paused,
|
||||||
# Whether THIS profile came from managed OAuth (vs manual paste).
|
# Whether THIS profile came from managed OAuth (vs manual paste).
|
||||||
"managed_profile": bool(profile.get("managed")),
|
"managed_profile": bool(profile.get("managed")),
|
||||||
# "relay" for the managed cloud path; empty for manual/token connect.
|
# "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 .. import cloud
|
||||||
from ..config import load_config
|
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 "")
|
access = str((body or {}).get("access") or "")
|
||||||
flow = str((body or {}).get("flow") or "") # github: "" install | "authorize"
|
flow = str((body or {}).get("flow") or "") # github: "" install | "authorize"
|
||||||
out = await asyncio.to_thread(
|
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
|
// Gmail — PER-TEST multi-account state (starts disconnected; managed connects add
|
||||||
// mailboxes instantly, mirroring the backend's gmail:account:<email> profiles).
|
// 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 = {
|
const gmailState = {
|
||||||
accounts: [] as {
|
accounts: [] as {
|
||||||
email: string; default: boolean; managed: boolean; scopes: string; needs_reauth: boolean;
|
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)
|
unauthorized?: ParkedMessage[]; // parked messages from unallowed senders (§19)
|
||||||
tools: ConnectorTool[];
|
tools: ConnectorTool[];
|
||||||
managed: boolean; // one-click managed OAuth available (needs cloud sign-in)
|
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)
|
managed_profile: boolean; // current profile came from managed OAuth (vs manual paste)
|
||||||
mode?: string; // "relay" for the managed cloud path; "" for manual/token connect
|
mode?: string; // "relay" for the managed cloud path; "" for manual/token connect
|
||||||
workspaces?: SlackWorkspace[]; // Slack only: connected workspaces (managed relay)
|
workspaces?: SlackWorkspace[]; // Slack only: connected workspaces (managed relay)
|
||||||
|
|||||||
@@ -824,7 +824,21 @@ export function ConnectSetup({
|
|||||||
)}
|
)}
|
||||||
{c.managed && !c.mcp && !manualOnly && (
|
{c.managed && !c.mcp && !manualOnly && (
|
||||||
<div className="space-y-2" data-testid="managed-connect">
|
<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}>
|
<button className={BTN_ACCENT} onClick={oneClick} disabled={waiting}>
|
||||||
{waiting ? "Check your browser…" : `Connect ${c.title} with one click`}
|
{waiting ? "Check your browser…" : `Connect ${c.title} with one click`}
|
||||||
</button>
|
</button>
|
||||||
@@ -837,7 +851,7 @@ export function ConnectSetup({
|
|||||||
// possibly-signed-in user (FB-013); the host keeps polling.
|
// possibly-signed-in user (FB-013); the host keeps polling.
|
||||||
<CloudStatusPending />
|
<CloudStatusPending />
|
||||||
)}
|
)}
|
||||||
{cloud?.signed_in && (
|
{!c.managed_paused && cloud?.signed_in && (
|
||||||
<div className="text-[11.5px] text-faint">or connect manually:</div>
|
<div className="text-[11.5px] text-faint">or connect manually:</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,13 +46,19 @@ export function CalendarDetail({ c, cloud, slack: _slack, onChanged }: DetailPro
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={PILL_ACCENT}
|
className={PILL_ACCENT + (c.managed_paused ? " opacity-50" : "")}
|
||||||
data-testid="add-account-btn"
|
data-testid="add-account-btn"
|
||||||
onClick={addAccount}
|
onClick={addAccount}
|
||||||
disabled={busy || !cloud?.signed_in}
|
disabled={busy || !cloud?.signed_in || c.managed_paused}
|
||||||
title={cloud?.signed_in ? "" : "Sign in to OpenWorker Cloud first"}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -48,13 +48,19 @@ export function GmailDetail({ c, cloud, slack: _slack, onChanged }: DetailProps)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={PILL_ACCENT}
|
className={PILL_ACCENT + (c.managed_paused ? " opacity-50" : "")}
|
||||||
data-testid="add-account-btn"
|
data-testid="add-account-btn"
|
||||||
onClick={addAccount}
|
onClick={addAccount}
|
||||||
disabled={busy || !cloud?.signed_in}
|
disabled={busy || !cloud?.signed_in || c.managed_paused}
|
||||||
title={cloud?.signed_in ? "" : "Sign in to OpenWorker Cloud first"}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ def test_cloud_status_signed_out(client):
|
|||||||
|
|
||||||
|
|
||||||
def test_connect_managed_requires_sign_in(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 body["ok"]
|
||||||
assert "not signed in" in body["error"]
|
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.
|
# "Restart": new manager + engine rebuilt from the persisted record.
|
||||||
mgr2 = SessionManager(workspace=None, provider=_shell_turns())
|
mgr2 = SessionManager(workspace=None, provider=_shell_turns())
|
||||||
_run_turn(TestClient(create_app(mgr2)), expect_prompts=0)
|
_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