You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
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).
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.)
Every existing Composio connection has been migrated onto the new keying (audit → wipe-and-reconnect).
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:
docs — docs#271 (merged 2026-07-09): connection-ownership + authority contract (connectors/ownership.mdx). This is the contract the api below fulfills.
database — database#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 andGET /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 (authorizeConnector → composio.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):
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.)
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.
List — GET /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.
Execute / disconnect — POST /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).
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 (
checkConnectorAuthorityfails 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.)
Goal
Composio connections are keyed on the join row (
account_artist_ids/artist_organization_ids), not on the bare canonicalartistId, so a connection is attributable to the specific(account, artist)or(org, artist)relationship that created it. Concretely, when done:getConnectors/getComposioToolsfor artist A return only the caller's own(account, A)connections plus any explicitly org-shared(org, A)connections — never another account's.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).(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:
connectors/ownership.mdx). This is the contract the api below fulfills.UNIQUE(account_id, artist_id)onaccount_artist_idsandUNIQUE(artist_id, organization_id)onartist_organization_ids. Verified via Supabase MCP: constraints live, 0 rows deduped, duplicate insert rejected with23505. This makes the join row a deterministic key.PRs (updated 2026-07-09)
ownerIdfrom bareartistIdto the join-row identifier at every call site (getConnectors,getComposioTools,authorizeConnector,executeConnectorAction)maingetComposioToolsunions the caller's(account, artist)connections with(org, artist)connections when the caller is a member of that orgmaincheckConnectorAuthorityto a roster-scoped check so a manager can establish their own(account, artist)connection (safe once keyed per-account)mainOpen — api
Re-key Composio
ownerIdfrom the bare artist id to the join row. (→ tbd — api)ownerId = accountId, and for artist connectors that's the bare canonicalartistId(lib/composio/toolRouter/getComposioTools.ts:86) — so two accounts connecting "for" canonical Drake share one Composio bucket thatgetConnectors(artistId)returns wholesale. P0b blocks using another account's connection, but they still physically share a bucket, andGET /api/connectorscan still list status across accounts.ownerIdon the join-rowidvs. the(account_id, artist_id)pair.artist_idrepoint (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→canonicalartist_idrepoint changes the pair → orphans the connection. (database#46'sUNIQUE(account_id, artist_id)makes the pair a safe key.)docs, then implement.ownerIdpassed to Composio at all four call sites (getConnectors,getComposioTools,authorizeConnector,executeConnectorAction) to the chosen join-row identifier.getConnectorsfor 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 andGET /api/connectorslisting); verified live with two accounts.Org-shared connection resolution. (→ tbd — api)
(account, artist)keying, an org that wants a shared connection needsgetComposioToolsto also resolve(org, artist)connections for members.getComposioToolsunions the caller's(account, artist)connections with(org, artist)connections when the caller is a member of that org.Restore the connections-tab flow for non-admin customers (Artist Settings → ⋯ → Connections). (→ tbd — api)
POST /api/connectors(authorize) tocheckConnectorAuthority(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 getsGET /api/connectors → 200(list still works — that path kept the flat check) butPOST /api/connectors → 403(Connect is blocked). P0b blocked it on purpose: under today's bare-artist keying (authorizeConnector→composio.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.(account, A)connection, never a shared/foreign one):(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.)validateAuthorizeConnectorRequestfrom the P0bcheckConnectorAuthority(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.GET /api/connectors?account_id=Areturns 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.POST /api/connectors/actionsandDELETE /api/connectorsresolve 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.(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 gets403/zero tools when trying to use them. Verified live with two non-admin accounts.Open — P2 ops (audit + migration, gated)
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).Architecture decisions (carried from chat#1860, still binding)
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.)ownerId, not by forking the artist.(account, artist); explicitly shareable to(org, artist). Whoever establishes a connection owns it; roster membership never inherits it.account_idoverride on connector endpoints is retained for Recoup-staff support, gated on RECOUP_ORG admin.Source references
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).