Skip to content

fix(connections): route the categorised tiles through Composio (#599) - #633

Merged
oxoxDev merged 3 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/599-composio-tiles
Aug 11, 2026
Merged

fix(connections): route the categorised tiles through Composio (#599)#633
oxoxDev merged 3 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/599-composio-tiles

Conversation

@CodeGhost21

@CodeGhost21 CodeGhost21 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #599.

Every Connect button in the categorised provider grid failed on a hosted tenant with provider '<id>' is not enabled on this host. This routes the tiles through Composio — the surface directly above them that already works — while keeping the native path as the self-hosted route.

Console-only, as the issue predicted: no manager or backend work, so it does not block on #319.

RCA

The issue's diagnosis is right but stops one level short. The #319 guard that exists to prevent exactly this 400 is present and correct — it just never fires.

  1. The grid renders all 11 tiles from CONNECTION_PROVIDERS and looks each up as states[p.id].
  2. states comes from GET …/connectionsproject_connections (src/server/ops/connections_read.rs:352), which emits one row per record.manifest.connections entry, plus extras only for providers Composio reports already connected. A provisioned tenant declares none, so states is {}.
  3. platformManaged was Object.values(states).some(s => s.credentialSource === "attested"). .some() over an empty array is false, so no tile inherited attested, and noRoute needs "none" where source was undefined.
  4. Every tile fell through to Connect → provider_config() finds no OPENCOMPANY_OAUTH_*400.

The root cause is a keying mismatch: a guard keyed off per-provider rows protecting a grid that renders independently of the manifest.

Two further defects fall out of the same mismatch:

  • Slug mismatch. Reconciled Composio rows are keyed by Composio slug (googlecalendar, googledrive, twitter); tiles by console id (google-calendar, google-drive, x). toolkit_slug() normalizes backend-side but the console did a raw states[p.id] lookup — so a genuinely connected Google Calendar never lit up its tile. This is why only some tiles showed "Connected ✓ via composio".
  • Dead ids. 8 of 11 ids have no well_known() key, so they could not connect even self-hosted with credentials registered.

API Or Behavior Changes

No host changes. Console behaviour:

  • connectRoute is the single decision, used for both what renders and what the click calls, so the button shown and the call made cannot disagree. Precedence: static → native (a registered provider application is a deliberate act by a self-hoster and must not be taken away); else Composio when it can authorize that toolkit; else managed; else unavailable.
  • A tile with no route renders no button. unavailable is the state that was missing — this is what stops the buttons lying.
  • Tiles carry their Composio toolkit slug and match host rows through it, so a connected googlecalendar / googledrive / twitter lights up the right tile. All eleven become connectable rather than three.
  • Disconnect only when there is a native credential to revoke. There is no Composio disconnect route on the host (/composio exposes status, token, authorize, connections and nothing else), so surfacing it on a Composio-only connection would blank a secret that was never set, report success and change nothing. An absent via still gets the button — that is an older host, not a Composio-owned connection.
  • The grid waits for the Composio probe before painting, so tiles do not flash "Not available" and then flip to Connect.

Two things worth reviewer attention

An existing e2e assertion was weakened, deliberately. oauth-onboarding-resume.spec.ts asserted a Connect button was enabled. On that harness — no [[connection]] entries, no composio feature, host.sh runs env -i with no OPENCOMPANY_OAUTH_* — that button could never have completed a handshake. It was asserting the bug. It now asserts the tile explains itself; #300's actual property (bounce-back lands in a working console, param stripped) is untouched and still asserted.

A regression this fix would otherwise have introduced. Fixing slug matching means more tiles correctly show as Composio-connected — which would have surfaced a Disconnect button that silently does nothing. Hence the via-gated Disconnect above.

Tests

  • npm run typecheck
  • npm run typecheck:e2e
  • npm run typecheck:unit
  • npm test — 291 passed (16 new in test/unit/connection-route.test.ts)
  • npm run build
  • npm run e2e96 passed, 8 skipped against a live host

The 8 skips are pre-existing: the LIVE_BRAIN-gated specs (#467) and the Brain spec parked by #302.

New unit coverage pins the route matrix directly, including the two regression guards: every tile routes to Composio on a tenant that declares no connections, and every tile reports unavailable when no route can succeed.

test/unit/connection-route.test.ts also asserts the unavailable case empirically end-to-end — the e2e run confirms that on a host with no route the tile now says so rather than offering a Connect that 400s.

Documentation

Module docs in frontend/src/lib/connections.ts describe the two routes and why connectRoute can answer "neither". The id doc no longer says an id outside well_known() is a dead tile — that is now the Composio path's job — and the two DEAD TILE markers are removed as resolved.

Not in scope

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for native, hosted, managed, and unavailable connection routes.
    • Connections can open hosted authorization flows and track completion automatically.
    • Added clearer status messages and management guidance for unavailable or externally managed connections.
    • Improved provider matching across naming conventions, including Google services and X/Twitter.
  • Bug Fixes

    • Improved connection-state detection when providers are configured through hosted services.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 07295e13-cb81-495b-b35d-bfbef28d14a9

📥 Commits

Reviewing files that changed from the base of the PR and between 9747d5e and a828690.

📒 Files selected for processing (4)
  • frontend/src/lib/connections.ts
  • frontend/src/views/ConnectionsView.tsx
  • frontend/test/unit/connection-route.test.ts
  • src/server/ops/connections_read.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/views/ConnectionsView.tsx
  • frontend/src/lib/connections.ts

📝 Walkthrough

Walkthrough

The connection catalog now includes canonical Composio toolkit slugs and route selection helpers. ConnectionsView probes Composio, starts hosted OAuth, polls connection state, and renders native, Composio, managed, or unavailable states. Tests validate routing and hosted behavior.

Changes

Connection routing

Layer / File(s) Summary
Route contracts and toolkit mappings
frontend/src/lib/connections.ts
Providers now include Composio toolkit slugs. Helpers normalize identifiers, reconcile connection states, validate Composio authorization, and select connection routes.
Hosted connection flow
frontend/src/views/ConnectionsView.tsx
The view probes Composio, opens hosted OAuth, polls for completion, computes routes, and renders route-specific actions.
Route validation
frontend/test/unit/connection-route.test.ts, frontend/test/e2e/oauth-onboarding-resume.spec.ts, src/server/ops/connections_read.rs
Tests validate route precedence, toolkit mappings, authorization eligibility, catalog completeness, canonical slugs, and unavailable Slack rendering.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: oxoxdev

Sequence Diagram(s)

sequenceDiagram
  participant ConnectionsView
  participant ComposioStatus
  participant ComposioAuthorize
  participant CompanyConnections
  ConnectionsView->>ComposioStatus: Probe Composio reachability
  ComposioStatus-->>ConnectionsView: Return reachability and toolkit access
  ConnectionsView->>ComposioAuthorize: Authorize provider toolkit
  ComposioAuthorize-->>ConnectionsView: Return hosted OAuth URL
  ConnectionsView->>CompanyConnections: Poll connection state
  CompanyConnections-->>ConnectionsView: Return updated state
  ConnectionsView->>ConnectionsView: Render connected or unavailable tile
Loading

Poem

I’m a rabbit with routes in a row,
Native or hosted, now tiles know where to go.
Slugs hop neatly, states match just right,
OAuth opens in a fresh tab of light.
If no path exists, the tile tells so—
Thump, thump, ship the flow! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes routing categorised connection tiles through Composio, which is the main change.
Linked Issues check ✅ Passed The changes satisfy issue #599 by adding Composio routing, native fallback, slug mappings, route gating, and connection-state handling.
Out of Scope Changes check ✅ Passed The implementation and tests remain within issue #599 scope, with no unrelated host or backend architecture changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
frontend/test/unit/connection-route.test.ts (1)

49-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case that pins direct-id precedence.

Every fixture holds one row, so the direct states[provider.id] lookup and the normalized scan return the same value. A change that removed the direct lookup would still pass. Add a case with both a manifest row and a Composio-keyed row for the same tile.

♻️ Proposed test
+  it("prefers the row keyed by the tile's own id over a normalized match", () => {
+    const manifest = { provider: "google-calendar", connected: false };
+    const composio = { provider: "googlecalendar", connected: true };
+    expect(
+      connectionStateFor(tile("google-calendar"), {
+        "google-calendar": manifest,
+        googlecalendar: composio,
+      }),
+    ).toBe(manifest);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/test/unit/connection-route.test.ts` around lines 49 - 73, Add a test
in the connectionStateFor suite with both a direct manifest-id state and a
Composio-keyed state for the same tile, using distinct objects. Assert that
connectionStateFor returns the direct states[tile.id] entry, preserving
direct-id precedence over the normalized scan.
frontend/src/views/ConnectionsView.tsx (1)

126-132: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cancel in-flight poll iterations, not only pending timers.

The cleanup clears scheduled setTimeout ids. It does not stop a poll iteration that is already awaiting listComposioConnections. When that request resolves after a company switch, the iteration calls setBusy, can raise a success toast for the previous company, and re-arms a timer in the new pollTimers.current. Add a generation guard so an iteration started under one company stops after the switch.

♻️ Proposed guard
+  // Bumped on every company change; a poll iteration that started under an
+  // older generation stops instead of touching the new company's state.
+  const pollGeneration = useRef(0);
+
   useEffect(() => {
     const timers = pollTimers.current;
     return () => {
+      pollGeneration.current += 1;
       Object.values(timers).forEach((id) => window.clearTimeout(id));
       pollTimers.current = {};
     };
   }, [company]);

Then capture const generation = pollGeneration.current; in connectComposio and return early from poll when generation !== pollGeneration.current.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/views/ConnectionsView.tsx` around lines 126 - 132, Update the
polling flow in connectComposio to use a generation guard: capture the current
pollGeneration when starting an iteration, and have poll return immediately when
that captured generation no longer matches. Increment pollGeneration during the
company-change cleanup in the useEffect so in-flight requests cannot update
state, show toasts, or schedule timers for the previous company.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/lib/connections.ts`:
- Line 70: Update the shared toolkit normalization used by toolkit_slug so the
reconciliation alias "x" resolves to the canonical "twitter" slug, ensuring
manifest providers with "x" match Composio Twitter state; alternatively, replace
the toolkit value with "twitter" consistently while preserving all other
canonical toolkit slugs.

In `@frontend/src/views/ConnectionsView.tsx`:
- Around line 182-184: Handle the null result from window.open in the
authorization flow around startComposioAuthorize: when the popup is blocked,
show a toast instructing the operator to allow popups and return before
displaying the sign-in message or starting the 120-second polling/loading flow;
preserve the existing behavior when a window handle is returned.
- Line 346: Bound the status-loading flow around getComposioStatus so a
non-settling request cannot keep reachSettled false indefinitely. Add an abort
signal or timeout to the underlying OpenCompanyClient/BrowserTransport request,
and ensure timeout handling settles the loading state while preserving normal
successful responses.

---

Nitpick comments:
In `@frontend/src/views/ConnectionsView.tsx`:
- Around line 126-132: Update the polling flow in connectComposio to use a
generation guard: capture the current pollGeneration when starting an iteration,
and have poll return immediately when that captured generation no longer
matches. Increment pollGeneration during the company-change cleanup in the
useEffect so in-flight requests cannot update state, show toasts, or schedule
timers for the previous company.

In `@frontend/test/unit/connection-route.test.ts`:
- Around line 49-73: Add a test in the connectionStateFor suite with both a
direct manifest-id state and a Composio-keyed state for the same tile, using
distinct objects. Assert that connectionStateFor returns the direct
states[tile.id] entry, preserving direct-id precedence over the normalized scan.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9459cad6-1460-43ee-a0f9-6e3218e85b4a

📥 Commits

Reviewing files that changed from the base of the PR and between 409b3fd and 9747d5e.

📒 Files selected for processing (4)
  • frontend/src/lib/connections.ts
  • frontend/src/views/ConnectionsView.tsx
  • frontend/test/e2e/oauth-onboarding-resume.spec.ts
  • frontend/test/unit/connection-route.test.ts

Comment thread frontend/src/lib/connections.ts
Comment thread frontend/src/views/ConnectionsView.tsx
Comment thread frontend/src/views/ConnectionsView.tsx

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 major, 1 minor. The major is sequencing with #637. Checks are green now — the five failures from earlier have cleared.

The RCA goes one level deeper than the issue and lands in the right place. platformManaged was Object.values(states).some(s => s.credentialSource === "attested"), and .some() over an empty array is false — so on a provisioned tenant that declares no manifest.connections, states is {}, the #319 guard never fires, and every tile falls through to a 400. "A guard keyed off per-provider rows protecting a grid that renders independently of the manifest" is the actual defect, and it explains all three symptoms rather than just the reported one.

Stating toolkit per tile rather than deriving it is the right call. The tempting fix is a console-side normalize() mirroring the backend's toolkit_slug(), and that would have been a second implementation of the same rule — the drift trap this codebase avoids elsewhere (grants_cover_server moved rather than reimplemented in #631, one validator for default servers in #554). Eleven explicit values are auditable in a way an algorithm copy is not.


Major — collides with #637 on frontend/src/views/ConnectionsView.tsx

#637 (feat(connections): broker providers through the company's own TinyHumans credential, also yours) edits the same view. It changes whether a tenant has a credential to connect with; this changes how a tile decides which route to take. Those two decisions meet in the same component, and both PRs are rewriting how a tile resolves its route — so this is a semantic overlap, not just a textual one: after both land, a tile's route depends on the manifest, the Composio reconciliation, and now the company credential tier.

Neither PR mentions the other. Worth agreeing an order, and whichever lands second should say what a tile does when the company credential is present but the provider is not in states — that combination does not exist in either PR alone.

Minor — nothing couples the toolkit table to the backend's toolkit_slug()

The doc says the field "mirrors the backend's toolkit_slug()" (src/server/ops/connections_read.rs:239). Explicit is right, but a mirror with no reflection test drifts silently: change toolkit_slug and eleven console tiles quietly authorize against slugs the backend no longer produces, with the failure landing as "provider not enabled" — the exact symptom this PR fixes.

A single test feeding all eleven console ids through the backend's normalizer and asserting equality with the table would pin it, and would fail loudly on the day someone edits either side.

@CodeGhost21

Copy link
Copy Markdown
Collaborator Author

Pushed 2d6bb3e. Taking the review points in turn.

Major — sequencing with #637

Agreed the overlap is semantic, not just textual, and you're right that neither PR mentioned the other.

Proposed order: #637 first, this second. #637 changes what a tenant has; this changes how a tile spends it. Landing the capability before the router means the router is written against the full tier set rather than being amended into it. It is also the cheaper rebase — #637 touches ConnectionsView in the section JSX and one useState, while this rewrites the route decision and the card, so replaying the smaller edit onto the larger one is the wrong way round.

What a tile does when the company credential is set but the provider is not in states — the combination neither PR covers alone:

The tile offers a working Composio Connect.

state is undefined, so the static/attested arms do not fire; hasCredential is true because #637 reports credentialSource: "company" rather than "none"; the tile routes to Composio. That is the #599 outcome reached through #586's credential.

This is by construction rather than by luck, and I have made it explicit rather than leaving it as a happy accident. connectRoute names exactly two tiers — static and attested — because those are the two that describe the local host: one says a native handshake can complete here, the other says none ever can. Every other tier is a statement about which credential the host presents to Composio, which the router reads through the boolean hasCredential. So a new Composio tier is additive here by construction, and company needs no case of its own.

Pinned in connection-route.test.ts so a later change cannot quietly break the composition:

it("routes on whether a Composio credential exists, not on which tier it is", )

It asserts both halves — a company-tier host with no states row gets a Composio route, and a row reporting credentialSource: "company" is not mistaken for the native hatch. That last one matters: only static may mean "a local handshake can complete", and #637's own types.ts doc agrees the company key never routes the native catalog.

One consequence worth naming for whoever rebases: the interaction is confined to ComposioReach.hasCredential. If #637 later makes the company key route the native catalog too, that assumption breaks and connectRoute needs a real case — the doc says so at the call site.

Minor — nothing coupled the table to toolkit_slug()

Fair, and the failure mode you describe is exactly the one this PR fixes, which makes it a bad one to reintroduce. Added console_toolkit_slugs_are_canonical_under_this_normalizer in src/server/ops/connections_read.rs — it reads the real frontend/src/lib/connections.ts and feeds every toolkit through the real toolkit_slug.

Two deliberate choices:

  • It asserts a fixed point (toolkit_slug(t) == t), not toolkit_slug(id) == toolkit. The latter is false for x/twitter by design — that alias is why the table is explicit — so asserting it would force the test to encode the exception and re-create the drift it exists to catch.
  • It asserts the entry count first. Without that, a renamed field or reformatted catalog leaves the loop iterating over an empty list and passing, which is the failure mode where a test reports coverage it does not have.

I verified it fails rather than assuming: temporarily setting one slug to "Google-Calendar" produces

console toolkit "Google-Calendar" is not canonical under toolkit_slug; the console
would authorize a slug this host reconciles rows under a different key

CodeRabbit

All three were valid; all three fixed.

x/twitter reconciliation (connections.ts) — real, and worse than "cannot match". connectionStateFor already matched both spellings, so a lone twitter row was found. The gap is when both rows exist: a manifest provider = "x" gives a disconnected x row, Composio's connected twitter arrives appended, and taking the first match reported the tile disconnected while the account was connected. A connected row now wins; direct-id precedence still decides when nothing is connected. I did not take the suggested fix of aliasing inside the shared normalization — toolkit_slug is the host's reconciliation rule and a console-driven special case does not belong in it; the union belongs where the two rows meet.

Blocked popup — taken as proposed.

Unbounded status request — real, and a regression this PR introduced: the grid did not wait on this call before. Bounded at 5s via Promise.race, mirroring the host's own COMPOSIO_PROBE_TIMEOUT on the connections read path. I did not add abort support to OpenCompanyClient; that is a shared-transport change and a larger blast radius than this PR should carry, worth its own issue.

Nitpick — direct-id precedence unpinned: correct, every fixture held one row. Now covered by the two-row cases above, one of which asserts precedence with nothing connected.

Validation

cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test (1810 passed), typecheck, typecheck:e2e, typecheck:unit, npm test (295 passed, +4), npm run build, npm run e2e (96 passed, 8 skipped — the pre-existing LIVE_BRAIN gates and the Brain spec parked by #302).

@CodeGhost21

Copy link
Copy Markdown
Collaborator Author

CI: the one red check is main's, not this branch's

Rust (openhuman, tinycortex) fails on 2d6bb3e. It is pre-existing on main and unrelated to this PR.

workflows::gate::tests::a_plain_read_and_a_metered_read_do_not_gate ... FAILED
src/workflows/gate.rs:367
[GatedToolCall { node_id: "read", slug: "read_workspace_state",
  reason: "'read_workspace_state' has an external effect and this desk runs supervised" }]

Evidence it is not this branch:

  • The same test fails on main. Run 31487915510 (merge of feat(workspace): hold bytes in the shared tree, GridFS on MongoDB (#553) #611) fails on the identical test — 2844 passed; 1 failed. This PR reports 2845 passed; 1 failed; the +1 is the reflection test added here, which passes in that lane.
  • src/workflows/gate.rs does not exist on this branch. It landed on main after this branch was cut, so it arrives only through the PR merge commit.
  • This job passed on the first commit of this PR (9747d5e, 22m12s). Nothing between then and now touched anything the gate can see: the four files here are frontend/src/lib/connections.ts, frontend/src/views/ConnectionsView.tsx, frontend/test/unit/connection-route.test.ts, and one added test in src/server/ops/connections_read.rs.

What the failure actually is, for whoever owns it: the test asserts that under supervised autonomy a pure read of the agent's own workspace is Reach::Nothing and must not park. Something now classifies read_workspace_state as having an external effect, so it gates. That is the shape of the approvals cluster — #559 ("a Composio read parks, so reading a mailbox costs the operator an approval") is the same complaint one tool over, and #561 covers the parking behaviour.

I have not touched it: it is outside #599 and belongs with the gate/approvals work rather than being smuggled into a connections PR. Happy to pick it up separately if you want it — say the word and I will file or fix it.

Every other check is green, including both Console and Console E2E, and CodeRabbit has moved to approved.

CodeGhost21 and others added 2 commits August 11, 2026 17:59
…umansai#599)

Every Connect button in the categorised provider grid failed on a hosted
tenant with "provider '<id>' is not enabled on this host".

The tinyhumansai#319 guard that exists to prevent exactly this 400 was present and
correct, but keyed off per-provider rows: `platformManaged` reads
`states.some(s => s.credentialSource === "attested")`, and `GET …/connections`
only answers for providers the manifest declares. A provisioned tenant
declares none, so `states` was `{}`, `.some()` over it was false, no tile
inherited `attested`, and all eleven fell through to a Connect that could
only ever 400.

`connectRoute` is now the one place that decides, and it can answer
"neither", so a button that cannot work is never rendered:

- `static` keeps the native hatch — a registered provider application is a
  deliberate act by a self-hoster and must not be taken away.
- else Composio, when it can authorize that toolkit. This is the only route
  on a tenant, and the one that makes a connection a capability the agents
  actually receive.
- else `managed` when a platform identity runs connections here.
- else `unavailable`, which is the state that was missing.

Also fixes two defects falling out of the same keying mismatch:

- Tiles now carry their Composio toolkit slug and match host rows through it,
  so a connected `googlecalendar` / `googledrive` / `twitter` lights up its
  `google-calendar` / `google-drive` / `x` tile instead of still saying
  Connect. The eight ids with no `well_known()` key become connectable at all.
- Disconnect is offered only when there is a native credential to revoke.
  There is no Composio disconnect route on the host, so surfacing it on a
  Composio-only connection would blank a secret that was never set, report
  success and change nothing. An absent `via` still gets the button — that is
  an older host, not a Composio-owned connection.

The e2e assertion that a Connect button is enabled was asserting the bug: the
harness company declares no connections, the binary carries no `composio`
feature and host.sh passes no OPENCOMPANY_OAUTH_*, so nothing could complete.
It now asserts the tile says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…le (tinyhumansai#599)

Review follow-ups on the categorised-tile routing.

**A connected row now wins over a disconnected one for the same tile.** The
host can emit two rows for one provider when its id and Composio slug do not
normalize together: `toolkit_slug("x")` is `x`, not `twitter`, so a manifest
declaring `provider = "x"` yields a disconnected `x` row while Composio's
connected `twitter` state arrives as a separate appended row. Taking the first
match reported that tile disconnected while the account was connected — the
"two surfaces disagreeing" failure tinyhumansai#316 set out to end. This extends the union
the host already does within a row (`native || composio_connected`) across the
alias it cannot see. Direct-id precedence still decides when nothing is
connected.

**A blocked popup is reported.** `window.open` returns null, and the operator
was told to finish in a tab that never opened while the tile spun for the full
two minutes.

**The Composio status probe is bounded.** The shared client has no abort or
timeout and the grid now waits on this call before painting, so a host that
accepts the connection and never answers held the page on skeletons forever.
Losing the race means "no Composio route we can confirm", which is what a null
`reach` already means. 5s, mirroring the host's own COMPOSIO_PROBE_TIMEOUT.

**The console slug table is pinned to the backend normalizer.** The table is
explicit rather than derived (`x` → `twitter` is not derivable), and its doc
called it a mirror of `toolkit_slug` with nothing enforcing that. A new test
reads the real catalog and feeds it through the real normalizer, asserting each
slug is a fixed point — verified to fail on drift, and counting the entries so
a renamed field cannot make it pass vacuously.

Also documents how this composes with tinyhumansai#586's `company` credential tier:
`connectRoute` names only `static` and `attested`, the two tiers that describe
the LOCAL host, and reads every other tier through `hasCredential`. So "the
company credential is set but the provider is not in states" needs no case of
its own — it lands on a working Composio Connect. Pinned by test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CodeGhost21
CodeGhost21 force-pushed the fix/599-composio-tiles branch from 2d6bb3e to cf5e49a Compare August 11, 2026 12:51
…nyhumansai#599)

`window.open` returns null whenever `noopener` or `noreferrer` is set — on a
successful open exactly as on a blocked one. The null check added last round as
"handle a blocked popup" therefore fired every time: manual testing against a
live Composio backend opened the tab and told the operator it had not, with an
error toast on a sign-in that was in fact underway.

Detecting a blocked popup would mean dropping `noopener`, which is what keeps a
third-party page we hand an OAuth URL from reaching back through
`window.opener`. That is the wrong trade for a nicer error message, so the check
goes and the reason it cannot come back is recorded at the call site.
`ComposioSection.signIn` opens the same URL the same way and likewise does not
check — the two now agree.

Caught only by driving a real browser; neither the unit suite nor any CI lane
builds the `composio` feature, so nothing automated exercises this path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CodeGhost21

Copy link
Copy Markdown
Collaborator Author

Manual test report

Rebased onto f6667d1 (clean, no conflicts) and re-ran everything. CI does not exercise this fix at alldefault = ["oauth", "platform-jwt"], so no lane builds the composio feature and nothing automated touches the route this PR adds. Everything below is a real host and a real browser.

It found a bug in the last round's review fix. Details at the end.

Rig

Two hosts, both on the harness company:

Build Composio Manifest
A default absent (inBuild: false) no [[connection]]
B --features composio → mock backend live (credentialSource: static, open mode) [[connection]] for x and google-calendar

Host B's manifest is deliberate: it reproduces the two-row collision. toolkit_slug("x") is x, not twitter, so the host cannot reconcile them and emits both:

{ "provider": "x",       "connected": false, "via": [] }
{ "provider": "twitter", "connected": true,  "via": ["composio"] }

google-calendar reconciles fine (toolkit_sluggooglecalendar), which isolates the alias as the only collision.

Scenario A — a host with no route

The issue's evidence table, reproduced verbatim: all eleven POST …/connections/{id}/start return 400 provider '<id>' is not enabled on this host.

Console All 11 tiles
main CONNECT — every one 400s
this PR Not available on this host.

Scenario B — Composio live

Tile main this PR
gmail connected + DISCONNECT (no-op) connected, "manage in Composio"
google-calendar connected + DISCONNECT (no-op) connected, "manage in Composio"
x not-connected + "Not available on this host" connected, "manage in Composio"
other 8 CONNECT → 400 CONNECT → works

The x row is the one worth pausing on. On main a connected X account renders as "Not available on this host" — the manifest row says credentialSource: "none", so the old code took the noRoute branch while Composio held a live connection.

Per-tile, old route vs the one the console now calls:

gmail            400 → 200 lk_gmail          notion        400 → 200 lk_notion
slack            400 → 200 lk_slack          google-drive  400 → 200 lk_googledrive
google-calendar  400 → 200 lk_googlecalendar dropbox       400 → 200 lk_dropbox
github           400 → 200 lk_github         stripe        400 → 200 lk_stripe
hubspot          400 → 200 lk_hubspot        linkedin      400 → 200 lk_linkedin
x                400 → 200 lk_twitter

The alias fix, isolated

Same host, console swapped between the two commits of this PR:

Tile bf5d6c0 (before review fix) a828690 (now)
x not-connected + CONNECT connected, via Composio
(other 10) identical identical

Only x moves — exactly the row with two host records, and nothing else regressed.

Clicking it

calls: POST …/composio/authorize     (not …/connections/stripe/start)
tab:   https://dashboard.composio.dev/link/lk_stripe
toast: Complete Stripe sign-in in the new tab.

The blocked-popup fix was wrong, and this is how it surfaced

Pushed a828690 to revert it.

window.open returns null whenever noopener or noreferrer is set — on success exactly as on a blocked popup. So the null check added last round fired every time. The first click run showed both at once:

tab:   https://dashboard.composio.dev/link/lk_stripe     ← opened fine
toast: Couldn't open the Stripe sign-in tab.             ← told the operator it hadn't

Detecting a genuinely blocked popup would mean dropping noopener, which is what stops a third-party page we hand an OAuth URL from reaching back through window.opener. That is the wrong trade for a nicer error, so the check is gone and the reason it cannot come back is recorded at the call site. ComposioSection.signIn opens the same URL the same way and never checked — the two now agree.

I took that review suggestion without testing it, which is what put it in. Flagging it plainly rather than quietly reverting.

CI

All nine green on the rebase (cf5e49a), including Rust (openhuman, tinycortex) — the lane that was red from the read_workspace_state merge race, now fixed on main. a828690 is building.

Local on the rebase: cargo test --lib 1908, --features openhuman,tinycortex --tests 2864 passed / 0 failed, fmt, clippy -D warnings, three typechecks, npm test 380, build, npm run e2e 96 passed / 8 pre-existing skips.

Still open for you

#637 has not landed, so this would now merge first — the opposite of the order I proposed. The composition still holds either way (connectRoute reads the tier through hasCredential rather than by name, pinned by test), so this is about rebase cost rather than correctness: #637 would replay its ConnectionsView edits onto this rather than the reverse. Happy either way — your call, and it needs your re-review regardless since the current CHANGES_REQUESTED predates all of the above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

connections: every categorised Connect tile fails on a hosted tenant — the tiles route through native OAuth, not Composio

2 participants