Files
PaperClipAI/packages/adapter-utils/src/runner-connectivity.ts
T
DottaandPaperclip f6aade3bae Add persistent Boat computer environments and runner ownership
Introduce company-scoped computer attachments, bounded runner and viewer ownership, persistent files and worktrees, Boat ingress, and native and legacy runtime integration behind the experimental Boat flag. Keep desktop UI and extended acceptance cases in the stacked feature branch.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-10 10:07:51 -05:00

191 lines
6.0 KiB
TypeScript

import type { AdapterExecutionTarget } from "./execution-target.js";
import { adapterExecutionTargetIsCommandBacked } from "./execution-target-kind.js";
export const PAPERCLIP_RUNNER_INGRESS_PORT = 43_127;
export const PAPERCLIP_RUNNER_CONNECT_PATH_PREFIX = "/api/runner/v1/connect";
export interface SecretHeader {
readonly name: string;
readonly value: string;
}
export interface RunnerIngressEndpoint {
readonly kind: "authenticated_websocket";
readonly websocketUrl: string;
readonly secretHeaders: readonly SecretHeader[];
readonly generation: string;
refresh(): Promise<RunnerIngressEndpoint>;
close(): Promise<void>;
}
export type PaperclipRunnerTransport =
| {
readonly mode: "local_loopback";
readonly connectUrl: string;
}
| {
readonly mode: "direct_outbound";
readonly connectUrl: string;
readonly caBundlePath?: string;
}
| {
readonly mode: "provider_ingress";
readonly listenAddress: "0.0.0.0";
readonly listenPort: number;
readonly listenPath: string;
readonly ingress: RunnerIngressEndpoint;
};
type RunnerIngressAuthorization =
| {
/** Per-run authorization resolved by the native runtime selection policy. */
readonly runnerIngressAuthorized: boolean;
/** @deprecated Use runnerIngressAuthorized. Retained for API compatibility. */
readonly enableRunnerPreviewIngress?: boolean;
}
| {
readonly runnerIngressAuthorized?: never;
/** @deprecated Use runnerIngressAuthorized. Retained for API compatibility. */
readonly enableRunnerPreviewIngress: boolean;
};
export class PaperclipRunnerTransportError extends Error {
readonly code:
| "runner_transport_ineligible"
| "runner_direct_wss_failed"
| "runner_ingress_unavailable";
constructor(
code: PaperclipRunnerTransportError["code"],
message: string,
options?: ErrorOptions,
) {
super(`${code}: ${message}`, options);
this.name = "PaperclipRunnerTransportError";
this.code = code;
}
}
function connectPath(runId: string): string {
if (!runId || runId.includes("/") || runId.includes("?") || runId.includes("#")) {
throw new PaperclipRunnerTransportError(
"runner_transport_ineligible",
"Runner run id is not safe for a WebSocket route.",
);
}
return `${PAPERCLIP_RUNNER_CONNECT_PATH_PREFIX}/${encodeURIComponent(runId)}`;
}
export function buildDirectRunnerConnectUrl(input: {
runnerPublicUrl: string;
runId: string;
}): string {
let url: URL;
try {
url = new URL(input.runnerPublicUrl);
} catch (error) {
throw new PaperclipRunnerTransportError(
"runner_direct_wss_failed",
"The configured runner public URL is invalid.",
{ cause: error },
);
}
if (url.protocol !== "wss:") {
throw new PaperclipRunnerTransportError(
"runner_direct_wss_failed",
"Remote runner connectivity requires a wss: runner public URL.",
);
}
if (url.username || url.password || url.search || url.hash) {
throw new PaperclipRunnerTransportError(
"runner_direct_wss_failed",
"Runner public URLs cannot contain userinfo, a query string, or a fragment.",
);
}
url.pathname = `${url.pathname.replace(/\/$/, "")}${connectPath(input.runId)}`;
return url.toString();
}
export async function resolvePaperclipRunnerTransport(input: {
target: AdapterExecutionTarget;
runId: string;
localConnectUrl: string;
runnerPublicUrl?: string | null;
runnerCaBundlePath?: string | null;
getRunnerIngressEndpoint?: (input: {
leaseId: string;
port: number;
path: string;
}) => Promise<RunnerIngressEndpoint>;
} & RunnerIngressAuthorization): Promise<PaperclipRunnerTransport> {
if (input.target.kind === "local") {
return { mode: "local_loopback", connectUrl: input.localConnectUrl };
}
if (
input.target.transport === "sandbox" &&
input.target.providerKey === "daytona" &&
input.target.effectiveCapabilities?.runnerWebSocketIngress !== true
) {
throw new PaperclipRunnerTransportError(
"runner_ingress_unavailable",
"Daytona runner execution requires provider WebSocket ingress capability.",
);
}
if (
adapterExecutionTargetIsCommandBacked(input.target) &&
input.target.effectiveCapabilities?.runnerWebSocketIngress === true
) {
const ingressAuthorized =
input.runnerIngressAuthorized ?? input.enableRunnerPreviewIngress ?? false;
if (!ingressAuthorized) {
throw new PaperclipRunnerTransportError(
"runner_ingress_unavailable",
"Runner ingress is not authorized for this Paperclip Runner run.",
);
}
const getRunnerIngressEndpoint =
input.getRunnerIngressEndpoint ?? input.target.getRunnerIngressEndpoint;
if (!input.target.leaseId || !getRunnerIngressEndpoint) {
throw new PaperclipRunnerTransportError(
"runner_ingress_unavailable",
"The sandbox runner ingress provider is unavailable for this lease.",
);
}
const port = input.target.transport === "computer" ? input.target.listenerPort : PAPERCLIP_RUNNER_INGRESS_PORT;
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new PaperclipRunnerTransportError("runner_ingress_unavailable", "Invalid allocated runner port.");
const path = connectPath(input.runId);
const ingress = await getRunnerIngressEndpoint({
leaseId: input.target.leaseId,
port,
path,
});
return {
mode: "provider_ingress",
listenAddress: "0.0.0.0",
listenPort: port,
listenPath: path,
ingress,
};
}
if (input.runnerPublicUrl) {
return {
mode: "direct_outbound",
connectUrl: buildDirectRunnerConnectUrl({
runnerPublicUrl: input.runnerPublicUrl,
runId: input.runId,
}),
...(input.runnerCaBundlePath
? { caBundlePath: input.runnerCaBundlePath }
: {}),
};
}
throw new PaperclipRunnerTransportError(
"runner_transport_ineligible",
"The remote execution target has neither runner WebSocket ingress nor an explicit runner public URL.",
);
}