Declarative kernel policy-routing reconciler. Define desired routing state in JSON; the daemon continuously reconciles the Linux kernel routing tables and policy rules to match — event-driven, idempotent, and hardened for production.
Multi-architecture images (linux/amd64, linux/arm64) are published automatically on every semantic release.
# Pinned release (recommended for deterministic CI / sandboxes)
docker pull ghcr.io/dedsecorg/agentic-route:1.0.0
# Moving major release (auto-receives non-breaking fixes)
docker pull ghcr.io/dedsecorg/agentic-route:v1
# Latest build from default branch
docker pull ghcr.io/dedsecorg/agentic-route:latestBecause routing reconciliation communicates directly with host Netlink sockets, the container requires the NET_ADMIN capability and host network namespace:
# Query routing state and status
docker run --rm \
--cap-add=NET_ADMIN \
--network=host \
ghcr.io/dedsecorg/agentic-route:v1 status
# Run the reconciliation loop in daemon mode
docker run -d \
--name agentic-route \
--restart unless-stopped \
--cap-add=NET_ADMIN \
--network=host \
-v /etc/hermes-route:/etc/hermes-route \
ghcr.io/dedsecorg/agentic-route:v1 daemonThis repository is indexed on Context7 with 148 code snippets for AI-assisted development. Use the Context7 MCP server or visit the link above for searchable documentation.
# Query via Context7 MCP
# "How to install agentic-route?"
# "agentic-route commands reference"
# "intent.json schema"VPN daemons (NordVPN, ProtonVPN, Tailscale, WireGuard) fight over the kernel routing table. They add/remove ip rule and ip route entries on connect/disconnect, often clobbering each other. The result: traffic leaks, SSH freezes, DNS breaks, mesh networks route through the wrong interface.
agentic-route fixes this by treating routing as declarative state:
- You declare intent (
/etc/agentic-route/intent.json) — forbidden rules, pinned routes, custom rules - Daemon discovers reality — reads live
ip rule show/ip route showvia Netlink - Reconciler computes delta — desired = discovered + intent
- Applies surgically — only
ip rule add/del/ip route replacefor actual drift - Emits status — writes
/run/agentic-route/state.jsonwith observed state + timestamp
+---------------------+ inotifywait +------------------+
| /etc/agentic-route/|<---------------------| intent.json |
| (directory watch) | (survives vim) | (user intent) |
+----------+----------+ +------------------+
| ^
| events | edit
v |
+---------------------+ ip monitor |
| FIFO multiplexer |<--------------------------+
| (exec 3<> "$FIFO") | kernel Netlink
+----------+----------+
| 200ms debounce
v
+---------------------+
| reconcile binary |
| (idempotent) |
+----------+----------+
| surgical ip rule/route
v
+---------------------+
| Kernel routing |
| tables + rules |
+---------------------+
Hardened against 5 classic Bash daemon bugs:
- Subshell scope isolation -> single consumer loop in main process
- Netlink echo loop -> idempotent reconcile + debounce
- Burst storm (50 events/100ms) ->
read -t 0.2drains burst - inotify inode trap -> directory watch survives
vimatomic rename - FIFO EOF death ->
exec 3<> "$FIFO"holds pipe open permanently
git clone https://github.com/dedsecorg/agentic-route
cd agentic-route
sudo -S -p '' ./install.shInstalls:
/usr/local/bin/agentic-route— main CLI/usr/local/bin/agentic-route-reconcile— idempotent one-shot/usr/local/bin/agentic-route-daemon— event-driven daemon/usr/local/lib/agentic-route/core.sh— reconciliation engine/etc/agentic-route/intent.json— your routing intent/etc/systemd/system/agentic-route-daemon.service— systemd unit
npx -y @smithery/cli install @dedsecorg/agentic-route{
"version": 1,
"comment": "User intent only. Daemon writes discovered state to /run/agentic-route/state.json.",
"forbidden_rules": [
{ "prio": 31580, "match": "suppress_prefixlength 0", "reason": "Remove VPN capture rule at this priority" },
{ "match": "100.64.0.0/10 lookup main", "reason": "Never let VPN redirect mesh CIDR through main table" }
],
"pinned_routes": [
{ "dst": "198.51.100.7/32", "via": "192.0.2.1", "dev": "eth0", "reason": "Keep VPN endpoint reachable outside tunnel" },
{ "dst": "default", "via": "192.0.2.1", "dev": "eth0", "reason": "Bare-metal fallback egress" }
],
"custom_rules": [
{ "prio": 480, "selector": "from all to 100.64.0.0/10", "action": "lookup 52" },
{ "prio": 32765, "selector": "not from all fwmark 0xe1f1", "action": "lookup 205" }
]
}| Section | Purpose |
|---|---|
forbidden_rules |
Rules to remove if present. Optional prio gates deletion to exact priority (prevents catching lookalikes). |
pinned_routes |
Routes to ensure exist. Uses ip route replace — idempotent. |
custom_rules |
Rules to add if missing. Merged with discovered rules. |
| Command | Description |
|---|---|
agentic-route status |
Show desired vs live rules/routes + drift |
agentic-route check |
Exit 0 if clean, 1 if drift (no mutation) |
agentic-route diff |
Precise read-only diff: +add, -del, ~replace |
agentic-route trace <target> [from <src>] |
Trace packet path via ip route get |
agentic-route enforce |
One-shot surgical reconciliation |
agentic-route reconcile |
Idempotent one-shot (discovers + intent + applies) |
agentic-route daemon |
Event-driven daemon (inotify + ip monitor + FIFO) |
agentic-route monitor |
Legacy: `ip monitor |
agentic-route mcp |
Read-only stdio MCP JSON-RPC server |
agentic-route api |
mTLS REST API server (port 8099, client cert required; see docs/pki.md) |
reconcile |
daemon |
|
|---|---|---|
| Trigger | Manual / cron | Events (file change + Netlink) |
| Latency | Immediate | < 200ms after event |
| Use case | CI, deploy hooks, on-demand | Continuous production |
| Idempotence | Strict | Strict |
sudo -S -p '' systemctl enable --now agentic-route-daemonHardened unit:
CAP_NET_ADMINonlyProtectSystem=strictReadWritePaths=/run/agentic-route /etc/agentic-routeRestart=always,RestartSec=2- Runs in tmpfs (
/run/agentic-route/)
Start read-only MCP server:
agentic-route mcpTools exposed:
route_status— live + desired rules/routesroute_check— drift detection (boolean)route_diff— precise diff outputroute_trace— packet path tracing
Mutation (enforce) deliberately not exposed — apply path stays a CLI/operator action.
Problem: NordVPN (egress), ProtonVPN (DNS only), Tailscale (mesh) all manage routing.
Solution: intent.json
{
"forbidden_rules": [
{ "prio": 31580, "match": "suppress_prefixlength 0", "reason": "Proton suppress-bypass" },
{ "prio": 31581, "match": "lookup 245447468", "reason": "Proton split-tunnel capture" },
{ "match": "100.64.0.0/10 lookup main", "reason": "Tailscale hijack prevention" }
],
"pinned_routes": [
{ "dst": "194.126.177.6/32", "via": "185.170.112.1", "dev": "eth0" },
{ "dst": "10.2.0.1/32", "dev": "proton0" },
{ "dst": "default", "via": "185.170.112.1", "dev": "eth0" }
],
"custom_rules": [
{ "prio": 480, "selector": "from all to 100.64.0.0/10", "action": "lookup 52" },
{ "prio": 32765, "selector": "not from all fwmark 0xe1f1", "action": "lookup 205" }
]
}Result:
- ProtonVPN never owns host egress (DNS only via
proton0) - Tailscale mesh (100.64.0.0/10) -> table 52, never main
- NordVPN (fwmark 0xe1f1) owns default egress via table 205
- Proton WG endpoint pinned via eth0 (tunnel never loses handshake)
- Bare-metal default route survives VPN restarts
- Linux kernel 4.19+ (Netlink
ip monitor) iproute2(ip,jq)inotify-tools(inotifywait)bash4.4+CAP_NET_ADMIN(for daemon)
MIT — see LICENSE.
- hermes-route — private fork with real IPs, same engine