Use the examples below as starting points, adapt them to your repo, and validate them with the Go-based CLI:
eunox validate ./eunox.policy.yaml
# reference the policy from a route's `policy:` in eunox.yaml, then:
eunox proxy --config ./eunox.yaml
| Policy | Upstream | What it enforces |
|---|---|---|
filesystem.policy.yaml
|
@modelcontextprotocol/server-filesystem |
Writes & deletes confined to /data/; executable
file types blocked.
|
postgres.policy.yaml
|
@modelcontextprotocol/server-postgres |
Non-SELECT SQL blocked; credential and audit tables
blocked.
|
github.policy.yaml
|
@modelcontextprotocol/server-github |
Write tools rate-limited to prevent runaway automation. |
slack.policy.yaml
|
@modelcontextprotocol/server-slack |
Direct messages restricted to your domain via
recipientDomain.
|
fetch.policy.yaml
|
mcp-server-fetch |
HTTP blocked; userinfo authority blocked; private RFC-1918 and metadata endpoints blocked (lexical SSRF guard). |
Filesystem
Stops the agent from reading sensitive file types and confines writes to
a sandbox directory. argumentSchema is a top-level field on
the constraint (not a condition); maxCalls uses
count.
schemaVersion: "0.1"
name: filesystem-agent
version: 0.1.0
capabilities:
- target: tool:read_file
actions: [call]
conditions:
- type: allowedExtensions
argument: path
extensions: [".csv", ".json", ".txt", ".md"]
- target: tool:write_file
actions: [call]
argumentSchema:
type: object
required: [path, content]
properties:
path: { type: string, pattern: "/data/.*" }
content: { type: string }
additionalProperties: false
conditions:
- type: allowedExtensions
argument: path
extensions: [".csv", ".json", ".txt", ".md"]
- type: maxCalls
count: 100
windowSeconds: 60
- target: tool:list_directory
actions: [call]
Postgres
SELECT-only, with a per-session call cap. Table-level
protection comes from running the upstream under a read-only database
role — the proxy can't reliably pull table names out of free-form SQL,
so it doesn't try.
schemaVersion: "0.1"
name: postgres-agent
version: 0.1.0
capabilities:
- target: tool:query
actions: [call]
conditions:
- type: allowedOperations
argument: sql
operations: [SELECT, EXPLAIN, SHOW]
- type: maxCalls
count: 100
windowSeconds: 60
GitHub
Read-heavy, write-rate-limited — protects against runaway issue or PR creation.
schemaVersion: "0.1"
name: github-agent
version: 0.1.0
capabilities:
- target: tool:search_repositories
actions: [call]
- target: tool:get_file_contents
actions: [call]
- target: tool:create_issue
actions: [call]
conditions:
- type: maxCalls
count: 10
windowSeconds: 3600
- target: tool:create_pull_request
actions: [call]
conditions:
- type: maxCalls
count: 5
windowSeconds: 3600
Slack
Restricts DMs to your own domain — a single
recipientDomain condition stops agents from leaking
conversations. Channel posting (post_message) addresses
Slack channel IDs (e.g. C12345), so a domain gate doesn't
apply there.
schemaVersion: "0.1"
name: slack-agent
version: 0.1.0
capabilities:
- target: tool:send_dm
actions: [call]
conditions:
- type: recipientDomain
argument: to
domains: [company.com]
- type: maxCalls
count: 20
windowSeconds: 3600
- target: tool:post_message
actions: [call]
conditions:
- type: maxCalls
count: 50
windowSeconds: 3600
Fetch — lexical SSRF guard
Blocks http://, userinfo authorities, and any bare IP-literal
host by only allowing a dotted DNS hostname — covering the private
RFC-1918 ranges and the cloud metadata endpoint
169.254.169.254 along with every other raw IP. The guard is
implemented purely with argumentSchema + maxCalls
— no custom condition module required. Read it as a
defense-in-depth guard rather than a complete SSRF solution —
pair with network-level controls in production.
schemaVersion: "0.1"
name: fetch-agent
version: 0.1.0
capabilities:
- target: tool:fetch
actions: [call]
argumentSchema:
type: object
required: [url]
additionalProperties: false
properties:
url:
type: string
# https-only, DNS-hostname allow-list. Go's regexp (RE2) has no
# lookahead, so this blocks by shape instead of enumerating bad
# prefixes: the host must be a dotted name ending in an alphabetic
# label, which structurally excludes bare IPv4/IPv6 literals
# (including the 169.254.169.254 metadata endpoint) and userinfo
# (user@host). It does not resolve hostnames, so a private name
# that resolves to an internal address is not caught here — pair
# with network egress controls for that.
pattern: '^https://(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}(?::\d{1,5})?(?:/\S*)?$'
method:
type: string
enum: [GET, HEAD, OPTIONS]
conditions:
- type: maxCalls
count: 60
windowSeconds: 60
tools/call,
resources/read, prompts/get,
sampling/createMessage, and unrecognized methods. Only
capabilities explicitly listed in the manifest are permitted.