diff --git a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift index 6c8f012c0cf..365c4e75698 100644 --- a/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/macos/Sources/OpenClawProtocol/GatewayModels.swift @@ -394,6 +394,7 @@ public struct SendParams: Codable, Sendable { public let gifplayback: Bool? public let channel: String? public let accountid: String? + public let threadid: String? public let sessionkey: String? public let idempotencykey: String @@ -405,6 +406,7 @@ public struct SendParams: Codable, Sendable { gifplayback: Bool?, channel: String?, accountid: String?, + threadid: String?, sessionkey: String?, idempotencykey: String ) { @@ -415,6 +417,7 @@ public struct SendParams: Codable, Sendable { self.gifplayback = gifplayback self.channel = channel self.accountid = accountid + self.threadid = threadid self.sessionkey = sessionkey self.idempotencykey = idempotencykey } @@ -426,6 +429,7 @@ public struct SendParams: Codable, Sendable { case gifplayback = "gifPlayback" case channel case accountid = "accountId" + case threadid = "threadId" case sessionkey = "sessionKey" case idempotencykey = "idempotencyKey" } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index 6c8f012c0cf..365c4e75698 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -394,6 +394,7 @@ public struct SendParams: Codable, Sendable { public let gifplayback: Bool? public let channel: String? public let accountid: String? + public let threadid: String? public let sessionkey: String? public let idempotencykey: String @@ -405,6 +406,7 @@ public struct SendParams: Codable, Sendable { gifplayback: Bool?, channel: String?, accountid: String?, + threadid: String?, sessionkey: String?, idempotencykey: String ) { @@ -415,6 +417,7 @@ public struct SendParams: Codable, Sendable { self.gifplayback = gifplayback self.channel = channel self.accountid = accountid + self.threadid = threadid self.sessionkey = sessionkey self.idempotencykey = idempotencykey } @@ -426,6 +429,7 @@ public struct SendParams: Codable, Sendable { case gifplayback = "gifPlayback" case channel case accountid = "accountId" + case threadid = "threadId" case sessionkey = "sessionKey" case idempotencykey = "idempotencyKey" } diff --git a/src/channels/telegram/allow-from.test.ts b/src/channels/telegram/allow-from.test.ts new file mode 100644 index 00000000000..eb60e9481e6 --- /dev/null +++ b/src/channels/telegram/allow-from.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { isNumericTelegramUserId, normalizeTelegramAllowFromEntry } from "./allow-from.js"; + +describe("telegram allow-from helpers", () => { + it("normalizes tg/telegram prefixes", () => { + expect(normalizeTelegramAllowFromEntry(" TG:123 ")).toBe("123"); + expect(normalizeTelegramAllowFromEntry("telegram:@someone")).toBe("@someone"); + }); + + it("accepts signed numeric IDs", () => { + expect(isNumericTelegramUserId("123456789")).toBe(true); + expect(isNumericTelegramUserId("-1001234567890")).toBe(true); + expect(isNumericTelegramUserId("@someone")).toBe(false); + expect(isNumericTelegramUserId("12 34")).toBe(false); + }); +}); diff --git a/src/channels/telegram/allow-from.ts b/src/channels/telegram/allow-from.ts index d1e0209bc23..deb41b05fc6 100644 --- a/src/channels/telegram/allow-from.ts +++ b/src/channels/telegram/allow-from.ts @@ -7,5 +7,5 @@ export function normalizeTelegramAllowFromEntry(raw: unknown): string { } export function isNumericTelegramUserId(raw: string): boolean { - return /^\d+$/.test(raw); + return /^-?\d+$/.test(raw); } diff --git a/src/commands/doctor-state-integrity.ts b/src/commands/doctor-state-integrity.ts index 7b6086e2659..f896d7fbb80 100644 --- a/src/commands/doctor-state-integrity.ts +++ b/src/commands/doctor-state-integrity.ts @@ -223,8 +223,10 @@ export async function noteStateIntegrity( if (configPath && existsFile(configPath) && process.platform !== "win32") { try { + const linkStat = fs.lstatSync(configPath); const stat = fs.statSync(configPath); - if ((stat.mode & 0o077) !== 0) { + const isSymlink = linkStat.isSymbolicLink(); + if (!isSymlink && (stat.mode & 0o077) !== 0) { warnings.push( `- Config file is group/world readable (${displayConfigPath ?? configPath}). Recommend chmod 600.`, ); diff --git a/src/security/audit-fs.ts b/src/security/audit-fs.ts index 7d6d6130f6e..34b329de9d7 100644 --- a/src/security/audit-fs.ts +++ b/src/security/audit-fs.ts @@ -80,7 +80,19 @@ export async function inspectPathPermissions( }; } - const bits = modeBits(st.mode); + let effectiveMode = st.mode; + let effectiveIsDir = st.isDir; + if (st.isSymlink) { + try { + const target = await fs.stat(targetPath); + effectiveMode = typeof target.mode === "number" ? target.mode : st.mode; + effectiveIsDir = target.isDirectory(); + } catch { + // Keep lstat-derived metadata when target lookup fails. + } + } + + const bits = modeBits(effectiveMode); const platform = opts?.platform ?? process.platform; if (platform === "win32") { @@ -89,8 +101,8 @@ export async function inspectPathPermissions( return { ok: true, isSymlink: st.isSymlink, - isDir: st.isDir, - mode: st.mode, + isDir: effectiveIsDir, + mode: effectiveMode, bits, source: "unknown", worldWritable: false, @@ -103,8 +115,8 @@ export async function inspectPathPermissions( return { ok: true, isSymlink: st.isSymlink, - isDir: st.isDir, - mode: st.mode, + isDir: effectiveIsDir, + mode: effectiveMode, bits, source: "windows-acl", worldWritable: acl.untrustedWorld.some((entry) => entry.canWrite), @@ -118,8 +130,8 @@ export async function inspectPathPermissions( return { ok: true, isSymlink: st.isSymlink, - isDir: st.isDir, - mode: st.mode, + isDir: effectiveIsDir, + mode: effectiveMode, bits, source: "posix", worldWritable: isWorldWritable(bits), diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index 27b5f425871..3da11d825ca 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -386,6 +386,38 @@ describe("security audit", () => { ).toBe(true); }); + it("uses symlink target permissions for config checks", async () => { + if (isWindows) { + return; + } + + const tmp = await makeTmpDir("config-symlink"); + const stateDir = path.join(tmp, "state"); + await fs.mkdir(stateDir, { recursive: true, mode: 0o700 }); + + const targetConfigPath = path.join(tmp, "managed-openclaw.json"); + await fs.writeFile(targetConfigPath, "{}\n", "utf-8"); + await fs.chmod(targetConfigPath, 0o444); + + const configPath = path.join(stateDir, "openclaw.json"); + await fs.symlink(targetConfigPath, configPath); + + const res = await runSecurityAudit({ + config: {}, + includeFilesystem: true, + includeChannelSecurity: false, + stateDir, + configPath, + }); + + expect(res.findings).toEqual( + expect.arrayContaining([expect.objectContaining({ checkId: "fs.config.symlink" })]), + ); + expect(res.findings.some((f) => f.checkId === "fs.config.perms_writable")).toBe(false); + expect(res.findings.some((f) => f.checkId === "fs.config.perms_world_readable")).toBe(false); + expect(res.findings.some((f) => f.checkId === "fs.config.perms_group_readable")).toBe(false); + }); + it("warns when small models are paired with web/browser tools", async () => { const cfg: OpenClawConfig = { agents: { defaults: { model: { primary: "ollama/mistral-8b" } } }, diff --git a/src/security/audit.ts b/src/security/audit.ts index 7059b0d008b..57ffa1aab94 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -186,6 +186,7 @@ async function collectFilesystemFindings(params: { exec: params.execIcacls, }); if (configPerms.ok) { + const skipReadablePermWarnings = configPerms.isSymlink; if (configPerms.isSymlink) { findings.push({ checkId: "fs.config.symlink", @@ -208,7 +209,7 @@ async function collectFilesystemFindings(params: { env: params.env, }), }); - } else if (configPerms.worldReadable) { + } else if (!skipReadablePermWarnings && configPerms.worldReadable) { findings.push({ checkId: "fs.config.perms_world_readable", severity: "critical", @@ -222,7 +223,7 @@ async function collectFilesystemFindings(params: { env: params.env, }), }); - } else if (configPerms.groupReadable) { + } else if (!skipReadablePermWarnings && configPerms.groupReadable) { findings.push({ checkId: "fs.config.perms_group_readable", severity: "warn",