diff --git a/pyproject.toml b/pyproject.toml index b22528dc..844f3a07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "iac-code" +name = "iac_code" dynamic = ["version"] description = "Your AI-powered Infrastructure as Code assistant" readme = "README.md" diff --git a/src/iac_code/acp/session.py b/src/iac_code/acp/session.py index a09099b9..8caa30f7 100644 --- a/src/iac_code/acp/session.py +++ b/src/iac_code/acp/session.py @@ -152,6 +152,8 @@ def _history_message_to_updates(msg: Message) -> list[Any]: _OPTION_ALLOW_ALWAYS = "allow_always" _OPTION_REJECT_ONCE = "reject_once" _OPTION_REJECT_ALWAYS = "reject_always" +_PREFIX_ALLOW_RULE = "allow_rule:" +_PREFIX_DENY_RULE = "deny_rule:" class ACPSession: @@ -373,6 +375,30 @@ async def cancel(self) -> None: logger.info("Session %s cancel requested", self.id) self._current_task.cancel() + def _get_permission_context(self): + """Read the agent_loop's mutable permission context.""" + return getattr(self.agent_loop, "_permission_context", None) + + def _set_permission_context(self, perm_ctx) -> None: + """Write back the updated permission context to agent_loop.""" + if hasattr(self.agent_loop, "_permission_context"): + self.agent_loop._permission_context = perm_ctx + + def _apply_rule(self, tool_name: str, rules_str: str, behavior: str) -> None: + """Apply rule-level permission to the session's permission_context.""" + from iac_code.services.permissions.storage import apply_session_rule + from iac_code.types.permissions import PermissionRuleValue + + perm_ctx = self._get_permission_context() + if perm_ctx is None: + return + for rule_content in rules_str.split(","): + rule_content = rule_content.strip() + if rule_content: + rule_value = PermissionRuleValue(tool_name=tool_name, rule_content=rule_content) + perm_ctx = apply_session_rule(perm_ctx, behavior, rule_value) + self._set_permission_context(perm_ctx) + async def _request_permission(self, event: PermissionRequestEvent) -> bool: tool_name = event.tool_name @@ -385,59 +411,113 @@ async def _request_permission(self, event: PermissionRequestEvent) -> bool: logger.debug("Permission auto-denied for tool %s (cached)", tool_name) return False - response = await self._conn.request_permission( - [ + # Extract suggestions from permission_result for rule-level options. + suggestions = [] + if ( + event.permission_result is not None + and hasattr(event.permission_result, "suggestions") + and event.permission_result.suggestions + ): + suggestions = event.permission_result.suggestions + + # Build dynamic option list aligned with local REPL behavior. + options: list[acp.schema.PermissionOption] = [ + acp.schema.PermissionOption( + option_id=_OPTION_ALLOW_ONCE, + name="Allow once", + kind="allow_once", + ), + ] + + if suggestions: + rules_display = ",".join(s.rule_content for s in suggestions) + options.append( acp.schema.PermissionOption( - option_id=_OPTION_ALLOW_ONCE, - name="Allow once", - kind="allow_once", - ), + option_id=_PREFIX_ALLOW_RULE + rules_display, + name='Always allow "{}" (this session)'.format(rules_display), + kind="allow_always", + ) + ) + else: + options.append( acp.schema.PermissionOption( option_id=_OPTION_ALLOW_ALWAYS, - name="Always allow", + name="Always allow this tool", kind="allow_always", - ), - acp.schema.PermissionOption( - option_id=_OPTION_REJECT_ONCE, - name="Reject once", - kind="reject_once", - ), + ) + ) + + options.append( + acp.schema.PermissionOption( + option_id=_OPTION_REJECT_ONCE, + name="Reject once", + kind="reject_once", + ) + ) + + if suggestions: + rules_display = ",".join(s.rule_content for s in suggestions) + options.append( acp.schema.PermissionOption( - option_id=_OPTION_REJECT_ALWAYS, - name="Always reject", + option_id=_PREFIX_DENY_RULE + rules_display, + name='Always deny "{}" (this session)'.format(rules_display), kind="reject_always", - ), - ], + ) + ) + + options.append( + acp.schema.PermissionOption( + option_id=_OPTION_REJECT_ALWAYS, + name="Always reject this tool", + kind="reject_always", + ), + ) + + # Build content with command details and suggested rule. + content_text = "Approve tool call: {}\nInput: {}".format(tool_name, event.tool_input) + if suggestions: + content_text += "\nSuggested rule: {}".format(",".join(s.rule_content for s in suggestions)) + + response = await self._conn.request_permission( + options, self.id, acp.schema.ToolCallUpdate( - tool_call_id=f"permission/{event.tool_use_id}", + tool_call_id="permission/{}".format(event.tool_use_id), title=event.tool_name, content=[ acp.schema.ContentToolCallContent( type="content", content=acp.schema.TextContentBlock( type="text", - text=f"Approve tool call {event.tool_name} with input: {event.tool_input}", + text=content_text, ), ) ], ), ) - # Interpret the outcome and update the permission cache + # Interpret the outcome and update permission state. if isinstance(response.outcome, acp.schema.AllowedOutcome): option_id = response.outcome.option_id if option_id == _OPTION_ALLOW_ALWAYS: self._cache_permission(tool_name, "always_allow") + elif option_id and option_id.startswith(_PREFIX_ALLOW_RULE): + rules_str = option_id[len(_PREFIX_ALLOW_RULE) :] + self._apply_rule(tool_name, rules_str, "allow") return True - # DeniedOutcome — the ACP SDK DeniedOutcome has no option_id field, - # so clients that want to signal "reject_always" should set - # _meta={"option_id": "reject_always"} on the *response* envelope. + # DeniedOutcome — parse option_id from meta or direct field. if isinstance(response.outcome, acp.schema.DeniedOutcome): - resp_meta = getattr(response, "field_meta", None) or {} - if resp_meta.get("option_id") == _OPTION_REJECT_ALWAYS: + option_id = getattr(response.outcome, "option_id", None) + if option_id is None: + resp_meta = getattr(response, "field_meta", None) or {} + option_id = resp_meta.get("option_id") + + if option_id == _OPTION_REJECT_ALWAYS: self._cache_permission(tool_name, "always_deny") + elif option_id and option_id.startswith(_PREFIX_DENY_RULE): + rules_str = option_id[len(_PREFIX_DENY_RULE) :] + self._apply_rule(tool_name, rules_str, "deny") return False diff --git a/src/iac_code/commands/auth.py b/src/iac_code/commands/auth.py index 82765b6f..842c392e 100644 --- a/src/iac_code/commands/auth.py +++ b/src/iac_code/commands/auth.py @@ -18,6 +18,7 @@ _save_yaml, get_active_provider_key, get_credentials_path, + get_llm_source, get_provider_config, get_settings_path, ) @@ -570,6 +571,24 @@ async def auth_command(context: "CommandContext | None" = None, **kwargs) -> str def _auth_flow(console, store) -> str | None: """Auth flow running inside alternate screen.""" + llm_source = get_llm_source() + if llm_source != "local": + lock_notice = _("LLM provider is locked by '{source}'. To change, modify llm_source in settings.yml.").format( + source=llm_source + ) + options = [_cloud_provider_display(p["name"]) for p in CLOUD_PROVIDERS] + idx = _select("{}\n\n{}".format(lock_notice, _("Select Cloud Provider")), options) + if idx is None: + return _("Auth cancelled") + provider = CLOUD_PROVIDERS[idx] + if provider["name"] == "aliyun": + result = _aliyun_auth_flow() + else: + result = _BACK + if isinstance(result, _BackSentinel): + return _("Auth cancelled") + return result + while True: # Step 0: Select category categories = [ diff --git a/src/iac_code/commands/model.py b/src/iac_code/commands/model.py index 119bb3bd..94013c0f 100644 --- a/src/iac_code/commands/model.py +++ b/src/iac_code/commands/model.py @@ -14,7 +14,7 @@ save_active_provider_config, select_model_interactive, ) -from iac_code.config import _load_yaml, get_active_provider_key, get_settings_path +from iac_code.config import _load_yaml, get_active_provider_key, get_llm_source, get_settings_path from iac_code.i18n import _ from iac_code.services.telemetry import log_event from iac_code.services.telemetry.names import Events @@ -44,6 +44,12 @@ def _get_active_provider_models() -> list[str]: async def model_command(context: "CommandContext | None" = None, args: list[str] | None = None, **kwargs) -> str | None: """Switch or display current model.""" + llm_source = get_llm_source() + if llm_source != "local": + return _("Model selection is locked by '{source}'. To change, modify llm_source in settings.yml.").format( + source=llm_source + ) + store = context.store if context else kwargs.get("store") args = args or [] diff --git a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po index ae93d53b..60a7047f 100644 --- a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-18 12:25+0800\n" +"POT-Creation-Date: 2026-05-18 16:59+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: de\n" @@ -543,212 +543,222 @@ msgstr "Eine frühere Sitzung fortsetzen" msgid "[conversation id or search term]" msgstr "[Konversations-ID oder Suchbegriff]" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:836 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "Navigieren" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 -#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 -#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 -#: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:368 +#: src/iac_code/commands/auth.py:402 src/iac_code/commands/auth.py:409 +#: src/iac_code/commands/auth.py:433 src/iac_code/commands/auth.py:836 +#: src/iac_code/commands/auth.py:1026 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "Bestätigen" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 -#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 -#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 -#: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 -#: src/iac_code/commands/auth.py:1007 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:366 +#: src/iac_code/commands/auth.py:368 src/iac_code/commands/auth.py:402 +#: src/iac_code/commands/auth.py:409 src/iac_code/commands/auth.py:433 +#: src/iac_code/commands/auth.py:836 src/iac_code/commands/auth.py:921 +#: src/iac_code/commands/auth.py:1026 msgid "Back" msgstr "Zurück" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Keep" msgstr "Behalten" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Re-enter" msgstr "Erneut eingeben" -#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 -#: src/iac_code/commands/auth.py:651 +#: src/iac_code/commands/auth.py:508 src/iac_code/commands/auth.py:650 +#: src/iac_code/commands/auth.py:670 msgid " (current)" msgstr " (aktuell)" -#: src/iac_code/commands/auth.py:510 +#: src/iac_code/commands/auth.py:511 msgid "Custom model..." msgstr "Benutzerdefiniertes Modell …" -#: src/iac_code/commands/auth.py:513 +#: src/iac_code/commands/auth.py:514 #, python-brace-format msgid "Select model for {provider}" msgstr "Modell für {provider} auswählen" -#: src/iac_code/commands/auth.py:515 +#: src/iac_code/commands/auth.py:516 msgid "Select model" msgstr "Modell auswählen" -#: src/iac_code/commands/auth.py:523 +#: src/iac_code/commands/auth.py:524 msgid "Enter custom model name: " msgstr "Benutzerdefinierten Modellnamen eingeben: " -#: src/iac_code/commands/auth.py:549 +#: src/iac_code/commands/auth.py:550 msgid "Error: console not available" msgstr "Fehler: Konsole nicht verfügbar" #: src/iac_code/commands/auth.py:576 +#, python-brace-format +msgid "" +"LLM provider is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "" +"LLM-Anbieter ist durch '{source}' gesperrt. Zum Ändern passen Sie " +"llm_source in settings.yml an." + +#: src/iac_code/commands/auth.py:580 src/iac_code/commands/auth.py:780 +msgid "Select Cloud Provider" +msgstr "Cloud-Anbieter auswählen" + +#: src/iac_code/commands/auth.py:582 src/iac_code/commands/auth.py:589 +#: src/iac_code/commands/auth.py:600 src/iac_code/commands/auth.py:695 +#: src/iac_code/commands/auth.py:711 src/iac_code/commands/auth.py:789 +#: src/iac_code/commands/auth.py:962 src/iac_code/commands/auth.py:1003 +msgid "Auth cancelled" +msgstr "Authentifizierung abgebrochen" + +#: src/iac_code/commands/auth.py:595 msgid "Configure LLM Provider" msgstr "LLM-Anbieter konfigurieren" -#: src/iac_code/commands/auth.py:577 +#: src/iac_code/commands/auth.py:596 msgid "Configure IaC Cloud Service" msgstr "IaC-Cloud-Dienst konfigurieren" -#: src/iac_code/commands/auth.py:579 +#: src/iac_code/commands/auth.py:598 msgid "Select configuration type" msgstr "Konfigurationstyp auswählen" -#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 -#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 -#: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 -msgid "Auth cancelled" -msgstr "Authentifizierung abgebrochen" - -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:654 msgid "Select provider" msgstr "Anbieter auswählen" -#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#: src/iac_code/commands/auth.py:675 src/iac_code/commands/auth.py:764 #, python-brace-format msgid "Select provider — {group}" msgstr "Anbieter auswählen — {group}" -#: src/iac_code/commands/auth.py:669 +#: src/iac_code/commands/auth.py:688 #, python-brace-format msgid "Configure {provider}" msgstr "{provider} konfigurieren" -#: src/iac_code/commands/auth.py:685 +#: src/iac_code/commands/auth.py:704 #, python-brace-format msgid "Enter API key for {provider}" msgstr "API-Key für {provider} eingeben" -#: src/iac_code/commands/auth.py:723 +#: src/iac_code/commands/auth.py:742 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}: {provider} / {model}" -#: src/iac_code/commands/auth.py:724 +#: src/iac_code/commands/auth.py:743 msgid "Configured" msgstr "Konfiguriert" -#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:750 src/iac_code/commands/auth.py:771 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" -#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +#: src/iac_code/commands/auth.py:751 src/iac_code/providers/registry.py:422 msgid "ZhiPu AI" msgstr "ZhiPu AI" -#: src/iac_code/commands/auth.py:733 +#: src/iac_code/commands/auth.py:752 msgid "Kimi" msgstr "Kimi" -#: src/iac_code/commands/auth.py:734 +#: src/iac_code/commands/auth.py:753 msgid "MiniMax" msgstr "MiniMax" -#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +#: src/iac_code/commands/auth.py:754 src/iac_code/providers/registry.py:424 msgid "Volcengine" msgstr "Volcengine" -#: src/iac_code/commands/auth.py:736 +#: src/iac_code/commands/auth.py:755 msgid "SiliconFlow" msgstr "SiliconFlow" -#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +#: src/iac_code/commands/auth.py:756 src/iac_code/providers/registry.py:415 msgid "DeepSeek" msgstr "DeepSeek" -#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +#: src/iac_code/commands/auth.py:757 src/iac_code/providers/registry.py:413 msgid "OpenAI" msgstr "OpenAI" -#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +#: src/iac_code/commands/auth.py:758 src/iac_code/providers/registry.py:414 msgid "Anthropic" msgstr "Anthropic" -#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +#: src/iac_code/commands/auth.py:759 src/iac_code/providers/registry.py:417 msgid "Google Gemini" msgstr "Google Gemini" -#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +#: src/iac_code/commands/auth.py:760 src/iac_code/providers/registry.py:430 msgid "Azure OpenAI" msgstr "Azure OpenAI" -#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +#: src/iac_code/commands/auth.py:761 src/iac_code/providers/registry.py:429 msgid "OpenRouter" msgstr "OpenRouter" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:762 msgid "Local" msgstr "Lokal" -#: src/iac_code/commands/auth.py:744 +#: src/iac_code/commands/auth.py:763 msgid "Compatible" msgstr "Kompatibel" -#: src/iac_code/commands/auth.py:761 -msgid "Select Cloud Provider" -msgstr "Cloud-Anbieter auswählen" - -#: src/iac_code/commands/auth.py:777 +#: src/iac_code/commands/auth.py:796 msgid "Credential" msgstr "Anmeldedaten" -#: src/iac_code/commands/auth.py:778 src/iac_code/commands/auth.py:875 -#: src/iac_code/commands/auth.py:980 src/iac_code/ui/renderer.py:418 +#: src/iac_code/commands/auth.py:797 src/iac_code/commands/auth.py:894 +#: src/iac_code/commands/auth.py:999 src/iac_code/ui/renderer.py:418 msgid "Region" msgstr "Region" -#: src/iac_code/commands/auth.py:780 +#: src/iac_code/commands/auth.py:799 msgid "Configure Alibaba Cloud" msgstr "Alibaba Cloud konfigurieren" -#: src/iac_code/commands/auth.py:863 +#: src/iac_code/commands/auth.py:882 msgid "Current configuration" msgstr "Aktuelle Konfiguration" -#: src/iac_code/commands/auth.py:865 +#: src/iac_code/commands/auth.py:884 msgid "Mode" msgstr "Modus" -#: src/iac_code/commands/auth.py:872 +#: src/iac_code/commands/auth.py:891 msgid "(not set)" msgstr "(nicht gesetzt)" -#: src/iac_code/commands/auth.py:889 +#: src/iac_code/commands/auth.py:908 msgid "Configure Alibaba Cloud credentials" msgstr "Alibaba Cloud-Anmeldedaten konfigurieren" -#: src/iac_code/commands/auth.py:902 +#: src/iac_code/commands/auth.py:921 msgid "Reconfigure credential" msgstr "Anmeldedaten neu konfigurieren" -#: src/iac_code/commands/auth.py:915 +#: src/iac_code/commands/auth.py:934 msgid "Select credential type" msgstr "Anmeldedatentyp auswählen" -#: src/iac_code/commands/auth.py:965 +#: src/iac_code/commands/auth.py:984 msgid "Configured: Alibaba Cloud credentials saved to ~/.iac-code" msgstr "Konfiguriert: Alibaba Cloud-Anmeldedaten unter ~/.iac-code gespeichert" -#: src/iac_code/commands/auth.py:972 +#: src/iac_code/commands/auth.py:991 msgid "Configure Alibaba Cloud region" msgstr "Alibaba Cloud-Region konfigurieren" -#: src/iac_code/commands/auth.py:998 +#: src/iac_code/commands/auth.py:1017 msgid "Configured: Alibaba Cloud region saved to ~/.iac-code" msgstr "Konfiguriert: Alibaba Cloud-Region unter ~/.iac-code gespeichert" @@ -781,8 +791,8 @@ msgstr "Der Befehl debug erfordert einen Kontext." msgid "No active session." msgstr "Keine aktive Sitzung." -#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:82 -#: src/iac_code/commands/model.py:86 +#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:88 +#: src/iac_code/commands/model.py:92 msgid "No configured providers. Run /auth first." msgstr "Keine konfigurierten Anbieter. Führen Sie zuerst /auth aus." @@ -844,17 +854,26 @@ msgstr "Befehlsvorschläge anzeigen" msgid "Exit" msgstr "Beenden" -#: src/iac_code/commands/model.py:75 src/iac_code/commands/model.py:130 +#: src/iac_code/commands/model.py:49 +#, python-brace-format +msgid "" +"Model selection is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "" +"Modellauswahl ist durch '{source}' gesperrt. Zum Ändern passen Sie " +"llm_source in settings.yml an." + +#: src/iac_code/commands/model.py:81 src/iac_code/commands/model.py:136 #, python-brace-format msgid "Model switched to: {model}" msgstr "Modell gewechselt auf: {model}" -#: src/iac_code/commands/model.py:79 +#: src/iac_code/commands/model.py:85 #, python-brace-format msgid "Current model: {model}" msgstr "Aktuelles Modell: {model}" -#: src/iac_code/commands/model.py:105 +#: src/iac_code/commands/model.py:111 #, python-brace-format msgid "Kept model as {model}" msgstr "Modell beibehalten: {model}" @@ -962,79 +981,79 @@ msgstr "" " Base URL korrekt ist (aktuell: {base_url}). Viele OpenAI-kompatible " "Endpunkte erfordern ein /v1-Suffix (z. B. {base_url}/v1)." -#: src/iac_code/providers/registry.py:406 +#: src/iac_code/providers/registry.py:411 msgid "Alibaba Cloud Bailian" msgstr "Alibaba Cloud Bailian" -#: src/iac_code/providers/registry.py:407 +#: src/iac_code/providers/registry.py:412 msgid "Alibaba Cloud Bailian Token Plan" msgstr "Alibaba Cloud Bailian Token Plan" -#: src/iac_code/providers/registry.py:411 +#: src/iac_code/providers/registry.py:416 msgid "OpenAPI Compatible" msgstr "OpenAPI-kompatibel" -#: src/iac_code/providers/registry.py:413 +#: src/iac_code/providers/registry.py:418 msgid "Kimi (China)" msgstr "Kimi (China)" -#: src/iac_code/providers/registry.py:414 +#: src/iac_code/providers/registry.py:419 msgid "Kimi (International)" msgstr "Kimi (International)" -#: src/iac_code/providers/registry.py:415 +#: src/iac_code/providers/registry.py:420 msgid "MiniMax (China)" msgstr "MiniMax (China)" -#: src/iac_code/providers/registry.py:416 +#: src/iac_code/providers/registry.py:421 msgid "MiniMax (International)" msgstr "MiniMax (International)" -#: src/iac_code/providers/registry.py:418 +#: src/iac_code/providers/registry.py:423 msgid "ZhiPu AI (International)" msgstr "ZhiPu AI (International)" -#: src/iac_code/providers/registry.py:420 +#: src/iac_code/providers/registry.py:425 msgid "SiliconFlow (China)" msgstr "SiliconFlow (China)" -#: src/iac_code/providers/registry.py:421 +#: src/iac_code/providers/registry.py:426 msgid "SiliconFlow (International)" msgstr "SiliconFlow (International)" -#: src/iac_code/providers/registry.py:422 +#: src/iac_code/providers/registry.py:427 msgid "Ollama (Local)" msgstr "Ollama (Lokal)" -#: src/iac_code/providers/registry.py:423 +#: src/iac_code/providers/registry.py:428 msgid "LM Studio (Local)" msgstr "LM Studio (Lokal)" -#: src/iac_code/providers/registry.py:426 +#: src/iac_code/providers/registry.py:431 msgid "ModelScope" msgstr "ModelScope" -#: src/iac_code/providers/registry.py:427 +#: src/iac_code/providers/registry.py:432 msgid "Alibaba Cloud CodingPlan" msgstr "Alibaba Cloud CodingPlan" -#: src/iac_code/providers/registry.py:428 +#: src/iac_code/providers/registry.py:433 msgid "Alibaba Cloud CodingPlan (International)" msgstr "Alibaba Cloud CodingPlan (International)" -#: src/iac_code/providers/registry.py:429 +#: src/iac_code/providers/registry.py:434 msgid "ZhiPu AI CodingPlan" msgstr "ZhiPu AI CodingPlan" -#: src/iac_code/providers/registry.py:430 +#: src/iac_code/providers/registry.py:435 msgid "ZhiPu AI CodingPlan (International)" msgstr "ZhiPu AI CodingPlan (International)" -#: src/iac_code/providers/registry.py:431 +#: src/iac_code/providers/registry.py:436 msgid "Volcengine CodingPlan" msgstr "Volcengine CodingPlan" -#: src/iac_code/providers/registry.py:432 +#: src/iac_code/providers/registry.py:437 msgid "Anthropic Compatible" msgstr "Anthropic-kompatibel" @@ -1055,7 +1074,7 @@ msgstr "" "aus settings.yml)." #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:185 src/iac_code/tools/bash/bash_tool.py:154 +#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:158 #, python-brace-format msgid "Allow {}?" msgstr "{} erlauben?" @@ -1257,11 +1276,11 @@ msgstr "{cmd} wird ausgeführt" msgid "Running command..." msgstr "Befehl wird ausgeführt …" -#: src/iac_code/tools/bash/command_parser.py:41 +#: src/iac_code/tools/bash/command_parser.py:42 msgid "parse error" msgstr "Analysefehler" -#: src/iac_code/tools/bash/command_parser.py:44 +#: src/iac_code/tools/bash/command_parser.py:46 msgid "unsupported shell construct" msgstr "Nicht unterstütztes Shell-Konstrukt" @@ -1270,53 +1289,57 @@ msgstr "Nicht unterstütztes Shell-Konstrukt" msgid "path outside allowed directories: {}" msgstr "Pfad außerhalb erlaubter Verzeichnisse: {}" -#: src/iac_code/tools/bash/permissions.py:101 +#: src/iac_code/tools/bash/permissions.py:135 #, python-brace-format msgid "matched deny rule(s): {}" msgstr "Übereinstimmende Ablehnungsregel(n): {}" -#: src/iac_code/tools/bash/permissions.py:108 +#: src/iac_code/tools/bash/permissions.py:142 #, python-brace-format msgid "matched ask rule(s): {}" msgstr "Übereinstimmende Abfrageregel(n): {}" -#: src/iac_code/tools/bash/permissions.py:120 -#: src/iac_code/tools/bash/permissions.py:178 +#: src/iac_code/tools/bash/permissions.py:154 +#: src/iac_code/tools/bash/permissions.py:220 #, python-brace-format msgid "matched allow rule(s): {}" msgstr "Übereinstimmende Erlaubnisregel(n): {}" -#: src/iac_code/tools/bash/permissions.py:129 +#: src/iac_code/tools/bash/permissions.py:162 +msgid "complex command requires confirmation" +msgstr "Komplexer Befehl erfordert Bestätigung" + +#: src/iac_code/tools/bash/permissions.py:171 msgid "sed in-place edit requires confirmation" msgstr "sed-In-Place-Bearbeitung erfordert Bestätigung" -#: src/iac_code/tools/bash/permissions.py:152 +#: src/iac_code/tools/bash/permissions.py:194 msgid "command failed basic safety checks" msgstr "Befehl hat grundlegende Sicherheitsprüfungen nicht bestanden" -#: src/iac_code/tools/bash/permissions.py:168 +#: src/iac_code/tools/bash/permissions.py:210 #, python-brace-format msgid "matched deny rule(s) on full command: {}" msgstr "Ablehnungsregel(n) für den vollständigen Befehl: {}" -#: src/iac_code/tools/bash/permissions.py:185 +#: src/iac_code/tools/bash/permissions.py:227 msgid "command too complex to analyze" msgstr "Befehl zu komplex für die Analyse" -#: src/iac_code/tools/bash/permissions.py:187 +#: src/iac_code/tools/bash/permissions.py:229 msgid "could not parse command" msgstr "Befehl konnte nicht analysiert werden" -#: src/iac_code/tools/bash/permissions.py:203 +#: src/iac_code/tools/bash/permissions.py:245 #, python-brace-format msgid "too many subcommands (>{})" msgstr "Zu viele Unterbefehle (>{})" -#: src/iac_code/tools/bash/permissions.py:215 +#: src/iac_code/tools/bash/permissions.py:258 msgid "multiple cd commands in compound command" msgstr "Mehrere cd-Befehle im zusammengesetzten Befehl" -#: src/iac_code/tools/bash/permissions.py:227 +#: src/iac_code/tools/bash/permissions.py:271 msgid "cd combined with git in compound command" msgstr "cd mit git kombiniert im zusammengesetzten Befehl" @@ -1588,13 +1611,13 @@ msgid "Log file" msgstr "Protokolldatei" #: src/iac_code/ui/renderer.py:351 src/iac_code/ui/renderer.py:621 -#: src/iac_code/ui/renderer.py:1380 +#: src/iac_code/ui/renderer.py:1402 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "Nachgedacht für {seconds:.1f}s" #: src/iac_code/ui/renderer.py:367 src/iac_code/ui/renderer.py:653 -#: src/iac_code/ui/renderer.py:1401 +#: src/iac_code/ui/renderer.py:1423 msgid "(ctrl+o to expand)" msgstr "(ctrl+o zum Aufklappen)" @@ -1654,24 +1677,29 @@ msgstr "Diese Aktion zulassen?" msgid "Yes, allow once" msgstr "Ja, einmal zulassen" -#: src/iac_code/ui/renderer.py:1304 +#: src/iac_code/ui/renderer.py:1306 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "Ja, \"{rule}\" immer erlauben (diese Sitzung)" -#: src/iac_code/ui/renderer.py:1309 +#: src/iac_code/ui/renderer.py:1311 msgid "Yes, allow always for this tool" msgstr "Ja, für dieses Tool immer zulassen" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "No, reject once" msgstr "Nein, einmal ablehnen" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "default" msgstr "Standard" -#: src/iac_code/ui/renderer.py:1314 +#: src/iac_code/ui/renderer.py:1321 +#, python-brace-format +msgid "No, always deny \"{rule}\" (this session)" +msgstr "Nein, immer \"{rule}\" ablehnen (diese Sitzung)" + +#: src/iac_code/ui/renderer.py:1326 msgid "No, always reject this tool" msgstr "Nein, dieses Tool immer ablehnen" diff --git a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po index 4b7c6b37..15909c6a 100644 --- a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-18 12:25+0800\n" +"POT-Creation-Date: 2026-05-18 16:59+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: es\n" @@ -544,212 +544,222 @@ msgstr "Reanudar una sesión anterior" msgid "[conversation id or search term]" msgstr "[id de conversación o término de búsqueda]" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:836 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "Navegar" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 -#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 -#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 -#: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:368 +#: src/iac_code/commands/auth.py:402 src/iac_code/commands/auth.py:409 +#: src/iac_code/commands/auth.py:433 src/iac_code/commands/auth.py:836 +#: src/iac_code/commands/auth.py:1026 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "Confirmar" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 -#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 -#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 -#: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 -#: src/iac_code/commands/auth.py:1007 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:366 +#: src/iac_code/commands/auth.py:368 src/iac_code/commands/auth.py:402 +#: src/iac_code/commands/auth.py:409 src/iac_code/commands/auth.py:433 +#: src/iac_code/commands/auth.py:836 src/iac_code/commands/auth.py:921 +#: src/iac_code/commands/auth.py:1026 msgid "Back" msgstr "Atrás" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Keep" msgstr "Conservar" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Re-enter" msgstr "Volver a introducir" -#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 -#: src/iac_code/commands/auth.py:651 +#: src/iac_code/commands/auth.py:508 src/iac_code/commands/auth.py:650 +#: src/iac_code/commands/auth.py:670 msgid " (current)" msgstr " (actual)" -#: src/iac_code/commands/auth.py:510 +#: src/iac_code/commands/auth.py:511 msgid "Custom model..." msgstr "Modelo personalizado..." -#: src/iac_code/commands/auth.py:513 +#: src/iac_code/commands/auth.py:514 #, python-brace-format msgid "Select model for {provider}" msgstr "Seleccionar modelo para {provider}" -#: src/iac_code/commands/auth.py:515 +#: src/iac_code/commands/auth.py:516 msgid "Select model" msgstr "Seleccionar modelo" -#: src/iac_code/commands/auth.py:523 +#: src/iac_code/commands/auth.py:524 msgid "Enter custom model name: " msgstr "Introduzca el nombre del modelo personalizado: " -#: src/iac_code/commands/auth.py:549 +#: src/iac_code/commands/auth.py:550 msgid "Error: console not available" msgstr "Error: la consola no está disponible" #: src/iac_code/commands/auth.py:576 +#, python-brace-format +msgid "" +"LLM provider is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "" +"El proveedor LLM está bloqueado por '{source}'. Para cambiar, modifique " +"llm_source en settings.yml." + +#: src/iac_code/commands/auth.py:580 src/iac_code/commands/auth.py:780 +msgid "Select Cloud Provider" +msgstr "Seleccionar proveedor de cloud" + +#: src/iac_code/commands/auth.py:582 src/iac_code/commands/auth.py:589 +#: src/iac_code/commands/auth.py:600 src/iac_code/commands/auth.py:695 +#: src/iac_code/commands/auth.py:711 src/iac_code/commands/auth.py:789 +#: src/iac_code/commands/auth.py:962 src/iac_code/commands/auth.py:1003 +msgid "Auth cancelled" +msgstr "Autenticación cancelada" + +#: src/iac_code/commands/auth.py:595 msgid "Configure LLM Provider" msgstr "Configurar proveedor LLM" -#: src/iac_code/commands/auth.py:577 +#: src/iac_code/commands/auth.py:596 msgid "Configure IaC Cloud Service" msgstr "Configurar servicio cloud IaC" -#: src/iac_code/commands/auth.py:579 +#: src/iac_code/commands/auth.py:598 msgid "Select configuration type" msgstr "Seleccionar tipo de configuración" -#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 -#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 -#: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 -msgid "Auth cancelled" -msgstr "Autenticación cancelada" - -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:654 msgid "Select provider" msgstr "Seleccionar proveedor" -#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#: src/iac_code/commands/auth.py:675 src/iac_code/commands/auth.py:764 #, python-brace-format msgid "Select provider — {group}" msgstr "Seleccionar proveedor — {group}" -#: src/iac_code/commands/auth.py:669 +#: src/iac_code/commands/auth.py:688 #, python-brace-format msgid "Configure {provider}" msgstr "Configurar {provider}" -#: src/iac_code/commands/auth.py:685 +#: src/iac_code/commands/auth.py:704 #, python-brace-format msgid "Enter API key for {provider}" msgstr "Introduzca la API key para {provider}" -#: src/iac_code/commands/auth.py:723 +#: src/iac_code/commands/auth.py:742 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}: {provider} / {model}" -#: src/iac_code/commands/auth.py:724 +#: src/iac_code/commands/auth.py:743 msgid "Configured" msgstr "Configurado" -#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:750 src/iac_code/commands/auth.py:771 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" -#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +#: src/iac_code/commands/auth.py:751 src/iac_code/providers/registry.py:422 msgid "ZhiPu AI" msgstr "ZhiPu AI" -#: src/iac_code/commands/auth.py:733 +#: src/iac_code/commands/auth.py:752 msgid "Kimi" msgstr "Kimi" -#: src/iac_code/commands/auth.py:734 +#: src/iac_code/commands/auth.py:753 msgid "MiniMax" msgstr "MiniMax" -#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +#: src/iac_code/commands/auth.py:754 src/iac_code/providers/registry.py:424 msgid "Volcengine" msgstr "Volcengine" -#: src/iac_code/commands/auth.py:736 +#: src/iac_code/commands/auth.py:755 msgid "SiliconFlow" msgstr "SiliconFlow" -#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +#: src/iac_code/commands/auth.py:756 src/iac_code/providers/registry.py:415 msgid "DeepSeek" msgstr "DeepSeek" -#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +#: src/iac_code/commands/auth.py:757 src/iac_code/providers/registry.py:413 msgid "OpenAI" msgstr "OpenAI" -#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +#: src/iac_code/commands/auth.py:758 src/iac_code/providers/registry.py:414 msgid "Anthropic" msgstr "Anthropic" -#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +#: src/iac_code/commands/auth.py:759 src/iac_code/providers/registry.py:417 msgid "Google Gemini" msgstr "Google Gemini" -#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +#: src/iac_code/commands/auth.py:760 src/iac_code/providers/registry.py:430 msgid "Azure OpenAI" msgstr "Azure OpenAI" -#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +#: src/iac_code/commands/auth.py:761 src/iac_code/providers/registry.py:429 msgid "OpenRouter" msgstr "OpenRouter" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:762 msgid "Local" msgstr "Local" -#: src/iac_code/commands/auth.py:744 +#: src/iac_code/commands/auth.py:763 msgid "Compatible" msgstr "Compatible" -#: src/iac_code/commands/auth.py:761 -msgid "Select Cloud Provider" -msgstr "Seleccionar proveedor de cloud" - -#: src/iac_code/commands/auth.py:777 +#: src/iac_code/commands/auth.py:796 msgid "Credential" msgstr "Credencial" -#: src/iac_code/commands/auth.py:778 src/iac_code/commands/auth.py:875 -#: src/iac_code/commands/auth.py:980 src/iac_code/ui/renderer.py:418 +#: src/iac_code/commands/auth.py:797 src/iac_code/commands/auth.py:894 +#: src/iac_code/commands/auth.py:999 src/iac_code/ui/renderer.py:418 msgid "Region" msgstr "Región" -#: src/iac_code/commands/auth.py:780 +#: src/iac_code/commands/auth.py:799 msgid "Configure Alibaba Cloud" msgstr "Configurar Alibaba Cloud" -#: src/iac_code/commands/auth.py:863 +#: src/iac_code/commands/auth.py:882 msgid "Current configuration" msgstr "Configuración actual" -#: src/iac_code/commands/auth.py:865 +#: src/iac_code/commands/auth.py:884 msgid "Mode" msgstr "Modo" -#: src/iac_code/commands/auth.py:872 +#: src/iac_code/commands/auth.py:891 msgid "(not set)" msgstr "(sin definir)" -#: src/iac_code/commands/auth.py:889 +#: src/iac_code/commands/auth.py:908 msgid "Configure Alibaba Cloud credentials" msgstr "Configurar credenciales de Alibaba Cloud" -#: src/iac_code/commands/auth.py:902 +#: src/iac_code/commands/auth.py:921 msgid "Reconfigure credential" msgstr "Reconfigurar la credencial" -#: src/iac_code/commands/auth.py:915 +#: src/iac_code/commands/auth.py:934 msgid "Select credential type" msgstr "Seleccionar tipo de credencial" -#: src/iac_code/commands/auth.py:965 +#: src/iac_code/commands/auth.py:984 msgid "Configured: Alibaba Cloud credentials saved to ~/.iac-code" msgstr "Configurado: credenciales de Alibaba Cloud guardadas en ~/.iac-code" -#: src/iac_code/commands/auth.py:972 +#: src/iac_code/commands/auth.py:991 msgid "Configure Alibaba Cloud region" msgstr "Configurar la región de Alibaba Cloud" -#: src/iac_code/commands/auth.py:998 +#: src/iac_code/commands/auth.py:1017 msgid "Configured: Alibaba Cloud region saved to ~/.iac-code" msgstr "Configurado: región de Alibaba Cloud guardada en ~/.iac-code" @@ -782,8 +792,8 @@ msgstr "El comando debug requiere un contexto." msgid "No active session." msgstr "No hay ninguna sesión activa." -#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:82 -#: src/iac_code/commands/model.py:86 +#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:88 +#: src/iac_code/commands/model.py:92 msgid "No configured providers. Run /auth first." msgstr "No hay proveedores configurados. Ejecute /auth primero." @@ -845,17 +855,26 @@ msgstr "Mostrar sugerencias de comandos" msgid "Exit" msgstr "Salir" -#: src/iac_code/commands/model.py:75 src/iac_code/commands/model.py:130 +#: src/iac_code/commands/model.py:49 +#, python-brace-format +msgid "" +"Model selection is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "" +"La selección de modelo está bloqueada por '{source}'. Para cambiar, " +"modifique llm_source en settings.yml." + +#: src/iac_code/commands/model.py:81 src/iac_code/commands/model.py:136 #, python-brace-format msgid "Model switched to: {model}" msgstr "Modelo cambiado a: {model}" -#: src/iac_code/commands/model.py:79 +#: src/iac_code/commands/model.py:85 #, python-brace-format msgid "Current model: {model}" msgstr "Modelo actual: {model}" -#: src/iac_code/commands/model.py:105 +#: src/iac_code/commands/model.py:111 #, python-brace-format msgid "Kept model as {model}" msgstr "Se mantiene el modelo como {model}" @@ -963,79 +982,79 @@ msgstr "" " correcta (actual: {base_url}). Muchos endpoints compatibles con OpenAI " "requieren el sufijo /v1 (p. ej., {base_url}/v1)." -#: src/iac_code/providers/registry.py:406 +#: src/iac_code/providers/registry.py:411 msgid "Alibaba Cloud Bailian" msgstr "Alibaba Cloud Bailian" -#: src/iac_code/providers/registry.py:407 +#: src/iac_code/providers/registry.py:412 msgid "Alibaba Cloud Bailian Token Plan" msgstr "Alibaba Cloud Bailian Token Plan" -#: src/iac_code/providers/registry.py:411 +#: src/iac_code/providers/registry.py:416 msgid "OpenAPI Compatible" msgstr "Compatible con OpenAPI" -#: src/iac_code/providers/registry.py:413 +#: src/iac_code/providers/registry.py:418 msgid "Kimi (China)" msgstr "Kimi (China)" -#: src/iac_code/providers/registry.py:414 +#: src/iac_code/providers/registry.py:419 msgid "Kimi (International)" msgstr "Kimi (Internacional)" -#: src/iac_code/providers/registry.py:415 +#: src/iac_code/providers/registry.py:420 msgid "MiniMax (China)" msgstr "MiniMax (China)" -#: src/iac_code/providers/registry.py:416 +#: src/iac_code/providers/registry.py:421 msgid "MiniMax (International)" msgstr "MiniMax (Internacional)" -#: src/iac_code/providers/registry.py:418 +#: src/iac_code/providers/registry.py:423 msgid "ZhiPu AI (International)" msgstr "ZhiPu AI (Internacional)" -#: src/iac_code/providers/registry.py:420 +#: src/iac_code/providers/registry.py:425 msgid "SiliconFlow (China)" msgstr "SiliconFlow (China)" -#: src/iac_code/providers/registry.py:421 +#: src/iac_code/providers/registry.py:426 msgid "SiliconFlow (International)" msgstr "SiliconFlow (Internacional)" -#: src/iac_code/providers/registry.py:422 +#: src/iac_code/providers/registry.py:427 msgid "Ollama (Local)" msgstr "Ollama (Local)" -#: src/iac_code/providers/registry.py:423 +#: src/iac_code/providers/registry.py:428 msgid "LM Studio (Local)" msgstr "LM Studio (Local)" -#: src/iac_code/providers/registry.py:426 +#: src/iac_code/providers/registry.py:431 msgid "ModelScope" msgstr "ModelScope" -#: src/iac_code/providers/registry.py:427 +#: src/iac_code/providers/registry.py:432 msgid "Alibaba Cloud CodingPlan" msgstr "Alibaba Cloud CodingPlan" -#: src/iac_code/providers/registry.py:428 +#: src/iac_code/providers/registry.py:433 msgid "Alibaba Cloud CodingPlan (International)" msgstr "Alibaba Cloud CodingPlan (Internacional)" -#: src/iac_code/providers/registry.py:429 +#: src/iac_code/providers/registry.py:434 msgid "ZhiPu AI CodingPlan" msgstr "ZhiPu AI CodingPlan" -#: src/iac_code/providers/registry.py:430 +#: src/iac_code/providers/registry.py:435 msgid "ZhiPu AI CodingPlan (International)" msgstr "ZhiPu AI CodingPlan (Internacional)" -#: src/iac_code/providers/registry.py:431 +#: src/iac_code/providers/registry.py:436 msgid "Volcengine CodingPlan" msgstr "Volcengine CodingPlan" -#: src/iac_code/providers/registry.py:432 +#: src/iac_code/providers/registry.py:437 msgid "Anthropic Compatible" msgstr "Compatible con Anthropic" @@ -1055,7 +1074,7 @@ msgstr "" " QwenPaw (elimine 'llm_source: qwenpaw' de settings.yml)." #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:185 src/iac_code/tools/bash/bash_tool.py:154 +#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:158 #, python-brace-format msgid "Allow {}?" msgstr "¿Permitir {}?" @@ -1257,11 +1276,11 @@ msgstr "Ejecutando {cmd}" msgid "Running command..." msgstr "Ejecutando comando..." -#: src/iac_code/tools/bash/command_parser.py:41 +#: src/iac_code/tools/bash/command_parser.py:42 msgid "parse error" msgstr "Error de análisis" -#: src/iac_code/tools/bash/command_parser.py:44 +#: src/iac_code/tools/bash/command_parser.py:46 msgid "unsupported shell construct" msgstr "Construcción de shell no soportada" @@ -1270,53 +1289,57 @@ msgstr "Construcción de shell no soportada" msgid "path outside allowed directories: {}" msgstr "Ruta fuera de los directorios permitidos: {}" -#: src/iac_code/tools/bash/permissions.py:101 +#: src/iac_code/tools/bash/permissions.py:135 #, python-brace-format msgid "matched deny rule(s): {}" msgstr "Regla(s) de denegación coincidente(s): {}" -#: src/iac_code/tools/bash/permissions.py:108 +#: src/iac_code/tools/bash/permissions.py:142 #, python-brace-format msgid "matched ask rule(s): {}" msgstr "Regla(s) de consulta coincidente(s): {}" -#: src/iac_code/tools/bash/permissions.py:120 -#: src/iac_code/tools/bash/permissions.py:178 +#: src/iac_code/tools/bash/permissions.py:154 +#: src/iac_code/tools/bash/permissions.py:220 #, python-brace-format msgid "matched allow rule(s): {}" msgstr "Regla(s) de permiso coincidente(s): {}" -#: src/iac_code/tools/bash/permissions.py:129 +#: src/iac_code/tools/bash/permissions.py:162 +msgid "complex command requires confirmation" +msgstr "El comando complejo requiere confirmación" + +#: src/iac_code/tools/bash/permissions.py:171 msgid "sed in-place edit requires confirmation" msgstr "La edición sed in situ requiere confirmación" -#: src/iac_code/tools/bash/permissions.py:152 +#: src/iac_code/tools/bash/permissions.py:194 msgid "command failed basic safety checks" msgstr "El comando no pasó las comprobaciones básicas de seguridad" -#: src/iac_code/tools/bash/permissions.py:168 +#: src/iac_code/tools/bash/permissions.py:210 #, python-brace-format msgid "matched deny rule(s) on full command: {}" msgstr "Regla(s) de denegación del comando completo: {}" -#: src/iac_code/tools/bash/permissions.py:185 +#: src/iac_code/tools/bash/permissions.py:227 msgid "command too complex to analyze" msgstr "Comando demasiado complejo para analizar" -#: src/iac_code/tools/bash/permissions.py:187 +#: src/iac_code/tools/bash/permissions.py:229 msgid "could not parse command" msgstr "No se pudo analizar el comando" -#: src/iac_code/tools/bash/permissions.py:203 +#: src/iac_code/tools/bash/permissions.py:245 #, python-brace-format msgid "too many subcommands (>{})" msgstr "Demasiados subcomandos (>{})" -#: src/iac_code/tools/bash/permissions.py:215 +#: src/iac_code/tools/bash/permissions.py:258 msgid "multiple cd commands in compound command" msgstr "Múltiples comandos cd en comando compuesto" -#: src/iac_code/tools/bash/permissions.py:227 +#: src/iac_code/tools/bash/permissions.py:271 msgid "cd combined with git in compound command" msgstr "cd combinado con git en comando compuesto" @@ -1588,13 +1611,13 @@ msgid "Log file" msgstr "Archivo de registro" #: src/iac_code/ui/renderer.py:351 src/iac_code/ui/renderer.py:621 -#: src/iac_code/ui/renderer.py:1380 +#: src/iac_code/ui/renderer.py:1402 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "Razonamiento durante {seconds:.1f} s" #: src/iac_code/ui/renderer.py:367 src/iac_code/ui/renderer.py:653 -#: src/iac_code/ui/renderer.py:1401 +#: src/iac_code/ui/renderer.py:1423 msgid "(ctrl+o to expand)" msgstr "(ctrl+o para expandir)" @@ -1654,24 +1677,29 @@ msgstr "¿Permitir esta acción?" msgid "Yes, allow once" msgstr "Sí, permitir una vez" -#: src/iac_code/ui/renderer.py:1304 +#: src/iac_code/ui/renderer.py:1306 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "Sí, permitir siempre \"{rule}\" (esta sesión)" -#: src/iac_code/ui/renderer.py:1309 +#: src/iac_code/ui/renderer.py:1311 msgid "Yes, allow always for this tool" msgstr "Sí, permitir siempre esta herramienta" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "No, reject once" msgstr "No, rechazar una vez" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "default" msgstr "predeterminado" -#: src/iac_code/ui/renderer.py:1314 +#: src/iac_code/ui/renderer.py:1321 +#, python-brace-format +msgid "No, always deny \"{rule}\" (this session)" +msgstr "No, siempre denegar \"{rule}\" (esta sesión)" + +#: src/iac_code/ui/renderer.py:1326 msgid "No, always reject this tool" msgstr "No, rechazar siempre esta herramienta" diff --git a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po index 8da9c6eb..498382b3 100644 --- a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-18 12:25+0800\n" +"POT-Creation-Date: 2026-05-18 16:59+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: fr\n" @@ -540,215 +540,225 @@ msgstr "Reprendre une session précédente" msgid "[conversation id or search term]" msgstr "[identifiant de conversation ou terme de recherche]" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:836 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "Naviguer" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 -#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 -#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 -#: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:368 +#: src/iac_code/commands/auth.py:402 src/iac_code/commands/auth.py:409 +#: src/iac_code/commands/auth.py:433 src/iac_code/commands/auth.py:836 +#: src/iac_code/commands/auth.py:1026 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "Confirmer" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 -#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 -#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 -#: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 -#: src/iac_code/commands/auth.py:1007 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:366 +#: src/iac_code/commands/auth.py:368 src/iac_code/commands/auth.py:402 +#: src/iac_code/commands/auth.py:409 src/iac_code/commands/auth.py:433 +#: src/iac_code/commands/auth.py:836 src/iac_code/commands/auth.py:921 +#: src/iac_code/commands/auth.py:1026 msgid "Back" msgstr "Retour" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Keep" msgstr "Conserver" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Re-enter" msgstr "Saisir à nouveau" -#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 -#: src/iac_code/commands/auth.py:651 +#: src/iac_code/commands/auth.py:508 src/iac_code/commands/auth.py:650 +#: src/iac_code/commands/auth.py:670 msgid " (current)" msgstr " (actuel)" -#: src/iac_code/commands/auth.py:510 +#: src/iac_code/commands/auth.py:511 msgid "Custom model..." msgstr "Modèle personnalisé…" -#: src/iac_code/commands/auth.py:513 +#: src/iac_code/commands/auth.py:514 #, python-brace-format msgid "Select model for {provider}" msgstr "Sélectionner le modèle pour {provider}" -#: src/iac_code/commands/auth.py:515 +#: src/iac_code/commands/auth.py:516 msgid "Select model" msgstr "Sélectionner le modèle" -#: src/iac_code/commands/auth.py:523 +#: src/iac_code/commands/auth.py:524 msgid "Enter custom model name: " msgstr "Saisir le nom du modèle personnalisé : " -#: src/iac_code/commands/auth.py:549 +#: src/iac_code/commands/auth.py:550 msgid "Error: console not available" msgstr "Erreur : console indisponible" #: src/iac_code/commands/auth.py:576 +#, python-brace-format +msgid "" +"LLM provider is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "" +"Le fournisseur LLM est verrouillé par '{source}'. Pour changer, modifiez " +"llm_source dans settings.yml." + +#: src/iac_code/commands/auth.py:580 src/iac_code/commands/auth.py:780 +msgid "Select Cloud Provider" +msgstr "Sélectionner le fournisseur cloud" + +#: src/iac_code/commands/auth.py:582 src/iac_code/commands/auth.py:589 +#: src/iac_code/commands/auth.py:600 src/iac_code/commands/auth.py:695 +#: src/iac_code/commands/auth.py:711 src/iac_code/commands/auth.py:789 +#: src/iac_code/commands/auth.py:962 src/iac_code/commands/auth.py:1003 +msgid "Auth cancelled" +msgstr "Authentification annulée" + +#: src/iac_code/commands/auth.py:595 msgid "Configure LLM Provider" msgstr "Configurer le fournisseur LLM" -#: src/iac_code/commands/auth.py:577 +#: src/iac_code/commands/auth.py:596 msgid "Configure IaC Cloud Service" msgstr "Configurer le service cloud IaC" -#: src/iac_code/commands/auth.py:579 +#: src/iac_code/commands/auth.py:598 msgid "Select configuration type" msgstr "Sélectionner le type de configuration" -#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 -#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 -#: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 -msgid "Auth cancelled" -msgstr "Authentification annulée" - -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:654 msgid "Select provider" msgstr "Sélectionner le fournisseur" -#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#: src/iac_code/commands/auth.py:675 src/iac_code/commands/auth.py:764 #, python-brace-format msgid "Select provider — {group}" msgstr "Sélectionner le fournisseur — {group}" -#: src/iac_code/commands/auth.py:669 +#: src/iac_code/commands/auth.py:688 #, python-brace-format msgid "Configure {provider}" msgstr "Configurer {provider}" -#: src/iac_code/commands/auth.py:685 +#: src/iac_code/commands/auth.py:704 #, python-brace-format msgid "Enter API key for {provider}" msgstr "Saisir la clé API pour {provider}" -#: src/iac_code/commands/auth.py:723 +#: src/iac_code/commands/auth.py:742 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status} : {provider} / {model}" -#: src/iac_code/commands/auth.py:724 +#: src/iac_code/commands/auth.py:743 msgid "Configured" msgstr "Configuré" -#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:750 src/iac_code/commands/auth.py:771 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" -#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +#: src/iac_code/commands/auth.py:751 src/iac_code/providers/registry.py:422 msgid "ZhiPu AI" msgstr "ZhiPu AI" -#: src/iac_code/commands/auth.py:733 +#: src/iac_code/commands/auth.py:752 msgid "Kimi" msgstr "Kimi" -#: src/iac_code/commands/auth.py:734 +#: src/iac_code/commands/auth.py:753 msgid "MiniMax" msgstr "MiniMax" -#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +#: src/iac_code/commands/auth.py:754 src/iac_code/providers/registry.py:424 msgid "Volcengine" msgstr "Volcengine" -#: src/iac_code/commands/auth.py:736 +#: src/iac_code/commands/auth.py:755 msgid "SiliconFlow" msgstr "SiliconFlow" -#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +#: src/iac_code/commands/auth.py:756 src/iac_code/providers/registry.py:415 msgid "DeepSeek" msgstr "DeepSeek" -#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +#: src/iac_code/commands/auth.py:757 src/iac_code/providers/registry.py:413 msgid "OpenAI" msgstr "OpenAI" -#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +#: src/iac_code/commands/auth.py:758 src/iac_code/providers/registry.py:414 msgid "Anthropic" msgstr "Anthropic" -#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +#: src/iac_code/commands/auth.py:759 src/iac_code/providers/registry.py:417 msgid "Google Gemini" msgstr "Google Gemini" -#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +#: src/iac_code/commands/auth.py:760 src/iac_code/providers/registry.py:430 msgid "Azure OpenAI" msgstr "Azure OpenAI" -#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +#: src/iac_code/commands/auth.py:761 src/iac_code/providers/registry.py:429 msgid "OpenRouter" msgstr "OpenRouter" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:762 msgid "Local" msgstr "Local" -#: src/iac_code/commands/auth.py:744 +#: src/iac_code/commands/auth.py:763 msgid "Compatible" msgstr "Compatible" -#: src/iac_code/commands/auth.py:761 -msgid "Select Cloud Provider" -msgstr "Sélectionner le fournisseur cloud" - -#: src/iac_code/commands/auth.py:777 +#: src/iac_code/commands/auth.py:796 msgid "Credential" msgstr "Identifiants" -#: src/iac_code/commands/auth.py:778 src/iac_code/commands/auth.py:875 -#: src/iac_code/commands/auth.py:980 src/iac_code/ui/renderer.py:418 +#: src/iac_code/commands/auth.py:797 src/iac_code/commands/auth.py:894 +#: src/iac_code/commands/auth.py:999 src/iac_code/ui/renderer.py:418 msgid "Region" msgstr "Région" -#: src/iac_code/commands/auth.py:780 +#: src/iac_code/commands/auth.py:799 msgid "Configure Alibaba Cloud" msgstr "Configurer Alibaba Cloud" -#: src/iac_code/commands/auth.py:863 +#: src/iac_code/commands/auth.py:882 msgid "Current configuration" msgstr "Configuration actuelle" -#: src/iac_code/commands/auth.py:865 +#: src/iac_code/commands/auth.py:884 msgid "Mode" msgstr "Mode" -#: src/iac_code/commands/auth.py:872 +#: src/iac_code/commands/auth.py:891 msgid "(not set)" msgstr "(non défini)" -#: src/iac_code/commands/auth.py:889 +#: src/iac_code/commands/auth.py:908 msgid "Configure Alibaba Cloud credentials" msgstr "Configurer les identifiants Alibaba Cloud" -#: src/iac_code/commands/auth.py:902 +#: src/iac_code/commands/auth.py:921 msgid "Reconfigure credential" msgstr "Reconfigurer les identifiants" -#: src/iac_code/commands/auth.py:915 +#: src/iac_code/commands/auth.py:934 msgid "Select credential type" msgstr "Sélectionner le type d’identifiants" -#: src/iac_code/commands/auth.py:965 +#: src/iac_code/commands/auth.py:984 msgid "Configured: Alibaba Cloud credentials saved to ~/.iac-code" msgstr "" "Configured: Alibaba Cloud credentials saved to ~/.iac-codeConfigured: " "Alibaba Cloud credentials saved to ~/.iac-codeConfiguration effectuée : " "identifiants Alibaba Cloud enregistrés dans ~/.iac-code" -#: src/iac_code/commands/auth.py:972 +#: src/iac_code/commands/auth.py:991 msgid "Configure Alibaba Cloud region" msgstr "Configurer la région Alibaba Cloud" -#: src/iac_code/commands/auth.py:998 +#: src/iac_code/commands/auth.py:1017 msgid "Configured: Alibaba Cloud region saved to ~/.iac-code" msgstr "" "Configured: Alibaba Cloud region saved to ~/.iac-codeConfigured: Alibaba " @@ -784,8 +794,8 @@ msgstr "La commande debug nécessite un contexte." msgid "No active session." msgstr "Aucune session active." -#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:82 -#: src/iac_code/commands/model.py:86 +#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:88 +#: src/iac_code/commands/model.py:92 msgid "No configured providers. Run /auth first." msgstr "Aucun fournisseur configuré. Exécutez d’abord /auth." @@ -847,17 +857,26 @@ msgstr "Afficher les suggestions de commandes" msgid "Exit" msgstr "Quitter" -#: src/iac_code/commands/model.py:75 src/iac_code/commands/model.py:130 +#: src/iac_code/commands/model.py:49 +#, python-brace-format +msgid "" +"Model selection is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "" +"La sélection du modèle est verrouillée par '{source}'. Pour changer, " +"modifiez llm_source dans settings.yml." + +#: src/iac_code/commands/model.py:81 src/iac_code/commands/model.py:136 #, python-brace-format msgid "Model switched to: {model}" msgstr "Modèle défini sur : {model}" -#: src/iac_code/commands/model.py:79 +#: src/iac_code/commands/model.py:85 #, python-brace-format msgid "Current model: {model}" msgstr "Modèle actuel : {model}" -#: src/iac_code/commands/model.py:105 +#: src/iac_code/commands/model.py:111 #, python-brace-format msgid "Kept model as {model}" msgstr "Modèle conservé : {model}" @@ -963,79 +982,79 @@ msgstr "" " correcte (actuelle : {base_url}). De nombreux points de terminaison " "compatibles OpenAI exigent le suffixe /v1 (p. ex. {base_url}/v1)." -#: src/iac_code/providers/registry.py:406 +#: src/iac_code/providers/registry.py:411 msgid "Alibaba Cloud Bailian" msgstr "Alibaba Cloud Bailian" -#: src/iac_code/providers/registry.py:407 +#: src/iac_code/providers/registry.py:412 msgid "Alibaba Cloud Bailian Token Plan" msgstr "Alibaba Cloud Bailian Token Plan" -#: src/iac_code/providers/registry.py:411 +#: src/iac_code/providers/registry.py:416 msgid "OpenAPI Compatible" msgstr "Compatible OpenAPI" -#: src/iac_code/providers/registry.py:413 +#: src/iac_code/providers/registry.py:418 msgid "Kimi (China)" msgstr "Kimi (Chine)" -#: src/iac_code/providers/registry.py:414 +#: src/iac_code/providers/registry.py:419 msgid "Kimi (International)" msgstr "Kimi (International)" -#: src/iac_code/providers/registry.py:415 +#: src/iac_code/providers/registry.py:420 msgid "MiniMax (China)" msgstr "MiniMax (Chine)" -#: src/iac_code/providers/registry.py:416 +#: src/iac_code/providers/registry.py:421 msgid "MiniMax (International)" msgstr "MiniMax (International)" -#: src/iac_code/providers/registry.py:418 +#: src/iac_code/providers/registry.py:423 msgid "ZhiPu AI (International)" msgstr "ZhiPu AI (International)" -#: src/iac_code/providers/registry.py:420 +#: src/iac_code/providers/registry.py:425 msgid "SiliconFlow (China)" msgstr "SiliconFlow (Chine)" -#: src/iac_code/providers/registry.py:421 +#: src/iac_code/providers/registry.py:426 msgid "SiliconFlow (International)" msgstr "SiliconFlow (International)" -#: src/iac_code/providers/registry.py:422 +#: src/iac_code/providers/registry.py:427 msgid "Ollama (Local)" msgstr "Ollama (Local)" -#: src/iac_code/providers/registry.py:423 +#: src/iac_code/providers/registry.py:428 msgid "LM Studio (Local)" msgstr "LM Studio (Local)" -#: src/iac_code/providers/registry.py:426 +#: src/iac_code/providers/registry.py:431 msgid "ModelScope" msgstr "ModelScope" -#: src/iac_code/providers/registry.py:427 +#: src/iac_code/providers/registry.py:432 msgid "Alibaba Cloud CodingPlan" msgstr "Alibaba Cloud CodingPlan" -#: src/iac_code/providers/registry.py:428 +#: src/iac_code/providers/registry.py:433 msgid "Alibaba Cloud CodingPlan (International)" msgstr "Alibaba Cloud CodingPlan (International)" -#: src/iac_code/providers/registry.py:429 +#: src/iac_code/providers/registry.py:434 msgid "ZhiPu AI CodingPlan" msgstr "ZhiPu AI CodingPlan" -#: src/iac_code/providers/registry.py:430 +#: src/iac_code/providers/registry.py:435 msgid "ZhiPu AI CodingPlan (International)" msgstr "ZhiPu AI CodingPlan (International)" -#: src/iac_code/providers/registry.py:431 +#: src/iac_code/providers/registry.py:436 msgid "Volcengine CodingPlan" msgstr "Volcengine CodingPlan" -#: src/iac_code/providers/registry.py:432 +#: src/iac_code/providers/registry.py:437 msgid "Anthropic Compatible" msgstr "Compatible Anthropic" @@ -1056,7 +1075,7 @@ msgstr "" "settings.yml)." #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:185 src/iac_code/tools/bash/bash_tool.py:154 +#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:158 #, python-brace-format msgid "Allow {}?" msgstr "Autoriser {} ?" @@ -1258,11 +1277,11 @@ msgstr "Exécution de {cmd}" msgid "Running command..." msgstr "Exécution de la commande…" -#: src/iac_code/tools/bash/command_parser.py:41 +#: src/iac_code/tools/bash/command_parser.py:42 msgid "parse error" msgstr "Erreur d'analyse" -#: src/iac_code/tools/bash/command_parser.py:44 +#: src/iac_code/tools/bash/command_parser.py:46 msgid "unsupported shell construct" msgstr "Construction shell non prise en charge" @@ -1271,53 +1290,57 @@ msgstr "Construction shell non prise en charge" msgid "path outside allowed directories: {}" msgstr "Chemin en dehors des répertoires autorisés : {}" -#: src/iac_code/tools/bash/permissions.py:101 +#: src/iac_code/tools/bash/permissions.py:135 #, python-brace-format msgid "matched deny rule(s): {}" msgstr "Règle(s) de refus correspondante(s) : {}" -#: src/iac_code/tools/bash/permissions.py:108 +#: src/iac_code/tools/bash/permissions.py:142 #, python-brace-format msgid "matched ask rule(s): {}" msgstr "Règle(s) de demande correspondante(s) : {}" -#: src/iac_code/tools/bash/permissions.py:120 -#: src/iac_code/tools/bash/permissions.py:178 +#: src/iac_code/tools/bash/permissions.py:154 +#: src/iac_code/tools/bash/permissions.py:220 #, python-brace-format msgid "matched allow rule(s): {}" msgstr "Règle(s) d'autorisation correspondante(s) : {}" -#: src/iac_code/tools/bash/permissions.py:129 +#: src/iac_code/tools/bash/permissions.py:162 +msgid "complex command requires confirmation" +msgstr "La commande complexe nécessite une confirmation" + +#: src/iac_code/tools/bash/permissions.py:171 msgid "sed in-place edit requires confirmation" msgstr "L'édition sed sur place nécessite une confirmation" -#: src/iac_code/tools/bash/permissions.py:152 +#: src/iac_code/tools/bash/permissions.py:194 msgid "command failed basic safety checks" msgstr "La commande n'a pas passé les vérifications de sécurité de base" -#: src/iac_code/tools/bash/permissions.py:168 +#: src/iac_code/tools/bash/permissions.py:210 #, python-brace-format msgid "matched deny rule(s) on full command: {}" msgstr "Règle(s) de refus pour la commande complète : {}" -#: src/iac_code/tools/bash/permissions.py:185 +#: src/iac_code/tools/bash/permissions.py:227 msgid "command too complex to analyze" msgstr "Commande trop complexe pour être analysée" -#: src/iac_code/tools/bash/permissions.py:187 +#: src/iac_code/tools/bash/permissions.py:229 msgid "could not parse command" msgstr "Impossible d'analyser la commande" -#: src/iac_code/tools/bash/permissions.py:203 +#: src/iac_code/tools/bash/permissions.py:245 #, python-brace-format msgid "too many subcommands (>{})" msgstr "Trop de sous-commandes (>{})" -#: src/iac_code/tools/bash/permissions.py:215 +#: src/iac_code/tools/bash/permissions.py:258 msgid "multiple cd commands in compound command" msgstr "Plusieurs commandes cd dans une commande composée" -#: src/iac_code/tools/bash/permissions.py:227 +#: src/iac_code/tools/bash/permissions.py:271 msgid "cd combined with git in compound command" msgstr "cd combiné avec git dans une commande composée" @@ -1588,13 +1611,13 @@ msgid "Log file" msgstr "Fichier journal" #: src/iac_code/ui/renderer.py:351 src/iac_code/ui/renderer.py:621 -#: src/iac_code/ui/renderer.py:1380 +#: src/iac_code/ui/renderer.py:1402 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "Réflexion pendant {seconds:.1f}s" #: src/iac_code/ui/renderer.py:367 src/iac_code/ui/renderer.py:653 -#: src/iac_code/ui/renderer.py:1401 +#: src/iac_code/ui/renderer.py:1423 msgid "(ctrl+o to expand)" msgstr "(ctrl+o pour développer)" @@ -1656,24 +1679,29 @@ msgstr "Autoriser cette action ?" msgid "Yes, allow once" msgstr "Oui, autoriser une fois" -#: src/iac_code/ui/renderer.py:1304 +#: src/iac_code/ui/renderer.py:1306 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "Oui, toujours autoriser \"{rule}\" (cette session)" -#: src/iac_code/ui/renderer.py:1309 +#: src/iac_code/ui/renderer.py:1311 msgid "Yes, allow always for this tool" msgstr "Oui, toujours autoriser pour cet outil" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "No, reject once" msgstr "Non, refuser une fois" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "default" msgstr "par défaut" -#: src/iac_code/ui/renderer.py:1314 +#: src/iac_code/ui/renderer.py:1321 +#, python-brace-format +msgid "No, always deny \"{rule}\" (this session)" +msgstr "Non, toujours refuser \"{rule}\" (cette session)" + +#: src/iac_code/ui/renderer.py:1326 msgid "No, always reject this tool" msgstr "Non, toujours refuser cet outil" diff --git a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po index e4908291..cb830a18 100644 --- a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-18 12:25+0800\n" +"POT-Creation-Date: 2026-05-18 16:59+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: ja\n" @@ -523,214 +523,224 @@ msgstr "以前のセッションを再開します" msgid "[conversation id or search term]" msgstr "[会話 ID または検索語]" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:836 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "移動" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 -#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 -#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 -#: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:368 +#: src/iac_code/commands/auth.py:402 src/iac_code/commands/auth.py:409 +#: src/iac_code/commands/auth.py:433 src/iac_code/commands/auth.py:836 +#: src/iac_code/commands/auth.py:1026 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "確認" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 -#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 -#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 -#: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 -#: src/iac_code/commands/auth.py:1007 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:366 +#: src/iac_code/commands/auth.py:368 src/iac_code/commands/auth.py:402 +#: src/iac_code/commands/auth.py:409 src/iac_code/commands/auth.py:433 +#: src/iac_code/commands/auth.py:836 src/iac_code/commands/auth.py:921 +#: src/iac_code/commands/auth.py:1026 msgid "Back" msgstr "戻る" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Keep" msgstr "維持" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Re-enter" msgstr "再入力" -#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 -#: src/iac_code/commands/auth.py:651 +#: src/iac_code/commands/auth.py:508 src/iac_code/commands/auth.py:650 +#: src/iac_code/commands/auth.py:670 msgid " (current)" msgstr " (現在)" -#: src/iac_code/commands/auth.py:510 +#: src/iac_code/commands/auth.py:511 msgid "Custom model..." msgstr "カスタムモデル…" -#: src/iac_code/commands/auth.py:513 +#: src/iac_code/commands/auth.py:514 #, python-brace-format msgid "Select model for {provider}" msgstr "{provider} のモデルを選択してください" -#: src/iac_code/commands/auth.py:515 +#: src/iac_code/commands/auth.py:516 msgid "Select model" msgstr "モデルを選択" -#: src/iac_code/commands/auth.py:523 +#: src/iac_code/commands/auth.py:524 msgid "Enter custom model name: " msgstr "カスタムモデル名を入力してください:" -#: src/iac_code/commands/auth.py:549 +#: src/iac_code/commands/auth.py:550 msgid "Error: console not available" msgstr "エラー:コンソールを使用できません" #: src/iac_code/commands/auth.py:576 +#, python-brace-format +msgid "" +"LLM provider is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "" +"LLM プロバイダーは '{source}' によりロックされています。変更するには settings.yml の llm_source " +"を修正してください。" + +#: src/iac_code/commands/auth.py:580 src/iac_code/commands/auth.py:780 +msgid "Select Cloud Provider" +msgstr "クラウドプロバイダーを選択" + +#: src/iac_code/commands/auth.py:582 src/iac_code/commands/auth.py:589 +#: src/iac_code/commands/auth.py:600 src/iac_code/commands/auth.py:695 +#: src/iac_code/commands/auth.py:711 src/iac_code/commands/auth.py:789 +#: src/iac_code/commands/auth.py:962 src/iac_code/commands/auth.py:1003 +msgid "Auth cancelled" +msgstr "認証をキャンセルしました" + +#: src/iac_code/commands/auth.py:595 msgid "Configure LLM Provider" msgstr "LLM プロバイダーを設定" -#: src/iac_code/commands/auth.py:577 +#: src/iac_code/commands/auth.py:596 msgid "Configure IaC Cloud Service" msgstr "IaC クラウドサービスを設定" -#: src/iac_code/commands/auth.py:579 +#: src/iac_code/commands/auth.py:598 msgid "Select configuration type" msgstr "設定の種類を選択" -#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 -#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 -#: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 -msgid "Auth cancelled" -msgstr "認証をキャンセルしました" - -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:654 msgid "Select provider" msgstr "プロバイダーを選択" -#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#: src/iac_code/commands/auth.py:675 src/iac_code/commands/auth.py:764 #, python-brace-format msgid "Select provider — {group}" msgstr "プロバイダーを選択 — {group}" -#: src/iac_code/commands/auth.py:669 +#: src/iac_code/commands/auth.py:688 #, python-brace-format msgid "Configure {provider}" msgstr "{provider} を設定" -#: src/iac_code/commands/auth.py:685 +#: src/iac_code/commands/auth.py:704 #, python-brace-format msgid "Enter API key for {provider}" msgstr "{provider} の API key を入力してください" -#: src/iac_code/commands/auth.py:723 +#: src/iac_code/commands/auth.py:742 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}:{provider} / {model}" -#: src/iac_code/commands/auth.py:724 +#: src/iac_code/commands/auth.py:743 msgid "Configured" msgstr "設定済み" -#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:750 src/iac_code/commands/auth.py:771 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" -#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +#: src/iac_code/commands/auth.py:751 src/iac_code/providers/registry.py:422 msgid "ZhiPu AI" msgstr "ZhiPu AI" -#: src/iac_code/commands/auth.py:733 +#: src/iac_code/commands/auth.py:752 msgid "Kimi" msgstr "Kimi" -#: src/iac_code/commands/auth.py:734 +#: src/iac_code/commands/auth.py:753 msgid "MiniMax" msgstr "MiniMax" -#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +#: src/iac_code/commands/auth.py:754 src/iac_code/providers/registry.py:424 msgid "Volcengine" msgstr "Volcengine" -#: src/iac_code/commands/auth.py:736 +#: src/iac_code/commands/auth.py:755 msgid "SiliconFlow" msgstr "SiliconFlow" -#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +#: src/iac_code/commands/auth.py:756 src/iac_code/providers/registry.py:415 msgid "DeepSeek" msgstr "DeepSeek" -#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +#: src/iac_code/commands/auth.py:757 src/iac_code/providers/registry.py:413 msgid "OpenAI" msgstr "OpenAI" -#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +#: src/iac_code/commands/auth.py:758 src/iac_code/providers/registry.py:414 msgid "Anthropic" msgstr "Anthropic" -#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +#: src/iac_code/commands/auth.py:759 src/iac_code/providers/registry.py:417 msgid "Google Gemini" msgstr "Google Gemini" -#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +#: src/iac_code/commands/auth.py:760 src/iac_code/providers/registry.py:430 msgid "Azure OpenAI" msgstr "Azure OpenAI" -#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +#: src/iac_code/commands/auth.py:761 src/iac_code/providers/registry.py:429 msgid "OpenRouter" msgstr "OpenRouter" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:762 msgid "Local" msgstr "ローカル" -#: src/iac_code/commands/auth.py:744 +#: src/iac_code/commands/auth.py:763 msgid "Compatible" msgstr "互換モード" -#: src/iac_code/commands/auth.py:761 -msgid "Select Cloud Provider" -msgstr "クラウドプロバイダーを選択" - -#: src/iac_code/commands/auth.py:777 +#: src/iac_code/commands/auth.py:796 msgid "Credential" msgstr "クレデンシャル" -#: src/iac_code/commands/auth.py:778 src/iac_code/commands/auth.py:875 -#: src/iac_code/commands/auth.py:980 src/iac_code/ui/renderer.py:418 +#: src/iac_code/commands/auth.py:797 src/iac_code/commands/auth.py:894 +#: src/iac_code/commands/auth.py:999 src/iac_code/ui/renderer.py:418 msgid "Region" msgstr "リージョン" -#: src/iac_code/commands/auth.py:780 +#: src/iac_code/commands/auth.py:799 msgid "Configure Alibaba Cloud" msgstr "Alibaba Cloud を設定" -#: src/iac_code/commands/auth.py:863 +#: src/iac_code/commands/auth.py:882 msgid "Current configuration" msgstr "現在の設定" -#: src/iac_code/commands/auth.py:865 +#: src/iac_code/commands/auth.py:884 msgid "Mode" msgstr "モード" -#: src/iac_code/commands/auth.py:872 +#: src/iac_code/commands/auth.py:891 msgid "(not set)" msgstr "(未設定)" -#: src/iac_code/commands/auth.py:889 +#: src/iac_code/commands/auth.py:908 msgid "Configure Alibaba Cloud credentials" msgstr "Alibaba Cloud のクレデンシャルを設定" -#: src/iac_code/commands/auth.py:902 +#: src/iac_code/commands/auth.py:921 msgid "Reconfigure credential" msgstr "クレデンシャルを再設定" -#: src/iac_code/commands/auth.py:915 +#: src/iac_code/commands/auth.py:934 msgid "Select credential type" msgstr "クレデンシャルの種類を選択" -#: src/iac_code/commands/auth.py:965 +#: src/iac_code/commands/auth.py:984 msgid "Configured: Alibaba Cloud credentials saved to ~/.iac-code" msgstr "" "Configured: Alibaba Cloud credentials saved to ~/.iac-code設定しました:Alibaba " "Cloud のクレデンシャルを ~/.iac-code に保存しました" -#: src/iac_code/commands/auth.py:972 +#: src/iac_code/commands/auth.py:991 msgid "Configure Alibaba Cloud region" msgstr "Alibaba Cloud のリージョンを設定" -#: src/iac_code/commands/auth.py:998 +#: src/iac_code/commands/auth.py:1017 msgid "Configured: Alibaba Cloud region saved to ~/.iac-code" msgstr "設定しました:Alibaba Cloud のリージョンを ~/.iac-code に保存しました" @@ -763,8 +773,8 @@ msgstr "debug コマンドにはコンテキストが必要です。" msgid "No active session." msgstr "アクティブなセッションがありません。" -#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:82 -#: src/iac_code/commands/model.py:86 +#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:88 +#: src/iac_code/commands/model.py:92 msgid "No configured providers. Run /auth first." msgstr "設定済みのプロバイダーがありません。先に /auth を実行してください。" @@ -826,17 +836,24 @@ msgstr "コマンド候補を表示" msgid "Exit" msgstr "終了" -#: src/iac_code/commands/model.py:75 src/iac_code/commands/model.py:130 +#: src/iac_code/commands/model.py:49 +#, python-brace-format +msgid "" +"Model selection is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "モデル選択は '{source}' によりロックされています。変更するには settings.yml の llm_source を修正してください。" + +#: src/iac_code/commands/model.py:81 src/iac_code/commands/model.py:136 #, python-brace-format msgid "Model switched to: {model}" msgstr "モデルを {model} に切り替えました" -#: src/iac_code/commands/model.py:79 +#: src/iac_code/commands/model.py:85 #, python-brace-format msgid "Current model: {model}" msgstr "現在のモデル:{model}" -#: src/iac_code/commands/model.py:105 +#: src/iac_code/commands/model.py:111 #, python-brace-format msgid "Kept model as {model}" msgstr "モデルを {model} のままにしました" @@ -936,79 +953,79 @@ msgstr "" "API から無効な応答が返りました。API Base URL が正しいか確認してください(現在:{base_url})。 多くの OpenAI " "互換エンドポイントでは /v1 接尾辞が必要です(例:{base_url}/v1)。" -#: src/iac_code/providers/registry.py:406 +#: src/iac_code/providers/registry.py:411 msgid "Alibaba Cloud Bailian" msgstr "Alibaba Cloud 百錬" -#: src/iac_code/providers/registry.py:407 +#: src/iac_code/providers/registry.py:412 msgid "Alibaba Cloud Bailian Token Plan" msgstr "Alibaba Cloud 百錬 Token Plan" -#: src/iac_code/providers/registry.py:411 +#: src/iac_code/providers/registry.py:416 msgid "OpenAPI Compatible" msgstr "OpenAPI 互換" -#: src/iac_code/providers/registry.py:413 +#: src/iac_code/providers/registry.py:418 msgid "Kimi (China)" msgstr "Kimi(中国版)" -#: src/iac_code/providers/registry.py:414 +#: src/iac_code/providers/registry.py:419 msgid "Kimi (International)" msgstr "Kimi(国際版)" -#: src/iac_code/providers/registry.py:415 +#: src/iac_code/providers/registry.py:420 msgid "MiniMax (China)" msgstr "MiniMax(中国版)" -#: src/iac_code/providers/registry.py:416 +#: src/iac_code/providers/registry.py:421 msgid "MiniMax (International)" msgstr "MiniMax(国際版)" -#: src/iac_code/providers/registry.py:418 +#: src/iac_code/providers/registry.py:423 msgid "ZhiPu AI (International)" msgstr "ZhiPu AI(国際版)" -#: src/iac_code/providers/registry.py:420 +#: src/iac_code/providers/registry.py:425 msgid "SiliconFlow (China)" msgstr "SiliconFlow(中国版)" -#: src/iac_code/providers/registry.py:421 +#: src/iac_code/providers/registry.py:426 msgid "SiliconFlow (International)" msgstr "SiliconFlow(国際版)" -#: src/iac_code/providers/registry.py:422 +#: src/iac_code/providers/registry.py:427 msgid "Ollama (Local)" msgstr "Ollama(ローカル)" -#: src/iac_code/providers/registry.py:423 +#: src/iac_code/providers/registry.py:428 msgid "LM Studio (Local)" msgstr "LM Studio(ローカル)" -#: src/iac_code/providers/registry.py:426 +#: src/iac_code/providers/registry.py:431 msgid "ModelScope" msgstr "ModelScope" -#: src/iac_code/providers/registry.py:427 +#: src/iac_code/providers/registry.py:432 msgid "Alibaba Cloud CodingPlan" msgstr "Alibaba Cloud CodingPlan" -#: src/iac_code/providers/registry.py:428 +#: src/iac_code/providers/registry.py:433 msgid "Alibaba Cloud CodingPlan (International)" msgstr "Alibaba Cloud CodingPlan(国際版)" -#: src/iac_code/providers/registry.py:429 +#: src/iac_code/providers/registry.py:434 msgid "ZhiPu AI CodingPlan" msgstr "ZhiPu AI CodingPlan" -#: src/iac_code/providers/registry.py:430 +#: src/iac_code/providers/registry.py:435 msgid "ZhiPu AI CodingPlan (International)" msgstr "ZhiPu AI CodingPlan(国際版)" -#: src/iac_code/providers/registry.py:431 +#: src/iac_code/providers/registry.py:436 msgid "Volcengine CodingPlan" msgstr "Volcengine CodingPlan" -#: src/iac_code/providers/registry.py:432 +#: src/iac_code/providers/registry.py:437 msgid "Anthropic Compatible" msgstr "Anthropic 互換" @@ -1028,7 +1045,7 @@ msgstr "" " 'llm_source: qwenpaw' を削除)。" #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:185 src/iac_code/tools/bash/bash_tool.py:154 +#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:158 #, python-brace-format msgid "Allow {}?" msgstr "{} を許可しますか?" @@ -1227,11 +1244,11 @@ msgstr "{cmd} を実行中" msgid "Running command..." msgstr "コマンドを実行しています…" -#: src/iac_code/tools/bash/command_parser.py:41 +#: src/iac_code/tools/bash/command_parser.py:42 msgid "parse error" msgstr "解析エラー" -#: src/iac_code/tools/bash/command_parser.py:44 +#: src/iac_code/tools/bash/command_parser.py:46 msgid "unsupported shell construct" msgstr "サポートされていないシェル構文" @@ -1240,53 +1257,57 @@ msgstr "サポートされていないシェル構文" msgid "path outside allowed directories: {}" msgstr "許可されたディレクトリの外のパス: {}" -#: src/iac_code/tools/bash/permissions.py:101 +#: src/iac_code/tools/bash/permissions.py:135 #, python-brace-format msgid "matched deny rule(s): {}" msgstr "一致した拒否ルール: {}" -#: src/iac_code/tools/bash/permissions.py:108 +#: src/iac_code/tools/bash/permissions.py:142 #, python-brace-format msgid "matched ask rule(s): {}" msgstr "一致した確認ルール: {}" -#: src/iac_code/tools/bash/permissions.py:120 -#: src/iac_code/tools/bash/permissions.py:178 +#: src/iac_code/tools/bash/permissions.py:154 +#: src/iac_code/tools/bash/permissions.py:220 #, python-brace-format msgid "matched allow rule(s): {}" msgstr "一致した許可ルール: {}" -#: src/iac_code/tools/bash/permissions.py:129 +#: src/iac_code/tools/bash/permissions.py:162 +msgid "complex command requires confirmation" +msgstr "複雑なコマンドは確認が必要です" + +#: src/iac_code/tools/bash/permissions.py:171 msgid "sed in-place edit requires confirmation" msgstr "sed のインプレース編集には確認が必要です" -#: src/iac_code/tools/bash/permissions.py:152 +#: src/iac_code/tools/bash/permissions.py:194 msgid "command failed basic safety checks" msgstr "コマンドが基本的な安全性チェックに失敗しました" -#: src/iac_code/tools/bash/permissions.py:168 +#: src/iac_code/tools/bash/permissions.py:210 #, python-brace-format msgid "matched deny rule(s) on full command: {}" msgstr "完全なコマンドに一致した拒否ルール: {}" -#: src/iac_code/tools/bash/permissions.py:185 +#: src/iac_code/tools/bash/permissions.py:227 msgid "command too complex to analyze" msgstr "コマンドが複雑すぎて分析できません" -#: src/iac_code/tools/bash/permissions.py:187 +#: src/iac_code/tools/bash/permissions.py:229 msgid "could not parse command" msgstr "コマンドを解析できませんでした" -#: src/iac_code/tools/bash/permissions.py:203 +#: src/iac_code/tools/bash/permissions.py:245 #, python-brace-format msgid "too many subcommands (>{})" msgstr "サブコマンドが多すぎます (>{})" -#: src/iac_code/tools/bash/permissions.py:215 +#: src/iac_code/tools/bash/permissions.py:258 msgid "multiple cd commands in compound command" msgstr "複合コマンド内に複数の cd コマンドがあります" -#: src/iac_code/tools/bash/permissions.py:227 +#: src/iac_code/tools/bash/permissions.py:271 msgid "cd combined with git in compound command" msgstr "複合コマンド内で cd と git が組み合わされています" @@ -1555,13 +1576,13 @@ msgid "Log file" msgstr "ログファイル" #: src/iac_code/ui/renderer.py:351 src/iac_code/ui/renderer.py:621 -#: src/iac_code/ui/renderer.py:1380 +#: src/iac_code/ui/renderer.py:1402 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "{seconds:.1f} 秒考えました" #: src/iac_code/ui/renderer.py:367 src/iac_code/ui/renderer.py:653 -#: src/iac_code/ui/renderer.py:1401 +#: src/iac_code/ui/renderer.py:1423 msgid "(ctrl+o to expand)" msgstr "(ctrl+o で展開)" @@ -1621,24 +1642,29 @@ msgstr "この操作を許可しますか?" msgid "Yes, allow once" msgstr "はい、今回のみ許可" -#: src/iac_code/ui/renderer.py:1304 +#: src/iac_code/ui/renderer.py:1306 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "はい、\"{rule}\" を常に許可(このセッション)" -#: src/iac_code/ui/renderer.py:1309 +#: src/iac_code/ui/renderer.py:1311 msgid "Yes, allow always for this tool" msgstr "はい、このツールは常に許可" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "No, reject once" msgstr "いいえ、今回は拒否" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "default" msgstr "既定" -#: src/iac_code/ui/renderer.py:1314 +#: src/iac_code/ui/renderer.py:1321 +#, python-brace-format +msgid "No, always deny \"{rule}\" (this session)" +msgstr "いいえ、常に \"{rule}\" を拒否(このセッション)" + +#: src/iac_code/ui/renderer.py:1326 msgid "No, always reject this tool" msgstr "いいえ、このツールは常に拒否" diff --git a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po index 15a269f6..68ff2f66 100644 --- a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-18 12:25+0800\n" +"POT-Creation-Date: 2026-05-18 16:59+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: pt\n" @@ -537,212 +537,222 @@ msgstr "Retomar uma sessão anterior" msgid "[conversation id or search term]" msgstr "[ID da conversa ou termo de busca]" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:836 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "Navegar" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 -#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 -#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 -#: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:368 +#: src/iac_code/commands/auth.py:402 src/iac_code/commands/auth.py:409 +#: src/iac_code/commands/auth.py:433 src/iac_code/commands/auth.py:836 +#: src/iac_code/commands/auth.py:1026 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "Confirmar" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 -#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 -#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 -#: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 -#: src/iac_code/commands/auth.py:1007 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:366 +#: src/iac_code/commands/auth.py:368 src/iac_code/commands/auth.py:402 +#: src/iac_code/commands/auth.py:409 src/iac_code/commands/auth.py:433 +#: src/iac_code/commands/auth.py:836 src/iac_code/commands/auth.py:921 +#: src/iac_code/commands/auth.py:1026 msgid "Back" msgstr "Voltar" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Keep" msgstr "Manter" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Re-enter" msgstr "Digitar novamente" -#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 -#: src/iac_code/commands/auth.py:651 +#: src/iac_code/commands/auth.py:508 src/iac_code/commands/auth.py:650 +#: src/iac_code/commands/auth.py:670 msgid " (current)" msgstr " (atual)" -#: src/iac_code/commands/auth.py:510 +#: src/iac_code/commands/auth.py:511 msgid "Custom model..." msgstr "Modelo personalizado..." -#: src/iac_code/commands/auth.py:513 +#: src/iac_code/commands/auth.py:514 #, python-brace-format msgid "Select model for {provider}" msgstr "Selecionar modelo para {provider}" -#: src/iac_code/commands/auth.py:515 +#: src/iac_code/commands/auth.py:516 msgid "Select model" msgstr "Selecionar modelo" -#: src/iac_code/commands/auth.py:523 +#: src/iac_code/commands/auth.py:524 msgid "Enter custom model name: " msgstr "Informe o nome do modelo personalizado: " -#: src/iac_code/commands/auth.py:549 +#: src/iac_code/commands/auth.py:550 msgid "Error: console not available" msgstr "Erro: console indisponível" #: src/iac_code/commands/auth.py:576 +#, python-brace-format +msgid "" +"LLM provider is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "" +"O provedor LLM está bloqueado por '{source}'. Para alterar, modifique " +"llm_source em settings.yml." + +#: src/iac_code/commands/auth.py:580 src/iac_code/commands/auth.py:780 +msgid "Select Cloud Provider" +msgstr "Selecionar provedor de nuvem" + +#: src/iac_code/commands/auth.py:582 src/iac_code/commands/auth.py:589 +#: src/iac_code/commands/auth.py:600 src/iac_code/commands/auth.py:695 +#: src/iac_code/commands/auth.py:711 src/iac_code/commands/auth.py:789 +#: src/iac_code/commands/auth.py:962 src/iac_code/commands/auth.py:1003 +msgid "Auth cancelled" +msgstr "Autenticação cancelada" + +#: src/iac_code/commands/auth.py:595 msgid "Configure LLM Provider" msgstr "Configurar provedor LLM" -#: src/iac_code/commands/auth.py:577 +#: src/iac_code/commands/auth.py:596 msgid "Configure IaC Cloud Service" msgstr "Configurar serviço de nuvem IaC" -#: src/iac_code/commands/auth.py:579 +#: src/iac_code/commands/auth.py:598 msgid "Select configuration type" msgstr "Selecionar tipo de configuração" -#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 -#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 -#: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 -msgid "Auth cancelled" -msgstr "Autenticação cancelada" - -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:654 msgid "Select provider" msgstr "Selecionar provedor" -#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#: src/iac_code/commands/auth.py:675 src/iac_code/commands/auth.py:764 #, python-brace-format msgid "Select provider — {group}" msgstr "Selecionar provedor — {group}" -#: src/iac_code/commands/auth.py:669 +#: src/iac_code/commands/auth.py:688 #, python-brace-format msgid "Configure {provider}" msgstr "Configurar {provider}" -#: src/iac_code/commands/auth.py:685 +#: src/iac_code/commands/auth.py:704 #, python-brace-format msgid "Enter API key for {provider}" msgstr "Informe a API key para {provider}" -#: src/iac_code/commands/auth.py:723 +#: src/iac_code/commands/auth.py:742 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}: {provider} / {model}" -#: src/iac_code/commands/auth.py:724 +#: src/iac_code/commands/auth.py:743 msgid "Configured" msgstr "Configurado" -#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:750 src/iac_code/commands/auth.py:771 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" -#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +#: src/iac_code/commands/auth.py:751 src/iac_code/providers/registry.py:422 msgid "ZhiPu AI" msgstr "ZhiPu AI" -#: src/iac_code/commands/auth.py:733 +#: src/iac_code/commands/auth.py:752 msgid "Kimi" msgstr "Kimi" -#: src/iac_code/commands/auth.py:734 +#: src/iac_code/commands/auth.py:753 msgid "MiniMax" msgstr "MiniMax" -#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +#: src/iac_code/commands/auth.py:754 src/iac_code/providers/registry.py:424 msgid "Volcengine" msgstr "Volcengine" -#: src/iac_code/commands/auth.py:736 +#: src/iac_code/commands/auth.py:755 msgid "SiliconFlow" msgstr "SiliconFlow" -#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +#: src/iac_code/commands/auth.py:756 src/iac_code/providers/registry.py:415 msgid "DeepSeek" msgstr "DeepSeek" -#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +#: src/iac_code/commands/auth.py:757 src/iac_code/providers/registry.py:413 msgid "OpenAI" msgstr "OpenAI" -#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +#: src/iac_code/commands/auth.py:758 src/iac_code/providers/registry.py:414 msgid "Anthropic" msgstr "Anthropic" -#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +#: src/iac_code/commands/auth.py:759 src/iac_code/providers/registry.py:417 msgid "Google Gemini" msgstr "Google Gemini" -#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +#: src/iac_code/commands/auth.py:760 src/iac_code/providers/registry.py:430 msgid "Azure OpenAI" msgstr "Azure OpenAI" -#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +#: src/iac_code/commands/auth.py:761 src/iac_code/providers/registry.py:429 msgid "OpenRouter" msgstr "OpenRouter" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:762 msgid "Local" msgstr "Local" -#: src/iac_code/commands/auth.py:744 +#: src/iac_code/commands/auth.py:763 msgid "Compatible" msgstr "Compatível" -#: src/iac_code/commands/auth.py:761 -msgid "Select Cloud Provider" -msgstr "Selecionar provedor de nuvem" - -#: src/iac_code/commands/auth.py:777 +#: src/iac_code/commands/auth.py:796 msgid "Credential" msgstr "Credencial" -#: src/iac_code/commands/auth.py:778 src/iac_code/commands/auth.py:875 -#: src/iac_code/commands/auth.py:980 src/iac_code/ui/renderer.py:418 +#: src/iac_code/commands/auth.py:797 src/iac_code/commands/auth.py:894 +#: src/iac_code/commands/auth.py:999 src/iac_code/ui/renderer.py:418 msgid "Region" msgstr "Região" -#: src/iac_code/commands/auth.py:780 +#: src/iac_code/commands/auth.py:799 msgid "Configure Alibaba Cloud" msgstr "Configurar Alibaba Cloud" -#: src/iac_code/commands/auth.py:863 +#: src/iac_code/commands/auth.py:882 msgid "Current configuration" msgstr "Configuração atual" -#: src/iac_code/commands/auth.py:865 +#: src/iac_code/commands/auth.py:884 msgid "Mode" msgstr "Modo" -#: src/iac_code/commands/auth.py:872 +#: src/iac_code/commands/auth.py:891 msgid "(not set)" msgstr "(não definido)" -#: src/iac_code/commands/auth.py:889 +#: src/iac_code/commands/auth.py:908 msgid "Configure Alibaba Cloud credentials" msgstr "Configurar credenciais da Alibaba Cloud" -#: src/iac_code/commands/auth.py:902 +#: src/iac_code/commands/auth.py:921 msgid "Reconfigure credential" msgstr "Reconfigurar credencial" -#: src/iac_code/commands/auth.py:915 +#: src/iac_code/commands/auth.py:934 msgid "Select credential type" msgstr "Selecionar tipo de credencial" -#: src/iac_code/commands/auth.py:965 +#: src/iac_code/commands/auth.py:984 msgid "Configured: Alibaba Cloud credentials saved to ~/.iac-code" msgstr "Configurado: credenciais da Alibaba Cloud salvas em ~/.iac-code" -#: src/iac_code/commands/auth.py:972 +#: src/iac_code/commands/auth.py:991 msgid "Configure Alibaba Cloud region" msgstr "Configurar região da Alibaba Cloud" -#: src/iac_code/commands/auth.py:998 +#: src/iac_code/commands/auth.py:1017 msgid "Configured: Alibaba Cloud region saved to ~/.iac-code" msgstr "Configurado: região da Alibaba Cloud salva em ~/.iac-code" @@ -775,8 +785,8 @@ msgstr "O comando debug requer um contexto." msgid "No active session." msgstr "Nenhuma sessão ativa." -#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:82 -#: src/iac_code/commands/model.py:86 +#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:88 +#: src/iac_code/commands/model.py:92 msgid "No configured providers. Run /auth first." msgstr "Nenhum provedor configurado. Execute /auth primeiro." @@ -838,17 +848,26 @@ msgstr "Mostrar sugestões de comando" msgid "Exit" msgstr "Sair" -#: src/iac_code/commands/model.py:75 src/iac_code/commands/model.py:130 +#: src/iac_code/commands/model.py:49 +#, python-brace-format +msgid "" +"Model selection is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "" +"A seleção de modelo está bloqueada por '{source}'. Para alterar, " +"modifique llm_source em settings.yml." + +#: src/iac_code/commands/model.py:81 src/iac_code/commands/model.py:136 #, python-brace-format msgid "Model switched to: {model}" msgstr "Modelo alterado para: {model}" -#: src/iac_code/commands/model.py:79 +#: src/iac_code/commands/model.py:85 #, python-brace-format msgid "Current model: {model}" msgstr "Modelo atual: {model}" -#: src/iac_code/commands/model.py:105 +#: src/iac_code/commands/model.py:111 #, python-brace-format msgid "Kept model as {model}" msgstr "Modelo mantido como {model}" @@ -954,79 +973,79 @@ msgstr "" "correta (atual: {base_url}). Muitos endpoints compatíveis com OpenAI " "exigem o sufixo /v1 (por exemplo, {base_url}/v1)." -#: src/iac_code/providers/registry.py:406 +#: src/iac_code/providers/registry.py:411 msgid "Alibaba Cloud Bailian" msgstr "Alibaba Cloud Bailian" -#: src/iac_code/providers/registry.py:407 +#: src/iac_code/providers/registry.py:412 msgid "Alibaba Cloud Bailian Token Plan" msgstr "Alibaba Cloud Bailian Token Plan" -#: src/iac_code/providers/registry.py:411 +#: src/iac_code/providers/registry.py:416 msgid "OpenAPI Compatible" msgstr "Compatível com OpenAPI" -#: src/iac_code/providers/registry.py:413 +#: src/iac_code/providers/registry.py:418 msgid "Kimi (China)" msgstr "Kimi (China)" -#: src/iac_code/providers/registry.py:414 +#: src/iac_code/providers/registry.py:419 msgid "Kimi (International)" msgstr "Kimi (Internacional)" -#: src/iac_code/providers/registry.py:415 +#: src/iac_code/providers/registry.py:420 msgid "MiniMax (China)" msgstr "MiniMax (China)" -#: src/iac_code/providers/registry.py:416 +#: src/iac_code/providers/registry.py:421 msgid "MiniMax (International)" msgstr "MiniMax (Internacional)" -#: src/iac_code/providers/registry.py:418 +#: src/iac_code/providers/registry.py:423 msgid "ZhiPu AI (International)" msgstr "ZhiPu AI (Internacional)" -#: src/iac_code/providers/registry.py:420 +#: src/iac_code/providers/registry.py:425 msgid "SiliconFlow (China)" msgstr "SiliconFlow (China)" -#: src/iac_code/providers/registry.py:421 +#: src/iac_code/providers/registry.py:426 msgid "SiliconFlow (International)" msgstr "SiliconFlow (Internacional)" -#: src/iac_code/providers/registry.py:422 +#: src/iac_code/providers/registry.py:427 msgid "Ollama (Local)" msgstr "Ollama (Local)" -#: src/iac_code/providers/registry.py:423 +#: src/iac_code/providers/registry.py:428 msgid "LM Studio (Local)" msgstr "LM Studio (Local)" -#: src/iac_code/providers/registry.py:426 +#: src/iac_code/providers/registry.py:431 msgid "ModelScope" msgstr "ModelScope" -#: src/iac_code/providers/registry.py:427 +#: src/iac_code/providers/registry.py:432 msgid "Alibaba Cloud CodingPlan" msgstr "Alibaba Cloud CodingPlan" -#: src/iac_code/providers/registry.py:428 +#: src/iac_code/providers/registry.py:433 msgid "Alibaba Cloud CodingPlan (International)" msgstr "Alibaba Cloud CodingPlan (Internacional)" -#: src/iac_code/providers/registry.py:429 +#: src/iac_code/providers/registry.py:434 msgid "ZhiPu AI CodingPlan" msgstr "ZhiPu AI CodingPlan" -#: src/iac_code/providers/registry.py:430 +#: src/iac_code/providers/registry.py:435 msgid "ZhiPu AI CodingPlan (International)" msgstr "ZhiPu AI CodingPlan (Internacional)" -#: src/iac_code/providers/registry.py:431 +#: src/iac_code/providers/registry.py:436 msgid "Volcengine CodingPlan" msgstr "Volcengine CodingPlan" -#: src/iac_code/providers/registry.py:432 +#: src/iac_code/providers/registry.py:437 msgid "Anthropic Compatible" msgstr "Compatível com Anthropic" @@ -1046,7 +1065,7 @@ msgstr "" "QwenPaw (remova 'llm_source: qwenpaw' do settings.yml)." #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:185 src/iac_code/tools/bash/bash_tool.py:154 +#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:158 #, python-brace-format msgid "Allow {}?" msgstr "Permitir {}?" @@ -1247,11 +1266,11 @@ msgstr "Executando {cmd}" msgid "Running command..." msgstr "Executando comando..." -#: src/iac_code/tools/bash/command_parser.py:41 +#: src/iac_code/tools/bash/command_parser.py:42 msgid "parse error" msgstr "Erro de análise" -#: src/iac_code/tools/bash/command_parser.py:44 +#: src/iac_code/tools/bash/command_parser.py:46 msgid "unsupported shell construct" msgstr "Construção de shell não suportada" @@ -1260,53 +1279,57 @@ msgstr "Construção de shell não suportada" msgid "path outside allowed directories: {}" msgstr "Caminho fora dos diretórios permitidos: {}" -#: src/iac_code/tools/bash/permissions.py:101 +#: src/iac_code/tools/bash/permissions.py:135 #, python-brace-format msgid "matched deny rule(s): {}" msgstr "Regra(s) de negação correspondente(s): {}" -#: src/iac_code/tools/bash/permissions.py:108 +#: src/iac_code/tools/bash/permissions.py:142 #, python-brace-format msgid "matched ask rule(s): {}" msgstr "Regra(s) de consulta correspondente(s): {}" -#: src/iac_code/tools/bash/permissions.py:120 -#: src/iac_code/tools/bash/permissions.py:178 +#: src/iac_code/tools/bash/permissions.py:154 +#: src/iac_code/tools/bash/permissions.py:220 #, python-brace-format msgid "matched allow rule(s): {}" msgstr "Regra(s) de permissão correspondente(s): {}" -#: src/iac_code/tools/bash/permissions.py:129 +#: src/iac_code/tools/bash/permissions.py:162 +msgid "complex command requires confirmation" +msgstr "Comando complexo requer confirmação" + +#: src/iac_code/tools/bash/permissions.py:171 msgid "sed in-place edit requires confirmation" msgstr "A edição sed no local requer confirmação" -#: src/iac_code/tools/bash/permissions.py:152 +#: src/iac_code/tools/bash/permissions.py:194 msgid "command failed basic safety checks" msgstr "O comando não passou nas verificações básicas de segurança" -#: src/iac_code/tools/bash/permissions.py:168 +#: src/iac_code/tools/bash/permissions.py:210 #, python-brace-format msgid "matched deny rule(s) on full command: {}" msgstr "Regra(s) de negação para o comando completo: {}" -#: src/iac_code/tools/bash/permissions.py:185 +#: src/iac_code/tools/bash/permissions.py:227 msgid "command too complex to analyze" msgstr "Comando complexo demais para analisar" -#: src/iac_code/tools/bash/permissions.py:187 +#: src/iac_code/tools/bash/permissions.py:229 msgid "could not parse command" msgstr "Não foi possível analisar o comando" -#: src/iac_code/tools/bash/permissions.py:203 +#: src/iac_code/tools/bash/permissions.py:245 #, python-brace-format msgid "too many subcommands (>{})" msgstr "Muitos subcomandos (>{})" -#: src/iac_code/tools/bash/permissions.py:215 +#: src/iac_code/tools/bash/permissions.py:258 msgid "multiple cd commands in compound command" msgstr "Múltiplos comandos cd em comando composto" -#: src/iac_code/tools/bash/permissions.py:227 +#: src/iac_code/tools/bash/permissions.py:271 msgid "cd combined with git in compound command" msgstr "cd combinado com git em comando composto" @@ -1577,13 +1600,13 @@ msgid "Log file" msgstr "Arquivo de log" #: src/iac_code/ui/renderer.py:351 src/iac_code/ui/renderer.py:621 -#: src/iac_code/ui/renderer.py:1380 +#: src/iac_code/ui/renderer.py:1402 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "Raciocínio por {seconds:.1f}s" #: src/iac_code/ui/renderer.py:367 src/iac_code/ui/renderer.py:653 -#: src/iac_code/ui/renderer.py:1401 +#: src/iac_code/ui/renderer.py:1423 msgid "(ctrl+o to expand)" msgstr "(ctrl+o para expandir)" @@ -1643,24 +1666,29 @@ msgstr "Permitir esta ação?" msgid "Yes, allow once" msgstr "Sim, permitir uma vez" -#: src/iac_code/ui/renderer.py:1304 +#: src/iac_code/ui/renderer.py:1306 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "Sim, sempre permitir \"{rule}\" (esta sessão)" -#: src/iac_code/ui/renderer.py:1309 +#: src/iac_code/ui/renderer.py:1311 msgid "Yes, allow always for this tool" msgstr "Sim, sempre permitir para esta ferramenta" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "No, reject once" msgstr "Não, rejeitar uma vez" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "default" msgstr "padrão" -#: src/iac_code/ui/renderer.py:1314 +#: src/iac_code/ui/renderer.py:1321 +#, python-brace-format +msgid "No, always deny \"{rule}\" (this session)" +msgstr "Não, sempre negar \"{rule}\" (esta sessão)" + +#: src/iac_code/ui/renderer.py:1326 msgid "No, always reject this tool" msgstr "Não, sempre rejeitar esta ferramenta" diff --git a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po index 36797ebd..1652a992 100644 --- a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-18 12:25+0800\n" +"POT-Creation-Date: 2026-05-18 16:59+0800\n" "PO-Revision-Date: 2026-04-02 00:00+0000\n" "Last-Translator: \n" "Language: zh\n" @@ -519,212 +519,220 @@ msgstr "恢复之前的会话" msgid "[conversation id or search term]" msgstr "[会话 ID 或搜索词]" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:836 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "导航" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 -#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 -#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 -#: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:368 +#: src/iac_code/commands/auth.py:402 src/iac_code/commands/auth.py:409 +#: src/iac_code/commands/auth.py:433 src/iac_code/commands/auth.py:836 +#: src/iac_code/commands/auth.py:1026 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "确认" -#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 -#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 -#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 -#: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 -#: src/iac_code/commands/auth.py:1007 +#: src/iac_code/commands/auth.py:246 src/iac_code/commands/auth.py:366 +#: src/iac_code/commands/auth.py:368 src/iac_code/commands/auth.py:402 +#: src/iac_code/commands/auth.py:409 src/iac_code/commands/auth.py:433 +#: src/iac_code/commands/auth.py:836 src/iac_code/commands/auth.py:921 +#: src/iac_code/commands/auth.py:1026 msgid "Back" msgstr "返回" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Keep" msgstr "保留" -#: src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:366 msgid "Re-enter" msgstr "重新输入" -#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 -#: src/iac_code/commands/auth.py:651 +#: src/iac_code/commands/auth.py:508 src/iac_code/commands/auth.py:650 +#: src/iac_code/commands/auth.py:670 msgid " (current)" msgstr " (当前)" -#: src/iac_code/commands/auth.py:510 +#: src/iac_code/commands/auth.py:511 msgid "Custom model..." msgstr "自定义模型..." -#: src/iac_code/commands/auth.py:513 +#: src/iac_code/commands/auth.py:514 #, python-brace-format msgid "Select model for {provider}" msgstr "为 {provider} 选择模型" -#: src/iac_code/commands/auth.py:515 +#: src/iac_code/commands/auth.py:516 msgid "Select model" msgstr "选择模型" -#: src/iac_code/commands/auth.py:523 +#: src/iac_code/commands/auth.py:524 msgid "Enter custom model name: " msgstr "输入自定义模型名称:" -#: src/iac_code/commands/auth.py:549 +#: src/iac_code/commands/auth.py:550 msgid "Error: console not available" msgstr "错误:控制台不可用" #: src/iac_code/commands/auth.py:576 +#, python-brace-format +msgid "" +"LLM provider is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "LLM 提供商已被 '{source}' 锁定。如需修改,请调整 settings.yml 中的 llm_source 配置。" + +#: src/iac_code/commands/auth.py:580 src/iac_code/commands/auth.py:780 +msgid "Select Cloud Provider" +msgstr "选择云服务商" + +#: src/iac_code/commands/auth.py:582 src/iac_code/commands/auth.py:589 +#: src/iac_code/commands/auth.py:600 src/iac_code/commands/auth.py:695 +#: src/iac_code/commands/auth.py:711 src/iac_code/commands/auth.py:789 +#: src/iac_code/commands/auth.py:962 src/iac_code/commands/auth.py:1003 +msgid "Auth cancelled" +msgstr "认证已取消" + +#: src/iac_code/commands/auth.py:595 msgid "Configure LLM Provider" msgstr "配置 LLM 提供商" -#: src/iac_code/commands/auth.py:577 +#: src/iac_code/commands/auth.py:596 msgid "Configure IaC Cloud Service" msgstr "配置 IaC 云服务" -#: src/iac_code/commands/auth.py:579 +#: src/iac_code/commands/auth.py:598 msgid "Select configuration type" msgstr "选择配置类型" -#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 -#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 -#: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 -msgid "Auth cancelled" -msgstr "认证已取消" - -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:654 msgid "Select provider" msgstr "选择提供商" -#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#: src/iac_code/commands/auth.py:675 src/iac_code/commands/auth.py:764 #, python-brace-format msgid "Select provider — {group}" msgstr "选择提供商 — {group}" -#: src/iac_code/commands/auth.py:669 +#: src/iac_code/commands/auth.py:688 #, python-brace-format msgid "Configure {provider}" msgstr "配置 {provider}" -#: src/iac_code/commands/auth.py:685 +#: src/iac_code/commands/auth.py:704 #, python-brace-format msgid "Enter API key for {provider}" msgstr "为 {provider} 输入 API 密钥" -#: src/iac_code/commands/auth.py:723 +#: src/iac_code/commands/auth.py:742 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}:{provider} / {model}" -#: src/iac_code/commands/auth.py:724 +#: src/iac_code/commands/auth.py:743 msgid "Configured" msgstr "已配置" -#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:750 src/iac_code/commands/auth.py:771 msgid "Alibaba Cloud" msgstr "阿里云" -#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +#: src/iac_code/commands/auth.py:751 src/iac_code/providers/registry.py:422 msgid "ZhiPu AI" msgstr "智谱 AI" -#: src/iac_code/commands/auth.py:733 +#: src/iac_code/commands/auth.py:752 msgid "Kimi" msgstr "Kimi" -#: src/iac_code/commands/auth.py:734 +#: src/iac_code/commands/auth.py:753 msgid "MiniMax" msgstr "MiniMax" -#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +#: src/iac_code/commands/auth.py:754 src/iac_code/providers/registry.py:424 msgid "Volcengine" msgstr "火山引擎" -#: src/iac_code/commands/auth.py:736 +#: src/iac_code/commands/auth.py:755 msgid "SiliconFlow" msgstr "硅基流动" -#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +#: src/iac_code/commands/auth.py:756 src/iac_code/providers/registry.py:415 msgid "DeepSeek" msgstr "DeepSeek" -#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +#: src/iac_code/commands/auth.py:757 src/iac_code/providers/registry.py:413 msgid "OpenAI" msgstr "OpenAI" -#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +#: src/iac_code/commands/auth.py:758 src/iac_code/providers/registry.py:414 msgid "Anthropic" msgstr "Anthropic" -#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +#: src/iac_code/commands/auth.py:759 src/iac_code/providers/registry.py:417 msgid "Google Gemini" msgstr "Google Gemini" -#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +#: src/iac_code/commands/auth.py:760 src/iac_code/providers/registry.py:430 msgid "Azure OpenAI" msgstr "Azure OpenAI" -#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +#: src/iac_code/commands/auth.py:761 src/iac_code/providers/registry.py:429 msgid "OpenRouter" msgstr "OpenRouter" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:762 msgid "Local" msgstr "本地模型" -#: src/iac_code/commands/auth.py:744 +#: src/iac_code/commands/auth.py:763 msgid "Compatible" msgstr "兼容模式" -#: src/iac_code/commands/auth.py:761 -msgid "Select Cloud Provider" -msgstr "选择云服务商" - -#: src/iac_code/commands/auth.py:777 +#: src/iac_code/commands/auth.py:796 msgid "Credential" msgstr "凭证" -#: src/iac_code/commands/auth.py:778 src/iac_code/commands/auth.py:875 -#: src/iac_code/commands/auth.py:980 src/iac_code/ui/renderer.py:418 +#: src/iac_code/commands/auth.py:797 src/iac_code/commands/auth.py:894 +#: src/iac_code/commands/auth.py:999 src/iac_code/ui/renderer.py:418 msgid "Region" msgstr "地域" -#: src/iac_code/commands/auth.py:780 +#: src/iac_code/commands/auth.py:799 msgid "Configure Alibaba Cloud" msgstr "配置阿里云" -#: src/iac_code/commands/auth.py:863 +#: src/iac_code/commands/auth.py:882 msgid "Current configuration" msgstr "当前配置" -#: src/iac_code/commands/auth.py:865 +#: src/iac_code/commands/auth.py:884 msgid "Mode" msgstr "模式" -#: src/iac_code/commands/auth.py:872 +#: src/iac_code/commands/auth.py:891 msgid "(not set)" msgstr "(未设置)" -#: src/iac_code/commands/auth.py:889 +#: src/iac_code/commands/auth.py:908 msgid "Configure Alibaba Cloud credentials" msgstr "配置阿里云凭证" -#: src/iac_code/commands/auth.py:902 +#: src/iac_code/commands/auth.py:921 msgid "Reconfigure credential" msgstr "重新配置凭证" -#: src/iac_code/commands/auth.py:915 +#: src/iac_code/commands/auth.py:934 msgid "Select credential type" msgstr "选择凭证类型" -#: src/iac_code/commands/auth.py:965 +#: src/iac_code/commands/auth.py:984 msgid "Configured: Alibaba Cloud credentials saved to ~/.iac-code" msgstr "已配置:阿里云凭证已保存到 ~/.iac-code" -#: src/iac_code/commands/auth.py:972 +#: src/iac_code/commands/auth.py:991 msgid "Configure Alibaba Cloud region" msgstr "配置阿里云地域" -#: src/iac_code/commands/auth.py:998 +#: src/iac_code/commands/auth.py:1017 msgid "Configured: Alibaba Cloud region saved to ~/.iac-code" msgstr "已配置:阿里云地域已保存到 ~/.iac-code" @@ -757,8 +765,8 @@ msgstr "调试命令需要上下文。" msgid "No active session." msgstr "没有活动的会话。" -#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:82 -#: src/iac_code/commands/model.py:86 +#: src/iac_code/commands/effort.py:54 src/iac_code/commands/model.py:88 +#: src/iac_code/commands/model.py:92 msgid "No configured providers. Run /auth first." msgstr "没有已配置的提供商。请先运行 /auth。" @@ -820,17 +828,24 @@ msgstr "显示命令建议" msgid "Exit" msgstr "退出" -#: src/iac_code/commands/model.py:75 src/iac_code/commands/model.py:130 +#: src/iac_code/commands/model.py:49 +#, python-brace-format +msgid "" +"Model selection is locked by '{source}'. To change, modify llm_source in " +"settings.yml." +msgstr "模型选择已被 '{source}' 锁定。如需修改,请调整 settings.yml 中的 llm_source 配置。" + +#: src/iac_code/commands/model.py:81 src/iac_code/commands/model.py:136 #, python-brace-format msgid "Model switched to: {model}" msgstr "模型已切换为:{model}" -#: src/iac_code/commands/model.py:79 +#: src/iac_code/commands/model.py:85 #, python-brace-format msgid "Current model: {model}" msgstr "当前模型:{model}" -#: src/iac_code/commands/model.py:105 +#: src/iac_code/commands/model.py:111 #, python-brace-format msgid "Kept model as {model}" msgstr "保持模型为 {model}" @@ -928,79 +943,79 @@ msgstr "" "API 返回了无效响应。请检查您的 API Base URL 是否正确(当前:{base_url})。许多 OpenAI 兼容端点需要 /v1 " "后缀(如 {base_url}/v1)。" -#: src/iac_code/providers/registry.py:406 +#: src/iac_code/providers/registry.py:411 msgid "Alibaba Cloud Bailian" msgstr "阿里云百炼" -#: src/iac_code/providers/registry.py:407 +#: src/iac_code/providers/registry.py:412 msgid "Alibaba Cloud Bailian Token Plan" msgstr "阿里云百炼 Token Plan" -#: src/iac_code/providers/registry.py:411 +#: src/iac_code/providers/registry.py:416 msgid "OpenAPI Compatible" msgstr "OpenAPI 兼容" -#: src/iac_code/providers/registry.py:413 +#: src/iac_code/providers/registry.py:418 msgid "Kimi (China)" msgstr "Kimi(中国版)" -#: src/iac_code/providers/registry.py:414 +#: src/iac_code/providers/registry.py:419 msgid "Kimi (International)" msgstr "Kimi(国际版)" -#: src/iac_code/providers/registry.py:415 +#: src/iac_code/providers/registry.py:420 msgid "MiniMax (China)" msgstr "MiniMax(中国版)" -#: src/iac_code/providers/registry.py:416 +#: src/iac_code/providers/registry.py:421 msgid "MiniMax (International)" msgstr "MiniMax(国际版)" -#: src/iac_code/providers/registry.py:418 +#: src/iac_code/providers/registry.py:423 msgid "ZhiPu AI (International)" msgstr "智谱 AI(国际版)" -#: src/iac_code/providers/registry.py:420 +#: src/iac_code/providers/registry.py:425 msgid "SiliconFlow (China)" msgstr "硅基流动(中国版)" -#: src/iac_code/providers/registry.py:421 +#: src/iac_code/providers/registry.py:426 msgid "SiliconFlow (International)" msgstr "硅基流动(国际版)" -#: src/iac_code/providers/registry.py:422 +#: src/iac_code/providers/registry.py:427 msgid "Ollama (Local)" msgstr "Ollama(本地)" -#: src/iac_code/providers/registry.py:423 +#: src/iac_code/providers/registry.py:428 msgid "LM Studio (Local)" msgstr "LM Studio(本地)" -#: src/iac_code/providers/registry.py:426 +#: src/iac_code/providers/registry.py:431 msgid "ModelScope" msgstr "魔搭" -#: src/iac_code/providers/registry.py:427 +#: src/iac_code/providers/registry.py:432 msgid "Alibaba Cloud CodingPlan" msgstr "阿里云编程计划" -#: src/iac_code/providers/registry.py:428 +#: src/iac_code/providers/registry.py:433 msgid "Alibaba Cloud CodingPlan (International)" msgstr "阿里云编程计划(国际版)" -#: src/iac_code/providers/registry.py:429 +#: src/iac_code/providers/registry.py:434 msgid "ZhiPu AI CodingPlan" msgstr "智谱 AI 编程计划" -#: src/iac_code/providers/registry.py:430 +#: src/iac_code/providers/registry.py:435 msgid "ZhiPu AI CodingPlan (International)" msgstr "智谱 AI 编程计划(国际版)" -#: src/iac_code/providers/registry.py:431 +#: src/iac_code/providers/registry.py:436 msgid "Volcengine CodingPlan" msgstr "火山引擎编程计划" -#: src/iac_code/providers/registry.py:432 +#: src/iac_code/providers/registry.py:437 msgid "Anthropic Compatible" msgstr "Anthropic 兼容" @@ -1019,7 +1034,7 @@ msgstr "" "'llm_source: qwenpaw')。" #: src/iac_code/services/permissions/pipeline.py:54 -#: src/iac_code/tools/base.py:185 src/iac_code/tools/bash/bash_tool.py:154 +#: src/iac_code/tools/base.py:190 src/iac_code/tools/bash/bash_tool.py:158 #, python-brace-format msgid "Allow {}?" msgstr "允许 {}?" @@ -1216,11 +1231,11 @@ msgstr "正在运行 {cmd}" msgid "Running command..." msgstr "正在运行命令..." -#: src/iac_code/tools/bash/command_parser.py:41 +#: src/iac_code/tools/bash/command_parser.py:42 msgid "parse error" msgstr "解析错误" -#: src/iac_code/tools/bash/command_parser.py:44 +#: src/iac_code/tools/bash/command_parser.py:46 msgid "unsupported shell construct" msgstr "不支持的 Shell 语法结构" @@ -1229,53 +1244,57 @@ msgstr "不支持的 Shell 语法结构" msgid "path outside allowed directories: {}" msgstr "路径不在允许的目录范围内:{}" -#: src/iac_code/tools/bash/permissions.py:101 +#: src/iac_code/tools/bash/permissions.py:135 #, python-brace-format msgid "matched deny rule(s): {}" msgstr "匹配到拒绝规则:{}" -#: src/iac_code/tools/bash/permissions.py:108 +#: src/iac_code/tools/bash/permissions.py:142 #, python-brace-format msgid "matched ask rule(s): {}" msgstr "匹配到询问规则:{}" -#: src/iac_code/tools/bash/permissions.py:120 -#: src/iac_code/tools/bash/permissions.py:178 +#: src/iac_code/tools/bash/permissions.py:154 +#: src/iac_code/tools/bash/permissions.py:220 #, python-brace-format msgid "matched allow rule(s): {}" msgstr "匹配到允许规则:{}" -#: src/iac_code/tools/bash/permissions.py:129 +#: src/iac_code/tools/bash/permissions.py:162 +msgid "complex command requires confirmation" +msgstr "复杂命令需要确认" + +#: src/iac_code/tools/bash/permissions.py:171 msgid "sed in-place edit requires confirmation" msgstr "sed 原地编辑需要确认" -#: src/iac_code/tools/bash/permissions.py:152 +#: src/iac_code/tools/bash/permissions.py:194 msgid "command failed basic safety checks" msgstr "命令未通过基本安全检查" -#: src/iac_code/tools/bash/permissions.py:168 +#: src/iac_code/tools/bash/permissions.py:210 #, python-brace-format msgid "matched deny rule(s) on full command: {}" msgstr "完整命令匹配到拒绝规则:{}" -#: src/iac_code/tools/bash/permissions.py:185 +#: src/iac_code/tools/bash/permissions.py:227 msgid "command too complex to analyze" msgstr "命令过于复杂,无法分析" -#: src/iac_code/tools/bash/permissions.py:187 +#: src/iac_code/tools/bash/permissions.py:229 msgid "could not parse command" msgstr "无法解析命令" -#: src/iac_code/tools/bash/permissions.py:203 +#: src/iac_code/tools/bash/permissions.py:245 #, python-brace-format msgid "too many subcommands (>{})" msgstr "子命令过多(>{})" -#: src/iac_code/tools/bash/permissions.py:215 +#: src/iac_code/tools/bash/permissions.py:258 msgid "multiple cd commands in compound command" msgstr "复合命令中包含多个 cd 命令" -#: src/iac_code/tools/bash/permissions.py:227 +#: src/iac_code/tools/bash/permissions.py:271 msgid "cd combined with git in compound command" msgstr "复合命令中 cd 与 git 组合使用" @@ -1544,13 +1563,13 @@ msgid "Log file" msgstr "日志文件" #: src/iac_code/ui/renderer.py:351 src/iac_code/ui/renderer.py:621 -#: src/iac_code/ui/renderer.py:1380 +#: src/iac_code/ui/renderer.py:1402 #, python-brace-format msgid "Thought for {seconds:.1f}s" msgstr "思考完成(耗时 {seconds:.1f}s)" #: src/iac_code/ui/renderer.py:367 src/iac_code/ui/renderer.py:653 -#: src/iac_code/ui/renderer.py:1401 +#: src/iac_code/ui/renderer.py:1423 msgid "(ctrl+o to expand)" msgstr "(ctrl+o 展开)" @@ -1610,24 +1629,29 @@ msgstr "是否允许执行此操作?" msgid "Yes, allow once" msgstr "是,仅本次允许" -#: src/iac_code/ui/renderer.py:1304 +#: src/iac_code/ui/renderer.py:1306 #, python-brace-format msgid "Yes, always allow \"{rule}\" (this session)" msgstr "是,本次会话始终允许 \"{rule}\"" -#: src/iac_code/ui/renderer.py:1309 +#: src/iac_code/ui/renderer.py:1311 msgid "Yes, allow always for this tool" msgstr "是,始终允许此工具" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "No, reject once" msgstr "否,仅本次拒绝" -#: src/iac_code/ui/renderer.py:1313 +#: src/iac_code/ui/renderer.py:1314 msgid "default" msgstr "默认" -#: src/iac_code/ui/renderer.py:1314 +#: src/iac_code/ui/renderer.py:1321 +#, python-brace-format +msgid "No, always deny \"{rule}\" (this session)" +msgstr "否,始终拒绝 \"{rule}\"(本次会话)" + +#: src/iac_code/ui/renderer.py:1326 msgid "No, always reject this tool" msgstr "否,始终拒绝此工具" diff --git a/src/iac_code/providers/registry.py b/src/iac_code/providers/registry.py index 435fb267..c72fa0af 100644 --- a/src/iac_code/providers/registry.py +++ b/src/iac_code/providers/registry.py @@ -66,9 +66,14 @@ def model_ids(self) -> list[str]: base_url="https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", models=[ ModelEntry("qwen3.6-plus", is_default=True), + ModelEntry("qwen3.6-flash"), + ModelEntry("deepseek-v4-pro"), + ModelEntry("deepseek-v4-flash"), ModelEntry("deepseek-v3.2"), + ModelEntry("glm-5.1"), ModelEntry("glm-5"), ModelEntry("MiniMax-M2.5"), + ModelEntry("kimi-k2.6"), ModelEntry("kimi-k2.5"), ], qwenpaw_provider_ids=["aliyun-tokenplan"], diff --git a/src/iac_code/tools/base.py b/src/iac_code/tools/base.py index 6025be58..0059beff 100644 --- a/src/iac_code/tools/base.py +++ b/src/iac_code/tools/base.py @@ -157,6 +157,11 @@ def get_tool_use_summary(self, input: dict | None = None) -> str | None: return None # --- Permission methods --- + @property + def supports_blanket_allow(self) -> bool: + """Whether this tool supports blanket 'always allow this tool' permission.""" + return True + @property def timeout(self) -> float | None: """Per-tool timeout in seconds. None means use the global default.""" diff --git a/src/iac_code/tools/bash/bash_tool.py b/src/iac_code/tools/bash/bash_tool.py index 5085039a..5d312983 100644 --- a/src/iac_code/tools/bash/bash_tool.py +++ b/src/iac_code/tools/bash/bash_tool.py @@ -131,6 +131,10 @@ def get_tool_use_summary(self, input: dict | None = None) -> str | None: return input.get("command", "")[:80] return None + @property + def supports_blanket_allow(self) -> bool: + return False + def is_read_only(self, input: dict | None = None) -> bool: return False diff --git a/src/iac_code/tools/bash/command_parser.py b/src/iac_code/tools/bash/command_parser.py index 2bd7e7be..6b3cc23f 100644 --- a/src/iac_code/tools/bash/command_parser.py +++ b/src/iac_code/tools/bash/command_parser.py @@ -23,6 +23,7 @@ class SimpleCommand: text: str argv: list[str] = field(default_factory=list) redirects: list[str] = field(default_factory=list) + is_complex: bool = False @dataclass @@ -40,10 +41,10 @@ def parse_command(command: str) -> ParseResult: if root.has_error or _tree_contains_error(root): return ParseResult(kind="parse_error", reason=_("parse error")) - if _scan_too_complex(root, source): + commands = _extract_simple_commands(root, source) + if not commands and _scan_too_complex(root, source): return ParseResult(kind="too_complex", reason=_("unsupported shell construct")) - commands = _extract_simple_commands(root, source) return ParseResult(kind="simple", commands=commands) @@ -117,15 +118,44 @@ def _command_invoked_name(command_node: Node, source: bytes) -> str | None: return None -def _scan_too_complex(node: Node, source: bytes) -> bool: +def _has_complex_descendant(node: Node, source: bytes) -> bool: + """Recursively check if any descendant is a complex construct.""" if node.type in _TOO_COMPLEX_TYPES: return True if node.type == "expansion" and "`" in _node_text(node, source): return True + for child in node.children: + if _has_complex_descendant(child, source): + return True + return False + + +def _node_is_complex(node: Node, source: bytes) -> bool: + """Check if a single command node is complex (dangerous builtin or complex construct in children).""" if node.type == "command": name = _command_invoked_name(node, source) if name is not None and name in DANGEROUS_BUILTINS: return True + for child in node.children: + if child.type in _TOO_COMPLEX_TYPES: + return True + if child.type == "expansion" and "`" in _node_text(child, source): + return True + if _has_complex_descendant(child, source): + return True + return False + + +def _scan_too_complex(node: Node, source: bytes) -> bool: + """Check if the ENTIRE top-level AST is irreducibly complex (no extractable commands).""" + if node.type in _TOO_COMPLEX_TYPES: + return True + if node.type == "expansion" and "`" in _node_text(node, source): + return True + if node.type in {"program", "list", "pipeline", "compound_statement"}: + return False + if node.type in {"command", "declaration_command", "redirected_statement"}: + return False for child in node.children: if _scan_too_complex(child, source): return True @@ -138,9 +168,10 @@ def _build_simple_command( *, redirects: list[str], text_override: str | None = None, + is_complex: bool = False, ) -> SimpleCommand: text = text_override if text_override is not None else _node_text(node, source) - return SimpleCommand(text=text, argv=_command_argv(node, source), redirects=list(redirects)) + return SimpleCommand(text=text, argv=_command_argv(node, source), redirects=list(redirects), is_complex=is_complex) def _extract_redirected_statement(node: Node, source: bytes) -> list[SimpleCommand]: @@ -154,13 +185,18 @@ def _extract_redirected_statement(node: Node, source: bytes) -> list[SimpleComma if body is None: return [] if body.type == "command": - return [_build_simple_command(body, source, redirects=redirects, text_override=_node_text(node, source))] - # Body is a compound (list, pipeline, etc.) — recurse to collect subcommands - # and attach the redirects to the last subcommand (matching bash semantics). + complex_flag = _node_is_complex(body, source) + return [ + _build_simple_command( + body, source, redirects=redirects, text_override=_node_text(node, source), is_complex=complex_flag + ) + ] inner = _collect_commands(body, source) if inner and redirects: last = inner[-1] - inner[-1] = SimpleCommand(text=last.text, argv=list(last.argv), redirects=list(last.redirects) + redirects) + inner[-1] = SimpleCommand( + text=last.text, argv=list(last.argv), redirects=list(last.redirects) + redirects, is_complex=last.is_complex + ) return inner @@ -178,7 +214,8 @@ def _collect_commands(node: Node, source: bytes) -> list[SimpleCommand]: if kind == "redirected_statement": return _extract_redirected_statement(node, source) if kind == "command": - return [_build_simple_command(node, source, redirects=[])] + complex_flag = _node_is_complex(node, source) + return [_build_simple_command(node, source, redirects=[], is_complex=complex_flag)] if kind == "declaration_command": return [SimpleCommand(text=_node_text(node, source), argv=_declaration_argv(node, source))] diff --git a/src/iac_code/tools/bash/permissions.py b/src/iac_code/tools/bash/permissions.py index 5c1e5713..a67c6036 100644 --- a/src/iac_code/tools/bash/permissions.py +++ b/src/iac_code/tools/bash/permissions.py @@ -30,7 +30,33 @@ def _collect_all_rules(rules_by_source: dict[str, list[str]]) -> list[str]: return out -def _generate_suggestions(command: str) -> list[PermissionRuleValue]: +def _generate_suggestions( + commands: list[SimpleCommand], sub_results: list[PermissionResult] | None = None +) -> list[PermissionRuleValue]: + """Generate suggestions from sub-commands, skipping dangerous builtins and already-allowed ones.""" + from iac_code.tools.bash.command_parser import DANGEROUS_BUILTINS + + seen: set[str] = set() + result: list[PermissionRuleValue] = [] + for i, cmd in enumerate(commands): + if not cmd.argv: + continue + if sub_results and i < len(sub_results) and sub_results[i].behavior == "allow": + continue + base = os.path.basename(cmd.argv[0]) + if not base: + continue + if base in DANGEROUS_BUILTINS: + continue + rule = "{}:*".format(base) + if rule not in seen: + seen.add(rule) + result.append(PermissionRuleValue(tool_name="bash", rule_content=rule)) + return result + + +def _generate_suggestions_from_text(command: str) -> list[PermissionRuleValue]: + """Fallback: generate suggestions from raw command text.""" normalized = normalize_command(command.strip()) first = normalized.split(None, 1)[0] if normalized else "" if not first: @@ -51,10 +77,18 @@ def _merge_results(results: list[PermissionResult]) -> PermissionResult: ) -def _with_suggestions_if_needed(result: PermissionResult, command: str) -> PermissionResult: +def _with_suggestions_if_needed( + result: PermissionResult, + command: str, + commands: list[SimpleCommand] | None = None, + sub_results: list[PermissionResult] | None = None, +) -> PermissionResult: if result.suggestions: return result - sug = _generate_suggestions(command) + if commands: + sug = _generate_suggestions(commands, sub_results=sub_results) + else: + sug = _generate_suggestions_from_text(command) if not sug: return result return PermissionResult( @@ -124,6 +158,14 @@ def bash_tool_check_permission( reason=PermissionDecisionReason(type="rule", detail=detail), ) + if cmd.is_complex: + detail = _("complex command requires confirmation") + return PermissionResult( + behavior="ask", + message=detail, + reason=PermissionDecisionReason(type="complex_command", detail=detail), + ) + base = cmd.argv[0] if cmd.argv else "" if os.path.basename(base) == "sed" and _sed_inplace_edit(cmd.argv): detail = _("sed in-place edit requires confirmation") @@ -208,6 +250,7 @@ async def bash_tool_has_permission(command: str, context: ToolPermissionContext) reason=PermissionDecisionReason(type="compound_limit", detail=detail), ), command, + commands=subcommands, ) cd_bases = [c for c in subcommands if _command_base(c) == "cd"] @@ -220,6 +263,7 @@ async def bash_tool_has_permission(command: str, context: ToolPermissionContext) reason=PermissionDecisionReason(type="compound_cd", detail=detail), ), command, + commands=subcommands, ) has_git = any(_command_base(c) == "git" for c in subcommands) @@ -232,9 +276,10 @@ async def bash_tool_has_permission(command: str, context: ToolPermissionContext) reason=PermissionDecisionReason(type="compound_cd_git", detail=detail), ), command, + commands=subcommands, ) compound_has_cd = bool(cd_bases) sub_results = [bash_tool_check_permission(sc, context, compound_has_cd=compound_has_cd) for sc in subcommands] merged = _merge_results(sub_results) - return _with_suggestions_if_needed(merged, command) + return _with_suggestions_if_needed(merged, command, commands=subcommands, sub_results=sub_results) diff --git a/src/iac_code/ui/renderer.py b/src/iac_code/ui/renderer.py index 4b3094f8..f0cb870d 100644 --- a/src/iac_code/ui/renderer.py +++ b/src/iac_code/ui/renderer.py @@ -1292,29 +1292,39 @@ async def prompt_permission(self, event: PermissionRequestEvent) -> bool: TextOption(label=_("Yes, allow once"), value="allow_once"), ] - has_suggestion = ( - event.permission_result is not None + suggestions = ( + event.permission_result.suggestions + if event.permission_result is not None and hasattr(event.permission_result, "suggestions") and event.permission_result.suggestions + else [] ) - if has_suggestion: - sug = event.permission_result.suggestions[0] + if suggestions: + rules_display = ", ".join(s.rule_content for s in suggestions) options.append( TextOption( - label=_('Yes, always allow "{rule}" (this session)').format(rule=sug.rule_content), + label=_('Yes, always allow "{rule}" (this session)').format(rule=rules_display), value="always_allow_rule", ) ) - else: + elif tool and tool.supports_blanket_allow: options.append(TextOption(label=_("Yes, allow always for this tool"), value="always_allow")) - options.extend( - [ - TextOption(label=_("No, reject once"), value="reject_once", description="({})".format(_("default"))), - TextOption(label=_("No, always reject this tool"), value="always_deny"), - ] + options.append( + TextOption(label=_("No, reject once"), value="reject_once", description="({})".format(_("default"))) ) + if suggestions: + rules_display = ", ".join(s.rule_content for s in suggestions) + options.append( + TextOption( + label=_('No, always deny "{rule}" (this session)').format(rule=rules_display), + value="always_deny_rule", + ) + ) + + options.append(TextOption(label=_("No, always reject this tool"), value="always_deny")) + select = Select( options=options, default_value="reject_once", @@ -1334,17 +1344,29 @@ async def prompt_permission(self, event: PermissionRequestEvent) -> bool: record_permission(cache, tool_name, "always_allow") return True if result == "always_allow_rule": - if has_suggestion and self._app_state_store is not None: + if suggestions and self._app_state_store is not None: perm_ctx = self._app_state_store.get_state().permission_context if perm_ctx is not None: import dataclasses from iac_code.services.permissions.storage import apply_session_rule - sug = event.permission_result.suggestions[0] - new_ctx = apply_session_rule(perm_ctx, "allow", sug) - self._app_state_store.set_state(lambda s: dataclasses.replace(s, permission_context=new_ctx)) + for sug in suggestions: + perm_ctx = apply_session_rule(perm_ctx, "allow", sug) + self._app_state_store.set_state(lambda s: dataclasses.replace(s, permission_context=perm_ctx)) return True + if result == "always_deny_rule": + if suggestions and self._app_state_store is not None: + perm_ctx = self._app_state_store.get_state().permission_context + if perm_ctx is not None: + import dataclasses + + from iac_code.services.permissions.storage import apply_session_rule + + for sug in suggestions: + perm_ctx = apply_session_rule(perm_ctx, "deny", sug) + self._app_state_store.set_state(lambda s: dataclasses.replace(s, permission_context=perm_ctx)) + return False if result == "always_deny": record_permission(cache, tool_name, "always_deny") return False diff --git a/tests/acp/test_permission_rules.py b/tests/acp/test_permission_rules.py new file mode 100644 index 00000000..f06fcbc4 --- /dev/null +++ b/tests/acp/test_permission_rules.py @@ -0,0 +1,336 @@ +"""Tests for ACP rule-level permission support.""" + +from __future__ import annotations + +from dataclasses import dataclass +from unittest.mock import MagicMock + +import acp +import acp.schema +import pytest + +from iac_code.acp.session import ( + _OPTION_ALLOW_ALWAYS, + _OPTION_ALLOW_ONCE, + _OPTION_REJECT_ALWAYS, + _OPTION_REJECT_ONCE, + _PREFIX_ALLOW_RULE, + _PREFIX_DENY_RULE, + ACPSession, +) +from iac_code.types.permissions import PermissionRuleValue, ToolPermissionContext +from iac_code.types.stream_events import MessageEndEvent, PermissionRequestEvent, TextDeltaEvent, Usage + + +@dataclass +class FakePermissionResult: + behavior: str = "ask" + message: str = "" + suggestions: list[PermissionRuleValue] | None = None + + +class _FakeLoop: + def __init__(self, permission_context=None): + self._permission_context = permission_context + + async def run_streaming(self, prompt: str): + yield TextDeltaEvent(text="ok") + yield MessageEndEvent(stop_reason="stop", usage=Usage()) + + +class _FakeConn: + def __init__(self, outcome): + self._outcome = outcome + self.last_options: list = [] + self.last_content: str = "" + + async def session_update(self, session_id, update, **kwargs): + pass + + async def request_permission(self, options, session_id, tool_call_update): + self.last_options = options + for content_item in tool_call_update.content: + if hasattr(content_item, "content") and hasattr(content_item.content, "text"): + self.last_content = content_item.content.text + return self._outcome + + +def _make_allowed_outcome(option_id: str): + outcome = acp.schema.AllowedOutcome(outcome="selected", optionId=option_id) + return MagicMock(outcome=outcome) + + +def _make_denied_outcome(option_id: str | None = None): + """Build a fake RequestPermissionResponse with DeniedOutcome. + + For DeniedOutcome, the ACP protocol has no option_id field on the outcome itself. + Clients encode the selected option in response.field_meta["option_id"]. + """ + outcome = acp.schema.DeniedOutcome(outcome="cancelled") + response = MagicMock(outcome=outcome) + response.field_meta = {"option_id": option_id} if option_id else {} + return response + + +def _make_event(tool_name="bash", tool_input=None, suggestions=None): + perm_result = FakePermissionResult(suggestions=suggestions) if suggestions else None + return PermissionRequestEvent( + tool_name=tool_name, + tool_input=tool_input or {"command": "git status"}, + tool_use_id="tu-123", + permission_result=perm_result, + ) + + +# --------------------------------------------------------------------------- +# Test: Dynamic option generation with suggestions +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_options_include_rule_suggestions_when_present(): + """When suggestions exist, options include rule-level allow/deny.""" + suggestions = [PermissionRuleValue(tool_name="bash", rule_content="git:*")] + conn = _FakeConn(_make_allowed_outcome(_OPTION_ALLOW_ONCE)) + session = ACPSession("s1", _FakeLoop(), conn) + event = _make_event(suggestions=suggestions) + + await session._request_permission(event) + + option_ids = [opt.option_id for opt in conn.last_options] + assert _OPTION_ALLOW_ONCE in option_ids + assert _PREFIX_ALLOW_RULE + "git:*" in option_ids + assert _PREFIX_DENY_RULE + "git:*" in option_ids + assert _OPTION_REJECT_ONCE in option_ids + assert _OPTION_REJECT_ALWAYS in option_ids + # allow_always (tool-level) should NOT be present when suggestions exist + assert _OPTION_ALLOW_ALWAYS not in option_ids + + +@pytest.mark.asyncio +async def test_options_fallback_to_tool_level_without_suggestions(): + """Without suggestions, options include tool-level allow_always.""" + conn = _FakeConn(_make_allowed_outcome(_OPTION_ALLOW_ONCE)) + session = ACPSession("s1", _FakeLoop(), conn) + event = _make_event(suggestions=None) + + await session._request_permission(event) + + option_ids = [opt.option_id for opt in conn.last_options] + assert _OPTION_ALLOW_ALWAYS in option_ids + # Rule-level options should NOT be present + assert not any(oid.startswith(_PREFIX_ALLOW_RULE) for oid in option_ids) + assert not any(oid.startswith(_PREFIX_DENY_RULE) for oid in option_ids) + + +# --------------------------------------------------------------------------- +# Test: allow_rule response applies rule to permission_context +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_allow_rule_applies_to_permission_context(): + """allow_rule:git:* → rule added to allow_rules['session'], returns True.""" + perm_ctx = ToolPermissionContext(cwd="/tmp") + conn = _FakeConn(_make_allowed_outcome(_PREFIX_ALLOW_RULE + "git:*")) + loop = _FakeLoop(permission_context=perm_ctx) + session = ACPSession("s1", loop, conn) + + suggestions = [PermissionRuleValue(tool_name="bash", rule_content="git:*")] + event = _make_event(suggestions=suggestions) + + result = await session._request_permission(event) + + assert result is True + updated_ctx = loop._permission_context + assert "session" in updated_ctx.allow_rules + assert "bash(git:*)" in updated_ctx.allow_rules["session"] + + +@pytest.mark.asyncio +async def test_allow_rule_multiple_suggestions(): + """allow_rule:curl:*,wget:* → both rules added.""" + perm_ctx = ToolPermissionContext(cwd="/tmp") + conn = _FakeConn(_make_allowed_outcome(_PREFIX_ALLOW_RULE + "curl:*,wget:*")) + loop = _FakeLoop(permission_context=perm_ctx) + session = ACPSession("s1", loop, conn) + + suggestions = [ + PermissionRuleValue(tool_name="bash", rule_content="curl:*"), + PermissionRuleValue(tool_name="bash", rule_content="wget:*"), + ] + event = _make_event(suggestions=suggestions) + + result = await session._request_permission(event) + + assert result is True + rules = loop._permission_context.allow_rules.get("session", []) + assert "bash(curl:*)" in rules + assert "bash(wget:*)" in rules + + +# --------------------------------------------------------------------------- +# Test: deny_rule response applies rule to permission_context +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_deny_rule_applies_to_permission_context(): + """deny_rule:curl:* → rule added to deny_rules['session'], returns False.""" + perm_ctx = ToolPermissionContext(cwd="/tmp") + conn = _FakeConn(_make_denied_outcome(_PREFIX_DENY_RULE + "curl:*")) + loop = _FakeLoop(permission_context=perm_ctx) + session = ACPSession("s1", loop, conn) + + suggestions = [PermissionRuleValue(tool_name="bash", rule_content="curl:*")] + event = _make_event(suggestions=suggestions) + + result = await session._request_permission(event) + + assert result is False + updated_ctx = loop._permission_context + assert "session" in updated_ctx.deny_rules + assert "bash(curl:*)" in updated_ctx.deny_rules["session"] + + +# --------------------------------------------------------------------------- +# Test: Existing tool-level behaviors unchanged +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_allow_once_returns_true_no_cache(): + """allow_once → returns True, no cache entry.""" + conn = _FakeConn(_make_allowed_outcome(_OPTION_ALLOW_ONCE)) + session = ACPSession("s1", _FakeLoop(), conn) + event = _make_event() + + result = await session._request_permission(event) + + assert result is True + assert "bash" not in session._permission_cache + + +@pytest.mark.asyncio +async def test_allow_always_caches_tool(): + """allow_always → returns True, caches tool-level decision.""" + conn = _FakeConn(_make_allowed_outcome(_OPTION_ALLOW_ALWAYS)) + session = ACPSession("s1", _FakeLoop(), conn) + event = _make_event() + + result = await session._request_permission(event) + + assert result is True + assert session._permission_cache.get("bash") == "always_allow" + + +@pytest.mark.asyncio +async def test_reject_once_returns_false(): + """reject_once → returns False, no cache entry.""" + conn = _FakeConn(_make_denied_outcome(_OPTION_REJECT_ONCE)) + session = ACPSession("s1", _FakeLoop(), conn) + event = _make_event() + + result = await session._request_permission(event) + + assert result is False + assert "bash" not in session._permission_cache + + +@pytest.mark.asyncio +async def test_reject_always_caches_tool(): + """reject_always → returns False, caches tool-level decision.""" + conn = _FakeConn(_make_denied_outcome(_OPTION_REJECT_ALWAYS)) + session = ACPSession("s1", _FakeLoop(), conn) + event = _make_event() + + result = await session._request_permission(event) + + assert result is False + assert session._permission_cache.get("bash") == "always_deny" + + +# --------------------------------------------------------------------------- +# Test: Cache short-circuits +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_cached_allow_skips_permission_request(): + """Cached always_allow short-circuits without calling request_permission.""" + conn = _FakeConn(_make_allowed_outcome(_OPTION_ALLOW_ONCE)) + session = ACPSession("s1", _FakeLoop(), conn) + session._permission_cache["bash"] = "always_allow" + event = _make_event() + + result = await session._request_permission(event) + + assert result is True + assert conn.last_options == [] # request_permission was never called + + +@pytest.mark.asyncio +async def test_cached_deny_skips_permission_request(): + """Cached always_deny short-circuits without calling request_permission.""" + conn = _FakeConn(_make_denied_outcome()) + session = ACPSession("s1", _FakeLoop(), conn) + session._permission_cache["bash"] = "always_deny" + event = _make_event() + + result = await session._request_permission(event) + + assert result is False + assert conn.last_options == [] + + +# --------------------------------------------------------------------------- +# Test: Content includes rule context +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_content_includes_suggested_rule(): + """ToolCallUpdate content includes suggested rule when suggestions exist.""" + suggestions = [PermissionRuleValue(tool_name="bash", rule_content="git:*")] + conn = _FakeConn(_make_allowed_outcome(_OPTION_ALLOW_ONCE)) + session = ACPSession("s1", _FakeLoop(), conn) + event = _make_event(suggestions=suggestions) + + await session._request_permission(event) + + assert "Suggested rule: git:*" in conn.last_content + assert "bash" in conn.last_content + + +@pytest.mark.asyncio +async def test_content_no_suggested_rule_without_suggestions(): + """ToolCallUpdate content does not include 'Suggested rule' when no suggestions.""" + conn = _FakeConn(_make_allowed_outcome(_OPTION_ALLOW_ONCE)) + session = ACPSession("s1", _FakeLoop(), conn) + event = _make_event(suggestions=None) + + await session._request_permission(event) + + assert "Suggested rule" not in conn.last_content + + +# --------------------------------------------------------------------------- +# Test: No permission_context graceful handling +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_allow_rule_without_permission_context_still_returns_true(): + """allow_rule still returns True even when no permission_context is available.""" + conn = _FakeConn(_make_allowed_outcome(_PREFIX_ALLOW_RULE + "git:*")) + loop = _FakeLoop(permission_context=None) + session = ACPSession("s1", loop, conn) + + suggestions = [PermissionRuleValue(tool_name="bash", rule_content="git:*")] + event = _make_event(suggestions=suggestions) + + result = await session._request_permission(event) + + assert result is True + # No crash, permission_context remains None + assert loop._permission_context is None diff --git a/tests/acp/test_permission_rules_e2e.py b/tests/acp/test_permission_rules_e2e.py new file mode 100644 index 00000000..10508145 --- /dev/null +++ b/tests/acp/test_permission_rules_e2e.py @@ -0,0 +1,137 @@ +"""End-to-end test: ACP permission flow with rule-level options. + +Verifies that the ACP session generates dynamic rule-level permission options +and correctly applies rules to the permission_context when selected. + +Run with: + uv run python -m pytest tests/acp/test_permission_rules_e2e.py -v -s +""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import acp +import acp.schema +import pytest + +from iac_code.acp.session import ACPSession +from iac_code.types.permissions import PermissionRuleValue, ToolPermissionContext +from iac_code.types.stream_events import MessageEndEvent, PermissionRequestEvent, TextDeltaEvent, Usage + + +class _PermissionTriggeringLoop: + """Fake agent loop that emits a PermissionRequestEvent with suggestions.""" + + def __init__(self, suggestions: list[PermissionRuleValue]): + self._permission_context = ToolPermissionContext(cwd="/tmp") + self._suggestions = suggestions + + async def run_streaming(self, prompt: str): + yield TextDeltaEvent(text="Running command...") + yield PermissionRequestEvent( + tool_name="bash", + tool_input={"command": "git push origin main"}, + tool_use_id="tu-perm-1", + permission_result=MagicMock( + behavior="ask", + suggestions=self._suggestions, + ), + ) + yield MessageEndEvent(stop_reason="stop", usage=Usage()) + + +class _CapturingConn: + """Fake ACP connection that captures permission requests and returns a configurable response.""" + + def __init__(self, response_option_id: str = "allow_once"): + self.updates: list = [] + self.captured_options: list[acp.schema.PermissionOption] = [] + self.captured_content: str = "" + self._response_option_id = response_option_id + + async def session_update(self, session_id, update, **kwargs): + self.updates.append(update) + + async def request_permission(self, options, session_id, tool_call_update): + self.captured_options = options + for item in tool_call_update.content: + if hasattr(item, "content") and hasattr(item.content, "text"): + self.captured_content = item.content.text + + # Determine if response is allow or deny + if self._response_option_id.startswith("allow") or self._response_option_id == "allow_once": + outcome = acp.schema.AllowedOutcome(outcome="selected", optionId=self._response_option_id) + else: + outcome = acp.schema.DeniedOutcome(outcome="cancelled") + resp = MagicMock(outcome=outcome, field_meta={"option_id": self._response_option_id}) + return resp + + +@pytest.mark.asyncio +async def test_permission_request_shows_rule_options(): + """ACP session generates rule-level options when suggestions are present.""" + suggestions = [PermissionRuleValue(tool_name="bash", rule_content="git:*")] + conn = _CapturingConn(response_option_id="allow_once") + loop = _PermissionTriggeringLoop(suggestions) + session = ACPSession("test-e2e-1", loop, conn) + + await session.prompt([acp.schema.TextContentBlock(type="text", text="push my code")]) + + option_ids = [opt.option_id for opt in conn.captured_options] + print("\n[Permission Options]:", option_ids) + + assert "allow_once" in option_ids + assert "allow_rule:git:*" in option_ids + assert "reject_once" in option_ids + assert "deny_rule:git:*" in option_ids + assert "reject_always" in option_ids + # tool-level allow_always should NOT be present when rule suggestions exist + assert "allow_always" not in option_ids + + +@pytest.mark.asyncio +async def test_permission_content_shows_command_and_rule(): + """ToolCallUpdate content includes the command and suggested rule.""" + suggestions = [PermissionRuleValue(tool_name="bash", rule_content="git:*")] + conn = _CapturingConn(response_option_id="allow_once") + loop = _PermissionTriggeringLoop(suggestions) + session = ACPSession("test-e2e-2", loop, conn) + + await session.prompt([acp.schema.TextContentBlock(type="text", text="push")]) + + print("\n[Content]:", conn.captured_content) + assert "git push origin main" in conn.captured_content + assert "Suggested rule: git:*" in conn.captured_content + + +@pytest.mark.asyncio +async def test_selecting_allow_rule_persists_to_permission_context(): + """Selecting allow_rule:git:* writes the rule to permission_context.allow_rules['session'].""" + suggestions = [PermissionRuleValue(tool_name="bash", rule_content="git:*")] + conn = _CapturingConn(response_option_id="allow_rule:git:*") + loop = _PermissionTriggeringLoop(suggestions) + session = ACPSession("test-e2e-3", loop, conn) + + await session.prompt([acp.schema.TextContentBlock(type="text", text="push")]) + + perm_ctx = loop._permission_context + print("\n[Allow Rules]:", perm_ctx.allow_rules) + assert "session" in perm_ctx.allow_rules + assert "bash(git:*)" in perm_ctx.allow_rules["session"] + + +@pytest.mark.asyncio +async def test_selecting_deny_rule_persists_to_permission_context(): + """Selecting deny_rule:git:* writes the rule to permission_context.deny_rules['session'].""" + suggestions = [PermissionRuleValue(tool_name="bash", rule_content="git:*")] + conn = _CapturingConn(response_option_id="deny_rule:git:*") + loop = _PermissionTriggeringLoop(suggestions) + session = ACPSession("test-e2e-4", loop, conn) + + await session.prompt([acp.schema.TextContentBlock(type="text", text="push")]) + + perm_ctx = loop._permission_context + print("\n[Deny Rules]:", perm_ctx.deny_rules) + assert "session" in perm_ctx.deny_rules + assert "bash(git:*)" in perm_ctx.deny_rules["session"] diff --git a/tests/commands/test_auth_flows.py b/tests/commands/test_auth_flows.py index ed9f6702..e553f625 100644 --- a/tests/commands/test_auth_flows.py +++ b/tests/commands/test_auth_flows.py @@ -370,3 +370,48 @@ def test_region_flow_creates_new_credential_when_missing(self, monkeypatch): assert "Configured" in result assert saved["credential"].region_id == "cn-hangzhou" + + +class TestAuthLlmSourceLock: + def test_auth_flow_locked_shows_cloud_select_with_lock_notice(self, monkeypatch): + """When llm_source is 'qwenpaw', _auth_flow shows lock notice in _select title.""" + titles_seen = [] + + def fake_select(title, options, default_index=0): + titles_seen.append(title) + return 0 # select first cloud provider (aliyun) + + monkeypatch.setattr("iac_code.commands.auth.get_llm_source", lambda: "qwenpaw") + monkeypatch.setattr("iac_code.commands.auth._select", fake_select) + monkeypatch.setattr("iac_code.commands.auth._aliyun_auth_flow", lambda: "cloud done") + result = _auth_flow(MagicMock(), MagicMock()) + assert result == "cloud done" + assert any("qwenpaw" in t for t in titles_seen) + + def test_auth_flow_locked_env_shows_lock_notice(self, monkeypatch): + """When llm_source is 'env', lock notice mentions 'env'.""" + titles_seen = [] + + def fake_select(title, options, default_index=0): + titles_seen.append(title) + return 0 + + monkeypatch.setattr("iac_code.commands.auth.get_llm_source", lambda: "env") + monkeypatch.setattr("iac_code.commands.auth._select", fake_select) + monkeypatch.setattr("iac_code.commands.auth._aliyun_auth_flow", lambda: "cloud done") + _auth_flow(MagicMock(), MagicMock()) + assert any("env" in t for t in titles_seen) + + def test_auth_flow_locked_escape_returns_cancelled(self, monkeypatch): + """When locked and user presses Esc, return cancelled.""" + monkeypatch.setattr("iac_code.commands.auth.get_llm_source", lambda: "qwenpaw") + monkeypatch.setattr("iac_code.commands.auth._select", lambda title, options, default_index=0: None) + result = _auth_flow(MagicMock(), MagicMock()) + assert "cancel" in result.lower() + + def test_auth_flow_normal_when_local(self, monkeypatch): + """When llm_source is 'local', _auth_flow shows category selection as usual.""" + monkeypatch.setattr("iac_code.commands.auth.get_llm_source", lambda: "local") + monkeypatch.setattr("iac_code.commands.auth._select", lambda title, options, default_index=0: None) + result = _auth_flow(MagicMock(), MagicMock()) + assert "cancel" in result.lower() diff --git a/tests/commands/test_model.py b/tests/commands/test_model.py index 500a52b9..5d73443c 100644 --- a/tests/commands/test_model.py +++ b/tests/commands/test_model.py @@ -11,6 +11,43 @@ ) +@pytest.mark.asyncio +class TestModelLocked: + async def test_model_locked_when_qwenpaw(self, monkeypatch): + monkeypatch.setattr("iac_code.commands.model.get_llm_source", lambda: "qwenpaw") + store = MagicMock() + context = MagicMock(store=store) + result = await model_command(context=context) + assert "locked" in result.lower() + assert "qwenpaw" in result + + async def test_model_locked_when_env(self, monkeypatch): + monkeypatch.setattr("iac_code.commands.model.get_llm_source", lambda: "env") + store = MagicMock() + context = MagicMock(store=store) + result = await model_command(context=context) + assert "locked" in result.lower() + assert "env" in result + + async def test_model_locked_with_args(self, monkeypatch): + monkeypatch.setattr("iac_code.commands.model.get_llm_source", lambda: "qwenpaw") + store = MagicMock() + context = MagicMock(store=store) + result = await model_command(context=context, args=["gpt-4"]) + assert "locked" in result.lower() + assert "qwenpaw" in result + + async def test_model_not_locked_when_local(self, monkeypatch): + monkeypatch.setattr("iac_code.commands.model.get_llm_source", lambda: "local") + monkeypatch.setattr("iac_code.commands.model.get_active_provider_key", lambda: "anthropic") + monkeypatch.setattr("iac_code.commands.model.save_active_provider_config", lambda p, m: None) + store = MagicMock() + context = MagicMock(store=store) + result = await model_command(context=context, args=["claude-opus-4-6"]) + assert "claude-opus-4-6" in result + assert "locked" not in result.lower() + + @pytest.fixture def fake_provider(): return { @@ -54,6 +91,7 @@ def test_returns_empty_when_no_active(self, monkeypatch): class TestModelCommand: async def test_explicit_args_switches_model(self, monkeypatch): calls = [] + monkeypatch.setattr("iac_code.commands.model.get_llm_source", lambda: "local") monkeypatch.setattr("iac_code.commands.model.get_active_provider_key", lambda: "anthropic") monkeypatch.setattr( "iac_code.commands.model.save_active_provider_config", @@ -69,6 +107,7 @@ async def test_explicit_args_switches_model(self, monkeypatch): store.set_state.assert_called_with(model="claude-opus-4-6") async def test_no_context_no_console_returns_current(self, monkeypatch): + monkeypatch.setattr("iac_code.commands.model.get_llm_source", lambda: "local") monkeypatch.setattr("iac_code.commands.model.get_active_provider_key", lambda: "anthropic") store = MagicMock() store.get_state.return_value = MagicMock(model="claude-sonnet-4-6") @@ -76,6 +115,7 @@ async def test_no_context_no_console_returns_current(self, monkeypatch): assert "claude-sonnet-4-6" in result async def test_no_configured_providers(self, monkeypatch): + monkeypatch.setattr("iac_code.commands.model.get_llm_source", lambda: "local") monkeypatch.setattr("iac_code.commands.model.get_configured_providers", lambda: []) monkeypatch.setattr("iac_code.commands.model.get_active_provider_key", lambda: None) store = MagicMock() @@ -89,6 +129,7 @@ async def test_no_configured_providers(self, monkeypatch): async def test_interactive_back_keeps_model(self, monkeypatch): from iac_code.commands.auth import _BACK + monkeypatch.setattr("iac_code.commands.model.get_llm_source", lambda: "local") monkeypatch.setattr("iac_code.commands.model.get_configured_providers", lambda: ["anthropic"]) monkeypatch.setattr("iac_code.commands.model.get_active_provider_key", lambda: "anthropic") monkeypatch.setattr( @@ -105,6 +146,7 @@ async def test_interactive_back_keeps_model(self, monkeypatch): assert "kept" in result.lower() or "claude-sonnet-4-6" in result async def test_interactive_selects_new_model(self, monkeypatch): + monkeypatch.setattr("iac_code.commands.model.get_llm_source", lambda: "local") monkeypatch.setattr("iac_code.commands.model.get_configured_providers", lambda: ["anthropic"]) monkeypatch.setattr("iac_code.commands.model.get_active_provider_key", lambda: "anthropic") monkeypatch.setattr( diff --git a/tests/tools/bash/test_command_parser.py b/tests/tools/bash/test_command_parser.py index 64cf48d1..cb1a4b0e 100644 --- a/tests/tools/bash/test_command_parser.py +++ b/tests/tools/bash/test_command_parser.py @@ -39,25 +39,87 @@ def test_semicolon(self): class TestParseTooComplex: - def test_command_substitution(self): + def test_command_substitution_marks_complex(self): r = parse_command("echo $(whoami)") - assert r.kind == "too_complex" + assert r.kind == "simple" + assert len(r.commands) == 1 + assert r.commands[0].is_complex is True - def test_backtick_substitution(self): + def test_backtick_substitution_marks_complex(self): r = parse_command("echo `whoami`") - assert r.kind == "too_complex" + assert r.kind == "simple" + assert len(r.commands) == 1 + assert r.commands[0].is_complex is True - def test_eval(self): + def test_eval_marks_complex(self): r = parse_command("eval 'rm -rf /'") - assert r.kind == "too_complex" + assert r.kind == "simple" + assert len(r.commands) == 1 + assert r.commands[0].is_complex is True - def test_source(self): + def test_source_marks_complex(self): r = parse_command("source ~/.bashrc") - assert r.kind == "too_complex" + assert r.kind == "simple" + assert len(r.commands) == 1 + assert r.commands[0].is_complex is True - def test_exec(self): + def test_exec_marks_complex(self): r = parse_command("exec /bin/bash") - assert r.kind == "too_complex" + assert r.kind == "simple" + assert len(r.commands) == 1 + assert r.commands[0].is_complex is True + + def test_standalone_subshell_marks_complex(self): + r = parse_command("$(whoami)") + assert r.kind == "simple" + assert len(r.commands) == 1 + assert r.commands[0].is_complex is True + + +class TestIsComplexField: + def test_simple_command_not_complex(self): + r = parse_command("ls -la") + assert r.kind == "simple" + assert r.commands[0].is_complex is False + + def test_eval_in_compound_marks_only_eval_complex(self): + r = parse_command("eval ls && mkdir -p xxx") + assert r.kind == "simple" + assert len(r.commands) == 2 + assert r.commands[0].is_complex is True + assert "eval" in r.commands[0].argv[0] + assert r.commands[1].is_complex is False + assert r.commands[1].argv[0] == "mkdir" + + def test_exec_in_compound_marks_only_exec_complex(self): + r = parse_command("exec /bin/bash && echo hello") + assert r.kind == "simple" + assert r.commands[0].is_complex is True + assert r.commands[1].is_complex is False + + def test_source_in_compound_marks_only_source_complex(self): + r = parse_command("source ~/.bashrc && ls") + assert r.kind == "simple" + assert r.commands[0].is_complex is True + assert r.commands[1].is_complex is False + + def test_command_substitution_in_arg_marks_complex(self): + r = parse_command("mkdir $(echo dir) && ls") + assert r.kind == "simple" + assert len(r.commands) == 2 + assert r.commands[0].is_complex is True + assert r.commands[1].is_complex is False + + def test_all_simple_commands_not_complex(self): + r = parse_command("ls && cat foo") + assert r.kind == "simple" + assert all(c.is_complex is False for c in r.commands) + + def test_pipe_with_eval_marks_eval_complex(self): + r = parse_command("eval 'ls' | grep foo") + assert r.kind == "simple" + assert r.commands[0].is_complex is True + assert r.commands[1].is_complex is False class TestParseEdgeCases: diff --git a/tests/tools/bash/test_permissions.py b/tests/tools/bash/test_permissions.py index 0ee0e2a6..6edb3489 100644 --- a/tests/tools/bash/test_permissions.py +++ b/tests/tools/bash/test_permissions.py @@ -70,3 +70,27 @@ def test_passthrough_for_unknown(self): cmd = SimpleCommand(text="docker build .", argv=["docker", "build", "."]) r = bash_tool_check_permission(cmd, _ctx()) assert r.behavior == "passthrough" + + +class TestIsComplexPermission: + def test_complex_command_defaults_to_ask(self): + cmd = SimpleCommand(text="eval ls", argv=["eval", "ls"], is_complex=True) + r = bash_tool_check_permission(cmd, _ctx()) + assert r.behavior == "ask" + + def test_complex_command_allow_rule_still_works(self): + ctx = _ctx(allow={"session": ["bash(eval:*)"]}) + cmd = SimpleCommand(text="eval ls", argv=["eval", "ls"], is_complex=True) + r = bash_tool_check_permission(cmd, ctx) + assert r.behavior == "allow" + + def test_complex_command_deny_rule_still_works(self): + ctx = _ctx(deny={"session": ["bash(eval:*)"]}) + cmd = SimpleCommand(text="eval ls", argv=["eval", "ls"], is_complex=True) + r = bash_tool_check_permission(cmd, ctx) + assert r.behavior == "deny" + + def test_non_complex_command_not_affected(self): + cmd = SimpleCommand(text="docker build .", argv=["docker", "build", "."], is_complex=False) + r = bash_tool_check_permission(cmd, _ctx()) + assert r.behavior == "passthrough" diff --git a/tests/tools/bash/test_permissions_integration.py b/tests/tools/bash/test_permissions_integration.py index 7579f9c5..63e757bd 100644 --- a/tests/tools/bash/test_permissions_integration.py +++ b/tests/tools/bash/test_permissions_integration.py @@ -373,3 +373,49 @@ async def test_compound_generates_suggestion(self): ctx = _ctx(cwd="/project") r = await bash_tool_has_permission("cd /project && mkdir foo", ctx) assert r.suggestions + + +class TestCompoundComplexSuggestions: + """Verify compound commands with complex sub-commands generate correct suggestions.""" + + @pytest.mark.asyncio + async def test_eval_and_mkdir_generates_mkdir_suggestion_only(self): + """eval ls && mkdir -p xxx: suggestion should be mkdir:*, not eval:*.""" + ctx = _ctx(cwd="/project") + r = await bash_tool_has_permission("eval ls && mkdir -p xxx", ctx) + assert r.behavior == "ask" + assert r.suggestions + rule_contents = [s.rule_content for s in r.suggestions] + assert "mkdir:*" in rule_contents + assert "eval:*" not in rule_contents + + @pytest.mark.asyncio + async def test_compound_all_allowed_still_allows(self): + """ls && cat foo: both readonly allowed.""" + ctx = _ctx() + r = await bash_tool_has_permission("ls && cat foo", ctx) + assert r.behavior == "allow" + + @pytest.mark.asyncio + async def test_compound_one_allowed_one_not(self): + """ls && mkdir foo: ls allowed, mkdir not → ask.""" + ctx = _ctx(cwd="/project") + r = await bash_tool_has_permission("ls && mkdir foo", ctx) + assert r.behavior in ("ask", "passthrough") + + @pytest.mark.asyncio + async def test_eval_with_allow_rule_and_mkdir_without(self): + """eval:* allowed, mkdir not → ask for mkdir.""" + ctx = _ctx(allow={"session": ["bash(eval:*)"]}, cwd="/project") + r = await bash_tool_has_permission("eval ls && mkdir -p xxx", ctx) + assert r.behavior in ("ask", "passthrough") + + @pytest.mark.asyncio + async def test_echo_with_subst_generates_echo_suggestion(self): + """echo $(whoami): echo is not dangerous, should generate echo:* suggestion.""" + ctx = _ctx() + r = await bash_tool_has_permission("echo $(whoami)", ctx) + assert r.behavior == "ask" + assert r.suggestions + rule_contents = [s.rule_content for s in r.suggestions] + assert "echo:*" in rule_contents diff --git a/tests/ui/test_renderer_prompt_permission.py b/tests/ui/test_renderer_prompt_permission.py index 2a298b92..16742558 100644 --- a/tests/ui/test_renderer_prompt_permission.py +++ b/tests/ui/test_renderer_prompt_permission.py @@ -14,10 +14,10 @@ from iac_code.ui.renderer import Renderer -def _make_renderer(app_state_store=None) -> Renderer: +def _make_renderer(app_state_store=None, tool=None) -> Renderer: console = Console(record=True) tool_registry = MagicMock() - tool_registry.get.return_value = None # no tool; renderer uses event.tool_name as display + tool_registry.get.return_value = tool return Renderer(console, tool_registry, app_state_store=app_state_store) @@ -118,3 +118,240 @@ async def test_prompt_with_store_none_still_works(self): with _patch_select("allow_once"): result = await renderer.prompt_permission(event) assert result is True + + +def _make_event_with_suggestion(tool_name: str = "bash") -> PermissionRequestEvent: + from iac_code.types.permissions import PermissionResult, PermissionRuleValue + + fut: asyncio.Future[bool] = asyncio.get_event_loop().create_future() + return PermissionRequestEvent( + tool_name=tool_name, + tool_input={"command": "mkdir foo"}, + tool_use_id="t1", + response_future=fut, + permission_result=PermissionResult( + behavior="ask", + suggestions=[PermissionRuleValue(tool_name="bash", rule_content="mkdir:*")], + ), + ) + + +class TestRuleLevelDeny: + @pytest.mark.asyncio + async def test_always_deny_rule_returns_false(self): + """Selecting 'always_deny_rule' should return False.""" + import dataclasses + + from iac_code.types.permissions import ToolPermissionContext + + store = AppStateStore() + store.set_state(lambda s: dataclasses.replace(s, permission_context=ToolPermissionContext())) + + renderer = _make_renderer(store) + event = _make_event_with_suggestion() + with _patch_select("always_deny_rule"): + result = await renderer.prompt_permission(event) + assert result is False + + @pytest.mark.asyncio + async def test_always_deny_rule_adds_deny_session_rule(self): + """'always_deny_rule' should apply a deny session rule to the permission context.""" + import dataclasses + + from iac_code.types.permissions import ToolPermissionContext + + store = AppStateStore() + store.set_state(lambda s: dataclasses.replace(s, permission_context=ToolPermissionContext())) + + renderer = _make_renderer(store) + event = _make_event_with_suggestion() + with _patch_select("always_deny_rule"): + await renderer.prompt_permission(event) + + ctx = store.get_state().permission_context + deny_rules = ctx.deny_rules.get("session", []) + assert any("mkdir:*" in r for r in deny_rules) + + @pytest.mark.asyncio + async def test_always_allow_rule_adds_allow_session_rule(self): + """'always_allow_rule' should apply an allow session rule.""" + import dataclasses + + from iac_code.types.permissions import ToolPermissionContext + + store = AppStateStore() + store.set_state(lambda s: dataclasses.replace(s, permission_context=ToolPermissionContext())) + + renderer = _make_renderer(store) + event = _make_event_with_suggestion() + with _patch_select("always_allow_rule"): + result = await renderer.prompt_permission(event) + + assert result is True + ctx = store.get_state().permission_context + allow_rules = ctx.allow_rules.get("session", []) + assert any("mkdir:*" in r for r in allow_rules) + + +def _make_event_with_multiple_suggestions(tool_name: str = "bash") -> PermissionRequestEvent: + from iac_code.types.permissions import PermissionResult, PermissionRuleValue + + fut: asyncio.Future[bool] = asyncio.get_event_loop().create_future() + return PermissionRequestEvent( + tool_name=tool_name, + tool_input={"command": "mkdir -p a && rm -rf b"}, + tool_use_id="t1", + response_future=fut, + permission_result=PermissionResult( + behavior="ask", + suggestions=[ + PermissionRuleValue(tool_name="bash", rule_content="mkdir:*"), + PermissionRuleValue(tool_name="bash", rule_content="rm:*"), + ], + ), + ) + + +class TestMultipleSuggestions: + @pytest.mark.asyncio + async def test_allow_rule_applies_all_suggestions(self): + """'always_allow_rule' with multiple suggestions should apply all rules.""" + import dataclasses + + from iac_code.types.permissions import ToolPermissionContext + + store = AppStateStore() + store.set_state(lambda s: dataclasses.replace(s, permission_context=ToolPermissionContext())) + + renderer = _make_renderer(store) + event = _make_event_with_multiple_suggestions() + with _patch_select("always_allow_rule"): + result = await renderer.prompt_permission(event) + + assert result is True + ctx = store.get_state().permission_context + allow_rules = ctx.allow_rules.get("session", []) + assert any("mkdir:*" in r for r in allow_rules) + assert any("rm:*" in r for r in allow_rules) + + @pytest.mark.asyncio + async def test_deny_rule_applies_all_suggestions(self): + """'always_deny_rule' with multiple suggestions should apply all rules.""" + import dataclasses + + from iac_code.types.permissions import ToolPermissionContext + + store = AppStateStore() + store.set_state(lambda s: dataclasses.replace(s, permission_context=ToolPermissionContext())) + + renderer = _make_renderer(store) + event = _make_event_with_multiple_suggestions() + with _patch_select("always_deny_rule"): + result = await renderer.prompt_permission(event) + + assert result is False + ctx = store.get_state().permission_context + deny_rules = ctx.deny_rules.get("session", []) + assert any("mkdir:*" in r for r in deny_rules) + assert any("rm:*" in r for r in deny_rules) + + @pytest.mark.asyncio + async def test_label_shows_all_rules(self): + """Option label should display all suggestion rules comma-separated.""" + from iac_code.tools.bash import BashTool + + store = AppStateStore() + tool = BashTool() + renderer = _make_renderer(store, tool=tool) + event = _make_event_with_multiple_suggestions() + + captured_labels = {} + + def capture_select_init(self, *, options, **kwargs): + captured_labels["all"] = [o.label for o in options] + self._options = options + self._default_value = kwargs.get("default_value") + + with patch("iac_code.ui.components.select.Select.__init__", capture_select_init): + with _patch_select("reject_once"): + await renderer.prompt_permission(event) + + labels_text = " ".join(captured_labels["all"]) + assert "mkdir:*" in labels_text + assert "rm:*" in labels_text + + +class TestSupportsBlanketAllow: + @pytest.mark.asyncio + async def test_bash_no_suggestions_hides_always_allow(self): + """Bash tool (supports_blanket_allow=False) without suggestions should NOT offer always_allow.""" + from iac_code.tools.bash import BashTool + + store = AppStateStore() + tool = BashTool() + renderer = _make_renderer(store, tool=tool) + event = _make_event("bash") + + captured_options = {} + + def capture_select_init(self, *, options, **kwargs): + captured_options["values"] = [o.value for o in options] + self._options = options + self._default_value = kwargs.get("default_value") + + with patch("iac_code.ui.components.select.Select.__init__", capture_select_init): + with _patch_select("reject_once"): + await renderer.prompt_permission(event) + + assert "always_allow" not in captured_options["values"] + + @pytest.mark.asyncio + async def test_bash_no_suggestions_still_has_always_deny(self): + """Bash tool without suggestions should still show always_deny option.""" + from iac_code.tools.bash import BashTool + + store = AppStateStore() + tool = BashTool() + renderer = _make_renderer(store, tool=tool) + event = _make_event("bash") + + captured_options = {} + + def capture_select_init(self, *, options, **kwargs): + captured_options["values"] = [o.value for o in options] + self._options = options + self._default_value = kwargs.get("default_value") + + with patch("iac_code.ui.components.select.Select.__init__", capture_select_init): + with _patch_select("always_deny"): + result = await renderer.prompt_permission(event) + + assert "always_deny" in captured_options["values"] + assert result is False + assert store.get_state().always_allow_rules["bash"] == "always_deny" + + @pytest.mark.asyncio + async def test_normal_tool_no_suggestions_shows_always_allow(self): + """Normal tool (supports_blanket_allow=True) without suggestions should show always_allow.""" + store = AppStateStore() + tool = MagicMock() + tool.supports_blanket_allow = True + tool.user_facing_name.return_value = "WebFetch" + tool.render_tool_use_message.return_value = None + renderer = _make_renderer(store, tool=tool) + event = _make_event("web_fetch") + + captured_options = {} + + def capture_select_init(self, *, options, **kwargs): + captured_options["values"] = [o.value for o in options] + self._options = options + self._default_value = kwargs.get("default_value") + + with patch("iac_code.ui.components.select.Select.__init__", capture_select_init): + with _patch_select("always_allow"): + result = await renderer.prompt_permission(event) + + assert "always_allow" in captured_options["values"] + assert result is True + assert store.get_state().always_allow_rules["web_fetch"] == "always_allow"