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
4import {4import {
5 InternalError,5 InternalError,
6 IntegrationAlreadyExistsError,6 IntegrationAlreadyExistsError,
7 IntegrationNotFoundError,
7 IntegrationSlug,8 IntegrationSlug,
8} from "@executor-js/sdk/shared";9} from "@executor-js/sdk/shared";
9 10
@@ -26,6 +27,16 @@ const DomainErrors = [ · 16 unmodified lines
26 IntegrationAlreadyExistsError,27 IntegrationAlreadyExistsError,
27] as const;28] as const;
28 29
30const IntegrationNotFound = IntegrationNotFoundError.annotate({ httpApiStatus: 404 });
31
32const UpdateSpecErrors = [
33 InternalError,
34 OpenApiParseError,
35 OpenApiExtractionError,
36 OpenApiOAuthError,
37 IntegrationNotFound,
38] as const;
39
29const SlugParams = {40const SlugParams = {
30 slug: Schema.String,41 slug: Schema.String,
31};42};
@@ -64,6 +75,7 @@ const AuthenticationResponse = Schema.Union([OAuthTemplatePayload, ApiKeyAuthMet · 32 unmodified lines
64const AddSpecPayload = Schema.Struct({75const AddSpecPayload = Schema.Struct({
65 spec: OpenApiSpecInputPayload,76 spec: OpenApiSpecInputPayload,
66 slug: Schema.String,77 slug: Schema.String,
78 name: Schema.optional(Schema.String),
67 description: Schema.optional(Schema.String),79 description: Schema.optional(Schema.String),
68 baseUrl: Schema.optional(Schema.String),80 baseUrl: Schema.optional(Schema.String),
69 headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),81 headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
@@ -92,6 +104,21 @@ const AddSpecResponse = Schema.Struct({ · 22 unmodified lines
92 toolCount: Schema.Number,104 toolCount: Schema.Number,
93});105});
94 106
107// Update the spec in place. Body optional fields only: an empty payload means
108// "re-fetch from the stored source URL".
109const UpdateSpecPayload = Schema.Struct({
110 spec: Schema.optional(OpenApiSpecInputPayload),
111});
112
113const 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
95const IntegrationView = Schema.Struct({122const IntegrationView = Schema.Struct({
96 slug: IntegrationSlug,123 slug: IntegrationSlug,
97 description: Schema.String,124 description: Schema.String,
@@ -161,6 +188,14 @@ export const OpenApiGroup = HttpApiGroup.make("openapi") · 63 unmodified lines
161 error: DomainErrors,188 error: DomainErrors,
162 }),189 }),
163 )190 )
191 .add(
192 HttpApiEndpoint.post("updateSpec", "/openapi/integrations/:slug/spec", {
193 params: SlugParams,
194 payload: UpdateSpecPayload,
195 success: UpdateSpecResponse,
196 error: UpdateSpecErrors,
197 }),
198 )
164 .add(199 .add(
165 HttpApiEndpoint.delete("removeSpec", "/openapi/integrations/:slug", {200 HttpApiEndpoint.delete("removeSpec", "/openapi/integrations/:slug", {
166 params: SlugParams,201 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
54 return yield* ext.addSpec({54 return yield* ext.addSpec({
55 spec: payload.spec,55 spec: payload.spec,
56 slug: payload.slug,56 slug: payload.slug,
57 name: payload.name,
57 description: payload.description,58 description: payload.description,
58 baseUrl: payload.baseUrl,59 baseUrl: payload.baseUrl,
59 headers: payload.headers ? { ...payload.headers } : undefined,60 headers: payload.headers ? { ...payload.headers } : undefined,
@@ -114,6 +115,22 @@ export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "ope · 54 unmodified lines
114 }),115 }),
115 ),116 ),
116 )117 )
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 )
117 .handle("removeSpec", ({ params }) =>134 .handle("removeSpec", ({ params }) =>
118 capture(135 capture(
119 Effect.gen(function* () {136 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