# The decision point for tool-call parameters. # # Cluster-scoped and platform-owned, like everything else in this repo: every # domain gateway asks the same server, so a rule is written once rather than # five times, and changing it does not mean redeploying five gateways. apiVersion: v1 kind: Namespace metadata: name: opa --- apiVersion: v1 kind: ConfigMap metadata: name: opa-policy namespace: opa data: policy.rego: | # Authorisation on tool-call *parameters*, not just the tool name. # # The estate already answers "may this identity call this tool?" — the MCP # server filters tool discovery by the caller's roles. What nothing answered is # "may it call it with *these* arguments". `write` authorises writing; it does # not authorise this particular record. A read cap counts calls; it does not # stop one call asking for ten thousand rows. # # This runs at the gateway, so it holds whether the caller is an agent behaving # itself, an agent that has been talked into something, or a person with curl. package kong import rego.v1 default allow := false # Everything that is not an MCP tool call passes: the session handshake, the # tool listing, the REST surface. This policy has an opinion about arguments # and no opinion about anything else. allow if not is_tool_call allow if { is_tool_call count(violations) == 0 } is_tool_call if { input.request.http.parsed_body.method == "tools/call" } tool := input.request.http.parsed_body.params.name args := object.get(input.request.http.parsed_body.params, "arguments", {}) # --- how much one call may read ------------------------------------------- # # The rate limit counts requests. Without this, one request asking for a # hundred thousand rows costs the same as one asking for ten — which is how a # volume cap is defeated without breaking it. max_page_size := 200 violations contains msg if { size := to_number(object.get(args, "query_page_size", 0)) size > max_page_size msg := sprintf("query_page_size %d exceeds the maximum of %d", [size, max_page_size]) } # --- what one call may commit --------------------------------------------- # # A price is the parameter that turns a permitted write into an expensive # mistake. The bound is deliberately wide: this is not pricing policy, it is a # guard against a value that can only be an error or an attack. max_unit_price := 100000 violations contains msg if { body := object.get(args, "body", {}) some field in ["buyprice", "msrp"] value := to_number(object.get(body, field, 0)) value > max_unit_price msg := sprintf("%s %v exceeds the maximum of %d", [field, value, max_unit_price]) } violations contains msg if { body := object.get(args, "body", {}) some field in ["buyprice", "msrp"] to_number(object.get(body, field, 1)) <= 0 msg := sprintf("%s must be greater than zero", [field]) } # What the caller is told. A refusal that does not say which argument was # refused sends someone looking in the wrong place. reason := concat("; ", violations) --- apiVersion: apps/v1 kind: Deployment metadata: name: opa namespace: opa spec: # Stateless: every decision is a function of the request. Two replicas because # it sits on the request path of every write in the estate — an OPA that is # down is a gateway that refuses. replicas: 2 selector: matchLabels: app: opa template: metadata: labels: app: opa spec: securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: opa image: openpolicyagent/opa:1.11.0 args: - run - --server - --addr=0.0.0.0:8181 - --log-level=info - /policy/policy.rego ports: - name: http containerPort: 8181 volumeMounts: - name: policy mountPath: /policy readOnly: true readinessProbe: httpGet: path: /health port: http initialDelaySeconds: 3 periodSeconds: 10 livenessProbe: httpGet: path: /health port: http initialDelaySeconds: 10 periodSeconds: 20 resources: requests: cpu: 20m memory: 64Mi limits: cpu: 500m memory: 256Mi securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] volumes: - name: policy configMap: name: opa-policy --- apiVersion: v1 kind: Service metadata: name: opa namespace: opa spec: selector: app: opa ports: - name: http port: 8181 targetPort: http