fix: address PR #1405 review feedback — security, voice loop, locale coverage, test fixes

- Point 4 (security): _resolve_workspace now validates against known workspaces
  from workspaces.json to prevent arbitrary path write via restore endpoint
- Point 5 (voice mode): bail out of voice mode on not-allowed, service-not-allowed,
  and audio-capture errors instead of infinite retry loop
- Point 1 (locale coverage): added ~40 new English keys as placeholders with
  TODO:translate comments in zh, zh-Hant, ko, ru, es, de, pt locales
- Point 2 (test fix): tightened test regex to anchor on branch-indicator class
  to avoid collision with _sessionLineageKey helper
- Point 3 (test fix): accept both inline and parentEl variable forms for
  body.appendChild pattern in pinned indicator test

All 6 previously failing tests now pass.
This commit is contained in:
bergeouss
2026-05-01 15:54:27 +00:00
parent ae40af03d7
commit d9f3a69d29
5 changed files with 295 additions and 5 deletions

View File

@@ -46,13 +46,29 @@ def _checkpoint_root() -> Path:
def _resolve_workspace(workspace: str) -> str:
"""Validate and return the canonical workspace path."""
"""Validate and return the canonical workspace path.
Security: workspace must match a known configured workspace
(from workspaces.json or session-attached workspaces).
"""
if not workspace or not isinstance(workspace, str):
raise ValueError("workspace is required")
# Basic path validation
resolved = os.path.realpath(workspace)
if not os.path.isdir(resolved):
raise ValueError(f"Workspace does not exist: {workspace}")
# Security: confirm workspace is in the known list
try:
from api.workspace import load_workspaces
known_paths = set()
for ws in load_workspaces():
p = ws.get("path", "")
if p:
known_paths.add(os.path.realpath(p))
if resolved not in known_paths:
raise ValueError(f"Workspace not in configured list: {workspace}")
except ImportError:
logger.warning("Could not load workspace list for rollback validation")
return resolved

View File

@@ -502,7 +502,7 @@ window._micPendingSend=window._micPendingSend||false;
}
return;
}
if(event.error==='not-allowed'){
if(event.error==='not-allowed'||event.error==='service-not-allowed'||event.error==='audio-capture'){
_deactivate();
showToast(t('mic_denied'));
return;

View File

@@ -1611,6 +1611,39 @@ const LOCALES = {
settings_label_tts_rate: 'Скорость речи',
settings_label_tts_pitch: 'Тон речи',
checkpoint_date: 'Date', // TODO: translate
checkpoint_diff_files_changed: (n) => `${n} file${n === 1 ? '' : 's'} changed`, // TODO: translate
checkpoint_diff_no_changes: 'No differences found between this checkpoint and the current workspace.', // TODO: translate
checkpoint_diff_title: 'Changes in checkpoint', // TODO: translate
checkpoint_empty: 'No checkpoints found for this workspace.', // TODO: translate
checkpoint_error: 'Failed to load checkpoints', // TODO: translate
checkpoint_files: 'Files', // TODO: translate
checkpoint_loading: 'Loading checkpoints…', // TODO: translate
checkpoint_message: 'Message', // TODO: translate
checkpoint_restore: 'Restore', // TODO: translate
checkpoint_restore_confirm_message: (ckpt) => `Restore workspace to checkpoint "${ckpt}"? This will overwrite files with the saved versions. Files added after this checkpoint will not be deleted.`, // TODO: translate
checkpoint_restore_confirm_title: 'Restore checkpoint?', // TODO: translate
checkpoint_restored: 'Checkpoint restored', // TODO: translate
checkpoint_title: 'Checkpoints', // TODO: translate
checkpoint_view_diff: 'View diff', // TODO: translate
insights_activity_by_day: 'Activity by Day', // TODO: translate
insights_activity_by_hour: 'Activity by Hour', // TODO: translate
insights_cost: 'Estimated Cost', // TODO: translate
insights_footer: 'Showing data from the last {days} days', // TODO: translate
insights_input_tokens: 'Input', // TODO: translate
insights_messages: 'Messages', // TODO: translate
insights_models: 'Models', // TODO: translate
insights_no_cost: 'N/A', // TODO: translate
insights_output_tokens: 'Output', // TODO: translate
insights_peak_hour: 'Peak: {hour}', // TODO: translate
insights_sessions: 'Sessions', // TODO: translate
insights_title: 'Usage Analytics', // TODO: translate
insights_token_breakdown: 'Token Breakdown', // TODO: translate
insights_tokens: 'Tokens', // TODO: translate
insights_total: 'Total', // TODO: translate
settings_desc_api_redact: 'Self-hosted users can disable for transparency (not recommended for shared instances).', // TODO: translate
settings_label_api_redact: 'Redact sensitive data in API responses', // TODO: translate
subagent_children: 'Subagent sessions', // TODO: translate
},
es: {
@@ -2356,6 +2389,46 @@ const LOCALES = {
settings_desc_tts_voice: 'Seleccionar voz para síntesis de voz',
settings_label_tts_rate: 'Velocidad de voz',
settings_label_tts_pitch: 'Tono de voz',
checkpoint_date: 'Date', // TODO: translate
checkpoint_diff_files_changed: (n) => `${n} file${n === 1 ? '' : 's'} changed`, // TODO: translate
checkpoint_diff_no_changes: 'No differences found between this checkpoint and the current workspace.', // TODO: translate
checkpoint_diff_title: 'Changes in checkpoint', // TODO: translate
checkpoint_empty: 'No checkpoints found for this workspace.', // TODO: translate
checkpoint_error: 'Failed to load checkpoints', // TODO: translate
checkpoint_files: 'Files', // TODO: translate
checkpoint_loading: 'Loading checkpoints…', // TODO: translate
checkpoint_message: 'Message', // TODO: translate
checkpoint_restore: 'Restore', // TODO: translate
checkpoint_restore_confirm_message: (ckpt) => `Restore workspace to checkpoint "${ckpt}"? This will overwrite files with the saved versions. Files added after this checkpoint will not be deleted.`, // TODO: translate
checkpoint_restore_confirm_title: 'Restore checkpoint?', // TODO: translate
checkpoint_restored: 'Checkpoint restored', // TODO: translate
checkpoint_title: 'Checkpoints', // TODO: translate
checkpoint_view_diff: 'View diff', // TODO: translate
insights_activity_by_day: 'Activity by Day', // TODO: translate
insights_activity_by_hour: 'Activity by Hour', // TODO: translate
insights_cost: 'Estimated Cost', // TODO: translate
insights_footer: 'Showing data from the last {days} days', // TODO: translate
insights_input_tokens: 'Input', // TODO: translate
insights_messages: 'Messages', // TODO: translate
insights_models: 'Models', // TODO: translate
insights_no_cost: 'N/A', // TODO: translate
insights_output_tokens: 'Output', // TODO: translate
insights_peak_hour: 'Peak: {hour}', // TODO: translate
insights_sessions: 'Sessions', // TODO: translate
insights_title: 'Usage Analytics', // TODO: translate
insights_token_breakdown: 'Token Breakdown', // TODO: translate
insights_tokens: 'Tokens', // TODO: translate
insights_total: 'Total', // TODO: translate
settings_desc_api_redact: 'Self-hosted users can disable for transparency (not recommended for shared instances).', // TODO: translate
settings_label_api_redact: 'Redact sensitive data in API responses', // TODO: translate
voice_error: 'Voice not supported in this browser', // TODO: translate
voice_listening: 'Listening…', // TODO: translate
voice_mode_active: 'Voice mode on', // TODO: translate
voice_mode_off: 'Voice mode off', // TODO: translate
voice_speaking: 'Speaking…', // TODO: translate
voice_thinking: 'Thinking…', // TODO: translate
voice_toggle: 'Voice input', // TODO: translate
subagent_children: 'Subagent sessions', // TODO: translate
},
de: {
@@ -3105,6 +3178,46 @@ const LOCALES = {
settings_label_tts_rate: 'Sprechgeschwindigkeit',
settings_label_tts_pitch: 'Tonhöhe',
checkpoint_date: 'Date', // TODO: translate
checkpoint_diff_files_changed: (n) => `${n} file${n === 1 ? '' : 's'} changed`, // TODO: translate
checkpoint_diff_no_changes: 'No differences found between this checkpoint and the current workspace.', // TODO: translate
checkpoint_diff_title: 'Changes in checkpoint', // TODO: translate
checkpoint_empty: 'No checkpoints found for this workspace.', // TODO: translate
checkpoint_error: 'Failed to load checkpoints', // TODO: translate
checkpoint_files: 'Files', // TODO: translate
checkpoint_loading: 'Loading checkpoints…', // TODO: translate
checkpoint_message: 'Message', // TODO: translate
checkpoint_restore: 'Restore', // TODO: translate
checkpoint_restore_confirm_message: (ckpt) => `Restore workspace to checkpoint "${ckpt}"? This will overwrite files with the saved versions. Files added after this checkpoint will not be deleted.`, // TODO: translate
checkpoint_restore_confirm_title: 'Restore checkpoint?', // TODO: translate
checkpoint_restored: 'Checkpoint restored', // TODO: translate
checkpoint_title: 'Checkpoints', // TODO: translate
checkpoint_view_diff: 'View diff', // TODO: translate
insights_activity_by_day: 'Activity by Day', // TODO: translate
insights_activity_by_hour: 'Activity by Hour', // TODO: translate
insights_cost: 'Estimated Cost', // TODO: translate
insights_footer: 'Showing data from the last {days} days', // TODO: translate
insights_input_tokens: 'Input', // TODO: translate
insights_messages: 'Messages', // TODO: translate
insights_models: 'Models', // TODO: translate
insights_no_cost: 'N/A', // TODO: translate
insights_output_tokens: 'Output', // TODO: translate
insights_peak_hour: 'Peak: {hour}', // TODO: translate
insights_sessions: 'Sessions', // TODO: translate
insights_title: 'Usage Analytics', // TODO: translate
insights_token_breakdown: 'Token Breakdown', // TODO: translate
insights_tokens: 'Tokens', // TODO: translate
insights_total: 'Total', // TODO: translate
settings_desc_api_redact: 'Self-hosted users can disable for transparency (not recommended for shared instances).', // TODO: translate
settings_label_api_redact: 'Redact sensitive data in API responses', // TODO: translate
voice_error: 'Voice not supported in this browser', // TODO: translate
voice_listening: 'Listening…', // TODO: translate
voice_mode_active: 'Voice mode on', // TODO: translate
voice_mode_off: 'Voice mode off', // TODO: translate
voice_speaking: 'Speaking…', // TODO: translate
voice_thinking: 'Thinking…', // TODO: translate
voice_toggle: 'Voice input', // TODO: translate
subagent_children: 'Subagent sessions', // TODO: translate
},
zh: {
@@ -3848,6 +3961,46 @@ const LOCALES = {
settings_desc_tts_voice: '选择语音合成声音',
settings_label_tts_rate: '语速',
settings_label_tts_pitch: '音调',
checkpoint_date: 'Date', // TODO: translate
checkpoint_diff_files_changed: (n) => `${n} file${n === 1 ? '' : 's'} changed`, // TODO: translate
checkpoint_diff_no_changes: 'No differences found between this checkpoint and the current workspace.', // TODO: translate
checkpoint_diff_title: 'Changes in checkpoint', // TODO: translate
checkpoint_empty: 'No checkpoints found for this workspace.', // TODO: translate
checkpoint_error: 'Failed to load checkpoints', // TODO: translate
checkpoint_files: 'Files', // TODO: translate
checkpoint_loading: 'Loading checkpoints…', // TODO: translate
checkpoint_message: 'Message', // TODO: translate
checkpoint_restore: 'Restore', // TODO: translate
checkpoint_restore_confirm_message: (ckpt) => `Restore workspace to checkpoint "${ckpt}"? This will overwrite files with the saved versions. Files added after this checkpoint will not be deleted.`, // TODO: translate
checkpoint_restore_confirm_title: 'Restore checkpoint?', // TODO: translate
checkpoint_restored: 'Checkpoint restored', // TODO: translate
checkpoint_title: 'Checkpoints', // TODO: translate
checkpoint_view_diff: 'View diff', // TODO: translate
insights_activity_by_day: 'Activity by Day', // TODO: translate
insights_activity_by_hour: 'Activity by Hour', // TODO: translate
insights_cost: 'Estimated Cost', // TODO: translate
insights_footer: 'Showing data from the last {days} days', // TODO: translate
insights_input_tokens: 'Input', // TODO: translate
insights_messages: 'Messages', // TODO: translate
insights_models: 'Models', // TODO: translate
insights_no_cost: 'N/A', // TODO: translate
insights_output_tokens: 'Output', // TODO: translate
insights_peak_hour: 'Peak: {hour}', // TODO: translate
insights_sessions: 'Sessions', // TODO: translate
insights_title: 'Usage Analytics', // TODO: translate
insights_token_breakdown: 'Token Breakdown', // TODO: translate
insights_tokens: 'Tokens', // TODO: translate
insights_total: 'Total', // TODO: translate
settings_desc_api_redact: 'Self-hosted users can disable for transparency (not recommended for shared instances).', // TODO: translate
settings_label_api_redact: 'Redact sensitive data in API responses', // TODO: translate
voice_error: 'Voice not supported in this browser', // TODO: translate
voice_listening: 'Listening…', // TODO: translate
voice_mode_active: 'Voice mode on', // TODO: translate
voice_mode_off: 'Voice mode off', // TODO: translate
voice_speaking: 'Speaking…', // TODO: translate
voice_thinking: 'Thinking…', // TODO: translate
voice_toggle: 'Voice input', // TODO: translate
subagent_children: 'Subagent sessions', // TODO: translate
},
// Traditional Chinese (zh-Hant)
@@ -4699,6 +4852,46 @@ const LOCALES = {
settings_label_tts_rate: '語速',
settings_label_tts_pitch: '音調',
checkpoint_date: 'Date', // TODO: translate
checkpoint_diff_files_changed: (n) => `${n} file${n === 1 ? '' : 's'} changed`, // TODO: translate
checkpoint_diff_no_changes: 'No differences found between this checkpoint and the current workspace.', // TODO: translate
checkpoint_diff_title: 'Changes in checkpoint', // TODO: translate
checkpoint_empty: 'No checkpoints found for this workspace.', // TODO: translate
checkpoint_error: 'Failed to load checkpoints', // TODO: translate
checkpoint_files: 'Files', // TODO: translate
checkpoint_loading: 'Loading checkpoints…', // TODO: translate
checkpoint_message: 'Message', // TODO: translate
checkpoint_restore: 'Restore', // TODO: translate
checkpoint_restore_confirm_message: (ckpt) => `Restore workspace to checkpoint "${ckpt}"? This will overwrite files with the saved versions. Files added after this checkpoint will not be deleted.`, // TODO: translate
checkpoint_restore_confirm_title: 'Restore checkpoint?', // TODO: translate
checkpoint_restored: 'Checkpoint restored', // TODO: translate
checkpoint_title: 'Checkpoints', // TODO: translate
checkpoint_view_diff: 'View diff', // TODO: translate
insights_activity_by_day: 'Activity by Day', // TODO: translate
insights_activity_by_hour: 'Activity by Hour', // TODO: translate
insights_cost: 'Estimated Cost', // TODO: translate
insights_footer: 'Showing data from the last {days} days', // TODO: translate
insights_input_tokens: 'Input', // TODO: translate
insights_messages: 'Messages', // TODO: translate
insights_models: 'Models', // TODO: translate
insights_no_cost: 'N/A', // TODO: translate
insights_output_tokens: 'Output', // TODO: translate
insights_peak_hour: 'Peak: {hour}', // TODO: translate
insights_sessions: 'Sessions', // TODO: translate
insights_title: 'Usage Analytics', // TODO: translate
insights_token_breakdown: 'Token Breakdown', // TODO: translate
insights_tokens: 'Tokens', // TODO: translate
insights_total: 'Total', // TODO: translate
settings_desc_api_redact: 'Self-hosted users can disable for transparency (not recommended for shared instances).', // TODO: translate
settings_label_api_redact: 'Redact sensitive data in API responses', // TODO: translate
voice_error: 'Voice not supported in this browser', // TODO: translate
voice_listening: 'Listening…', // TODO: translate
voice_mode_active: 'Voice mode on', // TODO: translate
voice_mode_off: 'Voice mode off', // TODO: translate
voice_speaking: 'Speaking…', // TODO: translate
voice_thinking: 'Thinking…', // TODO: translate
voice_toggle: 'Voice input', // TODO: translate
subagent_children: 'Subagent sessions', // TODO: translate
},
pt: {
@@ -5362,6 +5555,46 @@ const LOCALES = {
settings_desc_tts_voice: 'Selecionar voz para síntese de voz',
settings_label_tts_rate: 'Velocidade da fala',
settings_label_tts_pitch: 'Tom da fala',
checkpoint_date: 'Date', // TODO: translate
checkpoint_diff_files_changed: (n) => `${n} file${n === 1 ? '' : 's'} changed`, // TODO: translate
checkpoint_diff_no_changes: 'No differences found between this checkpoint and the current workspace.', // TODO: translate
checkpoint_diff_title: 'Changes in checkpoint', // TODO: translate
checkpoint_empty: 'No checkpoints found for this workspace.', // TODO: translate
checkpoint_error: 'Failed to load checkpoints', // TODO: translate
checkpoint_files: 'Files', // TODO: translate
checkpoint_loading: 'Loading checkpoints…', // TODO: translate
checkpoint_message: 'Message', // TODO: translate
checkpoint_restore: 'Restore', // TODO: translate
checkpoint_restore_confirm_message: (ckpt) => `Restore workspace to checkpoint "${ckpt}"? This will overwrite files with the saved versions. Files added after this checkpoint will not be deleted.`, // TODO: translate
checkpoint_restore_confirm_title: 'Restore checkpoint?', // TODO: translate
checkpoint_restored: 'Checkpoint restored', // TODO: translate
checkpoint_title: 'Checkpoints', // TODO: translate
checkpoint_view_diff: 'View diff', // TODO: translate
insights_activity_by_day: 'Activity by Day', // TODO: translate
insights_activity_by_hour: 'Activity by Hour', // TODO: translate
insights_cost: 'Estimated Cost', // TODO: translate
insights_footer: 'Showing data from the last {days} days', // TODO: translate
insights_input_tokens: 'Input', // TODO: translate
insights_messages: 'Messages', // TODO: translate
insights_models: 'Models', // TODO: translate
insights_no_cost: 'N/A', // TODO: translate
insights_output_tokens: 'Output', // TODO: translate
insights_peak_hour: 'Peak: {hour}', // TODO: translate
insights_sessions: 'Sessions', // TODO: translate
insights_title: 'Usage Analytics', // TODO: translate
insights_token_breakdown: 'Token Breakdown', // TODO: translate
insights_tokens: 'Tokens', // TODO: translate
insights_total: 'Total', // TODO: translate
settings_desc_api_redact: 'Self-hosted users can disable for transparency (not recommended for shared instances).', // TODO: translate
settings_label_api_redact: 'Redact sensitive data in API responses', // TODO: translate
voice_error: 'Voice not supported in this browser', // TODO: translate
voice_listening: 'Listening…', // TODO: translate
voice_mode_active: 'Voice mode on', // TODO: translate
voice_mode_off: 'Voice mode off', // TODO: translate
voice_speaking: 'Speaking…', // TODO: translate
voice_thinking: 'Thinking…', // TODO: translate
voice_toggle: 'Voice input', // TODO: translate
subagent_children: 'Subagent sessions', // TODO: translate
},
ko: {
_lang: 'ko',
@@ -6159,6 +6392,46 @@ const LOCALES = {
settings_desc_tts_voice: '음성 합성 음성 선택',
settings_label_tts_rate: '말 속도',
settings_label_tts_pitch: '말 톤',
checkpoint_date: 'Date', // TODO: translate
checkpoint_diff_files_changed: (n) => `${n} file${n === 1 ? '' : 's'} changed`, // TODO: translate
checkpoint_diff_no_changes: 'No differences found between this checkpoint and the current workspace.', // TODO: translate
checkpoint_diff_title: 'Changes in checkpoint', // TODO: translate
checkpoint_empty: 'No checkpoints found for this workspace.', // TODO: translate
checkpoint_error: 'Failed to load checkpoints', // TODO: translate
checkpoint_files: 'Files', // TODO: translate
checkpoint_loading: 'Loading checkpoints…', // TODO: translate
checkpoint_message: 'Message', // TODO: translate
checkpoint_restore: 'Restore', // TODO: translate
checkpoint_restore_confirm_message: (ckpt) => `Restore workspace to checkpoint "${ckpt}"? This will overwrite files with the saved versions. Files added after this checkpoint will not be deleted.`, // TODO: translate
checkpoint_restore_confirm_title: 'Restore checkpoint?', // TODO: translate
checkpoint_restored: 'Checkpoint restored', // TODO: translate
checkpoint_title: 'Checkpoints', // TODO: translate
checkpoint_view_diff: 'View diff', // TODO: translate
insights_activity_by_day: 'Activity by Day', // TODO: translate
insights_activity_by_hour: 'Activity by Hour', // TODO: translate
insights_cost: 'Estimated Cost', // TODO: translate
insights_footer: 'Showing data from the last {days} days', // TODO: translate
insights_input_tokens: 'Input', // TODO: translate
insights_messages: 'Messages', // TODO: translate
insights_models: 'Models', // TODO: translate
insights_no_cost: 'N/A', // TODO: translate
insights_output_tokens: 'Output', // TODO: translate
insights_peak_hour: 'Peak: {hour}', // TODO: translate
insights_sessions: 'Sessions', // TODO: translate
insights_title: 'Usage Analytics', // TODO: translate
insights_token_breakdown: 'Token Breakdown', // TODO: translate
insights_tokens: 'Tokens', // TODO: translate
insights_total: 'Total', // TODO: translate
settings_desc_api_redact: 'Self-hosted users can disable for transparency (not recommended for shared instances).', // TODO: translate
settings_label_api_redact: 'Redact sensitive data in API responses', // TODO: translate
voice_error: 'Voice not supported in this browser', // TODO: translate
voice_listening: 'Listening…', // TODO: translate
voice_mode_active: 'Voice mode on', // TODO: translate
voice_mode_off: 'Voice mode off', // TODO: translate
voice_speaking: 'Speaking…', // TODO: translate
voice_thinking: 'Thinking…', // TODO: translate
voice_toggle: 'Voice input', // TODO: translate
subagent_children: 'Subagent sessions', // TODO: translate
},
};

View File

@@ -235,8 +235,8 @@ def test_parent_indicator_clickable():
src = f.read()
# Find the parent indicator block
parent_block = re.search(
r'parent_session_id\).*?(?=titleRow\.appendChild)',
src, re.DOTALL
r'branch-indicator[\s\S]*?parent_session_id[\s\S]*?titleRow\.appendChild',
src
)
assert parent_block, "Could not find parent indicator block"
block = parent_block.group(0)

View File

@@ -11,7 +11,8 @@ def test_pinned_indicator_renders_inside_title_row():
title_row_idx = SESSIONS_JS.find("titleRow.className='session-title-row';")
assert title_row_idx != -1, "session title row construction not found"
assert "body.appendChild(_renderOneSession(s, Boolean(g.isPinned)))" in SESSIONS_JS
assert ("body.appendChild(_renderOneSession(s, Boolean(g.isPinned)))" in SESSIONS_JS
or "body.appendChild(parentEl)" in SESSIONS_JS)
assert "function _renderOneSession(s, isPinnedGroup=false)" in SESSIONS_JS
assert "if(s.pinned&&!isPinnedGroup){" in SESSIONS_JS