Code: Use OPA (Open Policy Agent) para corredores de permissão auditáveis

Quando um agente começa a usar ferramentas, ele deixa de ser só “texto” e vira execução: consulta dados, cria tickets, envia e-mail, abre incidentes, muda status, dispara cobranças. Nesse ponto, o risco mais comum não é o modelo “alucinar”. É o sistema não saber, de forma consistente, quando é permitido agir.

O erro clássico é espalhar regras de permissão pelo código: ifs, allowlists, flags e exceções locais. Isso funciona no início, mas não escala com múltiplos times e múltiplas ferramentas — e vira impossível auditar quando a decisão precisa ser defendida.

OPA (Open Policy Agent) existe para separar política de aplicação: a regra fica central e executável; o serviço pergunta “posso?” com contexto; a resposta volta com decisão e justificativa.


O desafio — Fazer permissão virar um corredor, não um argumento de reunião

Em agentic AI, “permissão” precisa ter três propriedades para ser governável: ser executável em runtime, ser auditável como artefato versionado, e ser consistente em todos os caminhos (inclusive retries, reprocessamento e exceções).

O desafio é tirar a permissão do “costume organizacional” e colocá-la como boundary técnico: se a ação tem efeito colateral, ela só acontece se a política autorizar, e essa autorização precisa virar evidência.


O cenário — O agente tinha acesso “só para ajudar” e virou operador sem querer

Um time libera para o agente uma API interna de “atualizar cadastro” para acelerar atendimento. Outro time libera “enviar e-mail” para follow-up automático. Um terceiro libera “abrir ticket” para incidentes. Cada integração é feita com regras locais: uma checagem aqui, um allowlist ali.

Quando surge um incidente contestado, a empresa descobre que não existe uma resposta única para “por que isso foi permitido”. Há versões diferentes da regra em lugares diferentes. A auditoria vira arqueologia. E, pior: a permissão começa a vazar por exceções acumuladas.


Visão AI — Regras no código, espalhadas e fáceis de contornar por acidente

No caminho “AI”, a abordagem mais rápida é checar permissão dentro do próprio serviço, com regras simples baseadas em papel, ambiente e uma lista de ações “permitidas”. Isso entrega resultado rápido, mas cria um sistema onde a política está duplicada e muda por acidente: um serviço atualiza, outro esquece, um terceiro cria exceção.

O código abaixo representa esse padrão: um gateway de ferramentas decide “pode ou não pode” com ifs locais e regras por convenção. Ele é prático, mas a política não é um artefato auditável nem consistente entre integrações.

"""
AI-style: permissão embutida no código (ifs locais).
Pros: rápido, simples, sem dependência externa.
Cons: política espalhada, difícil de auditar, fácil de divergir entre serviços.
"""

from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, Any, Literal

Effect = Literal["read", "side_effect"]

@dataclass(frozen=True)
class ToolRequest:
    actor: str              # ex: "agent:support_bot"
    role: str               # ex: "support"
    tool: str               # ex: "send_email"
    action: str             # ex: "send"
    effect: Effect          # read | side_effect
    environment: str        # prod | staging
    cohort: str             # ex: "region:BR"

def local_allow(req: ToolRequest) -> bool:
    # regra local (e incompleta)
    if req.environment != "prod":
        return True

    if req.effect == "read":
        return True

    # side effects em prod: só alguns papéis
    if req.role in ("sre", "compliance"):
        return True

    # “exceção” para suporte mandar e-mail
    if req.role == "support" and req.tool == "send_email" and req.action == "send":
        return True

    return False

def call_tool(req: ToolRequest, payload: Dict[str, Any]) -> Dict[str, Any]:
    if not local_allow(req):
        return {"outcome": "deny", "reason": "local_policy_denied"}

    # placeholder: aqui chamaria a ferramenta real
    return {"outcome": "ok", "tool": req.tool, "action": req.action, "payload": payload}

if __name__ == "__main__":
    req = ToolRequest(
        actor="agent:support_bot",
        role="support",
        tool="send_email",
        action="send",
        effect="side_effect",
        environment="prod",
        cohort="region:BR",
    )
    print(call_tool(req, {"to": "cliente@exemplo.com", "subject": "Atualização", "body": "..." }))

Esse código decide rápido, mas produz um tipo de fragilidade que aparece tarde: a política não tem versionamento central, não retorna “por que” de forma consistente, e a exceção vira hábito. Quando você adiciona mais ferramentas e mais times, o mesmo “pode” passa a significar coisas diferentes em lugares diferentes — e a organização perde a capacidade de provar permissão sob contestação.


Visão WM — OPA como plano de controle: política executável, auditável e consistente

No caminho “WM”, o objetivo é transformar permissão em decisão auditável. A aplicação não “inventa” regra; ela consulta uma política central (OPA) com contexto suficiente para a política ser precisa: ator, ação, escopo, efeito colateral, coorte, ambiente, versão e intenção operacional.

A ideia não é burocratizar. É permitir expansão por corredores: primeiro leitura, depois efeitos colaterais estreitos, depois expansão merecida. E cada decisão de permissão pode virar evidência portátil quando alguém pergunta “por que isso foi permitido?”.

Abaixo, um exemplo mínimo com duas peças: uma política em Rego (OPA) e um gateway em Python consultando OPA via HTTP. A política devolve uma decisão e um motivo; o gateway anexa isso como “recibo” operacional da permissão.

# policy.rego
# WM-style: política executável para tool calls
# - corredores por efeito (read vs side_effect)
# - restrições por ferramenta/ação em produção
# - condições por coorte e papel

package trajecta.tools

default decision := {"allow": false, "reason": "default_deny"}

# Leitura em geral é permitida (mas ainda auditável)
decision := {"allow": true, "reason": "read_allowed"} {
  input.effect == "read"
}

# Efeito colateral em produção: só papéis específicos, com exceções estreitas
decision := {"allow": true, "reason": "prod_side_effect_allowed"} {
  input.environment == "prod"
  input.effect == "side_effect"
  input.role == "sre"
}

decision := {"allow": true, "reason": "support_email_narrow_corridor"} {
  input.environment == "prod"
  input.effect == "side_effect"
  input.role == "support"
  input.tool == "send_email"
  input.action == "send"
  startswith(input.cohort, "region:")
}

# Bloqueio explícito: certas ações nunca podem ser automáticas
decision := {"allow": false, "reason": "never_auto_charge"} {
  input.tool == "charge_card"
  input.action == "charge"
}
"""
WM-style: gateway consulta OPA e registra a decisão como evidência.
- política fica versionada e auditável fora do app
- app vira um executor: pede decisão, aplica, e guarda motivo
"""

from __future__ import annotations
import json
import uuid
import requests
from dataclasses import dataclass, asdict
from typing import Dict, Any, Literal

Effect = Literal["read", "side_effect"]

@dataclass(frozen=True)
class ToolRequest:
    actor: str
    role: str
    tool: str
    action: str
    effect: Effect
    environment: str
    cohort: str
    correlation_id: str

def opa_decide(req: ToolRequest) -> Dict[str, Any]:
    payload = {"input": asdict(req)}
    r = requests.post(
        "http://localhost:8181/v1/data/trajecta/tools/decision",
        json=payload,
        timeout=2,
    )
    r.raise_for_status()
    return r.json()["result"]

def call_tool(req: ToolRequest, tool_payload: Dict[str, Any]) -> Dict[str, Any]:
    decision = opa_decide(req)

    receipt = {
        "receipt_id": str(uuid.uuid4()),
        "correlation_id": req.correlation_id,
        "actor": req.actor,
        "tool": req.tool,
        "action": req.action,
        "effect": req.effect,
        "environment": req.environment,
        "cohort": req.cohort,
        "policy_reason": decision.get("reason"),
        "policy_allow": decision.get("allow"),
    }

    if not decision.get("allow"):
        return {"outcome": "deny", "receipt": receipt}

    # placeholder: aqui chamaria a ferramenta real
    result = {"status": "executed", "tool": req.tool, "action": req.action}

    return {"outcome": "ok", "result": result, "receipt": receipt}

if __name__ == "__main__":
    req = ToolRequest(
        actor="agent:support_bot",
        role="support",
        tool="send_email",
        action="send",
        effect="side_effect",
        environment="prod",
        cohort="region:BR",
        correlation_id=f"corr-{uuid.uuid4()}",
    )
    out = call_tool(req, {"to": "cliente@exemplo.com", "subject": "Atualização", "body": "..."})
    print(json.dumps(out, indent=2, ensure_ascii=False))

Esse padrão muda o tipo de conversa que a organização consegue ter. Em vez de “temos logs” e “o código faz”, você passa a ter um artefato claro: a política que autorizou ou negou, com motivo, e a decisão anexada ao ato. A permissão deixa de ser “o que o time lembra” e vira “o que o sistema executa”.

Além disso, OPA favorece governança por versões: a política pode ser revisada, testada e promovida com gates, sem precisar redeploy de toda a aplicação. E, quando a autonomia cresce, ela cresce por corredor: você amplia regras de forma explícita, não por exceção silenciosa.


Comparação — Política espalhada vs política executável e auditável

No padrão AI, permissão é um detalhe local. Isso acelera o início, mas cria divergência inevitável: cada serviço cria sua versão da regra, e cada exceção vira precedente. A empresa perde consistência e perde auditabilidade no momento em que mais precisa.

No padrão WM, permissão vira um plano de controle: uma política executável, consultada de forma uniforme, com decisão e motivo anexados à ação. Isso não elimina todos os riscos, mas muda a geometria do sistema: tool access deixa de ser poder implícito e vira autorização explícita — e isso é o que permite escalar autonomia sem perder legitimidade.


O que ainda poderia melhorar — sinais de próxima maturidade

Ainda faltaria tornar os corredores mais expressivos sem virar “política infinita”: separar claramente permissões por tipo de efeito colateral, por classe de usuário e por contexto operacional, e garantir que exceções tenham expiração e dono. Também faltaria integrar a política a evidências mais robustas de versão e de responsabilidade, para que a decisão de permissão viaje junto com o restante do ato (contexto, fontes, checks e trilha de aprovação quando houver).

Por fim, faltaria amadurecer o “comportamento sob negação”: quando a política nega, o sistema deveria entrar em modos previsíveis (abster-se, escalar, pausar, reduzir escopo por coorte) em vez de “tentar contornar”. Esse é o passo em que política deixa de ser só gate e vira parte do design de hesitação.

Veja também: