项目文件夹

文件
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

11 KiB

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.