Skip to content

Curate integration & connection metadata; update OpenAPI specs in place#997

Merged
RhysSullivan merged 9 commits into
mainfrom
claude/integration-descriptions
Jun 15, 2026
Merged

Curate integration & connection metadata; update OpenAPI specs in place#997
RhysSullivan merged 9 commits into
mainfrom
claude/integration-descriptions

Conversation

@RhysSullivan

Copy link
Copy Markdown
Owner

What

Integrations and connections gain user-curated, agent-visible metadata, a way to refresh an OpenAPI spec without removing and re-adding the integration, and lazy propagation of tool changes to every member's connections.

Squashed onto current main from a longer working branch (cut ~45 PRs back), reconciled against the spec-blob-offload and telemetry work that landed in the meantime.

Sections

Name vs description, separated

  • Integrations get a name column distinct from description (name = display, description = agent context); readers fall back to description on pre-split rows so nothing breaks before a backfill.
  • Connections carry a nullable, agent-visible description.
  • Both reach agents: the execute tool's connection-prefix inventory, sources.list, and connections.list (slug/name echoes suppressed).
  • Add flows prefill the description from the source — OpenAPI info.description, GraphQL schema description, MCP server instructions.

Edit sheets

  • The integration Edit sheet has separate Name and Description fields with a live "what agents see" preview; a per-connection Edit sheet covers description + account label. The integration_renamed telemetry event is preserved on the new save path.
  • Plugin-owned configuration moved into the Edit sheet via a new editSheet slot (OpenAPI: spec update; MCP: auth methods). One Save applies the metadata change, then the staged plugin change.

Update an OpenAPI spec in place

  • openapi.updateSpec(slug, {spec?}) re-resolves the spec (stored source URL, Google Discovery bundle, or a pasted blob), rebuilds the tool catalog, and returns an added/removed tool diff. Connections, credentials, policies, and curated metadata survive. Spec text is stored in the plugin blob store by content hash, matching addSpec.
  • POST /openapi/integrations/:slug/spec.
  • Spec fetches stay unauthenticated by design: a spec can be hosted by a third party, so sending a connection credential there would leak it. Auth-gated specs use the paste path.

Multi-user tool convergence

  • Integration tools_revised_at + connection tools_synced_at stamps; tools.list lazily rebuilds any visible connection whose catalog predates its integration's last config change, under the reader's own binding — so a spec update by one member reaches other members' personal connections on their next read (the owner policy prevents the updater from writing their rows directly).
  • The drizzle runtime schema-ensure now adds nullable columns to existing tables on boot (self-host / D1); cloud migration 0003 adds the four columns for Postgres.

Tests

New SDK tests (including a two-subject shared-database convergence test) and e2e scenarios (tool-descriptions, openapi-update-spec). Full typecheck / lint / format green; affected unit suites green (openapi 152, sdk 334, others). The tool-descriptions and openapi-update-spec selfhost e2e scenarios pass against this reconciled branch.

Note: two unrelated connect-handoff selfhost e2e scenarios currently fail on an external emulators.dev 400 (a 128 KiB credential-value cap) — they don't touch this change and fail the same way on main.

Follow-ups (not in this PR)

  • Serve-stale + background refresh for the convergence path (today's rebuild is synchronous on read; fine for OpenAPI, matters once MCP/GraphQL TTL-drift refresh lands).
  • Paste box alongside re-fetch for URL-sourced specs behind auth.

Integrations and connections gain user-curated, agent-visible metadata,
a way to refresh an OpenAPI spec without re-adding it, and lazy
propagation of tool changes across every member's connections.

Descriptions & name/description split
- integration rows gain a `name` column distinct from `description`
  (name = display, description = agent-visible context); readers fall
  back to description on pre-split rows
- connections carry a nullable, agent-visible `description`
- both surface to agents: the execute tool's connection-prefix
  inventory, sources.list, and connections.list (slug/name echoes
  suppressed)
- add flows prefill the description from the source (OpenAPI
  info.description, GraphQL schema description, MCP server instructions)
- the integration Edit sheet has separate Name and Description fields
  with a live "what agents see" preview; per-connection Edit sheet for
  description + account label; the rename telemetry event is preserved

updateSpec
- openapi.updateSpec(slug, {spec?}) re-resolves the spec (stored source
  URL, Google Discovery bundle, or a pasted blob), rebuilds the tool
  catalog in place, and reports an added/removed tool diff; connections,
  credentials, policies, and curated metadata survive. Spec text is
  stored in the plugin blob store by content hash, matching addSpec.
- POST /openapi/integrations/:slug/spec
- spec fetches stay unauthenticated by design (a spec can be hosted by a
  third party; sending a credential there would leak it) — auth-gated
  specs use the paste path

Plugin config in the Edit sheet
- plugin-owned configuration moved into the integration Edit sheet via a
  new `editSheet` slot; OpenAPI contributes the spec-update controls, MCP
  its auth-method editor. One Save applies the metadata change then the
  staged plugin change.

Multi-user tool convergence
- integration `tools_revised_at` + connection `tools_synced_at` stamps;
  tools.list lazily rebuilds any visible connection whose catalog
  predates its integration's last config change, under the reader's own
  binding — so a spec update by one member reaches other members'
  personal connections on their next read (the owner policy prevents the
  updater from writing their rows directly)
- the drizzle runtime schema-ensure now adds nullable columns to
  existing tables on boot (self-host / D1); cloud migration 0003 adds the
  four columns for Postgres

Add-time description prefill, the metadata edit sheets, updateSpec, and
the multi-user convergence are covered by new SDK tests (including a
two-subject shared-database test) and e2e scenarios.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 13, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 6e56406 Commit Preview URL

Branch Preview URL
Jun 15 2026, 03:19 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 13, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 6e56406 Jun 15 2026, 03:20 AM

@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@pkg-pr-new

pkg-pr-new Bot commented Jun 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@997

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@997

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@997

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@997

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@997

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@997

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@997

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@997

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@997

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@997

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@997

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@997

executor

npm i https://pkg.pr.new/executor@997

commit: 6e56406

apps/local stood up its schema with CREATE TABLE IF NOT EXISTS plus a
hand-maintained list of ALTER TABLE ADD COLUMN statements, so columns
added by a later schema (integration.name, integration.tools_revised_at,
connection.description, connection.tools_synced_at) never reached
databases created by an earlier baseline — the first query against one of
them would 500.

Route local through ensureDrizzleRuntimeSchemaFromTables, the same boot
bring-up the self-host and Cloudflare hosts already use, so every nullable
column the live schema declares is added in place. This removes the
per-column drift between hosts; cloud Postgres keeps getting the same
columns from its generated drizzle migration.

Add a fumadb test proving an existing table gains the schema's new
nullable columns on ensure, and that a second ensure tolerates the
duplicates.
…rios

Two behaviors PR #997 introduced had no watchable coverage:

- metadata-editing (cross-target): an integration's name and description,
  and a connection's description, are editable after the fact and read back
  — the write half that openapi-update-spec's survives-a-refresh assertion
  assumes.
- spec-update-convergence (cloud, real multi-user org): when one member
  refreshes a shared integration's spec, a co-worker's OWN connection
  converges to the new tool catalog on the co-worker's next read, even
  though the editor can never write the co-worker's rows. Proves the lazy
  syncStaleConnectionTools path end-to-end.
Defaults to all-interfaces as before. Setting HOST=127.0.0.1 binds loopback
so the viewer can sit behind a `tailscale serve` proxy without colliding
with tailscaled's own bind on the tailnet IP:port.
Tools are materialized per connection (the tool rows are keyed by
owner/integration/connection); an integration has no tools, it has the
config that derives them. So the integration-side staleness marker is about
the config that changed, not the tools — name it for the cause. The
connection keeps tools_synced_at, which is accurate (connections do have
tools). The comparison now reads tools_synced_at < config_revised_at:
'this connection's tools predate the current config'.

Pre-release column, so the cloud migration + snapshot are edited in place;
the libSQL/D1 hosts pick up the new name via boot schema-ensure.
…scriptions

# Conflicts:
#	apps/cloud/drizzle/meta/0003_snapshot.json
#	apps/cloud/drizzle/meta/_journal.json
#	e2e/scripts/serve.ts
#	e2e/src/surfaces/mcp.ts
#	packages/react/src/pages/integration-detail.tsx
#	vendor/emulate
The merge commit captured bootstrap's transient regen of the self-host
routes against a stale built console-routes, dropping the {-$orgSlug}
prefix (admin/api-keys routes + routeTree.gen). Regenerate them so the
self-host console routes match main's org-slug scope.
Production data confirms `integration.description` was universally the
display NAME (367 rows: avg 13 chars, ~0 real sentences), never a description.

- description is now nullable — an actual, optional description distinct
  from the name.
- ONE cloud migration 0005: add name + config_revised_at (+ connection
  description/tools_synced_at), DROP NOT NULL on description, then backfill
  name = description (367/367 get a real name, verified none empty) and clear
  description to NULL so no row carries a duplicated title.

name stays column-nullable on purpose: SQLite boot-ensure hosts (self-host,
Cloudflare, local) cannot add a NOT NULL column to an existing table, so the
read path keeps the name ?? description ?? slug fallback.
@RhysSullivan RhysSullivan force-pushed the claude/integration-descriptions branch from 4c94350 to 827b06c Compare June 15, 2026 02:39
@RhysSullivan RhysSullivan marked this pull request as ready for review June 15, 2026 03:11
@RhysSullivan RhysSullivan merged commit 2b797e2 into main Jun 15, 2026
14 checks passed
@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds user-curated, agent-visible metadata (separate name/description on integrations, nullable description on connections), an in-place OpenAPI spec refresh endpoint that preserves connections and credentials, and a lazy multi-user tool-convergence mechanism built on config_revised_at/tools_synced_at stamps.

  • Name/description split: cloud migration 0005 backfills existing rows, SQLite hosts gain columns via the new ensureDrizzleRuntimeSchema evolution path, and rowToIntegration falls back to description ?? slug for pre-split rows so nothing breaks before migration.
  • openapi.updateSpec: resolves the stored source URL (or a caller-supplied blob), recompiles the spec outside a transaction, swaps the config and operations in a single transaction, then eagerly refreshes the updater's own connections; other members' connections converge lazily on their next tools.list.
  • Lazy convergence: syncStaleConnectionTools runs at the top of every toolsList call and rebuilds any connection whose tools_synced_at predates its integration's config_revised_at, scoped to the current subject's partition.

Confidence Score: 4/5

The change is safe to merge; the migration is backward-compatible (name backfill from description, then description cleared), the runtime-ensure evolution is idempotent, and the lazy convergence design has both unit and cloud e2e coverage.

The schema migration, lazy-sync logic, and in-place spec update are all well-tested, and the integration/connection description split is backward-compatible via the rowToIntegration fallback. The two findings are a stale comment citing the wrong migration number and a sequential connection-refresh loop that could be slow for large shared integrations — neither is a correctness issue.

packages/plugins/openapi/src/sdk/plugin.ts — the sequential Effect.forEach in updateSpec is worth revisiting as connection counts grow.

Important Files Changed

Filename Overview
packages/core/sdk/src/executor.ts Adds name/description split on integrations, connections.update, syncStaleConnectionTools for lazy convergence, and config_revised_at/tools_synced_at stamping. Logic is sound; the lazy sync design is well-motivated.
packages/plugins/openapi/src/sdk/plugin.ts Adds updateSpec which re-resolves the spec, swaps the stored operations, and refreshes connections in-place; sequential refresh loop could be slow for many connections.
packages/core/sdk/src/core-schema.ts Adds nullable name and config_revised_at columns to integration, and description and tools_synced_at to connection. One inline comment references the wrong migration number (0006 instead of 0005).
apps/cloud/drizzle/0005_integration_descriptions.sql Adds four columns, makes description nullable, and runs the name/description backfill UPDATE; logic matches the code's rowToIntegration fallback strategy.
packages/core/fumadb/src/adapters/drizzle/runtime.ts Adds addNullableColumnsSql and runs it outside the schema-creation transaction with per-statement duplicate-column error swallowing; idempotent and well-tested.
packages/react/src/components/metadata-edit-sheet.tsx New ConnectionEditSheet and IntegrationEditSheet components with agent-visible preview; save flow is correct, plugin section errors are surfaced inline.
packages/core/execution/src/description.ts Enriches the execute-tool inventory with connection/integration descriptions; identity-echo filtering and first-line/140-char truncation logic are correct.
e2e/cloud/spec-update-convergence.test.ts Two-subject convergence scenario against a real in-process spec server; correctly follows isolation rules and cleanup with Effect.ensuring.
packages/plugins/openapi/src/sdk/plugin.test.ts Four new unit tests covering URL-sourced, blob-sourced, multi-user, and not-found error paths for updateSpec; assertions are against values, not booleans.
packages/core/fumadb/src/adapters/drizzle/runtime-ensure.test.ts New Vitest test exercises old-baseline DB evolved to new schema at boot; idempotency verified with a second ensure call.
packages/core/api/src/connections/api.ts Adds PATCH /connections/:owner/:integration/:name with UpdateConnectionPayload; schema correctly uses optional fields for absent=unchanged semantics.
packages/plugins/openapi/src/api/group.ts Adds POST /openapi/integrations/:slug/spec endpoint with correct error surface matching the SDK's updateSpec error union.

Sequence Diagram

sequenceDiagram
    participant Admin
    participant API as POST /openapi/integrations/:slug/spec
    participant SpecServer as Upstream Spec URL
    participant DB as Database
    participant Colleague

    Admin->>API: "updateSpec(slug, {})"
    API->>SpecServer: fetch spec (unauthenticated)
    SpecServer-->>API: new spec text
    API->>DB: transaction: integrations.update(config+config_revised_at)
    API->>DB: transaction: putOperations(new tools)
    API->>DB: refresh admin's own connections (tools_synced_at stamped)
    API-->>Admin: "{addedTools, removedTools, toolCount}"

    Note over Colleague: Later — colleague calls tools.list
    Colleague->>DB: "syncStaleConnectionTools (tools_synced_at < config_revised_at)"
    DB-->>Colleague: stale connection detected
    Colleague->>DB: produceConnectionTools → stamps tools_synced_at
    Colleague->>DB: tools.list (now up-to-date)
    DB-->>Colleague: v2 tool catalog
Loading

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +123 to +125
// Display name. The pre-split field: `description` used to hold the
// name, so cloud backfills `name` from it (migration 0006) and other
// hosts fall back at read time (see rowToIntegration). Nullable because

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The comment references "migration 0006" but the migration that performs this backfill is 0005_integration_descriptions.sql (journal idx 5). A stale number here would mislead anyone trying to trace the column's origin.

Suggested change
// Display name. The pre-split field: `description` used to hold the
// name, so cloud backfills `name` from it (migration 0006) and other
// hosts fall back at read time (see rowToIntegration). Nullable because
// Display name. The pre-split field: `description` used to hold the
// name, so cloud backfills `name` from it (migration 0005) and other
// hosts fall back at read time (see rowToIntegration). Nullable because

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +925 to +936
yield* Effect.forEach(
connections,
(connection) =>
ctx.connections
.refresh({
owner: connection.owner,
integration: connection.integration,
name: connection.name,
})
.pipe(Effect.catchTag("ConnectionNotFoundError", () => Effect.succeed([]))),
{ discard: true },
).pipe(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Effect.forEach without a concurrency option executes sequentially. For an integration with many connections every refresh waits for the previous one to finish, making the updateSpec API call proportionally slower. Adding { concurrency: "unbounded" } alongside { discard: true } runs all refreshes in parallel. The lazy syncStaleConnectionTools path means any failures or missed connections still converge on the next read, so there is no correctness risk.

Suggested change
yield* Effect.forEach(
connections,
(connection) =>
ctx.connections
.refresh({
owner: connection.owner,
integration: connection.integration,
name: connection.name,
})
.pipe(Effect.catchTag("ConnectionNotFoundError", () => Effect.succeed([]))),
{ discard: true },
).pipe(
yield* Effect.forEach(
connections,
(connection) =>
ctx.connections
.refresh({
owner: connection.owner,
integration: connection.integration,
name: connection.name,
})
.pipe(Effect.catchTag("ConnectionNotFoundError", () => Effect.succeed([]))),
{ discard: true, concurrency: "unbounded" },
).pipe(

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