fix(protocol): return missing integration error

This commit is contained in:
Dax Raad
2026-09-13 20:00:46 -04:00
parent 82f713421f
commit aeed4b6375
11 changed files with 122 additions and 37 deletions
+2 -2
View File
@@ -109,8 +109,8 @@ Review endpoints in document order. For each endpoint, select one disposition an
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 025 | `GET` | `/api/integration` | `integration.list` | | |
| [ ] 026 | `GET` | `/api/integration/{integrationID}` | `integration.get` | | |
| [x] 025 | `GET` | `/api/integration` | `integration.list` | Keep | Full integration inventory is consumed by authentication and integration-selection clients. |
| [x] 026 | `GET` | `/api/integration/{integrationID}` | `integration.get` | Change | Missing integration now returns typed `404` instead of optional data. |
| [ ] 027 | `POST` | `/api/experimental/integration/wellknown` | `experimental.integration.wellknown.add` | | |
| [ ] 028 | `POST` | `/api/integration/{integrationID}/connect/key` | `integration.connect.key` | | |
| [ ] 029 | `POST` | `/api/integration/{integrationID}/connect/oauth` | `integration.oauth.connect` | | |
+1 -4
View File
@@ -1465,10 +1465,7 @@ export type IntegrationGetInput = {
readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined } | undefined
}
export type IntegrationGetOutput = {
readonly location: Location.PublicRef
readonly data: Integration.Info | undefined
}
export type IntegrationGetOutput = { readonly location: Location.PublicRef; readonly data: Integration.Info }
export type IntegrationGetOperation<E = never> = (input: IntegrationGetInput) => Effect.Effect<IntegrationGetOutput, E>
export type IntegrationWellknownAddInput = {
@@ -1100,7 +1100,7 @@ export function make(options: ClientOptions) {
path: `/api/integration/${encodeURIComponent(input.integrationID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
@@ -2502,6 +2502,14 @@ export type ProviderNotFoundError = {
export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError"
export type IntegrationNotFoundError = {
readonly _tag: "IntegrationNotFoundError"
readonly integrationID: string
readonly message: string
}
export const isIntegrationNotFoundError = (value: unknown): value is IntegrationNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationNotFoundError"
export type McpServerNotFoundError = {
readonly _tag: "McpServerNotFoundError"
readonly server: string
@@ -4509,7 +4517,7 @@ export type IntegrationGetInput = {
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
}
export type IntegrationGetOutput = { location: LocationPublicRef; data: IntegrationInfo | null }
export type IntegrationGetOutput = { location: LocationPublicRef; data: IntegrationInfo }
export type IntegrationWellknownAddInput = {
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
+5 -1
View File
@@ -268,7 +268,11 @@ export const make = Effect.fn("PluginHost.make")(function* (
},
integration: {
list: () => response(integration.list()),
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
get: Effect.fn(function* (input) {
const item = yield* integration.get(Integration.ID.make(input.integrationID))
if (!item) return yield* Effect.fail(new Error(`Integration not found: ${input.integrationID}`))
return yield* response(Effect.succeed(item))
}),
connect: {
key: (input) =>
integration.connection.key({
+28 -8
View File
@@ -5164,14 +5164,7 @@
"$ref": "#/components/schemas/Location.PublicRef"
},
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/Integration.Info"
},
{
"type": "null"
}
]
"$ref": "#/components/schemas/Integration.Info"
}
},
"required": ["location", "data"],
@@ -5199,6 +5192,16 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
}
}
}
}
},
"description": "Retrieve one integration and its authentication methods.",
@@ -15118,6 +15121,23 @@
"required": ["id", "type", "label"],
"additionalProperties": false
},
"IntegrationNotFoundErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["IntegrationNotFoundError"]
},
"integrationID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "integrationID", "message"],
"additionalProperties": false
},
"InvalidCursorErrorEncoded": {
"type": "object",
"properties": {
+9
View File
@@ -82,6 +82,15 @@ export class ProviderNotFoundError extends Schema.TaggedError<ProviderNotFoundEr
{ httpApiStatus: 404 },
) {}
export class IntegrationNotFoundError extends Schema.TaggedError<IntegrationNotFoundError>()(
"IntegrationNotFoundError",
{
integrationID: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
export class ProjectNotFoundError extends Schema.TaggedError<ProjectNotFoundError>()(
"ProjectNotFoundError",
{
+3 -2
View File
@@ -3,7 +3,7 @@ import { Location } from "@opencode/schema/location"
import { Form } from "@opencode/schema/form"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { InvalidRequestError } from "../errors.js"
import { IntegrationNotFoundError, InvalidRequestError } from "../errors.js"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
export const IntegrationGroup = HttpApiGroup.make("server.integration")
@@ -25,7 +25,8 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
HttpApiEndpoint.get("integration.get", "/api/integration/:integrationID", {
params: { integrationID: Integration.ID },
query: LocationQuery,
success: Location.response(Schema.UndefinedOr(Integration.Info)),
success: Location.response(Integration.Info),
error: IntegrationNotFoundError,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
+8 -2
View File
@@ -2,7 +2,7 @@ import { Integration } from "@opencode/core/integration"
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
import { InvalidRequestError } from "@opencode/protocol/errors"
import { IntegrationNotFoundError, InvalidRequestError } from "@opencode/protocol/errors"
import { response } from "../location"
import { WellKnown } from "@opencode/core/wellknown"
@@ -32,7 +32,13 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
"integration.get",
Effect.fn(function* (ctx) {
const service = yield* Integration.Service
return yield* response(service.get(ctx.params.integrationID))
const integration = yield* service.get(ctx.params.integrationID)
if (!integration)
return yield* new IntegrationNotFoundError({
integrationID: ctx.params.integrationID,
message: `Integration not found: ${ctx.params.integrationID}`,
})
return yield* response(Effect.succeed(integration))
}),
)
.handle(
+28 -8
View File
@@ -5164,14 +5164,7 @@
"$ref": "#/components/schemas/Location.PublicRef"
},
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/Integration.Info"
},
{
"type": "null"
}
]
"$ref": "#/components/schemas/Integration.Info"
}
},
"required": ["location", "data"],
@@ -5199,6 +5192,16 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
}
}
}
}
},
"description": "Retrieve one integration and its authentication methods.",
@@ -15118,6 +15121,23 @@
"required": ["id", "type", "label"],
"additionalProperties": false
},
"IntegrationNotFoundErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["IntegrationNotFoundError"]
},
"integrationID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "integrationID", "message"],
"additionalProperties": false
},
"InvalidCursorErrorEncoded": {
"type": "object",
"properties": {
+28 -8
View File
@@ -5164,14 +5164,7 @@
"$ref": "#/components/schemas/Location.PublicRef"
},
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/Integration.Info"
},
{
"type": "null"
}
]
"$ref": "#/components/schemas/Integration.Info"
}
},
"required": ["location", "data"],
@@ -5199,6 +5192,16 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
}
}
}
}
},
"description": "Retrieve one integration and its authentication methods.",
@@ -15118,6 +15121,23 @@
"required": ["id", "type", "label"],
"additionalProperties": false
},
"IntegrationNotFoundErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["IntegrationNotFoundError"]
},
"integrationID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "integrationID", "message"],
"additionalProperties": false
},
"InvalidCursorErrorEncoded": {
"type": "object",
"properties": {