chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions
+27 -9
View File
@@ -20,10 +20,16 @@ export type AssistantIdentity = {
};
function coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined {
if (typeof value !== "string") return undefined;
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) return undefined;
if (trimmed.length <= maxLength) return trimmed;
if (!trimmed) {
return undefined;
}
if (trimmed.length <= maxLength) {
return trimmed;
}
return trimmed.slice(0, maxLength);
}
@@ -32,17 +38,29 @@ function isAvatarUrl(value: string): boolean {
}
function looksLikeAvatarPath(value: string): boolean {
if (/[\\/]/.test(value)) return true;
if (/[\\/]/.test(value)) {
return true;
}
return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value);
}
function normalizeAvatarValue(value: string | undefined): string | undefined {
if (!value) return undefined;
if (!value) {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) return undefined;
if (isAvatarUrl(trimmed)) return trimmed;
if (looksLikeAvatarPath(trimmed)) return trimmed;
if (!/\s/.test(trimmed) && trimmed.length <= 4) return trimmed;
if (!trimmed) {
return undefined;
}
if (isAvatarUrl(trimmed)) {
return trimmed;
}
if (looksLikeAvatarPath(trimmed)) {
return trimmed;
}
if (!/\s/.test(trimmed) && trimmed.length <= 4) {
return trimmed;
}
return undefined;
}
+51 -17
View File
@@ -33,7 +33,9 @@ type TailscaleUser = {
type TailscaleWhoisLookup = (ip: string) => Promise<TailscaleWhoisIdentity | null>;
function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
if (a.length !== b.length) {
return false;
}
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
@@ -42,20 +44,34 @@ function normalizeLogin(login: string): string {
}
function isLoopbackAddress(ip: string | undefined): boolean {
if (!ip) return false;
if (ip === "127.0.0.1") return true;
if (ip.startsWith("127.")) return true;
if (ip === "::1") return true;
if (ip.startsWith("::ffff:127.")) return true;
if (!ip) {
return false;
}
if (ip === "127.0.0.1") {
return true;
}
if (ip.startsWith("127.")) {
return true;
}
if (ip === "::1") {
return true;
}
if (ip.startsWith("::ffff:127.")) {
return true;
}
return false;
}
function getHostName(hostHeader?: string): string {
const host = (hostHeader ?? "").trim().toLowerCase();
if (!host) return "";
if (!host) {
return "";
}
if (host.startsWith("[")) {
const end = host.indexOf("]");
if (end !== -1) return host.slice(1, end);
if (end !== -1) {
return host.slice(1, end);
}
}
const [name] = host.split(":");
return name ?? "";
@@ -66,7 +82,9 @@ function headerValue(value: string | string[] | undefined): string | undefined {
}
function resolveTailscaleClientIp(req?: IncomingMessage): string | undefined {
if (!req) return undefined;
if (!req) {
return undefined;
}
const forwardedFor = headerValue(req.headers?.["x-forwarded-for"]);
return forwardedFor ? parseForwardedForClientIp(forwardedFor) : undefined;
}
@@ -75,7 +93,9 @@ function resolveRequestClientIp(
req?: IncomingMessage,
trustedProxies?: string[],
): string | undefined {
if (!req) return undefined;
if (!req) {
return undefined;
}
return resolveGatewayClientIp({
remoteAddr: req.socket?.remoteAddress ?? "",
forwardedFor: headerValue(req.headers?.["x-forwarded-for"]),
@@ -85,9 +105,13 @@ function resolveRequestClientIp(
}
export function isLocalDirectRequest(req?: IncomingMessage, trustedProxies?: string[]): boolean {
if (!req) return false;
if (!req) {
return false;
}
const clientIp = resolveRequestClientIp(req, trustedProxies) ?? "";
if (!isLoopbackAddress(clientIp)) return false;
if (!isLoopbackAddress(clientIp)) {
return false;
}
const host = getHostName(req.headers?.host);
const hostIsLocal = host === "localhost" || host === "127.0.0.1" || host === "::1";
@@ -104,9 +128,13 @@ export function isLocalDirectRequest(req?: IncomingMessage, trustedProxies?: str
}
function getTailscaleUser(req?: IncomingMessage): TailscaleUser | null {
if (!req) return null;
if (!req) {
return null;
}
const login = req.headers["tailscale-user-login"];
if (typeof login !== "string" || !login.trim()) return null;
if (typeof login !== "string" || !login.trim()) {
return null;
}
const nameRaw = req.headers["tailscale-user-name"];
const profilePic = req.headers["tailscale-user-profile-pic"];
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : login.trim();
@@ -118,7 +146,9 @@ function getTailscaleUser(req?: IncomingMessage): TailscaleUser | null {
}
function hasTailscaleProxyHeaders(req?: IncomingMessage): boolean {
if (!req) return false;
if (!req) {
return false;
}
return Boolean(
req.headers["x-forwarded-for"] &&
req.headers["x-forwarded-proto"] &&
@@ -127,7 +157,9 @@ function hasTailscaleProxyHeaders(req?: IncomingMessage): boolean {
}
function isTailscaleProxyRequest(req?: IncomingMessage): boolean {
if (!req) return false;
if (!req) {
return false;
}
return isLoopbackAddress(req.socket?.remoteAddress) && hasTailscaleProxyHeaders(req);
}
@@ -191,7 +223,9 @@ export function resolveGatewayAuth(params: {
export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {
if (auth.mode === "token" && !auth.token) {
if (auth.allowTailscale) return;
if (auth.allowTailscale) {
return;
}
throw new Error(
"gateway auth mode is token, but no token was configured (set gateway.auth.token or OPENCLAW_GATEWAY_TOKEN)",
);
+6 -2
View File
@@ -38,11 +38,15 @@ async function loadBootFile(
try {
const content = await fs.readFile(bootPath, "utf-8");
const trimmed = content.trim();
if (!trimmed) return { status: "empty" };
if (!trimmed) {
return { status: "empty" };
}
return { status: "ok", content: trimmed };
} catch (err) {
const anyErr = err as { code?: string };
if (anyErr.code === "ENOENT") return { status: "missing" };
if (anyErr.code === "ENOENT") {
return { status: "missing" };
}
throw err;
}
}
+6 -2
View File
@@ -31,8 +31,12 @@ vi.mock("../infra/tailnet.js", () => ({
vi.mock("./client.js", () => ({
describeGatewayCloseCode: (code: number) => {
if (code === 1000) return "normal closure";
if (code === 1006) return "abnormal closure (no close frame)";
if (code === 1000) {
return "normal closure";
}
if (code === 1006) {
return "abnormal closure (no close frame)";
}
return undefined;
},
GatewayClient: class {
+11 -4
View File
@@ -190,11 +190,16 @@ export async function callGateway<T = unknown>(opts: CallGatewayOptions): Promis
let settled = false;
let ignoreClose = false;
const stop = (err?: Error, value?: T) => {
if (settled) return;
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
if (err) reject(err);
else resolve(value as T);
if (err) {
reject(err);
} else {
resolve(value as T);
}
};
const client = new GatewayClient({
@@ -228,7 +233,9 @@ export async function callGateway<T = unknown>(opts: CallGatewayOptions): Promis
}
},
onClose: (code, reason) => {
if (settled || ignoreClose) return;
if (settled || ignoreClose) {
return;
}
ignoreClose = true;
client.stop();
stop(new Error(formatCloseError(code, reason)));
+15 -5
View File
@@ -10,7 +10,9 @@ export type ChatAbortControllerEntry = {
export function isChatStopCommandText(text: string): boolean {
const trimmed = text.trim();
if (!trimmed) return false;
if (!trimmed) {
return false;
}
return trimmed.toLowerCase() === "/stop" || isAbortTrigger(trimmed);
}
@@ -74,8 +76,12 @@ export function abortChatRunById(
): { aborted: boolean } {
const { runId, sessionKey, stopReason } = params;
const active = ops.chatAbortControllers.get(runId);
if (!active) return { aborted: false };
if (active.sessionKey !== sessionKey) return { aborted: false };
if (!active) {
return { aborted: false };
}
if (active.sessionKey !== sessionKey) {
return { aborted: false };
}
ops.chatAbortedRuns.set(runId, Date.now());
active.controller.abort();
@@ -97,9 +103,13 @@ export function abortChatRunsForSessionKey(
const { sessionKey, stopReason } = params;
const runIds: string[] = [];
for (const [runId, active] of ops.chatAbortControllers) {
if (active.sessionKey !== sessionKey) continue;
if (active.sessionKey !== sessionKey) {
continue;
}
const res = abortChatRunById(ops, { runId, sessionKey, stopReason });
if (res.aborted) runIds.push(runId);
if (res.aborted) {
runIds.push(runId);
}
}
return { aborted: runIds.length > 0, runIds };
}
+21 -7
View File
@@ -23,18 +23,24 @@ type AttachmentLog = {
};
function normalizeMime(mime?: string): string | undefined {
if (!mime) return undefined;
if (!mime) {
return undefined;
}
const cleaned = mime.split(";")[0]?.trim().toLowerCase();
return cleaned || undefined;
}
async function sniffMimeFromBase64(base64: string): Promise<string | undefined> {
const trimmed = base64.trim();
if (!trimmed) return undefined;
if (!trimmed) {
return undefined;
}
const take = Math.min(256, trimmed.length);
const sliceLen = take - (take % 4);
if (sliceLen < 8) return undefined;
if (sliceLen < 8) {
return undefined;
}
try {
const head = Buffer.from(trimmed.slice(0, sliceLen), "base64");
@@ -67,7 +73,9 @@ export async function parseMessageWithAttachments(
const images: ChatImageContent[] = [];
for (const [idx, att] of attachments.entries()) {
if (!att) continue;
if (!att) {
continue;
}
const mime = att.mimeType ?? "";
const content = att.content;
const label = att.fileName || att.type || `attachment-${idx + 1}`;
@@ -132,12 +140,16 @@ export function buildMessageWithAttachments(
opts?: { maxBytes?: number },
): string {
const maxBytes = opts?.maxBytes ?? 2_000_000; // 2 MB
if (!attachments || attachments.length === 0) return message;
if (!attachments || attachments.length === 0) {
return message;
}
const blocks: string[] = [];
for (const [idx, att] of attachments.entries()) {
if (!att) continue;
if (!att) {
continue;
}
const mime = att.mimeType ?? "";
const content = att.content;
const label = att.fileName || att.type || `attachment-${idx + 1}`;
@@ -169,7 +181,9 @@ export function buildMessageWithAttachments(
blocks.push(dataUrl);
}
if (blocks.length === 0) return message;
if (blocks.length === 0) {
return message;
}
const separator = message.trim().length > 0 ? "\n\n" : "";
return `${message}${separator}${blocks.join("\n\n")}`;
}
+36 -12
View File
@@ -18,21 +18,31 @@ const ENVELOPE_CHANNELS = [
const MESSAGE_ID_LINE = /^\s*\[message_id:\s*[^\]]+\]\s*$/i;
function looksLikeEnvelopeHeader(header: string): boolean {
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(header)) return true;
if (/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(header)) return true;
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(header)) {
return true;
}
if (/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(header)) {
return true;
}
return ENVELOPE_CHANNELS.some((label) => header.startsWith(`${label} `));
}
export function stripEnvelope(text: string): string {
const match = text.match(ENVELOPE_PREFIX);
if (!match) return text;
if (!match) {
return text;
}
const header = match[1] ?? "";
if (!looksLikeEnvelopeHeader(header)) return text;
if (!looksLikeEnvelopeHeader(header)) {
return text;
}
return text.slice(match[0].length);
}
function stripMessageIdHints(text: string): string {
if (!text.includes("[message_id:")) return text;
if (!text.includes("[message_id:")) {
return text;
}
const lines = text.split(/\r?\n/);
const filtered = lines.filter((line) => !MESSAGE_ID_LINE.test(line));
return filtered.length === lines.length ? text : filtered.join("\n");
@@ -41,11 +51,17 @@ function stripMessageIdHints(text: string): string {
function stripEnvelopeFromContent(content: unknown[]): { content: unknown[]; changed: boolean } {
let changed = false;
const next = content.map((item) => {
if (!item || typeof item !== "object") return item;
if (!item || typeof item !== "object") {
return item;
}
const entry = item as Record<string, unknown>;
if (entry.type !== "text" || typeof entry.text !== "string") return item;
if (entry.type !== "text" || typeof entry.text !== "string") {
return item;
}
const stripped = stripMessageIdHints(stripEnvelope(entry.text));
if (stripped === entry.text) return item;
if (stripped === entry.text) {
return item;
}
changed = true;
return {
...entry,
@@ -56,10 +72,14 @@ function stripEnvelopeFromContent(content: unknown[]): { content: unknown[]; cha
}
export function stripEnvelopeFromMessage(message: unknown): unknown {
if (!message || typeof message !== "object") return message;
if (!message || typeof message !== "object") {
return message;
}
const entry = message as Record<string, unknown>;
const role = typeof entry.role === "string" ? entry.role.toLowerCase() : "";
if (role !== "user") return message;
if (role !== "user") {
return message;
}
let changed = false;
const next: Record<string, unknown> = { ...entry };
@@ -88,11 +108,15 @@ export function stripEnvelopeFromMessage(message: unknown): unknown {
}
export function stripEnvelopeFromMessages(messages: unknown[]): unknown[] {
if (messages.length === 0) return messages;
if (messages.length === 0) {
return messages;
}
let changed = false;
const next = messages.map((message) => {
const stripped = stripEnvelopeFromMessage(message);
if (stripped !== message) changed = true;
if (stripped !== message) {
changed = true;
}
return stripped;
});
return changed ? next : messages;
+3 -1
View File
@@ -146,7 +146,9 @@ r1USnb+wUdA7Zoj/mQ==
const error = await new Promise<Error>((resolve) => {
let settled = false;
const finish = (err: Error) => {
if (settled) return;
if (settled) {
return;
}
settled = true;
resolve(err);
};
+49 -17
View File
@@ -99,7 +99,9 @@ export class GatewayClient {
}
start() {
if (this.closed) return;
if (this.closed) {
return;
}
const url = this.opts.url ?? "ws://127.0.0.1:18789";
if (this.opts.tlsFingerprint && !url.startsWith("wss://")) {
this.opts.onConnectError?.(new Error("gateway tls fingerprint requires wss:// gateway url"));
@@ -173,7 +175,9 @@ export class GatewayClient {
}
private sendConnect() {
if (this.connectSent) return;
if (this.connectSent) {
return;
}
this.connectSent = true;
if (this.connectTimer) {
clearTimeout(this.connectTimer);
@@ -196,7 +200,9 @@ export class GatewayClient {
const nonce = this.connectNonce ?? undefined;
const scopes = this.opts.scopes ?? ["operator.admin"];
const device = (() => {
if (!this.opts.deviceIdentity) return undefined;
if (!this.opts.deviceIdentity) {
return undefined;
}
const payload = buildDeviceAuthPayload({
deviceId: this.opts.deviceIdentity.deviceId,
clientId: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
@@ -269,8 +275,11 @@ export class GatewayClient {
}
this.opts.onConnectError?.(err instanceof Error ? err : new Error(String(err)));
const msg = `gateway connect failed: ${String(err)}`;
if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE) logDebug(msg);
else logError(msg);
if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE) {
logDebug(msg);
} else {
logError(msg);
}
this.ws?.close(1008, "connect failed");
});
}
@@ -304,7 +313,9 @@ export class GatewayClient {
}
if (validateResponseFrame(parsed)) {
const pending = this.pending.get(parsed.id);
if (!pending) return;
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;
@@ -312,8 +323,11 @@ export class GatewayClient {
return;
}
this.pending.delete(parsed.id);
if (parsed.ok) pending.resolve(parsed.payload);
else pending.reject(new Error(parsed.error?.message ?? "unknown error"));
if (parsed.ok) {
pending.resolve(parsed.payload);
} else {
pending.reject(new Error(parsed.error?.message ?? "unknown error"));
}
}
} catch (err) {
logDebug(`gateway client parse error: ${String(err)}`);
@@ -323,14 +337,18 @@ export class GatewayClient {
private queueConnect() {
this.connectNonce = null;
this.connectSent = false;
if (this.connectTimer) clearTimeout(this.connectTimer);
if (this.connectTimer) {
clearTimeout(this.connectTimer);
}
this.connectTimer = setTimeout(() => {
this.sendConnect();
}, 750);
}
private scheduleReconnect() {
if (this.closed) return;
if (this.closed) {
return;
}
if (this.tickTimer) {
clearInterval(this.tickTimer);
this.tickTimer = null;
@@ -348,11 +366,17 @@ export class GatewayClient {
}
private startTickWatch() {
if (this.tickTimer) clearInterval(this.tickTimer);
if (this.tickTimer) {
clearInterval(this.tickTimer);
}
const interval = Math.max(this.tickIntervalMs, 1000);
this.tickTimer = setInterval(() => {
if (this.closed) return;
if (!this.lastTick) return;
if (this.closed) {
return;
}
if (!this.lastTick) {
return;
}
const gap = Date.now() - this.lastTick;
if (gap > this.tickIntervalMs * 2) {
this.ws?.close(4000, "tick timeout");
@@ -361,9 +385,13 @@ export class GatewayClient {
}
private validateTlsFingerprint(): Error | null {
if (!this.opts.tlsFingerprint || !this.ws) return null;
if (!this.opts.tlsFingerprint || !this.ws) {
return null;
}
const expected = normalizeFingerprint(this.opts.tlsFingerprint);
if (!expected) return new Error("gateway tls fingerprint missing");
if (!expected) {
return new Error("gateway tls fingerprint missing");
}
const socket = (
this.ws as WebSocket & {
_socket?: { getPeerCertificate?: () => { fingerprint256?: string } };
@@ -374,8 +402,12 @@ export class GatewayClient {
}
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");
if (!fingerprint) {
return new Error("gateway tls fingerprint unavailable");
}
if (fingerprint !== expected) {
return new Error("gateway tls fingerprint mismatch");
}
return null;
}
+30 -10
View File
@@ -93,7 +93,9 @@ function listReloadRules(): ReloadRule[] {
cachedReloadRules = null;
cachedRegistry = registry;
}
if (cachedReloadRules) return cachedReloadRules;
if (cachedReloadRules) {
return cachedReloadRules;
}
// Channel docking: plugins contribute hot reload/no-op prefixes here.
const channelReloadRules: ReloadRule[] = listChannelPlugins().flatMap((plugin) => [
...(plugin.reload?.configPrefixes ?? []).map(
@@ -117,7 +119,9 @@ function listReloadRules(): ReloadRule[] {
function matchRule(path: string): ReloadRule | null {
for (const rule of listReloadRules()) {
if (path === rule.prefix || path.startsWith(`${rule.prefix}.`)) return rule;
if (path === rule.prefix || path.startsWith(`${rule.prefix}.`)) {
return rule;
}
}
return null;
}
@@ -132,14 +136,18 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
}
export function diffConfigPaths(prev: unknown, next: unknown, prefix = ""): string[] {
if (prev === next) return [];
if (prev === next) {
return [];
}
if (isPlainObject(prev) && isPlainObject(next)) {
const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
const paths: string[] = [];
for (const key of keys) {
const prevValue = prev[key];
const nextValue = next[key];
if (prevValue === undefined && nextValue === undefined) continue;
if (prevValue === undefined && nextValue === undefined) {
continue;
}
const childPrefix = prefix ? `${prefix}.${key}` : key;
const childPaths = diffConfigPaths(prevValue, nextValue, childPrefix);
if (childPaths.length > 0) {
@@ -266,8 +274,12 @@ export function startGatewayConfigReloader(opts: {
let restartQueued = false;
const schedule = () => {
if (stopped) return;
if (debounceTimer) clearTimeout(debounceTimer);
if (stopped) {
return;
}
if (debounceTimer) {
clearTimeout(debounceTimer);
}
const wait = settings.debounceMs;
debounceTimer = setTimeout(() => {
void runReload();
@@ -275,7 +287,9 @@ export function startGatewayConfigReloader(opts: {
};
const runReload = async () => {
if (stopped) return;
if (stopped) {
return;
}
if (running) {
pending = true;
return;
@@ -296,7 +310,9 @@ export function startGatewayConfigReloader(opts: {
const changedPaths = diffConfigPaths(currentConfig, nextConfig);
currentConfig = nextConfig;
settings = resolveGatewayReloadSettings(nextConfig);
if (changedPaths.length === 0) return;
if (changedPaths.length === 0) {
return;
}
opts.log.info(`config change detected; evaluating reload (${changedPaths.join(", ")})`);
const plan = buildGatewayReloadPlan(changedPaths);
@@ -350,7 +366,9 @@ export function startGatewayConfigReloader(opts: {
watcher.on("unlink", schedule);
let watcherClosed = false;
watcher.on("error", (err) => {
if (watcherClosed) return;
if (watcherClosed) {
return;
}
watcherClosed = true;
opts.log.warn(`config watcher error: ${String(err)}`);
void watcher.close().catch(() => {});
@@ -359,7 +377,9 @@ export function startGatewayConfigReloader(opts: {
return {
stop: async () => {
stopped = true;
if (debounceTimer) clearTimeout(debounceTimer);
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = null;
watcherClosed = true;
await watcher.close().catch(() => {});
+30 -10
View File
@@ -1,12 +1,22 @@
const CONTROL_UI_AVATAR_PREFIX = "/avatar";
export function normalizeControlUiBasePath(basePath?: string): string {
if (!basePath) return "";
if (!basePath) {
return "";
}
let normalized = basePath.trim();
if (!normalized) return "";
if (!normalized.startsWith("/")) normalized = `/${normalized}`;
if (normalized === "/") return "";
if (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
if (!normalized) {
return "";
}
if (!normalized.startsWith("/")) {
normalized = `/${normalized}`;
}
if (normalized === "/") {
return "";
}
if (normalized.endsWith("/")) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
@@ -17,7 +27,9 @@ export function buildControlUiAvatarUrl(basePath: string, agentId: string): stri
}
function looksLikeLocalAvatarPath(value: string): boolean {
if (/[\\/]/.test(value)) return true;
if (/[\\/]/.test(value)) {
return true;
}
return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value);
}
@@ -27,8 +39,12 @@ export function resolveAssistantAvatarUrl(params: {
basePath?: string;
}): string | undefined {
const avatar = params.avatar?.trim();
if (!avatar) return undefined;
if (/^https?:\/\//i.test(avatar) || /^data:image\//i.test(avatar)) return avatar;
if (!avatar) {
return undefined;
}
if (/^https?:\/\//i.test(avatar) || /^data:image\//i.test(avatar)) {
return avatar;
}
const basePath = normalizeControlUiBasePath(params.basePath);
const baseAvatarPrefix = basePath
@@ -37,9 +53,13 @@ export function resolveAssistantAvatarUrl(params: {
if (basePath && avatar.startsWith(`${CONTROL_UI_AVATAR_PREFIX}/`)) {
return `${basePath}${avatar}`;
}
if (avatar.startsWith(baseAvatarPrefix)) return avatar;
if (avatar.startsWith(baseAvatarPrefix)) {
return avatar;
}
if (!params.agentId) return avatar;
if (!params.agentId) {
return avatar;
}
if (looksLikeLocalAvatarPath(avatar)) {
return buildControlUiAvatarUrl(basePath, params.agentId);
}
+36 -12
View File
@@ -40,7 +40,9 @@ function resolveControlUiRoot(): string | null {
path.resolve(process.cwd(), "dist", "control-ui"),
].filter((dir): dir is string => Boolean(dir));
for (const dir of candidates) {
if (fs.existsSync(path.join(dir, "index.html"))) return dir;
if (fs.existsSync(path.join(dir, "index.html"))) {
return dir;
}
}
return null;
}
@@ -103,8 +105,12 @@ export function handleControlUiAvatarRequest(
opts: { basePath?: string; resolveAvatar: (agentId: string) => ControlUiAvatarResolution },
): boolean {
const urlRaw = req.url;
if (!urlRaw) return false;
if (req.method !== "GET" && req.method !== "HEAD") return false;
if (!urlRaw) {
return false;
}
if (req.method !== "GET" && req.method !== "HEAD") {
return false;
}
const url = new URL(urlRaw, "http://localhost");
const basePath = normalizeControlUiBasePath(opts.basePath);
@@ -112,7 +118,9 @@ export function handleControlUiAvatarRequest(
const pathWithBase = basePath
? `${basePath}${CONTROL_UI_AVATAR_PREFIX}/`
: `${CONTROL_UI_AVATAR_PREFIX}/`;
if (!pathname.startsWith(pathWithBase)) return false;
if (!pathname.startsWith(pathWithBase)) {
return false;
}
const agentIdParts = pathname.slice(pathWithBase.length).split("/").filter(Boolean);
const agentId = agentIdParts[0] ?? "";
@@ -185,7 +193,9 @@ function injectControlUiConfig(html: string, opts: ControlUiInjectionOpts): stri
)};` +
`</script>`;
// Check if already injected
if (html.includes("__OPENCLAW_ASSISTANT_NAME__")) return html;
if (html.includes("__OPENCLAW_ASSISTANT_NAME__")) {
return html;
}
const headClose = html.indexOf("</head>");
if (headClose !== -1) {
return `${html.slice(0, headClose)}${script}${html.slice(headClose)}`;
@@ -227,10 +237,16 @@ function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndex
}
function isSafeRelativePath(relPath: string) {
if (!relPath) return false;
if (!relPath) {
return false;
}
const normalized = path.posix.normalize(relPath);
if (normalized.startsWith("../") || normalized === "..") return false;
if (normalized.includes("\0")) return false;
if (normalized.startsWith("../") || normalized === "..") {
return false;
}
if (normalized.includes("\0")) {
return false;
}
return true;
}
@@ -240,7 +256,9 @@ export function handleControlUiHttpRequest(
opts?: ControlUiRequestOptions,
): boolean {
const urlRaw = req.url;
if (!urlRaw) return false;
if (!urlRaw) {
return false;
}
if (req.method !== "GET" && req.method !== "HEAD") {
res.statusCode = 405;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
@@ -266,7 +284,9 @@ export function handleControlUiHttpRequest(
res.end();
return true;
}
if (!pathname.startsWith(`${basePath}/`)) return false;
if (!pathname.startsWith(`${basePath}/`)) {
return false;
}
}
const root = resolveControlUiRoot();
@@ -282,9 +302,13 @@ export function handleControlUiHttpRequest(
const uiPath =
basePath && pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) : pathname;
const rel = (() => {
if (uiPath === ROOT_PREFIX) return "";
if (uiPath === ROOT_PREFIX) {
return "";
}
const assetsIndex = uiPath.indexOf("/assets/");
if (assetsIndex >= 0) return uiPath.slice(assetsIndex + 1);
if (assetsIndex >= 0) {
return uiPath.slice(assetsIndex + 1);
}
return uiPath.slice(1);
})();
const requested = rel && !rel.endsWith("/") ? rel : `${rel}index.html`;
+3 -1
View File
@@ -64,7 +64,9 @@ export class ExecApprovalManager {
resolve(recordId: string, decision: ExecApprovalDecision, resolvedBy?: string | null): boolean {
const pending = this.pending.get(recordId);
if (!pending) return false;
if (!pending) {
return false;
}
clearTimeout(pending.timer);
pending.record.resolvedAtMs = Date.now();
pending.record.decision = decision;
+80 -30
View File
@@ -45,11 +45,17 @@ function randomImageProbeCode(len = 6): string {
}
function editDistance(a: string, b: string): number {
if (a === b) return 0;
if (a === b) {
return 0;
}
const aLen = a.length;
const bLen = b.length;
if (aLen === 0) return bLen;
if (bLen === 0) return aLen;
if (aLen === 0) {
return bLen;
}
if (bLen === 0) {
return aLen;
}
let prev = Array.from({ length: bLen + 1 }, (_v, idx) => idx);
let curr = Array.from({ length: bLen + 1 }, () => 0);
@@ -82,7 +88,9 @@ function extractPayloadText(result: unknown): string {
function parseJsonStringArray(name: string, raw?: string): string[] | undefined {
const trimmed = raw?.trim();
if (!trimmed) return undefined;
if (!trimmed) {
return undefined;
}
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) {
throw new Error(`${name} must be a JSON array of strings.`);
@@ -92,8 +100,12 @@ function parseJsonStringArray(name: string, raw?: string): string[] | undefined
function parseImageMode(raw?: string): "list" | "repeat" | undefined {
const trimmed = raw?.trim();
if (!trimmed) return undefined;
if (trimmed === "list" || trimmed === "repeat") return trimmed;
if (!trimmed) {
return undefined;
}
if (trimmed === "list" || trimmed === "repeat") {
return trimmed;
}
throw new Error("OPENCLAW_LIVE_CLI_BACKEND_IMAGE_MODE must be 'list' or 'repeat'.");
}
@@ -121,15 +133,20 @@ async function getFreePort(): Promise<number> {
}
const port = addr.port;
srv.close((err) => {
if (err) reject(err);
else resolve(port);
if (err) {
reject(err);
} else {
resolve(port);
}
});
});
});
}
async function isPortFree(port: number): Promise<boolean> {
if (!Number.isFinite(port) || port <= 0 || port > 65535) return false;
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
return false;
}
return await new Promise((resolve) => {
const srv = createServer();
srv.once("error", () => resolve(false));
@@ -146,7 +163,9 @@ async function getFreeGatewayPort(): Promise<number> {
const ok = (await Promise.all(candidates.map((candidate) => isPortFree(candidate)))).every(
Boolean,
);
if (ok) return port;
if (ok) {
return port;
}
}
throw new Error("failed to acquire a free gateway port block");
}
@@ -155,11 +174,16 @@ async function connectClient(params: { url: string; token: string }) {
return await new Promise<GatewayClient>((resolve, reject) => {
let settled = false;
const stop = (err?: Error, client?: GatewayClient) => {
if (settled) return;
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
if (err) reject(err);
else resolve(client as GatewayClient);
if (err) {
reject(err);
} else {
resolve(client as GatewayClient);
}
};
const client = new GatewayClient({
url: params.url,
@@ -388,7 +412,9 @@ describeLive("gateway live (cli backend)", () => {
}
const candidates = imageText.toUpperCase().match(/[A-Z0-9]{6,20}/g) ?? [];
const bestDistance = candidates.reduce((best, cand) => {
if (Math.abs(cand.length - imageCode.length) > 2) return best;
if (Math.abs(cand.length - imageCode.length) > 2) {
return best;
}
return Math.min(best, editDistance(cand, imageCode));
}, Number.POSITIVE_INFINITY);
if (!(bestDistance <= 5)) {
@@ -399,22 +425,46 @@ describeLive("gateway live (cli backend)", () => {
client.stop();
await server.close();
await fs.rm(tempDir, { recursive: true, force: true });
if (previous.configPath === undefined) delete process.env.OPENCLAW_CONFIG_PATH;
else process.env.OPENCLAW_CONFIG_PATH = previous.configPath;
if (previous.token === undefined) delete process.env.OPENCLAW_GATEWAY_TOKEN;
else process.env.OPENCLAW_GATEWAY_TOKEN = previous.token;
if (previous.skipChannels === undefined) delete process.env.OPENCLAW_SKIP_CHANNELS;
else process.env.OPENCLAW_SKIP_CHANNELS = previous.skipChannels;
if (previous.skipGmail === undefined) delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER;
else process.env.OPENCLAW_SKIP_GMAIL_WATCHER = previous.skipGmail;
if (previous.skipCron === undefined) delete process.env.OPENCLAW_SKIP_CRON;
else process.env.OPENCLAW_SKIP_CRON = previous.skipCron;
if (previous.skipCanvas === undefined) delete process.env.OPENCLAW_SKIP_CANVAS_HOST;
else process.env.OPENCLAW_SKIP_CANVAS_HOST = previous.skipCanvas;
if (previous.anthropicApiKey === undefined) delete process.env.ANTHROPIC_API_KEY;
else process.env.ANTHROPIC_API_KEY = previous.anthropicApiKey;
if (previous.anthropicApiKeyOld === undefined) delete process.env.ANTHROPIC_API_KEY_OLD;
else process.env.ANTHROPIC_API_KEY_OLD = previous.anthropicApiKeyOld;
if (previous.configPath === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = previous.configPath;
}
if (previous.token === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = previous.token;
}
if (previous.skipChannels === undefined) {
delete process.env.OPENCLAW_SKIP_CHANNELS;
} else {
process.env.OPENCLAW_SKIP_CHANNELS = previous.skipChannels;
}
if (previous.skipGmail === undefined) {
delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER;
} else {
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = previous.skipGmail;
}
if (previous.skipCron === undefined) {
delete process.env.OPENCLAW_SKIP_CRON;
} else {
process.env.OPENCLAW_SKIP_CRON = previous.skipCron;
}
if (previous.skipCanvas === undefined) {
delete process.env.OPENCLAW_SKIP_CANVAS_HOST;
} else {
process.env.OPENCLAW_SKIP_CANVAS_HOST = previous.skipCanvas;
}
if (previous.anthropicApiKey === undefined) {
delete process.env.ANTHROPIC_API_KEY;
} else {
process.env.ANTHROPIC_API_KEY = previous.anthropicApiKey;
}
if (previous.anthropicApiKeyOld === undefined) {
delete process.env.ANTHROPIC_API_KEY_OLD;
} else {
process.env.ANTHROPIC_API_KEY_OLD = previous.anthropicApiKeyOld;
}
}
}, 60_000);
});
@@ -44,7 +44,9 @@ const describeLive = LIVE || GATEWAY_LIVE ? describe : describe.skip;
function parseFilter(raw?: string): Set<string> | null {
const trimmed = raw?.trim();
if (!trimmed || trimmed === "all") return null;
if (!trimmed || trimmed === "all") {
return null;
}
const ids = trimmed
.split(",")
.map((s) => s.trim())
@@ -62,7 +64,9 @@ function assertNoReasoningTags(params: {
phase: string;
label: string;
}): void {
if (!params.text) return;
if (!params.text) {
return;
}
if (THINKING_TAG_RE.test(params.text) || FINAL_TAG_RE.test(params.text)) {
const snippet = params.text.length > 200 ? `${params.text.slice(0, 200)}` : params.text;
throw new Error(
@@ -81,22 +85,40 @@ function extractPayloadText(result: unknown): string {
}
function isMeaningful(text: string): boolean {
if (!text) return false;
if (!text) {
return false;
}
const trimmed = text.trim();
if (trimmed.toLowerCase() === "ok") return false;
if (trimmed.length < 60) return false;
if (trimmed.toLowerCase() === "ok") {
return false;
}
if (trimmed.length < 60) {
return false;
}
const words = trimmed.split(/\s+/g).filter(Boolean);
if (words.length < 12) return false;
if (words.length < 12) {
return false;
}
return true;
}
function isGoogleModelNotFoundText(text: string): boolean {
const trimmed = text.trim();
if (!trimmed) return false;
if (!/not found/i.test(trimmed)) return false;
if (/models\/.+ is not found for api version/i.test(trimmed)) return true;
if (/"status"\s*:\s*"NOT_FOUND"/.test(trimmed)) return true;
if (/"code"\s*:\s*404/.test(trimmed)) return true;
if (!trimmed) {
return false;
}
if (!/not found/i.test(trimmed)) {
return false;
}
if (/models\/.+ is not found for api version/i.test(trimmed)) {
return true;
}
if (/"status"\s*:\s*"NOT_FOUND"/.test(trimmed)) {
return true;
}
if (/"code"\s*:\s*404/.test(trimmed)) {
return true;
}
return false;
}
@@ -124,7 +146,9 @@ function isOpenAIReasoningSequenceError(error: string): boolean {
function isToolNonceRefusal(error: string): boolean {
const msg = error.toLowerCase();
if (!msg.includes("nonce")) return false;
if (!msg.includes("nonce")) {
return false;
}
return (
msg.includes("token") ||
msg.includes("secret") ||
@@ -226,11 +250,17 @@ function randomImageProbeCode(len = 6): string {
}
function editDistance(a: string, b: string): number {
if (a === b) return 0;
if (a === b) {
return 0;
}
const aLen = a.length;
const bLen = b.length;
if (aLen === 0) return bLen;
if (bLen === 0) return aLen;
if (aLen === 0) {
return bLen;
}
if (bLen === 0) {
return aLen;
}
let prev = Array.from({ length: bLen + 1 }, (_v, idx) => idx);
let curr = Array.from({ length: bLen + 1 }, () => 0);
@@ -264,15 +294,20 @@ async function getFreePort(): Promise<number> {
}
const port = addr.port;
srv.close((err) => {
if (err) reject(err);
else resolve(port);
if (err) {
reject(err);
} else {
resolve(port);
}
});
});
});
}
async function isPortFree(port: number): Promise<boolean> {
if (!Number.isFinite(port) || port <= 0 || port > 65535) return false;
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
return false;
}
return await new Promise((resolve) => {
const srv = createServer();
srv.once("error", () => resolve(false));
@@ -291,7 +326,9 @@ async function getFreeGatewayPort(): Promise<number> {
const ok = (await Promise.all(candidates.map((candidate) => isPortFree(candidate)))).every(
Boolean,
);
if (ok) return port;
if (ok) {
return port;
}
}
throw new Error("failed to acquire a free gateway port block");
}
@@ -305,11 +342,16 @@ async function connectClient(params: { url: string; token: string }) {
return await new Promise<GatewayClient>((resolve, reject) => {
let settled = false;
const stop = (err?: Error, client?: GatewayClient) => {
if (settled) return;
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
if (err) reject(err);
else resolve(client as GatewayClient);
if (err) {
reject(err);
} else {
resolve(client as GatewayClient);
}
};
const client = new GatewayClient({
url: params.url,
@@ -386,7 +428,9 @@ function sanitizeAuthConfig(params: {
agentDir: string;
}): OpenClawConfig["auth"] | undefined {
const auth = params.cfg.auth;
if (!auth) return auth;
if (!auth) {
return auth;
}
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
@@ -395,10 +439,14 @@ function sanitizeAuthConfig(params: {
if (auth.profiles) {
profiles = {};
for (const [profileId, profile] of Object.entries(auth.profiles)) {
if (!store.profiles[profileId]) continue;
if (!store.profiles[profileId]) {
continue;
}
profiles[profileId] = profile;
}
if (Object.keys(profiles).length === 0) profiles = undefined;
if (Object.keys(profiles).length === 0) {
profiles = undefined;
}
}
let order: Record<string, string[]> | undefined;
@@ -406,13 +454,19 @@ function sanitizeAuthConfig(params: {
order = {};
for (const [provider, ids] of Object.entries(auth.order)) {
const filtered = ids.filter((id) => Boolean(store.profiles[id]));
if (filtered.length === 0) continue;
if (filtered.length === 0) {
continue;
}
order[provider] = filtered;
}
if (Object.keys(order).length === 0) order = undefined;
if (Object.keys(order).length === 0) {
order = undefined;
}
}
if (!profiles && !order && !auth.cooldowns) return undefined;
if (!profiles && !order && !auth.cooldowns) {
return undefined;
}
return {
...auth,
profiles,
@@ -426,7 +480,9 @@ function buildMinimaxProviderOverride(params: {
baseUrl: string;
}): ModelProviderConfig | null {
const existing = params.cfg.models?.providers?.minimax;
if (!existing || !Array.isArray(existing.models) || existing.models.length === 0) return null;
if (!existing || !Array.isArray(existing.models) || existing.models.length === 0) {
return null;
}
return {
...existing,
api: params.api,
@@ -761,7 +817,9 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
} else {
const candidates = imageText.toUpperCase().match(/[A-Z0-9]{6,20}/g) ?? [];
const bestDistance = candidates.reduce((best, cand) => {
if (Math.abs(cand.length - imageCode.length) > 2) return best;
if (Math.abs(cand.length - imageCode.length) > 2) {
return best;
}
return Math.min(best, editDistance(cand, imageCode));
}, Number.POSITIVE_INFINITY);
// OCR / image-read flake: allow a small edit distance, but still require the "cat" token above.
@@ -977,7 +1035,9 @@ describeLive("gateway live (dev agent, profile keys)", () => {
const candidates: Array<Model<Api>> = [];
for (const model of wanted) {
if (PROVIDERS && !PROVIDERS.has(model.provider)) continue;
if (PROVIDERS && !PROVIDERS.has(model.provider)) {
continue;
}
try {
// eslint-disable-next-line no-await-in-loop
const apiKeyInfo = await getApiKeyForModel({
@@ -1042,7 +1102,9 @@ describeLive("gateway live (dev agent, profile keys)", () => {
);
it("z.ai fallback handles anthropic tool history", async () => {
if (!ZAI_FALLBACK) return;
if (!ZAI_FALLBACK) {
return;
}
const previous = {
configPath: process.env.OPENCLAW_CONFIG_PATH,
token: process.env.OPENCLAW_GATEWAY_TOKEN,
@@ -1069,7 +1131,9 @@ describeLive("gateway live (dev agent, profile keys)", () => {
const anthropic = modelRegistry.find("anthropic", "claude-opus-4-5") as Model<Api> | null;
const zai = modelRegistry.find("zai", "glm-4.7") as Model<Api> | null;
if (!anthropic || !zai) return;
if (!anthropic || !zai) {
return;
}
try {
await getApiKeyForModel({ model: anthropic, cfg });
await getApiKeyForModel({ model: zai, cfg });
+6 -2
View File
@@ -219,9 +219,13 @@ describe("gateway e2e", () => {
let didSendToken = false;
while (!next.done) {
const step = next.step;
if (!step) throw new Error("wizard missing step");
if (!step) {
throw new Error("wizard missing step");
}
const value = step.type === "text" ? wizardToken : null;
if (step.type === "text") didSendToken = true;
if (step.type === "text") {
didSendToken = true;
}
next = await client.request("wizard.next", {
sessionId,
answer: { stepId: step.id, value },
+78 -26
View File
@@ -104,10 +104,14 @@ export function resolveHookMappings(hooks?: HooksConfig): HookMappingResolved[]
const presets = hooks?.presets ?? [];
const gmailAllowUnsafe = hooks?.gmail?.allowUnsafeExternalContent;
const mappings: HookMappingConfig[] = [];
if (hooks?.mappings) mappings.push(...hooks.mappings);
if (hooks?.mappings) {
mappings.push(...hooks.mappings);
}
for (const preset of presets) {
const presetMappings = hookPresetMappings[preset];
if (!presetMappings) continue;
if (!presetMappings) {
continue;
}
if (preset === "gmail" && typeof gmailAllowUnsafe === "boolean") {
mappings.push(
...presetMappings.map((mapping) => ({
@@ -119,7 +123,9 @@ export function resolveHookMappings(hooks?: HooksConfig): HookMappingResolved[]
}
mappings.push(...presetMappings);
}
if (mappings.length === 0) return [];
if (mappings.length === 0) {
return [];
}
const configDir = path.dirname(CONFIG_PATH);
const transformsDir = hooks?.transformsDir
@@ -133,12 +139,18 @@ export async function applyHookMappings(
mappings: HookMappingResolved[],
ctx: HookMappingContext,
): Promise<HookMappingResult | null> {
if (mappings.length === 0) return null;
if (mappings.length === 0) {
return null;
}
for (const mapping of mappings) {
if (!mappingMatches(mapping, ctx)) continue;
if (!mappingMatches(mapping, ctx)) {
continue;
}
const base = buildActionFromMapping(mapping, ctx);
if (!base.ok) return base;
if (!base.ok) {
return base;
}
let override: HookTransformResult = null;
if (mapping.transform) {
@@ -149,9 +161,13 @@ export async function applyHookMappings(
}
}
if (!base.action) return { ok: true, action: null, skipped: true };
if (!base.action) {
return { ok: true, action: null, skipped: true };
}
const merged = mergeAction(base.action, override, mapping.action);
if (!merged.ok) return merged;
if (!merged.ok) {
return merged;
}
return merged;
}
return null;
@@ -197,11 +213,15 @@ function normalizeHookMapping(
function mappingMatches(mapping: HookMappingResolved, ctx: HookMappingContext) {
if (mapping.matchPath) {
if (mapping.matchPath !== normalizeMatchPath(ctx.path)) return false;
if (mapping.matchPath !== normalizeMatchPath(ctx.path)) {
return false;
}
}
if (mapping.matchSource) {
const source = typeof ctx.payload.source === "string" ? ctx.payload.source : undefined;
if (!source || source !== mapping.matchSource) return false;
if (!source || source !== mapping.matchSource) {
return false;
}
}
return true;
}
@@ -295,7 +315,9 @@ function validateAction(action: HookAction): HookMappingResult {
async function loadTransform(transform: HookMappingTransformResolved): Promise<HookTransformFn> {
const cached = transformCache.get(transform.modulePath);
if (cached) return cached;
if (cached) {
return cached;
}
const url = pathToFileURL(transform.modulePath).href;
const mod = (await import(url)) as Record<string, unknown>;
const fn = resolveTransformFn(mod, transform.exportName);
@@ -312,38 +334,60 @@ function resolveTransformFn(mod: Record<string, unknown>, exportName?: string):
}
function resolvePath(baseDir: string, target: string): string {
if (!target) return baseDir;
if (path.isAbsolute(target)) return target;
if (!target) {
return baseDir;
}
if (path.isAbsolute(target)) {
return target;
}
return path.join(baseDir, target);
}
function normalizeMatchPath(raw?: string): string | undefined {
if (!raw) return undefined;
if (!raw) {
return undefined;
}
const trimmed = raw.trim();
if (!trimmed) return undefined;
if (!trimmed) {
return undefined;
}
return trimmed.replace(/^\/+/, "").replace(/\/+$/, "");
}
function renderOptional(value: string | undefined, ctx: HookMappingContext) {
if (!value) return undefined;
if (!value) {
return undefined;
}
const rendered = renderTemplate(value, ctx).trim();
return rendered ? rendered : undefined;
}
function renderTemplate(template: string, ctx: HookMappingContext) {
if (!template) return "";
if (!template) {
return "";
}
return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_, expr: string) => {
const value = resolveTemplateExpr(expr.trim(), ctx);
if (value === undefined || value === null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (value === undefined || value === null) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return JSON.stringify(value);
});
}
function resolveTemplateExpr(expr: string, ctx: HookMappingContext) {
if (expr === "path") return ctx.path;
if (expr === "now") return new Date().toISOString();
if (expr === "path") {
return ctx.path;
}
if (expr === "now") {
return new Date().toISOString();
}
if (expr.startsWith("headers.")) {
return getByPath(ctx.headers, expr.slice("headers.".length));
}
@@ -360,7 +404,9 @@ function resolveTemplateExpr(expr: string, ctx: HookMappingContext) {
}
function getByPath(input: Record<string, unknown>, pathExpr: string): unknown {
if (!pathExpr) return undefined;
if (!pathExpr) {
return undefined;
}
const parts: Array<string | number> = [];
const re = /([^.[\]]+)|(\[(\d+)\])/g;
let match = re.exec(pathExpr);
@@ -374,13 +420,19 @@ function getByPath(input: Record<string, unknown>, pathExpr: string): unknown {
}
let current: unknown = input;
for (const part of parts) {
if (current === null || current === undefined) return undefined;
if (current === null || current === undefined) {
return undefined;
}
if (typeof part === "number") {
if (!Array.isArray(current)) return undefined;
if (!Array.isArray(current)) {
return undefined;
}
current = current[part] as unknown;
continue;
}
if (typeof current !== "object") return undefined;
if (typeof current !== "object") {
return undefined;
}
current = (current as Record<string, unknown>)[part];
}
return current;
+39 -13
View File
@@ -17,7 +17,9 @@ export type HooksConfigResolved = {
};
export function resolveHooksConfig(cfg: OpenClawConfig): HooksConfigResolved | null {
if (cfg.hooks?.enabled !== true) return null;
if (cfg.hooks?.enabled !== true) {
return null;
}
const token = cfg.hooks?.token?.trim();
if (!token) {
throw new Error("hooks.enabled requires hooks.token");
@@ -51,15 +53,21 @@ export function extractHookToken(req: IncomingMessage, url: URL): HookTokenResul
typeof req.headers.authorization === "string" ? req.headers.authorization.trim() : "";
if (auth.toLowerCase().startsWith("bearer ")) {
const token = auth.slice(7).trim();
if (token) return { token, fromQuery: false };
if (token) {
return { token, fromQuery: false };
}
}
const headerToken =
typeof req.headers["x-openclaw-token"] === "string"
? req.headers["x-openclaw-token"].trim()
: "";
if (headerToken) return { token: headerToken, fromQuery: false };
if (headerToken) {
return { token: headerToken, fromQuery: false };
}
const queryToken = url.searchParams.get("token");
if (queryToken) return { token: queryToken.trim(), fromQuery: true };
if (queryToken) {
return { token: queryToken.trim(), fromQuery: true };
}
return { token: undefined, fromQuery: false };
}
@@ -72,7 +80,9 @@ export async function readJsonBody(
let total = 0;
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => {
if (done) return;
if (done) {
return;
}
total += chunk.length;
if (total > maxBytes) {
done = true;
@@ -83,7 +93,9 @@ export async function readJsonBody(
chunks.push(chunk);
});
req.on("end", () => {
if (done) return;
if (done) {
return;
}
done = true;
const raw = Buffer.concat(chunks).toString("utf-8").trim();
if (!raw) {
@@ -98,7 +110,9 @@ export async function readJsonBody(
}
});
req.on("error", (err) => {
if (done) return;
if (done) {
return;
}
done = true;
resolve({ ok: false, error: String(err) });
});
@@ -123,7 +137,9 @@ export function normalizeWakePayload(
| { ok: true; value: { text: string; mode: "now" | "next-heartbeat" } }
| { ok: false; error: string } {
const text = typeof payload.text === "string" ? payload.text.trim() : "";
if (!text) return { ok: false, error: "text required" };
if (!text) {
return { ok: false, error: "text required" };
}
const mode = payload.mode === "next-heartbeat" ? "next-heartbeat" : "now";
return { ok: true, value: { text, mode } };
}
@@ -149,10 +165,16 @@ const getHookChannelSet = () => new Set<string>(listHookChannelValues());
export const getHookChannelError = () => `channel must be ${listHookChannelValues().join("|")}`;
export function resolveHookChannel(raw: unknown): HookMessageChannel | null {
if (raw === undefined) return "last";
if (typeof raw !== "string") return null;
if (raw === undefined) {
return "last";
}
if (typeof raw !== "string") {
return null;
}
const normalized = normalizeMessageChannel(raw);
if (!normalized || !getHookChannelSet().has(normalized)) return null;
if (!normalized || !getHookChannelSet().has(normalized)) {
return null;
}
return normalized as HookMessageChannel;
}
@@ -170,7 +192,9 @@ export function normalizeAgentPayload(
}
| { ok: false; error: string } {
const message = typeof payload.message === "string" ? payload.message.trim() : "";
if (!message) return { ok: false, error: "message required" };
if (!message) {
return { ok: false, error: "message required" };
}
const nameRaw = payload.name;
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "Hook";
const wakeMode = payload.wakeMode === "next-heartbeat" ? "next-heartbeat" : "now";
@@ -181,7 +205,9 @@ export function normalizeAgentPayload(
? sessionKeyRaw.trim()
: `hook:${idFactory()}`;
const channel = resolveHookChannel(payload.channel);
if (!channel) return { ok: false, error: getHookChannelError() };
if (!channel) {
return { ok: false, error: getHookChannelError() };
}
const toRaw = payload.to;
const to = typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
const modelRaw = payload.model;
+24 -8
View File
@@ -5,14 +5,20 @@ import { buildAgentMainSessionKey, normalizeAgentId } from "../routing/session-k
export function getHeader(req: IncomingMessage, name: string): string | undefined {
const raw = req.headers[name.toLowerCase()];
if (typeof raw === "string") return raw;
if (Array.isArray(raw)) return raw[0];
if (typeof raw === "string") {
return raw;
}
if (Array.isArray(raw)) {
return raw[0];
}
return undefined;
}
export function getBearerToken(req: IncomingMessage): string | undefined {
const raw = getHeader(req, "authorization")?.trim() ?? "";
if (!raw.toLowerCase().startsWith("bearer ")) return undefined;
if (!raw.toLowerCase().startsWith("bearer ")) {
return undefined;
}
const token = raw.slice(7).trim();
return token || undefined;
}
@@ -22,19 +28,25 @@ export function resolveAgentIdFromHeader(req: IncomingMessage): string | undefin
getHeader(req, "x-openclaw-agent-id")?.trim() ||
getHeader(req, "x-openclaw-agent")?.trim() ||
"";
if (!raw) return undefined;
if (!raw) {
return undefined;
}
return normalizeAgentId(raw);
}
export function resolveAgentIdFromModel(model: string | undefined): string | undefined {
const raw = model?.trim();
if (!raw) return undefined;
if (!raw) {
return undefined;
}
const m =
raw.match(/^openclaw[:/](?<agentId>[a-z0-9][a-z0-9_-]{0,63})$/i) ??
raw.match(/^agent:(?<agentId>[a-z0-9][a-z0-9_-]{0,63})$/i);
const agentId = m?.groups?.agentId;
if (!agentId) return undefined;
if (!agentId) {
return undefined;
}
return normalizeAgentId(agentId);
}
@@ -43,7 +55,9 @@ export function resolveAgentIdForRequest(params: {
model: string | undefined;
}): string {
const fromHeader = resolveAgentIdFromHeader(params.req);
if (fromHeader) return fromHeader;
if (fromHeader) {
return fromHeader;
}
const fromModel = resolveAgentIdFromModel(params.model);
return fromModel ?? "main";
@@ -56,7 +70,9 @@ export function resolveSessionKey(params: {
prefix: string;
}): string {
const explicit = getHeader(params.req, "x-openclaw-session-key")?.trim();
if (explicit) return explicit;
if (explicit) {
return explicit;
}
const user = params.user?.trim();
const mainKey = user ? `${params.prefix}-user:${user}` : `${params.prefix}:${randomUUID()}`;
+15 -5
View File
@@ -68,10 +68,16 @@ function fillPixel(
b: number,
a = 255,
) {
if (x < 0 || y < 0) return;
if (x >= width) return;
if (x < 0 || y < 0) {
return;
}
if (x >= width) {
return;
}
const idx = (y * width + x) * 4;
if (idx < 0 || idx + 3 >= buf.length) return;
if (idx < 0 || idx + 3 >= buf.length) {
return;
}
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
@@ -109,12 +115,16 @@ function drawGlyph5x7(params: {
color: { r: number; g: number; b: number; a?: number };
}) {
const rows = GLYPH_ROWS_5X7[params.char];
if (!rows) return;
if (!rows) {
return;
}
for (let row = 0; row < 7; row += 1) {
const bits = rows[row] ?? 0;
for (let col = 0; col < 5; col += 1) {
const on = (bits & (1 << (4 - col))) !== 0;
if (!on) continue;
if (!on) {
continue;
}
for (let dy = 0; dy < params.scale; dy += 1) {
for (let dx = 0; dx < params.scale; dx += 1) {
fillPixel(
+84 -28
View File
@@ -3,54 +3,80 @@ import net from "node:net";
import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js";
export function isLoopbackAddress(ip: string | undefined): boolean {
if (!ip) return false;
if (ip === "127.0.0.1") return true;
if (ip.startsWith("127.")) return true;
if (ip === "::1") return true;
if (ip.startsWith("::ffff:127.")) return true;
if (!ip) {
return false;
}
if (ip === "127.0.0.1") {
return true;
}
if (ip.startsWith("127.")) {
return true;
}
if (ip === "::1") {
return true;
}
if (ip.startsWith("::ffff:127.")) {
return true;
}
return false;
}
function normalizeIPv4MappedAddress(ip: string): string {
if (ip.startsWith("::ffff:")) return ip.slice("::ffff:".length);
if (ip.startsWith("::ffff:")) {
return ip.slice("::ffff:".length);
}
return ip;
}
function normalizeIp(ip: string | undefined): string | undefined {
const trimmed = ip?.trim();
if (!trimmed) return undefined;
if (!trimmed) {
return undefined;
}
return normalizeIPv4MappedAddress(trimmed.toLowerCase());
}
function stripOptionalPort(ip: string): string {
if (ip.startsWith("[")) {
const end = ip.indexOf("]");
if (end !== -1) return ip.slice(1, end);
if (end !== -1) {
return ip.slice(1, end);
}
}
if (net.isIP(ip)) {
return ip;
}
if (net.isIP(ip)) return ip;
const lastColon = ip.lastIndexOf(":");
if (lastColon > -1 && ip.includes(".") && ip.indexOf(":") === lastColon) {
const candidate = ip.slice(0, lastColon);
if (net.isIP(candidate) === 4) return candidate;
if (net.isIP(candidate) === 4) {
return candidate;
}
}
return ip;
}
export function parseForwardedForClientIp(forwardedFor?: string): string | undefined {
const raw = forwardedFor?.split(",")[0]?.trim();
if (!raw) return undefined;
if (!raw) {
return undefined;
}
return normalizeIp(stripOptionalPort(raw));
}
function parseRealIp(realIp?: string): string | undefined {
const raw = realIp?.trim();
if (!raw) return undefined;
if (!raw) {
return undefined;
}
return normalizeIp(stripOptionalPort(raw));
}
export function isTrustedProxyAddress(ip: string | undefined, trustedProxies?: string[]): boolean {
const normalized = normalizeIp(ip);
if (!normalized || !trustedProxies || trustedProxies.length === 0) return false;
if (!normalized || !trustedProxies || trustedProxies.length === 0) {
return false;
}
return trustedProxies.some((proxy) => normalizeIp(proxy) === normalized);
}
@@ -61,19 +87,31 @@ export function resolveGatewayClientIp(params: {
trustedProxies?: string[];
}): string | undefined {
const remote = normalizeIp(params.remoteAddr);
if (!remote) return undefined;
if (!isTrustedProxyAddress(remote, params.trustedProxies)) return remote;
if (!remote) {
return undefined;
}
if (!isTrustedProxyAddress(remote, params.trustedProxies)) {
return remote;
}
return parseForwardedForClientIp(params.forwardedFor) ?? parseRealIp(params.realIp) ?? remote;
}
export function isLocalGatewayAddress(ip: string | undefined): boolean {
if (isLoopbackAddress(ip)) return true;
if (!ip) return false;
if (isLoopbackAddress(ip)) {
return true;
}
if (!ip) {
return false;
}
const normalized = normalizeIPv4MappedAddress(ip.trim().toLowerCase());
const tailnetIPv4 = pickPrimaryTailnetIPv4();
if (tailnetIPv4 && normalized === tailnetIPv4.toLowerCase()) return true;
if (tailnetIPv4 && normalized === tailnetIPv4.toLowerCase()) {
return true;
}
const tailnetIPv6 = pickPrimaryTailnetIPv6();
if (tailnetIPv6 && ip.trim().toLowerCase() === tailnetIPv6.toLowerCase()) return true;
if (tailnetIPv6 && ip.trim().toLowerCase() === tailnetIPv6.toLowerCase()) {
return true;
}
return false;
}
@@ -97,14 +135,20 @@ export async function resolveGatewayBindHost(
if (mode === "loopback") {
// 127.0.0.1 rarely fails, but handle gracefully
if (await canBindToHost("127.0.0.1")) return "127.0.0.1";
if (await canBindToHost("127.0.0.1")) {
return "127.0.0.1";
}
return "0.0.0.0"; // extreme fallback
}
if (mode === "tailnet") {
const tailnetIP = pickPrimaryTailnetIPv4();
if (tailnetIP && (await canBindToHost(tailnetIP))) return tailnetIP;
if (await canBindToHost("127.0.0.1")) return "127.0.0.1";
if (tailnetIP && (await canBindToHost(tailnetIP))) {
return tailnetIP;
}
if (await canBindToHost("127.0.0.1")) {
return "127.0.0.1";
}
return "0.0.0.0";
}
@@ -114,15 +158,21 @@ export async function resolveGatewayBindHost(
if (mode === "custom") {
const host = customHost?.trim();
if (!host) return "0.0.0.0"; // invalid config → fall back to all
if (!host) {
return "0.0.0.0";
} // invalid config → fall back to all
if (isValidIPv4(host) && (await canBindToHost(host))) return host;
if (isValidIPv4(host) && (await canBindToHost(host))) {
return host;
}
// Custom IP failed → fall back to LAN
return "0.0.0.0";
}
if (mode === "auto") {
if (await canBindToHost("127.0.0.1")) return "127.0.0.1";
if (await canBindToHost("127.0.0.1")) {
return "127.0.0.1";
}
return "0.0.0.0";
}
@@ -155,9 +205,13 @@ export async function resolveGatewayListenHosts(
bindHost: string,
opts?: { canBindToHost?: (host: string) => Promise<boolean> },
): Promise<string[]> {
if (bindHost !== "127.0.0.1") return [bindHost];
if (bindHost !== "127.0.0.1") {
return [bindHost];
}
const canBind = opts?.canBindToHost ?? canBindToHost;
if (await canBind("::1")) return [bindHost, "::1"];
if (await canBind("::1")) {
return [bindHost, "::1"];
}
return [bindHost];
}
@@ -169,7 +223,9 @@ export async function resolveGatewayListenHosts(
*/
function isValidIPv4(host: string): boolean {
const parts = host.split(".");
if (parts.length !== 4) return false;
if (parts.length !== 4) {
return false;
}
return parts.every((part) => {
const n = parseInt(part, 10);
return !Number.isNaN(n) && n >= 0 && n <= 255 && part === String(n);
+39 -13
View File
@@ -59,18 +59,40 @@ const PLATFORM_DEFAULTS: Record<string, string[]> = {
function normalizePlatformId(platform?: string, deviceFamily?: string): string {
const raw = (platform ?? "").trim().toLowerCase();
if (raw.startsWith("ios")) return "ios";
if (raw.startsWith("android")) return "android";
if (raw.startsWith("mac")) return "macos";
if (raw.startsWith("darwin")) return "macos";
if (raw.startsWith("win")) return "windows";
if (raw.startsWith("linux")) return "linux";
if (raw.startsWith("ios")) {
return "ios";
}
if (raw.startsWith("android")) {
return "android";
}
if (raw.startsWith("mac")) {
return "macos";
}
if (raw.startsWith("darwin")) {
return "macos";
}
if (raw.startsWith("win")) {
return "windows";
}
if (raw.startsWith("linux")) {
return "linux";
}
const family = (deviceFamily ?? "").trim().toLowerCase();
if (family.includes("iphone") || family.includes("ipad") || family.includes("ios")) return "ios";
if (family.includes("android")) return "android";
if (family.includes("mac")) return "macos";
if (family.includes("windows")) return "windows";
if (family.includes("linux")) return "linux";
if (family.includes("iphone") || family.includes("ipad") || family.includes("ios")) {
return "ios";
}
if (family.includes("android")) {
return "android";
}
if (family.includes("mac")) {
return "macos";
}
if (family.includes("windows")) {
return "windows";
}
if (family.includes("linux")) {
return "linux";
}
return "unknown";
}
@@ -85,7 +107,9 @@ export function resolveNodeCommandAllowlist(
const allow = new Set([...base, ...extra].map((cmd) => cmd.trim()).filter(Boolean));
for (const blocked of deny) {
const trimmed = blocked.trim();
if (trimmed) allow.delete(trimmed);
if (trimmed) {
allow.delete(trimmed);
}
}
return allow;
}
@@ -96,7 +120,9 @@ export function isNodeCommandAllowed(params: {
allowlist: Set<string>;
}): { ok: true } | { ok: false; reason: string } {
const command = params.command.trim();
if (!command) return { ok: false, reason: "command required" };
if (!command) {
return { ok: false, reason: "command required" };
}
if (!params.allowlist.has(command)) {
return { ok: false, reason: "command not allowlisted" };
}
+15 -5
View File
@@ -81,11 +81,15 @@ export class NodeRegistry {
unregister(connId: string): string | null {
const nodeId = this.nodesByConn.get(connId);
if (!nodeId) return null;
if (!nodeId) {
return null;
}
this.nodesByConn.delete(connId);
this.nodesById.delete(nodeId);
for (const [id, pending] of this.pendingInvokes.entries()) {
if (pending.nodeId !== nodeId) continue;
if (pending.nodeId !== nodeId) {
continue;
}
clearTimeout(pending.timer);
pending.reject(new Error(`node disconnected (${pending.command})`));
this.pendingInvokes.delete(id);
@@ -160,8 +164,12 @@ export class NodeRegistry {
error?: { code?: string; message?: string } | null;
}): boolean {
const pending = this.pendingInvokes.get(params.id);
if (!pending) return false;
if (pending.nodeId !== params.nodeId) return false;
if (!pending) {
return false;
}
if (pending.nodeId !== params.nodeId) {
return false;
}
clearTimeout(pending.timer);
this.pendingInvokes.delete(params.id);
pending.resolve({
@@ -175,7 +183,9 @@ export class NodeRegistry {
sendEvent(nodeId: string, event: string, payload?: unknown): boolean {
const node = this.nodesById.get(nodeId);
if (!node) return false;
if (!node) {
return false;
}
return this.sendEventToSession(node, event, payload);
}
+48 -16
View File
@@ -45,17 +45,27 @@ function asMessages(val: unknown): OpenAiChatMessage[] {
}
function extractTextContent(content: unknown): string {
if (typeof content === "string") return content;
if (typeof content === "string") {
return content;
}
if (Array.isArray(content)) {
return content
.map((part) => {
if (!part || typeof part !== "object") return "";
if (!part || typeof part !== "object") {
return "";
}
const type = (part as { type?: unknown }).type;
const text = (part as { text?: unknown }).text;
const inputText = (part as { input_text?: unknown }).input_text;
if (type === "text" && typeof text === "string") return text;
if (type === "input_text" && typeof text === "string") return text;
if (typeof inputText === "string") return inputText;
if (type === "text" && typeof text === "string") {
return text;
}
if (type === "input_text" && typeof text === "string") {
return text;
}
if (typeof inputText === "string") {
return inputText;
}
return "";
})
.filter(Boolean)
@@ -75,10 +85,14 @@ function buildAgentPrompt(messagesUnknown: unknown): {
[];
for (const msg of messages) {
if (!msg || typeof msg !== "object") continue;
if (!msg || typeof msg !== "object") {
continue;
}
const role = typeof msg.role === "string" ? msg.role.trim() : "";
const content = extractTextContent(msg.content).trim();
if (!role || !content) continue;
if (!role || !content) {
continue;
}
if (role === "system" || role === "developer") {
systemParts.push(content);
continue;
@@ -115,7 +129,9 @@ function buildAgentPrompt(messagesUnknown: unknown): {
break;
}
}
if (currentIndex < 0) currentIndex = conversationEntries.length - 1;
if (currentIndex < 0) {
currentIndex = conversationEntries.length - 1;
}
const currentEntry = conversationEntries[currentIndex]?.entry;
if (currentEntry) {
const historyEntries = conversationEntries.slice(0, currentIndex).map((entry) => entry.entry);
@@ -147,7 +163,9 @@ function resolveOpenAiSessionKey(params: {
}
function coerceRequest(val: unknown): OpenAiChatCompletionRequest {
if (!val || typeof val !== "object") return {};
if (!val || typeof val !== "object") {
return {};
}
return val as OpenAiChatCompletionRequest;
}
@@ -157,7 +175,9 @@ export async function handleOpenAiHttpRequest(
opts: OpenAiHttpOptions,
): Promise<boolean> {
const url = new URL(req.url ?? "/", `http://${req.headers.host || "localhost"}`);
if (url.pathname !== "/v1/chat/completions") return false;
if (url.pathname !== "/v1/chat/completions") {
return false;
}
if (req.method !== "POST") {
sendMethodNotAllowed(res);
@@ -177,7 +197,9 @@ export async function handleOpenAiHttpRequest(
}
const body = await readJsonBodyOrError(req, res, opts.maxBodyBytes ?? 1024 * 1024);
if (body === undefined) return true;
if (body === undefined) {
return true;
}
const payload = coerceRequest(body);
const stream = Boolean(payload.stream);
@@ -254,14 +276,20 @@ export async function handleOpenAiHttpRequest(
let closed = false;
const unsubscribe = onAgentEvent((evt) => {
if (evt.runId !== runId) return;
if (closed) return;
if (evt.runId !== runId) {
return;
}
if (closed) {
return;
}
if (evt.stream === "assistant") {
const delta = evt.data?.delta;
const text = evt.data?.text;
const content = typeof delta === "string" ? delta : typeof text === "string" ? text : "";
if (!content) return;
if (!content) {
return;
}
if (!wroteRole) {
wroteRole = true;
@@ -323,7 +351,9 @@ export async function handleOpenAiHttpRequest(
deps,
);
if (closed) return;
if (closed) {
return;
}
if (!sawAssistantDelta) {
if (!wroteRole) {
@@ -362,7 +392,9 @@ export async function handleOpenAiHttpRequest(
});
}
} catch (err) {
if (closed) return;
if (closed) {
return;
}
writeSse(res, {
id: runId,
object: "chat.completion.chunk",
+6 -2
View File
@@ -73,7 +73,9 @@ function parseSseEvents(text: string): Array<{ event?: string; data: string }> {
}
async function ensureResponseConsumed(res: Response) {
if (res.bodyUsed) return;
if (res.bodyUsed) {
return;
}
try {
await res.text();
} catch {
@@ -493,7 +495,9 @@ describe("OpenResponses HTTP API (e2e)", () => {
const typeText = await resTypeMatch.text();
const typeEvents = parseSseEvents(typeText);
for (const event of typeEvents) {
if (event.data === "[DONE]") continue;
if (event.data === "[DONE]") {
continue;
}
const parsed = JSON.parse(event.data) as { type?: string };
expect(event.event).toBe(parsed.type);
}
+54 -18
View File
@@ -71,11 +71,17 @@ function writeSseEvent(res: ServerResponse, event: StreamingEvent) {
}
function extractTextContent(content: string | ContentPart[]): string {
if (typeof content === "string") return content;
if (typeof content === "string") {
return content;
}
return content
.map((part) => {
if (part.type === "input_text") return part.text;
if (part.type === "output_text") return part.text;
if (part.type === "input_text") {
return part.text;
}
if (part.type === "output_text") {
return part.text;
}
return "";
})
.filter(Boolean)
@@ -127,7 +133,9 @@ function applyToolChoice(params: {
toolChoice: CreateResponseBody["tool_choice"];
}): { tools: ClientToolDefinition[]; extraSystemPrompt?: string } {
const { tools, toolChoice } = params;
if (!toolChoice) return { tools };
if (!toolChoice) {
return { tools };
}
if (toolChoice === "none") {
return { tools: [] };
@@ -176,7 +184,9 @@ export function buildAgentPrompt(input: string | ItemParam[]): {
for (const item of input) {
if (item.type === "message") {
const content = extractTextContent(item.content).trim();
if (!content) continue;
if (!content) {
continue;
}
if (item.role === "system" || item.role === "developer") {
systemParts.push(content);
@@ -210,7 +220,9 @@ export function buildAgentPrompt(input: string | ItemParam[]): {
break;
}
}
if (currentIndex < 0) currentIndex = conversationEntries.length - 1;
if (currentIndex < 0) {
currentIndex = conversationEntries.length - 1;
}
const currentEntry = conversationEntries[currentIndex]?.entry;
if (currentEntry) {
@@ -257,7 +269,9 @@ function toUsage(
}
| undefined,
): Usage {
if (!value) return createEmptyUsage();
if (!value) {
return createEmptyUsage();
}
const input = value.input ?? 0;
const output = value.output ?? 0;
const cacheRead = value.cacheRead ?? 0;
@@ -320,7 +334,9 @@ export async function handleOpenResponsesHttpRequest(
opts: OpenResponsesHttpOptions,
): Promise<boolean> {
const url = new URL(req.url ?? "/", `http://${req.headers.host || "localhost"}`);
if (url.pathname !== "/v1/responses") return false;
if (url.pathname !== "/v1/responses") {
return false;
}
if (req.method !== "POST") {
sendMethodNotAllowed(res);
@@ -346,7 +362,9 @@ export async function handleOpenResponsesHttpRequest(
? limits.maxBodyBytes
: Math.max(limits.maxBodyBytes, limits.files.maxBytes * 2, limits.images.maxBytes * 2));
const body = await readJsonBodyOrError(req, res, maxBodyBytes);
if (body === undefined) return true;
if (body === undefined) {
return true;
}
// Validate request body with Zod
const parseResult = CreateResponseBodySchema.safeParse(body);
@@ -592,9 +610,15 @@ export async function handleOpenResponsesHttpRequest(
let finalizeRequested: { status: ResponseResource["status"]; text: string } | null = null;
const maybeFinalize = () => {
if (closed) return;
if (!finalizeRequested) return;
if (!finalUsage) return;
if (closed) {
return;
}
if (!finalizeRequested) {
return;
}
if (!finalUsage) {
return;
}
const usage = finalUsage;
closed = true;
@@ -642,7 +666,9 @@ export async function handleOpenResponsesHttpRequest(
};
const requestFinalize = (status: ResponseResource["status"], text: string) => {
if (finalizeRequested) return;
if (finalizeRequested) {
return;
}
finalizeRequested = { status, text };
maybeFinalize();
};
@@ -681,14 +707,20 @@ export async function handleOpenResponsesHttpRequest(
});
unsubscribe = onAgentEvent((evt) => {
if (evt.runId !== responseId) return;
if (closed) return;
if (evt.runId !== responseId) {
return;
}
if (closed) {
return;
}
if (evt.stream === "assistant") {
const delta = evt.data?.delta;
const text = evt.data?.text;
const content = typeof delta === "string" ? delta : typeof text === "string" ? text : "";
if (!content) return;
if (!content) {
return;
}
sawAssistantDelta = true;
accumulatedText += content;
@@ -740,7 +772,9 @@ export async function handleOpenResponsesHttpRequest(
finalUsage = extractUsageFromResult(result);
maybeFinalize();
if (closed) return;
if (closed) {
return;
}
// Fallback: if no streaming deltas were received, send the full response
if (!sawAssistantDelta) {
@@ -845,7 +879,9 @@ export async function handleOpenResponsesHttpRequest(
});
}
} catch (err) {
if (closed) return;
if (closed) {
return;
}
finalUsage = finalUsage ?? createEmptyUsage();
const errorResponse = createResponseResource({
+6 -2
View File
@@ -28,7 +28,9 @@ export type GatewayProbeResult = {
};
function formatError(err: unknown): string {
if (err instanceof Error) return err.message;
if (err instanceof Error) {
return err.message;
}
return String(err);
}
@@ -46,7 +48,9 @@ export async function probeGateway(opts: {
return await new Promise<GatewayProbeResult>((resolve) => {
let settled = false;
const settle = (result: Omit<GatewayProbeResult, "url">) => {
if (settled) return;
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
client.stop();
+6 -2
View File
@@ -47,7 +47,9 @@ const GATEWAY_CLIENT_MODE_SET = new Set<GatewayClientMode>(Object.values(GATEWAY
export function normalizeGatewayClientId(raw?: string | null): GatewayClientId | undefined {
const normalized = raw?.trim().toLowerCase();
if (!normalized) return undefined;
if (!normalized) {
return undefined;
}
return GATEWAY_CLIENT_ID_SET.has(normalized as GatewayClientId)
? (normalized as GatewayClientId)
: undefined;
@@ -59,7 +61,9 @@ export function normalizeGatewayClientName(raw?: string | null): GatewayClientNa
export function normalizeGatewayClientMode(raw?: string | null): GatewayClientMode | undefined {
const normalized = raw?.trim().toLowerCase();
if (!normalized) return undefined;
if (!normalized) {
return undefined;
}
return GATEWAY_CLIENT_MODE_SET.has(normalized as GatewayClientMode)
? (normalized as GatewayClientMode)
: undefined;
+3 -1
View File
@@ -321,7 +321,9 @@ export const validateWebLoginStartParams =
export const validateWebLoginWaitParams = ajv.compile<WebLoginWaitParams>(WebLoginWaitParamsSchema);
export function formatValidationErrors(errors: ErrorObject[] | null | undefined) {
if (!errors?.length) return "unknown validation error";
if (!errors?.length) {
return "unknown validation error";
}
const parts: string[] = [];
+15 -5
View File
@@ -17,11 +17,17 @@ const EVENT_SCOPE_GUARDS: Record<string, string[]> = {
function hasEventScope(client: GatewayWsClient, event: string): boolean {
const required = EVENT_SCOPE_GUARDS[event];
if (!required) return true;
if (!required) {
return true;
}
const role = client.connect.role ?? "operator";
if (role !== "operator") return false;
if (role !== "operator") {
return false;
}
const scopes = Array.isArray(client.connect.scopes) ? client.connect.scopes : [];
if (scopes.includes(ADMIN_SCOPE)) return true;
if (scopes.includes(ADMIN_SCOPE)) {
return true;
}
return required.some((scope) => scopes.includes(scope));
}
@@ -56,9 +62,13 @@ export function createGatewayBroadcaster(params: { clients: Set<GatewayWsClient>
}
logWs("out", "event", logMeta);
for (const c of params.clients) {
if (!hasEventScope(c, event)) continue;
if (!hasEventScope(c, event)) {
continue;
}
const slow = c.socket.bufferedAmount > MAX_BUFFERED_BYTES;
if (slow && opts?.dropIfSlow) continue;
if (slow && opts?.dropIfSlow) {
continue;
}
if (slow) {
try {
c.socket.close(1008, "slow consumer");
+6 -2
View File
@@ -5,7 +5,9 @@ export type BrowserControlServer = {
};
export async function startBrowserControlServerIfEnabled(): Promise<BrowserControlServer | null> {
if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER)) return null;
if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER)) {
return null;
}
// Lazy import: keeps startup fast, but still bundles for the embedded
// gateway (bun --compile) via the static specifier path.
const override = process.env.OPENCLAW_BROWSER_CONTROL_MODULE?.trim();
@@ -21,7 +23,9 @@ export async function startBrowserControlServerIfEnabled(): Promise<BrowserContr
typeof (mod as { stopBrowserControlService?: unknown }).stopBrowserControlService === "function"
? (mod as { stopBrowserControlService: () => Promise<void> }).stopBrowserControlService
: (mod as { stopBrowserControlServer?: () => Promise<void> }).stopBrowserControlServer;
if (!start) return null;
if (!start) {
return null;
}
await start();
return { stop: stop ?? (async () => {}) };
}
+21 -7
View File
@@ -30,7 +30,9 @@ function createRuntimeStore(): ChannelRuntimeStore {
}
function isAccountEnabled(account: unknown): boolean {
if (!account || typeof account !== "object") return true;
if (!account || typeof account !== "object") {
return true;
}
const enabled = (account as { enabled?: boolean }).enabled;
return enabled !== false;
}
@@ -66,7 +68,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
const getStore = (channelId: ChannelId): ChannelRuntimeStore => {
const existing = channelStores.get(channelId);
if (existing) return existing;
if (existing) {
return existing;
}
const next = createRuntimeStore();
channelStores.set(channelId, next);
return next;
@@ -92,16 +96,22 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
const startChannel = async (channelId: ChannelId, accountId?: string) => {
const plugin = getChannelPlugin(channelId);
const startAccount = plugin?.gateway?.startAccount;
if (!startAccount) return;
if (!startAccount) {
return;
}
const cfg = loadConfig();
resetDirectoryCache({ channel: channelId, accountId });
const store = getStore(channelId);
const accountIds = accountId ? [accountId] : plugin.config.listAccountIds(cfg);
if (accountIds.length === 0) return;
if (accountIds.length === 0) {
return;
}
await Promise.all(
accountIds.map(async (id) => {
if (store.tasks.has(id)) return;
if (store.tasks.has(id)) {
return;
}
const account = plugin.config.resolveAccount(cfg, id);
const enabled = plugin.config.isEnabled
? plugin.config.isEnabled(account, cfg)
@@ -186,7 +196,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
Array.from(knownIds.values()).map(async (id) => {
const abort = store.aborts.get(id);
const task = store.tasks.get(id);
if (!abort && !task && !plugin?.gateway?.stopAccount) return;
if (!abort && !task && !plugin?.gateway?.stopAccount) {
return;
}
abort?.abort();
if (plugin?.gateway?.stopAccount) {
const account = plugin.config.resolveAccount(cfg, id);
@@ -225,7 +237,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
const markChannelLoggedOut = (channelId: ChannelId, cleared: boolean, accountId?: string) => {
const plugin = getChannelPlugin(channelId);
if (!plugin) return;
if (!plugin) {
return;
}
const cfg = loadConfig();
const resolvedId =
accountId ??
+30 -10
View File
@@ -11,7 +11,9 @@ import { formatForLog } from "./ws-log.js";
*/
function shouldSuppressHeartbeatBroadcast(runId: string): boolean {
const runContext = getAgentRunContext(runId);
if (!runContext?.isHeartbeat) return false;
if (!runContext?.isHeartbeat) {
return false;
}
try {
const cfg = loadConfig();
@@ -52,22 +54,32 @@ export function createChatRunRegistry(): ChatRunRegistry {
const shift = (sessionId: string) => {
const queue = chatRunSessions.get(sessionId);
if (!queue || queue.length === 0) return undefined;
if (!queue || queue.length === 0) {
return undefined;
}
const entry = queue.shift();
if (!queue.length) chatRunSessions.delete(sessionId);
if (!queue.length) {
chatRunSessions.delete(sessionId);
}
return entry;
};
const remove = (sessionId: string, clientRunId: string, sessionKey?: string) => {
const queue = chatRunSessions.get(sessionId);
if (!queue || queue.length === 0) return undefined;
if (!queue || queue.length === 0) {
return undefined;
}
const idx = queue.findIndex(
(entry) =>
entry.clientRunId === clientRunId && (sessionKey ? entry.sessionKey === sessionKey : true),
);
if (idx < 0) return undefined;
if (idx < 0) {
return undefined;
}
const [entry] = queue.splice(idx, 1);
if (!queue.length) chatRunSessions.delete(sessionId);
if (!queue.length) {
chatRunSessions.delete(sessionId);
}
return entry;
};
@@ -137,7 +149,9 @@ export function createAgentEventHandler({
chatRunState.buffers.set(clientRunId, text);
const now = Date.now();
const last = chatRunState.deltaSentAt.get(clientRunId) ?? 0;
if (now - last < 150) return;
if (now - last < 150) {
return;
}
chatRunState.deltaSentAt.set(clientRunId, now);
const payload = {
runId: clientRunId,
@@ -202,12 +216,18 @@ export function createAgentEventHandler({
const shouldEmitToolEvents = (runId: string, sessionKey?: string) => {
const runContext = getAgentRunContext(runId);
const runVerbose = normalizeVerboseLevel(runContext?.verboseLevel);
if (runVerbose) return runVerbose === "on";
if (!sessionKey) return false;
if (runVerbose) {
return runVerbose === "on";
}
if (!sessionKey) {
return false;
}
try {
const { cfg, entry } = loadSessionEntry(sessionKey);
const sessionVerbose = normalizeVerboseLevel(entry?.verboseLevel);
if (sessionVerbose) return sessionVerbose === "on";
if (sessionVerbose) {
return sessionVerbose === "on";
}
const defaultVerbose = normalizeVerboseLevel(cfg.agents?.defaults?.verboseDefault);
return defaultVerbose === "on";
} catch {
+6 -2
View File
@@ -7,7 +7,9 @@ let maxChatHistoryMessagesBytes = DEFAULT_MAX_CHAT_HISTORY_MESSAGES_BYTES;
export const getMaxChatHistoryMessagesBytes = () => maxChatHistoryMessagesBytes;
export const __setMaxChatHistoryMessagesBytesForTest = (value?: number) => {
if (!process.env.VITEST && process.env.NODE_ENV !== "test") return;
if (!process.env.VITEST && process.env.NODE_ENV !== "test") {
return;
}
if (value === undefined) {
maxChatHistoryMessagesBytes = DEFAULT_MAX_CHAT_HISTORY_MESSAGES_BYTES;
return;
@@ -20,7 +22,9 @@ export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
export const getHandshakeTimeoutMs = () => {
if (process.env.VITEST && process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS) {
const parsed = Number(process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
}
return DEFAULT_HANDSHAKE_TIMEOUT_MS;
};
+24 -8
View File
@@ -13,15 +13,21 @@ export type ResolveBonjourCliPathOptions = {
export function formatBonjourInstanceName(displayName: string) {
const trimmed = displayName.trim();
if (!trimmed) return "OpenClaw";
if (/openclaw/i.test(trimmed)) return trimmed;
if (!trimmed) {
return "OpenClaw";
}
if (/openclaw/i.test(trimmed)) {
return trimmed;
}
return `${trimmed} (OpenClaw)`;
}
export function resolveBonjourCliPath(opts: ResolveBonjourCliPathOptions = {}): string | undefined {
const env = opts.env ?? process.env;
const envPath = env.OPENCLAW_CLI_PATH?.trim();
if (envPath) return envPath;
if (envPath) {
return envPath;
}
const statSync = opts.statSync ?? fs.statSync;
const isFile = (candidate: string) => {
@@ -35,7 +41,9 @@ export function resolveBonjourCliPath(opts: ResolveBonjourCliPathOptions = {}):
const execPath = opts.execPath ?? process.execPath;
const execDir = path.dirname(execPath);
const siblingCli = path.join(execDir, "openclaw");
if (isFile(siblingCli)) return siblingCli;
if (isFile(siblingCli)) {
return siblingCli;
}
const argv = opts.argv ?? process.argv;
const argvPath = argv[1];
@@ -45,9 +53,13 @@ export function resolveBonjourCliPath(opts: ResolveBonjourCliPathOptions = {}):
const cwd = opts.cwd ?? process.cwd();
const distCli = path.join(cwd, "dist", "index.js");
if (isFile(distCli)) return distCli;
if (isFile(distCli)) {
return distCli;
}
const binCli = path.join(cwd, "bin", "openclaw");
if (isFile(binCli)) return binCli;
if (isFile(binCli)) {
return binCli;
}
return undefined;
}
@@ -60,8 +72,12 @@ export async function resolveTailnetDnsHint(opts?: {
const env = opts?.env ?? process.env;
const envRaw = env.OPENCLAW_TAILNET_DNS?.trim();
const envValue = envRaw && envRaw.length > 0 ? envRaw.replace(/\.$/, "") : "";
if (envValue) return envValue;
if (opts?.enabled === false) return undefined;
if (envValue) {
return envValue;
}
if (opts?.enabled === false) {
return undefined;
}
const exec =
opts?.exec ??
+34 -13
View File
@@ -69,7 +69,9 @@ export function createHooksRequestHandler(
const { getHooksConfig, bindHost, port, logHooks, dispatchAgentHook, dispatchWakeHook } = opts;
return async (req, res) => {
const hooksConfig = getHooksConfig();
if (!hooksConfig) return false;
if (!hooksConfig) {
return false;
}
const url = new URL(req.url ?? "/", `http://${bindHost}:${port}`);
const basePath = hooksConfig.basePath;
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
@@ -233,21 +235,30 @@ export function createGatewayHttpServer(opts: {
async function handleRequest(req: IncomingMessage, res: ServerResponse) {
// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") return;
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") {
return;
}
try {
const configSnapshot = loadConfig();
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
if (await handleHooksRequest(req, res)) return;
if (await handleHooksRequest(req, res)) {
return;
}
if (
await handleToolsInvokeHttpRequest(req, res, {
auth: resolvedAuth,
trustedProxies,
})
)
) {
return;
if (await handleSlackHttpRequest(req, res)) return;
if (handlePluginRequest && (await handlePluginRequest(req, res))) return;
}
if (await handleSlackHttpRequest(req, res)) {
return;
}
if (handlePluginRequest && (await handlePluginRequest(req, res))) {
return;
}
if (openResponsesEnabled) {
if (
await handleOpenResponsesHttpRequest(req, res, {
@@ -255,8 +266,9 @@ export function createGatewayHttpServer(opts: {
config: openResponsesConfig,
trustedProxies,
})
)
) {
return;
}
}
if (openAiChatCompletionsEnabled) {
if (
@@ -264,12 +276,17 @@ export function createGatewayHttpServer(opts: {
auth: resolvedAuth,
trustedProxies,
})
)
) {
return;
}
}
if (canvasHost) {
if (await handleA2uiHttpRequest(req, res)) return;
if (await canvasHost.handleHttpRequest(req, res)) return;
if (await handleA2uiHttpRequest(req, res)) {
return;
}
if (await canvasHost.handleHttpRequest(req, res)) {
return;
}
}
if (controlUiEnabled) {
if (
@@ -277,15 +294,17 @@ export function createGatewayHttpServer(opts: {
basePath: controlUiBasePath,
resolveAvatar: (agentId) => resolveAgentAvatar(configSnapshot, agentId),
})
)
) {
return;
}
if (
handleControlUiHttpRequest(req, res, {
basePath: controlUiBasePath,
config: configSnapshot,
})
)
) {
return;
}
}
res.statusCode = 404;
@@ -308,7 +327,9 @@ export function attachGatewayUpgradeHandler(opts: {
}) {
const { httpServer, wss, canvasHost } = opts;
httpServer.on("upgrade", (req, socket, head) => {
if (canvasHost?.handleUpgrade(req, socket, head)) return;
if (canvasHost?.handleUpgrade(req, socket, head)) {
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
+9 -3
View File
@@ -75,7 +75,9 @@ export function startGatewayMaintenanceTimers(params: {
const dedupeCleanup = setInterval(() => {
const now = Date.now();
for (const [k, v] of params.dedupe) {
if (now - v.ts > DEDUPE_TTL_MS) params.dedupe.delete(k);
if (now - v.ts > DEDUPE_TTL_MS) {
params.dedupe.delete(k);
}
}
if (params.dedupe.size > DEDUPE_MAX) {
const entries = [...params.dedupe.entries()].toSorted((a, b) => a[1].ts - b[1].ts);
@@ -85,7 +87,9 @@ export function startGatewayMaintenanceTimers(params: {
}
for (const [runId, entry] of params.chatAbortControllers) {
if (now <= entry.expiresAtMs) continue;
if (now <= entry.expiresAtMs) {
continue;
}
abortChatRunById(
{
chatAbortControllers: params.chatAbortControllers,
@@ -103,7 +107,9 @@ export function startGatewayMaintenanceTimers(params: {
const ABORTED_RUN_TTL_MS = 60 * 60_000;
for (const [runId, abortedAt] of params.chatRunState.abortedRuns) {
if (now - abortedAt <= ABORTED_RUN_TTL_MS) continue;
if (now - abortedAt <= ABORTED_RUN_TTL_MS) {
continue;
}
params.chatRunState.abortedRuns.delete(runId);
params.chatRunBuffers.delete(runId);
params.chatDeltaSentAt.delete(runId);
+21 -7
View File
@@ -91,11 +91,15 @@ const WRITE_METHODS = new Set([
]);
function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["client"]) {
if (!client?.connect) return null;
if (!client?.connect) {
return null;
}
const role = client.connect.role ?? "operator";
const scopes = client.connect.scopes ?? [];
if (NODE_ROLE_METHODS.has(method)) {
if (role === "node") return null;
if (role === "node") {
return null;
}
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
}
if (role === "node") {
@@ -104,7 +108,9 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c
if (role !== "operator") {
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
}
if (scopes.includes(ADMIN_SCOPE)) return null;
if (scopes.includes(ADMIN_SCOPE)) {
return null;
}
if (APPROVAL_METHODS.has(method) && !scopes.includes(APPROVALS_SCOPE)) {
return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.approvals");
}
@@ -117,10 +123,18 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c
if (WRITE_METHODS.has(method) && !scopes.includes(WRITE_SCOPE)) {
return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.write");
}
if (APPROVAL_METHODS.has(method)) return null;
if (PAIRING_METHODS.has(method)) return null;
if (READ_METHODS.has(method)) return null;
if (WRITE_METHODS.has(method)) return null;
if (APPROVAL_METHODS.has(method)) {
return null;
}
if (PAIRING_METHODS.has(method)) {
return null;
}
if (READ_METHODS.has(method)) {
return null;
}
if (WRITE_METHODS.has(method)) {
return null;
}
if (ADMIN_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix))) {
return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin");
}
+30 -10
View File
@@ -28,18 +28,26 @@ function recordAgentRunSnapshot(entry: AgentRunSnapshot) {
}
function ensureAgentRunListener() {
if (agentRunListenerStarted) return;
if (agentRunListenerStarted) {
return;
}
agentRunListenerStarted = true;
onAgentEvent((evt) => {
if (!evt) return;
if (evt.stream !== "lifecycle") return;
if (!evt) {
return;
}
if (evt.stream !== "lifecycle") {
return;
}
const phase = evt.data?.phase;
if (phase === "start") {
const startedAt = typeof evt.data?.startedAt === "number" ? evt.data.startedAt : undefined;
agentRunStarts.set(evt.runId, startedAt ?? Date.now());
return;
}
if (phase !== "end" && phase !== "error") return;
if (phase !== "end" && phase !== "error") {
return;
}
const startedAt =
typeof evt.data?.startedAt === "number" ? evt.data.startedAt : agentRunStarts.get(evt.runId);
const endedAt = typeof evt.data?.endedAt === "number" ? evt.data.endedAt : undefined;
@@ -68,23 +76,35 @@ export async function waitForAgentJob(params: {
const { runId, timeoutMs } = params;
ensureAgentRunListener();
const cached = getCachedAgentRun(runId);
if (cached) return cached;
if (timeoutMs <= 0) return null;
if (cached) {
return cached;
}
if (timeoutMs <= 0) {
return null;
}
return await new Promise((resolve) => {
let settled = false;
const finish = (entry: AgentRunSnapshot | null) => {
if (settled) return;
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
unsubscribe();
resolve(entry);
};
const unsubscribe = onAgentEvent((evt) => {
if (!evt || evt.stream !== "lifecycle") return;
if (evt.runId !== runId) return;
if (!evt || evt.stream !== "lifecycle") {
return;
}
if (evt.runId !== runId) {
return;
}
const phase = evt.data?.phase;
if (phase !== "end" && phase !== "error") return;
if (phase !== "end" && phase !== "error") {
return;
}
const cached = getCachedAgentRun(runId);
if (cached) {
finish(cached);
+36 -12
View File
@@ -46,18 +46,32 @@ function normalizeNodeKey(value: string) {
function resolveBrowserNode(nodes: NodeSession[], query: string): NodeSession | null {
const q = query.trim();
if (!q) return null;
if (!q) {
return null;
}
const qNorm = normalizeNodeKey(q);
const matches = nodes.filter((node) => {
if (node.nodeId === q) return true;
if (typeof node.remoteIp === "string" && node.remoteIp === q) return true;
if (node.nodeId === q) {
return true;
}
if (typeof node.remoteIp === "string" && node.remoteIp === q) {
return true;
}
const name = typeof node.displayName === "string" ? node.displayName : "";
if (name && normalizeNodeKey(name) === qNorm) return true;
if (q.length >= 6 && node.nodeId.startsWith(q)) return true;
if (name && normalizeNodeKey(name) === qNorm) {
return true;
}
if (q.length >= 6 && node.nodeId.startsWith(q)) {
return true;
}
return false;
});
if (matches.length === 1) return matches[0] ?? null;
if (matches.length === 0) return null;
if (matches.length === 1) {
return matches[0] ?? null;
}
if (matches.length === 0) {
return null;
}
throw new Error(
`ambiguous node: ${q} (matches: ${matches
.map((node) => node.displayName || node.remoteIp || node.nodeId)
@@ -71,7 +85,9 @@ function resolveBrowserNodeTarget(params: {
}): NodeSession | null {
const policy = params.cfg.gateway?.nodes?.browser;
const mode = policy?.mode ?? "auto";
if (mode === "off") return null;
if (mode === "off") {
return null;
}
const browserNodes = params.nodes.filter((node) => isBrowserNode(node));
if (browserNodes.length === 0) {
if (policy?.node?.trim()) {
@@ -87,13 +103,19 @@ function resolveBrowserNodeTarget(params: {
}
return resolved;
}
if (mode === "manual") return null;
if (browserNodes.length === 1) return browserNodes[0] ?? null;
if (mode === "manual") {
return null;
}
if (browserNodes.length === 1) {
return browserNodes[0] ?? null;
}
return null;
}
async function persistProxyFiles(files: BrowserProxyFile[] | undefined) {
if (!files || files.length === 0) return new Map<string, string>();
if (!files || files.length === 0) {
return new Map<string, string>();
}
const mapping = new Map<string, string>();
for (const file of files) {
const buffer = Buffer.from(file.base64, "base64");
@@ -104,7 +126,9 @@ async function persistProxyFiles(files: BrowserProxyFile[] | undefined) {
}
function applyProxyPaths(result: unknown, mapping: Map<string, string>) {
if (!result || typeof result !== "object") return;
if (!result || typeof result !== "object") {
return;
}
const obj = result as Record<string, unknown>;
if (typeof obj.path === "string" && mapping.has(obj.path)) {
obj.path = mapping.get(obj.path);
+6 -2
View File
@@ -98,7 +98,9 @@ export const channelsHandlers: GatewayRequestHandlers = {
const defaultRuntime = runtime.channels[channelId];
const raw =
accounts?.[accountId] ?? (accountId === defaultAccountId ? defaultRuntime : undefined);
if (!raw) return undefined;
if (!raw) {
return undefined;
}
return raw;
};
@@ -171,7 +173,9 @@ export const channelsHandlers: GatewayRequestHandlers = {
probe: probeResult,
audit: auditResult,
});
if (lastProbeAt) snapshot.lastProbeAt = lastProbeAt;
if (lastProbeAt) {
snapshot.lastProbeAt = lastProbeAt;
}
const activity = getChannelActivity({
channel: channelId as never,
accountId,
+15 -5
View File
@@ -56,8 +56,12 @@ function resolveTranscriptPath(params: {
sessionFile?: string;
}): string | null {
const { sessionId, storePath, sessionFile } = params;
if (sessionFile) return sessionFile;
if (!storePath) return null;
if (sessionFile) {
return sessionFile;
}
if (!storePath) {
return null;
}
return path.join(path.dirname(storePath), `${sessionId}.jsonl`);
}
@@ -65,7 +69,9 @@ function ensureTranscriptFile(params: { transcriptPath: string; sessionId: strin
ok: boolean;
error?: string;
} {
if (fs.existsSync(params.transcriptPath)) return { ok: true };
if (fs.existsSync(params.transcriptPath)) {
return { ok: true };
}
try {
fs.mkdirSync(path.dirname(params.transcriptPath), { recursive: true });
const header = {
@@ -476,9 +482,13 @@ export const chatHandlers: GatewayRequestHandlers = {
context.logGateway.warn(`webchat dispatch failed: ${formatForLog(err)}`);
},
deliver: async (payload, info) => {
if (info.kind !== "final") return;
if (info.kind !== "final") {
return;
}
const text = payload.text?.trim() ?? "";
if (!text) return;
if (!text) {
return;
}
finalReplyParts.push(text);
},
});
+6 -2
View File
@@ -33,7 +33,9 @@ import type { GatewayRequestHandlers, RespondFn } from "./types.js";
function resolveBaseHash(params: unknown): string | null {
const raw = (params as { baseHash?: unknown })?.baseHash;
if (typeof raw !== "string") return null;
if (typeof raw !== "string") {
return null;
}
const trimmed = raw.trim();
return trimmed ? trimmed : null;
}
@@ -43,7 +45,9 @@ function requireConfigBaseHash(
snapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>,
respond: RespondFn,
): boolean {
if (!snapshot.exists) return true;
if (!snapshot.exists) {
return true;
}
const snapshotHash = resolveConfigSnapshotHash(snapshot);
if (!snapshotHash) {
respond(
@@ -117,7 +117,9 @@ describe("exec approval handlers", () => {
const context = {
broadcast: (event: string, payload: unknown) => {
if (event !== "exec.approval.requested") return;
if (event !== "exec.approval.requested") {
return;
}
const id = (payload as { id?: string })?.id ?? "";
void handlers["exec.approval.resolve"]({
params: { id, decision: "allow-once" },
+6 -2
View File
@@ -21,7 +21,9 @@ import type { GatewayRequestHandlers, RespondFn } from "./types.js";
function resolveBaseHash(params: unknown): string | null {
const raw = (params as { baseHash?: unknown })?.baseHash;
if (typeof raw !== "string") return null;
if (typeof raw !== "string") {
return null;
}
const trimmed = raw.trim();
return trimmed ? trimmed : null;
}
@@ -31,7 +33,9 @@ function requireApprovalsBaseHash(
snapshot: ExecApprovalsSnapshot,
respond: RespondFn,
): boolean {
if (!snapshot.exists) return true;
if (!snapshot.exists) {
return true;
}
if (!snapshot.hash) {
respond(
false,
+9 -3
View File
@@ -25,12 +25,18 @@ function isRollingLogFile(file: string): boolean {
async function resolveLogFile(file: string): Promise<string> {
const stat = await fs.stat(file).catch(() => null);
if (stat) return file;
if (!isRollingLogFile(file)) return file;
if (stat) {
return file;
}
if (!isRollingLogFile(file)) {
return file;
}
const dir = path.dirname(file);
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => null);
if (!entries) return file;
if (!entries) {
return file;
}
const candidates = await Promise.all(
entries
+6 -2
View File
@@ -38,9 +38,13 @@ export function uniqueSortedStrings(values: unknown[]) {
}
export function safeParseJson(value: string | null | undefined): unknown {
if (typeof value !== "string") return undefined;
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) return undefined;
if (!trimmed) {
return undefined;
}
try {
return JSON.parse(trimmed) as unknown;
} catch {
+18 -6
View File
@@ -33,13 +33,19 @@ import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-comma
import type { GatewayRequestHandlers } from "./types.js";
function isNodeEntry(entry: { role?: string; roles?: string[] }) {
if (entry.role === "node") return true;
if (Array.isArray(entry.roles) && entry.roles.includes("node")) return true;
if (entry.role === "node") {
return true;
}
if (Array.isArray(entry.roles) && entry.roles.includes("node")) {
return true;
}
return false;
}
function normalizeNodeInvokeResultParams(params: unknown): unknown {
if (!params || typeof params !== "object") return params;
if (!params || typeof params !== "object") {
return params;
}
const raw = params as Record<string, unknown>;
const normalized: Record<string, unknown> = { ...raw };
if (normalized.payloadJSON === null) {
@@ -284,11 +290,17 @@ export const nodeHandlers: GatewayRequestHandlers = {
});
nodes.sort((a, b) => {
if (a.connected !== b.connected) return a.connected ? -1 : 1;
if (a.connected !== b.connected) {
return a.connected ? -1 : 1;
}
const an = (a.displayName ?? a.nodeId).toLowerCase();
const bn = (b.displayName ?? b.nodeId).toLowerCase();
if (an < bn) return -1;
if (an > bn) return 1;
if (an < bn) {
return -1;
}
if (an > bn) {
return 1;
}
return a.nodeId.localeCompare(b.nodeId);
});
+21 -7
View File
@@ -199,9 +199,15 @@ export const sendHandlers: GatewayRequestHandlers = {
messageId: result.messageId,
channel,
};
if ("chatId" in result) payload.chatId = result.chatId;
if ("channelId" in result) payload.channelId = result.channelId;
if ("toJid" in result) payload.toJid = result.toJid;
if ("chatId" in result) {
payload.chatId = result.chatId;
}
if ("channelId" in result) {
payload.channelId = result.channelId;
}
if ("toJid" in result) {
payload.toJid = result.toJid;
}
if ("conversationId" in result) {
payload.conversationId = result.conversationId;
}
@@ -324,10 +330,18 @@ export const sendHandlers: GatewayRequestHandlers = {
messageId: result.messageId,
channel,
};
if (result.toJid) payload.toJid = result.toJid;
if (result.channelId) payload.channelId = result.channelId;
if (result.conversationId) payload.conversationId = result.conversationId;
if (result.pollId) payload.pollId = result.pollId;
if (result.toJid) {
payload.toJid = result.toJid;
}
if (result.channelId) {
payload.channelId = result.channelId;
}
if (result.conversationId) {
payload.conversationId = result.conversationId;
}
if (result.pollId) {
payload.pollId = result.pollId;
}
context.dedupe.set(`poll:${idem}`, {
ts: Date.now(),
ok: true,
+12 -4
View File
@@ -300,7 +300,9 @@ export const sessionsHandlers: GatewayRequestHandlers = {
const existed = Boolean(entry);
const queueKeys = new Set<string>(target.storeKeys);
queueKeys.add(target.canonicalKey);
if (sessionId) queueKeys.add(sessionId);
if (sessionId) {
queueKeys.add(sessionId);
}
clearSessionQueues([...queueKeys]);
stopSubagentsForRequester({ cfg, requesterSessionKey: target.canonicalKey });
if (sessionId) {
@@ -325,7 +327,9 @@ export const sessionsHandlers: GatewayRequestHandlers = {
store[primaryKey] = store[existingKey];
delete store[existingKey];
}
if (store[primaryKey]) delete store[primaryKey];
if (store[primaryKey]) {
delete store[primaryKey];
}
});
const archived: string[] = [];
@@ -336,7 +340,9 @@ export const sessionsHandlers: GatewayRequestHandlers = {
entry?.sessionFile,
target.agentId,
)) {
if (!fs.existsSync(candidate)) continue;
if (!fs.existsSync(candidate)) {
continue;
}
try {
archived.push(archiveFileOnDisk(candidate, "deleted"));
} catch {
@@ -443,7 +449,9 @@ export const sessionsHandlers: GatewayRequestHandlers = {
await updateSessionStore(storePath, (store) => {
const entryKey = compactTarget.primaryKey;
const entryToUpdate = store[entryKey];
if (!entryToUpdate) return;
if (!entryToUpdate) {
return;
}
delete entryToUpdate.inputTokens;
delete entryToUpdate.outputTokens;
delete entryToUpdate.totalTokens;
+25 -9
View File
@@ -38,17 +38,23 @@ function collectSkillBins(entries: SkillEntry[]): string[] {
const install = entry.metadata?.install ?? [];
for (const bin of required) {
const trimmed = bin.trim();
if (trimmed) bins.add(trimmed);
if (trimmed) {
bins.add(trimmed);
}
}
for (const bin of anyBins) {
const trimmed = bin.trim();
if (trimmed) bins.add(trimmed);
if (trimmed) {
bins.add(trimmed);
}
}
for (const spec of install) {
const specBins = spec?.bins ?? [];
for (const bin of specBins) {
const trimmed = String(bin).trim();
if (trimmed) bins.add(trimmed);
if (trimmed) {
bins.add(trimmed);
}
}
}
}
@@ -93,7 +99,9 @@ export const skillsHandlers: GatewayRequestHandlers = {
const bins = new Set<string>();
for (const workspaceDir of workspaceDirs) {
const entries = loadWorkspaceSkillEntries(workspaceDir, { config: cfg });
for (const bin of collectSkillBins(entries)) bins.add(bin);
for (const bin of collectSkillBins(entries)) {
bins.add(bin);
}
}
respond(true, { bins: [...bins].toSorted() }, undefined);
},
@@ -156,17 +164,25 @@ export const skillsHandlers: GatewayRequestHandlers = {
}
if (typeof p.apiKey === "string") {
const trimmed = p.apiKey.trim();
if (trimmed) current.apiKey = trimmed;
else delete current.apiKey;
if (trimmed) {
current.apiKey = trimmed;
} else {
delete current.apiKey;
}
}
if (p.env && typeof p.env === "object") {
const nextEnv = current.env ? { ...current.env } : {};
for (const [key, value] of Object.entries(p.env)) {
const trimmedKey = key.trim();
if (!trimmedKey) continue;
if (!trimmedKey) {
continue;
}
const trimmedVal = value.trim();
if (!trimmedVal) delete nextEnv[trimmedKey];
else nextEnv[trimmedKey] = trimmedVal;
if (!trimmedVal) {
delete nextEnv[trimmedKey];
} else {
nextEnv[trimmedKey] = trimmedVal;
}
}
current.env = nextEnv;
}
+15 -5
View File
@@ -15,10 +15,14 @@ type CostUsageCacheEntry = {
const costUsageCache = new Map<number, CostUsageCacheEntry>();
const parseDays = (raw: unknown): number => {
if (typeof raw === "number" && Number.isFinite(raw)) return Math.floor(raw);
if (typeof raw === "number" && Number.isFinite(raw)) {
return Math.floor(raw);
}
if (typeof raw === "string" && raw.trim() !== "") {
const parsed = Number(raw);
if (Number.isFinite(parsed)) return Math.floor(parsed);
if (Number.isFinite(parsed)) {
return Math.floor(parsed);
}
}
return 30;
};
@@ -35,7 +39,9 @@ async function loadCostUsageSummaryCached(params: {
}
if (cached?.inFlight) {
if (cached.summary) return cached.summary;
if (cached.summary) {
return cached.summary;
}
return await cached.inFlight;
}
@@ -46,7 +52,9 @@ async function loadCostUsageSummaryCached(params: {
return summary;
})
.catch((err) => {
if (entry.summary) return entry.summary;
if (entry.summary) {
return entry.summary;
}
throw err;
})
.finally(() => {
@@ -60,7 +68,9 @@ async function loadCostUsageSummaryCached(params: {
entry.inFlight = inFlight;
costUsageCache.set(days, entry);
if (entry.summary) return entry.summary;
if (entry.summary) {
return entry.summary;
}
return await inFlight;
}
+3 -1
View File
@@ -2,7 +2,9 @@ import type { NodeRegistry } from "./node-registry.js";
const isMobilePlatform = (platform: unknown): boolean => {
const p = typeof platform === "string" ? platform.trim().toLowerCase() : "";
if (!p) return false;
if (!p) {
return false;
}
return p.startsWith("ios") || p.startsWith("ipados") || p.startsWith("android");
};
+45 -15
View File
@@ -14,7 +14,9 @@ import { formatForLog } from "./ws-log.js";
export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt: NodeEvent) => {
switch (evt.event) {
case "voice.transcript": {
if (!evt.payloadJSON) return;
if (!evt.payloadJSON) {
return;
}
let payload: unknown;
try {
payload = JSON.parse(evt.payloadJSON) as unknown;
@@ -24,8 +26,12 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
const obj =
typeof payload === "object" && payload !== null ? (payload as Record<string, unknown>) : {};
const text = typeof obj.text === "string" ? obj.text.trim() : "";
if (!text) return;
if (text.length > 20_000) return;
if (!text) {
return;
}
if (text.length > 20_000) {
return;
}
const sessionKeyRaw = typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : "";
const cfg = loadConfig();
const rawMainKey = normalizeMainKey(cfg.session?.mainKey);
@@ -73,7 +79,9 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
return;
}
case "agent.request": {
if (!evt.payloadJSON) return;
if (!evt.payloadJSON) {
return;
}
type AgentDeepLink = {
message?: string;
sessionKey?: string | null;
@@ -91,8 +99,12 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
return;
}
const message = (link?.message ?? "").trim();
if (!message) return;
if (message.length > 20_000) return;
if (!message) {
return;
}
if (message.length > 20_000) {
return;
}
const channelRaw = typeof link?.channel === "string" ? link.channel.trim() : "";
const channel = normalizeChannelId(channelRaw) ?? undefined;
@@ -141,7 +153,9 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
return;
}
case "chat.subscribe": {
if (!evt.payloadJSON) return;
if (!evt.payloadJSON) {
return;
}
let payload: unknown;
try {
payload = JSON.parse(evt.payloadJSON) as unknown;
@@ -151,12 +165,16 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
const obj =
typeof payload === "object" && payload !== null ? (payload as Record<string, unknown>) : {};
const sessionKey = typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : "";
if (!sessionKey) return;
if (!sessionKey) {
return;
}
ctx.nodeSubscribe(nodeId, sessionKey);
return;
}
case "chat.unsubscribe": {
if (!evt.payloadJSON) return;
if (!evt.payloadJSON) {
return;
}
let payload: unknown;
try {
payload = JSON.parse(evt.payloadJSON) as unknown;
@@ -166,14 +184,18 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
const obj =
typeof payload === "object" && payload !== null ? (payload as Record<string, unknown>) : {};
const sessionKey = typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : "";
if (!sessionKey) return;
if (!sessionKey) {
return;
}
ctx.nodeUnsubscribe(nodeId, sessionKey);
return;
}
case "exec.started":
case "exec.finished":
case "exec.denied": {
if (!evt.payloadJSON) return;
if (!evt.payloadJSON) {
return;
}
let payload: unknown;
try {
payload = JSON.parse(evt.payloadJSON) as unknown;
@@ -184,7 +206,9 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
typeof payload === "object" && payload !== null ? (payload as Record<string, unknown>) : {};
const sessionKey =
typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : `node-${nodeId}`;
if (!sessionKey) return;
if (!sessionKey) {
return;
}
const runId = typeof obj.runId === "string" ? obj.runId.trim() : "";
const command = typeof obj.command === "string" ? obj.command.trim() : "";
const exitCode =
@@ -198,14 +222,20 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
let text = "";
if (evt.event === "exec.started") {
text = `Exec started (node=${nodeId}${runId ? ` id=${runId}` : ""})`;
if (command) text += `: ${command}`;
if (command) {
text += `: ${command}`;
}
} else if (evt.event === "exec.finished") {
const exitLabel = timedOut ? "timeout" : `code ${exitCode ?? "?"}`;
text = `Exec finished (node=${nodeId}${runId ? ` id=${runId}` : ""}, ${exitLabel})`;
if (output) text += `\n${output}`;
if (output) {
text += `\n${output}`;
}
} else {
text = `Exec denied (node=${nodeId}${runId ? ` id=${runId}` : ""}${reason ? `, ${reason}` : ""})`;
if (command) text += `: ${command}`;
if (command) {
text += `: ${command}`;
}
}
enqueueSystemEvent(text, { sessionKey, contextKey: runId ? `exec:${runId}` : "exec" });
+33 -11
View File
@@ -39,14 +39,18 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager {
const subscribe = (nodeId: string, sessionKey: string) => {
const normalizedNodeId = nodeId.trim();
const normalizedSessionKey = sessionKey.trim();
if (!normalizedNodeId || !normalizedSessionKey) return;
if (!normalizedNodeId || !normalizedSessionKey) {
return;
}
let nodeSet = nodeSubscriptions.get(normalizedNodeId);
if (!nodeSet) {
nodeSet = new Set<string>();
nodeSubscriptions.set(normalizedNodeId, nodeSet);
}
if (nodeSet.has(normalizedSessionKey)) return;
if (nodeSet.has(normalizedSessionKey)) {
return;
}
nodeSet.add(normalizedSessionKey);
let sessionSet = sessionSubscribers.get(normalizedSessionKey);
@@ -60,25 +64,35 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager {
const unsubscribe = (nodeId: string, sessionKey: string) => {
const normalizedNodeId = nodeId.trim();
const normalizedSessionKey = sessionKey.trim();
if (!normalizedNodeId || !normalizedSessionKey) return;
if (!normalizedNodeId || !normalizedSessionKey) {
return;
}
const nodeSet = nodeSubscriptions.get(normalizedNodeId);
nodeSet?.delete(normalizedSessionKey);
if (nodeSet?.size === 0) nodeSubscriptions.delete(normalizedNodeId);
if (nodeSet?.size === 0) {
nodeSubscriptions.delete(normalizedNodeId);
}
const sessionSet = sessionSubscribers.get(normalizedSessionKey);
sessionSet?.delete(normalizedNodeId);
if (sessionSet?.size === 0) sessionSubscribers.delete(normalizedSessionKey);
if (sessionSet?.size === 0) {
sessionSubscribers.delete(normalizedSessionKey);
}
};
const unsubscribeAll = (nodeId: string) => {
const normalizedNodeId = nodeId.trim();
const nodeSet = nodeSubscriptions.get(normalizedNodeId);
if (!nodeSet) return;
if (!nodeSet) {
return;
}
for (const sessionKey of nodeSet) {
const sessionSet = sessionSubscribers.get(sessionKey);
sessionSet?.delete(normalizedNodeId);
if (sessionSet?.size === 0) sessionSubscribers.delete(sessionKey);
if (sessionSet?.size === 0) {
sessionSubscribers.delete(sessionKey);
}
}
nodeSubscriptions.delete(normalizedNodeId);
};
@@ -90,9 +104,13 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager {
sendEvent?: NodeSendEventFn | null,
) => {
const normalizedSessionKey = sessionKey.trim();
if (!normalizedSessionKey || !sendEvent) return;
if (!normalizedSessionKey || !sendEvent) {
return;
}
const subs = sessionSubscribers.get(normalizedSessionKey);
if (!subs || subs.size === 0) return;
if (!subs || subs.size === 0) {
return;
}
const payloadJSON = toPayloadJSON(payload);
for (const nodeId of subs) {
@@ -105,7 +123,9 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager {
payload: unknown,
sendEvent?: NodeSendEventFn | null,
) => {
if (!sendEvent) return;
if (!sendEvent) {
return;
}
const payloadJSON = toPayloadJSON(payload);
for (const nodeId of nodeSubscriptions.keys()) {
sendEvent({ nodeId, event, payloadJSON });
@@ -118,7 +138,9 @@ export function createNodeSubscriptionManager(): NodeSubscriptionManager {
listConnected?: NodeListConnectedFn | null,
sendEvent?: NodeSendEventFn | null,
) => {
if (!sendEvent || !listConnected) return;
if (!sendEvent || !listConnected) {
return;
}
const payloadJSON = toPayloadJSON(payload);
for (const node of listConnected()) {
sendEvent({ nodeId: node.nodeId, event, payloadJSON });
+3 -1
View File
@@ -16,7 +16,9 @@ import { loadSessionEntry } from "./session-utils.js";
export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) {
const sentinel = await consumeRestartSentinel();
if (!sentinel) return;
if (!sentinel) {
return;
}
const payload = sentinel.payload;
const sessionKey = payload.sessionKey?.trim();
const message = formatRestartSentinelMessage(payload);
+3 -1
View File
@@ -129,7 +129,9 @@ export async function createGatewayRuntimeState(params: {
httpServers.push(httpServer);
httpBindHosts.push(host);
} catch (err) {
if (host === bindHosts[0]) throw err;
if (host === bindHosts[0]) {
throw err;
}
params.log.warn(
`gateway: failed to bind loopback alias ${host}:${params.port} (${String(err)})`,
);
+3 -1
View File
@@ -5,7 +5,9 @@ import { toAgentRequestSessionKey } from "../routing/session-key.js";
export function resolveSessionKeyForRun(runId: string) {
const cached = getAgentRunContext(runId)?.sessionKey;
if (cached) return cached;
if (cached) {
return cached;
}
const cfg = loadConfig();
const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath);
+6 -2
View File
@@ -11,8 +11,12 @@ export function normalizeVoiceWakeTriggers(input: unknown): string[] {
}
export function formatError(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === "string") return err;
if (err instanceof Error) {
return err.message;
}
if (typeof err === "string") {
return err;
}
const statusValue = (err as { status?: unknown })?.status;
const codeValue = (err as { code?: unknown })?.code;
const hasStatus = statusValue !== undefined;
+9 -3
View File
@@ -5,15 +5,21 @@ export function createWizardSessionTracker() {
const findRunningWizard = (): string | null => {
for (const [id, session] of wizardSessions) {
if (session.getStatus() === "running") return id;
if (session.getStatus() === "running") {
return id;
}
}
return null;
};
const purgeWizardSession = (id: string) => {
const session = wizardSessions.get(id);
if (!session) return;
if (session.getStatus() === "running") return;
if (!session) {
return;
}
if (session.getStatus() === "running") {
return;
}
wizardSessions.delete(id);
};
@@ -113,9 +113,13 @@ const createStubChannelPlugin = (params: {
deliveryMode: "direct",
resolveTarget: ({ to, allowFrom }) => {
const trimmed = to?.trim() ?? "";
if (trimmed) return { ok: true, to: trimmed };
if (trimmed) {
return { ok: true, to: trimmed };
}
const first = allowFrom?.[0];
if (first) return { ok: true, to: String(first) };
if (first) {
return { ok: true, to: String(first) };
}
return {
ok: false,
error: new Error(`missing target for ${params.id}`),
@@ -422,7 +422,9 @@ describe("gateway server agent", () => {
const finalChatP = onceMessage(
webchatWs,
(o) => {
if (o.type !== "event" || o.event !== "chat") return false;
if (o.type !== "event" || o.event !== "chat") {
return false;
}
const payload = o.payload as { state?: unknown; runId?: unknown } | undefined;
return payload?.state === "final" && payload.runId === "run-auto-1";
},
+3 -1
View File
@@ -17,7 +17,9 @@ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-cha
installGatewayTestHooks({ scope: "suite" });
async function waitForWsClose(ws: WebSocket, timeoutMs: number): Promise<boolean> {
if (ws.readyState === WebSocket.CLOSED) return true;
if (ws.readyState === WebSocket.CLOSED) {
return true;
}
return await new Promise((resolve) => {
const timer = setTimeout(() => resolve(ws.readyState === WebSocket.CLOSED), timeoutMs);
ws.once("close", () => {
@@ -19,7 +19,9 @@ installGatewayTestHooks({ scope: "suite" });
async function waitFor(condition: () => boolean, timeoutMs = 1500) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (condition()) return;
if (condition()) {
return;
}
await new Promise((r) => setTimeout(r, 5));
}
throw new Error("timeout waiting for condition");
@@ -127,8 +129,12 @@ describe("gateway server chat", () => {
opts?.onAgentRunStart?.(opts.runId ?? "idem-abort-1");
const signal = opts?.abortSignal;
await new Promise<void>((resolve) => {
if (!signal) return resolve();
if (signal.aborted) return resolve();
if (!signal) {
return resolve();
}
if (signal.aborted) {
return resolve();
}
signal.addEventListener("abort", () => resolve(), { once: true });
});
});
@@ -155,9 +161,12 @@ describe("gateway server chat", () => {
await new Promise<void>((resolve, reject) => {
const deadline = Date.now() + 1000;
const tick = () => {
if (spy.mock.calls.length > callsBefore) return resolve();
if (Date.now() > deadline)
if (spy.mock.calls.length > callsBefore) {
return resolve();
}
if (Date.now() > deadline) {
return reject(new Error("timeout waiting for getReplyFromConfig"));
}
setTimeout(tick, 5);
};
tick();
@@ -183,8 +192,12 @@ describe("gateway server chat", () => {
opts?.onAgentRunStart?.(opts.runId ?? "idem-abort-save-1");
const signal = opts?.abortSignal;
await new Promise<void>((resolve) => {
if (!signal) return resolve();
if (signal.aborted) return resolve();
if (!signal) {
return resolve();
}
if (signal.aborted) {
return resolve();
}
signal.addEventListener("abort", () => resolve(), { once: true });
});
});
@@ -222,8 +235,12 @@ describe("gateway server chat", () => {
opts?.onAgentRunStart?.(opts.runId ?? "idem-stop-1");
const signal = opts?.abortSignal;
await new Promise<void>((resolve) => {
if (!signal) return resolve();
if (signal.aborted) return resolve();
if (!signal) {
return resolve();
}
if (signal.aborted) {
return resolve();
}
signal.addEventListener("abort", () => resolve(), { once: true });
});
});
@@ -303,8 +320,12 @@ describe("gateway server chat", () => {
opts?.onAgentRunStart?.(opts.runId ?? "idem-abort-all-1");
const signal = opts?.abortSignal;
await new Promise<void>((resolve) => {
if (!signal) return resolve();
if (signal.aborted) return resolve();
if (!signal) {
return resolve();
}
if (signal.aborted) {
return resolve();
}
signal.addEventListener("abort", () => resolve(), { once: true });
});
});
@@ -369,8 +390,12 @@ describe("gateway server chat", () => {
agentStartedResolve?.();
const signal = opts?.abortSignal;
await new Promise<void>((resolve) => {
if (!signal) return resolve();
if (signal.aborted) return resolve();
if (!signal) {
return resolve();
}
if (signal.aborted) {
return resolve();
}
signal.addEventListener("abort", () => resolve(), { once: true });
});
});
@@ -38,7 +38,9 @@ afterAll(async () => {
async function waitFor(condition: () => boolean, timeoutMs = 1500) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (condition()) return;
if (condition()) {
return;
}
await new Promise((r) => setTimeout(r, 5));
}
throw new Error("timeout waiting for condition");
@@ -273,11 +275,17 @@ describe("gateway server chat", () => {
expect(defaultRes.ok).toBe(true);
const defaultMsgs = defaultRes.payload?.messages ?? [];
const firstContentText = (msg: unknown): string | undefined => {
if (!msg || typeof msg !== "object") return undefined;
if (!msg || typeof msg !== "object") {
return undefined;
}
const content = (msg as { content?: unknown }).content;
if (!Array.isArray(content) || content.length === 0) return undefined;
if (!Array.isArray(content) || content.length === 0) {
return undefined;
}
const first = content[0];
if (!first || typeof first !== "object") return undefined;
if (!first || typeof first !== "object") {
return undefined;
}
const text = (first as { text?: unknown }).text;
return typeof text === "string" ? text : undefined;
};
@@ -287,7 +295,9 @@ describe("gateway server chat", () => {
testState.agentConfig = undefined;
testState.sessionStorePath = undefined;
testState.sessionConfig = undefined;
if (webchatWs) webchatWs.close();
if (webchatWs) {
webchatWs.close();
}
await Promise.all(tempDirs.map((dir) => fs.rm(dir, { recursive: true, force: true })));
}
});
+5 -2
View File
@@ -27,8 +27,11 @@ beforeAll(async () => {
afterAll(async () => {
await server.close();
if (previousToken === undefined) delete process.env.OPENCLAW_GATEWAY_TOKEN;
else process.env.OPENCLAW_GATEWAY_TOKEN = previousToken;
if (previousToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = previousToken;
}
});
const openClient = async () => {
+3 -1
View File
@@ -39,7 +39,9 @@ async function waitForNonEmptyFile(pathname: string, timeoutMs = 2000) {
const startedAt = process.hrtime.bigint();
for (;;) {
const raw = await fs.readFile(pathname, "utf-8").catch(() => "");
if (raw.trim().length > 0) return raw;
if (raw.trim().length > 0) {
return raw;
}
const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
if (elapsedMs >= timeoutMs) {
throw new Error(`timeout waiting for file ${pathname}`);
+8 -3
View File
@@ -36,8 +36,11 @@ beforeAll(async () => {
afterAll(async () => {
await server.close();
if (previousToken === undefined) delete process.env.OPENCLAW_GATEWAY_TOKEN;
else process.env.OPENCLAW_GATEWAY_TOKEN = previousToken;
if (previousToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = previousToken;
}
});
const openClient = async (opts?: Parameters<typeof connectOk>[1]) => {
@@ -210,7 +213,9 @@ describe("gateway server health/presence", () => {
expect(evt.payload?.presence?.length).toBeGreaterThan(0);
expect(typeof evt.seq).toBe("number");
}
for (const c of clients) c.close();
for (const c of clients) {
c.close();
}
});
test("presence includes client fingerprint", async () => {
+6 -2
View File
@@ -366,8 +366,12 @@ export async function startGatewayServer(
let skillsRefreshTimer: ReturnType<typeof setTimeout> | null = null;
const skillsRefreshDelayMs = 30_000;
const skillsChangeUnsub = registerSkillsChangeListener((event) => {
if (event.reason === "remote-node") return;
if (skillsRefreshTimer) clearTimeout(skillsRefreshTimer);
if (event.reason === "remote-node") {
return;
}
if (skillsRefreshTimer) {
clearTimeout(skillsRefreshTimer);
}
skillsRefreshTimer = setTimeout(() => {
skillsRefreshTimer = null;
const latest = loadConfig();
@@ -376,7 +376,9 @@ describe("gateway server misc", () => {
test("auto-enables configured channel plugins on startup", async () => {
const configPath = process.env.OPENCLAW_CONFIG_PATH;
if (!configPath) throw new Error("Missing OPENCLAW_CONFIG_PATH");
if (!configPath) {
throw new Error("Missing OPENCLAW_CONFIG_PATH");
}
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
@@ -73,17 +73,23 @@ const connectNodeClient = async (params: {
commands: params.commands,
onEvent: params.onEvent,
onHelloOk: () => {
if (settled) return;
if (settled) {
return;
}
settled = true;
resolveReady?.();
},
onConnectError: (err) => {
if (settled) return;
if (settled) {
return;
}
settled = true;
rejectReady?.(err);
},
onClose: (code, reason) => {
if (settled) return;
if (settled) {
return;
}
settled = true;
rejectReady?.(new Error(`gateway closed (${code}): ${reason}`));
},
@@ -101,7 +107,9 @@ const connectNodeClient = async (params: {
async function waitForSignal(check: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (check()) return;
if (check()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error("timeout");
+12 -4
View File
@@ -87,7 +87,9 @@ describe("sessions_send gateway loopback", () => {
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send");
if (!tool) throw new Error("missing sessions_send tool");
if (!tool) {
throw new Error("missing sessions_send tool");
}
const result = await tool.execute("call-loopback", {
sessionKey: "main",
@@ -152,7 +154,9 @@ describe("sessions_send label lookup", () => {
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send");
if (!tool) throw new Error("missing sessions_send tool");
if (!tool) {
throw new Error("missing sessions_send tool");
}
// Send using label instead of sessionKey
const result = await tool.execute("call-by-label", {
@@ -172,7 +176,9 @@ describe("sessions_send label lookup", () => {
it("returns error when label not found", { timeout: 60_000 }, async () => {
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send");
if (!tool) throw new Error("missing sessions_send tool");
if (!tool) {
throw new Error("missing sessions_send tool");
}
const result = await tool.execute("call-missing-label", {
label: "nonexistent-label",
@@ -186,7 +192,9 @@ describe("sessions_send label lookup", () => {
it("returns error when neither sessionKey nor label provided", { timeout: 60_000 }, async () => {
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send");
if (!tool) throw new Error("missing sessions_send tool");
if (!tool) {
throw new Error("missing sessions_send tool");
}
const result = await tool.execute("call-no-key", {
message: "hello",
@@ -56,8 +56,11 @@ beforeAll(async () => {
afterAll(async () => {
await server.close();
if (previousToken === undefined) delete process.env.OPENCLAW_GATEWAY_TOKEN;
else process.env.OPENCLAW_GATEWAY_TOKEN = previousToken;
if (previousToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = previousToken;
}
});
const openClient = async (opts?: Parameters<typeof connectOk>[1]) => {
+6 -2
View File
@@ -3,8 +3,12 @@ import { Buffer } from "node:buffer";
const CLOSE_REASON_MAX_BYTES = 120;
export function truncateCloseReason(reason: string, maxBytes = CLOSE_REASON_MAX_BYTES): string {
if (!reason) return "invalid handshake";
if (!reason) {
return "invalid handshake";
}
const buf = Buffer.from(reason);
if (buf.length <= maxBytes) return reason;
if (buf.length <= maxBytes) {
return reason;
}
return buf.subarray(0, maxBytes).toString();
}
+6 -2
View File
@@ -18,7 +18,9 @@ export function createGatewayPluginRequestHandler(params: {
return async (req, res) => {
const routes = registry.httpRoutes ?? [];
const handlers = registry.httpHandlers ?? [];
if (routes.length === 0 && handlers.length === 0) return false;
if (routes.length === 0 && handlers.length === 0) {
return false;
}
if (routes.length > 0) {
const url = new URL(req.url ?? "/", "http://localhost");
@@ -42,7 +44,9 @@ export function createGatewayPluginRequestHandler(params: {
for (const entry of handlers) {
try {
const handled = await entry.handler(req, res);
if (handled) return true;
if (handled) {
return true;
}
} catch (err) {
log.warn(`plugin http handler failed (${entry.pluginId}): ${String(err)}`);
if (!res.headersSent) {
+9 -3
View File
@@ -95,7 +95,9 @@ export function attachGatewayWsConnectionHandler(params: {
let lastFrameId: string | undefined;
const setCloseCause = (cause: string, meta?: Record<string, unknown>) => {
if (!closeCause) closeCause = cause;
if (!closeCause) {
closeCause = cause;
}
if (meta && Object.keys(meta).length > 0) {
closeMeta = { ...closeMeta, ...meta };
}
@@ -125,10 +127,14 @@ export function attachGatewayWsConnectionHandler(params: {
});
const close = (code = 1000, reason?: string) => {
if (closed) return;
if (closed) {
return;
}
closed = true;
clearTimeout(handshakeTimer);
if (client) clients.delete(client);
if (client) {
clients.delete(client);
}
try {
socket.close(code, reason);
} catch {
@@ -61,10 +61,14 @@ const DEVICE_SIGNATURE_SKEW_MS = 10 * 60 * 1000;
function resolveHostName(hostHeader?: string): string {
const host = (hostHeader ?? "").trim().toLowerCase();
if (!host) return "";
if (!host) {
return "";
}
if (host.startsWith("[")) {
const end = host.indexOf("]");
if (end !== -1) return host.slice(1, end);
if (end !== -1) {
return host.slice(1, end);
}
}
const [name] = host.split(":");
return name ?? "";
@@ -229,7 +233,9 @@ export function attachGatewayWsMessageHandler(params: {
const isWebchatConnect = (p: ConnectParams | null | undefined) => isWebchatClient(p?.client);
socket.on("message", async (data) => {
if (isClosed()) return;
if (isClosed()) {
return;
}
const text = rawDataToString(data);
try {
const parsed = JSON.parse(text);
@@ -681,30 +687,40 @@ export function attachGatewayWsMessageHandler(params: {
const isPaired = paired?.publicKey === devicePublicKey;
if (!isPaired) {
const ok = await requirePairing("not-paired");
if (!ok) return;
if (!ok) {
return;
}
} else {
const allowedRoles = new Set(
Array.isArray(paired.roles) ? paired.roles : paired.role ? [paired.role] : [],
);
if (allowedRoles.size === 0) {
const ok = await requirePairing("role-upgrade", paired);
if (!ok) return;
if (!ok) {
return;
}
} else if (!allowedRoles.has(role)) {
const ok = await requirePairing("role-upgrade", paired);
if (!ok) return;
if (!ok) {
return;
}
}
const pairedScopes = Array.isArray(paired.scopes) ? paired.scopes : [];
if (scopes.length > 0) {
if (pairedScopes.length === 0) {
const ok = await requirePairing("scope-upgrade", paired);
if (!ok) return;
if (!ok) {
return;
}
} else {
const allowedScopes = new Set(pairedScopes);
const missingScope = scopes.find((scope) => !allowedScopes.has(scope));
if (missingScope) {
const ok = await requirePairing("scope-upgrade", paired);
if (!ok) return;
if (!ok) {
return;
}
}
}
}
@@ -828,7 +844,9 @@ export function attachGatewayWsMessageHandler(params: {
const instanceIdRaw = connectParams.client.instanceId;
const instanceId = typeof instanceIdRaw === "string" ? instanceIdRaw.trim() : "";
const nodeIdsForPairing = new Set<string>([nodeSession.nodeId]);
if (instanceId) nodeIdsForPairing.add(instanceId);
if (instanceId) {
nodeIdsForPairing.add(instanceId);
}
for (const nodeId of nodeIdsForPairing) {
void updatePairedNodeMetadata(nodeId, {
lastConnectedAtMs: nodeSession.connectedAtMs,
+99 -33
View File
@@ -14,12 +14,16 @@ export function readSessionMessages(
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile);
const filePath = candidates.find((p) => fs.existsSync(p));
if (!filePath) return [];
if (!filePath) {
return [];
}
const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
const messages: unknown[] = [];
for (const line of lines) {
if (!line.trim()) continue;
if (!line.trim()) {
continue;
}
try {
const parsed = JSON.parse(line);
if (parsed?.message) {
@@ -39,7 +43,9 @@ export function resolveSessionTranscriptCandidates(
agentId?: string,
): string[] {
const candidates: string[] = [];
if (sessionFile) candidates.push(sessionFile);
if (sessionFile) {
candidates.push(sessionFile);
}
if (storePath) {
const dir = path.dirname(storePath);
candidates.push(path.join(dir, `${sessionId}.jsonl`));
@@ -71,7 +77,9 @@ export function capArrayByJsonBytes<T>(
items: T[],
maxBytes: number,
): { items: T[]; bytes: number } {
if (items.length === 0) return { items, bytes: 2 };
if (items.length === 0) {
return { items, bytes: 2 };
}
const parts = items.map((item) => jsonUtf8Bytes(item));
let bytes = 2 + parts.reduce((a, b) => a + b, 0) + (items.length - 1);
let start = 0;
@@ -91,13 +99,21 @@ type TranscriptMessage = {
};
function extractTextFromContent(content: TranscriptMessage["content"]): string | null {
if (typeof content === "string") return content.trim() || null;
if (!Array.isArray(content)) return null;
if (typeof content === "string") {
return content.trim() || null;
}
if (!Array.isArray(content)) {
return null;
}
for (const part of content) {
if (!part || typeof part.text !== "string") continue;
if (!part || typeof part.text !== "string") {
continue;
}
if (part.type === "text" || part.type === "output_text" || part.type === "input_text") {
const trimmed = part.text.trim();
if (trimmed) return trimmed;
if (trimmed) {
return trimmed;
}
}
}
return null;
@@ -111,25 +127,33 @@ export function readFirstUserMessageFromTranscript(
): string | null {
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId);
const filePath = candidates.find((p) => fs.existsSync(p));
if (!filePath) return null;
if (!filePath) {
return null;
}
let fd: number | null = null;
try {
fd = fs.openSync(filePath, "r");
const buf = Buffer.alloc(8192);
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
if (bytesRead === 0) return null;
if (bytesRead === 0) {
return null;
}
const chunk = buf.toString("utf-8", 0, bytesRead);
const lines = chunk.split(/\r?\n/).slice(0, MAX_LINES_TO_SCAN);
for (const line of lines) {
if (!line.trim()) continue;
if (!line.trim()) {
continue;
}
try {
const parsed = JSON.parse(line);
const msg = parsed?.message as TranscriptMessage | undefined;
if (msg?.role === "user") {
const text = extractTextFromContent(msg.content);
if (text) return text;
if (text) {
return text;
}
}
} catch {
// skip malformed lines
@@ -138,7 +162,9 @@ export function readFirstUserMessageFromTranscript(
} catch {
// file read error
} finally {
if (fd !== null) fs.closeSync(fd);
if (fd !== null) {
fs.closeSync(fd);
}
}
return null;
}
@@ -154,14 +180,18 @@ export function readLastMessagePreviewFromTranscript(
): string | null {
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId);
const filePath = candidates.find((p) => fs.existsSync(p));
if (!filePath) return null;
if (!filePath) {
return null;
}
let fd: number | null = null;
try {
fd = fs.openSync(filePath, "r");
const stat = fs.fstatSync(fd);
const size = stat.size;
if (size === 0) return null;
if (size === 0) {
return null;
}
const readStart = Math.max(0, size - LAST_MSG_MAX_BYTES);
const readLen = Math.min(size, LAST_MSG_MAX_BYTES);
@@ -179,7 +209,9 @@ export function readLastMessagePreviewFromTranscript(
const msg = parsed?.message as TranscriptMessage | undefined;
if (msg?.role === "user" || msg?.role === "assistant") {
const text = extractTextFromContent(msg.content);
if (text) return text;
if (text) {
return text;
}
}
} catch {
// skip malformed
@@ -188,7 +220,9 @@ export function readLastMessagePreviewFromTranscript(
} catch {
// file error
} finally {
if (fd !== null) fs.closeSync(fd);
if (fd !== null) {
fs.closeSync(fd);
}
}
return null;
}
@@ -211,7 +245,9 @@ type TranscriptPreviewMessage = {
};
function normalizeRole(role: string | undefined, isTool: boolean): SessionPreviewItem["role"] {
if (isTool) return "tool";
if (isTool) {
return "tool";
}
switch ((role ?? "").toLowerCase()) {
case "user":
return "user";
@@ -227,8 +263,12 @@ function normalizeRole(role: string | undefined, isTool: boolean): SessionPrevie
}
function truncatePreviewText(text: string, maxChars: number): string {
if (maxChars <= 0 || text.length <= maxChars) return text;
if (maxChars <= 3) return text.slice(0, maxChars);
if (maxChars <= 0 || text.length <= maxChars) {
return text;
}
if (maxChars <= 3) {
return text.slice(0, maxChars);
}
return `${text.slice(0, maxChars - 3)}...`;
}
@@ -253,10 +293,16 @@ function extractPreviewText(message: TranscriptPreviewMessage): string | null {
}
function isToolCall(message: TranscriptPreviewMessage): boolean {
if (message.toolName || message.tool_name) return true;
if (!Array.isArray(message.content)) return false;
if (message.toolName || message.tool_name) {
return true;
}
if (!Array.isArray(message.content)) {
return false;
}
return message.content.some((entry) => {
if (entry?.name) return true;
if (entry?.name) {
return true;
}
const raw = typeof entry?.type === "string" ? entry.type.toLowerCase() : "";
return raw === "toolcall" || raw === "tool_call";
});
@@ -279,10 +325,14 @@ function extractToolNames(message: TranscriptPreviewMessage): string[] {
}
function extractMediaSummary(message: TranscriptPreviewMessage): string | null {
if (!Array.isArray(message.content)) return null;
if (!Array.isArray(message.content)) {
return null;
}
for (const entry of message.content) {
const raw = typeof entry?.type === "string" ? entry.type.trim().toLowerCase() : "";
if (!raw || raw === "text" || raw === "toolcall" || raw === "tool_call") continue;
if (!raw || raw === "text" || raw === "toolcall" || raw === "tool_call") {
continue;
}
return `[${raw}]`;
}
return null;
@@ -304,15 +354,21 @@ function buildPreviewItems(
const shown = toolNames.slice(0, 2);
const overflow = toolNames.length - shown.length;
text = `call ${shown.join(", ")}`;
if (overflow > 0) text += ` +${overflow}`;
if (overflow > 0) {
text += ` +${overflow}`;
}
}
}
if (!text) {
text = extractMediaSummary(message);
}
if (!text) continue;
if (!text) {
continue;
}
let trimmed = text.trim();
if (!trimmed) continue;
if (!trimmed) {
continue;
}
if (role === "user") {
trimmed = stripEnvelope(trimmed);
}
@@ -320,7 +376,9 @@ function buildPreviewItems(
items.push({ role, text: trimmed });
}
if (items.length <= maxItems) return items;
if (items.length <= maxItems) {
return items;
}
return items.slice(-maxItems);
}
@@ -334,7 +392,9 @@ function readRecentMessagesFromTranscript(
fd = fs.openSync(filePath, "r");
const stat = fs.fstatSync(fd);
const size = stat.size;
if (size === 0) return [];
if (size === 0) {
return [];
}
const readStart = Math.max(0, size - readBytes);
const readLen = Math.min(size, readBytes);
@@ -353,7 +413,9 @@ function readRecentMessagesFromTranscript(
const msg = parsed?.message as TranscriptPreviewMessage | undefined;
if (msg && typeof msg === "object") {
collected.push(msg);
if (collected.length >= maxMessages) break;
if (collected.length >= maxMessages) {
break;
}
}
} catch {
// skip malformed lines
@@ -363,7 +425,9 @@ function readRecentMessagesFromTranscript(
} catch {
return [];
} finally {
if (fd !== null) fs.closeSync(fd);
if (fd !== null) {
fs.closeSync(fd);
}
}
}
@@ -377,7 +441,9 @@ export function readSessionPreviewItemsFromTranscript(
): SessionPreviewItem[] {
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId);
const filePath = candidates.find((p) => fs.existsSync(p));
if (!filePath) return [];
if (!filePath) {
return [];
}
const boundedItems = Math.max(1, Math.min(maxItems, 50));
const boundedChars = Math.max(20, Math.min(maxChars, 2000));
+105 -35
View File
@@ -78,9 +78,15 @@ function resolveAvatarMime(filePath: string): string {
}
function isWorkspaceRelativePath(value: string): boolean {
if (!value) return false;
if (value.startsWith("~")) return false;
if (AVATAR_SCHEME_RE.test(value) && !WINDOWS_ABS_RE.test(value)) return false;
if (!value) {
return false;
}
if (value.startsWith("~")) {
return false;
}
if (AVATAR_SCHEME_RE.test(value) && !WINDOWS_ABS_RE.test(value)) {
return false;
}
return true;
}
@@ -89,19 +95,31 @@ function resolveIdentityAvatarUrl(
agentId: string,
avatar: string | undefined,
): string | undefined {
if (!avatar) return undefined;
if (!avatar) {
return undefined;
}
const trimmed = avatar.trim();
if (!trimmed) return undefined;
if (AVATAR_DATA_RE.test(trimmed) || AVATAR_HTTP_RE.test(trimmed)) return trimmed;
if (!isWorkspaceRelativePath(trimmed)) return undefined;
if (!trimmed) {
return undefined;
}
if (AVATAR_DATA_RE.test(trimmed) || AVATAR_HTTP_RE.test(trimmed)) {
return trimmed;
}
if (!isWorkspaceRelativePath(trimmed)) {
return undefined;
}
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const workspaceRoot = path.resolve(workspaceDir);
const resolved = path.resolve(workspaceRoot, trimmed);
const relative = path.relative(workspaceRoot, resolved);
if (relative.startsWith("..") || path.isAbsolute(relative)) return undefined;
if (relative.startsWith("..") || path.isAbsolute(relative)) {
return undefined;
}
try {
const stat = fs.statSync(resolved);
if (!stat.isFile() || stat.size > AVATAR_MAX_BYTES) return undefined;
if (!stat.isFile() || stat.size > AVATAR_MAX_BYTES) {
return undefined;
}
const buffer = fs.readFileSync(resolved);
const mime = resolveAvatarMime(resolved);
return `data:${mime};base64,${buffer.toString("base64")}`;
@@ -121,10 +139,14 @@ function formatSessionIdPrefix(sessionId: string, updatedAt?: number | null): st
}
function truncateTitle(text: string, maxLen: number): string {
if (text.length <= maxLen) return text;
if (text.length <= maxLen) {
return text;
}
const cut = text.slice(0, maxLen - 1);
const lastSpace = cut.lastIndexOf(" ");
if (lastSpace > maxLen * 0.6) return cut.slice(0, lastSpace) + "…";
if (lastSpace > maxLen * 0.6) {
return cut.slice(0, lastSpace) + "…";
}
return cut + "…";
}
@@ -132,7 +154,9 @@ export function deriveSessionTitle(
entry: SessionEntry | undefined,
firstUserMessage?: string | null,
): string | undefined {
if (!entry) return undefined;
if (!entry) {
return undefined;
}
if (entry.displayName?.trim()) {
return entry.displayName.trim();
@@ -166,8 +190,12 @@ export function loadSessionEntry(sessionKey: string) {
}
export function classifySessionKey(key: string, entry?: SessionEntry): GatewaySessionRow["kind"] {
if (key === "global") return "global";
if (key === "unknown") return "unknown";
if (key === "global") {
return "global";
}
if (key === "unknown") {
return "unknown";
}
if (entry?.chatType === "group" || entry?.chatType === "channel") {
return "group";
}
@@ -216,7 +244,9 @@ function listConfiguredAgentIds(cfg: OpenClawConfig): string[] {
if (agents.length > 0) {
const ids = new Set<string>();
for (const entry of agents) {
if (entry?.id) ids.add(normalizeAgentId(entry.id));
if (entry?.id) {
ids.add(normalizeAgentId(entry.id));
}
}
const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
ids.add(defaultId);
@@ -230,7 +260,9 @@ function listConfiguredAgentIds(cfg: OpenClawConfig): string[] {
const ids = new Set<string>();
const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
ids.add(defaultId);
for (const id of listExistingAgentIdsFromDisk()) ids.add(id);
for (const id of listExistingAgentIdsFromDisk()) {
ids.add(id);
}
const sorted = Array.from(ids).filter(Boolean);
sorted.sort((a, b) => a.localeCompare(b));
if (sorted.includes(defaultId)) {
@@ -253,7 +285,9 @@ export function listAgentsForGateway(cfg: OpenClawConfig): {
{ name?: string; identity?: GatewayAgentRow["identity"] }
>();
for (const entry of cfg.agents?.list ?? []) {
if (!entry?.id) continue;
if (!entry?.id) {
continue;
}
const identity = entry.identity
? {
name: entry.identity.name?.trim() || undefined,
@@ -296,8 +330,12 @@ export function listAgentsForGateway(cfg: OpenClawConfig): {
}
function canonicalizeSessionKeyForAgent(agentId: string, key: string): string {
if (key === "global" || key === "unknown") return key;
if (key.startsWith("agent:")) return key;
if (key === "global" || key === "unknown") {
return key;
}
if (key.startsWith("agent:")) {
return key;
}
return `agent:${normalizeAgentId(agentId)}:${key}`;
}
@@ -310,8 +348,12 @@ export function resolveSessionStoreKey(params: {
sessionKey: string;
}): string {
const raw = params.sessionKey.trim();
if (!raw) return raw;
if (raw === "global" || raw === "unknown") return raw;
if (!raw) {
return raw;
}
if (raw === "global" || raw === "unknown") {
return raw;
}
const parsed = parseAgentSessionKey(raw);
if (parsed) {
@@ -321,7 +363,9 @@ export function resolveSessionStoreKey(params: {
agentId,
sessionKey: raw,
});
if (canonical !== raw) return canonical;
if (canonical !== raw) {
return canonical;
}
return raw;
}
@@ -338,15 +382,23 @@ function resolveSessionStoreAgentId(cfg: OpenClawConfig, canonicalKey: string):
return resolveDefaultStoreAgentId(cfg);
}
const parsed = parseAgentSessionKey(canonicalKey);
if (parsed?.agentId) return normalizeAgentId(parsed.agentId);
if (parsed?.agentId) {
return normalizeAgentId(parsed.agentId);
}
return resolveDefaultStoreAgentId(cfg);
}
function canonicalizeSpawnedByForAgent(agentId: string, spawnedBy?: string): string | undefined {
const raw = spawnedBy?.trim();
if (!raw) return undefined;
if (raw === "global" || raw === "unknown") return raw;
if (raw.startsWith("agent:")) return raw;
if (!raw) {
return undefined;
}
if (raw === "global" || raw === "unknown") {
return raw;
}
if (raw.startsWith("agent:")) {
return raw;
}
return `agent:${normalizeAgentId(agentId)}:${raw}`;
}
@@ -372,7 +424,9 @@ export function resolveGatewaySessionStoreTarget(params: { cfg: OpenClawConfig;
const storeKeys = new Set<string>();
storeKeys.add(canonicalKey);
if (key && key !== canonicalKey) storeKeys.add(key);
if (key && key !== canonicalKey) {
storeKeys.add(key);
}
return {
agentId,
storePath,
@@ -509,23 +563,37 @@ export function listSessionsFromStore(params: {
let sessions = Object.entries(store)
.filter(([key]) => {
if (!includeGlobal && key === "global") return false;
if (!includeUnknown && key === "unknown") return false;
if (!includeGlobal && key === "global") {
return false;
}
if (!includeUnknown && key === "unknown") {
return false;
}
if (agentId) {
if (key === "global" || key === "unknown") return false;
if (key === "global" || key === "unknown") {
return false;
}
const parsed = parseAgentSessionKey(key);
if (!parsed) return false;
if (!parsed) {
return false;
}
return normalizeAgentId(parsed.agentId) === agentId;
}
return true;
})
.filter(([key, entry]) => {
if (!spawnedBy) return true;
if (key === "unknown" || key === "global") return false;
if (!spawnedBy) {
return true;
}
if (key === "unknown" || key === "global") {
return false;
}
return entry?.spawnedBy === spawnedBy;
})
.filter(([, entry]) => {
if (!label) return true;
if (!label) {
return true;
}
return entry?.label === label;
})
.map(([key, entry]) => {
@@ -628,7 +696,9 @@ export function listSessionsFromStore(params: {
storePath,
entry.sessionFile,
);
if (lastMsg) lastMessagePreview = lastMsg;
if (lastMsg) {
lastMessagePreview = lastMsg;
}
}
}
return { ...rest, derivedTitle, lastMessagePreview } satisfies GatewaySessionRow;
+15 -5
View File
@@ -13,7 +13,9 @@ describe("gateway sessions patch", () => {
patch: { elevatedLevel: "off" },
});
expect(res.ok).toBe(true);
if (!res.ok) return;
if (!res.ok) {
return;
}
expect(res.entry.elevatedLevel).toBe("off");
});
@@ -26,7 +28,9 @@ describe("gateway sessions patch", () => {
patch: { elevatedLevel: "on" },
});
expect(res.ok).toBe(true);
if (!res.ok) return;
if (!res.ok) {
return;
}
expect(res.entry.elevatedLevel).toBe("on");
});
@@ -41,7 +45,9 @@ describe("gateway sessions patch", () => {
patch: { elevatedLevel: null },
});
expect(res.ok).toBe(true);
if (!res.ok) return;
if (!res.ok) {
return;
}
expect(res.entry.elevatedLevel).toBeUndefined();
});
@@ -54,7 +60,9 @@ describe("gateway sessions patch", () => {
patch: { elevatedLevel: "maybe" },
});
expect(res.ok).toBe(false);
if (res.ok) return;
if (res.ok) {
return;
}
expect(res.error.message).toContain("invalid elevatedLevel");
});
@@ -78,7 +86,9 @@ describe("gateway sessions patch", () => {
loadGatewayModelCatalog: async () => [{ provider: "openai", id: "gpt-5.2" }],
});
expect(res.ok).toBe(true);
if (!res.ok) return;
if (!res.ok) {
return;
}
expect(res.entry.providerOverride).toBe("openai");
expect(res.entry.modelOverride).toBe("gpt-5.2");
expect(res.entry.authProfileOverride).toBeUndefined();
+54 -19
View File
@@ -76,10 +76,14 @@ export async function applySessionsPatchToStore(params: {
if ("spawnedBy" in patch) {
const raw = patch.spawnedBy;
if (raw === null) {
if (existing?.spawnedBy) return invalid("spawnedBy cannot be cleared once set");
if (existing?.spawnedBy) {
return invalid("spawnedBy cannot be cleared once set");
}
} else if (raw !== undefined) {
const trimmed = String(raw).trim();
if (!trimmed) return invalid("invalid spawnedBy: empty");
if (!trimmed) {
return invalid("invalid spawnedBy: empty");
}
if (!isSubagentSessionKey(storeKey)) {
return invalid("spawnedBy is only supported for subagent:* sessions");
}
@@ -96,9 +100,13 @@ export async function applySessionsPatchToStore(params: {
delete next.label;
} else if (raw !== undefined) {
const parsed = parseSessionLabel(raw);
if (!parsed.ok) return invalid(parsed.error);
if (!parsed.ok) {
return invalid(parsed.error);
}
for (const [key, entry] of Object.entries(store)) {
if (key === storeKey) continue;
if (key === storeKey) {
continue;
}
if (entry?.label === parsed.label) {
return invalid(`label already in use: ${parsed.label}`);
}
@@ -125,15 +133,20 @@ export async function applySessionsPatchToStore(params: {
`invalid thinkingLevel (use ${formatThinkingLevels(hintProvider, hintModel, "|")})`,
);
}
if (normalized === "off") delete next.thinkingLevel;
else next.thinkingLevel = normalized;
if (normalized === "off") {
delete next.thinkingLevel;
} else {
next.thinkingLevel = normalized;
}
}
}
if ("verboseLevel" in patch) {
const raw = patch.verboseLevel;
const parsed = parseVerboseOverride(raw);
if (!parsed.ok) return invalid(parsed.error);
if (!parsed.ok) {
return invalid(parsed.error);
}
applyVerboseOverride(next, parsed.value);
}
@@ -146,8 +159,11 @@ export async function applySessionsPatchToStore(params: {
if (!normalized) {
return invalid('invalid reasoningLevel (use "on"|"off"|"stream")');
}
if (normalized === "off") delete next.reasoningLevel;
else next.reasoningLevel = normalized;
if (normalized === "off") {
delete next.reasoningLevel;
} else {
next.reasoningLevel = normalized;
}
}
}
@@ -157,9 +173,14 @@ export async function applySessionsPatchToStore(params: {
delete next.responseUsage;
} else if (raw !== undefined) {
const normalized = normalizeUsageDisplay(String(raw));
if (!normalized) return invalid('invalid responseUsage (use "off"|"tokens"|"full")');
if (normalized === "off") delete next.responseUsage;
else next.responseUsage = normalized;
if (!normalized) {
return invalid('invalid responseUsage (use "off"|"tokens"|"full")');
}
if (normalized === "off") {
delete next.responseUsage;
} else {
next.responseUsage = normalized;
}
}
}
@@ -169,7 +190,9 @@ export async function applySessionsPatchToStore(params: {
delete next.elevatedLevel;
} else if (raw !== undefined) {
const normalized = normalizeElevatedLevel(String(raw));
if (!normalized) return invalid('invalid elevatedLevel (use "on"|"off"|"ask"|"full")');
if (!normalized) {
return invalid('invalid elevatedLevel (use "on"|"off"|"ask"|"full")');
}
// Persist "off" explicitly so patches can override defaults.
next.elevatedLevel = normalized;
}
@@ -181,7 +204,9 @@ export async function applySessionsPatchToStore(params: {
delete next.execHost;
} else if (raw !== undefined) {
const normalized = normalizeExecHost(String(raw));
if (!normalized) return invalid('invalid execHost (use "sandbox"|"gateway"|"node")');
if (!normalized) {
return invalid('invalid execHost (use "sandbox"|"gateway"|"node")');
}
next.execHost = normalized;
}
}
@@ -192,7 +217,9 @@ export async function applySessionsPatchToStore(params: {
delete next.execSecurity;
} else if (raw !== undefined) {
const normalized = normalizeExecSecurity(String(raw));
if (!normalized) return invalid('invalid execSecurity (use "deny"|"allowlist"|"full")');
if (!normalized) {
return invalid('invalid execSecurity (use "deny"|"allowlist"|"full")');
}
next.execSecurity = normalized;
}
}
@@ -203,7 +230,9 @@ export async function applySessionsPatchToStore(params: {
delete next.execAsk;
} else if (raw !== undefined) {
const normalized = normalizeExecAsk(String(raw));
if (!normalized) return invalid('invalid execAsk (use "off"|"on-miss"|"always")');
if (!normalized) {
return invalid('invalid execAsk (use "off"|"on-miss"|"always")');
}
next.execAsk = normalized;
}
}
@@ -214,7 +243,9 @@ export async function applySessionsPatchToStore(params: {
delete next.execNode;
} else if (raw !== undefined) {
const trimmed = String(raw).trim();
if (!trimmed) return invalid("invalid execNode: empty");
if (!trimmed) {
return invalid("invalid execNode: empty");
}
next.execNode = trimmed;
}
}
@@ -237,7 +268,9 @@ export async function applySessionsPatchToStore(params: {
});
} else if (raw !== undefined) {
const trimmed = String(raw).trim();
if (!trimmed) return invalid("invalid model: empty");
if (!trimmed) {
return invalid("invalid model: empty");
}
if (!params.loadGatewayModelCatalog) {
return {
ok: false,
@@ -291,7 +324,9 @@ export async function applySessionsPatchToStore(params: {
delete next.sendPolicy;
} else if (raw !== undefined) {
const normalized = normalizeSendPolicy(String(raw));
if (!normalized) return invalid('invalid sendPolicy (use "allow"|"deny")');
if (!normalized) {
return invalid('invalid sendPolicy (use "allow"|"deny")');
}
next.sendPolicy = normalized;
}
}
+11 -4
View File
@@ -33,11 +33,16 @@ export async function connectGatewayClient(params: {
return await new Promise<InstanceType<typeof GatewayClient>>((resolve, reject) => {
let settled = false;
const stop = (err?: Error, client?: InstanceType<typeof GatewayClient>) => {
if (settled) return;
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
if (err) reject(err);
else resolve(client as InstanceType<typeof GatewayClient>);
if (err) {
reject(err);
} else {
resolve(client as InstanceType<typeof GatewayClient>);
}
};
const client = new GatewayClient({
url: params.url,
@@ -112,7 +117,9 @@ export async function connectDeviceAuthReq(params: { url: string; token?: string
};
const handler = (data: WebSocket.RawData) => {
const obj = JSON.parse(rawDataToString(data)) as { type?: unknown; id?: unknown };
if (obj?.type !== "res" || obj?.id !== "c1") return;
if (obj?.type !== "res" || obj?.id !== "c1") {
return;
}
clearTimeout(timer);
ws.off("message", handler);
ws.off("close", closeHandler);
+22 -8
View File
@@ -441,9 +441,12 @@ vi.mock("../config/config.js", async () => {
...fileSession,
mainKey: fileSession.mainKey ?? "main",
};
if (typeof testState.sessionStorePath === "string")
if (typeof testState.sessionStorePath === "string") {
session.store = testState.sessionStorePath;
if (testState.sessionConfig) Object.assign(session, testState.sessionConfig);
}
if (testState.sessionConfig) {
Object.assign(session, testState.sessionConfig);
}
const fileGateway =
fileConfig.gateway &&
@@ -451,9 +454,15 @@ vi.mock("../config/config.js", async () => {
!Array.isArray(fileConfig.gateway)
? ({ ...(fileConfig.gateway as Record<string, unknown>) } as Record<string, unknown>)
: {};
if (testState.gatewayBind) fileGateway.bind = testState.gatewayBind;
if (testState.gatewayAuth) fileGateway.auth = testState.gatewayAuth;
if (testState.gatewayControlUi) fileGateway.controlUi = testState.gatewayControlUi;
if (testState.gatewayBind) {
fileGateway.bind = testState.gatewayBind;
}
if (testState.gatewayAuth) {
fileGateway.auth = testState.gatewayAuth;
}
if (testState.gatewayControlUi) {
fileGateway.controlUi = testState.gatewayControlUi;
}
const gateway = Object.keys(fileGateway).length > 0 ? fileGateway : undefined;
const fileCanvasHost =
@@ -462,8 +471,9 @@ vi.mock("../config/config.js", async () => {
!Array.isArray(fileConfig.canvasHost)
? ({ ...(fileConfig.canvasHost as Record<string, unknown>) } as Record<string, unknown>)
: {};
if (typeof testState.canvasHostPort === "number")
if (typeof testState.canvasHostPort === "number") {
fileCanvasHost.port = testState.canvasHostPort;
}
const canvasHost = Object.keys(fileCanvasHost).length > 0 ? fileCanvasHost : undefined;
const hooks = testState.hooksConfig ?? (fileConfig.hooks as HooksConfig | undefined);
@@ -472,8 +482,12 @@ vi.mock("../config/config.js", async () => {
fileConfig.cron && typeof fileConfig.cron === "object" && !Array.isArray(fileConfig.cron)
? ({ ...(fileConfig.cron as Record<string, unknown>) } as Record<string, unknown>)
: {};
if (typeof testState.cronEnabled === "boolean") fileCron.enabled = testState.cronEnabled;
if (typeof testState.cronStorePath === "string") fileCron.store = testState.cronStorePath;
if (typeof testState.cronEnabled === "boolean") {
fileCron.enabled = testState.cronEnabled;
}
if (typeof testState.cronStorePath === "string") {
fileCron.store = testState.cronStorePath;
}
const cron = Object.keys(fileCron).length > 0 ? fileCron : undefined;
const config = {
+21 -7
View File
@@ -22,7 +22,9 @@ type OpenAIResponseStreamEvent =
function extractLastUserText(input: unknown[]): string {
for (let i = input.length - 1; i >= 0; i -= 1) {
const item = input[i] as Record<string, unknown> | undefined;
if (!item || item.role !== "user") continue;
if (!item || item.role !== "user") {
continue;
}
const content = item.content;
if (Array.isArray(content)) {
const text = content
@@ -36,7 +38,9 @@ function extractLastUserText(input: unknown[]): string {
.map((c) => c.text)
.join("\n")
.trim();
if (text) return text;
if (text) {
return text;
}
}
}
return "";
@@ -45,7 +49,9 @@ function extractLastUserText(input: unknown[]): string {
function extractToolOutput(input: unknown[]): string {
for (const itemRaw of input) {
const item = itemRaw as Record<string, unknown> | undefined;
if (!item || item.type !== "function_call_output") continue;
if (!item || item.type !== "function_call_output") {
continue;
}
return typeof item.output === "string" ? item.output : "";
}
return "";
@@ -128,10 +134,18 @@ async function* fakeOpenAIResponsesStream(
}
function decodeBodyText(body: unknown): string {
if (!body) return "";
if (typeof body === "string") return body;
if (body instanceof Uint8Array) return Buffer.from(body).toString("utf8");
if (body instanceof ArrayBuffer) return Buffer.from(new Uint8Array(body)).toString("utf8");
if (!body) {
return "";
}
if (typeof body === "string") {
return body;
}
if (body instanceof Uint8Array) {
return Buffer.from(body).toString("utf8");
}
if (body instanceof ArrayBuffer) {
return Buffer.from(new Uint8Array(body)).toString("utf8");
}
return "";
}
+55 -21
View File
@@ -56,7 +56,9 @@ export async function writeSessionStore(params: {
mainKey?: string;
}): Promise<void> {
const storePath = params.storePath ?? testState.sessionStorePath;
if (!storePath) throw new Error("writeSessionStore requires testState.sessionStorePath");
if (!storePath) {
throw new Error("writeSessionStore requires testState.sessionStorePath");
}
const agentId = params.agentId ?? DEFAULT_AGENT_ID;
const store: Record<string, Partial<SessionEntry>> = {};
for (const [requestKey, entry] of Object.entries(params.entries)) {
@@ -148,21 +150,41 @@ async function cleanupGatewayTestHome(options: { restoreEnv: boolean }) {
vi.useRealTimers();
resetLogger();
if (options.restoreEnv) {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = previousUserProfile;
if (previousStateDir === undefined) delete process.env.OPENCLAW_STATE_DIR;
else process.env.OPENCLAW_STATE_DIR = previousStateDir;
if (previousConfigPath === undefined) delete process.env.OPENCLAW_CONFIG_PATH;
else process.env.OPENCLAW_CONFIG_PATH = previousConfigPath;
if (previousSkipBrowserControl === undefined)
if (previousHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = previousHome;
}
if (previousUserProfile === undefined) {
delete process.env.USERPROFILE;
} else {
process.env.USERPROFILE = previousUserProfile;
}
if (previousStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousStateDir;
}
if (previousConfigPath === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = previousConfigPath;
}
if (previousSkipBrowserControl === undefined) {
delete process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER;
else process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = previousSkipBrowserControl;
if (previousSkipGmailWatcher === undefined) delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER;
else process.env.OPENCLAW_SKIP_GMAIL_WATCHER = previousSkipGmailWatcher;
if (previousSkipCanvasHost === undefined) delete process.env.OPENCLAW_SKIP_CANVAS_HOST;
else process.env.OPENCLAW_SKIP_CANVAS_HOST = previousSkipCanvasHost;
} else {
process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = previousSkipBrowserControl;
}
if (previousSkipGmailWatcher === undefined) {
delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER;
} else {
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = previousSkipGmailWatcher;
}
if (previousSkipCanvasHost === undefined) {
delete process.env.OPENCLAW_SKIP_CANVAS_HOST;
} else {
process.env.OPENCLAW_SKIP_CANVAS_HOST = previousSkipCanvasHost;
}
}
if (options.restoreEnv && tempHome) {
await fs.rm(tempHome, {
@@ -281,7 +303,9 @@ export async function startServerWithClient(token?: string, opts?: GatewayServer
break;
} catch (err) {
const code = (err as { cause?: { code?: string } }).cause?.code;
if (code !== "EADDRINUSE") throw err;
if (code !== "EADDRINUSE") {
throw err;
}
port = await getFreePort();
}
}
@@ -359,8 +383,12 @@ export async function connectReq(
const password = opts?.password ?? defaultPassword;
const requestedScopes = Array.isArray(opts?.scopes) ? opts?.scopes : [];
const device = (() => {
if (opts?.device === null) return undefined;
if (opts?.device) return opts.device;
if (opts?.device === null) {
return undefined;
}
if (opts?.device) {
return opts.device;
}
const identity = loadOrCreateDeviceIdentity();
const signedAtMs = Date.now();
const payload = buildDeviceAuthPayload({
@@ -406,7 +434,9 @@ export async function connectReq(
}),
);
const isResponseForId = (o: unknown): boolean => {
if (!o || typeof o !== "object" || Array.isArray(o)) return false;
if (!o || typeof o !== "object" || Array.isArray(o)) {
return false;
}
const rec = o as Record<string, unknown>;
return rec.type === "res" && rec.id === id;
};
@@ -438,7 +468,9 @@ export async function rpcReq<T = unknown>(
}>(
ws,
(o) => {
if (!o || typeof o !== "object" || Array.isArray(o)) return false;
if (!o || typeof o !== "object" || Array.isArray(o)) {
return false;
}
const rec = o as Record<string, unknown>;
return rec.type === "res" && rec.id === id;
},
@@ -451,7 +483,9 @@ export async function waitForSystemEvent(timeoutMs = 2000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const events = peekSystemEvents(sessionKey);
if (events.length > 0) return events;
if (events.length > 0) {
return events;
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error("timeout waiting for system event");
+3 -1
View File
@@ -19,7 +19,9 @@ beforeEach(() => {
const resolveGatewayToken = (): string => {
const token = (testState.gatewayAuth as { token?: string } | undefined)?.token;
if (!token) throw new Error("test gateway token missing");
if (!token) {
throw new Error("test gateway token missing");
}
return token;
};
+27 -9
View File
@@ -45,12 +45,16 @@ type ToolsInvokeBody = {
};
function resolveSessionKeyFromBody(body: ToolsInvokeBody): string | undefined {
if (typeof body.sessionKey === "string" && body.sessionKey.trim()) return body.sessionKey.trim();
if (typeof body.sessionKey === "string" && body.sessionKey.trim()) {
return body.sessionKey.trim();
}
return undefined;
}
function resolveMemoryToolDisableReasons(cfg: ReturnType<typeof loadConfig>): string[] {
if (!process.env.VITEST) return [];
if (!process.env.VITEST) {
return [];
}
const reasons: string[] = [];
const plugins = cfg.plugins;
const slotRaw = plugins?.slots?.memory;
@@ -59,7 +63,9 @@ function resolveMemoryToolDisableReasons(cfg: ReturnType<typeof loadConfig>): st
const pluginsDisabled = plugins?.enabled === false;
const defaultDisabled = isTestDefaultMemorySlotDisabled(cfg);
if (pluginsDisabled) reasons.push("plugins.enabled=false");
if (pluginsDisabled) {
reasons.push("plugins.enabled=false");
}
if (slotDisabled) {
reasons.push(slotRaw === null ? "plugins.slots.memory=null" : 'plugins.slots.memory="none"');
}
@@ -75,8 +81,12 @@ function mergeActionIntoArgsIfSupported(params: {
args: Record<string, unknown>;
}): Record<string, unknown> {
const { toolSchema, action, args } = params;
if (!action) return args;
if (args.action !== undefined) return args;
if (!action) {
return args;
}
if (args.action !== undefined) {
return args;
}
// TypeBox schemas are plain objects; many tools define an `action` property.
const schemaObj = toolSchema as { properties?: Record<string, unknown> } | null;
const hasAction = Boolean(
@@ -85,7 +95,9 @@ function mergeActionIntoArgsIfSupported(params: {
schemaObj.properties &&
"action" in schemaObj.properties,
);
if (!hasAction) return args;
if (!hasAction) {
return args;
}
return { ...args, action };
}
@@ -95,7 +107,9 @@ export async function handleToolsInvokeHttpRequest(
opts: { auth: ResolvedGatewayAuth; maxBodyBytes?: number; trustedProxies?: string[] },
): Promise<boolean> {
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
if (url.pathname !== "/tools/invoke") return false;
if (url.pathname !== "/tools/invoke") {
return false;
}
if (req.method !== "POST") {
sendMethodNotAllowed(res, "POST");
@@ -116,7 +130,9 @@ export async function handleToolsInvokeHttpRequest(
}
const bodyUnknown = await readJsonBodyOrError(req, res, opts.maxBodyBytes ?? DEFAULT_BODY_BYTES);
if (bodyUnknown === undefined) return true;
if (bodyUnknown === undefined) {
return true;
}
const body = (bodyUnknown ?? {}) as ToolsInvokeBody;
const toolName = typeof body.tool === "string" ? body.tool.trim() : "";
@@ -175,7 +191,9 @@ export async function handleToolsInvokeHttpRequest(
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
const mergeAlsoAllow = (policy: typeof profilePolicy, alsoAllow?: string[]) => {
if (!policy?.allow || !Array.isArray(alsoAllow) || alsoAllow.length === 0) return policy;
if (!policy?.allow || !Array.isArray(alsoAllow) || alsoAllow.length === 0) {
return policy;
}
return { ...policy, allow: Array.from(new Set([...policy.allow, ...alsoAllow])) };
};
+129 -43
View File
@@ -27,8 +27,12 @@ const wsLog = createSubsystemLogger("gateway/ws");
export function shortId(value: string): string {
const s = value.trim();
if (UUID_RE.test(s)) return `${s.slice(0, 8)}${s.slice(-4)}`;
if (s.length <= 24) return s;
if (UUID_RE.test(s)) {
return `${s.slice(0, 8)}${s.slice(-4)}`;
}
if (s.length <= 24) {
return s;
}
return `${s.slice(0, 12)}${s.slice(-4)}`;
}
@@ -36,13 +40,19 @@ export function formatForLog(value: unknown): string {
try {
if (value instanceof Error) {
const parts: string[] = [];
if (value.name) parts.push(value.name);
if (value.message) parts.push(value.message);
if (value.name) {
parts.push(value.name);
}
if (value.message) {
parts.push(value.message);
}
const code =
"code" in value && (typeof value.code === "string" || typeof value.code === "number")
? String(value.code)
: "";
if (code) parts.push(`code=${code}`);
if (code) {
parts.push(`code=${code}`);
}
const combined = parts.filter(Boolean).join(": ").trim();
if (combined) {
return combined.length > LOG_VALUE_LIMIT
@@ -57,7 +67,9 @@ export function formatForLog(value: unknown): string {
const code =
typeof rec.code === "string" || typeof rec.code === "number" ? String(rec.code) : "";
const parts = [name, rec.message.trim()].filter(Boolean);
if (code) parts.push(`code=${code}`);
if (code) {
parts.push(`code=${code}`);
}
const combined = parts.join(": ").trim();
return combined.length > LOG_VALUE_LIMIT
? `${combined.slice(0, LOG_VALUE_LIMIT)}...`
@@ -68,7 +80,9 @@ export function formatForLog(value: unknown): string {
typeof value === "string" || typeof value === "number"
? String(value)
: JSON.stringify(value);
if (!str) return "";
if (!str) {
return "";
}
const redacted = redactSensitiveText(str, WS_LOG_REDACT_OPTIONS);
return redacted.length > LOG_VALUE_LIMIT
? `${redacted.slice(0, LOG_VALUE_LIMIT)}...`
@@ -80,12 +94,16 @@ export function formatForLog(value: unknown): string {
function compactPreview(input: string, maxLen = 160): string {
const oneLine = input.replace(/\s+/g, " ").trim();
if (oneLine.length <= maxLen) return oneLine;
if (oneLine.length <= maxLen) {
return oneLine;
}
return `${oneLine.slice(0, Math.max(0, maxLen - 1))}`;
}
export function summarizeAgentEventForWsLog(payload: unknown): Record<string, unknown> {
if (!payload || typeof payload !== "object") return {};
if (!payload || typeof payload !== "object") {
return {};
}
const rec = payload as Record<string, unknown>;
const runId = typeof rec.runId === "string" ? rec.runId : undefined;
const stream = typeof rec.stream === "string" ? rec.stream : undefined;
@@ -95,7 +113,9 @@ export function summarizeAgentEventForWsLog(payload: unknown): Record<string, un
rec.data && typeof rec.data === "object" ? (rec.data as Record<string, unknown>) : undefined;
const extra: Record<string, unknown> = {};
if (runId) extra.run = shortId(runId);
if (runId) {
extra.run = shortId(runId);
}
if (sessionKey) {
const parsed = parseAgentSessionKey(sessionKey);
if (parsed) {
@@ -105,47 +125,75 @@ export function summarizeAgentEventForWsLog(payload: unknown): Record<string, un
extra.session = sessionKey;
}
}
if (stream) extra.stream = stream;
if (seq !== undefined) extra.aseq = seq;
if (stream) {
extra.stream = stream;
}
if (seq !== undefined) {
extra.aseq = seq;
}
if (!data) return extra;
if (!data) {
return extra;
}
if (stream === "assistant") {
const text = typeof data.text === "string" ? data.text : undefined;
if (text?.trim()) extra.text = compactPreview(text);
if (text?.trim()) {
extra.text = compactPreview(text);
}
const mediaUrls = Array.isArray(data.mediaUrls) ? data.mediaUrls : undefined;
if (mediaUrls && mediaUrls.length > 0) extra.media = mediaUrls.length;
if (mediaUrls && mediaUrls.length > 0) {
extra.media = mediaUrls.length;
}
return extra;
}
if (stream === "tool") {
const phase = typeof data.phase === "string" ? data.phase : undefined;
const name = typeof data.name === "string" ? data.name : undefined;
if (phase || name) extra.tool = `${phase ?? "?"}:${name ?? "?"}`;
if (phase || name) {
extra.tool = `${phase ?? "?"}:${name ?? "?"}`;
}
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined;
if (toolCallId) extra.call = shortId(toolCallId);
if (toolCallId) {
extra.call = shortId(toolCallId);
}
const meta = typeof data.meta === "string" ? data.meta : undefined;
if (meta?.trim()) extra.meta = meta;
if (typeof data.isError === "boolean") extra.err = data.isError;
if (meta?.trim()) {
extra.meta = meta;
}
if (typeof data.isError === "boolean") {
extra.err = data.isError;
}
return extra;
}
if (stream === "lifecycle") {
const phase = typeof data.phase === "string" ? data.phase : undefined;
if (phase) extra.phase = phase;
if (typeof data.aborted === "boolean") extra.aborted = data.aborted;
if (phase) {
extra.phase = phase;
}
if (typeof data.aborted === "boolean") {
extra.aborted = data.aborted;
}
const error = typeof data.error === "string" ? data.error : undefined;
if (error?.trim()) extra.error = compactPreview(error, 120);
if (error?.trim()) {
extra.error = compactPreview(error, 120);
}
return extra;
}
const reason = typeof data.reason === "string" ? data.reason : undefined;
if (reason?.trim()) extra.reason = reason;
if (reason?.trim()) {
extra.reason = reason;
}
return extra;
}
export function logWs(direction: "in" | "out", kind: string, meta?: Record<string, unknown>) {
if (!shouldLogSubsystemToConsole("gateway/ws")) return;
if (!shouldLogSubsystemToConsole("gateway/ws")) {
return;
}
const style = getGatewayWsLogStyle();
if (!isVerbose()) {
logWsOptimized(direction, kind, meta);
@@ -172,7 +220,9 @@ export function logWs(direction: "in" | "out", kind: string, meta?: Record<strin
direction === "out" && kind === "res" && inflightKey
? (() => {
const startedAt = wsInflightSince.get(inflightKey);
if (startedAt === undefined) return undefined;
if (startedAt === undefined) {
return undefined;
}
wsInflightSince.delete(inflightKey);
return now - startedAt;
})()
@@ -201,10 +251,18 @@ export function logWs(direction: "in" | "out", kind: string, meta?: Record<strin
const restMeta: string[] = [];
if (meta) {
for (const [key, value] of Object.entries(meta)) {
if (value === undefined) continue;
if (key === "connId" || key === "id") continue;
if (key === "method" || key === "ok") continue;
if (key === "event") continue;
if (value === undefined) {
continue;
}
if (key === "connId" || key === "id") {
continue;
}
if (key === "method" || key === "ok") {
continue;
}
if (key === "event") {
continue;
}
restMeta.push(`${chalk.dim(key)}=${formatForLog(value)}`);
}
}
@@ -213,7 +271,9 @@ export function logWs(direction: "in" | "out", kind: string, meta?: Record<strin
if (connId) {
trailing.push(`${chalk.dim("conn")}=${chalk.gray(shortId(connId))}`);
}
if (id) trailing.push(`${chalk.dim("id")}=${chalk.gray(shortId(id))}`);
if (id) {
trailing.push(`${chalk.dim("id")}=${chalk.gray(shortId(id))}`);
}
const tokens = [prefix, statusToken, headline, durationToken, ...restMeta, ...trailing].filter(
(t): t is string => Boolean(t),
@@ -232,7 +292,9 @@ function logWsOptimized(direction: "in" | "out", kind: string, meta?: Record<str
if (direction === "in" && kind === "req" && inflightKey) {
wsInflightOptimized.set(inflightKey, Date.now());
if (wsInflightOptimized.size > 2000) wsInflightOptimized.clear();
if (wsInflightOptimized.size > 2000) {
wsInflightOptimized.clear();
}
return;
}
@@ -250,15 +312,21 @@ function logWsOptimized(direction: "in" | "out", kind: string, meta?: Record<str
return;
}
if (direction !== "out" || kind !== "res") return;
if (direction !== "out" || kind !== "res") {
return;
}
const startedAt = inflightKey ? wsInflightOptimized.get(inflightKey) : undefined;
if (inflightKey) wsInflightOptimized.delete(inflightKey);
if (inflightKey) {
wsInflightOptimized.delete(inflightKey);
}
const durationMs = typeof startedAt === "number" ? Date.now() - startedAt : undefined;
const shouldLog =
ok === false || (typeof durationMs === "number" && durationMs >= DEFAULT_WS_SLOW_MS);
if (!shouldLog) return;
if (!shouldLog) {
return;
}
const statusToken =
ok === undefined ? undefined : ok ? chalk.greenBright("✓") : chalk.redBright("✗");
@@ -267,9 +335,15 @@ function logWsOptimized(direction: "in" | "out", kind: string, meta?: Record<str
const restMeta: string[] = [];
if (meta) {
for (const [key, value] of Object.entries(meta)) {
if (value === undefined) continue;
if (key === "connId" || key === "id") continue;
if (key === "method" || key === "ok") continue;
if (value === undefined) {
continue;
}
if (key === "connId" || key === "id") {
continue;
}
if (key === "method" || key === "ok") {
continue;
}
restMeta.push(`${chalk.dim(key)}=${formatForLog(value)}`);
}
}
@@ -301,7 +375,9 @@ function logWsCompact(direction: "in" | "out", kind: string, meta?: Record<strin
}
const compactArrow = (() => {
if (kind === "req" || kind === "res") return "⇄";
if (kind === "req" || kind === "res") {
return "⇄";
}
return direction === "in" ? "←" : "→";
})();
const arrowColor =
@@ -340,10 +416,18 @@ function logWsCompact(direction: "in" | "out", kind: string, meta?: Record<strin
const restMeta: string[] = [];
if (meta) {
for (const [key, value] of Object.entries(meta)) {
if (value === undefined) continue;
if (key === "connId" || key === "id") continue;
if (key === "method" || key === "ok") continue;
if (key === "event") continue;
if (value === undefined) {
continue;
}
if (key === "connId" || key === "id") {
continue;
}
if (key === "method" || key === "ok") {
continue;
}
if (key === "event") {
continue;
}
restMeta.push(`${chalk.dim(key)}=${formatForLog(value)}`);
}
}
@@ -353,7 +437,9 @@ function logWsCompact(direction: "in" | "out", kind: string, meta?: Record<strin
trailing.push(`${chalk.dim("conn")}=${chalk.gray(shortId(connId))}`);
wsLastCompactConnId = connId;
}
if (id) trailing.push(`${chalk.dim("id")}=${chalk.gray(shortId(id))}`);
if (id) {
trailing.push(`${chalk.dim("id")}=${chalk.gray(shortId(id))}`);
}
const tokens = [prefix, statusToken, headline, durationToken, ...restMeta, ...trailing].filter(
(t): t is string => Boolean(t),