Commit Graph

234 Commits

Author SHA1 Message Date
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
jkomyno 27a4e6448e test(connected-accounts,py): patch time.time defensively in terminal-state test
`test_wait_for_connection_fails_fast_on_terminal_status` only patched
`time.sleep`, not `time.time`. A stuck CI runner could in principle
exhaust the 10s deadline before the first `retrieve()` call resolves,
in which case the test would assert against a
`ComposioSDKTimeoutError` instead of the `SDKError` it's actually
exercising. Match the monotonic-counter pattern used by the sibling
tests at lines 137 and 170 to remove that flake surface.

Surfaced by kieran-python review of PR #3344 (P3#4).
2026-05-07 10:56:29 +02:00
jkomyno a7c2d16bf3 test(connected-accounts): cover REVOKED rollout and forward-compat
Adds the test coverage flagged in PR #3344 review item P1#5:

TS (`ts/packages/core/test/connectedAccounts/connectedAccountAuthStates.test.ts`):
- `Oauth2RevokedConnectionDataSchema` parses with and without optional
  `revoked_at`
- `ConnectionDataSchema` discriminated union accepts REVOKED for both
  OAUTH2 and S2S_OAUTH2 (the preempted Apollo arm from P1#1)
- `transformConnectedAccountResponse` does not silently drop `state`
  for REVOKED on either auth scheme
- Closure test on the `ConnectionStatuses` enum so a future Apollo
  addition trips a CI failure on purpose

TS CLI (`connected-accounts.list.cmd.test.ts`):
- Server returning an unrecognized status no longer crashes the
  command — the `catchTag('ParseError')` fallback warns the user to
  run `composio upgrade` and renders the row anyway

Python (`tests/test_connected_accounts.py`):
- `wait_for_connection` fails fast on FAILED / EXPIRED / REVOKED
  (parameterized) — the contract added by this PR
- `wait_for_connection` does NOT treat INACTIVE as terminal, so a
  connection briefly transiting through INACTIVE still recovers to
  ACTIVE — guards against the regression called out in P1#2
2026-05-07 10:29:14 +02:00
Abir Taheer e3617d1251 style(python): ruff format test_tool_router.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 23:36:58 -07:00
Abir Taheer a03b3759ec fix(python): update test to expect coerced array for connected_accounts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 23:33:47 -07:00
Dhawal Upadhyay 73b42a29a8 Support SDK-local custom tool preload for v3.1 (#3349)
## Summary
- Add `preload` on TS/Python custom tools and custom toolkits so
SDK-created sessions can expose selected custom schemas directly from
`session.tools()`.
- Keep top-level `preload.tools` for backend-owned Composio tool slugs /
`all`; reject custom slugs there with guidance to use the SDK custom
definition flag, and reject bare-string Python misuse except `"all"`.
- Pass inline custom definitions on create/search so backend
validation/search has current definitions without persisting them;
direct `session.execute(...)` remains generated-client typed and does
not carry custom payload.
- Preserve `sessionPreset: "direct_tools"` /
`session_preset="direct_tools"`, including explicit override precedence;
direct-tools uses `preload.tools = "all"` only when the caller does not
override preload.
- Add OpenAI Agents SDK examples for explicit preload and
`sessionPreset: "direct_tools"` in TS and Python, including
toolkit-level preload with a nested `preload: false` / `preload=False`
override.

## Validation
- `pnpm --dir ts/packages/core test test/models/toolRouter.test.ts
test/models/customToolRouting.test.ts`
- `pnpm --dir ts/packages/core typecheck`
- `env COMPOSIO_CACHE_DIR=/private/tmp/composio-cache
python/.nox/chk/bin/python -m pytest python/tests/test_custom_tools.py
python/tests/test_tool_router.py`
- `python/.nox/chk/bin/python -m ruff check
python/examples/tool_router/preload.py
python/examples/tool_router/direct_tools_preset.py
python/composio/core/models/custom_tool.py
python/composio/core/models/custom_tool_types.py
python/composio/core/models/tool_router.py
python/composio/core/models/tool_router_session.py
python/tests/test_custom_tools.py python/tests/test_tool_router.py`
- `python/.nox/chk/bin/python -m py_compile
python/examples/tool_router/preload.py
python/examples/tool_router/direct_tools_preset.py`
- `pnpm --dir ts/examples/tool-router exec tsc --noEmit --skipLibCheck
--target ES2022 --module ESNext --moduleResolution Bundler
--allowSyntheticDefaultImports --strict src/preload.ts
src/direct-tools-preset.ts`
- `git diff --check`
2026-05-06 19:06:57 +05:30
Dhawal Upadhyay eae717ac20 Clean up inline custom tools payload typing 2026-05-06 18:44:40 +05:30
Dhawal Upadhyay d97fc8e17d Pass inline custom tools on session execute 2026-05-06 18:17:43 +05:30
Alberto Schiabel 2428a95549 fix(security): prevent path traversal in Python SDK FileDownloadable.download() (SEC-316) (#3352)
# Description

Fixes
[SEC-316](https://linear.app/composio/issue/SEC-316/composio-path-traversal-in-composio-python-sdk-allows-arbitrary-file)
— **HIGH severity (CVSS 8.2)** path traversal in the Composio Python
SDK.

`FileDownloadable.download()` in `python/composio/core/models/_files.py`
constructed the output path as `outdir / self.name`, where `self.name`
comes directly from the Composio API response. A compromised or
man-in-the-middle'd API server can return a filename like
`../../../../PWNED.sh` and write arbitrary files outside the configured
cache directory on the user's machine — leading to potential code
execution, configuration tampering, or data compromise.

The TypeScript SDK is **not** vulnerable: `downloadFileFromS3` in
`ts/packages/core/src/utils/fileUtils.node.ts` generates its own
filename via `generateTimestampedFilename()` and `saveFile` strips path
components with `platform.basename()` (line 428).

## What changed

`FileDownloadable.download()`:

- Strip directory components from `self.name` via `Path(...).name` so
traversal sequences (`../`, absolute paths, `\` on POSIX) collapse to a
basename.
- As a second-line defense, verify the resolved output path is contained
within `outdir` using `Path.is_relative_to()` (correct
directory-containment semantics — naive `str.startswith()` would be
vulnerable to sibling-prefix attacks like `outdir_evil/`).

This mirrors the equivalent fix already landed in `mercury`'s legacy
`FileDownloadable` (commits `e3915b82c3` + `c3d675dbcc` from PRs
ComposioHQ/mercury#20641 and #20654).

## How did I test this PR

- Added 4 unit tests in `TestFileDownloadablePathTraversal` covering:
- **Relative traversal** (`../../../PWNED.sh`) — collapsed to basename,
written inside `outdir`, file does NOT appear at the parent path.
- **Absolute path** (`/etc/passwd`) — basename strip yields `passwd`,
written inside `outdir`.
- **Dotdot-only name** (`..`) — `Path('..').name == '..'` survives
basename strip; the `is_relative_to()` check then rejects it with
`ErrorDownloadingFile`.
  - **Safe filename** (`report.pdf`) — pass-through unchanged.
- Ran `pytest tests/test_files.py -v` (110/110 passing — full file, no
regressions).

```
$ python -m pytest tests/test_files.py::TestFileDownloadablePathTraversal -v
tests/test_files.py::TestFileDownloadablePathTraversal::test_relative_traversal_is_neutralized PASSED
tests/test_files.py::TestFileDownloadablePathTraversal::test_absolute_path_is_neutralized PASSED
tests/test_files.py::TestFileDownloadablePathTraversal::test_dotdot_only_name_is_rejected PASSED
tests/test_files.py::TestFileDownloadablePathTraversal::test_safe_filename_passes_through PASSED
========================= 4 passed in 0.18s ==========================
```

## Reference

- CWE-22: https://cwe.mitre.org/data/definitions/22.html
- CVSS Vector: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N`

---
Triggered by: srujan@composio.dev | Source: slack
Session:
https://zen-api-production-4c98.up.railway.app/dashboard/#/chat/zen-3b30311d7b50
2026-05-06 14:14:03 +02:00
Dhawal Upadhyay d4f3c90ed5 Route preloaded custom tools locally 2026-05-06 17:30:04 +05:30
Dhawal Upadhyay d2155c028b Tighten Tool Router preload changes 2026-05-06 16:26:22 +05:30
Dhawal Upadhyay 847a8a04a6 Support SDK-local custom tool preload 2026-05-06 16:15:17 +05:30
Dhawal Upadhyay a2db64a150 support preloaded custom tool schemas 2026-05-06 16:15:17 +05:30
jkomyno 727cfafb0d test(security): add SEC-316 edge cases (subdir-traversal, empty name)
Two regression tests for `FileDownloadable.download()`:

- `test_basename_collapse_through_subdir_is_rejected`: `name="foo/.."`
  exercises the case where `Path(name).name` collapses to `..`. The
  plain `name=".."` test already covers the same `is_relative_to()`
  rejection, but the through-subdir form makes the basename
  normalization explicit. Also asserts (per the P3.1 reorder) that
  `outdir` is not created as a side effect of a rejected payload.

- `test_empty_name_safe_fails_at_write_time`: `name=""` makes
  `outfile == outdir`, which passes the containment check (a path is
  relative to itself). The download then safe-fails at
  `outfile.open("wb")` with `IsADirectoryError`. Pinning this
  documented behavior locks in the safe-fail and stops a future
  change from silently weakening the check without a test failure.
2026-05-06 11:55:02 +02:00
jkomyno 7a3d30a37c fix(security): apply SEC-316 defense-in-depth to RemoteFile.save()
`RemoteFile.save()` in `tool_router_session_files.py` shares the same
trust-boundary shape as the `FileDownloadable.download()` path traversal
fixed in SEC-316: the default save location is built as
`~/.composio/files/<self.filename>`, where `self.filename` is
`Path(mount_relative_path).name` derived from the (potentially
compromised or MITM'd) tool-router API response.

Today the basename strip alone makes this safe-by-OS-error — a server
returning `mount_relative_path == ".."` produces `filename == ".."`,
which collapses the resolved path to `~/.composio/` and `write_bytes()`
fails with `IsADirectoryError`. That's a non-security bug surface, but
it leaks intent through a confusing error and relies on a happy
accident of `Path` semantics.

Mirror the SEC-316 pattern: after building the default `save_path`,
verify `save_path.resolve().is_relative_to(default_dir.resolve())` and
raise `ValidationError` on mismatch. The check only runs on the
default-location branch — a caller-supplied `path` is fully trusted
and writes wherever the SDK user chooses.

Adds `test_save_default_location_rejects_dotdot_filename` covering the
`mount_relative_path="foo/.."` case (where `Path("foo/..").name == ".."`)
and asserting nothing is written under the cache dir.
2026-05-06 11:49:41 +02:00
Zen Agent 6e526c4ec6 style: ruff format SEC-316 tests
Apply `ruff format --config config/ruff.toml` to fix `with patch(...)`
multi-line indentation that the CI Lint and Format Check rejected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Co-authored-by: Srujan A <srujan@composio.dev>
2026-05-06 06:20:59 +00:00
Zen Agent f60cdf1ebf fix(security): prevent path traversal in FileDownloadable.download() (SEC-316)
The Composio Python SDK's `FileDownloadable.download()` constructed the
output path as `outdir / self.name`, where `self.name` is taken directly
from the Composio API response without sanitization. A compromised or
man-in-the-middle'd API server could return a filename like
`../../../../PWNED.sh` and write arbitrary files outside the configured
output directory on the user's machine.

Fix mirrors the equivalent change made in mercury's legacy
FileDownloadable (commit e3915b82c3 + c3d675dbcc):

- Strip directory components from `self.name` via `Path(...).name`,
  collapsing traversal sequences to a basename.
- As a second-line defense, verify the resolved output path stays
  within `outdir` using `Path.is_relative_to()` (correct directory
  containment semantics, vs. naive `str.startswith()` which is
  vulnerable to sibling-prefix attacks).

Adds 4 unit tests in `TestFileDownloadablePathTraversal` covering
relative traversal, absolute paths, dotdot-only names, and the safe
pass-through case.

Reference: https://cwe.mitre.org/data/definitions/22.html
CVSS: 8.2 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Co-authored-by: Srujan A <srujan@composio.dev>
2026-05-06 06:17:27 +00:00
Dhawal Upadhyay 82e0038c7c bump generated clients for v3.1 2026-05-05 14:44:43 +05:30
Dhawal Upadhyay fed23cdc56 fix(tool-router): use generated execute offload params 2026-04-30 17:52:44 +05:30
Dhawal Upadhyay 42035c835c fix(tool-router): opt agentic sessions into direct offload 2026-04-29 23:33:56 +05:30
Dhawal Upadhyay f846993a4d Add tool router preload support to Python SDK 2026-04-29 03:24:48 +05:30
Alberto Schiabel 09384bbe5e fix(python-sdk): prevent cross-tenant credential access in CustomTool [SEC-365] (#3302)
# Description

Fixes the cross-tenant credential access vulnerability tracked in
[SEC-365](https://linear.app/composio/issue/SEC-365/cross-tenant-credential-access)
(CWE-639) reported against
`python/composio/core/models/custom_tools.py`.

## What was wrong

`CustomTool.__call__` extracted `user_id` directly from the same
`**kwargs` dict that carries LLM-supplied tool arguments:

```python
def __call__(self, **kwargs: t.Any) -> t.Any:
    user_id = kwargs.pop("user_id", None) or "default"  # untrusted!
    request = self.request_model.model_validate(kwargs)
    ...
    auth_credentials=self.__get_auth_credentials(user_id),
```

Pydantic's `model_validate` ignores fields not declared in the schema,
so an LLM (or prompt-injection input) could include `user_id` in its
tool-call arguments and `__get_auth_credentials` would happily look up
that other tenant's OAuth tokens.

## What changed

- **`CustomTools.execute`** strips `user_id` from the LLM-supplied
`request` dict at the trust boundary, so only the explicit, server-side
`user_id` parameter reaches credential lookup. The original caller-owned
dict is left untouched.
- **`CustomTool`** now exposes a trusted internal entry point
`_invoke(user_id, request_kwargs)` where `user_id` is passed
structurally separate from the request kwargs dict (so an LLM-controlled
key inside the dict can never be promoted to override the trusted
value).
- **`CustomTool.__call__`** no longer reads `user_id` from `**kwargs` at
all and always uses the safe `"default"` user, with a docstring
directing direct callers to `CustomTools.execute` for trusted
multi-tenant use.

# How did I test this PR

Added `python/tests/test_custom_tools_security.py` with six regression
tests:

- `test_execute_strips_user_id_from_llm_request` — request dict
containing `user_id="victim-user"` is stripped; lookup uses the trusted
`user_id="trusted-user"`.
- `test_execute_falls_back_to_default_when_user_id_missing` — when no
trusted `user_id` is supplied, lookup uses `"default"` (NOT the
LLM-supplied value).
- `test_execute_does_not_mutate_caller_request_dict` — sanitization is
non-destructive on the caller's dict.
- `test_call_drops_user_id_smuggled_in_kwargs_dict` — `tool(**llm_dict)`
with `user_id` smuggled inside falls back to `"default"`.
- `test_invoke_uses_trusted_user_id_separate_from_request` — trusted
internal entry point passes the explicit `user_id` through.
- `test_execute_unknown_slug_raises` — sanity check, unknown slugs still
surface `NotFoundError`.

```bash
$ source .venv/bin/activate
$ python -m pytest tests/test_custom_tools_security.py -v
============================== 6 passed in 0.04s ===============================
```

Full Python test suite (excluding type-inference-only sessions, which
require all provider packages and run via `nox -s type_inference`):

```bash
$ python -m pytest tests/ --ignore=tests/test_type_inference_*
============================== 639 passed, 1 warning in 5.09s ==============================
```

Lint + type-check on changed files:

```bash
$ ruff check  --config config/ruff.toml composio/core/models/custom_tools.py tests/test_custom_tools_security.py
All checks passed!
$ ruff format --config config/ruff.toml --check composio/core/models/custom_tools.py tests/test_custom_tools_security.py
2 files already formatted
$ mypy --config-file config/mypy.ini composio/core/models/custom_tools.py tests/test_custom_tools_security.py
Success: no issues found in 2 source files
```

Triggered by: srujan@composio.dev | Source: slack
Session:
https://zen-api-production-4c98.up.railway.app/dashboard/#/chat/zen-34868b976538
2026-04-28 14:21:18 +02:00
Zen Agent 6b986cd8f9 feat(sdk): add allow_multiple guard to connectedAccounts.link()
Bring `link()` to parity with `initiate()` so the duplicate-connection
guard moves with customers as Composio-managed redirectable-OAuth
callers migrate from `connected_accounts/create` to
`connected_accounts/link` (SEC-339).

- TS: add `allowMultiple?: boolean` to `CreateConnectedAccountLinkOptions`.
  `link()` now pre-flights `connectedAccounts.list({ statuses: ['ACTIVE'] })`
  and throws `ComposioMultipleConnectedAccountsError` when an active
  connection exists and `allowMultiple` is not `true`.
- Python: add `allow_multiple: bool = False` to `connected_accounts.link()`
  with the same guard and the same `ComposioMultipleConnectedAccountsError`.
- Tests: existing `link()` tests now mock `list` returning empty (default
  no-existing-connection path); new tests cover the raise path and the
  opt-in `allowMultiple=True` skip path. Also add multi_account parity
  tests on the Python tool_router create() to mirror the TS suite.

Behavior change is intentional and patched separately with
`.changeset/sdk-link-allow-multiple.md`. The user-facing changelog entry
is split into a separate docs PR alongside the sandbox-size docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 11:48:04 +00:00
Zen Agent 1c3276ba8d feat(sdk): add workbench sandbox compute tier (sandboxSize / sandbox_size)
Expose `workbench.sandboxSize` (TypeScript) / `workbench.sandbox_size`
(Python) on `ToolRouter.create()` so callers can pick the workbench
sandbox compute tier (`standard`, `medium`, `large`, `xlarge`).
Optional; the server defaults to `standard` (1 vCPU / 1 GB) when
omitted, so existing callers keep current behavior.

- Bump stainless clients to pick up the field on the wire:
  - `@composio/client` 0.1.0-alpha.66 -> 0.1.0-alpha.67
  - `composio-client` 1.33.0 -> 1.34.0
- TS: extend `ToolRouterCreateSessionConfigSchema.workbench` with
  `sandboxSize` and forward it as snake_case `sandbox_size`. Export
  `SandboxSize` type and `SandboxSizeSchema` zod enum.
- Python: extend `ToolRouterWorkbenchConfig` TypedDict with
  `sandbox_size` and forward it on the create payload. Export
  `SandboxSize` literal alias.
- Changeset: `@composio/core` patch.
- Tests: cover the snake_case forwarding and zod enum rejection.

Docs (Configuring Sessions / Workbench / changelog) are split into a
separate PR so they can land alongside the SDK release.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 11:47:42 +00:00
Zen Agent 908aad7e1e fix(python-sdk): address SEC-365 review — loud failure + allowlist sanitization
Addresses review on PR #3302:

P2 #1 — `CustomTool.__call__` now raises `TypeError` if `user_id` appears in
`**kwargs` rather than silently dropping it. Silent drop expanded the blast
radius (a tenant registered as `user_id="default"` would have absorbed those
calls) and swallowed prompt-injection signals. The trusted entry point is
`CustomTools.execute(slug, request, user_id=...)`.

P2 #2 — `CustomTools.execute` now sanitizes via an *allowlist* of fields
declared on the tool's Pydantic `request_model` (canonical names + aliases),
not a denylist of just `"user_id"`. Future identity-bearing keys
(`tenant_id`, `org_id`, `connected_account_id`, …) cannot reopen
CWE-639 by being smuggled through `request`.

P3 — also:
- Renamed `CustomTool._invoke` to `invoke_trusted` so the security
  contract is in the name, not just the docstring.
- Tightened `CustomTools.execute` return type from `t.Dict` to `t.Any`
  (the user's tool function can return anything; `_invoke` already had
  `t.Any`).
- Consolidated repeated SEC-365/CWE-639 docstring blocks into a single
  module-level note in `custom_tools.py`.
- Added a load-bearing security-invariant note to
  `custom_tool_execution.py` (the parallel session/tool-router path):
  on that path `user_id` arrives via `SessionContext`, never via
  `arguments`.

Tests:
- New: `test_execute_strips_unexpected_fields_via_allowlist` — pins the
  durable allowlist behaviour against future identity kwargs.
- New: `test_execute_keeps_aliases_declared_on_request_model` — ensures
  Pydantic field aliases are honoured.
- New: `test_invoke_trusted_uses_explicit_user_id_over_smuggled_one` —
  pins the `invoke_trusted` defense-in-depth contract when a
  `user_id` is smuggled inside `request_kwargs`.
- New: `test_tools_execute_e2e_strips_user_id_through_full_stack` — E2E
  through `Tools.execute → _execute_custom_tool → CustomTools.execute`.
- Updated: `test_call_raises_typeerror_when_user_id_smuggled_in_kwargs`
  (previously codified the silent-drop behaviour).
- Refactored shared mock-client/tool wiring into pytest fixtures.

639 → 642 Python tests pass. Lint, format, mypy clean on changed files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Co-authored-by: srujan <srujan@composio.dev>
2026-04-28 09:54:02 +00:00
Zen Agent f6cdf7aca0 fix(python-sdk): prevent cross-tenant credential access via user_id kwarg [SEC-365]
CustomTool.__call__ was extracting user_id directly from the same **kwargs
dict that carries LLM-supplied tool arguments. Because Pydantic's
model_validate ignores fields not declared in the schema, an LLM (or
prompt-injection input) could include user_id in its tool-call arguments
and have CustomTool.__get_auth_credentials look up another tenant's OAuth
tokens (CWE-639).

Fix:
- CustomTools.execute strips user_id from the LLM-supplied request dict at
  the trust boundary so only the explicit, trusted user_id parameter is
  honored.
- CustomTool now exposes a separate trusted entry point (_invoke) where
  user_id is passed structurally separate from the request kwargs dict.
  CustomTool.__call__ no longer reads user_id from kwargs at all and always
  uses the safe "default" user, with a docstring pointing direct callers to
  CustomTools.execute.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Co-authored-by: Srujan A <srujan@composio.dev>
2026-04-28 09:50:23 +00:00
Musthaq Ahamad ebc9778595 feat(sdk): default-off auto file upload/download; add dangerouslyAllowAutoUploadDownloadFiles (#3260)
## Summary

Automatic tool file upload/download for `file_uploadable` fields is
**off by default** in TypeScript and Python. Callers must explicitly opt
in, and uploads from local paths are constrained by a fail-closed
allowlist.

## Changes

- **Removed (breaking):** `autoUploadDownloadFiles` (TS) /
`auto_upload_download_files` (Python) — the legacy default-on flag is
gone, not just deprecated.
- **New opt-in:** `dangerouslyAllowAutoUploadDownloadFiles` (TS) /
`dangerously_allow_auto_upload_download_files` (Python). When `true`,
`tools.get(...)` collapses `file_uploadable` schemas to `{ type:
'string', format: 'path' }` and the SDK stages local paths/URLs at
execute time.
- **New:** `fileUploadDirs?: string[] | false` — fail-closed allowlist
for local upload paths. `undefined` → `[<home>/.composio/temp]`; `false`
→ reject all local paths (URLs / `File` objects unaffected); explicit
`string[]` replaces the default. Components are matched on a path
boundary after `realpath`.
- **New:** `fileDownloadDir?: string` — directory where
`file_downloadable` results are staged.
- **New:** `beforeFileUpload` hook receives `source: 'path' | 'url' |
'file'` (TS) / `'path' | 'url'` (Python) so it can branch on input type.
- **New (TS):** when auto-upload is **off** and an LLM-driven
`tools.execute` is called against a tool with `file_uploadable` inputs,
the SDK emits a one-shot warning per tool slug pointing at
`composio.files.upload()` for manual staging.

## Migration

To restore previous behavior:

```ts
new Composio({
  apiKey: process.env.COMPOSIO_API_KEY!,
  dangerouslyAllowAutoUploadDownloadFiles: true,
  // Optional: tighten the allowlist beyond the default ~/.composio/temp
  fileUploadDirs: ['/srv/uploads'],
});
```

```python
Composio(api_key="...", dangerously_allow_auto_upload_download_files=True)
```

If you previously passed the legacy flag, remove it. There is no
transitional warning — TS and Python both reject the unknown property at
the type/keyword-arg level.

## Versioning

| Package | Bump |
| ------- | ---- |
| `@composio/core` | minor |
| `composio` (Python) | minor |
| Other `@composio/*` packages | patch (via changesets
`updateInternalDependencies: "patch"`) |

See
`docs/content/changelog/04-24-26-legacy-auto-upload-config-removal.mdx`
for the full migration writeup.
2026-04-28 10:55:03 +05:30
Musthaq Ahamad 27ed0c9bd6 security(core): sensitive path blocklist + beforeFileUpload hook (patch) (#3262)
## Summary

Security-hardening for automatic file upload in `@composio/core` (patch
release per changeset).

### Changes
- **Default denylist** for local paths before auto-upload /
`files.upload`: blocks common credential directories (e.g. `.ssh`,
`.aws`) and credential-like filenames (e.g. `.env`, default SSH private
keys). Resolves symlinks when the path exists.
- **Config:** `sensitiveFileUploadProtection`,
`fileUploadPathDenySegments` on `Composio`.
- **`beforeFileUpload`** hook (e.g. with `composio.tools.get` /
`tools.execute`): rewrite path, return `false` to abort, or throw.
- **Errors:** `ComposioSensitiveFilePathBlockedError`,
`ComposioFileUploadAbortedError`; file modifier errors exported from
`@composio/core` errors entry.
- **Changeset:** patch bump for `@composio/core`.

### Notes
- URLs and `File` blobs are not subject to the path denylist
(unchanged).
- Opt out of path checks only if required:
`sensitiveFileUploadProtection: false`.

### Tests
- `pnpm test` in `ts/packages/core` (799 tests) passed locally before
commit.

Made with [Cursor](https://cursor.com)
2026-04-23 18:42:16 +05:30
abir fcb02efe7d fix: use omit sentinel and dict-splat for alias (not yet in stainless types)
The composio-client 1.31.0 stainless types don't include alias on
.create(), .link.create(), or .session.link() yet. Use dict-splat
for alias on those methods, and omit sentinel for .patch() params.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Co-authored-by: Zen Agent <zen@composio.dev>
2026-04-10 00:18:06 +00:00
abir e4bcb6b95b fix: update test assertion for direct kwargs passing
The link() method now passes callback_url=None directly instead of
omitting the kwarg. The stainless client handles None by not sending
the field, so the behavior is identical.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Co-authored-by: Zen Agent <zen@composio.dev>
2026-04-10 00:11:05 +00:00
Dhawal Upadhyay db8f08ba95 fix(tool-router): tighten annotation routing and e2e retries 2026-03-24 15:29:24 -07:00
Dhawal Upadhyay 16fbcb92f5 style(python): format custom tool routing changes 2026-03-24 15:08:09 -07:00
Dhawal Upadhyay 24918adc01 fix(tool-router): harden multi-execute edge cases 2026-03-24 15:06:56 -07:00
Dhawal Upadhyay 635473ac37 fix(tool-router): align session and context response shapes 2026-03-24 14:59:04 -07:00
Dhawal Upadhyay cce496cc79 fix(python): unify ctx.execute() to return SessionExecuteResponse
Both session.execute() and ctx.execute() now return the same
SessionExecuteResponse type. No more ToolExecutionResponse dict
for ctx — consistent API whether called from user code or from
within a custom tool.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:34:18 -07:00
Dhawal Upadhyay b7424401e2 fix(python): use generated client types everywhere, remove custom TypedDicts
- session.execute() returns SessionExecuteResponse for both local and remote
- proxy_execute() returns SessionProxyExecuteResponse directly from client
  (no more manual dict construction)
- Remove dead ProxyExecuteResponse/ProxyExecuteBinaryData TypedDicts
- SessionContext protocol uses generated types
- No t.Any return types on new methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:30:29 -07:00
Dhawal Upadhyay 975d0dd23f fix(python): return SessionExecuteResponse for local tools too
Both local and remote session.execute() now return
SessionExecuteResponse — consistent type, supports attribute
access (result.data, result.error, result.log_id) everywhere.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:25:51 -07:00
Dhawal Upadhyay 35d0af43e1 fix(python): preserve backward compat for remote session.execute()
Remote path returns the SessionExecuteResponse client model as-is
(supports attribute access like result.data, result.log_id).
Only local custom tool path returns a dict.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:22:43 -07:00
Dhawal Upadhyay d21e7e76f4 fix(python): address PR review comments
- Fix async validation bypass for single-param tools: check
  asyncio.iscoroutinefunction(fn) before wrapping in _infer_tool
- Remove inline # slug/name comments from example (jkomyno review)
- Add test for async single-param rejection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:11:03 -07:00