Skip to content

Commit 90fd26b

Browse files
authored
fix(infra): restore symlink rejection in tryReadSecretFileSync (#84711)
* fix(infra): restore symlink rejection in tryReadSecretFileSync The local wrapper added in 9e4eca0 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)
1 parent 3844513 commit 90fd26b

8 files changed

Lines changed: 26 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818

1919
- WhatsApp: update Baileys to `7.0.0-rc12`.
2020
- 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.
21+
- 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.
2122
- 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)
2223
- Doctor: remove unrecognized `models.providers.*.models[*].compat.thinkingFormat` values during `doctor --fix` so stale provider model config can validate after upgrade. Fixes #77803.
2324
- 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`.

extensions/irc/src/accounts.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,7 @@ describe("resolveIrcAccount", () => {
163163
},
164164
});
165165

166-
const account = resolveIrcAccount({ cfg });
167-
expect(account.password).toBe("");
168-
expect(account.passwordSource).toBe("none");
166+
expect(() => resolveIrcAccount({ cfg })).toThrow(/IRC password file.*must not be a symlink/);
169167
fs.rmSync(dir, { recursive: true, force: true });
170168
});
171169

extensions/line/src/accounts.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,9 @@ describe("LINE accounts", () => {
196196
},
197197
};
198198

199-
const account = resolveLineAccount({ cfg });
200-
expect(account.channelAccessToken).toBe("");
201-
expect(account.channelSecret).toBe("");
202-
expect(account.tokenSource).toBe("none");
199+
expect(() => resolveLineAccount({ cfg })).toThrow(
200+
/LINE credential file.*must not be a symlink/,
201+
);
203202
});
204203
});
205204

extensions/nextcloud-talk/src/setup.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,9 @@ describe("resolveNextcloudTalkAccount", () => {
387387
},
388388
} as CoreConfig;
389389

390-
const account = resolveNextcloudTalkAccount({ cfg });
391-
expect(account.secret).toBe("");
392-
expect(account.secretSource).toBe("none");
390+
expect(() => resolveNextcloudTalkAccount({ cfg })).toThrow(
391+
/Nextcloud Talk bot secret file.*must not be a symlink/,
392+
);
393393
fs.rmSync(dir, { recursive: true, force: true });
394394
});
395395

extensions/telegram/src/account-inspect.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,18 @@ function inspectTokenFile(pathValue: unknown): {
3838
if (!tokenFile) {
3939
return null;
4040
}
41-
const token = tryReadSecretFileSync(tokenFile, "Telegram bot token", {
42-
rejectSymlink: true,
43-
});
41+
let token: string | undefined;
42+
try {
43+
token = tryReadSecretFileSync(tokenFile, "Telegram bot token", {
44+
rejectSymlink: true,
45+
});
46+
} catch {
47+
return {
48+
token: "",
49+
tokenSource: "tokenFile",
50+
tokenStatus: "configured_unavailable",
51+
};
52+
}
4453
return {
4554
token: token ?? "",
4655
tokenSource: "tokenFile",

extensions/telegram/src/token.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ describe("resolveTelegramToken", () => {
101101
fs.symlinkSync(tokenFile, tokenLink);
102102

103103
const cfg = { channels: { telegram: { tokenFile: tokenLink } } } as OpenClawConfig;
104-
const res = resolveTelegramToken(cfg);
105-
expect(res.token).toBe("");
106-
expect(res.source).toBe("none");
104+
expect(() => resolveTelegramToken(cfg)).toThrow(
105+
/channels\.telegram\.tokenFile.*must not be a symlink/,
106+
);
107107
});
108108

109109
it("does not fall back to config when tokenFile is missing", () => {

extensions/zalo/src/token.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ describe("resolveZaloToken", () => {
8484
const cfg = {
8585
tokenFile: tokenLink,
8686
} as ZaloConfig;
87-
const res = resolveZaloToken(cfg);
88-
expect(res.token).toBe("");
89-
expect(res.source).toBe("none");
87+
expect(() => resolveZaloToken(cfg)).toThrow(/Zalo token file.*must not be a symlink/);
9088
fs.rmSync(dir, { recursive: true, force: true });
9189
});
9290
});

src/infra/secret-file.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,17 @@
11
import "./fs-safe-defaults.js";
2-
import {
3-
readSecretFileSync as readSecretFileSyncImpl,
4-
tryReadSecretFileSync as tryReadSecretFileSyncImpl,
5-
} from "@openclaw/fs-safe/secret";
2+
import { readSecretFileSync as readSecretFileSyncImpl } from "@openclaw/fs-safe/secret";
63
import { resolveUserPath } from "../utils.js";
74

85
export {
96
DEFAULT_SECRET_FILE_MAX_BYTES,
107
PRIVATE_SECRET_DIR_MODE,
118
PRIVATE_SECRET_FILE_MODE,
129
readSecretFileSync,
10+
tryReadSecretFileSync,
1311
type SecretFileReadOptions,
1412
} from "@openclaw/fs-safe/secret";
1513
export { writeSecretFileAtomic as writePrivateSecretFileAtomic } from "@openclaw/fs-safe/secret";
1614

17-
export function tryReadSecretFileSync(
18-
filePath: string | undefined,
19-
label: string,
20-
options: Parameters<typeof tryReadSecretFileSyncImpl>[2] = {},
21-
): string | undefined {
22-
try {
23-
return tryReadSecretFileSyncImpl(filePath, label, options);
24-
} catch {
25-
return undefined;
26-
}
27-
}
28-
2915
export type SecretFileReadResult =
3016
| {
3117
ok: true;

0 commit comments

Comments
 (0)