项目文件夹

文件
Rohit C Prasad 479db317b8 Generic multi-account layer for connectors (batch-2 spec + step 0)
Slack/Gmail/Calendar/HubSpot each grew a bespoke accounts module; this
is the same proven shape parameterized by connector: profiles at
<name>:account:<id>, pointer-only :default, lazy legacy migration. A
descriptor opts in via account_field (creds field or @identity). Connect
adds accounts instead of overwriting, connector_list grows a generic
accounts branch, and /v1/connectors/{name}/accounts/{id}/disconnect|default
routes cover every account-patterned connector. Spec for the batch in
docs/CONNECTORS-BATCH2-SPEC.md.
2026-07-13 12:31:22 -07:00

208 行
11 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# Connectors Batch 2 — Notion, Attio (managed OAuth) + PostHog, Mixpanel, Amplitude, Apollo, Hunter (manual) — with a generic multi-account layer
Status: agreed 2026-07-10 (Rohit + agent). Supersedes nothing; extends M3.6.
## Goals
1. Seven new connectors: **Notion + Attio** with one-click managed OAuth via the
ocw-connect broker (manual token paste also available, as always), and
**PostHog, Mixpanel, Amplitude, Apollo, Hunter** as manual API-key connectors.
2. **Multi-account from day one, for all seven.** Users routinely hold multiple
accounts of the same SaaS (two Notion workspaces, several PostHog projects).
We already do this bespoke for Slack (workspaces), Gmail/GCal (accounts), and
HubSpot (portals) — this batch introduces the GENERIC account layer so we
never write a bespoke module again.
3. Every tool result and approval card names the account it touched.
## Non-goals
- Migrating gmail/gcal/hubspot/slack onto the generic layer (later refactor;
their shipped behavior must not churn in the same batch that adds 7 connectors).
- Write tools beyond the minimal useful set (this batch is read-heavy).
- Webhooks/relay inbound for any of the seven (outbound/read only).
- Per-account privacy filters (Gmail-style "never show agents") — later if asked.
## 1. Generic account layer — `coworker/connectors/accounts.py`
Same shape as `gcal_accounts.py`, parameterized by connector name:
- Profiles at `<connector>:account:<account_id>`; `<connector>:default` is a
POINTER ONLY: `{"default_account": <id>}` (plus connector-wide settings like
`enabled`). Never tokens.
- `list_accounts(secrets, connector) -> [(id, profile)]`
- `default_account(secrets, connector) -> id` (pointer, else sole account)
- `resolve(secrets, connector, account="") -> (profile_key, profile)` — explicit
account, else default, else the sole account; `{}` when nothing connected.
- `add_account(secrets, connector, account_id, fields)` — used by both manual
connect and the managed OAuth callback; sets the default pointer if first.
- `set_default`, `disconnect_account` (last disconnect deletes the pointer).
- **Legacy migration**: a token-bearing `<connector>:default` (from any older
build) is treated as one account and lazily rewritten on first touch —
mirrors the shipped gmail/gcal migration; no user action.
**Account id per connector** (stable, human-meaningful):
| connector | account_id | shown as |
|---|---|---|
| notion | workspace_id (from OAuth token response) | workspace_name |
| attio | workspace_id (from `GET /v2/self`) | workspace name |
| posthog | project_id | `host · project` |
| mixpanel | project_id | project_id |
| amplitude | api_key last 6 | `key …abc123` |
| apollo | validator identity (account email) | email |
| hunter | validator identity (account email) | email |
Descriptor gains `account_field: str = ""` — the creds field (or `"@identity"`
sentinel = use ValidationResult.identity) that names the account. Non-empty
`account_field` marks the connector as account-patterned everywhere
(connect path, connector_list, server routes, GUI).
## 2. Desktop plumbing
- **Connect (manual)**: for account-patterned connectors, `connect_connector`
validates, derives the account id, and writes `<name>:account:<id>` via
`add_account` instead of overwriting `<name>:default`. Re-connecting the same
id updates it; a different id adds a second account. Managed OAuth callback
does the same with broker-returned identity.
- **connector_list**: generic branch for account-patterned connectors, same
shape as gmail/gcal: `accounts: [{account, default, managed}]`,
`connected = len(accounts) > 0`, `account` = default account (back-compat).
- **Routes** (generic, allowlisted to account-patterned connectors):
- `POST /v1/connectors/{name}/accounts/{id}/disconnect` (+ best-effort
cloud_disconnect for managed profiles)
- `POST /v1/connectors/{name}/accounts/{id}/default`
(gcal keeps its existing specific routes; no alias churn.)
- **Tools**: every tool gets `account: str = ""`; shared helper
`_account_profile(secrets, connector, account, *fields)` (resolve →
`ensure_fresh_connector_token(profile_key=…)` for managed → field check);
every result passes through `_acct_result(account_id, result)` so approvals
and transcripts name the account.
## 3. The seven connectors
### Notion (managed + manual)
- OAuth: authorize `https://api.notion.com/v1/oauth/authorize` (`owner=user`),
token exchange with **HTTP Basic** (client_id:secret). Tokens are long-lived
(no refresh flow). Token response carries `workspace_id`/`workspace_name`
the broker forwards them; desktop stores them on the account profile.
- Manual path: paste an internal-integration token; account id = workspace id
from `GET /v1/users/me` (bot owner workspace).
- Tools: `notion_search(query, account)` (POST /v1/search),
`notion_read_page(page_id, account)` (page + block children, text-flattened),
`notion_query_database(database_id, filter_json, account)`,
`notion_create_page(parent_id, title, markdown_body, account)` [write].
- Validator: `GET /v1/users/me`.
### Attio (managed + manual)
- OAuth: authorize `https://app.attio.com/authorize`, token
`https://app.attio.com/oauth/token` (standard code exchange). Long-lived
access token. Identity: `GET /v2/self` → workspace id + name.
- Manual path: workspace API key (same header).
- Tools: `attio_list_objects(account)` (GET /v2/objects),
`attio_search_records(object, query, account)` (POST
/v2/objects/{object}/records/query), `attio_get_record(object, record_id,
account)`, `attio_create_note(parent_object, parent_record_id, title,
content, account)` [write].
- Validator: `GET /v2/self`.
### PostHog (manual)
- Fields: `base_url` (default `https://us.posthog.com`), `api_key` (personal),
`project_id`. Bearer auth.
- Tools: `posthog_query(hogql, account)` (POST /api/projects/{pid}/query,
HogQLQuery kind), `posthog_list_insights(query, max_results, account)`.
- Validator: `GET /api/users/@me/` → email.
### Mixpanel (manual)
- Fields: service account `username` + `secret`, `project_id`. HTTP Basic.
- Tools: `mixpanel_segmentation(event, from_date, to_date, unit, where,
account)` (GET mixpanel.com/api/query/segmentation),
`mixpanel_top_events(max_results, account)` (GET /api/query/events/top).
- Validator: `GET https://mixpanel.com/api/app/me`.
### Amplitude (manual)
- Fields: `api_key`, `secret_key`. HTTP Basic. Dashboard REST API.
- Tools: `amplitude_active_users(start, end, metric, account)` (GET
amplitude.com/api/2/users), `amplitude_event_totals(event_type, start, end,
account)` (GET /api/2/events/segmentation).
- Validator: `GET /api/2/annotations`.
### Apollo (manual)
- Field: `api_key` (X-Api-Key header).
- Tools: `apollo_enrich_person(email, name, company_domain, account)` (POST
/api/v1/people/match), `apollo_enrich_company(domain, account)` (GET
/api/v1/organizations/enrich), `apollo_search_people(query, max_results,
account)` (POST /api/v1/mixed_people/search).
- Validator: `GET /api/v1/auth/health`.
### Hunter (manual)
- Field: `api_key` (query param).
- Tools: `hunter_domain_search(domain, max_results, account)`,
`hunter_find_email(domain, first_name, last_name, account)`,
`hunter_verify_email(email, account)`.
- Validator: `GET /v2/account` → email.
All tools approval-gated (house default); `caps=[<connector>, "read"|"write"]`.
Writes in batch: `notion_create_page`, `attio_create_note` only.
## 4. Broker (ocw-connect)
- Two `Provider` entries: `notion` (Basic-auth token exchange, no refresh,
forward `workspace_id`/`workspace_name`/`bot_id` in the callback result) and
`attio` (standard exchange). No scope sets (both fix scopes at app level).
- Redirect URIs: `https://api.opencoworker.app/v1/oauth/{notion,attio}/callback`.
- Stateless as ever: tokens go to the desktop, nothing at rest.
- **Prereqs (Rohit)**: register the Notion public integration + Attio app,
add client ids/secrets to the `ocw-connect/oauth-providers` secret
(key names only in any transcript), approve terraform apply.
## 5. GUI
- **AccountsDetail** (generic detail page, registered for all seven): accounts
group with Default badge / set-default / × disconnect-account, " Add
account" (managed → `connectManaged`; manual → the connector's field form),
Tools disclosure, footer Disconnect-all. Modeled on CalendarDetail; replaces
GenericDetail for account-patterned connectors.
- ConnectorsList row status shows `N accounts` when >1.
- Add-connection modal: Notion/Attio get One click | Manual pills (≥2 modes);
the five manual ones keep the plain field form.
- Visual review in the dev app before PR (house rule).
## 6. Tests
- `tests/test_accounts.py`: generic layer — add/list/resolve/default/disconnect
/legacy-migration, per connector-name isolation.
- `test_connectors.py`: routing tests for all new tools (fake `_request`),
not-connected errors, account param → profile resolution, `_acct_result`
stamping, write tools approval-pinned.
- `test_setup.py`-style: connector_list accounts branch; connect twice with
different ids → two accounts, default stable.
- Server route tests: generic disconnect/default (allowlist rejects
non-account connectors).
- Broker: provider config tests (authorize URL shape, Basic-auth exchange for
notion), callback forwarding of workspace fields.
- e2e: accounts page spec (fixtures with two Notion accounts), add-account
modal, default switch; connectors-list count row.
- Live drills (with ledger entries for anything caught): two Notion
workspaces one-click; Attio workspace; each manual key validated; account
disambiguation in approvals; dual-account tool calls.
## 7. Build order (each step lands green + committed)
- **Step 0** — accounts.py + descriptor `account_field` + connect-path +
connector_list branch + generic routes + tests.
- **Step 1** — the five manual connectors on the pattern (descriptors,
validators, tools, tool_defs) + tests. [task #27]
- **Step 2** — broker notion/attio providers + tests; deploy after prereqs +
explicit approval. [task #28]
- **Step 3** — desktop notion/attio (descriptors managed=True, callback
branches, tools) + tests. [task #29]
- **Step 4** — GUI AccountsDetail + modal pills + e2e; visual review.
- **Step 5** — live drills, ledger, then DMG rebuild (full build_dmg.sh,
signed). [task #30]
## Open questions (defaulted, flag to change)
- Attio/Notion write tools kept minimal (create page / note). More on ask.
- PostHog EU cloud → covered by `base_url`; self-hosted too.
- Amplitude EU residency zone (analytics.eu.amplitude.com) — not in v1.