| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142 |
- import { Pty } from "@opencode-ai/schema/pty"
- import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
- import { Location } from "@opencode-ai/schema/location"
- import { Schema } from "effect"
- import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
- import { ForbiddenError, PtyNotFoundError } from "../errors"
- import { LocationQuery, locationQueryOpenApi } 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: Pty.ID },
- 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: Pty.ID },
- 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: Pty.ID },
- 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: Pty.ID },
- 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: Pty.ID },
- 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." }))
|