Skip to content

Reach a person's own Gmail, Slack or Linear through Composio - #481

Draft
mxmzb wants to merge 238 commits into
mainfrom
composio-transport
Draft

Reach a person's own Gmail, Slack or Linear through Composio#481
mxmzb wants to merge 238 commits into
mainfrom
composio-transport

Conversation

@mxmzb

@mxmzb mxmzb commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

What this adds

Composio becomes a fourth way a connector is reached, alongside MCP, the Google Drive REST adapter, and Routines.

An app an operator enables is an ordinary mcp_servers row marked provenance = "composio" with url composio://<toolkit>. A new transport dials it, and each call runs in the asking person's own account: the deployment holds one key for the broker, and which person's Gmail or Slack it opens is decided entirely by the user id sent alongside that key — taken from their authenticated session, never from anything a model supplied.

Everything downstream is untouched. The permission check, the rule engine and the audit row work exactly as they do for Drive and Notion.

Read this first: steps 1 and 2 only, deliberately

Nothing in the shipped product installs a Composio client, reads COMPOSIO_API_KEY, enables an app, or creates a connection row. That is intended for this change and it has a visible consequence: the "you have not connected this app" refusal is reachable, while the thing it tells you to go and do does not exist yet.

Concretely, so nobody has to discover it:

  • useComposioClient has no caller under server/src, so on any real deployment the client is null and every Composio path answers empty or refuses.
  • @composio/core is therefore in devDependencies, not dependencies — only the opt-in live test imports it. Step 3 must move that one line back and re-run bun install the moment a file under server/src imports the SDK. Watch for that import; it is the trigger.
  • No COMPOSIO_API_KEY entry was added to .env.example. An entry there is a promise the running product reads the variable, and nothing does. .env.example is byte-identical to main.
  • When step 3 lands, docker-compose.yml, charts/openbot/templates/_helpers.tpl and docs/configuration.md all need the key forwarding, and @composio/client resolves to an alpha (0.1.0-alpha.76, pinned by @composio/core) which will then enter the production image. Exact text for all four is in the review ledger.

The guarantee, and how it is held

The branch's central claim is that three previously independent derivations — which protocol dials a row, whose credential it spends, whose name the audit trail records — plus a fourth nobody had noticed (which app at the broker a row is) are now resolved once, in access.ts, and read as fields everywhere else.

Each of these is gated by a test that fails when the guarantee fails, verified by mutating the source and watching the test die rather than by the suite being green:

  • The id sent to the vendor is the asker's. Send the bot's instead and two tests fail naming both ids.
  • A __version in the model's own arguments cannot stand in for a recorded one, and cannot choose which revision of an action runs.
  • A brokered call is refused when nobody is attributed, and when the asker has not connected the app — including when a colleague has. Drop the actor term from that gate and it becomes "has anybody connected this", handing the broker a stranger's id.
  • A curated catalogue entry cannot be shadowed into a broker by a tampered provenance column, and a brokered row whose id collides with a curated slug is refused rather than dialled at the curated vendor on the wrong credential.
  • A recorded effect may narrow what a bot may do and may never widen it: a reviewed write list outranks a value in a vendor column, and an unrecognised or empty value is a write.

Behaviour changes outside Composio

Two, both small and both corrections:

An MCP server's declared effect is now believed. The MCP specification defines annotations.destructiveHint, servers publish it, and the listing code was dropping it — so a tool its own vendor called destructive classified as a read wherever a hand-written write list omitted it. That hint is now read. readOnlyHint is deliberately not read: the SDK's own types warn against making tool decisions from annotations sent by untrusted servers, and honouring it would let anyone who can add a server by URL declare everything read-only and be believed. Drive is unaffected by construction — it never enters that code path. For Notion the only cell that changes is a destructive-declared tool absent from the reviewed list; nothing can move toward read.

A failed query no longer carries its statement onward. Several call sites copied a database error's message into last_error and into the audit payload, which meant a failed query put its SQL and parameters in both. Deployment faults are now recognised by shape and raised rather than relayed.

Migration

0029_composio — a composio_connections table and three columns on mcp_tools (effect, destructive, version).

It was 0028 until this rebase; main had already taken that number. The snapshot is regenerated rather than renamed, because a rename alone would have left the newest snapshot describing a database missing the two columns main's own 0028 adds — which would make the next schema generation emit commands to drop them. That was reproduced before being fixed, and the drift probe is clean now.

composio_connections.user_id has no foreign key, by design. The row must outlive the person so offboarding can find it; that is the whole reason the table exists rather than reusing the existing credential join. Offboarding now deletes those rows and audits each one — note it shuts the gate this deployment owns and does not revoke at Composio, which the vendorRevoked: false field records honestly.

Review

Two full unbiased review rounds, then a targeted fix cycle.

Round one produced thirty mandatory findings, all fixed. Round two — the byte-identical confirmation round — produced about twenty-five, roughly half of them gaps in round one's own fixes, and the correctness half of those is fixed here. Its most consequential finding is worth naming, because it is the kind only a second round catches: the first round fixed a data-loss bug where an empty listing wiped every tool row, and a later fix made the transport throw instead of returning empty. Both were correct. Together they routed the first round's tests around the guard, so it could be deleted outright with the suite still green. It is genuinely gated now.

Two recurring causes account for most of the rest, and both are worth knowing when reading this diff: a comment asserting something the code or the dependency does not do, with code then written to match the comment; and a test that passes regardless of the behaviour it names. One false sentence in an interface's own documentation produced both a real bug and the two test stubs that could not catch it.

The remaining test-quality tail is deliberately deferred and listed in full in the review ledger — assertions comparing a value to itself, loops that pass vacuously on an empty set, and pre-existing debt in the files this branch happens to touch.

Known and deliberately not fixed here

  • A brokered row's app is resolved from its url, but the vendor's own API has no field for it — the call identifies the app by the action slug alone. Our seam now passes app and action as a named pair so they cannot silently transpose, and states the obligation to refuse a mismatch for whoever writes the real adapter.
  • A run nobody can be identified for records initiator_kind: "person" beside actor: "unattributed". Not a contradiction: one field says what set the run in motion, the other whose account it reached. Recording it as deployment would assert the call went out on the deployment's credential when it never went out at all. Main's DEPLOYMENT_INITIATOR documentation claims that case and nothing sends it there, so the overlap is in the prose; narrowing that sentence is not this branch's call.
  • Delete-and-recreate restores permissions in three other places — MCP servers, skills, and bot-to-bot grants. Real, out of this subject, and written up for follow-up.

Verification

  • bun test server/tests2071 pass, 4 skip, 0 fail across 136 files. The 4 skips are the live vendor suite, which needs an API key. Test count accounts exactly: 1841 at the merge base, +83 from main, +151 from this branch.
  • typecheck, format:check, lint clean.
  • drizzle-kit check clean; the CI drift probe reports no unwritten migration.
  • The migration applies cleanly on top of main's own 0028 on a scratch database, landing both sets of columns.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x

mxmzb and others added 25 commits September 10, 2026 18:49
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…sted

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
The plan's code blocks were written before being run through the formatter, and
the executor was told three times to copy them verbatim — so it correctly chose
verbatim over formatted and reported the deviation rather than silently fixing
it. Whitespace only: six wrap sites across the two files, no content change, and
the suite is the same 24 passing tests either side of this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
… connections

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…he real thing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…places

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…and check the new columns round-trip

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…ture ids

The Composio fixtures at the bottom of this file insert at `gmail`, `notion`,
`bot_helper` and `user_asker`, and none of those ids is a choice:
`seedNotionServer` needs `catalogueEntry("notion")` to resolve to the real
catalogue entry, and `gmail` is the toolkit slug that gets sent to the vendor.
`freshDatabase` deleted them unconditionally to make room, which against a
database somebody is using is a lot to take.

`mcp_user_credentials` references `mcp_servers.id`, so removing a real `notion`
row takes every person's per-user credential row with it and leaves their
encrypted vault rows referenced by nothing — unreachable from any screen and
invisible to `retireConnectionsFor`, which exists to stop exactly that state.
Removing a real Bot takes six tables: its channel memberships, its agent
profile, everyone's preferences for it, its routines and all of their run
history, its component exclusions and its plugin grants. The fixtures then
re-insert byte-identical look-alikes, so nothing on screen would say it
happened.

The `*WasAlreadyConfigured` pattern the older suites use cannot help here: they
only read those rows, so skipping a delete is enough for them, while a fixture
that inserts at an id cannot coexist with a real row there at all — skipping the
delete would just turn the disaster into a primary-key conflict, and
capture-and-restore restores after the cascade has already run. So this refuses
to run instead: a file-level guard looks for the rows the suite intends to own
and throws naming what it found and where to point DATABASE_URL. That is what
makes the deletes safe, and the comment now says so.

Two more things while in here. There was no file-level `afterAll`, so the last
test's fixtures were permanent — one run left behind a `notion` server row and a
`notion-fetch` action nobody configured, which makes that database advertise a
connector nobody set up, and on the next run the rotation suites read the leak
as the deployment's own row and correctly declined to clean it. And the
connection delete matched on the toolkit alone, which is every person's Gmail
connection rather than the fixture's; it now names the two people this file
invents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
`serverWasAlreadyConfigured` and `toolWasAlreadyAdvertised` were captured in a
file-level `beforeAll` and read by the file-level `afterAll`, which deleted the
`google-drive` server row and its `search_files` tool row when they were
`false`. Both were initialised to `false`, so the value that authorised the
delete was also the value they held before anything had looked. A flag meaning
"delete this" must not default to the value that authorises deletion, because a
setup that aborts leaves every flag sitting at its default.

The guard added alongside them is what made that path routine rather than
exceptional. It is a file-level `beforeAll` ahead of the capturing one, and it
throws by design whenever the database already holds the fixture ids this suite
inserts at — a documented outcome rather than a crash. bun runs `afterAll`
anyway, and the capturing hook never got to run, so the teardown concluded it
had created rows it had never looked at and removed them. Verified against a
database holding an operator's own `google-drive` connector and its advertised
`search_files` tool, plus a `gmail` row to trip the guard: the run refused as
designed, `gmail` survived, and the two rows the suite had no business touching
were gone. The cascade off `mcp_servers.id` then takes every person's per-user
credential row and strands their encrypted vault rows behind a dangling
reference, invisible to `retireConnectionsFor` — the exact harm the guard's own
comment cites as its reason for existing.

So the flags now count creations. `suiteCreatedServerRow` and
`suiteCreatedToolRow` are set to `true` only where the capture actually ran and
found the row absent, and the teardown deletes on `true`. `false` covers both
"the deployment already had it" and "nobody ever looked", which is the right
answer for both: neither is this suite's row to remove. An early throw leaves
both at their initialisers and nothing is deleted.

Gating that `afterAll` on `ownsFixtureIds` would have worked today and answers a
different ownership question — which ids the guard cleared, not which rows this
run wrote — and would break silently if either hook moved. The two
describe-scoped `notionWasAlreadyConfigured` teardowns keep the inverted shape;
a file-level `beforeAll` that throws stops describe-scoped hooks from running at
all, so that path cannot reach them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…ial behind it

`reachedAs` was derived from the collapsed `CredentialSource`, and
`CREDENTIAL_BY_AUTH` maps both `none` and `builtin` to `"none"` — so by the
time the answer was chosen, the two kinds were indistinguishable. They do not
share an answer. A credential-free endpoint is public: it touches nobody's
account and answers every person identically, so naming the asker asserts a
per-person attribution that does not exist. The builtin one has no credential
for the opposite reason — the call runs against this deployment's own tables as
the person whose turn it is, so the person is exactly who it reached.

Collapsing both to one credential source erased that distinction at precisely
the point it mattered, and gave `person` to a public endpoint. `REACHED_AS_BY_AUTH`
is keyed on the auth kind instead, which is the field the answer actually depends
on, and being a `Record` over that union it makes the compiler ask the question
for any auth kind added later. That is what the module already claimed and could
not deliver: the existing `Record` only forced a new kind to declare a credential
source, and `reachedAs` fell out of that, so the first `none` entry would have got
no compile error, no failing test, and a wrong audit row.

No catalogue slug uses `none` today, so nothing changes at runtime — Drive and
Notion are `user-oauth`, Routines is `builtin`, and all three still resolve to
the person. The new test constructs a `none` entry directly so the file's stated
exhaustiveness property holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…om an entry

`transportFor` took a catalogue entry and read `entry?.transport ?? "mcp"` off it. That was
complete while every server either had a frozen entry or was somebody's MCP endpoint. A Composio
app is neither — no entry, so the fallback answered MCP and `composio://gmail` would have been
dialled as an HTTP server.

`accessFor` already resolves the kind once for every row shape. Both call sites in the store now
read it off `access` and pass the kind, so this file only does the lookup.

The Drive test asserted the same fallback through `transportFor(null)`. It now composes through
`accessFor`, which is where the absent-entry decision moved; the property it checks is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
Three of the four stated that the catalogue entry decides the protocol, which
is precisely the defect this branch removed: a Composio app has no entry, so
deriving the transport from one dialled `composio://gmail` as an HTTP MCP
server. A reader reconstructing the rule from those comments reconstructs the
bug. `accessFor` decides now, once, for every row shape, and the comments say
so.

The fourth comment was not wrong, only orphaned: it was written about `entry`,
and a later commit inserted the `access` block between it and the return, so it
had come to read as a preamble to something that is never null. Moved back
above the line it describes, unchanged.

The listing path now has a test. It asserts that a Composio row reaches the
Composio client, and it goes red when `refreshTools` resolves the transport
from the entry instead — verified by making that change and watching it fail.
The calling path cannot be covered until a version argument is threaded
through, so it is left for later.

A Drive test's negative assertion became a positive one. Asserting "not the
Drive adapter" for an entry-less server would have been satisfied by any
wrongly resolved transport, including the very defect under repair; it now
asserts the MCP adapter by identity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
The phrase substitution landed without rewrapping the paragraph around it, so a
commit whose whole purpose was making four comments readable left one of them
the widest line in its file by twenty-nine columns, wrapping mid-link in any
ordinary editor. Biome does not reflow prose, so no gate caught it. Whitespace
only: the sentence is unchanged and the paragraph now wraps at the same width as
the two above it.
…has not connected

A brokered app holds one deployment key and relies on Composio to keep people's
accounts apart, so the only thing that separates them is the person id the call
runs under. Two states therefore have to be refused before anything is spent at
the vendor: a run attributed to nobody, and a person who has not connected that
app at all.

Both are refused in `connectionTokenFor`, beside the `user-oauth` refusals they
are modelled on, for the same two reasons — the person gets a sentence naming
the step they can take, and no call is spent finding out. The transport refuses
again as a last line.

`connectionTokenFor` now takes the `ServerAccess` descriptor its callers already
hold, and the per-person branch reads `access.credential` rather than the
catalogue entry's auth kind, so both branches decide from one source instead of
two that can disagree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…e's mailbox

The brokered branch of `connectionTokenFor` refuses two things: a run that is
not attributed to anybody, and an asker who has not connected the app. Both
shipped with nothing in the suite exercising them. A throw planted as the first
statement inside the `if (access.credential === "brokered")` branch left the
whole suite green at 1883 pass — nothing entered that arm, so either refusal
could have been deleted without a single test noticing. Silently deletable is
the worst state for a security refusal to be in.

Two tests now enter it, and each goes red when its own refusal is deleted.
Removing the `if (!actorId)` block makes the unattributed test fail on the wrong
refusal reaching it; removing the `if (!connected)` block makes the not-
connected test fail because the call resolves and the stub records a slug, which
is a call spent at the broker to find out what the row already knew. Both assert
that the vendor stub was never reached, so the refusal is proven to happen
before a call is spent rather than merely somewhere.

The docblock above the function had stopped enumerating the branches it governs
while the code appealed to it as authority. It described a two-case function
keyed on auth kinds the body no longer reads, said nothing about the brokered
arm that carries both refusals, and stated unconditionally that a refresh token
is exchanged per call — telling a reader on the brokered path that something is
exchanged where no refresh token exists at all. It now names all four kinds, and
says which paths have nothing to cache versus the one where not caching is a
decision.

`row.url` went with it. It was read nowhere in the function, and dropping it
fits the parameter back onto one line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
Both call sites in the plugin store still called classifyTool with three
arguments, so every Composio action was classified by the catalogue's
hand-written write list. A Composio app has no such list and no catalogue
entry at all, so every action read as a write regardless of the effect the
vendor recorded when the action was listed.

callTool now selects the recorded effect alongside the input schema and
passes it; listServers already selected every column, so its tool map only
needed the argument. destructive and version are selected in callTool but
not yet read: version is consumed by a later change and destructive by the
confirmation card, and selecting them now keeps that a one-line diff rather
than a re-shaped query.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…l path

The recorded effect was already being selected and passed at both call sites,
but nothing asserted the result at either one. The call path's audit row and the
admin page's listing both depended on it and neither would have said so if the
argument went away.

The admin-page site had no coverage at all, and it needed nothing deferred to
test it: listServers reads the seeded row directly, so a plain assertion on the
returned tool's effect is enough.

Each test now fails only when its own call site regresses. Reverting the
callTool call to three arguments fails the audit-row test alone; reverting the
listServers call fails the listing test alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
The block claimed a server with no catalogue entry behind it was a write
throughout, because nothing reviewed said any tool of theirs only reads. A test
in this same branch asserts the opposite: composio-classify.test.ts takes a
recorded `read` on a null entry and expects a read, as does the store
integration test just added for the brokered call path.

It also described the advertised-and-absent-from-the-write-list case as the only
way to produce a read, and said so as "it is the only one". A second
read-producing case has existed unmentioned since the recorded effect was
introduced: an advertised action whose recorded effect is exactly `read`, which
is a read regardless of the write list and regardless of whether an entry
exists. The doc now names both sources, the order they are consulted in, and
both ways a read can be earned.

The reconciliation helper's justification inherited the same stale premise. Its
conclusion is still right — an entry-less server is not reconciled — but the
reason is no longer that all of its tools are already writes. It is that a
brokered app is classified from the vendor's per-action label rather than from a
hand-written list, so there is no under-inclusion here to find.

Comments only; no behaviour changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
mxmzb and others added 30 commits September 13, 2026 15:30
…l a missing mode from an empty one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
… suite a DOM early enough to see it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…or every app that has one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
… say nothing where there is none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…connection paths now take

`confirmBrokeredConnection` inserted on the column defaults, so every consent
connection made since migration 0030 read `verified: false` beside a null
`verified_at` — byte-identical to a key somebody typed in that nobody ever
checked — and only the rows the backfill touched told the truth. The upsert
moves into `recordBrokeredConnection`, which spells `verified` and `verifiedAt`
together and leaves `connected_at` alone, and the confirm calls it with
`verified: true`: a consent screen is a verification by construction, since the
person authenticated at the vendor and Composio answered that the account is
live. One writer, so the verified and unverified paths cannot drift into two row
shapes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…nd what it asks for

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…d forget a check when its account goes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…t on a key app will actually ask for

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…r its values

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…ail it never does

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…'s reason in the form that asked for it

`verified` stopped meaning a probe the moment the confirm began writing it true off a
consent screen and migration 0030 backfilled every consent row that came before, so the
button gated on it alone was drawn on every connected Gmail and could only fail. It is
gated on the kind as well, and the flag's own doc no longer claims a call was spent.

The sentence for an unverified key asserted a property of the app — that it publishes
nothing checkable — while the store writes `false` on every key connection it makes. It
now says only what is true of all three states behind that word, and the reason waits for
the verification probe to send one.

A key Composio refused said so in the vendor's words, on a banner behind the dialog's own
backdrop. The refusal is rendered in the form too, under the button that was pressed.

And an administrator gets back the line the row replaced: the half saying setup is
complete without them is passed separately and kept by the kind that does not leave.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…the account a row test builds

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
… states the answer left

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…er the app

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…lished

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…ndor rejects it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…es live at the app

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…ck where it is bad

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…lly the kinds so a catalogue gone flat reads as one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…r no longer offers

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…urvive a reload

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…used one still says so after a reload

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…so a key app still connects without one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…ame one the check never would, and refuse a re-check where there is no key

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…eck never attempts

An unverified row against an app that has a probe used to have one producer: a connect whose
probe failed and whose withdrawal then failed too. A refused re-check now writes the same pair,
over an account it deliberately leaves alone — the account predates the press and is the
person's own, so taking it away to report a bad key would destroy the thing they came to repair.

So the row said the deployment could not withdraw the account, which is true on one path and
false on the other. It now states the refusal and the standing account without naming a cause,
and the read's own argument records both producers and that which of the two is not recoverable
from the pair.

The comment above the last branch said it stood for every page load. The connections read
carries the probe now, so what is left there is the absence of any answer at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…ody tried

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…hat leave the account standing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…othing spent cannot withhold the only press that could spend one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
…ame both fields the read now carries

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2mm9gARmD13tnasEztb4x
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.

1 participant