This PR:
- completes the Python `0.16.0` CHANGELOG entry, which previously
documented only the version + dependency bump even though six
user-facing fixes landed on the unreleased `0.16.0` since the entry was
written (`py@0.15.0..HEAD`)
- refreshes the `0.16.0` date to its actual release date (`2026-06-25`)
- is a prerequisite for tagging `py@0.16.0` and publishing to PyPI —
PyPI is currently at `0.15.0`; `0.16.0` was bumped in-repo but never
tagged or published, and no further version bump was made for the six
follow-up fixes
## Context
`0.16.0` is unreleased (PyPI's latest stable is `0.15.0`, there is no
`py@0.16.0` tag), and `pyproject.toml` / `__version__.py` were never
bumped past `0.16.0`. So these six fixes belong to `0.16.0`, not a new
version. The newly documented changes:
- file `$ref`/`$defs` dereferencing so auto upload/download flags behind
a reference aren't dropped (e.g. `GMAIL_GET_ATTACHMENT`)
- disabling retries on non-idempotent `tools.execute()` /
`tools.proxy()` writes to prevent duplicate side effects
- bounded `(connect, read)` timeouts on session file transfers and
presigned S3 paths
- centralized provider-visible tool schema aliasing across Anthropic,
Claude Agent SDK, Gemini, Google ADK, and OpenAI Agents
- deep-copying the OpenAI Agents tool schema before stripping
`examples`/`default`
Plus the already-noted `composio-client` `1.39.0` → `1.41.0` bump and
the dependency-range / uv-lock refresh.
Once merged, release with:
\`\`\`bash
git tag py@0.16.0 <merge-commit> && git push origin py@0.16.0
\`\`\`
Review follow-up to #3657 (squash-merged before these review fixes could
land on it). No behaviour change to the retry scoping itself — this
tightens correctness, docs, and test coverage around the
`without_retries` mechanism.
- **Correct the `without_retries` caching rationale.** The previous
docstring justified caching with "each clone creates a new `ContextVar`,
and dynamically-created `ContextVar`s are never garbage-collected." That
is false for this code path: `request_ctx` is only ever `.get()`, never
`.set()`, so it is reclaimed by refcounting on drop (measured: 500k
get-only clones leak ~0 KiB; the CPython leak only occurs with
`.set()`). Caching is kept because it avoids constructing a fresh
`HttpClient` on every `execute`/`proxy` (the hottest path) — the real,
stated justification now.
- **Preserve `_strict_response_validation` on the clone.** The
Stainless-generated `copy()` drops it, so the no-retry sibling silently
reverted to the default (`False`) even when the parent had it enabled.
The `copy()` override now threads it through `_extra_kwargs` alongside
`provider`, so the sibling differs from the parent only in
`max_retries`.
- **Document the scope boundary.** The `without_retries` docstring now
states that only `tools.execute` / `tools.proxy` are de-retried; other
non-idempotent writes (`auth_configs.create`/`update`/`delete`,
`mcp.update`/`delete`, `connected_accounts.delete`/`refresh`,
`link.create`) keep the default retries (most are naturally idempotent
on retry; the durable fix is backend idempotency keys).
- **Add regression coverage:** the clone preserves
`_strict_response_validation`, plus contract guards pinning the
Stainless `copy`/`_extra_kwargs`/`with_options` internals the override
relies on — so a `composio_client` regen that breaks the contract fails
as an obvious assertion rather than a cryptic `TypeError`.
- Minor: fix the `x-sdk-version` `"unknwon"` → `"unknown"` typo and
tighten `te.Self` typing on the cache field/property.
No changeset (Python-only change; changesets are TypeScript-only).
This PR:
- stops the Python SDK from silently retrying `tools.execute` and
`tools.proxy`, which are non-idempotent POST writes. The
Stainless-generated client retries POSTs by default (`max_retries=2`) on
read timeouts, 429, 5xx, and connection errors. A read timeout is unsafe
to retry — the request may already be in flight on the backend — so a
retry can duplicate the side effect, e.g. send an email twice. Reported
in https://github.com/ComposioHQ/composio/issues/3586
- routes both write paths through a new `client.without_retries` — a
cached, retry-disabled (`max_retries=0`) clone of the `HttpClient`.
Reads and lists keep the default retries, so resilience is unchanged for
idempotent calls
- overrides `HttpClient.copy()` to re-inject the required `provider`
keyword and re-aliases `with_options`, since the generated `copy()`
rebuilds via `self.__class__(...)` without `provider` — previously
`with_options(max_retries=0)` raised `TypeError` on the subclass
- caches the no-retry sibling instead of cloning per call: each clone
creates a new `ContextVar`, and dynamically-created `ContextVar`s are
never garbage-collected
- adds `tests/test_no_retry_writes.py`: writes hit the transport exactly
once on a retryable 5xx, reads still retry-then-succeed (proving the
scoping), and a regression guard for the `copy()` override
- interim fix only — the durable solution is idempotency keys, tracked
in https://github.com/ComposioHQ/composio/issues/3654 (gated on backend
support), which supersedes this once available
No changeset (Python-only change; changesets are TypeScript-only).
## Problem
`OpenAIAgentsProvider.wrap_tool` mutates the caller's
`Tool.input_parameters`. It shallow-copies the schema and then strips
keys from the nested property dicts **in place**:
```python
modified_schema = tool.input_parameters.copy() # shallow copy
modified_schema["additionalProperties"] = False
_remove_examples_from_schema(modified_schema) # del prop["examples"/"pattern"/"default"]
```
`_remove_examples_from_schema` recurses into `schema["properties"][*]`
and does `del prop_value["examples"]`, `del prop_value["pattern"]`, `del
prop_value["default"]`. Because `dict.copy()` is shallow,
`modified_schema["properties"]` (and every nested property dict) is the
**same object** as the original, so those deletions strip
`examples`/`pattern`/**`default`** out of the caller's
`Tool.input_parameters`.
Since essentially every Composio tool carries `default`/`examples` on
its properties, wrapping a tool silently corrupts its schema. Wrapping
the same `Tool` twice, reusing it with another provider, or inspecting
it after wrapping then sees the defaults gone.
This is the only wrap path using a shallow `.copy()` + nested mutation —
the rest of the codebase deep-copies before mutating schemas, e.g.
`composio/utils/shared.py` (`schema = copy.deepcopy(schema)`, with the
docstring *"The schema is deep-copied before any mutation so the
caller's original is never modified."*) and `Tools._get`
(`tool.model_copy(deep=True)`).
## Fix
Deep-copy the schema before mutating
(`copy.deepcopy(tool.input_parameters)`), so
`_remove_examples_from_schema` only edits the provider-local copy.
One-line change + `import copy`.
## Test
Added
`python/providers/openai_agents/tests/test_provider.py::test_wrap_tool_does_not_mutate_input_parameters`
— builds a tool whose properties carry `default`/`examples`/`pattern`,
snapshots `input_parameters`, calls `wrap_tool`, and asserts both sides
of the contract:
- the caller's original `Tool.input_parameters` is unchanged
- the returned `FunctionTool.params_json_schema` is still normalized for
OpenAI Agents (`additionalProperties: false`, with
`default`/`examples`/`pattern` stripped)
- **Before:** the assertion fails — `owner` loses `default`/`examples`,
`repo` loses `pattern`.
- **After:** `Tool.input_parameters` is untouched while the wrapped
schema remains sanitized.
Verification:
- `uv run pytest python/providers/openai_agents/tests/test_provider.py
-q` → **1 passed**
- `uv run ruff check
python/providers/openai_agents/composio_openai_agents/provider.py
python/providers/openai_agents/tests/test_provider.py` → clean
- `uv run ruff format --check
python/providers/openai_agents/composio_openai_agents/provider.py
python/providers/openai_agents/tests/test_provider.py` → clean
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
This PR:
- supersedes https://github.com/ComposioHQ/composio/pull/3625,
https://github.com/ComposioHQ/composio/pull/3629, and the Python
SDK/provider-side fix proposed by
https://github.com/ComposioHQ/composio/pull/3504
- adds `alias_tool_input_schema` / `restore_tool_arguments` for
provider-visible schemas and backend argument restoration
- keeps `substitute_reserved_python_keywords` /
`reinstate_reserved_python_keywords` as compatibility wrappers
- preserves the existing Python keyword alias style (`from` ->
`from_rs`), aliases invalid Python parameter names like `$top`, avoids
leading-underscore aliases so Pydantic-backed providers can build
models, and caps aliases at 64 characters for Anthropic-style tool
schema validators
- dereferences internal `$ref` / `$defs` before aliasing so referenced
object properties are exposed through the same safe provider-visible
names
- restores Gemini manual `handle_response` arguments with the per-tool
alias map before execution
- wraps Google ADK tools with aliased callable signatures and restores
original backend argument names
- wraps Anthropic and Claude Agent SDK tool schemas with the same shared
alias map and restores original backend argument names before execution
- fixes the package-local Claude Agent SDK provider tests so they run
without `pytest-asyncio` and keep using the provider-visible schema
conversion hook
- adds dependency-light core tests for helper behavior, `$ref` aliasing,
Pydantic model compatibility, Gemini manual response, Google ADK
wrapping, Anthropic wrapping, and Claude Agent SDK wrapping
Local validation:
- `uv run --project python pytest
python/tests/test_tool_schema_aliasing.py
python/tests/test_schema_parser.py python/tests/test_json_schema.py
python/tests/test_imports.py -q` -> 112 passed
- `PYTHONPATH=python uv run --project python --with-editable
./python/providers/claude_agent_sdk pytest
python/providers/claude_agent_sdk/tests/test_provider.py -q
--timeout=20` -> 16 passed
- `uv run --project python --with-editable ./python/providers/langchain
pytest --ignore-glob='python/tests/test_type_inference*.py'
python/tests/ -q` -> 742 passed, 31 skipped
- `uv run --project python --with-editable ./python --with-editable
./python/providers/langchain python <LangChain $top wrap smoke>` ->
wrapped args schema field `param_top`
- `uv run --project python --with-editable ./python --with-editable
./python/providers/langgraph python <LangGraph $top wrap smoke>` ->
wrapped args schema field `param_top`
- `uv run --project python ruff check --config python/config/ruff.toml
python/composio/utils/shared.py
python/tests/test_tool_schema_aliasing.py` -> passed
- `uv run --project python ruff format --check --config
python/config/ruff.toml python/composio/utils/shared.py
python/tests/test_tool_schema_aliasing.py` -> passed
- `git diff --check` -> passed
Note: #3500's hosted MCP emission-layer report is already closed; this
PR covers the Python SDK/provider surface rather than changing hosted
MCP schema emission.
## Problem
Tools whose parameter schemas express their file fields through a
`$ref`/`$defs` indirection lose auto file handling. With
`dangerously_allow_auto_upload_download_files=True`, a
`file_downloadable` output behind a `$ref` is never downloaded (the raw
`{name, mimetype, s3url}` object is passed through), and a
`file_uploadable` input behind a `$ref` is never staged — silently, with
no error.
`GMAIL_GET_ATTACHMENT` is the canonical shape — the flag sits two `$ref`
hops deep:
```
data → $ref → #/$defs/GetAttachmentResponse → file → $ref → #/$defs/FileDownloadable (file_downloadable: true)
```
**Root cause:** the hand-rolled walkers in
`composio/core/models/_files.py` (`_has_file_property`,
`_substitute_file_upload_value`, `_substitute_file_download_value`, …)
recurse through `properties`/`anyOf`/`oneOf`/`allOf`/`items` but **never
dereference `$ref`**, so a flagged node reachable only through a
reference is invisible to them.
Fixes https://github.com/ComposioHQ/composio/issues/3506
## Approach
Rather than teach each walker to dereference — which means threading a
root schema through ~10 methods, re-deriving `$ref` handling in each,
and risking unbounded recursion on cyclic schemas — this introduces a
single **`dereference_json_schema`** utility and inlines references
**once at the `FileHelper` boundary** (`process_file_uploadable_schema`,
`substitute_file_uploads`, `substitute_file_downloads`). The walkers
stay reference-agnostic.
This mirrors the TypeScript SDK's `dereferenceJsonSchema` (the
counterpart fix in #3566 for the TS twin of this bug, #3307) so both
SDKs share one behavioral contract. Resolving once at the boundary also
gives every walker `$ref` support for free — including keywords they
never special-cased (`additionalProperties`, `patternProperties`,
`prefixItems`, `not`), because the resolver walks containers
reflectively.
## What's in the utility (`composio/utils/json_schema.py`)
Faithful port of the TS guards:
- **Cycle safety** — breaks both `$ref` cycles and live-object identity
cycles with a permissive `{type: object, additionalProperties: true}`
sentinel. (A per-node lazy resolver without threaded cycle tracking
infinite-loops on a self-referential schema; there's a regression test
for exactly this.)
- **Depth caps** — `MAX_REF_CHAIN_DEPTH=100` / `MAX_NODE_DEPTH=512`
raise a typed `JSONSchemaRefResolutionError` instead of exhausting the
interpreter (with a `RecursionError` backstop converted to the same
typed error).
- **Sibling-keyword merge** — Draft 2020-12 semantics (siblings win on
collision), so a `description`/`default` next to a `$ref` survives.
- **External-ref passthrough** — `http(s)://` refs are left untouched
and logged once for audit.
- **`sentinel` mode** — a dangling `$ref` (an API schema that emits
`$ref` with no `$defs` block,
https://github.com/ComposioHQ/composio/issues/3307) degrades to the
permissive sentinel + an LLM-visible hint, instead of aborting the tool
call. The `FileHelper` uses this mode for API-sourced schemas.
- **Non-mutating** — returns a new schema; `$defs`/`definitions` are
stripped from the inlined root.
## Tests
- `python/tests/test_json_schema.py` — 30 cases mirroring the TS
`jsonSchema.test.ts` contract (chains, reflective container walk, legacy
`definitions`, sibling merge, cycles, depth caps, pointer-escape
decoding, array-index pointers, full `sentinel`-mode suite).
- `python/tests/test_files.py` — end-to-end regressions through the
public methods: download/upload behind `$ref`/`$defs` (the #3506 repro),
the LLM-facing input transform, graceful degradation of a dangling
`$ref`, and a **self-referential schema that must not recurse
infinitely**.
- `nox -s chk` (ruff + mypy) green; `nox -s tst` for the file/execution
suites green.
## Notes
Supersedes #3545 (third-party PR that fixed the same bug by threading
`root_schema` through each walker). That diagnosis was correct; this
takes the centralized route consistent with the TS SDK and adds the
cycle/depth guards. No changeset — Python-package change.
## Summary
- add bounded `(connect, read)` timeouts to Python SDK session file
downloads and uploads
- add the same timeout coverage to presigned S3 upload/download paths
used by file helpers
- convert timeout/request failures into the SDK's existing file errors
and cover them with tests
Fixes#3560Fixes#3561Fixes#3562
## Changes
- `RemoteFile.buffer()` now uses the existing file transfer timeout
constants and preserves file context on request failures
- `ToolRouterSessionFilesMount.upload()` and S3 upload helpers now bound
presigned `PUT` requests
- `FileDownloadable.download()` now bounds streaming `GET` requests,
wraps streaming read failures, and closes the response
- added regression tests for request timeouts and timeout arguments
- added a changeset for the published SDK change
## Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change
## How Has This Been Tested?
- [x] `D:\anaconda3\envs\yolov12\python.exe -m ruff check
python\composio\core\models\tool_router_session_files.py
python\composio\core\models\_files.py
python\tests\test_tool_router_session_files.py
python\tests\test_files.py`
- [x] `D:\anaconda3\envs\yolov12\python.exe -m ruff format --check
python\composio\core\models\tool_router_session_files.py
python\composio\core\models\_files.py
python\tests\test_tool_router_session_files.py
python\tests\test_files.py`
- [x] `COMPOSIO_CACHE_DIR=<repo>\.tmp-composio-cache
D:\anaconda3\envs\yolov12\python.exe -m pytest
python\tests\test_tool_router_session_files.py
python\tests\test_files.py::TestUploadBytesToS3
python\tests\test_files.py::TestFileDownloadablePathTraversal::test_safe_filename_passes_through
python\tests\test_files.py::TestFileDownloadablePathTraversal::test_download_uses_timeout
python\tests\test_files.py::TestFileDownloadablePathTraversal::test_download_timeout_raises_error
python\tests\test_files.py::TestFileDownloadablePathTraversal::test_download_stream_timeout_raises_error`
Note: I also ran the full
`python\tests\test_tool_router_session_files.py
python\tests\test_files.py` pair locally. The new and related tests
passed, but the full run has one pre-existing Windows-specific failure
in
`TestFileDownloadablePathTraversal::test_empty_name_safe_fails_at_write_time`:
opening a directory for writing raises `PermissionError` on Windows
instead of the test's expected `IsADirectoryError`.
## Screenshots (if applicable)
N/A
## Checklist
- [x] I have read the Code of Conduct and this PR adheres to it
- [x] I ran linters/tests locally and they passed
- [ ] I updated documentation as needed
- [x] I added tests or explain why not applicable
- [x] I added a changeset if this change affects published packages
## Additional context
This keeps the existing `(5s connect, 60s read)` timeout convention
already used by the Python SDK's URL fetch helpers.
---------
Co-authored-by: jkomyno <alberto@composio.dev>
This PR:
- fixes
https://github.com/ComposioHQ/composio/actions/runs/27964338497/job/82754032484
- aligns `python/composio/__version__.py` with the current Python
package version `0.16.0`
- adds the missing `0.16.0` Python changelog entry for the dependency
refresh
- restores `pnpm run test:release-workflow` on `next`
Refreshes the Python SDK's core + provider dependency ranges and uv
locks to the **newest cutoff-compatible** versions, and makes the locks
self-consistent.
## What changed
- Bumps core + provider dependency ranges (`pyproject.toml` /
`setup.py`) and regenerates all three uv locks (root, `openai`,
`claude_agent_sdk`) to the newest available versions.
- Keeps `composio-client==1.41.0` (the current newest, released
2026-06-19).
- Caps `crewai>=0.134.0,<0.135.0`: crewai 1.x requires
`pydantic>=2.11.9,<2.13`, which conflicts with composio core's
`pydantic>=2.13.4`. Staying on crewai 0.x keeps the newest pydantic for
everything else; the provider only uses `crewai.tools.BaseTool`, which
is unaffected.
- Drops the one-shot `--exclude-newer` cutoff that had been recorded in
the lock `[options]` but was not backed by any `[tool.uv]` config — it
broke `uv lock --check` and held packages a few releases behind newest.
- Pins nox-installed tools/stubs in `noxfile.py` to the matching newest
versions.
## Resolution result
- Root lock: **161 upgrades, 0 downgrades** vs `next`.
- Provider locks: **0 downgrades** vs `next` (openai +14,
claude_agent_sdk +29).
## Verification
- `uv lock --check` — clean on all three locks.
- `uv run --frozen --all-packages pytest python/tests/test_imports.py` —
8/8 passing.
- `uv run --frozen nox -s chk` — ruff clean + mypy 0 issues across 80
source files.
Supersedes https://github.com/ComposioHQ/composio/pull/3622 (which was
opened from a fork).
This PR:
- bumps the TS catalog `@composio/client` from `0.1.0-alpha.72` to
`0.1.0-alpha.74` (published from ComposioHQ/composio-base-ts#84) and
refreshes `pnpm-lock.yaml`
- bumps the Python `composio-client` pin from `1.39.0` to `1.41.0`
(published from ComposioHQ/composio-base-py#69)
- adds a patch changeset for `@composio/core` and `@composio/cli` so the
client bump actually ships in the next release
## Context
Neither client is auto-bumped in this repo — there is no
Stainless→consumer bot, and Dependabot does not touch the pnpm
`catalog:` pin (TS) or the exact `composio-client==` pin (Py), so these
were a manual catch-up. The Python `1.41.0` PyPI publish initially
failed with a `403 Forbidden` (stale `PYPI_TOKEN`); it was re-run
successfully before this bump, so `1.41.0` is live on PyPI.
This PR:
- documents the pending Python `0.14.0` and `0.15.0` releases in
`python/CHANGELOG.md`
- aligns `python/composio/__version__.py` with `python/pyproject.toml`
at `0.15.0`
- adds a release guard so Python package/provider versions, runtime
version, and changelog entries cannot drift silently
- leaves Python release tags and PyPI publishing untouched; `py@0.14.0`
should still be published before `py@0.15.0`
## Summary
`triggers.subscribe()` crashes with `KeyError: 'nanoId'` on the **first
event** for any project configured with webhook version **V3**. The
realtime (Pusher) channel now delivers the modern V3 envelope
(snake_case: `metadata.trigger_id` / `connected_account_id` /
`auth_config_id`, with no top-level `nanoId`), but the realtime parser
(`TriggerSubscription._parse_payload`) only understood the **legacy**
envelope and accessed `data["metadata"]["nanoId"]` directly — so the
very first inbound frame raises inside the Pusher `_on_message` callback
and the registered handler is never invoked.
The SDK already knows how to normalize the V3 envelope — it does so on
the webhook path (`_normalize_v3_payload`) — that logic was simply never
wired into the realtime path.
This has affected the realtime subscribe path for V3 projects since the
V3 trigger payload format was introduced; webhook-version V1/V2 projects
are unaffected (they still receive the legacy realtime envelope).
## Changes
- Extract the V3 → `TriggerEvent` normalization into a shared
module-level helper (`_build_trigger_event_from_v3`) plus an envelope
detector (`_is_v3_envelope`); the webhook path's `_normalize_v3_payload`
now delegates to it, so webhook and realtime share one source of truth.
- `TriggerSubscription._parse_payload` now detects the V3 envelope and
normalizes it, and keeps the existing legacy mapping for V1/V2 payloads.
- Harden `_parse_payload`: unknown/malformed frames are logged and
skipped (return `None`) instead of raising, so a single bad frame can no
longer tear down an active subscription.
- Add tests covering V3 realtime parsing, legacy realtime parsing, and
malformed-frame handling.
## Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change
## How Has This Been Tested?
- `ruff check` + `ruff format` (config/ruff.toml) — clean.
- `uv run pytest tests/test_triggers.py` — **52 passed** (49 existing +
3 new).
- New tests:
- `test_parse_payload_v3_realtime_envelope` — a V3 realtime frame parses
to a `TriggerEvent` (no `KeyError`).
- `test_parse_payload_legacy_envelope` — V1/V2 legacy frame still parses
correctly.
- `test_parse_payload_malformed_returns_none` — bad/partial frames are
skipped, not raised.
## Checklist
- [x] I have read the Code of Conduct and this PR adheres to it
- [x] I ran linters/tests locally and they passed
- [ ] I updated documentation as needed
- [x] I added tests or explain why not applicable
- [ ] I added a changeset if this change affects published packages
## Additional context
The realtime envelope is identical in shape to the V3 webhook envelope,
so the shared normalizer keeps the two delivery channels consistent. No
public API changes.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: jkomyno <alberto@composio.dev>
Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
## What
`triggers.create(userId/user_id, ...)` already accepts a user id and
uses it to resolve the connected account, but **dropped it** when
building the `trigger_instances.upsert` body. With [2FA for
triggers](https://linear.app/composio/issue/PLEN-2580) on the backend,
2FA-enabled projects need `user_id` on the upsert to verify the pinned
connected account belongs to the caller's user.
This forwards `user_id` to upsert in **both SDKs**:
- **TS** (`ts/packages/core/src/models/Triggers.ts`): adds `user_id` to
the upsert params via a `& { user_id?: string }` bridge.
- **Python** (`python/composio/core/models/triggers.py`): forwards
`user_id` via the client's supported `extra_body` escape hatch.
Both bridges exist only until `@composio/client` / `composio-client`
regenerate from the updated OpenAPI spec with a native `user_id` field —
drop them then.
## Safety
Backends without trigger 2FA ignore the extra field, so this is safe to
land ahead of / alongside the backend rollout (hermes PR #10635).
## Tests
- TS: upsert-body assertion added.
- Python: `tests/test_triggers.py::test_create_with_user_id` asserts
`extra_body == {"user_id": ...}`. 49 tests pass.
Closes part of PLEN-2580 (SDK follow-up to the agreed approach).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: jkomyno <alberto@composio.dev>
## Summary
Models — and some MCP transports — occasionally emit tool-call arguments
as a **JSON string instead of an object/dict**. The most visible trigger
is `COMPOSIO_MULTI_EXECUTE_TOOL` on the Vercel AI SDK, where streaming
fails with:
> `messages.3.content.1.tool_use.input: Input should be a valid
dictionary`
Until now only a handful of providers guarded against this, each with
its own slightly different inline check, leaving most providers
vulnerable and behaviour inconsistent across the SDK.
This PR centralizes the coercion into **one helper per language** and
routes **every** provider through it, in both the TypeScript and Python
SDKs.
Closes https://github.com/ComposioHQ/composio/issues/2406
## What changed
**TypeScript** — new `normalizeToolArguments` in `@composio/core`
(exported), used by every provider:
`vercel`, `cloudflare`, `openai-agents`, `openai` (ChatCompletions +
Responses), `anthropic`, `google`, `langchain`, `llamaindex`,
`claude-agent-sdk`, `mastra`.
**Python** — new `normalize_tool_arguments` in `composio.utils.shared`,
used by every provider:
`openai`, `openai-responses`, `anthropic`, `google`, `langchain`,
`langgraph`, `crewai`, `autogen`, `llamaindex`, `gemini`, `google-adk`,
`openai-agents`, `claude-agent-sdk`.
Shared semantics (identical in both languages):
| Input | Result |
| --- | --- |
| object / dict | returned unchanged |
| JSON string | parsed to object |
| empty / whitespace string | `{}` |
| `null` / `undefined` / `None` | `{}` |
| array, primitive, unparseable string, JSON that isn't an object |
**typed error** (`ComposioInvalidToolArgumentsError` / `InvalidParams`)
with the original parse error as cause |
The typed error replaces the previous grab-bag of behaviours: a raw
`SyntaxError`/`JSONDecodeError`, or — worse — silently forwarding a
malformed string downstream.
## Why this supersedes the open PRs
This consolidates and extends three open PRs that each addressed a slice
of the problem inconsistently. Their authors are credited as co-authors
on the relevant commits:
- **#3489** (LlamaIndex + Claude Agent SDK, TS) — @srijanarya
- **#3438** (Anthropic, Google, LangChain, TS) — @aptsalt
- **#3437** (Google ADK empty schemas + name fix, Python) —
@pragnyanramtha — its empty-`input_parameters` / missing-description
handling and the `gemini` → `google_adk` provider-name fix are folded in
here.
Compared to the three combined, this PR additionally: covers **every**
provider in **both** SDKs (not a subset of one), defines a single source
of truth instead of per-provider snippets, normalizes empty/`null`
payloads to `{}`, and raises an actionable typed error instead of
leaking `SyntaxError` or forwarding a bad string.
## Tests
- Exhaustive unit tests for both helpers (object passthrough,
JSON-string parse, empty/null → `{}`, malformed/non-object → typed
error).
- Per-provider regression tests across the touched TypeScript providers
(string path, malformed-string path, empty-payload path).
- Full `@composio/core` + touched-provider TS suites pass; `typecheck`
and `lint` clean. Python `ruff` clean and new test green.
## Changeset
Patch bump for all affected TypeScript packages (`@composio/core` + the
providers). Python follows its own versioning, so no changeset there.
---------
Co-authored-by: srijanarya <74669415+srijanarya@users.noreply.github.com>
Co-authored-by: Deepak Singh Kandari <deepaksinghkandari07@gmail.com>
Co-authored-by: Pragnyan Ramtha <pragnyanramtha@gmail.com>
## Summary
Depends on #3492.
This completes the Phase 2 migration by removing the transitional
version-file layer and making `mise.toml` plus `mise.lock` the
repository toolchain source of truth. It also moves runtime test
matrices into `toolchain-versions.json`, so CI matrix changes are
explicit and reviewable without reintroducing `.nvmrc`, `.dvmrc`,
`.bun-version`, or `.python-version`.
The Node.js e2e matrix now starts at the latest Node 22 LTS line and
also covers the latest Node 24 and Node 25 lines. That removes Node 20
from the well-known e2e versions while keeping us covered on the
runtimes SDK users are moving toward.
## Rationale
Phase 1 introduced mise side by side with the existing version files to
keep the first PR low-risk. Phase 2 removes that compatibility layer so
there is one place to update tool versions. That avoids silent drift
between local setup, GitHub Actions, Docker E2E images, release docs,
and install-time checks.
The composite setup actions now install Node, Bun, Python, and uv
through mise by default, with explicit version overrides only where a
matrix needs them. New GitHub actions added in this PR are pinned by
release commit SHA and include the release version comment.
## What changed
- Deleted the transitional root/version files: `.nvmrc`, `.bun-version`,
`.dvmrc`, root `.python-version`, and `python/.python-version`.
- Removed `idiomatic_version_file_enable_tools` from `mise.toml` and
added a committed `mise.lock` for linux/macOS x64/arm64 tool resolution.
- Replaced `BYPASS_BUN_VERSION_CHECK` with `BYPASS_TOOLCHAIN_CHECK`, and
made Docker E2E image installs use that bypass because they receive
explicit build args instead of installing mise.
- Updated Node/Bun and Python/uv composite actions to default to mise,
remove `*-version-file` inputs, report resolved versions, and cache pnpm
after `corepack enable`.
- Centralized CI runtime matrices in `toolchain-versions.json` for TS
E2E, Python tests, and CLI npm fallback coverage.
- Updated the Node E2E matrix to `22.22.3`, `24.16.0`, and `25.9.0`,
removing Node 20 from the well-known runtime versions.
- Updated workflows, docs, E2E helpers, Dockerfiles, and release
guidance to reference `mise.toml` / `mise.lock`.
## Verification
- Verified latest Node 22/24/25 releases from the official Node dist
index: `22.22.3`, `24.16.0`, `25.9.0`
(https://nodejs.org/dist/index.json).
- `pnpm install --frozen-lockfile`
- `pnpm --filter @e2e-tests/utils typecheck`
- `pnpm --filter @e2e-tests/utils exec tsc --noEmit --target es2022
--module esnext --moduleResolution bundler --types bun
--resolveJsonModule --skipLibCheck --strict scripts/docker-build.ts`
- `bash -n ts/scripts/pre-install.sh && bun run
ts/scripts/pre-install/check-toolchain.ts && BYPASS_TOOLCHAIN_CHECK=1
bash ts/scripts/pre-install.sh`
- `pnpm exec prettier --check ...` on touched YAML/Markdown/TS/JSON
files
- `ruby -e "require \"yaml\"; ARGV.each { |f| YAML.load_file(f) }" ...`
on touched actions/workflows
- `mise lock --platform linux-x64,linux-arm64,macos-arm64,macos-x64 &&
git diff --exit-code mise.lock`
- `mise exec node@22.22.3 -- pnpm --filter @e2e-tests/utils typecheck`
- `mise exec node@22.22.3 -- pnpm --filter @e2e-tests/node-esm-basic
typecheck`
- `mise exec node@22.22.3 -- pnpm --filter @e2e-tests/node-cjs-basic
typecheck`
- `docker manifest inspect node:24.16.0-slim`
- `docker manifest inspect node:25.9.0-slim`
- `git diff --check`
This PR is **part 1 of 3** splitting
https://github.com/ComposioHQ/composio/pull/3505 to make the removal of
the old 2025 custom tools easier to review. It carries the **Python**
slice.
- removes the legacy `composio.tools.custom_tool` registry path:
`core.models.custom_tools`, `ExecuteRequestFn`, the old fallback
execution wiring, the security tests, and the example
- preserves the 2026 tool-router APIs: `composio.experimental.tool()`,
`composio.experimental.Toolkit`, inline custom-tool execution,
preload/attach/use flows, and `session.custom_tools()`
- bumps the Python workspace and all provider packages `0.13.1` →
`0.14.0`
## Notes
- This slice is a byte-identical subset of #3505 — the three split
branches recombine to that PR's exact tree. See #3505 for the original
local verification logs (`uv run --frozen nox -s chk`, targeted pytest);
CI re-runs per PR.
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>
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>
## 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>
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>
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>
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>
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>
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>
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.
# 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
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>
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>
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>
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).
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).
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