fix(opa): regenerate opa-policies-configmap from on-disk policies (eng#118, cycle 175)

Configmap was stale from commit 39b21dc; on-disk policies were updated
by scobleclaw#42 (commit bfe6e4c) but configmap was never re-applied.
This caused OPA /decision (OLD policy) to disagree with /allow and
/needs_l1 (NEW policy) for kamelet_ref=search-memory.

Re-generates the configmap from the current policies/*.rego files.
The configmap is also namespace-agnostic now (namespace field stripped
from the manifest) so it can be applied to scoble-policy namespace
via kubectl -n scoble-policy apply -f.
This commit is contained in:
agent-cos 2026-06-09 20:42:42 -07:00
parent 2d95d3a53e
commit 887ed949fa

View file

@ -9,14 +9,13 @@ apiVersion: v1
kind: ConfigMap
metadata:
name: opa-policies
namespace: scobleclaw
labels:
app: opa
component: l0-judge
data:
lib.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`
@ -25,10 +24,25 @@ data:
# 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 = {
@ -37,35 +51,60 @@ data:
"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. Each per-agent package re-emits these into its own
# `deny` set via `deny[reason] { lib.base_deny[reason] }`, giving each
# agent a single `data.scobleclaw.<agent>.deny` path.
# ---------------------------------------------------------------------------
# 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(
@ -73,18 +112,36 @@ data:
[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])
}
main-agent.rego: |
package scobleclaw.main_agent
# main-agent.rego — policy for the primary orchestrator agent.
# Per scobleclaw#42: API path is `data.scobleclaw.main_agent.allow`.
#
# 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",
@ -97,18 +154,36 @@ data:
"kamelet:source/kubectl-exec",
"kamelet:source/age-decrypt",
}
# Re-emit lib base_deny into the per-agent deny set.
# ---------------------------------------------------------------------------
# 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]
@ -116,36 +191,53 @@ data:
}
demo-concierge.rego: |
package scobleclaw.demo_concierge
# demo-concierge.rego — policy for the demo concierge agent.
# Per scobleclaw#42: API path is `data.scobleclaw.demo_concierge.allow`.
#
# Per scobleclaw#42 refactor: this file now lives at
# `package scobleclaw.demo_concierge` (was `package scobleclaw` in the
# package-root layout). API path is now
# `data.scobleclaw.demo_concierge.allow`.
#
# Scope: read-only Forgejo + OB1 read access. No writes anywhere. This
# is a demo / smoke-test agent; its allowlist is intentionally small.
import data.scobleclaw.lib
demo_concierge_allowed_kamelets = {
"kamelet:source/forgejo-search",
"kamelet:source/forgejo-read",
"kamelet:source/ob1-search",
"kamelet:source/kamelet-dispatch",
}
# Re-emit lib base_deny into the per-agent deny set.
# ---------------------------------------------------------------------------
# Deny set — re-emit base_deny from lib, plus per-agent rules.
# ---------------------------------------------------------------------------
deny[reason] {
lib.base_deny[reason]
}
# Per-agent deny: demo-concierge is read-only.
# Per-agent deny rule: demo-concierge is read-only. Any kamelet whose
# name suggests a Forgejo write (starts with `forgejo-create`) is
# denied at L0. Note: this is in addition to the lib base_deny rules
# (external email, forgejo writes outside engineering, payment kamelets).
deny[reason] {
input.agent_id == "demo-concierge"
startswith(input.kamelet_ref, "kamelet:source/forgejo-create")
reason := sprintf("demo-concierge is read-only; %v not permitted", [input.kamelet_ref])
}
# Allow only if kamelet is on the allowlist AND no shared deny rule fires.
allow {
input.agent_id == "demo-concierge"
demo_concierge_allowed_kamelets[input.kamelet_ref]
count(deny) == 0
}
# Demo-concierge never needs L1 — if a kamelet isn't on its allowlist, it
# is a misconfiguration and should be denied at L0 so the operator notices.
needs_l1 {
input.agent_id == "demo-concierge"
not demo_concierge_allowed_kamelets[input.kamelet_ref]
@ -153,31 +245,48 @@ data:
}
demo-watchdog.rego: |
package scobleclaw.demo_watchdog
# demo-watchdog.rego — policy for the demo watchdog agent.
# Per scobleclaw#42: API path is `data.scobleclaw.demo_watchdog.allow`.
#
# Per scobleclaw#42 refactor: this file now lives at
# `package scobleclaw.demo_watchdog` (was `package scobleclaw` in the
# package-root layout). API path is now
# `data.scobleclaw.demo_watchdog.allow`.
#
# Scope: monitoring-only. Can read OB1 thoughts and ping Discord, but
# cannot touch Forgejo, kubectl, or any kamelet with side-effects.
import data.scobleclaw.lib
demo_watchdog_allowed_kamelets = {
"kamelet:source/ob1-search",
"kamelet:source/ob1-list",
"kamelet:source/discord-post",
"kamelet:source/kamelet-dispatch",
}
# Re-emit lib base_deny into the per-agent deny set.
# ---------------------------------------------------------------------------
# Deny set — re-emit base_deny from lib (watchdog has no per-agent
# deny rules of its own; the lib's external-email / forgejo / payment
# blocks are sufficient).
# ---------------------------------------------------------------------------
deny[reason] {
lib.base_deny[reason]
}
allow {
input.agent_id == "demo-watchdog"
demo_watchdog_allowed_kamelets[input.kamelet_ref]
count(deny) == 0
}
needs_l1 {
input.agent_id == "demo-watchdog"
not demo_watchdog_allowed_kamelets[input.kamelet_ref]
not lib.payment_kamelets[input.kamelet_ref]
}
# Watchdog is the only agent allowed to post to Discord from L0. The
# shared `is_external_email` rule still applies, so Discord DMs to
# external addresses are blocked.