Follow-up to https://github.com/ComposioHQ/composio/pull/3885, which is
now merged into `next`.
This PR:
- reports pending-login cache read failures as I/O errors instead of
masking them as decode failures (`composio login --poll`)
- restores pairwise polarity assertions for the `semverComparator`
Array.sort compatibility case
- consolidates the duplicated ephemeral HTTP server test helper in
`test/__utils__/http-server.ts`
- makes server-owned connected-account statuses forward-compatible while
preserving the credential-stripping schema allowlist
- redacts raw payload values from connected-account parse warnings and
adds regression coverage for unknown statuses and secret-bearing
malformed payloads
- documents why the client-singleton test re-derives the consumer cache
cwd hash
## Rebase notes
Rebased onto `origin/next` at `ef1ec9148`.
- Dropped the empty `GITHUB_*` compatibility commit; that contract is
already present on `next` through
https://github.com/ComposioHQ/composio/pull/3880.
- Adapted the connected-account decoder rename to the service extracted
upstream in `connected-account-selection.ts`.
## Context
Fix-forward PR for actionable Cursor Bugbot findings on the guardrails
stack (#3877–#3885). The stale decode-error string match and unused
release helper were already resolved upstream, and the `vi.stubEnv(...,
undefined)` concern is not actionable on Vitest 4.1.10.
Fixes#4030
## Problem
`checkForLatestVersionFromNPM` queried the right package but named the
wrong one in the notice it printed:
```ts
// ts/packages/core/src/utils/version.ts:29
const packageName = '@composio/core';
...
// ts/packages/core/src/utils/version.ts:52
`🚀 Upgrade available! Your composio-core version (${currentVersionFromPackageJson}) is behind. …`
```
`composio-core` is the v2 npm package. A v3 user saw a notice naming a
package they had not installed, and the obvious next step — `npm i
composio-core@latest` — installs the legacy package. That is a downgrade
path, not just a wording slip.
Leftover string from the v2 SDK: the surrounding code was migrated to
`@composio/core`, the message was not.
## Fix
One line — interpolate the constant already in scope, as the issue
suggested:
```diff
-`🚀 Upgrade available! Your composio-core version (${currentVersionFromPackageJson}) is behind. …`
+`🚀 Upgrade available! Your ${packageName} version (${currentVersionFromPackageJson}) is behind. …`
```
I used the constant rather than hardcoding `@composio/core` so the
notice and the registry URL now derive from a single source and cannot
drift apart again.
## Verification
Ran the issue's reproduction against the fix, with the registry response
stubbed and `CI`/`DEVELOPMENT` unset:
```text
🚀 Upgrade available! Your @composio/core version (0.14.1) is behind. Latest version: 0.99.0.
registry URL actually queried: https://registry.npmjs.org/@composio/core/latest
```
The name in the message and the package actually queried now match
exactly.
| Check | Result |
|---|---|
| `tsc --noEmit` | exit 0 |
| Full core suite | 1037 passed / 3 failed — identical to unmodified
`next` |
The 3 failures are pre-existing and environmental on the Windows machine
used here: two `EPERM: symlink` (needs Developer Mode) and one POSIX
path-separator assumption in `fileUtils.test.ts`. Confirmed by stashing
and re-running against `next`.
## Notes
- No test added. Interpolating the constant makes this class of drift
structurally impossible, which is a stronger guarantee than asserting
exact log text. There is also a practical cost: #4026's branch
introduces `ts/packages/core/test/utils/version.test.ts`, so adding that
same new file here would create an add/add conflict between two open
PRs. Happy to add coverage as a follow-up once that one lands.
- Related, deliberately left out of scope.
`ts/packages/cli/src/effects/find-composio-core-generated.ts:38` has the
same stale name, and arguably worse:
```ts
const fix = `Install \`composio-core\` with \`${installCmd} composio-core\`, …`;
```
- The code twelve lines below probes `import composio` — the v3 Python
package — so a user following that message runs `uv pip install
composio-core`, installs v2, and the check still fails. I left it alone
because `@composio/cli` is a separate package with its own release
process (it is excluded from changesets), and the two are independent
string literals with no shared function, so there is no single root to
fix. Worth its own issue.
- I checked the other `composio-core` references repo-wide: the
changelog entries and `docs/content/docs/migration-guide/new-sdk.mdx`
correctly cite v2 as history, and the remainder are CLI test-fixture
directory names. Nothing else needed changing.
Fixes#4032
## Problem
The JSDoc on `getToolkitVersionsFromEnv` documented a precedence order
the function does not implement:
```ts
// ts/packages/core/src/utils/sdk.ts:65-69
* 2. Environment variables (COMPOSIO_TOOLKIT_VERSION_<TOOLKIT_NAME>)
* 3. User-provided toolkit version mappings (defaultVersions object)
```
The implementation spreads user-provided values last, so they win:
```ts
const toolkitVersions = {
...toolkitVersionsFromEnv,
...userProvidedToolkitVersions,
};
```
Three independent sources already agreed the code is right and the JSDoc
was the outlier:
1. The inline comment two lines above the spread — "user provided values
act as overrides" (`sdk.ts:91`).
2. `test/core/versions.test.ts:150` (`'should prioritize user-provided
versions over environment variables'`) and `:395` (`'should demonstrate
priority order: user object > env vars > fallback'`).
3. The Python SDK's docstring for the equivalent `get_toolkit_versions`,
which had the order correct.
This misleads anyone debugging a version pin: someone whose
`COMPOSIO_TOOLKIT_VERSION_GITHUB` appears to be ignored reads this and
concludes the env-var handling is broken, when the real cause is their
own `toolkitVersions` object overriding it.
## Fix
Swapped the two lines. Comment-only — no runtime behaviour changes.
```diff
- * 2. Environment variables (COMPOSIO_TOOLKIT_VERSION_<TOOLKIT_NAME>)
- * 3. User-provided toolkit version mappings (defaultVersions object)
+ * 2. User-provided toolkit version mappings (defaultVersions object)
+ * 3. Environment variables (COMPOSIO_TOOLKIT_VERSION_<TOOLKIT_NAME>)
```
I kept this to a pure swap rather than also rewording. The Python
docstring is the parity target, and the two lists are now item-for-item
identical:
1. If `defaultVersions` is a string, use it as global version for all
toolkits
2. User-provided toolkit version mappings (`defaultVersions` object)
3. Environment variables (`COMPOSIO_TOOLKIT_VERSION_<TOOLKIT_NAME>`)
4. Fallback to `'latest'` if no versions are configured
Adding clarifying prose on the TypeScript side alone would have traded
one cross-SDK drift for another.
## Verification
Exercised all four documented items against real behaviour, not just the
one in the report:
```text
env : COMPOSIO_TOOLKIT_VERSION_GITHUB=from_env_20250101_00, _SLACK=slack_from_env
user : { github: "from_user_20260101_00" }
result: { github: 'from_user_20260101_00', slack: 'slack_from_env' }
github winner : from_user_20260101_00 <- user-provided (doc item 2)
slack winner : slack_from_env <- env var, untouched by user (doc item 3)
string form : 20260101_00 <- doc item 1
empty form : latest <- doc item 4
```
The slack case is the one worth noting: items 2 and 3 are not a strict
either/or fallback — the two maps merge per toolkit, and env vars still
apply for toolkits the user's object does not mention. The numbered list
describes that correctly now, since what it orders is precedence.
| Check | Result |
|---|---|
| `tsc --noEmit` | exit 0 |
| `test/core/versions.test.ts` | 32 passed |
I ran the targeted suite rather than the full one: the change is a
comment and cannot affect anything outside this file, and those 32 tests
are exactly the ones asserting this precedence.
## Notes
- No changeset — comments only, nothing in the published output changes.
- No test added: the existing tests at `versions.test.ts:150` and `:395`
already lock this behaviour in. They are what proved the code was
correct and the doc was wrong, so the doc is now covered by them too.
This PR:
- fixes#4024
- supersedes #4025 with target-filesystem detection instead of
unconditional case folding
- uses `is-fs-case-sensitive` on the nearest existing directory so mixed
mounts and case-sensitive filesystem configurations are handled
correctly
- preserves exact matching on case-sensitive filesystems and falls back
to conservative case-insensitive matching when detection fails
- keeps Node-only detection behind `#platform` and mirrors its runtime
dependency in `@composio/slim`, preserving the workerd-safe bundle and
Slim import
- adds focused Node, workerd, and matcher regressions plus an
`@composio/core` patch changeset
- verifies 1,044 core tests, five CLI upload tests, the Slim
build/import smoke, typecheck, build, ATTW, publint, lint, formatting,
frozen install, and changeset validation
**Stacked on #3927** (A1 — direct-to-PostHog + install→Apollo identity).
Targets that branch; will auto-retarget to `next` when #3927 merges.
Review only this diff.
## What
Makes every CLI event self-describe its funnel stage, so the adoption
funnel can be queried without string-matching event names — which is how
it silently broke in the first place.
- **`journey_stage`** on every event: `install | setup | login | connect
| execute | other`. Declared as an exported typed const plus a single
exhaustive map, `as const satisfies Record<CliAnalyticsEventName,
CliJourneyStage>` — so **the compiler refuses to build if a future event
has no stage**. No per-event literals; stamped in one shared builder.
- **`cli_channel`** alongside `cli_version`, reusing the existing
`inferSkillReleaseChannel()` / `CLI_RELEASE_CHANNELS`. Makes "did this
fix reach stable users?" answerable — the question that went unanswered
in the 0.2.30 assetless-release incident.
- **Installer origin fix:** `install.sh` ran `"$exe" install` *without*
`COMPOSIO_CLI_INVOCATION_ORIGIN=installer` (the `setup` call on line 410
had it), so a genuine install was indistinguishable from a user running
`composio install` by hand. Now tagged, with a regression test that
reads `install.sh` and asserts it.
## Stage mapping (reviewed against 30d volume)
`install` = `CLI_INSTALL_*` · `setup` = `CLI_SETUP_*` + `CLI_PLUGIN_*` ·
`login` = `CLI_LOGIN_*` · `connect` = `CLI_LINK_*` · `execute` =
`CLI_EXECUTE_*` + `CLI_TOOL_INVOCATION_*` · `other` = `CLI_COMMAND_*`,
`CLI_SEARCH_*`, `CLI_PROXY_*`, `CLI_RUN_*`, `CLI_LOGOUT_*`
Judgment calls, made with data rather than by default:
- **`proxy` → `other`.** 2,060 of 2,140 proxy users (96%) already
execute; only 80 are proxy-only. Reclassifying would move the activated
cohort ~1%, so `execute` stays precisely "tool execution".
- **`run` → `other`.** `run` children emit their own execute events
carrying `parent_run_id`; staging the parent as `execute` would
double-count.
- **`search` → `other`.** Not a required funnel stage. Candidate for a
"discovery" step later if the flow page wants one.
## Note for whoever builds the flow page
`CLI_TOOL_INVOCATION_*` and `CLI_EXECUTE_FAILED` can both fire for a
single failure, so the `execute` stage **double-counts at the event
level**. Count **distinct users per stage**, not events.
## Known coverage limit
npm and Homebrew installs don't run `install.sh`, so they emit no
`CLI_INSTALL_*`. The `install` stage means "installed via install.sh",
not all installs.
## Verification
`pnpm typecheck` clean · `validate:boundaries` OK (46 disables, **zero
added**) · CLI suite **107 files / 971 passed** (+15 tests) ·
`dispatch.ts` identity/transport untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This PR:
- fixes https://github.com/ComposioHQ/composio/issues/3932
- replaces the private CLI package semver with the non-release
`0.0.0-development` sentinel
- injects the exact GitHub release tag version into every standalone
binary and verifies it during the build
- makes every push to `next` a beta, with explicit semver selection for
intentional minor or major betas and stable releases only through beta
promotion
- resolves skill installation, setup repair, companion repair, and
upgrades from packaged `release-tag.txt` metadata
- adds regression coverage for release selection, compiled version
identity, and packaged-tag asset lookup; all 1,009 CLI tests and the
monorepo typecheck pass
- updates the direct CLI changelog and release guidance; no Changeset is
added because CLI packages are excluded from Changesets
This PR:
- fixes
https://github.com/ComposioHQ/composio/actions/runs/30535159056/job/90850055191
by keeping checksum generation independent of unbuilt CLI runtime
packages
- extracts the shared Effect teardown into a dependency-light module
while preserving existing script imports
- adds an executable release regression that generates a checksum
manifest from a clean fixture
- verifies the fix with `pnpm test:release-workflow`, `pnpm
build:packages`, CLI typecheck, the full CLI test suite, and `pnpm
validate:changesets`
## What
Restores the two docs-prose skills that were dropped from the skill
tree:
- `good-docs-writing` — the house documentation style guide (Voice,
Structure, Terminology, Punctuation, Code examples, Formatting), for
drafting or revising docs prose.
- `good-docs-audit` — the review process that audits a doc/README/prose
block against that guide and reports `file:line` findings with concrete
rewrites. Report-only by default.
## Why
Both existed at `d17a268d` under `.claude/skills/` and were deleted in
#3666 (`chore(agents): normalize repo guidance skills`), which renamed
the rest of the tree into the current taxonomy but did not carry these
two over. Nothing replaced them: there is no docs-voice guidance in the
repo today, and `docs-decisions` covers docs-site mechanics (Fumadocs,
changelogs, ADRs), not prose style.
## Shape of the restore
Rule content is restored unchanged from `d17a268d`. Only the file layout
differs, because the old flat single-file form no longer passes
`validate:agent-skills` (SKILL.md is now capped at 80 lines and a linked
`references/` directory is required):
```
.agents/skills/good-docs-writing/SKILL.md # routing + the rules that matter most
.agents/skills/good-docs-writing/references/style-guide.md
.agents/skills/good-docs-audit/SKILL.md # routing + non-negotiables
.agents/skills/good-docs-audit/references/audit-process.md
```
They live under `.agents/skills` (canonical); `.claude/skills` is a
symlink, so there is no second copy.
Wiring, all three places the taxonomy is asserted:
- `ts/scripts/validate-agent-skills.mjs` — added to the `expectedSkills`
gate
- `ts/scripts/test-skill-routing.mjs` — one routing probe each (the
validator fails on any skill without a probe)
- `AGENTS.md` — added to the Skill Routing list
## Test results
- `node ts/scripts/test-skill-routing.mjs` — passes, 18 probes over 18
skills. Both new probes resolve to a unique top match.
- `node ts/scripts/validate-agent-skills.mjs` — zero findings against
tracked files.
- `pnpm lint-staged` — no staged file matches a configured task
(patterns cover `ts/packages/**` and `python/**`).
- Markdown formatted with the repo Prettier config.
Two notes on how those were run. `pnpm validate:*` could not be used
locally: pnpm's pre-run dependency check tries to purge and reinstall
the workspace and aborts without a TTY, so the scripts were invoked
directly with `node` (same entrypoints the pnpm scripts use).
Separately, `validate-agent-skills` reports stale-guidance hits under
`docs/.eve/dev-runtime/snapshots/**` on my machine; those are untracked,
gitignored local eve snapshots, unrelated to this change, and won't
exist in CI. The commit used `--no-verify` because the pre-commit hook
is just the `pnpm lint-staged` wrapper that hits the same pnpm abort;
lint-staged was run manually instead.
No changeset: guidance and tooling only, no published package touched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (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.14.1
### Patch Changes
- 577a3d4: Replace the backtracking leading/trailing-slash-trim regexes
in the Cloudflare Workers/Edge platform path helpers with index-walk
loops, closing a polynomial-time regular expression denial-of-service
(CodeQL js/polynomial-redos) on long runs of slash characters. Output is
unchanged for every input.
- 503b50a: Refresh runtime dependencies across the TypeScript SDK
packages.
- 2f63fe5: Guard Tool Router session URL uploads against SSRF,
revalidate redirect targets, and enforce a streamed 100 MiB response
limit across TypeScript URL upload paths.
## @composio/experimental@0.2.1
### Patch Changes
- 503b50a: Refresh runtime dependencies across the TypeScript SDK
packages.
## @composio/mastra@0.10.3
### Patch Changes
- 503b50a: Refresh runtime dependencies across the TypeScript SDK
packages.
## @composio/slim@0.14.1
### Patch Changes
- 503b50a: Refresh runtime dependencies across the TypeScript SDK
packages.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
When the bare CLI runs inside a plugin-capable agent host (Claude Code,
Codex) and the Composio plugin is not installed there, print one
throttled stderr tip pointing at 'composio setup'. Deliberately visible
in non-TTY shells: the primary audience is the agent driving the CLI.
Host detection reads CLAUDECODE / CODEX_THREAD_ID / CODEX_SANDBOX under
their raw names (bypassing the COMPOSIO_-prefixing ConfigProvider).
Plugin presence comes from cheap host state reads: Claude Code's
installed_plugins.json and Codex's config.toml plugin section. Throttle
state lives in its own ~/.composio/plugin-hint.json to avoid colliding
with the update-check state file #3961 is rewriting.
Suppressed when: no host marker, plugin installed (or state unreadable),
shown within 24h, or COMPOSIO_CLI_INVOCATION_ORIGIN=run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## What
Closes the PRDE-1138 gap: an unattended agent hitting plain `composio
login` in a pipe dead-ended at browser OAuth. Now the default headless
path either completes unattended or tells the agent exactly what to run
— without ever auto-creating an account for a human.
- **Offer**: non-interactive login instructions gain a second hint —
"For unattended agents: run `composio login --agent`" — alongside the
existing URL + `composio login --poll` flow.
- **Perform (reuse only)**: when `~/.composio/agent.json` already holds
a READY agent identity, plain headless `composio login` completes
unattended by reusing it. New `getStoredReadyAgent` in
`services/agents.ts` is the reuse-only counterpart of
`getOrSignupReadyAgent` — it never falls back to signup.
- **Guardrail** (the design constraint from the ticket): account
creation stays exclusively behind the explicit `composio login --agent`
/ `composio signup` commands. A human piping `composio login` with no
stored agent identity gets the same URL instructions as before and no
account. Asserted at both unit and E2E level (`/api/signup` is never
called on the default path).
- `install.sh --agent` login now sets
`COMPOSIO_CLI_INVOCATION_ORIGIN=installer` for analytics attribution.
- Root `.dockerignore`: host `node_modules` (pnpm store symlinks that
break outside the host) no longer enter the e2e image build context;
without this, e2e images cannot build from a checkout that has run `pnpm
install`.
## Verification
- `pnpm typecheck` green; `@composio/cli` suite green — 1001 passed, 1
skipped; `validate:boundaries` clean.
- New Docker E2E suite `ts/e2e-tests/cli/agent-signin` (10/10) against a
mock `agents.composio.dev` (`COMPOSIO_AGENTS_BASE_URL`): unattended
nothing→authenticated (`login --agent` then `whoami` in one container),
headless-human guardrail (instructions printed, zero signup calls),
stored-identity reuse (login completes + whoami reports `account_type:
agent`). No real accounts are created.
## Notes
- No VHS recordings: the visible change is piped-mode text output, which
VHS interactive recordings don't demonstrate.
- No changeset by design (`@composio/cli` is Changesets-ignored);
human-facing note added to `ts/packages/cli/CHANGELOG.md` under 0.3.0.
getStoredReadyAgent collapsed every whoami failure into Option.none via
Effect.option and fell back to the on-disk identity, so a revoked agent
key (HTTP 401/403) still completed headless login. Auth rejections now
return no agent; only transport-shaped failures keep the offline
fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A root .dockerignore excluding **/node_modules broke Dockerfile.node's
runtime fixtures: suites without a setup phase (claude-agent-sdk) resolve
deps through node_modules copied in via COPY ts/e2e-tests/. BuildKit's
per-Dockerfile ignore keeps the exclusion for Dockerfile.cli, which
installs inside the image and must not see host pnpm symlinks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This PR:
- follows up https://github.com/ComposioHQ/composio/pull/3943, replacing
its hardcoded message with a fix for the cause it worked around
- stops `install.sh` redirecting `composio install`'s stderr to a temp
file — that redirect is what silenced the command, since the CLI gates
every log, note and outro on stderr being a terminal, so the captured
file was always empty and the replay printed nothing
- probes `"$exe" --version` before delegating, so a binary that cannot
run still falls back to inline setup without spilling loader errors —
the reason the redirect existed in the first place
- makes `composio install` fall back to plain `ui.error` lines when
there is nothing to decorate, so container and CI installs that pipe the
session to a build log still get the report; interactive output is
unchanged. `canDecorate` is constant for a run, so each channel binds to
its decorated or plain form once rather than re-testing per message
- drops the unconditional "Composio was added to your PATH" line, which
printed on three paths where nothing was written: shell-not-detected and
unsafe-install-dir (both exit 0 after reporting manual setup), and the
inline fallback's unsupported-shell and unwritable-rc branches
- adds regression tests using the production `makeTerminalUI`
implementation with captured streams, so the real decoration gate is
exercised — the shared test double reports `canDecorate: false` but
prints everything anyway, which is why this went unnoticed through two
prior attempts
## Context
Measured against the released binary, `composio install` writes 0 bytes
when its stderr is not a terminal, and exits 0 without touching any rc
file when `$SHELL` is unset. Both were invisible to the installer:
| case | exit | stderr bytes | PATH block written |
| --- | --- | --- | --- |
| `2>file`, as `install.sh` did | 0 | 0 | yes |
| `$SHELL` unset | 0 | 0 | no |
| stderr on a terminal | 0 | full report | yes |
With the redirect gone, the delegated path prints what the user was
missing — `Updated ~/.zshrc` and `Restart your shell to apply changes` /
`source ~/.zshrc` — and the manual-setup text on the paths where PATH
genuinely was not touched. The closing block no longer needs a PATH
claim of its own, and the fallback keeps printing its own
`refresh_command`.
067c67b695 already tried to surface this
output by capturing it to a temp file and replaying it on success. That
replay has been a no-op for the same reason, so this guidance has never
reached a user since the delegation was introduced.
## How Has This Been Tested?
- new tests fail with `expected '' to contain 'PATH: will add'` (empty
output — the production bug) without the `install.cmd.ts` change, and
pass with it
- full CLI suite: 957 passed, 1 skipped; `pnpm typecheck` 14/14;
`validate:boundaries`, eslint and prettier clean, no new eslint disables
- released binary in a sandboxed `$HOME`: stderr to a file yields 0
bytes; stderr on a pty yields the full report
- CLI from source with stderr captured: 155 bytes of plain guidance
where it previously wrote nothing; decorated output on a terminal
unchanged
- `install.sh` driven through a harness built from the real script under
a pty: a working binary's stderr reaches the terminal, a broken binary's
loader noise stays suppressed and the fallback writes the PATH block and
prints `source ~/.zshrc`. The same harness against `next` shows the
swallow.
No changeset: `@composio/cli` is Changesets-ignored and `install.sh` is
not a published package. The CLI note went into `CHANGELOG.md` directly.
`install.sh` is served from `next`, so the installer half of this
reaches new installs on merge and works with already-released binaries.
The `install.cmd.ts` half only affects captured-stderr installs and
rides the next CLI release.
This PR:
- restores TypeDoc generation after
https://github.com/ComposioHQ/composio/pull/3966 by resolving
`@composio/core`'s TypeScript dev dependency from `catalog:ts6`
- keeps core typechecking on the root TypeScript 7 compiler while
TypeDoc receives the TS6 compiler API
- documents the additional compiler-API consumer in the shared
TypeScript catalog
- commits the regenerated `session-files.mdx` output that could not be
produced while the workflow was broken
- verifies `generate:docs`, `typecheck`, and `build` for
`@composio/core`
Teach non-interactive login to reuse an existing READY agent identity without creating one implicitly. Preserve explicit --no-browser and --no-wait contracts, and branch on TerminalUI.canPrompt so piping stdout alone remains interactive.
Add unattended guidance, installer analytics attribution, and focused unit and Docker E2E coverage. Exclude host node_modules from Docker build contexts.
## Summary
`TerminalUI` classified the terminal with a single aggregate:
```ts
isInteractive = stdinIsTTY && stdoutIsTTY && stderrIsTTY;
```
and reused it for three unrelated decisions: whether Clack may prompt,
whether `ui.output()` emits machine data, and caller behavior in login,
setup, and the update notice. That let output routing change program
behavior:
- Redirecting stdout made `login` look non-interactive even though stdin
and stderr were still usable, so `composio login | tee log` silently
switched to the headless instructions path.
- Redirecting only stdin made `ui.output()` write machine data onto a
human-visible stdout terminal, because the output gate keyed off the
prompting aggregate instead of stdout.
- Capturing stderr changed stdout data emission, even though it is
purely a reporting-mode concern.
This PR separates the three capability axes so each stream owns exactly
one decision:
| Axis | Rule |
| --- | --- |
| Prompting | `canPrompt = stdinIsTTY && stderrIsTTY` — stdout is
irrelevant |
| Machine output | `ui.output(data)` writes only when stdout is **not**
a TTY, or with explicit `{ force: true }` (unchanged) |
| Decoration | `canDecorate = stderrIsTTY` (unchanged) |
The aggregate `isInteractive` capability is removed rather than renamed,
so no caller can reintroduce the conflation.
## Changes
- `services/terminal-ui.ts`: `TerminalCapabilities` now exposes
`canPrompt` instead of `isInteractive`; `output()` gates on
`stdoutIsTTY` alone. The live implementation is now built by an exported
`makeTerminalUI(streams)` factory so tests can exercise real behavior
against injected streams; `TerminalUILive` passes
`process.{stdin,stdout,stderr}` as before.
- `commands/login.cmd.ts`: waiting for browser login is a prompting
decision (`canPrompt`); intro/notes are stderr chrome (`canDecorate`).
Piping stdout no longer reroutes authentication.
- `commands/setup.cmd.ts`: the `--yes` guards for local changes now key
off `canPrompt`.
- `services/update-check.ts`: the update notice is advisory output for a
human watching stderr, not part of any command's result, so it now keys
off `canDecorate` — piping stdout no longer suppresses it, and it still
never touches stdout, so piped data stays clean.
- Telemetry continues to receive the raw per-stream TTY bits
(unchanged).
- `ts/packages/cli/AGENTS.md`: output conventions now document the three
independent stream contracts.
## Tests
- Full 8-combination TTY matrix at the classifier level.
- Behavioral tests with fake sink streams and mocked Clack prompts,
proving behavior rather than returned booleans:
- stdin redirected, stdout+stderr TTY: prompts fall back to defaults
without invoking Clack, `ui.output()` does **not** leak JSON to the
visible terminal, `force` still writes, decoration renders;
- stdout piped, stdin+stderr TTY: the Clack prompt actually runs,
machine data emits, decoration renders;
- stderr captured, stdin+stdout TTY: no prompts, no decoration, stdout
suppressed unless forced;
- everything redirected: machine data still emits.
- Login regression: with stdout piped but stdin/stderr TTYs, `composio
login --no-browser` enters the interactive wait (poll spinner) path and
does not print the headless/agent instructions.
- Update-notice regressions: still shown when stdout is piped or stdin
is redirected; suppressed only when stderr is captured.
## Notes
This is the shared prerequisite for
https://github.com/ComposioHQ/composio/pull/3951 (captured-stderr plain
renderer) and https://github.com/ComposioHQ/composio/pull/3945
(agent-friendly headless login). It intentionally implements neither
feature; both PRs should rebase onto this once merged. No changeset:
`@composio/cli` is Changesets-ignored.
## Verification
- `pnpm typecheck` (repo root)
- `pnpm --filter @composio/cli test` (validate:skills,
validate:boundaries, full vitest suite)
- `eslint` and `prettier --check` on all touched files
VHS recordings skipped: internal capability wiring, no visible
command-surface change.
This PR:
- replaces ESLint with oxlint across the pnpm workspace and the
Bun-based docs site, porting the rules to `.oxlintrc.json` /
`docs/.oxlintrc.json` with behavior parity (restricted-syntax selectors
kept via `oxlint-plugin-eslint`)
- migrates typecheck to TypeScript 7 (`typescript@^7.0.2` catalog) and
keeps a TS6 pin for JS compiler API consumers via a named `ts6` pnpm
catalog (`ts/scripts/validate-examples.ts`, the `@composio/cli` generate
pipeline). The CLI's `typescript` dependency rebinds only the
compiler-API import — its typecheck still runs the root TS7 `tsc`, since
the alias package only ships a `tsc6` bin (documented in
`ts/packages/cli/AGENTS.md`)
- removes the `paths` mappings that pointed `@composio/core` (and, in
`experimental`, `@composio/json-schema-to-zod` plus core-internal
`#`-imports) at sibling `src` directories: under TS7, tsdown's
tsgo-based dts step emitted stray `.d.ts` files next to those
out-of-root sources on every dependent package build. Workspace deps now
resolve through their built dist types, which turbo's `dependsOn:
^build` already guarantees exist — and which the deep-path exports
(`@composio/core/*`) always used anyway
- renames the cli boundary tooling `eslint-boundaries*` →
`lint-boundaries*` and hardens the scanner to reject `oxlint-disable`
spellings so the disable manifest cannot be bypassed
- rewrites inline `eslint-disable` comments to oxlint rule names
(comment-only; no runtime changes), and adds **one new** declared
boundary: `tool-file-uploads.ts` needs `no-restricted-imports` disabled
for `node:crypto` (MD5 for the presigned-upload checksum is not in Web
Crypto), because oxlint also catches dynamic `await import()` where
ESLint did not. The manifest grows 46 → 47 deliberately
- updates CI path filters, `turbo.jsonc` lint inputs, and the docs
typescript-check workflow (renamed to "Docs - Lint and TypeScript
Validation" since it now lints too); drops `eslint`,
`typescript-eslint`, `eslint-config-next`, and `globals` from the
dependency graphs
- ships no changeset: I built `@composio/core` and `@composio/anthropic`
on this branch and on the pre-migration base and diffed the emitted
`dist/**/*.d.mts`. The provider output is byte-identical. Core's output
is **semantically identical but not byte-identical**: TS7 changes quote
style (`"x"` → `'x'`), object-property and union-member ordering in
inferred types, and picks equivalent shorter re-export alias paths for
five signatures (e.g. `OpenAI.Beta.Threads.Runs.Run` →
`OpenAI.Beta.Threads.Run` — verified both names alias the same type in
the shipped typings). Chunk-name hashes shift as a consequence. No type
gains, losses, or shape changes; `attw` and `publint` pass on the TS7
build
## Context
First of a three-PR split of #3958. The type-safety refactors are
stacked on this branch and merge after it:
- docs: https://github.com/ComposioHQ/composio/pull/3967
- `@composio/core`: https://github.com/ComposioHQ/composio/pull/3968
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.
## 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>
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.
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.
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
## 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.
## 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>
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>