Repository navigation
Let a deployment hold its own client for Composio apps that need one (Shopify) - #950
Conversation
…(Shopify) Composio publishes Shopify with OAUTH2 and S2S_OAUTH2 only and no managed auth, so OpenBot hid it from the picker and refused it at enable. S2S_OAUTH2 is a client-credentials grant: Composio exchanges a client id and secret for the token itself, with no consent screen, so all it needs is the deployment's own client. That client is read from COMPOSIO_<SLUG>_CLIENT_ID and COMPOSIO_<SLUG>_CLIENT_SECRET, both or nothing. While it is held, S2S_OAUTH2 is offered as a field scheme, ranked after a plain key. The auth config is created with the client as its credentials, and the connection is created without status ACTIVE, which Composio refuses for the missing access_token; left out, Composio performs the exchange itself. Without the client the app stays unsupported, and the refusal names the two variables. Fixes #885 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
guidovizoso
left a comment
There was a problem hiding this comment.
The mechanics look right. S2S_OAUTH2 is only offered while both variables are set, it ranks last behind a plain key, the connection is created without status: "ACTIVE", a typed status is dropped, and the secret stays in the server's environment and on the config at Composio. One thing I'd like resolved before this merges.
1. Who can connect an S2S app (blocking)
As far as I can tell, an app connected with typed values can be connected as a personal account by anyone who sees it enabled (connectBrokeredWithFields: "Connect this person with the secret they typed", with a holder on the account). That's fine for a key app, because the secret is the person's own. With S2S_OAUTH2, the person types only the store subdomain, and the credential is entirely the deployment's client. So any user could connect "their" Shopify and get the full store access of the organization's Shopify app, without an administrator granting it to them. The audit trail would then attribute that to them as their own account.
Could S2S apps be restricted to shared accounts that an administrator connects and then grants to Bots the usual way? If personal connections are intended, could you say why they're acceptable here?
2. Rotating the client (docs)
ensureAuthConfig reuses a standing config, so changing COMPOSIO_<SLUG>_CLIENT_SECRET later doesn't update the config already at Composio. Please document how to rotate it, for example by disabling and re-enabling the app or by updating the config at Composio.
3. Nit: the empty-client fallback
deploymentClientOf(toolkit) ?? {} would create an S2S config with no client if the variables disappeared between the directory read and the enable. The comment says that path isn't reachable. Refusing there would make that explicit rather than relying on the comment.
…ment-client # Conflicts: # docs/configuration.md
An S2S_OAUTH2 app signs in with the deployment's OAuth client, so a personal account on it would be the organization's access under one person's name. The connect route and the store now refuse a personal account for such an app, the listing carries `sharedOnly`, and the Connections and admin Plugins pages offer no personal Connect (and no switch back to personal once shared). Enabling an S2S app whose client variables have gone is refused by name instead of creating a config with no client. docs/configuration.md says how to rotate the client, since a standing config is reused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, all three addressed in 632186a:
Could you take another look? |
…ment-client # Conflicts: # CHANGELOG.md
What this changes
Fixes #885.
Composio publishes Shopify with
OAUTH2andS2S_OAUTH2only, and manages no auth for it. OpenBot read that asunsupported, so Shopify was hidden from the picker and enabling it answered 503 ("needs an OAuth application registered by whoever runs this deployment…").S2S_OAUTH2is a client-credentials grant: Composio exchanges a client id and secret for the token itself, with no consent screen and no redirect. All OpenBot was missing was a place for the deployment's own client.COMPOSIO_<SLUG>_CLIENT_ID/COMPOSIO_<SLUG>_CLIENT_SECRET(COMPOSIO_SHOPIFY_CLIENT_ID, …; a dash in the slug becomes_). Both or nothing: one without the other is no client. This lives in the newserver/src/plugins/deployment-clients.ts.S2S_OAUTH2is a field scheme, but only while that client is held. It is added toFIELD_SCHEME_NAMESand ranks last inFIELD_SCHEME_ORDER, so an app that also takes a plainAPI_KEYstill asks for the key. Managed consent still wins over it.connectionOfdrops the scheme when the client is missing, so such an app staysunsupportedand hidden as before. Its refusal now names the two variables to set.credentials: { client_id, client_secret }(ensureAuthConfig) rather than{}. Thecredentialstype on the vendor seam is widened for this one case.status: "ACTIVE"instate.valforS2S_OAUTH2(connectWithFields). With it, Composio refuses (access_token: Required), because ACTIVE claims the token is already there. Without it, Composio performs the client-credentials exchange itself. On this branch a typed value namedstatusis dropped, so the existing guarantee still holds: no form value can set the state Composio is asked for.FIELD_SCHEMESinapp/src/components/plugins/brokered-account-row.tsxgets the scheme too, so the row draws the connect form (for Shopify, Composio's ownsubdomainfield) rather than sending the person to a consent screen.docs/configuration.md,docs/plugins/composio.md("The 56 that are not offered" now names this one exception) and.env.example.Out of scope: the post-connect identity check, which can withdraw a Shopify connection whose token is fine, is #886 (outside PR #905). Nothing here touches
IDENTITY_ACTION.Verified live against Composio and a real Shopify dev store, using the same change on an older OpenBot build: enable, connect with the store subdomain, list orders, refund. That run is where the
access_token: Requiredrefusal withstatus: "ACTIVE", and the exchange succeeding without it, were observed.Where it runs
COMPOSIO_API_KEYis. The auth config it creates lives at Composio, as every config already does.COMPOSIO_API_KEY. A replica missing it would show the app as unsupported and refuse to enable it with a sentence naming the variables. It would not create a broken config.ensureAuthConfigpath unchanged.Boundary and audit
unsupportedrefusal, now with a sentence naming the variables.Changelog
CHANGELOG.mdunderUnreleased("Shopify and other Composio apps on a server-to-server grant can be connected"). Unset, nothing changes for an existing deployment.Proof
server/tests/composio-deployment-client.test.ts: with the client set, Shopify's row is{ kind: "fields", authScheme: "S2S_OAUTH2" }. Without it, it isunsupportedand the reason namesCOMPOSIO_SHOPIFY_CLIENT_ID and COMPOSIO_SHOPIFY_CLIENT_SECRET. Half a client is still unsupported. A plainAPI_KEYis still preferred, and managed consent still wins. Blank or half pairs read as no client, values are trimmed, and a dashed slug maps to an underscored variable.server/tests/composio-adapter.test.ts: the auth config for an S2S app is created withcredentials: { client_id, client_secret }. The connection is created withval: { subdomain }: nostatus, and a typedstatusis dropped.bun test server/testsagainst a fresh pgvector/pg17 database migrated withscripts/migrate.ts: 6176 pass, 18 skip, 5 fail. The 5 failures (browser-routes.integration,learning-mastra,learning-langgraph) fail identically onmainin this environment (no built Recorder or Bot dependency trees) and are unrelated.bun test app/tests/brokered-account-row.test.tsx app/tests/connection-fields.test.tsx app/tests/shared-connected-accounts.test.tsx: 51 pass.bunx biome formatandbunx biome lint --error-on-warningson the changed files: clean.tsc --noEmitforserverandapp: clean.bun run typecheck:tests: clean against the baseline.composio-live(needs a Composio key), and the live Shopify flow on this exact branch (it was run on the older build the change was ported from).🤖 Generated with Claude Code