mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
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:
parent
9767e19b60
commit
cb6c47af08
21 changed files with 157 additions and 22 deletions
|
|
@ -4360,6 +4360,9 @@ class GatewaySlashCommandsMixin:
|
|||
a definitive BLOCKED message, same as the CLI deny flow.
|
||||
|
||||
``/deny`` denies the oldest; ``/deny all`` denies everything.
|
||||
``/deny <reason>`` (or ``/deny all <reason>``) attaches a one-line
|
||||
reason that is relayed back to the agent so it can adapt instead of
|
||||
only hearing "denied". Ported from qwibitai/nanoclaw#2832.
|
||||
"""
|
||||
source = event.source
|
||||
session_key = self._session_key_for_source(source)
|
||||
|
|
@ -4374,10 +4377,24 @@ class GatewaySlashCommandsMixin:
|
|||
return t("gateway.deny.stale")
|
||||
return t("gateway.deny.no_pending")
|
||||
|
||||
args = event.get_command_args().strip().lower()
|
||||
resolve_all = "all" in args
|
||||
# Parse args: a leading "all" token denies every pending command;
|
||||
# anything after it (or the whole arg string when "all" is absent) is
|
||||
# captured verbatim as the optional deny reason relayed to the agent.
|
||||
raw_args = event.get_command_args().strip()
|
||||
tokens = raw_args.split()
|
||||
resolve_all = bool(tokens) and tokens[0].lower() == "all"
|
||||
if resolve_all:
|
||||
reason = raw_args[len(tokens[0]):].strip()
|
||||
else:
|
||||
reason = raw_args
|
||||
# Cap to a sane one-liner; the agent only needs a short hint.
|
||||
if reason:
|
||||
reason = reason[:280].strip()
|
||||
|
||||
count = resolve_gateway_approval(session_key, "deny", resolve_all=resolve_all)
|
||||
count = resolve_gateway_approval(
|
||||
session_key, "deny", resolve_all=resolve_all,
|
||||
reason=reason or None,
|
||||
)
|
||||
if not count:
|
||||
return t("gateway.deny.no_pending")
|
||||
|
||||
|
|
@ -4386,7 +4403,14 @@ class GatewaySlashCommandsMixin:
|
|||
if _adapter:
|
||||
_adapter.resume_typing_for_chat(source.chat_id)
|
||||
|
||||
logger.info("User denied %d dangerous command(s) via /deny", count)
|
||||
logger.info(
|
||||
"User denied %d dangerous command(s) via /deny%s",
|
||||
count, " (with reason)" if reason else "",
|
||||
)
|
||||
if reason:
|
||||
if count > 1:
|
||||
return t("gateway.deny.denied_reason_plural", count=count, reason=reason)
|
||||
return t("gateway.deny.denied_reason_singular", reason=reason)
|
||||
if count > 1:
|
||||
return t("gateway.deny.denied_plural", count=count)
|
||||
return t("gateway.deny.denied_singular")
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
|
|||
CommandDef("stop", "Kill all running background processes", "Session"),
|
||||
CommandDef("approve", "Approve a pending dangerous command", "Session",
|
||||
gateway_only=True, args_hint="[session|always]"),
|
||||
CommandDef("deny", "Deny a pending dangerous command", "Session",
|
||||
gateway_only=True),
|
||||
CommandDef("deny", "Deny a pending dangerous command (optionally with a reason)", "Session",
|
||||
gateway_only=True, args_hint="[all] [reason]"),
|
||||
CommandDef("background", "Run a prompt in the background", "Session",
|
||||
aliases=("bg", "btw"), args_hint="<prompt>"),
|
||||
CommandDef("agents", "Show active agents and running tasks", "Session",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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 モデルでのみ利用できます。"
|
||||
|
|
|
|||
|
|
@ -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 모델에서만 사용할 수 있습니다."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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 模型。"
|
||||
|
|
|
|||
|
|
@ -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 模型。"
|
||||
|
|
|
|||
|
|
@ -324,6 +324,61 @@ class TestDenyCommand:
|
|||
result = await runner._handle_deny_command(_make_event("/deny"))
|
||||
assert "No pending command" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deny_with_reason_attaches_reason(self):
|
||||
"""/deny <reason> attaches the reason to the resolved entry."""
|
||||
from tools.approval import _ApprovalEntry, _gateway_queues
|
||||
|
||||
runner = _make_runner()
|
||||
source = _make_source()
|
||||
session_key = runner._session_key_for_source(source)
|
||||
|
||||
entry = _ApprovalEntry({"command": "test"})
|
||||
_gateway_queues[session_key] = [entry]
|
||||
|
||||
result = await runner._handle_deny_command(
|
||||
_make_event("/deny that path is still in use")
|
||||
)
|
||||
assert entry.result == "deny"
|
||||
assert entry.reason == "that path is still in use"
|
||||
assert "that path is still in use" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deny_all_with_reason(self):
|
||||
"""/deny all <reason> denies everything and relays one reason."""
|
||||
from tools.approval import _ApprovalEntry, _gateway_queues
|
||||
|
||||
runner = _make_runner()
|
||||
source = _make_source()
|
||||
session_key = runner._session_key_for_source(source)
|
||||
|
||||
e1 = _ApprovalEntry({"command": "cmd1"})
|
||||
e2 = _ApprovalEntry({"command": "cmd2"})
|
||||
_gateway_queues[session_key] = [e1, e2]
|
||||
|
||||
result = await runner._handle_deny_command(
|
||||
_make_event("/deny all wrong directory")
|
||||
)
|
||||
assert "2 commands" in result
|
||||
assert all(e.result == "deny" for e in [e1, e2])
|
||||
assert all(e.reason == "wrong directory" for e in [e1, e2])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deny_plain_has_no_reason(self):
|
||||
"""A bare /deny leaves the reason unset (regression guard)."""
|
||||
from tools.approval import _ApprovalEntry, _gateway_queues
|
||||
|
||||
runner = _make_runner()
|
||||
source = _make_source()
|
||||
session_key = runner._session_key_for_source(source)
|
||||
|
||||
entry = _ApprovalEntry({"command": "test"})
|
||||
_gateway_queues[session_key] = [entry]
|
||||
|
||||
await runner._handle_deny_command(_make_event("/deny"))
|
||||
assert entry.result == "deny"
|
||||
assert entry.reason is None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Bare "yes" must NOT trigger approval
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class TestApprovalInterrupt:
|
|||
elapsed = time.monotonic() - start
|
||||
|
||||
assert not t.is_alive(), "approval wait did not return after interrupt"
|
||||
assert result_holder["result"] == {"resolved": True, "choice": "deny"}
|
||||
assert result_holder["result"] == {"resolved": True, "choice": "deny", "reason": None}
|
||||
# Must be far below the 300s timeout — the interrupt, not the deadline,
|
||||
# is what released the wait.
|
||||
assert elapsed < 10, f"interrupt path too slow ({elapsed:.1f}s)"
|
||||
|
|
@ -157,4 +157,4 @@ class TestApprovalInterrupt:
|
|||
t.join(timeout=10)
|
||||
assert not t.is_alive()
|
||||
# Timed out (no resolution) because the foreign interrupt was ignored.
|
||||
assert result_holder["result"] == {"resolved": False, "choice": None}
|
||||
assert result_holder["result"] == {"resolved": False, "choice": None, "reason": None}
|
||||
|
|
|
|||
|
|
@ -1370,12 +1370,16 @@ _permanent_approved: set = set()
|
|||
|
||||
class _ApprovalEntry:
|
||||
"""One pending dangerous-command approval inside a gateway session."""
|
||||
__slots__ = ("event", "data", "result")
|
||||
__slots__ = ("event", "data", "result", "reason")
|
||||
|
||||
def __init__(self, data: dict):
|
||||
self.event = threading.Event()
|
||||
self.data = data # command, description, pattern_keys, …
|
||||
self.result: Optional[str] = None # "once"|"session"|"always"|"deny"
|
||||
# Optional free-text reason supplied with an explicit deny
|
||||
# (``/deny <reason>``) so the agent can adapt instead of only
|
||||
# hearing "denied". Ported from qwibitai/nanoclaw#2832.
|
||||
self.reason: Optional[str] = None
|
||||
|
||||
|
||||
_gateway_queues: dict[str, list] = {} # session_key → [_ApprovalEntry, …]
|
||||
|
|
@ -1408,7 +1412,8 @@ def unregister_gateway_notify(session_key: str) -> None:
|
|||
|
||||
|
||||
def resolve_gateway_approval(session_key: str, choice: str,
|
||||
resolve_all: bool = False) -> int:
|
||||
resolve_all: bool = False,
|
||||
reason: Optional[str] = None) -> int:
|
||||
"""Called by the gateway's /approve or /deny handler to unblock
|
||||
waiting agent thread(s).
|
||||
|
||||
|
|
@ -1416,6 +1421,10 @@ def resolve_gateway_approval(session_key: str, choice: str,
|
|||
resolved at once (``/approve all``). Otherwise only the oldest one
|
||||
is resolved (FIFO).
|
||||
|
||||
*reason* is an optional free-text explanation attached to an explicit
|
||||
deny (``/deny <reason>``). It is relayed back to the agent in the
|
||||
BLOCKED message so it can adapt instead of only hearing "denied".
|
||||
|
||||
Returns the number of approvals resolved (0 means nothing was pending).
|
||||
"""
|
||||
with _lock:
|
||||
|
|
@ -1432,6 +1441,8 @@ def resolve_gateway_approval(session_key: str, choice: str,
|
|||
|
||||
for entry in targets:
|
||||
entry.result = choice
|
||||
if reason:
|
||||
entry.reason = reason
|
||||
entry.event.set()
|
||||
return len(targets)
|
||||
|
||||
|
|
@ -2206,7 +2217,7 @@ def _await_gateway_decision(session_key: str, notify_cb, approval_data: dict,
|
|||
surface=surface,
|
||||
choice=_outcome,
|
||||
)
|
||||
return {"resolved": resolved, "choice": choice}
|
||||
return {"resolved": resolved, "choice": choice, "reason": entry.reason}
|
||||
|
||||
|
||||
def check_all_command_guards(command: str, env_type: str,
|
||||
|
|
@ -2480,6 +2491,7 @@ def check_all_command_guards(command: str, env_type: str,
|
|||
}
|
||||
resolved = decision["resolved"]
|
||||
choice = decision["choice"]
|
||||
deny_reason = decision.get("reason")
|
||||
|
||||
if not resolved or choice is None or choice == "deny":
|
||||
# Consent contract: silence is NOT consent, and an explicit
|
||||
|
|
@ -2495,21 +2507,28 @@ def check_all_command_guards(command: str, env_type: str,
|
|||
reason = "denied by user"
|
||||
timeout_addendum = ""
|
||||
outcome = "denied"
|
||||
# An explicit deny may carry a free-text reason
|
||||
# (``/deny <reason>``) so the agent can adapt rather than only
|
||||
# hearing "denied". Relayed verbatim; generic attribution.
|
||||
reason_addendum = ""
|
||||
if outcome == "denied" and deny_reason:
|
||||
reason_addendum = f' Reason given by the user: "{deny_reason}".'
|
||||
return {
|
||||
"approved": False,
|
||||
"message": (
|
||||
f"BLOCKED: Command {reason}. The user has NOT consented "
|
||||
f"to this action. Do NOT retry this command, do NOT "
|
||||
f"rephrase it, and do NOT attempt the same outcome via "
|
||||
f"a different command. Stop the current workflow and "
|
||||
f"wait for the user to respond before taking any "
|
||||
f"further destructive or irreversible action."
|
||||
f"{timeout_addendum}"
|
||||
f"BLOCKED: Command {reason}.{reason_addendum} The user "
|
||||
f"has NOT consented to this action. Do NOT retry this "
|
||||
f"command, do NOT rephrase it, and do NOT attempt the "
|
||||
f"same outcome via a different command. Stop the "
|
||||
f"current workflow and wait for the user to respond "
|
||||
f"before taking any further destructive or "
|
||||
f"irreversible action.{timeout_addendum}"
|
||||
),
|
||||
"pattern_key": primary_key,
|
||||
"description": combined_desc,
|
||||
"outcome": outcome,
|
||||
"user_consent": False,
|
||||
"deny_reason": deny_reason,
|
||||
}
|
||||
|
||||
# User approved — persist based on scope (same logic as CLI)
|
||||
|
|
@ -2773,22 +2792,27 @@ def check_execute_code_guard(code: str, env_type: str,
|
|||
|
||||
resolved = decision["resolved"]
|
||||
choice = decision["choice"]
|
||||
deny_reason = decision.get("reason")
|
||||
|
||||
if not resolved or choice is None or choice == "deny":
|
||||
reason = "timed out without user response" if not resolved else "denied by user"
|
||||
addendum = " Silence is not consent." if not resolved else ""
|
||||
reason_addendum = ""
|
||||
if resolved and choice == "deny" and deny_reason:
|
||||
reason_addendum = f' Reason given by the user: "{deny_reason}".'
|
||||
return {
|
||||
"approved": False,
|
||||
"message": (
|
||||
f"BLOCKED: execute_code script {reason}. The user has NOT "
|
||||
f"consented to running this code. Do NOT retry, do NOT rephrase "
|
||||
f"the script, and do NOT attempt the same outcome via a "
|
||||
f"different tool.{addendum}"
|
||||
f"BLOCKED: execute_code script {reason}.{reason_addendum} The "
|
||||
f"user has NOT consented to running this code. Do NOT retry, "
|
||||
f"do NOT rephrase the script, and do NOT attempt the same "
|
||||
f"outcome via a different tool.{addendum}"
|
||||
),
|
||||
"pattern_key": pattern_key,
|
||||
"description": description,
|
||||
"outcome": "timeout" if not resolved else "denied",
|
||||
"user_consent": False,
|
||||
"deny_reason": deny_reason,
|
||||
}
|
||||
|
||||
# Approved — persist based on scope (same logic as check_all_command_guards).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue