Commit Graph

356 Commits

Author SHA1 Message Date
sdkrelease[bot] dbe5a63965 Release: update version 2026-08-27 18:07:50 +00:00
Alberto Schiabel 08306f8bc1 Merge branch 'next' into fix/error-subclass-names 2026-08-27 18:51:36 +02:00
Alberto Schiabel 3c7b938bd1 Merge branch 'next' into fix/telemetry-request-timeout 2026-08-27 18:27:32 +02:00
Alberto Schiabel 81631f83f4 Merge branch 'next' into fix/strict-mode-keep-optional-parameters 2026-08-27 15:14:24 +02:00
jkomyno 9692db5c0a fix(openai-agents): honor the strict option
OpenAIAgentsProvider accepted { strict } but always registered tools with
strict: false and additionalProperties: true. Strict mode now registers
tools with strict: true and a schema normalized by toStrictJsonSchema
(optional parameters required-nullable), drops null arguments the tool
schema rejects before execution, and registers tools strict mode cannot
express without strict mode with a warning.

Co-authored-by: AseemPrasad <aseemprasad0520@gmail.com>

Claude-Session: https://claude.ai/code/session_01TDrxCHn2hg51HmxVstSUgs
2026-08-26 17:34:08 +02:00
jkomyno 3c3b4dae94 fix(mastra): keep optional parameters under strict mode
MastraProvider strict mode used the root-only, input-mutating
removeNonRequiredProperties, so "strict" meant something different from
the OpenAI providers. It now runs the same toStrictJsonSchema rewrite:
optional parameters become required-nullable, tools strict mode cannot
express keep their original schema with a warning, and null arguments the
tool schema rejects are dropped before execution.

Co-authored-by: AseemPrasad <aseemprasad0520@gmail.com>

Claude-Session: https://claude.ai/code/session_01TDrxCHn2hg51HmxVstSUgs
2026-08-26 17:34:08 +02:00
jkomyno 4f69975475 fix(core): keep optional parameters under strict mode instead of dropping them
toStrictJsonSchema now follows the contract OpenAI documents for structured
outputs: every property becomes required and optional ones are widened to
accept null, so the model keeps every parameter it could pass before. Type
arrays stay as they are (the API accepts them and rejects type next to
anyOf), so nullable objects stay nullable. Constructs strict mode cannot
express (objects with arbitrary keys, allOf, prefixItems, unresolved $refs,
non-object roots) are reported in `unsupported` instead of being narrowed.

omitNullToolArguments drops a null argument only where the tool's own
schema rejects it, so nullable fields keep an explicit null. The keyword
taxonomy shared by the three schema walkers now lives in one place.

Co-authored-by: AseemPrasad <aseemprasad0520@gmail.com>

Claude-Session: https://claude.ai/code/session_01TDrxCHn2hg51HmxVstSUgs
2026-08-26 17:19:30 +02:00
Alberto Schiabel 9e958948c5 fix(core): block sensitive upload paths hidden behind a symlinked directory (#4218)
# Description

Found by the scheduled security audit, while checking whether a test
failure on my other PR was pre-existing. It was — and the reason it
fails is a real gap in a shipped security control.

`isBlockedSensitiveFileUploadPath` (the GHSA-hp3h-89pf-5q58 denylist)
matched deny segments **only against the symlink-resolved path**. That
catches a benign name pointing at a secret — `~/innocent-name ->
~/nested/.aws/creds`, which the existing test covers — but misses the
inverse:

| Layout | Written path | Resolved path | Blocked before? |
|---|---|---|---|
| `~/innocent -> ~/.aws/creds` | no `.aws` | **`.aws`** | yes |
| `~/.claude -> /state/claude` | **`.claude`** | no `.claude` | **no** |

`~/.claude/settings.json` resolves to `/state/claude/settings.json`,
which has no `.claude` segment, so it sailed through — and `.claude` is
on the denylist precisely because it *"may contain API keys and project
context read by assistants"*.

This layout is not exotic. Dotfile managers (chezmoi, stow, yadm) and
containerised home directories produce it routinely — Composio's own
agent sandbox image has `/home/zen/.claude -> /state/claude`. For every
user in that shape the control was silently inactive, which is the worst
failure mode for a denylist: no error, no warning, upload proceeds.

The same hiding trick applies to the basename check (`~/.env ->
/state/plain-config`), so that path is fixed too.

## Why CI never caught this

`ts/packages/core/test/utils/sensitiveFileUploadPaths.test.ts` **already
asserts** the blocked behaviour:

```ts
expect(isBlockedSensitiveFileUploadPath(path.join(os.homedir(), '.claude', 'settings.json'))).toBe(true);
```

That assertion has been failing on `next` on any machine where
`~/.claude` is a symlink. It passes in CI only because `~/.claude` does
not exist on the runners: `existsSync` is false, no `realpath` runs, and
the written path keeps its `.claude` segment. The test is
environment-dependent, so green CI was never evidence the control
worked.

## The fix

The TypeScript `normalizePath` helper now returns both the written and
resolved segments, and the segment scan and basename check each consider
both. The Python guard now applies the same rule. Either path can carry
the denied name, so both SDKs inspect both forms.

# How did I test this PR

**The TypeScript fix is gated by tests — 3 fail without it, 13/13 pass
with it.**

Without the `src` change (test file only):

```
× blocks common credential directory segments
× blocks a sensitive directory that is itself a symlink to a plain path
× blocks a denied basename whose symlink target is named innocuously
  Tests  3 failed | 10 passed (13)
```

With the fix:

```
  Test Files  1 passed (1)
        Tests  13 passed (13)
```

Note the first of those three is the **pre-existing** assertion quoted
above — this PR turns it green rather than adding it.

Three tests added, each building a real symlink in a temp dir:
- sensitive directory that is itself a symlink to a plain path (the
`~/.claude -> /state/claude` case), asserting both
`isBlockedSensitiveFileUploadPath` and that `assertSafeFileUploadPath`
throws
- denied basename whose symlink target is named innocuously (`.env ->
plain-config`)
- **negative case**: an ordinary file reached through a symlinked
directory (`docs/document.pdf`) is still allowed, so the fix does not
over-block

The Python parity change adds the same three cases. Before the Python
source change, the sensitive written directory and basename both
returned `False`; with the fix, all 11 focused Python tests pass.

**Full verification:**

| Command | Result |
|---|---|
| `vitest run` in `ts/packages/core` | **48 files, 1114 tests passed** |
| `pnpm typecheck` (workspace) | **14/14 tasks successful**, exit 0 |
| `oxlint` on both changed files | exit 0, clean |
| `prettier --check` on both changed files | "All matched files use
Prettier code style!" |
| `nox -s chk` in `python/` | Ruff and mypy passed |
| `nox -s tst` in `python/` | **1339 passed, 33 skipped**, exit 0 |

# Security

- No dependency changes, no new network calls, no new imports. The diff
is limited to the equivalent TypeScript and Python guards, their tests,
and the required `@composio/core` patch changeset.
- This **strengthens** an existing control and cannot weaken it: the
previous match set is a strict subset of the new one, so nothing that
was blocked before is allowed now. The added negative test pins that the
widening does not over-block ordinary files.
- **Grype** — `grype dir:ts/packages/core --only-fixed --fail-on medium`
→ reported below.
- **Socket** — could not run; `doppler secrets get SOCKET_API_TOKEN
--plain --project hermes --config dev_zen` returns empty in this cron
sandbox, so `socket ci` exits `Auth Error`. Reporting rather than
skipping silently.
- Unrelated pre-existing note: the repo's `pnpm audit --prod` comment
flags `extract-zip <=2.0.1` with `Patched versions >=2.0.2`, a version
that does not exist on npm. Details in #4217.

Origin: cron-48e51eab745f /
[zen-cron-44e260352d1a](https://zen.corp.composio.io/dashboard/#/chat/zen-cron-44e260352d1a)

Triggered by: saransh@composio.dev | Source: unknown
Session:
https://zen.corp.composio.io/dashboard/#/chat/zen-cron-44e260352d1a
2026-08-25 21:48:55 +02:00
jkomyno 449f4e128a chore: add changeset for symlink path protection 2026-08-25 21:02:36 +02:00
Alberto Schiabel f174b28c3a fix(sdk): omit empty file-uploadable arguments from tool execution (#4238)
This PR:

- closes https://github.com/ComposioHQ/composio/issues/4233
- stops forwarding `""` for a `file_uploadable` parameter (e.g. Gmail
`attachment`) to the backend, which rejected it with `Input should be a
valid dictionary or instance of FileUploadable`
- Python: parameterizes the upload walker on a `leaf` handler and adds
`FileHelper.drop_empty_file_uploads()`, run on both `Tools.execute`
paths when `dangerously_allow_auto_upload_download_files` is off (the
default)
- TypeScript: moves the walker into the runtime-neutral
`walkFileUploadableLeaves()` with a `DELETE_VALUE` sentinel and
`dropEmptyFileUploads()`, so the flag-off path also works on edge
runtimes; with the flag on, `''` is no longer attempted as an upload
(previously threw `Either path or blob must be provided`)
- TypeScript: `schemaHasFileProperty()` now looks through
`$defs`/`definitions`, so the flag-off pass (and the existing one-shot
warning) fire for `$ref`-based file schemas
- keeps each SDK's existing `null` semantics: Python omits `None` as
before, TypeScript still passes `null` through for schemas with a `null`
variant
- adds regression tests for both SDKs and a `@composio/core` changeset

## Context

Reproduced against staging with `composio==0.16.0` and the current SDK:
the live `GMAIL_CREATE_EMAIL_DRAFT` schema is `anyOf[FileUploadable,
array[FileUploadable]]` with no `null` variant, and `""` yields the
exact error from the issue on `no_auth` file tools
(`TEXT_TO_PDF_UPLOAD_FILE`). With this change the backend sees the
parameter as not provided, matching what the playground UI sends.
2026-08-25 16:01:54 +02:00
jkomyno fe66cbeb77 fix(sdk): omit empty file-uploadable arguments from tool execution
Both SDKs forwarded "" for a file_uploadable parameter (e.g. Gmail
attachment) verbatim to the backend, which rejected it with a Pydantic
validation error. Python only dropped it inside the opt-in auto-upload
walker; TypeScript never did, and with auto-upload on it tried to upload
the empty string.

Run a schema-aware, upload-free pass on the default execute path that
omits empty file values, and reuse the same walker for staging when
auto-upload is enabled.

Closes #4233
2026-08-25 01:58:52 +02:00
Alberto Schiabel 700327c2a6 Merge branch 'next' into chore/changesets-v3-migration 2026-08-25 01:18:03 +02:00
tgolob db7b576437 fix(ts): declare the supported Node.js engine floor (#4219)
## Summary

The ESM-only transition in #3494 established Node.js 22.22.3 as the
minimum supported runtime for the public TypeScript SDK packages, but
their published manifests still omit `engines.node`.

That leaves package managers without a package-level compatibility
signal before an older runtime encounters an ESM loading failure. For
example, the current `@composio/core@0.17.0` package fails with
`ERR_REQUIRE_ESM` when required on Node.js 22.0.0, while the same load
succeeds on Node.js 22.22.3.

This aligns the published metadata with the support floor already
documented and tested by the repository.

Related: #3494

## Changes

- Add `"engines": { "node": ">=22.22.3" }` to all 14 public TypeScript
release workspaces.
- Add a release-workflow invariant that discovers public TypeScript
workspaces from the root workspace configuration and rejects missing or
drifted Node.js engine ranges.
- Add a patch changeset covering exactly those 14 published packages.

<details>
<summary>Package scope</summary>

- Core packages: `@composio/core`, `@composio/slim`,
`@composio/experimental`, and `@composio/json-schema-to-zod`
- Providers: Anthropic, Claude Agent SDK, Cloudflare, Google, LangChain,
LlamaIndex, Mastra, OpenAI Agents, OpenAI, and Vercel
- Excluded as private/unpublished: CLI, CLI keyring, CLI local tools,
JSON Schema to Effect Schema, and TypeScript builders

</details>

## Type of change

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

## How Has This Been Tested?

Validated with the repository-pinned Node.js 24.17.0, pnpm 11.8.0, and
Bun 1.4.0 toolchain:

- `pnpm install --frozen-lockfile`
- `pnpm run test:release-workflow`
- `pnpm validate:changesets`
- `pnpm exec changeset status --since=origin/next` (exactly 14 patch
releases)
- `pnpm build:packages` (19/19 packages)
- `pnpm typecheck` (14/14 tasks)
- `pnpm lint:packages` (successful; only pre-existing warnings in
untouched source files)
- Prettier over every touched file
- `git diff --check origin/next...HEAD`

The published-package probe also confirmed that `@composio/core@0.17.0`
has no `engines` metadata, CommonJS loading fails on Node.js 22.0.0, and
the same package loads successfully on Node.js 22.22.3.

No lockfiles, generated files, or runtime source files changed.

## Screenshots (if applicable)

Not applicable.

## Checklist

- [x] I have read the Code of Conduct and this PR adheres to it
- [x] I ran linters/tests locally and they passed
- [x] 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

The runtime floor itself is not new: #3494 shipped and documented it as
a breaking change in the existing `0.x` line. This patch makes registry
metadata accurately reflect that existing support contract, so the
accompanying releases are patches.

---------

Co-authored-by: Tomas Golob <tgolob@users.noreply.github.com>
Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
Co-authored-by: jkomyno <alberto@composio.dev>
2026-08-25 01:11:05 +02:00
jkomyno e4189aeb57 chore(release): migrate to Changesets v3 2026-08-25 01:01:10 +02:00
AseemPrasad e57661755b Merge branch 'next' of https://github.com/ComposioHQ/composio into asimcomposio
# Conflicts:
#	python/composio/core/provider/_openai_responses.py
#	ts/packages/providers/openai/src/OpenAIResponsesProvider.ts
2026-08-23 01:19:42 +05:30
AseemPrasad 04817cb20d fix(openai): recursive strict-mode schema normalization for structured outputs (+ Python parity) 2026-08-23 00:17:57 +05:30
Alberto Schiabel d544006a25 fix(sdk): pin the validated address when fetching URLs (SSRF DNS rebinding) (#4172)
Fixes #4151.

## The problem

Both SDKs validated a URL by resolving its hostname, and then handed the
*hostname* to the HTTP client, which resolved it again when it opened
the socket. Two lookups, two answers: a short-TTL record under an
attacker's control answers publicly for the check and with
`169.254.169.254`, `127.0.0.1`, or RFC 1918 space for the connect. The
guard passes and the connection lands inside the network — classic
TOCTOU DNS rebinding, documented in both modules until now as a known
residual.

```mermaid
sequenceDiagram
    participant SDK
    participant DNS as Attacker DNS
    participant Meta as 169.254.169.254
    Note over SDK,Meta: before
    SDK->>DNS: resolve evil.example.com (validate)
    DNS-->>SDK: 93.184.216.34 — passes the guard
    SDK->>DNS: resolve evil.example.com (connect)
    DNS-->>SDK: 169.254.169.254
    SDK->>Meta: GET /latest/meta-data/…
    Meta-->>SDK: credentials
```

## The fix

Resolve once, validate every answer, then connect to the address that
was validated. There is no second lookup left to rebind.

- **Python** — `safe_get` / `safe_request` mount a transport adapter
that swaps the connect target for the duration of the socket connect
only. The `Host` header and TLS SNI keep the hostname, so certificate
verification is unchanged; rewriting `conn._dns_host` for the whole
connection would have sent `Host: <ip>` and offered the IP as SNI,
failing against every real origin. Every fetch call site now goes
through those two helpers, so no `requests.get` sits next to a bare
check any more:
- `_files.py::_fetch_file_from_url`,
`_files.py::FileDownloadable.download`
  - `tool_router_session_files.py::_fetch_url_bytes`
  - `safe_request`, per redirect hop
- **TypeScript** — `assertSafeFetchTarget` returns the validated address
and `ssrfSafeFetch` hands `fetch` a dispatcher pinned to it, re-pinned
per redirect hop. The dispatcher goes to the runtime's own `fetch`, so
callers that stub `globalThis.fetch` keep working. The pinned `lookup`
answers both shapes Node calls it with — the address *list* it uses for
Happy Eyeballs, and the single `(address, family)` it uses when
`autoSelectFamily` is off — since answering in the wrong shape is
rejected as an invalid address.
- A fail-closed peer assertion runs on the Python side before a byte is
written to the socket — redundant while pinning works, and a tripwire if
a urllib3 upgrade ever breaks it.
- `workerd` is unchanged: it already fails closed for user-supplied
URLs.

Redirect *validation* already existed in both SDKs (`safe_request` /
`ssrfSafeFetch`); what was missing was re-pinning each hop.

## Tests

The existing suites could not express this bug: they mock both the
resolver and the HTTP client, so check and use are the same mock. The
new tests use real sockets.

- `python/tests/test_url_safety_pinning.py` — two loopback servers and a
resolver that answers the first lookup with one endpoint and every later
one with another, which is what a short-TTL rebinding record does.
Asserts the rebound endpoint receives **zero** connections, and that
`Host` still carries the hostname. Both tests fail on `next` and pass
here.
- `ts/packages/core/test/utils/pinnedDispatcher.node.test.ts` — a real
server plus a hostname under `.invalid`, which RFC 2606 guarantees never
resolves. A request that arrives proves the connect used the pinned
address and never consulted DNS. The third case shows the contrast:
unpinned, the same fetch cannot resolve at all.
- `ssrfGuard.test.ts` gains assertions that each hop is pinned to that
hop's own validated address.
- `pinnedDispatcher.node.test.ts` also pins with
`setDefaultAutoSelectFamily(false)`, which is the branch Node takes for
the single-address callback.

## Notes

- Supersedes #4157, which diagnosed this correctly. Its post-response
peer check turned out not to hold: with an HTTP/1.0 or `Connection:
close` server, urllib3 detaches the socket (`conn.sock is None`) while
`r.content` still returns the full body, so the check fails open exactly
where exfiltration succeeds. That is why the assertion here runs at
connect time instead.
- The Python package now declares `urllib3>=2` directly. `url_safety`
imports it for `NameResolutionError`, which only exists from 2.0, and
the pinning adapter reaches into 2.x connection internals; `requests`
alone allows 1.x, where `import composio` would have failed outright.
- `@composio/core` gains an `undici` dependency, pinned to `^7`: undici
8 dispatchers are rejected by the `fetch` in every Node version this
package supports (22/24/25, verified). The real-socket test runs on the
full CI matrix, so a future incompatibility fails loudly instead of
silently un-pinning.
- `undici` is imported on first pinned request rather than at module
load: importing it installs a process-wide global dispatcher, which
would have handed the host application's own unrelated `fetch` calls
this package's undici merely because it imported `@composio/core`.
- Residuals, now documented in the modules:
- Requests routed through an environment proxy keep the pre-flight check
only. The proxy resolves the hostname itself and the SDK cannot see or
pin that resolution.
- A process that does perform a pinned fetch still ends up on this
package's `Agent` if nothing had claimed the global dispatcher slot yet.
undici defines that slot non-configurable, so it cannot be handed back —
assigning `undefined` leaves the runtime's own `fetch` asserting on a
missing dispatcher.
2026-08-20 13:34:37 +02:00
sdkrelease[bot] f031c27cd1 Release: update version (#4161)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to next, this PR will
be updated.


# Releases
## @composio/core@0.17.0

### Minor Changes

- 760f8d0: Allow OpenAI and Anthropic provider tool-call helpers to
execute through a supplied Tool Router session. Session meta-tools now
retain their session context while provider argument normalization
remains intact; existing user-ID calls continue to use direct execution.
Anthropic helper failures now preserve their error text in `{ error }`
results without changing successful payloads. Custom provider subclasses
overriding `executeToolCall` or `handleToolCalls` may require updates
because these methods now accept session targets.

### Patch Changes

- 6ba9179: Validate the URLs that come from API responses before
fetching them. Tool-execution downloads (`s3Url`), S3 presigned uploads
(`new_presigned_url`), Tool Router session file downloads
(`RemoteFile.buffer()` / `blob()` / `text()` / `save()`) and session
file uploads (`upload_url`) now go through the same SSRF guard that
already covered user-supplied URLs, so a response naming a private,
loopback, or link-local address is refused instead of fetched. Redirect
hops are re-validated. Edge runtimes keep their current behavior:
session file transfers are not blocked there, since a Worker cannot
resolve DNS to check and its `fetch` does not originate inside the
caller's network.
## @composio/anthropic@0.11.0

### Minor Changes

- 760f8d0: Allow OpenAI and Anthropic provider tool-call helpers to
execute through a supplied Tool Router session. Session meta-tools now
retain their session context while provider argument normalization
remains intact; existing user-ID calls continue to use direct execution.
Anthropic helper failures now preserve their error text in `{ error }`
results without changing successful payloads. Custom provider subclasses
overriding `executeToolCall` or `handleToolCalls` may require updates
because these methods now accept session targets.
## @composio/openai@0.12.0

### Minor Changes

- 760f8d0: Allow OpenAI and Anthropic provider tool-call helpers to
execute through a supplied Tool Router session. Session meta-tools now
retain their session context while provider argument normalization
remains intact; existing user-ID calls continue to use direct execution.
Anthropic helper failures now preserve their error text in `{ error }`
results without changing successful payloads. Custom provider subclasses
overriding `executeToolCall` or `handleToolCalls` may require updates
because these methods now accept session targets.
## @composio/slim@0.17.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-19 00:37:19 +02:00
Soumya Medapati 760f8d0367 fix(sdk): route provider tool calls through sessions (#4098)
## Problem

Provider tool-call helpers always used the globally injected direct
`Tools.execute` function. When a model received tools from
`session.tools()`, calling `handleToolCalls` or `handle_tool_calls`
therefore discarded the Tool Router session context and caused session
meta-tools such as `COMPOSIO_SEARCH_TOOLS` to fail.

Calling `session.execute()` manually preserved the session, but bypassed
provider behavior such as Anthropic input normalization and schema-alias
restoration.

## Root fix

- Add an explicit execution target to the non-agentic provider helpers:
- TypeScript: `handleToolCalls(session, response)` and
`executeToolCall(session, call)`
- Python: `handle_tool_calls(response=response, session=session)` and
`execute_tool_call(tool_call=call, session=session)`
- Route normalized provider arguments through the supplied Tool Router
session.
- Map session responses back to each helper's existing result shape.
- Keep provider-specific normalization before execution, including
Anthropic schema-alias restoration.
- Reject direct-only options and modifiers when the selected target is a
session, including plain JavaScript calls that bypass the TypeScript
overloads.
- Update OpenAI and Anthropic examples to use the session-aware helpers.
- Harden the docs policy test so setup and execution split across fences
in one sample are still detected.

## Docs review follow-ups

- Reword the concepts-page prohibition so it forbids user-ID-bound
helper calls, not the helpers themselves, matching the provider pages in
this PR.
- Add minimum-version callouts to the OpenAI and Anthropic provider
pages (Python `composio` newer than 0.19.0; TypeScript `@composio/core`
≥ 0.17.0 with `@composio/openai` ≥ 0.12.0 / `@composio/anthropic` ≥
0.11.0), pointing older versions at `session.execute()`.
- Bump `docs/package.json` to `@composio/core` `^0.15.0` and
`@composio/openai` `^0.11.0` (the published majors at the time of the
bump; `@composio/core` 0.16.0 and `composio` 0.19.0 have since released
from `next` without this PR, so its changeset will publish core 0.17.0
and the next Python minor) and annotate each `@errors: 2345` Twoslash
marker with a TODO naming the minor version that retires it; since this
changeset releases minors, all three pins need a manual range bump to
retire the markers. This version of twoslash only throws on *unlisted*
errors, so a stale marker cannot break the build — it would only mask
future TS2345s, which the TODOs now track.
- Update `SESSION_GUARDRAILS` (the block appended to `.md` responses for
agents): add a session-execution bullet (scoped to the OpenAI and
Anthropic helpers, with `session.execute()` for every other provider)
and qualify the direct-execution list with "with a user ID". The
session-execution static test now scans the guardrail blocks like the
execute-version test already did.
- Tighten the docs detector: the Python branch is bounded to the helper
call's argument list (tolerating one level of nested calls) instead of
running past the closing paren, and the TypeScript branch catches whole
user-ID identifiers (`userId`, `user_id`, `uid`) without flagging
session variables like `userSession` — each edge has a regression test.
- Note on the Google provider page that its `executeToolCall` is not
session-aware yet.

## Compatibility and release

Existing user-ID calls remain unchanged and continue to use direct tool
execution. The new session call forms are additive.

The changeset applies minor releases to `@composio/core`,
`@composio/openai`, and `@composio/anthropic` — the new session
overloads are a type-level break for provider subclasses, so patch was
too small. The configured fixed group also includes `@composio/slim`.

The docs site intentionally checks examples against currently published
SDK declarations. The three new TypeScript calls therefore carry exact
Twoslash `TS2345` release-skew annotations; remove them (per the inline
TODOs) once `docs/package.json` picks up `@composio/core` ≥ 0.17.0,
`@composio/openai` ≥ 0.12.0, and `@composio/anthropic` ≥ 0.11.0.

## Verification

- `@composio/core`: 1,061 tests passed; typecheck passed
- `@composio/openai`: 34 tests passed; typecheck passed
- `@composio/anthropic`: 53 tests passed; typecheck passed
- Python provider and aliasing suites: 40 passed, 4 skipped
- Focused Python mypy and Ruff checks passed
- Docs static suite: 208 tests passed (including the new guardrail-scan
and detector cases)
- Docs production build passed with the bumped `@composio/core` 0.15.0 /
`@composio/openai` 0.11.0, including Twoslash, TypeScript, and all
generated pages
- Docs lint passed; lint reports only existing warnings
- Changeset status reports the expected minor packages

---------

Co-authored-by: Soumya Medapati <soumyamedapati@soumyas-air.local.meter>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: jkomyno <alberto@composio.dev>
2026-08-18 23:55:11 +02:00
Alberto Schiabel 6ba9179b48 fix(files): validate URLs from API responses before fetching them (#4146)
This PR:

- builds on top of https://github.com/ComposioHQ/composio/pull/4144
- routes every fetch whose URL comes from an API response through the
SSRF guards that already existed — ten sinks, five per SDK: the
tool-execution download, both S3 presigned uploads,
`RemoteFile.buffer()`/`blob()`, and the Tool Router session file upload
- adds `safe_request()` (Python), which follows redirects itself and
re-validates each hop, so a validated URL cannot 302 into private space
and an S3 307 region redirect still works
- makes `RemoteFile.buffer()` (Python) share `_fetch_url_bytes` with the
user-supplied-URL path instead of duplicating it — it previously read
`response.content` with no size cap, no redirect control, and no target
validation
- adds `ssrfSafeFetchWhereSupported` (TypeScript): the full guard on
Node, a plain `fetch` on workerd, so Tool Router session file transfers
keep working in edge runtimes rather than failing closed
- documents DNS rebinding as a known residual in both guards
- adds tests that assert the guard *runs* — blocked URL, sink never
called, nothing written — rather than that a transfer succeeds

## Context

`python/AGENTS.md` states the trust boundary: every field of an API
response is untrusted input, because the backend may be compromised or
the connection MITM'd. Both SDKs enforced that for URLs a *user* passes
in (`composio.utils.url_safety`, `ssrfGuard.node.ts`) and left the URLs
an API *response* supplies unguarded — backwards relative to the stated
model. A response naming an internal address turned the SDK into a
request proxy for it, and the fetched bytes were written to disk or
returned to the caller, typically into an LLM context.

`RemoteFile.buffer()` was the weakest of the ten: four lines above it,
`_fetch_from_url` validated its target, refused redirects, and streamed
against a 100 MiB cap; `buffer()` did none of the three. The only
difference between them was which side of the trust boundary the URL
arrived from.

Three decisions worth review:

- **The guard is unconditional.** Presigned URLs are public, so nothing
legitimate should resolve to private space. There is no escape hatch,
and no new configuration surface.
- **The tool-execution download stays uncapped.** It streams straight to
disk, so `_MAX_RESPONSE_SIZE` — a memory-exhaustion bound — does not
apply, and tool attachments legitimately exceed it.
`RemoteFile.buffer()` *is* capped, because it buffers in memory.
- **Uploads follow redirects with re-validation rather than refusing
them**, since S3 can answer a PUT with a 307 region redirect. Downloads
keep `allow_redirects=False`, matching the existing fetch paths.

Python now matches the TypeScript guard's per-hop re-validation, which
was the better of the two implementations.

Verified locally: `make chk` and `make tst` clean on the Python side;
`pnpm -C packages/core test` 1095 passed, typecheck and lint clean.
2026-08-18 13:25:01 +02:00
Rohit c0f16093ed fix(errors): set this.name on three ComposioError subclasses
ComposioToolVersionRequiredError, JsonSchemaToZodError, and
JsonSchemaRefResolutionError omitted the this.name assignment their sibling
subclasses perform, so instances inherited the base class field value and
reported name 'ComposioError'. error.name is forwarded to error telemetry,
which mis-grouped these three distinct error types under the base name.
2026-08-13 14:19:49 +05:30
Rohit 9545806a62 fix(telemetry): bound telemetry requests with an AbortSignal timeout
sendMetric and sendErrorLog awaited fetch() with no timeout, so a stalled
telemetry endpoint could leave an awaited SDK call pending indefinitely.
Bound both best-effort requests with an AbortController and a cleared timer
via a shared postWithTimeout helper, mirroring the npm version-check bound in
utils/version.ts. The existing catch still swallows the abort, preserving the
never-throws contract.
2026-08-13 13:42:01 +05:30
sdkrelease[bot] 02196de0b5 Release: update version (#4104)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to next, this PR will
be updated.


# Releases
## @composio/core@0.16.0

### Minor Changes

- 5e57815: Keep free-form object roots, `patternProperties`, and
`additionalProperties` when parsing a tool schema.

`ToolSchema.parse` used to reject a bare `{ "type": "object" }` root,
drop root `patternProperties` as an unknown key, and reject a root
`additionalProperties` written as a schema instead of a boolean.
Free-form roots now parse successfully, and both constraints survive
parsing exactly as written. The public `ToolSchema` type now makes
`properties` optional to reflect those valid property-less object
schemas.

This matters downstream. Every provider reads `inputParameters` after
parsing, so a tool that declares dynamic keys had those rules stripped
before the model ever saw them.

An omitted `additionalProperties` still stays omitted. The parser does
not invent a value, because each converter decides its own default.

  **What no longer works**

Parsing no longer fails on a schema-valued root `additionalProperties`.

  ```ts
  const tool = ToolSchema.parse({
    slug: 'MY_TOOL',
    inputParameters: {
      type: 'object',
      properties: { name: { type: 'string' } },
      additionalProperties: { type: 'number' },
    },
    // ...
  });

  // before: parsing failed, because only a boolean was accepted
// now: tool.inputParameters.additionalProperties is { type: 'number' }
  ```

  **What to do instead**

Nothing, if you only ever passed boolean values. That case is unchanged.

If your code assumed `inputParameters` never carries
`patternProperties`, or that `additionalProperties` is always a boolean,
widen that assumption. Both keywords can now appear, and
`additionalProperties` can be a boolean or a schema object.

### Patch Changes

- a2f6b96: Add `type: "object"` to nested JSON Schema nodes that carry
`properties` without an explicit type, so tool schemas work with strict
OpenAPI 3.0 consumers like Google Gemini.
- c625edc: Reuse fetched tool schemas when provider-wrapped tools
execute to avoid a redundant retrieval request.
- Updated dependencies [5e57815]
  - @composio/json-schema-to-zod@0.3.0
## @composio/json-schema-to-zod@0.3.0

### Minor Changes

- 5e57815: Keep the content of free-form object arguments, and apply
dynamic key rules only where they belong.

An object schema with no named properties, written as `{ "type":
"object" }` or `{ "properties": {} }`, used to become a strict empty
object. The only value that passed was `{}`. Any real payload was
rejected. Such an object is now open, so arbitrary content is accepted
and preserved. This works at the root, nested inside another object, and
inside array items.

Dynamic keys are now routed to the schemas that actually claim them.
Every matching `patternProperties` entry validates its key. A
schema-valued `additionalProperties` applies only to keys that no
declared property and no pattern matches. Before this change a key could
pass by satisfying an unrelated pattern instead of its own.

  **What no longer works**

Converting a property-less object no longer gives you a schema that
rejects everything.

  ```ts
  const schema = jsonSchemaToZod({ type: 'object' });

  schema.parse({ anything: 1 });
  // before: threw, because the result was z.object({}).strict()
  // now:    returns { anything: 1 }
  ```

  **What to do instead**

If you want a property-less object to stay closed, say so in the JSON
Schema:

  ```ts
const schema = jsonSchemaToZod({ type: 'object', additionalProperties:
false });

  schema.parse({ anything: 1 }); // throws, as before
  ```

Objects that name at least one property are unchanged. They still reject
unknown keys when `additionalProperties` is omitted.
## @composio/claude-agent-sdk@0.11.0

### Minor Changes

- ac6bbab: Register each tool with its complete schema, so root rules
reach the Claude Agent SDK.

The provider used to register a raw property shape. A raw shape is only
the map of named properties, so it cannot carry any root rule.
`additionalProperties` and `patternProperties` were dropped before the
SDK saw them.

Two things went wrong because of that. Free-form object arguments lost
their content. An argument the tool never declared was quietly removed,
and the tool ran anyway.

  Both are fixed. The provider now registers the whole object schema.

  **What no longer works**

  An undeclared argument no longer passes silently.

  ```ts
// The tool declares `to` and nothing else. The model also sends
`hallucinated`.
  { to: 'someone@example.com', hallucinated: 'value' }

// before: `hallucinated` was stripped, and the tool ran with { to:
'...' }
// now: the caller receives an error result, and the tool does not run
  ```

Tools with no input parameters behave the same way. They stay closed and
reject every argument.

  **What to do instead**

If the model should be allowed to send extra keys, say so in the tool
schema:

  ```json
  {
    "type": "object",
    "properties": { "to": { "type": "string" } },
    "additionalProperties": true
  }
  ```

If an argument is one the tool really accepts, declare it in
`properties`. Rejection is usually the better outcome, because the agent
sees the error and can correct itself instead of running with a silently
dropped argument.

This brings the provider in line with `@composio/vercel`,
`@composio/langchain`, and `@composio/llamaindex`, which already
registered complete schemas.
## @composio/google@0.10.2

### Patch Changes

- a2f6b96: Add missing object types to nested Google GenAI tool schemas
so Gemini accepts function declarations with property maps. This release
requires `@composio/core >=0.16.0 <1.0.0` for the shared schema
normalizer.
## @composio/slim@0.16.0

### Patch Changes

- Updated dependencies [5e57815]
  - @composio/json-schema-to-zod@0.3.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-11 10:54:21 +02:00
sergioperezcheco a2f6b9699b fix(json-schema): integrate Google object type normalization
Co-authored-by: jkomyno <alberto@composio.dev>
2026-08-10 22:01:09 +02:00
Alberto Schiabel 1af694baee Merge branch 'next' into fix/cross-sdk-json-schema-object-conversion 2026-08-10 21:23:10 +02:00
Alberto Schiabel c625edc0f7 fix(core): reuse fetched schemas for provider execution (#4103)
Reuse fetched raw tool schemas for provider-wrapped execution while preserving modifier isolation, runtime validation, and unknown-slug fallback behavior.

Refresh the Python 3.12.13 mise lock entries required by the Audit freshness gate.

Co-authored-by: breezeFur <voidexl@outlook.com>
2026-08-10 13:34:20 +02:00
sdkrelease[bot] 13cba53b1d Release: update version (#4038)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to next, this PR will
be updated.


# Releases
## @composio/core@0.15.0

### Minor Changes

- 1503786: Replace the loose JSON Schema property type with a recursive,
type-safe definition.

`JSONSchemaProperty` (re-exported from `@composio/core` and reachable
through
`Tool.input_parameters` / `Tool.output_parameters`) is now a concrete
recursive
interface instead of effectively `any`. Runtime behavior is unchanged,
but
  consumer code that indexed into it without narrowing (for example
`schema.properties.foo.type` or `schema.default.someField`) may see new
type
errors: `properties` entries are now possibly `undefined` and `default`
/
`enum` values are `unknown`. Narrow with optional chaining or explicit
type
  guards when upgrading.

### Patch Changes

- 2ac6ad3: Bound the background npm version check so registry outages
cannot leave the request pending indefinitely.
- 5105612: Match sensitive upload path segments using the target
filesystem's actual case sensitivity so case-insensitive mounts cannot
bypass the denylist without over-blocking distinct paths on
case-sensitive mounts.
- 051c8c5: Redact secrets that appear inside JSON payloads in telemetry
error text. The key/value rule required the separator to follow the key
name directly, so a serialized body such as `{"api_key": "..."}` — the
shape error messages usually carry — was sent unredacted.
- e5c9ada: Refresh the OpenAI runtime dependency to version 7.
- ecd0861: Release unread response bodies on the paths the SDK knowingly
abandons: cancel every intermediate redirect body in `ssrfSafeFetch`,
and the response body before throwing on `!response.ok` in both
URL-upload call sites, instead of leaving them for the garbage collector
to reclaim.
- 2a6a051: Remove the unused internal `isNewerVersion` helper.
- Updated dependencies [1503786]
  - @composio/json-schema-to-zod@0.2.2
## @composio/openai@0.11.0

### Minor Changes

- e5c9ada: Add support for OpenAI versions 6 and 7.
## @composio/experimental@0.2.2

### Patch Changes

- 4ceaede: Refresh the TypeBox runtime dependency.
## @composio/json-schema-to-zod@0.2.2

### Patch Changes

- 1503786: Model the parser-supported `min`, `max`, and `example` JSON
Schema extensions in the exported recursive schema type.
## @composio/slim@0.15.0

### Patch Changes

- e5c9ada: Refresh the OpenAI runtime dependency to version 7.
- Updated dependencies [1503786]
  - @composio/json-schema-to-zod@0.2.2

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-07 22:16:46 +05:30
jkomyno 3fef432dc0 fix: fail closed on invalid dynamic schemas 2026-08-07 21:03:08 +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 8d4bb3bf7e chore: merge next into json schema conversion fix 2026-08-07 18:44:34 +05:30
jkomyno 4a89ff112c fix(json-schema): enforce dynamic object contracts 2026-08-07 00:06:55 +05:30
jkomyno 063f03a72f docs(changesets): explain each behavior change with a before and after 2026-08-06 22:50:46 +05:30
jkomyno ac6bbabd31 fix(claude-agent-sdk): register complete tool schemas
The provider registered tools through jsonSchemaToZodShape, which returns only
the per-property map. Root-level constraints -- `additionalProperties`, boolean
or schema-valued, and `patternProperties` -- are structurally unrepresentable in
a raw shape, so they were dropped before the Claude Agent SDK ever saw them:
free-form object arguments lost their content, and an unknown key was silently
stripped from tool input rather than rejected.

Registering the complete object schema fixes both. A tool with no input
parameters keeps its closed root explicitly, since a bare `properties: {}` is
now an open object.

Proven at the real SDK boundary rather than against a mocked `tool()`: the new
suite drives createSdkMcpServer over an in-memory MCP transport and asserts on
the advertised inputSchema and on real tools/call results.
2026-08-06 21:59:12 +05:30
jkomyno cb6fe9bf64 chore(changesets): release the schema conversion fixes as minor bumps
Python's unknown-key rejection changes behavior for the langchain, langgraph,
and crewai providers rather than only fixing a defect, so the release carries a
minor bump across both SDKs instead of a patch.
2026-08-06 21:00:15 +05:30
jkomyno 5e57815af1 fix(json-schema): accept and preserve dynamic object content across converters
Property-less objects (`{ type: "object" }`, `properties: {}`) were being
collapsed to a strict empty object by the Zod converter and given an implicit
`additionalProperties: false` by the Effect converter, so valid free-form
payloads such as METABASE_POST_API_CARD.dataset_query were rejected at the CLI
boundary. `ToolSchema.parse` separately dropped root `patternProperties` and
rejected a schema-valued root `additionalProperties`.

Dynamic keys are now routed to exactly the schemas that apply to them, and the
shared acceptance contract lives in one checked-in corpus with byte-identical
TypeScript and Python copies.
2026-08-06 20:33:53 +05:30
Alberto Schiabel ecd0861741 fix(core): release unread response bodies (#4078)
This PR:
- follows [Undici's
recommendation](https://github.com/nodejs/undici#garbage-collection) to
consume or cancel unread response bodies instead of leaving
connection-resource cleanup to the garbage collector, which can increase
connection pressure and reduce reuse
- cancels every intermediate redirect body in `ssrfSafeFetch` before
validating and following the next hop
- cancels non-success response bodies in both URL-upload callers before
preserving their existing errors
- adds regression coverage for redirect ordering, redirect-budget
exhaustion, and both caller error paths
- adds a patch changeset for `@composio/core`
- supersedes https://github.com/ComposioHQ/composio/pull/4018 and
credits @pacocartones as co-author

## Verification

- `pnpm --filter @composio/core test ssrfGuard fileUtils
ToolRouterSessionFilesMount` (4 files, 58 tests passed)
- `pnpm --filter @composio/core test` (47 files, 1,049 tests passed)
- `pnpm --filter @composio/core typecheck`
- `pnpm lint:packages` (0 errors; existing warnings only)
- Prettier check on all touched files
- `pnpm validate:changesets`

Co-authored-by: pacocartones <pacocartones@users.noreply.github.com>
2026-08-06 17:52:02 +05:30
Rajarshi Datta 2a6a051bc3 refactor(core): remove unused isNewerVersion helper (#4029)
This PR:

- closes #4028
- removes the unused `isNewerVersion` helper from the internal version
utility
- avoids defining malformed-version behavior for a helper with no
callers or package export
- retains `next`'s tested npm version-check timeout implementation
- adds a patch changeset for `@composio/core`
- refreshes Python 3.12.13 artifact metadata in `mise.lock` after the
upstream build was republished

## Verification

- version utility tests: 2 passed
- core typecheck: passed
- changeset validation: passed
- `mise 2026.5.18 lock --platform
linux-x64,linux-arm64,macos-arm64,macos-x64`: clean

---------

Co-authored-by: jkomyno <alberto@composio.dev>
2026-08-06 09:32:53 +05:30
jkomyno 86cc3cc399 chore: merge next into dependabot remediation 2026-08-05 21:29:53 +05:30
Rajarshi Datta 27fbb464f7 Merge branch 'next' into fix/version-check-fetch-timeout 2026-08-04 11:48:10 +05:30
Alberto Schiabel 1503786358 refactor(core): forbid explicit any and type JSON Schema recursively (#3968)
This PR:

- is refreshed directly onto the current next branch now that #3966 and
#3967 have merged; the PR contains only its intended type-safety work
- enforces no explicit any through the standard .oxlintrc.json, pnpm
lint, and plain oxlint --fix in lint-staged; docs remain covered by
their nested configuration
- replaces the loose JSONSchemaProperty in @composio/core with a
concrete recursive interface, removing explicit any reachable through
Tool.inputParameters and Tool.outputParameters
- includes a minor changeset documenting the type narrowing for
properties, default, and enum consumers
- adds a compile-only public-contract test for recursive schemas,
known-key validation, extension keywords, and Tool reachability
- removes explicit any from FileToolModifier, ts-builders, core and CLI
tests, provider tests, and json-schema-to-zod tests
- keeps TypeScript ESLint-parity rules enabled and closes the
tool-router example lint/typecheck gap
- makes no lockfile changes and no runtime behavior changes

## Context

Final PR from the original #3958 split. The migration foundations are
already on next via #3966 and #3967, so this branch no longer carries a
stacked base commit.
2026-08-03 22:24:57 +05:30
jkomyno 2ac6ad320a fix(core): complete version check timeout guard 2026-08-03 18:53:18 +05:30
jkomyno e5c9adabc8 chore(deps): migrate OpenAI runtime to v7 2026-08-03 17:05:23 +05:30
jkomyno 4ceaede87a chore(deps): refresh runtime support dependencies 2026-08-03 17:05:20 +05:30
Alberto Schiabel 51056129ee fix(core): detect filesystem case sensitivity for uploads (#4036)
This PR:
- fixes #4024
- supersedes #4025 with target-filesystem detection instead of
unconditional case folding
- uses `is-fs-case-sensitive` on the nearest existing directory so mixed
mounts and case-sensitive filesystem configurations are handled
correctly
- preserves exact matching on case-sensitive filesystems and falls back
to conservative case-insensitive matching when detection fails
- keeps Node-only detection behind `#platform` and mirrors its runtime
dependency in `@composio/slim`, preserving the workerd-safe bundle and
Slim import
- adds focused Node, workerd, and matcher regressions plus an
`@composio/core` patch changeset
- verifies 1,044 core tests, five CLI upload tests, the Slim
build/import smoke, typecheck, build, ATTW, publint, lint, formatting,
frozen install, and changeset validation
2026-08-03 16:32:40 +05:30
sdkrelease[bot] fdbd44f9eb Release: update version (#3906)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to next, this PR will
be updated.


# Releases
## @composio/core@0.14.1

### Patch Changes

- 577a3d4: Replace the backtracking leading/trailing-slash-trim regexes
in the Cloudflare Workers/Edge platform path helpers with index-walk
loops, closing a polynomial-time regular expression denial-of-service
(CodeQL js/polynomial-redos) on long runs of slash characters. Output is
unchanged for every input.
- 503b50a: Refresh runtime dependencies across the TypeScript SDK
packages.
- 2f63fe5: Guard Tool Router session URL uploads against SSRF,
revalidate redirect targets, and enforce a streamed 100 MiB response
limit across TypeScript URL upload paths.
## @composio/experimental@0.2.1

### Patch Changes

- 503b50a: Refresh runtime dependencies across the TypeScript SDK
packages.
## @composio/mastra@0.10.3

### Patch Changes

- 503b50a: Refresh runtime dependencies across the TypeScript SDK
packages.
## @composio/slim@0.14.1

### Patch Changes

- 503b50a: Refresh runtime dependencies across the TypeScript SDK
packages.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-30 16:25:33 +05:30
Saransh Rana 577a3d4013 security: fix 3 CodeQL alerts (2026-07-22) (#3909)
## Summary

Automated weekly CodeQL remediation run for 2026-07-22. Closes three
`js/polynomial-redos` code scanning alerts (all severity high) in the
Cloudflare Workers / Edge platform shim.

`ts/packages/core/src/platform/workerd.ts` implements the `Platform`
interface for edge runtimes, where Node's `path` module is unavailable,
so path joining and basename extraction were done with regexes. Three of
those regexes (`/\/+$/` twice, `/^\/+|\/+$/g` once) backtrack
quadratically on long runs of `/`, so a path segment made of many
slashes turns a trim into a hang. They are replaced with index-walk
helpers that scan character codes, which is linear and allocates nothing
when the string is already trimmed.

Output is unchanged for every input. Equivalence was verified
exhaustively over all 137,257 strings of length 6 or less drawn from
`{'/', 'a', '\n', '\r', '.', ' ', '\\'}`, plus 300k randomized
multi-segment `joinPath` compositions and a Unicode surrogate spot
check, with zero mismatches against the original regexes. The `\n` and
`\r` cases matter because JavaScript `$` without the `m` flag anchors to
end of input only, so `/\/+$/` must not trim `"a/\n"`; the index walk
agrees.

All 15 open alerts on `next` were triaged. 3 are fixed here, 12 are
skipped with a recommended disposition for a human to action. Details
below.

## Changes

- `ts/packages/core/src/platform/workerd.ts`: add module private
`trimTrailingSlashes` and `trimSurroundingSlashes`, use them in
`joinPath` and `basename`. No export surface change, no `Platform`
interface change, no other platform implementation touched.
- `ts/packages/core/test/platform/workerd.test.ts`: new behavior pinning
test file.
- `.changeset/fix-workerd-slash-trim-redos.md`: patch bump for
`@composio/core`.

## Type of change

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

## Fixed

| Alert | Rule | Severity | File | Fix | Test that proves it |
|---|---|---|---|---|---|
|
[#55](https://github.com/ComposioHQ/composio/security/code-scanning/55)
| `js/polynomial-redos` | High |
`ts/packages/core/src/platform/workerd.ts:21` | `joinPath` first
segment: `/\/+$/` replaced with `trimTrailingSlashes` | `joinPath
matches the pre-fix regex implementation for single segments` |
|
[#56](https://github.com/ComposioHQ/composio/security/code-scanning/56)
| `js/polynomial-redos` | High |
`ts/packages/core/src/platform/workerd.ts:23` | `joinPath` later
segments: `/^\/+\|\/+$/g` replaced with `trimSurroundingSlashes` |
`joinPath matches the pre-fix regex implementation for every segment
pair` |
|
[#57](https://github.com/ComposioHQ/composio/security/code-scanning/57)
| `js/polynomial-redos` | High |
`ts/packages/core/src/platform/workerd.ts:43` | `basename`: `/\/+$/`
replaced with `trimTrailingSlashes` | `basename matches the pre-fix
regex implementation for every input` |

On test design: the equivalence tests compare the new implementation
against a copy of the pre-fix regex logic, so they pin behavior but pass
in both states by construction. The test that is actually red before the
fix and green after is `rejects the backtracking slash regexes at the
source level`, which asserts the two vulnerable patterns are absent from
`workerd.ts`. That is what would catch a silent revert to the regex
form. A timing based "pathological input completes within N ms" test was
deliberately not written, because it is flaky in CI.

## Skipped

Twelve alerts were read and deliberately not fixed. None were dismissed
via the API; that decision is yours.

### Test only code, recommend `dismiss: used in tests`


**[#47](https://github.com/ComposioHQ/composio/security/code-scanning/47)**
`py/incomplete-url-substring-sanitization`, High,
`python/tests/test_files.py:2518`. The flagged line is `assert
"example.com" in sanitized`, a pytest assertion checking that a logging
redaction helper preserved the host. It is not a URL authorization
check.

```
gh api -X PATCH /repos/ComposioHQ/composio/code-scanning/alerts/47 \
  -f state=dismissed -f dismissed_reason="used in tests" \
  -f dismissed_comment="Substring check is a pytest assertion on a log-redaction helper, not an authorization check. codeql-autofix 2026-07-22, PR #3909"
```


**[#54](https://github.com/ComposioHQ/composio/security/code-scanning/54)**
`js/incomplete-sanitization`, High,
`ts/packages/core/test/models/verifyWebhook.integration.test.ts:339`.
`fixture.payload.replace('{', '{ ')` deliberately corrupts a payload so
the test can assert webhook verification fails on whitespace change. It
is the opposite of a sanitizer.

```
gh api -X PATCH /repos/ComposioHQ/composio/code-scanning/alerts/54 \
  -f state=dismissed -f dismissed_reason="used in tests" \
  -f dismissed_comment="Deliberate payload corruption in a negative webhook-verification test, not a sanitizer. codeql-autofix 2026-07-22, PR #3909"
```

### False positive, recommend `dismiss: false positive`


**[#105](https://github.com/ComposioHQ/composio/security/code-scanning/105)**
and
**[#106](https://github.com/ComposioHQ/composio/security/code-scanning/106)**
`js/prototype-polluting-assignment`, Medium,
`ts/packages/core/src/utils/jsonSchema.ts:255-256`.

The flagged statements are `delete out.$defs` and `delete
out.definitions`. Both use hardcoded literal keys, never attacker
controlled. `out` can never alias `Object.prototype`: every return path
in `walk()` constructs a fresh object via `Object.fromEntries` or object
spread, except the primitive passthrough at line 197, which cannot
return `Object.prototype` because the top level call is seeded with a
value already checked by `isPlainObject`. Two upstream guards further
block the flow CodeQL believes exists: `cloneChildren` filters
`POLLUTING_KEYS` (`__proto__`, `constructor`, `prototype`) at line 171,
and `tryStep` gates property access with
`Object.prototype.hasOwnProperty.call` at line 91. CodeQL cannot see
through that guard because it is wrapped in a tagged union return.

```
gh api -X PATCH /repos/ComposioHQ/composio/code-scanning/alerts/105 \
  -f state=dismissed -f dismissed_reason="false positive" \
  -f dismissed_comment="delete uses a literal key and out is always a freshly constructed object; POLLUTING_KEYS filter and hasOwnProperty guard block the modeled flow. codeql-autofix 2026-07-22, PR #3909"
```

```
gh api -X PATCH /repos/ComposioHQ/composio/code-scanning/alerts/106 \
  -f state=dismissed -f dismissed_reason="false positive" \
  -f dismissed_comment="Same flow as alert 105, on delete out.definitions. codeql-autofix 2026-07-22, PR #3909"
```

### Needs a decision, recommend `keep open`

These are not obviously wrong, but the correct fix changes generated or
rendered output, which is a product decision rather than an automated
one. They are left open on purpose.

| Alert | Rule | File | Why it needs a human |
|---|---|---|---|
|
[#48](https://github.com/ComposioHQ/composio/security/code-scanning/48)
| `js/incomplete-sanitization` |
`docs/app/llms.mdx/[[...slug]]/route.ts:626` | The only runtime site in
this group. Escapes `\|` for a markdown table cell without escaping
backslash first, so a description containing `\|` can break out of the
cell. Impact is table formatting in an LLM facing docs endpoint, not
injection into a code sink. Fixing it changes rendered docs output, and
this file is outside the `@composio/core` test baseline used for this
run. |
|
[#49](https://github.com/ComposioHQ/composio/security/code-scanning/49)
| `js/incomplete-sanitization` |
`docs/scripts/generate-meta-tools.ts:176` | Escapes `"` into YAML
frontmatter without escaping backslash first. Build time, first party
tool metadata. Backslash first escaping changes generated docs. |
|
[#50](https://github.com/ComposioHQ/composio/security/code-scanning/50)
| `js/incomplete-sanitization` |
`ts/packages/core/scripts/generate-docs.ts:1008` | `escapeTextForMdx`,
same class. Build time only. |
|
[#51](https://github.com/ComposioHQ/composio/security/code-scanning/51)
| `js/incomplete-sanitization` |
`ts/packages/core/scripts/generate-docs.ts:1008` | Second instance on
the same chain as #50. |
|
[#88](https://github.com/ComposioHQ/composio/security/code-scanning/88)
| `js/incomplete-sanitization` |
`docs/scripts/generate-meta-tools.ts:134` | Markdown table escaping of
`\|` over first party metadata. |
|
[#89](https://github.com/ComposioHQ/composio/security/code-scanning/89)
| `js/incomplete-sanitization` |
`docs/scripts/generate-meta-tools.ts:136` | Same as #88 on the fallback
description branch. |
|
[#90](https://github.com/ComposioHQ/composio/security/code-scanning/90)
| `js/incomplete-multi-character-sanitization` |
`docs/agent/lib/docs.ts:232` | `toCleanMarkdown` strips JSX and HTML
tags in a single pass, so `<<div>div>` leaves `<div>`. It cleans MDX for
LLM context and its output is never rendered as HTML, so it is not a
security sanitizer. Duplicated verbatim in #92. |
|
[#92](https://github.com/ComposioHQ/composio/security/code-scanning/92)
| `js/incomplete-multi-character-sanitization` |
`docs/scripts/build-agent-index.ts:155` | Verbatim duplicate of the
`toCleanMarkdown` helper in #90. If one is addressed, both should be,
ideally by extracting the shared helper. |

## Scanning config recommendations

Six of the twelve skips (#49, #50, #51, #88, #89, #92) share one root
cause: CodeQL scans build and docs generation scripts, and flags string
escaping over first party metadata that never crosses a trust boundary
at runtime.

This repo uses CodeQL **default setup** (`analysis_key` is
`dynamic/github-code-scanning/codeql:analyze`, and there is no committed
CodeQL workflow), so `paths-ignore` is not available. The two options
are to migrate to advanced setup with a committed workflow so
`paths-ignore` can exclude these directories, or to bulk dismiss the
alerts individually.

**Recommendation: do neither blindly, and do not add `paths-ignore` for
`**/scripts/**`.** These are real findings in real code, just low
priority ones over trusted input. Excluding the directory would also
blind the scanner to genuine future vulnerabilities in build tooling,
which is where supply chain problems tend to live. The better path is a
single follow up PR that fixes the escaping properly, escaping backslash
before the other characters, with the generated output diff reviewed as
part of that PR. If the team decides the churn is not worth it,
dismissing each as `won't fix` with that justification is the honest
alternative, and it keeps future scanning intact.

## How Has This Been Tested?

```
pnpm --filter "@composio/core^..." run build   # build workspace deps once
pnpm --filter @composio/core run test
pnpm --filter @composio/core run typecheck
```

- Pre-fix baseline on clean `next` at `2f63fe540`: **44 test files, 1034
tests, 0 failures.**
- On this branch: **45 test files, 1040 tests, 0 failures.** That is the
baseline plus this PR's 6 new tests.
- Typecheck: clean.
- There were no pre-existing failures, so nothing here is masking one.

Toolchain note for reproduction: the workspace `preinstall` hook
requires `mise`, and `@composio/json-schema-to-zod` must be built before
the `@composio/core` suite will resolve its imports. Without that build,
9 test files fail with `Failed to resolve entry for package
"@composio/json-schema-to-zod"`, which is an environment artifact and
not a real failure.

## Remaining backlog

- Fixable alerts beyond the 15 alert fix cap: **0**. All 15 open alerts
on `next` were triaged in this run.
- Untriaged alerts beyond the 50 alert triage bound: **0**. The bound
was not reached.
- Open alerts remaining after this PR merges: **12**, all listed above
with a recommended disposition. Four of them have ready to run dismissal
commands, which would permanently shrink every future run's fetch.

## Post-fix security scan

`composio-security:security-diff-scan` was run on this PR's diff as the
final gate, because a remediation can introduce its own bug and CodeQL
will not re-run until CI does.

**Verdict: clean. Zero reportable findings.**

Scope: merge-base `2f63fe540` to head `558d730e6`, all three changed
files. Every deep-review row has a completion receipt, so coverage is
complete rather than sampled. Discovery emitted no technically plausible
candidates, so validation and attack-path analysis were skipped under
the scan's documented zero-candidate rule.

Checks that were run and came back negative:

- The fix removes the denial-of-service primitive rather than relocating
it. No regex remains on the trimming paths, and all three loops strictly
progress toward their bound, so there is no unbounded-work or
infinite-loop path.
- Surrogate-pair safety. `charCodeAt` compares UTF-16 code units, and
`/` is U+002F, which can never appear as either half of a surrogate
pair. No astral-plane input can make the index walk disagree with the
regex it replaced.
- No security control changed. `joinPath` feeds `resolvePath`, which
`sensitiveFileUploadPaths.ts:39` consumes for its upload denylist. That
unchanged consumer was inspected as a negative control: byte-identical
output means it cannot observe a difference, and it independently
re-splits on slash runs at line 48 anyway.
- No new allocation or memory amplification. Both helpers return the
input unchanged when nothing needs trimming, so the fix allocates
strictly less than the `.replace()` calls it replaced.
- The new test file reproduces the two pre-fix regex literals as
reference implementations. They are applied only to a literal `CASES`
array, so there is no taint source, and `test/` is absent from this
package's `files` allowlist, so they are never published to SDK
consumers.

No new critical or high finding was introduced by this diff, so no
commit needed reverting and no alert was reclassified.

## 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 documented behavior changed)
- [x] I added tests or explain why not applicable
- [x] I added a changeset if this change affects published packages

## Additional context

Generated by the `codeql-autofix` skill. The bot never dismisses,
closes, or modifies alerts, and never merges or enables auto merge.
Alerts close only because merged code no longer contains the flaw.

---------

Co-authored-by: Saransh <saranshrana@Saranshs-MacBook-Pro.local>
Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-28 13:26:41 +05:30
Alberto Schiabel 503b50ab02 chore(deps): refresh SDK, Python, and CI dependencies (#3955)
This PR:

- splits https://github.com/ComposioHQ/composio/pull/3953 in two: this
PR carries every dependency and GitHub Actions bump **except** the docs
site, which follows in a stacked PR
- consolidates and supersedes Dependabot PRs #3915, #3916, and #3934
through #3942
- adopts TypeScript 7.0.2 for primary compilation while retaining the
`@typescript/typescript6` API lane that TypeScript-ESLint still
requires, following the upstream side-by-side guidance
- refreshes Python core and provider dependencies, lockfiles, and the
Ruff 0.16 lint configuration
- updates every GitHub Action with a verified newer official release,
including majors, while retaining immutable commit SHA pins and
migrating setup-uv cache pruning
- deletes four per-package `eslint.config.mjs` shims: under ESLint 10
the default per-file config lookup re-anchors the root config's globs
into each package, so `pnpm lint` stayed green while the CLI's
try/catch, `process.env` and node-builtin bans went unenforced
- bounds and documents the new `brace-expansion` and `@hono/node-server`
security overrides, raising the `@hono/node-server` floor to 2.0.10 to
clear GHSA-9mqv-5hh9-4cgg
- preserves intentional compatibility fixtures and lanes for AI SDK 6,
Zod 3, TypeScript 5.8, Mastra AI SDK 5, and Python provider constraints

## Context

The docs site is a separate Bun workspace with its own `bun.lock` and is
not a pnpm workspace member, so the two halves share no lockfile and no
build. Splitting them keeps the Fumadocs 11 migration (a breaking API
change with real refactoring) reviewable on its own, independently of
the mechanical version bumps here.

The `brace-expansion` override deliberately spans majors:
GHSA-mh99-v99m-4gvg (HIGH) is published as a single `<=5.0.7` range with
no 1.x or 2.x backport, so narrowing it to the 5.x line puts
`brace-expansion` 2.1.2 back under `core>minimatch>brace-expansion` and
`pnpm audit --prod --audit-level=high` exits 1. Verified both ways; the
trade-off it buys is recorded inline in `pnpm-workspace.yaml`.

Verified on this branch standalone: `pnpm install --frozen-lockfile`,
`pnpm lint`, `pnpm typecheck`, `pnpm build:packages`, `pnpm test` (963
tests, 26/26 tasks), and `pnpm audit --prod --audit-level=high`.
2026-07-27 17:57:29 +05:30
Shrey Singla 2f63fe5407 fix(core): secure Tool Router session URL uploads (#3900)
## Summary

- route Tool Router session URL uploads through the SDK's SSRF-safe
fetch path
- validate every redirect target and reject destinations that resolve to
non-public network addresses
- stream URL response bodies with a 100 MiB limit instead of buffering
unbounded responses
- fail closed for URL uploads on edge runtimes where DNS validation is
unavailable
- apply the bounded response reader to the existing core URL-upload path
as well
- add a patch changeset for `@composio/core`

## Root cause

Tool Router session file uploads fetched string URL inputs directly and
buffered the entire response. That bypassed the SSRF checks already used
by other SDK upload paths and allowed an untrusted URL to target
internal network resources or return an unbounded response body.

## Impact

Node.js and Bun URL uploads now validate resolved addresses before
connecting and revalidate redirect hops. Cloudflare Workers and other
edge runtimes reject URL inputs with a typed error; callers can fetch
trusted content themselves and pass a `File` or `ArrayBuffer` instead.

## Validation

- focused SSRF and file upload tests: 59 passed
- full `@composio/core` suite: 1,034 passed
- TypeScript checks with both tsgo and tsc
- ESLint and Prettier
- package build, ATTW, and publint
- changeset validation
- Cloudflare Workers live E2E: 5 passed
- live Node Tool Router upload/list/download/delete flow
- live Node Tool Router HTTPS URL upload and cleanup

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/ComposioHQ/codesmith/composio/pr/3900"><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=1787224599&installation_model_id=428187&pr_number=3900&repository=ComposioHQ%2Fcomposio&return_to=https%3A%2F%2Fgithub.com%2FComposioHQ%2Fcomposio%2Fpull%2F3900&signature=ef519ac58a7945667565c1c143983f353b8947c988079d76c04afa9ab3b111d1"><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 -->
2026-07-22 14:51:39 +05:30
Alberto Schiabel 9498679c26 fix(release): guard and document CLI releases (#3895)
This PR:
- removes the stale `@composio/cli` changeset that wedges
`changesets/action` and preserves its release note in the CLI changelog
- adds `validate:changesets` before the TypeScript release action and
covers ignored-package changesets in the release regression suite
- makes the guard read changeset files directly so it also works in
shallow and detached CI checkouts
- refreshes `mise.lock` after the pinned Python standalone artifacts
moved to the 20260718 build
- adds the repo-local `cli-release` skill with beta, stable-promotion,
verification, and failure-recovery procedures
- replaces the contradictory "stable via changeset" contributor guidance
with the tested-beta promotion path
- extends skill taxonomy, routing probes, and PR path filters so the
guard cannot silently drift

## Regression coverage

The validator test creates a changeset fixture outside a Git repository,
verifies that an ignored CLI package is rejected, then verifies that a
normal package changeset passes. This reproduces the shallow-checkout
failure without relying on a local `next` ref.

## Verification

- `pnpm validate:agent-skills`
- `pnpm validate:skill-routing`
- `pnpm validate:changesets`
- `pnpm test:release-workflow`
- `pnpm lint`
- `pnpm install --frozen-lockfile`
- all 24 TypeScript package test tasks
- skill-creator `quick_validate.py`
- Prettier check
- `git diff --check`
- manual beta release
[`@composio/cli@0.2.33-beta.294`](https://github.com/ComposioHQ/composio/releases/tag/%40composio/cli%400.2.33-beta.294):
33/33 release and installation jobs passed
2026-07-20 23:17:03 +04:00
xHai 8467efdfb9 fix(cli): whoami respects the org selected via orgs switch (#3832)
## Summary

`composio whoami` reports the wrong organization after `composio orgs
switch`.
It always shows the API key's home org, ignoring the switched-to org,
even
though `composio orgs list` correctly reflects the selection.

**Root cause:** `whoami` resolves session info via
`getSessionInfoByUserApiKey`,
which sent only `x-user-api-key` and omitted `x-org-id`. Without that
header the
backend falls back to the API key's home org. Consumer commands
(`search`,
`execute`, …) already forward the selected org via
`resolveCommandProject`, so
only `whoami` was affected — a display bug, not an API bug.

**Verification that the backend honors the header** (direct calls to
`GET /api/v3/auth/session/info`):

| `x-org-id` | returned project/org |
|---|---|
| org A | org A |
| org B | org B |
| (omitted) | API key's home org |

## Changes

- `getSessionInfoByUserApiKey` accepts an optional `orgId` and forwards
it as
`x-org-id` when present (login-time callers pass none, so their behavior
is
  unchanged).
- `whoami` passes the selected global org (`ctx.data.orgId`).
- Unit tests asserting the header is sent when an org is selected and
omitted
  otherwise.

## Type of change
- [x] Bug fix

## How Has This Been Tested?

- New unit tests: `test/src/services/composio-clients.test.ts` (2
cases).
- Existing `whoami.cmd.test.ts` still passes (5 tests total green).
- Manual end-to-end with a real UAK against two orgs: a binary built
from this
  branch reports the switched org correctly (`orgs switch` → `whoami`),
  whereas the released 0.2.31 binary always reported the home org.

## 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 docs affected)
- [x] I added tests or explain why not applicable
- [x] I added a changeset if this change affects published packages
2026-07-17 00:11:33 +04:00