From 7f8d5bc9e913dc64efa3d99637401280efa87d1d Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Sun, 18 Oct 2026 22:30:16 -0600 Subject: [PATCH 1/2] fix(tui): remediate permission audit findings on delete scope, edit consent, MCP claims, and Spanish translations --- src/isycode/locales/en/LC_MESSAGES/isycode.po | 228 ++++++++++++++++++ src/isycode/locales/es/LC_MESSAGES/isycode.po | 228 ++++++++++++++++++ src/isycode/locales/zh/LC_MESSAGES/isycode.po | 228 ++++++++++++++++++ src/isycode/tui_app_authority.py | 30 +-- src/isycode/tui_app_tools.py | 12 +- src/isycode/tui_screens_approval.py | 48 ++-- tests/test_tui_permission_audit.py | 105 ++++++++ 7 files changed, 846 insertions(+), 33 deletions(-) create mode 100644 tests/test_tui_permission_audit.py diff --git a/src/isycode/locales/en/LC_MESSAGES/isycode.po b/src/isycode/locales/en/LC_MESSAGES/isycode.po index 4471926..448e1c8 100644 --- a/src/isycode/locales/en/LC_MESSAGES/isycode.po +++ b/src/isycode/locales/en/LC_MESSAGES/isycode.po @@ -2375,3 +2375,231 @@ msgstr "Show a preview box below the file tree in the sidebar." msgid "OpenAI & ChatGPT · choose connection method…" msgstr "OpenAI & ChatGPT · choose connection method…" + +msgid "Owner" +msgstr "Owner" + +msgid "Scope" +msgstr "Scope" + +msgid "Duration" +msgstr "Duration" + +msgid "Not granted" +msgstr "Not granted" + +msgid "workspace authority" +msgstr "workspace authority" + +msgid "workspace file edit" +msgstr "workspace file edit" + +msgid "local MCP server" +msgstr "local MCP server" + +msgid "local MCP tool" +msgstr "local MCP tool" + +msgid "tailscale private-access" +msgstr "tailscale private-access" + +msgid "sandboxed command run" +msgstr "sandboxed command run" + +msgid "git commit" +msgstr "git commit" + +msgid "browser page read" +msgstr "browser page read" + +msgid "workspace context pack" +msgstr "workspace context pack" + +msgid "workspace memory" +msgstr "workspace memory" + +msgid "session deletion" +msgstr "session deletion" + +msgid "session delete" +msgstr "session delete" + +msgid "chat session store" +msgstr "chat session store" + +msgid "workspace trust" +msgstr "workspace trust" + +msgid "lsp diagnostics" +msgstr "lsp diagnostics" + +msgid "git" +msgstr "git" + +msgid "sandboxed commands" +msgstr "sandboxed commands" + +msgid "external model review" +msgstr "external model review" + +msgid "provider network access" +msgstr "provider network access" + +msgid "Gateway MCP target" +msgstr "Gateway MCP target" + +msgid "local LSP process" +msgstr "local LSP process" + +msgid "broker preview" +msgstr "broker preview" + +msgid "Gateway MCP tool" +msgstr "Gateway MCP tool" + +msgid "Gateway semantic HTTP" +msgstr "Gateway semantic HTTP" + +msgid "local LSP search" +msgstr "local LSP search" + +msgid "semantic broker provision" +msgstr "semantic broker provision" + +msgid "broker operation" +msgstr "broker operation" + +msgid "the text shown below" +msgstr "the text shown below" + +msgid "listed workspace files" +msgstr "listed workspace files" + +msgid "{count} files" +msgstr "{count} files" + +msgid "one-use approval" +msgstr "one-use approval" + +msgid "one-use, one HTTP request" +msgstr "one-use, one HTTP request" + +msgid "one-use, digest-bound approval" +msgstr "one-use, digest-bound approval" + +msgid "one-use, stops after {timeout} s" +msgstr "one-use, stops after {timeout} s" + +msgid "persistent workspace grant" +msgstr "persistent workspace grant" + +msgid "persistent workspace setting" +msgstr "persistent workspace setting" + +msgid "persistent workspace trust" +msgstr "persistent workspace trust" + +msgid "one conversation grant" +msgstr "one conversation grant" + +msgid "until ISyCode exits" +msgstr "until ISyCode exits" + +msgid "one-use tool call" +msgstr "one-use tool call" + +msgid "persistent grant (until revoked)" +msgstr "persistent grant (until revoked)" + +msgid "persistent grant (every call still approved)" +msgstr "persistent grant (every call still approved)" + +msgid "one-use, grants revoked after attempt" +msgstr "one-use, grants revoked after attempt" + +msgid "one-use, grant revoked after operation" +msgstr "one-use, grant revoked after operation" + +msgid "no tools, no file access, no retries" +msgstr "no tools, no file access, no retries" + +msgid "no other host actions, no filesystem access" +msgstr "no other host actions, no filesystem access" + +msgid "no other files, no commands, no secrets" +msgstr "no other files, no commands, no secrets" + +msgid "no push, no hooks, no other files" +msgstr "no push, no hooks, no other files" + +msgid "no writes, no commands, no secrets" +msgstr "no writes, no commands, no secrets" + +msgid "no authority, no grants, no other memory" +msgstr "no authority, no grants, no other memory" + +msgid "no other sessions, no filesystem access" +msgstr "no other sessions, no filesystem access" + +msgid "other conversations, unconfirmed deletions" +msgstr "other conversations, unconfirmed deletions" + +msgid "sensitive files, .isyroot, and unapproved folder writes stay off-limits" +msgstr "sensitive files, .isyroot, and unapproved folder writes stay off-limits" + +msgid "privilege escalation, ungranted capabilities" +msgstr "privilege escalation, ungranted capabilities" + +msgid "commits, secrets, authority changes" +msgstr "commits, secrets, authority changes" + +msgid "secrets, credentials, project files" +msgstr "secrets, credentials, project files" + +msgid "file modifications, network access" +msgstr "file modifications, network access" + +msgid "remote push, git hooks" +msgstr "remote push, git hooks" + +msgid "network access, unsandboxed execution" +msgstr "network access, unsandboxed execution" + +msgid "network blocked, no other paths, no undo" +msgstr "network blocked, no other paths, no undo" + +msgid "no other MCP calls; runs with user permissions (no filesystem isolation)" +msgstr "no other MCP calls; runs with user permissions (no filesystem isolation)" + +msgid "no other MCP servers; runs with user permissions (no filesystem isolation)" +msgstr "no other MCP servers; runs with user permissions (no filesystem isolation)" + +msgid "no other tool calls, no automatic retries" +msgstr "no other tool calls, no automatic retries" + +msgid "no tool execution, no file access, no secrets" +msgstr "no tool execution, no file access, no secrets" + +msgid "no automatic tool runs, no local filesystem access" +msgstr "no automatic tool runs, no local filesystem access" + +msgid "no internet, no general commands, no writes" +msgstr "no internet, no general commands, no writes" + +msgid "no Docker, no containers, no workspace.files.read" +msgstr "no Docker, no containers, no workspace.files.read" + +msgid "no other MCP calls, no local filesystem access" +msgstr "no other MCP calls, no local filesystem access" + +msgid "no writes, no commands, no model context" +msgstr "no writes, no commands, no model context" + +msgid "no writes, no network, no Gateway" +msgstr "no writes, no network, no Gateway" + +msgid "no credentials, no host network, no other containers" +msgstr "no credentials, no host network, no other containers" + +msgid "no provision, no other containers, no host access" +msgstr "no provision, no other containers, no host access" diff --git a/src/isycode/locales/es/LC_MESSAGES/isycode.po b/src/isycode/locales/es/LC_MESSAGES/isycode.po index fa77d03..ef6cd91 100644 --- a/src/isycode/locales/es/LC_MESSAGES/isycode.po +++ b/src/isycode/locales/es/LC_MESSAGES/isycode.po @@ -2375,3 +2375,231 @@ msgstr "Mostrar un cuadro de vista previa debajo del árbol de archivos en la ba msgid "OpenAI & ChatGPT · choose connection method…" msgstr "OpenAI y ChatGPT · elegir método de conexión…" + +msgid "Owner" +msgstr "Propietario" + +msgid "Scope" +msgstr "Alcance" + +msgid "Duration" +msgstr "Duración" + +msgid "Not granted" +msgstr "Exclusiones" + +msgid "workspace authority" +msgstr "autoridad del espacio de trabajo" + +msgid "workspace file edit" +msgstr "edición de archivo del espacio de trabajo" + +msgid "local MCP server" +msgstr "servidor MCP local" + +msgid "local MCP tool" +msgstr "herramienta MCP local" + +msgid "tailscale private-access" +msgstr "acceso privado de Tailscale" + +msgid "sandboxed command run" +msgstr "ejecución de comando en sandbox" + +msgid "git commit" +msgstr "confirmación de git" + +msgid "browser page read" +msgstr "lectura de página del navegador" + +msgid "workspace context pack" +msgstr "paquete de contexto del espacio de trabajo" + +msgid "workspace memory" +msgstr "memoria del espacio de trabajo" + +msgid "session deletion" +msgstr "eliminación de conversación" + +msgid "session delete" +msgstr "eliminación de conversación" + +msgid "chat session store" +msgstr "almacén de sesiones de chat" + +msgid "workspace trust" +msgstr "confianza del espacio de trabajo" + +msgid "lsp diagnostics" +msgstr "diagnósticos LSP" + +msgid "git" +msgstr "git" + +msgid "sandboxed commands" +msgstr "comandos en sandbox" + +msgid "external model review" +msgstr "revisión de modelo externo" + +msgid "provider network access" +msgstr "acceso a red del proveedor" + +msgid "Gateway MCP target" +msgstr "destino MCP de Gateway" + +msgid "local LSP process" +msgstr "proceso LSP local" + +msgid "broker preview" +msgstr "vista previa del broker" + +msgid "Gateway MCP tool" +msgstr "herramienta MCP de Gateway" + +msgid "Gateway semantic HTTP" +msgstr "HTTP semántico de Gateway" + +msgid "local LSP search" +msgstr "búsqueda LSP local" + +msgid "semantic broker provision" +msgstr "aprovisionamiento de broker semántico" + +msgid "broker operation" +msgstr "operación del broker" + +msgid "the text shown below" +msgstr "el texto mostrado a continuación" + +msgid "listed workspace files" +msgstr "archivos del espacio de trabajo listados" + +msgid "{count} files" +msgstr "{count} archivos" + +msgid "one-use approval" +msgstr "aprobación de un solo uso" + +msgid "one-use, one HTTP request" +msgstr "un solo uso, una solicitud HTTP" + +msgid "one-use, digest-bound approval" +msgstr "aprobación de un solo uso ligada al resumen" + +msgid "one-use, stops after {timeout} s" +msgstr "un solo uso, se detiene tras {timeout} s" + +msgid "persistent workspace grant" +msgstr "permiso persistente del espacio de trabajo" + +msgid "persistent workspace setting" +msgstr "configuración persistente del espacio de trabajo" + +msgid "persistent workspace trust" +msgstr "confianza persistente del espacio de trabajo" + +msgid "one conversation grant" +msgstr "permiso para una conversación" + +msgid "until ISyCode exits" +msgstr "hasta que ISyCode se cierre" + +msgid "one-use tool call" +msgstr "llamada a herramienta de un solo uso" + +msgid "persistent grant (until revoked)" +msgstr "permiso persistente (hasta su revocación)" + +msgid "persistent grant (every call still approved)" +msgstr "permiso persistente (cada llamada requiere aprobación)" + +msgid "one-use, grants revoked after attempt" +msgstr "un solo uso, permisos revocados tras el intento" + +msgid "one-use, grant revoked after operation" +msgstr "un solo uso, permiso revocado tras la operación" + +msgid "no tools, no file access, no retries" +msgstr "sin herramientas, sin acceso a archivos, sin reintentos" + +msgid "no other host actions, no filesystem access" +msgstr "sin otras acciones de host, sin acceso al sistema de archivos" + +msgid "no other files, no commands, no secrets" +msgstr "sin otros archivos, sin comandos, sin secretos" + +msgid "no push, no hooks, no other files" +msgstr "sin push, sin hooks, sin otros archivos" + +msgid "no writes, no commands, no secrets" +msgstr "sin escrituras, sin comandos, sin secretos" + +msgid "no authority, no grants, no other memory" +msgstr "sin autoridad, sin permisos, sin otra memoria" + +msgid "no other sessions, no filesystem access" +msgstr "sin otras sesiones, sin acceso al sistema de archivos" + +msgid "other conversations, unconfirmed deletions" +msgstr "otras conversaciones, eliminaciones sin confirmar" + +msgid "sensitive files, .isyroot, and unapproved folder writes stay off-limits" +msgstr "archivos sensibles, .isyroot y carpetas no aprobadas quedan fuera de límites" + +msgid "privilege escalation, ungranted capabilities" +msgstr "escalada de privilegios, capacidades no otorgadas" + +msgid "commits, secrets, authority changes" +msgstr "commits, secretos, cambios de autoridad" + +msgid "secrets, credentials, project files" +msgstr "secretos, credenciales, archivos del proyecto" + +msgid "file modifications, network access" +msgstr "modificación de archivos, acceso a la red" + +msgid "remote push, git hooks" +msgstr "push remoto, hooks de git" + +msgid "network access, unsandboxed execution" +msgstr "acceso a la red, ejecución fuera de sandbox" + +msgid "network blocked, no other paths, no undo" +msgstr "red bloqueada, sin otras rutas, sin deshacer" + +msgid "no other MCP calls; runs with user permissions (no filesystem isolation)" +msgstr "sin otras llamadas MCP; se ejecuta con permisos de usuario (sin aislamiento de archivos)" + +msgid "no other MCP servers; runs with user permissions (no filesystem isolation)" +msgstr "sin otros servidores MCP; se ejecuta con permisos de usuario (sin aislamiento de archivos)" + +msgid "no other tool calls, no automatic retries" +msgstr "sin otras llamadas a herramientas, sin reintentos automáticos" + +msgid "no tool execution, no file access, no secrets" +msgstr "sin ejecución de herramientas, sin acceso a archivos, sin secretos" + +msgid "no automatic tool runs, no local filesystem access" +msgstr "sin ejecuciones automáticas de herramientas, sin acceso a archivos locales" + +msgid "no internet, no general commands, no writes" +msgstr "sin internet, sin comandos generales, sin escrituras" + +msgid "no Docker, no containers, no workspace.files.read" +msgstr "sin Docker, sin contenedores, sin workspace.files.read" + +msgid "no other MCP calls, no local filesystem access" +msgstr "sin otras llamadas MCP, sin acceso a archivos locales" + +msgid "no writes, no commands, no model context" +msgstr "sin escrituras, sin comandos, sin contexto del modelo" + +msgid "no writes, no network, no Gateway" +msgstr "sin escrituras, sin red, sin Gateway" + +msgid "no credentials, no host network, no other containers" +msgstr "sin credenciales, sin red del host, sin otros contenedores" + +msgid "no provision, no other containers, no host access" +msgstr "sin aprovisionamiento, sin otros contenedores, sin acceso al host" diff --git a/src/isycode/locales/zh/LC_MESSAGES/isycode.po b/src/isycode/locales/zh/LC_MESSAGES/isycode.po index b5724db..59be7be 100644 --- a/src/isycode/locales/zh/LC_MESSAGES/isycode.po +++ b/src/isycode/locales/zh/LC_MESSAGES/isycode.po @@ -2375,3 +2375,231 @@ msgstr "在侧边栏文件树下方显示预览框。" msgid "OpenAI & ChatGPT · choose connection method…" msgstr "OpenAI 和 ChatGPT · 选择连接方式…" + +msgid "Owner" +msgstr "所有者" + +msgid "Scope" +msgstr "范围" + +msgid "Duration" +msgstr "期限" + +msgid "Not granted" +msgstr "未授予" + +msgid "workspace authority" +msgstr "工作区权限" + +msgid "workspace file edit" +msgstr "工作区文件编辑" + +msgid "local MCP server" +msgstr "本地 MCP 服务器" + +msgid "local MCP tool" +msgstr "本地 MCP 工具" + +msgid "tailscale private-access" +msgstr "Tailscale 私有访问" + +msgid "sandboxed command run" +msgstr "沙箱命令运行" + +msgid "git commit" +msgstr "git 提交" + +msgid "browser page read" +msgstr "浏览器页面读取" + +msgid "workspace context pack" +msgstr "工作区上下文包" + +msgid "workspace memory" +msgstr "工作区记忆" + +msgid "session deletion" +msgstr "会话删除" + +msgid "session delete" +msgstr "会话删除" + +msgid "chat session store" +msgstr "聊天会话存储" + +msgid "workspace trust" +msgstr "工作区信任" + +msgid "lsp diagnostics" +msgstr "LSP 诊断" + +msgid "git" +msgstr "git" + +msgid "sandboxed commands" +msgstr "沙箱命令" + +msgid "external model review" +msgstr "外部模型审查" + +msgid "provider network access" +msgstr "提供商网络访问" + +msgid "Gateway MCP target" +msgstr "Gateway MCP 目标" + +msgid "local LSP process" +msgstr "本地 LSP 进程" + +msgid "broker preview" +msgstr "代理预览" + +msgid "Gateway MCP tool" +msgstr "Gateway MCP 工具" + +msgid "Gateway semantic HTTP" +msgstr "Gateway 语义 HTTP" + +msgid "local LSP search" +msgstr "本地 LSP 搜索" + +msgid "semantic broker provision" +msgstr "语义代理配置" + +msgid "broker operation" +msgstr "代理操作" + +msgid "the text shown below" +msgstr "下方显示的文本" + +msgid "listed workspace files" +msgstr "列出的工作区文件" + +msgid "{count} files" +msgstr "{count} 个文件" + +msgid "one-use approval" +msgstr "单次使用审批" + +msgid "one-use, one HTTP request" +msgstr "单次使用,一次 HTTP 请求" + +msgid "one-use, digest-bound approval" +msgstr "单次使用、绑定摘要的审批" + +msgid "one-use, stops after {timeout} s" +msgstr "单次使用,{timeout} 秒后停止" + +msgid "persistent workspace grant" +msgstr "持久工作区授权" + +msgid "persistent workspace setting" +msgstr "持久工作区设置" + +msgid "persistent workspace trust" +msgstr "持久工作区信任" + +msgid "one conversation grant" +msgstr "单会话授权" + +msgid "until ISyCode exits" +msgstr "直到 ISyCode 退出" + +msgid "one-use tool call" +msgstr "单次工具调用" + +msgid "persistent grant (until revoked)" +msgstr "持久授权(直到撤销)" + +msgid "persistent grant (every call still approved)" +msgstr "持久授权(每次调用仍需批准)" + +msgid "one-use, grants revoked after attempt" +msgstr "单次使用,尝试后撤销授权" + +msgid "one-use, grant revoked after operation" +msgstr "单次使用,操作后撤销授权" + +msgid "no tools, no file access, no retries" +msgstr "无工具,无文件访问,无重试" + +msgid "no other host actions, no filesystem access" +msgstr "无其他主机操作,无文件系统访问" + +msgid "no other files, no commands, no secrets" +msgstr "无其他文件,无命令,无机密" + +msgid "no push, no hooks, no other files" +msgstr "不推送,无钩子,无其他文件" + +msgid "no writes, no commands, no secrets" +msgstr "无写入,无命令,无机密" + +msgid "no authority, no grants, no other memory" +msgstr "无权限,无授权,无其他记忆" + +msgid "no other sessions, no filesystem access" +msgstr "无其他会话,无文件系统访问" + +msgid "other conversations, unconfirmed deletions" +msgstr "其他会话,未确认的删除" + +msgid "sensitive files, .isyroot, and unapproved folder writes stay off-limits" +msgstr "敏感文件、.isyroot 以及未批准的文件夹写入仍处于禁止状态" + +msgid "privilege escalation, ungranted capabilities" +msgstr "权限提升、未授予的功能" + +msgid "commits, secrets, authority changes" +msgstr "提交、机密、权限更改" + +msgid "secrets, credentials, project files" +msgstr "机密、凭据、项目文件" + +msgid "file modifications, network access" +msgstr "文件修改、网络访问" + +msgid "remote push, git hooks" +msgstr "远程推送、git 钩子" + +msgid "network access, unsandboxed execution" +msgstr "网络访问、非沙箱执行" + +msgid "network blocked, no other paths, no undo" +msgstr "网络已阻止,无其他路径,不可撤销" + +msgid "no other MCP calls; runs with user permissions (no filesystem isolation)" +msgstr "无其他 MCP 调用;以用户权限运行(无文件系统隔离)" + +msgid "no other MCP servers; runs with user permissions (no filesystem isolation)" +msgstr "无其他 MCP 服务器;以用户权限运行(无文件系统隔离)" + +msgid "no other tool calls, no automatic retries" +msgstr "无其他工具调用,无自动重试" + +msgid "no tool execution, no file access, no secrets" +msgstr "无工具执行,无文件访问,无机密" + +msgid "no automatic tool runs, no local filesystem access" +msgstr "无自动工具运行,无本地文件系统访问" + +msgid "no internet, no general commands, no writes" +msgstr "无网络,无通用命令,无写入" + +msgid "no Docker, no containers, no workspace.files.read" +msgstr "无 Docker,无容器,无 workspace.files.read" + +msgid "no other MCP calls, no local filesystem access" +msgstr "无其他 MCP 调用,无本地文件系统访问" + +msgid "no writes, no commands, no model context" +msgstr "无写入,无命令,无模型上下文" + +msgid "no writes, no network, no Gateway" +msgstr "无写入,无网络,无 Gateway" + +msgid "no credentials, no host network, no other containers" +msgstr "无凭据,无主机网络,无其他容器" + +msgid "no provision, no other containers, no host access" +msgstr "无配置,无其他容器,无主机访问" diff --git a/src/isycode/tui_app_authority.py b/src/isycode/tui_app_authority.py index 5e67e8a..3d7c822 100644 --- a/src/isycode/tui_app_authority.py +++ b/src/isycode/tui_app_authority.py @@ -494,12 +494,14 @@ async def _change_session_delete_grant(self, enabled: bool) -> None: "Allow conversation deletion?" if enabled else "Disable conversation deletion?", f"This permission applies only to conversation {session_id or 'previously selected'} in this workspace. " "Every deletion still requires reviewing the exact conversation and confirming it.", - "Allow deletion" if enabled else "Disable deletion")) + "Allow deletion" if enabled else "Disable deletion", owner="session delete", + scope=f"conversation {session_id or 'current'}", duration="one conversation grant", + not_granted="other conversations, unconfirmed deletions")) if accepted: authority = WorkspaceAuthority(self._workspace_root) - targets = authority.policy().get("grants", {}).get("session.delete", {}).get("targets", []) - authority.set_grant("session.delete", enabled=enabled, - targets=sorted(set(targets + ([session_id] if enabled else [])))) + existing = authority.policy().get("grants", {}).get("session.delete", {}).get("targets", []) + targets = [session_id] if (enabled and session_id) else ([t for t in existing if t != session_id] if session_id else []) + authority.set_grant("session.delete", enabled=bool(targets), targets=sorted(set(targets))) self._open_authority_menu() async def _change_workspace_write_grant(self, enabled: bool) -> None: @@ -509,21 +511,19 @@ async def _change_workspace_write_grant(self, enabled: bool) -> None: return accepted = await self._await_screen(TailscaleConfirmScreen( "Allow file edits in this workspace?" if enabled else "Turn off file edits?", - (f"The assistant may propose creating, changing, moving and deleting files inside " - f"{self._workspace_root}. File edits follow your per-folder approval setting; other changes " - "ask first. A file that changed after review is never touched, and /undo reverts the " - "last change. Sensitive files and .isyroot stay off-limits." if enabled else - "The assistant can no longer propose file changes in this workspace. Files already " - "changed stay as they are."), - "Allow edits" if enabled else "Turn off edits")) + (f"The assistant may propose creating, changing, moving and deleting files inside {self._workspace_root}. " + "File edits follow your per-folder approval setting; other changes ask first. A file that changed after " + "review is never touched, and /undo reverts the last change. Sensitive files and .isyroot stay off-limits." + if enabled else "The assistant can no longer propose file changes in this workspace. Files already changed stay as they are."), + "Allow edits" if enabled else "Turn off edits", owner="workspace authority", + scope=str(self._workspace_root), duration="persistent workspace grant", + not_granted="sensitive files, .isyroot, and unapproved folder writes stay off-limits")) if accepted: try: authority = WorkspaceAuthority(self._workspace_root) for action in FILE_CHANGE_GRANTS: - authority.set_grant(action, enabled=enabled, - path_prefixes=[self._workspace_root] if enabled else []) - self._append(" File edits allowed; folder approval settings still apply." if enabled - else " File edits turned off for this workspace.", GREEN) + authority.set_grant(action, enabled=enabled, path_prefixes=[self._workspace_root] if enabled else []) + self._append(" File edits allowed; folder approval settings still apply." if enabled else " File edits turned off for this workspace.", GREEN) except (WorkspaceAuthorityError, OSError, ValueError) as exc: self._append(f" Edit permission could not be saved ({type(exc).__name__}).", RED) self._open_authority_menu() diff --git a/src/isycode/tui_app_tools.py b/src/isycode/tui_app_tools.py index c64fb5d..b221c53 100644 --- a/src/isycode/tui_app_tools.py +++ b/src/isycode/tui_app_tools.py @@ -636,7 +636,11 @@ async def _start_local_mcp(self, name: str) -> None: f"Start MCP server · {name}", f"Runs this program from your MCP config in {self._workspace_root} with your " f"user's permissions (network included) until ISyCode exits. Extra environment: " - f"{env_keys}.", shlex.join(preview.config.argv), "Start server")): + f"{env_keys}.", shlex.join(preview.config.argv), "Start server", + owner="local MCP server", + scope=name, + duration="until ISyCode exits", + not_granted="no other MCP servers; runs with user permissions (no filesystem isolation)")): self._append(f" MCP {name} not started.", MUTED) return approval = self._action_approvals.issue(preview.request, ttl_seconds=60) @@ -662,7 +666,11 @@ async def _call_local_mcp(self, function: str, arguments) -> str: if not await self._await_screen(LocalMCPConfirmScreen( f"Call MCP tool · {server}.{tool}", "Sends exactly these arguments to the local server. Its answer is untrusted data.", - json.dumps(preview.arguments, ensure_ascii=False, indent=2), "Call once")): + json.dumps(preview.arguments, ensure_ascii=False, indent=2), "Call once", + owner="local MCP tool", + scope=f"{server}.{tool}", + duration="one-use tool call", + not_granted="no other tool calls, no automatic retries")): self._append(" MCP call rejected · nothing was sent", MUTED) return json.dumps({"status": "rejected_by_user"}) outcome = await mcp_owner.call(preview, self._action_approvals.issue(preview.request, ttl_seconds=60)) diff --git a/src/isycode/tui_screens_approval.py b/src/isycode/tui_screens_approval.py index 9292777..36b6264 100644 --- a/src/isycode/tui_screens_approval.py +++ b/src/isycode/tui_screens_approval.py @@ -84,10 +84,10 @@ def compose(self) -> ComposeResult: yield Static(tr("External model review · GPT 6 Luna · OpenAI API"), id="review-consent-title", classes="isy-dialog-title") with VerticalScroll(id="review-consent-body", classes="isy-dialog-body"): yield Static( - "Owner: external model review\n" - "Scope: the text shown below\n" - "Duration: one-use, one HTTP request\n" - "Not granted: no tools, no file access, no retries", + f"{tr('Owner')}: {tr('external model review')}\n" + f"{tr('Scope')}: {tr('the text shown below')}\n" + f"{tr('Duration')}: {tr('one-use, one HTTP request')}\n" + f"{tr('Not granted')}: {tr('no tools, no file access, no retries')}", id="review-consent-structural", markup=False) yield Static(tr("Only the text below will be sent. The reviewer has no tools. One HTTP request, max 1,200 output tokens; automatic retries are disabled."), id="review-consent-warning") with Vertical(id="review-consent-artifact", classes="isy-dialog-detail"): @@ -124,10 +124,10 @@ def _structural_summary(self, *, owner: str, scope: str, duration: str, not_granted: str) -> Static: """F-08: the four structural fields every approval must declare.""" text = Text() - text.append(f"Owner: {owner}\n", style="bold") - text.append(f"Scope: {scope}\n") - text.append(f"Duration: {duration}\n") - text.append(f"Not granted: {not_granted}") + text.append(f"{tr('Owner')}: {tr(owner)}\n", style="bold") + text.append(f"{tr('Scope')}: {tr(scope)}\n") + text.append(f"{tr('Duration')}: {tr(duration)}\n") + text.append(f"{tr('Not granted')}: {tr(not_granted)}") return Static(text, classes="isy-dialog-structural", markup=False) def action_approve(self) -> None: @@ -211,20 +211,28 @@ class TailscaleConfirmScreen(ApprovalScreen): BINDINGS = [Binding("escape", "cancel", "Cancel"), Binding("ctrl+c", "cancel", "Cancel", show=False)] - def __init__(self, title: str, details: str, confirm_label: str): + def __init__(self, title: str, details: str, confirm_label: str, + *, owner: str = "tailscale private-access", + scope: str | None = None, + duration: str = "one-use approval", + not_granted: str = "no other host actions, no filesystem access"): super().__init__() self.title_text = tr(title) self.details = tr(details) self.confirm_label = confirm_label + self.owner = owner + self.scope = scope if scope is not None else self.title_text + self.duration = duration + self.not_granted = not_granted def compose(self) -> ComposeResult: with Vertical(id="tailscale-confirm-card", classes="isy-dialog"): yield Static(self.title_text, id="tailscale-confirm-title", classes="isy-dialog-title") with VerticalScroll(id="tailscale-confirm-body", classes="isy-dialog-body"): yield self._structural_summary( - owner="tailscale private-access", scope=self.title_text, - duration="one-use approval", - not_granted="no other host actions, no filesystem access") + owner=self.owner, scope=self.scope, + duration=self.duration, + not_granted=self.not_granted) with Vertical(id="tailscale-confirm-copy", classes="isy-dialog-detail"): yield Static(Text(self.details)) with Horizontal(id="tailscale-confirm-actions", classes="isy-dialog-actions"): @@ -520,18 +528,26 @@ class LocalMCPConfirmScreen(ApprovalScreen): """ BINDINGS = [Binding("escape", "cancel", "Cancel")] - def __init__(self, title: str, body: str, payload: str, approve_label: str) -> None: + def __init__(self, title: str, body: str, payload: str, approve_label: str, + *, owner: str = "local MCP server", + scope: str | None = None, + duration: str = "one-use approval", + not_granted: str = "no other MCP calls; runs with user permissions (no filesystem isolation)") -> None: super().__init__() self.title_text, self.body, self.payload, self.approve_label = title, body, payload, approve_label + self.owner = owner + self.scope = scope if scope is not None else title + self.duration = duration + self.not_granted = not_granted def compose(self) -> ComposeResult: with Vertical(id="local-mcp-card", classes="isy-dialog"): yield Static(self.title_text, id="local-mcp-title", classes="isy-dialog-title") with VerticalScroll(id="local-mcp-body", classes="isy-dialog-body"): yield self._structural_summary( - owner="local MCP server", scope=self.title_text, - duration="one-use approval", - not_granted="no other MCP calls, no filesystem access") + owner=self.owner, scope=self.scope, + duration=self.duration, + not_granted=self.not_granted) yield Static(Text(self.body)) with Vertical(id="local-mcp-payload", classes="isy-dialog-detail"): yield Static(Text(self.payload)) diff --git a/tests/test_tui_permission_audit.py b/tests/test_tui_permission_audit.py new file mode 100644 index 0000000..7856258 --- /dev/null +++ b/tests/test_tui_permission_audit.py @@ -0,0 +1,105 @@ +"""Tests for remediation of TUI permission audit findings.""" +from __future__ import annotations + +import json +from pathlib import Path +import pytest + +from isycode.localization import configure_locale, tr +from isycode.tui_app_authority import AuthorityMixin +from isycode.tui_screens_approval import ApprovalScreen, LocalMCPConfirmScreen, TailscaleConfirmScreen +from isycode.workspace_authority import WorkspaceAuthority + + +def test_spanish_structural_summary_labels_and_values(): + configure_locale("es") + screen = ApprovalScreen() + widget = screen._structural_summary( + owner="workspace authority", + scope="/workspace", + duration="persistent workspace grant", + not_granted="sensitive files, .isyroot, and unapproved folder writes stay off-limits", + ) + rendered = widget._Static__content.plain + + assert "Propietario: autoridad del espacio de trabajo" in rendered + assert "Alcance: /workspace" in rendered + assert "Duración: permiso persistente del espacio de trabajo" in rendered + assert "Exclusiones: archivos sensibles, .isyroot y carpetas no aprobadas quedan fuera de límites" in rendered + assert "Owner:" not in rendered + assert "Scope:" not in rendered + assert "Duration:" not in rendered + assert "Not granted:" not in rendered + + +def test_tailscale_confirm_screen_supports_custom_structural_summary(): + screen = TailscaleConfirmScreen( + "Allow file edits in this workspace?", + "Details...", + "Allow edits", + owner="workspace authority", + scope="/test/workspace", + duration="persistent workspace grant", + not_granted="sensitive files stay off-limits", + ) + assert screen.owner == "workspace authority" + assert screen.scope == "/test/workspace" + assert screen.duration == "persistent workspace grant" + assert screen.not_granted == "sensitive files stay off-limits" + + +def test_local_mcp_confirm_does_not_promise_unapplied_filesystem_restriction(): + screen = LocalMCPConfirmScreen( + "Start MCP server · test", + "Runs with user permissions...", + "python3 server.py", + "Start server", + ) + assert "no filesystem access" not in screen.not_granted.lower() + assert "user permissions" in screen.not_granted.lower() + + +@pytest.mark.asyncio +async def test_session_delete_grant_does_not_reactivate_previous_session(tmp_path): + class FakeApp(AuthorityMixin): + def __init__(self, root: Path): + self._workspace_root = root + self._active_chat_session_id = None + self.menu_opened = False + self.appended = [] + + def _append(self, text, color=None): + self.appended.append(text) + + def _open_authority_menu(self): + self.menu_opened = True + + async def _await_screen(self, screen): + return True + + root = tmp_path / "workspace" + root.mkdir() + app = FakeApp(root) + authority = WorkspaceAuthority(root) + + # 1. Authorize session A + app._active_chat_session_id = "session-A" + await app._change_session_delete_grant(enabled=True) + policy = authority.policy() + assert policy["grants"]["session.delete"]["enabled"] is True + assert policy["grants"]["session.delete"]["targets"] == ["session-A"] + + # 2. Disable session A + await app._change_session_delete_grant(enabled=False) + policy = authority.policy() + assert policy["grants"]["session.delete"]["enabled"] is False + assert "session-A" not in policy["grants"]["session.delete"].get("targets", []) + + # 3. Authorize session B + app._active_chat_session_id = "session-B" + await app._change_session_delete_grant(enabled=True) + policy = authority.policy() + assert policy["grants"]["session.delete"]["enabled"] is True + assert policy["grants"]["session.delete"]["targets"] == ["session-B"] + # Verify session A is NOT reactivated + assert "session-A" not in policy["grants"]["session.delete"]["targets"] From 4134aec779f6ab1c99818909e9a6c5e1094b8e9b Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Mon, 19 Oct 2026 00:29:44 -0600 Subject: [PATCH 2/2] fix(tui): auto-adjust workspace banner on SidePanel resize and prevent test race --- src/isycode/tui_widgets.py | 11 +++++++++++ tests/test_visual_tui.py | 1 + 2 files changed, 12 insertions(+) diff --git a/src/isycode/tui_widgets.py b/src/isycode/tui_widgets.py index c4048b4..81ef855 100644 --- a/src/isycode/tui_widgets.py +++ b/src/isycode/tui_widgets.py @@ -116,6 +116,10 @@ def set_compact(self, compact: bool) -> None: header.append(mark, style=CYAN if mode == "Classic" else MUTED) self.update(header) + def on_resize(self, event) -> None: + if self.is_mounted and getattr(self, "app", None) and self.app.is_mounted: + self.set_compact(True) + class ActivityStatus(Static): """Repaint the cat after this widget receives its final resized geometry.""" @@ -403,6 +407,13 @@ def on_mount(self) -> None: button.compact = True self.query_one("#show-overview", Button).add_class("rail-lit") + def on_resize(self, event) -> None: + if self.is_mounted and getattr(self, "app", None) and self.app.is_mounted: + try: + self.app.query_one(Banner).set_compact(True) + except Exception: + pass + class ToolActivityGroup(ExpandableBox): """Consecutive identical operations form one parent with retained branches.""" diff --git a/tests/test_visual_tui.py b/tests/test_visual_tui.py index 2e8caa9..0f8b7cc 100644 --- a/tests/test_visual_tui.py +++ b/tests/test_visual_tui.py @@ -20,6 +20,7 @@ async def _wait_for_banner_to_fit(pilot, banner, panel): remaining = deadline - asyncio.get_running_loop().time() if remaining <= 0: raise AssertionError("workspace banner did not fit after sidebar layout") + banner.set_compact(True) await pilot.pause(min(0.05, remaining))