Commit Graph

172 Commits

Author SHA1 Message Date
Checo a3f6b9c8f2 fix(py/triggers): swallow malformed chunked frame instead of tearing down subscription (#3892)
## Problem

TriggerSubscription._handle_chunked_events is bound directly as a Pysher
channel callback. Pysher channel dispatch has no local exception
boundary, so malformed chunk frames reached websocket-client error
handling and marked the realtime connection failed. The normal payload
parser already skips malformed frames, but the chunked path did not.

## Fix

- Validate that each decoded frame is an object with string id/chunk,
integer non-boolean index, and boolean final fields.
- Contain decoder, validation, and reassembly failures at the callback
boundary and log only a bounded frame preview.
- Clear partial state only when the frame carries a validated string id,
allowing that id to be reused safely.
- Preserve the valid chunk reassembly path and fix the Chunked docstring
typo.

## Regression coverage

Tests cover invalid JSON, non-object frames, missing fields, unhashable
ids, wrong field types, unexpected decoder failures, and successful
same-id reassembly immediately after a bad frame clears partial state.

## Verification

- nox -s tst -- tests/test_triggers.py: 83 passed
- make chk: Ruff and mypy passed

---------

Signed-off-by: Checo <sergio@checo.cc>
Co-authored-by: Checo <sergio@checo.cc>
Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-28 20:08:54 +05:30
Checo 2a2d77a764 fix(py/triggers): stop() deadlock + subscribe() timeout connection leak (#3890)
Fixes #3858 (both bugs reported there).

Bug 1 — TriggerSubscription.stop() deadlocks when called from a trigger
callback. pysher's Connection.disconnect runs socket.close() then
join(timeout) on the websocket thread, but callbacks execute on that
same thread's dispatch path (_handle_event blocks on the callback's
future.result()), so joining the thread from within a callback that runs
on it deadlocks the whole process and _alive is never cleared, so a main
thread parked in wait_forever() never exits. Fix: clear _alive
synchronously (so wait_forever unblocks immediately) and run
disconnect() on a daemon thread, breaking the reentrancy while still
closing the socket.

Bug 2 — _SubcriptionBuilder.connect() leaks a websocket thread on
timeout. The deadline path raised ComposioSDKTimeoutError without
calling pusher.disconnect(), so pysher's run loop kept redialing every
reconnect_interval for the life of the process — one leaked thread per
timed-out subscribe(), and if a leaked connection later succeeded it
would authenticate and subscribe the channel of an abandoned
TriggerSubscription, silently consuming events. Fix: wrap the wait loop
in try/except so the timeout path tears the connection down before
re-raising.

Added regression tests (TestTriggerSubscriptionStop and
TestSubscriptionBuilderConnectTimeout) that reproduce both failure modes
offline with mocked pusher connections; all 73 tests in
tests/test_triggers.py pass, ruff check + format clean.

---------

Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
Co-authored-by: jkomyno <alberto.schiabel@gmail.com>
2026-07-28 17:59:14 +05:30
manjunath bhaskar 8cb290be35 fix(schema_converter): allOf containing a false member must reject every value (#3921)
### Summary

Preserves JSON Schema boolean-schema semantics throughout the Python
schema converter:

- an unsatisfiable `allOf` now propagates through nested combiners
instead of being dropped;
- impossible array items, object properties, and local definition
references remain rejecting;
- composed impossible schemas use a Pydantic type that rejects every
value, including JSON `null`;
- the existing `json_schema_to_pydantic_type(False) is None`
compatibility behavior remains unchanged.

### Problem

The original fix handled only a literal `false` directly inside one
`allOf`. Recursive filtering still interpreted the same `None` value as
both "discard this union branch" and "the whole schema is impossible."
That widened nested `allOf` schemas to `str`, removed impossible
properties/items/definitions, and allowed JSON `null` because Pydantic
treats a bare `None` annotation as `NoneType`.

### Fix

- Add a private unsatisfiable marker with Pydantic core- and JSON-schema
hooks.
- Drop that marker only from `anyOf`/`oneOf`; propagate it through
`allOf`.
- Preserve rejecting semantics for nested object properties, array
items, and local `$defs`/`definitions` references.
- Emit `{"not": {}}` from the rejecting Pydantic type.
- Correct the related empty-`allOf` and all-false-`anyOf` expectations.

### Testing

- `uv run pytest tests/test_schema_converter.py
tests/test_schema_parser.py -q` - 106 passed
- `uv run pytest -m schema -q` - 71 passed
- `make tst` - 928 passed
- `make chk` - Ruff and mypy passed across core, providers, tests, and
scripts

Regression coverage includes direct and nested `allOf`, JSON `null`,
empty arrays with impossible items, optional and required impossible
properties, local definition references, generated JSON Schema, and
shared model consumers.

### Note

#3876 adds coverage for the same function's `anyOf` handling but does
not change this `allOf` propagation path.

---------

Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-28 14:15:49 +05:30
Alberto Schiabel 026c104fe8 fix(py): validate canonical URL targets (#3962)
This PR:
- builds on top of https://github.com/ComposioHQ/composio/pull/3823
- carries forward the Requests canonicalization hardening proposed in
https://github.com/ComposioHQ/composio/pull/3810
- resolves and validates the prepared hostname that Requests will
actually connect to
- blocks backslash parser differentials and rejects malformed ports
before DNS resolution
- adds focused URL-safety coverage while preserving both existing upload
entry points

Validation:
- `uv run --frozen pytest tests/test_url_safety.py tests/test_files.py
tests/test_tool_router_session_files.py -q` (177 passed)
- `make chk`
2026-07-28 14:03:53 +05:30
Checo f016bcebb9 fix(mimetypes): make guess() case-insensitive on file extensions (#3899)
guess() in composio/utils/mimetypes.py looked up Path(file).suffix
against the lowercase _types table without normalizing case, so
uppercase suffixes like .PNG or .PDF resolved to
application/octet-stream instead of the correct MIME type. Since guess()
feeds the mimetype field on file uploads (core/models/_files.py),
uploads from phone cameras, scanners and Windows — where uppercase
extensions are common — were getting the wrong content type.

The fix is a one-line normalization: Path(file).suffix.lower() before
the table lookup. This aligns guess() with its sibling
get_extension_from_mime_type, which is already case-insensitive, and
with the stdlib mimetypes.guess_type. I updated the existing test that
documented the old case-sensitive behavior to assert the
case-insensitive result instead.

Fixes #3857

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/ComposioHQ/codesmith/composio/pr/3899"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787222508&installation_model_id=428187&pr_number=3899&repository=ComposioHQ%2Fcomposio&return_to=https%3A%2F%2Fgithub.com%2FComposioHQ%2Fcomposio%2Fpull%2F3899&signature=51d4b1022aa69bc47f45a7daf2e704db96c5eb2a2f267141010ca769b502a062"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>/codesmith</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

Signed-off-by: sergioperezcheco <checo520@outlook.com>
Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-28 13:18:55 +05:30
eeshsaxena d94aa32a41 test: add unit tests for the schema_converter boolean-schema filter (#3876)
## Description

`composio.utils.schema_converter._filter_boolean_schemas` pre-processes
JSON
Schemas before handing them to `json-schema-to-pydantic`, which doesn't
support boolean schemas (draft-06+ allows `true`/`false` as schemas). It
replaces `true` with `{}`, drops `false`, and recurses through combiner
arrays (`anyOf`/`allOf`/`oneOf`), `properties`, `items` and `$defs`. It
had no
test coverage.

This adds `python/tests/test_schema_converter.py` covering:

- `True` -> `{}` and `False` -> `None`
- non-schema values passing through unchanged
- lists dropping `False` entries, and an all-`False` list becoming
`None`
- combiner arrays dropping `False` entries, and the whole combiner being
  removed when every entry is `False`
- a `False` property being dropped while siblings are kept
- plain keys being preserved

No source changes.

## Verification

`pytest python/tests/test_schema_converter.py` - 9 passed. `ruff check`
/
`ruff format` clean.

Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-28 13:18:40 +05:30
eeshsaxena 80e09ad0f0 test: add unit tests for pydantic utils (none_to_omit, parse_pydantic_error) (#3784)
## Description

`composio/utils/pydantic.py` had no dedicated test module. This adds
`tests/test_pydantic_utils.py` covering:

- `none_to_omit` converting `None` to the `omit` sentinel while passing
through other values (including falsy `0`/`""`)
- `parse_pydantic_error` listing missing fields, reporting type errors
with the offending parameter, and handling both together

No source changes.

## Verification

`pytest tests/test_pydantic_utils.py` - 5 passed. `ruff check` / `ruff
format --check` clean.

Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-28 13:05:51 +05:30
Anas Khan 3b28dc756e fix(py): handle list-valued JSON Schema type in openapi signatures (#3822)
`_type_to_parameter` in `python/composio/utils/openapi.py` assumed a
property's
`type` was a scalar string. JSON Schema Draft 2020-12 and OpenAPI 3.1
can express
a nullable field as a list of types instead, for example
`{"type": ["string", "null"]}`. Passing that list to the scalar
membership check
raised `TypeError: unhashable type: 'list'`, which could break tool
signature
generation in providers such as `google_adk`.

## Changes

- Handle list-valued `type` definitions as unions.
- Re-run every member through `_type_to_parameter` with a copy of the
full
property schema and only `type` replaced. This preserves sibling fields
such
as an array's `items`, so `{"type": ["array", "null"], "items": ...}`
resolves
  to `Optional[List[...]]`.
- Preserve scalar error behavior for invalid member types: an unknown
type in a
  list raises `InvalidSchemaError` instead of silently becoming `Any`.
- Keep the existing empty-list fallback to `Any`.
- Add regression coverage for nullable arrays and scalar/list
unknown-type
  parity alongside the existing list-valued type cases.

## Type of change

- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change

## Verification

From `python/`:

- `uv run pytest tests/test_openapi.py -q` — 18 passed.
- `make chk` — Ruff and mypy passed across the Python SDK, providers,
tests,
  examples, and scripts.

## 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 (not applicable: internal
helper)
- [x] I added regression tests
- [ ] I added a changeset (not applicable: Python-only change)

---------

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Signed-off-by: jkomyno <alberto@composio.dev>
Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-15 23:34:26 +04:00
Alberto Schiabel c4071feab4 fix(py): guard URL uploads and redact telemetry (#3823)
This PR:
- ports the TypeScript SDK's public-network guard to Python URL file
uploads
- rejects non-HTTP(S), private, loopback, link-local, reserved, and
mixed DNS targets before a request is made
- applies the guard to both `FileUploadable.from_url()` and Tool Router
session-file URL uploads
- redacts URL queries, Authorization credentials, and secret-like
key/value pairs before error telemetry leaves the process
- adds focused SSRF and redaction regressions alongside the existing
file-upload coverage

Validation:
- `uv run --frozen pytest tests/test_url_safety.py
tests/test_redaction.py tests/test_files.py -q`
- `uv run --frozen ruff check …`
- `uv run --frozen ruff format --check …`
2026-07-14 14:32:05 +04:00
Alberto Schiabel 3a5e62feb4 fix(py): support list-valued schema types in tool signatures (#3815)
This PR:

- builds on top of https://github.com/ComposioHQ/composio/pull/3754
- normalizes list-valued JSON Schema `type` fields before scalar type
lookups
- applies the handling to direct fields and `anyOf`/`oneOf` options
- adds regressions for nullable, single, mixed, unknown, and
combiner-nested type lists
- credits @anxkhn as a Git co-author

## Context

JSON Schema Draft 2020-12 and OpenAPI 3.1 allow `type` to be an array.
The previous parser passed that list to a dictionary lookup, raising
`TypeError: unhashable type: 'list'`. This replacement covers the direct
case from #3754 and the same shape nested in a combiner.

---------

Co-authored-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
2026-07-13 17:00:21 +04:00
MatinGathani d5a37efb85 fix(python): handle title-less top-level schemas (#3325)
## Summary

Fixes `json_schema_to_model` when a top-level JSON schema omits `title`
or sets it to `null`. This is the remaining direct-helper path;
anonymous nested objects from #2435 are already handled by
`schema_converter.py`.

- falls back to `GeneratedModel` only when the title is `None`,
preserving intentionally empty string titles
- adds regression coverage for missing, `null`, and empty titles, model
construction, and required-field validation

## Validation

- `ruff format --check python/composio/utils/shared.py
python/tests/test_schema_parser.py`
- `ruff check python/composio/utils/shared.py
python/tests/test_schema_parser.py`
- `PYTHONPATH="$PWD/python"
/Users/jkomyno/work/composio/composio2/.venv/bin/pytest -q
python/tests/test_schema_parser.py`

This is a follow-up to #2435; it intentionally does not close that
already-resolved issue.

Co-authored-by: jkomyno <alberto@composio.dev>
Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-13 14:01:32 +04:00
eeshsaxena 3e37f3c7a4 test: add unit tests for the mimetypes utility (#3750)
## Summary

`composio/utils/mimetypes.py` had no test coverage. This adds a suite
for both of its public helpers:

**`guess()`** (filename/extension → MIME type):
- known extensions resolve correctly (json, html, png, mp4, …)
- the lookup table is lower-case, so an upper-case suffix (`photo.PNG`)
falls back to the default
- unknown / missing extensions return `application/octet-stream`
- only the final suffix is used (`archive.tar.gz` == `archive.gz`)
- `Path` objects are accepted, not just strings

**`get_extension_from_mime_type()`** (MIME type → extension):
- direct mappings (`image/png` → `png`, `application/vnd.ms-excel` →
`xls`, …)
- parameters are stripped (`text/plain; charset=utf-8` → `txt`) and
matching is case-insensitive
- structured `+suffix` handling: known prefixes win
(`application/atom+xml` → `atom`), otherwise the suffix is used
(`application/vnd.api+json` → `json`, `application/foo+bar` → `bar`)
- subtype fallback (`application/x-custom` → `x-custom`), empty subtype
→ `txt`, and malformed/empty input → `bin`

Tests only — no library code changed. `pytest
python/tests/test_mimetypes.py` passes (`32 passed`); `ruff check` /
`ruff format --check` are clean.

Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-07 00:44:15 +04:00
Alberto Schiabel fc17c37bf9 fix(cli): enforce sensitive-file upload denylist (GHSA-hp3h-89pf-5q58) (#3763)
This PR:

- fixes #3746 / GHSA-hp3h-89pf-5q58 — the CLI's tool file-upload path
bypassed the sensitive-file denylist
- **root cause:** the denylist was enforced at the *caller* layer, so
`@composio/cli`'s duplicate upload path (`readFileFromDisk`) read and
uploaded any local path a tool argument pointed at — `~/.ssh/id_rsa`,
`~/.aws/credentials`, `.env`, etc. — enabling credential exfiltration
via `composio execute` / `composio run` (incl. LLM-driven agents hit by
prompt injection)
- **unified fix:** one canonical guard, exported from `@composio/core`
and enforced at the read primitive in every SDK
- `core`: the guard now routes fs/path access through the internal
`#platform` abstraction (adds a `realpathSync` method), so it carries
**no static `node:*` imports** and is exported from the package root:
`assertSafeFileUploadPath`, `isBlockedSensitiveFileUploadPath`,
`BUILTIN_FILE_UPLOAD_PATH_DENY_SEGMENTS`
- `cli`: `readFileFromDisk` calls the shared `assertSafeFileUploadPath`
before `fs.readFile`; URLs and `File` objects are unaffected (matches
core)
- `python`: already routes all uploads through the guarded
`FileUploadable.from_path` — no functional change; adds regression tests
for parity
- supersedes #3755, which exported the then-node-only guard from core's
**main** `index.ts` and would have injected `node:fs` into the shared
`dist/index.mjs` (the workerd/edge-light entry), likely breaking
Cloudflare Workers / Vercel Edge
- changesets: `@composio/core` minor (new public exports),
`@composio/cli` patch (security fix)

## Verification

- core: full suite passes (997), incl. new root-export +
realpath-symlink cases
- **edge bundle stays clean**: built `dist/index.mjs` has no static
`node:*` imports and reaches the platform only via `#platform`
- cli: new regression test drives the real `uploadToolInputFiles →
readFileFromDisk → guard` chain — `~/.ssh/id_rsa` and `.env` throw
`ComposioSensitiveFilePathBlockedError` before any `createPresignedURL`
call; a normal file still uploads
- python: `test_files.py` + sensitive-path suite pass (145), incl. 3 new
`from_path` guard tests
- `tsgo` typecheck + eslint clean for core and cli
2026-07-07 00:30:40 +04:00
Alberto Schiabel 820abb9072 fix(sdk): resolve toolkit versions case-insensitively in both SDKs (#3762)
## Summary

Toolkit version maps are keyed by **normalized (lowercase) slugs** on
the *write* side — env vars (`COMPOSIO_TOOLKIT_VERSION_*`) and
user-supplied dicts both get their keys lowercased when the map is
built. But the *lookup* read the map with the **raw slug**. A pin
configured under a different casing (e.g. `{ "GitHub": "20250101_00" }`
or `COMPOSIO_TOOLKIT_VERSION_GITHUB` looked up as `GitHub`) therefore
missed the map and silently fell back to `"latest"`, which
`Tools.execute` then rejects with a version-required error — discarding
the user's explicit pin.

Rather than patch only the read side, this **centralizes the
normalization rule** into a single helper per SDK and routes **both**
the map-building (write) and lookup (read) paths through it, so the two
sides can never drift apart again:

- **Python** — `normalize_toolkit_slug()` in
`python/composio/utils/toolkit_version.py`
- **TypeScript** — `normalizeToolkitSlug()` in
`ts/packages/core/src/utils/toolkitVersion.ts`, imported by
`getToolkitVersionsFromEnv` in `sdk.ts`

Behavior is now identical across the two SDKs. This **supersedes
#3759**, which patched only the Python read side and left TS diverging.

## Backend verification

Confirmed against the backend (staging, **1403 toolkits across 15
pages**): every toolkit `slug` is already lowercase (the mixed-case
`name` field is display-only). So the *primary* execute path — which
resolves via `tool.toolkit.slug` — was never broken in practice. This
change:

- **hardens** the direct-util and mixed-case-config paths (a user
passing `{ "GitHub": "…" }` now resolves as expected), and
- **removes a latent cross-SDK divergence**: the TS suite previously
asserted `getToolkitVersion` was *case-sensitive* (`versions.test.ts`),
locking in the bug for an input the production code path can't actually
produce. That test is flipped to assert case-insensitive resolution.

## Tests

- **Python** (`python/tests/test_toolkit_version.py`): case-insensitive
lookup via `get_toolkit_versions` round-trip, and against a raw
pre-normalized map. `18 passed`.
- **TypeScript** (`ts/packages/core/test/core/versions.test.ts`):
replaced the case-sensitivity assertion with case-insensitive resolution
+ a write→read round-trip of a mixed-case user pin. Full core suite `997
passed`.
- Typecheck (`tsc --noEmit`) clean; `ruff` clean.

## Notes

- Changeset added: `@composio/core` patch.
- No public API change; the fix is behavioral (case-insensitive) plus an
internal shared helper.
2026-07-06 15:30:06 +04:00
Alberto Schiabel 2e42d75259 fix(py): apply skip_default to agentic provider signatures (langchain, langgraph, autogen) (#3730)
## Summary

Agentic providers honor `schema_config={"skip_defaults": True}` for
`args_schema` (defaults stripped, fields become required) but were
**not** applying the same `skip_default` to the function
`__signature__`, so the wrapped tool's signature and `args_schema`
disagreed about which defaulted params were optional. The sibling
`llamaindex` provider already passed it to both.

**Supersedes #3727.** This is the polished version, now that the ag2
migration (#3729) landed and `composio-autogen` imports again — so the
autogen case uses a real import instead of the earlier source-load stub,
and the parity tests run in CI.

## Changes

- **langchain / langgraph** (original commit by @anxkhn): pass
`skip_default=self.skip_default` to
`get_signature_format_from_schema_params`.
- **autogen**: same one-line alignment. autogen builds **only**
`__signature__` (no `args_schema`), so it never had the *mismatch* — but
its signature still ignored `skip_defaults`. Aligned for consistency;
verified on the signature alone.
- **tests**: `TestAgenticSkipDefaultsParity` guards each provider with
`pytest.importorskip`; adds real-import autogen regression tests (no
stub).
- **CI + nox**: install `providers/autogen` in the unit-test job
(`py.test.yml`) and the nox `tst` session so the autogen parity tests
actually run, plus a **hard `import composio_autogen` smoke check** in
CI.

## Why the CI import guard (not just the tests)

The `importorskip`-guarded unit tests **skip** when a provider can't be
imported — which is the correct behavior for optional providers, but
means they'd *silently skip* if autogen broke on a fresh install
(exactly the failure that #3729 just fixed). The extra `python -c
"import composio_autogen"` in the import step is a **hard** check that
fails loudly, so a future dependency breakage can't slip through as a
green build.

`providers/langchain` + `providers/autogen` co-install cleanly
(verified: 59 packages, ag2 0.14.0, no conflict). langgraph continues to
skip in CI — its code path is identical to langchain's, which runs.

## Type of change

- [x] Bug fix

## How Has This Been Tested?

- Base venv (langchain only): langchain parity **passes**, langgraph +
autogen **skip** (no error).
- CI-mirror (langchain + autogen, editable): **4 passed, 2 skipped
(langgraph)** — autogen tests actually run and pass.
- Regression proof, each provider: reverting only the provider line
fails the skip-defaults case with `assert 5 is Parameter.empty`;
restoring passes. Verified for langchain **and** autogen.
- `ruff check`, `ruff format --check`, `mypy`, and full
`tests/test_provider.py` (29 passed, 4 skipped) → clean. `py.test.yml`
parses.

## Checklist

- [x] I ran linters/tests locally and they passed
- [x] I added tests: langchain/langgraph parity + regression, and
autogen parity that runs in CI
- [ ] Changeset: not applicable — Python-only change.

## Credits

Original fix by @anxkhn (#3713 → #3727), preserved as the first commit
and co-authored on the follow-up.

---------

Co-authored-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
2026-07-02 15:38:53 +04:00
Alberto Schiabel e78ed312c6 fix(core): run all parallel OpenAI tool calls from the first choice (#3726)
This PR:

- supersedes https://github.com/ComposioHQ/composio/pull/3712,
preserving the original commits by @serhiizghama (kept as
author/co-author)
- fixes `OpenAIProvider.handleToolCalls` to execute **every** tool call
in an assistant message — it previously only read `tool_calls[0]`, so
parallel tool calls (on by default) were dropped and their
`tool_call_id`s went unanswered, failing the next request with the "must
be followed by tool messages responding to each `tool_call_id`" error
- limits execution to the **first choice** — with `n > 1` the previous
loop ran each tool call once per choice and orphaned the `tool_call_id`s
belonging to the alternative completions
- aligns the Python SDK (`OpenAIProvider.handle_tool_calls`) with the
same first-choice behavior so both SDKs stay in parity
- fixes the stale `@composio/openai` test that modeled "multiple tool
calls" as separate `n` choices rather than one message's `tool_calls`
array
- clarifies in the changeset that the calls run **sequentially**, in the
order the model returned them (here "parallel" means the model issued
several calls in one turn, not concurrent execution), so each
`tool_call_id` is answered exactly once and deterministically
- adds regression tests (TS + Python): both calls run for parallel tool
calls in one message, and only the first choice runs when `n > 1`

Full `@composio/core` (996) and `@composio/openai` suites green; Python
`handle_tool_calls` tests, Ruff, and mypy clean; `tsc` clean.

---------

Co-authored-by: serhiizghama <zmrser@gmail.com>
2026-07-02 13:27:49 +04:00
Alberto Schiabel a69c3edea3 fix(triggers): align create() user_id resolution across SDKs (#3723)
This PR:

- builds on top of https://github.com/ComposioHQ/composio/pull/3714 —
addresses its code-review findings
- restores TS/Python parity on slug validation: Python `create()` now
validates the slug up-front and raises a new `TriggerTypeNotFound`,
mirroring the TS `ComposioTriggerTypeNotFoundError`
- switches both SDKs to the native `user_id` upsert field — TS drops the
`& { user_id }` intersection and Python drops the `extra_body` shim
(both pinned clients already expose the field)
- treats a blank/whitespace `userId` (TS) or
`user_id`/`connected_account_id` (Python) as missing instead of
forwarding it to the backend
- documents the error-contract change (`create()` no longer throws
`ComposioConnectedAccountNotFoundError` — that case now surfaces from
the backend) in the changeset and `ts/docs/api/triggers.md`, and bumps
the changeset from `patch` to `minor`
- adds Python tests for the 2FA (both-provided) path, unknown-slug, and
blank `userId`; tightens the TS auto-resolve assertion and adds an
empty-`userId` case

## ⚠️ Release gating

Since #3714, `triggers.create()` (both SDKs) relies on the backend
resolving the trigger connection from `user_id` on upsert
([ComposioHQ/platform#10932](https://github.com/ComposioHQ/platform/pull/10932)).
There is **no client-side fallback** — reintroducing one would undo
#3714's intent, so this is a release-sequencing requirement, not a code
change here.

- Do not release `@composio/core` or the Python SDK until #10932 is live
in all regions.
- Self-hosted / on-prem deployments must be on a backend version that
includes #10932.

## Review findings addressed

| # | Finding | Resolution |
|---|---------|------------|
| P1-1 | Coupled to unreleased backend, no fallback | Documented
dependency in changeset; flagged release-gating above (no fallback by
design) |
| P2-2 | Cross-SDK parity on slug validation | Kept `getType` in both;
added Python `TriggerTypeNotFound` mirroring TS |
| P2-3 | `user_id` bridge redundant in both SDKs | TS uses native field
directly; Python uses `user_id=` kwarg |
| P2-4 | Error-contract change shipped silently | Documented in
changeset + docs `Throws` section; `minor` bump |
| P2-5 | Python missing 2FA (both-provided) test | Added
`test_create_with_user_id_and_connected_account_id` |
| P3 6–13 | Cleanups | undefined-assertion, `none_to_omit`, docstring,
`parsedBody.data`, redundant `else`, stale comments, blank-string guard
|

## Verification

- TS: `vitest` 59 passed, `typecheck` clean, `eslint`/`prettier` clean
- Python: `pytest tests/test_triggers.py` 69 passed, `nox -s chk` (ruff
+ mypy) clean
2026-07-02 11:40:47 +04:00
Anas Khan c660007c71 fix(py): handle typeless and empty combiner schemas in openapi signatures (#3710)
### Summary
`function_signature_from_jsonschema` in
`python/composio/utils/openapi.py` builds the `__signature__` for tools
wrapped by the `google_adk` provider
(`providers/google_adk/composio_google_adk/provider.py:66`). It already
falls back to `Any` for a top-level property with no `type`, but the
nested combiner helpers do not, so real combiner shapes crash at wrap
time:

- a `oneOf`/`anyOf` option with no `type` key -> `KeyError: 'type'`
- an empty `oneOf`/`anyOf` list -> `TypeError: Cannot take a Union of no
types`
- an empty `allOf` list -> `KeyError: 'type'` (merges to `{}`)

This extends the existing "default to Any" intent to nested options.
Follow-up to #3708, which fixed the same bug family in `shared.py` and
noted this `openapi.py` case for a separate PR.

Fixes #

### Changes
- `_type_to_parameter`: read `schema.get("type")` instead of
`schema["type"]`; when the type is missing, return `t.Any` (mirrors the
top-level fallback). An unknown *string* type still raises
`InvalidSchemaError`.
- `_handle_composite_type`: return `t.Any` for an empty option list
instead of building an empty `Union`. Empty `allOf` merges to `{}` and
resolves to `Any` via the above.
- New `python/tests/test_openapi.py` (11 tests): this builder had none.
Asserts the five former crashes now resolve to `Any`, and that existing
shapes are unchanged.

### Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change

### How Has This Been Tested?
From `python/`:

```
uv run pytest tests/test_openapi.py -q   # 11 passed
make chk                                 # ruff + mypy -> clean
```

To confirm the tests guard the fix, the five former crashes (typeless
`anyOf`/`oneOf` option, empty `anyOf`/`oneOf`/`allOf`) raise on the
unmodified file and now resolve to `Any`; regressions cover
`anyOf:[str,null]` -> `Optional[str]`, a 4-member union, enum, array,
object, and a typeless property.

### 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 (internal util)
- [x] I added tests (new test_openapi.py)
- [ ] I added a changeset (Python-only, not needed)

Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-02 11:40:35 +04:00
Anas Khan b0ab8eacff fix(py): handle 4+ option and typeless anyOf/oneOf unions in tool signatures (#3708)
## Summary
`get_signature_format_from_schema_params` in
`python/composio/utils/shared.py` builds the `__signature__` for the
tool wrappers used by the langchain, langgraph, llamaindex, and autogen
providers. Its `oneOf`/`anyOf` handling hardcoded a ladder for exactly
1, 2, or 3 union members and raised `ValueError("Invalid 'oneOf'
schema")` for any union with four or more options. It also indexed
`PYDANTIC_TYPE_TO_PYTHON_TYPE[ptype.get("type")]` directly, so any
combiner option missing a `type` key produced `None` and raised
`KeyError(None)`.

Both shapes are valid JSON Schema and appear in real tool input schemas,
so a tool with a 4-way union, or a combiner option without an explicit
`type`, makes those four providers raise at tool-wrap time. This
replaces the ladder with a single map + `functools.reduce` path that
mirrors the existing `_build_union_from_options` in
`schema_converter.py`, keeping the 1/2/3-member and nullable `[type,
null]` outputs identical to before.

Fixes #

## Changes
- `python/composio/utils/shared.py`: in
`get_signature_format_from_schema_params`, replace the 1/2/3-member
`oneOf`/`anyOf` ladder (which raised `ValueError` for 4+ members) with a
map-each-option-to-a-Python-type + `functools.reduce` Union build that
supports any member count. Unknown/missing option types map to
`typing.Any` instead of raising `KeyError`. Adds `from functools import
reduce` and removes three now-unnecessary `# type: ignore` comments.
- `python/tests/test_schema_parser.py`: add
`TestGetSignatureFormatFromSchemaParams` (8 cases) covering 4- and
5-member unions, an `anyOf` option missing `type`, an all-typeless
`anyOf`, and regressions for the previously supported single/2/3-member
and nullable `[type, null]` shapes.

## Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change

## How Has This Been Tested?
From `python/`:

```
uv run pytest tests/test_schema_parser.py::TestGetSignatureFormatFromSchemaParams -v   # 8 passed
uv run pytest tests/test_schema_parser.py -q                                           # 75 passed
make chk   # ruff + mypy -> clean, no new ignores
```

To confirm the tests guard the fix, the new test class was also run
against the unmodified `shared.py`: 4 of the 8 fail there
(`ValueError`/`KeyError` on the 4+/typeless cases), and all 8 pass with
this change. The nullable `anyOf [type, null]` case still resolves to
`Union[str, Any, NoneType]` (its pre-existing behavior); collapsing
`null` into `Optional` lives in `schema_converter` and is intentionally
left out of this minimal fix, with the test documenting current
behavior.

## 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
- [ ] I added a changeset if this change affects published packages

## Additional context
No linked issue: this is a self-identified, reproduced bug in the older
schema->signature path. The fix deliberately mirrors
`schema_converter._build_union_from_options` so the two schema paths
agree on N-ary union handling. The same bug family exists in
`python/composio/utils/openapi.py`
(`function_signature_from_jsonschema`, used by the google_adk provider)
but is kept out of this PR to stay single-purpose; happy to follow up on
it separately.
2026-07-02 11:39:41 +04:00
Anshu Garg 20a4711ee0 feat(triggers): resolve connection from user_id server-side in create() (#3714)
## What

`triggers.create()` used to make an extra **`connectedAccounts.list()`**
call to turn a `user_id` into a `connected_account_id` before calling
upsert. The backend now resolves the connection from `user_id` directly
(picks the first active connection for the user + the trigger's toolkit,
parity with tool execution), so the SDK passes `user_id` straight
through and drops the round-trip.

- **TS** (`ts/packages/core`): removed the `connectedAccounts.list()`
block in `Triggers.create()`. `user_id` was already forwarded to upsert
(for 2FA); this just removes the now-redundant lookup. `getType` is kept
for an early, clear "trigger type not found" error.
`connected_account_id` is passed when the caller pins one.
- **Python** (`python/composio`): dropped
`_get_connected_account_for_user`; `user_id` is sent via `extra_body`
and `connected_account_id` is omitted when not provided.

## Why

The SDK only ever holds a `user_id`, so every `triggers.create()` paid
an extra API call to find a connection. The backend change
(ComposioHQ/platform#10932) makes trigger upsert accept `user_id` and
auto-resolve — this PR consumes that.

## Behavior notes

- When `connected_account_id` is omitted, the backend picks the first
active connection ordered by `created_at desc`. The TS SDK's old
`list()[0]` ordering was unspecified — selection is now deterministic
and consistent with tool execution.
- "No connected account found" / "connected account not found" now
surface from the **backend** instead of a client-side pre-check.
- When 2FA is enabled and `connected_account_id` is pinned, the backend
validates that `user_id` owns it (unchanged; `user_id` was already
forwarded).
- The `extra_body` / `& { user_id }` bridges are still needed only until
`@composio/client` / `composio_client` regenerate with `user_id` on the
upsert type (the field is already in the OpenAPI contract).

## Tests

- TS: `vitest run test/models/triggers.test.ts` → **58 passed** (create
tests rewritten to assert no `connectedAccounts.list()` call + `user_id`
forwarded).
- Python: `pytest tests/test_triggers.py` → **66 passed** (same).

## Depends on

Backend: ComposioHQ/platform#10932 (`feat(apollo): resolve trigger
connection from user_id on upsert (PLEN-2580)`).

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

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 01:05:07 +04:00
Alberto Schiabel 9901e01a80 feat(py): add tool router session deletion
Release Python 0.17.1 with Tool Router session deletion support.
2026-06-28 22:59:25 +04:00
Alberto Schiabel 956f9be9b4 chore(agents): normalize repo guidance skills (#3666)
This PR:
- replaces the duplicated repo-local skill set with the requested
canonical `.agents/skills` taxonomy and short router `SKILL.md` files
backed by first-level references
- makes `.claude/skills` a compatibility symlink to `.agents/skills` and
removes hand-maintained Claude skill/rule copies
- moves docs agent guidance and decisions into neutral
`docs/agent-guidance/` and `docs/decisions/`, with `docs/CLAUDE.md`
reduced to a shim
- retires CLI Cursor rules after migrating CLI design, Effect source,
and client-cache sync guidance into `AGENTS.md` and `cli-command`
- adds nested `AGENTS.md` files for TS, core, providers, e2e, Python,
Python providers, and docs
- adds `pnpm validate:agent-skills` to validate skill frontmatter,
taxonomy, references, symlink invariants, stale paths, and command names
- implements missing Python `tst` and `snt` nox sessions that existing
Makefile targets already exposed
- no changeset: repository guidance/tooling only, no published SDK
package behavior

## Verification
- `pnpm validate:agent-skills` -> `Validated 14 canonical agent skills
and guidance invariants.`
- `for skill in .agents/skills/*; do python
/Users/jkomyno/.codex/skills/.system/skill-creator/scripts/quick_validate.py
"$skill" || exit 1; done` -> 14x `Skill is valid!`
- `pnpm --dir ts/packages/cli validate:skills` -> `Validated
composio-cli skill builds for stable and beta.`
- `cd python && uv run nox --list` -> includes `tst` and `snt`
- `cd python && uv run nox -s snt` -> 18 passed
- `cd python && uv run nox -s tst -- tests/test_imports.py` -> 8 passed
- `git diff --check` / `git diff --cached --check` -> clean
- stale reference search for retired docs/Claude/Cursor paths -> no
matches

## Forward Tests
- TypeScript core bug: loaded `bug-fixing`, `typescript-sdk`,
`typescript-testing`; found correct root/ts/core `AGENTS.md` route.
- Python provider: loaded `python-providers`, `python-testing`; found
missing nox sessions, fixed here.
- CLI command: loaded `cli-command`; found recording/changeset wording
gaps, fixed here.
- Cross-SDK drift: loaded `cross-sdk-parity`; again found Python nox
drift, fixed here.
- Docs + decision: loaded `docs-decisions`; found decision
template/index and Twoslash path gaps, fixed here.

## Notes
The first normal `git commit` attempt hit a lint-staged/Git stash
limitation while replacing `.claude/skills/` with a symlink (`path ...
beyond a symbolic link`). The final commit used `--no-verify` after the
validators and formatting checks above passed.
2026-06-27 00:35:57 +04:00
Alberto Schiabel e7bb45f449 fix(py): normalize generated SDK doc examples (#3689)
This PR:
- normalizes Python SDK reference examples generated from source
docstrings
- rewrites the `Triggers.parse()` docstring so the generated
Flask/FastAPI examples are copy-pasteable
- strips nested Markdown fences before wrapping generated examples in
MDX code fences
- regenerates `docs/content/reference/sdk-reference/python/`
- adds regression coverage for the `Triggers.parse()` generated example
and fenced example normalization
- does not include a changeset because this only changes docs
generation, generated docs, and tests

## Verification

- `cd python && .venv/bin/python -m pytest tests/test_generate_docs.py
-q`
- `cd python && .venv/bin/ruff check --config config/ruff.toml
composio/core/models/triggers.py scripts/generate-docs.py
tests/test_generate_docs.py`
- `cd python && .venv/bin/ruff format --check --config config/ruff.toml
composio/core/models/triggers.py scripts/generate-docs.py
tests/test_generate_docs.py`
- `cd python && .venv/bin/python scripts/generate-docs.py`
- `cd docs && bun install --frozen-lockfile`
- `cd docs && bun run types:check`
2026-06-26 15:12:15 +04:00
Rahul Tarak d17a268d3f docs: sessions-first rewrite — new guides, examples & components (+ core 0.13.0 SDK changes) (#3637)
Integration branch for the next docs release: a **sessions-first
documentation rewrite** — new and rewritten guides, example pages,
interactive components, and docs tooling — plus the supporting SDK
changes that the new docs describe.

The bulk of this PR is docs (~24k lines across ~150 commits); the SDK
changes (~5k lines) back the new guides.

## Documentation (the bulk)

- **Sessions-first restructure** — reorganized navigation and section
structure (incl. the "Sandbox (prev workbench)" section), with
v3-reorganization redirects so old URLs keep resolving.
- **Rewritten core guides** — quickstart, configuring sessions, triggers
(creating + subscribing to events), proxy-execute, toolkits
enable/disable, and common FAQ, rewritten in the house voice.
- **New example pages** — local-sandbox PR reviewer, daily standup bot,
and slack bot, with runnable build-ups.
- **New interactive components & diagrams** — triggers flow animation,
manage-connections visual, connection-refresh visual, and the
terminal-kit components.
- **Docs tooling** — a docs-graph link-graph connectivity checker,
search reprioritization (deprioritize legacy pages), and SDK-reference
regeneration.

## Supporting SDK changes

**`@composio/core` → 0.13.0 (minor)**
- `composio.sessions.create()` as the first-class sessions API
(`composio.create()` kept as an alias).
- **MCP is opt-in:** default `create()` / `use()` return native-tool
sessions (`SessionWithoutMcp`); pass `{ mcp: true }` to surface
`session.mcp`. _Migration: read `session.mcp` only after creating with
`{ mcp: true }`._
- `session.sandbox` is the canonical resolved config;
`session.workbench` kept as a deprecated alias. `sandbox` is the
preferred session-config key (`workbench` still accepted).
- `connectedAccounts.updateAcl()` graduated from experimental (alias
kept).
- `triggers.parse()` (parse + optionally verify an incoming webhook) and
`triggers.setWebhookSubscription()`.

**`@composio/experimental` → minor** — local-workbench helpers moved
onto the `@composio/experimental/workbench` subpath (out of
`@composio/core/experimental`), keeping the ~14 KB embedded Python
helper out of core. Plus the experimental Pi provider.

**`@composio/slim` → minor.**

**Python → 0.17.0** — mirrors the TS surface: `composio.sessions` mount
(`tool_router` deprecated), `triggers.parse()` /
`set_webhook_subscription()`, the `sandbox` config key, and
`connected_accounts.update_acl()`.

## Review response (#3664)

Addressed the `@composio/core` review:
- **Security:** `triggers.parse()` no longer fails open — a
present-but-empty `verifySecret` (e.g. unset `COMPOSIO_WEBHOOK_SECRET`)
now throws instead of silently skipping verification; omitting it stays
an explicit opt-out (both SDKs).
- Removed snake_case leakage from `transformWebhookSubscription` (+ the
index signature that allowed it).
- **Removed** the TS-only `connectedAccounts.link()` toolkit
auto-resolve (shipped with cancellability / orphaned-auth-config bugs
and was effectively undocumented; to be reintroduced properly later).
- Unified Python error types on `ValidationError`; added `mcp=True`
Python tests; fixed runtime-portability + error-type test assertions.
- Polished deprecation messages; fixed the backwards `/experimental`
`@deprecated` note and the `SessionWithMcp` JSDoc.

## Testing

- **TS:** `@composio/core` + `@composio/experimental` typecheck pass;
vitest green for the touched suites.
- **Python:** `test_tool_router.py` + `test_triggers.py` pass (161
tests).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Kshitij Jhunjhunwala <kj@composio.dev>
Co-authored-by: Malay Vasa <malayvasa@gmail.com>
Co-authored-by: Sarah Simionescu <sarah@composio.dev>
Co-authored-by: Kshitij Jhunjhunwala <113939507+KJ-11@users.noreply.github.com>
2026-06-25 18:27:43 -07:00
Alberto Schiabel 31315357dd fix(py): correct without_retries rationale, preserve strict validation, pin copy() contract (#3658)
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).
2026-06-25 02:46:51 +04:00
Alberto Schiabel 09b552aa03 fix(py): disable retries on non-idempotent tool writes to prevent duplicate side effects (#3657)
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).
2026-06-25 02:24:27 +04:00
Alberto Schiabel d94a49ff43 fix(py): centralize tool schema aliasing (#3652)
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.
2026-06-25 01:37:12 +04:00
Alberto Schiabel aeeb471c63 fix(py): resolve $ref/$defs so file flags behind a reference aren't dropped (#3653)
## 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.
2026-06-25 00:53:50 +04:00
Xiaoju dd40a9bb6f fix(py): bound file transfer requests with timeouts (#3563)
## 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 #3560
Fixes #3561
Fixes #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>
2026-06-24 23:00:10 +04:00
Anshu Garg 56e0d781f3 fix(triggers): handle V3 envelope in realtime subscribe path [SUP-392] (#3567)
## 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>
2026-06-16 15:39:23 +04:00
Anshu Garg a94715f577 feat(sdk): forward user_id on triggers.create for trigger 2FA (#3576)
## 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>
2026-06-16 13:10:37 +04:00
Alberto Schiabel ce4b213361 fix(providers): normalize string tool-call arguments across all providers (TS + Python) (#3514)
## 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>
2026-06-16 12:49:37 +04:00
Alberto Schiabel 2758287c87 feat(py): remove legacy custom tools (#3508)
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.
2026-06-04 23:53:26 -07:00
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
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 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
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 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 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 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 26eda4606d fix(python): attach existing tool router sessions 2026-05-07 02:11:26 -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