scobleclaw/scoble-policy/opa/policies/lib.rego

104 lines
4 KiB
Rego

package scobleclaw.lib
# lib.rego — shared helpers + base deny rules for all agents.
#
# Per scobleclaw#42 refactor: this file lives at `package scobleclaw.lib`
# (was `package scobleclaw` in the package-root layout). Per-agent policy
# files (main_agent, demo_concierge, demo_watchdog) `import data.scobleclaw.lib`
# to pull in the constants, helpers, and base deny rules. Each per-agent
# package re-aggregates `base_deny` into its own `deny` set so the API
# surface is `data.scobleclaw.<agent>.deny` (one path per agent).
# ---------------------------------------------------------------------------
# Input shape (L0 contract)
# ---------------------------------------------------------------------------
# {
# "agent_id": "main-agent" | "demo-concierge" | "demo-watchdog",
# "kamelet_ref": "kamelet:source/forgejo-search" | ...,
# "params": { ... agent-supplied, validated against the kamelet schema },
# "request_id": "uuid",
# "timestamp": "2026-06-08T16:00:00Z"
# }
# ---------------------------------------------------------------------------
# Constants — the hard organizational boundaries
# ---------------------------------------------------------------------------
# Internal email domain. Anything else is treated as external.
internal_email_domain = "scoble.me"
# Kamelets that move money or have irreversible side-effects. They are
# ALWAYS denied at L0. L1/L2 cannot override an L0 deny.
payment_kamelets = {
"kamelet:source/stripe-charge",
"kamelet:source/paypal-transfer",
"kamelet:source/ach-debit",
"kamelet:sink/wire-transfer",
}
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
# True if `domain` is an external email domain (i.e. NOT internal).
is_external_email(domain) {
domain != internal_email_domain
domain != ""
}
# True if `ref` is a payment kamelet. Uses v0/v1-portable set index
# membership (collection[key]) rather than the `in` operator.
is_payment_kamelet(ref) {
payment_kamelets[ref]
}
# True if the params dict looks like a Forgejo write targeting a repo
# other than the allowed owner/repo. We pattern-match on the well-known
# kamelet param names rather than hardcoding the kamelet_ref, so this
# rule catches both `forgejo-create-issue` and any future Forgejo write
# kamelet.
is_forgejo_write_outside_allowed(params) {
params.owner != "marcus"
}
is_forgejo_write_outside_allowed(params) {
params.owner == "marcus"
params.repo != "engineering"
}
# ---------------------------------------------------------------------------
# Base deny rules — every agent inherits these by importing this package
# and re-emitting the `base_deny` set into its own `deny` set.
# ---------------------------------------------------------------------------
#
# Why `base_deny` instead of just `deny`? With per-agent packages, a
# `deny` set in this lib package would be exposed at
# `data.scobleclaw.lib.deny` (one shared path) and would force every
# caller to merge it with the per-agent set. By naming it `base_deny`
# here and letting each agent package own the final `deny` set, the
# API stays clean: one `deny` path per agent, and the lib package
# contributes only shared rules.
# Rule 1: no external email. Any kamelet whose params include a `to` field
# with an external email domain is denied.
base_deny[reason] {
input.kamelet_ref != ""
input.params.to != ""
is_external_email(input.params.domain)
reason := sprintf("external email not permitted (domain=%v, to=%v)", [input.params.domain, input.params.to])
}
# Rule 2: no Forgejo writes outside marcus/engineering.
base_deny[reason] {
is_forgejo_write_outside_allowed(input.params)
reason := sprintf(
"forgejo writes restricted to marcus/engineering (got owner=%v repo=%v)",
[input.params.owner, input.params.repo],
)
}
# Rule 3: no payment kamelets. Period.
base_deny[reason] {
is_payment_kamelet(input.kamelet_ref)
reason := sprintf("payment kamelet blocked at L0: %v", [input.kamelet_ref])
}