主题
OpenClaw 源码解读(十一)基础设施层
本文基于 OpenClaw 2026.3.2 源码,深入解读基础设施层(Infrastructure Layer)的架构设计与实现细节。基础设施层覆盖
src/infra/和src/logging/两大目录,是整个系统的"地基"。
一、模块概览
基础设施层是 OpenClaw 的"看不见的英雄"——它不直接面向用户,但为所有上层模块提供安全、可靠、可观测的运行基础。从文件操作安全、网络防护、日志记录到进程锁、错误处理,这一层的代码质量直接决定了系统的健壮性。
1.1 子模块分类
| 子模块 | 核心文件 | 职责 |
|---|---|---|
| 文件系统安全 | fs-safe.ts、path-guards.ts、path-safety.ts、hardlink-guards.ts、safe-open-sync.ts、boundary-path.ts、boundary-file-read.ts | 防路径遍历、防符号链接攻击、防 TOCTOU 竞态 |
| 网络安全 | net/ssrf.ts、net/fetch-guard.ts、net/proxy-fetch.ts、net/hostname.ts | SSRF 防护、DNS Pinning、代理支持 |
| 日志系统 | logging/logger.ts、logging/subsystem.ts、logging/levels.ts、logging/redact.ts、logging/console.ts | 结构化日志、子系统分级、敏感信息脱敏 |
| 重试与退避 | retry.ts、backoff.ts、retry-policy.ts | 指数退避重试、瞬态错误分类 |
| 进程管理 | gateway-lock.ts、restart.ts、restart-sentinel.ts、unhandled-rejections.ts | 单实例锁、优雅重启、未处理异常 |
| 错误处理 | errors.ts、diagnostic-events.ts、diagnostic-flags.ts | 错误分类/格式化/脱敏、诊断事件 |
| 设备与配对 | device-identity.ts、device-pairing.ts、pairing-token.ts、pairing-files.ts | 设备身份管理、配对令牌 |
| 心跳系统 | heartbeat-runner.ts、heartbeat-events.ts、heartbeat-wake.ts、heartbeat-visibility.ts | 会话心跳、事件过滤、唤醒策略 |
| 命令执行安全 | exec-safety.ts、exec-approvals.ts、exec-host.ts、exec-safe-bin-*.ts | 命令白名单、二进制信任链、执行审批 |
| 更新系统 | update-check.ts、update-runner.ts、update-global.ts | 版本检查、自动更新 |
| 服务发现 | bonjour.ts、bonjour-discovery.ts、bonjour-ciao.ts | 局域网 mDNS 服务发现 |
| 系统信息 | os-summary.ts、machine-name.ts、system-presence.ts | OS 探测、设备信息 |
| 隧道与远程 | ssh-tunnel.ts、ssh-config.ts、tailscale.ts、scp-host.ts | SSH 隧道、Tailscale 集成 |
1.2 系统全景架构
┌─────────────────────────────────────────────────────────────────────────┐
│ 基础设施层全景架构 │
│ │
│ ┌── 安全层 ─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 文件安全 网络安全 命令执行安全 │ │
│ │ ├── path-guards ├── ssrf.ts ├── exec-safety │ │
│ │ ├── fs-safe ├── fetch-guard ├── exec-approvals │ │
│ │ ├── hardlink-guards ├── dns pinning ├── exec-allowlist │ │
│ │ ├── boundary-path └── proxy-fetch └── safe-bin-trust │ │
│ │ └── safe-open-sync │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── 可观测层 ───────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 日志系统 错误处理 诊断系统 │ │
│ │ ├── logger.ts ├── errors.ts ├── diag-events │ │
│ │ ├── subsystem.ts ├── error classify ├── diag-flags │ │
│ │ ├── levels.ts └── error format └── runtime- │ │
│ │ ├── redact.ts (脱敏) status │ │
│ │ └── console.ts │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── 可靠性层 ───────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 重试/退避 进程管理 心跳系统 │ │
│ │ ├── retry.ts ├── gateway-lock.ts ├── heartbeat-runner │ │
│ │ ├── backoff.ts ├── restart.ts ├── heartbeat-events │ │
│ │ └── retry-policy.ts ├── restart-sentinel ├── heartbeat-wake │ │
│ │ └── unhandled-rej └── heartbeat-filter │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── 连接层 ─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 设备配对 服务发现 隧道/远程 更新系统 │ │
│ │ ├── device-id ├── bonjour ├── ssh-tunnel ├── check │ │
│ │ ├── pairing ├── bonjour-disc ├── ssh-config ├── run │ │
│ │ └── auth-store └── bonjour-ciao ├── tailscale └── glob │ │
│ │ └── scp-host │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘基础设施层/01-infographic-infra-overview-1775150726193.png)
二、文件系统安全
2.1 路径守卫(path-guards.ts)
所有涉及用户可控路径的文件操作,都必须通过路径守卫验证:
typescript
export function isPathInside(parentDir: string, targetPath: string): boolean {
const resolvedParent = path.resolve(parentDir) + path.sep;
const resolvedTarget = path.resolve(targetPath);
return resolvedTarget.startsWith(resolvedParent);
}这个函数看起来简单,却是防止路径遍历攻击的核心。攻击者可能传入 ../../etc/passwd,经过 path.resolve() 解析后会变成绝对路径,然后 startsWith 检查确保目标在允许的目录内。
2.2 安全文件操作(fs-safe.ts)
fs-safe.ts 封装了所有文件操作,增加了安全检查:
typescript
export async function safeReadFile(rootDir: string, relativePath: string): Promise<string> {
const absolutePath = path.resolve(rootDir, relativePath);
// 1. 路径遍历检查
if (!isPathInside(rootDir, absolutePath)) {
throw new PathTraversalError(absolutePath);
}
// 2. 符号链接检查(防止通过 symlink 逃逸)
const realPath = await fs.realpath(absolutePath);
if (!isPathInside(rootDir, realPath)) {
throw new SymlinkEscapeError(absolutePath, realPath);
}
// 3. 实际读取
return await fs.readFile(realPath, "utf8");
}2.3 硬链接守卫(hardlink-guards.ts)
除了符号链接,硬链接也是一种攻击向量。硬链接不改变路径,但文件内容可能指向敏感文件:
typescript
export async function assertNotHardlink(filePath: string): Promise<void> {
const stat = await fs.stat(filePath);
// 硬链接数 > 1 说明文件有额外的硬链接
if (stat.nlink > 1) {
throw new HardlinkDetectedError(filePath, stat.nlink);
}
}2.4 安全同步打开(safe-open-sync.ts)
某些场景需要同步打开文件(如配置文件加载),但必须防止 TOCTOU(Time-Of-Check-To-Time-Of-Use)竞态:
typescript
export function safeOpenSync(filePath: string, flags: string): number {
// O_NOFOLLOW 标志:不跟随符号链接
// 如果目标是符号链接,直接失败
const fd = fsSync.openSync(filePath, flags | fsSync.constants.O_NOFOLLOW);
// 打开后再验证 fstat(防止 TOCTOU)
const stat = fsSync.fstatSync(fd);
if (!stat.isFile()) {
fsSync.closeSync(fd);
throw new Error("Not a regular file");
}
return fd;
}2.5 边界路径(boundary-path.ts / boundary-file-read.ts)
"边界"概念用于限制 AI Agent 的文件访问范围:
typescript
export function isBoundaryAllowed(
boundaries: string[],
targetPath: string
): boolean {
// 目标路径必须在至少一个边界目录内
return boundaries.some(boundary => isPathInside(boundary, targetPath));
}Agent 的文件操作(读、写、执行)都受边界约束,防止 AI 访问用户不允许的目录。
基础设施层/02-infographic-file-security-layers-1775150727032.png)
三、网络安全
3.1 SSRF 防护(net/ssrf.ts)
SSRF(Server-Side Request Forgery)是 AI 系统最危险的攻击向量之一。如果 AI 可以访问 URL,攻击者可能诱导 AI 访问内网服务。
两阶段防护策略:
Phase 1: 预 DNS 检查(快速拒绝)
│
├── 被封锁的主机名?
│ localhost, *.localhost, *.local, *.internal
│ metadata.google.internal (GCP 元数据服务)
│
├── 直接 IP 地址?
│ 检查 RFC 1918 / RFC 4193 / RFC 5737 等私有/特殊地址
│ 包括 IPv4-mapped IPv6 (::ffff:10.0.0.1)
│ 包括旧式 IPv4 字面量 (0x7f000001)
│
└── 主机名白名单检查(hostnameAllowlist)
Phase 2: 后 DNS 检查(防 DNS 重绑定)
│
├── DNS 解析所有返回的 IP 地址
│
├── 对每个地址重新运行 Phase 1 的 IP 检查
│ → 防止 public-hostname → private-ip 的 DNS 重绑定攻击
│
└── 如果所有地址通过 → 创建 Pinned Lookup
→ 固定 DNS 结果,防止后续请求时 DNS 再次解析到不同 IP基础设施层/03-infographic-ssrf-two-phase-1775150727861.png)
3.2 DNS Pinning(防 DNS 重绑定)
DNS 重绑定攻击的原理:
- 攻击者的域名第一次解析到公网 IP(通过 SSRF 检查)
- TTL 过期后,同一域名解析到内网 IP(绕过检查)
OpenClaw 通过 DNS Pinning 防御:
typescript
export function createPinnedLookup(params: {
hostname: string;
addresses: string[]; // DNS 解析结果(已验证安全)
}): typeof dnsLookupCb {
// 返回一个自定义 lookup 函数
// 该函数总是返回预先解析好的地址
// 不会再次查询 DNS
}
export function createPinnedDispatcher(pinned: PinnedHostname): Dispatcher {
// 创建一个使用 pinned lookup 的 HTTP 客户端
// 所有请求都使用固定的 IP 地址
return new Agent({ connect: { lookup: pinned.lookup } });
}3.3 特殊 IP 地址检测
系统对各种 IP 地址格式都有防护:
| 攻击手法 | 示例 | 防护 |
|---|---|---|
| 标准私有 IP | 10.0.0.1, 192.168.1.1 | RFC 1918 检查 |
| IPv6 私有 | fd00::1 | RFC 4193 检查 |
| IPv4-mapped IPv6 | ::ffff:127.0.0.1 | 提取嵌入的 IPv4 再检查 |
| 旧式 IPv4 字面量 | 0x7f000001(= 127.0.0.1) | 检测非标准格式 |
| 十进制 IP | 2130706433(= 127.0.0.1) | 检测整数格式 |
| 混合记法 | 0177.0.0.1(八进制) | 检测非标准 IPv4 |
| GCP 元数据 | metadata.google.internal | 显式封锁 |
| AWS 元数据 | 169.254.169.254 | Link-local 检查 |
基础设施层/04-infographic-ip-detection-matrix-1775150728760.png)
3.4 安全 Fetch(net/fetch-guard.ts)
对外部 HTTP 请求的统一安全封装:
typescript
export async function safeFetch(url: string, options?: RequestInit) {
const parsed = new URL(url);
// 1. SSRF 检查 + DNS Pinning
const pinned = await resolvePinnedHostnameWithPolicy(parsed.hostname, { policy });
// 2. 创建固定 IP 的 HTTP 客户端
const dispatcher = createPinnedDispatcher(pinned);
// 3. 发起请求(使用固定的 DNS 结果)
const response = await fetch(url, { ...options, dispatcher });
// 4. 清理
await closeDispatcher(dispatcher);
return response;
}3.5 代理支持(net/proxy-fetch.ts)
在企业网络环境中,需要通过 HTTP 代理访问外部服务:
typescript
export function resolveProxyEnv(): { http?: string; https?: string; noProxy?: string } {
return {
http: process.env.HTTP_PROXY || process.env.http_proxy,
https: process.env.HTTPS_PROXY || process.env.https_proxy,
noProxy: process.env.NO_PROXY || process.env.no_proxy,
};
}当检测到代理配置时,SSRF 策略会自动调整:代理场景下浏览器可能绕过本地 DNS 解析,因此在 strict 模式下会拒绝请求。
四、日志系统
4.1 日志分级
typescript
// logging/levels.ts
const ALLOWED_LOG_LEVELS = [
"silent", // 完全静默
"fatal", // 致命错误
"error", // 错误
"warn", // 警告
"info", // 信息(默认)
"debug", // 调试
"trace", // 追踪(最详细)
];基于 tslog,按数值排序:fatal(0) → error(1) → warn(2) → info(3) → debug(4) → trace(5),silent(∞) 禁用所有输出。
4.2 子系统日志(SubsystemLogger)
每个模块创建自己的子系统日志器:
typescript
// logging/subsystem.ts
const log = createSubsystemLogger("memory/search");
log.info("Starting search"); // [memory/search] Starting search
log.debug("Query details", { q }); // 只在 debug 级别输出
log.error("Search failed", { err }); // 文件 + 控制台都输出每个日志器支持双输出通道:
| 通道 | 格式 | 控制 |
|---|---|---|
| 控制台 | 彩色前缀 + 时间 + 消息 | logging.console.level |
| 文件 | JSON 结构化日志 | logging.file.level |
基础设施层/05-infographic-logging-dual-channel-1775150729616.png)
4.3 子系统颜色分配
typescript
const SUBSYSTEM_COLORS = ["cyan", "green", "yellow", "blue", "magenta", "red"];
function pickSubsystemColor(subsystem: string): string {
// 基于子系统名称的哈希值分配颜色
let hash = 0;
for (let i = 0; i < subsystem.length; i++) {
hash = (hash * 31 + subsystem.charCodeAt(i)) | 0;
}
const idx = Math.abs(hash) % SUBSYSTEM_COLORS.length;
return SUBSYSTEM_COLORS[idx];
}效果:
[memory/search] Starting search ← cyan
[telegram] New message received ← green
[discord] Bot connected ← yellow
[gateway] Client connected ← blue4.4 子系统前缀智能简化
为了控制台美观,显示时会去除冗余前缀:
原始子系统名 → 显示名
gateway/channels/telegram → telegram
channels/discord → discord
gateway/browser → browser
agents/tools/cron → tools/cron (保留最后 2 段)规则:
- 去除
gateway、channels、providers前缀 - 频道名(telegram、discord 等)直接使用单个词
- 其他保留最后 2 段
4.5 敏感信息脱敏(redact.ts)
所有日志输出都经过敏感信息自动脱敏:
typescript
const DEFAULT_REDACT_PATTERNS = [
// 环境变量赋值
/\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)\b\s*[=:]\s*(.+)/,
// JSON 字段
/"(?:apiKey|token|secret|password)"\s*:\s*"([^"]+)"/,
// CLI 标志
/--(?:api-key|token|secret|password)\s+(.+)/,
// Authorization 头
/Authorization\s*:\s*Bearer\s+([A-Za-z0-9._\-+=]+)/,
// PEM 私钥块
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/,
// 常见 Token 前缀
/\b(sk-[A-Za-z0-9_-]{8,})\b/, // OpenAI
/\b(ghp_[A-Za-z0-9]{20,})\b/, // GitHub
/\b(github_pat_[A-Za-z0-9_]{20,})\b/, // GitHub PAT
/\b(xox[baprs]-[A-Za-z0-9-]{10,})\b/, // Slack
/\b(AIza[0-9A-Za-z\-_]{20,})\b/, // Google
/\b(npm_[A-Za-z0-9]{10,})\b/, // npm
/\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/, // Telegram Bot
];脱敏策略:
sk-proj-abcdefghijklmnop1234567890
→ sk-pro…7890 (保留前 6 位 + 后 4 位)
-----BEGIN RSA PRIVATE KEY-----
MIIBogIBAAJBALRi...
-----END RSA PRIVATE KEY-----
→ -----BEGIN RSA PRIVATE KEY-----
…redacted…
-----END RSA PRIVATE KEY-----安全正则表达式:所有脱敏 pattern 通过 compileSafeRegex() 编译,防止 ReDoS(正则表达式拒绝服务攻击)。
基础设施层/06-infographic-redaction-system-1775150730535.png)
4.6 Windows/CI 特殊处理
typescript
function writeConsoleLine(level: LogLevel, line: string) {
// Windows + GitHub Actions 环境下过滤非法 Unicode 代理对
// 防止日志输出破坏 CI 日志格式
const sanitized = process.platform === "win32" && process.env.GITHUB_ACTIONS === "true"
? line.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "?").replace(/[\uD800-\uDFFF]/g, "?")
: line;
}五、重试与退避
5.1 通用重试框架(retry.ts)
typescript
export async function retryAsync<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
// 1. 是否可重试?
if (!options.isRetryable?.(err)) {
throw err;
}
// 2. 还有重试次数?
if (attempt >= options.maxRetries) {
throw err;
}
// 3. 计算退避延迟
const delay = computeBackoff(attempt, options);
// 4. 等待
await sleep(delay);
// 5. 可选的 onRetry 回调
options.onRetry?.(err, attempt + 1);
}
}
throw lastError;
}5.2 指数退避(backoff.ts)
typescript
export function computeExponentialBackoff(params: {
attempt: number; // 当前重试次数
baseMs: number; // 基础延迟(默认 500ms)
maxMs: number; // 最大延迟(默认 30000ms)
jitterFactor: number; // 抖动系数(默认 0.2)
}): number {
// delay = min(baseMs × 2^attempt, maxMs)
const raw = Math.min(params.baseMs * 2 ** params.attempt, params.maxMs);
// 加入随机抖动:±jitterFactor
const jitter = raw * params.jitterFactor * (2 * Math.random() - 1);
return Math.max(0, Math.round(raw + jitter));
}抖动(Jitter) 的作用:防止多个客户端在同一时间重试("惊群效应")。
5.3 瞬态错误识别(retry-policy.ts)
typescript
export function isTransientError(err: unknown): boolean {
const message = formatErrorMessage(err);
const code = extractErrorCode(err);
// HTTP 429 Too Many Requests
if (/429|rate.?limit|too many requests/i.test(message)) return true;
// HTTP 5xx Server Error
if (/\b5\d{2}\b/.test(message)) return true;
// 网络错误
if (code === "ECONNRESET" || code === "ECONNREFUSED" || code === "ETIMEDOUT") return true;
// Cloudflare 防护
if (/cloudflare/i.test(message)) return true;
return false;
}基础设施层/07-infographic-retry-backoff-1775150731266.png)
六、进程管理
6.1 Gateway 单实例锁(gateway-lock.ts)
Gateway 是整个系统的核心,必须保证只有一个实例在运行:
acquireGatewayLock()
│
├── 计算锁文件路径
│ ~/.openclaw/state/gateway.{sha256(configPath)[:8]}.lock
│ (不同配置文件 → 不同锁,支持多配置共存)
│
├── 尝试创建锁文件(O_EXCL 排他模式)
│ │
│ ├── 成功 → 写入 { pid, createdAt, configPath, startTime }
│ │ 返回 GatewayLockHandle(包含 release 方法)
│ │
│ └── 失败(EEXIST)→ 锁已存在
│ │
│ ├── 读取锁文件中的 PID
│ │
│ ├── 检查锁持有者是否存活
│ │ ├── 端口探测:连接 Gateway 端口
│ │ │ └── 端口空闲 → 持有者已死
│ │ │
│ │ ├── PID 存活检查:kill(pid, 0)
│ │ │ └── 进程不存在 → 持有者已死
│ │ │
│ │ └── Linux 额外验证:
│ │ ├── /proc/{pid}/stat → startTime 比对
│ │ │ (防止 PID 复用:新进程与旧锁同 PID 但 startTime 不同)
│ │ │
│ │ └── /proc/{pid}/cmdline → 进程参数检查
│ │ (确认进程确实是 Gateway 而非其他程序)
│ │
│ ├── 持有者已死 → 删除锁文件 → 重试
│ ├── 持有者状态未知 + 锁文件过期(>30s)→ 删除锁文件 → 重试
│ └── 持有者存活 → 等待 100ms → 继续轮询(最多 5s)
│
└── 超时 → 抛出 GatewayLockErrorPID 复用防护(Linux 特有):Linux 的 PID 是循环分配的,一个旧进程的 PID 可能被新进程复用。通过比对 /proc/{pid}/stat 中的 startTime 字段(第 22 个字段),可以区分"同 PID 的不同进程"。
基础设施层/08-infographic-gateway-lock-1775150732159.png)
6.2 优雅重启(restart.ts)
typescript
export function scheduleGatewaySigusr1Restart() {
// 发送 SIGUSR1 信号给自身
// Gateway 捕获信号后执行优雅关闭 → 重新启动
process.kill(process.pid, "SIGUSR1");
}6.3 未处理异常兜底(unhandled-rejections.ts)
typescript
export function installUnhandledRejectionHandler() {
process.on("unhandledRejection", (reason) => {
// 1. 格式化错误(含脱敏)
const message = formatUncaughtError(reason);
// 2. 写入日志
log.error(`Unhandled rejection: ${message}`);
// 3. 不立即退出,让正常的错误处理流程有机会运行
// 但记录到诊断事件中
});
}七、错误处理
7.1 错误格式化
所有错误在格式化时都自动脱敏:
typescript
export function formatErrorMessage(err: unknown): string {
let formatted: string;
if (err instanceof Error) {
formatted = err.message || err.name || "Error";
} else {
formatted = JSON.stringify(err);
}
// 脱敏:防止 API Key 等通过错误消息泄露
return redactSensitiveText(formatted);
}7.2 错误图遍历
复杂错误可能有嵌套的 cause 链。collectErrorGraphCandidates 使用 BFS 遍历整个错误图:
typescript
export function collectErrorGraphCandidates(err: unknown): unknown[] {
// BFS 遍历错误链:err → err.cause → err.cause.cause → ...
// 也遍历 errors 数组(AggregateError)
// 返回所有错误节点,用于后续分类/匹配
}7.3 errno 分类
typescript
export function isErrno(err: unknown): err is NodeJS.ErrnoException {
return Boolean(err && typeof err === "object" && "code" in err);
}
export function hasErrnoCode(err: unknown, code: string): boolean {
return isErrno(err) && err.code === code;
}
// 使用示例:
if (hasErrnoCode(err, "ENOENT")) { /* 文件不存在 */ }
if (hasErrnoCode(err, "EACCES")) { /* 权限不足 */ }
if (hasErrnoCode(err, "EBUSY")) { /* 资源忙 */ }基础设施层/09-infographic-error-handling-1775150733017.png)
八、命令执行安全
8.1 执行审批系统(exec-approvals.ts)
AI Agent 执行系统命令前,必须经过审批:
| 策略 | 说明 |
|---|---|
ask | 每次都询问用户确认 |
allow-always | 允许所有命令(信任模式) |
safe-bin-only | 只允许白名单中的可执行文件 |
deny-always | 禁止所有命令执行 |
8.2 安全二进制信任链(exec-safe-bin-trust.ts)
safe-bin-only 模式下,系统维护一个可信二进制列表:
可信二进制验证链:
│
├── 文件名在白名单中? (ls, cat, git, node, ...)
│
├── 文件路径在安全目录中?
│ /usr/bin/, /usr/local/bin/, /opt/homebrew/bin/
│
├── 文件所有者是 root 或当前用户?
│
├── 文件权限:非 world-writable?
│
└── 文件大小合理?(非空)8.3 命令白名单模式(exec-allowlist-pattern.ts)
支持 glob 模式的命令白名单:
json5
{
"exec": {
"allowlist": [
"git *", // 允许所有 git 命令
"npm run *", // 允许所有 npm scripts
"python scripts/*" // 允许 scripts/ 下的 Python 脚本
]
}
}基础设施层/10-infographic-exec-security-1775150733786.png)
九、心跳系统
9.1 心跳概念
心跳(Heartbeat)是主会话的定期唤醒机制。即使用户没有发消息,AI 也可以通过心跳处理:
- 定时任务到期的提醒
- 系统事件(如频道状态变更)
- 挂起的通知
9.2 心跳运行器(heartbeat-runner.ts)
heartbeat-runner.ts
│
├── 定期触发(configurable interval)
│
├── 收集待处理事件
│ ├── 系统事件队列
│ ├── Cron 任务到期事件
│ └── 频道状态变更事件
│
├── 过滤事件(heartbeat-events-filter.ts)
│ ├── 过滤重复事件
│ ├── 过滤已过期事件
│ └── 按优先级排序
│
├── 构建心跳消息
│ ├── 事件摘要
│ ├── 可用操作
│ └── 上下文信息
│
└── 触发 Agent Turn
├── 如果有待处理事件 → 执行
└── 如果无事件 → 跳过(节省 API 调用)9.3 心跳可见性(heartbeat-visibility.ts)
控制心跳在不同频道的可见性:
typescript
function resolveHeartbeatVisibility(session: SessionContext): HeartbeatVisibility {
// main session → 可见(在最后活跃的频道)
// group session → 不可见(组不需要心跳)
// cron session → 内部可见
}基础设施层/11-infographic-heartbeat-system-1775150734648.png)
十、设备配对与身份
10.1 设备身份(device-identity.ts)
每个 OpenClaw 实例有唯一的设备 ID:
typescript
export function resolveDeviceId(): string {
// 1. 检查已存在的 device-id 文件
// 2. 如果不存在,生成新的 UUID
// 3. 持久化到 ~/.openclaw/device-id
}10.2 配对令牌(pairing-token.ts)
用于 DM 安全配对的一次性令牌:
typescript
export function generatePairingToken(): string {
// 生成 6 位数字码(000000-999999)
// 有效期内可用于验证身份
}10.3 设备认证存储(device-auth-store.ts)
存储已认证设备的信息:
- 设备 ID
- 设备名称
- 最后活跃时间
- 权限级别
十一、服务发现
11.1 mDNS/Bonjour 发现(bonjour.ts / bonjour-discovery.ts)
在局域网中自动发现其他 OpenClaw 实例:
OpenClaw Gateway A OpenClaw Gateway B
│ │
├── 广播 mDNS 服务 ├── 广播 mDNS 服务
│ _openclaw._tcp, port=18789 │ _openclaw._tcp, port=18789
│ │
└── 发现 B ← mDNS 查询 → └── 发现 A基于 ciao 库(macOS 原生 mDNS 绑定)实现。
十二、隧道与远程访问
12.1 SSH 隧道(ssh-tunnel.ts)
建立 SSH 反向隧道,让远程设备访问本地 Gateway:
本地 Gateway (localhost:18789)
│
├── ssh -R remotePort:127.0.0.1:18789 user@remote-host
│
└── 远程设备通过 remotePort 访问 Gateway12.2 Tailscale 集成(tailscale.ts)
检测 Tailscale VPN 状态并自动配置:
typescript
export async function getTailscaleStatus(): Promise<TailscaleStatus | null> {
// 1. 执行 tailscale status --json
// 2. 解析返回的设备列表
// 3. 获取当前设备的 Tailscale IP
}通过 Tailscale Serve/Funnel 可以安全地将 Gateway 暴露到公网。
十三、更新系统
13.1 版本检查(update-check.ts)
typescript
export async function checkForUpdates(): Promise<UpdateInfo | null> {
// 1. 查询 npm registry 获取最新版本
// 2. 比较 current vs latest
// 3. 如果有更新 → 返回 UpdateInfo
// 4. 缓存检查结果(避免频繁查询)
}13.2 更新运行器(update-runner.ts)
检查更新 → 有新版本?
│
├── 否 → 返回
│
└── 是 → 下载并安装
├── npm install -g openclaw@latest
├── 或 Homebrew: brew upgrade openclaw
└── 安装后触发优雅重启基础设施层/12-infographic-connectivity-layer-1775150735384.png)
十四、与其他模块的交互
┌─── 所有模块 ─────────────────────────────────────────────────────┐
│ │
│ Agent、Gateway、Channel、Memory、Browser、Cron、Plugins │
│ │
│ ├── 文件操作 ──→ fs-safe.ts、path-guards.ts │
│ ├── HTTP 请求 ──→ fetch-guard.ts、ssrf.ts │
│ ├── 日志输出 ──→ subsystem.ts、redact.ts │
│ ├── 错误处理 ──→ errors.ts、retry.ts │
│ ├── 命令执行 ──→ exec-safety.ts、exec-approvals.ts │
│ └── 进程管理 ──→ gateway-lock.ts、restart.ts │
└────────────────────────────────────────────────────────────────────┘基础设施层/13-infographic-module-interactions-1775150736147.png)
十五、关键设计决策与权衡
15.1 防御深度(Defense in Depth)
安全措施不是单层的,而是多层叠加:
| 层次 | 文件安全 | 网络安全 | 命令安全 |
|---|---|---|---|
| 第 1 层 | 路径遍历检查 | 主机名封锁 | 策略检查 |
| 第 2 层 | 符号链接检查 | DNS 预检查 | 白名单匹配 |
| 第 3 层 | 硬链接检查 | DNS 后检查 | 二进制信任链 |
| 第 4 层 | TOCTOU 防护 | DNS Pinning | 执行审批 |
| 第 5 层 | 边界约束 | Pinned Dispatcher | 结果脱敏 |
15.2 Fail Closed(安全失败原则)
所有安全检查在不确定时选择拒绝:
typescript
// SSRF 检查中
if (!parseCanonicalIpAddress(normalized)) {
// 无法解析的 IPv6 字面量 → 视为危险 → 拒绝
if (normalized.includes(":")) return true; // blocked
}
// Gateway 锁中
if (ownerStatus === "unknown") {
// 无法确定锁持有者是否存活 → 等待(不强制接管)
await sleep(pollIntervalMs);
}15.3 脱敏无处不在
几乎所有输出路径都经过脱敏:
- 日志消息 →
redactSensitiveText() - 错误消息 →
formatErrorMessage()内置脱敏 - 工具输出 →
redactToolDetail() - 会话记录 →
redactSensitiveText()
这确保了即使开发者在日志中打印了含有 API Key 的变量,脱敏机制也会自动替换。
基础设施层/14-infographic-design-principles-1775150737000.png)
15.4 为什么使用自定义日志而非 pino/winston?
OpenClaw 的日志系统基于 tslog,但加了大量自定义逻辑:
- 子系统着色:按哈希分配颜色,视觉区分模块
- 智能前缀简化:去除冗余层级
- 双通道输出:控制台(人类友好)+ 文件(机器友好)
- 深度脱敏:15 种 pattern,覆盖主流 API Token 格式
- 进度条兼容:输出前清除活跃的进度条(
clearActiveProgressLine)
十六、总结
OpenClaw 的基础设施层是一个安全优先、可靠至上的系统底座,有以下亮点:
- 五层文件安全防护:路径遍历 → 符号链接 → 硬链接 → TOCTOU → 边界约束
- 两阶段 SSRF 防护:预 DNS 检查 + 后 DNS 检查 + DNS Pinning,覆盖十余种 IP 混淆手法
- 全链路脱敏:15 种敏感信息 pattern,错误/日志/工具输出全覆盖
- PID 复用安全的进程锁:Linux startTime 比对,防止 PID 回收导致的误判
- 结构化子系统日志:彩色前缀 + 智能简化 + 双通道输出 + 进度条兼容
- 指数退避重试:抖动防惊群 + 瞬态错误分类 + 通用重试框架
- 分层命令执行安全:策略 → 白名单 → 二进制信任链 → 审批
- 局域网服务发现:mDNS/Bonjour 自动发现其他实例
- Fail Closed 安全原则:不确定时一律拒绝,宁可误报不可漏放
整个设计体现了"个人助手运行在你自己的机器上"这一理念对安全的极端重视——即使是本地运行,也要防范所有已知的攻击向量。