fix(daemon): harden workspace memory detail extraction

This commit is contained in:
叶公 2026-07-07 19:51:05 +08:00
parent 6ba0ba7994
commit d1cbf11abf
2 changed files with 40 additions and 10 deletions

View file

@ -46,6 +46,12 @@ describe('extractRememberErrorDetails', () => {
expect(extractRememberErrorDetails({ data: 'raw data detail' })).toBe(
'raw data detail',
);
expect(
extractRememberErrorDetails({
data: 'ERR_BRIDGE_INTERNAL',
message: 'Connection to memory service refused',
}),
).toBe('Connection to memory service refused');
expect(
extractRememberErrorDetails({
data: { message: 'provider rejected the request' },
@ -56,6 +62,9 @@ describe('extractRememberErrorDetails', () => {
cause: new Error('nested failure reason'),
}),
).toBe('nested failure reason');
expect(extractRememberErrorDetails({ cause: 'string cause reason' })).toBe(
'string cause reason',
);
expect(extractRememberErrorDetails('raw string error')).toBe(
'raw string error',
);
@ -71,12 +80,14 @@ describe('extractRememberErrorDetails', () => {
});
it('normalizes hidden separators before redacting credentials', () => {
const details = extractRememberErrorDetails(
new Error('Authorization: Bearer\u200bsecret-token-value'),
);
for (const separator of ['\u200b', '\u2060', '\u2064']) {
const details = extractRememberErrorDetails(
new Error(`Authorization: Bearer${separator}secret-token-value`),
);
expect(details).toBe('Authorization: <redacted>');
expect(details).not.toContain('secret-token-value');
expect(details).toBe('Authorization: <redacted>');
expect(details).not.toContain('secret-token-value');
}
});
it('normalizes line separators before redacting credentials', () => {
@ -103,6 +114,19 @@ describe('extractRememberErrorDetails', () => {
expect(extractRememberErrorDetails(cyclic)).toBeUndefined();
});
it('limits cause traversal depth', () => {
const root: Record<string, unknown> = {};
let current = root;
for (let index = 0; index < 60; index += 1) {
const next: Record<string, unknown> = {};
current['cause'] = next;
current = next;
}
current['message'] = 'too deep';
expect(extractRememberErrorDetails(root)).toBeUndefined();
});
it('caps long details', () => {
const details = extractRememberErrorDetails(new Error('x'.repeat(1100)));

View file

@ -7,6 +7,7 @@
import { redactLogCredentials } from '@qwen-code/acp-bridge/logRedaction';
const MAX_REMEMBER_ERROR_DETAILS_CHARS = 1000;
const MAX_REMEMBER_ERROR_CAUSE_DEPTH = 50;
function errorCodeFromRecord(
record: Record<string, unknown>,
@ -43,11 +44,11 @@ export function extractRememberErrorCode(
function detailFromRecord(
record: Record<string, unknown>,
seen: WeakSet<object>,
depth: number,
): string | undefined {
// Bridge errors carry the best failure reason in `data`; top-level
// `message` and `cause` are generic fallbacks.
const data = record['data'];
if (typeof data === 'string' && data.length > 0) return data;
if (data && typeof data === 'object') {
const dataRecord = data as Record<string, unknown>;
const details = dataRecord['details'];
@ -59,9 +60,11 @@ function detailFromRecord(
const message = record['message'];
if (typeof message === 'string' && message.length > 0) return message;
if (typeof data === 'string' && data.length > 0) return data;
const cause = record['cause'];
if (cause && typeof cause === 'object') {
return rawRememberErrorDetails(cause, seen);
if (cause != null) {
return rawRememberErrorDetails(cause, seen, depth + 1);
}
return undefined;
@ -70,12 +73,14 @@ function detailFromRecord(
function rawRememberErrorDetails(
err: unknown,
seen: WeakSet<object>,
depth: number,
): string | undefined {
if (depth > MAX_REMEMBER_ERROR_CAUSE_DEPTH) return undefined;
if (typeof err === 'string' && err.length > 0) return err;
if (!err || typeof err !== 'object') return undefined;
if (seen.has(err)) return undefined;
seen.add(err);
return detailFromRecord(err as Record<string, unknown>, seen);
return detailFromRecord(err as Record<string, unknown>, seen, depth);
}
function shouldReplaceControlChar(code: number): boolean {
@ -84,6 +89,7 @@ function shouldReplaceControlChar(code: number): boolean {
(code >= 127 && code <= 159) ||
(code >= 0x200b && code <= 0x200f) ||
(code >= 0x2028 && code <= 0x202e) ||
(code >= 0x2060 && code <= 0x2064) ||
(code >= 0x2066 && code <= 0x2069) ||
code === 0xfeff
);
@ -119,7 +125,7 @@ function sanitizeRememberErrorDetails(details: string): string | undefined {
}
export function extractRememberErrorDetails(err: unknown): string | undefined {
const raw = rawRememberErrorDetails(err, new WeakSet<object>());
const raw = rawRememberErrorDetails(err, new WeakSet<object>(), 0);
if (!raw) return undefined;
return sanitizeRememberErrorDetails(raw);
}