fix(infra): restore symlink rejection in tryReadSecretFileSync (#84711)

* fix(infra): restore symlink rejection in tryReadSecretFileSync

The local wrapper added in 9e4eca00ff swallowed all errors from
@openclaw/fs-safe@0.2.7's tryReadSecretFileSync via a bare try/catch,
silently downgrading every rejectSymlink: true caller (Telegram, LINE,
Zalo, IRC, Nextcloud Talk credential files) to accept symlinked
credential files. It also broke the infra-state CI shard's symlink
expectation that #84595 had just realigned with the new fail-closed
upstream contract.

Restore the direct re-export so the upstream contract surfaces:
undefined for blank/missing/not-found, FsSafeError for symlink,
oversize, non-regular file, and hardlink validation failures.

* test(plugins): align stale symlink tests with fail-closed contract

5 token/account resolver tests still asserted the pre-fs-safe-0.2.7
"silent skip" behavior (token: "", source: "none") on rejected symlinks;
they passed only because the swallow-all wrapper in secret-file.ts hid
the throw. Restoring the upstream fail-closed contract surfaces the
throw, so update the tests to expect FsSafeError.

inspectTelegramAccount reports credential status (its return type has an
explicit configured_unavailable state for "configured but unreadable"),
so its callsite is the right boundary to catch the FsSafeError and map
it to configured_unavailable rather than letting the throw bubble.

Affected:
- extensions/zalo/src/token.test.ts
- extensions/line/src/accounts.test.ts
- extensions/telegram/src/token.test.ts
- extensions/irc/src/accounts.test.ts
- extensions/nextcloud-talk/src/setup.test.ts
- extensions/telegram/src/account-inspect.ts (catch + report status)
This commit is contained in:
Dallin Romney
2026-05-20 15:21:13 -07:00
committed by GitHub
parent 3844513431
commit 90fd26b602
8 changed files with 26 additions and 35 deletions

View File

@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
- WhatsApp: update Baileys to `7.0.0-rc12`.
- Dependencies: update `@openclaw/fs-safe` to `0.2.7` so OpenClaw's default Python-helper-off policy keeps best-effort Node write fallbacks for private stores, secret writes, run logs, and media attachments on Linux/macOS.
- Infra/secrets: restore the fail-closed contract for `tryReadSecretFileSync` so credential loaders that pass `rejectSymlink: true` (Telegram, LINE, Zalo, IRC, Nextcloud Talk tokens) refuse symlinked credential files instead of silently accepting them, and the infra-state CI shard's secret-file symlink test passes again. Thanks @romneyda.
- Browser: honor the configured image sanitization limit for screenshots and labeled snapshots so browser-captured images follow the same resize policy as other image results. (#84595)
- Doctor: remove unrecognized `models.providers.*.models[*].compat.thinkingFormat` values during `doctor --fix` so stale provider model config can validate after upgrade. Fixes #77803.
- Status: show the configured default, session-selected model, reason, clear hint, and docs link when a session remains pinned to a model that differs from `agents.defaults.model.primary`.

View File

@@ -163,9 +163,7 @@ describe("resolveIrcAccount", () => {
},
});
const account = resolveIrcAccount({ cfg });
expect(account.password).toBe("");
expect(account.passwordSource).toBe("none");
expect(() => resolveIrcAccount({ cfg })).toThrow(/IRC password file.*must not be a symlink/);
fs.rmSync(dir, { recursive: true, force: true });
});

View File

@@ -196,10 +196,9 @@ describe("LINE accounts", () => {
},
};
const account = resolveLineAccount({ cfg });
expect(account.channelAccessToken).toBe("");
expect(account.channelSecret).toBe("");
expect(account.tokenSource).toBe("none");
expect(() => resolveLineAccount({ cfg })).toThrow(
/LINE credential file.*must not be a symlink/,
);
});
});

View File

@@ -387,9 +387,9 @@ describe("resolveNextcloudTalkAccount", () => {
},
} as CoreConfig;
const account = resolveNextcloudTalkAccount({ cfg });
expect(account.secret).toBe("");
expect(account.secretSource).toBe("none");
expect(() => resolveNextcloudTalkAccount({ cfg })).toThrow(
/Nextcloud Talk bot secret file.*must not be a symlink/,
);
fs.rmSync(dir, { recursive: true, force: true });
});

View File

@@ -38,9 +38,18 @@ function inspectTokenFile(pathValue: unknown): {
if (!tokenFile) {
return null;
}
const token = tryReadSecretFileSync(tokenFile, "Telegram bot token", {
rejectSymlink: true,
});
let token: string | undefined;
try {
token = tryReadSecretFileSync(tokenFile, "Telegram bot token", {
rejectSymlink: true,
});
} catch {
return {
token: "",
tokenSource: "tokenFile",
tokenStatus: "configured_unavailable",
};
}
return {
token: token ?? "",
tokenSource: "tokenFile",

View File

@@ -101,9 +101,9 @@ describe("resolveTelegramToken", () => {
fs.symlinkSync(tokenFile, tokenLink);
const cfg = { channels: { telegram: { tokenFile: tokenLink } } } as OpenClawConfig;
const res = resolveTelegramToken(cfg);
expect(res.token).toBe("");
expect(res.source).toBe("none");
expect(() => resolveTelegramToken(cfg)).toThrow(
/channels\.telegram\.tokenFile.*must not be a symlink/,
);
});
it("does not fall back to config when tokenFile is missing", () => {

View File

@@ -84,9 +84,7 @@ describe("resolveZaloToken", () => {
const cfg = {
tokenFile: tokenLink,
} as ZaloConfig;
const res = resolveZaloToken(cfg);
expect(res.token).toBe("");
expect(res.source).toBe("none");
expect(() => resolveZaloToken(cfg)).toThrow(/Zalo token file.*must not be a symlink/);
fs.rmSync(dir, { recursive: true, force: true });
});
});

View File

@@ -1,8 +1,5 @@
import "./fs-safe-defaults.js";
import {
readSecretFileSync as readSecretFileSyncImpl,
tryReadSecretFileSync as tryReadSecretFileSyncImpl,
} from "@openclaw/fs-safe/secret";
import { readSecretFileSync as readSecretFileSyncImpl } from "@openclaw/fs-safe/secret";
import { resolveUserPath } from "../utils.js";
export {
@@ -10,22 +7,11 @@ export {
PRIVATE_SECRET_DIR_MODE,
PRIVATE_SECRET_FILE_MODE,
readSecretFileSync,
tryReadSecretFileSync,
type SecretFileReadOptions,
} from "@openclaw/fs-safe/secret";
export { writeSecretFileAtomic as writePrivateSecretFileAtomic } from "@openclaw/fs-safe/secret";
export function tryReadSecretFileSync(
filePath: string | undefined,
label: string,
options: Parameters<typeof tryReadSecretFileSyncImpl>[2] = {},
): string | undefined {
try {
return tryReadSecretFileSyncImpl(filePath, label, options);
} catch {
return undefined;
}
}
export type SecretFileReadResult =
| {
ok: true;