Curate integration & connection metadata; update OpenAPI specs in place#997
Conversation
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.
Deploying with
|
| 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 |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | 6e56406 | Jun 15 2026, 03:20 AM |
Cloudflare previewTorn down — the PR is closed. |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
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.
4c94350 to
827b06c
Compare
Greptile SummaryThis PR adds user-curated, agent-visible metadata (separate
Confidence Score: 4/5The 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
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile |
| // 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 |
There was a problem hiding this comment.
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.
| // 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!
| 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( |
There was a problem hiding this comment.
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.
| 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( |
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
mainfrom 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
namecolumn distinct fromdescription(name = display, description = agent context); readers fall back to description on pre-split rows so nothing breaks before a backfill.description.sources.list, andconnections.list(slug/name echoes suppressed).info.description, GraphQL schema description, MCP serverinstructions.Edit sheets
integration_renamedtelemetry event is preserved on the new save path.editSheetslot (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, matchingaddSpec.POST /openapi/integrations/:slug/spec.Multi-user tool convergence
tools_revised_at+ connectiontools_synced_atstamps;tools.listlazily 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).0003adds 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). Thetool-descriptionsandopenapi-update-specselfhost e2e scenarios pass against this reconciled branch.Note: two unrelated
connect-handoffselfhost e2e scenarios currently fail on an externalemulators.dev400 (a 128 KiB credential-value cap) — they don't touch this change and fail the same way onmain.Follow-ups (not in this PR)