fix(opa): regenerate opa-policies-configmap from on-disk policies (eng#118, cycle 175)
Configmap was stale from commit39b21dc; on-disk policies were updated by scobleclaw#42 (commitbfe6e4c) 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:
parent
2d95d3a53e
commit
887ed949fa
1 changed files with 146 additions and 37 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue