scobleclaw/scoble-policy/opa/policies/main-agent.rego

70 lines
2.7 KiB
Rego

package scobleclaw.main_agent
# main-agent.rego — policy for the primary orchestrator agent.
#
# Per scobleclaw#42 refactor: this file now lives at
# `package scobleclaw.main_agent` (was `package scobleclaw` in the
# package-root layout). API path is now `data.scobleclaw.main_agent.allow`
# (was `data.scobleclaw.allow`). The shared `lib` package contributes
# the base deny rules, which are re-emitted into the per-agent `deny`
# set below so callers see a single `deny` path per agent.
#
# Scope: this is the agent that drives engineering work (Forgejo
# reads/writes, OB1 captures, kamelet dispatch). It has the tightest
# allowlist.
import data.scobleclaw.lib
# ---------------------------------------------------------------------------
# Per-agent allowlist
# ---------------------------------------------------------------------------
# Kamelets main-agent is allowed to invoke. Anything else is `needs_l1`
# (caller decides whether to escalate).
main_agent_allowed_kamelets = {
"kamelet:source/forgejo-search",
"kamelet:source/forgejo-read",
"kamelet:source/forgejo-create-issue",
"kamelet:source/forgejo-create-pr",
"kamelet:source/forgejo-comment",
"kamelet:source/ob1-capture",
"kamelet:source/ob1-search",
"kamelet:source/kamelet-dispatch",
"kamelet:source/kubectl-exec",
"kamelet:source/age-decrypt",
}
# ---------------------------------------------------------------------------
# Deny set — re-emit base_deny from lib, plus per-agent rules.
# ---------------------------------------------------------------------------
# Each rule below re-publishes a base_deny[reason] entry into the
# per-agent `deny` set. This keeps the API surface to a single
# `data.scobleclaw.main_agent.deny` path while still giving lib
# ownership of the shared rules.
deny[reason] {
lib.base_deny[reason]
}
# ---------------------------------------------------------------------------
# Per-agent allow rule
# ---------------------------------------------------------------------------
allow {
input.agent_id == "main-agent"
main_agent_allowed_kamelets[input.kamelet_ref]
# If the kamelet is on the allowlist, the shared deny rules still apply.
# Rego semantics: the rule body is true only when ALL expressions hold;
# `count(deny) == 0` makes the deny-set check explicit in the rule head.
count(deny) == 0
}
# If the agent is main-agent but the kamelet is NOT on its allowlist, we
# mark it `needs_l1` so the caller can decide to escalate to an LLM judge
# or Discord. We do NOT deny outright — a new kamelet might legitimately
# need to be added to the allowlist via a policy PR.
needs_l1 {
input.agent_id == "main-agent"
not main_agent_allowed_kamelets[input.kamelet_ref]
not lib.payment_kamelets[input.kamelet_ref]
}