Commit Graph

295 Commits

Author SHA1 Message Date
Alberto Schiabel 26ec42a38c chore(py): prepare 0.19.0 release (#4114)
This PR:

- builds on top of https://github.com/ComposioHQ/composio/pull/4094
- bumps `composio` and all 12 provider distributions from `0.18.2` to
`0.19.0`
- keeps `python/composio/__version__.py` and `uv.lock` aligned with
package metadata
- documents recursive provider argument-presence preservation in the
existing cross-SDK changelog
- passes the release-workflow guard, Ruff, mypy, provider type
inference, and 1,167 Python tests
- builds and validates 26 wheel/sdist artifacts with Twine
- must be retargeted to `next` after #4094 merges
2026-08-11 10:44:44 +02:00
jkomyno 64fbc4ee03 fix(py): preserve nested provider argument presence 2026-08-10 22:14:21 +02:00
Alberto Schiabel 82e25a603e Merge branch 'fix/cross-sdk-json-schema-object-conversion' into fix/python-provider-argument-serialization 2026-08-10 21:23:35 +02:00
Alberto Schiabel 1af694baee Merge branch 'next' into fix/cross-sdk-json-schema-object-conversion 2026-08-10 21:23:10 +02:00
jkomyno 5bb1e20518 fix(py): preserve serialized provider arguments
Co-authored-by: Aditya Chawla <aditya17.mitmpl2023@learner.manipal.edu>
2026-08-10 20:02:23 +02:00
jkomyno 191d919eef fix(python): harden dynamic schema validation 2026-08-07 21:56:39 +05:30
Alberto Schiabel c337e5696e chore(sdk): prepare Python 0.18.2 and TypeScript 0.15.0 releases (#4095)
Prepares the next SDK train: Python `composio` `0.18.2` and the
TypeScript packages that the pending Changesets will publish
(`@composio/core` and `@composio/slim` `0.15.0`, `@composio/openai`
`0.11.0`, `@composio/experimental` and `@composio/json-schema-to-zod`
`0.2.2`).

No CLI release is part of this train.

## What this PR does

- bumps Python core and all 13 provider packages to `0.18.2`, including
`python/composio/__version__.py` and the root plus per-provider
`uv.lock` files
- adds `docs/content/changelog/08-07-26-sdk-releases.mdx`, the combined
customer-facing changelog for both SDKs
- documents `@composio/core` `0.15.0` in that entry so the generated
release PR passes the release-workflow guard
- leaves the pending Changesets untouched; the generated release PR
remains responsible for npm versioning and publication

## Release sequence

1. Merge this PR.
2. Let Changesets regenerate the "Release: update version" PR, wait for
it to go green, then merge it. Merging publishes the TypeScript
packages.
3. Tag the resulting `next` commit `py@0.18.2` to publish the Python
packages to PyPI.

## Notes

- The root `uv.lock` diff carries ~28 lines of marker-annotation churn
beyond the version bumps. That is the repo-pinned `uv` `0.8.19`
normalising markers the committed lockfile had recorded differently; no
dependency versions change.
- `@composio/mastra` and the TypeScript providers are unchanged: their
declared peer ranges still accept `@composio/core` `0.15.0`, so
Changesets does not bump them.

## Verification

- `pnpm test:release-workflow` — passes (Python package, runtime, and
provider versions agree; both SDK versions documented in the changelog)
- `uv run pytest tests/` in `python/` — 925 passed, 35 skipped
- `uv run --package composio python -c "import composio"` — reports
`0.18.2`
- `bun test tests/static/` in `docs/` — 195 pass, 1 pre-existing failure
(`getLLMText — version pointer`, also fails on a clean tree)
- `bun run types:check` in `docs/` — MDX generation and route typegen
succeed; the final `tsc` step could not run locally because
`typescript-7` is absent from `docs/node_modules`
2026-08-07 21:35:55 +05:30
jkomyno 3fef432dc0 fix: fail closed on invalid dynamic schemas 2026-08-07 21:03:08 +05:30
jkomyno 1f94167595 fix(python): walk only schema positions when checking dynamic refs
The dynamic-key reference walker descended into every nested dict, so a
$ref-shaped value stored under const, default, enum, or examples was
treated as a schema reference and raised while the tool was wrapped.
Restrict the walk to Draft 7 schema positions.
2026-08-07 19:39:06 +05:30
LHMQ878 051c8c5357 fix(telemetry): redact secrets inside JSON payloads (#4043)
## Summary

Telemetry error text is redacted before it leaves the process, but the
key/value rule only matches when the separator follows the key name
directly:

```
\b(authorization|api[-_]?key|...|pwd)\b(\s*[:=]+\s*)(["']?)([^\s"',}&]+)\3
```

In JSON — and in a Python `dict` repr — the key's own closing quote sits
between the name and the colon, so `{"api_key": "..."}` never matches
and the value is sent verbatim. That is the shape error messages usually
carry: an API error envelope, a rejected request body echoed back, or an
f-string interpolating a config dict. Both SDKs share the pattern and
both are affected.

The fix moves the optional quote into the separator group
(`(["']?\s*[:=]+\s*)`). The key name, separator, and quoting are all
preserved in the output; only the value is replaced.

Measured with `redact_sensitive_text` / `redactSensitiveText` directly,
before and after:

| input | before | after |
|---|---|---|
| `api_key=sk-1` | redacted | redacted |
| `api_key: "sk-1"` | redacted | redacted |
| `x-api-key: sk-live-hdr` | redacted | redacted |
| `{"api_key": "sk-live-abc"}` | **leaks** | `{"api_key": "[REDACTED]"}`
|
| `{"api_key":"sk-live-abc"}` | **leaks** | redacted |
| `{"api_key" : "sk-live-abc"}` | **leaks** | redacted |
| `{"refresh_token":"rt-abc.def-123"}` | **leaks** | redacted |
| `{"x-api-key":"sk-hdr","user":"bob"}` | **leaks** | redacted,
`"user":"bob"` kept |
| `{'client_secret': 'cs-live-abc'}` | **leaks** | redacted |
| `the password field is required` | untouched | untouched |
| `no separator here "api_key" and nothing else` | untouched | untouched
|

The end-to-end path in Python is `composio/core/models/base.py:60-61`,
which passes `str(e)` and `traceback.format_exc()` through the redactor
into the telemetry `error` field. A tool failure whose message
interpolates the rejected request body — `Error executing tool: request
body was rejected: {"toolkit": "GMAIL", "arguments": {"api_key": "...",
...}}` — leaked the customer's key today.

## Changes

- `python/composio/utils/redaction.py`,
`ts/packages/core/src/telemetry/redact.ts`: allow a quote between the
key name and the separator.
- Tests on both sides for JSON, minified JSON, spaced-colon JSON, dict
repr, the realistic serialized-payload shape, key/quote preservation,
and no-false-positive cases.
- Changeset for `@composio/core`.

## Type of change

- [x] Bug fix

## How Has This Been Tested?

Node 24.13.0 / pnpm 11.8.0 / Python 3.10.20 (uv), Windows.

- `uv run pytest tests/test_redaction.py` — 13 passed. With
`redaction.py` reverted and the new tests kept, 9 of them fail; the 4
that still pass are the pre-existing test plus the three
no-false-positive controls.
- `npx vitest run test/telemetry/redact.test.ts` in `ts/packages/core` —
10 passed. With `redact.ts` reverted, the 3 new cases fail.
- Full Python suite: 913 passed, same 4 failures as on `next` unchanged
(3 are `ModuleNotFoundError: composio_langchain` from providers not
installed in my env; 1 is `test_empty_name_safe_fails_at_write_time`,
which expects `IsADirectoryError` but Windows raises `PermissionError`
when opening a directory). Collection 944 → 956, exactly the 12 tests
added.
- Full `@composio/core` vitest: 798 passed / 2 failed, byte-identical to
the unchanged baseline (794 passed / same 2 failed — both Windows
path-and-symlink cases). 11 test files fail to load in both runs because
I installed with `--ignore-scripts`; the repo's `preinstall` hook
doesn't run in my shell.
- `ruff format --check` and `ruff check` clean on the two Python files;
`prettier --write` applied to the two TypeScript files.

## 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 — no user-facing behavior change
to document
- [x] I added tests or explain why not applicable
- [x] I added a changeset if this change affects published packages

## Additional context

This is defence-in-depth, so it stays deliberately narrow: same denylist
of key names, same value character class, no new rules. Bare-prefix
secrets (`sk-...`, `ghp_...`) and PEM blocks are still not matched by
either SDK — worth a separate look, but widening the pattern is a
different risk trade-off than closing a shape the rule already intends
to cover.
2026-08-07 19:14:49 +05:30
jkomyno 4f009baa69 fix(python): resolve nested dynamic refs from document root 2026-08-07 19:11:18 +05:30
jkomyno 4a89ff112c fix(json-schema): enforce dynamic object contracts 2026-08-07 00:06:55 +05:30
jkomyno bbeacac653 fix(python): preserve and police dynamic object content in schema conversion
`json_schema_to_model` and `json_schema_to_pydantic_type` now share one
compiled object policy, so provider-facing Pydantic models stop silently
discarding valid free-form arguments and stop ignoring `patternProperties` and
explicit `additionalProperties` controls. Accepted dynamic keys are written back
into `__pydantic_extra__`, which is what `LangchainProvider.wrap_tool` re-reads
with `getattr`.

Acceptance and rejection are asserted from the shared cross-SDK corpus, so the
Python entry points cannot diverge from the TypeScript converters.
2026-08-06 20:40:59 +05:30
Alberto Schiabel 0ae443bece chore(sdk): prepare Python 0.18.1 and TypeScript 0.14.1 releases (#3988)
This PR:
- prepares Python SDK core and provider packages for `0.18.1`, including
runtime and lockfile metadata
- adds the canonical combined changelog for Python `0.18.1` and the
TypeScript releases tracked by
https://github.com/ComposioHQ/composio/pull/3906
- documents `@composio/core` `0.14.1` so the generated release PR passes
the release-workflow guard
- refreshes `mise.lock` so the TypeScript audit workflow reaches the
dependency audit
- keeps the pending TypeScript Changesets untouched; the generated
release PR remains responsible for npm versioning and publication
- validates Python with Ruff/mypy, 944 tests, all 13 package builds, and
`twine check`
- validates TypeScript/docs with the full 26-task `pnpm test`,
high-severity production audit, 100 docs static tests, link checks, docs
typecheck, and production build
2026-07-30 16:03:55 +05:30
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
Alberto Schiabel facd40258c chore(py): prepare 0.18.0 release (#3825)
This PR:
- builds on https://github.com/ComposioHQ/composio/pull/3823 and
https://github.com/ComposioHQ/composio/pull/3824
- bumps the Python SDK and every provider package from `0.17.1` to
`0.18.0`
- synchronizes the runtime version and root `uv.lock`
- adds the canonical July 16 changelog entry, including URL-upload and
telemetry security, trigger connection-resolution behavior, provider
schema fixes, and the `pyautogen` to `ag2` migration
- strengthens release guards for Python workflow invocation, provider
metadata, and current Python and TypeScript changelog coverage
- limits provider packaging to directories containing `pyproject.toml`
- makes provider cleanup, installation, and builds stop on the first
failure
- incorporates the merged TypeScript changelog and missing Changeset
from https://github.com/ComposioHQ/composio/pull/3849

After merging, create and push the annotated `py@0.18.0` tag from the
merged `next` commit.

## Validation

- `pnpm test:release-workflow`
- `uv lock --check`
- `uv run --frozen python -c "import composio; assert
composio.__version__ == \"0.18.0\""`
- `pnpm exec prettier --check
docs/content/changelog/07-16-26-python-sdk-018.mdx
test/release-workflow.test.ts`
- `cd docs && bun run test`
- `cd docs && bun run lint:links`
- `cd docs && bun run types:check`
- `cd python && make build`
- `cd python && uv tool run twine check dist/*`
2026-07-16 00:33:30 +04:00
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
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 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 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 9df19b46cc chore(python): release 0.17.0 (#3678)
Python SDK 0.17.0 (mirrors the TS `@composio/core` 0.13.0 surface).

`make bump` (minor) bumped `pyproject.toml` + all provider packages to
0.17.0; this PR also syncs `composio/__version__.py` and adds the `##
[0.17.0]` CHANGELOG entry so the release guard (`__version__` ==
`pyproject` == `CHANGELOG[0]`) passes — verified locally (`release
workflow test passed`).

## 0.17.0 surface
- `triggers.parse()` (parse + optionally verify a webhook; empty
`verify_secret` now raises instead of silently skipping) and
`triggers.set_webhook_subscription()`
- `composio.sessions` is the canonical sessions mount;
`composio.tool_router` deprecated alias
- MCP is opt-in (`mcp=True`); default sessions return native tools
- Prefer the `sandbox` session config key (`workbench` still accepted)
- `connected_accounts.update_acl()` (graduated from experimental)

Merging this triggers the Python publish.

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:28:20 -07: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
Alberto Schiabel e408af962a fix(py): align Python release metadata (#3634)
This PR:
- fixes
https://github.com/ComposioHQ/composio/actions/runs/27964338497/job/82754032484
- aligns `python/composio/__version__.py` with the current Python
package version `0.16.0`
- adds the missing `0.16.0` Python changelog entry for the dependency
refresh
- restores `pnpm run test:release-workflow` on `next`
2026-06-22 20:42:56 +04:00
Alberto Schiabel a0bef5d986 chore(deps): bump composio client SDKs (TS alpha.74, Py 1.41.0) (#3609)
This PR:

- bumps the TS catalog `@composio/client` from `0.1.0-alpha.72` to
`0.1.0-alpha.74` (published from ComposioHQ/composio-base-ts#84) and
refreshes `pnpm-lock.yaml`
- bumps the Python `composio-client` pin from `1.39.0` to `1.41.0`
(published from ComposioHQ/composio-base-py#69)
- adds a patch changeset for `@composio/core` and `@composio/cli` so the
client bump actually ships in the next release

## Context

Neither client is auto-bumped in this repo — there is no
Stainless→consumer bot, and Dependabot does not touch the pnpm
`catalog:` pin (TS) or the exact `composio-client==` pin (Py), so these
were a manual catch-up. The Python `1.41.0` PyPI publish initially
failed with a `403 Forbidden` (stale `PYPI_TOKEN`); it was re-run
successfully before this bump, so `1.41.0` is live on PyPI.
2026-06-19 18:42:59 +04:00
Alberto Schiabel eeaac651d8 chore(py): document pending releases (#3583)
This PR:
- documents the pending Python `0.14.0` and `0.15.0` releases in
`python/CHANGELOG.md`
- aligns `python/composio/__version__.py` with `python/pyproject.toml`
at `0.15.0`
- adds a release guard so Python package/provider versions, runtime
version, and changelog entries cannot drift silently
- leaves Python release tags and PyPI publishing untouched; `py@0.14.0`
should still be published before `py@0.15.0`
2026-06-18 12:33:42 +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 4fe776898a chore(toolchain): finish mise migration (#3493)
## Summary

Depends on #3492.

This completes the Phase 2 migration by removing the transitional
version-file layer and making `mise.toml` plus `mise.lock` the
repository toolchain source of truth. It also moves runtime test
matrices into `toolchain-versions.json`, so CI matrix changes are
explicit and reviewable without reintroducing `.nvmrc`, `.dvmrc`,
`.bun-version`, or `.python-version`.

The Node.js e2e matrix now starts at the latest Node 22 LTS line and
also covers the latest Node 24 and Node 25 lines. That removes Node 20
from the well-known e2e versions while keeping us covered on the
runtimes SDK users are moving toward.

## Rationale

Phase 1 introduced mise side by side with the existing version files to
keep the first PR low-risk. Phase 2 removes that compatibility layer so
there is one place to update tool versions. That avoids silent drift
between local setup, GitHub Actions, Docker E2E images, release docs,
and install-time checks.

The composite setup actions now install Node, Bun, Python, and uv
through mise by default, with explicit version overrides only where a
matrix needs them. New GitHub actions added in this PR are pinned by
release commit SHA and include the release version comment.

## What changed

- Deleted the transitional root/version files: `.nvmrc`, `.bun-version`,
`.dvmrc`, root `.python-version`, and `python/.python-version`.
- Removed `idiomatic_version_file_enable_tools` from `mise.toml` and
added a committed `mise.lock` for linux/macOS x64/arm64 tool resolution.
- Replaced `BYPASS_BUN_VERSION_CHECK` with `BYPASS_TOOLCHAIN_CHECK`, and
made Docker E2E image installs use that bypass because they receive
explicit build args instead of installing mise.
- Updated Node/Bun and Python/uv composite actions to default to mise,
remove `*-version-file` inputs, report resolved versions, and cache pnpm
after `corepack enable`.
- Centralized CI runtime matrices in `toolchain-versions.json` for TS
E2E, Python tests, and CLI npm fallback coverage.
- Updated the Node E2E matrix to `22.22.3`, `24.16.0`, and `25.9.0`,
removing Node 20 from the well-known runtime versions.
- Updated workflows, docs, E2E helpers, Dockerfiles, and release
guidance to reference `mise.toml` / `mise.lock`.

## Verification

- Verified latest Node 22/24/25 releases from the official Node dist
index: `22.22.3`, `24.16.0`, `25.9.0`
(https://nodejs.org/dist/index.json).
- `pnpm install --frozen-lockfile`
- `pnpm --filter @e2e-tests/utils typecheck`
- `pnpm --filter @e2e-tests/utils exec tsc --noEmit --target es2022
--module esnext --moduleResolution bundler --types bun
--resolveJsonModule --skipLibCheck --strict scripts/docker-build.ts`
- `bash -n ts/scripts/pre-install.sh && bun run
ts/scripts/pre-install/check-toolchain.ts && BYPASS_TOOLCHAIN_CHECK=1
bash ts/scripts/pre-install.sh`
- `pnpm exec prettier --check ...` on touched YAML/Markdown/TS/JSON
files
- `ruby -e "require \"yaml\"; ARGV.each { |f| YAML.load_file(f) }" ...`
on touched actions/workflows
- `mise lock --platform linux-x64,linux-arm64,macos-arm64,macos-x64 &&
git diff --exit-code mise.lock`
- `mise exec node@22.22.3 -- pnpm --filter @e2e-tests/utils typecheck`
- `mise exec node@22.22.3 -- pnpm --filter @e2e-tests/node-esm-basic
typecheck`
- `mise exec node@22.22.3 -- pnpm --filter @e2e-tests/node-cjs-basic
typecheck`
- `docker manifest inspect node:24.16.0-slim`
- `docker manifest inspect node:25.9.0-slim`
- `git diff --check`
2026-06-15 14:03:21 +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