diff --git a/examples/next-js/src/lib/rivet-client.ts b/examples/next-js/src/lib/rivet-client.ts index 5c05d4d35..e687b3c66 100644 --- a/examples/next-js/src/lib/rivet-client.ts +++ b/examples/next-js/src/lib/rivet-client.ts @@ -2,7 +2,7 @@ import { createClient, createRivetKit } from "@rivetkit/next-js/client"; import type { registry } from "@/rivet/registry"; -// TODO: Auto-trigger start by sending request to health endpoint - -const client = createClient(); +const client = createClient( + process.env.NEXT_RIVET_ENDPOINT ?? "http://localhost:3000/api/rivet", +); export const { useActor } = createRivetKit(client); diff --git a/packages/next-js/src/mod.ts b/packages/next-js/src/mod.ts index 25da837c2..a206fadff 100644 --- a/packages/next-js/src/mod.ts +++ b/packages/next-js/src/mod.ts @@ -8,15 +8,20 @@ export const toNextHandler = ( inputConfig.disableDefaultServer = true; // Configure serverless - const publicUrl = - process.env.NEXT_PUBLIC_SITE_URL ?? - process.env.NEXT_PUBLIC_VERCEL_URL ?? - `http://127.0.0.1:${process.env.PORT ?? 3000}`; inputConfig.runnerKind = "serverless"; - inputConfig.runEngine = true; - inputConfig.autoConfigureServerless = { - url: `${publicUrl}/api/rivet/start`, - }; + + // Auto-configure serverless runner if not in prod + if (process.env.NODE_ENV !== "production") { + const publicUrl = + process.env.NEXT_PUBLIC_SITE_URL ?? + process.env.NEXT_PUBLIC_VERCEL_URL ?? + `http://127.0.0.1:${process.env.PORT ?? 3000}`; + + inputConfig.runEngine = true; + inputConfig.autoConfigureServerless = { + url: `${publicUrl}/api/rivet/start`, + }; + } // Next logs this on every request inputConfig.noWelcome = true; diff --git a/packages/rivetkit/scripts/dump-openapi.ts b/packages/rivetkit/scripts/dump-openapi.ts index bf6fb7dda..970278043 100644 --- a/packages/rivetkit/scripts/dump-openapi.ts +++ b/packages/rivetkit/scripts/dump-openapi.ts @@ -1,5 +1,6 @@ import * as fs from "node:fs/promises"; import { resolve } from "node:path"; +import { ClientConfigSchema } from "@/client/config"; import { createFileSystemOrMemoryDriver } from "@/drivers/file-system/mod"; import type { ManagerDriver } from "@/manager/driver"; import { createManagerRouter } from "@/manager/router"; @@ -39,7 +40,10 @@ function main() { getOrCreateInspectorAccessToken: unimplemented, }; - const client = createClientWithDriver(managerDriver); + const client = createClientWithDriver( + managerDriver, + ClientConfigSchema.parse({}), + ); const { openapi } = createManagerRouter( registryConfig, diff --git a/packages/rivetkit/src/actor/errors.ts b/packages/rivetkit/src/actor/errors.ts index 60c564bb1..fdf3a3f35 100644 --- a/packages/rivetkit/src/actor/errors.ts +++ b/packages/rivetkit/src/actor/errors.ts @@ -54,17 +54,6 @@ export class ActorError extends Error { // Force stringify to return the message return this.message; } - - /** - * Serialize error for HTTP response - */ - serializeForHttp() { - return { - type: this.code, - message: this.message, - metadata: this.metadata, - }; - } } export class InternalError extends ActorError { diff --git a/packages/rivetkit/src/client/client.ts b/packages/rivetkit/src/client/client.ts index 8ba0c2ee2..bb7500207 100644 --- a/packages/rivetkit/src/client/client.ts +++ b/packages/rivetkit/src/client/client.ts @@ -168,16 +168,12 @@ export class ClientRaw { /** * Creates an instance of Client. - * - * @param {string} managerEndpoint - The manager endpoint. See {@link https://rivet.dev/docs/setup|Initial Setup} for instructions on getting the manager endpoint. - * @param {ClientConfig} [opts] - Options for configuring the client. - * @see {@link https://rivet.dev/docs/setup|Initial Setup} */ - public constructor(driver: ManagerDriver, opts?: ClientConfig) { + public constructor(driver: ManagerDriver, config: ClientConfig) { this.#driver = driver; - this.#encodingKind = opts?.encoding ?? "bare"; - this[TRANSPORT_SYMBOL] = opts?.transport ?? "websocket"; + this.#encodingKind = config.encoding ?? "bare"; + this[TRANSPORT_SYMBOL] = config.transport ?? "websocket"; } /** @@ -406,7 +402,7 @@ export type AnyClient = Client>; export function createClientWithDriver>( driver: ManagerDriver, - config?: ClientConfig, + config: ClientConfig, ): Client { const client = new ClientRaw(driver, config); diff --git a/packages/rivetkit/src/client/config.ts b/packages/rivetkit/src/client/config.ts index b1211da57..e66dd7d00 100644 --- a/packages/rivetkit/src/client/config.ts +++ b/packages/rivetkit/src/client/config.ts @@ -39,6 +39,9 @@ export const ClientConfigSchema = z.object({ // See RunConfig.getUpgradeWebSocket getUpgradeWebSocket: z.custom().optional(), + + /** Whether to automatically perform health checks when the client is created. */ + disableHealthCheck: z.boolean().optional().default(false), }); export type ClientConfig = z.infer; diff --git a/packages/rivetkit/src/client/utils.ts b/packages/rivetkit/src/client/utils.ts index e58611d4f..e6919500f 100644 --- a/packages/rivetkit/src/client/utils.ts +++ b/packages/rivetkit/src/client/utils.ts @@ -8,6 +8,7 @@ import { HTTP_RESPONSE_ERROR_VERSIONED } from "@/schemas/client-protocol/version import { contentTypeForEncoding, deserializeWithEncoding, + encodingIsBinary, serializeWithEncoding, } from "@/serde"; import { httpUserAgent } from "@/utils"; @@ -120,14 +121,18 @@ export async function sendHttpRequest< ); } + // Decode metadata based on encoding - only binary encodings have CBOR-encoded metadata + let decodedMetadata: unknown; + if (responseData.metadata && encodingIsBinary(opts.encoding)) { + decodedMetadata = cbor.decode(new Uint8Array(responseData.metadata)); + } + // Throw structured error throw new ActorError( responseData.group, responseData.code, responseData.message, - responseData.metadata - ? cbor.decode(new Uint8Array(responseData.metadata)) - : undefined, + decodedMetadata, ); } diff --git a/packages/rivetkit/src/common/router.ts b/packages/rivetkit/src/common/router.ts index 180cadf4e..94e6dd3db 100644 --- a/packages/rivetkit/src/common/router.ts +++ b/packages/rivetkit/src/common/router.ts @@ -5,10 +5,12 @@ import { getRequestEncoding, getRequestExposeInternalError, } from "@/actor/router-endpoints"; +import type { RunnerConfig } from "@/registry/run-config"; +import { getEndpoint } from "@/remote-manager-driver/api-utils"; import { HttpResponseError } from "@/schemas/client-protocol/mod"; import { HTTP_RESPONSE_ERROR_VERSIONED } from "@/schemas/client-protocol/versioned"; import { encodingIsBinary, serializeWithEncoding } from "@/serde"; -import { bufferToArrayBuffer } from "@/utils"; +import { bufferToArrayBuffer, VERSION } from "@/utils"; import { getLogger, type Logger } from "./log"; import { deconstructError, stringifyError } from "./utils"; @@ -79,3 +81,50 @@ export function handleRouteError(error: unknown, c: HonoContext) { // TODO: Remove any return c.body(output as any, { status: statusCode }); } + +/** + * Metadata response interface for the /metadata endpoint + */ +export interface MetadataResponse { + runtime: string; + version: string; + runner?: { + kind: + | { serverless: Record } + | { normal: Record }; + }; + /** + * Endpoint that the client should connect to to access this runner. + * + * If defined, will override the endpoint the user has configured on startup. + * + * This is helpful if attempting to connect to a serverless runner, so the serverless runner can define where the main endpoint lives. + * + * This is also helpful for setting up clean redirects as needed. + **/ + clientEndpoint?: string; +} + +export function handleMetadataRequest(c: HonoContext, runConfig: RunnerConfig) { + const response: MetadataResponse = { + runtime: "rivetkit", + version: VERSION, + runner: { + kind: + runConfig.runnerKind === "serverless" + ? { serverless: {} } + : { normal: {} }, + }, + clientEndpoint: getEndpoint(runConfig), + }; + + return c.json(response); +} + +export function handleHealthRequest(c: HonoContext) { + return c.json({ + status: "ok", + runtime: "rivetkit", + version: VERSION, + }); +} diff --git a/packages/rivetkit/src/driver-test-suite/mod.ts b/packages/rivetkit/src/driver-test-suite/mod.ts index 90950ba14..915dff244 100644 --- a/packages/rivetkit/src/driver-test-suite/mod.ts +++ b/packages/rivetkit/src/driver-test-suite/mod.ts @@ -3,6 +3,7 @@ import { createNodeWebSocket, type NodeWebSocket } from "@hono/node-ws"; import { bundleRequire } from "bundle-require"; import invariant from "invariant"; import { describe } from "vitest"; +import { ClientConfigSchema } from "@/client/config"; import type { Encoding, Transport } from "@/client/mod"; import { configureInspectorAccessToken } from "@/inspector/utils"; import { createManagerRouter } from "@/manager/router"; @@ -230,7 +231,10 @@ export async function createTestRuntime( // Create router const managerDriver = driver.manager(registry.config, config); - const client = createClientWithDriver(managerDriver); + const client = createClientWithDriver( + managerDriver, + ClientConfigSchema.parse({}), + ); configureInspectorAccessToken(config, managerDriver); const { router } = createManagerRouter( registry.config, diff --git a/packages/rivetkit/src/drivers/default.ts b/packages/rivetkit/src/drivers/default.ts index 829e3e983..86a491a6a 100644 --- a/packages/rivetkit/src/drivers/default.ts +++ b/packages/rivetkit/src/drivers/default.ts @@ -14,12 +14,6 @@ export function chooseDefaultDriver(runConfig: RunnerConfig): DriverConfig { ); } - if (runConfig.runnerKind === "serverless" && !runConfig.endpoint) { - throw new UserError( - "Cannot use 'serverless' runnerKind without the 'endpoint' config set.", - ); - } - if (runConfig.driver) { return runConfig.driver; } diff --git a/packages/rivetkit/src/drivers/file-system/manager.ts b/packages/rivetkit/src/drivers/file-system/manager.ts index a6622857b..2ae2516fd 100644 --- a/packages/rivetkit/src/drivers/file-system/manager.ts +++ b/packages/rivetkit/src/drivers/file-system/manager.ts @@ -6,6 +6,7 @@ import { handleWebSocketConnect, } from "@/actor/router-endpoints"; import { createClientWithDriver } from "@/client/client"; +import { ClientConfigSchema } from "@/client/config"; import { InlineWebSocketAdapter2 } from "@/common/inline-websocket-adapter2"; import { noopNext } from "@/common/utils"; import type { @@ -116,7 +117,10 @@ export class FileSystemManagerDriver implements ManagerDriver { } // Actors run on the same node as the manager, so we create a dummy actor router that we route requests to - const inlineClient = createClientWithDriver(this); + const inlineClient = createClientWithDriver( + this, + ClientConfigSchema.parse({}), + ); this.#actorDriver = this.#driverConfig.actor( registryConfig, runConfig, diff --git a/packages/rivetkit/src/manager/router.ts b/packages/rivetkit/src/manager/router.ts index 8e65c47a0..f468df105 100644 --- a/packages/rivetkit/src/manager/router.ts +++ b/packages/rivetkit/src/manager/router.ts @@ -24,9 +24,12 @@ import { WS_PROTOCOL_TRANSPORT, } from "@/common/actor-router-consts"; import { + handleHealthRequest, + handleMetadataRequest, handleRouteError, handleRouteNotFound, loggerMiddleware, + type MetadataResponse, } from "@/common/router"; import { assertUnreachable, @@ -207,13 +210,9 @@ function addServerlessRoutes( return await actorDriver.serverlessHandleStart(c); }); - router.get("/health", (c) => { - return c.json({ - status: "ok", - runtime: "rivetkit", - version: VERSION, - }); - }); + router.get("/health", (c) => handleHealthRequest(c)); + + router.get("/metadata", (c) => handleMetadataRequest(c, runConfig)); } function addManagerRoutes( @@ -642,14 +641,9 @@ function addManagerRoutes( }); } - router.get("/health", (c) => { - return c.json({ - status: "ok", - rivetkit: { - version: VERSION, - }, - }); - }); + router.get("/health", (c) => handleHealthRequest(c, runConfig)); + + router.get("/metadata", (c) => handleMetadataRequest(c, runConfig)); managerDriver.modifyManagerRouter?.( registryConfig, diff --git a/packages/rivetkit/src/registry/mod.ts b/packages/rivetkit/src/registry/mod.ts index 589762ad0..73084676c 100644 --- a/packages/rivetkit/src/registry/mod.ts +++ b/packages/rivetkit/src/registry/mod.ts @@ -1,5 +1,6 @@ import invariant from "invariant"; import { type Client, createClientWithDriver } from "@/client/client"; +import type { ClientConfig } from "@/client/config"; import { configureBaseLogger, configureDefaultLogger } from "@/common/log"; import type { ActorDriver } from "@/driver-helpers/mod"; import { chooseDefaultDriver } from "@/drivers/default"; @@ -10,6 +11,11 @@ import { isInspectorEnabled, } from "@/inspector/utils"; import { createManagerRouter } from "@/manager/router"; +import { + getDatacenters, + type RunnerConfigRequest, + updateRunnerConfig, +} from "@/remote-manager-driver/api-endpoints"; import pkg from "../../package.json" with { type: "json" }; import { type RegistryActors, @@ -231,32 +237,32 @@ async function configureServerlessRunner(config: RunnerConfig): Promise { ? config.autoConfigureServerless : {}; - // Make the request to fetch all datacenters - const dcsUrl = `${config.endpoint}/datacenters`; + // Create a ClientConfig from RunnerConfig for API calls + const clientConfig: ClientConfig = { + endpoint: config.endpoint, + token: config.token, + namespace: config.namespace, + runnerName: config.runnerName, + encoding: config.encoding, + transport: config.transport, + headers: config.headers, + getUpgradeWebSocket: config.getUpgradeWebSocket, + disableHealthCheck: true, // We don't need health check for this operation + }; + // Fetch all datacenters logger().debug({ msg: "fetching datacenters", - url: dcsUrl, + endpoint: config.endpoint, }); - - const dcsResponse = await fetch(dcsUrl, { - headers: { - ...(config.token ? { Authorization: `Bearer ${config.token}` } : {}), - }, - }); - - if (!dcsResponse.ok) { - const errorText = await dcsResponse.text(); - throw new Error( - `failed to configure serverless runner: ${dcsResponse.status} ${dcsResponse.statusText} - ${errorText}`, - ); - } - - const dcsRes = (await dcsResponse.json()) as { - datacenters: { name: string }[]; - }; + const dcsRes = await getDatacenters(clientConfig); // Build the request body + logger().debug({ + msg: "configuring serverless runner", + runnerName: config.runnerName, + namespace: config.namespace, + }); const serverlessConfig = { serverless: { url: @@ -271,35 +277,12 @@ async function configureServerlessRunner(config: RunnerConfig): Promise { customConfig.slotsPerRunner ?? config.totalSlots ?? 1000, }, }; - const requestBody = Object.fromEntries( - dcsRes.datacenters.map((dc) => [dc.name, serverlessConfig]), - ); - - // Make the request to configure the serverless runner - const configUrl = `${config.endpoint}/runner-configs/${config.runnerName}?namespace=${config.namespace}`; - - logger().debug({ - msg: "configuring serverless runner", - url: configUrl, - config: serverlessConfig.serverless, + await updateRunnerConfig(clientConfig, config.runnerName, { + datacenters: Object.fromEntries( + dcsRes.datacenters.map((dc) => [dc.name, serverlessConfig]), + ), }); - const response = await fetch(configUrl, { - method: "PUT", - headers: { - "Content-Type": "application/json", - ...(config.token ? { Authorization: `Bearer ${config.token}` } : {}), - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error( - `failed to configure serverless runner: ${response.status} ${response.statusText} - ${errorText}`, - ); - } - logger().info({ msg: "serverless runner configured successfully", runnerName: config.runnerName, diff --git a/packages/rivetkit/src/remote-manager-driver/api-endpoints.ts b/packages/rivetkit/src/remote-manager-driver/api-endpoints.ts index 7ae0e2e63..db99c9c4f 100644 --- a/packages/rivetkit/src/remote-manager-driver/api-endpoints.ts +++ b/packages/rivetkit/src/remote-manager-driver/api-endpoints.ts @@ -1,5 +1,6 @@ import { serializeActorKey } from "@/actor/keys"; import type { ClientConfig } from "@/client/client"; +import type { MetadataResponse } from "@/common/router"; import type { ActorsCreateRequest, ActorsCreateResponse, @@ -75,3 +76,52 @@ export async function destroyActor( `/actors/${encodeURIComponent(actorId)}`, ); } + +// MARK: Get metadata +export async function getMetadata( + config: ClientConfig, +): Promise { + return apiCall(config, "GET", `/metadata`); +} + +// MARK: Get datacenters +export interface DatacentersResponse { + datacenters: { name: string }[]; +} + +export async function getDatacenters( + config: ClientConfig, +): Promise { + return apiCall(config, "GET", `/datacenters`); +} + +// MARK: Update runner config +export interface RunnerConfigRequest { + datacenters: Record< + string, + { + serverless: { + url: string; + headers: Record; + max_runners: number; + min_runners: number; + request_lifespan: number; + runners_margin: number; + slots_per_runner: number; + }; + } + >; +} + +export async function updateRunnerConfig( + config: ClientConfig, + runnerName: string, + request: RunnerConfigRequest, +): Promise { + return apiCall( + config, + "PUT", + `/runner-configs/${runnerName}`, + request, + ); +} diff --git a/packages/rivetkit/src/remote-manager-driver/mod.ts b/packages/rivetkit/src/remote-manager-driver/mod.ts index 2821ac325..a52e75a11 100644 --- a/packages/rivetkit/src/remote-manager-driver/mod.ts +++ b/packages/rivetkit/src/remote-manager-driver/mod.ts @@ -4,7 +4,7 @@ import invariant from "invariant"; import { deserializeActorKey, serializeActorKey } from "@/actor/keys"; import { generateRandomString } from "@/actor/utils"; import type { ClientConfig } from "@/client/client"; -import { noopNext } from "@/common/utils"; +import { noopNext, stringifyError } from "@/common/utils"; import type { ActorOutput, CreateInput, @@ -27,6 +27,7 @@ import { destroyActor, getActor, getActorByKey, + getMetadata, getOrCreateActor, } from "./api-endpoints"; import { EngineApiError, getEndpoint } from "./api-utils"; @@ -47,11 +48,68 @@ import { createWebSocketProxy } from "./ws-proxy"; // }; // })(); +// Global cache to store metadata check promises for each endpoint +const metadataCheckCache = new Map>(); + export class RemoteManagerDriver implements ManagerDriver { #config: ClientConfig; + #metadataPromise: Promise | undefined; constructor(runConfig: ClientConfig) { this.#config = runConfig; + + // Perform metadata check if enabled + if (!runConfig.disableHealthCheck) { + this.#metadataPromise = this.#performMetadataCheck(runConfig); + this.#metadataPromise.catch((error) => { + logger().error({ + msg: "metadata check failed", + error: error instanceof Error ? error.message : String(error), + }); + }); + } + } + + async #performMetadataCheck(config: ClientConfig): Promise { + const endpoint = getEndpoint(config); + + // Check if metadata check is already in progress or completed for this endpoint + const existingPromise = metadataCheckCache.get(endpoint); + if (existingPromise) { + return existingPromise; + } + + // Create and store the promise immediately to prevent racing requests + const metadataCheckPromise = (async () => { + try { + const metadataData = await getMetadata(config); + + if (metadataData.clientEndpoint) { + logger().info({ + msg: "received new client endpoint from metadata", + endpoint: metadataData.clientEndpoint, + }); + this.#config.endpoint = metadataData.clientEndpoint; + } + + // Log successful metadata check with runtime and version info + logger().info({ + msg: "connected to rivetkit manager", + runtime: metadataData.runtime, + version: metadataData.version, + runner: metadataData.runner, + }); + } catch (error) { + logger().error({ + msg: "failed to connect to metadata endpoint", + endpoint, + error: stringifyError(error), + }); + } + })(); + + metadataCheckCache.set(endpoint, metadataCheckPromise); + return metadataCheckPromise; } async getForId({ @@ -59,6 +117,11 @@ export class RemoteManagerDriver implements ManagerDriver { name, actorId, }: GetForIdInput): Promise { + // Wait for metadata check to complete if in progress + if (this.#metadataPromise) { + await this.#metadataPromise; + } + // Fetch from API if not in cache const response = await getActor(this.#config, name, actorId); const actor = response.actors[0]; @@ -91,6 +154,11 @@ export class RemoteManagerDriver implements ManagerDriver { name, key, }: GetWithKeyInput): Promise { + // Wait for metadata check to complete if in progress + if (this.#metadataPromise) { + await this.#metadataPromise; + } + logger().debug({ msg: "getWithKey: searching for actor", name, key }); // If not in local cache, fetch by key from API @@ -128,6 +196,11 @@ export class RemoteManagerDriver implements ManagerDriver { async getOrCreateWithKey( input: GetOrCreateWithKeyInput, ): Promise { + // Wait for metadata check to complete if in progress + if (this.#metadataPromise) { + await this.#metadataPromise; + } + const { c, name, key, input: actorInput, region } = input; logger().info({ @@ -169,6 +242,11 @@ export class RemoteManagerDriver implements ManagerDriver { key, input, }: CreateInput): Promise { + // Wait for metadata check to complete if in progress + if (this.#metadataPromise) { + await this.#metadataPromise; + } + logger().info({ msg: "creating actor via engine api", name, key }); // Create actor via engine API @@ -191,6 +269,11 @@ export class RemoteManagerDriver implements ManagerDriver { } async destroyActor(actorId: string): Promise { + // Wait for metadata check to complete if in progress + if (this.#metadataPromise) { + await this.#metadataPromise; + } + logger().info({ msg: "destroying actor via engine api", actorId }); await destroyActor(this.#config, actorId); @@ -199,6 +282,11 @@ export class RemoteManagerDriver implements ManagerDriver { } async sendRequest(actorId: string, actorRequest: Request): Promise { + // Wait for metadata check to complete if in progress + if (this.#metadataPromise) { + await this.#metadataPromise; + } + return await sendHttpRequestToActor(this.#config, actorId, actorRequest); } @@ -210,6 +298,11 @@ export class RemoteManagerDriver implements ManagerDriver { connId?: string, connToken?: string, ): Promise { + // Wait for metadata check to complete if in progress + if (this.#metadataPromise) { + await this.#metadataPromise; + } + return await openWebSocketToActor( this.#config, path, @@ -226,6 +319,11 @@ export class RemoteManagerDriver implements ManagerDriver { actorRequest: Request, actorId: string, ): Promise { + // Wait for metadata check to complete if in progress + if (this.#metadataPromise) { + await this.#metadataPromise; + } + return await sendHttpRequestToActor(this.#config, actorId, actorRequest); } @@ -238,6 +336,11 @@ export class RemoteManagerDriver implements ManagerDriver { connId?: string, connToken?: string, ): Promise { + // Wait for metadata check to complete if in progress + if (this.#metadataPromise) { + await this.#metadataPromise; + } + const upgradeWebSocket = this.#config.getUpgradeWebSocket?.(); invariant(upgradeWebSocket, "missing getUpgradeWebSocket"); diff --git a/packages/rivetkit/src/test/mod.ts b/packages/rivetkit/src/test/mod.ts index 332fa9418..5d1ca637c 100644 --- a/packages/rivetkit/src/test/mod.ts +++ b/packages/rivetkit/src/test/mod.ts @@ -2,6 +2,7 @@ import { createServer } from "node:net"; import { serve as honoServe, type ServerType } from "@hono/node-server"; import { createNodeWebSocket } from "@hono/node-ws"; import { type TestContext, vi } from "vitest"; +import { ClientConfigSchema } from "@/client/config"; import { type Client, createClient } from "@/client/mod"; import { chooseDefaultDriver } from "@/drivers/default"; import { createFileSystemOrMemoryDriver } from "@/drivers/file-system/mod"; @@ -31,7 +32,10 @@ function serve(registry: Registry, inputConfig?: InputConfig): ServerType { const runConfig = RunnerConfigSchema.parse(inputConfig); const driver = inputConfig.driver ?? createFileSystemOrMemoryDriver(false); const managerDriver = driver.manager(registry.config, config); - const client = createClientWithDriver(managerDriver); + const client = createClientWithDriver( + managerDriver, + ClientConfigSchema.parse({}), + ); configureInspectorAccessToken(config, managerDriver); const { router } = createManagerRouter( registry.config,