diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bfb8116e111..cfff0003a9b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -31,7 +31,7 @@ /src/gateway/**/*secret*.ts @openclaw/openclaw-secops /src/gateway/security-path*.ts @openclaw/openclaw-secops /src/gateway/resolve-configured-secret-input-string*.ts @openclaw/openclaw-secops -/src/gateway/protocol/**/*secret*.ts @openclaw/openclaw-secops +/packages/gateway-protocol/src/**/*secret*.ts @openclaw/openclaw-secops /src/gateway/server-methods/secrets*.ts @openclaw/openclaw-secops /src/agents/*auth*.ts @openclaw/openclaw-secops /src/agents/**/*auth*.ts @openclaw/openclaw-secops diff --git a/.github/codeql/codeql-channel-runtime-boundary-critical-security.yml b/.github/codeql/codeql-channel-runtime-boundary-critical-security.yml index b8819f85bfe..c19ec1e4ba6 100644 --- a/.github/codeql/codeql-channel-runtime-boundary-critical-security.yml +++ b/.github/codeql/codeql-channel-runtime-boundary-critical-security.yml @@ -19,7 +19,7 @@ paths: - src/config/types.channel*.ts - src/gateway/server-channel*.ts - src/gateway/server-methods/channels.ts - - src/gateway/protocol/schema/channels.ts + - packages/gateway-protocol/src/schema/channels.ts - src/infra/channel-*.ts - src/infra/exec-approval-channel-runtime.ts - src/infra/outbound/channel-*.ts diff --git a/.github/codeql/codeql-core-auth-secrets-critical-quality.yml b/.github/codeql/codeql-core-auth-secrets-critical-quality.yml index 9aeeb51607a..4f24485cd7b 100644 --- a/.github/codeql/codeql-core-auth-secrets-critical-quality.yml +++ b/.github/codeql/codeql-core-auth-secrets-critical-quality.yml @@ -30,7 +30,7 @@ paths: - src/gateway/**/*auth*.ts - src/gateway/*secret*.ts - src/gateway/**/*secret*.ts - - src/gateway/protocol/**/*secret*.ts + - packages/gateway-protocol/src/**/*secret*.ts - src/gateway/resolve-configured-secret-input-string*.ts - src/gateway/security-path*.ts - src/gateway/server-methods/secrets*.ts diff --git a/.github/codeql/codeql-core-auth-secrets-critical-security.yml b/.github/codeql/codeql-core-auth-secrets-critical-security.yml index 41909ae8348..af2d4d3e200 100644 --- a/.github/codeql/codeql-core-auth-secrets-critical-security.yml +++ b/.github/codeql/codeql-core-auth-secrets-critical-security.yml @@ -30,7 +30,7 @@ paths: - src/gateway/**/*auth*.ts - src/gateway/*secret*.ts - src/gateway/**/*secret*.ts - - src/gateway/protocol/**/*secret*.ts + - packages/gateway-protocol/src/**/*secret*.ts - src/gateway/resolve-configured-secret-input-string*.ts - src/gateway/security-path*.ts - src/gateway/server-methods/secrets*.ts diff --git a/.github/codeql/codeql-gateway-runtime-boundary-critical-quality.yml b/.github/codeql/codeql-gateway-runtime-boundary-critical-quality.yml index 0744fdb38d6..43c00e01358 100644 --- a/.github/codeql/codeql-gateway-runtime-boundary-critical-quality.yml +++ b/.github/codeql/codeql-gateway-runtime-boundary-critical-quality.yml @@ -15,7 +15,7 @@ query-filters: paths: - src/gateway/method-scopes.ts - - src/gateway/protocol + - packages/gateway-protocol/src - src/gateway/server-methods - src/gateway/server-methods.ts - src/gateway/server-methods-list.ts diff --git a/.github/labeler.yml b/.github/labeler.yml index f18873bd7ff..ad68ab3502b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -188,7 +188,7 @@ - "ui/**" - "src/gateway/control-ui.ts" - "src/gateway/control-ui-shared.ts" - - "src/gateway/protocol/**" + - "packages/gateway-protocol/src/**" - "src/gateway/server-methods/chat.ts" - "src/infra/control-ui-assets.ts" @@ -196,6 +196,7 @@ - changed-files: - any-glob-to-any-file: - "src/gateway/**" + - "packages/gateway-protocol/src/**" - "src/daemon/**" - "docs/gateway/**" diff --git a/.github/workflows/codeql-critical-quality.yml b/.github/workflows/codeql-critical-quality.yml index ffb769e8fdf..eaff98effee 100644 --- a/.github/workflows/codeql-critical-quality.yml +++ b/.github/workflows/codeql-critical-quality.yml @@ -106,13 +106,13 @@ on: - "src/gateway/**/*auth*.ts" - "src/gateway/*secret*.ts" - "src/gateway/**/*secret*.ts" - - "src/gateway/protocol/**/*secret*.ts" + - "packages/gateway-protocol/src/**/*secret*.ts" - "src/gateway/resolve-configured-secret-input-string*.ts" - "src/gateway/security-path*.ts" - "src/gateway/server-methods/secrets*.ts" - "src/gateway/server-startup-memory.ts" - "src/gateway/method-scopes.ts" - - "src/gateway/protocol/**" + - "packages/gateway-protocol/src/**" - "src/gateway/server-methods/**" - "src/gateway/server-methods.ts" - "src/gateway/server-methods-list.ts" @@ -244,14 +244,14 @@ jobs: src/config/*) config=true ;; - src/gateway/protocol/*secret*.ts|src/gateway/server-methods/secrets*.ts) + packages/gateway-protocol/src/*secret*.ts|packages/gateway-protocol/src/**/*secret*.ts|src/gateway/server-methods/secrets*.ts) core_auth_secrets=true gateway=true ;; src/agents/*auth*.ts|src/agents/auth-health*.ts|src/agents/auth-profiles|src/agents/auth-profiles/*|src/agents/bash-tools.exec-host-shared.ts|src/agents/sandbox|src/agents/sandbox.ts|src/agents/sandbox-*.ts|src/agents/sandbox/*|src/cron/service/jobs.ts|src/cron/stagger.ts|src/gateway/*auth*.ts|src/gateway/*secret*.ts|src/gateway/resolve-configured-secret-input-string*.ts|src/gateway/security-path*.ts|src/infra/secret-file*.ts|src/secrets/*|src/security/*) core_auth_secrets=true ;; - src/gateway/method-scopes.ts|src/gateway/protocol/*|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts) + packages/gateway-protocol/src/*|packages/gateway-protocol/src/**/*|src/gateway/method-scopes.ts|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts) gateway=true ;; packages/memory-host-sdk/*|src/commands/doctor-cron-dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*) diff --git a/AGENTS.md b/AGENTS.md index b720d28c955..2193661b679 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,9 +35,9 @@ Skills own workflows; root owns hard policy and routing. ## Map -- Core TS: `src/`, `ui/`, `packages/`; plugins: `extensions/`; SDK: `src/plugin-sdk/*`; channels: `src/channels/*`; loader: `src/plugins/*`; protocol: `src/gateway/protocol/*`; docs/apps: `docs/`, `apps/`. +- Core TS: `src/`, `ui/`, `packages/`; plugins: `extensions/`; SDK: `src/plugin-sdk/*`; channels: `src/channels/*`; loader: `src/plugins/*`; protocol: `packages/gateway-protocol/*`; docs/apps: `docs/`, `apps/`. - Installers: sibling `../openclaw.ai`. -- Scoped guides: `extensions/`, `src/{plugin-sdk,channels,plugins,gateway,gateway/protocol,agents}/`, `test/helpers*/`, `docs/`, `ui/`, `scripts/`. +- Scoped guides: `extensions/`, `src/{plugin-sdk,channels,plugins,gateway,agents}/`, `packages/`, `test/helpers*/`, `docs/`, `ui/`, `scripts/`. ## Docs diff --git a/config/knip.config.ts b/config/knip.config.ts index aa3d74b58d9..23d44365c77 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -168,6 +168,14 @@ const config = { entry: ["src/index.ts!", "src/*.ts!", "src/harness/**/*.ts!"], project: ["src/**/*.ts!"], }, + "packages/gateway-client": { + entry: ["src/index.ts!"], + project: ["src/**/*.ts!"], + }, + "packages/gateway-protocol": { + entry: ["src/index.ts!", "src/schema.ts!"], + project: ["src/**/*.ts!"], + }, "packages/*": { entry: ["index.js!", "scripts/postinstall.js!"], project: ["index.js!", "scripts/**/*.js!"], diff --git a/docs/concepts/typebox.md b/docs/concepts/typebox.md index dea53b4e7db..54d7a291f01 100644 --- a/docs/concepts/typebox.md +++ b/docs/concepts/typebox.md @@ -53,8 +53,8 @@ Authoritative advertised **discovery** inventory lives in ## Where the schemas live -- Source: `src/gateway/protocol/schema.ts` -- Runtime validators (AJV): `src/gateway/protocol/index.ts` +- Source: `packages/gateway-protocol/src/schema.ts` +- Runtime validators (AJV): `packages/gateway-protocol/src/index.ts` - Advertised feature/discovery registry: `src/gateway/server-methods-list.ts` - Server handshake + method dispatch: `src/gateway/server.impl.ts` - Node client: `src/gateway/client.ts` @@ -195,7 +195,7 @@ Example: add a new `system.echo` request that returns `{ ok: true, text }`. 1. **Schema (source of truth)** -Add to `src/gateway/protocol/schema.ts`: +Add to `packages/gateway-protocol/src/schema.ts`: ```ts export const SystemEchoParamsSchema = Type.Object( @@ -223,7 +223,7 @@ export type SystemEchoResult = Static; 2. **Validation** -In `src/gateway/protocol/index.ts`, export an AJV validator: +In `packages/gateway-protocol/src/index.ts`, export an AJV validator: ```ts export const validateSystemEchoParams = ajv.compile(SystemEchoParamsSchema); @@ -272,7 +272,7 @@ Unknown frame types are preserved as raw payloads for forward compatibility. ## Versioning + compatibility -- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`. +- `PROTOCOL_VERSION` lives in `packages/gateway-protocol/src/version.ts`. - Clients send `minProtocol` + `maxProtocol`; the server rejects ranges that do not include its current protocol. - The Swift models keep unknown frame types to avoid breaking older clients. diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index 5af70336558..92ae9e4b5cc 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -104,7 +104,7 @@ within their overall connection budget instead of surfacing it as a terminal handshake failure. `server`, `features`, `snapshot`, and `policy` are all required by the schema -(`src/gateway/protocol/schema/frames.ts`). `auth` is also required and reports +(`packages/gateway-protocol/src/schema/frames.ts`). `auth` is also required and reports the negotiated role/scopes. `pluginSurfaceUrls` is optional and maps plugin surface names, such as `canvas`, to scoped hosted URLs. @@ -648,7 +648,7 @@ terminal summary, and sanitized error text. ## Versioning -- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`. +- `PROTOCOL_VERSION` lives in `packages/gateway-protocol/src/version.ts`. - Clients send `minProtocol` + `maxProtocol`; the server rejects ranges that do not include its current protocol. Current clients and servers require protocol v4. @@ -664,8 +664,8 @@ stable across protocol v4 and are the expected baseline for third-party clients. | Constant | Default | Source | | ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ | -| `PROTOCOL_VERSION` | `4` | `src/gateway/protocol/version.ts` | -| `MIN_CLIENT_PROTOCOL_VERSION` | `4` | `src/gateway/protocol/version.ts` | +| `PROTOCOL_VERSION` | `4` | `packages/gateway-protocol/src/version.ts` | +| `MIN_CLIENT_PROTOCOL_VERSION` | `4` | `packages/gateway-protocol/src/version.ts` | | Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) | | Preauth / connect-challenge timeout | `15_000` ms | `src/gateway/handshake-timeouts.ts` (config/env can raise the paired server/client budget) | | Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) | @@ -818,7 +818,7 @@ Migration target: This protocol exposes the **full gateway API** (status, channels, models, chat, agent, sessions, nodes, approvals, etc.). The exact surface is defined by the -TypeBox schemas in `src/gateway/protocol/schema.ts`. +TypeBox schemas in `packages/gateway-protocol/src/schema.ts`. ## Related diff --git a/package.json b/package.json index 4490207d3c8..d643214861d 100644 --- a/package.json +++ b/package.json @@ -1623,7 +1623,7 @@ "start": "node openclaw.mjs", "test": "node scripts/test-projects.mjs", "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all", - "test:auth:compat": "node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts", + "test:auth:compat": "node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts && node scripts/run-vitest.mjs run packages/gateway-protocol/src/connect-error-details.test.ts", "test:build:singleton": "node scripts/test-built-plugin-singleton.mjs", "test:build:status-message-runtime": "node scripts/test-built-status-message-runtime.mjs", "test:bundled": "node scripts/run-vitest.mjs run --config test/vitest/vitest.bundled.config.ts", diff --git a/packages/gateway-client/package.json b/packages/gateway-client/package.json new file mode 100644 index 00000000000..766ede13770 --- /dev/null +++ b/packages/gateway-client/package.json @@ -0,0 +1,36 @@ +{ + "name": "@openclaw/gateway-client", + "version": "0.0.0-private", + "private": true, + "files": [ + "dist" + ], + "type": "module", + "main": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs", + "default": "./dist/index.mjs" + }, + "./readiness": { + "types": "./dist/readiness.d.mts", + "import": "./dist/readiness.mjs", + "default": "./dist/readiness.mjs" + }, + "./timeouts": { + "types": "./dist/timeouts.d.mts", + "import": "./dist/timeouts.mjs", + "default": "./dist/timeouts.mjs" + } + }, + "scripts": { + "build": "tsdown src/index.ts src/readiness.ts src/timeouts.ts --no-config --platform node --format esm --dts --out-dir dist --clean" + }, + "dependencies": { + "@openclaw/gateway-protocol": "workspace:*", + "ipaddr.js": "2.4.0", + "ws": "8.21.0" + } +} diff --git a/packages/gateway-client/src/client.ts b/packages/gateway-client/src/client.ts new file mode 100644 index 00000000000..d1b19dd97a2 --- /dev/null +++ b/packages/gateway-client/src/client.ts @@ -0,0 +1,1512 @@ +import { randomUUID } from "node:crypto"; +import { + type ConnectParams, + type EventFrame, + type HelloOk, + MIN_CLIENT_PROTOCOL_VERSION, + PROTOCOL_VERSION, + type RequestFrame, + validateEventFrame, + validateRequestFrame, + validateResponseFrame, +} from "@openclaw/gateway-protocol"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, + type GatewayClientMode, + type GatewayClientName, +} from "@openclaw/gateway-protocol/client-info"; +import { + ConnectErrorDetailCodes, + formatConnectErrorMessage, + readConnectErrorDetailCode, + readConnectErrorRecoveryAdvice, + readPairingConnectErrorDetails, + type ConnectErrorRecoveryAdvice, +} from "@openclaw/gateway-protocol/connect-error-details"; +import { resolveGatewayStartupRetryAfterMs } from "@openclaw/gateway-protocol/startup-unavailable"; +import ipaddr from "ipaddr.js"; +import { WebSocket, type ClientOptions, type CertMeta } from "ws"; +import { buildDeviceAuthPayloadV3 } from "./device-auth.js"; +import { resolveConnectChallengeTimeoutMs } from "./timeouts.js"; + +export type DeviceIdentity = { + deviceId: string; + privateKeyPem: string; + publicKeyPem: string; +}; + +export type DeviceAuthTokenRecord = { + token?: string; + scopes?: string[]; +}; + +// The package stays reusable by depending on host callbacks for OpenClaw-owned +// state: device keys, token storage, proxy routing, logging, and TLS formatting. +export type GatewayClientHostDeps = { + loadOrCreateDeviceIdentity?: () => DeviceIdentity | undefined; + signDevicePayload?: (privateKeyPem: string, payload: string) => string; + publicKeyRawBase64UrlFromPem?: (publicKeyPem: string) => string; + loadDeviceAuthToken?: (params: { + deviceId: string; + role: string; + env?: NodeJS.ProcessEnv; + }) => DeviceAuthTokenRecord | null; + storeDeviceAuthToken?: (params: { + deviceId: string; + role: string; + token: string; + scopes: string[]; + env?: NodeJS.ProcessEnv; + }) => void; + clearDeviceAuthToken?: (params: { + deviceId: string; + role: string; + env?: NodeJS.ProcessEnv; + }) => void; + beforeConnect?: () => void; + registerGatewayLoopbackBypass?: (url: string) => (() => void) | undefined; + logDebug?: (message: string) => void; + logError?: (message: string) => void; + redactForLog?: (message: string) => string; + normalizeTlsFingerprint?: (fingerprint: string | undefined) => string; +}; + +function normalizeOptionalString(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + return trimmed || undefined; +} + +function normalizeLowercaseStringOrEmpty(value: unknown): string { + return typeof value === "string" ? value.trim().toLowerCase() : ""; +} + +function resolveSafeTimeoutDelayMs(value: number): number { + return Math.max(0, Math.min(value, 2_147_483_647)); +} + +function rawDataToString(data: unknown): string { + if (typeof data === "string") { + return data; + } + if (Buffer.isBuffer(data)) { + return data.toString("utf8"); + } + if (data instanceof ArrayBuffer) { + return Buffer.from(data).toString("utf8"); + } + if (Array.isArray(data)) { + return Buffer.concat(data.map((entry) => Buffer.from(entry))).toString("utf8"); + } + return String(data); +} + +function isSensitiveUrlQueryParamName(key: string): boolean { + return /(?:token|password|secret|key|auth|credential)/iu.test(key); +} + +function normalizeFingerprint(fingerprint: string | undefined): string { + return (fingerprint ?? "").replaceAll(":", "").trim().toLowerCase(); +} + +function parseHostForAddressChecks( + host: string, +): { isLocalhost: boolean; unbracketedHost: string } | null { + if (!host) { + return null; + } + const normalizedHost = host.toLowerCase().trim(); + const canonicalHost = normalizedHost.replace(/\.+$/, ""); + if (canonicalHost === "localhost") { + return { isLocalhost: true, unbracketedHost: canonicalHost }; + } + return { + isLocalhost: false, + // URL.hostname canonicalizes IPv6 with brackets in some call sites. Strip + // them before net.isIP so address checks do not fall back to hostname rules. + unbracketedHost: + normalizedHost.startsWith("[") && normalizedHost.endsWith("]") + ? normalizedHost.slice(1, -1) + : normalizedHost, + }; +} + +type ParsedIpAddress = ipaddr.IPv4 | ipaddr.IPv6; + +const PRIVATE_OR_LOOPBACK_IPV4_RANGES = new Set([ + "loopback", + "private", + "linkLocal", + "carrierGradeNat", +]); + +const PRIVATE_OR_LOOPBACK_IPV6_RANGES = new Set([ + "loopback", + "linkLocal", + "uniqueLocal", + "deprecatedSiteLocal", +]); + +function parseGatewayIpAddress(host: string): ParsedIpAddress | null { + const normalized = host.toLowerCase(); + if (ipaddr.IPv4.isValid(normalized) && !ipaddr.IPv4.isValidFourPartDecimal(normalized)) { + return null; + } + if (!ipaddr.isValid(normalized)) { + return null; + } + const parsed = ipaddr.parse(normalized); + // WHATWG URL canonicalization can turn ::ffff:127.0.0.1 into ::ffff:7f00:1. + // Normalize mapped forms so IPv4 loopback/private policy stays identical. + if (parsed.kind() === "ipv6") { + const ipv6 = parsed as ipaddr.IPv6; + if (ipv6.isIPv4MappedAddress()) { + return ipv6.toIPv4Address(); + } + } + return parsed; +} + +function isPrivateOrLoopbackIpAddress(address: ParsedIpAddress): boolean { + const ranges = + address.kind() === "ipv4" ? PRIVATE_OR_LOOPBACK_IPV4_RANGES : PRIVATE_OR_LOOPBACK_IPV6_RANGES; + return ranges.has(address.range()); +} + +function isLoopbackHost(host: string): boolean { + const parsed = parseHostForAddressChecks(host); + if (!parsed) { + return false; + } + if (parsed.isLocalhost) { + return true; + } + const address = parseGatewayIpAddress(parsed.unbracketedHost); + if (!address) { + return false; + } + return address.range() === "loopback"; +} + +function isPrivateOrLoopbackHost(host: string): boolean { + const parsed = parseHostForAddressChecks(host); + if (!parsed) { + return false; + } + if (parsed.isLocalhost) { + return true; + } + const address = parseGatewayIpAddress(parsed.unbracketedHost); + if (!address) { + return false; + } + return isPrivateOrLoopbackIpAddress(address); +} + +function isTrustedPlaintextWebSocketHost(hostname: string): boolean { + if (isPrivateOrLoopbackHost(hostname)) { + return true; + } + const normalized = hostname.toLowerCase().trim().replace(/\.+$/, ""); + // Plain ws:// is still useful for local discovery and Tailnet names. Public + // hostnames must use wss:// unless the caller opts into the private break-glass. + return normalized.endsWith(".local") || normalized.endsWith(".ts.net"); +} + +function isSecureWebSocketUrl(rawUrl: string, options?: { allowPrivateWs?: boolean }): boolean { + try { + const url = new URL(rawUrl); + const protocol = + url.protocol === "https:" ? "wss:" : url.protocol === "http:" ? "ws:" : url.protocol; + if (protocol === "wss:") { + return true; + } + if (protocol !== "ws:") { + return false; + } + if (isLoopbackHost(url.hostname) || isTrustedPlaintextWebSocketHost(url.hostname)) { + return true; + } + if (options?.allowPrivateWs === true) { + const hostForIpCheck = + url.hostname.startsWith("[") && url.hostname.endsWith("]") + ? url.hostname.slice(1, -1) + : url.hostname; + return ( + isPrivateOrLoopbackHost(url.hostname) || parseGatewayIpAddress(hostForIpCheck) === null + ); + } + return false; + } catch { + return false; + } +} + +type Pending = { + resolve: (value: unknown) => void; + reject: (err: unknown) => void; + expectFinal: boolean; + timeout: NodeJS.Timeout | null; + cleanup?: () => void; + onAccepted?: (payload: unknown) => void; + acceptedNotified?: boolean; +}; + +export type GatewayClientRequestOptions = { + expectFinal?: boolean; + timeoutMs?: number | null; + signal?: AbortSignal; + /** Called once for expectFinal requests after an accepted response, before the final result. */ + onAccepted?: (payload: unknown) => void; +}; + +type GatewayClientErrorShape = { + code?: string; + message?: string; + details?: unknown; + retryable?: boolean; + retryAfterMs?: number; +}; + +type SelectedConnectAuth = { + authToken?: string; + authBootstrapToken?: string; + authDeviceToken?: string; + authPassword?: string; + authApprovalRuntimeToken?: string; + signatureToken?: string; + resolvedDeviceToken?: string; + storedToken?: string; + storedScopes?: string[]; + usingStoredDeviceToken?: boolean; +}; + +type StoredDeviceAuth = { + token?: string; + scopes?: string[]; +}; + +type AssembledConnect = { + params: ConnectParams; + authApprovalRuntimeToken: string | undefined; + resolvedDeviceToken: string | undefined; + storedToken: string | undefined; + usingStoredDeviceToken: boolean | undefined; +}; + +type FingerprintCheckingClientOptions = Omit & { + checkServerIdentity?: (servername: string, cert: CertMeta) => Error | undefined; +}; + +const DEFAULT_GATEWAY_CLIENT_URL = "ws://127.0.0.1:18789"; +const DEFAULT_CLIENT_VERSION = "0.0.0"; + +export type GatewayReconnectPausedInfo = { + code: number; + reason: string; + detailCode: string | null; +}; + +export class GatewayClientRequestError extends Error { + readonly gatewayCode: string; + readonly details?: unknown; + readonly retryable: boolean; + readonly retryAfterMs?: number; + + constructor(error: GatewayClientErrorShape) { + super(formatConnectErrorMessage({ message: error.message, details: error.details })); + this.name = "GatewayClientRequestError"; + this.gatewayCode = error.code ?? "UNAVAILABLE"; + this.details = error.details; + this.retryable = error.retryable === true; + this.retryAfterMs = error.retryAfterMs; + } +} + +const GATEWAY_CONNECT_ASSEMBLY_ERROR = Symbol("gateway.connectAssemblyError"); + +type GatewayConnectAssemblyError = Error & { + [GATEWAY_CONNECT_ASSEMBLY_ERROR]?: true; +}; + +function markGatewayConnectAssemblyError(error: Error): Error { + Object.defineProperty(error, GATEWAY_CONNECT_ASSEMBLY_ERROR, { + configurable: true, + value: true, + }); + return error; +} + +export function isGatewayConnectAssemblyError(value: unknown): value is Error { + return ( + value instanceof Error && + (value as GatewayConnectAssemblyError)[GATEWAY_CONNECT_ASSEMBLY_ERROR] === true + ); +} + +export type GatewayClientOptions = { + url?: string; // ws://127.0.0.1:18789 + connectChallengeTimeoutMs?: number; + /** @deprecated Use connectChallengeTimeoutMs. */ + connectDelayMs?: number; + /** + * Server-side pre-auth handshake budget. Config-derived local clients use + * this to keep the connect-challenge watchdog aligned with the gateway. + */ + preauthHandshakeTimeoutMs?: number; + tickWatchMinIntervalMs?: number; + requestTimeoutMs?: number; + token?: string; + bootstrapToken?: string; + deviceToken?: string; + password?: string; + approvalRuntimeToken?: string; + instanceId?: string; + clientName?: GatewayClientName; + clientDisplayName?: string; + clientVersion?: string; + platform?: string; + deviceFamily?: string; + mode?: GatewayClientMode; + role?: string; + scopes?: string[]; + caps?: string[]; + commands?: string[]; + permissions?: Record; + pathEnv?: string; + env?: NodeJS.ProcessEnv; + deviceIdentity?: DeviceIdentity | null; + hostDeps?: GatewayClientHostDeps; + minProtocol?: number; + maxProtocol?: number; + tlsFingerprint?: string; + onEvent?: (evt: EventFrame) => void; + onHelloOk?: (hello: HelloOk) => void; + onConnectError?: (err: Error) => void; + onReconnectPaused?: (info: GatewayReconnectPausedInfo) => void; + onClose?: (code: number, reason: string) => void; + onGap?: (info: { expected: number; received: number }) => void; +}; + +export const GATEWAY_CLOSE_CODE_HINTS: Readonly> = { + 1000: "normal closure", + 1006: "abnormal closure (no close frame)", + 1008: "policy violation", + 1012: "service restart", + 1013: "try again later", +}; + +export function describeGatewayCloseCode(code: number): string | undefined { + return GATEWAY_CLOSE_CODE_HINTS[code]; +} + +function readConnectChallengeTimeoutOverride( + opts: Pick, +): number | undefined { + if ( + typeof opts.connectChallengeTimeoutMs === "number" && + Number.isFinite(opts.connectChallengeTimeoutMs) + ) { + return opts.connectChallengeTimeoutMs; + } + if (typeof opts.connectDelayMs === "number" && Number.isFinite(opts.connectDelayMs)) { + return opts.connectDelayMs; + } + return undefined; +} + +function isGatewayClientStoppedError(err: unknown): boolean { + const message = err instanceof Error ? err.message : String(err); + return message === "gateway client stopped" || message === "Error: gateway client stopped"; +} + +function formatGatewayClientErrorForLog(err: unknown): string { + const redactedUrlLikeString = String(err) + .replace(/\/\/([^@/?#\s]+)@/g, "//***:***@") + .replace(/(Authorization:\s*Bearer\s+)[^\s]+/giu, "$1***") + .replace(/([?&])([^=&\s]+)=([^&#\s"'<>)]*)/g, (match, prefix: string, key: string) => + isSensitiveUrlQueryParamName(key) ? `${prefix}${key}=***` : match, + ); + return redactedUrlLikeString; +} + +export function resolveGatewayClientConnectChallengeTimeoutMs( + opts: Pick< + GatewayClientOptions, + "connectChallengeTimeoutMs" | "connectDelayMs" | "preauthHandshakeTimeoutMs" + >, +): number { + return resolveConnectChallengeTimeoutMs(readConnectChallengeTimeoutOverride(opts), { + configuredTimeoutMs: opts.preauthHandshakeTimeoutMs, + }); +} + +const FORCE_STOP_TERMINATE_GRACE_MS = 250; +const STOP_AND_WAIT_TIMEOUT_MS = 1_000; + +type PendingStop = { + ws: WebSocket; + promise: Promise; + resolve: () => void; +}; + +export class GatewayClient { + private ws: WebSocket | null = null; + private opts: GatewayClientOptions; + private deps: Required; + private pending = new Map(); + private backoffMs = 1000; + private closed = false; + private lastSeq: number | null = null; + private connectNonce: string | null = null; + private connectSent = false; + private connectTimer: NodeJS.Timeout | null = null; + private reconnectTimer: NodeJS.Timeout | null = null; + private pendingDeviceTokenRetry = false; + private deviceTokenRetryBudgetUsed = false; + private approvalRuntimeTokenCompatibilityDisabled = false; + private approvalRuntimeTokenRetryBudgetUsed = false; + private pendingStartupReconnectDelayMs: number | null = null; + private pendingConnectErrorDetailCode: string | null = null; + private pendingConnectErrorDetails: unknown = null; + // Track last tick to detect silent stalls. + private lastTick: number | null = null; + private tickIntervalMs = 30_000; + private tickTimer: NodeJS.Timeout | null = null; + private readonly requestTimeoutMs: number; + private pendingStop: PendingStop | null = null; + private socketOpened = false; + + constructor(opts: GatewayClientOptions) { + this.deps = { + // Defaults keep the package inert outside OpenClaw; device signing throws + // only when a caller actually supplies a device identity without host deps. + loadOrCreateDeviceIdentity: opts.hostDeps?.loadOrCreateDeviceIdentity ?? (() => undefined), + signDevicePayload: + opts.hostDeps?.signDevicePayload ?? + (() => { + throw new Error("GatewayClient device signature dependency is not configured"); + }), + publicKeyRawBase64UrlFromPem: + opts.hostDeps?.publicKeyRawBase64UrlFromPem ?? + (() => { + throw new Error("GatewayClient public key dependency is not configured"); + }), + loadDeviceAuthToken: opts.hostDeps?.loadDeviceAuthToken ?? (() => null), + storeDeviceAuthToken: opts.hostDeps?.storeDeviceAuthToken ?? (() => {}), + clearDeviceAuthToken: opts.hostDeps?.clearDeviceAuthToken ?? (() => {}), + beforeConnect: opts.hostDeps?.beforeConnect ?? (() => {}), + registerGatewayLoopbackBypass: + opts.hostDeps?.registerGatewayLoopbackBypass ?? (() => undefined), + logDebug: opts.hostDeps?.logDebug ?? (() => {}), + logError: opts.hostDeps?.logError ?? (() => {}), + redactForLog: opts.hostDeps?.redactForLog ?? ((message) => message), + normalizeTlsFingerprint: opts.hostDeps?.normalizeTlsFingerprint ?? normalizeFingerprint, + }; + this.opts = { + ...opts, + deviceIdentity: + opts.deviceIdentity === null + ? undefined + : (opts.deviceIdentity ?? this.deps.loadOrCreateDeviceIdentity()), + }; + this.requestTimeoutMs = + typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs) + ? resolveSafeTimeoutDelayMs(opts.requestTimeoutMs) + : 30_000; + } + + start() { + if (this.closed) { + return; + } + this.clearReconnectTimer(); + this.clearConnectChallengeTimeout(); + this.connectNonce = null; + this.connectSent = false; + const url = this.opts.url ?? DEFAULT_GATEWAY_CLIENT_URL; + if (this.opts.tlsFingerprint && !url.startsWith("wss://")) { + this.notifyConnectError(new Error("gateway tls fingerprint requires wss:// gateway url")); + return; + } + + const allowPrivateWs = + (this.opts.env ?? process.env).OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1"; + // Block plaintext before device-token lookup. Credentials may be loaded from + // host storage later in sendConnect(), and chat payloads are sensitive too. + if (!isSecureWebSocketUrl(url, { allowPrivateWs })) { + // Safe hostname extraction - avoid throwing on malformed URLs in error path + let displayHost = url; + try { + displayHost = new URL(url).hostname || url; + } catch { + // Use raw URL if parsing fails + } + const error = new Error( + `SECURITY ERROR: Cannot connect to "${displayHost}" over plaintext ws://. ` + + "Both credentials and chat data would be exposed to network interception. " + + "Use wss:// for remote URLs. Safe defaults: keep gateway.bind=loopback and connect via SSH tunnel " + + "(ssh -N -L 18789:127.0.0.1:18789 user@gateway-host), or use Tailscale Serve/Funnel. " + + (allowPrivateWs + ? "" + : "Break-glass (trusted private networks only): set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1. ") + + "Run `openclaw doctor --fix` for guidance.", + ); + this.notifyConnectError(error); + return; + } + // Allow node screen snapshots and other large responses. + this.deps.beforeConnect(); + const wsOptions: FingerprintCheckingClientOptions = { + maxPayload: 25 * 1024 * 1024, + }; + if (url.startsWith("wss://") && this.opts.tlsFingerprint) { + wsOptions.rejectUnauthorized = false; + wsOptions.checkServerIdentity = (_hostValue: string, cert: CertMeta) => { + const fingerprintValue = + typeof cert === "object" && cert && "fingerprint256" in cert + ? ((cert as { fingerprint256?: string }).fingerprint256 ?? "") + : ""; + const fingerprint = this.deps.normalizeTlsFingerprint( + typeof fingerprintValue === "string" ? fingerprintValue : "", + ); + const expected = this.deps.normalizeTlsFingerprint(this.opts.tlsFingerprint ?? ""); + if (!expected) { + return undefined; + } + if (!fingerprint) { + return new Error("Missing server TLS fingerprint"); + } + if (fingerprint !== expected) { + return new Error("Server TLS fingerprint mismatch"); + } + return undefined; + }; + } + let ws: WebSocket; + // Managed proxies can intercept local traffic; the host owns the bypass + // lifecycle and must remove it immediately after the socket is created. + const unregisterGatewayLoopbackBypass = this.deps.registerGatewayLoopbackBypass(url); + try { + ws = new WebSocket(url, wsOptions as ClientOptions); + } catch (error) { + this.notifyConnectError(error instanceof Error ? error : new Error(String(error))); + return; + } finally { + unregisterGatewayLoopbackBypass?.(); + } + this.ws = ws; + this.socketOpened = false; + this.connectNonce = null; + this.connectSent = false; + this.clearConnectChallengeTimeout(); + + ws.on("open", () => { + this.socketOpened = true; + if (url.startsWith("wss://") && this.opts.tlsFingerprint) { + const tlsError = this.validateTlsFingerprint(); + if (tlsError) { + this.notifyConnectError(tlsError); + this.ws?.close(1008, tlsError.message); + return; + } + } + this.beginPreauthHandshake(); + }); + ws.on("message", (data) => this.handleMessage(rawDataToString(data))); + ws.on("close", (code, reason) => { + const reasonText = rawDataToString(reason); + const connectErrorDetailCode = this.pendingConnectErrorDetailCode; + const connectErrorDetails = this.pendingConnectErrorDetails; + this.pendingConnectErrorDetailCode = null; + this.pendingConnectErrorDetails = null; + if (this.ws === ws) { + this.ws = null; + } + this.socketOpened = false; + this.resolvePendingStop(ws); + if (this.pendingStartupReconnectDelayMs !== null) { + this.scheduleReconnect(); + return; + } + // Clear persisted device auth state only when device-token auth was active. + // Shared token/password failures can return the same close reason but should + // not erase a valid cached device token. + if ( + code === 1008 && + normalizeLowercaseStringOrEmpty(reasonText).includes("device token mismatch") && + !this.opts.token && + !this.opts.password && + this.opts.deviceIdentity + ) { + const deviceId = this.opts.deviceIdentity.deviceId; + const role = this.opts.role ?? "operator"; + try { + this.deps.clearDeviceAuthToken({ deviceId, role, env: this.opts.env }); + this.logDebug(`cleared stale device-auth token for device ${deviceId}`); + } catch (err) { + this.logDebug( + `failed clearing stale device-auth token for device ${deviceId}: ${String(err)}`, + ); + } + } + this.flushPendingErrors(new Error(`gateway closed (${code}): ${reasonText}`)); + if ( + this.shouldPauseReconnectAfterAuthFailure({ + detailCode: connectErrorDetailCode, + details: connectErrorDetails, + }) + ) { + this.opts.onReconnectPaused?.({ + code, + reason: reasonText, + detailCode: connectErrorDetailCode, + }); + this.opts.onClose?.(code, reasonText); + return; + } + this.scheduleReconnect(); + this.opts.onClose?.(code, reasonText); + }); + ws.on("error", (err) => { + this.logDebug(`gateway client error: ${formatGatewayClientErrorForLog(err)}`); + if (!this.connectSent) { + this.notifyConnectError(err instanceof Error ? err : new Error(String(err))); + } + }); + } + + stop() { + void this.beginStop(); + } + + async stopAndWait(opts?: { timeoutMs?: number }): Promise { + // Some callers need teardown ordering, not just "close requested". Wait for + // the socket to close or the terminate fallback to fire. + const stopPromise = this.beginStop(); + if (!stopPromise) { + return; + } + const timeoutMs = + typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) + ? Math.max(1, Math.floor(opts.timeoutMs)) + : STOP_AND_WAIT_TIMEOUT_MS; + let timeout: NodeJS.Timeout | null = null; + try { + await Promise.race([ + stopPromise, + new Promise((_, reject) => { + timeout = setTimeout(() => { + reject(new Error(`gateway client stop timed out after ${timeoutMs}ms`)); + }, timeoutMs); + timeout.unref?.(); + }), + ]); + } finally { + if (timeout) { + clearTimeout(timeout); + } + } + } + + private beginStop(): Promise | null { + this.closed = true; + this.pendingDeviceTokenRetry = false; + this.deviceTokenRetryBudgetUsed = false; + this.pendingStartupReconnectDelayMs = null; + this.pendingConnectErrorDetailCode = null; + this.pendingConnectErrorDetails = null; + this.clearReconnectTimer(); + if (this.tickTimer) { + clearInterval(this.tickTimer); + this.tickTimer = null; + } + this.clearConnectChallengeTimeout(); + if (this.pendingStop) { + this.flushPendingErrors(new Error("gateway client stopped")); + return this.pendingStop.promise; + } + const ws = this.ws; + this.ws = null; + if (ws) { + const stopPromise = this.createPendingStop(ws); + ws.close(); + const forceTerminateTimer = setTimeout(() => { + try { + ws.terminate(); + } catch {} + this.resolvePendingStop(ws); + }, FORCE_STOP_TERMINATE_GRACE_MS); + forceTerminateTimer.unref?.(); + this.flushPendingErrors(new Error("gateway client stopped")); + return stopPromise; + } + this.flushPendingErrors(new Error("gateway client stopped")); + return null; + } + + private createPendingStop(ws: WebSocket): Promise { + if (this.pendingStop?.ws === ws) { + return this.pendingStop.promise; + } + let resolve!: () => void; + const promise = new Promise((res) => { + resolve = res; + }); + this.pendingStop = { ws, promise, resolve }; + return promise; + } + + private resolvePendingStop(ws: WebSocket): void { + if (this.pendingStop?.ws !== ws) { + return; + } + const { resolve } = this.pendingStop; + this.pendingStop = null; + resolve(); + } + + private logDebug(message: string): void { + this.deps.logDebug(this.deps.redactForLog(message)); + } + + private logError(message: string): void { + this.deps.logError(this.deps.redactForLog(message)); + } + + private sendConnect() { + if (this.connectSent) { + return; + } + const nonce = normalizeOptionalString(this.connectNonce) ?? ""; + if (!nonce) { + this.notifyConnectError(new Error("gateway connect challenge missing nonce")); + this.ws?.close(1008, "connect challenge missing nonce"); + return; + } + const role = this.opts.role ?? "operator"; + let assembled: AssembledConnect; + try { + // Build the full connect frame before marking connectSent so synchronous + // signing/storage failures surface as connect-assembly errors, not RPCs. + assembled = this.assembleConnectParams({ role, nonce }); + } catch (err) { + this.handleConnectFailure(err); + return; + } + + this.connectSent = true; + this.clearConnectChallengeTimeout(); + + void this.request("connect", assembled.params) + .then((helloOk) => { + this.pendingDeviceTokenRetry = false; + this.deviceTokenRetryBudgetUsed = false; + this.pendingStartupReconnectDelayMs = null; + this.pendingConnectErrorDetailCode = null; + this.pendingConnectErrorDetails = null; + const authInfo = helloOk?.auth; + if (authInfo?.deviceToken && this.opts.deviceIdentity) { + this.deps.storeDeviceAuthToken({ + deviceId: this.opts.deviceIdentity.deviceId, + role: authInfo.role ?? role, + token: authInfo.deviceToken, + scopes: authInfo.scopes ?? [], + env: this.opts.env, + }); + } + this.backoffMs = 1000; + this.tickIntervalMs = + typeof helloOk.policy?.tickIntervalMs === "number" + ? helloOk.policy.tickIntervalMs + : 30_000; + this.lastTick = Date.now(); + this.startTickWatch(); + this.opts.onHelloOk?.(helloOk); + }) + .catch((err) => { + this.pendingConnectErrorDetailCode = + err instanceof GatewayClientRequestError ? readConnectErrorDetailCode(err.details) : null; + this.pendingConnectErrorDetails = + err instanceof GatewayClientRequestError ? err.details : null; + const shouldRetryWithDeviceToken = this.shouldRetryWithStoredDeviceToken({ + error: err, + explicitGatewayToken: normalizeOptionalString(this.opts.token), + resolvedDeviceToken: assembled.resolvedDeviceToken, + storedToken: assembled.storedToken, + }); + if ( + this.opts.deviceIdentity && + assembled.usingStoredDeviceToken && + err instanceof GatewayClientRequestError && + readConnectErrorDetailCode(err.details) === + ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH + ) { + const deviceId = this.opts.deviceIdentity.deviceId; + try { + this.deps.clearDeviceAuthToken({ deviceId, role, env: this.opts.env }); + this.logDebug(`cleared stale device-auth token for device ${deviceId}`); + } catch (clearErr) { + this.logDebug( + `failed clearing stale device-auth token for device ${deviceId}: ${String(clearErr)}`, + ); + } + } + if (shouldRetryWithDeviceToken) { + this.pendingDeviceTokenRetry = true; + this.deviceTokenRetryBudgetUsed = true; + this.backoffMs = Math.min(this.backoffMs, 250); + } + const startupRetryAfterMs = resolveGatewayStartupRetryAfterMs(err); + if (startupRetryAfterMs !== null) { + this.pendingStartupReconnectDelayMs = startupRetryAfterMs; + this.logDebug(`gateway connect failed: ${formatGatewayClientErrorForLog(err)}`); + this.ws?.close(1013, "gateway starting"); + return; + } + if ( + this.shouldRetryWithoutApprovalRuntimeToken({ + error: err, + authApprovalRuntimeToken: assembled.authApprovalRuntimeToken, + }) + ) { + this.approvalRuntimeTokenCompatibilityDisabled = true; + this.approvalRuntimeTokenRetryBudgetUsed = true; + this.backoffMs = Math.min(this.backoffMs, 250); + this.logDebug("gateway rejected approval runtime auth field; retrying without it"); + this.ws?.close(1008, "connect retry"); + return; + } + this.notifyConnectError(err instanceof Error ? err : new Error(String(err))); + const msg = `gateway connect failed: ${formatGatewayClientErrorForLog(err)}`; + if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE || isGatewayClientStoppedError(err)) { + this.logDebug(msg); + } else { + this.logError(msg); + } + this.ws?.close(1008, "connect failed"); + }); + } + + private assembleConnectParams(params: { role: string; nonce: string }): AssembledConnect { + const { role, nonce } = params; + // Auth selection is intentionally centralized: retry decisions depend on + // whether a token was explicit, cached, or compatibility-derived. + const selectedAuth = this.selectConnectAuth(role); + const { + authToken, + authBootstrapToken, + authDeviceToken, + authPassword, + authApprovalRuntimeToken, + signatureToken, + resolvedDeviceToken, + storedToken, + storedScopes, + usingStoredDeviceToken, + } = selectedAuth; + + if (this.pendingDeviceTokenRetry && authDeviceToken) { + this.pendingDeviceTokenRetry = false; + } + + const auth = + authToken || + authBootstrapToken || + authPassword || + resolvedDeviceToken || + authApprovalRuntimeToken + ? { + token: authToken, + bootstrapToken: authBootstrapToken, + deviceToken: authDeviceToken ?? resolvedDeviceToken, + password: authPassword, + approvalRuntimeToken: authApprovalRuntimeToken, + } + : undefined; + const signedAtMs = Date.now(); + const scopes = this.resolveConnectScopes({ + usingStoredDeviceToken, + storedScopes, + }); + const platform = this.opts.platform ?? process.platform; + + return { + params: { + minProtocol: this.opts.minProtocol ?? MIN_CLIENT_PROTOCOL_VERSION, + maxProtocol: this.opts.maxProtocol ?? PROTOCOL_VERSION, + client: { + id: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, + displayName: this.opts.clientDisplayName, + version: this.opts.clientVersion ?? DEFAULT_CLIENT_VERSION, + platform, + deviceFamily: this.opts.deviceFamily, + mode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND, + instanceId: this.opts.instanceId, + }, + caps: Array.isArray(this.opts.caps) ? this.opts.caps : [], + commands: Array.isArray(this.opts.commands) ? this.opts.commands : undefined, + permissions: + this.opts.permissions && typeof this.opts.permissions === "object" + ? this.opts.permissions + : undefined, + pathEnv: this.opts.pathEnv, + auth, + role, + scopes, + device: this.buildDeviceConnectParams({ + nonce, + role, + scopes, + signatureToken, + signedAtMs, + platform, + }), + }, + authApprovalRuntimeToken, + resolvedDeviceToken, + storedToken, + usingStoredDeviceToken, + }; + } + + private buildDeviceConnectParams(params: { + nonce: string; + role: string; + scopes: string[]; + signatureToken: string | undefined; + signedAtMs: number; + platform: string; + }): ConnectParams["device"] { + if (!this.opts.deviceIdentity) { + return undefined; + } + const { nonce, role, scopes, signatureToken, signedAtMs, platform } = params; + // The signed payload mirrors server verification exactly; keep metadata + // normalized here so different hosts sign the same logical device facts. + const payload = buildDeviceAuthPayloadV3({ + deviceId: this.opts.deviceIdentity.deviceId, + clientId: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, + clientMode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND, + role, + scopes, + signedAtMs, + token: signatureToken ?? null, + nonce, + platform, + deviceFamily: this.opts.deviceFamily, + }); + const signature = this.deps.signDevicePayload(this.opts.deviceIdentity.privateKeyPem, payload); + return { + id: this.opts.deviceIdentity.deviceId, + publicKey: this.deps.publicKeyRawBase64UrlFromPem(this.opts.deviceIdentity.publicKeyPem), + signature, + signedAt: signedAtMs, + nonce, + }; + } + + private handleConnectFailure(err: unknown) { + const error = err instanceof Error ? err : new Error(String(err)); + this.clearConnectChallengeTimeout(); + this.closed = true; + this.notifyConnectError(markGatewayConnectAssemblyError(error)); + const msg = `gateway connect failed: ${formatGatewayClientErrorForLog(error)}`; + if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE || isGatewayClientStoppedError(error)) { + this.logDebug(msg); + } else { + this.logError(msg); + } + this.ws?.close(1008, "connect failed"); + } + + private notifyConnectError(error: Error) { + try { + this.opts.onConnectError?.(error); + } catch (err) { + this.logDebug( + `gateway client connect error handler error: ${formatGatewayClientErrorForLog(err)}`, + ); + } + } + + private resolveConnectScopes(params: { + usingStoredDeviceToken?: boolean; + storedScopes?: string[]; + }): string[] { + // Reuse cached scopes only when the client is reusing the cached device token. + // Callers that ask for explicit scopes should keep that request so the + // server can authorize it or drive the normal scope-upgrade flow. + if (Array.isArray(this.opts.scopes)) { + return this.opts.scopes; + } + if ( + params.usingStoredDeviceToken && + Array.isArray(params.storedScopes) && + params.storedScopes.length > 0 + ) { + return params.storedScopes; + } + return this.opts.scopes ?? ["operator.admin"]; + } + + private loadStoredDeviceAuth(role: string): StoredDeviceAuth | null { + if (!this.opts.deviceIdentity) { + return null; + } + const storedAuth = this.deps.loadDeviceAuthToken({ + deviceId: this.opts.deviceIdentity.deviceId, + role, + env: this.opts.env, + }); + if (!storedAuth) { + return null; + } + return { + token: storedAuth.token, + scopes: storedAuth.scopes, + }; + } + + private shouldPauseReconnectAfterAuthFailure(params: { + detailCode: string | null; + details?: unknown; + }): boolean { + const { detailCode, details } = params; + if (!detailCode) { + return false; + } + const pairingDetails = readPairingConnectErrorDetails(details); + if ( + detailCode === ConnectErrorDetailCodes.PAIRING_REQUIRED && + (pairingDetails?.pauseReconnect === false || + pairingDetails?.recommendedNextStep === "wait_then_retry") + ) { + return false; + } + if ( + detailCode === ConnectErrorDetailCodes.AUTH_TOKEN_MISSING || + detailCode === ConnectErrorDetailCodes.AUTH_BOOTSTRAP_TOKEN_INVALID || + detailCode === ConnectErrorDetailCodes.AUTH_PASSWORD_MISSING || + detailCode === ConnectErrorDetailCodes.AUTH_PASSWORD_MISMATCH || + detailCode === ConnectErrorDetailCodes.AUTH_RATE_LIMITED || + detailCode === ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH || + detailCode === ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH || + detailCode === ConnectErrorDetailCodes.PAIRING_REQUIRED || + detailCode === ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED || + detailCode === ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED || + detailCode === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH + ) { + return true; + } + if (detailCode === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH) { + return !this.pendingDeviceTokenRetry; + } + return false; + } + + private shouldRetryWithStoredDeviceToken(params: { + error: unknown; + explicitGatewayToken?: string; + storedToken?: string; + resolvedDeviceToken?: string; + }): boolean { + if (this.deviceTokenRetryBudgetUsed) { + return false; + } + if (params.resolvedDeviceToken) { + return false; + } + if (!params.explicitGatewayToken || !params.storedToken) { + return false; + } + if (!this.isTrustedDeviceRetryEndpoint()) { + return false; + } + if (!(params.error instanceof GatewayClientRequestError)) { + return false; + } + const detailCode = readConnectErrorDetailCode(params.error.details); + const advice: ConnectErrorRecoveryAdvice = readConnectErrorRecoveryAdvice(params.error.details); + const retryWithDeviceTokenRecommended = + advice.recommendedNextStep === "retry_with_device_token"; + return ( + advice.canRetryWithDeviceToken === true || + retryWithDeviceTokenRecommended || + detailCode === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH + ); + } + + private shouldRetryWithoutApprovalRuntimeToken(params: { + error: unknown; + authApprovalRuntimeToken?: string; + }): boolean { + if (this.approvalRuntimeTokenRetryBudgetUsed) { + return false; + } + if (!params.authApprovalRuntimeToken) { + return false; + } + if (!(params.error instanceof GatewayClientRequestError)) { + return false; + } + if (params.error.gatewayCode !== "INVALID_REQUEST") { + return false; + } + const message = normalizeLowercaseStringOrEmpty(params.error.message); + return message.includes("invalid connect params") && message.includes("approvalruntimetoken"); + } + + private isTrustedDeviceRetryEndpoint(): boolean { + const rawUrl = this.opts.url ?? "ws://127.0.0.1:18789"; + try { + const parsed = new URL(rawUrl); + const protocol = + parsed.protocol === "https:" + ? "wss:" + : parsed.protocol === "http:" + ? "ws:" + : parsed.protocol; + if (isLoopbackHost(parsed.hostname)) { + return true; + } + return protocol === "wss:" && Boolean(this.opts.tlsFingerprint?.trim()); + } catch { + return false; + } + } + + private selectConnectAuth(role: string): SelectedConnectAuth { + const explicitGatewayToken = normalizeOptionalString(this.opts.token); + const explicitBootstrapToken = normalizeOptionalString(this.opts.bootstrapToken); + const explicitDeviceToken = normalizeOptionalString(this.opts.deviceToken); + const authPassword = normalizeOptionalString(this.opts.password); + const authApprovalRuntimeToken = this.approvalRuntimeTokenCompatibilityDisabled + ? undefined + : normalizeOptionalString(this.opts.approvalRuntimeToken); + const storedAuth = this.loadStoredDeviceAuth(role); + const storedToken = storedAuth?.token ?? null; + const storedScopes = storedAuth?.scopes; + const shouldUseDeviceRetryToken = + this.pendingDeviceTokenRetry && + !explicitDeviceToken && + Boolean(explicitGatewayToken) && + Boolean(storedToken) && + this.isTrustedDeviceRetryEndpoint(); + const resolvedDeviceToken = + explicitDeviceToken ?? + (shouldUseDeviceRetryToken || + (!(explicitGatewayToken || authPassword) && (!explicitBootstrapToken || Boolean(storedToken))) + ? (storedToken ?? undefined) + : undefined); + const reusingStoredDeviceToken = + Boolean(resolvedDeviceToken) && + !explicitDeviceToken && + Boolean(storedToken) && + resolvedDeviceToken === storedToken; + // Legacy compatibility: keep `auth.token` populated for device-token auth when + // no explicit shared token is present. + const authToken = explicitGatewayToken ?? resolvedDeviceToken; + const authBootstrapToken = + !explicitGatewayToken && !resolvedDeviceToken && !authPassword + ? explicitBootstrapToken + : undefined; + return { + authToken, + authBootstrapToken, + authDeviceToken: shouldUseDeviceRetryToken ? (storedToken ?? undefined) : undefined, + authPassword, + authApprovalRuntimeToken, + signatureToken: authToken ?? authBootstrapToken ?? undefined, + resolvedDeviceToken, + storedToken: storedToken ?? undefined, + storedScopes, + usingStoredDeviceToken: reusingStoredDeviceToken, + }; + } + + private handleMessage(raw: string) { + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (err) { + this.logDebug(`gateway client parse error: ${formatGatewayClientErrorForLog(err)}`); + return; + } + if (validateEventFrame(parsed)) { + this.lastTick = Date.now(); + const evt = parsed; + if (evt.event === "connect.challenge") { + const payload = evt.payload as { nonce?: unknown } | undefined; + const nonce = payload && typeof payload.nonce === "string" ? payload.nonce : null; + if (!nonce || nonce.trim().length === 0) { + this.notifyConnectError(new Error("gateway connect challenge missing nonce")); + this.ws?.close(1008, "connect challenge missing nonce"); + return; + } + this.connectNonce = nonce.trim(); + if (this.socketOpened) { + this.sendConnect(); + } + return; + } + try { + const seq = typeof evt.seq === "number" ? evt.seq : null; + if (seq !== null) { + if (this.lastSeq !== null && seq > this.lastSeq + 1) { + this.opts.onGap?.({ expected: this.lastSeq + 1, received: seq }); + } + this.lastSeq = seq; + } + if (evt.event === "tick") { + this.lastTick = Date.now(); + } + this.opts.onEvent?.(evt); + } catch (err) { + this.logDebug(`gateway client event handler error: ${formatGatewayClientErrorForLog(err)}`); + } + return; + } + if (validateResponseFrame(parsed)) { + this.lastTick = Date.now(); + const pending = this.pending.get(parsed.id); + if (!pending) { + return; + } + // If the payload is an ack with status accepted, keep waiting for final. + const payload = parsed.payload as { status?: unknown } | undefined; + const status = payload?.status; + if (pending.expectFinal && status === "accepted") { + if (!pending.acceptedNotified) { + pending.acceptedNotified = true; + try { + pending.onAccepted?.(parsed.payload); + } catch (err) { + this.logDebug( + `gateway client accepted callback error: ${formatGatewayClientErrorForLog(err)}`, + ); + } + } + return; + } + this.pending.delete(parsed.id); + pending.cleanup?.(); + if (parsed.ok) { + pending.resolve(parsed.payload); + } else { + pending.reject( + new GatewayClientRequestError({ + code: parsed.error?.code, + message: parsed.error?.message ?? "unknown error", + details: parsed.error?.details, + retryable: parsed.error?.retryable, + retryAfterMs: parsed.error?.retryAfterMs, + }), + ); + } + } + } + + private beginPreauthHandshake() { + if (this.connectSent) { + return; + } + if (this.connectNonce && !this.connectSent) { + this.armConnectChallengeTimeout(); + this.sendConnect(); + return; + } + this.armConnectChallengeTimeout(); + } + + private clearConnectChallengeTimeout() { + if (this.connectTimer) { + clearTimeout(this.connectTimer); + this.connectTimer = null; + } + } + + private clearReconnectTimer() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + } + + private armConnectChallengeTimeout() { + const connectChallengeTimeoutMs = resolveGatewayClientConnectChallengeTimeoutMs(this.opts); + const armedAt = Date.now(); + this.clearConnectChallengeTimeout(); + this.connectTimer = setTimeout(() => { + if (this.connectSent || this.ws?.readyState !== WebSocket.OPEN) { + return; + } + const elapsedMs = Date.now() - armedAt; + this.notifyConnectError( + new Error( + `gateway connect challenge timeout (waited ${elapsedMs}ms, limit ${connectChallengeTimeoutMs}ms)`, + ), + ); + this.ws?.close(1008, "connect challenge timeout"); + }, connectChallengeTimeoutMs); + } + + private scheduleReconnect() { + if (this.closed) { + return; + } + if (this.tickTimer) { + clearInterval(this.tickTimer); + this.tickTimer = null; + } + this.clearReconnectTimer(); + const startupDelay = this.pendingStartupReconnectDelayMs; + this.pendingStartupReconnectDelayMs = null; + const delay = startupDelay ?? this.backoffMs; + if (startupDelay === null) { + this.backoffMs = Math.min(this.backoffMs * 2, 30_000); + } + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; + this.start(); + }, delay); + } + + private flushPendingErrors(err: Error) { + for (const [, p] of this.pending) { + p.cleanup?.(); + p.reject(err); + } + this.pending.clear(); + } + + private startTickWatch() { + if (this.tickTimer) { + clearInterval(this.tickTimer); + } + const rawMinInterval = this.opts.tickWatchMinIntervalMs; + const minInterval = + typeof rawMinInterval === "number" && Number.isFinite(rawMinInterval) + ? Math.max(1, Math.min(30_000, rawMinInterval)) + : 1000; + const interval = Math.max(this.tickIntervalMs, minInterval); + this.tickTimer = setInterval(() => { + if (this.closed) { + return; + } + if (!this.lastTick) { + return; + } + if (this.pending.size > 0) { + return; + } + const gap = Date.now() - this.lastTick; + if (gap > this.tickIntervalMs * 2) { + this.ws?.close(4000, "tick timeout"); + } + }, interval); + } + + private validateTlsFingerprint(): Error | null { + if (!this.opts.tlsFingerprint || !this.ws) { + return null; + } + const expected = this.deps.normalizeTlsFingerprint(this.opts.tlsFingerprint); + if (!expected) { + return new Error("gateway tls fingerprint missing"); + } + const socket = ( + this.ws as WebSocket & { + _socket?: { getPeerCertificate?: () => { fingerprint256?: string } }; + } + )["_socket"]; + if (!socket || typeof socket.getPeerCertificate !== "function") { + return new Error("gateway tls fingerprint unavailable"); + } + const cert = socket.getPeerCertificate(); + const fingerprint = this.deps.normalizeTlsFingerprint(cert?.fingerprint256 ?? ""); + if (!fingerprint) { + return new Error("gateway tls fingerprint unavailable"); + } + if (fingerprint !== expected) { + return new Error("gateway tls fingerprint mismatch"); + } + return null; + } + + async request>( + method: string, + params?: unknown, + opts?: GatewayClientRequestOptions, + ): Promise { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error("gateway not connected"); + } + if (opts?.signal?.aborted) { + throw createGatewayRequestAbortError(method); + } + const id = randomUUID(); + const frame: RequestFrame = { type: "req", id, method, params }; + if (!validateRequestFrame(frame)) { + throw new Error( + `invalid request frame: ${JSON.stringify(validateRequestFrame.errors, null, 2)}`, + ); + } + const expectFinal = opts?.expectFinal === true; + const timeoutMs = + opts?.timeoutMs === null + ? null + : typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) + ? resolveSafeTimeoutDelayMs(opts.timeoutMs) + : expectFinal + ? null + : this.requestTimeoutMs; + const signal = opts?.signal; + const p = new Promise((resolve, reject) => { + let abortHandler: (() => void) | undefined; + const timeout = + timeoutMs === null + ? null + : setTimeout(() => { + const pending = this.pending.get(id); + this.pending.delete(id); + pending?.cleanup?.(); + reject(new Error(`gateway request timeout for ${method}`)); + }, timeoutMs); + const cleanup = () => { + if (timeout) { + clearTimeout(timeout); + } + if (signal && abortHandler) { + signal.removeEventListener("abort", abortHandler); + } + }; + abortHandler = () => { + const pending = this.pending.get(id); + this.pending.delete(id); + pending?.cleanup?.(); + reject(createGatewayRequestAbortError(method)); + }; + this.pending.set(id, { + resolve: (value) => resolve(value as T), + reject, + expectFinal, + timeout, + cleanup, + onAccepted: opts?.onAccepted, + }); + signal?.addEventListener("abort", abortHandler, { once: true }); + }); + this.ws.send(JSON.stringify(frame)); + return p; + } +} + +function createGatewayRequestAbortError(method: string): Error { + const err = new Error(`gateway request aborted for ${method}`); + err.name = "AbortError"; + return err; +} diff --git a/packages/gateway-client/src/device-auth.ts b/packages/gateway-client/src/device-auth.ts new file mode 100644 index 00000000000..10cbdd4ef96 --- /dev/null +++ b/packages/gateway-client/src/device-auth.ts @@ -0,0 +1,64 @@ +export function normalizeDeviceMetadataForAuth(value?: string | null): string { + if (typeof value !== "string") { + return ""; + } + const trimmed = value.trim(); + if (!trimmed) { + return ""; + } + return trimmed.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32)); +} + +type DeviceAuthPayloadParams = { + deviceId: string; + clientId: string; + clientMode: string; + role: string; + scopes: string[]; + signedAtMs: number; + token?: string | null; + nonce: string; +}; + +type DeviceAuthPayloadV3Params = DeviceAuthPayloadParams & { + platform?: string | null; + deviceFamily?: string | null; +}; + +export function buildDeviceAuthPayload(params: DeviceAuthPayloadParams): string { + const scopes = params.scopes.join(","); + const token = params.token ?? ""; + return [ + "v2", + params.deviceId, + params.clientId, + params.clientMode, + params.role, + scopes, + String(params.signedAtMs), + token, + params.nonce, + ].join("|"); +} + +export function buildDeviceAuthPayloadV3(params: DeviceAuthPayloadV3Params): string { + const scopes = params.scopes.join(","); + const token = params.token ?? ""; + // Device signatures are byte-for-byte compared by the gateway. Normalize + // optional metadata before joining so case differences do not break auth. + const platform = normalizeDeviceMetadataForAuth(params.platform); + const deviceFamily = normalizeDeviceMetadataForAuth(params.deviceFamily); + return [ + "v3", + params.deviceId, + params.clientId, + params.clientMode, + params.role, + scopes, + String(params.signedAtMs), + token, + params.nonce, + platform, + deviceFamily, + ].join("|"); +} diff --git a/packages/gateway-client/src/event-loop-ready.ts b/packages/gateway-client/src/event-loop-ready.ts new file mode 100644 index 00000000000..3796ce10981 --- /dev/null +++ b/packages/gateway-client/src/event-loop-ready.ts @@ -0,0 +1,116 @@ +function resolveSafeTimeoutDelayMs(value: number): number { + return Math.max(0, Math.min(value, 2_147_483_647)); +} + +export type EventLoopReadyResult = { + ready: boolean; + elapsedMs: number; + maxDriftMs: number; + checks: number; + aborted: boolean; +}; + +type EventLoopReadyOptions = { + maxWaitMs?: number; + intervalMs?: number; + driftThresholdMs?: number; + consecutiveReadyChecks?: number; + signal?: AbortSignal; +}; + +const DEFAULT_MAX_WAIT_MS = 10_000; +const DEFAULT_INTERVAL_MS = 1; +const DEFAULT_DRIFT_THRESHOLD_MS = 200; +const DEFAULT_CONSECUTIVE_READY_CHECKS = 2; + +function resolvePositiveInteger(value: number | undefined, fallback: number): number { + return Number.isFinite(value) && value !== undefined ? Math.max(1, Math.floor(value)) : fallback; +} + +export async function waitForEventLoopReady( + options: EventLoopReadyOptions = {}, +): Promise { + const maxWaitMs = resolveSafeTimeoutDelayMs(options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS); + const intervalMs = resolvePositiveInteger(options.intervalMs, DEFAULT_INTERVAL_MS); + const driftThresholdMs = resolvePositiveInteger( + options.driftThresholdMs, + DEFAULT_DRIFT_THRESHOLD_MS, + ); + const consecutiveReadyChecks = resolvePositiveInteger( + options.consecutiveReadyChecks, + DEFAULT_CONSECUTIVE_READY_CHECKS, + ); + const signal = options.signal; + + const startedAt = Date.now(); + let readyChecks = 0; + let checks = 0; + let maxDriftMs = 0; + + return await new Promise((resolve) => { + let settled = false; + let timer: ReturnType | null = null; + const clearTimer = () => { + if (timer) { + clearTimeout(timer); + timer = null; + } + }; + const finish = (ready: boolean, aborted = false) => { + if (settled) { + return; + } + settled = true; + clearTimer(); + signal?.removeEventListener("abort", onAbort); + resolve({ + ready, + elapsedMs: Math.max(0, Date.now() - startedAt), + maxDriftMs, + checks, + aborted, + }); + }; + const onAbort = () => { + finish(false, true); + }; + if (signal?.aborted) { + finish(false, true); + return; + } + signal?.addEventListener("abort", onAbort, { once: true }); + + const scheduleNext = () => { + if (signal?.aborted) { + finish(false, true); + return; + } + const elapsedMs = Math.max(0, Date.now() - startedAt); + const remainingMs = maxWaitMs - elapsedMs; + if (remainingMs <= 0) { + finish(false); + return; + } + const delayMs = Math.min(intervalMs, remainingMs); + const scheduledAt = Date.now(); + timer = setTimeout(() => { + timer = null; + checks += 1; + const driftMs = Math.max(0, Date.now() - scheduledAt - delayMs); + maxDriftMs = Math.max(maxDriftMs, driftMs); + if (driftMs > driftThresholdMs) { + readyChecks = 0; + } else { + readyChecks += 1; + } + if (readyChecks >= consecutiveReadyChecks) { + finish(true); + return; + } + scheduleNext(); + }, delayMs); + }; + + scheduleNext(); + }); +} diff --git a/packages/gateway-client/src/index.ts b/packages/gateway-client/src/index.ts new file mode 100644 index 00000000000..f43b4a52755 --- /dev/null +++ b/packages/gateway-client/src/index.ts @@ -0,0 +1,5 @@ +export * from "./client.js"; +export * from "./device-auth.js"; +export * from "./event-loop-ready.js"; +export * from "./readiness.js"; +export * from "./timeouts.js"; diff --git a/packages/gateway-client/src/readiness.ts b/packages/gateway-client/src/readiness.ts new file mode 100644 index 00000000000..8b95cb58a80 --- /dev/null +++ b/packages/gateway-client/src/readiness.ts @@ -0,0 +1,46 @@ +import type { GatewayClient, GatewayClientOptions } from "./client.js"; +import { waitForEventLoopReady, type EventLoopReadyResult } from "./event-loop-ready.js"; +import { resolveConnectChallengeTimeoutMs } from "./timeouts.js"; + +export type GatewayClientStartReadinessOptions = { + timeoutMs?: number; + clientOptions?: Pick< + GatewayClientOptions, + "connectChallengeTimeoutMs" | "connectDelayMs" | "preauthHandshakeTimeoutMs" + >; + signal?: AbortSignal; +}; + +function resolveGatewayClientStartReadinessTimeoutMs( + options: GatewayClientStartReadinessOptions = {}, +): number { + if (typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs)) { + return options.timeoutMs; + } + const clientOptions = options.clientOptions ?? {}; + const timeoutOverride = + typeof clientOptions.connectChallengeTimeoutMs === "number" && + Number.isFinite(clientOptions.connectChallengeTimeoutMs) + ? clientOptions.connectChallengeTimeoutMs + : typeof clientOptions.connectDelayMs === "number" && + Number.isFinite(clientOptions.connectDelayMs) + ? clientOptions.connectDelayMs + : undefined; + return resolveConnectChallengeTimeoutMs(timeoutOverride, { + configuredTimeoutMs: clientOptions.preauthHandshakeTimeoutMs, + }); +} + +export async function startGatewayClientWhenEventLoopReady( + client: GatewayClient, + options: GatewayClientStartReadinessOptions = {}, +): Promise { + const readiness = await waitForEventLoopReady({ + maxWaitMs: resolveGatewayClientStartReadinessTimeoutMs(options), + signal: options.signal, + }); + if (readiness.ready && !readiness.aborted && options.signal?.aborted !== true) { + client.start(); + } + return readiness; +} diff --git a/packages/gateway-client/src/timeouts.ts b/packages/gateway-client/src/timeouts.ts new file mode 100644 index 00000000000..7163fc28d9b --- /dev/null +++ b/packages/gateway-client/src/timeouts.ts @@ -0,0 +1,96 @@ +function parseStrictPositiveInteger(value: string): number | undefined { + if (!/^[1-9]\d*$/u.test(value)) { + return undefined; + } + const parsed = Number(value); + return Number.isSafeInteger(parsed) ? parsed : undefined; +} + +export const DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 15_000; +export const MIN_CONNECT_CHALLENGE_TIMEOUT_MS = 250; +export const MAX_CONNECT_CHALLENGE_TIMEOUT_MS = DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS; + +export function clampConnectChallengeTimeoutMs( + timeoutMs: number, + maxTimeoutMs = MAX_CONNECT_CHALLENGE_TIMEOUT_MS, +): number { + return Math.max( + MIN_CONNECT_CHALLENGE_TIMEOUT_MS, + Math.min(Math.max(MIN_CONNECT_CHALLENGE_TIMEOUT_MS, maxTimeoutMs), timeoutMs), + ); +} + +export function getConnectChallengeTimeoutMsFromEnv( + env: NodeJS.ProcessEnv = process.env, +): number | undefined { + const raw = env.OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS; + if (raw) { + const parsed = parseStrictPositiveInteger(raw); + if (parsed !== undefined) { + return parsed; + } + } + return undefined; +} + +function normalizePositiveTimeoutMs(timeoutMs: unknown): number | undefined { + return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 + ? timeoutMs + : undefined; +} + +export function resolveConnectChallengeTimeoutMs( + timeoutMs?: number | null, + params?: { + env?: NodeJS.ProcessEnv; + configuredTimeoutMs?: number | null; + }, +): number { + const configuredPreauthTimeoutMs = resolvePreauthHandshakeTimeoutMs({ + env: params?.env, + configuredTimeoutMs: params?.configuredTimeoutMs, + }); + // The client watchdog must never fire before the server-side preauth timeout. + // Tests may raise the env override above that server default, so widen the cap. + const maxTimeoutMs = Math.max(DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS, configuredPreauthTimeoutMs); + if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) { + return clampConnectChallengeTimeoutMs(timeoutMs, maxTimeoutMs); + } + const envOverride = getConnectChallengeTimeoutMsFromEnv(params?.env); + if (envOverride !== undefined) { + return clampConnectChallengeTimeoutMs(envOverride, Math.max(maxTimeoutMs, envOverride)); + } + return clampConnectChallengeTimeoutMs(configuredPreauthTimeoutMs, maxTimeoutMs); +} + +export function getPreauthHandshakeTimeoutMsFromEnv(env: NodeJS.ProcessEnv = process.env): number { + const configuredTimeout = + env.OPENCLAW_HANDSHAKE_TIMEOUT_MS || (env.VITEST && env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS); + if (configuredTimeout) { + const parsed = parseStrictPositiveInteger(configuredTimeout); + if (parsed !== undefined) { + return parsed; + } + } + return DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS; +} + +export function resolvePreauthHandshakeTimeoutMs(params?: { + env?: NodeJS.ProcessEnv; + configuredTimeoutMs?: number | null; +}): number { + const env = params?.env ?? process.env; + const configuredTimeout = + env.OPENCLAW_HANDSHAKE_TIMEOUT_MS || (env.VITEST && env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS); + if (configuredTimeout) { + const parsed = parseStrictPositiveInteger(configuredTimeout); + if (parsed !== undefined) { + return parsed; + } + } + const configured = normalizePositiveTimeoutMs(params?.configuredTimeoutMs); + if (configured !== undefined) { + return configured; + } + return DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS; +} diff --git a/packages/gateway-protocol/package.json b/packages/gateway-protocol/package.json new file mode 100644 index 00000000000..b7b2c0a57b6 --- /dev/null +++ b/packages/gateway-protocol/package.json @@ -0,0 +1,49 @@ +{ + "name": "@openclaw/gateway-protocol", + "version": "0.0.0-private", + "private": true, + "files": [ + "dist" + ], + "type": "module", + "main": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs", + "default": "./dist/index.mjs" + }, + "./client-info": { + "types": "./dist/client-info.d.mts", + "import": "./dist/client-info.mjs", + "default": "./dist/client-info.mjs" + }, + "./connect-error-details": { + "types": "./dist/connect-error-details.d.mts", + "import": "./dist/connect-error-details.mjs", + "default": "./dist/connect-error-details.mjs" + }, + "./schema": { + "types": "./dist/schema.d.mts", + "import": "./dist/schema.mjs", + "default": "./dist/schema.mjs" + }, + "./startup-unavailable": { + "types": "./dist/startup-unavailable.d.mts", + "import": "./dist/startup-unavailable.mjs", + "default": "./dist/startup-unavailable.mjs" + }, + "./version": { + "types": "./dist/version.d.mts", + "import": "./dist/version.mjs", + "default": "./dist/version.mjs" + } + }, + "scripts": { + "build": "tsdown src/index.ts src/client-info.ts src/connect-error-details.ts src/schema.ts src/startup-unavailable.ts src/version.ts --no-config --platform node --format esm --dts --out-dir dist --clean" + }, + "dependencies": { + "typebox": "1.1.38" + } +} diff --git a/src/gateway/protocol/channels.schema.test.ts b/packages/gateway-protocol/src/channels.schema.test.ts similarity index 100% rename from src/gateway/protocol/channels.schema.test.ts rename to packages/gateway-protocol/src/channels.schema.test.ts diff --git a/src/gateway/protocol/client-info.ts b/packages/gateway-protocol/src/client-info.ts similarity index 91% rename from src/gateway/protocol/client-info.ts rename to packages/gateway-protocol/src/client-info.ts index fe6576c13c4..6109f91e2c1 100644 --- a/src/gateway/protocol/client-info.ts +++ b/packages/gateway-protocol/src/client-info.ts @@ -1,4 +1,10 @@ -import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; +function normalizeOptionalLowercaseString(raw?: string | null): string | undefined { + if (typeof raw !== "string") { + return undefined; + } + const normalized = raw.trim().toLowerCase(); + return normalized || undefined; +} export const GATEWAY_CLIENT_IDS = { WEBCHAT_UI: "webchat-ui", diff --git a/src/gateway/protocol/connect-error-details.test.ts b/packages/gateway-protocol/src/connect-error-details.test.ts similarity index 100% rename from src/gateway/protocol/connect-error-details.test.ts rename to packages/gateway-protocol/src/connect-error-details.test.ts diff --git a/src/gateway/protocol/connect-error-details.ts b/packages/gateway-protocol/src/connect-error-details.ts similarity index 96% rename from src/gateway/protocol/connect-error-details.ts rename to packages/gateway-protocol/src/connect-error-details.ts index a931afb488b..4e681416d27 100644 --- a/src/gateway/protocol/connect-error-details.ts +++ b/packages/gateway-protocol/src/connect-error-details.ts @@ -1,5 +1,22 @@ -import { normalizeOptionalString } from "../../shared/string-coerce.js"; -import { normalizeArrayBackedTrimmedStringList } from "../../shared/string-normalization.js"; +function normalizeOptionalString(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + return trimmed || undefined; +} + +function normalizeArrayBackedTrimmedStringList(value: unknown): string[] | undefined { + if (!Array.isArray(value)) { + return undefined; + } + const values = value + .map((entry) => normalizeOptionalString(entry)) + .filter((entry): entry is string => Boolean(entry)); + // Pairing details omit absent lists. Emitting empty arrays makes clients think + // the gateway intentionally supplied scope/role context when it did not. + return values.length > 0 ? values : undefined; +} export const ConnectErrorDetailCodes = { AUTH_REQUIRED: "AUTH_REQUIRED", diff --git a/src/gateway/protocol/cron-validators.test.ts b/packages/gateway-protocol/src/cron-validators.test.ts similarity index 100% rename from src/gateway/protocol/cron-validators.test.ts rename to packages/gateway-protocol/src/cron-validators.test.ts diff --git a/src/gateway/protocol/exec-approvals-validators.test.ts b/packages/gateway-protocol/src/exec-approvals-validators.test.ts similarity index 100% rename from src/gateway/protocol/exec-approvals-validators.test.ts rename to packages/gateway-protocol/src/exec-approvals-validators.test.ts diff --git a/src/gateway/protocol/index.test.ts b/packages/gateway-protocol/src/index.test.ts similarity index 99% rename from src/gateway/protocol/index.test.ts rename to packages/gateway-protocol/src/index.test.ts index 895542af081..e860f7ca97f 100644 --- a/src/gateway/protocol/index.test.ts +++ b/packages/gateway-protocol/src/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { TALK_TEST_PROVIDER_ID } from "../../test-utils/talk-test-provider.js"; +import { TALK_TEST_PROVIDER_ID } from "../../../src/test-utils/talk-test-provider.js"; import * as protocol from "./index.js"; import { formatValidationErrors, diff --git a/src/gateway/protocol/index.ts b/packages/gateway-protocol/src/index.ts similarity index 98% rename from src/gateway/protocol/index.ts rename to packages/gateway-protocol/src/index.ts index dd591ec660e..df761b69178 100644 --- a/src/gateway/protocol/index.ts +++ b/packages/gateway-protocol/src/index.ts @@ -1,6 +1,4 @@ import { Compile, type Validator as TypeBoxValidator } from "typebox/compile"; -import { uniqueStrings } from "../../shared/string-normalization.js"; -import type { SessionsPatchResult } from "../session-utils.types.js"; import { type AgentEvent, AgentEventSchema, @@ -1266,3 +1264,26 @@ export type { UpdateRunParams, ChatInjectParams, }; +function uniqueStrings(values: string[]): string[] { + return [...new Set(values)]; +} + +// The protocol package cannot import core session types. This local structural +// result mirrors the wire contract and keeps the package independent of src/. +type SessionsPatchResult = { + ok: true; + path: string; + key: string; + entry: Record; + resolved?: { + modelProvider?: string; + model?: string; + agentRuntime?: GatewayAgentRuntime; + }; +}; + +type GatewayAgentRuntime = { + id: string; + fallback?: "openclaw" | "none"; + source: "env" | "agent" | "defaults" | "model" | "provider" | "implicit" | "session-key"; +}; diff --git a/src/gateway/protocol/native-protocol-levels.guard.test.ts b/packages/gateway-protocol/src/native-protocol-levels.guard.test.ts similarity index 91% rename from src/gateway/protocol/native-protocol-levels.guard.test.ts rename to packages/gateway-protocol/src/native-protocol-levels.guard.test.ts index ed1a98acb43..6feb8f5f881 100644 --- a/src/gateway/protocol/native-protocol-levels.guard.test.ts +++ b/packages/gateway-protocol/src/native-protocol-levels.guard.test.ts @@ -26,7 +26,7 @@ function extractInteger( const match = pattern.exec(content); if (!match) { throw new Error( - `${relativePath}: missing ${label}; keep native Gateway protocol levels in sync with src/gateway/protocol/version.ts.`, + `${relativePath}: missing ${label}; keep native Gateway protocol levels in sync with packages/gateway-protocol/src/version.ts.`, ); } return Number.parseInt(match[1], 10); @@ -37,7 +37,7 @@ function assertLevelsMatch(relativePath: string, actual: ProtocolLevels): void { return; } throw new Error( - `${relativePath}: Gateway protocol level mismatch: expected min=${expectedLevels.min} max=${expectedLevels.max} from src/gateway/protocol/version.ts, got min=${actual.min} max=${actual.max}. Update the native constants/generated artifacts before shipping.`, + `${relativePath}: Gateway protocol level mismatch: expected min=${expectedLevels.min} max=${expectedLevels.max} from packages/gateway-protocol/src/version.ts, got min=${actual.min} max=${actual.max}. Update the native constants/generated artifacts before shipping.`, ); } @@ -57,7 +57,7 @@ describe("native Gateway protocol levels", () => { it("match the TypeScript source of truth", async () => { if (MIN_CLIENT_PROTOCOL_VERSION > PROTOCOL_VERSION) { throw new Error( - `src/gateway/protocol/version.ts: MIN_CLIENT_PROTOCOL_VERSION (${MIN_CLIENT_PROTOCOL_VERSION}) must not exceed PROTOCOL_VERSION (${PROTOCOL_VERSION}).`, + `packages/gateway-protocol/src/version.ts: MIN_CLIENT_PROTOCOL_VERSION (${MIN_CLIENT_PROTOCOL_VERSION}) must not exceed PROTOCOL_VERSION (${PROTOCOL_VERSION}).`, ); } diff --git a/src/gateway/protocol/primitives.secretref.test.ts b/packages/gateway-protocol/src/primitives.secretref.test.ts similarity index 95% rename from src/gateway/protocol/primitives.secretref.test.ts rename to packages/gateway-protocol/src/primitives.secretref.test.ts index 7a30b2d878b..649e2ff89f6 100644 --- a/src/gateway/protocol/primitives.secretref.test.ts +++ b/packages/gateway-protocol/src/primitives.secretref.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { INVALID_EXEC_SECRET_REF_IDS, VALID_EXEC_SECRET_REF_IDS, -} from "../../test-utils/secret-ref-test-vectors.js"; +} from "../../../src/test-utils/secret-ref-test-vectors.js"; import { SecretInputSchema, SecretRefSchema } from "./schema/primitives.js"; describe("gateway protocol SecretRef schema", () => { diff --git a/src/gateway/protocol/push.test.ts b/packages/gateway-protocol/src/push.test.ts similarity index 100% rename from src/gateway/protocol/push.test.ts rename to packages/gateway-protocol/src/push.test.ts diff --git a/src/gateway/protocol/schema.ts b/packages/gateway-protocol/src/schema.ts similarity index 95% rename from src/gateway/protocol/schema.ts rename to packages/gateway-protocol/src/schema.ts index 8f23712efcc..a14ea64a0af 100644 --- a/src/gateway/protocol/schema.ts +++ b/packages/gateway-protocol/src/schema.ts @@ -1,3 +1,4 @@ +export * from "./schema/primitives.js"; export * from "./schema/agent.js"; export * from "./schema/agents-models-skills.js"; export * from "./schema/artifacts.js"; diff --git a/src/gateway/protocol/schema/agent.test.ts b/packages/gateway-protocol/src/schema/agent.test.ts similarity index 85% rename from src/gateway/protocol/schema/agent.test.ts rename to packages/gateway-protocol/src/schema/agent.test.ts index 93214be0e13..dac19a785fd 100644 --- a/src/gateway/protocol/schema/agent.test.ts +++ b/packages/gateway-protocol/src/schema/agent.test.ts @@ -1,8 +1,22 @@ import { Value } from "typebox/value"; import { describe, expect, it } from "vitest"; -import type { AgentInternalEvent } from "../../../agents/internal-events.js"; import { AgentParamsSchema } from "./agent.js"; +type AgentInternalEvent = { + type: "task_completion"; + source: string; + childSessionKey: string; + childSessionId: string; + announceType: string; + taskLabel: string; + status: "ok" | "error"; + statusLabel: string; + result: string; + attachments?: unknown[]; + mediaUrls?: string[]; + replyInstruction?: string; +}; + function makeAgentParamsWithInternalEvent(event: AgentInternalEvent) { return { message: "A music generation task finished. Process the completion update now.", diff --git a/src/gateway/protocol/schema/agent.ts b/packages/gateway-protocol/src/schema/agent.ts similarity index 96% rename from src/gateway/protocol/schema/agent.ts rename to packages/gateway-protocol/src/schema/agent.ts index 67e54cab700..768893147b3 100644 --- a/src/gateway/protocol/schema/agent.ts +++ b/packages/gateway-protocol/src/schema/agent.ts @@ -1,11 +1,16 @@ import { Type } from "typebox"; -import { - AGENT_INTERNAL_EVENT_SOURCES, - AGENT_INTERNAL_EVENT_STATUSES, - AGENT_INTERNAL_EVENT_TYPE_TASK_COMPLETION, -} from "../../../agents/internal-event-contract.js"; import { InputProvenanceSchema, NonEmptyString, SessionLabelString } from "./primitives.js"; +const AGENT_INTERNAL_EVENT_TYPE_TASK_COMPLETION = "task_completion"; +const AGENT_INTERNAL_EVENT_SOURCES = [ + "subagent", + "cron", + "image_generation", + "video_generation", + "music_generation", +] as const; +const AGENT_INTERNAL_EVENT_STATUSES = ["ok", "timeout", "error", "unknown"] as const; + export const AgentGeneratedAttachmentSchema = Type.Object( { type: Type.Optional(Type.String({ enum: ["image", "audio", "video", "file"] })), diff --git a/src/gateway/protocol/schema/agents-models-skills.test.ts b/packages/gateway-protocol/src/schema/agents-models-skills.test.ts similarity index 100% rename from src/gateway/protocol/schema/agents-models-skills.test.ts rename to packages/gateway-protocol/src/schema/agents-models-skills.test.ts diff --git a/src/gateway/protocol/schema/agents-models-skills.ts b/packages/gateway-protocol/src/schema/agents-models-skills.ts similarity index 100% rename from src/gateway/protocol/schema/agents-models-skills.ts rename to packages/gateway-protocol/src/schema/agents-models-skills.ts diff --git a/src/gateway/protocol/schema/artifacts.ts b/packages/gateway-protocol/src/schema/artifacts.ts similarity index 100% rename from src/gateway/protocol/schema/artifacts.ts rename to packages/gateway-protocol/src/schema/artifacts.ts diff --git a/src/gateway/protocol/schema/channels.ts b/packages/gateway-protocol/src/schema/channels.ts similarity index 100% rename from src/gateway/protocol/schema/channels.ts rename to packages/gateway-protocol/src/schema/channels.ts diff --git a/src/gateway/protocol/schema/commands.ts b/packages/gateway-protocol/src/schema/commands.ts similarity index 100% rename from src/gateway/protocol/schema/commands.ts rename to packages/gateway-protocol/src/schema/commands.ts diff --git a/src/gateway/protocol/schema/config.ts b/packages/gateway-protocol/src/schema/config.ts similarity index 100% rename from src/gateway/protocol/schema/config.ts rename to packages/gateway-protocol/src/schema/config.ts diff --git a/src/gateway/protocol/schema/cron.ts b/packages/gateway-protocol/src/schema/cron.ts similarity index 100% rename from src/gateway/protocol/schema/cron.ts rename to packages/gateway-protocol/src/schema/cron.ts diff --git a/src/gateway/protocol/schema/devices.ts b/packages/gateway-protocol/src/schema/devices.ts similarity index 100% rename from src/gateway/protocol/schema/devices.ts rename to packages/gateway-protocol/src/schema/devices.ts diff --git a/src/gateway/protocol/schema/environments.ts b/packages/gateway-protocol/src/schema/environments.ts similarity index 100% rename from src/gateway/protocol/schema/environments.ts rename to packages/gateway-protocol/src/schema/environments.ts diff --git a/src/gateway/protocol/schema/error-codes.ts b/packages/gateway-protocol/src/schema/error-codes.ts similarity index 100% rename from src/gateway/protocol/schema/error-codes.ts rename to packages/gateway-protocol/src/schema/error-codes.ts diff --git a/src/gateway/protocol/schema/exec-approvals.ts b/packages/gateway-protocol/src/schema/exec-approvals.ts similarity index 100% rename from src/gateway/protocol/schema/exec-approvals.ts rename to packages/gateway-protocol/src/schema/exec-approvals.ts diff --git a/src/gateway/protocol/schema/frames.ts b/packages/gateway-protocol/src/schema/frames.ts similarity index 100% rename from src/gateway/protocol/schema/frames.ts rename to packages/gateway-protocol/src/schema/frames.ts diff --git a/src/gateway/protocol/schema/logs-chat.ts b/packages/gateway-protocol/src/schema/logs-chat.ts similarity index 100% rename from src/gateway/protocol/schema/logs-chat.ts rename to packages/gateway-protocol/src/schema/logs-chat.ts diff --git a/src/gateway/protocol/schema/nodes.ts b/packages/gateway-protocol/src/schema/nodes.ts similarity index 100% rename from src/gateway/protocol/schema/nodes.ts rename to packages/gateway-protocol/src/schema/nodes.ts diff --git a/src/gateway/protocol/schema/plugin-approvals.ts b/packages/gateway-protocol/src/schema/plugin-approvals.ts similarity index 89% rename from src/gateway/protocol/schema/plugin-approvals.ts rename to packages/gateway-protocol/src/schema/plugin-approvals.ts index eb33e26843b..1f821233542 100644 --- a/src/gateway/protocol/schema/plugin-approvals.ts +++ b/packages/gateway-protocol/src/schema/plugin-approvals.ts @@ -1,11 +1,10 @@ import { Type } from "typebox"; -import { - MAX_PLUGIN_APPROVAL_TIMEOUT_MS, - PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH, - PLUGIN_APPROVAL_TITLE_MAX_LENGTH, -} from "../../../infra/plugin-approvals.js"; import { NonEmptyString } from "./primitives.js"; +const MAX_PLUGIN_APPROVAL_TIMEOUT_MS = 600_000; +const PLUGIN_APPROVAL_TITLE_MAX_LENGTH = 80; +const PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH = 256; + export const PluginApprovalRequestParamsSchema = Type.Object( { pluginId: Type.Optional(NonEmptyString), diff --git a/src/gateway/protocol/schema/plugins.ts b/packages/gateway-protocol/src/schema/plugins.ts similarity index 100% rename from src/gateway/protocol/schema/plugins.ts rename to packages/gateway-protocol/src/schema/plugins.ts diff --git a/src/gateway/protocol/schema/primitives.ts b/packages/gateway-protocol/src/schema/primitives.ts similarity index 90% rename from src/gateway/protocol/schema/primitives.ts rename to packages/gateway-protocol/src/schema/primitives.ts index 0e3534d8e52..e6bcf33a1f7 100644 --- a/src/gateway/protocol/schema/primitives.ts +++ b/packages/gateway-protocol/src/schema/primitives.ts @@ -1,15 +1,16 @@ import { Type } from "typebox"; -import { ENV_SECRET_REF_ID_RE } from "../../../config/types.secrets.js"; +import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../client-info.js"; import { EXEC_SECRET_REF_ID_JSON_SCHEMA_PATTERN, FILE_SECRET_REF_ID_ABSOLUTE_JSON_SCHEMA_PATTERN, FILE_SECRET_REF_ID_INVALID_ESCAPE_JSON_SCHEMA_PATTERN, SECRET_PROVIDER_ALIAS_PATTERN, SINGLE_VALUE_FILE_REF_ID, -} from "../../../secrets/ref-contract.js"; -import { INPUT_PROVENANCE_KIND_VALUES } from "../../../sessions/input-provenance.js"; -import { SESSION_LABEL_MAX_LENGTH } from "../../../sessions/session-label.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../client-info.js"; +} from "../secret-ref-contract.js"; + +const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/; +const INPUT_PROVENANCE_KIND_VALUES = ["external_user", "inter_session", "internal_system"] as const; +const SESSION_LABEL_MAX_LENGTH = 512; export const NonEmptyString = Type.String({ minLength: 1 }); export const CHAT_SEND_SESSION_KEY_MAX_LENGTH = 512; diff --git a/src/gateway/protocol/schema/protocol-schemas.ts b/packages/gateway-protocol/src/schema/protocol-schemas.ts similarity index 100% rename from src/gateway/protocol/schema/protocol-schemas.ts rename to packages/gateway-protocol/src/schema/protocol-schemas.ts diff --git a/src/gateway/protocol/schema/push.ts b/packages/gateway-protocol/src/schema/push.ts similarity index 100% rename from src/gateway/protocol/schema/push.ts rename to packages/gateway-protocol/src/schema/push.ts diff --git a/src/gateway/protocol/schema/secrets.ts b/packages/gateway-protocol/src/schema/secrets.ts similarity index 100% rename from src/gateway/protocol/schema/secrets.ts rename to packages/gateway-protocol/src/schema/secrets.ts diff --git a/src/gateway/protocol/schema/sessions.ts b/packages/gateway-protocol/src/schema/sessions.ts similarity index 100% rename from src/gateway/protocol/schema/sessions.ts rename to packages/gateway-protocol/src/schema/sessions.ts diff --git a/src/gateway/protocol/schema/snapshot.ts b/packages/gateway-protocol/src/schema/snapshot.ts similarity index 100% rename from src/gateway/protocol/schema/snapshot.ts rename to packages/gateway-protocol/src/schema/snapshot.ts diff --git a/src/gateway/protocol/schema/tasks.ts b/packages/gateway-protocol/src/schema/tasks.ts similarity index 100% rename from src/gateway/protocol/schema/tasks.ts rename to packages/gateway-protocol/src/schema/tasks.ts diff --git a/src/gateway/protocol/schema/types.ts b/packages/gateway-protocol/src/schema/types.ts similarity index 100% rename from src/gateway/protocol/schema/types.ts rename to packages/gateway-protocol/src/schema/types.ts diff --git a/src/gateway/protocol/schema/wizard.ts b/packages/gateway-protocol/src/schema/wizard.ts similarity index 100% rename from src/gateway/protocol/schema/wizard.ts rename to packages/gateway-protocol/src/schema/wizard.ts diff --git a/packages/gateway-protocol/src/secret-ref-contract.ts b/packages/gateway-protocol/src/secret-ref-contract.ts new file mode 100644 index 00000000000..b5aea66bc69 --- /dev/null +++ b/packages/gateway-protocol/src/secret-ref-contract.ts @@ -0,0 +1,7 @@ +export const SINGLE_VALUE_FILE_REF_ID = "value"; + +export const SECRET_PROVIDER_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/; +export const FILE_SECRET_REF_ID_ABSOLUTE_JSON_SCHEMA_PATTERN = "^/"; +export const FILE_SECRET_REF_ID_INVALID_ESCAPE_JSON_SCHEMA_PATTERN = "~(?:[^01]|$)"; +export const EXEC_SECRET_REF_ID_JSON_SCHEMA_PATTERN = + "^(?!.*(?:^|/)\\.{1,2}(?:/|$))[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$"; diff --git a/src/gateway/protocol/startup-unavailable.ts b/packages/gateway-protocol/src/startup-unavailable.ts similarity index 100% rename from src/gateway/protocol/startup-unavailable.ts rename to packages/gateway-protocol/src/startup-unavailable.ts diff --git a/src/gateway/protocol/talk-config.contract.test.ts b/packages/gateway-protocol/src/talk-config.contract.test.ts similarity index 96% rename from src/gateway/protocol/talk-config.contract.test.ts rename to packages/gateway-protocol/src/talk-config.contract.test.ts index 3c429530354..123e70cb26f 100644 --- a/src/gateway/protocol/talk-config.contract.test.ts +++ b/packages/gateway-protocol/src/talk-config.contract.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import { describe, expect, it } from "vitest"; -import { buildTalkConfigResponse } from "../../config/talk.js"; +import { buildTalkConfigResponse } from "../../../src/config/talk.js"; import { validateTalkConfigResult } from "./index.js"; type ExpectedSelection = { diff --git a/src/gateway/protocol/version.ts b/packages/gateway-protocol/src/version.ts similarity index 100% rename from src/gateway/protocol/version.ts rename to packages/gateway-protocol/src/version.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4b7ea357b0..446c9502a77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1743,6 +1743,24 @@ importers: specifier: 2.9.0 version: 2.9.0 + packages/gateway-client: + dependencies: + '@openclaw/gateway-protocol': + specifier: workspace:* + version: link:../gateway-protocol + ipaddr.js: + specifier: 2.4.0 + version: 2.4.0 + ws: + specifier: 8.21.0 + version: 8.21.0 + + packages/gateway-protocol: + dependencies: + typebox: + specifier: 1.1.38 + version: 1.1.38 + packages/memory-host-sdk: {} packages/plugin-package-contract: {} diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index 227b22c9ced..c581c0c1cc5 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -49,7 +49,8 @@ const FAST_INSTALL_SMOKE_SCOPE_RE = /^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|agents-delete-shared-workspace-docker\.sh|gateway-network-docker\.sh)$|extensions\/[^/]+\/(?:package\.json|openclaw\.plugin\.json)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/; const FULL_INSTALL_SMOKE_SCOPE_RE = /^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/install(?:-cli)?\.sh$|scripts\/install\.ps1$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|qr-import-docker\.sh|bun-global-install-smoke\.sh)$|\.github\/workflows\/(?:install-smoke|website-installer-sync)\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/; -const FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE = /^src\/(?:channels|gateway|plugin-sdk|plugins)\//; +const FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE = + /^(?:src\/(?:channels|gateway|plugin-sdk|plugins)\/|packages\/gateway-(?:client|protocol)\/src\/)/; const NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE = /^(src\/plugins\/contracts\/(?:inventory\/bundled-capability-metadata|registry|tts-contract-suites)\.ts$|scripts\/test-projects(?:\.test-support)?\.mjs$|test\/scripts\/test-projects\.test\.ts$)/; const NODE_FAST_CI_ROUTING_SCOPE_RE = diff --git a/scripts/dev/gateway-smoke.ts b/scripts/dev/gateway-smoke.ts index 230f66258c2..d38cc00f1bb 100644 --- a/scripts/dev/gateway-smoke.ts +++ b/scripts/dev/gateway-smoke.ts @@ -1,8 +1,8 @@ -import { createArgReader, createGatewayWsClient, resolveGatewayUrl } from "./gateway-ws-client.ts"; import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION, -} from "../../src/gateway/protocol/version.ts"; +} from "../../packages/gateway-protocol/src/version.js"; +import { createArgReader, createGatewayWsClient, resolveGatewayUrl } from "./gateway-ws-client.ts"; function writeStdoutLine(message: string): void { process.stdout.write(`${message}\n`); diff --git a/scripts/dev/ios-node-e2e.ts b/scripts/dev/ios-node-e2e.ts index b56354cb288..e6930c4d0f8 100644 --- a/scripts/dev/ios-node-e2e.ts +++ b/scripts/dev/ios-node-e2e.ts @@ -1,8 +1,8 @@ -import { createArgReader, createGatewayWsClient, resolveGatewayUrl } from "./gateway-ws-client.ts"; import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION, -} from "../../src/gateway/protocol/version.ts"; +} from "../../packages/gateway-protocol/src/version.js"; +import { createArgReader, createGatewayWsClient, resolveGatewayUrl } from "./gateway-ws-client.ts"; function writeStdoutLine(message = ""): void { process.stdout.write(`${message}\n`); diff --git a/scripts/protocol-gen-swift.ts b/scripts/protocol-gen-swift.ts index 3bcf707e8f3..99d09f45ad2 100644 --- a/scripts/protocol-gen-swift.ts +++ b/scripts/protocol-gen-swift.ts @@ -6,7 +6,7 @@ import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION, ProtocolSchemas, -} from "../src/gateway/protocol/schema.js"; +} from "../packages/gateway-protocol/src/schema.js"; type JsonSchema = { type?: string | string[]; diff --git a/scripts/protocol-gen.ts b/scripts/protocol-gen.ts index 80d40e735f8..865dc2d3bc8 100644 --- a/scripts/protocol-gen.ts +++ b/scripts/protocol-gen.ts @@ -1,7 +1,7 @@ import { promises as fs } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { ProtocolSchemas } from "../src/gateway/protocol/schema.js"; +import { ProtocolSchemas } from "../packages/gateway-protocol/src/schema.js"; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, ".."); diff --git a/scripts/run-node-watch-paths.mjs b/scripts/run-node-watch-paths.mjs index c92fef8a3f3..916afdedbeb 100644 --- a/scripts/run-node-watch-paths.mjs +++ b/scripts/run-node-watch-paths.mjs @@ -4,7 +4,18 @@ import { BUNDLED_PLUGIN_ROOT_DIR, } from "./lib/bundled-plugin-paths.mjs"; -export const runNodeSourceRoots = ["src", BUNDLED_PLUGIN_ROOT_DIR]; +const RUN_NODE_PACKAGE_SOURCE_ROOTS = [ + // Gateway runtime code now lives in package sources, but pnpm dev/watch still + // runs the root dist entrypoint. Treat these package roots like src/. + "packages/gateway-client/src", + "packages/gateway-protocol/src", +]; + +export const runNodeSourceRoots = [ + "src", + ...RUN_NODE_PACKAGE_SOURCE_ROOTS, + BUNDLED_PLUGIN_ROOT_DIR, +]; export const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.config.ts"]; export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles]; export const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]); @@ -50,6 +61,11 @@ const isRelevantRunNodePath = (repoPath, isRelevantBundledPluginPath) => { if (normalizedPath.startsWith("src/")) { return !isIgnoredSourcePath(normalizedPath.slice("src/".length)); } + for (const sourceRoot of RUN_NODE_PACKAGE_SOURCE_ROOTS) { + if (normalizedPath.startsWith(`${sourceRoot}/`)) { + return !isIgnoredSourcePath(normalizedPath.slice(sourceRoot.length + 1)); + } + } if (normalizedPath.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) { return isRelevantBundledPluginPath(normalizedPath.slice(BUNDLED_PLUGIN_PATH_PREFIX.length)); } diff --git a/src/acp/server.ts b/src/acp/server.ts index e9f87aee7cf..4453e3c9829 100644 --- a/src/acp/server.ts +++ b/src/acp/server.ts @@ -2,15 +2,15 @@ import { Readable, Writable } from "node:stream"; import { fileURLToPath } from "node:url"; import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"; -import { getRuntimeConfig } from "../config/config.js"; -import { resolveGatewayClientBootstrap } from "../gateway/client-bootstrap.js"; -import { startGatewayClientWhenEventLoopReady } from "../gateway/client-start-readiness.js"; -import { GatewayClient } from "../gateway/client.js"; import { GATEWAY_CLIENT_CAPS, GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, -} from "../gateway/protocol/client-info.js"; +} from "../../packages/gateway-protocol/src/client-info.js"; +import { getRuntimeConfig } from "../config/config.js"; +import { resolveGatewayClientBootstrap } from "../gateway/client-bootstrap.js"; +import { startGatewayClientWhenEventLoopReady } from "../gateway/client-start-readiness.js"; +import { GatewayClient } from "../gateway/client.js"; import { isMainModule } from "../infra/is-main.js"; import { routeLogsToStderr } from "../logging/console.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; diff --git a/src/acp/translator.cancel-scoping.test.ts b/src/acp/translator.cancel-scoping.test.ts index 6e5f189e8bb..1b892a0c669 100644 --- a/src/acp/translator.cancel-scoping.test.ts +++ b/src/acp/translator.cancel-scoping.test.ts @@ -1,7 +1,7 @@ import type { CancelNotification, PromptRequest, PromptResponse } from "@agentclientprotocol/sdk"; import { describe, expect, it, vi } from "vitest"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; -import type { EventFrame } from "../gateway/protocol/index.js"; import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.event-ledger.test.ts b/src/acp/translator.event-ledger.test.ts index 2d7a281343a..b060745fff8 100644 --- a/src/acp/translator.event-ledger.test.ts +++ b/src/acp/translator.event-ledger.test.ts @@ -4,8 +4,8 @@ import type { PromptRequest, } from "@agentclientprotocol/sdk"; import { describe, expect, it, vi } from "vitest"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; -import type { EventFrame } from "../gateway/protocol/index.js"; import { createInMemoryAcpEventLedger, type AcpEventLedger } from "./event-ledger.js"; import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; diff --git a/src/acp/translator.permission-relay.test.ts b/src/acp/translator.permission-relay.test.ts index e8872d97849..75e5d431fa7 100644 --- a/src/acp/translator.permission-relay.test.ts +++ b/src/acp/translator.permission-relay.test.ts @@ -1,7 +1,7 @@ import type { CancelNotification } from "@agentclientprotocol/sdk"; import { describe, expect, it, vi } from "vitest"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; -import type { EventFrame } from "../gateway/protocol/index.js"; import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { promptAgent } from "./translator.prompt-harness.test-support.js"; diff --git a/src/acp/translator.prompt-harness.test-support.ts b/src/acp/translator.prompt-harness.test-support.ts index b12701ac3a1..d809559de9c 100644 --- a/src/acp/translator.prompt-harness.test-support.ts +++ b/src/acp/translator.prompt-harness.test-support.ts @@ -1,7 +1,7 @@ import type { PromptRequest } from "@agentclientprotocol/sdk"; import { expect, vi } from "vitest"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; -import type { EventFrame } from "../gateway/protocol/index.js"; import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.session-rate-limit.test.ts b/src/acp/translator.session-rate-limit.test.ts index 5b6a0282892..7a9edc22ddc 100644 --- a/src/acp/translator.session-rate-limit.test.ts +++ b/src/acp/translator.session-rate-limit.test.ts @@ -6,8 +6,8 @@ import type { SetSessionModeRequest, } from "@agentclientprotocol/sdk"; import { describe, expect, it, vi } from "vitest"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; -import type { EventFrame } from "../gateway/protocol/index.js"; import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.ts b/src/acp/translator.ts index f4e8529196d..a9c2a549f4e 100644 --- a/src/acp/translator.ts +++ b/src/acp/translator.ts @@ -33,9 +33,9 @@ import type { ToolCallLocation, ToolKind, } from "@agentclientprotocol/sdk"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import { BASE_THINKING_LEVELS } from "../auto-reply/thinking.shared.js"; import type { GatewayClient } from "../gateway/client.js"; -import type { EventFrame } from "../gateway/protocol/index.js"; import type { GatewaySessionRow, SessionsListResult } from "../gateway/session-utils.js"; import { createFixedWindowRateLimiter, diff --git a/src/agents/embedded-agent-runner/run/payloads.test.ts b/src/agents/embedded-agent-runner/run/payloads.test.ts index c3f2568cf8d..e95f08d4dbd 100644 --- a/src/agents/embedded-agent-runner/run/payloads.test.ts +++ b/src/agents/embedded-agent-runner/run/payloads.test.ts @@ -198,7 +198,7 @@ describe("buildEmbeddedRunPayloads tool-error warnings", () => { it("does not replay raw-looking accumulated tool output when final answer text is available", () => { const payloads = buildPayloads({ assistantTexts: [ - "/root/openclaw/src/gateway/protocol/schema/protocol-schemas.ts:181: PluginControlUiDescriptorSchema,", + "/root/openclaw/packages/gateway-protocol/src/schema/protocol-schemas.ts:181: PluginControlUiDescriptorSchema,", "The schema export is fixed.", ], lastAssistant: { diff --git a/src/agents/tools/embedded-gateway-stub.ts b/src/agents/tools/embedded-gateway-stub.ts index 116b6733472..c050327746e 100644 --- a/src/agents/tools/embedded-gateway-stub.ts +++ b/src/agents/tools/embedded-gateway-stub.ts @@ -1,6 +1,9 @@ +import type { + SessionsListParams, + SessionsResolveParams, +} from "../../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { CallGatewayOptions } from "../../gateway/call.js"; -import type { SessionsListParams, SessionsResolveParams } from "../../gateway/protocol/index.js"; import type { ReadSessionMessagesAsyncOptions } from "../../gateway/session-utils.fs.js"; import type { SessionsListResult } from "../../gateway/session-utils.types.js"; import type { SessionsResolveResult } from "../../gateway/sessions-resolve.js"; diff --git a/src/agents/tools/gateway.ts b/src/agents/tools/gateway.ts index 02a72370add..537648cb4e8 100644 --- a/src/agents/tools/gateway.ts +++ b/src/agents/tools/gateway.ts @@ -1,3 +1,7 @@ +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../../packages/gateway-protocol/src/client-info.js"; import { getRuntimeConfig, resolveGatewayPort } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { callGateway } from "../../gateway/call.js"; @@ -7,7 +11,6 @@ import { type OperatorScope, } from "../../gateway/method-scopes.js"; import { getOperatorApprovalRuntimeToken } from "../../gateway/operator-approval-runtime-token.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../gateway/protocol/client-info.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { normalizeLowercaseStringOrEmpty, diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index c16bf46023c..584f86e0ad2 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -1,4 +1,8 @@ import { Type, type TSchema } from "typebox"; +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../../packages/gateway-protocol/src/client-info.js"; import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js"; import type { InboundEventKind } from "../../channels/inbound-event/kind.js"; import { @@ -21,7 +25,6 @@ import { getScopedChannelsCommandSecretTargets } from "../../cli/command-secret- import { resolveMessageSecretScope } from "../../cli/message-secret-scope.js"; import { getRuntimeConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../gateway/protocol/client-info.js"; import { getToolResult, runMessageAction } from "../../infra/outbound/message-action-runner.js"; import { resolveAllowedMessageActions } from "../../infra/outbound/outbound-policy.js"; import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js"; diff --git a/src/agents/tools/nodes-tool.ts b/src/agents/tools/nodes-tool.ts index e46a9395c60..cc709278558 100644 --- a/src/agents/tools/nodes-tool.ts +++ b/src/agents/tools/nodes-tool.ts @@ -1,8 +1,8 @@ import crypto from "node:crypto"; import { Type } from "typebox"; +import { readConnectPairingRequiredMessage } from "../../../packages/gateway-protocol/src/connect-error-details.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { OperatorScope } from "../../gateway/method-scopes.js"; -import { readConnectPairingRequiredMessage } from "../../gateway/protocol/connect-error-details.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { resolveNodePairApprovalScopes } from "../../infra/node-pairing-authz.js"; import type { GatewayMessageChannel } from "../../utils/message-channel.js"; diff --git a/src/agents/tools/sessions-resolution.ts b/src/agents/tools/sessions-resolution.ts index e759fe37bae..c3176697db7 100644 --- a/src/agents/tools/sessions-resolution.ts +++ b/src/agents/tools/sessions-resolution.ts @@ -1,9 +1,9 @@ -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { callGateway } from "../../gateway/call.js"; import { GATEWAY_CLIENT_IDS, normalizeGatewayClientId, -} from "../../gateway/protocol/client-info.js"; +} from "../../../packages/gateway-protocol/src/client-info.js"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { callGateway } from "../../gateway/call.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { listSpawnedSessionKeys, diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index dfc1ef0eeaa..9b6e4799c12 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -1,10 +1,13 @@ import type { TSchema } from "typebox"; +import type { + GatewayClientMode, + GatewayClientName, +} from "../../../packages/gateway-protocol/src/client-info.js"; import type { AgentTool, AgentToolResult } from "../../agents/runtime/index.js"; import type { ReplyPayload } from "../../auto-reply/reply-payload.js"; import type { MsgContext } from "../../auto-reply/templating.js"; import type { MarkdownTableMode } from "../../config/types.base.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import type { GatewayClientMode, GatewayClientName } from "../../gateway/protocol/client-info.js"; import type { MessagePresentation } from "../../interactive/payload.js"; import type { OutboundMediaAccess } from "../../media/load-options.js"; import type { PollInput } from "../../polls.js"; diff --git a/src/cli/capability-cli.ts b/src/cli/capability-cli.ts index 466bf4d30e6..77baefa7031 100644 --- a/src/cli/capability-cli.ts +++ b/src/cli/capability-cli.ts @@ -5,6 +5,10 @@ import path from "node:path"; import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; import type { Command } from "commander"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; import { resolveAgentDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { listProfilesForProvider, @@ -32,7 +36,6 @@ import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { buildGatewayConnectionDetailsWithResolvers } from "../gateway/connection-details.js"; import { isLoopbackHost } from "../gateway/net.js"; import { ADMIN_SCOPE } from "../gateway/operator-scopes.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; import { generateImage, listRuntimeImageGenerationProviders } from "../image-generation/runtime.js"; import type { ImageGenerationBackground, diff --git a/src/cli/command-secret-gateway.ts b/src/cli/command-secret-gateway.ts index 4950466de8d..88044959de0 100644 --- a/src/cli/command-secret-gateway.ts +++ b/src/cli/command-secret-gateway.ts @@ -1,8 +1,11 @@ +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; +import { validateSecretsResolveResult } from "../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveSecretInputRef } from "../config/types.secrets.js"; import { callGateway } from "../gateway/call.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; -import { validateSecretsResolveResult } from "../gateway/protocol/index.js"; import { formatErrorMessage } from "../infra/errors.js"; import { resolveManifestContractOwnerPluginId } from "../plugins/plugin-registry.js"; import { diff --git a/src/cli/devices-cli.runtime.ts b/src/cli/devices-cli.runtime.ts index 5e6deb44735..ad654399c2d 100644 --- a/src/cli/devices-cli.runtime.ts +++ b/src/cli/devices-cli.runtime.ts @@ -1,3 +1,11 @@ +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; +import { + readConnectPairingRequiredMessage, + type ConnectPairingRequiredDetails, +} from "../../packages/gateway-protocol/src/connect-error-details.js"; import { buildGatewayConnectionDetails, callGateway, @@ -5,11 +13,6 @@ import { } from "../gateway/call.js"; import { ADMIN_SCOPE, PAIRING_SCOPE, type OperatorScope } from "../gateway/method-scopes.js"; import { isLoopbackHost } from "../gateway/net.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; -import { - readConnectPairingRequiredMessage, - type ConnectPairingRequiredDetails, -} from "../gateway/protocol/connect-error-details.js"; import { approveDevicePairing, formatDevicePairingForbiddenMessage, diff --git a/src/cli/gateway-cli/call.ts b/src/cli/gateway-cli/call.ts index 4669e70bcfe..2083f09df1c 100644 --- a/src/cli/gateway-cli/call.ts +++ b/src/cli/gateway-cli/call.ts @@ -1,7 +1,10 @@ import type { Command } from "commander"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../../packages/gateway-protocol/src/client-info.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { callGateway } from "../../gateway/call.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../gateway/protocol/client-info.js"; import { parseTimeoutMsWithFallback } from "../parse-timeout.js"; import { withProgress } from "../progress.js"; diff --git a/src/cli/gateway-rpc.runtime.ts b/src/cli/gateway-rpc.runtime.ts index f0707099581..69571539ee5 100644 --- a/src/cli/gateway-rpc.runtime.ts +++ b/src/cli/gateway-rpc.runtime.ts @@ -1,5 +1,8 @@ +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; import { callGateway } from "../gateway/call.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; import type { GatewayRpcOpts } from "./gateway-rpc.types.js"; import { parseTimeoutMsWithFallback } from "./parse-timeout.js"; import { withProgress } from "./progress.js"; diff --git a/src/cli/gateway-rpc.ts b/src/cli/gateway-rpc.ts index 26b364924b3..fa2db3f8d9b 100644 --- a/src/cli/gateway-rpc.ts +++ b/src/cli/gateway-rpc.ts @@ -1,6 +1,9 @@ import type { Command } from "commander"; +import type { + GatewayClientMode, + GatewayClientName, +} from "../../packages/gateway-protocol/src/client-info.js"; import type { OperatorScope } from "../gateway/operator-scopes.js"; -import type { GatewayClientMode, GatewayClientName } from "../gateway/protocol/client-info.js"; import type { DeviceIdentity } from "../infra/device-identity.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; import type { GatewayRpcOpts } from "./gateway-rpc.types.js"; diff --git a/src/cli/logs-cli.ts b/src/cli/logs-cli.ts index 0ed3f0dc244..df00f896ccb 100644 --- a/src/cli/logs-cli.ts +++ b/src/cli/logs-cli.ts @@ -1,13 +1,16 @@ import { setTimeout as delay } from "node:timers/promises"; import type { Command } from "commander"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; +import { readConnectPairingRequiredMessage } from "../../packages/gateway-protocol/src/connect-error-details.js"; import { buildGatewayConnectionDetails, isGatewayTransportError, type GatewayConnectionDetails, } from "../gateway/call.js"; import { isLoopbackHost } from "../gateway/net.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; -import { readConnectPairingRequiredMessage } from "../gateway/protocol/connect-error-details.js"; import { computeBackoff } from "../infra/backoff.js"; import { formatErrorMessage } from "../infra/errors.js"; import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; diff --git a/src/cli/nodes-cli/rpc.runtime.ts b/src/cli/nodes-cli/rpc.runtime.ts index d3d52788f35..78639e45fd9 100644 --- a/src/cli/nodes-cli/rpc.runtime.ts +++ b/src/cli/nodes-cli/rpc.runtime.ts @@ -1,6 +1,9 @@ +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../../packages/gateway-protocol/src/client-info.js"; import { callGateway } from "../../gateway/call.js"; import type { OperatorScope } from "../../gateway/method-scopes.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../gateway/protocol/client-info.js"; import { parseTimeoutMsWithFallback } from "../parse-timeout.js"; import { withProgress } from "../progress.js"; import type { NodesRpcOpts } from "./types.js"; diff --git a/src/commands/agent-via-gateway.ts b/src/commands/agent-via-gateway.ts index 6d08e719cb0..1a5ed32b0fb 100644 --- a/src/commands/agent-via-gateway.ts +++ b/src/commands/agent-via-gateway.ts @@ -1,5 +1,9 @@ import { randomUUID } from "node:crypto"; import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { formatCliCommand } from "../cli/command-format.js"; import type { CliDeps } from "../cli/deps.types.js"; @@ -13,7 +17,6 @@ import { type GatewayRequestFunction, } from "../gateway/call.js"; import { ADMIN_SCOPE } from "../gateway/operator-scopes.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import { routeLogsToStderr } from "../logging/console.js"; import { diff --git a/src/commands/doctor-ui.ts b/src/commands/doctor-ui.ts index 877849e32d8..3aa8cb2a99d 100644 --- a/src/commands/doctor-ui.ts +++ b/src/commands/doctor-ui.ts @@ -24,7 +24,7 @@ export async function maybeRepairUiProtocolFreshness( return; } - const schemaPath = path.join(root, "src/gateway/protocol/schema.ts"); + const schemaPath = path.join(root, "packages/gateway-protocol/src/schema.ts"); const uiHealth = await resolveControlUiDistIndexHealth({ root, argv1: process.argv[1], @@ -92,7 +92,7 @@ export async function maybeRepairUiProtocolFreshness( "log", `--since=${uiMtimeIso}`, "--format=%h %s", - "src/gateway/protocol/schema.ts", + "packages/gateway-protocol/src/schema.ts", ], { timeoutMs: 5000 }, ).catch(() => null); diff --git a/src/commands/message.ts b/src/commands/message.ts index 82db69dcc95..dd8e5ee3f33 100644 --- a/src/commands/message.ts +++ b/src/commands/message.ts @@ -1,3 +1,7 @@ +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { CHANNEL_MESSAGE_ACTION_NAMES } from "../channels/plugins/message-action-names.js"; import type { ChannelMessageActionName } from "../channels/plugins/types.public.js"; @@ -8,7 +12,6 @@ import { resolveMessageSecretScope } from "../cli/message-secret-scope.js"; import { createOutboundSendDeps, type CliDeps } from "../cli/outbound-send-deps.js"; import { withProgress } from "../cli/progress.js"; import { getRuntimeConfig } from "../config/config.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; import type { OutboundSendDeps } from "../infra/outbound/deliver.js"; import { runMessageAction } from "../infra/outbound/message-action-runner.js"; import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; diff --git a/src/commands/status.command-report-data.ts b/src/commands/status.command-report-data.ts index 57f343dbc6f..751083884e0 100644 --- a/src/commands/status.command-report-data.ts +++ b/src/commands/status.command-report-data.ts @@ -1,4 +1,4 @@ -import type { ConnectPairingRequiredReason } from "../gateway/protocol/connect-error-details.js"; +import type { ConnectPairingRequiredReason } from "../../packages/gateway-protocol/src/connect-error-details.js"; import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js"; import type { resolveOsSummary } from "../infra/os-summary.js"; import type { PluginCompatibilityNotice } from "../plugins/status.js"; diff --git a/src/commands/status.command-sections.ts b/src/commands/status.command-sections.ts index dd45c7967a8..6062cb85a40 100644 --- a/src/commands/status.command-sections.ts +++ b/src/commands/status.command-sections.ts @@ -1,9 +1,9 @@ -import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js"; import { buildPairingConnectRecoveryTitle, describePairingConnectRequirement, type ConnectPairingRequiredReason, -} from "../gateway/protocol/connect-error-details.js"; +} from "../../packages/gateway-protocol/src/connect-error-details.js"; +import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js"; import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js"; import type { Tone } from "../memory-host-sdk/status.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index 3db8f13a3e1..99b993cb729 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -1,10 +1,10 @@ -import { withProgress } from "../cli/progress.js"; import { normalizePairingConnectRequestId, readConnectPairingRequiredMessage, readPairingConnectErrorDetails, type ConnectPairingRequiredReason, -} from "../gateway/protocol/connect-error-details.js"; +} from "../../packages/gateway-protocol/src/connect-error-details.js"; +import { withProgress } from "../cli/progress.js"; import { readRestartSentinel } from "../infra/restart-sentinel.js"; import { type RuntimeEnv } from "../runtime.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; diff --git a/src/commands/status.scan.shared.ts b/src/commands/status.scan.shared.ts index 790463b295c..8b484a15c3e 100644 --- a/src/commands/status.scan.shared.ts +++ b/src/commands/status.scan.shared.ts @@ -1,10 +1,13 @@ import { existsSync } from "node:fs"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; import type { OpenClawConfig } from "../config/types.js"; import { buildGatewayConnectionDetailsWithResolvers } from "../gateway/connection-details.js"; import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js"; import { resolveGatewayProbeTarget } from "../gateway/probe-target.js"; import type { GatewayProbeResult, probeGateway as probeGatewayFn } from "../gateway/probe.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; import type { MemoryProviderStatus } from "../memory-host-sdk/engine-storage.js"; import { defaultSlotIdForKey } from "../plugins/slots.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; diff --git a/src/crestodian/tui-backend.ts b/src/crestodian/tui-backend.ts index 5243de26abf..36d9a21b381 100644 --- a/src/crestodian/tui-backend.ts +++ b/src/crestodian/tui-backend.ts @@ -1,5 +1,8 @@ import { randomUUID } from "node:crypto"; -import type { SessionsPatchParams, SessionsPatchResult } from "../gateway/protocol/index.js"; +import type { + SessionsPatchParams, + SessionsPatchResult, +} from "../../packages/gateway-protocol/src/index.js"; import { buildAgentMainSessionKey } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import type { diff --git a/src/cron/cron-protocol-conformance.test.ts b/src/cron/cron-protocol-conformance.test.ts index 5815640ed52..e9ee9f1f0aa 100644 --- a/src/cron/cron-protocol-conformance.test.ts +++ b/src/cron/cron-protocol-conformance.test.ts @@ -1,12 +1,12 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { MACOS_APP_SOURCES_DIR } from "../compat/legacy-names.js"; import { CronDeliverySchema, CronJobStateSchema, CronRunLogEntrySchema, -} from "../gateway/protocol/schema.js"; +} from "../../packages/gateway-protocol/src/schema.js"; +import { MACOS_APP_SOURCES_DIR } from "../compat/legacy-names.js"; type SchemaLike = { anyOf?: Array; diff --git a/src/cron/cron-protocol-schema.test.ts b/src/cron/cron-protocol-schema.test.ts index 76e6689f089..4fcd5b5f37f 100644 --- a/src/cron/cron-protocol-schema.test.ts +++ b/src/cron/cron-protocol-schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { CronJobStateSchema } from "../gateway/protocol/schema.js"; +import { CronJobStateSchema } from "../../packages/gateway-protocol/src/schema.js"; type SchemaLike = { properties?: Record; diff --git a/src/cron/normalize.test.ts b/src/cron/normalize.test.ts index 851bac55088..81a92580caf 100644 --- a/src/cron/normalize.test.ts +++ b/src/cron/normalize.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { validateCronAddParams, validateCronUpdateParams } from "../gateway/protocol/index.js"; +import { + validateCronAddParams, + validateCronUpdateParams, +} from "../../packages/gateway-protocol/src/index.js"; import { normalizeCronJobCreate, normalizeCronJobPatch } from "./normalize.js"; import { DEFAULT_TOP_OF_HOUR_STAGGER_MS } from "./stagger.js"; diff --git a/src/gateway/call.ts b/src/gateway/call.ts index 4b3df026ecd..d12d73fbfbc 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -1,4 +1,8 @@ import { randomUUID } from "node:crypto"; +import { + MIN_CLIENT_PROTOCOL_VERSION, + PROTOCOL_VERSION, +} from "../../packages/gateway-protocol/src/index.js"; import { getRuntimeConfig } from "../config/io.js"; import { resolveConfigPath as resolveConfigPathFromPaths, @@ -49,7 +53,6 @@ import { resolveLeastPrivilegeOperatorScopesForMethod, type OperatorScope, } from "./method-scopes.js"; -import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION } from "./protocol/index.js"; export type { GatewayConnectionDetails }; export type GatewayRequestFunction = >( diff --git a/src/gateway/client.test.ts b/src/gateway/client.test.ts index 6191d2225a0..1ee537c9996 100644 --- a/src/gateway/client.test.ts +++ b/src/gateway/client.test.ts @@ -1,9 +1,12 @@ import { Buffer } from "node:buffer"; import { generateKeyPairSync } from "node:crypto"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { + MIN_CLIENT_PROTOCOL_VERSION, + PROTOCOL_VERSION, +} from "../../packages/gateway-protocol/src/index.js"; import type { DeviceIdentity } from "../infra/device-identity.js"; import { captureEnv } from "../test-utils/env.js"; -import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION } from "./protocol/index.js"; type MockLoggingConfig = { redactPatterns?: string[]; @@ -358,6 +361,34 @@ describe("GatewayClient security checks", () => { client.stop(); }); + it("does not treat hostnames starting with 127 as loopback", () => { + const onConnectError = vi.fn(); + const client = new GatewayClient({ + url: "ws://127.example.com:18789", + onConnectError, + }); + + client.start(); + + expectSecurityConnectError(onConnectError, { expectTailscaleHint: true }); + expect(wsInstances.length).toBe(0); + client.stop(); + }); + + it("allows ws:// to IPv4-mapped loopback addresses", () => { + const onConnectError = vi.fn(); + const client = new GatewayClient({ + url: "ws://[::ffff:127.0.0.1]:18789", + onConnectError, + }); + + client.start(); + + expect(onConnectError).not.toHaveBeenCalled(); + expect(wsInstances.length).toBe(1); + client.stop(); + }); + it("bootstraps inherited managed proxy routing before proxy-mode loopback WebSocket creation", () => { process.env.OPENCLAW_PROXY_ACTIVE = "1"; process.env.OPENCLAW_PROXY_LOOPBACK_MODE = "proxy"; @@ -515,6 +546,20 @@ describe("GatewayClient security checks", () => { client.stop(); }); + it("allows ws:// to IPv6 link-local addresses across fe80::/10", () => { + const onConnectError = vi.fn(); + const client = new GatewayClient({ + url: "ws://[fe90::1]:18789", + onConnectError, + }); + + client.start(); + + expect(onConnectError).not.toHaveBeenCalled(); + expect(wsInstances.length).toBe(1); + client.stop(); + }); + it("allows ws:// hostnames with OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1", () => { process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS = "1"; const onConnectError = vi.fn(); diff --git a/src/gateway/client.ts b/src/gateway/client.ts index c810c11e199..62c50e254db 100644 --- a/src/gateway/client.ts +++ b/src/gateway/client.ts @@ -1,5 +1,16 @@ -import { randomUUID } from "node:crypto"; -import { WebSocket, type ClientOptions, type CertMeta } from "ws"; +import { + GatewayClient as BaseGatewayClient, + GATEWAY_CLOSE_CODE_HINTS as BASE_GATEWAY_CLOSE_CODE_HINTS, + GatewayClientRequestError as BaseGatewayClientRequestError, + describeGatewayCloseCode as baseDescribeGatewayCloseCode, + isGatewayConnectAssemblyError as baseIsGatewayConnectAssemblyError, + resolveGatewayClientConnectChallengeTimeoutMs as baseResolveGatewayClientConnectChallengeTimeoutMs, +} from "../../packages/gateway-client/src/index.js"; +import type { + GatewayClientMode, + GatewayClientName, +} from "../../packages/gateway-protocol/src/client-info.js"; +import type { EventFrame, HelloOk } from "../../packages/gateway-protocol/src/index.js"; import { clearDeviceAuthToken, loadDeviceAuthToken, @@ -16,156 +27,102 @@ import { registerManagedProxyGatewayLoopbackBypass, } from "../infra/net/proxy/proxy-lifecycle.js"; import { normalizeFingerprint } from "../infra/tls/fingerprint.js"; -import { rawDataToString } from "../infra/ws.js"; import { logDebug, logError } from "../logger.js"; import { redactToolPayloadText } from "../logging/redact.js"; -import { isSensitiveUrlQueryParamName } from "../shared/net/redact-sensitive-url.js"; -import { - normalizeLowercaseStringOrEmpty, - normalizeOptionalString, -} from "../shared/string-coerce.js"; -import { - GATEWAY_CLIENT_MODES, - GATEWAY_CLIENT_NAMES, - type GatewayClientMode, - type GatewayClientName, -} from "../utils/message-channel.js"; -import { resolveSafeTimeoutDelayMs } from "../utils/timer-delay.js"; import { VERSION } from "../version.js"; -import { buildDeviceAuthPayloadV3 } from "./device-auth.js"; -import { resolveConnectChallengeTimeoutMs } from "./handshake-timeouts.js"; -import { isLoopbackHost, isSecureWebSocketUrl } from "./net.js"; -import { - ConnectErrorDetailCodes, - formatConnectErrorMessage, - readConnectErrorDetailCode, - readConnectErrorRecoveryAdvice, - readPairingConnectErrorDetails, - type ConnectErrorRecoveryAdvice, -} from "./protocol/connect-error-details.js"; -import { - type ConnectParams, - type EventFrame, - type HelloOk, - MIN_CLIENT_PROTOCOL_VERSION, - PROTOCOL_VERSION, - type RequestFrame, - validateEventFrame, - validateRequestFrame, - validateResponseFrame, -} from "./protocol/index.js"; -import { resolveGatewayStartupRetryAfterMs } from "./protocol/startup-unavailable.js"; -type Pending = { - resolve: (value: unknown) => void; - reject: (err: unknown) => void; - expectFinal: boolean; - timeout: NodeJS.Timeout | null; - cleanup?: () => void; - onAccepted?: (payload: unknown) => void; - acceptedNotified?: boolean; +export type DeviceAuthTokenRecord = { + token?: string; + scopes?: string[]; +}; + +export type GatewayClientHostDeps = { + loadOrCreateDeviceIdentity?: () => DeviceIdentity | undefined; + signDevicePayload?: (privateKeyPem: string, payload: string) => string; + publicKeyRawBase64UrlFromPem?: (publicKeyPem: string) => string; + loadDeviceAuthToken?: (params: { + deviceId: string; + role: string; + env?: NodeJS.ProcessEnv; + }) => DeviceAuthTokenRecord | null; + storeDeviceAuthToken?: (params: { + deviceId: string; + role: string; + token: string; + scopes: string[]; + env?: NodeJS.ProcessEnv; + }) => void; + clearDeviceAuthToken?: (params: { + deviceId: string; + role: string; + env?: NodeJS.ProcessEnv; + }) => void; + beforeConnect?: () => void; + registerGatewayLoopbackBypass?: (url: string) => (() => void) | undefined; + logDebug?: (message: string) => void; + logError?: (message: string) => void; + redactForLog?: (message: string) => string; + normalizeTlsFingerprint?: (fingerprint: string | undefined) => string; }; export type GatewayClientRequestOptions = { expectFinal?: boolean; timeoutMs?: number | null; signal?: AbortSignal; - /** Called once for expectFinal requests after an accepted response, before the final result. */ onAccepted?: (payload: unknown) => void; }; -type GatewayClientErrorShape = { - code?: string; - message?: string; - details?: unknown; - retryable?: boolean; - retryAfterMs?: number; -}; - -type SelectedConnectAuth = { - authToken?: string; - authBootstrapToken?: string; - authDeviceToken?: string; - authPassword?: string; - authApprovalRuntimeToken?: string; - signatureToken?: string; - resolvedDeviceToken?: string; - storedToken?: string; - storedScopes?: string[]; - usingStoredDeviceToken?: boolean; -}; - -type StoredDeviceAuth = { - token?: string; - scopes?: string[]; -}; - -type AssembledConnect = { - params: ConnectParams; - authApprovalRuntimeToken: string | undefined; - resolvedDeviceToken: string | undefined; - storedToken: string | undefined; - usingStoredDeviceToken: boolean | undefined; -}; - -type FingerprintCheckingClientOptions = Omit & { - checkServerIdentity?: (servername: string, cert: CertMeta) => Error | undefined; -}; - -const DEFAULT_GATEWAY_CLIENT_URL = "ws://127.0.0.1:18789"; - export type GatewayReconnectPausedInfo = { code: number; reason: string; detailCode: string | null; }; -export class GatewayClientRequestError extends Error { - readonly gatewayCode: string; - readonly details?: unknown; - readonly retryable: boolean; - readonly retryAfterMs?: number; - - constructor(error: GatewayClientErrorShape) { - super(formatConnectErrorMessage({ message: error.message, details: error.details })); - this.name = "GatewayClientRequestError"; - this.gatewayCode = error.code ?? "UNAVAILABLE"; - this.details = error.details; - this.retryable = error.retryable === true; - this.retryAfterMs = error.retryAfterMs; - } -} - -const GATEWAY_CONNECT_ASSEMBLY_ERROR = Symbol("gateway.connectAssemblyError"); - -type GatewayConnectAssemblyError = Error & { - [GATEWAY_CONNECT_ASSEMBLY_ERROR]?: true; +type GatewayClientErrorShape = { + message: string; + code?: string; + details?: unknown; + retryable?: boolean; + retryAfterMs?: number; }; -function markGatewayConnectAssemblyError(error: Error): Error { - Object.defineProperty(error, GATEWAY_CONNECT_ASSEMBLY_ERROR, { - configurable: true, - value: true, - }); - return error; +type GatewayClientInternalAccess = { + opts: GatewayClientOptions; + ws: unknown; + pending: Map; + tickIntervalMs: number; + lastTick: number | null; + startTickWatch: () => void; + handleMessage: (raw: string) => void; +}; + +export const GATEWAY_CLOSE_CODE_HINTS: Readonly> = + BASE_GATEWAY_CLOSE_CODE_HINTS; + +export const GatewayClientRequestError = BaseGatewayClientRequestError as unknown as { + new (error: GatewayClientErrorShape): Error & { + readonly gatewayCode: string; + readonly details?: unknown; + readonly retryable: boolean; + readonly retryAfterMs?: number; + }; +}; + +export type GatewayClientRequestError = InstanceType; + +export function describeGatewayCloseCode(code: number): string | undefined { + return baseDescribeGatewayCloseCode(code); } export function isGatewayConnectAssemblyError(value: unknown): value is Error { - return ( - value instanceof Error && - (value as GatewayConnectAssemblyError)[GATEWAY_CONNECT_ASSEMBLY_ERROR] === true - ); + return baseIsGatewayConnectAssemblyError(value); } export type GatewayClientOptions = { - url?: string; // ws://127.0.0.1:18789 + url?: string; connectChallengeTimeoutMs?: number; /** @deprecated Use connectChallengeTimeoutMs. */ connectDelayMs?: number; - /** - * Server-side pre-auth handshake budget. Config-derived local clients use - * this to keep the connect-challenge watchdog aligned with the gateway. - */ preauthHandshakeTimeoutMs?: number; tickWatchMinIntervalMs?: number; requestTimeoutMs?: number; @@ -189,6 +146,7 @@ export type GatewayClientOptions = { pathEnv?: string; env?: NodeJS.ProcessEnv; deviceIdentity?: DeviceIdentity | null; + hostDeps?: GatewayClientHostDeps; minProtocol?: number; maxProtocol?: number; tlsFingerprint?: string; @@ -200,45 +158,26 @@ export type GatewayClientOptions = { onGap?: (info: { expected: number; received: number }) => void; }; -export const GATEWAY_CLOSE_CODE_HINTS: Readonly> = { - 1000: "normal closure", - 1006: "abnormal closure (no close frame)", - 1008: "policy violation", - 1012: "service restart", - 1013: "try again later", -}; - -export function describeGatewayCloseCode(code: number): string | undefined { - return GATEWAY_CLOSE_CODE_HINTS[code]; -} - -function readConnectChallengeTimeoutOverride( - opts: Pick, -): number | undefined { - if ( - typeof opts.connectChallengeTimeoutMs === "number" && - Number.isFinite(opts.connectChallengeTimeoutMs) - ) { - return opts.connectChallengeTimeoutMs; - } - if (typeof opts.connectDelayMs === "number" && Number.isFinite(opts.connectDelayMs)) { - return opts.connectDelayMs; - } - return undefined; -} - -function isGatewayClientStoppedError(err: unknown): boolean { - const message = err instanceof Error ? err.message : String(err); - return message === "gateway client stopped" || message === "Error: gateway client stopped"; -} - -function formatGatewayClientErrorForLog(err: unknown): string { - const redactedUrlLikeString = String(err) - .replace(/\/\/([^@/?#\s]+)@/g, "//***:***@") - .replace(/([?&])([^=&\s]+)=([^&#\s"'<>)]*)/g, (match, prefix: string, key: string) => - isSensitiveUrlQueryParamName(key) ? `${prefix}${key}=***` : match, - ); - return redactToolPayloadText(redactedUrlLikeString); +function createOpenClawGatewayClientHostDeps( + overrides?: GatewayClientHostDeps, +): GatewayClientHostDeps { + return { + // This wrapper is the only place the package reaches into OpenClaw runtime + // state. Keep device identity, token storage, proxy, and redaction here. + loadOrCreateDeviceIdentity, + signDevicePayload, + publicKeyRawBase64UrlFromPem, + loadDeviceAuthToken, + storeDeviceAuthToken, + clearDeviceAuthToken, + beforeConnect: ensureInheritedManagedProxyRoutingActive, + registerGatewayLoopbackBypass: registerManagedProxyGatewayLoopbackBypass, + normalizeTlsFingerprint: (fingerprint) => normalizeFingerprint(fingerprint ?? ""), + logDebug, + logError, + redactForLog: redactToolPayloadText, + ...overrides, + }; } export function resolveGatewayClientConnectChallengeTimeoutMs( @@ -247,1032 +186,81 @@ export function resolveGatewayClientConnectChallengeTimeoutMs( "connectChallengeTimeoutMs" | "connectDelayMs" | "preauthHandshakeTimeoutMs" >, ): number { - return resolveConnectChallengeTimeoutMs(readConnectChallengeTimeoutOverride(opts), { - configuredTimeoutMs: opts.preauthHandshakeTimeoutMs, - }); + return baseResolveGatewayClientConnectChallengeTimeoutMs(opts); } -const FORCE_STOP_TERMINATE_GRACE_MS = 250; -const STOP_AND_WAIT_TIMEOUT_MS = 1_000; - -type PendingStop = { - ws: WebSocket; - promise: Promise; - resolve: () => void; -}; - export class GatewayClient { - private ws: WebSocket | null = null; - private opts: GatewayClientOptions; - private pending = new Map(); - private backoffMs = 1000; - private closed = false; - private lastSeq: number | null = null; - private connectNonce: string | null = null; - private connectSent = false; - private connectTimer: NodeJS.Timeout | null = null; - private reconnectTimer: NodeJS.Timeout | null = null; - private pendingDeviceTokenRetry = false; - private deviceTokenRetryBudgetUsed = false; - private approvalRuntimeTokenCompatibilityDisabled = false; - private approvalRuntimeTokenRetryBudgetUsed = false; - private pendingStartupReconnectDelayMs: number | null = null; - private pendingConnectErrorDetailCode: string | null = null; - private pendingConnectErrorDetails: unknown = null; - // Track last tick to detect silent stalls. - private lastTick: number | null = null; - private tickIntervalMs = 30_000; - private tickTimer: NodeJS.Timeout | null = null; - private readonly requestTimeoutMs: number; - private pendingStop: PendingStop | null = null; - private socketOpened = false; + #client: BaseGatewayClient; constructor(opts: GatewayClientOptions) { - this.opts = { + this.#client = new BaseGatewayClient({ ...opts, - deviceIdentity: - opts.deviceIdentity === null - ? undefined - : (opts.deviceIdentity ?? loadOrCreateDeviceIdentity()), - }; - this.requestTimeoutMs = - typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs) - ? resolveSafeTimeoutDelayMs(opts.requestTimeoutMs) - : 30_000; - } - - start() { - if (this.closed) { - return; - } - this.clearReconnectTimer(); - this.clearConnectChallengeTimeout(); - this.connectNonce = null; - this.connectSent = false; - const url = this.opts.url ?? DEFAULT_GATEWAY_CLIENT_URL; - if (this.opts.tlsFingerprint && !url.startsWith("wss://")) { - this.notifyConnectError(new Error("gateway tls fingerprint requires wss:// gateway url")); - return; - } - - const allowPrivateWs = process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1"; - // Security check: block ALL plaintext ws:// to non-loopback addresses (CWE-319, CVSS 9.8) - // This protects both credentials AND chat/conversation data from MITM attacks. - // Device tokens may be loaded later in sendConnect(), so we block regardless of hasCredentials. - if (!isSecureWebSocketUrl(url, { allowPrivateWs })) { - // Safe hostname extraction - avoid throwing on malformed URLs in error path - let displayHost = url; - try { - displayHost = new URL(url).hostname || url; - } catch { - // Use raw URL if parsing fails - } - const error = new Error( - `SECURITY ERROR: Cannot connect to "${displayHost}" over plaintext ws://. ` + - "Both credentials and chat data would be exposed to network interception. " + - "Use wss:// for remote URLs. Safe defaults: keep gateway.bind=loopback and connect via SSH tunnel " + - "(ssh -N -L 18789:127.0.0.1:18789 user@gateway-host), or use Tailscale Serve/Funnel. " + - (allowPrivateWs - ? "" - : "Break-glass (trusted private networks only): set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1. ") + - "Run `openclaw doctor --fix` for guidance.", - ); - this.notifyConnectError(error); - return; - } - // Allow node screen snapshots and other large responses. - ensureInheritedManagedProxyRoutingActive(); - const wsOptions: FingerprintCheckingClientOptions = { - maxPayload: 25 * 1024 * 1024, - }; - if (url.startsWith("wss://") && this.opts.tlsFingerprint) { - wsOptions.rejectUnauthorized = false; - wsOptions.checkServerIdentity = (_hostValue: string, cert: CertMeta) => { - const fingerprintValue = - typeof cert === "object" && cert && "fingerprint256" in cert - ? ((cert as { fingerprint256?: string }).fingerprint256 ?? "") - : ""; - const fingerprint = normalizeFingerprint( - typeof fingerprintValue === "string" ? fingerprintValue : "", - ); - const expected = normalizeFingerprint(this.opts.tlsFingerprint ?? ""); - if (!expected) { - return undefined; - } - if (!fingerprint) { - return new Error("Missing server TLS fingerprint"); - } - if (fingerprint !== expected) { - return new Error("Server TLS fingerprint mismatch"); - } - return undefined; - }; - } - let ws: WebSocket; - const unregisterGatewayLoopbackBypass = registerManagedProxyGatewayLoopbackBypass(url); - try { - ws = new WebSocket(url, wsOptions as ClientOptions); - } catch (error) { - this.notifyConnectError(error instanceof Error ? error : new Error(String(error))); - return; - } finally { - unregisterGatewayLoopbackBypass?.(); - } - this.ws = ws; - this.socketOpened = false; - this.connectNonce = null; - this.connectSent = false; - this.clearConnectChallengeTimeout(); - - ws.on("open", () => { - this.socketOpened = true; - if (url.startsWith("wss://") && this.opts.tlsFingerprint) { - const tlsError = this.validateTlsFingerprint(); - if (tlsError) { - this.notifyConnectError(tlsError); - this.ws?.close(1008, tlsError.message); - return; - } - } - this.beginPreauthHandshake(); - }); - ws.on("message", (data) => this.handleMessage(rawDataToString(data))); - ws.on("close", (code, reason) => { - const reasonText = rawDataToString(reason); - const connectErrorDetailCode = this.pendingConnectErrorDetailCode; - const connectErrorDetails = this.pendingConnectErrorDetails; - this.pendingConnectErrorDetailCode = null; - this.pendingConnectErrorDetails = null; - if (this.ws === ws) { - this.ws = null; - } - this.socketOpened = false; - this.resolvePendingStop(ws); - if (this.pendingStartupReconnectDelayMs !== null) { - this.scheduleReconnect(); - return; - } - // Clear persisted device auth state only when device-token auth was active. - // Shared token/password failures can return the same close reason but should - // not erase a valid cached device token. - if ( - code === 1008 && - normalizeLowercaseStringOrEmpty(reasonText).includes("device token mismatch") && - !this.opts.token && - !this.opts.password && - this.opts.deviceIdentity - ) { - const deviceId = this.opts.deviceIdentity.deviceId; - const role = this.opts.role ?? "operator"; - try { - clearDeviceAuthToken({ deviceId, role, env: this.opts.env }); - logDebug(`cleared stale device-auth token for device ${deviceId}`); - } catch (err) { - logDebug( - `failed clearing stale device-auth token for device ${deviceId}: ${String(err)}`, - ); - } - } - this.flushPendingErrors(new Error(`gateway closed (${code}): ${reasonText}`)); - if ( - this.shouldPauseReconnectAfterAuthFailure({ - detailCode: connectErrorDetailCode, - details: connectErrorDetails, - }) - ) { - this.opts.onReconnectPaused?.({ - code, - reason: reasonText, - detailCode: connectErrorDetailCode, - }); - this.opts.onClose?.(code, reasonText); - return; - } - this.scheduleReconnect(); - this.opts.onClose?.(code, reasonText); - }); - ws.on("error", (err) => { - logDebug(`gateway client error: ${formatGatewayClientErrorForLog(err)}`); - if (!this.connectSent) { - this.notifyConnectError(err instanceof Error ? err : new Error(String(err))); - } + clientVersion: opts.clientVersion ?? VERSION, + hostDeps: createOpenClawGatewayClientHostDeps(opts.hostDeps), }); + this.installInternalTestAccessors(); } - stop() { - void this.beginStop(); - } - - async stopAndWait(opts?: { timeoutMs?: number }): Promise { - // Some callers need teardown ordering, not just "close requested". Wait for - // the socket to close or the terminate fallback to fire. - const stopPromise = this.beginStop(); - if (!stopPromise) { - return; - } - const timeoutMs = - typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) - ? Math.max(1, Math.floor(opts.timeoutMs)) - : STOP_AND_WAIT_TIMEOUT_MS; - let timeout: NodeJS.Timeout | null = null; - try { - await Promise.race([ - stopPromise, - new Promise((_, reject) => { - timeout = setTimeout(() => { - reject(new Error(`gateway client stop timed out after ${timeoutMs}ms`)); - }, timeoutMs); - timeout.unref?.(); - }), - ]); - } finally { - if (timeout) { - clearTimeout(timeout); - } - } - } - - private beginStop(): Promise | null { - this.closed = true; - this.pendingDeviceTokenRetry = false; - this.deviceTokenRetryBudgetUsed = false; - this.pendingStartupReconnectDelayMs = null; - this.pendingConnectErrorDetailCode = null; - this.pendingConnectErrorDetails = null; - this.clearReconnectTimer(); - if (this.tickTimer) { - clearInterval(this.tickTimer); - this.tickTimer = null; - } - this.clearConnectChallengeTimeout(); - if (this.pendingStop) { - this.flushPendingErrors(new Error("gateway client stopped")); - return this.pendingStop.promise; - } - const ws = this.ws; - this.ws = null; - if (ws) { - const stopPromise = this.createPendingStop(ws); - ws.close(); - const forceTerminateTimer = setTimeout(() => { - try { - ws.terminate(); - } catch {} - this.resolvePendingStop(ws); - }, FORCE_STOP_TERMINATE_GRACE_MS); - forceTerminateTimer.unref?.(); - this.flushPendingErrors(new Error("gateway client stopped")); - return stopPromise; - } - this.flushPendingErrors(new Error("gateway client stopped")); - return null; - } - - private createPendingStop(ws: WebSocket): Promise { - if (this.pendingStop?.ws === ws) { - return this.pendingStop.promise; - } - let resolve!: () => void; - const promise = new Promise((res) => { - resolve = res; - }); - this.pendingStop = { ws, promise, resolve }; - return promise; - } - - private resolvePendingStop(ws: WebSocket): void { - if (this.pendingStop?.ws !== ws) { - return; - } - const { resolve } = this.pendingStop; - this.pendingStop = null; - resolve(); - } - - private sendConnect() { - if (this.connectSent) { - return; - } - const nonce = normalizeOptionalString(this.connectNonce) ?? ""; - if (!nonce) { - this.notifyConnectError(new Error("gateway connect challenge missing nonce")); - this.ws?.close(1008, "connect challenge missing nonce"); - return; - } - const role = this.opts.role ?? "operator"; - let assembled: AssembledConnect; - try { - assembled = this.assembleConnectParams({ role, nonce }); - } catch (err) { - this.handleConnectFailure(err); - return; - } - - this.connectSent = true; - this.clearConnectChallengeTimeout(); - - void this.request("connect", assembled.params) - .then((helloOk) => { - this.pendingDeviceTokenRetry = false; - this.deviceTokenRetryBudgetUsed = false; - this.pendingStartupReconnectDelayMs = null; - this.pendingConnectErrorDetailCode = null; - this.pendingConnectErrorDetails = null; - const authInfo = helloOk?.auth; - if (authInfo?.deviceToken && this.opts.deviceIdentity) { - storeDeviceAuthToken({ - deviceId: this.opts.deviceIdentity.deviceId, - role: authInfo.role ?? role, - token: authInfo.deviceToken, - scopes: authInfo.scopes ?? [], - env: this.opts.env, - }); - } - this.backoffMs = 1000; - this.tickIntervalMs = - typeof helloOk.policy?.tickIntervalMs === "number" - ? helloOk.policy.tickIntervalMs - : 30_000; - this.lastTick = Date.now(); - this.startTickWatch(); - this.opts.onHelloOk?.(helloOk); - }) - .catch((err) => { - this.pendingConnectErrorDetailCode = - err instanceof GatewayClientRequestError ? readConnectErrorDetailCode(err.details) : null; - this.pendingConnectErrorDetails = - err instanceof GatewayClientRequestError ? err.details : null; - const shouldRetryWithDeviceToken = this.shouldRetryWithStoredDeviceToken({ - error: err, - explicitGatewayToken: normalizeOptionalString(this.opts.token), - resolvedDeviceToken: assembled.resolvedDeviceToken, - storedToken: assembled.storedToken, - }); - if ( - this.opts.deviceIdentity && - assembled.usingStoredDeviceToken && - err instanceof GatewayClientRequestError && - readConnectErrorDetailCode(err.details) === - ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH - ) { - const deviceId = this.opts.deviceIdentity.deviceId; - try { - clearDeviceAuthToken({ deviceId, role, env: this.opts.env }); - logDebug(`cleared stale device-auth token for device ${deviceId}`); - } catch (clearErr) { - logDebug( - `failed clearing stale device-auth token for device ${deviceId}: ${String(clearErr)}`, - ); - } - } - if (shouldRetryWithDeviceToken) { - this.pendingDeviceTokenRetry = true; - this.deviceTokenRetryBudgetUsed = true; - this.backoffMs = Math.min(this.backoffMs, 250); - } - const startupRetryAfterMs = resolveGatewayStartupRetryAfterMs(err); - if (startupRetryAfterMs !== null) { - this.pendingStartupReconnectDelayMs = startupRetryAfterMs; - logDebug(`gateway connect failed: ${formatGatewayClientErrorForLog(err)}`); - this.ws?.close(1013, "gateway starting"); - return; - } - if ( - this.shouldRetryWithoutApprovalRuntimeToken({ - error: err, - authApprovalRuntimeToken: assembled.authApprovalRuntimeToken, - }) - ) { - this.approvalRuntimeTokenCompatibilityDisabled = true; - this.approvalRuntimeTokenRetryBudgetUsed = true; - this.backoffMs = Math.min(this.backoffMs, 250); - logDebug("gateway rejected approval runtime auth field; retrying without it"); - this.ws?.close(1008, "connect retry"); - return; - } - this.notifyConnectError(err instanceof Error ? err : new Error(String(err))); - const msg = `gateway connect failed: ${formatGatewayClientErrorForLog(err)}`; - if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE || isGatewayClientStoppedError(err)) { - logDebug(msg); - } else { - logError(msg); - } - this.ws?.close(1008, "connect failed"); - }); - } - - private assembleConnectParams(params: { role: string; nonce: string }): AssembledConnect { - const { role, nonce } = params; - const selectedAuth = this.selectConnectAuth(role); - const { - authToken, - authBootstrapToken, - authDeviceToken, - authPassword, - authApprovalRuntimeToken, - signatureToken, - resolvedDeviceToken, - storedToken, - storedScopes, - usingStoredDeviceToken, - } = selectedAuth; - - if (this.pendingDeviceTokenRetry && authDeviceToken) { - this.pendingDeviceTokenRetry = false; - } - - const auth = - authToken || - authBootstrapToken || - authPassword || - resolvedDeviceToken || - authApprovalRuntimeToken - ? { - token: authToken, - bootstrapToken: authBootstrapToken, - deviceToken: authDeviceToken ?? resolvedDeviceToken, - password: authPassword, - approvalRuntimeToken: authApprovalRuntimeToken, - } - : undefined; - const signedAtMs = Date.now(); - const scopes = this.resolveConnectScopes({ - usingStoredDeviceToken, - storedScopes, - }); - const platform = this.opts.platform ?? process.platform; - - return { - params: { - minProtocol: this.opts.minProtocol ?? MIN_CLIENT_PROTOCOL_VERSION, - maxProtocol: this.opts.maxProtocol ?? PROTOCOL_VERSION, - client: { - id: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, - displayName: this.opts.clientDisplayName, - version: this.opts.clientVersion ?? VERSION, - platform, - deviceFamily: this.opts.deviceFamily, - mode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND, - instanceId: this.opts.instanceId, - }, - caps: Array.isArray(this.opts.caps) ? this.opts.caps : [], - commands: Array.isArray(this.opts.commands) ? this.opts.commands : undefined, - permissions: - this.opts.permissions && typeof this.opts.permissions === "object" - ? this.opts.permissions - : undefined, - pathEnv: this.opts.pathEnv, - auth, - role, - scopes, - device: this.buildDeviceConnectParams({ - nonce, - role, - scopes, - signatureToken, - signedAtMs, - platform, - }), + private installInternalTestAccessors(): void { + // Existing gateway tests inspect a few internals to drive watchdog and + // frame-handling edge cases. Forward those slots without making the package + // class inherit from this wrapper or leaking package-private types in d.ts. + const target = this as unknown as GatewayClientInternalAccess; + const base = this.#client as unknown as GatewayClientInternalAccess; + Object.defineProperties(target, { + opts: { + configurable: true, + get: () => base.opts, + }, + ws: { + configurable: true, + get: () => base.ws, + set: (value: unknown) => { + base.ws = value; + }, + }, + pending: { + configurable: true, + get: () => base.pending, + }, + tickIntervalMs: { + configurable: true, + get: () => base.tickIntervalMs, + set: (value: number) => { + base.tickIntervalMs = value; + }, + }, + lastTick: { + configurable: true, + get: () => base.lastTick, + set: (value: number | null) => { + base.lastTick = value; + }, }, - authApprovalRuntimeToken, - resolvedDeviceToken, - storedToken, - usingStoredDeviceToken, - }; - } - - private buildDeviceConnectParams(params: { - nonce: string; - role: string; - scopes: string[]; - signatureToken: string | undefined; - signedAtMs: number; - platform: string; - }): ConnectParams["device"] { - if (!this.opts.deviceIdentity) { - return undefined; - } - const { nonce, role, scopes, signatureToken, signedAtMs, platform } = params; - const payload = buildDeviceAuthPayloadV3({ - deviceId: this.opts.deviceIdentity.deviceId, - clientId: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, - clientMode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND, - role, - scopes, - signedAtMs, - token: signatureToken ?? null, - nonce, - platform, - deviceFamily: this.opts.deviceFamily, }); - const signature = signDevicePayload(this.opts.deviceIdentity.privateKeyPem, payload); - return { - id: this.opts.deviceIdentity.deviceId, - publicKey: publicKeyRawBase64UrlFromPem(this.opts.deviceIdentity.publicKeyPem), - signature, - signedAt: signedAtMs, - nonce, - }; + target.startTickWatch = () => base.startTickWatch(); + target.handleMessage = (raw: string) => base.handleMessage(raw); } - private handleConnectFailure(err: unknown) { - const error = err instanceof Error ? err : new Error(String(err)); - this.clearConnectChallengeTimeout(); - this.closed = true; - this.notifyConnectError(markGatewayConnectAssemblyError(error)); - const msg = `gateway connect failed: ${formatGatewayClientErrorForLog(error)}`; - if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE || isGatewayClientStoppedError(error)) { - logDebug(msg); - } else { - logError(msg); - } - this.ws?.close(1008, "connect failed"); + start(): void { + this.#client.start(); } - private notifyConnectError(error: Error) { - try { - this.opts.onConnectError?.(error); - } catch (err) { - logDebug( - `gateway client connect error handler error: ${formatGatewayClientErrorForLog(err)}`, - ); - } + stop(): void { + this.#client.stop(); } - private resolveConnectScopes(params: { - usingStoredDeviceToken?: boolean; - storedScopes?: string[]; - }): string[] { - // Reuse cached scopes only when the client is reusing the cached device token. - // Callers that ask for explicit scopes should keep that request so the - // server can authorize it or drive the normal scope-upgrade flow. - if (Array.isArray(this.opts.scopes)) { - return this.opts.scopes; - } - if ( - params.usingStoredDeviceToken && - Array.isArray(params.storedScopes) && - params.storedScopes.length > 0 - ) { - return params.storedScopes; - } - return this.opts.scopes ?? ["operator.admin"]; + stopAndWait(opts?: { timeoutMs?: number }): Promise { + return this.#client.stopAndWait(opts); } - private loadStoredDeviceAuth(role: string): StoredDeviceAuth | null { - if (!this.opts.deviceIdentity) { - return null; - } - const storedAuth = loadDeviceAuthToken({ - deviceId: this.opts.deviceIdentity.deviceId, - role, - env: this.opts.env, - }); - if (!storedAuth) { - return null; - } - return { - token: storedAuth.token, - scopes: storedAuth.scopes, - }; - } - - private shouldPauseReconnectAfterAuthFailure(params: { - detailCode: string | null; - details?: unknown; - }): boolean { - const { detailCode, details } = params; - if (!detailCode) { - return false; - } - const pairingDetails = readPairingConnectErrorDetails(details); - if ( - detailCode === ConnectErrorDetailCodes.PAIRING_REQUIRED && - (pairingDetails?.pauseReconnect === false || - pairingDetails?.recommendedNextStep === "wait_then_retry") - ) { - return false; - } - if ( - detailCode === ConnectErrorDetailCodes.AUTH_TOKEN_MISSING || - detailCode === ConnectErrorDetailCodes.AUTH_BOOTSTRAP_TOKEN_INVALID || - detailCode === ConnectErrorDetailCodes.AUTH_PASSWORD_MISSING || - detailCode === ConnectErrorDetailCodes.AUTH_PASSWORD_MISMATCH || - detailCode === ConnectErrorDetailCodes.AUTH_RATE_LIMITED || - detailCode === ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH || - detailCode === ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH || - detailCode === ConnectErrorDetailCodes.PAIRING_REQUIRED || - detailCode === ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED || - detailCode === ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED || - detailCode === ConnectErrorDetailCodes.CLIENT_VERSION_MISMATCH - ) { - return true; - } - if (detailCode === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH) { - return !this.pendingDeviceTokenRetry; - } - return false; - } - - private shouldRetryWithStoredDeviceToken(params: { - error: unknown; - explicitGatewayToken?: string; - storedToken?: string; - resolvedDeviceToken?: string; - }): boolean { - if (this.deviceTokenRetryBudgetUsed) { - return false; - } - if (params.resolvedDeviceToken) { - return false; - } - if (!params.explicitGatewayToken || !params.storedToken) { - return false; - } - if (!this.isTrustedDeviceRetryEndpoint()) { - return false; - } - if (!(params.error instanceof GatewayClientRequestError)) { - return false; - } - const detailCode = readConnectErrorDetailCode(params.error.details); - const advice: ConnectErrorRecoveryAdvice = readConnectErrorRecoveryAdvice(params.error.details); - const retryWithDeviceTokenRecommended = - advice.recommendedNextStep === "retry_with_device_token"; - return ( - advice.canRetryWithDeviceToken === true || - retryWithDeviceTokenRecommended || - detailCode === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH - ); - } - - private shouldRetryWithoutApprovalRuntimeToken(params: { - error: unknown; - authApprovalRuntimeToken?: string; - }): boolean { - if (this.approvalRuntimeTokenRetryBudgetUsed) { - return false; - } - if (!params.authApprovalRuntimeToken) { - return false; - } - if (!(params.error instanceof GatewayClientRequestError)) { - return false; - } - if (params.error.gatewayCode !== "INVALID_REQUEST") { - return false; - } - const message = normalizeLowercaseStringOrEmpty(params.error.message); - return message.includes("invalid connect params") && message.includes("approvalruntimetoken"); - } - - private isTrustedDeviceRetryEndpoint(): boolean { - const rawUrl = this.opts.url ?? "ws://127.0.0.1:18789"; - try { - const parsed = new URL(rawUrl); - const protocol = - parsed.protocol === "https:" - ? "wss:" - : parsed.protocol === "http:" - ? "ws:" - : parsed.protocol; - if (isLoopbackHost(parsed.hostname)) { - return true; - } - return protocol === "wss:" && Boolean(this.opts.tlsFingerprint?.trim()); - } catch { - return false; - } - } - - private selectConnectAuth(role: string): SelectedConnectAuth { - const explicitGatewayToken = normalizeOptionalString(this.opts.token); - const explicitBootstrapToken = normalizeOptionalString(this.opts.bootstrapToken); - const explicitDeviceToken = normalizeOptionalString(this.opts.deviceToken); - const authPassword = normalizeOptionalString(this.opts.password); - const authApprovalRuntimeToken = this.approvalRuntimeTokenCompatibilityDisabled - ? undefined - : normalizeOptionalString(this.opts.approvalRuntimeToken); - const storedAuth = this.loadStoredDeviceAuth(role); - const storedToken = storedAuth?.token ?? null; - const storedScopes = storedAuth?.scopes; - const shouldUseDeviceRetryToken = - this.pendingDeviceTokenRetry && - !explicitDeviceToken && - Boolean(explicitGatewayToken) && - Boolean(storedToken) && - this.isTrustedDeviceRetryEndpoint(); - const resolvedDeviceToken = - explicitDeviceToken ?? - (shouldUseDeviceRetryToken || - (!(explicitGatewayToken || authPassword) && (!explicitBootstrapToken || Boolean(storedToken))) - ? (storedToken ?? undefined) - : undefined); - const reusingStoredDeviceToken = - Boolean(resolvedDeviceToken) && - !explicitDeviceToken && - Boolean(storedToken) && - resolvedDeviceToken === storedToken; - // Legacy compatibility: keep `auth.token` populated for device-token auth when - // no explicit shared token is present. - const authToken = explicitGatewayToken ?? resolvedDeviceToken; - const authBootstrapToken = - !explicitGatewayToken && !resolvedDeviceToken && !authPassword - ? explicitBootstrapToken - : undefined; - return { - authToken, - authBootstrapToken, - authDeviceToken: shouldUseDeviceRetryToken ? (storedToken ?? undefined) : undefined, - authPassword, - authApprovalRuntimeToken, - signatureToken: authToken ?? authBootstrapToken ?? undefined, - resolvedDeviceToken, - storedToken: storedToken ?? undefined, - storedScopes, - usingStoredDeviceToken: reusingStoredDeviceToken, - }; - } - - private handleMessage(raw: string) { - let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch (err) { - logDebug(`gateway client parse error: ${formatGatewayClientErrorForLog(err)}`); - return; - } - if (validateEventFrame(parsed)) { - this.lastTick = Date.now(); - const evt = parsed; - if (evt.event === "connect.challenge") { - const payload = evt.payload as { nonce?: unknown } | undefined; - const nonce = payload && typeof payload.nonce === "string" ? payload.nonce : null; - if (!nonce || nonce.trim().length === 0) { - this.notifyConnectError(new Error("gateway connect challenge missing nonce")); - this.ws?.close(1008, "connect challenge missing nonce"); - return; - } - this.connectNonce = nonce.trim(); - if (this.socketOpened) { - this.sendConnect(); - } - return; - } - try { - const seq = typeof evt.seq === "number" ? evt.seq : null; - if (seq !== null) { - if (this.lastSeq !== null && seq > this.lastSeq + 1) { - this.opts.onGap?.({ expected: this.lastSeq + 1, received: seq }); - } - this.lastSeq = seq; - } - if (evt.event === "tick") { - this.lastTick = Date.now(); - } - this.opts.onEvent?.(evt); - } catch (err) { - logDebug(`gateway client event handler error: ${formatGatewayClientErrorForLog(err)}`); - } - return; - } - if (validateResponseFrame(parsed)) { - this.lastTick = Date.now(); - const pending = this.pending.get(parsed.id); - if (!pending) { - return; - } - // If the payload is an ack with status accepted, keep waiting for final. - const payload = parsed.payload as { status?: unknown } | undefined; - const status = payload?.status; - if (pending.expectFinal && status === "accepted") { - if (!pending.acceptedNotified) { - pending.acceptedNotified = true; - try { - pending.onAccepted?.(parsed.payload); - } catch (err) { - logDebug( - `gateway client accepted callback error: ${formatGatewayClientErrorForLog(err)}`, - ); - } - } - return; - } - this.pending.delete(parsed.id); - pending.cleanup?.(); - if (parsed.ok) { - pending.resolve(parsed.payload); - } else { - pending.reject( - new GatewayClientRequestError({ - code: parsed.error?.code, - message: parsed.error?.message ?? "unknown error", - details: parsed.error?.details, - retryable: parsed.error?.retryable, - retryAfterMs: parsed.error?.retryAfterMs, - }), - ); - } - } - } - - private beginPreauthHandshake() { - if (this.connectSent) { - return; - } - if (this.connectNonce && !this.connectSent) { - this.armConnectChallengeTimeout(); - this.sendConnect(); - return; - } - this.armConnectChallengeTimeout(); - } - - private clearConnectChallengeTimeout() { - if (this.connectTimer) { - clearTimeout(this.connectTimer); - this.connectTimer = null; - } - } - - private clearReconnectTimer() { - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - } - - private armConnectChallengeTimeout() { - const connectChallengeTimeoutMs = resolveGatewayClientConnectChallengeTimeoutMs(this.opts); - const armedAt = Date.now(); - this.clearConnectChallengeTimeout(); - this.connectTimer = setTimeout(() => { - if (this.connectSent || this.ws?.readyState !== WebSocket.OPEN) { - return; - } - const elapsedMs = Date.now() - armedAt; - this.notifyConnectError( - new Error( - `gateway connect challenge timeout (waited ${elapsedMs}ms, limit ${connectChallengeTimeoutMs}ms)`, - ), - ); - this.ws?.close(1008, "connect challenge timeout"); - }, connectChallengeTimeoutMs); - } - - private scheduleReconnect() { - if (this.closed) { - return; - } - if (this.tickTimer) { - clearInterval(this.tickTimer); - this.tickTimer = null; - } - this.clearReconnectTimer(); - const startupDelay = this.pendingStartupReconnectDelayMs; - this.pendingStartupReconnectDelayMs = null; - const delay = startupDelay ?? this.backoffMs; - if (startupDelay === null) { - this.backoffMs = Math.min(this.backoffMs * 2, 30_000); - } - this.reconnectTimer = setTimeout(() => { - this.reconnectTimer = null; - this.start(); - }, delay); - } - - private flushPendingErrors(err: Error) { - for (const [, p] of this.pending) { - p.cleanup?.(); - p.reject(err); - } - this.pending.clear(); - } - - private startTickWatch() { - if (this.tickTimer) { - clearInterval(this.tickTimer); - } - const rawMinInterval = this.opts.tickWatchMinIntervalMs; - const minInterval = - typeof rawMinInterval === "number" && Number.isFinite(rawMinInterval) - ? Math.max(1, Math.min(30_000, rawMinInterval)) - : 1000; - const interval = Math.max(this.tickIntervalMs, minInterval); - this.tickTimer = setInterval(() => { - if (this.closed) { - return; - } - if (!this.lastTick) { - return; - } - if (this.pending.size > 0) { - return; - } - const gap = Date.now() - this.lastTick; - if (gap > this.tickIntervalMs * 2) { - this.ws?.close(4000, "tick timeout"); - } - }, interval); - } - - private validateTlsFingerprint(): Error | null { - if (!this.opts.tlsFingerprint || !this.ws) { - return null; - } - const expected = normalizeFingerprint(this.opts.tlsFingerprint); - if (!expected) { - return new Error("gateway tls fingerprint missing"); - } - const socket = ( - this.ws as WebSocket & { - _socket?: { getPeerCertificate?: () => { fingerprint256?: string } }; - } - )["_socket"]; - if (!socket || typeof socket.getPeerCertificate !== "function") { - return new Error("gateway tls fingerprint unavailable"); - } - const cert = socket.getPeerCertificate(); - const fingerprint = normalizeFingerprint(cert?.fingerprint256 ?? ""); - if (!fingerprint) { - return new Error("gateway tls fingerprint unavailable"); - } - if (fingerprint !== expected) { - return new Error("gateway tls fingerprint mismatch"); - } - return null; - } - - async request>( + request>( method: string, params?: unknown, opts?: GatewayClientRequestOptions, ): Promise { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error("gateway not connected"); - } - if (opts?.signal?.aborted) { - throw createGatewayRequestAbortError(method); - } - const id = randomUUID(); - const frame: RequestFrame = { type: "req", id, method, params }; - if (!validateRequestFrame(frame)) { - throw new Error( - `invalid request frame: ${JSON.stringify(validateRequestFrame.errors, null, 2)}`, - ); - } - const expectFinal = opts?.expectFinal === true; - const timeoutMs = - opts?.timeoutMs === null - ? null - : typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) - ? resolveSafeTimeoutDelayMs(opts.timeoutMs) - : expectFinal - ? null - : this.requestTimeoutMs; - const signal = opts?.signal; - const p = new Promise((resolve, reject) => { - let abortHandler: (() => void) | undefined; - const timeout = - timeoutMs === null - ? null - : setTimeout(() => { - const pending = this.pending.get(id); - this.pending.delete(id); - pending?.cleanup?.(); - reject(new Error(`gateway request timeout for ${method}`)); - }, timeoutMs); - const cleanup = () => { - if (timeout) { - clearTimeout(timeout); - } - if (signal && abortHandler) { - signal.removeEventListener("abort", abortHandler); - } - }; - abortHandler = () => { - const pending = this.pending.get(id); - this.pending.delete(id); - pending?.cleanup?.(); - reject(createGatewayRequestAbortError(method)); - }; - this.pending.set(id, { - resolve: (value) => resolve(value as T), - reject, - expectFinal, - timeout, - cleanup, - onAccepted: opts?.onAccepted, - }); - signal?.addEventListener("abort", abortHandler, { once: true }); - }); - this.ws.send(JSON.stringify(frame)); - return p; + return this.#client.request(method, params, opts); } } -function createGatewayRequestAbortError(method: string): Error { - const err = new Error(`gateway request aborted for ${method}`); - err.name = "AbortError"; - return err; -} +export type { DeviceIdentity }; diff --git a/src/gateway/gateway-cli-backend.connect.test.ts b/src/gateway/gateway-cli-backend.connect.test.ts index adb7578569e..a3b15ab0941 100644 --- a/src/gateway/gateway-cli-backend.connect.test.ts +++ b/src/gateway/gateway-cli-backend.connect.test.ts @@ -5,9 +5,9 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { type RawData, WebSocketServer } from "ws"; +import { PROTOCOL_VERSION } from "../../packages/gateway-protocol/src/index.js"; import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js"; import { connectTestGatewayClient } from "./gateway-cli-backend.live-helpers.js"; -import { PROTOCOL_VERSION } from "./protocol/index.js"; const GATEWAY_CONNECT_TIMEOUT_MS = 5_000; const tempRoots: string[] = []; diff --git a/src/gateway/gateway-cli-backend.live-helpers.ts b/src/gateway/gateway-cli-backend.live-helpers.ts index 629cde9a6ba..93ee9df4c82 100644 --- a/src/gateway/gateway-cli-backend.live-helpers.ts +++ b/src/gateway/gateway-cli-backend.live-helpers.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import { listCliRuntimeModelBackendBindings, resolveCliBackendLiveTest, @@ -22,7 +23,6 @@ import { getFreePortBlockWithPermissionFallback } from "../test-utils/ports.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { startGatewayClientWhenEventLoopReady } from "./client-start-readiness.js"; import { GatewayClient, type GatewayClientOptions } from "./client.js"; -import type { EventFrame } from "./protocol/index.js"; // Aggregate docker live runs can contend on startup enough that the gateway // websocket handshake needs a wider budget than the single-provider reruns. diff --git a/src/gateway/gateway-codex-harness.live.test.ts b/src/gateway/gateway-codex-harness.live.test.ts index 4c7d2a23e73..11474ceff09 100644 --- a/src/gateway/gateway-codex-harness.live.test.ts +++ b/src/gateway/gateway-codex-harness.live.test.ts @@ -5,6 +5,7 @@ import os from "node:os"; import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { describe, expect, it } from "vitest"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import { renderBitmapTextPngBase64, renderSolidColorPngBase64, @@ -35,7 +36,6 @@ import { type CronListJob, } from "./live-agent-probes.js"; import { restoreLiveEnv, snapshotLiveEnv, type LiveEnvSnapshot } from "./live-env-test-helpers.js"; -import type { EventFrame } from "./protocol/index.js"; const LIVE = isLiveTestEnabled(); const CODEX_HARNESS_LIVE = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CODEX_HARNESS); diff --git a/src/gateway/gateway-misc.test.ts b/src/gateway/gateway-misc.test.ts index f64ad1f3934..ef0da07f9ae 100644 --- a/src/gateway/gateway-misc.test.ts +++ b/src/gateway/gateway-misc.test.ts @@ -3,6 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import * as os from "node:os"; import * as path from "node:path"; import { beforeAll, beforeEach, describe, expect, it, test, vi } from "vitest"; +import type { RequestFrame } from "../../packages/gateway-protocol/src/index.js"; import { onDiagnosticEvent, resetDiagnosticEventsForTest, @@ -20,7 +21,6 @@ import { resolveNodeCommandAllowlist, } from "./node-command-policy.js"; import type { SerializedEventPayload } from "./node-registry.js"; -import type { RequestFrame } from "./protocol/index.js"; import { createGatewayBroadcaster } from "./server-broadcast.js"; import { createChatRunRegistry } from "./server-chat.js"; import { MAX_BUFFERED_BYTES } from "./server-constants.js"; diff --git a/src/gateway/node-command-policy.test.ts b/src/gateway/node-command-policy.test.ts index e2f1f926328..bce1175e8c3 100644 --- a/src/gateway/node-command-policy.test.ts +++ b/src/gateway/node-command-policy.test.ts @@ -1,4 +1,8 @@ import { afterEach, describe, expect, it } from "vitest"; +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../packages/gateway-protocol/src/client-info.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; @@ -8,7 +12,6 @@ import { normalizeDeclaredNodeCommands, resolveNodeCommandAllowlist, } from "./node-command-policy.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js"; describe("gateway/node-command-policy", () => { afterEach(() => { diff --git a/src/gateway/node-connect-reconcile.test.ts b/src/gateway/node-connect-reconcile.test.ts index d308a646dd6..a6b2e33c592 100644 --- a/src/gateway/node-connect-reconcile.test.ts +++ b/src/gateway/node-connect-reconcile.test.ts @@ -1,8 +1,11 @@ import { describe, expect, it, vi } from "vitest"; +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../packages/gateway-protocol/src/client-info.js"; +import type { ConnectParams } from "../../packages/gateway-protocol/src/index.js"; import type { NodePairingPairedNode, NodePairingRequestInput } from "../infra/node-pairing.js"; import { reconcileNodePairingOnConnect } from "./node-connect-reconcile.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js"; -import type { ConnectParams } from "./protocol/index.js"; function makeNodeConnectParams(overrides?: Partial): ConnectParams { return { diff --git a/src/gateway/node-connect-reconcile.ts b/src/gateway/node-connect-reconcile.ts index 5fb1788ef8f..5d8f8cf9b58 100644 --- a/src/gateway/node-connect-reconcile.ts +++ b/src/gateway/node-connect-reconcile.ts @@ -1,3 +1,4 @@ +import type { ConnectParams } from "../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { NodePairingPairedNode, @@ -10,7 +11,6 @@ import { resolveNodeCommandAllowlist, resolveNodePairingCommandAllowlist, } from "./node-command-policy.js"; -import type { ConnectParams } from "./protocol/index.js"; export type NodeConnectPairingReconcileResult = { nodeId: string; diff --git a/src/gateway/node-invoke-system-run-approval.ts b/src/gateway/node-invoke-system-run-approval.ts index c2c91247192..1d8cbea9c6e 100644 --- a/src/gateway/node-invoke-system-run-approval.ts +++ b/src/gateway/node-invoke-system-run-approval.ts @@ -1,3 +1,7 @@ +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; import { resolveSystemRunApprovalRuntimeContext } from "../infra/system-run-approval-context.js"; import { resolveSystemRunCommandRequest } from "../infra/system-run-command.js"; import { asNullableRecord } from "../shared/record-coerce.js"; @@ -11,7 +15,6 @@ import { evaluateSystemRunApprovalMatch, toSystemRunApprovalMismatchError, } from "./node-invoke-system-run-approval-match.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "./protocol/client-info.js"; type SystemRunParamsLike = { command?: unknown; diff --git a/src/gateway/operator-approvals-client.ts b/src/gateway/operator-approvals-client.ts index 5b5e1c79cdd..da005144e73 100644 --- a/src/gateway/operator-approvals-client.ts +++ b/src/gateway/operator-approvals-client.ts @@ -1,10 +1,13 @@ +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isLoopbackIpAddress } from "../shared/net/ip.js"; import { resolveGatewayClientBootstrap } from "./client-bootstrap.js"; import { startGatewayClientWhenEventLoopReady } from "./client-start-readiness.js"; import { GatewayClient, type GatewayClientOptions } from "./client.js"; import { getOperatorApprovalRuntimeToken } from "./operator-approval-runtime-token.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "./protocol/client-info.js"; function isLoopbackGatewayUrl(rawUrl: string): boolean { try { diff --git a/src/gateway/probe.close-drain.test.ts b/src/gateway/probe.close-drain.test.ts index cc18e572a39..48976aaff32 100644 --- a/src/gateway/probe.close-drain.test.ts +++ b/src/gateway/probe.close-drain.test.ts @@ -4,8 +4,8 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { type RawData, WebSocketServer } from "ws"; +import { PROTOCOL_VERSION } from "../../packages/gateway-protocol/src/index.js"; import { probeGateway } from "./probe.js"; -import { PROTOCOL_VERSION } from "./protocol/index.js"; const tempRoots: string[] = []; diff --git a/src/gateway/probe.ts b/src/gateway/probe.ts index 7a388d19b34..bf91b2a1674 100644 --- a/src/gateway/probe.ts +++ b/src/gateway/probe.ts @@ -1,5 +1,9 @@ import { randomUUID } from "node:crypto"; import path from "node:path"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; import { resolveStateDir } from "../config/paths.js"; import { loadDeviceAuthToken } from "../infra/device-auth-store.js"; import { formatErrorMessage } from "../infra/errors.js"; @@ -8,7 +12,6 @@ import { MAX_SAFE_TIMEOUT_DELAY_MS, resolveSafeTimeoutDelayMs } from "../utils/t import { startGatewayClientWhenEventLoopReady } from "./client-start-readiness.js"; import { GatewayClient, GatewayClientRequestError } from "./client.js"; import { READ_SCOPE } from "./method-scopes.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "./protocol/client-info.js"; export type GatewayProbeAuth = { token?: string; diff --git a/src/gateway/protocol/AGENTS.md b/src/gateway/protocol/AGENTS.md deleted file mode 100644 index e4e9183d6a7..00000000000 --- a/src/gateway/protocol/AGENTS.md +++ /dev/null @@ -1,28 +0,0 @@ -# Gateway Protocol Boundary - -This directory defines the Gateway wire contract for operator clients and -nodes. - -## Public Contracts - -- Docs: - - `docs/gateway/protocol.md` - - `docs/gateway/bridge-protocol.md` - - `docs/concepts/architecture.md` -- Definition files: - - `src/gateway/protocol/schema.ts` - - `src/gateway/protocol/schema/*.ts` - - `src/gateway/protocol/index.ts` - -## Boundary Rules - -- Treat schema changes as protocol changes, not local refactors. -- Prefer additive evolution. If a change is incompatible, handle versioning - explicitly and update all affected clients. -- Keep schema, runtime validators, docs, tests, and generated client artifacts - in sync. -- New Gateway methods, events, or payload fields should land through the typed - protocol definitions here rather than ad hoc JSON shapes elsewhere. -- Keep protocol modules data-first and acyclic. Do not route protocol exports - back through heavier gateway runtime or server-method helpers that make the - contract surface expensive or order-dependent at import time. diff --git a/src/gateway/protocol/CLAUDE.md b/src/gateway/protocol/CLAUDE.md deleted file mode 120000 index 47dc3e3d863..00000000000 --- a/src/gateway/protocol/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/src/gateway/reconnect-gating.test.ts b/src/gateway/reconnect-gating.test.ts index 62fee08a13e..2faecc790d0 100644 --- a/src/gateway/reconnect-gating.test.ts +++ b/src/gateway/reconnect-gating.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { ConnectErrorDetailCodes } from "../../packages/gateway-protocol/src/connect-error-details.js"; import { type GatewayErrorInfo, isNonRecoverableAuthError } from "../../ui/src/ui/gateway.ts"; -import { ConnectErrorDetailCodes } from "./protocol/connect-error-details.js"; function makeError(detailCode: string): GatewayErrorInfo { return { code: "connect_failed", message: "auth failed", details: { code: detailCode } }; diff --git a/src/gateway/server-methods.control-plane-rate-limit.test.ts b/src/gateway/server-methods.control-plane-rate-limit.test.ts index 2d35e971884..8f7dc463415 100644 --- a/src/gateway/server-methods.control-plane-rate-limit.test.ts +++ b/src/gateway/server-methods.control-plane-rate-limit.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { isRetryableGatewayStartupUnavailableError } from "../../packages/gateway-protocol/src/startup-unavailable.js"; import { testing as controlPlaneRateLimitTesting, resolveControlPlaneRateLimitKey, } from "./control-plane-rate-limit.js"; import { STARTUP_UNAVAILABLE_GATEWAY_METHODS } from "./methods/core-descriptors.js"; -import { isRetryableGatewayStartupUnavailableError } from "./protocol/startup-unavailable.js"; import { handleGatewayRequest } from "./server-methods.js"; import type { GatewayRequestHandler } from "./server-methods/types.js"; diff --git a/src/gateway/server-methods.ts b/src/gateway/server-methods.ts index 394ffab99ab..798660e6876 100644 --- a/src/gateway/server-methods.ts +++ b/src/gateway/server-methods.ts @@ -1,3 +1,8 @@ +import { ErrorCodes, errorShape } from "../../packages/gateway-protocol/src/index.js"; +import { + gatewayStartupUnavailableDetails, + GATEWAY_STARTUP_RETRY_AFTER_MS, +} from "../../packages/gateway-protocol/src/startup-unavailable.js"; import { getPluginRegistryState } from "../plugins/runtime-state.js"; import { withPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js"; import { formatControlPlaneActor, resolveControlPlaneActor } from "./control-plane-audit.js"; @@ -11,11 +16,6 @@ import { isCoreGatewayMethodClassified, type GatewayMethodRegistry, } from "./methods/registry.js"; -import { ErrorCodes, errorShape } from "./protocol/index.js"; -import { - gatewayStartupUnavailableDetails, - GATEWAY_STARTUP_RETRY_AFTER_MS, -} from "./protocol/startup-unavailable.js"; import { isRoleAuthorizedForMethod, parseGatewayRole } from "./role-policy.js"; import type { GatewayRequestHandler, diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index b66707d5acf..337ca105c73 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -1,5 +1,18 @@ import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; +import { + GATEWAY_CLIENT_CAPS, + GATEWAY_CLIENT_MODES, + hasGatewayClientCap, +} from "../../../packages/gateway-protocol/src/client-info.js"; +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateAgentIdentityParams, + validateAgentParams, + validateAgentWaitParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { listAgentIds, resolveDefaultAgentId, @@ -119,19 +132,6 @@ import { } from "../chat-attachments.js"; import { resolveAssistantAvatarUrl } from "../control-ui-shared.js"; import { ADMIN_SCOPE } from "../method-scopes.js"; -import { - GATEWAY_CLIENT_CAPS, - GATEWAY_CLIENT_MODES, - hasGatewayClientCap, -} from "../protocol/client-info.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateAgentIdentityParams, - validateAgentParams, - validateAgentWaitParams, -} from "../protocol/index.js"; import { emitGatewaySessionEndPluginHook, emitGatewaySessionStartPluginHook, diff --git a/src/gateway/server-methods/agents.ts b/src/gateway/server-methods/agents.ts index 6404e3eb71d..9b4384374c9 100644 --- a/src/gateway/server-methods/agents.ts +++ b/src/gateway/server-methods/agents.ts @@ -1,5 +1,17 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateAgentsCreateParams, + validateAgentsDeleteParams, + validateAgentsFilesGetParams, + validateAgentsFilesListParams, + validateAgentsFilesSetParams, + validateAgentsListParams, + validateAgentsUpdateParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { findOverlappingWorkspaceAgentIds } from "../../agents/agent-delete-safety.js"; import { listAgentIds, @@ -32,18 +44,6 @@ import { movePathToTrash } from "../../plugin-sdk/browser-maintenance.js"; import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js"; import { normalizeOptionalString as resolveOptionalStringParam } from "../../shared/string-coerce.js"; import { resolveUserPath } from "../../utils.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateAgentsCreateParams, - validateAgentsDeleteParams, - validateAgentsFilesGetParams, - validateAgentsFilesListParams, - validateAgentsFilesSetParams, - validateAgentsListParams, - validateAgentsUpdateParams, -} from "../protocol/index.js"; import { listAgentsForGateway } from "../session-utils.js"; import { AgentConfigPreconditionError, diff --git a/src/gateway/server-methods/approval-shared.test.ts b/src/gateway/server-methods/approval-shared.test.ts index af756984a4a..3c0c320eb3d 100644 --- a/src/gateway/server-methods/approval-shared.test.ts +++ b/src/gateway/server-methods/approval-shared.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { GATEWAY_CLIENT_IDS } from "../../../packages/gateway-protocol/src/client-info.js"; import { ExecApprovalManager } from "../exec-approval-manager.js"; -import { GATEWAY_CLIENT_IDS } from "../protocol/client-info.js"; import { handleApprovalResolve, handleApprovalWaitDecision, diff --git a/src/gateway/server-methods/approval-shared.ts b/src/gateway/server-methods/approval-shared.ts index 57300797df1..af5da9cd5ef 100644 --- a/src/gateway/server-methods/approval-shared.ts +++ b/src/gateway/server-methods/approval-shared.ts @@ -1,3 +1,4 @@ +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { hasApprovalTurnSourceRoute } from "../../infra/approval-turn-source.js"; import type { ExecApprovalDecision } from "../../infra/exec-approvals.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; @@ -7,7 +8,6 @@ import type { ExecApprovalRecord, } from "../exec-approval-manager.js"; import { ADMIN_SCOPE, APPROVALS_SCOPE } from "../method-scopes.js"; -import { ErrorCodes, errorShape } from "../protocol/index.js"; import type { GatewayClient, GatewayRequestContext, RespondFn } from "./types.js"; const APPROVAL_NOT_FOUND_DETAILS = { diff --git a/src/gateway/server-methods/artifacts.ts b/src/gateway/server-methods/artifacts.ts index 0cf4f8cb066..ecb739b5c7c 100644 --- a/src/gateway/server-methods/artifacts.ts +++ b/src/gateway/server-methods/artifacts.ts @@ -1,4 +1,13 @@ import { createHash } from "node:crypto"; +import { + ErrorCodes, + errorShape, + type ArtifactSummary, + type ArtifactsGetParams, + validateArtifactsDownloadParams, + validateArtifactsGetParams, + validateArtifactsListParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { @@ -10,15 +19,6 @@ import { import { asOptionalRecord } from "../../shared/record-coerce.js"; import { normalizeOptionalString as asNonEmptyString } from "../../shared/string-coerce.js"; import { getTaskSessionLookupByIdForStatus } from "../../tasks/task-status-access.js"; -import { - ErrorCodes, - errorShape, - type ArtifactSummary, - type ArtifactsGetParams, - validateArtifactsDownloadParams, - validateArtifactsGetParams, - validateArtifactsListParams, -} from "../protocol/index.js"; import { resolveSessionKeyForRun } from "../server-session-key.js"; import { resolveSessionStoreAgentId, diff --git a/src/gateway/server-methods/channels.ts b/src/gateway/server-methods/channels.ts index 6dfdafe9461..608411f4f0d 100644 --- a/src/gateway/server-methods/channels.ts +++ b/src/gateway/server-methods/channels.ts @@ -1,3 +1,12 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateChannelsStartParams, + validateChannelsStopParams, + validateChannelsLogoutParams, + validateChannelsStatusParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { buildChannelUiCatalog } from "../../channels/plugins/catalog.js"; import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js"; import { @@ -21,15 +30,6 @@ import { DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS, evaluateChannelHealth, } from "../channel-health-policy.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateChannelsStartParams, - validateChannelsStopParams, - validateChannelsLogoutParams, - validateChannelsStatusParams, -} from "../protocol/index.js"; import { resolveGatewayPluginConfig } from "../runtime-plugin-config.js"; import type { ChannelRuntimeSnapshot } from "../server-channel-runtime.types.js"; import { formatForLog } from "../ws-log.js"; diff --git a/src/gateway/server-methods/chat.directive-tags.test.ts b/src/gateway/server-methods/chat.directive-tags.test.ts index a37d03a88f4..f92ad52b5ad 100644 --- a/src/gateway/server-methods/chat.directive-tags.test.ts +++ b/src/gateway/server-methods/chat.directive-tags.test.ts @@ -3,18 +3,18 @@ import os from "node:os"; import path from "node:path"; import { CURRENT_SESSION_VERSION } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { + GATEWAY_CLIENT_CAPS, + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../../packages/gateway-protocol/src/client-info.js"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; +import { CHAT_SEND_SESSION_KEY_MAX_LENGTH } from "../../../packages/gateway-protocol/src/schema.js"; import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js"; import { setReplyPayloadMetadata } from "../../auto-reply/reply-payload.js"; import type { MsgContext } from "../../auto-reply/templating.js"; import { appendSessionTranscriptMessage } from "../../config/sessions/transcript-append.js"; import { resolveMirroredTranscriptText } from "../../config/sessions/transcript-mirror.js"; -import { - GATEWAY_CLIENT_CAPS, - GATEWAY_CLIENT_MODES, - GATEWAY_CLIENT_NAMES, -} from "../protocol/client-info.js"; -import { ErrorCodes } from "../protocol/index.js"; -import { CHAT_SEND_SESSION_KEY_MAX_LENGTH } from "../protocol/schema/primitives.js"; import { readSessionTranscriptIndex } from "../session-transcript-index.fs.js"; import type { GatewayRequestContext } from "./types.js"; diff --git a/src/gateway/server-methods/chat.send-deleted-agent.test.ts b/src/gateway/server-methods/chat.send-deleted-agent.test.ts index 6e909562a7d..c807e20403a 100644 --- a/src/gateway/server-methods/chat.send-deleted-agent.test.ts +++ b/src/gateway/server-methods/chat.send-deleted-agent.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ErrorCodes } from "../protocol/index.js"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import { chatHandlers } from "./chat.js"; import { mockDeletedAgentSession, diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index fb7334c6968..cfbdb67009d 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -7,6 +7,22 @@ import { isReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts, } from "openclaw/plugin-sdk/reply-payload"; +import { + GATEWAY_CLIENT_CAPS, + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, + hasGatewayClientCap, +} from "../../../packages/gateway-protocol/src/client-info.js"; +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateChatAbortParams, + validateChatHistoryParams, + validateChatInjectParams, + validateChatSendParams, +} from "../../../packages/gateway-protocol/src/index.js"; +import { CHAT_SEND_SESSION_KEY_MAX_LENGTH } from "../../../packages/gateway-protocol/src/schema.js"; import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "../../agents/agent-scope.js"; import { rewriteTranscriptEntriesInSessionFile } from "../../agents/embedded-agent-runner/transcript-rewrite.js"; import { runAgentHarnessBeforeMessageWriteHook } from "../../agents/harness/hook-helpers.js"; @@ -105,22 +121,6 @@ import { createManagedOutgoingImageBlocks, } from "../managed-image-attachments.js"; import { ADMIN_SCOPE } from "../method-scopes.js"; -import { - GATEWAY_CLIENT_CAPS, - GATEWAY_CLIENT_MODES, - GATEWAY_CLIENT_NAMES, - hasGatewayClientCap, -} from "../protocol/client-info.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateChatAbortParams, - validateChatHistoryParams, - validateChatInjectParams, - validateChatSendParams, -} from "../protocol/index.js"; -import { CHAT_SEND_SESSION_KEY_MAX_LENGTH } from "../protocol/schema/primitives.js"; import { getMaxChatHistoryMessagesBytes } from "../server-constants.js"; import { readSessionTranscriptIndex } from "../session-transcript-index.fs.js"; import { diff --git a/src/gateway/server-methods/commands.test.ts b/src/gateway/server-methods/commands.test.ts index 0f98295a01b..0b0243d0bb5 100644 --- a/src/gateway/server-methods/commands.test.ts +++ b/src/gateway/server-methods/commands.test.ts @@ -152,7 +152,7 @@ vi.mock("../../channels/plugins/index.js", () => ({ }), })); -import { ErrorCodes, errorShape } from "../protocol/index.js"; +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { COMMAND_ALIAS_MAX_ITEMS, COMMAND_ARG_CHOICES_MAX_ITEMS, @@ -160,7 +160,7 @@ import { COMMAND_DESCRIPTION_MAX_LENGTH, COMMAND_LIST_MAX_ITEMS, COMMAND_NAME_MAX_LENGTH, -} from "../protocol/schema/commands.js"; +} from "../../../packages/gateway-protocol/src/schema.js"; import { commandsHandlers, buildCommandsListResult } from "./commands.js"; function callHandler(params: Record = {}) { diff --git a/src/gateway/server-methods/commands.ts b/src/gateway/server-methods/commands.ts index a34d533b564..21c6017e594 100644 --- a/src/gateway/server-methods/commands.ts +++ b/src/gateway/server-methods/commands.ts @@ -1,3 +1,25 @@ +import type { + CommandEntry, + CommandsListResult, +} from "../../../packages/gateway-protocol/src/index.js"; +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateCommandsListParams, +} from "../../../packages/gateway-protocol/src/index.js"; +import { + COMMAND_ALIAS_MAX_ITEMS, + COMMAND_ARG_CHOICES_MAX_ITEMS, + COMMAND_ARG_DESCRIPTION_MAX_LENGTH, + COMMAND_ARG_NAME_MAX_LENGTH, + COMMAND_ARGS_MAX_ITEMS, + COMMAND_CHOICE_LABEL_MAX_LENGTH, + COMMAND_CHOICE_VALUE_MAX_LENGTH, + COMMAND_DESCRIPTION_MAX_LENGTH, + COMMAND_LIST_MAX_ITEMS, + COMMAND_NAME_MAX_LENGTH, +} from "../../../packages/gateway-protocol/src/schema.js"; import { listAgentIds, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { listChatCommandsForConfig } from "../../auto-reply/commands-registry.js"; import type { @@ -11,25 +33,6 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { getPluginCommandSpecs } from "../../plugins/command-specs.js"; import { listPluginCommands } from "../../plugins/commands.js"; import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; -import type { CommandEntry, CommandsListResult } from "../protocol/index.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateCommandsListParams, -} from "../protocol/index.js"; -import { - COMMAND_ALIAS_MAX_ITEMS, - COMMAND_ARG_CHOICES_MAX_ITEMS, - COMMAND_ARG_DESCRIPTION_MAX_LENGTH, - COMMAND_ARG_NAME_MAX_LENGTH, - COMMAND_ARGS_MAX_ITEMS, - COMMAND_CHOICE_LABEL_MAX_LENGTH, - COMMAND_CHOICE_VALUE_MAX_LENGTH, - COMMAND_DESCRIPTION_MAX_LENGTH, - COMMAND_LIST_MAX_ITEMS, - COMMAND_NAME_MAX_LENGTH, -} from "../protocol/schema/commands.js"; import type { GatewayRequestHandlers, RespondFn } from "./types.js"; type SerializedArg = NonNullable[number]; diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index 194f58f1f31..089a67b8b10 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -1,4 +1,16 @@ import { execFile } from "node:child_process"; +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateConfigApplyParams, + validateConfigGetParams, + validateConfigPatchParams, + validateConfigSchemaLookupParams, + validateConfigSchemaLookupResult, + validateConfigSchemaParams, + validateConfigSetParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { createConfigIO, parseConfigJson5, @@ -34,18 +46,6 @@ import { resolveControlPlaneActor, summarizeChangedPaths, } from "../control-plane-audit.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateConfigApplyParams, - validateConfigGetParams, - validateConfigPatchParams, - validateConfigSchemaLookupParams, - validateConfigSchemaLookupResult, - validateConfigSchemaParams, - validateConfigSetParams, -} from "../protocol/index.js"; import { resolveBaseHashParam } from "./base-hash.js"; import { commitGatewayConfigWrite, diff --git a/src/gateway/server-methods/connect.ts b/src/gateway/server-methods/connect.ts index 309693782a3..3a107452f3c 100644 --- a/src/gateway/server-methods/connect.ts +++ b/src/gateway/server-methods/connect.ts @@ -1,4 +1,4 @@ -import { ErrorCodes, errorShape } from "../protocol/index.js"; +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import type { GatewayRequestHandlers } from "./types.js"; export const connectHandlers: GatewayRequestHandlers = { diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index fca9f25b210..01356e22f83 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -1,3 +1,17 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateCronAddParams, + validateCronGetParams, + validateCronListParams, + validateCronRemoveParams, + validateCronRunParams, + validateCronRunsParams, + validateCronStatusParams, + validateCronUpdateParams, + validateWakeParams, +} from "../../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { resolveCronDeliveryPreviews } from "../../cron/delivery-preview.js"; import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js"; @@ -18,20 +32,6 @@ import { import { listConfiguredAnnounceChannelIdsForConfig } from "../../plugins/channel-plugin-ids.js"; import { isSubagentSessionKey } from "../../routing/session-key.js"; import { normalizeMessageChannel } from "../../utils/message-channel.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateCronAddParams, - validateCronGetParams, - validateCronListParams, - validateCronRemoveParams, - validateCronRunParams, - validateCronRunsParams, - validateCronStatusParams, - validateCronUpdateParams, - validateWakeParams, -} from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; function listConfiguredAnnounceChannelIds(cfg: OpenClawConfig): string[] { diff --git a/src/gateway/server-methods/devices.ts b/src/gateway/server-methods/devices.ts index 723f8329374..41f9c7b174d 100644 --- a/src/gateway/server-methods/devices.ts +++ b/src/gateway/server-methods/devices.ts @@ -1,3 +1,14 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateDevicePairApproveParams, + validateDevicePairListParams, + validateDevicePairRemoveParams, + validateDevicePairRejectParams, + validateDeviceTokenRevokeParams, + validateDeviceTokenRotateParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { approveDevicePairing, formatDevicePairingForbiddenMessage, @@ -13,17 +24,6 @@ import { rotateDeviceToken, summarizeDeviceTokens, } from "../../infra/device-pairing.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateDevicePairApproveParams, - validateDevicePairListParams, - validateDevicePairRemoveParams, - validateDevicePairRejectParams, - validateDeviceTokenRevokeParams, - validateDeviceTokenRotateParams, -} from "../protocol/index.js"; import type { GatewayClient, GatewayRequestHandlers } from "./types.js"; const DEVICE_TOKEN_ROTATION_DENIED_MESSAGE = "device token rotation denied"; diff --git a/src/gateway/server-methods/diagnostics.ts b/src/gateway/server-methods/diagnostics.ts index a0a3c36f505..50404099bce 100644 --- a/src/gateway/server-methods/diagnostics.ts +++ b/src/gateway/server-methods/diagnostics.ts @@ -1,8 +1,8 @@ +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { getDiagnosticStabilitySnapshot, normalizeDiagnosticStabilityQuery, } from "../../logging/diagnostic-stability.js"; -import { ErrorCodes, errorShape } from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; export const diagnosticsHandlers: GatewayRequestHandlers = { diff --git a/src/gateway/server-methods/environments.test.ts b/src/gateway/server-methods/environments.test.ts index f66c93f91b4..ccd5993b47d 100644 --- a/src/gateway/server-methods/environments.test.ts +++ b/src/gateway/server-methods/environments.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import { listDevicePairing } from "../../infra/device-pairing.js"; import { listNodePairing } from "../../infra/node-pairing.js"; -import { ErrorCodes } from "../protocol/index.js"; import { environmentsHandlers } from "./environments.js"; vi.mock("../../infra/device-pairing.js", () => ({ diff --git a/src/gateway/server-methods/environments.ts b/src/gateway/server-methods/environments.ts index cc7f9771115..78fde85becb 100644 --- a/src/gateway/server-methods/environments.ts +++ b/src/gateway/server-methods/environments.ts @@ -1,15 +1,15 @@ -import { listDevicePairing } from "../../infra/device-pairing.js"; -import { listNodePairing } from "../../infra/node-pairing.js"; -import type { NodeListNode } from "../../shared/node-list-types.js"; -import { normalizeSortedUniqueTrimmedStringList } from "../../shared/string-normalization.js"; -import { createKnownNodeCatalog, listKnownNodes } from "../node-catalog.js"; import { type EnvironmentSummary, ErrorCodes, errorShape, validateEnvironmentsListParams, validateEnvironmentsStatusParams, -} from "../protocol/index.js"; +} from "../../../packages/gateway-protocol/src/index.js"; +import { listDevicePairing } from "../../infra/device-pairing.js"; +import { listNodePairing } from "../../infra/node-pairing.js"; +import type { NodeListNode } from "../../shared/node-list-types.js"; +import { normalizeSortedUniqueTrimmedStringList } from "../../shared/string-normalization.js"; +import { createKnownNodeCatalog, listKnownNodes } from "../node-catalog.js"; import { respondInvalidParams, respondUnavailableOnThrow } from "./nodes.helpers.js"; import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; diff --git a/src/gateway/server-methods/exec-approval.ts b/src/gateway/server-methods/exec-approval.ts index dc483550fcd..5b0de81a9aa 100644 --- a/src/gateway/server-methods/exec-approval.ts +++ b/src/gateway/server-methods/exec-approval.ts @@ -1,3 +1,12 @@ +import { GATEWAY_CLIENT_IDS } from "../../../packages/gateway-protocol/src/client-info.js"; +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateExecApprovalGetParams, + validateExecApprovalRequestParams, + validateExecApprovalResolveParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { resolveExecCommandHighlighting } from "../../config/exec-command-highlighting.js"; import { resolveCommandAnalysisSummaryForDisplay } from "../../infra/command-analysis/explain.js"; import { @@ -22,15 +31,6 @@ import { import { resolveSystemRunApprovalRequestContext } from "../../infra/system-run-approval-context.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import type { ExecApprovalManager } from "../exec-approval-manager.js"; -import { GATEWAY_CLIENT_IDS } from "../protocol/client-info.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateExecApprovalGetParams, - validateExecApprovalRequestParams, - validateExecApprovalResolveParams, -} from "../protocol/index.js"; import { handleApprovalWaitDecision, handlePendingApprovalRequest, diff --git a/src/gateway/server-methods/exec-approvals.ts b/src/gateway/server-methods/exec-approvals.ts index 0fbed7b4be4..3dfaf5f1592 100644 --- a/src/gateway/server-methods/exec-approvals.ts +++ b/src/gateway/server-methods/exec-approvals.ts @@ -1,3 +1,11 @@ +import { + ErrorCodes, + errorShape, + validateExecApprovalsGetParams, + validateExecApprovalsNodeGetParams, + validateExecApprovalsNodeSetParams, + validateExecApprovalsSetParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { ensureExecApprovals, mergeExecApprovalsSocketDefaults, @@ -7,14 +15,6 @@ import { type ExecApprovalsFile, type ExecApprovalsSnapshot, } from "../../infra/exec-approvals.js"; -import { - ErrorCodes, - errorShape, - validateExecApprovalsGetParams, - validateExecApprovalsNodeGetParams, - validateExecApprovalsNodeSetParams, - validateExecApprovalsSetParams, -} from "../protocol/index.js"; import { resolveBaseHashParam } from "./base-hash.js"; import { respondUnavailableOnNodeInvokeError, diff --git a/src/gateway/server-methods/health.ts b/src/gateway/server-methods/health.ts index 77d7a7449ab..8064a7c61c4 100644 --- a/src/gateway/server-methods/health.ts +++ b/src/gateway/server-methods/health.ts @@ -1,8 +1,8 @@ +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import type { ChannelAccountSnapshot } from "../../channels/plugins/types.public.js"; import type { ChannelHealthSummary, HealthSummary } from "../../commands/health.types.js"; import { getStatusSummary } from "../../commands/status.js"; import { getGatewayModelPricingHealth } from "../model-pricing-cache-state.js"; -import { ErrorCodes, errorShape } from "../protocol/index.js"; import type { ChannelRuntimeSnapshot } from "../server-channel-runtime.types.js"; import { HEALTH_REFRESH_INTERVAL_MS } from "../server-constants.js"; import { formatError } from "../server-utils.js"; diff --git a/src/gateway/server-methods/logs.ts b/src/gateway/server-methods/logs.ts index e3e575e6aa7..3eed2380653 100644 --- a/src/gateway/server-methods/logs.ts +++ b/src/gateway/server-methods/logs.ts @@ -1,10 +1,10 @@ -import { readConfiguredLogTail } from "../../logging/log-tail.js"; import { ErrorCodes, errorShape, formatValidationErrors, validateLogsTailParams, -} from "../protocol/index.js"; +} from "../../../packages/gateway-protocol/src/index.js"; +import { readConfiguredLogTail } from "../../logging/log-tail.js"; import type { GatewayRequestHandlers } from "./types.js"; export const logsHandlers: GatewayRequestHandlers = { diff --git a/src/gateway/server-methods/models-auth-status.ts b/src/gateway/server-methods/models-auth-status.ts index dd84e5dd9be..b094f5d3898 100644 --- a/src/gateway/server-methods/models-auth-status.ts +++ b/src/gateway/server-methods/models-auth-status.ts @@ -1,3 +1,4 @@ +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { resolveDefaultAgentDir } from "../../agents/agent-scope.js"; import { type AuthHealthSummary, @@ -29,7 +30,6 @@ import type { UsageProviderId, UsageWindow } from "../../infra/provider-usage.ty import { createSubsystemLogger } from "../../logging/subsystem.js"; import { refreshActiveSecretsRuntimeSnapshot } from "../../secrets/runtime.js"; import { abortChatRunsForProvider, type ChatAbortOps } from "../chat-abort.js"; -import { ErrorCodes, errorShape } from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; diff --git a/src/gateway/server-methods/models.test.ts b/src/gateway/server-methods/models.test.ts index 762520a83dc..cd17c865ed5 100644 --- a/src/gateway/server-methods/models.test.ts +++ b/src/gateway/server-methods/models.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { ErrorCodes } from "../protocol/index.js"; import { modelsHandlers } from "./models.js"; type Deferred = { diff --git a/src/gateway/server-methods/models.ts b/src/gateway/server-methods/models.ts index 2412f272c00..9be914f9cf7 100644 --- a/src/gateway/server-methods/models.ts +++ b/src/gateway/server-methods/models.ts @@ -1,3 +1,9 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateModelsListParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { DEFAULT_PROVIDER } from "../../agents/defaults.js"; import { @@ -7,12 +13,6 @@ import { import { resolveVisibleModelCatalog } from "../../agents/model-catalog-visibility.js"; import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js"; import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateModelsListParams, -} from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; type ModelsListView = ModelCatalogBrowseView; diff --git a/src/gateway/server-methods/native-hook-relay.ts b/src/gateway/server-methods/native-hook-relay.ts index 3debcaf225c..1a7a9ec0fb7 100644 --- a/src/gateway/server-methods/native-hook-relay.ts +++ b/src/gateway/server-methods/native-hook-relay.ts @@ -1,8 +1,8 @@ +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { invokeNativeHookRelay, type NativeHookRelayProcessResponse, } from "../../agents/harness/native-hook-relay.js"; -import { ErrorCodes, errorShape } from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; export const nativeHookRelayHandlers: GatewayRequestHandlers = { diff --git a/src/gateway/server-methods/nodes-pending.ts b/src/gateway/server-methods/nodes-pending.ts index 28bd26ce57b..91e5fbe2fce 100644 --- a/src/gateway/server-methods/nodes-pending.ts +++ b/src/gateway/server-methods/nodes-pending.ts @@ -1,15 +1,15 @@ +import { + ErrorCodes, + errorShape, + validateNodePendingDrainParams, + validateNodePendingEnqueueParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { drainNodePendingWork, enqueueNodePendingWork, type NodePendingWorkPriority, type NodePendingWorkType, } from "../node-pending-work.js"; -import { - ErrorCodes, - errorShape, - validateNodePendingDrainParams, - validateNodePendingEnqueueParams, -} from "../protocol/index.js"; import { respondInvalidParams, respondUnavailableOnThrow } from "./nodes.helpers.js"; import { maybeSendNodeWakeNudge, diff --git a/src/gateway/server-methods/nodes.handlers.invoke-result.ts b/src/gateway/server-methods/nodes.handlers.invoke-result.ts index 5bcb9aa4c8a..f92553502b5 100644 --- a/src/gateway/server-methods/nodes.handlers.invoke-result.ts +++ b/src/gateway/server-methods/nodes.handlers.invoke-result.ts @@ -1,4 +1,8 @@ -import { ErrorCodes, errorShape, validateNodeInvokeResultParams } from "../protocol/index.js"; +import { + ErrorCodes, + errorShape, + validateNodeInvokeResultParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { respondInvalidParams } from "./nodes.helpers.js"; import type { GatewayRequestHandler } from "./types.js"; diff --git a/src/gateway/server-methods/nodes.helpers.ts b/src/gateway/server-methods/nodes.helpers.ts index b9bf9436081..39c0a9c5d37 100644 --- a/src/gateway/server-methods/nodes.helpers.ts +++ b/src/gateway/server-methods/nodes.helpers.ts @@ -1,6 +1,10 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, +} from "../../../packages/gateway-protocol/src/index.js"; +import type { ValidationError } from "../../../packages/gateway-protocol/src/index.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; -import { ErrorCodes, errorShape, formatValidationErrors } from "../protocol/index.js"; -import type { ValidationError } from "../protocol/index.js"; export { safeParseJson } from "../server-json.js"; import { formatForLog } from "../ws-log.js"; import type { RespondFn } from "./types.js"; diff --git a/src/gateway/server-methods/nodes.invoke-wake.test.ts b/src/gateway/server-methods/nodes.invoke-wake.test.ts index 4caa85b8224..f6cbf9d0ee0 100644 --- a/src/gateway/server-methods/nodes.invoke-wake.test.ts +++ b/src/gateway/server-methods/nodes.invoke-wake.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ErrorCodes } from "../protocol/index.js"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import { clearNodeWakeState, maybeSendNodeWakeNudge, diff --git a/src/gateway/server-methods/nodes.ts b/src/gateway/server-methods/nodes.ts index 4bc7686dc0c..27bded09707 100644 --- a/src/gateway/server-methods/nodes.ts +++ b/src/gateway/server-methods/nodes.ts @@ -1,4 +1,21 @@ import { randomUUID } from "node:crypto"; +import { + type ConnectParams, + ErrorCodes, + errorShape, + validateNodeDescribeParams, + validateNodeEventParams, + validateNodeInvokeParams, + validateNodeListParams, + validateNodePendingAckParams, + validateNodePairApproveParams, + validateNodePairListParams, + validateNodePairRejectParams, + validateNodePairRemoveParams, + validateNodePairRequestParams, + validateNodePairVerifyParams, + validateNodeRenameParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { getRuntimeConfig } from "../../config/io.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { listDevicePairing } from "../../infra/device-pairing.js"; @@ -42,23 +59,6 @@ import { applyPluginNodeInvokePolicy } from "../node-invoke-plugin-policy.js"; import { sanitizeNodeInvokeParamsForForwarding } from "../node-invoke-sanitize.js"; import type { NodeSession } from "../node-registry.js"; import { refreshClientPluginNodeCapability } from "../plugin-node-capability.js"; -import { - type ConnectParams, - ErrorCodes, - errorShape, - validateNodeDescribeParams, - validateNodeEventParams, - validateNodeInvokeParams, - validateNodeListParams, - validateNodePendingAckParams, - validateNodePairApproveParams, - validateNodePairListParams, - validateNodePairRejectParams, - validateNodePairRemoveParams, - validateNodePairRequestParams, - validateNodePairVerifyParams, - validateNodeRenameParams, -} from "../protocol/index.js"; import type { NodeEventContext } from "../server-node-events-types.js"; import { NODE_WAKE_RECONNECT_POLL_MS, diff --git a/src/gateway/server-methods/plugin-approval.ts b/src/gateway/server-methods/plugin-approval.ts index 13c12b5a241..34c47d16ee5 100644 --- a/src/gateway/server-methods/plugin-approval.ts +++ b/src/gateway/server-methods/plugin-approval.ts @@ -1,4 +1,11 @@ import { randomUUID } from "node:crypto"; +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validatePluginApprovalRequestParams, + validatePluginApprovalResolveParams, +} from "../../../packages/gateway-protocol/src/index.js"; import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js"; import type { ExecApprovalDecision } from "../../infra/exec-approvals.js"; import type { PluginApprovalRequestPayload } from "../../infra/plugin-approvals.js"; @@ -9,13 +16,6 @@ import { } from "../../infra/plugin-approvals.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import type { ExecApprovalManager } from "../exec-approval-manager.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validatePluginApprovalRequestParams, - validatePluginApprovalResolveParams, -} from "../protocol/index.js"; import { handleApprovalResolve, handleApprovalWaitDecision, diff --git a/src/gateway/server-methods/plugin-host-hooks.ts b/src/gateway/server-methods/plugin-host-hooks.ts index 38e8d4deb0e..8b85e973273 100644 --- a/src/gateway/server-methods/plugin-host-hooks.ts +++ b/src/gateway/server-methods/plugin-host-hooks.ts @@ -1,3 +1,11 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validatePluginsSessionActionParams, + validatePluginsSessionActionResult, + validatePluginsUiDescriptorsParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { isPluginJsonValue } from "../../plugins/host-hooks.js"; @@ -10,14 +18,6 @@ import { import { isRecord } from "../../shared/record-coerce.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { ADMIN_SCOPE, READ_SCOPE, WRITE_SCOPE } from "../operator-scopes.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validatePluginsSessionActionParams, - validatePluginsSessionActionResult, - validatePluginsUiDescriptorsParams, -} from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; const log = createSubsystemLogger("gateway/plugin-host-hooks"); diff --git a/src/gateway/server-methods/push.test.ts b/src/gateway/server-methods/push.test.ts index d39c7380eda..6ffaa8ad94e 100644 --- a/src/gateway/server-methods/push.test.ts +++ b/src/gateway/server-methods/push.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ErrorCodes } from "../protocol/index.js"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import { pushHandlers } from "./push.js"; const mocks = vi.hoisted(() => ({ diff --git a/src/gateway/server-methods/push.ts b/src/gateway/server-methods/push.ts index 5455d934e32..a61ad466d46 100644 --- a/src/gateway/server-methods/push.ts +++ b/src/gateway/server-methods/push.ts @@ -1,3 +1,12 @@ +import { + ErrorCodes, + errorShape, + validatePushTestParams, + validateWebPushSubscribeParams, + validateWebPushTestParams, + validateWebPushUnsubscribeParams, + validateWebPushVapidPublicKeyParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { clearApnsRegistrationIfCurrent, loadApnsRegistration, @@ -14,15 +23,6 @@ import { resolveVapidKeys, } from "../../infra/push-web.js"; import { normalizeStringifiedOptionalString } from "../../shared/string-coerce.js"; -import { - ErrorCodes, - errorShape, - validatePushTestParams, - validateWebPushSubscribeParams, - validateWebPushTestParams, - validateWebPushUnsubscribeParams, - validateWebPushVapidPublicKeyParams, -} from "../protocol/index.js"; import { respondInvalidParams, respondUnavailableOnThrow } from "./nodes.helpers.js"; import { normalizeTrimmedString } from "./record-shared.js"; import type { GatewayRequestHandlers } from "./types.js"; diff --git a/src/gateway/server-methods/secrets.ts b/src/gateway/server-methods/secrets.ts index 721d4e05d6e..7d9c9874350 100644 --- a/src/gateway/server-methods/secrets.ts +++ b/src/gateway/server-methods/secrets.ts @@ -1,11 +1,11 @@ -import { isKnownSecretTargetId } from "../../secrets/target-registry.js"; import { ErrorCodes, errorShape, type ValidationError, validateSecretsResolveParams, validateSecretsResolveResult, -} from "../protocol/index.js"; +} from "../../../packages/gateway-protocol/src/index.js"; +import { isKnownSecretTargetId } from "../../secrets/target-registry.js"; import type { GatewayRequestHandlers } from "./types.js"; function errorMessage(error: unknown): string { diff --git a/src/gateway/server-methods/send.ts b/src/gateway/server-methods/send.ts index bfccd2feb65..5f8ebf13c95 100644 --- a/src/gateway/server-methods/send.ts +++ b/src/gateway/server-methods/send.ts @@ -1,3 +1,11 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateMessageActionParams, + validatePollParams, + validateSendParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { sendDurableMessageBatch } from "../../channels/message/runtime.js"; import { normalizeChannelId } from "../../channels/plugins/index.js"; @@ -31,14 +39,6 @@ import { readStringValue, } from "../../shared/string-coerce.js"; import { ADMIN_SCOPE } from "../operator-scopes.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateMessageActionParams, - validatePollParams, - validateSendParams, -} from "../protocol/index.js"; import { resolveGatewayPluginConfig } from "../runtime-plugin-config.js"; import { formatForLog } from "../ws-log.js"; import type { GatewayRequestContext, GatewayRequestHandlers, RespondFn } from "./types.js"; diff --git a/src/gateway/server-methods/server-methods.test.ts b/src/gateway/server-methods/server-methods.test.ts index d3995488de8..92f10136caa 100644 --- a/src/gateway/server-methods/server-methods.test.ts +++ b/src/gateway/server-methods/server-methods.test.ts @@ -5,6 +5,7 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { validateExecApprovalRequestParams } from "../../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { emitAgentEvent } from "../../infra/agent-events.js"; import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js"; @@ -16,7 +17,6 @@ import { resetLogger, setLoggerOverride } from "../../logging.js"; import { asOptionalRecord } from "../../shared/record-coerce.js"; import { projectRecentChatDisplayMessages } from "../chat-display-projection.js"; import { ExecApprovalManager } from "../exec-approval-manager.js"; -import { validateExecApprovalRequestParams } from "../protocol/index.js"; import { waitForAgentJob } from "./agent-job.js"; import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; import { normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize.js"; diff --git a/src/gateway/server-methods/sessions.send-deleted-agent.test.ts b/src/gateway/server-methods/sessions.send-deleted-agent.test.ts index 532c3e2dcf5..99fa6f50223 100644 --- a/src/gateway/server-methods/sessions.send-deleted-agent.test.ts +++ b/src/gateway/server-methods/sessions.send-deleted-agent.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ErrorCodes } from "../protocol/index.js"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import { mockDeletedAgentSession, resetDeletedAgentSessionMocks, diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index faec15889cf..370c9ef4076 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -1,6 +1,31 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { GATEWAY_CLIENT_IDS } from "../../../packages/gateway-protocol/src/client-info.js"; +import { + ErrorCodes, + errorShape, + type SessionOperationEvent, + validateSessionsAbortParams, + validateSessionsCleanupParams, + validateSessionsCompactParams, + validateSessionsCompactionBranchParams, + validateSessionsCompactionGetParams, + validateSessionsCompactionListParams, + validateSessionsCompactionRestoreParams, + validateSessionsCreateParams, + validateSessionsDeleteParams, + validateSessionsDescribeParams, + validateSessionsListParams, + validateSessionsMessagesSubscribeParams, + validateSessionsMessagesUnsubscribeParams, + validateSessionsPatchParams, + validateSessionsPluginPatchParams, + validateSessionsPreviewParams, + validateSessionsResetParams, + validateSessionsResolveParams, + validateSessionsSendParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { resolveModelAgentRuntimeMetadata } from "../../agents/agent-runtime-metadata.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { @@ -49,31 +74,6 @@ import { readStringValue, } from "../../shared/string-coerce.js"; import { ADMIN_SCOPE } from "../operator-scopes.js"; -import { GATEWAY_CLIENT_IDS } from "../protocol/client-info.js"; -import { - ErrorCodes, - errorShape, - type SessionOperationEvent, - validateSessionsAbortParams, - validateSessionsCleanupParams, - validateSessionsCompactParams, - validateSessionsCompactionBranchParams, - validateSessionsCompactionGetParams, - validateSessionsCompactionListParams, - validateSessionsCompactionRestoreParams, - validateSessionsCreateParams, - validateSessionsDeleteParams, - validateSessionsDescribeParams, - validateSessionsListParams, - validateSessionsMessagesSubscribeParams, - validateSessionsMessagesUnsubscribeParams, - validateSessionsPatchParams, - validateSessionsPluginPatchParams, - validateSessionsPreviewParams, - validateSessionsResetParams, - validateSessionsResolveParams, - validateSessionsSendParams, -} from "../protocol/index.js"; import { resolveSessionKeyForRun } from "../server-session-key.js"; import { forkCompactionCheckpointTranscriptAsync, diff --git a/src/gateway/server-methods/shared-types.ts b/src/gateway/server-methods/shared-types.ts index 153f9cb2823..b94a4ef1bee 100644 --- a/src/gateway/server-methods/shared-types.ts +++ b/src/gateway/server-methods/shared-types.ts @@ -1,3 +1,8 @@ +import type { + ConnectParams, + ErrorShape, + RequestFrame, +} from "../../../packages/gateway-protocol/src/index.js"; import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js"; import type { CliDeps } from "../../cli/deps.types.js"; import type { HealthSummary } from "../../commands/health.types.js"; @@ -11,7 +16,6 @@ import type { ExecApprovalManager, ExecApprovalRecord } from "../exec-approval-m import type { GatewayMethodRegistryView } from "../methods/descriptor.js"; import type { NodeRegistry } from "../node-registry.js"; import type { PluginNodeCapabilitySurface } from "../plugin-node-capability.js"; -import type { ConnectParams, ErrorShape, RequestFrame } from "../protocol/index.js"; import type { GatewayBroadcastFn, GatewayBroadcastToConnIdsFn } from "../server-broadcast-types.js"; import type { ChannelRuntimeSnapshot } from "../server-channel-runtime.types.js"; import type { BufferedAgentEvent } from "../server-chat-state.js"; diff --git a/src/gateway/server-methods/skills-upload.ts b/src/gateway/server-methods/skills-upload.ts index 44868bc5e90..f94134b739c 100644 --- a/src/gateway/server-methods/skills-upload.ts +++ b/src/gateway/server-methods/skills-upload.ts @@ -1,10 +1,3 @@ -import { - installSkillArchiveFromPath, - type SkillArchiveInstallFailureKind, - validateRequestedSkillSlug, -} from "../../agents/skills-archive-install.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { formatErrorMessage } from "../../infra/errors.js"; import { ErrorCodes, errorShape, @@ -13,8 +6,15 @@ import { validateSkillsUploadBeginParams, validateSkillsUploadChunkParams, validateSkillsUploadCommitParams, -} from "../protocol/index.js"; -import type { ErrorShape } from "../protocol/index.js"; +} from "../../../packages/gateway-protocol/src/index.js"; +import type { ErrorShape } from "../../../packages/gateway-protocol/src/index.js"; +import { + installSkillArchiveFromPath, + type SkillArchiveInstallFailureKind, + validateRequestedSkillSlug, +} from "../../agents/skills-archive-install.js"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { formatErrorMessage } from "../../infra/errors.js"; import { defaultSkillUploadStore, normalizeSkillUploadSha256, diff --git a/src/gateway/server-methods/skills.ts b/src/gateway/server-methods/skills.ts index d29bb2de2cd..946aa0d2bf4 100644 --- a/src/gateway/server-methods/skills.ts +++ b/src/gateway/server-methods/skills.ts @@ -1,3 +1,16 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateSkillsBinsParams, + validateSkillsDetailParams, + validateSkillsInstallParams, + validateSkillsSearchParams, + validateSkillsSecurityVerdictsParams, + validateSkillsSkillCardParams, + validateSkillsStatusParams, + validateSkillsUpdateParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { listAgentIds, resolveAgentWorkspaceDir, @@ -25,19 +38,6 @@ import { formatErrorMessage } from "../../infra/errors.js"; import { getRemoteSkillEligibility } from "../../infra/skills-remote.js"; import { normalizeAgentId } from "../../routing/session-key.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateSkillsBinsParams, - validateSkillsDetailParams, - validateSkillsInstallParams, - validateSkillsSearchParams, - validateSkillsSecurityVerdictsParams, - validateSkillsSkillCardParams, - validateSkillsStatusParams, - validateSkillsUpdateParams, -} from "../protocol/index.js"; import { updateSkillConfigEntry } from "./skills-config-mutations.js"; import { installUploadedSkillArchive, skillsUploadHandlers } from "./skills-upload.js"; import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; diff --git a/src/gateway/server-methods/system.ts b/src/gateway/server-methods/system.ts index 084a9debd5a..f3793937d31 100644 --- a/src/gateway/server-methods/system.ts +++ b/src/gateway/server-methods/system.ts @@ -1,3 +1,4 @@ +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { resolveMainSessionKeyFromConfig } from "../../config/sessions.js"; import { loadOrCreateDeviceIdentity, @@ -12,7 +13,6 @@ import { normalizeOptionalString, readStringValue, } from "../../shared/string-coerce.js"; -import { ErrorCodes, errorShape } from "../protocol/index.js"; import { broadcastPresenceSnapshot } from "../server/presence-events.js"; import type { GatewayRequestHandlers } from "./types.js"; diff --git a/src/gateway/server-methods/talk-client.ts b/src/gateway/server-methods/talk-client.ts index ed669cc5884..dd550314fa4 100644 --- a/src/gateway/server-methods/talk-client.ts +++ b/src/gateway/server-methods/talk-client.ts @@ -1,3 +1,11 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateTalkClientCreateParams, + validateTalkClientSteerParams, + validateTalkClientToolCallParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -9,14 +17,6 @@ import { import { REALTIME_VOICE_AGENT_CONTROL_TOOL } from "../../talk/agent-run-control-shared.js"; import { controlRealtimeVoiceAgentRun } from "../../talk/agent-run-control.js"; import { resolveConfiguredRealtimeVoiceProvider } from "../../talk/provider-resolver.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateTalkClientCreateParams, - validateTalkClientSteerParams, - validateTalkClientToolCallParams, -} from "../protocol/index.js"; import { startTalkRealtimeAgentConsult } from "../talk-agent-consult.js"; import { formatForLog } from "../ws-log.js"; import { diff --git a/src/gateway/server-methods/talk-session.ts b/src/gateway/server-methods/talk-session.ts index 5f5158fac36..f462a23ab71 100644 --- a/src/gateway/server-methods/talk-session.ts +++ b/src/gateway/server-methods/talk-session.ts @@ -1,13 +1,3 @@ -import { - normalizeOptionalLowercaseString, - normalizeOptionalString, -} from "../../shared/string-coerce.js"; -import { REALTIME_VOICE_AGENT_CONSULT_TOOL } from "../../talk/agent-consult-tool.js"; -import { REALTIME_VOICE_AGENT_CONTROL_TOOL } from "../../talk/agent-run-control-shared.js"; -import { controlRealtimeVoiceAgentRun } from "../../talk/agent-run-control.js"; -import { resolveConfiguredRealtimeVoiceProvider } from "../../talk/provider-resolver.js"; -import type { TalkBrain, TalkMode, TalkTransport } from "../../talk/talk-events.js"; -import { ADMIN_SCOPE } from "../operator-scopes.js"; import { ErrorCodes, errorShape, @@ -21,7 +11,17 @@ import { validateTalkSessionSteerParams, validateTalkSessionSubmitToolResultParams, validateTalkSessionTurnParams, -} from "../protocol/index.js"; +} from "../../../packages/gateway-protocol/src/index.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; +import { REALTIME_VOICE_AGENT_CONSULT_TOOL } from "../../talk/agent-consult-tool.js"; +import { REALTIME_VOICE_AGENT_CONTROL_TOOL } from "../../talk/agent-run-control-shared.js"; +import { controlRealtimeVoiceAgentRun } from "../../talk/agent-run-control.js"; +import { resolveConfiguredRealtimeVoiceProvider } from "../../talk/provider-resolver.js"; +import type { TalkBrain, TalkMode, TalkTransport } from "../../talk/talk-events.js"; +import { ADMIN_SCOPE } from "../operator-scopes.js"; import { resolveSessionKeyFromResolveParams } from "../sessions-resolve.js"; import { cancelTalkHandoffTurn, diff --git a/src/gateway/server-methods/talk-shared.ts b/src/gateway/server-methods/talk-shared.ts index 7d3cb66a905..d50dd204641 100644 --- a/src/gateway/server-methods/talk-shared.ts +++ b/src/gateway/server-methods/talk-shared.ts @@ -1,3 +1,4 @@ +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../../config/types.js"; import { listRealtimeTranscriptionProviders } from "../../realtime-transcription/provider-registry.js"; import type { RealtimeTranscriptionProviderConfig } from "../../realtime-transcription/provider-types.js"; @@ -14,7 +15,6 @@ import type { } from "../../talk/provider-types.js"; import type { TalkEvent } from "../../talk/talk-events.js"; import { ADMIN_SCOPE } from "../operator-scopes.js"; -import { ErrorCodes } from "../protocol/index.js"; import type { TalkHandoffTurnResult } from "../talk-handoff.js"; import { asRecord } from "./record-shared.js"; diff --git a/src/gateway/server-methods/talk.test.ts b/src/gateway/server-methods/talk.test.ts index 0b0e47953c5..f7f19a59173 100644 --- a/src/gateway/server-methods/talk.test.ts +++ b/src/gateway/server-methods/talk.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../../config/config.js"; import { normalizeResolvedSecretInputString } from "../../config/types.secrets.js"; -import { ErrorCodes } from "../protocol/index.js"; import { talkHandlers } from "./talk.js"; const mocks = vi.hoisted(() => ({ diff --git a/src/gateway/server-methods/talk.ts b/src/gateway/server-methods/talk.ts index e2a56f56ee8..7534967d306 100644 --- a/src/gateway/server-methods/talk.ts +++ b/src/gateway/server-methods/talk.ts @@ -1,3 +1,13 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + type TalkSpeakParams, + validateTalkCatalogParams, + validateTalkConfigParams, + validateTalkModeParams, + validateTalkSpeakParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { readConfigFileSnapshot } from "../../config/config.js"; import { redactConfigObject } from "../../config/redact-snapshot.js"; import { @@ -29,16 +39,6 @@ import { type TtsDirectiveOverrides, } from "../../tts/tts.js"; import { ADMIN_SCOPE, TALK_SECRETS_SCOPE } from "../operator-scopes.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - type TalkSpeakParams, - validateTalkCatalogParams, - validateTalkConfigParams, - validateTalkModeParams, - validateTalkSpeakParams, -} from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; import { asRecord } from "./record-shared.js"; import { talkClientHandlers } from "./talk-client.js"; diff --git a/src/gateway/server-methods/tasks.ts b/src/gateway/server-methods/tasks.ts index 56c8a909417..ca8e393bf21 100644 --- a/src/gateway/server-methods/tasks.ts +++ b/src/gateway/server-methods/tasks.ts @@ -1,3 +1,13 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + type TaskSummary, + type TasksListParams, + validateTasksCancelParams, + validateTasksGetParams, + validateTasksListParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { parseAgentSessionKey } from "../../routing/session-key.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { cancelDetachedTaskRunById } from "../../tasks/detached-task-runtime.js"; @@ -8,16 +18,6 @@ import { formatTaskStatusTitle, sanitizeTaskStatusText, } from "../../tasks/task-status.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - type TaskSummary, - type TasksListParams, - validateTasksCancelParams, - validateTasksGetParams, - validateTasksListParams, -} from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; const DEFAULT_TASKS_LIST_LIMIT = 100; diff --git a/src/gateway/server-methods/tools-catalog.test.ts b/src/gateway/server-methods/tools-catalog.test.ts index 802d38a1915..0ed0750f6a0 100644 --- a/src/gateway/server-methods/tools-catalog.test.ts +++ b/src/gateway/server-methods/tools-catalog.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import { ensureStandalonePluginToolRegistryLoaded, resolvePluginTools, } from "../../plugins/tools.js"; -import { ErrorCodes } from "../protocol/index.js"; import { toolsCatalogHandlers } from "./tools-catalog.js"; vi.mock("../../config/config.js", () => ({ diff --git a/src/gateway/server-methods/tools-catalog.ts b/src/gateway/server-methods/tools-catalog.ts index c1748552af7..df4ccfa1990 100644 --- a/src/gateway/server-methods/tools-catalog.ts +++ b/src/gateway/server-methods/tools-catalog.ts @@ -1,3 +1,10 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + type ToolsCatalogResult, + validateToolsCatalogParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { listAgentIds, resolveAgentDir, @@ -19,13 +26,6 @@ import { resolvePluginTools, } from "../../plugins/tools.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - type ToolsCatalogResult, - validateToolsCatalogParams, -} from "../protocol/index.js"; import type { GatewayRequestHandlers, RespondFn } from "./types.js"; type ToolCatalogEntry = { diff --git a/src/gateway/server-methods/tools-effective.test.ts b/src/gateway/server-methods/tools-effective.test.ts index b9d5253515b..054b855c2fe 100644 --- a/src/gateway/server-methods/tools-effective.test.ts +++ b/src/gateway/server-methods/tools-effective.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import type { McpToolCatalog, SessionMcpRuntime } from "../../agents/agent-bundle-mcp-types.js"; import { setPluginToolMeta } from "../../plugins/tools.js"; -import { ErrorCodes } from "../protocol/index.js"; import { testing, toolsEffectiveHandlers } from "./tools-effective.js"; const runtimeMocks = vi.hoisted(() => ({ diff --git a/src/gateway/server-methods/tools-effective.ts b/src/gateway/server-methods/tools-effective.ts index 7ebe1a81c69..4986ecd1ee1 100644 --- a/src/gateway/server-methods/tools-effective.ts +++ b/src/gateway/server-methods/tools-effective.ts @@ -1,3 +1,9 @@ +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateToolsEffectiveParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { buildEffectiveToolInventoryGroups, buildRuntimeCompatibleToolInventory, @@ -10,12 +16,6 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { logDebug, logWarn } from "../../logger.js"; import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateToolsEffectiveParams, -} from "../protocol/index.js"; import { applyFinalEffectiveToolPolicy, buildBundleMcpToolsFromCatalog, diff --git a/src/gateway/server-methods/tools-invoke.ts b/src/gateway/server-methods/tools-invoke.ts index 98be8685763..f30019f4d77 100644 --- a/src/gateway/server-methods/tools-invoke.ts +++ b/src/gateway/server-methods/tools-invoke.ts @@ -1,11 +1,11 @@ -import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { ErrorCodes, errorShape, formatValidationErrors, validateToolsInvokeParams, type ToolsInvokeResult, -} from "../protocol/index.js"; +} from "../../../packages/gateway-protocol/src/index.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { invokeGatewayTool } from "../tools-invoke-shared.js"; import type { GatewayRequestHandlers } from "./types.js"; diff --git a/src/gateway/server-methods/tts.test.ts b/src/gateway/server-methods/tts.test.ts index 3b79b53f4d5..62358592d92 100644 --- a/src/gateway/server-methods/tts.test.ts +++ b/src/gateway/server-methods/tts.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ErrorCodes } from "../protocol/index.js"; +import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; const mocks = vi.hoisted(() => ({ getRuntimeConfig: vi.fn(() => ({})), diff --git a/src/gateway/server-methods/tts.ts b/src/gateway/server-methods/tts.ts index 4aada1074b7..9418eaa272f 100644 --- a/src/gateway/server-methods/tts.ts +++ b/src/gateway/server-methods/tts.ts @@ -1,3 +1,4 @@ +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { canonicalizeSpeechProviderId, @@ -21,7 +22,6 @@ import { setTtsProvider, textToSpeech, } from "../../tts/tts.js"; -import { ErrorCodes, errorShape } from "../protocol/index.js"; import { formatForLog } from "../ws-log.js"; import type { GatewayRequestHandlers } from "./types.js"; diff --git a/src/gateway/server-methods/update.test.ts b/src/gateway/server-methods/update.test.ts index 75dc986139a..1c776fc932d 100644 --- a/src/gateway/server-methods/update.test.ts +++ b/src/gateway/server-methods/update.test.ts @@ -99,7 +99,7 @@ vi.mock("../../infra/update-runner.js", () => ({ runGatewayUpdate: runGatewayUpdateMock, })); -vi.mock("../protocol/index.js", () => ({ +vi.mock("../../../packages/gateway-protocol/src/index.js", () => ({ validateUpdateStatusParams: () => true, validateUpdateRunParams: () => true, })); diff --git a/src/gateway/server-methods/update.ts b/src/gateway/server-methods/update.ts index 78d6fd52a73..cf0d134d1ad 100644 --- a/src/gateway/server-methods/update.ts +++ b/src/gateway/server-methods/update.ts @@ -1,5 +1,9 @@ import { randomUUID } from "node:crypto"; import os from "node:os"; +import { + validateUpdateRunParams, + validateUpdateStatusParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { isRestartEnabled } from "../../config/commands.flags.js"; import { extractDeliveryInfo } from "../../config/sessions.js"; import { resolveOpenClawPackageRoot } from "../../infra/openclaw-root.js"; @@ -15,7 +19,6 @@ import { } from "../../infra/update-restart-sentinel-payload.js"; import { resolveUpdateInstallSurface, runGatewayUpdate } from "../../infra/update-runner.js"; import { formatControlPlaneActor, resolveControlPlaneActor } from "../control-plane-audit.js"; -import { validateUpdateRunParams, validateUpdateStatusParams } from "../protocol/index.js"; import { getLatestUpdateRestartSentinel, recordLatestUpdateRestartSentinel, diff --git a/src/gateway/server-methods/usage.ts b/src/gateway/server-methods/usage.ts index a3c681aae5a..e3fcb81f30e 100644 --- a/src/gateway/server-methods/usage.ts +++ b/src/gateway/server-methods/usage.ts @@ -1,4 +1,10 @@ import fs from "node:fs"; +import { + ErrorCodes, + errorShape, + formatValidationErrors, + validateSessionsUsageParams, +} from "../../../packages/gateway-protocol/src/index.js"; import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { resolveSessionFilePath, @@ -39,12 +45,6 @@ import type { SessionsUsageResult, } from "../../shared/usage-types.js"; import { runTasksWithConcurrency } from "../../utils/run-with-concurrency.js"; -import { - ErrorCodes, - errorShape, - formatValidationErrors, - validateSessionsUsageParams, -} from "../protocol/index.js"; import { resolveSessionStoreAgentId, resolveStoredSessionKeyForAgentStore, diff --git a/src/gateway/server-methods/validation.ts b/src/gateway/server-methods/validation.ts index f80ab751ae1..3627098bff4 100644 --- a/src/gateway/server-methods/validation.ts +++ b/src/gateway/server-methods/validation.ts @@ -1,5 +1,9 @@ -import { ErrorCodes, errorShape, formatValidationErrors } from "../protocol/index.js"; -import type { ValidationError } from "../protocol/index.js"; +import { + ErrorCodes, + errorShape, + formatValidationErrors, +} from "../../../packages/gateway-protocol/src/index.js"; +import type { ValidationError } from "../../../packages/gateway-protocol/src/index.js"; import type { RespondFn } from "./types.js"; export type Validator = ((params: unknown) => params is T) & { diff --git a/src/gateway/server-methods/voicewake-routing.ts b/src/gateway/server-methods/voicewake-routing.ts index a23a2ca7a6c..34dccc283e5 100644 --- a/src/gateway/server-methods/voicewake-routing.ts +++ b/src/gateway/server-methods/voicewake-routing.ts @@ -1,10 +1,10 @@ +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { loadVoiceWakeRoutingConfig, normalizeVoiceWakeRoutingConfig, setVoiceWakeRoutingConfig, validateVoiceWakeRoutingConfigInput, } from "../../infra/voicewake-routing.js"; -import { ErrorCodes, errorShape } from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; export const voicewakeRoutingHandlers: GatewayRequestHandlers = { diff --git a/src/gateway/server-methods/voicewake.ts b/src/gateway/server-methods/voicewake.ts index 3f43488aa98..ff92fc5f7b1 100644 --- a/src/gateway/server-methods/voicewake.ts +++ b/src/gateway/server-methods/voicewake.ts @@ -1,5 +1,5 @@ +import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js"; import { loadVoiceWakeConfig, setVoiceWakeTriggers } from "../../infra/voicewake.js"; -import { ErrorCodes, errorShape } from "../protocol/index.js"; import { normalizeVoiceWakeTriggers } from "../server-utils.js"; import { formatForLog } from "../ws-log.js"; import type { GatewayRequestHandlers } from "./types.js"; diff --git a/src/gateway/server-methods/web.ts b/src/gateway/server-methods/web.ts index fefaf5bc2b5..6b7bfb5b737 100644 --- a/src/gateway/server-methods/web.ts +++ b/src/gateway/server-methods/web.ts @@ -1,12 +1,12 @@ -import { listChannelPlugins } from "../../channels/plugins/index.js"; -import type { ChannelId } from "../../channels/plugins/types.public.js"; import { ErrorCodes, errorShape, formatValidationErrors, validateWebLoginStartParams, validateWebLoginWaitParams, -} from "../protocol/index.js"; +} from "../../../packages/gateway-protocol/src/index.js"; +import { listChannelPlugins } from "../../channels/plugins/index.js"; +import type { ChannelId } from "../../channels/plugins/types.public.js"; import { formatForLog } from "../ws-log.js"; import type { GatewayRequestHandlers, RespondFn } from "./types.js"; diff --git a/src/gateway/server-methods/wizard.ts b/src/gateway/server-methods/wizard.ts index 84f00d97bf2..708e82ee927 100644 --- a/src/gateway/server-methods/wizard.ts +++ b/src/gateway/server-methods/wizard.ts @@ -1,7 +1,4 @@ import { randomUUID } from "node:crypto"; -import { defaultRuntime } from "../../runtime.js"; -import { readStringValue } from "../../shared/string-coerce.js"; -import { WizardSession } from "../../wizard/session.js"; import { ErrorCodes, errorShape, @@ -9,7 +6,10 @@ import { validateWizardNextParams, validateWizardStartParams, validateWizardStatusParams, -} from "../protocol/index.js"; +} from "../../../packages/gateway-protocol/src/index.js"; +import { defaultRuntime } from "../../runtime.js"; +import { readStringValue } from "../../shared/string-coerce.js"; +import { WizardSession } from "../../wizard/session.js"; import { formatForLog } from "../ws-log.js"; import type { GatewayRequestContext, GatewayRequestHandlers, RespondFn } from "./types.js"; import { assertValidParams } from "./validation.js"; diff --git a/src/gateway/server-node-events.test.ts b/src/gateway/server-node-events.test.ts index fd44c75ff1e..f712539c6cb 100644 --- a/src/gateway/server-node-events.test.ts +++ b/src/gateway/server-node-events.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { PROTOCOL_VERSION } from "../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../config/config.js"; import { NodeRegistry } from "./node-registry.js"; -import { PROTOCOL_VERSION } from "./protocol/index.js"; import type { GatewayWsClient } from "./server/ws-types.js"; import type { loadSessionEntry as loadSessionEntryType } from "./session-utils.js"; diff --git a/src/gateway/server-plugins.ts b/src/gateway/server-plugins.ts index 1efc9b6d1f0..6f304d7c55f 100644 --- a/src/gateway/server-plugins.ts +++ b/src/gateway/server-plugins.ts @@ -1,5 +1,11 @@ import { randomUUID } from "node:crypto"; import { performance } from "node:perf_hooks"; +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../packages/gateway-protocol/src/client-info.js"; +import type { ErrorShape } from "../../packages/gateway-protocol/src/index.js"; +import { PROTOCOL_VERSION } from "../../packages/gateway-protocol/src/version.js"; import { normalizeModelRef, parseModelRef } from "../agents/model-selection.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -18,9 +24,6 @@ import { resolveGlobalSingleton } from "../shared/global-singleton.js"; import { uniqueStrings } from "../shared/string-normalization.js"; import { resolveSafeTimeoutDelayMs } from "../utils/timer-delay.js"; import { ADMIN_SCOPE, APPROVALS_SCOPE, WRITE_SCOPE } from "./method-scopes.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js"; -import type { ErrorShape } from "./protocol/index.js"; -import { PROTOCOL_VERSION } from "./protocol/version.js"; import type { GatewayRequestContext, GatewayRequestHandler, diff --git a/src/gateway/server-shared.ts b/src/gateway/server-shared.ts index 919fafe5d17..39b0e6f12ea 100644 --- a/src/gateway/server-shared.ts +++ b/src/gateway/server-shared.ts @@ -1,4 +1,4 @@ -import type { ErrorShape } from "./protocol/index.js"; +import type { ErrorShape } from "../../packages/gateway-protocol/src/index.js"; export type DedupeEntry = { ts: number; diff --git a/src/gateway/server.auth.browser-hardening.test.ts b/src/gateway/server.auth.browser-hardening.test.ts index 5c74498a15c..62141e4374a 100644 --- a/src/gateway/server.auth.browser-hardening.test.ts +++ b/src/gateway/server.auth.browser-hardening.test.ts @@ -3,7 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, test } from "vitest"; import { WebSocket } from "ws"; -import { ConnectErrorDetailCodes } from "../gateway/protocol/connect-error-details.js"; +import { ConnectErrorDetailCodes } from "../../packages/gateway-protocol/src/connect-error-details.js"; import { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, diff --git a/src/gateway/server.auth.shared.ts b/src/gateway/server.auth.shared.ts index 861038cdecf..b6ae6587215 100644 --- a/src/gateway/server.auth.shared.ts +++ b/src/gateway/server.auth.shared.ts @@ -2,10 +2,13 @@ import os from "node:os"; import path from "node:path"; import { expect } from "vitest"; import { WebSocket } from "ws"; +import { + MIN_PROBE_PROTOCOL_VERSION, + PROTOCOL_VERSION, +} from "../../packages/gateway-protocol/src/index.js"; import { withEnvAsync } from "../test-utils/env.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { buildDeviceAuthPayload } from "./device-auth.js"; -import { MIN_PROBE_PROTOCOL_VERSION, PROTOCOL_VERSION } from "./protocol/index.js"; import { createGatewaySuiteHarness, connectReq, @@ -420,7 +423,7 @@ export { withRuntimeVersionEnv, writeTrustedProxyControlUiConfig, }; -export { ConnectErrorDetailCodes } from "./protocol/connect-error-details.js"; +export { ConnectErrorDetailCodes } from "../../packages/gateway-protocol/src/connect-error-details.js"; export { getPreauthHandshakeTimeoutMsFromEnv } from "./handshake-timeouts.js"; -export { PROTOCOL_VERSION } from "./protocol/index.js"; +export { PROTOCOL_VERSION } from "../../packages/gateway-protocol/src/index.js"; export { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; diff --git a/src/gateway/server.ios-client-id.test.ts b/src/gateway/server.ios-client-id.test.ts index ff873cdd77c..2505aeb8373 100644 --- a/src/gateway/server.ios-client-id.test.ts +++ b/src/gateway/server.ios-client-id.test.ts @@ -1,6 +1,9 @@ import { describe, expect, test } from "vitest"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js"; -import { validateConnectParams } from "./protocol/index.js"; +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../packages/gateway-protocol/src/client-info.js"; +import { validateConnectParams } from "../../packages/gateway-protocol/src/index.js"; function makeConnectParams(clientId: string) { return { diff --git a/src/gateway/server.sessions.permissions-hooks.test.ts b/src/gateway/server.sessions.permissions-hooks.test.ts index 0953fe746bd..9a5a21a5749 100644 --- a/src/gateway/server.sessions.permissions-hooks.test.ts +++ b/src/gateway/server.sessions.permissions-hooks.test.ts @@ -3,8 +3,11 @@ import os from "node:os"; import path from "node:path"; import { expect, test, vi } from "vitest"; import { WebSocket } from "ws"; +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../packages/gateway-protocol/src/client-info.js"; import { isSessionPatchEvent } from "../hooks/internal-hooks.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js"; import { connectOk, rpcReq, diff --git a/src/gateway/server.talk-config.test.ts b/src/gateway/server.talk-config.test.ts index dcae34484ec..fa663caa3ec 100644 --- a/src/gateway/server.talk-config.test.ts +++ b/src/gateway/server.talk-config.test.ts @@ -1,6 +1,7 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { validateTalkConfigResult } from "../../packages/gateway-protocol/src/index.js"; import { normalizeResolvedSecretInputString } from "../config/types.secrets.js"; import { loadOrCreateDeviceIdentity, @@ -9,7 +10,6 @@ import { } from "../infra/device-identity.js"; import { withEnvAsync } from "../test-utils/env.js"; import { buildDeviceAuthPayload } from "./device-auth.js"; -import { validateTalkConfigResult } from "./protocol/index.js"; import { withSpeechProviders } from "./talk.test-helpers.js"; import { connectOk, diff --git a/src/gateway/server/health-state.ts b/src/gateway/server/health-state.ts index 5ca4b298277..34adef08b74 100644 --- a/src/gateway/server/health-state.ts +++ b/src/gateway/server/health-state.ts @@ -1,3 +1,4 @@ +import type { Snapshot } from "../../../packages/gateway-protocol/src/index.js"; import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { getHealthSnapshot, type HealthSummary } from "../../commands/health.js"; import { createConfigIO, getRuntimeConfig } from "../../config/io.js"; @@ -7,7 +8,6 @@ import { listSystemPresence } from "../../infra/system-presence.js"; import { getUpdateAvailable } from "../../infra/update-startup.js"; import { normalizeMainKey } from "../../routing/session-key.js"; import { resolveGatewayAuth } from "../auth.js"; -import type { Snapshot } from "../protocol/index.js"; import type { ChannelRuntimeSnapshot } from "../server-channel-runtime.types.js"; import type { GatewayEventLoopHealth } from "./event-loop-health.js"; diff --git a/src/gateway/server/plugins-http.ts b/src/gateway/server/plugins-http.ts index f75dcf2578f..c78975daca2 100644 --- a/src/gateway/server/plugins-http.ts +++ b/src/gateway/server/plugins-http.ts @@ -1,11 +1,14 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { Duplex } from "node:stream"; +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../../packages/gateway-protocol/src/client-info.js"; +import { PROTOCOL_VERSION } from "../../../packages/gateway-protocol/src/index.js"; import type { createSubsystemLogger } from "../../logging/subsystem.js"; import type { PluginRegistry } from "../../plugins/registry.js"; import { withPluginRuntimeGatewayRequestScope } from "../../plugins/runtime/gateway-request-scope.js"; import type { AuthorizedGatewayHttpRequest } from "../http-utils.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../protocol/client-info.js"; -import { PROTOCOL_VERSION } from "../protocol/index.js"; import type { GatewayRequestContext, GatewayRequestOptions } from "../server-methods/types.js"; import { resolvePluginRouteRuntimeOperatorScopes } from "./plugin-route-runtime-scopes.js"; import { diff --git a/src/gateway/server/ws-connection.startup.test.ts b/src/gateway/server/ws-connection.startup.test.ts index 0af25d5ebb8..72de53c0a1c 100644 --- a/src/gateway/server/ws-connection.startup.test.ts +++ b/src/gateway/server/ws-connection.startup.test.ts @@ -1,14 +1,17 @@ import { EventEmitter } from "node:events"; import { describe, expect, it, vi } from "vitest"; import type { WebSocketServer } from "ws"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../protocol/client-info.js"; -import { PROTOCOL_VERSION } from "../protocol/index.js"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../../packages/gateway-protocol/src/client-info.js"; +import { PROTOCOL_VERSION } from "../../../packages/gateway-protocol/src/index.js"; import { GATEWAY_STARTUP_CLOSE_CODE, GATEWAY_STARTUP_CLOSE_REASON, GATEWAY_STARTUP_PENDING_CLOSE_CAUSE, GATEWAY_STARTUP_UNAVAILABLE_REASON, -} from "../protocol/startup-unavailable.js"; +} from "../../../packages/gateway-protocol/src/startup-unavailable.js"; import { attachGatewayWsConnectionHandler } from "./ws-connection.js"; function createLogger() { diff --git a/src/gateway/server/ws-connection.ts b/src/gateway/server/ws-connection.ts index f878ae4cab7..da77aad6183 100644 --- a/src/gateway/server/ws-connection.ts +++ b/src/gateway/server/ws-connection.ts @@ -1,6 +1,10 @@ import { randomUUID } from "node:crypto"; import type { Socket } from "node:net"; import type { RawData, WebSocket, WebSocketServer } from "ws"; +import { + GATEWAY_STARTUP_CLOSE_CODE, + GATEWAY_STARTUP_PENDING_CLOSE_CAUSE, +} from "../../../packages/gateway-protocol/src/startup-unavailable.js"; import { getRuntimeConfig } from "../../config/io.js"; import { removeRemoteNodeInfo } from "../../infra/skills-remote.js"; import { upsertPresence } from "../../infra/system-presence.js"; @@ -16,10 +20,6 @@ import { resolveHostedPluginSurfaceUrl } from "../hosted-plugin-surface-url.js"; import type { GatewayMethodRegistry } from "../methods/registry.js"; import { isLoopbackAddress } from "../net.js"; import type { PluginNodeCapabilitySurface } from "../plugin-node-capability.js"; -import { - GATEWAY_STARTUP_CLOSE_CODE, - GATEWAY_STARTUP_PENDING_CLOSE_CAUSE, -} from "../protocol/startup-unavailable.js"; import { MAX_PAYLOAD_BYTES, MAX_PREAUTH_PAYLOAD_BYTES } from "../server-constants.js"; import { clearNodeWakeState } from "../server-methods/nodes-wake-state.js"; import type { GatewayRequestContext, GatewayRequestHandlers } from "../server-methods/types.js"; diff --git a/src/gateway/server/ws-connection/connect-policy.ts b/src/gateway/server/ws-connection/connect-policy.ts index 6a7c03c04ca..8631c922de8 100644 --- a/src/gateway/server/ws-connection/connect-policy.ts +++ b/src/gateway/server/ws-connection/connect-policy.ts @@ -1,4 +1,4 @@ -import type { ConnectParams } from "../../protocol/index.js"; +import type { ConnectParams } from "../../../../packages/gateway-protocol/src/index.js"; import type { GatewayRole } from "../../role-policy.js"; import { roleCanSkipDeviceIdentity } from "../../role-policy.js"; diff --git a/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts b/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts index a75cdd74001..aad83942107 100644 --- a/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts +++ b/src/gateway/server/ws-connection/handshake-auth-helpers.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from "vitest"; +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../../../packages/gateway-protocol/src/client-info.js"; +import type { ConnectParams } from "../../../../packages/gateway-protocol/src/schema.js"; import type { AuthRateLimiter } from "../../auth-rate-limit.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../protocol/client-info.js"; -import type { ConnectParams } from "../../protocol/schema/types.js"; import { BROWSER_ORIGIN_RATE_LIMIT_KEY_PREFIX, BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP, diff --git a/src/gateway/server/ws-connection/handshake-auth-helpers.ts b/src/gateway/server/ws-connection/handshake-auth-helpers.ts index 63b2582cbcf..c283568fb1a 100644 --- a/src/gateway/server/ws-connection/handshake-auth-helpers.ts +++ b/src/gateway/server/ws-connection/handshake-auth-helpers.ts @@ -1,3 +1,8 @@ +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../../../packages/gateway-protocol/src/client-info.js"; +import type { ConnectParams } from "../../../../packages/gateway-protocol/src/index.js"; import { verifyDeviceSignature } from "../../../infra/device-identity.js"; import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js"; import type { AuthRateLimiter } from "../../auth-rate-limit.js"; @@ -10,8 +15,6 @@ import { isPrivateOrLoopbackHost, resolveHostName, } from "../../net.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../protocol/client-info.js"; -import type { ConnectParams } from "../../protocol/index.js"; import type { AuthProvidedKind } from "./auth-messages.js"; export const BROWSER_ORIGIN_LOOPBACK_RATE_LIMIT_IP = "198.18.0.1"; diff --git a/src/gateway/server/ws-connection/message-handler.post-connect-health.test.ts b/src/gateway/server/ws-connection/message-handler.post-connect-health.test.ts index 46e3a30d112..656cd8c5017 100644 --- a/src/gateway/server/ws-connection/message-handler.post-connect-health.test.ts +++ b/src/gateway/server/ws-connection/message-handler.post-connect-health.test.ts @@ -1,10 +1,10 @@ import type { IncomingMessage } from "node:http"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { WebSocket } from "ws"; +import { PROTOCOL_VERSION } from "../../../../packages/gateway-protocol/src/index.js"; import type { HealthSummary } from "../../../commands/health.types.js"; import type { ResolvedGatewayAuth } from "../../auth.js"; import { getOperatorApprovalRuntimeToken } from "../../operator-approval-runtime-token.js"; -import { PROTOCOL_VERSION } from "../../protocol/index.js"; import { handleGatewayRequest } from "../../server-methods.js"; import type { GatewayRequestContext } from "../../server-methods/types.js"; diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 2bcfa146fa2..421f3213f0e 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -3,6 +3,37 @@ import type { IncomingMessage } from "node:http"; import os from "node:os"; import path from "node:path"; import type { RawData, WebSocket } from "ws"; +import { + GATEWAY_CLIENT_IDS, + GATEWAY_CLIENT_MODES, +} from "../../../../packages/gateway-protocol/src/client-info.js"; +import { + buildPairingConnectCloseReason, + buildPairingConnectErrorDetails, + buildPairingConnectErrorMessage, + ConnectErrorDetailCodes, + type ConnectPairingRequiredReason, + resolveDeviceAuthConnectErrorDetailCode, + resolveAuthConnectErrorDetailCode, +} from "../../../../packages/gateway-protocol/src/connect-error-details.js"; +import { + type ConnectParams, + ErrorCodes, + type ErrorShape, + errorShape, + formatValidationErrors, + MIN_PROBE_PROTOCOL_VERSION, + PROTOCOL_VERSION, + validateConnectParams, + validateRequestFrame, +} from "../../../../packages/gateway-protocol/src/index.js"; +import { + gatewayStartupUnavailableDetails, + GATEWAY_STARTUP_CLOSE_CODE, + GATEWAY_STARTUP_CLOSE_REASON, + GATEWAY_STARTUP_PENDING_CLOSE_CAUSE, + GATEWAY_STARTUP_RETRY_AFTER_MS, +} from "../../../../packages/gateway-protocol/src/startup-unavailable.js"; import { getRuntimeConfig } from "../../../config/io.js"; import { resolveStateDir } from "../../../config/paths.js"; import { @@ -88,34 +119,6 @@ import { resolvePluginNodeCapabilityTtlMs, setClientPluginNodeCapability, } from "../../plugin-node-capability.js"; -import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../protocol/client-info.js"; -import { - buildPairingConnectCloseReason, - buildPairingConnectErrorDetails, - buildPairingConnectErrorMessage, - ConnectErrorDetailCodes, - type ConnectPairingRequiredReason, - resolveDeviceAuthConnectErrorDetailCode, - resolveAuthConnectErrorDetailCode, -} from "../../protocol/connect-error-details.js"; -import { - type ConnectParams, - ErrorCodes, - type ErrorShape, - errorShape, - formatValidationErrors, - MIN_PROBE_PROTOCOL_VERSION, - PROTOCOL_VERSION, - validateConnectParams, - validateRequestFrame, -} from "../../protocol/index.js"; -import { - gatewayStartupUnavailableDetails, - GATEWAY_STARTUP_CLOSE_CODE, - GATEWAY_STARTUP_CLOSE_REASON, - GATEWAY_STARTUP_PENDING_CLOSE_CAUSE, - GATEWAY_STARTUP_RETRY_AFTER_MS, -} from "../../protocol/startup-unavailable.js"; import { parseGatewayRole } from "../../role-policy.js"; import { MAX_BUFFERED_BYTES, diff --git a/src/gateway/server/ws-connection/unauthorized-flood-guard.test.ts b/src/gateway/server/ws-connection/unauthorized-flood-guard.test.ts index 8c750570dcf..946519d1619 100644 --- a/src/gateway/server/ws-connection/unauthorized-flood-guard.test.ts +++ b/src/gateway/server/ws-connection/unauthorized-flood-guard.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ErrorCodes, errorShape } from "../../protocol/index.js"; +import { ErrorCodes, errorShape } from "../../../../packages/gateway-protocol/src/index.js"; import { isUnauthorizedRoleError, UnauthorizedFloodGuard } from "./unauthorized-flood-guard.js"; describe("UnauthorizedFloodGuard", () => { diff --git a/src/gateway/server/ws-connection/unauthorized-flood-guard.ts b/src/gateway/server/ws-connection/unauthorized-flood-guard.ts index f7a7636b594..2898e8d13c3 100644 --- a/src/gateway/server/ws-connection/unauthorized-flood-guard.ts +++ b/src/gateway/server/ws-connection/unauthorized-flood-guard.ts @@ -1,4 +1,4 @@ -import { ErrorCodes, type ErrorShape } from "../../protocol/index.js"; +import { ErrorCodes, type ErrorShape } from "../../../../packages/gateway-protocol/src/index.js"; export type UnauthorizedFloodGuardOptions = { closeAfter?: number; diff --git a/src/gateway/server/ws-types.ts b/src/gateway/server/ws-types.ts index 459324e5279..62454a70192 100644 --- a/src/gateway/server/ws-types.ts +++ b/src/gateway/server/ws-types.ts @@ -1,6 +1,6 @@ import type { WebSocket } from "ws"; +import type { ConnectParams } from "../../../packages/gateway-protocol/src/index.js"; import type { PluginNodeCapabilityClient } from "../plugin-node-capability.js"; -import type { ConnectParams } from "../protocol/index.js"; export type GatewayWsClient = PluginNodeCapabilityClient & { socket: WebSocket; diff --git a/src/gateway/session-patch-hooks.ts b/src/gateway/session-patch-hooks.ts index 9f6250470c4..37e7872c90a 100644 --- a/src/gateway/session-patch-hooks.ts +++ b/src/gateway/session-patch-hooks.ts @@ -1,3 +1,4 @@ +import type { SessionsPatchParams } from "../../packages/gateway-protocol/src/index.js"; import type { SessionEntry } from "../config/sessions.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { @@ -6,7 +7,6 @@ import { type SessionPatchHookContext, type SessionPatchHookEvent, } from "../hooks/internal-hooks.js"; -import type { SessionsPatchParams } from "./protocol/index.js"; export function triggerSessionPatchHook(params: { cfg: OpenClawConfig; diff --git a/src/gateway/session-reset-service.ts b/src/gateway/session-reset-service.ts index d1414ac0ae6..699c7c75db3 100644 --- a/src/gateway/session-reset-service.ts +++ b/src/gateway/session-reset-service.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { ErrorCodes, errorShape } from "../../packages/gateway-protocol/src/index.js"; import { getAcpSessionManager } from "../acp/control-plane/manager.js"; import { getAcpRuntimeBackend } from "../acp/runtime/registry.js"; import { readAcpSessionEntry, upsertAcpSessionMeta } from "../acp/runtime/session-meta.js"; @@ -47,7 +48,6 @@ import { listActiveSessionsForShutdown, noteActiveSessionForShutdown, } from "./active-sessions-shutdown-tracker.js"; -import { ErrorCodes, errorShape } from "./protocol/index.js"; import { findDirectChildSessionsForParent } from "./session-child-sessions.js"; import { archiveSessionTranscriptsDetailed, diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index f956d64087c..c61755095a1 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import type { SessionsListParams } from "../../packages/gateway-protocol/src/index.js"; import { resolveModelAgentRuntimeMetadata } from "../agents/agent-runtime-metadata.js"; import { listAgentIds, @@ -2101,7 +2102,7 @@ function compareSessionEntryPairsByUpdatedAt(a: SessionEntryPair, b: SessionEntr } function resolveSessionsListLimit( - opts: import("./protocol/index.js").SessionsListParams, + opts: SessionsListParams, defaultLimit?: number, ): number | undefined { if (typeof opts.limit !== "number" || !Number.isFinite(opts.limit)) { @@ -2110,7 +2111,7 @@ function resolveSessionsListLimit( return Math.max(1, Math.floor(opts.limit)); } -function resolveSessionsListOffset(opts: import("./protocol/index.js").SessionsListParams): number { +function resolveSessionsListOffset(opts: SessionsListParams): number { if (typeof opts.offset !== "number" || !Number.isFinite(opts.offset)) { return 0; } @@ -2160,7 +2161,7 @@ function sortAndLimitSessionEntries( function filterSessionEntries(params: { cfg: OpenClawConfig; store: Record; - opts: import("./protocol/index.js").SessionsListParams; + opts: SessionsListParams; now: number; rowContext?: SessionListRowContext; }): SessionEntryPair[] { @@ -2268,7 +2269,7 @@ function filterSessionEntries(params: { function selectSessionEntries(params: { cfg: OpenClawConfig; store: Record; - opts: import("./protocol/index.js").SessionsListParams; + opts: SessionsListParams; now: number; rowContext?: SessionListRowContext; defaultLimit?: number; @@ -2295,7 +2296,7 @@ function selectSessionEntries(params: { export function filterAndSortSessionEntries(params: { cfg: OpenClawConfig; store: Record; - opts: import("./protocol/index.js").SessionsListParams; + opts: SessionsListParams; now: number; rowContext?: SessionListRowContext; }): [string, SessionEntry][] { @@ -2307,7 +2308,7 @@ export function listSessionsFromStore(params: { storePath: string; store: Record; modelCatalog?: ModelCatalogEntry[]; - opts: import("./protocol/index.js").SessionsListParams; + opts: SessionsListParams; }): SessionsListResult { const { cfg, storePath, store, opts } = params; const now = Date.now(); @@ -2382,7 +2383,7 @@ export async function listSessionsFromStoreAsync(params: { storePath: string; store: Record; modelCatalog?: ModelCatalogEntry[]; - opts: import("./protocol/index.js").SessionsListParams; + opts: SessionsListParams; }): Promise { const { cfg, storePath, store, opts } = params; const now = Date.now(); diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index f1a1ee8e951..3451825d23a 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -1,4 +1,10 @@ import { randomUUID } from "node:crypto"; +import { + ErrorCodes, + type ErrorShape, + errorShape, + type SessionsPatchParams, +} from "../../packages/gateway-protocol/src/index.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { normalizeInheritedToolAllowlist, @@ -45,12 +51,6 @@ import { normalizeOptionalLowercaseString, normalizeOptionalString, } from "../shared/string-coerce.js"; -import { - ErrorCodes, - type ErrorShape, - errorShape, - type SessionsPatchParams, -} from "./protocol/index.js"; function invalid(message: string): { ok: false; error: ErrorShape } { return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, message) }; diff --git a/src/gateway/sessions-resolve-store.test.ts b/src/gateway/sessions-resolve-store.test.ts index 758e377cbb8..c16d92c949d 100644 --- a/src/gateway/sessions-resolve-store.test.ts +++ b/src/gateway/sessions-resolve-store.test.ts @@ -1,9 +1,9 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; +import { ErrorCodes } from "../../packages/gateway-protocol/src/index.js"; import { resolveStorePath, saveSessionStore } from "../config/sessions.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { withStateDirEnv } from "../test-helpers/state-dir-env.js"; -import { ErrorCodes } from "./protocol/index.js"; import { resolveSessionKeyFromResolveParams } from "./sessions-resolve.js"; describe("resolveSessionKeyFromResolveParams store canonicalization", () => { diff --git a/src/gateway/sessions-resolve.test.ts b/src/gateway/sessions-resolve.test.ts index 2e20dae7ad8..bfd30d5ac0f 100644 --- a/src/gateway/sessions-resolve.test.ts +++ b/src/gateway/sessions-resolve.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ErrorCodes } from "../../packages/gateway-protocol/src/index.js"; import type { SessionEntry } from "../config/sessions/types.js"; -import { ErrorCodes } from "./protocol/index.js"; const hoisted = vi.hoisted(() => ({ loadSessionStoreMock: vi.fn(), diff --git a/src/gateway/sessions-resolve.ts b/src/gateway/sessions-resolve.ts index 9736313befd..0b198a7c078 100644 --- a/src/gateway/sessions-resolve.ts +++ b/src/gateway/sessions-resolve.ts @@ -1,14 +1,14 @@ -import { loadSessionStore, updateSessionStore, type SessionEntry } from "../config/sessions.js"; -import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { resolveSessionIdMatchSelection } from "../sessions/session-id-resolution.js"; -import { parseSessionLabel } from "../sessions/session-label.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; import { ErrorCodes, type ErrorShape, errorShape, type SessionsResolveParams, -} from "./protocol/index.js"; +} from "../../packages/gateway-protocol/src/index.js"; +import { loadSessionStore, updateSessionStore, type SessionEntry } from "../config/sessions.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { resolveSessionIdMatchSelection } from "../sessions/session-id-resolution.js"; +import { parseSessionLabel } from "../sessions/session-label.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { filterAndSortSessionEntries, listSessionsFromStore, diff --git a/src/gateway/talk-agent-consult.ts b/src/gateway/talk-agent-consult.ts index a3eecea8ec0..744d609ee1c 100644 --- a/src/gateway/talk-agent-consult.ts +++ b/src/gateway/talk-agent-consult.ts @@ -1,7 +1,12 @@ import { randomUUID } from "node:crypto"; +import { + ErrorCodes, + errorShape, + type ConnectParams, + type ErrorShape, +} from "../../packages/gateway-protocol/src/index.js"; import { normalizeTalkSection } from "../config/talk.js"; import { buildRealtimeVoiceAgentConsultChatMessage } from "../talk/agent-consult-tool.js"; -import { ErrorCodes, errorShape, type ConnectParams, type ErrorShape } from "./protocol/index.js"; import { chatHandlers } from "./server-methods/chat.js"; import type { GatewayClient, diff --git a/src/gateway/test-helpers.e2e.ts b/src/gateway/test-helpers.e2e.ts index 4ea2ef2c9e4..9c774c91f5a 100644 --- a/src/gateway/test-helpers.e2e.ts +++ b/src/gateway/test-helpers.e2e.ts @@ -2,6 +2,7 @@ import { writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { WebSocket } from "ws"; +import { PROTOCOL_VERSION } from "../../packages/gateway-protocol/src/index.js"; import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js"; import { clearSessionStoreCacheForTest } from "../config/sessions/store.js"; import { @@ -21,7 +22,6 @@ import { } from "../utils/message-channel.js"; import { GatewayClient } from "./client.js"; import { buildDeviceAuthPayloadV3 } from "./device-auth.js"; -import { PROTOCOL_VERSION } from "./protocol/index.js"; import { startGatewayServer } from "./server.js"; export async function getFreeGatewayPort(): Promise { diff --git a/src/gateway/test-helpers.server.ts b/src/gateway/test-helpers.server.ts index 45668191c4d..eb00c2347c4 100644 --- a/src/gateway/test-helpers.server.ts +++ b/src/gateway/test-helpers.server.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, expect, vi } from "vitest"; import { WebSocket } from "ws"; import "./test-helpers.mocks.js"; +import { PROTOCOL_VERSION } from "../../packages/gateway-protocol/src/index.js"; import { parseConfigJson5, resetConfigRuntimeState } from "../config/config.js"; import { clearSessionStoreCacheForTest, @@ -39,7 +40,6 @@ import { captureEnv } from "../test-utils/env.js"; import { getDeterministicFreePortBlock } from "../test-utils/ports.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { buildDeviceAuthPayloadV3 } from "./device-auth.js"; -import { PROTOCOL_VERSION } from "./protocol/index.js"; import type { GatewayServerOptions } from "./server.js"; import { resetTestPluginRegistry } from "./test-helpers.plugin-registry.js"; import { diff --git a/src/hooks/internal-hooks.ts b/src/hooks/internal-hooks.ts index 568419e2bd8..47f9d9f1ab1 100644 --- a/src/hooks/internal-hooks.ts +++ b/src/hooks/internal-hooks.ts @@ -5,11 +5,11 @@ * like command processing, session lifecycle, etc. */ +import type { SessionsPatchParams } from "../../packages/gateway-protocol/src/schema.js"; import type { WorkspaceBootstrapFile } from "../agents/workspace.js"; import type { CliDeps } from "../cli/outbound-send-deps.js"; import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import type { SessionsPatchParams } from "../gateway/protocol/schema/types.js"; import { formatErrorMessage } from "../infra/errors.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveGlobalSingleton } from "../shared/global-singleton.js"; diff --git a/src/infra/exec-approval-channel-runtime.ts b/src/infra/exec-approval-channel-runtime.ts index 0c313eafc66..7d90c527235 100644 --- a/src/infra/exec-approval-channel-runtime.ts +++ b/src/infra/exec-approval-channel-runtime.ts @@ -1,8 +1,8 @@ +import { readConnectErrorDetailCode } from "../../packages/gateway-protocol/src/connect-error-details.js"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import { startGatewayClientWhenEventLoopReady } from "../gateway/client-start-readiness.js"; import type { GatewayClient, GatewayReconnectPausedInfo } from "../gateway/client.js"; import { createOperatorApprovalsGatewayClient } from "../gateway/operator-approvals-client.js"; -import { readConnectErrorDetailCode } from "../gateway/protocol/connect-error-details.js"; -import type { EventFrame } from "../gateway/protocol/index.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { formatErrorMessage } from "./errors.js"; import type { diff --git a/src/infra/watch-node.test.ts b/src/infra/watch-node.test.ts index 918a8c1cf3e..06644c98604 100644 --- a/src/infra/watch-node.test.ts +++ b/src/infra/watch-node.test.ts @@ -130,10 +130,15 @@ describe("watch-node script", () => { ]; expect(watchPaths).toEqual(runNodeWatchedPaths); expect(watchPaths).toContain("extensions"); + expect(watchPaths).toContain("packages/gateway-client/src"); + expect(watchPaths).toContain("packages/gateway-protocol/src"); expect(watchPaths).toContain("tsdown.config.ts"); expect(watchOptions.ignoreInitial).toBe(true); expect(watchOptions.ignored("src")).toBe(false); expect(watchOptions.ignored("src/infra")).toBe(false); + expect(watchOptions.ignored("packages/gateway-client/src/client.ts")).toBe(false); + expect(watchOptions.ignored("packages/gateway-client/src/client.test.ts")).toBe(true); + expect(watchOptions.ignored("packages/gateway-protocol/src/schema/cron.ts")).toBe(false); expect(watchOptions.ignored("extensions")).toBe(false); expect(watchOptions.ignored("extensions/voice-call")).toBe(false); expect(watchOptions.ignored("extensions/voice-call/dist")).toBe(true); diff --git a/src/mcp/channel-bridge.ts b/src/mcp/channel-bridge.ts index 3b23eae8a72..929a6ffee16 100644 --- a/src/mcp/channel-bridge.ts +++ b/src/mcp/channel-bridge.ts @@ -1,8 +1,8 @@ import { randomUUID } from "node:crypto"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { GatewayClient } from "../gateway/client.js"; -import type { EventFrame } from "../gateway/protocol/index.js"; import { extractFirstTextBlock } from "../shared/chat-message-content.js"; import { normalizeLowercaseStringOrEmpty, @@ -97,7 +97,7 @@ export class OpenClawChannelBridge { import("../gateway/client.js"), import("../gateway/client-start-readiness.js"), import("../gateway/method-scopes.js"), - import("../gateway/protocol/client-info.js"), + import("../../packages/gateway-protocol/src/client-info.js"), ]); const bootstrap = await resolveGatewayClientBootstrap({ config: this.cfg, diff --git a/src/node-host/runner.credentials.test.ts b/src/node-host/runner.credentials.test.ts index 9991b5f79de..e802c172338 100644 --- a/src/node-host/runner.credentials.test.ts +++ b/src/node-host/runner.credentials.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; +import { ConnectErrorDetailCodes } from "../../packages/gateway-protocol/src/connect-error-details.js"; import type { OpenClawConfig } from "../config/config.js"; -import { ConnectErrorDetailCodes } from "../gateway/protocol/connect-error-details.js"; import { withEnvAsync } from "../test-utils/env.js"; import { handleNodeHostReconnectPaused, diff --git a/src/node-host/runner.ts b/src/node-host/runner.ts index 1c730a329c0..d99edab2f07 100644 --- a/src/node-host/runner.ts +++ b/src/node-host/runner.ts @@ -1,10 +1,13 @@ import fs from "node:fs"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; +import { ConnectErrorDetailCodes } from "../../packages/gateway-protocol/src/connect-error-details.js"; import { getRuntimeConfig, type OpenClawConfig } from "../config/config.js"; import { startGatewayClientWhenEventLoopReady } from "../gateway/client-start-readiness.js"; import { GatewayClient, type GatewayReconnectPausedInfo } from "../gateway/client.js"; import { resolveGatewayConnectionAuth } from "../gateway/connection-auth.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; -import { ConnectErrorDetailCodes } from "../gateway/protocol/connect-error-details.js"; import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js"; import type { SkillBinTrustEntry } from "../infra/exec-approvals.js"; import { resolveExecutableFromPathEnv } from "../infra/executable-path.js"; diff --git a/src/plugin-sdk/gateway-runtime.ts b/src/plugin-sdk/gateway-runtime.ts index 6d7cebd7f48..b6ad7e6ab1b 100644 --- a/src/plugin-sdk/gateway-runtime.ts +++ b/src/plugin-sdk/gateway-runtime.ts @@ -38,6 +38,6 @@ export { createOperatorApprovalsGatewayClient, withOperatorApprovalsGatewayClient, } from "../gateway/operator-approvals-client.js"; -export { ErrorCodes, errorShape } from "../gateway/protocol/index.js"; -export type { EventFrame } from "../gateway/protocol/index.js"; +export { ErrorCodes, errorShape } from "../../packages/gateway-protocol/src/index.js"; +export type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; export type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js"; diff --git a/src/plugins/cli-gateway-nodes-runtime.ts b/src/plugins/cli-gateway-nodes-runtime.ts index b98273ccabb..889e4608c4f 100644 --- a/src/plugins/cli-gateway-nodes-runtime.ts +++ b/src/plugins/cli-gateway-nodes-runtime.ts @@ -1,6 +1,9 @@ import { randomUUID } from "node:crypto"; +import { + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; import { callGateway } from "../gateway/call.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; import type { PluginRuntime } from "./runtime/types.js"; export function createPluginCliGatewayNodesRuntime(): PluginRuntime["nodes"] { diff --git a/src/plugins/contracts/host-hooks.contract.test.ts b/src/plugins/contracts/host-hooks.contract.test.ts index 71529658a69..f337a0b9baf 100644 --- a/src/plugins/contracts/host-hooks.contract.test.ts +++ b/src/plugins/contracts/host-hooks.contract.test.ts @@ -5,12 +5,12 @@ import { registerTestPlugin, } from "openclaw/plugin-sdk/plugin-test-contracts"; import { afterEach, describe, expect, it } from "vitest"; -import { loadSessionStore, updateSessionStore, type SessionEntry } from "../../config/sessions.js"; -import { APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE } from "../../gateway/operator-scopes.js"; import { validatePluginsUiDescriptorsParams, validateSessionsPluginPatchParams, -} from "../../gateway/protocol/index.js"; +} from "../../../packages/gateway-protocol/src/index.js"; +import { loadSessionStore, updateSessionStore, type SessionEntry } from "../../config/sessions.js"; +import { APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE } from "../../gateway/operator-scopes.js"; import { buildGatewaySessionRow } from "../../gateway/session-utils.js"; import { withTempConfig } from "../../gateway/test-temp-config.js"; import { emitAgentEvent, resetAgentEventsForTest } from "../../infra/agent-events.js"; diff --git a/src/plugins/contracts/scheduled-turns.contract.test.ts b/src/plugins/contracts/scheduled-turns.contract.test.ts index f6af9ca1a49..5913b25c6de 100644 --- a/src/plugins/contracts/scheduled-turns.contract.test.ts +++ b/src/plugins/contracts/scheduled-turns.contract.test.ts @@ -264,7 +264,8 @@ describe("plugin scheduled turns", () => { }); it("builds payloads accepted by the real cron.add protocol validator", async () => { - const { validateCronAddParams } = await import("../../gateway/protocol/index.js"); + const { validateCronAddParams } = + await import("../../../packages/gateway-protocol/src/index.js"); workflowMocks.cronAdd.mockImplementation(async (body: unknown) => { expect(validateCronAddParams(body)).toBe(true); expect((body as { delivery?: unknown }).delivery).toEqual({ diff --git a/src/scripts/ci-changed-scope.test.ts b/src/scripts/ci-changed-scope.test.ts index 037ba730d90..494aefaf48a 100644 --- a/src/scripts/ci-changed-scope.test.ts +++ b/src/scripts/ci-changed-scope.test.ts @@ -450,7 +450,16 @@ describe("detectChangedScope", () => { runChangedSmoke: true, runControlUiI18n: false, }); - expect(detectChangedScope(["src/gateway/protocol/messages.ts"])).toEqual({ + expect(detectChangedScope(["packages/gateway-protocol/src/schema/messages.ts"])).toEqual({ + runNode: true, + runMacos: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: true, + runControlUiI18n: false, + }); + expect(detectChangedScope(["packages/gateway-client/src/client.ts"])).toEqual({ runNode: true, runMacos: false, runAndroid: false, @@ -512,6 +521,10 @@ describe("detectChangedScope", () => { runFastInstallSmoke: true, runFullInstallSmoke: false, }); + expect(detectInstallSmokeScope(["packages/gateway-client/src/client.ts"])).toEqual({ + runFastInstallSmoke: true, + runFullInstallSmoke: false, + }); expect(detectInstallSmokeScope([bundledPluginFile("matrix", "index.ts")])).toEqual({ runFastInstallSmoke: false, runFullInstallSmoke: false, diff --git a/src/secrets/exec-secret-ref-id-parity.test.ts b/src/secrets/exec-secret-ref-id-parity.test.ts index ee5a8b73da2..42050aa687b 100644 --- a/src/secrets/exec-secret-ref-id-parity.test.ts +++ b/src/secrets/exec-secret-ref-id-parity.test.ts @@ -1,7 +1,7 @@ import { Compile } from "typebox/compile"; import { describe, expect, it } from "vitest"; +import { SecretRefSchema as GatewaySecretRefSchema } from "../../packages/gateway-protocol/src/schema.js"; import { validateConfigObjectRaw } from "../config/validation.js"; -import { SecretRefSchema as GatewaySecretRefSchema } from "../gateway/protocol/schema/primitives.js"; import { buildSecretInputSchema } from "../plugin-sdk/secret-input-schema.js"; import { INVALID_FILE_SECRET_REF_IDS, diff --git a/src/tui/commands.ts b/src/tui/commands.ts index 76964d6e437..ced0e59e8c8 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -1,8 +1,8 @@ import type { SlashCommand } from "@earendil-works/pi-tui"; +import type { CommandEntry } from "../../packages/gateway-protocol/src/index.js"; import { listChatCommands, listChatCommandsForConfig } from "../auto-reply/commands-registry.js"; import { formatThinkingLevels, listThinkingLevelLabels } from "../auto-reply/thinking.js"; import type { OpenClawConfig } from "../config/types.js"; -import type { CommandEntry } from "../gateway/protocol/index.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; const VERBOSE_LEVELS = ["on", "off"]; diff --git a/src/tui/embedded-backend.ts b/src/tui/embedded-backend.ts index 3e75e3d7cb5..75160590eff 100644 --- a/src/tui/embedded-backend.ts +++ b/src/tui/embedded-backend.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import type { SessionsPatchResult } from "../../packages/gateway-protocol/src/index.js"; import { agentCommandFromIngress } from "../agents/agent-command.js"; import { resolveSessionAgentId } from "../agents/agent-scope.js"; import { ensureContextWindowCacheLoaded } from "../agents/context.js"; @@ -24,7 +25,6 @@ import { resolveMergedAssistantText, shouldSuppressAssistantEventForLiveChat, } from "../gateway/live-chat-projector.js"; -import type { SessionsPatchResult } from "../gateway/protocol/index.js"; import { getMaxChatHistoryMessagesBytes } from "../gateway/server-constants.js"; import { injectTimestamp, diff --git a/src/tui/gateway-chat.ts b/src/tui/gateway-chat.ts index 8421cb0bdd4..e17fff0252a 100644 --- a/src/tui/gateway-chat.ts +++ b/src/tui/gateway-chat.ts @@ -1,4 +1,20 @@ import { randomUUID } from "node:crypto"; +import { + GATEWAY_CLIENT_CAPS, + GATEWAY_CLIENT_MODES, + GATEWAY_CLIENT_NAMES, +} from "../../packages/gateway-protocol/src/client-info.js"; +import { + type HelloOk, + MIN_CLIENT_PROTOCOL_VERSION, + PROTOCOL_VERSION, + type CommandEntry, + type CommandsListParams, + type CommandsListResult, + type SessionsListParams, + type SessionsPatchResult, + type SessionsPatchParams, +} from "../../packages/gateway-protocol/src/index.js"; import { getRuntimeConfig } from "../config/config.js"; import { assertExplicitGatewayAuthModeWhenBothConfigured } from "../gateway/auth-mode-policy.js"; import { resolveGatewayInteractiveSurfaceAuth } from "../gateway/auth-surface-resolution.js"; @@ -10,22 +26,6 @@ import { import { startGatewayClientWhenEventLoopReady } from "../gateway/client-start-readiness.js"; import { GatewayClient, GatewayClientRequestError } from "../gateway/client.js"; import { isLoopbackHost } from "../gateway/net.js"; -import { - GATEWAY_CLIENT_CAPS, - GATEWAY_CLIENT_MODES, - GATEWAY_CLIENT_NAMES, -} from "../gateway/protocol/client-info.js"; -import { - type HelloOk, - MIN_CLIENT_PROTOCOL_VERSION, - PROTOCOL_VERSION, - type CommandEntry, - type CommandsListParams, - type CommandsListResult, - type SessionsListParams, - type SessionsPatchResult, - type SessionsPatchParams, -} from "../gateway/protocol/index.js"; import { formatErrorMessage } from "../infra/errors.js"; import { VERSION } from "../version.js"; import { TUI_SETUP_AUTH_SOURCE_CONFIG, TUI_SETUP_AUTH_SOURCE_ENV } from "./setup-launch-env.js"; diff --git a/src/tui/tui-backend.ts b/src/tui/tui-backend.ts index 5e445964a31..dff41c86c3e 100644 --- a/src/tui/tui-backend.ts +++ b/src/tui/tui-backend.ts @@ -4,7 +4,7 @@ import type { SessionsListParams, SessionsPatchParams, SessionsPatchResult, -} from "../gateway/protocol/index.js"; +} from "../../packages/gateway-protocol/src/index.js"; import type { ResponseUsageMode, SessionInfo, SessionScope } from "./tui-types.js"; export type ChatSendOptions = { diff --git a/src/tui/tui-command-handlers.ts b/src/tui/tui-command-handlers.ts index cc647c0edf8..c4ab88a63b6 100644 --- a/src/tui/tui-command-handlers.ts +++ b/src/tui/tui-command-handlers.ts @@ -1,5 +1,6 @@ import { randomUUID } from "node:crypto"; import type { Component, SelectItem, TUI } from "@earendil-works/pi-tui"; +import type { SessionsPatchResult } from "../../packages/gateway-protocol/src/index.js"; import { modelKey } from "../agents/model-ref-shared.js"; import { normalizeGroupActivation } from "../auto-reply/group-activation.js"; import { @@ -8,7 +9,6 @@ import { resolveResponseUsageMode, } from "../auto-reply/thinking.js"; import { isChatStopCommandText } from "../gateway/chat-abort.js"; -import type { SessionsPatchResult } from "../gateway/protocol/index.js"; import { formatRelativeTimestamp } from "../infra/format-time/format-relative.ts"; import { normalizeAgentId } from "../routing/session-key.js"; import { helpText, parseCommand } from "./commands.js"; diff --git a/src/tui/tui-session-actions.ts b/src/tui/tui-session-actions.ts index d3dd94adcda..9ebf6257615 100644 --- a/src/tui/tui-session-actions.ts +++ b/src/tui/tui-session-actions.ts @@ -1,6 +1,6 @@ import type { TUI } from "@earendil-works/pi-tui"; +import type { SessionsPatchResult } from "../../packages/gateway-protocol/src/index.js"; import { resolveSessionInfoModelSelection } from "../agents/model-selection-display.js"; -import type { SessionsPatchResult } from "../gateway/protocol/index.js"; import { normalizeAgentId, normalizeMainKey, diff --git a/src/tui/tui.ts b/src/tui/tui.ts index b9d16cae7dd..cb433df9202 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -12,10 +12,10 @@ import { Text, TUI, } from "@earendil-works/pi-tui"; +import type { CommandEntry } from "../../packages/gateway-protocol/src/index.js"; import { resolveAgentIdByWorkspacePath, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { getRuntimeConfig, type OpenClawConfig } from "../config/config.js"; import { isChatStopCommandText } from "../gateway/chat-abort.js"; -import type { CommandEntry } from "../gateway/protocol/index.js"; import { registerUncaughtExceptionHandler } from "../infra/unhandled-rejections.js"; import { setConsoleSubsystemFilter } from "../logging/console.js"; import { loggingState } from "../logging/state.js"; diff --git a/src/utils/message-channel.ts b/src/utils/message-channel.ts index 3a6839ff445..ebf21ccf3de 100644 --- a/src/utils/message-channel.ts +++ b/src/utils/message-channel.ts @@ -1,6 +1,3 @@ -import { listBundledChannelCatalogEntries } from "../channels/bundled-channel-catalog-read.js"; -import { getChatChannelMeta } from "../channels/chat-meta.js"; -import { getRegisteredChannelPluginMeta, normalizeChatChannelId } from "../channels/registry.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, @@ -8,7 +5,10 @@ import { type GatewayClientName, normalizeGatewayClientMode, normalizeGatewayClientName, -} from "../gateway/protocol/client-info.js"; +} from "../../packages/gateway-protocol/src/client-info.js"; +import { listBundledChannelCatalogEntries } from "../channels/bundled-channel-catalog-read.js"; +import { getChatChannelMeta } from "../channels/chat-meta.js"; +import { getRegisteredChannelPluginMeta, normalizeChatChannelId } from "../channels/registry.js"; export { isDeliverableMessageChannel, isGatewayMessageChannel, diff --git a/test/vitest/vitest.gateway-client.config.ts b/test/vitest/vitest.gateway-client.config.ts index 44c30f6047e..45ad82a0c56 100644 --- a/test/vitest/vitest.gateway-client.config.ts +++ b/test/vitest/vitest.gateway-client.config.ts @@ -3,14 +3,13 @@ import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; export function createGatewayClientVitestConfig(env?: Record) { return createScopedVitestConfig( [ - "src/gateway/protocol/**/*.test.ts", + "packages/gateway-protocol/src/**/*.test.ts", "src/gateway/**/*client*.test.ts", "src/gateway/**/*reconnect*.test.ts", "src/gateway/**/*android-node*.test.ts", "src/gateway/**/*gateway-cli-backend*.test.ts", ], { - dir: "src/gateway", env, exclude: ["src/gateway/**/*server*.test.ts"], name: "gateway-client", diff --git a/test/vitest/vitest.gateway-core.config.ts b/test/vitest/vitest.gateway-core.config.ts index 3de855f5df2..e831ef93124 100644 --- a/test/vitest/vitest.gateway-core.config.ts +++ b/test/vitest/vitest.gateway-core.config.ts @@ -2,7 +2,7 @@ import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; const nonCoreGatewayTestExclude = [ "src/gateway/server-methods/**/*.test.ts", - "src/gateway/protocol/**/*.test.ts", + "packages/gateway-protocol/src/**/*.test.ts", "src/gateway/**/*client*.test.ts", "src/gateway/**/*reconnect*.test.ts", "src/gateway/**/*android-node*.test.ts", diff --git a/test/vitest/vitest.shared.config.ts b/test/vitest/vitest.shared.config.ts index 5ad198fd0e2..8f14b7b7eb2 100644 --- a/test/vitest/vitest.shared.config.ts +++ b/test/vitest/vitest.shared.config.ts @@ -173,6 +173,42 @@ export const sharedVitestConfig = { find: "@openclaw/whatsapp/api.js", replacement: path.join(repoRoot, "extensions", "whatsapp", "api.ts"), }, + { + find: "@openclaw/gateway-protocol/client-info", + replacement: path.join(repoRoot, "packages", "gateway-protocol", "src", "client-info.ts"), + }, + { + find: "@openclaw/gateway-protocol/connect-error-details", + replacement: path.join( + repoRoot, + "packages", + "gateway-protocol", + "src", + "connect-error-details.ts", + ), + }, + { + find: "@openclaw/gateway-protocol/schema", + replacement: path.join(repoRoot, "packages", "gateway-protocol", "src", "schema.ts"), + }, + { + find: "@openclaw/gateway-protocol/startup-unavailable", + replacement: path.join( + repoRoot, + "packages", + "gateway-protocol", + "src", + "startup-unavailable.ts", + ), + }, + { + find: "@openclaw/gateway-protocol/version", + replacement: path.join(repoRoot, "packages", "gateway-protocol", "src", "version.ts"), + }, + { + find: "@openclaw/gateway-protocol", + replacement: path.join(repoRoot, "packages", "gateway-protocol", "src", "index.ts"), + }, ...sourcePluginSdkSubpaths.map((subpath) => ({ find: `openclaw/plugin-sdk/${subpath}`, replacement: path.join(repoRoot, "src", "plugin-sdk", `${subpath}.ts`), @@ -403,7 +439,7 @@ export const sharedVitestConfig = { "src/webchat/**", "src/gateway/server.ts", "src/gateway/client.ts", - "src/gateway/protocol/**", + "packages/gateway-protocol/src/**", "src/infra/tailscale.ts", ], }, diff --git a/test/vitest/vitest.unit-support.config.ts b/test/vitest/vitest.unit-support.config.ts index 524b54383d7..cba4deedf3c 100644 --- a/test/vitest/vitest.unit-support.config.ts +++ b/test/vitest/vitest.unit-support.config.ts @@ -3,5 +3,10 @@ import { createUnitVitestConfigWithOptions } from "./vitest.unit.config.ts"; export default createUnitVitestConfigWithOptions(process.env, { name: "unit-support", includePatterns: ["packages/**/*.test.ts"], + extraExcludePatterns: [ + // The gateway-protocol package rides with gateway-client because the client + // package owns the browser/runtime protocol compatibility lane. + "packages/gateway-protocol/src/**/*.test.ts", + ], passWithNoTests: true, }); diff --git a/tsconfig.json b/tsconfig.json index 9e35ca92e9d..00e0c60c2fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,21 @@ "openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.ts"], "@openclaw/agent-core": ["./packages/agent-core/src/index.ts"], "@openclaw/agent-core/*": ["./packages/agent-core/src/*"], + "@openclaw/gateway-client": ["./packages/gateway-client/src/index.ts"], + "@openclaw/gateway-client/*": ["./packages/gateway-client/src/*"], + "@openclaw/gateway-protocol": ["./packages/gateway-protocol/src/index.ts"], + "@openclaw/gateway-protocol/client-info": [ + "./packages/gateway-protocol/src/client-info.ts" + ], + "@openclaw/gateway-protocol/connect-error-details": [ + "./packages/gateway-protocol/src/connect-error-details.ts" + ], + "@openclaw/gateway-protocol/schema": ["./packages/gateway-protocol/src/schema.ts"], + "@openclaw/gateway-protocol/startup-unavailable": [ + "./packages/gateway-protocol/src/startup-unavailable.ts" + ], + "@openclaw/gateway-protocol/version": ["./packages/gateway-protocol/src/version.ts"], + "@openclaw/gateway-protocol/*": ["./packages/gateway-protocol/src/*"], "@openclaw/sdk": ["./packages/sdk/src/index.ts"], "@openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"], "@openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.ts"], diff --git a/tsdown.config.ts b/tsdown.config.ts index 64d4f226d7c..02df9f98dcc 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -142,6 +142,17 @@ function nodeBuildConfig(config: UserConfig): UserConfig { }; } +function nodeWorkspacePackageBuildConfig(config: UserConfig): UserConfig { + return { + ...config, + env, + format: "esm", + platform: "node", + sourcemap: OUTPUT_SOURCE_MAPS, + inputOptions: buildInputOptions, + }; +} + const bundledPluginBuildEntries = collectBundledPluginBuildEntries(); const shouldBuildPrivateQaEntries = process.env.OPENCLAW_BUILD_PRIVATE_QA === "1"; const productionPluginSdkEntrypoints = shouldBuildPrivateQaEntries @@ -296,7 +307,7 @@ function buildDockerE2eHarnessEntries(): Record { "config/config": "src/config/config.ts", "crestodian/crestodian": "src/crestodian/crestodian.ts", "crestodian/rescue-message": "src/crestodian/rescue-message.ts", - "gateway/protocol/index": "src/gateway/protocol/index.ts", + "gateway/protocol/index": "packages/gateway-protocol/src/index.ts", "infra/errors": "src/infra/errors.ts", "infra/ws": "src/infra/ws.ts", "plugin-sdk/provider-onboard": "src/plugin-sdk/provider-onboard.ts", @@ -337,6 +348,29 @@ function buildAgentCoreDistEntries(): Record { }; } +function buildGatewayProtocolDistEntries(): Record { + return { + // Package exports resolve from packages/gateway-protocol/dist, while the + // root build still emits dist/gateway/protocol/index for Docker harnesses. + index: "packages/gateway-protocol/src/index.ts", + "client-info": "packages/gateway-protocol/src/client-info.ts", + "connect-error-details": "packages/gateway-protocol/src/connect-error-details.ts", + schema: "packages/gateway-protocol/src/schema.ts", + "startup-unavailable": "packages/gateway-protocol/src/startup-unavailable.ts", + version: "packages/gateway-protocol/src/version.ts", + }; +} + +function buildGatewayClientDistEntries(): Record { + return { + // Keep package entrypoints explicit so package.json exports and root build + // config cannot drift when client internals are split again. + index: "packages/gateway-client/src/index.ts", + readiness: "packages/gateway-client/src/readiness.ts", + timeouts: "packages/gateway-client/src/timeouts.ts", + }; +} + function shouldExternalizeAgentCoreDependency(id: string): boolean { return ( id === "ignore" || @@ -349,6 +383,19 @@ function shouldExternalizeAgentCoreDependency(id: string): boolean { ); } +function shouldExternalizeGatewayProtocolDependency(id: string): boolean { + return id === "typebox" || id.startsWith("typebox/"); +} + +function shouldExternalizeGatewayClientDependency(id: string): boolean { + return ( + id === "ws" || + id.startsWith("ws/") || + id === "@openclaw/gateway-protocol" || + id.startsWith("@openclaw/gateway-protocol/") + ); +} + const coreDistEntries = buildCoreDistEntries(); const dockerE2eHarnessEntries = buildDockerE2eHarnessEntries(); const rootBundledPluginBuildEntries = bundledPluginBuildEntries.filter( @@ -391,6 +438,24 @@ export default defineConfig([ neverBundle: shouldExternalizeAgentCoreDependency, }, }), + nodeWorkspacePackageBuildConfig({ + clean: true, + dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined, + entry: buildGatewayProtocolDistEntries(), + outDir: "packages/gateway-protocol/dist", + deps: { + neverBundle: shouldExternalizeGatewayProtocolDependency, + }, + }), + nodeWorkspacePackageBuildConfig({ + clean: true, + dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined, + entry: buildGatewayClientDistEntries(), + outDir: "packages/gateway-client/dist", + deps: { + neverBundle: shouldExternalizeGatewayClientDependency, + }, + }), nodeBuildConfig({ // Build core entrypoints, plugin-sdk subpaths, bundled plugin entrypoints, // and bundled hooks in one graph so runtime singletons are emitted once. diff --git a/ui/src/test-helpers/control-ui-e2e.ts b/ui/src/test-helpers/control-ui-e2e.ts index 41b1426b05a..2ddf7bb05ed 100644 --- a/ui/src/test-helpers/control-ui-e2e.ts +++ b/ui/src/test-helpers/control-ui-e2e.ts @@ -5,8 +5,8 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import type { Page } from "playwright"; import { createServer, type ViteDevServer } from "vite"; +import { PROTOCOL_VERSION } from "../../../packages/gateway-protocol/src/version.js"; import { CONTROL_UI_BOOTSTRAP_CONFIG_PATH } from "../../../src/gateway/control-ui-contract.js"; -import { PROTOCOL_VERSION } from "../../../src/gateway/protocol/version.js"; const require = createRequire(import.meta.url); const json5EsmPath = require.resolve("json5/dist/index.mjs"); diff --git a/ui/src/ui/app-gateway.node.test.ts b/ui/src/ui/app-gateway.node.test.ts index f62a8b95675..42fcc87c1c6 100644 --- a/ui/src/ui/app-gateway.node.test.ts +++ b/ui/src/ui/app-gateway.node.test.ts @@ -1,7 +1,7 @@ // @vitest-environment node import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ConnectErrorDetailCodes } from "../../../packages/gateway-protocol/src/connect-error-details.js"; import { GATEWAY_EVENT_UPDATE_AVAILABLE } from "../../../src/gateway/events.js"; -import { ConnectErrorDetailCodes } from "../../../src/gateway/protocol/connect-error-details.js"; import type { ActivityEntry } from "./activity-model.ts"; import { connectGateway, resolveControlUiClientVersion } from "./app-gateway.ts"; import type { GatewayHelloOk } from "./gateway.ts"; diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index 06ed6c36576..1e520d12616 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -1,8 +1,8 @@ +import { ConnectErrorDetailCodes } from "../../../packages/gateway-protocol/src/connect-error-details.js"; import { GATEWAY_EVENT_UPDATE_AVAILABLE, type GatewayUpdateAvailableEventPayload, } from "../../../src/gateway/events.js"; -import { ConnectErrorDetailCodes } from "../../../src/gateway/protocol/connect-error-details.js"; import { clearPendingQueueItemsForRun, createChatSessionsLoadOverrides, diff --git a/ui/src/ui/chat/slash-commands.browser-import.test.ts b/ui/src/ui/chat/slash-commands.browser-import.test.ts index 8da1b47a13d..1ad477a0d77 100644 --- a/ui/src/ui/chat/slash-commands.browser-import.test.ts +++ b/ui/src/ui/chat/slash-commands.browser-import.test.ts @@ -5,11 +5,15 @@ import { describe, expect, it } from "vitest"; type SlashCommandsModule = typeof import("./slash-commands.js"); const browserImportPath: string = "./slash-commands.ts?browser-import"; -function importLines(source: string): string[] { - return source - .split(/\r?\n/u) - .filter((line) => line.startsWith("import ")) - .map((line) => line.trim()); +function importDeclarations(source: string): string[] { + return (source.match(/^import[\s\S]*?;$/gmu) ?? []).map((declaration) => + declaration + .replace(/\s+/gu, " ") + .replace(/\{\s+/gu, "{ ") + .replace(/,\s*\}/gu, " }") + .replace(/\s+\}/gu, " }") + .trim(), + ); } describe("slash command browser import", () => { @@ -55,24 +59,24 @@ describe("slash command browser import", () => { argOptions: undefined, tier: "essential", }); - expect(importLines(slashCommands)).toEqual([ + expect(importDeclarations(slashCommands)).toEqual([ + 'import type { CommandEntry, CommandsListResult } from "../../../../packages/gateway-protocol/src/index.js";', 'import { buildBuiltinChatCommands } from "../../../../src/auto-reply/commands-registry.shared.js";', - 'import type { CommandEntry, CommandsListResult } from "../../../../src/gateway/protocol/index.js";', 'import type { GatewayBrowserClient } from "../gateway.ts";', 'import type { IconName } from "../icons.ts";', 'import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";', ]); - expect(importLines(sharedRegistry)).toEqual([ + expect(importDeclarations(sharedRegistry)).toEqual([ 'import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";', 'import { normalizeStringEntries } from "../shared/string-normalization.js";', 'import { COMMAND_ARG_FORMATTERS } from "./commands-args.js";', - "import type {", + 'import type { ChatCommandDefinition, CommandArgChoiceContext, CommandCategory, CommandScope, CommandTier } from "./commands-registry.types.js";', 'import { BASE_THINKING_LEVELS, type ThinkLevel } from "./thinking.shared.js";', ]); - expect(importLines(serverRegistry)).toEqual([ + expect(importDeclarations(serverRegistry)).toEqual([ 'import { listLoadedChannelPlugins } from "../channels/plugins/registry-loaded.js";', 'import { getActivePluginChannelRegistryVersionFromState } from "../plugins/runtime-channel-state.js";', - "import {", + 'import { assertCommandRegistry, buildBuiltinChatCommands, defineChatCommand } from "./commands-registry.shared.js";', 'import type { ChatCommandDefinition } from "./commands-registry.types.js";', 'import { listThinkingLevels } from "./thinking.js";', ]); diff --git a/ui/src/ui/chat/slash-commands.ts b/ui/src/ui/chat/slash-commands.ts index 97785dd0f50..e84a69b8dee 100644 --- a/ui/src/ui/chat/slash-commands.ts +++ b/ui/src/ui/chat/slash-commands.ts @@ -1,5 +1,8 @@ +import type { + CommandEntry, + CommandsListResult, +} from "../../../../packages/gateway-protocol/src/index.js"; import { buildBuiltinChatCommands } from "../../../../src/auto-reply/commands-registry.shared.js"; -import type { CommandEntry, CommandsListResult } from "../../../../src/gateway/protocol/index.js"; import type { GatewayBrowserClient } from "../gateway.ts"; import type { IconName } from "../icons.ts"; import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts"; diff --git a/ui/src/ui/connect-error.test.ts b/ui/src/ui/connect-error.test.ts index d57b8079d4a..0e4cf900d6a 100644 --- a/ui/src/ui/connect-error.test.ts +++ b/ui/src/ui/connect-error.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ConnectErrorDetailCodes } from "../../../src/gateway/protocol/connect-error-details.js"; +import { ConnectErrorDetailCodes } from "../../../packages/gateway-protocol/src/connect-error-details.js"; import { formatConnectError } from "./connect-error.ts"; describe("formatConnectError", () => { diff --git a/ui/src/ui/connect-error.ts b/ui/src/ui/connect-error.ts index ffdfd04e7a9..6d6f03ce461 100644 --- a/ui/src/ui/connect-error.ts +++ b/ui/src/ui/connect-error.ts @@ -4,7 +4,7 @@ import { formatConnectPairingRequiredMessage, readConnectPairingRequiredMessage, readPairingConnectErrorDetails, -} from "../../../src/gateway/protocol/connect-error-details.js"; +} from "../../../packages/gateway-protocol/src/connect-error-details.js"; import { resolveGatewayErrorDetailCode } from "./gateway.ts"; import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts"; diff --git a/ui/src/ui/controllers/scope-errors.ts b/ui/src/ui/controllers/scope-errors.ts index 3f8ad112cc3..33df80fe71f 100644 --- a/ui/src/ui/controllers/scope-errors.ts +++ b/ui/src/ui/controllers/scope-errors.ts @@ -1,4 +1,4 @@ -import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connect-error-details.js"; +import { ConnectErrorDetailCodes } from "../../../../packages/gateway-protocol/src/connect-error-details.js"; import { GatewayRequestError, resolveGatewayErrorDetailCode } from "../gateway.ts"; export function isMissingOperatorReadScopeError(err: unknown): boolean { diff --git a/ui/src/ui/gateway.node.test.ts b/ui/src/ui/gateway.node.test.ts index 334d8fc161e..bcaec860e53 100644 --- a/ui/src/ui/gateway.node.test.ts +++ b/ui/src/ui/gateway.node.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION, -} from "../../../src/gateway/protocol/version.js"; +} from "../../../packages/gateway-protocol/src/version.js"; import { createStorageMock } from "../test-helpers/storage.ts"; import { loadDeviceAuthToken, storeDeviceAuthToken } from "./device-auth.ts"; import type { DeviceIdentity } from "./device-identity.ts"; diff --git a/ui/src/ui/gateway.ts b/ui/src/ui/gateway.ts index 6b21e7e449d..c81f5d4e35e 100644 --- a/ui/src/ui/gateway.ts +++ b/ui/src/ui/gateway.ts @@ -1,25 +1,25 @@ -import { buildDeviceAuthPayload } from "../../../src/gateway/device-auth.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, type GatewayClientMode, type GatewayClientName, -} from "../../../src/gateway/protocol/client-info.js"; +} from "../../../packages/gateway-protocol/src/client-info.js"; import { ConnectErrorDetailCodes, formatConnectErrorMessage, readConnectErrorRecoveryAdvice, readConnectErrorDetailCode, readPairingConnectErrorDetails, -} from "../../../src/gateway/protocol/connect-error-details.js"; +} from "../../../packages/gateway-protocol/src/connect-error-details.js"; import { isRetryableGatewayStartupUnavailableError, resolveGatewayStartupRetryAfterMs, -} from "../../../src/gateway/protocol/startup-unavailable.js"; +} from "../../../packages/gateway-protocol/src/startup-unavailable.js"; import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION, -} from "../../../src/gateway/protocol/version.js"; +} from "../../../packages/gateway-protocol/src/version.js"; +import { buildDeviceAuthPayload } from "../../../src/gateway/device-auth.js"; import { clearDeviceAuthToken, loadDeviceAuthToken, storeDeviceAuthToken } from "./device-auth.ts"; import { loadOrCreateDeviceIdentity, signDevicePayload } from "./device-identity.ts"; import { generateUUID } from "./uuid.ts"; diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 6c00d03737d..6b15eea6e4b 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -787,19 +787,19 @@ export type ModelCatalogEntry = { }; export type ToolCatalogProfile = - import("../../../src/gateway/protocol/schema/types.js").ToolCatalogProfile; + import("../../../packages/gateway-protocol/src/schema.js").ToolCatalogProfile; export type ToolCatalogEntry = - import("../../../src/gateway/protocol/schema/types.js").ToolCatalogEntry; + import("../../../packages/gateway-protocol/src/schema.js").ToolCatalogEntry; export type ToolCatalogGroup = - import("../../../src/gateway/protocol/schema/types.js").ToolCatalogGroup; + import("../../../packages/gateway-protocol/src/schema.js").ToolCatalogGroup; export type ToolsCatalogResult = - import("../../../src/gateway/protocol/schema/types.js").ToolsCatalogResult; + import("../../../packages/gateway-protocol/src/schema.js").ToolsCatalogResult; export type ToolsEffectiveEntry = - import("../../../src/gateway/protocol/schema/types.js").ToolsEffectiveEntry; + import("../../../packages/gateway-protocol/src/schema.js").ToolsEffectiveEntry; export type ToolsEffectiveGroup = - import("../../../src/gateway/protocol/schema/types.js").ToolsEffectiveGroup; + import("../../../packages/gateway-protocol/src/schema.js").ToolsEffectiveGroup; export type ToolsEffectiveResult = - import("../../../src/gateway/protocol/schema/types.js").ToolsEffectiveResult; + import("../../../packages/gateway-protocol/src/schema.js").ToolsEffectiveResult; export type ModelAuthExpiry = import("../../../src/gateway/server-methods/models-auth-status.js").ModelAuthExpiry; diff --git a/ui/src/ui/views/login-gate.test.ts b/ui/src/ui/views/login-gate.test.ts index 647f583579a..fac4dc44439 100644 --- a/ui/src/ui/views/login-gate.test.ts +++ b/ui/src/ui/views/login-gate.test.ts @@ -2,7 +2,7 @@ import { render } from "lit"; import { beforeEach, describe, expect, it } from "vitest"; -import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connect-error-details.js"; +import { ConnectErrorDetailCodes } from "../../../../packages/gateway-protocol/src/connect-error-details.js"; import { i18n } from "../../i18n/index.ts"; import type { AppViewState } from "../app-view-state.ts"; import { renderLoginGate, resolveLoginFailureFeedback } from "./login-gate.ts"; diff --git a/ui/src/ui/views/login-gate.ts b/ui/src/ui/views/login-gate.ts index 0bf438c5151..582201f244a 100644 --- a/ui/src/ui/views/login-gate.ts +++ b/ui/src/ui/views/login-gate.ts @@ -1,5 +1,5 @@ import { html } from "lit"; -import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connect-error-details.js"; +import { ConnectErrorDetailCodes } from "../../../../packages/gateway-protocol/src/connect-error-details.js"; import { t } from "../../i18n/index.ts"; import type { AppViewState } from "../app-view-state.ts"; import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "../external-link.ts"; diff --git a/ui/src/ui/views/overview-hints.ts b/ui/src/ui/views/overview-hints.ts index d3f759c30da..32d9ae6b7f5 100644 --- a/ui/src/ui/views/overview-hints.ts +++ b/ui/src/ui/views/overview-hints.ts @@ -1,7 +1,7 @@ import { ConnectErrorDetailCodes, readConnectPairingRequiredMessage, -} from "../../../../src/gateway/protocol/connect-error-details.js"; +} from "../../../../packages/gateway-protocol/src/connect-error-details.js"; import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts"; const AUTH_REQUIRED_CODES = new Set([ diff --git a/ui/src/ui/views/overview.node.test.ts b/ui/src/ui/views/overview.node.test.ts index a806ec56727..7f1ee3a67f5 100644 --- a/ui/src/ui/views/overview.node.test.ts +++ b/ui/src/ui/views/overview.node.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node import { describe, expect, it } from "vitest"; -import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connect-error-details.js"; +import { ConnectErrorDetailCodes } from "../../../../packages/gateway-protocol/src/connect-error-details.js"; import { resolveAuthHintKind, resolvePairingHint,