refactor(core): canonicalize pty service (#32182)
This commit is contained in:
parent
7efade2d53
commit
f2cf607376
30 changed files with 1132 additions and 504 deletions
144
packages/server/src/groups/pty.ts
Normal file
144
packages/server/src/groups/pty.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { Pty } from "@opencode-ai/core/pty"
|
||||
import { PtyID } from "@opencode-ai/core/pty/schema"
|
||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ForbiddenError, PtyNotFoundError } from "../errors"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const PTY_CONNECT_TICKET_QUERY = "ticket"
|
||||
export const PTY_CONNECT_TOKEN_HEADER = "x-opencode-ticket"
|
||||
export const PTY_CONNECT_TOKEN_HEADER_VALUE = "1"
|
||||
|
||||
const PTY_CONNECT_PATH = /^\/api\/pty\/[^/]+\/connect$/
|
||||
|
||||
// Authorization middleware skips credential checks when this matches; the PTY connect handler
|
||||
// is then responsible for consuming and validating the ticket.
|
||||
export function hasPtyConnectTicketURL(url: URL) {
|
||||
return PTY_CONNECT_PATH.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
}
|
||||
|
||||
export const PtyGroup = HttpApiGroup.make("server.pty")
|
||||
.add(
|
||||
HttpApiEndpoint.get("pty.list", "/api/pty", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(Pty.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.list",
|
||||
summary: "List PTY sessions",
|
||||
description: "List PTY sessions for a location, including exited sessions retained until removal.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("pty.create", "/api/pty", {
|
||||
query: LocationQuery,
|
||||
payload: Pty.CreateInput,
|
||||
success: Location.response(Pty.Info),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.create",
|
||||
summary: "Create PTY session",
|
||||
description: "Create a pseudo-terminal session for a location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("pty.get", "/api/pty/:ptyID", {
|
||||
params: { ptyID: PtyID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(Pty.Info),
|
||||
error: PtyNotFoundError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.get",
|
||||
summary: "Get PTY session",
|
||||
description: "Get one PTY session, including its exit code once exited.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("pty.update", "/api/pty/:ptyID", {
|
||||
params: { ptyID: PtyID },
|
||||
query: LocationQuery,
|
||||
payload: Pty.UpdateInput,
|
||||
success: Location.response(Pty.Info),
|
||||
error: PtyNotFoundError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.update",
|
||||
summary: "Update PTY session",
|
||||
description: "Update the title or viewport size of one PTY session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("pty.remove", "/api/pty/:ptyID", {
|
||||
params: { ptyID: PtyID },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: PtyNotFoundError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.remove",
|
||||
summary: "Remove PTY session",
|
||||
description: "Terminate and remove one PTY session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("pty.connectToken", "/api/pty/:ptyID/connect-token", {
|
||||
params: { ptyID: PtyID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(PtyTicket.ConnectToken),
|
||||
error: [ForbiddenError, PtyNotFoundError],
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.connectToken",
|
||||
summary: "Create PTY WebSocket token",
|
||||
description: "Create a short-lived single-use ticket for opening a PTY WebSocket connection.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
// Query fields are decoded in the raw handler after the existence check so a missing
|
||||
// session responds with an empty 404 before any upgrade work.
|
||||
HttpApiEndpoint.get("pty.connect", "/api/pty/:ptyID/connect", {
|
||||
params: { ptyID: PtyID },
|
||||
success: Schema.Boolean,
|
||||
error: [ForbiddenError, PtyNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.connect",
|
||||
summary: "Connect to PTY session",
|
||||
description: "Establish a WebSocket connection streaming PTY output and accepting terminal input.",
|
||||
transform: (operation) => ({
|
||||
...operation,
|
||||
parameters: [
|
||||
...(operation.parameters ?? []),
|
||||
...["location[directory]", "location[workspace]", "cursor", PTY_CONNECT_TICKET_QUERY].map((name) => ({
|
||||
in: "query",
|
||||
name,
|
||||
schema: { type: "string" },
|
||||
})),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "pty", description: "Experimental location-scoped PTY routes." }))
|
||||
.middleware(LocationMiddleware)
|
||||
Loading…
Add table
Add a link
Reference in a new issue