Six code-review nits from the multi-agent review on PR #3412:
P1.1 — python/setup.py was still pinning composio-client==1.36.0 while
pyproject.toml is on 1.39.0. The Experimental TypedDict that the new
link() docstring imports only exists in 1.39+, so anyone installing
via the legacy setup.py path would crash at import. Bumped to 1.39.0.
P1.2 — exceptions.py ComposioSharedAccessDeniedError and
ComposioSharedConnectionNotAccessibleError docstrings told users to
call composio.connected_accounts.update_acl(), which no longer exists.
Rewrote both to composio.experimental.update_acl().
P2.5 — ExperimentalAPI.update_acl returned t.Any; restored the typed
ConnectedAccountPatchResponse return so callers reading .id / .status /
.success keep type-safety at the API boundary.
P2.6 — ExperimentalAPI.__init__(client: Optional[Any]) tightened to
Optional[HttpClient]. The None branch stays for the one test that
exercises it; production code in sdk.py always passes a real client.
P2.7 — Dropped redundant alias=omit / connection=omit on the patch()
call. Verified against the composio-client==1.39.0 wheel: both default
to omit. Sibling ConnectedAccounts.update() already follows the cleaner
pattern. Test assertions updated to match.
P2.10 — Extracted the substring "acl_config_for_shared is only valid on
SHARED" into ACL_ONLY_FOR_SHARED_ERROR_FRAGMENT in experimental.py, and
reused it from connected_accounts.py + tool_router_session.py. Single
source of truth so a server-side message tweak doesn't silently
downgrade three call sites to generic BadRequestError.
Tests: 201/201 pass. mypy on composio/ clean.
Pushed back on the remaining P2 (TS divergence) in the review — the
companion TS PR #3424 ships the exact same restructure, so parity is
preserved once both land together.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ExperimentalAPI was sitting inside custom_tool.py because custom tools
were the first thing the SDK exposed on the composio.experimental
namespace. Now that update_acl has moved onto the same namespace,
keeping the class in custom_tool.py reads wrong — grepping for
update_acl lands in a file named after custom tools.
Splits the class out into core/models/experimental.py. custom_tool.py
keeps the custom-tool machinery (ExperimentalToolkit, decorator
helpers, serializers); experimental.py imports what it needs from
custom_tool.py and is now the home for anything on the
composio.experimental namespace.
Pure rearrangement — no behaviour change, no public-import-path change
(composio.core.models still re-exports ExperimentalAPI). Stack-frame
depth in _get_caller_locals(depth=2) stays correct: the
decorator → user-module hop is the same regardless of which file
ExperimentalAPI.tool lives in.
Tests still pass (201/201), mypy still clean on composio/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the Python SDK with the experimental wire shape used by Shared
Connections. The flat `account_type` / `acl_config_for_shared` kwargs on
`link()` and `authorize()` have moved under a single `experimental` dict,
and `update_acl` has moved off `composio.connected_accounts` onto the
`composio.experimental` namespace — same precedent as
`composio.experimental.tool` / `composio.experimental.Toolkit`.
The `experimental` namespace is the signal that the shape may change in
future releases. Pinning a SHARED connection in a session config and
direct execute by `connectedAccountId` are unchanged — only the
connection-create / patch / authorize surfaces are namespaced.
Also surfaces the `account_type=` filter on `composio.connected_accounts.list()`
so SHARED connections can be listed without dropping to the raw client.
The wire keeps this as a flat query param (`?account_type=`), so the
SDK keeps it flat too with the experimental signal carried in the value
enum description.
Caller migration:
# before
composio.connected_accounts.link(
user_id, auth_config_id,
account_type="SHARED",
acl_config_for_shared={"allow_all_users": True},
)
composio.connected_accounts.update_acl(
"ca_abc", allow_all_users=True,
)
session.authorize(
"github",
account_type="SHARED",
acl_config_for_shared={"allow_all_users": True},
)
# after
composio.connected_accounts.link(
user_id, auth_config_id,
experimental={
"account_type": "SHARED",
"acl_config_for_shared": {"allow_all_users": True},
},
)
composio.experimental.update_acl(
"ca_abc", allow_all_users=True,
)
session.authorize(
"github",
experimental={
"account_type": "SHARED",
"acl_config_for_shared": {"allow_all_users": True},
},
)
# new — list SHARED connections
shared = composio.connected_accounts.list(
account_type="SHARED",
user_ids=["user_creator"],
)
composio-client bumped from 1.38.0 -> 1.39.0 so the generated typed
client carries the Experimental TypedDicts for link.create,
tool_router.session.link, and connected_accounts.patch.
Tests cover: experimental block forwarding (link + authorize),
no-op when omitted, empty-list preservation, typed error mapping for
the PRIVATE-with-ACL case, experimental.update_acl body construction +
deny-list handling + ValidationError when no fields are provided +
ValidationError when called without a bound client, and
list(account_type="SHARED") flat-filter delegation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses 2 review comments from @abir-taheer on #3401:
- tests/test_connected_accounts.py:546 — remove Hermes PR reference
- models/connected_accounts.py:708 — update_acl docstring too long;
match the style of other methods in the file (cf. update())
Swept the full PR diff for the same patterns rather than just the two
flagged lines:
- Removed Hermes PR numbers and TS-test cross-reference from
TestConnectedAccountsAcl class docstring.
- Trimmed update_acl docstring — kept ~10 lines (one-line description,
param list, one example) instead of the original ~40 lines with the
resolution-rule table, multiple examples, and verbose footgun-warning
block. Matches the style of update() / delete() / refresh() in the
same file. Resolution rule + per-list caps stay documented on
ConnectedAccountAclConfigSchema (where the contract belongs).
- Removed internal backend error codes from exception class docstrings
(``ConnectedAccount_AclOnlyForShared``,
``ConnectedAccount_SharedAccessDenied``,
``ToolRouterV2_SharedConnectionNotAccessible``). Surfacing the
internal code names leaks implementation details.
- Reworded "users in the project" / "project member" / "Open access to
everyone in the project" — users don't belong to a project entity in
Composio's data model. Replaced with phrasing focused on ``user_id``
and the pinning mechanism.
- Removed "Set at create time only" wording from the link() param
description on connected_accounts.py and tool_router_session.py (the
corresponding upstream API description has the same wording removed).
- Cleaned inline catch comments referencing internal error code names.
No behaviour changes — docstring / comment cleanup only.
Verified: nox -s chk clean, ruff format --check clean, 49/49 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI / review issues on #3401:
1. "Lint and Format Check" failed — `ruff format --check` flagged
`composio/core/models/tool_router_session.py` and
`tests/test_connected_accounts.py`. Local `nox -s chk` only runs
`ruff check` (lint), not `ruff format --check`, so the gap slipped
through. Ran `ruff format` on both.
2. Bugbot caught uv.lock drift — pyproject.toml bumped to 0.14.0 but
uv.lock still recorded 0.13.0 from when the dep bump commit was
created. Ran `uv lock` to regenerate; the lockfile now records 0.14.0.
No behaviour changes — formatting + lockfile sync only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Python mirror of #3392 (TypeScript SDK). Same Apollo-side feature shipped
across four Hermes PRs (#9860, #9882, #9887 internal, #9902), now exposed
through @composio/* Python SDK wrappers.
Bumped composio-client 1.37.0 → 1.38.0 (the regenerated client published
by composio-base-py#66 — contains account_type + acl_config_for_shared on
LinkCreateParams, ConnectedAccountPatchParams, SessionLinkParams, the
retrieve response, and the list response item).
SDK changes:
- composio.connected_accounts.link() now accepts `account_type` and
`acl_config_for_shared`. Default behaviour (omit both) creates a
PRIVATE connection exactly as before.
- New composio.connected_accounts.update_acl(nanoid, *, allow_all_users,
allowed_user_ids, not_allowed_user_ids) — wraps PATCH /connected_accounts/{id}
with the same semantics as the TS sibling. PATCH semantics: omit a
param to leave unchanged; pass [] to clear an allow/deny list. Raises
ValidationError if all three are None.
- composio.tool_router_session.authorize() options gain `account_type`
and `acl_config_for_shared` — the /tool_router/session/{id}/link
endpoint accepts the same fields.
- New typed errors in composio.exceptions:
* ComposioAclOnlyForSharedError (400) — wired at link() / update_acl()
/ authorize() catch sites via the same
`acl_config_for_shared is only valid on SHARED` substring match used
in the TS SDK. Verified against Apollo's createConnectedAccount.ts.
* ComposioSharedAccessDeniedError (403) — exported, not yet wrapped.
Wraps when Tools.execute() error mapping lands in a follow-up.
* ComposioSharedConnectionNotAccessibleError (400) — exported, not
yet wrapped. Wraps when ToolRouterSession.create()/patch() session-
validator error mapping lands.
Plumbing:
- python/composio/client/types.py re-exports `link_create_params` (the
generated client's TypedDicts for link create payloads, including
`ACLConfigForShared`).
Tests: 12 new tests in TestConnectedAccountsAcl class covering link()
ACL forwarding (4), AclOnlyForShared mapping on link() (1), pass-through
of unrelated BadRequestError (1), update_acl() body construction (3),
empty-fields rejection (1), AclOnlyForShared mapping on update_acl() (1),
pass-through of unrelated BadRequestError on update_acl() (1). 49 tests
pass (37 pre-existing + 12 new); full suite 635 pass / 31 skipped.
Verified: nox -s chk clean (ruff + mypy on src + tests).
Version: 0.13.0 → 0.14.0 (minor — additive surface).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
initiate() previously emitted a one-time deprecation warning whenever the
response carried a redirectable OAuth scheme (OAUTH1, OAUTH2, DCR_OAUTH),
regardless of whether the auth config was Composio-managed (subject to the
2026-07-03 cutover) or custom (unaffected). The wording was conditional —
'If this auth config is Composio-managed…' — so callers using their own
OAuth apps could ignore it, but the warning still printed and added noise
to logs.
Apollo already emits the SEC-339 'Deprecation' / 'Sunset' / 'Link
rel="deprecation"' headers (RFC 9745 / RFC 8594) only on the retiring
branch — managed + redirectable OAuth + LD didn't block. The SDKs now
read the 'Deprecation' header from the response and gate the warning on
its presence. Custom auth configs and non-OAuth schemes get a clean
response from the server and now stay silent in the SDK as well.
TS:
- Chain '.withResponse()' on the underlying client.connectedAccounts.create
call to expose the raw Response. Defensive fallback for tests that mock
create() as a plain Promise.
- Drop the LEGACY_RETIRING_OAUTH_SCHEMES set; the apollo header is now the
sole signal.
- New tests in describe('initiate deprecation header gate', ...) pin the
contract: warns when Deprecation is present, silent when absent, and
one-time-per-process even across multiple managed-OAuth calls. A
mockApiPromiseWithHeaders helper wraps mocked values as APIPromise-shape
thenables so the new tests can simulate apollo's header behavior.
Python:
- Switch to client.connected_accounts.with_raw_response.create so we can
read raw.headers.get('Deprecation'). Defensive fallback to the parsed-
only path for test mocks that don't stub with_raw_response.
- Drop _LEGACY_RETIRING_OAUTH_SCHEMES; same simplification as TS.
- New TestInitiateDeprecationHeaderGate class with the same three pinning
cases. A _set_initiate_response helper routes mock responses through
the with_raw_response surface that initiate() now consumes. Pre-existing
initiate tests migrated to the helper; one assertion that inspected
create.call_args.kwargs now reads with_raw_response.create.call_args.kwargs.
No public API change for either SDK — initiate() returns the same
ConnectionRequest shape and respects the same allowMultiple guard.
ComposioLegacyConnectedAccountsEndpointRetiredError continues to surface
from the 400 retired-path response.
Changeset bumps @composio/core as a patch.
Net −56 lines of comment prose. Earlier commits added multi-paragraph
blocks explaining each forward-compat guard, sentinel choice, and
schema-bypass cast — most of that is rederivable from the surrounding
code. Keep one-line WHY notes where the intent is non-obvious; drop
the prose elsewhere.
`test_wait_for_connection_fails_fast_on_terminal_status` only patched
`time.sleep`, not `time.time`. A stuck CI runner could in principle
exhaust the 10s deadline before the first `retrieve()` call resolves,
in which case the test would assert against a
`ComposioSDKTimeoutError` instead of the `SDKError` it's actually
exercising. Match the monotonic-counter pattern used by the sibling
tests at lines 137 and 170 to remove that flake surface.
Surfaced by kieran-python review of PR #3344 (P3#4).
Adds the test coverage flagged in PR #3344 review item P1#5:
TS (`ts/packages/core/test/connectedAccounts/connectedAccountAuthStates.test.ts`):
- `Oauth2RevokedConnectionDataSchema` parses with and without optional
`revoked_at`
- `ConnectionDataSchema` discriminated union accepts REVOKED for both
OAUTH2 and S2S_OAUTH2 (the preempted Apollo arm from P1#1)
- `transformConnectedAccountResponse` does not silently drop `state`
for REVOKED on either auth scheme
- Closure test on the `ConnectionStatuses` enum so a future Apollo
addition trips a CI failure on purpose
TS CLI (`connected-accounts.list.cmd.test.ts`):
- Server returning an unrecognized status no longer crashes the
command — the `catchTag('ParseError')` fallback warns the user to
run `composio upgrade` and renders the row anyway
Python (`tests/test_connected_accounts.py`):
- `wait_for_connection` fails fast on FAILED / EXPIRED / REVOKED
(parameterized) — the contract added by this PR
- `wait_for_connection` does NOT treat INACTIVE as terminal, so a
connection briefly transiting through INACTIVE still recovers to
ACTIVE — guards against the regression called out in P1#2
Bring `link()` to parity with `initiate()` so the duplicate-connection
guard moves with customers as Composio-managed redirectable-OAuth
callers migrate from `connected_accounts/create` to
`connected_accounts/link` (SEC-339).
- TS: add `allowMultiple?: boolean` to `CreateConnectedAccountLinkOptions`.
`link()` now pre-flights `connectedAccounts.list({ statuses: ['ACTIVE'] })`
and throws `ComposioMultipleConnectedAccountsError` when an active
connection exists and `allowMultiple` is not `true`.
- Python: add `allow_multiple: bool = False` to `connected_accounts.link()`
with the same guard and the same `ComposioMultipleConnectedAccountsError`.
- Tests: existing `link()` tests now mock `list` returning empty (default
no-existing-connection path); new tests cover the raise path and the
opt-in `allowMultiple=True` skip path. Also add multi_account parity
tests on the Python tool_router create() to mirror the TS suite.
Behavior change is intentional and patched separately with
`.changeset/sdk-link-allow-multiple.md`. The user-facing changelog entry
is split into a separate docs PR alongside the sandbox-size docs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The composio-client 1.31.0 stainless types don't include alias on
.create(), .link.create(), or .session.link() yet. Use dict-splat
for alias on those methods, and omit sentinel for .patch() params.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zen Agent <zen@composio.dev>
The link() method now passes callback_url=None directly instead of
omitting the kwarg. The stainless client handles None by not sending
the field, so the behavior is identical.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zen Agent <zen@composio.dev>
Add tests verifying that passing an explicit status in OAuth1 params
overrides the auto-detected value, mirroring the existing OAuth2
override tests in both TypeScript and Python.