feat(opa): Layered judge L0 — OPA deployment + 4 Rego policies (eng#40)
Deploys openpolicyagent/opa:0.68.0 as a single-replica ClusterIP
service in the scobleclaw namespace, with a ConfigMap-mounted
policy bundle under /policies.
Policies (package scobleclaw):
_lib.rego shared deny rules (external email, forgejo
write outside marcus/engineering, payment
kamelets) + helpers (is_external_email,
is_payment_kamelet, is_forgejo_write_outside_allowed)
main-agent.rego 10-kamelet allowlist for main-agent
demo-concierge.rego read-only 4-kamelet allowlist
demo-watchdog.rego 4-kamelet allowlist (ob1 + discord)
L0 contract:
POST /v1/data/scobleclaw/allow -> {allow: bool}
POST /v1/data/scobleclaw/needs_l1 -> {needs_l1: bool}
POST /v1/data/scobleclaw/deny -> [reason, ...]
Verified end-to-end against the running OPA pod:
allow=true for main-agent + kamelet:source/forgejo-search
deny=[reason] for external email, forgejo write to stranger/not-mine,
payment kamelet (stripe-charge), demo-concierge forgejo-create
needs_l1=true for unknown kamelet (e.g. some-new-kamelet) on main-agent
needs_l1=false for payment kamelet (L1 cannot override L0 deny)
Out of scope (separate tickets): judge-integration Camel-K binding,
llm-classifier-integration (L1), discord-escalation-integration (L2),
audit-sink OPA log tailing.
This commit is contained in:
parent
a453892fd1
commit
39b21dcda5
9 changed files with 540 additions and 0 deletions
83
scoble-policy/opa/README.md
Normal file
83
scoble-policy/opa/README.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# opa/
|
||||
|
||||
OPA deployment + Rego policy bundle for the **L0 (static policy) layer** of
|
||||
the layered judge stack (eng#40).
|
||||
|
||||
## Layout
|
||||
|
||||
- `opa-deployment.yaml` — ServiceAccount + Deployment (1 replica, 64Mi mem, 8181)
|
||||
- `opa-service.yaml` — ClusterIP service on :8181, `opa.scobleclaw.svc.cluster.local`
|
||||
- `opa-policies-configmap.yaml` — ConfigMap containing the 4 .rego files
|
||||
- `policies/` — source-of-truth copy of the .rego files (GitOps reference;
|
||||
the ConfigMap is the runtime mount)
|
||||
|
||||
## Policies
|
||||
|
||||
All policies share the `scobleclaw` package. `_lib.rego` defines the base
|
||||
deny rules and helpers (`is_external_email`, `is_payment_kamelet`,
|
||||
`is_forgejo_write_outside_allowed`); the per-agent files add their own
|
||||
allowlist + agent-specific deny rules.
|
||||
|
||||
| Agent | File | Allowlist size | Per-agent denies |
|
||||
|-----------------|-----------------------|----------------|------------------|
|
||||
| main-agent | `main-agent.rego` | 10 | none beyond _lib |
|
||||
| demo-concierge | `demo-concierge.rego` | 4 | read-only |
|
||||
| demo-watchdog | `demo-watchdog.rego` | 4 | none beyond _lib |
|
||||
|
||||
## L0 contract
|
||||
|
||||
- **Input** (`POST /v1/data/scobleclaw/allow`):
|
||||
```json
|
||||
{ "input": { "agent_id": "main-agent", "kamelet_ref": "kamelet:source/forgejo-search", "params": {}, "request_id": "...", "timestamp": "..." } }
|
||||
```
|
||||
- **Output**:
|
||||
```json
|
||||
{ "result": true }
|
||||
```
|
||||
or `{ "result": false }` if `deny` is non-empty or the kamelet is not on
|
||||
the agent's allowlist. The judge-integration Camel-K binding translates
|
||||
`(allow, deny, needs_l1)` into a single `{decision, reasons, decision_id}`
|
||||
envelope for the audit-sink.
|
||||
|
||||
## Apply
|
||||
|
||||
```sh
|
||||
kubectl apply -f scoble-policy/opa/
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
```sh
|
||||
# deployment health
|
||||
kubectl -n scobleclaw get deploy opa # 1/1 Ready
|
||||
|
||||
# service exists, ClusterIP
|
||||
kubectl -n scobleclaw get svc opa # ClusterIP 8181
|
||||
|
||||
# policies loaded
|
||||
kubectl -n scobleclaw exec deploy/opa -- \
|
||||
wget -qO- http://localhost:8181/v1/policies | jq
|
||||
|
||||
# smoke test (allow)
|
||||
kubectl -n scobleclaw exec deploy/opa -- \
|
||||
wget -qO- --post-data='{"input":{"agent_id":"main-agent","kamelet_ref":"kamelet:source/forgejo-search","params":{}}}' \
|
||||
--header='Content-Type: application/json' \
|
||||
http://localhost:8181/v1/data/scobleclaw/allow
|
||||
# expected: {"result": true}
|
||||
|
||||
# smoke test (deny: external email)
|
||||
kubectl -n scobleclaw exec deploy/opa -- \
|
||||
wget -qO- --post-data='{"input":{"agent_id":"main-agent","kamelet_ref":"kamelet:source/send-email","params":{"to":"bob@evil.com","domain":"evil.com"}}}' \
|
||||
--header='Content-Type: application/json' \
|
||||
http://localhost:8181/v1/data/scobleclaw/allow
|
||||
# expected: {"result": false}
|
||||
```
|
||||
|
||||
## Out of scope
|
||||
|
||||
- judge-integration Camel-K binding (separate ticket — calls OPA at
|
||||
`/v1/data/scobleclaw/allow`)
|
||||
- llm-classifier-integration (L1)
|
||||
- discord-escalation-integration (L2)
|
||||
- audit-sink OPA log tailing (separate ticket, blocked on CDI/CRD scope
|
||||
creep — see eng#46/eng#93/eng#94)
|
||||
76
scoble-policy/opa/opa-deployment.yaml
Normal file
76
scoble-policy/opa/opa-deployment.yaml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
# Single-replica OPA deployment. Fail-closed: if the pod dies, agents fall
|
||||
# back to deny-by-default (the L1/L2 callers treat an OPA timeout as deny).
|
||||
#
|
||||
# Resource footprint is intentionally small: 64Mi mem, 100m CPU. Rego
|
||||
# evaluation is <1ms per call so we don't need a beefy pod.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: opa
|
||||
namespace: scobleclaw
|
||||
labels:
|
||||
app: opa
|
||||
component: l0-judge
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
# Recreate avoids split-brain while the ConfigMap volume is being
|
||||
# remounted on a content change.
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: opa
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: opa
|
||||
component: l0-judge
|
||||
spec:
|
||||
serviceAccountName: opa
|
||||
containers:
|
||||
- name: opa
|
||||
image: openpolicyagent/opa:0.68.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- "run"
|
||||
- "--server"
|
||||
- "--addr=http://0.0.0.0:8181"
|
||||
- "--log-level=info"
|
||||
- "--log-format=json"
|
||||
- "/policies"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8181
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 2
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
volumeMounts:
|
||||
- name: policies
|
||||
mountPath: /policies
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: policies
|
||||
configMap:
|
||||
name: opa-policies
|
||||
137
scoble-policy/opa/opa-policies-configmap.yaml
Normal file
137
scoble-policy/opa/opa-policies-configmap.yaml
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
---
|
||||
# ConfigMap containing the Rego policy bundle. Mounted at /policies in the
|
||||
# OPA pod. Policies share the `scobleclaw` package; per-agent files add
|
||||
# their own allow + agent-specific deny rules on top of _lib's base denies.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: opa-policies
|
||||
namespace: scobleclaw
|
||||
labels:
|
||||
app: opa
|
||||
component: l0-judge
|
||||
data:
|
||||
_lib.rego: |
|
||||
package scobleclaw
|
||||
|
||||
internal_email_domain = "scoble.me"
|
||||
|
||||
payment_kamelets = {
|
||||
"kamelet:source/stripe-charge",
|
||||
"kamelet:source/paypal-transfer",
|
||||
"kamelet:source/ach-debit",
|
||||
"kamelet:sink/wire-transfer",
|
||||
}
|
||||
|
||||
is_external_email(domain) {
|
||||
domain != internal_email_domain
|
||||
domain != ""
|
||||
}
|
||||
|
||||
is_payment_kamelet(ref) {
|
||||
payment_kamelets[ref]
|
||||
}
|
||||
|
||||
is_forgejo_write_outside_allowed(params) {
|
||||
params.owner != "marcus"
|
||||
}
|
||||
|
||||
is_forgejo_write_outside_allowed(params) {
|
||||
params.owner == "marcus"
|
||||
params.repo != "engineering"
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
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],
|
||||
)
|
||||
}
|
||||
|
||||
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_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",
|
||||
}
|
||||
|
||||
allow {
|
||||
input.agent_id == "main-agent"
|
||||
main_agent_allowed_kamelets[input.kamelet_ref]
|
||||
count(deny) == 0
|
||||
}
|
||||
|
||||
needs_l1 {
|
||||
input.agent_id == "main-agent"
|
||||
not main_agent_allowed_kamelets[input.kamelet_ref]
|
||||
not payment_kamelets[input.kamelet_ref]
|
||||
}
|
||||
demo-concierge.rego: |
|
||||
package scobleclaw
|
||||
|
||||
demo_concierge_allowed_kamelets = {
|
||||
"kamelet:source/forgejo-search",
|
||||
"kamelet:source/forgejo-read",
|
||||
"kamelet:source/ob1-search",
|
||||
"kamelet:source/kamelet-dispatch",
|
||||
}
|
||||
|
||||
allow {
|
||||
input.agent_id == "demo-concierge"
|
||||
demo_concierge_allowed_kamelets[input.kamelet_ref]
|
||||
count(deny) == 0
|
||||
}
|
||||
|
||||
needs_l1 {
|
||||
input.agent_id == "demo-concierge"
|
||||
not demo_concierge_allowed_kamelets[input.kamelet_ref]
|
||||
not payment_kamelets[input.kamelet_ref]
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
demo-watchdog.rego: |
|
||||
package scobleclaw
|
||||
|
||||
demo_watchdog_allowed_kamelets = {
|
||||
"kamelet:source/ob1-search",
|
||||
"kamelet:source/ob1-list",
|
||||
"kamelet:source/discord-post",
|
||||
"kamelet:source/kamelet-dispatch",
|
||||
}
|
||||
|
||||
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 payment_kamelets[input.kamelet_ref]
|
||||
}
|
||||
21
scoble-policy/opa/opa-service.yaml
Normal file
21
scoble-policy/opa/opa-service.yaml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
# ClusterIP service exposing OPA at :8181. Cluster-internal only — the
|
||||
# judge-integration (Camel-K) pods in the scobleclaw namespace will resolve
|
||||
# `opa.scobleclaw.svc.cluster.local:8181`.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: opa
|
||||
namespace: scobleclaw
|
||||
labels:
|
||||
app: opa
|
||||
component: l0-judge
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: opa
|
||||
ports:
|
||||
- name: http
|
||||
port: 8181
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
11
scoble-policy/opa/opa-serviceaccount.yaml
Normal file
11
scoble-policy/opa/opa-serviceaccount.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
# ServiceAccount for OPA. Minimal — no RBAC bindings required for OPA itself,
|
||||
# but a dedicated SA keeps the audit trail clean.
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: opa
|
||||
namespace: scobleclaw
|
||||
labels:
|
||||
app: opa
|
||||
component: l0-judge
|
||||
93
scoble-policy/opa/policies/_lib.rego
Normal file
93
scoble-policy/opa/policies/_lib.rego
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package scobleclaw
|
||||
|
||||
# _lib.rego — shared helpers + base deny rules for all agents.
|
||||
# Every per-agent policy (main_agent, demo_concierge, demo_watchdog)
|
||||
# imports this file by including these rules in its own `allow` / `deny`
|
||||
# definitions. Rego merges rules across files in the same package, so
|
||||
# helpers below are visible everywhere in `scobleclaw.*`.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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. An agent file only needs
|
||||
# to add its own `deny` rules on top.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Rule 1: no external email. Any kamelet whose params include a `to` field
|
||||
# with an external email domain is denied.
|
||||
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.
|
||||
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.
|
||||
deny[reason] {
|
||||
is_payment_kamelet(input.kamelet_ref)
|
||||
reason := sprintf("payment kamelet blocked at L0: %v", [input.kamelet_ref])
|
||||
}
|
||||
36
scoble-policy/opa/policies/demo-concierge.rego
Normal file
36
scoble-policy/opa/policies/demo-concierge.rego
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package scobleclaw
|
||||
|
||||
# demo-concierge.rego — policy for the demo concierge agent.
|
||||
#
|
||||
# Scope: read-only Forgejo + OB1 read access. No writes anywhere. This is a
|
||||
# demo / smoke-test agent; its allowlist is intentionally small.
|
||||
|
||||
demo_concierge_allowed_kamelets = {
|
||||
"kamelet:source/forgejo-search",
|
||||
"kamelet:source/forgejo-read",
|
||||
"kamelet:source/ob1-search",
|
||||
"kamelet:source/kamelet-dispatch",
|
||||
}
|
||||
|
||||
# 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]
|
||||
not payment_kamelets[input.kamelet_ref]
|
||||
}
|
||||
|
||||
# Per-agent deny rule: demo-concierge is read-only. Any kamelet whose
|
||||
# params suggest a write (presence of `body` + `create_*` action) is denied.
|
||||
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])
|
||||
}
|
||||
29
scoble-policy/opa/policies/demo-watchdog.rego
Normal file
29
scoble-policy/opa/policies/demo-watchdog.rego
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package scobleclaw
|
||||
|
||||
# demo-watchdog.rego — policy for the demo watchdog agent.
|
||||
#
|
||||
# Scope: monitoring-only. Can read OB1 thoughts and ping Discord, but cannot
|
||||
# touch Forgejo, kubectl, or any kamelet with side-effects.
|
||||
|
||||
demo_watchdog_allowed_kamelets = {
|
||||
"kamelet:source/ob1-search",
|
||||
"kamelet:source/ob1-list",
|
||||
"kamelet:source/discord-post",
|
||||
"kamelet:source/kamelet-dispatch",
|
||||
}
|
||||
|
||||
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 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.
|
||||
54
scoble-policy/opa/policies/main-agent.rego
Normal file
54
scoble-policy/opa/policies/main-agent.rego
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package scobleclaw
|
||||
|
||||
# main-agent.rego — policy for the primary orchestrator agent.
|
||||
#
|
||||
# Scope: this is the agent that drives engineering work (Forgejo reads/writes,
|
||||
# OB1 captures, kamelet dispatch). It has the tightest allowlist.
|
||||
#
|
||||
# This file uses the same package as _lib.rego, so all the shared `deny`
|
||||
# rules apply automatically. We add a per-agent `allow` rule that defines
|
||||
# the kamelet allowlist and a couple of agent-specific deny rules.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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 payment_kamelets[input.kamelet_ref]
|
||||
}
|
||||
|
||||
# Anything not main-agent falls through to the per-agent policy files.
|
||||
Loading…
Add table
Add a link
Reference in a new issue