Commit Graph

369 Commits

Author SHA1 Message Date
Dhawal Upadhyay 156753245c chore(py): bump sdk packages to 0.13.0 2026-05-07 19:17:57 +05:30
Dhawal Upadhyay b48711c587 Clarify custom preload use behavior 2026-05-07 16:44:22 +05:30
Dhawal Upadhyay ffb7539bb5 Fix ToolRouter example staging runs 2026-05-07 16:42:08 +05:30
Dhawal Upadhyay 0c0d535275 fix(python): use generated custom tool preload types 2026-05-07 16:10:38 +05:30
Dhawal Upadhyay bccd32beae Refine ToolRouter preload docs and constants 2026-05-07 16:10:38 +05:30
Abir Taheer c96f094b15 feat(core): add session.update() for partial config updates (#3362)
## Summary
- Adds `session.update()` method to both TypeScript and Python SDKs
- Calls the existing `PATCH /api/v3/tool_router/session/{id}` endpoint
- Accepts the same config shape as `create()` (toolkits, tools, tags,
auth_configs, connected_accounts, manage_connections, workbench,
multi_account, preload)
- Mutates the session object in-place (preload, configVersion, warnings)

## Usage

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

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

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

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

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

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

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

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

## TypeScript SDK — BREAKING fixes

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

## Python SDK — DEGRADED fixes

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

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

# How did I test this PR

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 02:07:36 -07:00
jkomyno 5bdaafd506 docs: trim verbose comments across the REVOKED-rollout commits
Net −56 lines of comment prose. Earlier commits added multi-paragraph
blocks explaining each forward-compat guard, sentinel choice, and
schema-bypass cast — most of that is rederivable from the surrounding
code. Keep one-line WHY notes where the intent is non-obvious; drop
the prose elsewhere.
2026-05-07 10:59:43 +02:00
jkomyno 27a4e6448e test(connected-accounts,py): patch time.time defensively in terminal-state test
`test_wait_for_connection_fails_fast_on_terminal_status` only patched
`time.sleep`, not `time.time`. A stuck CI runner could in principle
exhaust the 10s deadline before the first `retrieve()` call resolves,
in which case the test would assert against a
`ComposioSDKTimeoutError` instead of the `SDKError` it's actually
exercising. Match the monotonic-counter pattern used by the sibling
tests at lines 137 and 170 to remove that flake surface.

Surfaced by kieran-python review of PR #3344 (P3#4).
2026-05-07 10:56:29 +02:00
jkomyno d5bead2524 refactor(connected-accounts,py): hoist terminal_states to module-level frozenset
Promote the in-method `terminal_states = ("FAILED", "EXPIRED", "REVOKED")`
tuple to a module-level `_TERMINAL_CONNECTION_STATES: FrozenSet[str]`
constant alongside `_LEGACY_RETIRING_OAUTH_SCHEMES` (line 29). Two
benefits:

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

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

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

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

Python (`tests/test_connected_accounts.py`):
- `wait_for_connection` fails fast on FAILED / EXPIRED / REVOKED
  (parameterized) — the contract added by this PR
- `wait_for_connection` does NOT treat INACTIVE as terminal, so a
  connection briefly transiting through INACTIVE still recovers to
  ACTIVE — guards against the regression called out in P1#2
2026-05-07 10:29:14 +02:00
jkomyno 4e1883089d fix(connected-accounts,py): drop INACTIVE from terminal states in wait_for_connection
The previous patch added FAILED / EXPIRED / INACTIVE / REVOKED to the
terminal-state set, flipping the contract from "loop until timeout for
any non-ACTIVE" to "raise SDKError immediately on any of those four".
INACTIVE means "user disabled, can be reactivated" — a connection can
briefly transit through it before settling on ACTIVE, and failing fast
there regressed callers that previously recovered.

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

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

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

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

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

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

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

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

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

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

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

## Validation
- `pnpm --dir ts/packages/core test test/models/toolRouter.test.ts
test/models/customToolRouting.test.ts`
- `pnpm --dir ts/packages/core typecheck`
- `env COMPOSIO_CACHE_DIR=/private/tmp/composio-cache
python/.nox/chk/bin/python -m pytest python/tests/test_custom_tools.py
python/tests/test_tool_router.py`
- `python/.nox/chk/bin/python -m ruff check
python/examples/tool_router/preload.py
python/examples/tool_router/direct_tools_preset.py
python/composio/core/models/custom_tool.py
python/composio/core/models/custom_tool_types.py
python/composio/core/models/tool_router.py
python/composio/core/models/tool_router_session.py
python/tests/test_custom_tools.py python/tests/test_tool_router.py`
- `python/.nox/chk/bin/python -m py_compile
python/examples/tool_router/preload.py
python/examples/tool_router/direct_tools_preset.py`
- `pnpm --dir ts/examples/tool-router exec tsc --noEmit --skipLibCheck
--target ES2022 --module ESNext --moduleResolution Bundler
--allowSyntheticDefaultImports --strict src/preload.ts
src/direct-tools-preset.ts`
- `git diff --check`
2026-05-06 19:06:57 +05:30
Dhawal Upadhyay 98569bcf6f Respect base URL in Python ToolRouter examples 2026-05-06 18:52:18 +05:30
Dhawal Upadhyay eae717ac20 Clean up inline custom tools payload typing 2026-05-06 18:44:40 +05:30
Dhawal Upadhyay 8254999914 Address ToolRouter example review comments 2026-05-06 18:28:44 +05:30
Dhawal Upadhyay 490332887d Tighten inline custom execute typing 2026-05-06 18:23:53 +05:30
Dhawal Upadhyay d97fc8e17d Pass inline custom tools on session execute 2026-05-06 18:17:43 +05:30
Dhawal Upadhyay 006273a4f7 Fix Python ToolRouter typing checks 2026-05-06 17:57:09 +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 9ad5705661 Add Tool Router preload examples 2026-05-06 16:35:33 +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
Dhawal Upadhyay fb8549fce7 Bump generated clients for v3.1 (#3348)
## Summary
- bump generated TS client to @composio/client 0.1.0-alpha.69 and Python
client to composio-client 1.36.0
- page through ToolRouter session tools in TS/Python using the generated
v3.1 cursor response
- accept generated connected account status REVOKED and fail connection
waits immediately for terminal states

## Validation
- pnpm --filter @composio/core typecheck
- pnpm --filter @composio/core test -- test/tools/tools.test.ts
test/models/toolRouter.test.ts
test/connectedAccounts/connectedAccounts.test.ts
test/connectedAccounts/connectionRequest.test.ts
- uv run --frozen ruff check python/composio/core/models/tools.py
python/tests/test_tool_execution.py
- uv run --frozen pytest
python/tests/test_tool_execution.py::TestToolExecution::test_get_raw_tool_router_meta_tools_fetches_all_pages
python/tests/test_tool_router.py

Note: pnpm install --frozen-lockfile hits the repo preinstall Bun guard
locally because this machine has Bun 1.2.10 and the branch expects
1.3.10; dependency sync was completed with --ignore-scripts for local
verification.
2026-05-06 16:10:38 +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 b6eeb85287 refactor(security): run SEC-316 containment check before mkdir
Reorder `FileDownloadable.download()` so the `is_relative_to()`
containment check fires before `outdir.mkdir(parents=True)`, so a
rejected payload doesn't leave the cache directory tree created as a
side effect. `Path.resolve(strict=False)` (Python's default) resolves
non-existent paths logically, so the check is well-defined without
the directory existing first.

No behavior change for accepted payloads.
2026-05-06 11:49:51 +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