## Summary
- Rename the permission prompt action from “Allow for session” to “Allow
for 1 hr” in browser and native UI flows.
- Cache allow decisions with a 1-hour expiry in
`tool-permissions-cache.json`, with an in-memory fast path and
stale-entry pruning.
- Preserve existing permission snapshot cache behavior while keeping
cached allow decisions across CLI invocations until expiry.
## Tests
- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm build:packages`
- `cd ts/packages/cli && pnpm run typecheck`
## Notes
- The decision enum remains `allow_session` for compatibility with
existing backend/native callback payloads; only the UX/caching semantics
now expose and enforce a 1-hour duration.
## Summary
- Pulls the per-cwd CLI session id (already generated by
`getCurrentCwdSessionId()` and used as `cli_session_id` on analytics
events) and attaches it as the `x-cli-session-id` header on every
request from the composio client.
- Backend reads the header off `requestInfo`, threads it through
`enrichSessionInfo`, and writes `session_info.cli_session_id` on every
tool execution log row.
Companion hermes PR: ComposioHQ/hermes#9990
## Why
The CLI already tracks a stable per-cwd session id for analytics, but
tool execution logs are blind to it — there's no way to filter the logs
UI / retool back to a single CLI session. Wiring the header through lets
us group all tool calls from one CLI workflow together.
## Test plan
- [x] `pnpm --filter @composio/cli typecheck` clean
- [ ] Run `composio tools execute <slug>` against a build with the
hermes companion deployed, confirm ClickHouse tool log has
`session_info.cli_session_id`
- [ ] Confirm header is absent (and log field absent) when no cwd
session is cached
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes
[PLEN-2450](https://linear.app/composio/issue/PLEN-2450/toolsexecute-fails-for-all-mcp-toolkits-strict-zod-validation-rejects)
· refs https://github.com/ComposioHQ/composio/issues/3354
## Summary
`composio.tools.execute('<MCP_TOOL>', ...)` currently throws a
`ZodError` for **every** tool from an MCP-backed toolkit (`granola_mcp`,
`apify_mcp`, `tavily_mcp`, …) on `@composio/core` 0.6.x–0.9.x. The
reporter on #3354 confirmed all three versions hit the identical
failure; users are working around it by dropping to `@composio/client`
and losing modifiers, custom-tool routing, version checks, and provider
wrapping.
**Root cause** (`ts/packages/core/src/models/Tools.ts` +
`src/types/tool.types.ts`):
- `tools.execute` and `tools.get` both run the API response through
`transformToolCases → ToolSchema.parse`.
- `ParametersSchema` requires `{ type: 'object', properties: {...} }`.
- The API returns `output_parameters: {}` for MCP tools that have no
declared output schema, so the validator throws.
**Fix:** a tiny module-private `normalizeRawToolParameters` helper
applied to both `input_parameters` and `output_parameters` inside
`transformToolCases`. `null` / `undefined` / `{}` all map to
`undefined`; everything else passes through to strict Zod validation.
This:
- Preserves the existing `outputParameters: undefined` ⇒ "no declared
schema" contract — `outputParameters` was already optional in the public
`Tool` type, so no public API change.
- Fixes both `tools.execute` and `tools.list` (single funnel covers both
paths).
- Doesn't relax `ParametersSchema` — tools that actually declare a
schema remain strictly validated.
- Forward-compatible for every present / future `*_mcp` toolkit with no
toolkit allow-list.
## Testing
Red → green confirmed locally. New `describe('MCP-shaped tool metadata
(PLEN-2450)')` block in `tools.test.ts` covers:
1. `getRawComposioToolBySlug` does not throw `ZodError` for
`output_parameters: {}`.
2. Empty `output_parameters` is normalized to `undefined` on the
transformed `Tool`.
3. Both-empty edge case (`input_parameters: {}` + `output_parameters:
{}`).
4. `tools.list` path also tolerates empty `output_parameters`.
5. End-to-end `tools.execute` succeeds against an MCP-shaped fixture.
All five tests **fail** on `next` without the helper (reproducing the
exact `ZodError` from the report) and **pass** with it. Full
`@composio/core` suite: **904 / 904 green**.
```bash
cd ts/packages/core
pnpm vitest run tools.test -t "MCP-shaped tool metadata" # → PASS (5) FAIL (0)
pnpm vitest run # → PASS (904) FAIL (0)
```
## Test plan
- [x] Red phase confirmed: 5 new tests fail with the exact `ZodError`
from the report.
- [x] Green phase confirmed: 5 new tests pass after the helper lands.
- [x] Full `@composio/core` test suite stays green (904 tests).
- [x] `pnpm typecheck` clean.
- [ ] Optional live sanity: build a tarball and run the reporter's
`repro.mjs` against a sandbox user (`pg-test-9f4d0df7-…`) — not
blocking; the mocked end-to-end test pins the same path.
## Follow-ups (out of scope)
- **Python SDK** — verify whether `composio` Python has the same
over-strict Pydantic check and apply the equivalent normalization if so.
> Note: not proposing an API-side change. `output_parameters: {}` is the
correct JSON Schema for "no declared schema" (the empty schema validates
anything); the bug was the SDK's Zod validator being over-strict, not
the API. The fix belongs at the SDK layer, where it now lives.
Co-authored-by: jkomyno <12381818+jkomyno@users.noreply.github.com>
## Summary
- Refresh the browser fallback tool-approval page with the Composio CLI
landing page visual language: light dithered background, mono terminal
card, and brand-blue actions.
- Serve the approval HTML directly from the temporary `127.0.0.1`
callback server instead of writing/opening a `file://` page, so approval
buttons call back to the active CLI process with relative local routes.
- Keep the native macOS sidecar as the preferred prompt when available;
the browser prompt remains the fallback when the sidecar is missing or
fails before returning a decision.
- Add a CLI patch changeset.
## Tests
- `pnpm exec eslint ts/packages/cli/src/services/tool-permissions.ts`
- `pnpm --filter @composio/cli typecheck:src`
- `git diff --check`
## Notes
- Initially `typecheck:src` failed before workspace package build
artifacts existed in the fresh worktree; after building required
workspace dependencies it passed.
---------
Co-authored-by: Malay Vasa <malayvasa@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
This updates the TypeScript SDK's automatic file upload/download
modifier to handle tool schemas that accept either a single file or a
list of files.
Tools can expose file inputs as a composed schema such as:
```json
{
"anyOf": [
{ "type": "object", "file_uploadable": true },
{ "type": "array", "items": { "type": "object", "file_uploadable": true } }
]
}
```
When auto-upload is enabled, the SDK presents those file-uploadable
objects to models as file path strings. If the model supplies a list of
local paths, the old upload resolver selected the first file-bearing
branch, usually the single-file branch, and left the list unchanged.
That means the backend could receive local paths instead of staged `{
name, mimetype, s3key }` descriptors.
## Changes
- Adds runtime-shape-aware composed-schema selection for file
upload/download modifiers.
- Chooses the array branch for list values while preserving single-file
behavior for string/File values.
- Preserves the existing fallback behavior: if no runtime shape matches,
use the first file-bearing variant.
- Keeps `anyOf`, `oneOf`, and `allOf` on the same composed-schema path
used by the SDK today.
- Adds regression coverage for single-file vs multi-file unions, array
item unions, and download parity.
- Adds a patch changeset for `@composio/core`.
## Why
Some file-capable tool schemas are backward compatible at the API level
by accepting both a single file and multiple files. The TypeScript SDK
should transform a list of local paths into a list of staged file
descriptors, just as it already does for direct array schemas.
## Verification
- `pnpm --filter @composio/core exec prettier --check
src/utils/modifiers/FileToolModifier.node.ts
test/tools/fileModifiers.test.ts`
Blocked locally:
- `pnpm --filter @composio/core test -- fileModifiers.test.ts` fails
before collecting tests because this workspace cannot resolve the
unbuilt `@composio/json-schema-to-zod` package entry.
- `pnpm --filter @composio/core typecheck:tsc` is also blocked by
existing local generated-client/workspace dependency errors unrelated to
this patch.
---------
Co-authored-by: Zen <zen@composio.dev>
## Summary
- `composio upgrade` now accepts an optional `<version>` argument so
users can install any published CLI release — stable or beta — without
having to wait for the channel pointer to move.
- Accepts `0.13.1`, `v0.13.1`, `0.13.1-beta.42`, or the full
`@composio/cli@0.13.1` tag (normalized internally).
- When a version is provided, the "already on latest" guard is skipped
so the command also supports downgrades and reinstalls. `--beta`
continues to control the no-arg case.
## Examples
```bash
composio upgrade # latest stable (unchanged)
composio upgrade --beta # latest beta (unchanged)
composio upgrade 0.13.1 # pin to a specific stable
composio upgrade 0.13.1-beta.42 # install a specific beta
composio upgrade @composio/cli@0.13.1 # full release tag also accepted
```
## Implementation
- `upgrade.cmd.ts`: added optional `version` Args, normalizes input to a
`@composio/cli@<semver>` release tag, passes it through to the service.
- `upgrade-binary.ts`: `upgrade`/`fetchLatestRelease` now accept a `tag`
option that fetches the GitHub release directly via
`/releases/tags/<tag>`, bypassing the channel resolver. When a tag is
explicit, the update-availability check is skipped.
- Falls back to the explicit tag for `installSkillSafe` when the upgrade
completes without returning one.
## Test plan
- [x] `pnpm typecheck` (CLI package) — clean
- [x] `pnpm test` in `ts/packages/cli` — 703 passed, 1 skipped, no
regressions in `upgrade-binary.test.ts`
- [ ] Manual: `composio upgrade 0.13.0` from a newer beta to confirm
downgrade path works against real GitHub releases
- [ ] Manual: `composio upgrade --beta` still resolves the latest beta
when no version is given
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.10.0
### Minor Changes
- 42ebff3: feat(connected-accounts): namespace SHARED-connection surface
under `experimental`
Aligns the TypeScript SDK with the experimental wire shape used by
Shared Connections. The flat `accountType` / `aclConfigForShared`
options on `connectedAccounts.link()` and `session.authorize()` have
moved under a single `experimental` block, and
`connectedAccounts.updateAcl()` has moved off the class onto a top-level
`experimental_updateAcl(composio, id, opts)` export — same precedent as
`experimental_createTool` / `experimental_createToolkit`.
The `experimental` namespace is the signal that the shape may change in
future releases. Pinning a SHARED connection in a session config
(`connectedAccounts: { gmail: [...] }`) and direct execute by
`connectedAccountId` are unchanged — only the connection-create / patch
/ authorize surfaces are namespaced.
Also surfaces the `accountType` filter on `connectedAccounts.list()` so
SHARED connections can be listed without dropping to the raw client. The
wire keeps this as a flat query param, so the SDK keeps it flat too.
`@composio/client` bumped from `0.1.0-alpha.71` → `0.1.0-alpha.72` so
the generated typed client carries the `Experimental` namespaces for
`link.create`, `toolRouter.session.link`, and `connectedAccounts.patch`.
Caller migration:
```typescript
// before
await composio.connectedAccounts.link('user_id', 'auth_config_id', {
accountType: 'SHARED',
aclConfigForShared: { allowAllUsers: true },
});
await composio.connectedAccounts.updateAcl('ca_abc', { allowAllUsers:
true });
await session.authorize('github', {
accountType: 'SHARED',
aclConfigForShared: { allowAllUsers: true },
});
// after
await composio.connectedAccounts.link('user_id', 'auth_config_id', {
experimental: {
accountType: 'SHARED',
aclConfigForShared: { allowAllUsers: true },
},
});
await experimental_updateAcl(composio, 'ca_abc', { allowAllUsers: true
});
await session.authorize('github', {
experimental: {
accountType: 'SHARED',
aclConfigForShared: { allowAllUsers: true },
},
});
// new — list SHARED connections
const shared = await composio.connectedAccounts.list({
accountType: 'SHARED',
userIds: ['user_creator'],
});
```
## @composio/cli-local-tools@0.0.4
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/anthropic@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/claude-agent-sdk@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/cloudflare@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/google@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/langchain@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/llamaindex@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/mastra@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/openai@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/openai-agents@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/vercel@1.0.0
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
## @composio/cli@0.2.30
### Patch Changes
- Updated dependencies [42ebff3]
- @composio/core@0.10.0
- @composio/cli-local-tools@0.0.4
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: jkomyno <12381818+jkomyno@users.noreply.github.com>
## Summary
Moves the SHARED connected-accounts surface in the Python SDK under the
`composio.experimental` namespace, mirroring the wire-level restructure
of the same fields. Same precedent as `composio.experimental.tool` /
`composio.experimental.Toolkit` — the namespace tells callers the shape
may still change.
- `link()` and `session.authorize()`: flat `account_type` /
`acl_config_for_shared` kwargs → single `experimental={}` dict
- `composio.connected_accounts.update_acl(...)` →
`composio.experimental.update_acl(...)`
- `composio.connected_accounts.list(...)` now exposes
`account_type='PRIVATE' | 'SHARED' | 'ALL'` (flat — the wire keeps this
as a query param)
- `composio-client` bumped `1.38.0` → `1.39.0` so the generated typed
client carries the `Experimental` TypedDicts
Pinning a SHARED connection in a session (`connected_accounts={...}`)
and direct execute by `connected_account_id` are unchanged — only the
connection-create / patch / authorize surfaces moved.
## Caller migration
```python
# before
composio.connected_accounts.link(
user_id, auth_config_id,
account_type="SHARED",
acl_config_for_shared={"allow_all_users": True},
)
composio.connected_accounts.update_acl("ca_abc", allow_all_users=True)
session.authorize(
"github",
account_type="SHARED",
acl_config_for_shared={"allow_all_users": True},
)
# after
composio.connected_accounts.link(
user_id, auth_config_id,
experimental={
"account_type": "SHARED",
"acl_config_for_shared": {"allow_all_users": True},
},
)
composio.experimental.update_acl("ca_abc", allow_all_users=True)
session.authorize(
"github",
experimental={
"account_type": "SHARED",
"acl_config_for_shared": {"allow_all_users": True},
},
)
# new — list SHARED connections
shared = composio.connected_accounts.list(
account_type="SHARED",
user_ids=["user_creator"],
)
```
## Tests
- `link()` forwards the experimental block / omits it when not provided
/ preserves explicit empty lists
- `link()` maps the `acl_config_for_shared is only valid on SHARED` 400
to `ComposioAclOnlyForSharedError`; non-ACL 400s re-raise
- `experimental.update_acl()` body construction, deny-list handling,
`ValidationError` when no fields provided, `ValidationError` when no
client is bound, same 400-mapper as `link()`
- `authorize()` forwards the experimental block
- `list(account_type="SHARED")` delegates the flat filter through to the
client
## Test plan
- [ ] `nox -s fmt` and `nox -s chk` clean for `composio/` (pre-existing
4 mypy errors in `providers/langgraph` and `providers/langchain` are
unrelated — same on `origin/next`)
- [ ] `pytest tests/test_connected_accounts.py
tests/test_tool_router.py` → 136 passed
- [ ] Docs page update (separate PR — code samples already on the
experimental shape there; `account_type=` list filter example to be
added once this lands)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Three types (LinkCreateParams, SessionLinkParams, ConnectedAccountListParamsRaw)
were imported and re-exported but had zero external consumers. The named-type
imports of LinkCreateParams / SessionLinkParams were only referenced in a
descriptive comment; ConnectedAccountListParamsRaw wasn't used at all.
Rewrote the comment so it documents the structural-typing rationale without
referencing the now-unimported namespace names.
Typecheck clean, 919/919 tests pass.
Six code-review nits from the multi-agent review on PR #3424:
P1.1 — `experimental_updateAcl(composio, id, opts)` doesn't match the
`experimental_createTool` precedent it cited. That export is a *pure
factory* (returns a value object, no client, no I/O), whereas
`updateAcl` takes a client and performs a network call. Costs the
current shape was paying:
- Awkward call site (composio passed as first arg every time)
- Bypassed `telemetry.instrument(this, 'ConnectedAccounts')` — the
proxy only wraps class methods
- Diverged from the Python sibling (#3412) which mounts at
`composio.experimental.update_acl(id, opts)`
Moved to `composio.experimental.updateAcl(id, opts)` as a method on a
new `Experimental` class. Top-level `experimental_*` prefix is now
reserved for stateless factories, matching the Python parity.
P2.2 — `ToolRouterSession.authorize` was re-inlining the same
camelCase→snake_case wire mapping that `ConnectedAccounts.link` already
encoded. Extracted `serializeExperimentalForWire` /
`serializeAclConfigForWire` into `models/Experimental.ts` and imported
both call sites from there. Next field added to the experimental block
now lands in one place.
P2.3 — `AuthorizeOptionsSchema` was a structural duplicate of
`ConnectedAccountExperimentalSchema`. Composed from the canonical
schema instead.
P2.4 — Substring match `acl_config_for_shared is only valid on SHARED`
extracted to `ACL_ONLY_FOR_SHARED_ERROR_FRAGMENT` in
`models/Experimental.ts` and reused from `ConnectedAccounts.link`,
`ToolRouterSession.authorize`, and `Experimental.updateAcl`. Single
source of truth so a server-side message tweak doesn't silently
downgrade three call sites to generic BadRequestError.
P2.5 — `ConnectedAccountsErrors.ts` `possibleFixes` strings on
`ComposioSharedAccessDeniedError` and
`ComposioSharedConnectionNotAccessibleError` were pointing at
`composio.connectedAccounts.updateAcl()` (method now removed). Rewrote
to `composio.experimental.updateAcl()` so the typed-error hint
compiles.
P2.6 — Telemetry bypass auto-fixed by P1.1. `Experimental` is now an
instrumented class; methods on it get the same telemetry proxy as
`ConnectedAccounts`.
P2.7 — Inline `import('../types/connectedAccounts.types')` type at the
old `serializeExperimentalForWire` declaration is gone with the
function move. Sibling models use plain top-level imports.
Tests: 919/919 pass, typecheck clean.
**Push-back on remaining findings:**
- P2.8 (`ComposioFailedToCreateConnectedAccountLink` missing `Error`
suffix) — pre-existing naming, not introduced by this PR. Rename with
back-compat alias is bigger scope; deferred.
- P2.9 (TS Zod caps vs Python no caps) — the TS caps are a UX win
(clear ValidationError vs server 400). Better direction is adding the
caps to Python in a follow-up, not dropping them from TS.
- All P3 nice-to-haves deferred.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the TypeScript SDK with the experimental wire shape used by
Shared Connections. The flat accountType / aclConfigForShared options
on connectedAccounts.link() and session.authorize() have moved under a
single experimental block, and connectedAccounts.updateAcl() has moved
off the class onto a top-level experimental_updateAcl(composio, id, opts)
export — same precedent as experimental_createTool /
experimental_createToolkit.
The experimental namespace is the signal that the shape may change in
future releases. Pinning a SHARED connection in a session config and
direct execute by connectedAccountId are unchanged — only the
connection-create / patch / authorize surfaces are namespaced.
Also surfaces the accountType filter on connectedAccounts.list() so
SHARED connections can be listed without dropping to the raw client.
The wire keeps this as a flat query param, so the SDK keeps it flat too.
@composio/client bumped from 0.1.0-alpha.71 -> 0.1.0-alpha.72 so the
generated typed client carries the Experimental namespaces for
link.create, toolRouter.session.link, and connectedAccounts.patch.
Caller migration:
// before
await composio.connectedAccounts.link('user_id', 'auth_config_id', {
accountType: 'SHARED',
aclConfigForShared: { allowAllUsers: true },
});
await composio.connectedAccounts.updateAcl('ca_abc', { allowAllUsers: true });
await session.authorize('github', {
accountType: 'SHARED',
aclConfigForShared: { allowAllUsers: true },
});
// after
await composio.connectedAccounts.link('user_id', 'auth_config_id', {
experimental: {
accountType: 'SHARED',
aclConfigForShared: { allowAllUsers: true },
},
});
await experimental_updateAcl(composio, 'ca_abc', { allowAllUsers: true });
await session.authorize('github', {
experimental: {
accountType: 'SHARED',
aclConfigForShared: { allowAllUsers: true },
},
});
// new — list SHARED connections
const shared = await composio.connectedAccounts.list({
accountType: 'SHARED',
userIds: ['user_creator'],
});
Tests cover: experimental block forwarding (link + authorize), no-op
when omitted, empty-array preservation, typed error mapping for the
PRIVATE-with-ACL case, experimental_updateAcl body construction +
deny-list handling + ValidationError when no fields are provided, and
list(accountType: 'SHARED') flat-filter delegation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Remove darwin-x64 from local toolkit platform lists (beeper-imessage,
peekaboo, chrome-devtools) and drop the corresponding bundled binary
path entries.
- Skip building native Swift sidecar binaries (peekaboo, imessage-cli,
composio-native-ui) for darwin-x64; remove the bun-darwin-x64 matrix
entry from `build-cli-binaries.yml` so we no longer produce a darwin-x64
CLI bundle.
- Locally force-disable enhanced controls / elicitation on darwin-x64 in
`tool-permissions.ts` even when the backend opts the org in (logs a
debug message).
## Test plan
- [x] `pnpm --filter @composio/cli typecheck`
- [x] `pnpm --filter @composio/cli-local-tools typecheck`
- [ ] CI green on the build workflow (no darwin-x64 job runs)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Add a CLI consumer permission cache backed by
`~/.composio/tool-permissions-cache.json` with a 5 minute TTL.
- Read enhanced-controls state from `/api/v3.1/org/consumer/config` and
resolve stored connected-account permissions from
`/api/v3.1/consumer/permissions/resolve`.
- Thread consumer cache scope through Tool Router session creation so
CLI link sessions use the enhanced public link URL when enhanced
controls are enabled.
- Add execute/run-compatible gating: CLI execute checks cached
permission config before the Tool Router execute call and opens the
bundled macOS native UI sidecar for ask modes.
- Use the sidecar `--callback-file` JSON decision channel, pass the
selected connection `word_id` to the UI account line, and treat no
response within 30 seconds as denial.
- Fall back to browser approval when the native sidecar is
unavailable/non-macOS.
## Stack
- Base PR: #3418 (`pi/swift-sidecar-ui-dfceb48e`) — native UI sidecar.
- This PR: CLI permissions cache and native sidecar gating integration.
## Tests
- `pnpm --filter @composio/cli typecheck`
- `pnpm exec eslint
ts/packages/cli/src/effects/create-tool-router-session.ts
ts/packages/cli/src/services/tools-executor.ts
ts/packages/cli/src/services/native-ui-sidecar.ts
ts/packages/cli/src/services/tool-permissions.ts`
- `pnpm --filter @composio/cli build`
- `pnpm --filter @composio/cli-local-tools build:composio-native-ui --
--target darwin-arm64`
- Earlier on this branch: `pnpm install --frozen-lockfile`, `pnpm
build:packages`
## Notes / Risks
- Native approval UI is macOS-only. Non-macOS or missing sidecar
binaries fall back to the temporary browser approval page.
- The CLI gates locally and does not pass `experimental.permissions`
into Tool Router sessions yet, because direct CLI execute does not
provide MCP elicitation transport. It does pass
`experimental.link_url_overwrite` for enhanced link flows.
- On a cold cache, the first consumer Tool Router session creation
refreshes permission state synchronously; fresh cache entries are
returned immediately while a background refresh updates disk for future
calls.
## Summary
- Scaffold a macOS Swift `composio-native-ui` sidecar with a borderless
floating native overlay and Metal-driven pulsing/liquid-glass visual
treatment.
- Add a hidden/dev `composio dev native-ui` command that spawns the
bundled sidecar from CLI local-tool assets.
- Wire the sidecar into local-tool binary build scripts and CLI binary
release packaging checks for Darwin artifacts.
## Tests
- `pnpm install --frozen-lockfile`
- `pnpm typecheck`
- `pnpm --filter @composio/cli build`
- `pnpm --filter @composio/cli-local-tools build:composio-native-ui --
--target darwin-arm64`
- Manual launch via
`COMPOSIO_LOCAL_TOOLS_BIN_DIR=../cli-local-tools/local-tools-binaries
bun dist/bin.mjs dev native-ui --timeout 35`
## Notes / Risks
- This is a scaffold/prototype and is intentionally opened as a draft
PR.
- The visual effect uses a transparent AppKit panel,
`NSVisualEffectView`, and a Metal overlay. True optical distortion of
arbitrary content behind the window would require a different approach
such as ScreenCaptureKit/backdrop sampling and associated permissions.
- Generated sidecar executables remain ignored; release jobs rebuild
them before packaging artifacts.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Description
Weekly automated CLAUDE.md refresh. Updates stale CLAUDE.md files to
reflect recent codebase changes.
Files updated:
- `CLAUDE.md` — 403 -> 97 lines. Rewrote to current state: removed stale
`examples/` top-level entry, added `cli-keyring` and `cli-local-tools`
packages, noted default branch is `next`, kept the Bun version-mismatch
gotcha and the e2e Docker note, pointed agents at `AGENTS.md`,
`docs/CLAUDE.md`, and `ts/packages/cli/AGENTS.md`.
- `ts/packages/cli/AGENTS.md` (which `ts/packages/cli/CLAUDE.md`
symlinks to) — 379 -> 197 lines. Replaced the outdated command table
(the `manage` namespace was removed in #2937 and many new top-level
commands were added: `agent`, `auth-configs`, `connected-accounts`,
`orgs`, `projects`, `toolkits`, `tools`, `triggers`, `logs`, `init`,
`install`, `signup`, `proxy`, `listen`, `run`, `dev`, `artifacts`,
`config`, `local-tools`). Added a note that `@composio/cli-keyring`
powers OS credential storage. Dropped duplicate Effect / Clack
"Reference Source" sections (already consolidated). Kept the
load-bearing pieces: required typecheck, output-conventions (stdout vs
stderr), Effect patterns, release workflow.
- `docs/CLAUDE.md` — unchanged. Already current (last updated
2026-04-21) and within the 30-70 line target at 107 lines; the
`.claude/` subtree matches what it documents.
# How did I test this PR
- Verified line counts: root 97 (target 60-150), `docs/CLAUDE.md` 107
(unchanged), `ts/packages/cli/AGENTS.md` 197.
- Cross-checked the CLI command table against
`ts/packages/cli/src/commands/` (`ls ts/packages/cli/src/commands/`) and
against recent commits (`02e72116d`, `3415a4a15`, `d1ef6b25e`,
`67867aea1`).
- Cross-checked `ts/packages/` listing against the structure block in
the root `CLAUDE.md` (cli, cli-keyring, cli-local-tools, core,
json-schema-to-zod, providers, ts-builders).
- Confirmed `ts/packages/cli/CLAUDE.md` is still the symlink to
`AGENTS.md` after rewriting.
Origin: cron-update-claude-docs /
[zen-cron-248f7d9ad575](https://zen-api-production-4c98.up.railway.app/dashboard/#/chat/zen-cron-248f7d9ad575)
Triggered by: karan@composio.dev | Source: cron
Session:
https://zen-api-production-4c98.up.railway.app/dashboard/#/chat/zen-cron-248f7d9ad575
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/cli-local-tools@0.0.3
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/core@0.9.1
### Patch Changes
- 84a3a07: Add `accountType` and per-user ACL support for SHARED
connected accounts.
- **`accountType` on create**: `composio.connectedAccounts.link(userId,
authConfigId, { accountType: 'SHARED' })` creates a SHARED connection.
Default remains `PRIVATE`. A SHARED connection can be used by other
`userId`s, but only when the connection is explicitly pinned in a
tool-router session's config and only when the requesting `userId`
passes the connection's ACL.
- **`accountType` on retrieve**: `get()` and `list()` responses now
include `accountType` (`'PRIVATE' | 'SHARED'`).
- **`aclConfigForShared` on create + retrieve**: per-user ACL block — `{
allowAllUsers, allowedUserIds, notAllowedUserIds }`. On responses the
field is `undefined` when the caller isn't authorised to see the ACL, so
callers can distinguish _"I can't see the ACL"_ from _"ACL is the
default deny-by-default state"_.
- **`updateAcl()` method** (new):
`composio.connectedAccounts.updateAcl(nanoid, { allowAllUsers,
allowedUserIds, notAllowedUserIds })` writes the ACL via `PATCH`. PATCH
semantics — omit a field to leave it unchanged; pass an empty array to
clear an allow/deny list. At least one field required. Calling on a
PRIVATE connection raises `ComposioAclOnlyForSharedError` (400).
- **`ToolRouterSession.authorize()` options gain `accountType` +
`aclConfigForShared`**, so a SHARED connection with an ACL can be
created in one call from inside a tool-router session.
ACL resolution rule (deny wins):
1. requesting `userId` ∈ `notAllowedUserIds` → DENY
2. `allowAllUsers === true` → ALLOW
3. requesting `userId` ∈ `allowedUserIds` → ALLOW
4. otherwise → DENY (deny-by-default)
Limits: each ACL list accepts up to 1000 entries; each `userId` is
1..256 characters. The SDK enforces these caps at the input boundary.
New error classes:
- `ComposioSharedAccessDeniedError` (403) — surfaces from direct
`connectedAccountId` execution paths when the requesting user fails the
ACL.
- `ComposioAclOnlyForSharedError` (400) — ACL fields sent on a PRIVATE
connection.
- `ComposioSharedConnectionNotAccessibleError` (400) — tool-router
session create / PATCH with a pinned SHARED connection the session user
cannot use.
No breaking changes. Existing `link()` callers without the new options
get a `PRIVATE` connection exactly as today; existing `get()` / `list()`
callers see new optional fields.
The Python SDK mirror ships in a separate PR.
- c358ffa: Fix false-positive `initiate()` deprecation warning for
custom auth configs (SEC-339 follow-up).
`composio.connectedAccounts.initiate()` previously emitted a one-time
`console.warn` on every redirectable-OAuth response, regardless of
whether the auth config was Composio-managed (subject to the 2026-07-03
cutover) or custom (unaffected). The wording was conditional ("If this
auth config is Composio-managed…") so callers using their own OAuth apps
could ignore it, but the warning still printed and caused noise in logs.
Apollo already emits the SEC-339 `Deprecation` / `Sunset` / `Link
rel="deprecation"` headers (RFC 9745 / RFC 8594) **only** on the
retiring branch — managed + redirectable OAuth. The SDK now reads the
`Deprecation` header from the response (via `APIPromise.withResponse()`)
and gates the warning on its presence. Custom auth configs and non-OAuth
schemes get a clean response from the server and now stay silent in the
SDK as well.
- **Behavior change:** No warning is emitted for `initiate()` calls
against custom OAuth auth configs or non-OAuth schemes (API key, bearer,
basic). Managed-OAuth callers continue to get exactly one warning per
process, now with revised wording that points at the response's `Sunset`
header for the precise cutover date.
- **No public API change:** `initiate()` returns the same
`ConnectionRequest` shape and respects the same `allowMultiple` guard.
`ComposioLegacyConnectedAccountsEndpointRetiredError` continues to
surface from the 400 retired-path response.
- **Test scaffolding:** new mock helper `mockApiPromiseWithHeaders()` in
`connectedAccounts.test.ts` wraps a value as an `APIPromise`-shaped
thenable so the new tests can simulate apollo's header behavior.
Pre-existing initiate tests using `mockResolvedValueOnce` continue to
pass via the SDK's defensive fallback when `withResponse` is absent on
the mock.
Python SDK gets the matching change in the same release train.
## @composio/anthropic@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/claude-agent-sdk@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/cloudflare@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/google@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/langchain@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/llamaindex@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/mastra@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/openai@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/openai-agents@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/vercel@0.9.1
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
## @composio/cli@0.2.29
### Patch Changes
- Updated dependencies [84a3a07]
- Updated dependencies [c358ffa]
- @composio/core@0.9.1
- @composio/cli-local-tools@0.0.3
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Two more spots leaking the same immutability wording removed from the
upstream API description:
- ComposioAclOnlyForSharedError possibleFixes said "Set accountType:
'SHARED' at create time". Reworded to "Use accountType: 'SHARED'".
- ComposioSharedConnectionNotAccessibleError JSDoc said "caught at
session-create time". Replaced "caught" framing with "Raised at
session-create time" — "session-create time" stays because it's
user-facing (tells the SDK caller when the error is raised, i.e.
during session.create() / session.patch()).
Also tightened the surrounding wording — class doc cross-references the
actual SDK method names (`toolRouter.session.create()` /
`session.patch()`) instead of abstract phrasing.
No behaviour changes — docstring cleanup only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final sweep caught three more spots leaking internal detail:
- `CreateConnectedAccountLinkOptionsSchema.aclConfigForShared` JSDoc
said "the backend rejects ACL on a PRIVATE connection". Replaced with
"raises ComposioAclOnlyForSharedError on a PRIVATE connection" —
describes the SDK contract from the caller's perspective.
- `UpdateConnectedAccountAclParamsSchema` JSDoc had a parallel
"Backend rejects ACL writes on a PRIVATE connection" sentence.
Reworded the same way.
- Test comment in toolRouter.test.ts had a "Bugbot-flagged gap"
annotation referencing internal review tooling. Replaced with a
neutral description of what the tests verify.
No behaviour changes — JSDoc / comment cleanup only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more JSDoc spots — `ConnectedAccountTypes` and the `accountType` field
on `CreateConnectedAccountLinkOptionsSchema` — claimed `accountType` was
"set at creation time only — cannot be changed later". The wording has
been removed from the corresponding API parameter description upstream,
so the SDK shouldn't echo it independently.
No behaviour changes — JSDoc-only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses 4 review comments from @abir-taheer on #3392:
- transformers/connectedAccounts.ts:21 — remove Hermes PR references
- types/connectedAccounts.types.ts:79 — users don't belong to projects
- types/connectedAccounts.types.ts:55 — drop backend storage detail
- models/ToolRouterSession.ts:330 — remove internal information
Swept the full PR diff for similar leaks rather than just the four
flagged lines, since the same patterns repeat across the new files:
- Removed Hermes PR numbers (#9860 / #9882 / #9902) from inline
comments and JSDoc in transformers, types, model, test describe block,
and changeset.
- Removed "JSONB column on `connected_accounts`" storage detail.
- Reworded "any user_id in the project" / "users in the project" /
"project member" / "project/org API key" — users don't belong to a
project entity in Composio's data model, so the wording was
misleading. Replaced with phrasing focused on the mechanism (pinning
in a tool-router session, authorisation via creator or API key) and
on the `userId` identifier.
- Removed "Backend response: 400 with code `ConnectedAccount_...`"
blocks from error class JSDoc — surfacing the internal error code
name leaked implementation details.
- Cleaned up the augmented `RawConnectedAccountResponseWithLabels`
intersection — `account_type` and `acl_config_for_shared` are now on
the regenerated `@composio/client@0.1.0-alpha.71` types, so the
manual augmentation lines (which carried the Hermes refs) can be
deleted entirely. Field access still works through the existing
intersection used for `word_id` / `alias`.
Also tightened a pre-existing leak I introduced earlier in the PR's
own JSDoc on `update()` — dropped the "historical JSDoc mentioning
alias/credentials" callout since that's internal noise.
No behaviour changes — pure docstring / comment / changeset cleanup.
Verified: pnpm typecheck clean, 915 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bugbot caught a parity gap (cursor[bot] review on #3392): authorize()
accepted aclConfigForShared and passed it straight to the wire, while
ConnectedAccounts.link() validated through CreateConnectedAccountLinkOptionsSchema
(which enforces the 1000-entry list cap and 1..256 char user_id length from
ConnectedAccountAclConfigSchema). Callers using authorize() with oversized
lists or invalid user IDs got opaque backend 400s instead of clear SDK-level
ValidationError, breaking the changeset's claim that "The SDK enforces these
caps via Zod at the input boundary."
Added a local `AuthorizeOptionsSchema` (callbackUrl + alias + accountType +
aclConfigForShared) and a safeParse + ValidationError throw at the top of
authorize(), mirroring the pattern in link() / updateAcl(). The shape is
identical to link's options sub-shape so the same caps apply.
Tests (toolRouter.test.ts):
- forwards accountType + nested acl_config_for_shared on the wire
- throws ValidationError when allowedUserIds exceeds 1000 entries
- throws ValidationError when a user_id exceeds 256 chars
- throws ValidationError when a user_id is empty
915 tests pass (4 new + 911 prior). pnpm typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@composio/client@0.1.0-alpha.71 (just bumped in 483af3607) tightened
SessionLinkResponse to require `account_type: 'PRIVATE' | 'SHARED'` —
the four test mocks of `client.toolRouter.session.link` no longer
satisfy the typed surface.
CI was failing Build + Typecheck + every E2E job with the same 3 errors
at connected-accounts.link.cmd.test.ts:287, 341, 373 (cascade — Typecheck
fails first, Build/E2E inherit). All fixed by adding `account_type: 'PRIVATE'
as const` at the four mock sites (3 in connected-accounts.link.cmd.test.ts,
1 in test-layer.ts default).
PRIVATE chosen as the default because that matches what the backend now
returns when account_type isn't supplied on input.
Lesson: when bumping @composio/client, run `pnpm -r typecheck` at the
workspace root, not just `pnpm --filter @composio/core typecheck`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that #77 in composio-base-ts merged and @composio/client@0.1.0-alpha.71 is
published with account_type + acl_config_for_shared in the typed surface,
all the forward-compat shims come out.
Removed:
- AclConfigWireShape, LinkCreateBodyWithAcl, ConnectedAccountPatchBodyWithAcl
local types
- 3x `as unknown as Parameters<typeof X>[N]` casts in ConnectedAccounts.ts +
ToolRouterSession.ts
Now uses the generated types directly:
- ConnectedAccountPatchParams, ConnectedAccountPatchResponse from
@composio/client/resources/connected-accounts
- LinkCreateParams + LinkCreateParams.ACLConfigForShared from
@composio/client/resources/link
- SessionLinkParams + SessionLinkParams.ACLConfigForShared from
@composio/client/resources/tool-router/session/session
Review fixes from #3392 multi-agent review:
- P1-1: moved orphaned JSDoc back onto ComposioLegacyConnectedAccountsEndpoint-
RetiredError (was stranded above ComposioSharedAccessDeniedError)
- P1-2: added empty-array footgun warning to updateAcl() JSDoc — passing
`notAllowedUserIds: []` silently clears the deny list
- P1-3: changeset reconciled with PR body (Python ships as separate PR)
- P2-9: updateAcl() returns ConnectedAccountPatchResponse instead of void,
matching sibling update()/refresh() ergonomics
- P2-11: clarifying JSDoc on update() pointing at updateAcl() and noting the
pre-existing "alias/credentials" doc is inaccurate (this PR doesn't fix it)
- P3-12: callbackUrl/alias guards normalized to `!== undefined` (was a mix
of `&&` and `!= null`) — empty strings now propagate correctly
- P3-17: refine-guard comment on UpdateConnectedAccountAclParamsSchema noting
ZodEffects doesn't expose .shape
P2-6/P2-7: typed error mapping for ComposioAclOnlyForSharedError (400)
wired at three call sites:
- ConnectedAccounts.link() — wraps existing
ComposioFailedToCreateConnectedAccountLink catch
- ConnectedAccounts.updateAcl() — new catch with rethrow fallback
- ToolRouterSession.authorize() — new catch with rethrow fallback
Substring match on `'acl_config_for_shared is only valid on SHARED'` —
verified against apps/apollo/src/lib/connected_accounts/createConnectedAccount.ts
in hermes#9902. ComposioSharedAccessDeniedError and
ComposioSharedConnectionNotAccessibleError remain exported but not yet
thrown — they'll be wrapped when Tools.execute() and ToolRouterSession.create()
error-mapping lands in a follow-up.
Tests: 12 new behaviour tests in test/connectedAccounts/connectedAccounts.test.ts
covering link() ACL forwarding (5), updateAcl() body construction + empty-object
refine + AclOnlyForShared mapping (6), and link() error-mapping fallback (1).
911 tests pass (up from 899).
Verified: pnpm typecheck clean, pnpm build clean (publint + attw clean).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original draft built against 9902's PR description shape — three flat
top-level ACL fields (`allow_all_users`, `allowed_user_ids`,
`not_allowed_user_ids`). The merged design (deployed to staging now)
collapsed them into a single JSONB column and exposes them on the wire
as a nested `acl_config_for_shared` object. This commit reshapes the SDK
to match.
Wire contract changes verified against staging-backend.composio.dev:
POST /connected_accounts/link body: account_type, acl_config_for_shared
POST /connected_accounts body.connection.{account_type, acl_config_for_shared}
PATCH /connected_accounts/{id} body: alias, connection, acl_config_for_shared
GET /connected_accounts item.{account_type, acl_config_for_shared?}
GET /connected_accounts/{id} response.{account_type, acl_config_for_shared?}
POST /tool_router/session/{id}/link body: account_type, acl_config_for_shared
SDK changes:
- types: `ConnectedAccountAclSchema` → `ConnectedAccountAclConfigSchema`
(input — all fields optional) + `ConnectedAccountAclConfigResponseSchema`
(output — all fields populated when block is visible). Added per-user_id
length cap (1..256) matching aclConfig.ts. `link()` options exposes
`aclConfigForShared?` instead of three flat fields. Retrieve response
exposes `aclConfigForShared?` (undefined when caller can't see ACL).
- model: `link()` and `updateAcl()` serialize the SDK's flat camelCase
ACL params into the nested snake_case `acl_config_for_shared` wire
block via a new `serializeAclConfigForWire` helper. Local body types
updated to match.
- transformer: maps `acl_config_for_shared` (nested) → `aclConfigForShared`
(nested camelCase). Forwards `undefined` when the block is absent.
- ToolRouterSession.authorize() options gain `accountType` +
`aclConfigForShared` — the `/tool_router/session/{id}/link` endpoint
accepts the same fields.
- changeset rewritten to document the nested shape.
Verified: pnpm typecheck clean, build clean, 899 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds SDK wrappers for Hermes #9860, #9882, #9902 — three Apollo-side
features that previously had no high-level surface. Existing call sites
are unchanged; new fields/methods are opt-in.
- `link()` accepts `accountType` ('PRIVATE' | 'SHARED') and ACL fields
(`allowAllUsers`, `allowedUserIds`, `notAllowedUserIds`) to create a
SHARED connection in one call.
- `get()` / `list()` responses now expose `accountType`. ACL fields are
forwarded when present (backend hides them for non-creator/non-API-key
callers — left as undefined in the SDK shape).
- New `updateAcl(nanoid, params)` method wires the existing
`client.connectedAccounts.patch` (PATCH /connected_accounts/{id}) for
ACL writes. PATCH semantics: omit a field to leave unchanged; pass
empty array to clear an allow/deny list. At least one field required.
- New error classes: `ComposioSharedAccessDeniedError` (403),
`ComposioAclOnlyForSharedError` (400),
`ComposioSharedConnectionNotAccessibleError` (400).
The wire-level fields (account_type, allow_all_users, etc.) are not yet
in @composio/client@0.1.0-alpha.70's typed surface — they'll land when
Stainless regenerates against the post-merge backend spec. Two narrow
local types (`LinkCreateBodyWithAcl`, `ConnectedAccountPatchBodyWithAcl`)
+ boundary casts paper over the gap until then; remove once the client
is bumped.
Verified: pnpm typecheck clean, build clean, 899 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
initiate() previously emitted a one-time deprecation warning whenever the
response carried a redirectable OAuth scheme (OAUTH1, OAUTH2, DCR_OAUTH),
regardless of whether the auth config was Composio-managed (subject to the
2026-07-03 cutover) or custom (unaffected). The wording was conditional —
'If this auth config is Composio-managed…' — so callers using their own
OAuth apps could ignore it, but the warning still printed and added noise
to logs.
Apollo already emits the SEC-339 'Deprecation' / 'Sunset' / 'Link
rel="deprecation"' headers (RFC 9745 / RFC 8594) only on the retiring
branch — managed + redirectable OAuth + LD didn't block. The SDKs now
read the 'Deprecation' header from the response and gate the warning on
its presence. Custom auth configs and non-OAuth schemes get a clean
response from the server and now stay silent in the SDK as well.
TS:
- Chain '.withResponse()' on the underlying client.connectedAccounts.create
call to expose the raw Response. Defensive fallback for tests that mock
create() as a plain Promise.
- Drop the LEGACY_RETIRING_OAUTH_SCHEMES set; the apollo header is now the
sole signal.
- New tests in describe('initiate deprecation header gate', ...) pin the
contract: warns when Deprecation is present, silent when absent, and
one-time-per-process even across multiple managed-OAuth calls. A
mockApiPromiseWithHeaders helper wraps mocked values as APIPromise-shape
thenables so the new tests can simulate apollo's header behavior.
Python:
- Switch to client.connected_accounts.with_raw_response.create so we can
read raw.headers.get('Deprecation'). Defensive fallback to the parsed-
only path for test mocks that don't stub with_raw_response.
- Drop _LEGACY_RETIRING_OAUTH_SCHEMES; same simplification as TS.
- New TestInitiateDeprecationHeaderGate class with the same three pinning
cases. A _set_initiate_response helper routes mock responses through
the with_raw_response surface that initiate() now consumes. Pre-existing
initiate tests migrated to the helper; one assertion that inspected
create.call_args.kwargs now reads with_raw_response.create.call_args.kwargs.
No public API change for either SDK — initiate() returns the same
ConnectionRequest shape and respects the same allowMultiple guard.
ComposioLegacyConnectedAccountsEndpointRetiredError continues to surface
from the 400 retired-path response.
Changeset bumps @composio/core as a patch.
## Summary
- Fixes SDK custom-tool mapping for custom toolkits that reuse the same
child tool slug, e.g. `APP_A.VERSION` and `APP_B.VERSION`.
- Adds toolkit-qualified lookup (`toolkit + original slug`) while
keeping bare original-slug lookup only when it is unambiguous.
- Throws a client-side ambiguity error for manual
`session.execute('VERSION')` when multiple custom toolkit tools share
`VERSION`, instead of falling through to backend execution.
- Keeps `session.customToolkits()` / `session.customTools()` mapped to
the backend-assigned final slugs for duplicate child names.
## Repro / validation
Validated locally with a temporary Tool Router script (not committed)
using two dummy custom toolkits that both define `VERSION`:
- Current `next` SDK fails before high-level use with `Custom tool slug
collision: original slug "VERSION" maps to multiple final slugs...`.
- Tool Router backend accepts the same payload and returns both
`LOCAL_DUMMY_APP_A_REPRO_VERSION` and `LOCAL_DUMMY_APP_B_REPRO_VERSION`.
- This branch builds the SDK map successfully and high-level SDK
execution routes each final slug to its own internal handler:
- `LOCAL_DUMMY_APP_A_REPRO_VERSION` -> `{ app: "A", version: "1.0.0" }`
- `LOCAL_DUMMY_APP_B_REPRO_VERSION` -> `{ app: "B", version: "2.0.0" }`
## Tests
- `pnpm --filter @composio/core typecheck`
- `pnpm --filter @composio/core exec vitest run
test/models/customTool.test.ts test/models/toolRouter.test.ts
test/models/customToolRouting.test.ts`
## Summary
Follow-up to #3358. The `.transform()` added on the `connectedAccounts`
Zod schema made the inferred TypeScript type resolve to the
**post-transform output** (`Record<string, string[]>`), so TS users
still hit `error TS2322: Type 'string' is not assignable to type
'string[]'` even though the runtime accepted strings. The runtime fix
landed; the public TS surface didn't follow.
This PR moves the coercion out of the schema and into
`ToolRouter.create`, mirroring the Python implementation. The public
type is now `Record<string, string | string[]>` and runtime behavior is
unchanged.
## Why CI didn't catch this in #3358
The PR's own test file (`toolRouter.test.ts`) already assigns a `string`
to `ToolRouterCreateSessionConfig`, but `ts/packages/core/tsconfig.json`
has `"include": ["src/**/*.ts"]` — `test/` is not typechecked. So `tsc
--noEmit clean` was technically true while masking the issue. Verified
empirically by dropping a typecheck file into `src/` on `next`:
```
src/__type_check.ts(8,7): error TS2322: Type 'string' is not assignable to type 'string[]'.
```
After this PR, the same file compiles cleanly.
## Changes
- **`ts/packages/core/src/types/toolRouter.types.ts`**: drop
`.transform()` from the `connectedAccounts` schema; `z.infer` now
exposes `Record<string, string | string[]>`.
- **`ts/packages/core/src/models/ToolRouter.ts`**: coerce `string →
[string]` inside `create` before sending to the backend (which expects
arrays). Preserves the original `connected_accounts: undefined` behavior
when the field is omitted (the parent schema's `.partial()` strips the
`default({})`).
- **`ts/packages/core/src/types/toolRouter.types.ts`**: fix stale
`@param {Record<string, string>}` JSDoc on the same field.
- **`.changeset/v31-connected-accounts-array.md`**: drop the inaccurate
*"Bumps `@composio/client` to `0.1.0-alpha.70`"* line — that bump landed
in a separate commit on `next` (`7c1443838`), not in #3358's diff (`git
diff 2bbd791de^1 2bbd791de` shows zero `pnpm-lock.yaml`/`package.json`
changes).
- **`.changeset/fix-connected-accounts-string-type.md`**: new `patch`
changeset for `@composio/core`.
## Test plan
- [x] `pnpm exec tsc --noEmit -p tsconfig.json` clean on
`@composio/core`
- [x] `pnpm exec vitest run` — 883/883 tests pass (incl. 110 in
`toolRouter.test.ts`)
- [x] `pnpm prettier --check` clean on touched files
- [x] Spot-checked: assigning `{ connectedAccounts: { gmail: 'ca_xxx' }
}` to `ToolRouterCreateSessionConfig` now typechecks; assigning the
array form still typechecks.
The Zod `.transform()` added in #3358 made the inferred type of
`ToolRouterCreateSessionConfig['connectedAccounts']` resolve to
`Record<string, string[]>` (the post-transform output), so TypeScript
users still hit `Type 'string' is not assignable to type 'string[]'`
even though the runtime accepted strings. The runtime fix landed; the
public TS surface didn't follow.
Drop the schema transform and coerce inside `ToolRouter.create` instead,
mirroring the Python implementation. The public type is now
`Record<string, string | string[]>` and behavior is unchanged.
While here:
- fix the JSDoc on `connectedAccounts` (was `Record<string, string>`)
- drop the inaccurate `@composio/client 0.1.0-alpha.70` bump line from
the v31-connected-accounts-array changeset (this PR didn't perform
that bump; it landed in a separate commit on `next`)
Same pattern as ToolRouterCreateSessionConfigSchema — own Zod schema
with no defaults and nullable clearing fields. No longer references
create config types that have incompatible defaults.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# 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
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>
Three commands (`connected-accounts.list`, `connections.list`,
`connections.remove`) carried verbatim copies of the same 13-line
`Effect.catchTag('ParseError', ...)` forward-compat guard. Future
changes to the warning message or fallback semantics would have to
touch all three sites in lockstep.
Lift the guard into `src/effects/decode-connected-account-list.ts` so
each call site collapses to a single `yield*`. The helper carries the
WHY (forward-compat with Apollo enum additions) and the safety
justification for the cast in one place.
Surfaced by Cursor Bugbot on commit afb33deef.
Custom tools are set at create time only — PATCH can't change them,
so recomputing the preloaded slugs list after update is dead code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stainless client already validates SessionPatchParams. Replace the
ToolRouterUpdateSessionConfigSchema with a plain interface and coerce
connectedAccounts string→string[] in the transform layer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>