Commit Graph

5111 Commits

Author SHA1 Message Date
Alberto Schiabel 022ecf6176 docs(agents): require zod and effect/Schema at data boundaries (#3969)
This PR:

- codifies the schema-parsing convention in agent guidance: parse
untyped/external data once at the boundary with zod (`z.infer`
downstream) in the docs site and SDK packages, and with `effect/Schema`
in the CLI
- bans hand-rolled structural guards (`'x' in obj` / `typeof` chains),
`as`-casts of parsed JSON, and fake validators such as `z.custom(() =>
true)`
- updates `docs/AGENTS.md`, `ts/AGENTS.md`,
`ts/packages/core/AGENTS.md`, and the existing trust-boundary rule in
`ts/packages/cli/AGENTS.md`

## Context

Follow-up to the review feedback that led to the #3958 split (#3966 /
#3967 / #3968): the docs type-safety work initially shipped a suite of
conditional structural utility helpers instead of zod schemas. This
records the convention so agents and contributors reach for schemas by
default.
2026-07-28 18:44:01 +05:30
Bill a84d05fc99 fix(cli): resolve pusher-js constructor across ESM/CJS interop shapes (#3926)
## Summary

`composio listen` crashes with `TypeError: Object is not a constructor`
immediately after realtime init in every compiled release binary that
bundles pusher-js 8.5.0 (reproduced on darwin-arm64 0.2.33-beta.291–298
and 0.2.32-beta.289, and linux-x64 0.2.33-beta.298). Full diagnosis in
#3918.

**Root cause (corrected after independent verification):** pusher-js
changed the shape of its CJS export between versions — 8.4.x exports the
constructor itself (`module.exports = Pusher`), **8.5.0's Node bundle
exports a namespace object** (`module.exports = { Pusher }`; its webpack
bundle ends with `module.exports.Pusher = __webpack_exports__`), and
8.6.0 exports the constructor again. This is an undocumented upstream
packaging regression — reported in
[pusher/pusher-js#935](https://github.com/pusher/pusher-js/issues/935),
fixed by
[pusher/pusher-js#936](https://github.com/pusher/pusher-js/pull/936),
first released in 8.6.0 (2026-07-23) whose release notes do not mention
it. With 8.5.0 (what the crashing published binaries bundled),
`pusherModule.default` is not callable under **either Node or Bun, from
source or compiled** — the constructor is only reachable via the named
`Pusher` export. The earlier "build-target-dependent Bun interop" theory
in this PR's first revision and in #3918 is not correct: compiled
interop probes built with bun 1.3.10 for darwin-arm64 and bun-linux-x64
embed byte-identical bundles, so the shape varies by pusher-js version,
not by build target. "Works from source" reports came from non-frozen
installs resolving `^8.5.0` to 8.6.0.

Note: `next` now pins pusher-js 8.6.0 (#3955), which incidentally
restores a callable `module.default` in new builds — and this branch has
since been merged up with `next`, so it resolves 8.6.0 too. This PR
makes that robustness deliberate instead of incidental: pusher-js has
changed its export shape twice within two minor versions, and the new
real-module regression test has now passed against both shapes (8.5.0
pre-merge, 8.6.0 post-merge).

Fixes #3918

## Changes

- `ts/packages/cli/src/services/triggers-realtime.ts`:
- `resolvePusherConstructor` probes the interop shapes in precedence
order — `module.default.default` (defensive double-wrap),
`module.default` (8.4.0/8.6.0 class export), the module object itself,
then the named `Pusher` export on either level (`module.default.Pusher`
is the 8.5.0 shape that crashed release binaries) — returning the first
callable candidate via `Arr.findFirst` with
`Predicate.hasProperty`/`Predicate.isFunction` narrowing (no `as`
casts).
- Resolution failure surfaces as the service's typed
`TriggerRealtimeSubscriptionError` instead of an untyped `TypeError`.
- `new Pusher(...)` itself is now wrapped in `Effect.try`, so a
callable-but-non-constructable candidate would also land on the typed
error channel instead of escaping as an untyped defect past the listen
commands' `catchTag`.
- `ts/packages/cli/test/src/services/triggers-realtime.test.ts`:
regression tests pinning every interop shape, precedence (including
`default.Pusher` isolated from the top-level `Pusher` fallback), the
module-object fallback, the no-constructor failure case, and the **real
installed pusher-js module** — so the next packaging drift fails the
unit suite rather than only the compiled binary.
- `ts/packages/cli/CHANGELOG.md`: Patch Changes entry under 0.3.0 (the
CLI is Changesets-ignored, so notes go in the changelog directly),
stating the verified root cause.

## Type of change

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

## How Has This Been Tested?

- `pnpm exec vitest run test/src/services/triggers-realtime.test.ts` — 9
passed.
- `pnpm --filter @composio/cli test` — full package suite green
(includes `validate:skills` and `validate:boundaries`).
- CLI package `pnpm typecheck` (src + test) and `prettier --check`
clean.
- Independent empirical verification with the release toolchain (bun
1.3.10, pinned in `mise.toml`):
- pusher-js **8.5.0**: `module.default` is `{ Pusher }` (not callable)
under Node 24, Bun from source, and `bun build --compile --production`
binaries — the pre-fix `new (module.default)(...)` throws the exact
#3918 `TypeError` in all three environments; the resolver picks the
named `Pusher` export and constructs successfully.
- pusher-js **8.4.0** and **8.6.0**: `module.default` is the
constructor; the resolver picks it directly and constructs successfully.
- darwin-arm64 vs cross-compiled bun-linux-x64 binaries embed
hash-identical pusher bundles — the interop shape does not vary by build
target, only by pusher-js version.
- Compiled-binary before/after on darwin-arm64 and linux-x64 as
described in the original report (crash reproduced from a pre-fix `next`
binary; fixed binary subscribes, holds the full `--timeout` window,
disables the temporary trigger on exit, exit 0); end-to-end Gmail
trigger delivery verified twice.

## Screenshots (if applicable)

N/A — no visible command-surface change (internal wiring fix).

## Remaining follow-up (out of scope for this PR)

- Release CI (`build-cli-binaries.yml`) smoke-tests binaries with only
`--version`/`--help`; a per-target step exercising the bundled
`import('pusher-js')` + constructor resolution would catch the next
interop drift before publishing (requested in #3918).
- The listen commands append network/login remediation hints to every
`TriggerRealtimeSubscriptionError`, which is misleading for packaging
failures, and the probed module shape is not logged on failure; a
`reason` discriminant on the error would make a future variant easier to
diagnose.

## 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 (CLI `CHANGELOG.md`; no
docs-site change — behavior matches what `composio listen` already
documents)
- [x] I added tests (`test/src/services/triggers-realtime.test.ts`)
- [x] I added a changeset if this change affects published packages —
not applicable: `@composio/cli` is excluded from Changesets by design;
changelog entry added instead

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-28 18:36:08 +05:30
jkomyno a59b6480dd fix(cli): read user and CLI config from the cache dir in analytics
The analytics module resolved user_data.json and config.json from a
hardcoded ~/.composio, but the CLI writes both under the cache dir
(COMPOSIO_CACHE_DIR override, ~/.composio otherwise; see
setup-cache-dir.ts and cli-user-config.ts). With the override set,
telemetry silently lost the api key fingerprint, org_id, and the
keychain-mode signal it needs for identity attribution.
2026-07-28 18:11:57 +05:30
jkomyno 3083137ffe fix(cli): stop sending free-form strings in telemetry event properties
PostHog-bound events carried raw error messages, the user's search query,
and the proxied endpoint URL. Free text like this can embed API keys,
customer identifiers, and private URLs, and the client-side scrubber only
catches known token shapes. Record the shape instead: error_name only for
failures, query_length/query_term_count for search, has_endpoint for proxy.

The codact-failure path keeps its error_message: it goes to Composio's own
authenticated endpoint, not to PostHog.
2026-07-28 18:07:17 +05:30
Checo 2a2d77a764 fix(py/triggers): stop() deadlock + subscribe() timeout connection leak (#3890)
Fixes #3858 (both bugs reported there).

Bug 1 — TriggerSubscription.stop() deadlocks when called from a trigger
callback. pysher's Connection.disconnect runs socket.close() then
join(timeout) on the websocket thread, but callbacks execute on that
same thread's dispatch path (_handle_event blocks on the callback's
future.result()), so joining the thread from within a callback that runs
on it deadlocks the whole process and _alive is never cleared, so a main
thread parked in wait_forever() never exits. Fix: clear _alive
synchronously (so wait_forever unblocks immediately) and run
disconnect() on a daemon thread, breaking the reentrancy while still
closing the socket.

Bug 2 — _SubcriptionBuilder.connect() leaks a websocket thread on
timeout. The deadline path raised ComposioSDKTimeoutError without
calling pusher.disconnect(), so pysher's run loop kept redialing every
reconnect_interval for the life of the process — one leaked thread per
timed-out subscribe(), and if a leaked connection later succeeded it
would authenticate and subscribe the channel of an abandoned
TriggerSubscription, silently consuming events. Fix: wrap the wait loop
in try/except so the timeout path tears the connection down before
re-raising.

Added regression tests (TestTriggerSubscriptionStop and
TestSubscriptionBuilderConnectTimeout) that reproduce both failure modes
offline with mocked pusher connections; all 73 tests in
tests/test_triggers.py pass, ruff check + format clean.

---------

Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
Co-authored-by: jkomyno <alberto.schiabel@gmail.com>
2026-07-28 17:59:14 +05:30
Alberto Schiabel 16fa3d963a chore(docs): migrate the docs site to Fumadocs 11 (#3956)
## Summary

- upgrades `fumadocs-openapi` 10 → 11, `fumadocs-mdx` 14 → 15, and
`fumadocs-core` / `fumadocs-ui` 16.4 → 16.13
- migrates the Fumadocs OpenAPI API while preserving the custom schema
renderer
- restores local `$ref` resolution in both the visible API schema UI and
generated LLM markdown
- reduces API-reference client payloads by slicing the bundled OpenAPI
document to each page's reachable operations and components
- restores required badges for GET parameters
- normalizes the OpenAPI `no_auth` sentinel so explicitly public
endpoints render without authentication
- moves to `getOpenAPIPageProps()` / `OpenAPIPageProps` and removes
obsolete CSS overrides

## Correctness fixes

Fumadocs 11 changed the page contract from a server-resolved document id
to a client-side bundled document. That exposed several silent
regressions:

- **Reference resolution:** bundled documents retain local `$ref`s. The
LLM renderer now dereferences them, including alias chains and cycles,
while the custom schema renderer uses Fumadocs' resolver and retains raw
reference identity for stable deduplication.
- **Dereference reuse:** repeated LLM-page requests reuse the
dereferenced copy for each cached bundled document instead of walking
the complete spec per page.
- **Client payload size:** each API page now receives only its selected
operations and transitively reachable components. The slicer falls back
to the complete document for non-component pointers, deep component
pointers, missing operations, or dangling references.
- **Required badges:** `readOnly` cannot distinguish GET inputs from
responses. The renderer now uses the page hook's client name to identify
responses.
- **Recursive rendering:** schema markdown rendering now caps both
structural recursion and nested array type rendering.
- **No-auth normalization:** the undeclared `no_auth` sentinel is
removed without discarding any real security alternatives that may
accompany it.
- **Contract drift:** code consuming `getSchema()` now treats `bundled`
as required, matching the upstream type.

Review follow-up also replaces the new OpenAPI `any` types with typed
Fumadocs page props and a narrow recursive schema model. Historical
Fumadocs 10/11 migration explanations live here in the PR, not as
version-specific source comments; source comments retain only durable
invariants.

## Payload impact

| | before | after |
| --- | --- | --- |
| bundled document | 451 KB | 7.7 KB avg / 30 KB worst |
| served page HTML | 693 KB | 198 KB |
| 10-page sample | 6.55 MB | 2.02 MB (69% smaller) |

## Verification

- `bun install --frozen-lockfile`
- `bun run test` — 89 pass
- `bun run lint:links` — 0 errors
- `bun run lint` — 0 errors (77 existing warnings)
- `bun run types:check`
- `bun run build`
- production server + `bun run test:integration` — 74 pass, including
v3.1/v3 API pages, redirects, search, and LLM endpoints

The production build has one existing Turbopack NFT tracing warning from
`next.config.mjs`; it does not fail the build.

## Production vs preview checks

A live sample comparison between [production](https://docs.composio.dev)
and the [PR
preview](https://docs-git-chore-docs-fumadocs-11.preview.composio.dev)
found no docs regression:

- all 14 representative routes returned 200 with matching titles,
headings, canonical production URLs, and key content
- redirects for `/`, `/api-reference`, `/tools`, and `/docs/welcome`
matched exactly
- the sampled pages exposed the same 1,137 internal-link targets; a
balanced sample of 29 links resolved successfully on both deployments
- sampled v3 and v3.1 OpenAPI pages retained endpoint paths, required
fields, response schemas, and legacy indicators
- the generated OpenAPI LLM page was byte-for-byte identical
- selecting TypeScript in a hydrated browser rendered both inactive-tab
examples and synchronized the language tab groups
- `llms.txt` retained the same 139 unique lines in a different order
- `/docs/quickstart.md` only added an explicit `[#next]` heading anchor

The sampled OpenAPI HTML was roughly 35–42% smaller in the preview,
consistent with document slicing rather than missing rendered content.
2026-07-28 15:26:57 +05:30
Alberto Schiabel 24151cbe14 fix(cli): make source-map enrichment best-effort end to end (#3963)
This PR:
- fixes #3925
- treats source maps as best-effort metadata end to end: a missing,
malformed, invalid, consumer-rejected, or unreadable map — and a valid
map pointing at an original source that was never shipped — all fall
back to the raw stack location instead of aborting error capture
- decodes untrusted source-map JSON with Effect Schema and wraps
`source-map-js` exceptions in a typed `SourceMapResolutionError`
- splits the fallible work into `enrichFromMapFile` and recovers from it
with `orElseSucceed`, so `getSourcesFromMapFile`,
`getErrorRelatedSources`, `maybeMapSourcemaps` and `captureErrors` all
end up with a `never` error channel
- models mapped diagnostic variants with `Data.TaggedEnum`, using its
constructors and `$is` refinements instead of hand-written tagged
records
- removes the unchecked generic JSON cast, and drops a redundant
`node_modules` filter that interpolated an unescaped working directory
into a `RegExp`
- adds regression coverage for invalid JSON, invalid shapes, invalid
mappings, nullable optional keys, missing original sources, unreadable
maps, valid enrichment, and end-to-end diagnostic preservation
- records the user-visible fix in the CLI changelog without adding a
Changeset

## Context

`captureErrors` runs inside the top-level `Effect.catchAll` in
`cli-main.ts`, so any failure while enriching a diagnostic was the CLI's
error reporter crashing while reporting an error. #3925 reported one
trigger (malformed `.map` JSON), but three more paths had the same
effect and are fixed here:

- a **valid map whose original source was never shipped** — the common
shape for a compiled binary — failed on the `PlatformError` from reading
that source
- an **unreadable map**, e.g. a directory sitting at the `.map` path,
failed with `EISDIR`
- a **`.ts` stack frame** skips source maps entirely and reads the file
directly, with the same unguarded read

The Effect Schema decoder also has to be no stricter than
`source-map-js` itself, or valid maps silently lose enrichment. `file`
and `sourceRoot` are now `NullOr`: bundlers that emit no output filename
write `"file": null` rather than omitting the key, and the consumer
accepts it.

Two incidental correctness fixes come along with the rewrite: original
source paths are resolved with the platform path separator instead of a
hardcoded `/`, and the `node_modules` filter no longer mistakes
directories such as `node_modules_local` for real dependency paths. Both
matter on Windows.

## Verification

- `pnpm --filter @composio/cli exec vitest run test/src/effect-errors` —
32 tests passed
- `pnpm --filter @composio/cli exec vitest run` — 973 passed, 1 skipped,
109 files
- every new behavioural test was confirmed to fail against `next`, with
the source tree reverted underneath them
- `pnpm typecheck` — 14 tasks passed across 19 TypeScript packages
- `pnpm --filter @composio/cli build` — passed
- boundary validation (46 registered disables, none added), targeted
ESLint, and Prettier — passed
2026-07-28 15:12:14 +05:30
manjunath bhaskar 8cb290be35 fix(schema_converter): allOf containing a false member must reject every value (#3921)
### Summary

Preserves JSON Schema boolean-schema semantics throughout the Python
schema converter:

- an unsatisfiable `allOf` now propagates through nested combiners
instead of being dropped;
- impossible array items, object properties, and local definition
references remain rejecting;
- composed impossible schemas use a Pydantic type that rejects every
value, including JSON `null`;
- the existing `json_schema_to_pydantic_type(False) is None`
compatibility behavior remains unchanged.

### Problem

The original fix handled only a literal `false` directly inside one
`allOf`. Recursive filtering still interpreted the same `None` value as
both "discard this union branch" and "the whole schema is impossible."
That widened nested `allOf` schemas to `str`, removed impossible
properties/items/definitions, and allowed JSON `null` because Pydantic
treats a bare `None` annotation as `NoneType`.

### Fix

- Add a private unsatisfiable marker with Pydantic core- and JSON-schema
hooks.
- Drop that marker only from `anyOf`/`oneOf`; propagate it through
`allOf`.
- Preserve rejecting semantics for nested object properties, array
items, and local `$defs`/`definitions` references.
- Emit `{"not": {}}` from the rejecting Pydantic type.
- Correct the related empty-`allOf` and all-false-`anyOf` expectations.

### Testing

- `uv run pytest tests/test_schema_converter.py
tests/test_schema_parser.py -q` - 106 passed
- `uv run pytest -m schema -q` - 71 passed
- `make tst` - 928 passed
- `make chk` - Ruff and mypy passed across core, providers, tests, and
scripts

Regression coverage includes direct and nested `allOf`, JSON `null`,
empty arrays with impossible items, optional and required impossible
properties, local definition references, generated JSON Schema, and
shared model consumers.

### Note

#3876 adds coverage for the same function's `anyOf` handling but does
not change this `allOf` propagation path.

---------

Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-28 14:15:49 +05:30
Alberto Schiabel 026c104fe8 fix(py): validate canonical URL targets (#3962)
This PR:
- builds on top of https://github.com/ComposioHQ/composio/pull/3823
- carries forward the Requests canonicalization hardening proposed in
https://github.com/ComposioHQ/composio/pull/3810
- resolves and validates the prepared hostname that Requests will
actually connect to
- blocks backslash parser differentials and rejects malformed ports
before DNS resolution
- adds focused URL-safety coverage while preserving both existing upload
entry points

Validation:
- `uv run --frozen pytest tests/test_url_safety.py tests/test_files.py
tests/test_tool_router_session_files.py -q` (177 passed)
- `make chk`
2026-07-28 14:03:53 +05:30
Devdatta Talele 3fc91bc65d docs: fix GitHub issue links in example READMEs (#3957)
## Summary

Fix the "GitHub Issues" links shared by ten TypeScript example READMEs.
They pointed to `github.com/composio/composio/issues`, which returns 404
because the repository belongs to the `ComposioHQ` organization.

The links now point to `github.com/ComposioHQ/composio/issues`, which
returns 200.

## Changes

- Correct the GitHub organization in ten `ts/examples/*/README.md`
files.
- Leave the root README star-badge target unchanged, per review
feedback.

## Type of change

- [x] Documentation

## How has this been tested?

```console
$ curl -sSIL -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0' \
    https://github.com/composio/composio/issues
404

$ curl -sSIL -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0' \
    https://github.com/ComposioHQ/composio/issues
200

$ git diff --check origin/next...HEAD
# no output
```

## Checklist

- [x] I have read the Code of Conduct and this PR adheres to it.
- [x] I updated documentation as needed.
- [x] Tests are not applicable because this changes Markdown links only.
- [x] A changeset is not needed for documentation-only changes.
2026-07-28 13:33:44 +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
Abhishek Patil b85cea7618 docs: replace masking-disable instructions with Proxy Execute (#3887)
## What changed

- Kept the credential-masking explanation and masked-value examples in
the Connected Accounts guide.
- Removed only the dashboard and API instructions for disabling masking.
- Added guidance to use Proxy execute when calling a provider API
directly.

No changelog files are changed.

## Why

Credential masking remains useful product documentation, but the guide
should not direct users to disable it. Proxy execute covers direct
provider API calls while Composio injects the connected account
credentials server-side.

## Impact

Users still understand how credential masking appears in
connected-account responses, while direct provider API use cases are
routed to Proxy execute.

## Validation

- `bun run types:check`
- `bun run lint:links`
- `bun run test` — 33 tests passed
- `git diff --check`

`bun run lint` still reports 23 existing errors in unrelated
React/Next.js files; this PR only changes MDX content.
2026-07-28 13:21:07 +05:30
Checo f016bcebb9 fix(mimetypes): make guess() case-insensitive on file extensions (#3899)
guess() in composio/utils/mimetypes.py looked up Path(file).suffix
against the lowercase _types table without normalizing case, so
uppercase suffixes like .PNG or .PDF resolved to
application/octet-stream instead of the correct MIME type. Since guess()
feeds the mimetype field on file uploads (core/models/_files.py),
uploads from phone cameras, scanners and Windows — where uppercase
extensions are common — were getting the wrong content type.

The fix is a one-line normalization: Path(file).suffix.lower() before
the table lookup. This aligns guess() with its sibling
get_extension_from_mime_type, which is already case-insensitive, and
with the stdlib mimetypes.guess_type. I updated the existing test that
documented the old case-sensitive behavior to assert the
case-insensitive result instead.

Fixes #3857

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/ComposioHQ/codesmith/composio/pr/3899"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787222508&installation_model_id=428187&pr_number=3899&repository=ComposioHQ%2Fcomposio&return_to=https%3A%2F%2Fgithub.com%2FComposioHQ%2Fcomposio%2Fpull%2F3899&signature=51d4b1022aa69bc47f45a7daf2e704db96c5eb2a2f267141010ca769b502a062"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>/codesmith</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

Signed-off-by: sergioperezcheco <checo520@outlook.com>
Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-28 13:18:55 +05:30
eeshsaxena d94aa32a41 test: add unit tests for the schema_converter boolean-schema filter (#3876)
## Description

`composio.utils.schema_converter._filter_boolean_schemas` pre-processes
JSON
Schemas before handing them to `json-schema-to-pydantic`, which doesn't
support boolean schemas (draft-06+ allows `true`/`false` as schemas). It
replaces `true` with `{}`, drops `false`, and recurses through combiner
arrays (`anyOf`/`allOf`/`oneOf`), `properties`, `items` and `$defs`. It
had no
test coverage.

This adds `python/tests/test_schema_converter.py` covering:

- `True` -> `{}` and `False` -> `None`
- non-schema values passing through unchanged
- lists dropping `False` entries, and an all-`False` list becoming
`None`
- combiner arrays dropping `False` entries, and the whole combiner being
  removed when every entry is `False`
- a `False` property being dropped while siblings are kept
- plain keys being preserved

No source changes.

## Verification

`pytest python/tests/test_schema_converter.py` - 9 passed. `ruff check`
/
`ruff format` clean.

Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-28 13:18:40 +05:30
eeshsaxena 80e09ad0f0 test: add unit tests for pydantic utils (none_to_omit, parse_pydantic_error) (#3784)
## Description

`composio/utils/pydantic.py` had no dedicated test module. This adds
`tests/test_pydantic_utils.py` covering:

- `none_to_omit` converting `None` to the `omit` sentinel while passing
through other values (including falsy `0`/`""`)
- `parse_pydantic_error` listing missing fields, reporting type errors
with the offending parameter, and handling both together

No source changes.

## Verification

`pytest tests/test_pydantic_utils.py` - 5 passed. `ruff check` / `ruff
format --check` clean.

Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
2026-07-28 13:05:51 +05:30
Kshitij Jhunjhunwala a205b68b0f chore(cli): decommission npm and Homebrew install channels (PRDE-1155, PRDE-1156)
curl install.sh is now the only CLI install channel.

- INSTALL.md: drop npm/pnpm/yarn section; Windows guidance is WSL
- install.sh: Windows error no longer advises the dead npm package
- build-cli-binaries.yml: strip npm section from generated release INSTALL.md
- cli.install-health-check.yml: drop stale npm dist-tag/Homebrew comment
- cli.test-installation.yml: delete no-op npm-fallback job and its
  toolchain-versions feeder job; prune summary references
- delete cli.bump-homebrew-tap.yml + bump-homebrew-formula.py (automation
  never fired: GITHUB_TOKEN-published releases suppress release-triggered
  workflows and HOMEBREW_TAP_TOKEN is dead)
- ts/packages/cli/package.json: drop unused publishConfig (package is private)
- cli-release skill + CLI AGENTS.md: remove Homebrew steps from release docs
- toolchain-versions.json: drop now-unused node_install_compat matrix

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:56:09 -07:00
Kshitij Jhunjhunwala 6dfe1ea439 Merge branch 'next' into claude/cli-adoption-funnel-d95660 2026-07-27 15:46:17 -07:00
Kshitij Jhunjhunwala 15007c26ad chore(cli): rename COMPOSIO_POSTHOG_KEY to COMPOSIO_POSTHOG_PROJECT_API_KEY
PostHog's own term for the public write-only phc_* credential is the
project API key; the old name read like a secret. Covers the env var,
the build-time bake define, the baked constant, and the workflow env.
The GitHub repo variable itself must be renamed manually.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:14:28 -07:00
Kshitij Jhunjhunwala f043061952 fix(cli): harden client-side analytics identity and event hygiene
- trust a persisted apollo_user_id without a fingerprint only in the
  keychain security modes with a parseable user config
- rotate to a fresh install_id when a different Apollo user links on
  the same machine, instead of refusing to alias
- bootstrap-stitch missing identities from the resolved api key via
  session/info (24h throttle, 1.5s timeout, all failures swallowed)
- redact secret-shaped tokens from outgoing event properties
- make analytics.json writes atomic (temp file + rename)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:14:28 -07:00
Kshitij Jhunjhunwala b93a05ea82 fix(cli): link the analytics identity only after credentials persist
All human login flows now call linkApolloIdentityForAnalytics after
ctx.login instead of before, and every agent entry point links through
the loginWithAgentIdentity chokepoint (best-effort, never fatal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:13:07 -07:00
Anshu Garg 57557fc54b docs(webhooks): render the webhook payload reference (PLEN-2793) (#3910)
## Summary

Implements the docs half of **PLEN-2793** — renders a typed public
reference for the payloads Composio delivers to customer webhook URLs,
under **API Reference → Webhook Events**.

Pairs with platform PR ComposioHQ/platform#11389, which generates the
spec.

## What's here

- **`docs/public/openapi-webhooks.json`** — the generated OpenAPI
**3.1** spec (top-level `webhooks`: `composio.trigger.message`,
`composio.connected_account.expired`, `composio.trigger.disabled`),
produced from the webhook payload Zod schemas in Apollo (SSOT). Finishes
the earlier untracked WIP: adds the missing `trigger.disabled` event and
reuses shared schemas as `$ref` (`WebhookEventEnvelope`,
`ConnectedAccountDetailed`).
- **`docs/lib/openapi.ts`** — adds the spec as a **second input** to the
v3.1 reference source. Fumadocs (`fumadocs-openapi`) renders the
`webhooks` block natively; operations group under the "Webhook Events"
tag at `api-reference/webhook-events/*`.
- **`webhook-events/index.mdx`** — landing page listing the three
events, cross-linked to the Webhook Subscriptions API and signature
verification.

## Why a separate 3.1 spec

`webhooks` is a 3.1-only top-level field; the main `openapi.json` is
3.0. A separate file keeps the main spec (and its sync pipeline)
untouched and avoids the docs' union-normalisation pass. See the
platform PR for the full rationale.

## Verified locally

- [x] `bun scripts/validate-links.ts` — **0 errors** (the generated
`handle_composio_*` webhook pages resolve; no collision with the
hand-authored `index.mdx`)
- [x] `bun test tests/static/` — 33 pass / 0 fail
- [x] `bun run types:check` — clean

## Notes

- Setup, subscription, and signature-verification docs already exist
(`setting-up-triggers/*`) and are cross-linked, not rewritten.
- Follow-up (optional): auto-sync the webhooks spec from Apollo on prod
deploy (currently committed by hand), mirroring the main-spec
`docs-update-data` pipeline.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: jkomyno <alberto@composio.dev>
2026-07-27 22:17:22 +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
Alberto Schiabel 473e9d20ad fix(cli): make version update checks reliable (#3952)
This PR:

- builds on top of https://github.com/ComposioHQ/composio/pull/3944
- reports `checkStatus: "unknown"` instead of fabricating an up-to-date
result when GitHub refreshes fail
- preserves the last successful release cache and throttles failed
attempts separately
- uses `Command.provideEffectDiscard` to start background checks after
parsing and skips explicit `composio version --check` invocations
- validates installed release versions before semver comparison
- adds deterministic unit concurrency coverage and compiled-binary CLI
E2E coverage

## Verification

- `pnpm --filter @composio/cli test` (`963` passed, `1` skipped)
- `pnpm typecheck` (`14/14` tasks)
- `pnpm build:packages` (`19/19` tasks)
- `pnpm --filter @e2e-tests/cli-version test:e2e:cli` (`9` passed)
- `pnpm build:binary` followed by `pnpm install:binary`
- installed Mach-O binary smoke checks for plain version, live and
cached `version --check`, unknown status, and both leading `--log-level`
forms
- scoped ESLint on all changed TypeScript files
- `git diff --check`

Repository-wide `pnpm lint` remains red on 52 existing errors across
unrelated docs and scripts; the changed TypeScript files pass scoped
ESLint.
2026-07-27 12:49:17 +05:30
Kshitij Jhunjhunwala 17360fb5b3 fix(install): tell users to restart their shell after install (#3943)
## Summary

A user reported that a first-time CLI install "didn't get added to their
PATH". Investigation showed the PATH block *is* written correctly — but
the user never learns about it: `install.sh` delegates shell integration
to `composio install` with stderr captured to a temp file, and the CLI's
TerminalUI suppresses all decoration when stderr isn't a TTY. So the
"Updated ~/.zshrc" / "Restart your shell" guidance is silently
swallowed, the installer ends with "To get started, run: composio
--help", and the user gets `command not found` in their current shell.

This is the minimal fix: state explicitly in the installer's closing
message that composio was added to PATH and takes effect after
restarting the shell.

## Changes

- `install.sh`: closing message now reads "Composio was added to your
PATH — restart your shell (or open a new terminal) for it to take
effect." before the get-started commands. The block prints on every
install path (delegated and fallback).

## Type of change

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

## How Has This Been Tested?

- `bash -n install.sh`
- Stubbed end-to-end run of the post-download section (delegation
success and failure paths) in a sandboxed `$HOME`, verifying the message
prints in both.
- Reproduced the original issue beforehand by running the real installer
(released `@composio/cli@0.2.32`) into a fake `$HOME`: PATH block
written, zero user-facing messaging about it.

## Screenshots (if applicable)

```
Composio was added to your PATH — restart your shell (or open a new terminal) for it to take effect.
Then, to get started, run:

  composio --help
  composio login
```

## 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 — shell-script-only
messaging change; no test harness exists for install.sh output
- [x] I added a changeset if this change affects published packages —
not applicable, install.sh is not a published package and
`@composio/cli` is Changesets-ignored

## Additional context

install.sh is served from `next`, so this reaches all new installs
immediately on merge and works with the already-released binary — no CLI
release required.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-26 20:00:22 -07:00
Kshitij Jhunjhunwala c19255ee40 feat(cli): add composio version --check for machine-readable update status (#3944)
Part of
[PRDE-1143](https://linear.app/composio/issue/PRDE-1143/prompt-users-to-upgrade-composio-cli-when-new-versions-are-available).

## What

`composio version --check` prints one JSON object to stdout and exits 0:

```json
{"current":"0.2.31","latestStable":"0.2.32","updateAvailable":true,"lastChecked":"2026-07-24T19:15:15.413Z"}
```

The startup upgrade banner is TTY-gated, so scripts and agent-driven
(piped) CLI usage never learn a newer stable exists. `--check` gives
them an explicit query: if `updateAvailable`, run `composio upgrade`.

It reuses the update notice's self-throttled release cache
(`~/.composio/update-check.json`, unchanged shape — PRDE-1144 reads it):
fresh <24h means no network call; stale means a synchronous GitHub fetch
with a 10s timeout whose failures degrade to the cached/absent state
instead of erroring. Purely additive — the existing banner behavior is
untouched.

## Verification

Before writing anything, the existing notice pipeline was verified
empirically with a real stale install (stable `0.2.31` binary, isolated
`$HOME`, real pty): background fetch completes even on fast commands,
the banner fires on the next TTY run, piped output stays silent, and
symlinked installs resolve `release-tag.txt` correctly.

- 4 unit tests for `getUpdateStatus`; full CLI suite 955 passed,
typecheck clean.
- Exercised live from source: pure JSON on stdout, empty stderr, exit 0.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-26 19:59:24 -07:00
Alberto Schiabel ceb98789c8 Merge branch 'next' into claude/cli-adoption-funnel-d95660 2026-07-25 07:31:22 +01:00
sdkrelease[bot] 50b587a2b3 docs: update toolkits, API spec, and meta tools data (#3749)
## Summary
Automated sync of backend data into the docs site. Triggered by:
`workflow_dispatch`.

## What changed
- **Toolkit catalog** (`docs/public/data/toolkits.json`,
`toolkits-list.json`) — refreshed list of available toolkits, auth
schemes, and tools from the backend API
- **OpenAPI specs** (`docs/public/openapi.json`,
`docs/public/openapi-v3.json`) — latest v3.1 and v3.0 API specifications
fetched from production
- **API reference pages** (`docs/content/reference/api-reference/`,
`docs/content/reference/v3/api-reference/`) — regenerated index pages
for both API versions
- **Meta tools reference** (`docs/public/data/meta-tools.json`,
`docs/content/toolkits/meta-tools/*.mdx`) — updated meta tool schemas
and reference docs

Co-authored-by: sudodaksh <23355449+sudodaksh@users.noreply.github.com>
2026-07-25 09:00:03 +05:30
Sarah Simionescu d7492833db fix(ci): give the docs data sync its own production key, and alert when it fails (#3947)
## Why

`docs.composio.dev/toolkits` has been serving a stale catalog. Toolkits
added to
production since 2026-07-13 — `saperly`, among others — have no page and
404,
while being live in the API and in `composio search`.

The page is statically generated from the committed
`docs/public/data/toolkits.json`,
which this workflow is supposed to refresh every 5 hours. It has failed
on **every
run since 2026-07-13** (60 of the last 100 runs), starting with the run
immediately
after #3770.

## Root cause

#3770 correctly moved this job onto production and added a guard
rejecting any
non-production base URL. But `secrets.COMPOSIO_API_KEY` is shared with
`ts.test-e2e`, `py.test`, `py.check` and `ts.examples-nightly` — all of
which pin
`COMPOSIO_BASE_URL` to staging. It is a staging credential, and it
returns 401
against `backend.composio.dev`.

Evidence it was live, not expired: `ts.examples-nightly` has been green
against
staging using that same key.

Repointing the shared secret at production is not an option — it would
break the
staging suites. So this job takes its own key.

## Changes

1. **`COMPOSIO_DOCS_API_KEY`** — the docs job reads a dedicated
production secret.
The env var name stays `COMPOSIO_API_KEY` because that is what the
generators read.
2. **Failure alerting** — an `if: failure()` step files one tracking
issue, deduped
   by label so a 5-hourly cron does not open 60 duplicates.

## Why alerting matters here

This workflow fails silently by construction. When it breaks, the
catalog simply
stops being refreshed, the docs site keeps building from the last good
commit, and
nothing 500s. There is no user-visible signal short of someone noticing
a missing
toolkit page. That is how this went unnoticed for 11 days.

## Required before merge

`COMPOSIO_DOCS_API_KEY` must be set with **production** read access and
these
permission areas: Toolkits (read), Tools (read), Triggers (read), and
**Sessions (read and write)** — `generate-meta-tools.ts` creates a
tool-router
session and then reads its tool list, so write-only is not sufficient.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-24 19:35:24 -07:00
daksh 12db4def0d ci(docs): run the failure alert outside the docs working directory
The job sets `defaults.run.working-directory: ./docs`, which the alerting step
inherited. That path only exists once checkout has run, so any failure before it
— the app-token step, for instance — would leave the runner unable to start the
step's shell. Combined with continue-on-error, the alert would be dropped
silently for exactly the earliest and least visible failures.

Pin the step to ${{ github.workspace }}, which the runner creates before any
step executes.

Reported by Cursor Bugbot on #3947.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 19:17:40 -07:00
daksh 0454f423a7 ci(docs): give the docs data sync its own production API key
`secrets.COMPOSIO_API_KEY` is shared with ts.test-e2e, py.test, py.check and
ts.examples-nightly. Those pin COMPOSIO_BASE_URL to staging, and the nightly
examples run is currently green against staging with this key — so the secret is
a live staging credential, not an expired one.

#3770 moved this job to production and added a guard rejecting any non-production
base URL, but no production credential was provisioned. The staging key has
returned 401 against backend.composio.dev on every run since, silently freezing
the committed toolkit catalog.

Repointing the shared secret at production would fix docs and break the staging
suites, and py.check sets no base URL at all — a production key there would aim
type-inference tests at production. So take a separate secret instead.

Requires COMPOSIO_DOCS_API_KEY to be set with production read access.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 18:41:00 -07:00
daksh 7af576a976 ci(docs): file a tracking issue when the docs data sync fails
The scheduled `Docs - Update Data` workflow has failed on every run since
2026-07-13 and nobody noticed for 11 days. The failure mode is silent by
construction: when the sync breaks, `docs/public/data/toolkits.json` simply
stops being refreshed, the docs site keeps building from the last good commit,
and nothing 500s. Toolkits added to production after the last green run just
have no page and 404 — which is how `saperly` ended up missing from
docs.composio.dev/toolkits while being live in the API.

Add a `failure()` step that opens one tracking issue, deduped by label so a
5-hourly cron does not file 60 duplicates. The body names the most likely cause
up front: `COMPOSIO_API_KEY` needs production read access against
backend.composio.dev, and a staging-only key returns 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 18:37:10 -07:00
Kshitij Jhunjhunwala 304e2fda3c feat(cli): add composio version --check for machine-readable update status
The startup upgrade banner is TTY-gated, so scripts and agent-driven (piped)
CLI usage never learn a newer stable exists. `composio version --check`
prints a JSON status to stdout — {current, latestStable, updateAvailable,
lastChecked} — reusing the update notice's self-throttled release cache
(fresh <24h: no network; stale: synchronous fetch, 10s timeout, failures
degrade to the cached/absent state).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:22:22 -07:00
Kshitij Jhunjhunwala 465946ab66 chore(cli): drop the journey-stage contract comment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 11:56:24 -07:00
Kshitij Jhunjhunwala 2b9044cc8d Merge branch 'claude/cli-adoption-funnel-d95660' into claude/posthog-a2-taxonomy 2026-07-24 10:57:08 -07:00
Kshitij Jhunjhunwala f051e7701c chore(cli): drop explanatory comments in dispatch.ts
Strip the review-round comments per feedback; rationale for the non-obvious
ones lives in the fix commits (c22c7b32 post-alias fallback, b9740b8c credential
ordering).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:57:05 -07:00
Kshitij Jhunjhunwala 7c0864a6b0 Merge branch 'claude/cli-adoption-funnel-d95660' into claude/posthog-a2-taxonomy 2026-07-24 10:45:18 -07:00
Kshitij Jhunjhunwala 401140a525 chore(cli): minimize build-script diff for the posthog key bake
Revert the gratuitous restructuring of the three binary build scripts. The
earlier version pulled `'bun'` out of the args array and dropped
`as const satisfies ReadonlyArray<string>` to work around a typecheck error —
but a variadic tuple (`['bun', ...posthogBakeArgs(), ...] as const`) keeps its
first element known, so `Command.make(...args)` still typechecks. Each script's
diff is now just the `posthogBakeArgs` import plus the one inserted spread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:44:56 -07:00
Kshitij Jhunjhunwala 10c99471fc fix(install): tell users to restart their shell after install
The CLI's PATH-setup output is swallowed by the installer's stderr
capture, so first-time users were never told the PATH change only takes
effect in a new shell — they ran `composio --help` in the same terminal,
got "command not found", and concluded the install had failed to set up
PATH. State it explicitly in the closing message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:20:13 -07:00
Kshitij Jhunjhunwala 294e7fb129 Merge branch 'claude/cli-adoption-funnel-d95660' into claude/posthog-a2-taxonomy 2026-07-24 09:35:42 -07:00
Kshitij Jhunjhunwala 28717a9631 feat(cli): attach org_id to telemetry events
Going direct to PostHog dropped the org/project context Apollo's Segment path
enriched server-side. org_id is available in ~/.composio/user_data.json — which
the analytics worker already reads — so post-login events now carry it. project_id
is per-session and not reliably in user config, so it is left for a follow-up
rather than fabricated; the PR description is corrected accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 09:35:15 -07:00
Kshitij Jhunjhunwala 646c2c5a83 Merge branch 'claude/cli-adoption-funnel-d95660' into claude/posthog-a2-taxonomy 2026-07-24 09:26:56 -07:00
Kshitij Jhunjhunwala 909f7234fe Merge remote-tracking branch 'origin/next' into claude/cli-adoption-funnel-d95660 2026-07-24 09:25:57 -07:00
Kshitij Jhunjhunwala c22c7b325b fix(cli): never reuse the aliased install_id as an anonymous identity
Once $create_alias fires, PostHog permanently merges install_id into the Apollo
person. Falling back to that same install_id on logout or on a credential
fingerprint mismatch therefore did NOT anonymise anything — person-level
insights still resolved those events to the previous user. The guard added in
b9740b8c changed the mechanism of the cross-user attribution bug rather than
removing it.

getDistinctId now returns:
  - the bare install_id only while the install has never been aliased, so the
    login alias still merges the pre-login funnel correctly;
  - user_<credential fingerprint> when a credential is active but does not match
    the stored identity — pseudonymous per credential, never merged;
  - anon_<install_id> when signed out after an alias.

Verified live against a throwaway PostHog project: the bare install_id and the
Apollo id both resolve to person 42ff3120, while post-logout events under
anon_<install_id> resolve to a different person entirely. The earlier trace had
shown both ids collapsing to one person; I read that as the merge working and
missed that it also meant the logout fallback attributed to the logged-out user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:33:13 -07:00
Kshitij Jhunjhunwala b9740b8c30 fix(cli): bind analytics identity to the caller's credential; propagate spawn failure
Two bugbot findings on the previous fix.

- The credential fingerprint was read back via getUserApiKey, but every login
  path resolves identity BEFORE persisting the new credential (link at 327/512/720
  vs ctx.login at 342/560/756). So the stored fingerprint described the previous
  key — or none on a fresh install — and could never match, leaving every
  post-login event anonymous. That defeated the identity stitch entirely. The
  credential is now passed in explicitly by the caller (each already holds the
  key it just validated), so identity no longer depends on write ordering.
- spawnWorker swallowed DetachedProcessSpawnError and enqueue returned true
  regardless, so a failed spawn still marked the alias as sent and it was never
  retried — defeating the field added for exactly that purpose. Spawn failure
  now propagates through enqueue.

Added a regression test that reproduces the login ordering (no readable
credential at link time); verified it fails without the fix, producing the
install_id instead of the Apollo id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:05:52 -07:00
Kshitij Jhunjhunwala cbc6cfc64b fix(cli): tag installer-driven agent login; hoist channel; correct changelog
Review follow-ups.

- install.sh ran `login --agent` without an origin marker while its `install`
  and `setup` invocations both set one, so installer-driven agent logins were
  indistinguishable from manual ones — the exact ambiguity this PR removes for
  the other two.
- Hoist the release channel to a module constant; both inputs are constants, so
  recomputing it per event was pointless.
- The changelog overstated the gap: a Homebrew user running `composio install`
  by hand still emits install events, they just carry no installer origin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 16:48:52 -07:00
Kshitij Jhunjhunwala 2208ed6541 fix(cli): tie analytics identity to the active credential; harden logout + alias
Review follow-ups on the direct-to-PostHog telemetry.

- Cross-user attribution (the serious one). getDistinctId trusted the persisted
  apollo_user_id regardless of which credential was actually in use, while
  getUserApiKey gives COMPOSIO_USER_API_KEY precedence over the stored config.
  A script running under user B's env key therefore emitted events attributed
  to whoever last logged in — and a backfilling command under B's key would
  overwrite the stored id, flipping later events the other way. The pre-rewrite
  code derived identity from the key itself and did not have this problem.
  Now a fingerprint of the resolving credential is persisted alongside the id,
  and the id is only used while that fingerprint still matches; otherwise we
  fall back to the anonymous install_id. Unsure attribution is anonymous rather
  than wrong.
- clearApolloIdentityForAnalytics ignored the opt-out and called
  getOrCreateInstallId, so `composio logout` with telemetry disabled created
  ~/.composio/analytics.json and minted an install id — contradicting the
  invariant asserted elsewhere in these tests. It now reads state and does
  nothing when there is nothing to clear.
- An install is aliased at most once, ever. Logging in as a second user on the
  same device no longer emits a second $create_alias for an install PostHog has
  already identified.
- Dropped the now-unused readApolloUserId.

Tests: stale-identity fallback under a differing env key, logout-while-disabled
creating no file, and no re-alias across logout + login as a different user.
The post-login test now establishes identity through the public API rather than
hand-writing state, so it exercises the credential binding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 16:47:52 -07:00
Kshitij Jhunjhunwala 5254e90751 feat(cli): stamp telemetry with journey_stage + cli_channel; tag installer origin
Every CLI event now carries journey_stage (install|setup|login|connect|
execute|other) via an exhaustive event->stage map in events.ts, and
cli_channel (stable|beta) derived from the existing
inferSkillReleaseChannel(APP_VERSION). Both are applied by a single
buildEvent factory so no builder hand-writes them.

install.sh now sets COMPOSIO_CLI_INVOCATION_ORIGIN=installer on its
"$exe" install run (matching the setup run), so script installs are
distinguishable from a user manually running `composio install`.

npm and Homebrew installs never run install.sh, so the install stage
only covers install.sh installs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:49:31 -07:00
Kshitij Jhunjhunwala b9d0e68ecd chore(cli): put changelog note under 0.3.0, not an invented Unreleased heading
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:13:09 -07:00
Kshitij Jhunjhunwala ecbd1a33ce fix(cli): address PR review — identity coverage, logout, alias retry; trim comments
Review feedback (KJ):
- Strip the added explanatory comments across dispatch.ts, constants.ts,
  _shared.ts, build-binary.ts and the workflow; keep only pre-existing ones.
- Shorten the CHANGELOG entry to a single line.
- Revert the .gitignore addition.

Bugbot findings:
- Already-authenticated installs never stitched (events stayed anonymous after
  upgrade, since only `login` persisted the id). Backfill now runs from any
  command that already resolves a session: whoami, init, connected-accounts
  link, plus the existing login paths.
- `composio login --agent` skipped the stitch; the agent branch now resolves
  session info best-effort and links.
- `composio logout` left `apollo_user_id` behind, so later events kept
  attributing to the previous person. Added clearApolloIdentityForAnalytics.
- A failed alias was never retried, because any persisted `apollo_user_id`
  counted as "already aliased" — including when nothing was ever enqueued (no
  project key / telemetry off). Track `aliased_apollo_user_id` separately and
  only record it once the enqueue actually happened.

Tests: alias retry after a skipped enqueue, and logout reverting to install_id.
Full suite 956 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:07:33 -07:00
Kshitij Jhunjhunwala 64dc2488d2 fix(cli): honor disabled telemetry in identity link; make alias idempotent
Review follow-ups on the direct-to-PostHog telemetry:

- linkApolloIdentityForAnalytics now checks the disabled/opt-out state BEFORE
  touching disk, so a disabled (TELEMETRY_DISABLED/CI/etc.) login no longer
  creates ~/.composio/analytics.json with the install_id + Apollo id. Honors
  the disabled-means-no-op contract.
- Emit the $create_alias only on the first association of an install with an
  Apollo user: a repeat login is redundant, and PostHog rejects re-aliasing one
  install into a second account. Gates on the persisted apollo_user_id.

Tests: disabled path now asserts the identity file is absent; alias test
asserts a repeat login does not re-spawn the alias worker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:25:42 -07:00