diff --git a/.changeset/lean-connection-health.md b/.changeset/lean-connection-health.md new file mode 100644 index 000000000..02577c496 --- /dev/null +++ b/.changeset/lean-connection-health.md @@ -0,0 +1,5 @@ +--- +"@executor-js/sdk": patch +--- + +Keep `connections.list` health output compact unless callers opt into diagnostics with `verbose: true`. Default list responses now retain only the health status, identity, and check timestamp; verbose responses continue to include HTTP status, diagnostic detail, and bounded upstream response samples. diff --git a/packages/core/sdk/src/connections.test.ts b/packages/core/sdk/src/connections.test.ts index 7ed046f65..937aae3ba 100644 --- a/packages/core/sdk/src/connections.test.ts +++ b/packages/core/sdk/src/connections.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; -import { Effect, Predicate, Result } from "effect"; +import { Effect, Predicate, Result, Schema } from "effect"; import { AuthTemplateSlug, @@ -11,6 +11,7 @@ import { ToolName, } from "./ids"; import { createExecutor } from "./executor"; +import { HealthCheckResult } from "./health-check"; import { definePlugin } from "./plugin"; import type { CredentialProvider } from "./provider"; import { makeTestConfig, makeTestExecutor } from "./testing"; @@ -43,6 +44,11 @@ const memoryProvider = (): CredentialProvider => { const INTEG = IntegrationSlug.make("vercel"); const TEMPLATE = AuthTemplateSlug.make("apiKey"); +const ConnectionListHealthOutput = Schema.Struct({ + connections: Schema.Array(Schema.Struct({ lastHealth: Schema.NullOr(HealthCheckResult) })), +}); +const decodeConnectionListHealthOutput = Schema.decodeUnknownEffect(ConnectionListHealthOutput); + const demoPlugin = definePlugin(() => ({ id: "demo" as const, credentialProviders: [memoryProvider()], @@ -263,6 +269,58 @@ describe("connections.create", () => { }); describe("connections.list / get", () => { + it.effect("only includes full health diagnostics in verbose core tool output", () => + Effect.gen(function* () { + const config = makeTestConfig({ plugins: [demoPlugin] as const, coreTools: {} }); + const executor = yield* createExecutor(config); + yield* executor.demo.seed(); + yield* executor.connections.create({ + owner: "org", + name: ConnectionName.make("health"), + integration: INTEG, + template: TEMPLATE, + value: "v", + }); + + const health = { + status: "healthy" as const, + identity: "account@example.com", + checkedAt: 1234, + httpStatus: 200, + detail: "GET /me returned 200", + responseSample: [{ path: "user.email", value: "account@example.com" }], + }; + yield* Effect.promise(() => + config.db.updateMany("connection", { + where: (b) => b.and(b("integration", "=", String(INTEG)), b("name", "=", "health")), + set: { last_health: health }, + }), + ); + + const list = (input: { readonly verbose?: boolean }) => + executor + .execute(ToolAddress.make("executor.coreTools.connections.list"), { + integration: String(INTEG), + owner: "org", + ...input, + }) + .pipe(Effect.flatMap(decodeConnectionListHealthOutput)); + + const defaultList = yield* list({}); + const nonVerboseList = yield* list({ verbose: false }); + const verboseList = yield* list({ verbose: true }); + const summary = { + status: "healthy", + identity: "account@example.com", + checkedAt: 1234, + }; + + expect(defaultList.connections[0]?.lastHealth).toEqual(summary); + expect(nonVerboseList.connections[0]?.lastHealth).toEqual(summary); + expect(verboseList.connections[0]?.lastHealth).toEqual(health); + }), + ); + it.effect("lists created connections and filters by integration", () => Effect.gen(function* () { const executor = yield* setup(); diff --git a/packages/core/sdk/src/core-tools.ts b/packages/core/sdk/src/core-tools.ts index e6dc78ce7..e78da61f3 100644 --- a/packages/core/sdk/src/core-tools.ts +++ b/packages/core/sdk/src/core-tools.ts @@ -88,10 +88,10 @@ const ConnectionsListInput = Schema.Struct({ verbose: Schema.optional(Schema.Boolean), }); -/** Lean per-connection shape for list scans. Omits the full `oauthScope` - * grant string (a single connection's scope list can run to thousands of - * characters and dominates the payload) in favor of `oauthScopeCount`. The - * full scope is included only when the caller passes `verbose: true`. */ +/** Lean per-connection shape for list scans. The default projection summarizes + * the full `oauthScope` grant string as `oauthScopeCount` and trims health + * probe diagnostics. Those optional fields are populated only for `verbose: + * true`. */ const ConnectionListItem = Schema.Struct({ owner: OwnerSchema, name: Schema.String, @@ -389,8 +389,8 @@ const connectionToOutput = (connection: Connection) => ({ const oauthScopeCount = (scope: string | null | undefined): number | null => scope == null ? null : scope.split(/\s+/).filter(Boolean).length; -/** Lean projection for `connections.list`. Summarizes `oauthScope` to a count - * unless `verbose`, where the full grant string is included too. */ +/** Lean projection for `connections.list`. Summarizes `oauthScope` and health + * diagnostics unless `verbose`, where the full grant string is included too. */ const connectionToListItem = (connection: Connection, verbose: boolean) => ({ owner: connection.owner, name: String(connection.name), @@ -404,7 +404,17 @@ const connectionToListItem = (connection: Connection, verbose: boolean) => ({ oauthClient: connection.oauthClient == null ? null : String(connection.oauthClient), oauthClientOwner: connection.oauthClientOwner ?? null, oauthScopeCount: oauthScopeCount(connection.oauthScope), - lastHealth: connection.lastHealth ?? null, + // Keep full probe diagnostics behind the explicit verbose opt-in. + lastHealth: + connection.lastHealth == null || verbose + ? (connection.lastHealth ?? null) + : { + status: connection.lastHealth.status, + ...(connection.lastHealth.identity !== undefined + ? { identity: connection.lastHealth.identity } + : {}), + checkedAt: connection.lastHealth.checkedAt, + }, ...(verbose ? { oauthScope: connection.oauthScope ?? null } : {}), }); diff --git a/packages/core/sdk/src/executor.test.ts b/packages/core/sdk/src/executor.test.ts index 029b06d75..00b878fd6 100644 --- a/packages/core/sdk/src/executor.test.ts +++ b/packages/core/sdk/src/executor.test.ts @@ -438,7 +438,7 @@ describe("createExecutor", () => { const listed = yield* executor.execute( ToolAddress.make("executor.coreTools.connections.list"), - { integration: "diagnostics" }, + { integration: "diagnostics", verbose: true }, ); expect(listed).toMatchObject({ connections: [