Skip to content

feat(security): Broker service for WYSIWYE action approval and execution #232

Description

@rdwj

Problem

Agents currently execute tool calls and actions directly — there is no centralized gatekeeper that evaluates whether an action should proceed, routes high-risk operations for human review, or guarantees that what the human approved is exactly what runs. The agent's reasoning loop has unchecked authority over the action plane.

Context: The WYSIWYE Safety Model

This is the output/action side of the WYSIWYE (What You See Is What You Execute) safety architecture:

  • Airlock (feat(security): Airlock service for tainted I/O mediation #230): handles the input side. All tainted/untrusted data passes through the Airlock before the agent sees it.
  • Broker (this issue): handles the output/action side. The agent proposes actions → the Broker evaluates them against rules → auto-approves low-risk actions or routes high-risk ones to a human → executes exactly what was approved. The agent never executes directly.

Together, the Airlock and Broker form a complete mediation layer: nothing tainted gets in without sanitization, and nothing consequential goes out without approval.

The key invariant is WYSIWYE: the action representation shown to the human for approval is the exact representation that executes. No hidden transforms, no post-approval rewrites, no string interpolation between the approval UI and the execution path. The serialized action is frozen at approval time and replayed byte-for-byte at execution time.

Proposed Solution

A Broker service that is the sole execution gateway for all agent actions:

Core flow

Agent reasoning loop
  → proposes ActionPlan (one or more tool calls with arguments)
  → Broker receives the ActionPlan
  → Broker evaluates each action against the rule engine
  → Low-risk actions: auto-approved, executed immediately
  → High-risk actions: routed to human via HITL channel
  → Human sees the exact action representation
  → Human approves or denies
  → Broker executes the approved ActionPlan verbatim (WYSIWYE guarantee)
  → Results flow back to the agent

What it enforces

  • Sole executor: the agent process has no direct path to execute actions. All tool dispatch goes through the Broker. This is a structural guarantee, not a convention.
  • WYSIWYE integrity: the ActionPlan is serialized and hashed at proposal time. The hash is shown alongside the human-readable representation. At execution time, the Broker verifies the hash before dispatching. Any mutation between approval and execution is a hard failure.
  • Rule-based auto-approval: a configurable rule engine determines which actions are low-risk enough to auto-approve without human review.
  • Audit trail: every action — auto-approved or human-reviewed — is logged with the full ActionPlan, the rule that matched, the decision, and the execution result.

ActionPlan structure

@dataclass
class Action:
    tool: str
    arguments: dict[str, Any]
    rationale: str  # agent's explanation of why this action is needed

@dataclass  
class ActionPlan:
    actions: list[Action]
    plan_hash: str  # SHA-256 of canonical serialization
    proposed_at: datetime
    context: str  # summary of the agent's reasoning state

Rule engine for auto-approval

The rule engine determines the disposition of each action. Rules are evaluated in order; first match wins.

broker:
  mode: enforce  # enforce | observe
  rules:
    - tool: "kubectl_get"
      action: auto_approve
    - tool: "kubectl_*"
      args_match: { verb: ["delete", "drain", "scale"] }
      action: require_human
    - tool: "send_email"
      action: deny
    - tool: "webfetch"
      args_match: { url: "https://*.internal.redhat.com/*" }
      action: auto_approve
    - tool: "*"
      action: require_human  # default: humans review unknown actions

Relationship with per-tool permissions (#164)

The per-tool permission system (#164) operates at the individual tool-call level within the agent process — it gates whether a tool call is allowed, denied, or requires an ask. The Broker operates at a higher level:

  • Permissions (feat: per-tool permission policy (allow | deny | ask) #164): "Is the agent allowed to call this tool with these arguments?" — enforced inside the agent process before the call is proposed.
  • Broker (this issue): "Should this proposed action actually execute, and does a human need to see it first?" — enforced outside the agent process as the execution gateway.

They compose: a tool call passes permission checks first, then the resulting ActionPlan passes through the Broker. A tool can be allowed by permissions but still require_human at the Broker level (e.g., a read-only Kubernetes tool is allowed but the Broker still wants human sign-off on production namespace queries).

Integration points

  • UI (ui-template#21): the HITL approval UI renders the ActionPlan for human review. The UI must display the action representation and hash, and transmit the approval decision back to the Broker.
  • Gateway (gateway-template#16): the HITL routing in the gateway carries approval requests from the Broker to the UI and approval decisions back. The gateway does not interpret or transform the ActionPlan — it is an opaque envelope.
  • Airlock (feat(security): Airlock service for tainted I/O mediation #230): the Airlock and Broker share an audit log and potentially a common policy engine. Input sanitization (Airlock) and output approval (Broker) are complementary.
  • Credential isolation (feat(security): Add credential isolation guidance and enforcement for code-execution tools #193): the Broker's execution context should follow the same credential isolation principles — the Broker executes actions with the minimum credentials required, not the agent's full credential set.

Design Questions (need design session)

  • Where does the Broker live? Options: sidecar container, embedded in BaseAgent (but architecturally separated from the reasoning loop), standalone service, or gateway extension. The WYSIWYE guarantee is strongest when the Broker is a separate process — the agent literally cannot bypass it.
  • Rule language: The YAML rule format above is a starting point. Should we adopt a real policy language (OPA/Rego, CEL, Casbin) for more expressive rules, or is a simple allowlist/denylist sufficient for v1? OPA gives us policy-as-code with testing; CEL is lighter weight and embeddable. Simple YAML is easiest to start with but may not scale.
  • ActionPlan granularity: Should the Broker gate individual tool calls, or batches of related calls (an "action plan")? Batch approval is better UX (one approval for a coherent set of actions) but harder to reason about (what if the human approves 3 of 5 actions?).
  • WYSIWYE hash verification: SHA-256 of canonical JSON is straightforward, but what is the canonical form? Sorted keys, no whitespace, UTF-8 normalized? Need a spec.
  • Timeout and failure semantics: If a human doesn't respond to an approval request within N minutes, does the action fail, retry, or escalate? What if the Broker process crashes between approval and execution?
  • Multi-agent delegation: When an agent delegates to a subagent, does the subagent's Broker inherit the parent's rules, or does it have its own rule set? This interacts with permission scopes from feat: per-tool permission policy (allow | deny | ask) #164.

Non-Goals

Acceptance Criteria

  • Design decision on where the Broker lives (sidecar vs. standalone vs. embedded)
  • Rule language decision for v1 (simple YAML, OPA, CEL, or other)
  • ActionPlan schema defined with canonical serialization spec for WYSIWYE hash
  • Prototype implementation gating at least one tool category (e.g., mutating kubectl calls)
  • WYSIWYE hash verification: demonstrate that a mutated ActionPlan is rejected at execution time
  • HITL approval flow working end-to-end (Broker → gateway → UI → approval → Broker → execute)
  • Audit logging for all actions with decision, rule matched, and execution result
  • Documentation in docs/architecture.md covering the Broker's role in the WYSIWYE safety model
  • Integration test demonstrating auto-approve, require-human, and deny rule paths

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions