Skip to content

Connector connection keying: scope Composio connections per (account, artist) / (org, artist) #1865

Description

@sweetmantech

Follow-up to chat#1860 (connector-authority hardening — acute cross-account act-as-artist vector remediated and verified in prod, 2026-07-09). That issue's interim P0b cut (api#772, merged) already blocks a roster-only account from using another account's connectors at the authority layer (checkConnectorAuthority fails closed; verified live: authorize/execute/disconnect all return 403). This issue tracks the remaining, non-acute redesign: change how connections are stored in Composio so isolation lives on the connection itself, enable org-shared connections, and migrate existing connections onto the new keying.

(Business context, once — out of scope for this layer: connectors are how customers publish to their real social accounts; keying connections properly is what lets us scale connector adoption without cross-account leakage. This is remediation follow-through, not a gate on the funnel chat#1859.)

Scope (sweetman, 2026-07-09): split out of chat#1860 so the acute security work could close. Nothing here is acute — P0b already closed the live vector. This is the "proper keying" redesign plus a metadata-listing leak and org-sharing feature, to be picked up after vacation. No time pressure; do it right.

Goal

Composio connections are keyed on the join row (account_artist_ids / artist_organization_ids), not on the bare canonical artistId, so a connection is attributable to the specific (account, artist) or (org, artist) relationship that created it. Concretely, when done:

  1. getConnectors / getComposioTools for artist A return only the caller's own (account, A) connections plus any explicitly org-shared (org, A) connections — never another account's.
  2. The GET /api/connectors?account_id= listing no longer surfaces another account's connection status for a shared artist (closes the metadata-listing leak P0b deliberately left on the flat check).
  3. A second account in the same org can use A's connection only if it was established as an org connection; nobody else can. (Verified live.)
  4. Every existing Composio connection has been migrated onto the new keying (audit → wipe-and-reconnect).
  5. The connections tab works for non-admin customers again. A non-admin account that rosters artist A can open Artist Settings → ⋯ → Connections, see its own connections, click Connect to establish a new one (owned by its (account, A) join row), and execute/disconnect it. P0b currently returns 403 on authorize for this case (see the dedicated item below) — restoring it is gated on the re-key.

Code that keys on the bare artist id today: lib/composio/toolRouter/getComposioTools.ts:86 (ownerId: effectiveArtistId), and the connector authorize/execute/disconnect paths that store/read under the artist account id.

Prerequisites already shipped (context)

Both landed as part of chat#1860 and are done — this tracker builds directly on them:

  • docsdocs#271 (merged 2026-07-09): connection-ownership + authority contract (connectors/ownership.mdx). This is the contract the api below fulfills.
  • databasedatabase#46 (merged + applied to prod 2026-07-09): UNIQUE(account_id, artist_id) on account_artist_ids and UNIQUE(artist_id, organization_id) on artist_organization_ids. Verified via Supabase MCP: constraints live, 0 rows deduped, duplicate insert rejected with 23505. This makes the join row a deterministic key.

PRs (updated 2026-07-09)

PR Item Base State
tbd — api Re-key Composio ownerId from bare artistId to the join-row identifier at every call site (getConnectors, getComposioTools, authorizeConnector, executeConnectorAction) main ⬜ not started — blocked on the id-vs-pair design decision below
tbd — api Org-shared resolution: getComposioTools unions the caller's (account, artist) connections with (org, artist) connections when the caller is a member of that org main ⬜ not started — depends on the re-key
tbd — api Restore connections-tab for non-admin customers: revert the authorize gate from P0b's checkConnectorAuthority to a roster-scoped check so a manager can establish their own (account, artist) connection (safe once keyed per-account) main ⬜ not started — depends on the re-key; fixes the live 403 on Connect for non-admins
tbd — ops P2 audit: full Composio connection matrix (account × artist × toolkit) to size the wipe n/a ⬜ not started
tbd — ops P2 wipe-and-reconnect: delete existing Composio connections + require reconnect under the new keying n/a ⬜ not started — after the re-key + audit; destructive, do not run casually

Merge sequencing: re-key first (docs + database prereqs already landed), then org-shared union (needs the re-key's owner-keying in place). Ops last and in order: audit (know who is impacted) → wipe-and-reconnect (migrate). The wipe deletes live customer connections — run it deliberately, with the audit in hand, never before the api re-key is stable on prod.

Open — api

  • Re-key Composio ownerId from the bare artist id to the join row. (→ tbd — api)

    • Why: connections are stored in Composio under ownerId = accountId, and for artist connectors that's the bare canonical artistId (lib/composio/toolRouter/getComposioTools.ts:86) — so two accounts connecting "for" canonical Drake share one Composio bucket that getConnectors(artistId) returns wholesale. P0b blocks using another account's connection, but they still physically share a bucket, and GET /api/connectors can still list status across accounts.
    • ⚠️ Open design decision (blocking — decide + document before implementing): key the ownerId on the join-row id vs. the (account_id, artist_id) pair.
      • Join-row id: survives a same-row artist_id repoint (like the funnel canonical backfill), but a remove+re-add of the roster link mints a new id → orphans the old connection.
      • (account_id, artist_id) pair: stable across remove+re-add, but a dupe→canonical artist_id repoint changes the pair → orphans the connection. (database#46's UNIQUE(account_id, artist_id) makes the pair a safe key.)
      • Pick one, document the tradeoff and the orphan-handling in docs, then implement.
    • Fix: change the ownerId passed to Composio at all four call sites (getConnectors, getComposioTools, authorizeConnector, executeConnectorAction) to the chosen join-row identifier.
    • Done when: getConnectors for artist A returns only the caller's own connections; a second account with A in its roster but no connection of its own sees none of the first account's connections (chat agent tools and GET /api/connectors listing); verified live with two accounts.
  • Org-shared connection resolution. (→ tbd — api)

    • Why: with per-(account, artist) keying, an org that wants a shared connection needs getComposioTools to also resolve (org, artist) connections for members.
    • Fix: getComposioTools unions the caller's (account, artist) connections with (org, artist) connections when the caller is a member of that org.
    • Done when: an org member can use a connection established as an org connection for artist A; a non-member with A only in their roster cannot; verified live. (Accepted for v1: orgs are flat — any org member can establish and use an org connection; precedence when a personal and an org connection collide on one toolkit is deferred.)
  • Restore the connections-tab flow for non-admin customers (Artist Settings → ⋯ → Connections). (→ tbd — api)

    • Why: P0b narrowed POST /api/connectors (authorize) to checkConnectorAuthority (lib/composio/connectors/validateAuthorizeConnectorRequest.ts), which fails closed for a non-admin account that only rosters artist A (no org tie, no ownership). Verified live during P0b: a non-staff account rostering an artist gets GET /api/connectors → 200 (list still works — that path kept the flat check) but POST /api/connectors → 403 (Connect is blocked). P0b blocked it on purpose: under today's bare-artist keying (authorizeConnectorcomposio.create(accountId = A, …), lib/composio/connectors/authorizeConnector.ts:47), establishing a connection "for A" writes into a shared Composio bucket other accounts can see/use — so it could not be safely allowed until connections are per-account. Admin/staff accounts are unaffected (admin bypass), which is why the tab still works for Recoup staff today.
    • Required changes (all safe only after the owner re-key above, because each one then resolves the caller's own (account, A) connection, never a shared/foreign one):
      1. Owner keying — pass the caller's (account, artist) join-row identifier as the Composio owner instead of the bare artist id at authorize (composio.create, authorizeConnector.ts:47), list (getConnectors, getConnectorsHandler.ts:41), execute, and disconnect. (This is the re-key item above; it is the prerequisite for the rest.)
      2. Authorize gate — with connections now private to the caller's join row, allow a caller who rosters artist A to establish their own connection: revert validateAuthorizeConnectorRequest from the P0b checkConnectorAuthority (self/org/staff-only) to a roster-scoped check (e.g. checkAccountArtistAccess). Establishing is safe because it only creates the caller's own (account, A) connection — it cannot touch anyone else's.
      3. ListGET /api/connectors?account_id=A returns the caller's own (account, A) connections (plus org-shared (org, A)), so the tab shows the manager their own connection status — not a foreign/shared one and not an empty list.
      4. Execute / disconnectPOST /api/connectors/actions and DELETE /api/connectors resolve and act on the caller's own (account, A) connection, so a manager can use and remove their own connection. The P0b security invariant is preserved automatically: because the owner key is the caller's join row, a roster-only caller can never resolve — and therefore never use — another account's connection.
    • Done when: signed in as a non-admin account that rosters artist A (no org tie), in Artist Settings → ⋯ → Connections — (a) previously-established connections for that account show as connected; (b) clicking Connect completes the Composio OAuth flow and the new connection is owned by (account, A); (c) the manager can execute an action through it and disconnect it; (d) a different non-admin account that only rosters A sees none of the first account's connections and gets 403/zero tools when trying to use them. Verified live with two non-admin accounts.

Open — P2 ops (audit + migration, gated)

  • Composio connection audit. (→ tbd — ops) Enumerate every existing Composio connection with its ownerId, toolkit, and state, mapped to account/artist. There is no local connections table — requires the Composio API. Deliverable: an account × artist × toolkit matrix so we know exactly who the wipe impacts (expected near-zero beyond sweetman — confirm).
  • Wipe-and-reconnect migration. (→ tbd — ops) After the re-key lands and the matrix is in hand, delete all existing Composio connections and require reconnection under the new join-row keying. This sidesteps the attribution gap (Composio records no establisher for connections currently under bare artist ids). Destructive — deletes live customer connections; run only with the audit in hand and the re-key stable on prod.

Architecture decisions (carried from chat#1860, still binding)

  • Association ≠ authority. account_artist_ids (and org/workspace membership) grants read/analytics only. Act-as-artist authority is connection-scoped. (Already enforced for connector use by P0b; this issue extends it to connection storage/listing.)
  • Artists stay canonical/shared; connections are scoped, not artists. Isolation lives on the connection via the join-row ownerId, not by forking the artist.
  • Connection ownership is self-anchored, keyed on the join row. Private by default to (account, artist); explicitly shareable to (org, artist). Whoever establishes a connection owns it; roster membership never inherits it.
  • Admin override stays, admin-only. The account_id override on connector endpoints is retained for Recoup-staff support, gated on RECOUP_ORG admin.
  • Migration = wipe + reconnect, not attribute-and-remap (Composio records no establisher; near-zero live usage).

Source references

  • Parent: chat#1860 (acute remediation — done) · trigger: chat#1859 (connector map).
  • Shipped prereqs: docs#271, database#46.
  • Code anchors: api/lib/composio/toolRouter/getComposioTools.ts (owner keying, :86), api/lib/composio/toolRouter/fetchOwnerTools.ts, api/lib/composio/connectors/{authorizeConnector,executeConnectorAction}.ts, api/lib/composio/checkConnectorAuthority.ts (P0b authority check, unchanged by this work).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions