UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,006 lines (998 loc) 604 kB
import { $ as DEFAULT_CHAT_CHANNEL, A as getChildLogger, B as resolveConfigPath, C as setVerbose, D as colorize, F as CONFIG_PATH, L as STATE_DIR, M as getResolvedLoggerSettings, O as isRich, R as isNixMode, U as resolveGatewayLockDir, W as resolveGatewayPort, X as resolveStateDir, Z as CHANNEL_IDS, j as getLogger, k as theme, l as setConsoleSubsystemFilter, n as isTruthyEnvValue, o as createSubsystemLogger, p as defaultRuntime, r as logAcceptedEnvOption, s as runtimeForLogger, st as getActivePluginRegistry, u as setConsoleTimestampPrefix } from "./entry.js"; import { I as resolveHooksGmailModel, N as resolveAllowedModelRef, O as getModelRefStatus, P as resolveConfiguredModelRef, R as resolveThinkingDefault, _t as DEFAULT_MODEL, gt as DEFAULT_CONTEXT_TOKENS, k as isCliProvider, vt as DEFAULT_PROVIDER } from "./auth-profiles-CfFGCDJa.js"; import { t as formatCliCommand } from "./command-format-3xiXujG0.js"; import { E as DEFAULT_ACCOUNT_ID, F as resolveAgentIdFromSessionKey, H as parseAgentSessionKey, N as normalizeAgentId, P as normalizeMainKey, R as toAgentRequestSessionKey, T as resolveWorkspaceTemplateDir, V as isSubagentSessionKey, _ as DEFAULT_MEMORY_FILENAME, b as DEFAULT_USER_FILENAME, c as resolveDefaultAgentId, d as DEFAULT_AGENTS_FILENAME, g as DEFAULT_MEMORY_ALT_FILENAME, h as DEFAULT_IDENTITY_FILENAME, i as resolveAgentModelFallbacksOverride, k as buildAgentMainSessionKey, l as resolveSessionAgentId, m as DEFAULT_HEARTBEAT_FILENAME, n as resolveAgentConfig, p as DEFAULT_BOOTSTRAP_FILENAME, r as resolveAgentDir, s as resolveAgentWorkspaceDir, t as listAgentIds, v as DEFAULT_SOUL_FILENAME, w as resolveDefaultAgentWorkspaceDir, x as ensureAgentWorkspace, y as DEFAULT_TOOLS_FILENAME } from "./agent-scope-jm0ZdXwM.js"; import { b as truncateUtf16Safe, g as shortenHomePath, m as resolveUserPath, o as ensureDir, t as CONFIG_DIR } from "./utils-PmTbZoD1.js"; import { a as logDebug, c as logWarn, n as runExec, t as runCommandWithTimeout } from "./exec-BIMFe4XS.js"; import { t as resolveOpenClawPackageRoot } from "./openclaw-root-Dean7Fp2.js"; import "./github-copilot-token-rP-6QdKv.js"; import "./pi-model-discovery-CsRo-xMp.js"; import { A as resolveAgentMaxConcurrent, E as applyLegacyMigrations, M as VERSION, a as readConfigFileSnapshot, i as parseConfigJson5, j as resolveSubagentMaxConcurrent, l as validateConfigObjectWithPlugins, n as createConfigIO, o as resolveConfigSnapshotHash, r as loadConfig, s as writeConfigFile, t as migrateLegacyConfig, u as OpenClawSchema } from "./config-DCT1RAo6.js"; import { o as isTestDefaultMemorySlotDisabled } from "./manifest-registry-tuAcHxrV.js"; import "./server-context-CM_E6wD5.js"; import { d as ensurePortAvailable, f as inspectPortUsage, m as formatPortDiagnostics, n as formatErrorMessage } from "./errors-DdT2Dtkb.js"; import { t as rawDataToString } from "./ws-DlSkA_BG.js"; import { r as startBrowserControlServiceFromConfig, t as createBrowserControlContext } from "./control-service-Ds9ompnU.js"; import { t as ensureOpenClawCliOnPath } from "./path-env-CUhrC5DA.js"; import { i as enableTailscaleServe, n as disableTailscaleServe, o as getTailnetHostname, r as enableTailscaleFunnel, t as disableTailscaleFunnel } from "./tailscale-BUcKO8Rr.js"; import { n as pickPrimaryTailnetIPv6, t as pickPrimaryTailnetIPv4 } from "./tailnet-BijMqkqa.js"; import { a as resolveGatewayBindHost, n as isLoopbackHost$2, o as resolveGatewayClientIp, r as isTrustedProxyAddress, s as resolveGatewayListenHosts, t as isLoopbackAddress } from "./net-DaJz_a4n.js"; import { i as resolveGatewayAuth, n as authorizeGatewayConnect, r as isLocalDirectRequest, t as assertGatewayAuthConfigured } from "./auth-viF_w60n.js"; import { $ as validatePollParams, A as validateDevicePairListParams, B as validateLogsTailParams, C as validateCronListParams, Ct as ErrorCodes, D as validateCronStatusParams, Dt as buildDeviceAuthPayload, E as validateCronRunsParams, Et as parseSessionLabel, F as validateExecApprovalResolveParams, G as validateNodeInvokeResultParams, H as validateNodeDescribeParams, I as validateExecApprovalsGetParams, J as validateNodePairListParams, K as validateNodeListParams, L as validateExecApprovalsNodeGetParams, M as validateDeviceTokenRevokeParams, Mt as verifyDeviceSignature, N as validateDeviceTokenRotateParams, O as validateCronUpdateParams, P as validateExecApprovalRequestParams, Q as validateNodeRenameParams, R as validateExecApprovalsNodeSetParams, S as validateCronAddParams, St as PROTOCOL_VERSION, T as validateCronRunParams, U as validateNodeEventParams, V as validateModelsListParams, W as validateNodeInvokeParams, X as validateNodePairRequestParams, Y as validateNodePairRejectParams, Z as validateNodePairVerifyParams, _ as validateConfigGetParams, _t as validateWebLoginWaitParams, a as validateAgentWaitParams, at as validateSessionsPatchParams, b as validateConfigSetParams, bt as validateWizardStartParams, c as validateAgentsFilesSetParams, ct as validateSessionsResolveParams, d as validateChannelsStatusParams, dt as validateSkillsStatusParams, et as validateRequestFrame, f as validateChatAbortParams, ft as validateSkillsUpdateParams, g as validateConfigApplyParams, gt as validateWebLoginStartParams, h as validateChatSendParams, ht as validateWakeParams, i as validateAgentParams, it as validateSessionsListParams, j as validateDevicePairRejectParams, jt as normalizeDevicePublicKeyBase64Url, k as validateDevicePairApproveParams, kt as deriveDeviceIdFromPublicKey, l as validateAgentsListParams, lt as validateSkillsBinsParams, m as validateChatInjectParams, mt as validateUpdateRunParams, n as formatValidationErrors, nt as validateSessionsCompactParams, o as validateAgentsFilesGetParams, ot as validateSessionsPreviewParams, p as validateChatHistoryParams, pt as validateTalkModeParams, q as validateNodePairApproveParams, r as validateAgentIdentityParams, rt as validateSessionsDeleteParams, s as validateAgentsFilesListParams, st as validateSessionsResetParams, tt as validateSendParams, u as validateChannelsLogoutParams, ut as validateSkillsInstallParams, v as validateConfigPatchParams, vt as validateWizardCancelParams, w as validateCronRemoveParams, wt as errorShape, x as validateConnectParams, xt as validateWizardStatusParams, y as validateConfigSchemaParams, yt as validateWizardNextParams, z as validateExecApprovalsSetParams } from "./client-cU7Xg1MO.js"; import { i as loadGatewayTlsRuntime$1, n as callGateway } from "./call-CfqL-4Nc.js"; import { f as GATEWAY_CLIENT_IDS, i as isGatewayMessageChannel, l as normalizeMessageChannel, m as GATEWAY_CLIENT_NAMES, n as isDeliverableMessageChannel, p as GATEWAY_CLIENT_MODES, r as isGatewayCliClient, s as isWebchatClient, t as INTERNAL_MESSAGE_CHANNEL } from "./message-channel-CAFcg7mw.js"; import { t as formatDocsLink } from "./links-jGisPfXW.js"; import { r as buildChannelUiCatalog, t as applyPluginAutoEnable } from "./plugin-auto-enable-CIVp3SAp.js"; import { n as listChannelPlugins, r as normalizeChannelId, t as getChannelPlugin } from "./plugins-TrKFfrLt.js"; import "./logging-fywhKCmE.js"; import "./accounts-B5QZU96b.js"; import { $ as approveNodePairing, $n as resolveTtsPrefsPath, $t as listSessionsFromStore, An as consumeGatewaySigusr1RestartAuthorization, Bn as isSystemEventContextChanged, Bt as emitAgentEvent, C as buildControlUiAvatarUrl, Cn as resolveSessionDeliveryTarget, Cr as resolveAgentIdentity, Ct as normalizeMimeList, Dn as runWithModelFallback, Dr as createInternalHookEvent, Dt as runEmbeddedPiAgent, Er as clearInternalHooks, Et as stopSubagentsForRequester, Fn as normalizeCronJobPatch, Ft as getHookType, G as normalizeSendPolicy, Gn as OPENAI_TTS_MODELS, H as setCliSessionId, Hn as requestHeartbeatNow, Ht as onAgentEvent, I as getChannelActivity, In as migrateLegacyCronPayload, It as isExternalHookSession, J as primeRemoteSkillsCache, Jn as isTtsEnabled, Jt as loadProviderUsageSummary, K as resolveSendPolicy, Kn as OPENAI_TTS_VOICES, Kt as loadModelCatalog, Lt as initSubagentRegistry, M as onHeartbeatEvent, Mn as scheduleGatewaySigusr1Restart, Mt as createOpenClawTools, Nn as setGatewaySigusr1RestartPolicy, Nt as buildSafeExternalPrompt, O as buildHistoryContextFromEntries, Or as registerInternalHook, Ot as abortEmbeddedPiRun, Pn as normalizeCronJobCreate, Pt as detectSuspiciousPatterns, Q as setSkillsRemoteRegistry, Qn as resolveTtsConfig, Qt as listAgentsForGateway, R as dispatchInboundMessage, Rt as resolveAgentTimeoutMs, S as CONTROL_UI_AVATAR_PREFIX, Sn as resolveOutboundTarget, St as extractImageContentFromSource, T as resolveAssistantAvatarUrl, Tn as resetDirectoryCache, Tr as resolveIdentityName, Tt as isAbortTrigger, U as runCliAgent, Ut as registerAgentRunContext, V as getCliSessionId, Vt as getAgentRunContext, W as loadCostUsageSummary, Wn as getPluginToolMeta, Wt as resolveAnnounceTargetFromKey, X as refreshRemoteBinsForConnectedNodes, Xn as resolveTtsApiKey, Y as recordRemoteNodeInfo, Yn as isTtsProviderConfigured, Z as refreshRemoteNodeBins, Zn as resolveTtsAutoMode, _n as ensureOutboundSessionEntry, _r as formatUserTime, _t as DEFAULT_INPUT_PDF_MAX_PAGES, an as capArrayByJsonBytes, ar as setCommandLaneConcurrency, at as verifyNodeToken, bt as DEFAULT_INPUT_TIMEOUT_MS, cn as resolveSessionTranscriptCandidates, cr as stopDiagnosticHeartbeat, d as handleReset, dn as hasNonzeroUsage, dt as DEFAULT_INPUT_FILE_MAX_BYTES, en as loadCombinedSessionStoreForGateway, er as resolveTtsProviderOrder, et as listNodePairing, fn as lookupContextTokens, ft as DEFAULT_INPUT_FILE_MAX_CHARS, gr as normalizePollInput, gt as DEFAULT_INPUT_MAX_REDIRECTS, hn as normalizeGroupActivation, ht as DEFAULT_INPUT_IMAGE_MIMES, in as archiveFileOnDisk, it as updatePairedNodeMetadata, j as getLastHeartbeatEvent, jn as isGatewaySigusr1RestartExternallyAllowed, jt as registerUnhandledRejectionHandler, k as resolveHeartbeatVisibility, kn as authorizeGatewaySigusr1Restart, kr as triggerInternalHook, kt as waitForEmbeddedPiRunEnd, ln as stripEnvelopeFromMessages, lr as isDiagnosticsEnabled, lt as applyVerboseOverride, mn as clearSessionQueues, mr as stripHeartbeatToken, mt as DEFAULT_INPUT_IMAGE_MAX_BYTES, n as handleSlackHttpRequest, nn as resolveGatewaySessionStoreTarget, nr as setTtsProvider, nt as renamePairedNode, on as readSessionMessages, or as CommandLane, ot as getSkillsSnapshotVersion, pn as extractShortModelName, pt as DEFAULT_INPUT_FILE_MIMES, q as getRemoteSkillEligibility, qn as getTtsProvider, qt as applyModelOverrideToSessionEntry, rn as resolveSessionModelRef, rr as textToSpeech, rt as requestNodePairing, sn as readSessionPreviewItemsFromTranscript, sr as startDiagnosticHeartbeat, st as registerSkillsChangeListener, t as loadOpenClawPlugins, tn as loadSessionEntry, tr as setTtsEnabled, tt as rejectNodePairing, ur as DEFAULT_HEARTBEAT_ACK_MAX_CHARS, ut as parseVerboseOverride, vn as resolveOutboundSessionRoute, vr as resolveUserTimeFormat, vt as DEFAULT_INPUT_PDF_MAX_PIXELS, w as normalizeControlUiBasePath, wr as resolveEffectiveMessagesConfig, wt as formatZonedTimestamp, xt as extractFileContentFromSource, yr as resolveUserTimezone, yt as DEFAULT_INPUT_PDF_MIN_TEXT_CHARS, z as createReplyDispatcher, zn as enqueueSystemEvent, zt as clearAgentRunContext } from "./loader-BYWxo-_j.js"; import { n as withProgress } from "./progress-Dn3kWpaL.js"; import "./prompt-style-D5D7b3cX.js"; import { t as WizardCancelledError } from "./prompts-CSlE5VZB.js"; import { t as resolveChannelDefaultAccountId } from "./helpers-DfgBr1D5.js"; import "./onboard-channels-CF7lTDNu.js"; import "./archive-mFgwsll-.js"; import "./installs-C5cjVarj.js"; import "./manager-rDmdE7O9.js"; import { o as resolveStorePath, r as resolveSessionTranscriptPath } from "./paths-RvF0P6tQ.js"; import "./sqlite-B_L84oiu.js"; import { m as detectMime, r as saveMediaBuffer } from "./routes-yI5QIzeL.js"; import { B as normalizeVerboseLevel, H as supportsXHighThinking, I as normalizeElevatedLevel, L as normalizeReasoningLevel, M as formatThinkingLevels, N as formatXHighModelHint, R as normalizeThinkLevel, z as normalizeUsageDisplay } from "./pi-embedded-helpers-DJgCXZEz.js"; import { a as normalizeReplyPayloadsForDelivery, t as deliverOutboundPayloads } from "./deliver-eE21zdeQ.js"; import { F as resolveExplicitAgentSessionKey, I as resolveMainSessionKey, J as expandPolicyWithPluginGroups, K as buildPluginToolGroups, L as resolveMainSessionKeyFromConfig, P as resolveAgentMainSessionKey, Q as stripPluginOnlyAllowlist, S as mergeDeliveryContext, X as normalizeToolName, Z as resolveToolProfilePolicy, b as deliveryContextFromSession, d as loadSessionStore, g as updateSessionStore, q as collectExplicitAllowlist, w as normalizeSessionDeliveryFields, z as snapshotSessionOrigin } from "./sandbox-Cnq9TXEn.js"; import { m as formatUsd, p as formatTokenCount } from "./channel-summary-BkqO8zZ9.js"; import { i as getMachineDisplayName, r as createBrowserRouteDispatcher } from "./wsl-DASmek7h.js"; import { d as hasBinary, i as loadWorkspaceSkillEntries, r as buildWorkspaceSkillSnapshot } from "./skills-DtwGIkTI.js"; import "./image-CXg7Z0WD.js"; import { c as normalizeExecApprovals, g as saveExecApprovals, l as readExecApprovalsSnapshot, m as resolveExecApprovalsSocketPath, r as ensureExecApprovals } from "./exec-approvals-WdYFyy5N.js"; import "./redact-CDPAzwi8.js"; import "./tool-display-BMYWrp0L.js"; import { t as parseAbsoluteTimeMs } from "./parse-BB0Cqon8.js"; import { a as summarizeRestartSentinel, n as formatDoctorNonInteractiveHint, r as formatRestartSentinelMessage, s as writeRestartSentinel, t as consumeRestartSentinel } from "./restart-sentinel-DywisDen.js"; import { n as resolveMessageChannelSelection } from "./channel-selection-BAwiO0li.js"; import { c as resolveSubagentToolPolicy, i as filterToolsByPolicy, o as resolveEffectiveToolPolicy, s as resolveGroupToolPolicy } from "./commands-DMKDOFmC.js"; import "./pairing-store-DMex6WWe.js"; import "./login-qr-sEcxw1_U.js"; import { r as runCommandWithRuntime } from "./cli-utils-DFbPmfWB.js"; import "./pairing-labels-C6I3dD-m.js"; import { t as buildChannelAccountSnapshot } from "./status-IDW1pkc0.js"; import "./channels-status-issues-DRXQXvhY.js"; import { n as createOutboundSendDeps, t as createDefaultDeps } from "./deps-wXkiMwLZ.js"; import "./daemon-runtime-ELWW7q0C.js"; import "./service-BoDHq_LN.js"; import "./systemd-DAgZTW06.js"; import "./shared-DBGw227P.js"; import { a as runDaemonStop, i as runDaemonStart, n as runDaemonStatus, o as runDaemonUninstall, r as runDaemonRestart, s as runDaemonInstall } from "./daemon-cli-yy_MAYxv.js"; import "./service-audit-Bf33pqEM.js"; import "./table-f0EgX-YI.js"; import { n as resolveWideAreaDiscoveryDomain, r as writeWideAreaGatewayZone } from "./widearea-dns-D4yoSNNw.js"; import { a as toOptionString, i as parsePort$1, n as extractGatewayMiskeys, r as maybeExplainGatewayServiceStop, t as describeUnknownError } from "./shared-D42-KbPa.js"; import { i as probeGateway } from "./audit-D-OqdjQu.js"; import { _ as resolveControlUiRootSync, g as resolveControlUiRootOverrideSync, h as ensureControlUiAssetsBuilt, n as installSkill, v as discoverGatewayBeacons } from "./onboard-skills-DdFGj9pt.js"; import { c as startHeartbeatRunner, n as formatHealthChannelLines, o as runHeartbeatOnce, r as getHealthSnapshot, s as setHeartbeatsEnabled } from "./health-format-Ct8J0fwc.js"; import { c as compareSemverStrings, f as DEFAULT_PACKAGE_CHANNEL, h as normalizeUpdateChannel, s as checkUpdateStatus, t as runGatewayUpdate, u as resolveNpmChannelTag } from "./update-runner-8XFxQglY.js"; import "./github-copilot-auth-BHLcQ1sN.js"; import "./logging-BnUUuH3y.js"; import { i as shouldIncludeHook, n as loadWorkspaceHookEntries, r as resolveHookConfig } from "./hooks-status-I3Y60zVN.js"; import { f as runOnboardingWizard, n as getStatusSummary, s as loadAgentIdentity, u as loadAgentIdentityFromWorkspace } from "./status-BIWeu5mN.js"; import { t as buildWorkspaceSkillStatus } from "./skills-status-CWkBweWW.js"; import "./tui-DBmFYQTk.js"; import { i as setGatewayWsLogStyle, n as logWs, r as summarizeAgentEventForWsLog, t as formatForLog } from "./ws-log-C4IerKk4.js"; import { T as resolveGmailHookRuntimeConfig, _ as buildGogWatchServeArgs, i as ensureTailscaleEndpoint, v as buildGogWatchStartArgs } from "./gmail-setup-utils-4czdWNCN.js"; import { a as createOutboundSendDeps$1, i as resolveAgentOutboundTarget, r as resolveAgentDeliveryPlan, t as agentCommand } from "./agent-BBI-UGkN.js"; import "./node-service-BW_LhY5w.js"; import "./status.update-ChwCPLBt.js"; import { n as forceFreePortAndWait } from "./ports-B27T1uRn.js"; import { spawn, spawnSync } from "node:child_process"; import path from "node:path"; import os from "node:os"; import chalk from "chalk"; import * as fsSync from "node:fs"; import fs, { constants } from "node:fs"; import JSON5 from "json5"; import fs$1 from "node:fs/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; import crypto, { createHash, randomUUID } from "node:crypto"; import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent"; import { z } from "zod"; import { createServer } from "node:http"; import { WebSocketServer } from "ws"; import { Buffer as Buffer$1 } from "node:buffer"; import net from "node:net"; import chokidar from "chokidar"; import { createServer as createServer$1 } from "node:https"; import { Cron } from "croner"; //#region src/infra/ssh-config.ts function parsePort(value) { if (!value) return; const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) return; return parsed; } function parseSshConfigOutput(output) { const result = { identityFiles: [] }; const lines = output.split("\n"); for (const raw of lines) { const line = raw.trim(); if (!line) continue; const [key, ...rest] = line.split(/\s+/); const value = rest.join(" ").trim(); if (!key || !value) continue; switch (key) { case "user": result.user = value; break; case "hostname": result.host = value; break; case "port": result.port = parsePort(value); break; case "identityfile": if (value !== "none") result.identityFiles.push(value); break; default: break; } } return result; } async function resolveSshConfig(target, opts = {}) { const sshPath = "/usr/bin/ssh"; const args = ["-G"]; if (target.port > 0 && target.port !== 22) args.push("-p", String(target.port)); if (opts.identity?.trim()) args.push("-i", opts.identity.trim()); const userHost = target.user ? `${target.user}@${target.host}` : target.host; args.push("--", userHost); return await new Promise((resolve) => { const child = spawn(sshPath, args, { stdio: [ "ignore", "pipe", "ignore" ] }); let stdout = ""; child.stdout?.setEncoding("utf8"); child.stdout?.on("data", (chunk) => { stdout += String(chunk); }); const timeoutMs = Math.max(200, opts.timeoutMs ?? 800); const timer = setTimeout(() => { try { child.kill("SIGKILL"); } finally { resolve(null); } }, timeoutMs); child.once("error", () => { clearTimeout(timer); resolve(null); }); child.once("exit", (code) => { clearTimeout(timer); if (code !== 0 || !stdout.trim()) { resolve(null); return; } resolve(parseSshConfigOutput(stdout)); }); }); } //#endregion //#region src/infra/ssh-tunnel.ts function isErrno(err) { return Boolean(err && typeof err === "object" && "code" in err); } function parseSshTarget(raw) { const trimmed = raw.trim().replace(/^ssh\s+/, ""); if (!trimmed) return null; const [userPart, hostPart] = trimmed.includes("@") ? (() => { const idx = trimmed.indexOf("@"); const user = trimmed.slice(0, idx).trim(); const host = trimmed.slice(idx + 1).trim(); return [user || void 0, host]; })() : [void 0, trimmed]; const colonIdx = hostPart.lastIndexOf(":"); if (colonIdx > 0 && colonIdx < hostPart.length - 1) { const host = hostPart.slice(0, colonIdx).trim(); const portRaw = hostPart.slice(colonIdx + 1).trim(); const port = Number.parseInt(portRaw, 10); if (!host || !Number.isFinite(port) || port <= 0) return null; if (host.startsWith("-")) return null; return { user: userPart, host, port }; } if (!hostPart) return null; if (hostPart.startsWith("-")) return null; return { user: userPart, host: hostPart, port: 22 }; } async function pickEphemeralPort() { return await new Promise((resolve, reject) => { const server = net.createServer(); server.once("error", reject); server.listen(0, "127.0.0.1", () => { const addr = server.address(); server.close(() => { if (!addr || typeof addr === "string") { reject(/* @__PURE__ */ new Error("failed to allocate a local port")); return; } resolve(addr.port); }); }); }); } async function canConnectLocal(port) { return await new Promise((resolve) => { const socket = net.connect({ host: "127.0.0.1", port }); const done = (ok) => { socket.removeAllListeners(); socket.destroy(); resolve(ok); }; socket.once("connect", () => done(true)); socket.once("error", () => done(false)); socket.setTimeout(250, () => done(false)); }); } async function waitForLocalListener(port, timeoutMs) { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { if (await canConnectLocal(port)) return; await new Promise((r) => setTimeout(r, 50)); } throw new Error(`ssh tunnel did not start listening on localhost:${port}`); } async function startSshPortForward(opts) { const parsed = parseSshTarget(opts.target); if (!parsed) throw new Error(`invalid SSH target: ${opts.target}`); let localPort = opts.localPortPreferred; try { await ensurePortAvailable(localPort); } catch (err) { if (isErrno(err) && err.code === "EADDRINUSE") localPort = await pickEphemeralPort(); else throw err; } const userHost = parsed.user ? `${parsed.user}@${parsed.host}` : parsed.host; const args = [ "-N", "-L", `${localPort}:127.0.0.1:${opts.remotePort}`, "-p", String(parsed.port), "-o", "ExitOnForwardFailure=yes", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "UpdateHostKeys=yes", "-o", "ConnectTimeout=5", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3" ]; if (opts.identity?.trim()) args.push("-i", opts.identity.trim()); args.push("--", userHost); const stderr = []; const child = spawn("/usr/bin/ssh", args, { stdio: [ "ignore", "ignore", "pipe" ] }); child.stderr?.setEncoding("utf8"); child.stderr?.on("data", (chunk) => { const lines = String(chunk).split("\n").map((l) => l.trim()).filter(Boolean); stderr.push(...lines); }); const stop = async () => { if (child.killed) return; child.kill("SIGTERM"); await new Promise((resolve) => { const t = setTimeout(() => { try { child.kill("SIGKILL"); } finally { resolve(); } }, 1500); child.once("exit", () => { clearTimeout(t); resolve(); }); }); }; try { await Promise.race([waitForLocalListener(localPort, Math.max(250, opts.timeoutMs)), new Promise((_, reject) => { child.once("exit", (code, signal) => { reject(/* @__PURE__ */ new Error(`ssh exited (${code ?? "null"}${signal ? `/${signal}` : ""})`)); }); })]); } catch (err) { await stop(); const suffix = stderr.length > 0 ? `\n${stderr.join("\n")}` : ""; throw new Error(`${err instanceof Error ? err.message : String(err)}${suffix}`, { cause: err }); } return { parsedTarget: parsed, localPort, remotePort: opts.remotePort, pid: typeof child.pid === "number" ? child.pid : null, stderr, stop }; } //#endregion //#region src/commands/gateway-status/helpers.ts function parseIntOrNull(value) { const s = typeof value === "string" ? value.trim() : typeof value === "number" || typeof value === "bigint" ? String(value) : ""; if (!s) return null; const n = Number.parseInt(s, 10); return Number.isFinite(n) ? n : null; } function parseTimeoutMs(raw, fallbackMs) { const value = typeof raw === "string" ? raw.trim() : typeof raw === "number" || typeof raw === "bigint" ? String(raw) : ""; if (!value) return fallbackMs; const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`invalid --timeout: ${value}`); return parsed; } function normalizeWsUrl(value) { const trimmed = value.trim(); if (!trimmed) return null; if (!trimmed.startsWith("ws://") && !trimmed.startsWith("wss://")) return null; return trimmed; } function resolveTargets(cfg, explicitUrl) { const targets = []; const add = (t) => { if (!targets.some((x) => x.url === t.url)) targets.push(t); }; const explicit = typeof explicitUrl === "string" ? normalizeWsUrl(explicitUrl) : null; if (explicit) add({ id: "explicit", kind: "explicit", url: explicit, active: true }); const remoteUrl = typeof cfg.gateway?.remote?.url === "string" ? normalizeWsUrl(cfg.gateway.remote.url) : null; if (remoteUrl) add({ id: "configRemote", kind: "configRemote", url: remoteUrl, active: cfg.gateway?.mode === "remote" }); add({ id: "localLoopback", kind: "localLoopback", url: `ws://127.0.0.1:${resolveGatewayPort(cfg)}`, active: cfg.gateway?.mode !== "remote" }); return targets; } function resolveProbeBudgetMs(overallMs, kind) { if (kind === "localLoopback") return Math.min(800, overallMs); if (kind === "sshTunnel") return Math.min(2e3, overallMs); return Math.min(1500, overallMs); } function sanitizeSshTarget(value) { if (typeof value !== "string") return null; const trimmed = value.trim(); if (!trimmed) return null; return trimmed.replace(/^ssh\\s+/, ""); } function resolveAuthForTarget(cfg, target, overrides) { const tokenOverride = overrides.token?.trim() ? overrides.token.trim() : void 0; const passwordOverride = overrides.password?.trim() ? overrides.password.trim() : void 0; if (tokenOverride || passwordOverride) return { token: tokenOverride, password: passwordOverride }; if (target.kind === "configRemote" || target.kind === "sshTunnel") { const token = typeof cfg.gateway?.remote?.token === "string" ? cfg.gateway.remote.token.trim() : ""; const remotePassword = (cfg.gateway?.remote)?.password; const password = typeof remotePassword === "string" ? remotePassword.trim() : ""; return { token: token.length > 0 ? token : void 0, password: password.length > 0 ? password : void 0 }; } const envToken = process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || ""; const envPassword = process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || ""; const cfgToken = typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway.auth.token.trim() : ""; const cfgPassword = typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway.auth.password.trim() : ""; return { token: envToken || cfgToken || void 0, password: envPassword || cfgPassword || void 0 }; } function pickGatewaySelfPresence(presence) { if (!Array.isArray(presence)) return null; const entries = presence; const self = entries.find((e) => e.mode === "gateway" && e.reason === "self") ?? entries.find((e) => typeof e.text === "string" && String(e.text).startsWith("Gateway:")) ?? null; if (!self) return null; return { host: typeof self.host === "string" ? self.host : void 0, ip: typeof self.ip === "string" ? self.ip : void 0, version: typeof self.version === "string" ? self.version : void 0, platform: typeof self.platform === "string" ? self.platform : void 0 }; } function extractConfigSummary(snapshotUnknown) { const snap = snapshotUnknown; const path = typeof snap?.path === "string" ? snap.path : null; const exists = Boolean(snap?.exists); const valid = Boolean(snap?.valid); const issuesRaw = Array.isArray(snap?.issues) ? snap.issues : []; const legacyRaw = Array.isArray(snap?.legacyIssues) ? snap.legacyIssues : []; const cfg = snap?.config ?? {}; const gateway = cfg.gateway ?? {}; const wideArea = (cfg.discovery ?? {}).wideArea ?? {}; const remote = gateway.remote ?? {}; const auth = gateway.auth ?? {}; const controlUi = gateway.controlUi ?? {}; const tailscale = gateway.tailscale ?? {}; const authMode = typeof auth.mode === "string" ? auth.mode : null; const authTokenConfigured = typeof auth.token === "string" ? auth.token.trim().length > 0 : false; const authPasswordConfigured = typeof auth.password === "string" ? auth.password.trim().length > 0 : false; const remoteUrl = typeof remote.url === "string" ? normalizeWsUrl(remote.url) : null; const remoteTokenConfigured = typeof remote.token === "string" ? remote.token.trim().length > 0 : false; const remotePasswordConfigured = typeof remote.password === "string" ? String(remote.password).trim().length > 0 : false; const wideAreaEnabled = typeof wideArea.enabled === "boolean" ? wideArea.enabled : null; return { path, exists, valid, issues: issuesRaw.filter((i) => Boolean(i && typeof i.path === "string" && typeof i.message === "string")).map((i) => ({ path: i.path, message: i.message })), legacyIssues: legacyRaw.filter((i) => Boolean(i && typeof i.path === "string" && typeof i.message === "string")).map((i) => ({ path: i.path, message: i.message })), gateway: { mode: typeof gateway.mode === "string" ? gateway.mode : null, bind: typeof gateway.bind === "string" ? gateway.bind : null, port: parseIntOrNull(gateway.port), controlUiEnabled: typeof controlUi.enabled === "boolean" ? controlUi.enabled : null, controlUiBasePath: typeof controlUi.basePath === "string" ? controlUi.basePath : null, authMode, authTokenConfigured, authPasswordConfigured, remoteUrl, remoteTokenConfigured, remotePasswordConfigured, tailscaleMode: typeof tailscale.mode === "string" ? tailscale.mode : null }, discovery: { wideAreaEnabled } }; } function buildNetworkHints(cfg) { const tailnetIPv4 = pickPrimaryTailnetIPv4(); const port = resolveGatewayPort(cfg); return { localLoopbackUrl: `ws://127.0.0.1:${port}`, localTailnetUrl: tailnetIPv4 ? `ws://${tailnetIPv4}:${port}` : null, tailnetIPv4: tailnetIPv4 ?? null }; } function renderTargetHeader(target, rich) { const kindLabel = target.kind === "localLoopback" ? "Local loopback" : target.kind === "sshTunnel" ? "Remote over SSH" : target.kind === "configRemote" ? target.active ? "Remote (configured)" : "Remote (configured, inactive)" : "URL (explicit)"; return `${colorize(rich, theme.heading, kindLabel)} ${colorize(rich, theme.muted, target.url)}`; } function renderProbeSummaryLine(probe, rich) { if (probe.ok) { const latency = typeof probe.connectLatencyMs === "number" ? `${probe.connectLatencyMs}ms` : "unknown"; return `${colorize(rich, theme.success, "Connect: ok")} (${latency}) · ${colorize(rich, theme.success, "RPC: ok")}`; } const detail = probe.error ? ` - ${probe.error}` : ""; if (probe.connectLatencyMs != null) { const latency = typeof probe.connectLatencyMs === "number" ? `${probe.connectLatencyMs}ms` : "unknown"; return `${colorize(rich, theme.success, "Connect: ok")} (${latency}) · ${colorize(rich, theme.error, "RPC: failed")}${detail}`; } return `${colorize(rich, theme.error, "Connect: failed")}${detail}`; } //#endregion //#region src/commands/gateway-status.ts async function gatewayStatusCommand(opts, runtime) { const startedAt = Date.now(); const cfg = loadConfig(); const rich = isRich() && opts.json !== true; const overallTimeoutMs = parseTimeoutMs(opts.timeout, 3e3); const wideAreaDomain = resolveWideAreaDiscoveryDomain({ configDomain: cfg.discovery?.wideArea?.domain }); const baseTargets = resolveTargets(cfg, opts.url); const network = buildNetworkHints(cfg); const discoveryTimeoutMs = Math.min(1200, overallTimeoutMs); const discoveryPromise = discoverGatewayBeacons({ timeoutMs: discoveryTimeoutMs, wideAreaDomain }); let sshTarget = sanitizeSshTarget(opts.ssh) ?? sanitizeSshTarget(cfg.gateway?.remote?.sshTarget); let sshIdentity = sanitizeSshTarget(opts.sshIdentity) ?? sanitizeSshTarget(cfg.gateway?.remote?.sshIdentity); const remotePort = resolveGatewayPort(cfg); let sshTunnelError = null; let sshTunnelStarted = false; if (!sshTarget) sshTarget = inferSshTargetFromRemoteUrl(cfg.gateway?.remote?.url); if (sshTarget) { const resolved = await resolveSshTarget(sshTarget, sshIdentity, overallTimeoutMs); if (resolved) { sshTarget = resolved.target; if (!sshIdentity && resolved.identity) sshIdentity = resolved.identity; } } const { discovery, probed } = await withProgress({ label: "Inspecting gateways…", indeterminate: true, enabled: opts.json !== true }, async () => { const tryStartTunnel = async () => { if (!sshTarget) return null; try { const tunnel = await startSshPortForward({ target: sshTarget, identity: sshIdentity ?? void 0, localPortPreferred: remotePort, remotePort, timeoutMs: Math.min(1500, overallTimeoutMs) }); sshTunnelStarted = true; return tunnel; } catch (err) { sshTunnelError = err instanceof Error ? err.message : String(err); return null; } }; const discoveryTask = discoveryPromise.catch(() => []); const tunnelTask = sshTarget ? tryStartTunnel() : Promise.resolve(null); const [discovery, tunnelFirst] = await Promise.all([discoveryTask, tunnelTask]); if (!sshTarget && opts.sshAuto) { const user = process.env.USER?.trim() || ""; const candidates = discovery.map((b) => { const host = b.tailnetDns || b.lanHost || b.host; if (!host?.trim()) return null; const sshPort = typeof b.sshPort === "number" && b.sshPort > 0 ? b.sshPort : 22; const base = user ? `${user}@${host.trim()}` : host.trim(); return sshPort !== 22 ? `${base}:${sshPort}` : base; }).filter((candidate) => Boolean(candidate && parseSshTarget(candidate))); if (candidates.length > 0) sshTarget = candidates[0] ?? null; } const tunnel = tunnelFirst || (sshTarget && !sshTunnelStarted && !sshTunnelError ? await tryStartTunnel() : null); const tunnelTarget = tunnel ? { id: "sshTunnel", kind: "sshTunnel", url: `ws://127.0.0.1:${tunnel.localPort}`, active: true, tunnel: { kind: "ssh", target: sshTarget ?? "", localPort: tunnel.localPort, remotePort, pid: tunnel.pid } } : null; const targets = tunnelTarget ? [tunnelTarget, ...baseTargets.filter((t) => t.url !== tunnelTarget.url)] : baseTargets; try { return { discovery, probed: await Promise.all(targets.map(async (target) => { const auth = resolveAuthForTarget(cfg, target, { token: typeof opts.token === "string" ? opts.token : void 0, password: typeof opts.password === "string" ? opts.password : void 0 }); const timeoutMs = resolveProbeBudgetMs(overallTimeoutMs, target.kind); const probe = await probeGateway({ url: target.url, auth, timeoutMs }); return { target, probe, configSummary: probe.configSnapshot ? extractConfigSummary(probe.configSnapshot) : null, self: pickGatewaySelfPresence(probe.presence) }; })) }; } finally { if (tunnel) try { await tunnel.stop(); } catch {} } }); const reachable = probed.filter((p) => p.probe.ok); const ok = reachable.length > 0; const multipleGateways = reachable.length > 1; const primary = reachable.find((p) => p.target.kind === "explicit") ?? reachable.find((p) => p.target.kind === "sshTunnel") ?? reachable.find((p) => p.target.kind === "configRemote") ?? reachable.find((p) => p.target.kind === "localLoopback") ?? null; const warnings = []; if (sshTarget && !sshTunnelStarted) warnings.push({ code: "ssh_tunnel_failed", message: sshTunnelError ? `SSH tunnel failed: ${String(sshTunnelError)}` : "SSH tunnel failed to start; falling back to direct probes." }); if (multipleGateways) warnings.push({ code: "multiple_gateways", message: "Unconventional setup: multiple reachable gateways detected. Usually one gateway per network is recommended unless you intentionally run isolated profiles, like a rescue bot (see docs: /gateway#multiple-gateways-same-host).", targetIds: reachable.map((p) => p.target.id) }); if (opts.json) { runtime.log(JSON.stringify({ ok, ts: Date.now(), durationMs: Date.now() - startedAt, timeoutMs: overallTimeoutMs, primaryTargetId: primary?.target.id ?? null, warnings, network, discovery: { timeoutMs: discoveryTimeoutMs, count: discovery.length, beacons: discovery.map((b) => ({ instanceName: b.instanceName, displayName: b.displayName ?? null, domain: b.domain ?? null, host: b.host ?? null, lanHost: b.lanHost ?? null, tailnetDns: b.tailnetDns ?? null, gatewayPort: b.gatewayPort ?? null, sshPort: b.sshPort ?? null, wsUrl: (() => { const host = b.tailnetDns || b.lanHost || b.host; const port = b.gatewayPort ?? 18789; return host ? `ws://${host}:${port}` : null; })() })) }, targets: probed.map((p) => ({ id: p.target.id, kind: p.target.kind, url: p.target.url, active: p.target.active, tunnel: p.target.tunnel ?? null, connect: { ok: p.probe.ok, latencyMs: p.probe.connectLatencyMs, error: p.probe.error, close: p.probe.close }, self: p.self, config: p.configSummary, health: p.probe.health, summary: p.probe.status, presence: p.probe.presence })) }, null, 2)); if (!ok) runtime.exit(1); return; } runtime.log(colorize(rich, theme.heading, "Gateway Status")); runtime.log(ok ? `${colorize(rich, theme.success, "Reachable")}: yes` : `${colorize(rich, theme.error, "Reachable")}: no`); runtime.log(colorize(rich, theme.muted, `Probe budget: ${overallTimeoutMs}ms`)); if (warnings.length > 0) { runtime.log(""); runtime.log(colorize(rich, theme.warn, "Warning:")); for (const w of warnings) runtime.log(`- ${w.message}`); } runtime.log(""); runtime.log(colorize(rich, theme.heading, "Discovery (this machine)")); const discoveryDomains = wideAreaDomain ? `local. + ${wideAreaDomain}` : "local."; runtime.log(discovery.length > 0 ? `Found ${discovery.length} gateway(s) via Bonjour (${discoveryDomains})` : `Found 0 gateways via Bonjour (${discoveryDomains})`); if (discovery.length === 0) runtime.log(colorize(rich, theme.muted, "Tip: if the gateway is remote, mDNS won’t cross networks; use Wide-Area Bonjour (split DNS) or SSH tunnels.")); runtime.log(""); runtime.log(colorize(rich, theme.heading, "Targets")); for (const p of probed) { runtime.log(renderTargetHeader(p.target, rich)); runtime.log(` ${renderProbeSummaryLine(p.probe, rich)}`); if (p.target.tunnel?.kind === "ssh") runtime.log(` ${colorize(rich, theme.muted, "ssh")}: ${colorize(rich, theme.command, p.target.tunnel.target)}`); if (p.probe.ok && p.self) { const host = p.self.host ?? "unknown"; const ip = p.self.ip ? ` (${p.self.ip})` : ""; const platform = p.self.platform ? ` · ${p.self.platform}` : ""; const version = p.self.version ? ` · app ${p.self.version}` : ""; runtime.log(` ${colorize(rich, theme.info, "Gateway")}: ${host}${ip}${platform}${version}`); } if (p.configSummary) { const c = p.configSummary; const wideArea = c.discovery.wideAreaEnabled === true ? "enabled" : c.discovery.wideAreaEnabled === false ? "disabled" : "unknown"; runtime.log(` ${colorize(rich, theme.info, "Wide-area discovery")}: ${wideArea}`); } runtime.log(""); } if (!ok) runtime.exit(1); } function inferSshTargetFromRemoteUrl(rawUrl) { if (typeof rawUrl !== "string") return null; const trimmed = rawUrl.trim(); if (!trimmed) return null; let host = null; try { host = new URL(trimmed).hostname || null; } catch { return null; } if (!host) return null; const user = process.env.USER?.trim() || ""; return user ? `${user}@${host}` : host; } function buildSshTarget(input) { const host = input.host?.trim() ?? ""; if (!host) return null; const user = input.user?.trim() ?? ""; const base = user ? `${user}@${host}` : host; const port = input.port ?? 22; if (port && port !== 22) return `${base}:${port}`; return base; } async function resolveSshTarget(rawTarget, identity, overallTimeoutMs) { const parsed = parseSshTarget(rawTarget); if (!parsed) return null; const config = await resolveSshConfig(parsed, { identity: identity ?? void 0, timeoutMs: Math.min(800, overallTimeoutMs) }); if (!config) return { target: rawTarget, identity: identity ?? void 0 }; const target = buildSshTarget({ user: config.user ?? parsed.user, host: config.host ?? parsed.host, port: config.port ?? parsed.port }); if (!target) return { target: rawTarget, identity: identity ?? void 0 }; return { target, identity: identity ?? config.identityFiles.find((entry) => entry.trim().length > 0)?.trim() ?? void 0 }; } //#endregion //#region src/cli/gateway-cli/call.ts const gatewayCallOpts = (cmd) => cmd.option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)").option("--token <token>", "Gateway token (if required)").option("--password <password>", "Gateway password (password auth)").option("--timeout <ms>", "Timeout in ms", "10000").option("--expect-final", "Wait for final response (agent)", false).option("--json", "Output JSON", false); const callGatewayCli = async (method, opts, params) => withProgress({ label: `Gateway ${method}`, indeterminate: true, enabled: opts.json !== true }, async () => await callGateway({ url: opts.url, token: opts.token, password: opts.password, method, params, expectFinal: Boolean(opts.expectFinal), timeoutMs: Number(opts.timeout ?? 1e4), clientName: GATEWAY_CLIENT_NAMES.CLI, mode: GATEWAY_CLIENT_MODES.CLI })); //#endregion //#region src/cli/gateway-cli/discover.ts function parseDiscoverTimeoutMs(raw, fallbackMs) { if (raw === void 0 || raw === null) return fallbackMs; const value = typeof raw === "string" ? raw.trim() : typeof raw === "number" || typeof raw === "bigint" ? String(raw) : null; if (value === null) throw new Error("invalid --timeout"); if (!value) return fallbackMs; const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`invalid --timeout: ${value}`); return parsed; } function pickBeaconHost(beacon) { const host = beacon.tailnetDns || beacon.lanHost || beacon.host; return host?.trim() ? host.trim() : null; } function pickGatewayPort(beacon) { const port = beacon.gatewayPort ?? 18789; return port > 0 ? port : 18789; } function dedupeBeacons(beacons) { const out = []; const seen = /* @__PURE__ */ new Set(); for (const b of beacons) { const host = pickBeaconHost(b) ?? ""; const key = [ b.domain ?? "", b.instanceName ?? "", b.displayName ?? "", host, String(b.port ?? ""), String(b.gatewayPort ?? "") ].join("|"); if (seen.has(key)) continue; seen.add(key); out.push(b); } return out; } function renderBeaconLines(beacon, rich) { const nameRaw = (beacon.displayName || beacon.instanceName || "Gateway").trim(); const domainRaw = (beacon.domain || "local.").trim(); const title = colorize(rich, theme.accentBright, nameRaw); const domain = colorize(rich, theme.muted, domainRaw); const host = pickBeaconHost(beacon); const gatewayPort = pickGatewayPort(beacon); const scheme = beacon.gatewayTls ? "wss" : "ws"; const wsUrl = host ? `${scheme}://${host}:${gatewayPort}` : null; const lines = [`- ${title} ${domain}`]; if (beacon.tailnetDns) lines.push(` ${colorize(rich, theme.info, "tailnet")}: ${beacon.tailnetDns}`); if (beacon.lanHost) lines.push(` ${colorize(rich, theme.info, "lan")}: ${beacon.lanHost}`); if (beacon.host) lines.push(` ${colorize(rich, theme.info, "host")}: ${beacon.host}`); if (wsUrl) lines.push(` ${colorize(rich, theme.muted, "ws")}: ${colorize(rich, theme.command, wsUrl)}`); if (beacon.role) lines.push(` ${colorize(rich, theme.muted, "role")}: ${beacon.role}`); if (beacon.transport) lines.push(` ${colorize(rich, theme.muted, "transport")}: ${beacon.transport}`); if (beacon.gatewayTls) { const fingerprint = beacon.gatewayTlsFingerprintSha256 ? `sha256 ${beacon.gatewayTlsFingerprintSha256}` : "enabled"; lines.push(` ${colorize(rich, theme.muted, "tls")}: ${fingerprint}`); } if (typeof beacon.sshPort === "number" && beacon.sshPort > 0 && host) { const ssh = `ssh -N -L 18789:127.0.0.1:18789 <user>@${host} -p ${beacon.sshPort}`; lines.push(` ${colorize(rich, theme.muted, "ssh")}: ${colorize(rich, theme.command, ssh)}`); } return lines; } //#endregion //#region src/gateway/server/close-reason.ts const CLOSE_REASON_MAX_BYTES = 120; function truncateCloseReason(reason, maxBytes = CLOSE_REASON_MAX_BYTES) { if (!reason) return "invalid handshake"; const buf = Buffer$1.from(reason); if (buf.length <= maxBytes) return reason; return buf.subarray(0, maxBytes).toString(); } //#endregion //#region src/infra/exec-approval-forwarder.ts const log$3 = createSubsystemLogger("gateway/exec-approvals"); const DEFAULT_MODE = "session"; function normalizeMode(mode) { return mode ?? DEFAULT_MODE; } function matchSessionFilter(sessionKey, patterns) { return patterns.some((pattern) => { try { return sessionKey.includes(pattern) || new RegExp(pattern).test(sessionKey); } catch { return sessionKey.includes(pattern); } }); } function shouldForward(params) { const config = params.config; if (!config?.enabled) return false; if (config.agentFilter?.length) { const agentId = params.request.request.agentId ?? parseAgentSessionKey(params.request.request.sessionKey)?.agentId; if (!agentId) return false; if (!config.agentFilter.includes(agentId)) return false; } if (config.sessionFilter?.length) { const sessionKey = params.request.request.sessionKey; if (!sessionKey) return false; if (!matchSessionFilter(sessionKey, config.sessionFilter)) return false; } return true; } function buildTargetKey(target) { const channel = normalizeMessageChannel(target.channel) ?? target.channel; const accountId = target.accountId ?? ""; const threadId = target.threadId ?? ""; return [ channel, target.to, accountId, threadId ].join(":"); } function buildRequestMessage(request, nowMs) { const lines = ["🔒 Exec approval required", `ID: ${request.id}`]; lines.push(`Command: ${request.request.command}`); if (request.request.cwd) lines.push(`CWD: ${request.request.cwd}`); if (request.request.host) lines.push(`Host: ${request.request.host}`); if (request.request.agentId) lines.push(`Agent: ${request.request.agentId}`); if (request.request.security) lines.push(`Security: ${request.request.security}`); if (request.request.ask) lines.push(`Ask: ${request.request.ask}`); const expiresIn = Math.max(0, Math.round((request.expiresAtMs - nowMs) / 1e3)); lines.push(`Expires in: ${expiresIn}s`); lines.push("Reply with: /approve <id> allow-once|allow-always|deny"); return lines.join("\n"); } function decisionLabel(decision) { if (decision === "allow-once") return "allowed once"; if (decision === "allow-always") return "allowed always"; return "denied"; } function buildResolvedMessage(resolved) { return `${`✅ Exec approval ${decisionLabel(resolved.decision)}.`}${resolved.resolvedBy ? ` Resolved by ${resolved.resolvedBy}.` : ""} ID: ${resolved.id}`; } function buildExpiredMessage(request) { return `⏱️ Exec approval expired. ID: ${request.id}`; } function defaultResolveSessionTarget(params) { const sessionKey = params.request.request.sessionKey?.trim(); if (!sessionKey) return null; const agentId = parseAgentSessionKey(sessionKey)?.agentId ?? params.request.request.agentId ?? "main"; const entry = loadSessionStore(resolveStorePath(params.cfg.session?.store, { agentId }))[sessionKey]; if (!entry) return null; const target = resolveSessionDeliveryTarget({ entry, requestedChannel: "last" }); if (!target.channel || !target.to) return null; if (!isDeliverableMessageChannel(target.channel)) return null; return { channel: target.channel, to: target.to, accountId: target.accountId, threadId: target.threadId }; } async function deliverToTargets(params) { const deliveries = params.targets.map(async (target) => { if (params.shouldSend && !params.shouldSend()) return; const channel = normalizeMessageChannel(target.channel) ?? target.channel; if (!isDeliverableMessageChannel(channel)) return; try { await params.deliver({ cfg: params.cfg, channel, to: target.to, accountId: target.accountId, threadId: target.threadId, payloads: [{ text: params.text }] }); } catch (err) { log$3.error(`exec approvals: failed to deliver to ${channel}:${target.to}: ${String(err)}`); } }); await Promise.allSettled(deliveries); } function createExecApprovalForwarder(deps = {}) { const getConfig = deps.getConfig ?? loadConfig; const deliver = deps.deliver ?? deliverOutboundPayloads; const nowMs = deps.nowMs ?? Date.now; const resolveSessionTarget = deps.resolveSessionTarget ?? defaultResolveSessionTarget; const pending = /* @__PURE__ */ new Map(); const handleRequested = async (request) => { const cfg = getConfig(); const config = cfg.approvals?.exec; if (!shouldForward({ config, request })) return; const mode = normalizeMode(config?.mode); const targets = []; const seen = /* @__PURE__ */ new Set(); if (mode === "session" || mode === "both") { const sessionTarget = resolveSessionTarget({ cfg, request }); if (sessionTarget) { const key = buildTargetKey(sessionTarget); if (!seen.has(key)) { seen.add(key); targets.push({ ...sessionTarget, source: "session" }); } } } if (mode === "targets" || mode === "both") { const explicitTargets = config?.targets ?? []; for (const target of explicitTargets) { const key = buildTargetKey(target); if (seen.has(key)) continue; seen.add(key); targets.push({ ...target, source: "tar