ASK KNOX
beta
LESSON 198

Escalation Over Hard Block

When an agent exceeds its authority, the instinct is to reject the action. The Principal Broker converts it to an escalation instead. Here's why that design decision matters — and what it preserves.

9 min read·Agent Authority & Safety Systems

The obvious design for an authority system is rejection. Agent tries to do something beyond its ceiling — you block it, return an error, log the attempt. Simple, clean, safe.

The Principal Broker does something different. When an agent exceeds its authority, the system converts the action request into an escalation and routes it to the agent's manager.

This is not a softer version of rejection. It is a fundamentally different design philosophy, and understanding why it works this way will change how you think about building agentic systems.

The Problem with Hard Rejection

Hard rejection answers the question "should this action happen now?" with a permanent no. But that's often not the right answer. The right answer is more often "this action requires a human decision before it happens."

Consider a trading bot that wants to place a $800 position. Its ceiling is $500. Under a rejection model:

  • The action fails
  • The bot logs an error
  • The position goes unplaced
  • Knox sees a failure in the logs hours later and has to figure out what happened

Under an escalation model:

  • The action is converted to an escalation
  • The OpenClaw (the CEO agent, the bot's manager) receives it
  • Knox gets a Discord notification: "Foresight wants to place $800 on TRUMP-YES. Ceiling is $500. Approve?"
  • Knox approves or denies in seconds
  • If approved, the action is taken. If denied, it isn't.

The escalation model preserves the intent. The rejection model destroys it.

The convert_to_escalation() Method

Here is the actual implementation from broker/core/authority.py:

def convert_to_escalation(
    self,
    message: A2AMessage,
    reason: str,
) -> A2AMessage:
    card = self.registry.get(message.envelope.from_agent)
    manager = "openclaw"
    if card and card.org.reportsTo:
        manager = card.org.reportsTo

    escalation = A2AMessage(
        envelope={
            "from": message.envelope.from_agent,
            "to": manager,
            "correlationId": message.envelope.correlation_id,
            "parentMessageId": message.envelope.message_id,
            "priority": "high",
        },
        type="escalation",
        subtype=f"authority.exceeded.{message.subtype}",
        payload={
            "escalationTier": 1,
            "urgency": "standard",
            "situation": (
                f"Agent {message.envelope.from_agent} wants to take "
                f"an action that exceeds its authority ceiling."
            ),
            "authorityGap": reason,
            "originalIntent": {
                "type": message.type,
                "subtype": message.subtype,
                "payload": message.payload,
                "from_agent": message.envelope.from_agent,
            },
            "recommendation": (
                "Review original intent and approve or deny."
            ),
            "defaultAction": (
                "Action will not be taken if no response"
            ),
            "defaultActionAt": self._default_action_time(message),
        },
        audit={"agentVersion": message.audit.agent_version},
    )
    return escalation

Walk through what this constructs.

The envelope. The escalation is routed from the original agent (preserving accountability — it was Foresight that wanted to do this) to the agent's manager (derived from the org chart, not hardcoded). The priority is set to high automatically, since any escalation is by definition time-sensitive.

The subtype. authority.exceeded.trade.execute tells the receiving agent exactly what happened and what the original action was. The manager doesn't have to guess.

The originalIntent. The full original payload is embedded in the escalation. When the OpenClaw forwards this to Knox, Knox can see the exact trade that was requested — asset, size, direction, rationale. This is what makes escalations actionable rather than just notifications.

The defaultAction. If no response is received, the action is not taken. This is the fail-safe. Escalations don't auto-approve. They auto-expire. This is critical: an unanswered escalation is a blocked action, not a delayed approval.

The defaultActionAt. The expiry time is calculated dynamically based on message priority:

def _default_action_time(self, message: A2AMessage) -> str:
    delays = {
        "low": 240,
        "normal": 60,
        "high": 5,
        "critical": 1,
    }
    minutes = delays.get(message.envelope.priority, 60)
    dt = datetime.now(timezone.utc) + timedelta(minutes=minutes)
    return dt.isoformat()

A low-priority escalation lives for 4 hours. A critical one expires in 1 minute. This creates appropriate urgency without requiring manual prioritization.

The Org Chart Dependency

The escalation routing uses the org chart, not a hardcoded list of managers. manager = card.org.reportsTo — if the agent card says the agent reports to vp-trading, the escalation goes to vp-trading, not directly to Knox.

This matters for two reasons.

First, it scales. In a small system you can route everything to a single human. As the system grows, escalations need to flow through the right chain of command. A VP-level agent receiving a trading escalation can handle it in context — it knows the strategy, it knows the risk parameters, it can make a real decision rather than passing everything upward.

Second, it encodes the org structure as routing logic. The org chart is not documentation. It is operational. Change an agent's reportsTo field and its escalations route differently. The broker enforces the org structure mechanically rather than relying on agents to know who to contact.

When Escalation Does Not Apply

The escalation path is for authority ceiling breaches. It is not for hard blocks.

Hard blocks are actions that are never allowed regardless of authority, approval, or escalation. Accessing wallet private keys. Dropping production databases. Impersonating Knox. These cannot be escalated because there is no approval that makes them acceptable.

The separation is explicit in the codebase: hard_blocks.py and authority.py are different modules. Hard blocks reject immediately. Authority breaches escalate. The two paths do not overlap.

When you are deciding whether something belongs in the hard blocks list or should be handled via authority ceilings, ask: "Is there any combination of approval, context, and authority level under which this action should be taken?" If yes, it belongs in authority ceilings. If no, it belongs in hard blocks.

Handling Escalations in Practice

An escalation is only as good as the process that handles it. The conversion mechanism is working correctly if escalations surface to a human who can act on them. If escalations pile up unread in a queue, the system has converted a blocked action into a silently dropped one — which is worse, because you have the false impression that something is being handled.

In the operator's setup:

  • Escalations route to the OpenClaw via NATS
  • The OpenClaw formats them and sends a Discord notification to Knox
  • Knox has a single-message approve/deny flow
  • Response time on high-priority escalations should be under 5 minutes

The technical mechanism is only half the system. The operational response loop is the other half.

What This Philosophy Gets You

The escalation-over-rejection design gives you three things that pure rejection does not.

Preserved intent. Agent judgment is part of the system's intelligence. When a bot identifies a trade that exceeds its ceiling, that identification has value. Escalation surfaces it. Rejection buries it.

Human-in-the-loop without friction. The escalation does the work of formatting, routing, and providing context. Knox doesn't have to dig through logs to understand what happened. The situation, the gap, the original intent, and the recommended action are all in the escalation payload.

Auditable decision trail. Every escalation is a message in the broker's audit log. Every approval or denial is a subsequent message. The entire decision chain is queryable. Six months from now, you can look up exactly what happened and why.

Hard rejection is easier to build. Escalation is the right architecture.