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