feat(approvals): /deny <reason> relays denial reason to the agent (port nanoclaw#2832) (#54518)

* feat(approvals): /deny <reason> relays denial reason to the agent

Port from qwibitai/nanoclaw#2832 (reject with reason).

Gateway /deny now accepts an optional trailing reason (/deny <reason>
or /deny all <reason>). The reason rides on the per-session approval
entry through resolve_gateway_approval -> _await_gateway_decision and is
appended to the BLOCKED tool result the agent receives, so a declined
agent can adapt instead of only hearing 'denied'.

Adapted to hermes-agent's synchronous single-command /deny model: no DB
state, no second-message capture step, no migration. Reason is capped at
280 chars and threaded through both the terminal-command guard and the
execute_code guard. Plain /deny and the approve paths are unchanged.

- tools/approval.py: _ApprovalEntry.reason; resolve_gateway_approval gains
  optional reason; _await_gateway_decision returns it; both gateway BLOCKED
  messages include it
- gateway/slash_commands.py: parse leading 'all' + trailing reason
- locales/en.yaml: deny.denied_reason_{singular,plural}
- hermes_cli/commands.py: /deny args_hint '[all] [reason]'
- tests: 3 new (with-reason, all+reason, plain-deny regression)

* fix(ci): localize deny-reason keys across all locales + update interrupt-path assertions

CI surfaced two enforced invariants broken by the deny-with-reason change:
- test_i18n catalog-parity requires every locale to carry the same keys as
  en.yaml with matching placeholders. Added deny.denied_reason_singular/plural
  (with {count}/{reason}) to all 15 non-English locales.
- test_approval_interrupt asserts the exact dict from _await_gateway_decision,
  which now carries a 'reason' key (None on the interrupt/timeout paths).
This commit is contained in:
Teknium
2026-07-05 02:22:08 -07:00
committed by GitHub
parent 9767e19b60
commit cb6c47af08
21 changed files with 157 additions and 22 deletions
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "Geen hangende opdrag om te weier nie."
denied_singular: "❌ Opdrag geweier."
denied_plural: "❌ Opdragte geweier ({count} opdragte)."
denied_reason_singular: "❌ Opdrag geweier. Rede aan die agent oorgedra: \"{reason}\""
denied_reason_plural: "❌ Opdragte geweier ({count} opdragte). Rede aan die agent oorgedra: \"{reason}\""
fast:
not_supported: "⚡ /fast is slegs beskikbaar vir OpenAI-modelle wat Priority Processing ondersteun."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "Kein ausstehender Befehl zum Ablehnen."
denied_singular: "❌ Befehl abgelehnt."
denied_plural: "❌ Befehle abgelehnt ({count} Befehle)."
denied_reason_singular: "❌ Befehl abgelehnt. Grund an den Agenten weitergeleitet: \"{reason}\""
denied_reason_plural: "❌ Befehle abgelehnt ({count} Befehle). Grund an den Agenten weitergeleitet: \"{reason}\""
fast:
not_supported: "⚡ /fast ist nur für OpenAI-Modelle mit Priority Processing verfügbar."
+2
View File
@@ -122,6 +122,8 @@ gateway:
no_pending: "No pending command to deny."
denied_singular: "❌ Command denied."
denied_plural: "❌ Commands denied ({count} commands)."
denied_reason_singular: "❌ Command denied. Reason relayed to the agent: \"{reason}\""
denied_reason_plural: "❌ Commands denied ({count} commands). Reason relayed to the agent: \"{reason}\""
fast:
not_supported: "⚡ /fast is only available for OpenAI models that support Priority Processing."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "No hay ningún comando pendiente que denegar."
denied_singular: "❌ Comando denegado."
denied_plural: "❌ Comandos denegados ({count} comandos)."
denied_reason_singular: "❌ Comando denegado. Motivo transmitido al agente: \"{reason}\""
denied_reason_plural: "❌ Comandos denegados ({count} comandos). Motivo transmitido al agente: \"{reason}\""
fast:
not_supported: "⚡ /fast solo está disponible para modelos de OpenAI que admiten Priority Processing."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "Aucune commande en attente de refus."
denied_singular: "❌ Commande refusée."
denied_plural: "❌ Commandes refusées ({count} commandes)."
denied_reason_singular: "❌ Commande refusée. Raison transmise à l'agent: \"{reason}\""
denied_reason_plural: "❌ Commandes refusées ({count} commandes). Raison transmise à l'agent: \"{reason}\""
fast:
not_supported: "⚡ /fast n'est disponible que pour les modèles OpenAI qui prennent en charge Priority Processing."
+2
View File
@@ -111,6 +111,8 @@ gateway:
no_pending: "Níl aon ordú ag fanacht le diúltú."
denied_singular: "❌ Ordú diúltaithe."
denied_plural: "❌ Orduithe diúltaithe ({count} ordú)."
denied_reason_singular: "❌ Ordú diúltaithe. Cúis curtha ar aghaidh chuig an ngníomhaire: \"{reason}\""
denied_reason_plural: "❌ Orduithe diúltaithe ({count} ordú). Cúis curtha ar aghaidh chuig an ngníomhaire: \"{reason}\""
fast:
not_supported: "⚡ Tá /fast ar fáil amháin do shamhlacha OpenAI a thacaíonn le Priority Processing."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "Nincs elutasítható függőben lévő parancs."
denied_singular: "❌ Parancs elutasítva."
denied_plural: "❌ Parancsok elutasítva ({count} parancs)."
denied_reason_singular: "❌ Parancs elutasítva. Indok továbbítva az ügynöknek: \"{reason}\""
denied_reason_plural: "❌ Parancsok elutasítva ({count} parancs). Indok továbbítva az ügynöknek: \"{reason}\""
fast:
not_supported: "⚡ A /fast csak olyan OpenAI modelleknél érhető el, amelyek támogatják a Priority Processinget."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "Nessun comando in attesa da negare."
denied_singular: "❌ Comando negato."
denied_plural: "❌ Comandi negati ({count} comandi)."
denied_reason_singular: "❌ Comando negato. Motivo inoltrato all'agente: \"{reason}\""
denied_reason_plural: "❌ Comandi negati ({count} comandi). Motivo inoltrato all'agente: \"{reason}\""
fast:
not_supported: "⚡ /fast è disponibile solo per i modelli OpenAI che supportano Priority Processing."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "拒否待ちのコマンドはありません。"
denied_singular: "❌ コマンドを拒否しました。"
denied_plural: "❌ コマンドを拒否しました ({count} 件)。"
denied_reason_singular: "❌ コマンドを拒否しました。 理由をエージェントに伝達しました: \"{reason}\""
denied_reason_plural: "❌ コマンドを拒否しました ({count} 件)。 理由をエージェントに伝達しました: \"{reason}\""
fast:
not_supported: "⚡ /fast は Priority Processing をサポートする OpenAI モデルでのみ利用できます。"
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "거부 대기 중인 명령이 없습니다."
denied_singular: "❌ 명령이 거부되었습니다."
denied_plural: "❌ 명령이 거부되었습니다 ({count}개)."
denied_reason_singular: "❌ 명령이 거부되었습니다. 사유를 에이전트에 전달함: \"{reason}\""
denied_reason_plural: "❌ 명령이 거부되었습니다 ({count}개). 사유를 에이전트에 전달함: \"{reason}\""
fast:
not_supported: "⚡ /fast는 Priority Processing을 지원하는 OpenAI 모델에서만 사용할 수 있습니다."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "Não há nenhum comando pendente para negar."
denied_singular: "❌ Comando negado."
denied_plural: "❌ Comandos negados ({count} comandos)."
denied_reason_singular: "❌ Comando negado. Motivo repassado ao agente: \"{reason}\""
denied_reason_plural: "❌ Comandos negados ({count} comandos). Motivo repassado ao agente: \"{reason}\""
fast:
not_supported: "⚡ /fast só está disponível para modelos da OpenAI que suportam Priority Processing."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "Нет команды для отклонения."
denied_singular: "❌ Команда отклонена."
denied_plural: "❌ Команды отклонены ({count} команд)."
denied_reason_singular: "❌ Команда отклонена. Причина передана агенту: \"{reason}\""
denied_reason_plural: "❌ Команды отклонены ({count} команд). Причина передана агенту: \"{reason}\""
fast:
not_supported: "⚡ /fast доступен только для моделей OpenAI, поддерживающих Priority Processing."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "Reddedilecek bekleyen komut yok."
denied_singular: "❌ Komut reddedildi."
denied_plural: "❌ Komutlar reddedildi ({count} komut)."
denied_reason_singular: "❌ Komut reddedildi. Gerekçe ajana iletildi: \"{reason}\""
denied_reason_plural: "❌ Komutlar reddedildi ({count} komut). Gerekçe ajana iletildi: \"{reason}\""
fast:
not_supported: "⚡ /fast yalnızca Priority Processing destekleyen OpenAI modellerinde kullanılabilir."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "Немає команди для відхилення."
denied_singular: "❌ Команду відхилено."
denied_plural: "❌ Команди відхилено ({count} команд)."
denied_reason_singular: "❌ Команду відхилено. Причину передано агентові: \"{reason}\""
denied_reason_plural: "❌ Команди відхилено ({count} команд). Причину передано агентові: \"{reason}\""
fast:
not_supported: "⚡ /fast доступний лише для моделей OpenAI, які підтримують Priority Processing."
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "沒有待拒絕的指令。"
denied_singular: "❌ 指令已拒絕。"
denied_plural: "❌ 指令已拒絕({count} 條指令)。"
denied_reason_singular: "❌ 指令已拒絕。 已將原因轉達給代理: \"{reason}\""
denied_reason_plural: "❌ 指令已拒絕({count} 條指令)。 已將原因轉達給代理: \"{reason}\""
fast:
not_supported: "⚡ /fast 僅適用於支援 Priority Processing 的 OpenAI 模型。"
+2
View File
@@ -107,6 +107,8 @@ gateway:
no_pending: "没有待拒绝的命令。"
denied_singular: "❌ 命令已拒绝。"
denied_plural: "❌ 命令已拒绝({count} 条命令)。"
denied_reason_singular: "❌ 命令已拒绝。 已将原因转达给代理: \"{reason}\""
denied_reason_plural: "❌ 命令已拒绝({count} 条命令)。 已将原因转达给代理: \"{reason}\""
fast:
not_supported: "⚡ /fast 仅适用于支持优先处理(Priority Processing)的 OpenAI 模型。"