diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index cadaa8b85a3f..f05a8cd70647 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -1685,7 +1685,7 @@ export function make(options: ClientOptions) { path: `/api/integration`, query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401, 404, 503], empty: false, }, requestOptions, @@ -1697,7 +1697,7 @@ export function make(options: ClientOptions) { path: `/api/integration/${encodeURIComponent(input.integrationID)}`, query: { location: input["location"] }, successStatus: 200, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401, 404, 503], empty: false, }, requestOptions, @@ -1711,7 +1711,7 @@ export function make(options: ClientOptions) { query: { location: input["location"] }, body: { key: input["key"], answer: input["answer"], label: input["label"] }, successStatus: 204, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401, 404, 503], empty: true, }, requestOptions, @@ -1724,7 +1724,7 @@ export function make(options: ClientOptions) { query: { location: input["location"] }, body: { methodID: input["methodID"], answer: input["answer"], label: input["label"] }, successStatus: 204, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401, 404, 503], empty: true, }, requestOptions, @@ -1739,7 +1739,7 @@ export function make(options: ClientOptions) { query: { location: input["location"] }, body: { methodID: input["methodID"], answer: input["answer"], label: input["label"] }, successStatus: 200, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401, 404, 503], empty: false, }, requestOptions, @@ -1751,7 +1751,7 @@ export function make(options: ClientOptions) { path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`, query: { location: input["location"] }, successStatus: 200, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401, 404, 503], empty: false, }, requestOptions, @@ -1764,7 +1764,7 @@ export function make(options: ClientOptions) { query: { location: input["location"] }, body: { code: input["code"] }, successStatus: 204, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401, 404, 503], empty: true, }, requestOptions, @@ -1791,7 +1791,7 @@ export function make(options: ClientOptions) { query: { location: input["location"] }, body: { methodID: input["methodID"], label: input["label"] }, successStatus: 200, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401, 404, 503], empty: false, }, requestOptions, @@ -1803,7 +1803,7 @@ export function make(options: ClientOptions) { path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`, query: { location: input["location"] }, successStatus: 200, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401, 404, 503], empty: false, }, requestOptions, diff --git a/packages/protocol/src/groups/integration.ts b/packages/protocol/src/groups/integration.ts index 49b0c78206de..3e326fe86204 100644 --- a/packages/protocol/src/groups/integration.ts +++ b/packages/protocol/src/groups/integration.ts @@ -8,6 +8,7 @@ import { IntegrationMethodNotFoundError, IntegrationNotFoundError, InvalidRequestError, + ServiceUnavailableError, } from "../errors.js" import { LocationQuery, locationQueryOpenApi } from "./location.js" @@ -16,6 +17,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") HttpApiEndpoint.get("integration.list", "/api/integration", { query: LocationQuery, success: Location.response(Schema.Array(Integration.Info)), + error: ServiceUnavailableError, }) .annotateMerge(locationQueryOpenApi) .annotateMerge( @@ -31,7 +33,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") params: { integrationID: Integration.ID }, query: LocationQuery, success: Location.response(Integration.Info), - error: IntegrationNotFoundError, + error: [IntegrationNotFoundError, ServiceUnavailableError], }) .annotateMerge(locationQueryOpenApi) .annotateMerge( @@ -68,7 +70,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") label: Schema.optional(Schema.String), }), success: HttpApiSchema.NoContent, - error: [IntegrationNotFoundError, InvalidRequestError], + error: [IntegrationNotFoundError, InvalidRequestError, ServiceUnavailableError], }) .annotateMerge(locationQueryOpenApi) .annotateMerge( @@ -89,7 +91,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") label: Schema.optional(Schema.String), }), success: HttpApiSchema.NoContent, - error: [IntegrationNotFoundError, InvalidRequestError], + error: [IntegrationNotFoundError, InvalidRequestError, ServiceUnavailableError], }) .annotateMerge(locationQueryOpenApi) .annotateMerge( @@ -110,7 +112,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") label: Schema.optional(Schema.String), }), success: Location.response(Integration.Attempt), - error: InvalidRequestError, + error: [InvalidRequestError, ServiceUnavailableError], }) .annotateMerge(locationQueryOpenApi) .annotateMerge( @@ -126,7 +128,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") params: { integrationID: Integration.ID, attemptID: Integration.AttemptID }, query: LocationQuery, success: Location.response(Integration.AttemptStatus), - error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError], + error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, ServiceUnavailableError], }) .annotateMerge(locationQueryOpenApi) .annotateMerge( @@ -146,7 +148,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") query: LocationQuery, payload: Schema.Struct({ code: Schema.optional(Schema.String) }), success: HttpApiSchema.NoContent, - error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, InvalidRequestError], + error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, InvalidRequestError, ServiceUnavailableError], }, ) .annotateMerge(locationQueryOpenApi) @@ -182,7 +184,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") label: Schema.optional(Schema.String), }), success: Location.response(Integration.CommandAttempt), - error: [IntegrationNotFoundError, IntegrationMethodNotFoundError, InvalidRequestError], + error: [IntegrationNotFoundError, IntegrationMethodNotFoundError, InvalidRequestError, ServiceUnavailableError], }) .annotateMerge(locationQueryOpenApi) .annotateMerge( @@ -198,7 +200,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration") params: { integrationID: Integration.ID, attemptID: Integration.AttemptID }, query: LocationQuery, success: Location.response(Integration.CommandAttemptStatus), - error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError], + error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, ServiceUnavailableError], }) .annotateMerge(locationQueryOpenApi) .annotateMerge( diff --git a/packages/server/src/handlers/integration.ts b/packages/server/src/handlers/integration.ts index 299fe6fad30b..d2d327868fc4 100644 --- a/packages/server/src/handlers/integration.ts +++ b/packages/server/src/handlers/integration.ts @@ -1,5 +1,4 @@ import { Integration } from "@opencode/core/integration" -import { Plugin } from "@opencode/core/plugin" import { Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/http-api" import { Api } from "../api" @@ -9,7 +8,7 @@ import { IntegrationNotFoundError, InvalidRequestError, } from "@opencode/protocol/errors" -import { response } from "../location" +import { awaitPlugins, response } from "../location" import { WellKnown } from "@opencode/core/wellknown" import { Config } from "@opencode/core/config" import { ManagedPolicy } from "@opencode/core/managed-policy" @@ -54,7 +53,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" .handle( "integration.list", Effect.fn(function* () { - yield* Plugin.awaitActivation + yield* awaitPlugins const service = yield* Integration.Service const current = yield* statements() return yield* response( @@ -75,6 +74,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" .handle( "integration.get", Effect.fn(function* (ctx) { + yield* awaitPlugins const service = yield* Integration.Service const integration = yield* service.get(ctx.params.integrationID) if (!integration) @@ -106,6 +106,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" .handle( "integration.connect.key", Effect.fn(function* (ctx) { + yield* awaitPlugins yield* requireConnectable(ctx.params.integrationID) const service = yield* Integration.Service if (!(yield* service.get(ctx.params.integrationID))) @@ -127,6 +128,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" .handle( "integration.connect.external", Effect.fn(function* (ctx) { + yield* awaitPlugins yield* requireConnectable(ctx.params.integrationID) const service = yield* Integration.Service if (!(yield* service.get(ctx.params.integrationID))) @@ -148,6 +150,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" .handle( "integration.oauth.connect", Effect.fn(function* (ctx) { + yield* awaitPlugins yield* requireConnectable(ctx.params.integrationID) const service = yield* Integration.Service return yield* response( @@ -165,6 +168,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" .handle( "integration.oauth.status", Effect.fn(function* (ctx) { + yield* awaitPlugins const service = yield* Integration.Service if (!(yield* service.get(ctx.params.integrationID))) return yield* new IntegrationNotFoundError({ @@ -193,6 +197,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" .handle( "integration.oauth.complete", Effect.fn(function* (ctx) { + yield* awaitPlugins const service = yield* Integration.Service if (!(yield* service.get(ctx.params.integrationID))) return yield* new IntegrationNotFoundError({ @@ -242,6 +247,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" .handle( "integration.command.connect", Effect.fn(function* (ctx) { + yield* awaitPlugins yield* requireConnectable(ctx.params.integrationID) const service = yield* Integration.Service const integration = yield* service.get(ctx.params.integrationID) @@ -270,6 +276,7 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration" .handle( "integration.command.status", Effect.fn(function* (ctx) { + yield* awaitPlugins const service = yield* Integration.Service if (!(yield* service.get(ctx.params.integrationID))) return yield* new IntegrationNotFoundError({ diff --git a/packages/server/test/provider.test.ts b/packages/server/test/provider.test.ts index 4e132e7fd453..85e4c6914b3e 100644 --- a/packages/server/test/provider.test.ts +++ b/packages/server/test/provider.test.ts @@ -41,10 +41,15 @@ it.live( yield* sdk.register( Plugin.define({ id: "slow-plugin", - effect: () => + effect: (ctx) => Effect.gen(function* () { yield* Deferred.succeed(started, undefined) yield* Deferred.await(release) + yield* ctx.integration.transform((editor) => + editor.update("late-integration", (integration) => { + integration.name = "Late integration" + }), + ) }), }), ) @@ -58,7 +63,11 @@ it.live( return handler( new Request(url, { method, - headers: { authorization: `Basic ${btoa("opencode:secret")}` }, + headers: { + authorization: `Basic ${btoa("opencode:secret")}`, + ...(method === "POST" ? { "content-type": "application/json" } : {}), + }, + ...(method === "POST" ? { body: JSON.stringify({ methodID: "missing" }) } : {}), signal, }), ) @@ -69,6 +78,15 @@ it.live( const agents = yield* request("GET", "/api/agent").pipe(Effect.forkScoped) yield* Deferred.await(started) const integrations = yield* request("GET", "/api/integration").pipe(Effect.forkScoped) + const integration = yield* request("GET", "/api/integration/late-integration").pipe( + Effect.forkScoped({ startImmediately: true }), + ) + const oauth = yield* request("GET", "/api/integration/late-integration/connect/oauth/missing").pipe( + Effect.forkScoped({ startImmediately: true }), + ) + const command = yield* request("POST", "/api/integration/late-integration/connect/command").pipe( + Effect.forkScoped({ startImmediately: true }), + ) const get = yield* request("GET", "/api/provider/custom").pipe(Effect.timeout("2 seconds")) expect(get.status).toBe(404) expect(yield* Effect.promise(() => get.json())).toMatchObject({ @@ -77,6 +95,9 @@ it.live( }) expect(providers.pollUnsafe()).toBeUndefined() expect(agents.pollUnsafe()).toBeUndefined() + expect(integration.pollUnsafe()).toBeUndefined() + expect(oauth.pollUnsafe()).toBeUndefined() + expect(command.pollUnsafe()).toBeUndefined() yield* Deferred.succeed(release, undefined) const list = yield* Fiber.join(providers) expect(list.status).toBe(200) @@ -97,6 +118,15 @@ it.live( expect(yield* Effect.promise(() => discovered.json())).toMatchObject({ data: expect.arrayContaining([expect.objectContaining({ id: "opencode" })]), }) + const item = yield* Fiber.join(integration) + expect(item.status).toBe(200) + expect(yield* Effect.promise(() => item.json())).toMatchObject({ data: { id: "late-integration" } }) + const attempt = yield* Fiber.join(oauth) + expect(attempt.status).toBe(404) + expect(yield* Effect.promise(() => attempt.json())).toMatchObject({ _tag: "IntegrationAttemptNotFoundError" }) + const commandResult = yield* Fiber.join(command) + expect(commandResult.status).toBe(404) + expect(yield* Effect.promise(() => commandResult.json())).toMatchObject({ _tag: "IntegrationMethodNotFoundError" }) }), 15_000, )