History

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

* Curate integration & connection metadata; update OpenAPI specs in place 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. * Evolve apps/local SQLite columns on boot like the other hosts 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. * Cover metadata editing and multi-user spec convergence with e2e scenarios 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. * Let the e2e viewer bind a specific host (HOST env) 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. * Rename integration.tools_revised_at → config_revised_at 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. * Regenerate the self-host route tree the merge left stale 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. * Split integration name from description; backfill cloud data clean 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.

Rhys Sullivan committed 5 months ago

Commit 2b797e21 parent aa148a275 files · +4183 −550Browse files

Showing the changes to packages/plugins/openapi/src/api (2 of 75 files). Show all

Unified · Split

2 files
  1. packages/plugins/openapi/src/api/group.ts +35 −0
  2. packages/plugins/openapi/src/api/handlers.ts +17 −0
packages/plugins/openapi/src/api/group.ts+35−0
@@ -4,6 +4,7 @@ import { ApiKeyAuthMethod, ApiKeyAuthTemplate } from "@executor-js/sdk/http-auth · 3 unmodified lines
44 import {
55 InternalError,
66 IntegrationAlreadyExistsError,
7+ IntegrationNotFoundError,
78 IntegrationSlug,
89 } from "@executor-js/sdk/shared";
910
@@ -26,6 +27,16 @@ const DomainErrors = [ · 16 unmodified lines
2627 IntegrationAlreadyExistsError,
2728 ] as const;
2829
30+const IntegrationNotFound = IntegrationNotFoundError.annotate({ httpApiStatus: 404 });
31+
32+const UpdateSpecErrors = [
33+ InternalError,
34+ OpenApiParseError,
35+ OpenApiExtractionError,
36+ OpenApiOAuthError,
37+ IntegrationNotFound,
38+] as const;
39+
2940 const SlugParams = {
3041 slug: Schema.String,
3142 };
@@ -64,6 +75,7 @@ const AuthenticationResponse = Schema.Union([OAuthTemplatePayload, ApiKeyAuthMet · 32 unmodified lines
6475 const AddSpecPayload = Schema.Struct({
6576 spec: OpenApiSpecInputPayload,
6677 slug: Schema.String,
78+ name: Schema.optional(Schema.String),
6779 description: Schema.optional(Schema.String),
6880 baseUrl: Schema.optional(Schema.String),
6981 headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
@@ -92,6 +104,21 @@ const AddSpecResponse = Schema.Struct({ · 22 unmodified lines
92104 toolCount: Schema.Number,
93105 });
94106
107+// Update the spec in place. Body optional fields only: an empty payload means
108+// "re-fetch from the stored source URL".
109+const UpdateSpecPayload = Schema.Struct({
110+ spec: Schema.optional(OpenApiSpecInputPayload),
111+});
112+
113+const UpdateSpecResponse = Schema.Struct({
114+ slug: IntegrationSlug,
115+ toolCount: Schema.Number,
116+ /** Tool names new in this spec version (same diff for every connection). */
117+ addedTools: Schema.Array(Schema.String),
118+ /** Tool names the new spec no longer defines. */
119+ removedTools: Schema.Array(Schema.String),
120+});
121+
95122 const IntegrationView = Schema.Struct({
96123 slug: IntegrationSlug,
97124 description: Schema.String,
@@ -161,6 +188,14 @@ export const OpenApiGroup = HttpApiGroup.make("openapi") · 63 unmodified lines
161188 error: DomainErrors,
162189 }),
163190 )
191+ .add(
192+ HttpApiEndpoint.post("updateSpec", "/openapi/integrations/:slug/spec", {
193+ params: SlugParams,
194+ payload: UpdateSpecPayload,
195+ success: UpdateSpecResponse,
196+ error: UpdateSpecErrors,
197+ }),
198+ )
164199 .add(
165200 HttpApiEndpoint.delete("removeSpec", "/openapi/integrations/:slug", {
166201 params: SlugParams,
packages/plugins/openapi/src/api/handlers.ts+17−0
@@ -54,6 +54,7 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 53 unmodified lines
5454 return yield* ext.addSpec({
5555 spec: payload.spec,
5656 slug: payload.slug,
57+ name: payload.name,
5758 description: payload.description,
5859 baseUrl: payload.baseUrl,
5960 headers: payload.headers ? { ...payload.headers } : undefined,
@@ -114,6 +115,22 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 54 unmodified lines
114115 }),
115116 ),
116117 )
118+ .handle("updateSpec", ({ params, payload }) =>
119+ capture(
120+ Effect.gen(function* () {
121+ const ext = yield* OpenApiExtensionService;
122+ const result = yield* ext.updateSpec(params.slug, {
123+ ...(payload.spec !== undefined ? { spec: payload.spec } : {}),
124+ });
125+ return {
126+ slug: result.slug,
127+ toolCount: result.toolCount,
128+ addedTools: [...result.addedTools],
129+ removedTools: [...result.removedTools],
130+ };
131+ }),
132+ ),
133+ )
117134 .handle("removeSpec", ({ params }) =>
118135 capture(
119136 Effect.gen(function* () {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t