fix(tui): preserve credential-like tokens in render sanitization

This commit is contained in:
Vignesh Natarajan
2026-03-05 21:05:52 -08:00
parent 5d4b04040d
commit 8d4a2f2c59
3 changed files with 30 additions and 1 deletions

View File

@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- TUI/token copy-safety rendering: treat long credential-like mixed alphanumeric tokens (including quoted forms) as copy-sensitive in render sanitization so formatter hard-wrap guards no longer inject visible spaces into auth-style values before display. (#26710) Thanks @jasonthane.
- WhatsApp/self-chat response prefix fallback: stop forcing `"[openclaw]"` as the implicit outbound response prefix when no identity name or response prefix is configured, so blank/default prefix settings no longer inject branding text unexpectedly in self-chat flows. (#27962) Thanks @ecanmor.
- Memory/QMD search result decoding: accept `qmd search` hits that only include `file` URIs (for example `qmd://collection/path.md`) without `docid`, resolve them through managed collection roots, and keep multi-collection results keyed by file fallback so valid QMD hits no longer collapse to empty `memory_search` output. (#28181) Thanks @0x76696265.
- Memory/QMD collection-name conflict recovery: when `qmd collection add` fails because another collection already occupies the same `path + pattern`, detect the conflicting collection from `collection list`, remove it, and retry add so agent-scoped managed collections are created deterministically instead of being silently skipped; also add warning-only fallback when qmd metadata is unavailable to avoid destructive guesses. (#25496) Thanks @Ramsbaby.

View File

@@ -249,6 +249,20 @@ describe("sanitizeRenderableText", () => {
expect(sanitized).toBe(input);
});
it("preserves long credential-like mixed alnum tokens for copy safety", () => {
const input = "e3b19c3b87bcf364b23eebb2c276e96ec478956ba1d84c93";
const sanitized = sanitizeRenderableText(input);
expect(sanitized).toBe(input);
});
it("preserves quoted credential-like mixed alnum tokens for copy safety", () => {
const input = "'e3b19c3b87bcf364b23eebb2c276e96ec478956ba1d84c93'";
const sanitized = sanitizeRenderableText(input);
expect(sanitized).toBe(input);
});
it("wraps rtl lines with directional isolation marks", () => {
const input = "مرحبا بالعالم";
const sanitized = sanitizeRenderableText(input);

View File

@@ -11,6 +11,8 @@ const BINARY_LINE_REPLACEMENT_THRESHOLD = 12;
const URL_PREFIX_RE = /^(https?:\/\/|file:\/\/)/i;
const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
const FILE_LIKE_RE = /^[a-zA-Z0-9._-]+$/;
const EDGE_PUNCTUATION_RE = /^[`"'([{<]+|[`"')\]}>.,:;!?]+$/g;
const TOKENISH_MIN_LENGTH = 24;
const RTL_SCRIPT_RE = /[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/;
const BIDI_CONTROL_RE = /[\u202a-\u202e\u2066-\u2069]/;
const RTL_ISOLATE_START = "\u2067";
@@ -56,6 +58,9 @@ function chunkToken(token: string, maxChars: number): string[] {
}
function isCopySensitiveToken(token: string): boolean {
const coreToken = token.replace(EDGE_PUNCTUATION_RE, "");
const candidate = coreToken || token;
if (URL_PREFIX_RE.test(token)) {
return true;
}
@@ -73,7 +78,16 @@ function isCopySensitiveToken(token: string): boolean {
if (token.includes("/") || token.includes("\\")) {
return true;
}
return token.includes("_") && FILE_LIKE_RE.test(token);
if (token.includes("_") && FILE_LIKE_RE.test(token)) {
return true;
}
// Preserve long credential-like tokens (hex/base62/etc.) to avoid introducing
// visible spaces that users may copy back into secrets.
if (candidate.length >= TOKENISH_MIN_LENGTH && /[a-z]/i.test(candidate) && /\d/.test(candidate)) {
return true;
}
return false;
}
function normalizeLongTokenForDisplay(token: string): string {