refactor(protocol): refine audited endpoints

This commit is contained in:
Dax Raad
2026-09-13 23:07:28 -04:00
parent e9bb6d490b
commit 40f4778296
45 changed files with 686 additions and 598 deletions
+18 -18
View File
@@ -120,17 +120,17 @@ Review endpoints in document order. For each endpoint, select one disposition an
| [ ] 033 | `POST` | `/api/integration/{integrationID}/connect/command` | `integration.command.connect` | | |
| [ ] 034 | `GET` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.status` | | |
| [ ] 035 | `DELETE` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.cancel` | | |
| [ ] 036 | `GET` | `/api/mcp` | `mcp.list` | | |
| [ ] 037 | `PUT` | `/api/mcp/{server}` | `mcp.add` | | |
| [ ] 038 | `DELETE` | `/api/mcp/{server}` | `mcp.remove` | | |
| [ ] 039 | `POST` | `/api/mcp/{server}/connect` | `mcp.connect` | | |
| [ ] 040 | `POST` | `/api/mcp/{server}/disconnect` | `mcp.disconnect` | | |
| [ ] 041 | `GET` | `/api/mcp/resource` | `mcp.resource.catalog` | | |
| [ ] 042 | `PATCH` | `/api/credential/{credentialID}` | `credential.update` | | |
| [ ] 043 | `DELETE` | `/api/credential/{credentialID}` | `credential.remove` | | |
| [ ] 044 | `POST` | `/api/credential/{credentialID}/activate` | `credential.activate` | | |
| [ ] 045 | `GET` | `/api/websearch/provider` | `websearch.providers` | | |
| [ ] 046 | `POST` | `/api/websearch` | `websearch.query` | | |
| [x] 036 | `GET` | `/api/mcp` | `mcp.list` | Keep | MCP inventory and connection status retained. |
| [x] 037 | `PUT` | `/api/experimental/mcp/{server}` | `experimental.mcp.add` | Experimental-only | Runtime-only MCP override; does not persist configuration. |
| [x] 038 | `DELETE` | `/api/experimental/mcp/{server}` | `experimental.mcp.remove` | Experimental-only | Runtime removal override; missing server returns `404`. |
| [x] 039 | `POST` | `/api/experimental/mcp/{server}/connect` | `experimental.mcp.connect` | Experimental-only | Runtime connection override retained outside the stable API. |
| [x] 040 | `POST` | `/api/experimental/mcp/{server}/disconnect` | `experimental.mcp.disconnect` | Experimental-only | Runtime disconnection override retained outside the stable API. |
| [ ] 041 | `GET` | `/api/mcp/resource` | `mcp.resource.catalog` | | Deferred for later review. |
| [x] 042 | `PATCH` | `/api/credential/{credentialID}` | `credential.update` | Change | Removed redundant location query; credentials and events are global. |
| [x] 043 | `DELETE` | `/api/credential/{credentialID}` | `credential.remove` | Change | Removed redundant location query; credentials and events are global. |
| [x] 044 | `POST` | `/api/credential/{credentialID}/activate` | `credential.activate` | Change | Removed redundant location query; credentials and events are global. |
| [x] 045 | `GET` | `/api/websearch/provider` | `websearch.providers` | Keep | Provider availability remains location-scoped; singular resource path retained. |
| [x] 046 | `POST` | `/api/websearch` | `websearch.query` | Keep | Unknown provider remains an invalid request; published time documented as Unix epoch milliseconds. |
## Group 4: Session lifecycle
@@ -138,13 +138,13 @@ Review endpoints in document order. For each endpoint, select one disposition an
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 047 | `GET` | `/api/session` | `session.list` | | |
| [ ] 048 | `POST` | `/api/session` | `session.create` | | |
| [ ] 049 | `GET` | `/api/session/stats` | `session.stats` | | |
| [ ] 050 | `GET` | `/api/session/active` | `session.active` | | |
| [ ] 051 | `GET` | `/api/session/{sessionID}` | `session.get` | | |
| [ ] 052 | `DELETE` | `/api/session/{sessionID}` | `session.remove` | | |
| [ ] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | | |
| [x] 047 | `GET` | `/api/session` | `session.list` | Keep | Existing filtering, ordering, and cursor contract retained for now. |
| [x] 048 | `POST` | `/api/session` | `session.create` | Keep | Existing creation contract retained; model reference includes optional variant. |
| [x] 049 | `GET` | `/api/experimental/session/stats` | `experimental.session.stats` | Experimental-only | Session analytics retained outside the stable API commitment. |
| [x] 050 | `GET` | `/api/session/active` | `session.active` | Keep | Status record retained for future active-state expansion. |
| [x] 051 | `GET` | `/api/session/{sessionID}` | `session.get` | Keep | Specific session read and typed `404` retained. |
| [x] 052 | `DELETE` | `/api/session/{sessionID}` | `session.remove` | Keep | Session and child deletion with typed `404` retained. |
| [x] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | Change | Request now accepts optional branded `before` message ID; omission copies full history. |
| [ ] 054 | `POST` | `/api/session/{sessionID}/agent` | `session.switchAgent` | | |
| [ ] 055 | `POST` | `/api/session/{sessionID}/model` | `session.switchModel` | | |
| [ ] 056 | `POST` | `/api/session/{sessionID}/rename` | `session.rename` | | |
@@ -35,11 +35,11 @@ for (const shared of [true, false]) {
const url = new URL(route.request().url())
const target = url.searchParams.get("location[directory]") ?? directory
requests.push({ path: url.pathname, directory: target })
if (url.pathname === "/api/mcp/figma-desktop/connect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/connect") {
connected.add(target)
return route.fulfill({ status: 204 })
}
if (url.pathname === "/api/mcp/figma-desktop/disconnect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/disconnect") {
connected.delete(target)
return route.fulfill({ status: 204 })
}
@@ -72,7 +72,7 @@ for (const shared of [true, false]) {
await expect(toggle).toBeChecked()
await expect(toggle).toBeEnabled()
expect(connected).toEqual(new Set([workspace]))
expect(requests).toContainEqual({ path: "/api/mcp/figma-desktop/connect", directory: workspace })
expect(requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace })
expect(requests).toContainEqual({ path: "/api/mcp/resource", directory: workspace })
expect(requests.every((request) => request.directory === workspace)).toBe(true)
await testInfo.attach("workspace-connected", { body: await page.screenshot(), contentType: "image/png" })
@@ -82,7 +82,7 @@ for (const shared of [true, false]) {
await expect(toggle).not.toBeChecked()
await expect(toggle).toBeEnabled()
expect(connected.size).toBe(0)
expect(requests).toContainEqual({ path: "/api/mcp/figma-desktop/disconnect", directory: workspace })
expect(requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/disconnect", directory: workspace })
expect(requests.every((request) => request.directory === workspace)).toBe(true)
})
}
@@ -114,11 +114,11 @@ for (const surface of ["popover", "dialog"] as const) {
const url = new URL(route.request().url())
const target = url.searchParams.get("location[directory]") ?? directory
requests.push({ path: url.pathname, directory: target })
if (url.pathname === "/api/mcp/figma-desktop/disconnect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/disconnect") {
state.status = "disabled"
return route.fulfill({ status: 204 })
}
if (url.pathname === "/api/mcp/figma-desktop/connect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/connect") {
state.status = state.fail ? "failed" : "connected"
// Connection failures are reported by the refreshed status, not the HTTP response.
return route.fulfill({ status: 204 })
@@ -167,7 +167,7 @@ for (const surface of ["popover", "dialog"] as const) {
await expect(toggle).toBeChecked({ checked: surface === "popover" })
await expect(toggle).toBeEnabled()
expect(requests.filter((request) => request.path.endsWith("/connect"))).toEqual([
{ path: "/api/mcp/figma-desktop/connect", directory: workspace },
{ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace },
])
expect(requests.every((request) => request.directory === workspace)).toBe(true)
await expect(toast).toHaveCSS("opacity", "1")
@@ -62,7 +62,7 @@ test("every MCP row hit area toggles exactly once and keeps the submenu open", a
await expect(submenu).toBeVisible()
if (target === "keyboard") await expect(toggle).toBeFocused()
expect(writes).toHaveLength(index + 1)
expect(writes[index]).toBe(`/api/mcp/figma/${enabled ? "connect" : "disconnect"}`)
expect(writes[index]).toBe(`/api/experimental/mcp/figma/${enabled ? "connect" : "disconnect"}`)
}
})
@@ -69,7 +69,7 @@ export const DialogFork: Component = () => {
const dir = base64Encode(location().directory)
serverSDK.api.session
.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
.fork({ sessionID, before: item.id })
.then((forked) => {
data.session.remember(forked)
dialog.close()
@@ -105,7 +105,7 @@ export const SettingsProviders: Component<{
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
await Promise.all(
credentials.map((credential) => serverSdk.api.credential.remove({ credentialID: credential.id, location })),
credentials.map((credential) => serverSdk.api.credential.remove({ credentialID: credential.id })),
)
showToast({
variant: "success",
-1
View File
@@ -263,7 +263,6 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
forkSession: async (params) => {
const forked = await input.client.session.fork({
sessionID: params.sessionId,
boundary: { type: "through" },
})
const state = await attach(forked, forked.location.directory, params.mcpServers ?? [])
await replay(state)
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { handlePromptErrors, requireInteractive } from "../../../ui/prompt"
import { createClient, loadIntegrations, location, request } from "./shared"
import { createClient, loadIntegrations, request } from "./shared"
import { chooseCredential, chooseIntegration } from "./account"
export default Runtime.handler(
@@ -35,7 +35,7 @@ const logout = Effect.fn("cli.auth.logout.run")(function* (input: {
const credentialID = yield* chooseCredential(integration, "log out", input.credential)
const progress = spinner()
progress.start("Removing credential...")
yield* request((signal) => client.credential.remove({ credentialID, location }, { signal })).pipe(
yield* request((signal) => client.credential.remove({ credentialID }, { signal })).pipe(
Effect.tap(() => Effect.sync(() => progress.stop(`Removed account from ${integration.name}`))),
Effect.tapCause(() => Effect.sync(() => progress.stop("Failed to remove credential", 1))),
)
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { handlePromptErrors, requireInteractive } from "../../../ui/prompt"
import { createClient, loadIntegrations, location, request } from "./shared"
import { createClient, loadIntegrations, request } from "./shared"
import { chooseCredential, chooseIntegration } from "./account"
export default Runtime.handler(
@@ -35,7 +35,7 @@ const switchAccount = Effect.fn("cli.auth.switch.run")(function* (input: {
const credentialID = yield* chooseCredential(integration, "switch to", input.credential)
const progress = spinner()
progress.start("Switching account...")
yield* request((signal) => client.credential.activate({ credentialID, location }, { signal })).pipe(
yield* request((signal) => client.credential.activate({ credentialID }, { signal })).pipe(
Effect.tap(() => Effect.sync(() => progress.stop(`Switched account for ${integration.name}`))),
Effect.tapCause(() => Effect.sync(() => progress.stop("Failed to switch account", 1))),
)
@@ -29,7 +29,7 @@ export default Runtime.handler(
yield* Effect.forEach(
credentials,
(connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id, location })),
(connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id })),
{ discard: true },
)
process.stdout.write(`Removed OAuth credentials for ${input.name}` + EOL)
+2 -2
View File
@@ -112,7 +112,7 @@ async function selectSession(input: {
return {
session: input.fork
? await input.client.session
.fork({ sessionID: explicit.id, boundary: { type: "through" } }, ...requestOptions(input.signal))
.fork({ sessionID: explicit.id }, ...requestOptions(input.signal))
.catch((error) => {
throw new SessionTargetMutationError(error)
})
@@ -126,7 +126,7 @@ async function selectSession(input: {
return {
session: input.fork
? await input.client.session
.fork({ sessionID: selected.id, boundary: { type: "through" } }, ...requestOptions(input.signal))
.fork({ sessionID: selected.id }, ...requestOptions(input.signal))
.catch((error) => {
throw new SessionTargetMutationError(error)
})
@@ -168,7 +168,7 @@ describe("acp service lifecycle", () => {
method: "POST",
path: "/api/session/ses_loaded/fork",
query: {},
body: { boundary: { type: "through" } },
body: {},
})
})
+4 -14
View File
@@ -217,7 +217,7 @@ export type SessionRemoveInput = { readonly sessionID: Session.ID }
export type SessionRemoveOutput = void
export type SessionRemoveOperation<E = never> = (input: SessionRemoveInput) => Effect.Effect<SessionRemoveOutput, E>
export type SessionForkInput = { readonly sessionID: Session.ID; readonly boundary: Session.ForkRequestBoundary }
export type SessionForkInput = { readonly sessionID: Session.ID; readonly before?: SessionMessage.ID | undefined }
export type SessionForkOutput = Session.Info
export type SessionForkOperation<E = never> = (input: SessionForkInput) => Effect.Effect<SessionForkOutput, E>
@@ -1641,29 +1641,19 @@ export interface McpApi<E = never> {
readonly resource: { readonly catalog: McpResourceCatalogOperation<E> }
}
export type CredentialUpdateInput = {
readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined } | undefined
readonly label: string
}
export type CredentialUpdateInput = { readonly credentialID: Credential.ID; readonly label: string }
export type CredentialUpdateOutput = void
export type CredentialUpdateOperation<E = never> = (
input: CredentialUpdateInput,
) => Effect.Effect<CredentialUpdateOutput, E>
export type CredentialActivateInput = {
readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined } | undefined
}
export type CredentialActivateInput = { readonly credentialID: Credential.ID }
export type CredentialActivateOutput = void
export type CredentialActivateOperation<E = never> = (
input: CredentialActivateInput,
) => Effect.Effect<CredentialActivateOutput, E>
export type CredentialRemoveInput = {
readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined } | undefined
}
export type CredentialRemoveInput = { readonly credentialID: Credential.ID }
export type CredentialRemoveOutput = void
export type CredentialRemoveOperation<E = never> = (
input: CredentialRemoveInput,
+5 -10
View File
@@ -430,7 +430,7 @@ const EndpointSessionRemove = (raw: RawClient["server.session"]) => (input: Sess
const EndpointSessionFork = (raw: RawClient["server.session"]) => (input: SessionForkInput) =>
preserveEffect<SessionForkOutput>()(
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { boundary: input["boundary"] } }).pipe(
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { before: input["before"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
),
@@ -969,25 +969,20 @@ const EndpointCredentialUpdate = (raw: RawClient["server.credential"]) => (input
preserveEffect<CredentialUpdateOutput>()(
raw["credential.update"]({
params: { credentialID: input["credentialID"] },
query: { location: input["location"] },
payload: { label: input["label"] },
}).pipe(Effect.mapError(mapClientError)),
)
const EndpointCredentialActivate = (raw: RawClient["server.credential"]) => (input: CredentialActivateInput) =>
preserveEffect<CredentialActivateOutput>()(
raw["credential.activate"]({
params: { credentialID: input["credentialID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError)),
raw["credential.activate"]({ params: { credentialID: input["credentialID"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const EndpointCredentialRemove = (raw: RawClient["server.credential"]) => (input: CredentialRemoveInput) =>
preserveEffect<CredentialRemoveOutput>()(
raw["credential.remove"]({
params: { credentialID: input["credentialID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError)),
raw["credential.remove"]({ params: { credentialID: input["credentialID"] } }).pipe(Effect.mapError(mapClientError)),
)
const adaptGroupCredential = (raw: RawClient["server.credential"]) => ({
+11 -14
View File
@@ -517,7 +517,7 @@ export function make(options: ClientOptions) {
request<{ readonly data: SessionStatsOutput }>(
{
method: "GET",
path: `/api/session/stats`,
path: `/api/experimental/session/stats`,
query: {
from: input?.["from"],
to: input?.["to"],
@@ -613,7 +613,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`,
body: { boundary: input["boundary"] },
body: { before: input["before"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
empty: false,
@@ -1129,7 +1129,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],
declaredStatuses: [400, 401, 404],
empty: true,
},
requestOptions,
@@ -1156,7 +1156,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],
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
@@ -1169,7 +1169,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { code: input["code"] },
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 404],
empty: true,
},
requestOptions,
@@ -1196,7 +1196,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { methodID: input["methodID"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
@@ -1208,7 +1208,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],
declaredStatuses: [400, 401, 404],
empty: false,
},
requestOptions,
@@ -1244,7 +1244,7 @@ export function make(options: ClientOptions) {
request<McpAddOutput>(
{
method: "PUT",
path: `/api/mcp/${encodeURIComponent(input.server)}`,
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`,
query: { location: input["location"] },
body: { config: input["config"] },
successStatus: 204,
@@ -1257,7 +1257,7 @@ export function make(options: ClientOptions) {
request<McpRemoveOutput>(
{
method: "DELETE",
path: `/api/mcp/${encodeURIComponent(input.server)}`,
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
@@ -1269,7 +1269,7 @@ export function make(options: ClientOptions) {
request<McpConnectOutput>(
{
method: "POST",
path: `/api/mcp/${encodeURIComponent(input.server)}/connect`,
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/connect`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
@@ -1281,7 +1281,7 @@ export function make(options: ClientOptions) {
request<McpDisconnectOutput>(
{
method: "POST",
path: `/api/mcp/${encodeURIComponent(input.server)}/disconnect`,
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/disconnect`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
@@ -1310,7 +1310,6 @@ export function make(options: ClientOptions) {
{
method: "PATCH",
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
query: { location: input["location"] },
body: { label: input["label"] },
successStatus: 204,
declaredStatuses: [400, 401],
@@ -1323,7 +1322,6 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/credential/${encodeURIComponent(input.credentialID)}/activate`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
@@ -1335,7 +1333,6 @@ export function make(options: ClientOptions) {
{
method: "DELETE",
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
+21 -12
View File
@@ -2510,6 +2510,24 @@ export type IntegrationNotFoundError = {
export const isIntegrationNotFoundError = (value: unknown): value is IntegrationNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationNotFoundError"
export type IntegrationAttemptNotFoundError = {
readonly _tag: "IntegrationAttemptNotFoundError"
readonly integrationID: string
readonly attemptID: string
readonly message: string
}
export const isIntegrationAttemptNotFoundError = (value: unknown): value is IntegrationAttemptNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationAttemptNotFoundError"
export type IntegrationMethodNotFoundError = {
readonly _tag: "IntegrationMethodNotFoundError"
readonly integrationID: string
readonly methodID: string
readonly message: string
}
export const isIntegrationMethodNotFoundError = (value: unknown): value is IntegrationMethodNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationMethodNotFoundError"
export type McpServerNotFoundError = {
readonly _tag: "McpServerNotFoundError"
readonly server: string
@@ -3832,9 +3850,7 @@ export type SessionRemoveOutput = void
export type SessionForkInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly boundary: {
readonly boundary: { readonly type: "before"; readonly messageID: string } | { readonly type: "through" }
}["boundary"]
readonly before?: { readonly before?: string | undefined }["before"]
}
export type SessionForkOutput = { data: SessionInfo }["data"]
@@ -4691,23 +4707,16 @@ export type McpResourceCatalogOutput = { location: LocationPublicRef; data: McpR
export type CredentialUpdateInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly label: { readonly label: string }["label"]
}
export type CredentialUpdateOutput = void
export type CredentialActivateInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
}
export type CredentialActivateInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] }
export type CredentialActivateOutput = void
export type CredentialRemoveInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
}
export type CredentialRemoveInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] }
export type CredentialRemoveOutput = void
+2 -4
View File
@@ -239,12 +239,10 @@ test("credential.activate uses the public HTTP contract", async () => {
},
})
await client.credential.activate({ credentialID: "cred_work", location: { directory: "/tmp/project" } })
await client.credential.activate({ credentialID: "cred_work" })
expect(request?.method).toBe("POST")
expect(request?.url).toBe(
"http://localhost:3000/api/credential/cred_work/activate?location%5Bdirectory%5D=%2Ftmp%2Fproject",
)
expect(request?.url).toBe("http://localhost:3000/api/credential/cred_work/activate")
})
test("integration connections optionally submit a form answer", async () => {
+12 -7
View File
@@ -113,7 +113,12 @@ export class AuthorizationError extends Schema.TaggedError<AuthorizationError>()
cause: Schema.Defect(),
}) {}
export type Error = CodeRequiredError | AuthorizationError
export class AttemptNotFoundError extends Schema.TaggedError<AttemptNotFoundError>()("Integration.AttemptNotFound", {
integrationID: ID,
attemptID: AttemptID,
}) {}
export type Error = CodeRequiredError | AuthorizationError | AttemptNotFoundError
export { Event } from "@opencode/schema/integration"
@@ -188,13 +193,13 @@ export interface Interface extends State.Transformable<Editor> {
readonly status: (input: {
readonly integrationID: ID
readonly attemptID: AttemptID
}) => Effect.Effect<AttemptStatus>
}) => Effect.Effect<AttemptStatus, AttemptNotFoundError>
/** Completes the attempt and stores its credential. */
readonly complete: (input: {
readonly integrationID: ID
readonly attemptID: AttemptID
readonly code?: string
}) => Effect.Effect<void, CodeRequiredError | AuthorizationError>
}) => Effect.Effect<void, CodeRequiredError | AuthorizationError | AttemptNotFoundError>
/** Cancels an attempt and releases its resources. */
readonly cancel: (input: { readonly integrationID: ID; readonly attemptID: AttemptID }) => Effect.Effect<void>
}
@@ -207,7 +212,7 @@ export interface Interface extends State.Transformable<Editor> {
readonly status: (input: {
readonly integrationID: ID
readonly attemptID: AttemptID
}) => Effect.Effect<CommandAttemptStatus>
}) => Effect.Effect<CommandAttemptStatus, AttemptNotFoundError>
readonly cancel: (input: { readonly integrationID: ID; readonly attemptID: AttemptID }) => Effect.Effect<void>
}
}
@@ -718,7 +723,7 @@ const layer = Layer.effect(
status: Effect.fn("Integration.oauth.status")(function* (input) {
const attempt = (yield* SynchronizedRef.get(attempts)).get(input.attemptID)
if (!attempt || attempt.integrationID !== input.integrationID)
return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
return yield* new AttemptNotFoundError(input)
if (attempt.status === "failed") {
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
}
@@ -732,7 +737,7 @@ const layer = Layer.effect(
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
})
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
if (!attempt) return yield* new AttemptNotFoundError(input)
if (attempt.status !== "pending") return
if (attempt.authorization.mode === "code" && input.code === undefined) {
return yield* new CodeRequiredError({ attemptID: input.attemptID })
@@ -764,7 +769,7 @@ const layer = Layer.effect(
status: Effect.fn("Integration.command.status")(function* (input) {
const attempt = (yield* SynchronizedRef.get(commandAttempts)).get(input.attemptID)
if (!attempt || attempt.integrationID !== input.integrationID)
return yield* Effect.die(new Error(`Command attempt not found: ${input.attemptID}`))
return yield* new AttemptNotFoundError(input)
if (attempt.status === "pending") {
return {
status: attempt.status,
+5 -5
View File
@@ -94,7 +94,7 @@ type CompactInput = Parameters<Session.Handle["compact"]>[0] & { sessionID: Sess
type ForkInput = {
sessionID: SessionSchema.ID
boundary: SessionSchema.ForkRequestBoundary
before?: SessionMessage.ID
}
export {
@@ -308,17 +308,17 @@ const layer = Layer.effect(
.where(
and(
eq(SessionMessageTable.session_id, input.sessionID),
input.boundary.type === "before" ? eq(SessionMessageTable.id, input.boundary.messageID) : undefined,
input.before ? eq(SessionMessageTable.id, input.before) : undefined,
),
)
.orderBy(desc(SessionMessageTable.seq))
.limit(1)
.get()
.pipe(Effect.orDie)
if (!boundary && input.boundary.type === "before")
if (!boundary && input.before)
return yield* new MessageNotFoundError({
sessionID: input.sessionID,
messageID: input.boundary.messageID,
messageID: input.before,
})
if (!boundary) return yield* new ForkEmptyError({ sessionID: input.sessionID })
const sessionID = SessionSchema.ID.create()
@@ -336,7 +336,7 @@ const layer = Layer.effect(
yield* bus.publish(SessionEvent.Forked, {
sessionID,
parentID: parent.id,
boundary: { ...input.boundary, messageID: boundary.id },
boundary: { type: input.before ? "before" : "through", messageID: boundary.id },
...inherited,
})
return yield* result.get(sessionID).pipe(Effect.orDie)
@@ -75,11 +75,11 @@ function required<T>(value: T | undefined): T {
return value
}
function eventually<A>(
effect: Effect.Effect<A>,
function eventually<A, E>(
effect: Effect.Effect<A, E>,
predicate: (value: A) => boolean,
remaining = 1000,
): Effect.Effect<A, Error> {
): Effect.Effect<A, E | Error> {
return Effect.gen(function* () {
const value = yield* effect
if (predicate(value)) return value
+13 -13
View File
@@ -380,7 +380,7 @@ describe("Session.create", () => {
yield* session.prompt({ sessionID: created.id, text: "Fork context", resume: false })
yield* SessionInbox.promote(db, bus, created.id, "steer")
const forked = yield* session.fork({ sessionID: created.id, boundary: { type: "through" } })
const forked = yield* session.fork({ sessionID: created.id })
expect(forked.metadata).toEqual(metadata)
// Absent stays absent: no empty-object normalization.
@@ -402,7 +402,7 @@ describe("Session.create", () => {
yield* session.prompt({ sessionID: created.id, text: "Fork context", resume: false })
yield* SessionInbox.promote(db, bus, created.id, "steer")
const forked = yield* session.fork({ sessionID: created.id, boundary: { type: "through" } })
const forked = yield* session.fork({ sessionID: created.id })
expect(forked.permissions).toEqual(permissions)
const replaced = [{ action: "shell", resource: "*", effect: "ask" as const }]
@@ -541,7 +541,7 @@ describe("Session.create", () => {
yield* session.synthetic({ sessionID: parent.id, text: "parent note", resume: false })
yield* SessionInbox.promote(db, bus, parent.id, "steer")
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
const forked = yield* session.fork({ sessionID: parent.id })
const parentContext = yield* session.context(parent.id)
const forkContext = yield* session.context(forked.id)
const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))
@@ -596,7 +596,7 @@ describe("Session.create", () => {
yield* session.prompt({ sessionID: parent.id, text: "First", resume: false })
yield* SessionInbox.promote(db, bus, parent.id, "steer")
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
const forked = yield* session.fork({ sessionID: parent.id })
const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, forked.id)).get().pipe(Effect.orDie)
expect(forked.title).toBeUndefined()
@@ -614,7 +614,7 @@ describe("Session.create", () => {
yield* SessionInbox.promote(db, bus, parent.id, "steer")
yield* session.synthetic({ sessionID: parent.id, text: "Second", resume: false })
yield* SessionInbox.promote(db, bus, parent.id, "steer")
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
const forked = yield* session.fork({ sessionID: parent.id })
const original = (yield* session.context(forked.id)).map((message) => message.id)
const recorded = yield* db
.select()
@@ -657,7 +657,7 @@ describe("Session.create", () => {
{ discard: true },
)
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
const forked = yield* session.fork({ sessionID: parent.id })
const inheritedList = yield* entries.list(forked.id)
const inheritedValues = yield* entries.load(forked.id).pipe(Effect.flatMap(Instructions.read))
@@ -730,7 +730,7 @@ describe("Session.create", () => {
executed: true,
})
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
const forked = yield* session.fork({ sessionID: parent.id })
expect(yield* session.context(parent.id)).toMatchObject([
Expected.user("Run both tools"),
@@ -760,7 +760,7 @@ describe("Session.create", () => {
})
yield* bus.publish(SessionEvent.Shell.Started, { sessionID: parent.id, shell })
const running = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
const running = yield* session.fork({ sessionID: parent.id })
expect(yield* session.context(parent.id)).toMatchObject([
Expected.user("Run a shell"),
@@ -773,7 +773,7 @@ describe("Session.create", () => {
shell: { ...shell, status: "exited", exit: 0, time: { started: 0, completed: 1 } },
output: { output: "complete", cursor: 8, size: 8, truncated: false },
})
const completed = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
const completed = yield* session.fork({ sessionID: parent.id })
expect(yield* session.context(running.id)).toMatchObject([Expected.user("Run a shell")])
expect(yield* session.context(completed.id)).toMatchObject([
@@ -789,7 +789,7 @@ describe("Session.create", () => {
const parent = yield* session.create({ location })
expect(
yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } }).pipe(Effect.flip),
yield* session.fork({ sessionID: parent.id }).pipe(Effect.flip),
).toMatchObject({ _tag: "Session.ForkEmptyError", sessionID: parent.id })
}),
)
@@ -830,13 +830,13 @@ describe("Session.create", () => {
const forked = yield* session.fork({
sessionID: parent.id,
boundary: { type: "before", messageID: second.id },
before: second.id,
})
const beforeFirst = yield* session.fork({
sessionID: parent.id,
boundary: { type: "before", messageID: first.id },
before: first.id,
})
const complete = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
const complete = yield* session.fork({ sessionID: parent.id })
const context = yield* session.context(forked.id)
const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))
+1 -1
View File
@@ -186,7 +186,7 @@ describe("Session.diff", () => {
expect(yield* diff({ from: steer }).pipe(Effect.flip)).toMatchObject({
_tag: "Session.MessageNotFoundError",
})
const forked = yield* sessions.fork({ sessionID: created.id, boundary: { type: "through" } })
const forked = yield* sessions.fork({ sessionID: created.id })
expect((yield* sessions.diff({ sessionID: forked.id, context: 0 })).map(summarize)).toEqual([
["third.txt", "added", 1, 0],
])
+5 -5
View File
@@ -1497,7 +1497,7 @@ describe("SessionRunnerLLM", () => {
expect(continued.system.map((part) => part.text)).toContain("Checkpoint instructions")
expect(systemTexts(continued)).toEqual(["Newest instructions"])
const forked = yield* s.session.fork({ sessionID, boundary: { type: "before", messageID: after.id } })
const forked = yield* s.session.fork({ sessionID, before: after.id })
yield* s.session.prompt({ sessionID: forked.id, text: "Fork prompt", resume: false })
yield* s.session.resume(forked.id)
expect(s.requests.at(-1)?.messages[0]).toEqual(replacement[0])
@@ -1552,7 +1552,7 @@ describe("SessionRunnerLLM", () => {
s.systemBaseline = "Latest context"
yield* s.runPrompt("Third")
const forked = yield* s.session.fork({ sessionID, boundary: { type: "before", messageID: second.id } })
const forked = yield* s.session.fork({ sessionID, before: second.id })
expect(
yield* s.db.select().from(InstructionStateTable).where(eq(InstructionStateTable.session_id, forked.id)).get(),
).toMatchObject({
@@ -1597,14 +1597,14 @@ describe("SessionRunnerLLM", () => {
s.systemBaseline = "Changed context"
const second = yield* s.runPrompt("Second")
const child = yield* s.session.fork({ sessionID, boundary: { type: "before", messageID: second.id } })
const child = yield* s.session.fork({ sessionID, before: second.id })
const inheritedFirst = (yield* s.session.messages({ sessionID: child.id })).find(
(message) => message.type === "user" && message.text === "First",
)
if (!inheritedFirst) return yield* Effect.die(new Error("Nested fork boundary message not found"))
const grandchild = yield* s.session.fork({
sessionID: child.id,
boundary: { type: "before", messageID: inheritedFirst.id },
before: inheritedFirst.id,
})
expect(
@@ -2036,7 +2036,7 @@ describe("SessionRunnerLLM", () => {
yield* replaySessionProjection(sessionID)
const latest = yield* s.runPrompt("Third")
expect(systemTexts(s.requests[3])).toEqual(["Replacement context"])
const fork = yield* s.session.fork({ sessionID, boundary: { type: "before", messageID: latest.id } })
const fork = yield* s.session.fork({ sessionID, before: latest.id })
expect(
(yield* s.session.context(fork.id)).flatMap((message) => (message.type === "system" ? [message.text] : [])),
).toEqual(["Replacement context"])
+1 -1
View File
@@ -129,7 +129,7 @@ describe("Session.skill", () => {
resume: false,
})
yield* SessionInbox.promote(database.db, bus, session.id, "steer")
const forked = yield* sessions.fork({ sessionID: session.id, boundary: { type: "before", messageID: selected } })
const forked = yield* sessions.fork({ sessionID: session.id, before: selected })
expect(yield* sessions.messages({ sessionID: forked.id })).toEqual([
expect.objectContaining({ type: "user", text: "Before the skill" }),
+138 -133
View File
@@ -931,10 +931,10 @@
}
}
},
"/api/session/stats": {
"/api/experimental/session/stats": {
"get": {
"tags": ["session"],
"operationId": "session.stats",
"operationId": "experimental.session.stats",
"parameters": [
{
"name": "from",
@@ -1519,7 +1519,7 @@
}
}
},
"description": "Create a child session by copying projected history through or before a message boundary.",
"description": "Create a child session by copying projected history before a message. Omit before to copy the full history.",
"summary": "Fork session",
"requestBody": {
"content": {
@@ -1527,11 +1527,18 @@
"schema": {
"type": "object",
"properties": {
"boundary": {
"$ref": "#/components/schemas/Session.ForkRequestBoundary"
"before": {
"anyOf": [
{
"type": "string",
"pattern": "^msg_"
},
{
"type": "null"
}
]
}
},
"required": ["boundary"],
"additionalProperties": false
}
}
@@ -5373,6 +5380,16 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
}
}
}
}
},
"description": "Run a key authentication method and store the resulting credential.",
@@ -5644,6 +5661,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Poll the current status of an OAuth attempt.",
@@ -5813,6 +5847,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Complete a code-based OAuth attempt and store the resulting credential.",
@@ -5934,6 +5985,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationMethodNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationMethodNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Start a command authentication attempt.",
@@ -6060,6 +6128,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Poll the current status and output of a command authentication attempt.",
@@ -6232,10 +6317,10 @@
"summary": "List MCP servers"
}
},
"/api/mcp/{server}": {
"/api/experimental/mcp/{server}": {
"put": {
"tags": ["mcp"],
"operationId": "mcp.add",
"operationId": "experimental.mcp.add",
"parameters": [
{
"name": "server",
@@ -6331,7 +6416,7 @@
},
"delete": {
"tags": ["mcp"],
"operationId": "mcp.remove",
"operationId": "experimental.mcp.remove",
"parameters": [
{
"name": "server",
@@ -6412,10 +6497,10 @@
"summary": "Remove MCP server"
}
},
"/api/mcp/{server}/connect": {
"/api/experimental/mcp/{server}/connect": {
"post": {
"tags": ["mcp"],
"operationId": "mcp.connect",
"operationId": "experimental.mcp.connect",
"parameters": [
{
"name": "server",
@@ -6496,10 +6581,10 @@
"summary": "Connect MCP server"
}
},
"/api/mcp/{server}/disconnect": {
"/api/experimental/mcp/{server}/disconnect": {
"post": {
"tags": ["mcp"],
"operationId": "mcp.disconnect",
"operationId": "experimental.mcp.disconnect",
"parameters": [
{
"name": "server",
@@ -6675,36 +6760,6 @@
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
@@ -6764,36 +6819,6 @@
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
@@ -6838,36 +6863,6 @@
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
@@ -15121,6 +15116,46 @@
"required": ["id", "type", "label"],
"additionalProperties": false
},
"IntegrationAttemptNotFoundErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["IntegrationAttemptNotFoundError"]
},
"integrationID": {
"type": "string"
},
"attemptID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "integrationID", "attemptID", "message"],
"additionalProperties": false
},
"IntegrationMethodNotFoundErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["IntegrationMethodNotFoundError"]
},
"integrationID": {
"type": "string"
},
"methodID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "integrationID", "methodID", "message"],
"additionalProperties": false
},
"IntegrationNotFoundErrorEncoded": {
"type": "object",
"properties": {
@@ -16960,36 +16995,6 @@
}
]
},
"Session.ForkRequestBoundary": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["before"]
},
"messageID": {
"type": "string",
"pattern": "^msg_"
}
},
"required": ["type", "messageID"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["through"]
}
},
"required": ["type"],
"additionalProperties": false
}
]
},
"Session.Inbox.Compaction": {
"type": "object",
"properties": {
+2 -2
View File
@@ -43,7 +43,6 @@ type LocationGroups<LocationId extends HttpApiMiddleware.AnyId> =
| HttpApiGroup.AddMiddleware<typeof IntegrationGroup, LocationId>
| HttpApiGroup.AddMiddleware<typeof WebSearchGroup, LocationId>
| HttpApiGroup.AddMiddleware<typeof McpGroup, LocationId>
| HttpApiGroup.AddMiddleware<typeof CredentialGroup, LocationId>
| HttpApiGroup.AddMiddleware<typeof ProjectGroup, LocationId>
| HttpApiGroup.AddMiddleware<typeof FileSystemGroup, LocationId>
| HttpApiGroup.AddMiddleware<typeof CommandGroup, LocationId>
@@ -88,6 +87,7 @@ type ApiGroups<
| typeof MigrationGroup
| typeof GenerateGroup
| typeof PersistentPtyGroup
| typeof CredentialGroup
| LocationGroups<LocationId>
| FormGroups<LocationId, LocationService, FormLocationId, FormLocationService>
| SessionGroups<SessionLocationId, SessionLocationService>
@@ -158,7 +158,7 @@ const makeApiFromGroup = <
.add(ProviderGroup.middleware(locationMiddleware))
.add(IntegrationGroup.middleware(locationMiddleware))
.add(McpGroup.middleware(locationMiddleware))
.add(CredentialGroup.middleware(locationMiddleware))
.add(CredentialGroup)
.add(ProjectGroup.middleware(locationMiddleware))
.add(makeFormGroup(locationMiddleware, formLocationMiddleware))
.add(makePermissionGroup(locationMiddleware, sessionLocationMiddleware))
+20
View File
@@ -91,6 +91,26 @@ export class IntegrationNotFoundError extends Schema.TaggedError<IntegrationNotF
{ httpApiStatus: 404 },
) {}
export class IntegrationAttemptNotFoundError extends Schema.TaggedError<IntegrationAttemptNotFoundError>()(
"IntegrationAttemptNotFoundError",
{
integrationID: Schema.String,
attemptID: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
export class IntegrationMethodNotFoundError extends Schema.TaggedError<IntegrationMethodNotFoundError>()(
"IntegrationMethodNotFoundError",
{
integrationID: Schema.String,
methodID: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
export class ProjectNotFoundError extends Schema.TaggedError<ProjectNotFoundError>()(
"ProjectNotFoundError",
{
@@ -1,17 +1,14 @@
import { Credential } from "@opencode/schema/credential"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
export const CredentialGroup = HttpApiGroup.make("server.credential")
.add(
HttpApiEndpoint.patch("credential.update", "/api/credential/:credentialID", {
params: { credentialID: Credential.ID },
query: LocationQuery,
payload: Schema.Struct({ label: Schema.String }),
success: HttpApiSchema.NoContent,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "credential.update",
@@ -24,10 +21,8 @@ export const CredentialGroup = HttpApiGroup.make("server.credential")
.add(
HttpApiEndpoint.post("credential.activate", "/api/credential/:credentialID/activate", {
params: { credentialID: Credential.ID },
query: LocationQuery,
success: HttpApiSchema.NoContent,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "credential.activate",
@@ -39,10 +34,8 @@ export const CredentialGroup = HttpApiGroup.make("server.credential")
.add(
HttpApiEndpoint.delete("credential.remove", "/api/credential/:credentialID", {
params: { credentialID: Credential.ID },
query: LocationQuery,
success: HttpApiSchema.NoContent,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "credential.remove",
+11 -4
View File
@@ -3,7 +3,12 @@ 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 { IntegrationNotFoundError, InvalidRequestError } from "../errors.js"
import {
IntegrationAttemptNotFoundError,
IntegrationMethodNotFoundError,
IntegrationNotFoundError,
InvalidRequestError,
} from "../errors.js"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
export const IntegrationGroup = HttpApiGroup.make("server.integration")
@@ -63,7 +68,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
label: Schema.optional(Schema.String),
}),
success: HttpApiSchema.NoContent,
error: InvalidRequestError,
error: [IntegrationNotFoundError, InvalidRequestError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
@@ -100,6 +105,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],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
@@ -119,7 +125,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
query: LocationQuery,
payload: Schema.Struct({ code: Schema.optional(Schema.String) }),
success: HttpApiSchema.NoContent,
error: InvalidRequestError,
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, InvalidRequestError],
},
)
.annotateMerge(locationQueryOpenApi)
@@ -155,7 +161,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
label: Schema.optional(Schema.String),
}),
success: Location.response(Integration.CommandAttempt),
error: InvalidRequestError,
error: [IntegrationNotFoundError, IntegrationMethodNotFoundError, InvalidRequestError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
@@ -171,6 +177,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],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
+8 -8
View File
@@ -21,7 +21,7 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
),
)
.add(
HttpApiEndpoint.put("mcp.add", "/api/mcp/:server", {
HttpApiEndpoint.put("mcp.add", "/api/experimental/mcp/:server", {
params: { server: Schema.String },
query: LocationQuery,
// Wrapped in a struct because the client codegen flattens payload fields and cannot
@@ -32,14 +32,14 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "mcp.add",
identifier: "experimental.mcp.add",
summary: "Add MCP server",
description: "Add an MCP server at runtime or replace an existing one, connecting it immediately.",
}),
),
)
.add(
HttpApiEndpoint.delete("mcp.remove", "/api/mcp/:server", {
HttpApiEndpoint.delete("mcp.remove", "/api/experimental/mcp/:server", {
params: { server: Schema.String },
query: LocationQuery,
success: HttpApiSchema.NoContent,
@@ -48,14 +48,14 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "mcp.remove",
identifier: "experimental.mcp.remove",
summary: "Remove MCP server",
description: "Stop an MCP server and remove it from the runtime set until restart.",
}),
),
)
.add(
HttpApiEndpoint.post("mcp.connect", "/api/mcp/:server/connect", {
HttpApiEndpoint.post("mcp.connect", "/api/experimental/mcp/:server/connect", {
params: { server: Schema.String },
query: LocationQuery,
success: HttpApiSchema.NoContent,
@@ -64,14 +64,14 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "mcp.connect",
identifier: "experimental.mcp.connect",
summary: "Connect MCP server",
description: "Connect an MCP server at runtime, overriding a disabled configuration until restart.",
}),
),
)
.add(
HttpApiEndpoint.post("mcp.disconnect", "/api/mcp/:server/disconnect", {
HttpApiEndpoint.post("mcp.disconnect", "/api/experimental/mcp/:server/disconnect", {
params: { server: Schema.String },
query: LocationQuery,
success: HttpApiSchema.NoContent,
@@ -80,7 +80,7 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "mcp.disconnect",
identifier: "experimental.mcp.disconnect",
summary: "Disconnect MCP server",
description: "Disconnect an MCP server at runtime, removing its tools until reconnected.",
}),
+5 -4
View File
@@ -175,7 +175,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
),
)
.add(
HttpApiEndpoint.get("session.stats", "/api/session/stats", {
HttpApiEndpoint.get("session.stats", "/api/experimental/session/stats", {
query: Schema.Struct({
from: Schema.NumberFromString.pipe(Schema.optional),
to: Schema.NumberFromString.pipe(Schema.optional),
@@ -187,7 +187,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
error: InvalidRequestError,
}).annotateMerge(
OpenApi.annotations({
identifier: "session.stats",
identifier: "experimental.session.stats",
summary: "Get session statistics",
description: "Aggregate local session activity, usage, and tool reliability for a time range.",
}),
@@ -285,7 +285,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
.add(
HttpApiEndpoint.post("session.fork", "/api/session/:sessionID/fork", {
params: { sessionID: Session.ID },
payload: Schema.Struct({ boundary: Session.ForkRequestBoundary }),
payload: Schema.Struct({ before: SessionMessage.ID.pipe(Schema.optional) }),
success: Schema.Struct({ data: PublicSessionInfo }),
error: [SessionNotFoundError, MessageNotFoundError, InvalidRequestError],
})
@@ -294,7 +294,8 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
OpenApi.annotations({
identifier: "session.fork",
summary: "Fork session",
description: "Create a child session by copying projected history through or before a message boundary.",
description:
"Create a child session by copying projected history before a message. Omit before to copy the full history.",
}),
),
)
-6
View File
@@ -8,9 +8,3 @@ export const Boundary = Schema.Union([
Schema.Struct({ type: Schema.Literal("through"), messageID: SessionMessage.ID }),
]).annotate({ identifier: "Session.ForkBoundary" })
export type Boundary = typeof Boundary.Type
export const RequestBoundary = Schema.Union([
Schema.Struct({ type: Schema.Literal("before"), messageID: SessionMessage.ID }),
Schema.Struct({ type: Schema.Literal("through") }),
]).annotate({ identifier: "Session.ForkRequestBoundary" })
export type RequestBoundary = typeof RequestBoundary.Type
-2
View File
@@ -26,8 +26,6 @@ export const Event = SessionEvent
export { Revert }
export const ForkBoundary = SessionFork.Boundary
export type ForkBoundary = SessionFork.Boundary
export const ForkRequestBoundary = SessionFork.RequestBoundary
export type ForkRequestBoundary = SessionFork.RequestBoundary
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
+3 -1
View File
@@ -26,7 +26,9 @@ export const Result = Schema.Struct({
title: Schema.String.pipe(optional),
content: Schema.String.pipe(optional),
time: Schema.Struct({
published: Schema.Finite.pipe(optional),
published: Schema.Finite.pipe(optional).annotate({
description: "Publication time in milliseconds since the Unix epoch",
}),
}),
}).annotate({ identifier: "WebSearch.Result" })
+1 -1
View File
@@ -112,7 +112,7 @@ test("Promise instances are lazy, share by key and Location, and stay isolated b
)
await first.sessions.switchAgent({ sessionID, agent: "plan" })
const fork = await first.sessions.fork({ sessionID, boundary: { type: "through" } })
const fork = await first.sessions.fork({ sessionID })
expect(fork.metadata).toEqual(original.metadata)
expect(fork.location).toEqual(original.location)
expect(fork.fork?.sessionID).toBe(sessionID)
+7 -7
View File
@@ -1,4 +1,4 @@
import { Integration } from "@opencode/core/integration"
import { Credential } from "@opencode/core/credential"
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
@@ -8,24 +8,24 @@ export const CredentialHandler = HttpApiBuilder.group(Api, "server.credential",
.handle(
"credential.update",
Effect.fn(function* (ctx) {
const integration = yield* Integration.Service
yield* integration.connection.update(ctx.params.credentialID, { label: ctx.payload.label })
const credential = yield* Credential.Service
yield* credential.update(ctx.params.credentialID, { label: ctx.payload.label })
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"credential.activate",
Effect.fn(function* (ctx) {
const integration = yield* Integration.Service
yield* integration.connection.activate(ctx.params.credentialID)
const credential = yield* Credential.Service
yield* credential.activate(ctx.params.credentialID)
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"credential.remove",
Effect.fn(function* (ctx) {
const integration = yield* Integration.Service
yield* integration.connection.remove(ctx.params.credentialID)
const credential = yield* Credential.Service
yield* credential.remove(ctx.params.credentialID)
return HttpApiSchema.NoContent.make()
}),
),
+86 -22
View File
@@ -2,7 +2,12 @@ import { Integration } from "@opencode/core/integration"
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
import { IntegrationNotFoundError, InvalidRequestError } from "@opencode/protocol/errors"
import {
IntegrationAttemptNotFoundError,
IntegrationMethodNotFoundError,
IntegrationNotFoundError,
InvalidRequestError,
} from "@opencode/protocol/errors"
import { response } from "../location"
import { WellKnown } from "@opencode/core/wellknown"
@@ -61,6 +66,11 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
"integration.connect.key",
Effect.fn(function* (ctx) {
const service = yield* Integration.Service
if (!(yield* service.get(ctx.params.integrationID)))
return yield* new IntegrationNotFoundError({
integrationID: ctx.params.integrationID,
message: `Integration not found: ${ctx.params.integrationID}`,
})
yield* authorize(
service.connection.key({
integrationID: ctx.params.integrationID,
@@ -92,11 +102,27 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
"integration.oauth.status",
Effect.fn(function* (ctx) {
const service = yield* Integration.Service
return yield* response(
service.oauth.status({
if (!(yield* service.get(ctx.params.integrationID)))
return yield* new IntegrationNotFoundError({
integrationID: ctx.params.integrationID,
attemptID: ctx.params.attemptID,
}),
message: `Integration not found: ${ctx.params.integrationID}`,
})
return yield* response(
service.oauth
.status({
integrationID: ctx.params.integrationID,
attemptID: ctx.params.attemptID,
})
.pipe(
Effect.mapError(
(error) =>
new IntegrationAttemptNotFoundError({
integrationID: error.integrationID,
attemptID: error.attemptID,
message: `OAuth attempt not found: ${error.attemptID}`,
}),
),
),
)
}),
)
@@ -104,6 +130,11 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
"integration.oauth.complete",
Effect.fn(function* (ctx) {
const service = yield* Integration.Service
if (!(yield* service.get(ctx.params.integrationID)))
return yield* new IntegrationNotFoundError({
integrationID: ctx.params.integrationID,
message: `Integration not found: ${ctx.params.integrationID}`,
})
yield* service.oauth
.complete({
integrationID: ctx.params.integrationID,
@@ -111,19 +142,24 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
code: ctx.payload.code,
})
.pipe(
Effect.mapError(
(error) =>
new InvalidRequestError({
message:
error._tag === "Integration.CodeRequired"
? "Authorization code is required"
: "Authentication failed",
kind:
error._tag === "Integration.CodeRequired"
? "integration_code_required"
: "integration_authorization",
}),
),
Effect.mapError((error) => {
if (error._tag === "Integration.AttemptNotFound")
return new IntegrationAttemptNotFoundError({
integrationID: error.integrationID,
attemptID: error.attemptID,
message: `OAuth attempt not found: ${error.attemptID}`,
})
return new InvalidRequestError({
message:
error._tag === "Integration.CodeRequired"
? "Authorization code is required"
: "Authentication failed",
kind:
error._tag === "Integration.CodeRequired"
? "integration_code_required"
: "integration_authorization",
})
}),
)
return HttpApiSchema.NoContent.make()
}),
@@ -143,6 +179,18 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
"integration.command.connect",
Effect.fn(function* (ctx) {
const service = yield* Integration.Service
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}`,
})
if (!integration.methods.some((method) => method.type === "command" && method.id === ctx.payload.methodID))
return yield* new IntegrationMethodNotFoundError({
integrationID: ctx.params.integrationID,
methodID: ctx.payload.methodID,
message: `Integration method not found: ${ctx.payload.methodID}`,
})
return yield* response(
authorize(
service.command.connect({
@@ -158,11 +206,27 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
"integration.command.status",
Effect.fn(function* (ctx) {
const service = yield* Integration.Service
return yield* response(
service.command.status({
if (!(yield* service.get(ctx.params.integrationID)))
return yield* new IntegrationNotFoundError({
integrationID: ctx.params.integrationID,
attemptID: ctx.params.attemptID,
}),
message: `Integration not found: ${ctx.params.integrationID}`,
})
return yield* response(
service.command
.status({
integrationID: ctx.params.integrationID,
attemptID: ctx.params.attemptID,
})
.pipe(
Effect.mapError(
(error) =>
new IntegrationAttemptNotFoundError({
integrationID: error.integrationID,
attemptID: error.attemptID,
message: `Command attempt not found: ${error.attemptID}`,
}),
),
),
)
}),
)
+1 -1
View File
@@ -209,7 +209,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
"session.fork",
Effect.fn(function* (ctx) {
return {
data: yield* session.fork({ sessionID: ctx.params.sessionID, boundary: ctx.payload.boundary }).pipe(
data: yield* session.fork({ sessionID: ctx.params.sessionID, before: ctx.payload.before }).pipe(
Effect.catchTag("Session.NotFoundError", missingSession),
Effect.catchTag("Session.MessageNotFoundError", missingMessage),
Effect.catchTag(
+1
View File
@@ -160,6 +160,7 @@ function makeRoutes<AuthError, AuthServices>(
Layer.succeedContext(
Context.pick(
Database.Service,
Credential.Service,
PermissionSaved.Service,
PluginUpdate.Service,
Project.Service,
+2 -2
View File
@@ -680,7 +680,7 @@ function App(props: { pair?: DialogPairCredentials }) {
return
}
void client.api.session
.fork({ sessionID: match, boundary: { type: "through" } })
.fork({ sessionID: match })
.then((result) => route.navigate({ type: "session", sessionID: result.id, prompt: startupPrompt }))
.catch(toast.error)
})
@@ -693,7 +693,7 @@ function App(props: { pair?: DialogPairCredentials }) {
if (forked || !args.sessionID || !args.fork) return
forked = true
void client.api.session
.fork({ sessionID: args.sessionID, boundary: { type: "through" } })
.fork({ sessionID: args.sessionID })
.then((result) => route.navigate({ type: "session", sessionID: result.id, prompt: startupPrompt }))
.catch(toast.error)
})
@@ -200,7 +200,7 @@ function manageConnections(
onSelect: () => {
if (credentialConnections(current() ?? integration)[0]?.id === connection.id) return
void client.api.credential
.activate({ credentialID: connection.id, location: locationQuery(location) })
.activate({ credentialID: connection.id })
.catch(toast.error)
},
}
@@ -224,7 +224,7 @@ function manageConnections(
const label = value.trim()
if (!label) return
void client.api.credential
.update({ credentialID: option.value, label, location: locationQuery(location) })
.update({ credentialID: option.value, label })
.then(() => manageConnections(integration, methods, location, dialog, onConnected))
.catch(toast.error)
}}
@@ -241,7 +241,7 @@ function manageConnections(
if (deleting() !== option.value) return setDeleting(option.value)
const final = credentialConnections(current() ?? integration).length === 1
void client.api.credential
.remove({ credentialID: option.value, location: locationQuery(location) })
.remove({ credentialID: option.value })
.then(() => {
setDeleting(undefined)
if (!final) return
@@ -23,7 +23,7 @@ export function DialogFork(props: { sessionID: string; messageID?: string; onMov
const result = await client.api.session
.fork({
sessionID: props.sessionID,
boundary: messageID ? { type: "before", messageID } : { type: "through" },
before: messageID,
})
.catch((error) => {
toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })
@@ -112,7 +112,7 @@ async function renderMcp(options?: { failed?: boolean; location?: { directory: s
if (request.method === "DELETE") return new Response(null, { status: 204 })
return json({ location, data: { status: "pending" } })
}
if (url.pathname === "/api/mcp/linear/connect" && request.method === "POST") {
if (url.pathname === "/api/experimental/mcp/linear/connect" && request.method === "POST") {
connect++
return new Response(null, { status: 204 })
}
+1 -1
View File
@@ -11,7 +11,7 @@ test("stats shows only this year and returns after errors or success", async ()
setup.renderer.start()
const requests: URL[] = []
const calls = createFetch((url) => {
if (url.pathname !== "/api/session/stats") return undefined
if (url.pathname !== "/api/experimental/session/stats") return undefined
requests.push(url)
if (requests.length === 1) return json({ message: "offline" }, { status: 503 })
return json({
+138 -133
View File
@@ -931,10 +931,10 @@
}
}
},
"/api/session/stats": {
"/api/experimental/session/stats": {
"get": {
"tags": ["session"],
"operationId": "session.stats",
"operationId": "experimental.session.stats",
"parameters": [
{
"name": "from",
@@ -1519,7 +1519,7 @@
}
}
},
"description": "Create a child session by copying projected history through or before a message boundary.",
"description": "Create a child session by copying projected history before a message. Omit before to copy the full history.",
"summary": "Fork session",
"requestBody": {
"content": {
@@ -1527,11 +1527,18 @@
"schema": {
"type": "object",
"properties": {
"boundary": {
"$ref": "#/components/schemas/Session.ForkRequestBoundary"
"before": {
"anyOf": [
{
"type": "string",
"pattern": "^msg_"
},
{
"type": "null"
}
]
}
},
"required": ["boundary"],
"additionalProperties": false
}
}
@@ -5373,6 +5380,16 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
}
}
}
}
},
"description": "Run a key authentication method and store the resulting credential.",
@@ -5644,6 +5661,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Poll the current status of an OAuth attempt.",
@@ -5813,6 +5847,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Complete a code-based OAuth attempt and store the resulting credential.",
@@ -5934,6 +5985,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationMethodNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationMethodNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Start a command authentication attempt.",
@@ -6060,6 +6128,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Poll the current status and output of a command authentication attempt.",
@@ -6232,10 +6317,10 @@
"summary": "List MCP servers"
}
},
"/api/mcp/{server}": {
"/api/experimental/mcp/{server}": {
"put": {
"tags": ["mcp"],
"operationId": "mcp.add",
"operationId": "experimental.mcp.add",
"parameters": [
{
"name": "server",
@@ -6331,7 +6416,7 @@
},
"delete": {
"tags": ["mcp"],
"operationId": "mcp.remove",
"operationId": "experimental.mcp.remove",
"parameters": [
{
"name": "server",
@@ -6412,10 +6497,10 @@
"summary": "Remove MCP server"
}
},
"/api/mcp/{server}/connect": {
"/api/experimental/mcp/{server}/connect": {
"post": {
"tags": ["mcp"],
"operationId": "mcp.connect",
"operationId": "experimental.mcp.connect",
"parameters": [
{
"name": "server",
@@ -6496,10 +6581,10 @@
"summary": "Connect MCP server"
}
},
"/api/mcp/{server}/disconnect": {
"/api/experimental/mcp/{server}/disconnect": {
"post": {
"tags": ["mcp"],
"operationId": "mcp.disconnect",
"operationId": "experimental.mcp.disconnect",
"parameters": [
{
"name": "server",
@@ -6675,36 +6760,6 @@
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
@@ -6764,36 +6819,6 @@
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
@@ -6838,36 +6863,6 @@
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
@@ -15121,6 +15116,46 @@
"required": ["id", "type", "label"],
"additionalProperties": false
},
"IntegrationAttemptNotFoundErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["IntegrationAttemptNotFoundError"]
},
"integrationID": {
"type": "string"
},
"attemptID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "integrationID", "attemptID", "message"],
"additionalProperties": false
},
"IntegrationMethodNotFoundErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["IntegrationMethodNotFoundError"]
},
"integrationID": {
"type": "string"
},
"methodID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "integrationID", "methodID", "message"],
"additionalProperties": false
},
"IntegrationNotFoundErrorEncoded": {
"type": "object",
"properties": {
@@ -16960,36 +16995,6 @@
}
]
},
"Session.ForkRequestBoundary": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["before"]
},
"messageID": {
"type": "string",
"pattern": "^msg_"
}
},
"required": ["type", "messageID"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["through"]
}
},
"required": ["type"],
"additionalProperties": false
}
]
},
"Session.Inbox.Compaction": {
"type": "object",
"properties": {
+138 -133
View File
@@ -931,10 +931,10 @@
}
}
},
"/api/session/stats": {
"/api/experimental/session/stats": {
"get": {
"tags": ["session"],
"operationId": "session.stats",
"operationId": "experimental.session.stats",
"parameters": [
{
"name": "from",
@@ -1519,7 +1519,7 @@
}
}
},
"description": "Create a child session by copying projected history through or before a message boundary.",
"description": "Create a child session by copying projected history before a message. Omit before to copy the full history.",
"summary": "Fork session",
"requestBody": {
"content": {
@@ -1527,11 +1527,18 @@
"schema": {
"type": "object",
"properties": {
"boundary": {
"$ref": "#/components/schemas/Session.ForkRequestBoundary"
"before": {
"anyOf": [
{
"type": "string",
"pattern": "^msg_"
},
{
"type": "null"
}
]
}
},
"required": ["boundary"],
"additionalProperties": false
}
}
@@ -5373,6 +5380,16 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
}
}
}
}
},
"description": "Run a key authentication method and store the resulting credential.",
@@ -5644,6 +5661,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Poll the current status of an OAuth attempt.",
@@ -5813,6 +5847,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Complete a code-based OAuth attempt and store the resulting credential.",
@@ -5934,6 +5985,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationMethodNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationMethodNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Start a command authentication attempt.",
@@ -6060,6 +6128,23 @@
}
}
}
},
"404": {
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
},
{
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
}
]
}
}
}
}
},
"description": "Poll the current status and output of a command authentication attempt.",
@@ -6232,10 +6317,10 @@
"summary": "List MCP servers"
}
},
"/api/mcp/{server}": {
"/api/experimental/mcp/{server}": {
"put": {
"tags": ["mcp"],
"operationId": "mcp.add",
"operationId": "experimental.mcp.add",
"parameters": [
{
"name": "server",
@@ -6331,7 +6416,7 @@
},
"delete": {
"tags": ["mcp"],
"operationId": "mcp.remove",
"operationId": "experimental.mcp.remove",
"parameters": [
{
"name": "server",
@@ -6412,10 +6497,10 @@
"summary": "Remove MCP server"
}
},
"/api/mcp/{server}/connect": {
"/api/experimental/mcp/{server}/connect": {
"post": {
"tags": ["mcp"],
"operationId": "mcp.connect",
"operationId": "experimental.mcp.connect",
"parameters": [
{
"name": "server",
@@ -6496,10 +6581,10 @@
"summary": "Connect MCP server"
}
},
"/api/mcp/{server}/disconnect": {
"/api/experimental/mcp/{server}/disconnect": {
"post": {
"tags": ["mcp"],
"operationId": "mcp.disconnect",
"operationId": "experimental.mcp.disconnect",
"parameters": [
{
"name": "server",
@@ -6675,36 +6760,6 @@
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
@@ -6764,36 +6819,6 @@
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
@@ -6838,36 +6863,6 @@
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
@@ -15121,6 +15116,46 @@
"required": ["id", "type", "label"],
"additionalProperties": false
},
"IntegrationAttemptNotFoundErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["IntegrationAttemptNotFoundError"]
},
"integrationID": {
"type": "string"
},
"attemptID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "integrationID", "attemptID", "message"],
"additionalProperties": false
},
"IntegrationMethodNotFoundErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["IntegrationMethodNotFoundError"]
},
"integrationID": {
"type": "string"
},
"methodID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "integrationID", "methodID", "message"],
"additionalProperties": false
},
"IntegrationNotFoundErrorEncoded": {
"type": "object",
"properties": {
@@ -16960,36 +16995,6 @@
}
]
},
"Session.ForkRequestBoundary": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["before"]
},
"messageID": {
"type": "string",
"pattern": "^msg_"
}
},
"required": ["type", "messageID"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["through"]
}
},
"required": ["type"],
"additionalProperties": false
}
]
},
"Session.Inbox.Compaction": {
"type": "object",
"properties": {