Commit Graph

384 Commits

Author SHA1 Message Date
Alberto Schiabel 4367513f82 fix(python): guard enhance_schema_descriptions against empty schemas (#3398)
Stacked on #3397 · refs
https://github.com/ComposioHQ/composio/issues/3354

## Summary

The TS fix in #3397 handled the reported case (`output_parameters: {}`
from MCP toolkits) by normalizing empty schemas at `transformToolCases`.
While verifying the Python SDK didn't have the equivalent Zod check, I
found an **adjacent** crash on the input side.

The Python SDK does not have the same Pydantic check** —
`composio_client` types `input_parameters` / `output_parameters` as
`Dict[str, Optional[object]]`, which accepts `{}` trivially.
However, `Tools._get()` pipes every fetched tool's `input_parameters`
unconditionally through `FileHelper.enhance_schema_descriptions`
(`python/composio/core/models/tools.py:361-363`), which does:

```python
for _param, _schema in schema["properties"].items():
```

That crashes with `KeyError: 'properties'` on `schema={}`. Not reachable
for the specific `granola_mcp` tools in the original report (they all
have populated `input_parameters`), but it's the same API contract — any
MCP tool with no required inputs would hit this through the public
`Composio.tools.get(...)` call.

The sibling `FileHelper.process_file_uploadable_schema`
(`_files.py:760`) already guards this with `if "properties" not in
schema: return schema`. This applies the same guard to
`enhance_schema_descriptions`.

## Fix

```diff
 def enhance_schema_descriptions(self, schema: t.Dict) -> t.Dict:
     ...
+    if "properties" not in schema:
+        return schema
     required = schema.get("required") or []
     for _param, _schema in schema["properties"].items():
```

## Testing

Red → green confirmed locally. New
`TestEnhanceSchemaDescriptionsEmptySchema` class in
`tests/test_files.py` covers:

1. `schema={}` → returns `{}` (the bug repro).
2. Schema with metadata but no `properties` key → returned unchanged
(mirrors the sibling method).
3. Schema with `properties: {}` (empty dict) → already works today;
pinning behavior.
4. Populated schema → still enhanced (regression guard for the type-hint
/ required-marker enhancement).

Cases 1–2 **fail on `next`** with `KeyError: 'properties'`. All 4 pass
with the guard. Full `tests/test_files.py` stays green (116 tests).

```bash
cd python
.venv/bin/python -m pytest tests/test_files.py::TestEnhanceSchemaDescriptionsEmptySchema -v   # → 4 passed
.venv/bin/python -m pytest tests/test_files.py -q                                              # → 116 passed
```

## Stacked PR notes

- Base: `jkomyno/fix-mcp-empty-output-parameters` (#3397).
- This PR's diff against that base shows **only** the Python changes
(`_files.py` + `tests/test_files.py`).
- After #3397 merges, this should be retargeted to `next` (or rebased
automatically by GitHub).

---------

Co-authored-by: jkomyno <12381818+jkomyno@users.noreply.github.com>
2026-05-17 01:03:33 +04:00
Alberto Schiabel 5dcb363446 docs(py): add 0.13.1 entry to CHANGELOG (#3427)
Adds a `0.13.1` entry to `python/CHANGELOG.md` covering the changes
accumulated on `next` since the last Python release (`py@0.13.0`, May
7).

The CHANGELOG had been quietly abandoned since `0.8.11` (Sept 2025);
rather than try to backfill the 4-minor gap, this PR draws a line and
adds a banner pointing readers at the git log for that window. Future
releases start fresh from here.

## What's in 0.13.1

- **SHARED connected accounts (experimental)** —
`composio.experimental.update_acl`, `experimental={...}` on `link()` /
`session.authorize()`, `account_type` filter on `list()`. Mirrors
`@composio/core@0.10.0` shipping in #3425.
- **`initiate()` deprecation warning** — gated on response `Deprecation`
header (RFC 9745) instead of `auth_scheme`, eliminating false positives
for custom auth configs and non-OAuth schemes.
- **Schema converter** — preserves nullability on `null`-only `anyOf`
blocks.
- **Dep bump** — `composio-client` 1.36.0 → 1.39.0.

## Release plan

Documentation only — does NOT trigger a release on its own. The Python
release flow is tag-triggered:

```bash
# After this PR merges and after #3425 ships:
git tag py@0.13.1 <next HEAD sha>
git push origin py@0.13.1
```

That fires `.github/workflows/py.release.yml` which builds and publishes
to PyPI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: jkomyno <12381818+jkomyno@users.noreply.github.com>
2026-05-14 09:32:03 +02:00
Shrey Singla fccec8b143 fix(python-sdk): handle file upload list unions (#3373)
## Summary

This updates the Python SDK's automatic file upload/download
substitution so it can handle tool schemas that accept either a single
file or a list of files.

Tools can expose file inputs as a union such as:

```json
{
  "anyOf": [
    { "type": "object", "file_uploadable": true },
    { "type": "array", "items": { "type": "object", "file_uploadable": true } }
  ]
}
```

When auto-upload is enabled, the SDK presents those file-uploadable
objects to models as file path strings. If the model supplies a list of
local paths, the old resolver selected the first file-bearing union
branch, usually the single-file branch, and attempted to upload the
entire Python list as one path. That fails before the backend ever
receives the intended list of staged file descriptors.

## Changes

- Refactors upload substitution into a value/schema recursive walker via
`_substitute_file_upload_value`.
- Adds the same value/schema traversal shape for downloads via
`_substitute_file_download_value`.
- Selects composed-schema variants by runtime shape when possible, so
list values choose array branches and string/dict values keep the
single-value behavior.
- Preserves the existing fallback behavior: if no runtime shape matches,
use the first file-bearing variant.
- Treats `anyOf`, `oneOf`, and `allOf` consistently with the SDK's
existing composed-schema convention.
- Preserves root request/response dict mutation behavior for existing
call sites.
- Adds regression coverage for single-file vs multi-file unions, nested
array-item unions, download parity, and first-match fallback.

## Why

This is needed for file-capable tool schemas that are backward
compatible at the API level by accepting both a single file and multiple
files. The Python SDK should transform a list of local paths into a list
of staged `{name, mimetype, s3key}` descriptors, just as it already does
for direct array schemas.

## Verification

- `python -m ruff check python/tests/test_files.py
python/composio/core/models/_files.py`
- `python -m pytest python/tests/test_files.py -q` -> 119 passed
- `python -m pytest python/tests/test_files.py
python/tests/test_auto_upload_download_files.py
python/tests/test_upload_dir_allowlist.py
python/tests/test_tool_router_session_files.py -q` -> 166 passed
- Manual playground verification with a Gmail send using two local
attachments: both files were staged and sent successfully as an
attachment array.

Note: a full local `python/tests` run still has three unrelated failures
because `composio_langchain` is not installed in this environment; the
affected file-upload suites pass.

---------

Co-authored-by: Zen <zen@composio.dev>
2026-05-14 12:57:26 +05:30
venkat82 44972d90ad Merge remote-tracking branch 'origin/next' into feat/connected-accounts-experimental-py
# Conflicts:
#	python/composio/utils/schema_converter.py
2026-05-13 18:30:01 +05:30
venkat82 1645634866 fix(schema-converter): silence mypy on null-only-anyOf fallback return
The `return t.Optional[t.Any] if has_null else str` line introduced in
630038ab8 returns a union of `<typing special form>` and `type[str]`,
which mypy can't reconcile with the function's declared `t.Type` return.

Same `# type: ignore` pattern already used three other times in this
function (lines 280, 285, 287) for the identical reason — `t.Optional`
isn't a `type[T]`, it's a typing special form.

Unblocks Checks/common-checks on this PR and any other PR cut after
630038ab8 landed on next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:26:47 +05:30
venkat82 ed3bb936d2 fix(connected-accounts-py): address review feedback on #3412
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>
2026-05-13 18:22:07 +05:30
Abir Taheer d676cd8b30 chore(py): bump all packages to 0.13.1
Patch release matching TS 0.9.1 — includes account_type + per-user ACL
on SHARED connections, SEC-339 deprecation header fix, nullability fix
in schema converter, and docstring cleanup.

Core was erroneously bumped to 0.14.0 in #3401 without a release; since
0.14.0 was never published to PyPI, all packages align at 0.13.1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 16:47:39 -07:00
Yizuki_Ame 630038ab83 fix: preserve nullability in null-only anyOf and fix validate param alias
Merge PR #3173 as the preferred fix for the null-only anyOf schema behavior and validate alias handling.
2026-05-12 18:20:05 +02:00
venkat82 38620df152 refactor(experimental-py): move ExperimentalAPI to its own module
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>
2026-05-12 20:53:01 +05:30
venkat82 706b01ce75 feat(connected-accounts-py): namespace SHARED-connection surface under experimental
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>
2026-05-12 20:43:25 +05:30
venkat82 613d3804af docs(py): drop residual "at create time" wording in error class docstrings
Two more spots leaking the same immutability wording removed from the
upstream API description:

- ComposioAclOnlyForSharedError docstring said "sent on a PRIVATE
  connection — at create time or via PATCH" and the Fix suggested "set
  account_type='SHARED' at create time". Reworded to drop the
  "at create time" assertion.
- ComposioSharedConnectionNotAccessibleError docstring said "caught at
  session-create time". Replaced the "caught" framing with "Raised at
  session-create time" — "session-create time" stays because it's
  user-facing (tells the SDK caller when the error is raised, i.e.
  during session.create() / session.patch(), not later during
  execution).

Also tightened the surrounding wording — class doc cross-references the
actual SDK method names (``tool_router.session.create()`` /
``session.patch()``) instead of abstract phrasing.

No behaviour changes — docstring cleanup only.

Verified: ruff check + format clean, nox -s chk clean, 49 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:28:00 +05:30
venkat82 d27eecce20 docs(py): sanitize public docstrings — remove internal references
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>
2026-05-12 01:21:10 +05:30
venkat82 fafaa55f88 fix(py): ruff format the new files + regenerate uv.lock for 0.14.0
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>
2026-05-12 00:30:52 +05:30
venkat82 b283fbbccc feat(connected-accounts-py): surface account_type + per-user ACL on SHARED connections
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>
2026-05-12 00:20:01 +05:30
Composio c358ffaa3b fix(connected-accounts): SEC-339 gate initiate() warning on response Deprecation header
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.
2026-05-08 15:18:03 +05:30
Dhawal Upadhyay 156753245c chore(py): bump sdk packages to 0.13.0 2026-05-07 19:17:57 +05:30
Dhawal Upadhyay b48711c587 Clarify custom preload use behavior 2026-05-07 16:44:22 +05:30
Dhawal Upadhyay ffb7539bb5 Fix ToolRouter example staging runs 2026-05-07 16:42:08 +05:30
Dhawal Upadhyay 0c0d535275 fix(python): use generated custom tool preload types 2026-05-07 16:10:38 +05:30
Dhawal Upadhyay bccd32beae Refine ToolRouter preload docs and constants 2026-05-07 16:10:38 +05:30
Abir Taheer c96f094b15 feat(core): add session.update() for partial config updates (#3362)
## Summary
- Adds `session.update()` method to both TypeScript and Python SDKs
- Calls the existing `PATCH /api/v3/tool_router/session/{id}` endpoint
- Accepts the same config shape as `create()` (toolkits, tools, tags,
auth_configs, connected_accounts, manage_connections, workbench,
multi_account, preload)
- Mutates the session object in-place (preload, configVersion, warnings)

## Usage

### TypeScript
```typescript
const session = await composio.create('user_123', {
  toolkits: ['gmail'],
  manageConnections: true,
});

await session.update({
  toolkits: { enable: ['gmail', 'github'] },
  workbench: { enable: false },
});
```

### Python
```python
session = composio.create(user_id='user_123', toolkits=['gmail'])

session.update(
    toolkits={'enable': ['gmail', 'github']},
    workbench={'enable': False},
)
```

## Test plan
- [x] TypeScript: `tsc --noEmit` passes
- [x] TypeScript: all 874 existing tests pass
- [x] Python: mypy passes
- [x] Python: ruff lint + format clean
- [x] Python: all 82 tool_router tests pass
- [ ] Manual test: create session, call `session.update()`, verify
config changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-07 03:39:14 -07:00
Alberto Schiabel 0fd9196515 feat(connected-accounts): support REVOKED status added in Apollo #9550 (#3344)
# Description

Apollo (hermes#9550) added a new \`REVOKED\` value to the
connected-account status enum, alongside the existing \`INITIALIZING |
INITIATED | ACTIVE | FAILED | EXPIRED | INACTIVE\`. The Apollo
public-API response schema is \`z.nativeEnum(ConnectionStatusEnum)\`, so
any list / get / status-update endpoint can now return \`REVOKED\` and
clients must handle it.

This PR fixes every hand-written place in the TS and Python SDKs that
hard-coded the prior 6-value set and would either (a) fail-fast against
\`REVOKED\` (Zod / Effect Schema parse error) or (b) silently
misclassify it.

## TypeScript SDK — BREAKING fixes

| File | Change | Severity |
|---|---|---|
| \`ts/packages/core/src/types/connectedAccounts.types.ts\` | Add
\`REVOKED\` to \`ConnectedAccountStatuses\` and
\`ConnectedAccountStatusSchema\` (\`z.enum\`) | BREAKING —
\`z.enum().parse()\` would throw on responses containing the new value |
| \`ts/packages/cli/src/models/connected-accounts.ts\` | Add \`REVOKED\`
to Effect \`Schema.Literal(...)\` for \`ConnectedAccountItem.status\` |
BREAKING — \`Schema.Literal()\` enforces literal matching |
| \`ts/packages/cli/src/services/tool-router-session-connections.ts\` |
Add \`'REVOKED'\` case to the \`normalizeConnectedAccountStatus\` switch
| BREAKING — silent fall-through to \`'INACTIVE'\` masked the new status
|
| \`ts/packages/cli/src/services/composio-clients.ts\` | Replace the
explicit 6-value cast with a generic that resolves to whatever the
Stainless client currently accepts | Forwards \`REVOKED\` to Apollo;
auto-tracks future Stainless regenerations |
| \`ts/packages/core/src/models/ConnectedAccounts.ts\` | Cast
\`statuses\` to the Stainless params type so \`REVOKED\` (already
accepted by Apollo) flows through until \`@composio/client\` is
regenerated | Adapter shim; remove once \`@composio/client\` is
regenerated |

## Python SDK — DEGRADED fixes

| File | Change | Severity |
|---|---|---|
| \`python/composio/core/models/webhook_events.py\` | Add \`REVOKED\` to
\`ConnectionStatusEnum\` | DEGRADED — code that checked \`status in
ConnectionStatusEnum\` would have failed to recognise the new value |
| \`python/composio/core/models/connected_accounts.py\` |
\`ConnectionRequest.wait_for_connection\` now fails fast on terminal
non-ACTIVE states (\`FAILED\` / \`EXPIRED\` / \`INACTIVE\` /
\`REVOKED\`) instead of looping until the 60s timeout | DEGRADED —
REVOKED used to look identical to "still pending" until the timeout
fired |

The Stainless-generated \`@composio/client\` (TS) and
\`composio_client\` (Python) packages will pick up \`REVOKED\`
automatically when regenerated; this PR covers the hand-written SDK code
only.

# How did I test this PR

- \`pnpm typecheck\` (root) — clean (\`9 successful, 9 total\`)
- \`pnpm test\` (root) — 694 passed, 1 skipped (75 test files); examples
validation \`Validated 22 example packages\`
- Python: \`ruff check\` and \`ruff format --check\` clean on both
edited files
- Smoke-tested Python imports + enum membership:
  \`\`\`
  PYTHON: imports OK, REVOKED present: REVOKED
  \`\`\`

Triggered by: sarthak@composio.dev | Source: web
Session:
https://zen-api-production-4c98.up.railway.app/dashboard/#/chat/zen-14dc825802da
2026-05-07 12:19:19 +02:00
Abir Taheer df857bfdfe fix: remove extraneous f-string prefixes in Python example
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 03:17:29 -07:00
Abir Taheer 51ae6da240 fix: default requireExplicitSelection to true when multiAccount is enabled
Also makes multiAccount fields optional in ToolRouterUpdateSessionConfig
(PATCH semantics) and adds a PATCH-safe transform that doesn't inject
create-time defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 03:14:53 -07:00
Abir Taheer 0ff5bd6cc4 refactor(python): use Stainless types directly for session.update()
Drop manual camelCase→snake_case transformation and custom type
definitions. Use session_patch_params types from composio_client and
pass kwargs straight through to client.tool_router.session.patch().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 02:59:41 -07:00
Abir Taheer a9977af2c1 docs: add session.update() examples for TypeScript and Python
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 02:28:10 -07:00
Abir Taheer a0242d7b3a docs: clarify session use wording 2026-05-07 02:24:42 -07:00
Abir Taheer 26eda4606d fix(python): attach existing tool router sessions 2026-05-07 02:11:26 -07:00
Abir Taheer 4718495d10 feat(python): add session.update() for partial session config updates
Mirrors the TypeScript session.update() — calls the existing PATCH
endpoint and mutates the session's preload config in-place.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 02:07:36 -07:00
jkomyno 5bdaafd506 docs: trim verbose comments across the REVOKED-rollout commits
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.
2026-05-07 10:59:43 +02:00
jkomyno 27a4e6448e test(connected-accounts,py): patch time.time defensively in terminal-state test
`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).
2026-05-07 10:56:29 +02:00
jkomyno d5bead2524 refactor(connected-accounts,py): hoist terminal_states to module-level frozenset
Promote the in-method `terminal_states = ("FAILED", "EXPIRED", "REVOKED")`
tuple to a module-level `_TERMINAL_CONNECTION_STATES: FrozenSet[str]`
constant alongside `_LEGACY_RETIRING_OAUTH_SCHEMES` (line 29). Two
benefits:

- `frozenset` signals set-membership semantics (which is exactly what
  `if self.status in ...` does), versus `tuple` which signals an ordered
  sequence.
- The TS-contract docstring travels with the constant, making the
  invariant discoverable without descending into the method body.

Surfaced by kieran-python review of PR #3344 (P3#1).
2026-05-07 10:54:23 +02:00
jkomyno a7c2d16bf3 test(connected-accounts): cover REVOKED rollout and forward-compat
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
2026-05-07 10:29:14 +02:00
jkomyno 4e1883089d fix(connected-accounts,py): drop INACTIVE from terminal states in wait_for_connection
The previous patch added FAILED / EXPIRED / INACTIVE / REVOKED to the
terminal-state set, flipping the contract from "loop until timeout for
any non-ACTIVE" to "raise SDKError immediately on any of those four".
INACTIVE means "user disabled, can be reactivated" — a connection can
briefly transit through it before settling on ACTIVE, and failing fast
there regressed callers that previously recovered.

Align Python's terminal set with TS (`ConnectionRequest.ts`) at
[FAILED, EXPIRED, REVOKED]. INACTIVE remains non-terminal in both
SDKs, matching its "reactivatable" semantics.

Addresses PR #3344 review comments P1#2 (regression on INACTIVE) and
P1#3 (TS↔Python terminal-state divergence).
2026-05-07 10:17:31 +02:00
Zen Agent 0b32b158f3 feat(connected-accounts): support REVOKED status added in Apollo PR #9550
Apollo (hermes#9550) added REVOKED to the connected-account status enum.
The SDK had several places that hard-coded the prior 6 values and would
either fail-fast against the new value (BREAKING) or silently misclassify
it (DEGRADED).

TypeScript SDK (BREAKING fixes):
- Add REVOKED to `ConnectedAccountStatusSchema` / `ConnectedAccountStatuses`
  so `z.enum().parse(...)` no longer throws on responses that include the
  new status.
- Add REVOKED to the CLI's Effect `Schema.Literal(...)` for
  `ConnectedAccountItem.status`.
- Add REVOKED to the `normalizeConnectedAccountStatus` switch so a
  REVOKED account is no longer silently coerced to INACTIVE.
- Fix the connected-account-list raw-cast to forward REVOKED into the
  Stainless-generated client (the cast widens to whatever
  `client.connectedAccounts.list` accepts so it auto-tracks regenerations).

Python SDK (DEGRADED fixes):
- Add REVOKED to `ConnectionStatusEnum` so consumers checking
  `event.data.status in ConnectionStatusEnum` recognise the value.
- Update `ConnectionRequest.wait_for_connection()` to fail fast on
  terminal non-ACTIVE statuses (FAILED / EXPIRED / INACTIVE / REVOKED)
  instead of polling until the 60s timeout.

The Stainless-generated `@composio/client` and `composio_client` packages
will pick up the new enum value automatically when regenerated; this PR
covers the hand-written SDK code only.
2026-05-07 10:13:41 +02:00
Abir Taheer e3617d1251 style(python): ruff format test_tool_router.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 23:36:58 -07:00
Abir Taheer a03b3759ec fix(python): update test to expect coerced array for connected_accounts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 23:33:47 -07:00
Abir Taheer bd36c57aaf fix: accept string | string[] for connectedAccounts, coerce to array
Non-breaking: existing callers passing a single string per toolkit
continue to work. The string is silently coerced to a single-element
array to match the v3.1 wire format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 22:57:40 -07:00
Abir Taheer d2b5dda2d2 fix(python): use InlineCustomToolsWirePayload type for mypy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 22:07:25 -07:00
Abir Taheer b336a089ee refactor: simplify inline custom tools payload construction
Remove conditional spreads/checks — undefined properties are stripped
during JSON serialization anyway.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 22:00:04 -07:00
Abir Taheer 3fc9cfc305 fix: update connected_accounts to array type for v3.1 multi-account
Bumps @composio/client to 0.1.0-alpha.70 which changes
connected_accounts from Dict[str, str] to Dict[str, List[str]].
Updated both TS and Python SDK types to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 21:52:23 -07:00
Abir Taheer e156769eb6 fix(python): update connected_accounts type to Dict[str, List[str]]
Matches the v3.1 wire type from composio-client 1.37.0 where
connected_accounts maps toolkit slugs to arrays of account IDs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 21:46:08 -07:00
Abir Taheer 9c9c273ba1 docs(python): remove server implementation details from use() docstring
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 21:43:01 -07:00
Abir Taheer 8d27cb0c08 style(python): ruff format tool_router.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 21:34:21 -07:00
Abir Taheer 797cce87fd chore(python): bump composio-client to 1.37.0
Picks up session_execute_params.Experimental type needed for inline
custom tools on v3.1 execute/search/execute_meta endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 21:18:12 -07:00
Abir Taheer 366c795e4f feat(python): add custom tools support to session.use() via v3.1 attach
Mirrors the TS SDK changes:
- composio.use(id, custom_tools=..., custom_toolkits=...) POSTs to
  /api/v3.1/tool_router/session/{id}/attach with inline experimental
  payload when customs are provided, else falls back to v3 retrieve.
- build_custom_tools_map_from_response now accepts both create and
  retrieve experimental response types.
- CustomToolsMap gains a `tools` field to cache the raw standalone
  CustomTool list for future inline re-injection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 20:25:29 -07:00
Dhawal Upadhyay 73b42a29a8 Support SDK-local custom tool preload for v3.1 (#3349)
## Summary
- Add `preload` on TS/Python custom tools and custom toolkits so
SDK-created sessions can expose selected custom schemas directly from
`session.tools()`.
- Keep top-level `preload.tools` for backend-owned Composio tool slugs /
`all`; reject custom slugs there with guidance to use the SDK custom
definition flag, and reject bare-string Python misuse except `"all"`.
- Pass inline custom definitions on create/search so backend
validation/search has current definitions without persisting them;
direct `session.execute(...)` remains generated-client typed and does
not carry custom payload.
- Preserve `sessionPreset: "direct_tools"` /
`session_preset="direct_tools"`, including explicit override precedence;
direct-tools uses `preload.tools = "all"` only when the caller does not
override preload.
- Add OpenAI Agents SDK examples for explicit preload and
`sessionPreset: "direct_tools"` in TS and Python, including
toolkit-level preload with a nested `preload: false` / `preload=False`
override.

## Validation
- `pnpm --dir ts/packages/core test test/models/toolRouter.test.ts
test/models/customToolRouting.test.ts`
- `pnpm --dir ts/packages/core typecheck`
- `env COMPOSIO_CACHE_DIR=/private/tmp/composio-cache
python/.nox/chk/bin/python -m pytest python/tests/test_custom_tools.py
python/tests/test_tool_router.py`
- `python/.nox/chk/bin/python -m ruff check
python/examples/tool_router/preload.py
python/examples/tool_router/direct_tools_preset.py
python/composio/core/models/custom_tool.py
python/composio/core/models/custom_tool_types.py
python/composio/core/models/tool_router.py
python/composio/core/models/tool_router_session.py
python/tests/test_custom_tools.py python/tests/test_tool_router.py`
- `python/.nox/chk/bin/python -m py_compile
python/examples/tool_router/preload.py
python/examples/tool_router/direct_tools_preset.py`
- `pnpm --dir ts/examples/tool-router exec tsc --noEmit --skipLibCheck
--target ES2022 --module ESNext --moduleResolution Bundler
--allowSyntheticDefaultImports --strict src/preload.ts
src/direct-tools-preset.ts`
- `git diff --check`
2026-05-06 19:06:57 +05:30
Dhawal Upadhyay 98569bcf6f Respect base URL in Python ToolRouter examples 2026-05-06 18:52:18 +05:30
Dhawal Upadhyay eae717ac20 Clean up inline custom tools payload typing 2026-05-06 18:44:40 +05:30
Dhawal Upadhyay 8254999914 Address ToolRouter example review comments 2026-05-06 18:28:44 +05:30