Skip to content

Commit 01e6a03

Browse files
committed
feat(docker): add PIGuard prompt-injection sidecar to compose
- Add piguard (daemon) and piguard-gateway (HTTP bridge) services to all odek docker-compose profiles. - Mount a read-only ./piguard/models volume for the one-time-exported ONNX model and add docker/piguard/download-model.sh to populate it. - Wire bundled config.restricted.json and config.godmode.json to point the guard at http://piguard-gateway:8080/detect. - Document setup, optional surfaces, and how to disable the sidecar in docker/README.md.
1 parent 78fa7b5 commit 01e6a03

8 files changed

Lines changed: 214 additions & 5 deletions

File tree

docker/.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ GIT_COMMITTER_EMAIL=you@example.com
7171
# ODEK_SCHEDULES_TELEGRAM_ADMIN_CHATS=11111111 # operator chat IDs that may manage schedules and /restart
7272
# ODEK_SCHEDULES_TELEGRAM_ADMIN_USERS=11111111 # operator user IDs that may manage schedules and /restart
7373

74+
# ── Prompt-injection guard (PIGuard sidecar) ─────────────────────────────
75+
# The bundled config.*.json files point the guard at the in-compose sidecar.
76+
# To fall back to the local rule-only guard (no sidecar, no model download),
77+
# uncomment:
78+
# ODEK_GUARD_PROVIDER=local
79+
#
80+
# To enable optional surfaces, uncomment and set true:
81+
# ODEK_GUARD_SCAN_SKILLS=true
82+
# ODEK_GUARD_SCAN_TOOL_OUTPUTS=true
83+
# ODEK_GUARD_SCAN_TELEGRAM=true
84+
7485
# ── Semantic embeddings (llama.cpp sidecar; see docker/README.md) ────────
7586
# The compose file runs a private llama.cpp server (the `llama-embeddings`
7687
# service) that gives odek real semantic embeddings. Both bundled configs set

docker/README.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ For the full walkthrough, threat model, and tuning, see
2222
docker/
2323
├── Dockerfile # multi-stage build of the odek binary
2424
├── Dockerfile.embeddings # llama.cpp embeddings sidecar (bundled GGUF)
25-
├── docker-compose.yml # odek (4 profiles) + searxng + llama-embeddings
25+
├── docker-compose.yml # odek (4 profiles) + searxng + llama-embeddings + piguard
2626
├── config.restricted.json # Restricted permission policy
2727
├── config.godmode.json # Godmode (YOLO) permission policy
2828
├── .env.example # copy to .env, add your API key
29+
├── piguard/ # PIGuard model download helper
30+
│ ├── download-model.sh
31+
│ └── models/ # populated once by download-model.sh
2932
└── workspace/ # the dir the agent works in (mounted in)
3033
```
3134

@@ -246,6 +249,79 @@ See [`../docs/MEMORY.md`](../docs/MEMORY.md) → *Pluggable Embeddings*,
246249
sidecar, so nothing leaves the compose network; if you repoint it at a cloud API, that
247250
text egresses.
248251

252+
## Prompt-injection guard (PIGuard sidecar)
253+
254+
The compose setup can run a private **[go-prompt-injection-guard](https://github.com/BackendStack21/go-prompt-injection-guard)**
255+
sidecar for a semantic second opinion on high-trust inputs — no external guard API, no keys.
256+
257+
What it guards:
258+
259+
- **Memory** — legacy facts, `memory` tool writes, Extended Memory atoms/recall/user model.
260+
- **System prompt**`IDENTITY.md`, `--system`, and project-level `AGENTS.md`.
261+
- **MCP descriptions** — tool descriptions supplied by MCP servers.
262+
- Optionally: skills, Telegram captions/transcripts, and external tool outputs.
263+
264+
Both bundled configs (`config.restricted.json` and `config.godmode.json`) set:
265+
266+
```json
267+
"guard": {
268+
"provider": "piguard",
269+
"url": "http://piguard-gateway:8080/detect",
270+
"scan": { "memory": true, "system_prompt": true, "mcp_descriptions": true }
271+
}
272+
```
273+
274+
The sidecar is internal-only (no host port) and reachable only by the odek containers
275+
over the compose network at `http://piguard-gateway:8080`.
276+
277+
### One-time model download
278+
279+
The PIGuard model (~735 MB) is **not** baked into the image — it is exported once and
280+
mounted read-only from `./piguard/models`. Before starting odek with the guard for the
281+
first time, run:
282+
283+
```bash
284+
./piguard/download-model.sh
285+
```
286+
287+
This downloads the export script and requirements from the guard repo, exports the ONNX
288+
model and tokenizer inside a disposable Python container, and copies the result to
289+
`docker/piguard/models/`. It is safe to re-run; it skips if the model is already present.
290+
291+
### Running with the guard
292+
293+
No extra flags are needed — the bundled configs already point at the sidecar. The guard
294+
container (`piguard`) and its HTTP bridge (`piguard-gateway`) co-start with every odek
295+
profile:
296+
297+
```bash
298+
docker compose --profile restricted up --build
299+
```
300+
301+
If the sidecar is unavailable, the odek guard falls back to the local rule-based scan
302+
(`danger.ScanInjection`) because `fallback_to_local` is `true`.
303+
304+
### Disabling the sidecar
305+
306+
To use the local rule scan only (no model download, no extra container), set in `.env`:
307+
308+
```bash
309+
ODEK_GUARD_PROVIDER=local
310+
```
311+
312+
Or edit the `guard` block in `config.*.json` to `"provider": "local"`.
313+
314+
### Optional guard surfaces
315+
316+
The bundled configs enable only the core surfaces. To also guard skills, tool outputs, or
317+
Telegram media, set the corresponding `guard.scan.*` flag in `config.*.json` or via env:
318+
319+
```bash
320+
ODEK_GUARD_SCAN_SKILLS=true
321+
ODEK_GUARD_SCAN_TOOL_OUTPUTS=true
322+
ODEK_GUARD_SCAN_TELEGRAM=true
323+
```
324+
249325
## Verify the profiles differ
250326

251327
- **Restricted**: ask it to `rm -rf` everything in `/workspace` → denied, never runs.

docker/config.godmode.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@
2828
"dims": 768,
2929
"timeout_seconds": 10
3030
},
31+
"guard": {
32+
"provider": "piguard",
33+
"url": "http://piguard-gateway:8080/detect",
34+
"threshold": 0.9,
35+
"timeout_seconds": 5,
36+
"fallback_to_local": true,
37+
"scan": {
38+
"memory": true,
39+
"system_prompt": true,
40+
"mcp_descriptions": true,
41+
"skills": false,
42+
"tool_outputs": false,
43+
"telegram": false
44+
}
45+
},
3146
"memory": {
3247
"enabled": true,
3348
"facts_limit_user": 1500,

docker/config.restricted.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@
2828
"dims": 768,
2929
"timeout_seconds": 10
3030
},
31+
"guard": {
32+
"provider": "piguard",
33+
"url": "http://piguard-gateway:8080/detect",
34+
"threshold": 0.9,
35+
"timeout_seconds": 5,
36+
"fallback_to_local": true,
37+
"scan": {
38+
"memory": true,
39+
"system_prompt": true,
40+
"mcp_descriptions": true,
41+
"skills": false,
42+
"tool_outputs": false,
43+
"telegram": false
44+
}
45+
},
3146
"memory": {
3247
"enabled": true,
3348
"facts_limit_user": 1500,

docker/docker-compose.yml

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ services:
3030
- ./workspace:/workspace
3131
- ./config.restricted.json:/home/odek/.odek/config.json:ro
3232
restart: "no"
33-
depends_on: [searxng, llama-embeddings]
33+
depends_on: [searxng, llama-embeddings, piguard-gateway]
3434

3535
# ── Godmode (all permissions) — non-interactive, disposable container ──
3636
odek-godmode:
@@ -50,7 +50,7 @@ services:
5050
- ./workspace:/workspace
5151
- ./config.godmode.json:/home/odek/.odek/config.json:ro
5252
restart: "no"
53-
depends_on: [searxng, llama-embeddings]
53+
depends_on: [searxng, llama-embeddings, piguard-gateway]
5454

5555
# ── Telegram bot — Restricted (approvals via inline keyboards) ──
5656
# State (sessions, skills, telegram.pid) lives in the local ./.odek folder —
@@ -74,7 +74,7 @@ services:
7474
- ./.odek:/home/odek/.odek
7575
- ./config.restricted.json:/home/odek/.odek/config.json:ro
7676
restart: unless-stopped
77-
depends_on: [searxng, llama-embeddings]
77+
depends_on: [searxng, llama-embeddings, piguard-gateway]
7878

7979
# ── Telegram bot — Godmode (no prompts; unrestricted) ──
8080
# Same ./.odek state folder; config.godmode.json is layered on top of
@@ -93,7 +93,7 @@ services:
9393
- ./.odek:/home/odek/.odek
9494
- ./config.godmode.json:/home/odek/.odek/config.json:ro
9595
restart: unless-stopped
96-
depends_on: [searxng, llama-embeddings]
96+
depends_on: [searxng, llama-embeddings, piguard-gateway]
9797

9898
# ── llama.cpp embeddings — local backend for the shared "embedding" block ──
9999
# Co-starts with every odek profile so semantic embeddings work out of the box
@@ -151,3 +151,45 @@ services:
151151
# SearXNG needs outbound internet to query upstream engines (Google, Bing,
152152
# DuckDuckGo, …). Behind the advanced allowlisting egress proxy, permit those.
153153
restart: unless-stopped
154+
155+
# ── PIGuard prompt-injection sidecar ─────────────────────────────────────
156+
# Optional semantic second-opinion guard for memory, system prompt, MCP
157+
# descriptions, skills, Telegram, and tool outputs. The daemon loads a
158+
# one-time-exported ONNX model (~735 MB); run ./piguard/download-model.sh
159+
# before starting an odek profile to populate ./piguard/models.
160+
#
161+
# The daemon speaks newline-delimited JSON over a private Unix socket; the
162+
# gateway bridges that to plain HTTP on the compose network so odek can use
163+
# its existing HTTP-based piguard client without sharing a socket volume.
164+
piguard:
165+
profiles: ["restricted", "godmode", "telegram-restricted", "telegram-godmode"]
166+
image: ghcr.io/backendstack21/go-prompt-injection-guard:v1.0.0
167+
command:
168+
- "--socket=/run/piguard/piguard.sock"
169+
- "--model-dir=/models"
170+
- "--max-batch=32"
171+
- "--batch-wait=5ms"
172+
volumes:
173+
- ./piguard/models:/models:ro
174+
- piguard-sock:/run/piguard
175+
healthcheck:
176+
test: ["CMD-SHELL", "test -S /run/piguard/piguard.sock"]
177+
interval: 10s
178+
timeout: 3s
179+
retries: 5
180+
start_period: 60s
181+
restart: unless-stopped
182+
183+
piguard-gateway:
184+
profiles: ["restricted", "godmode", "telegram-restricted", "telegram-godmode"]
185+
image: ghcr.io/backendstack21/go-prompt-injection-guard-gateway:v1.0.0
186+
command: ["--addr=:8080", "--socket=/run/piguard/piguard.sock"]
187+
volumes:
188+
- piguard-sock:/run/piguard
189+
depends_on:
190+
piguard:
191+
condition: service_healthy
192+
restart: unless-stopped
193+
194+
volumes:
195+
piguard-sock:

docker/piguard/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# PIGuard model files — downloaded once by download-model.sh; do not commit.
2+
*.onnx
3+
*.data
4+
*.json
5+
tokenizer.*
6+
!/.gitkeep
7+
!/.gitignore

docker/piguard/download-model.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
# Download the PIGuard ONNX model and tokenizer into docker/piguard/models/.
3+
# This is a one-time step required before the PIGuard sidecar can start in
4+
# Docker Compose. The exported model is ~735 MB.
5+
6+
set -euo pipefail
7+
8+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9+
MODEL_DIR="${SCRIPT_DIR}/models"
10+
mkdir -p "${MODEL_DIR}"
11+
12+
if [ -f "${MODEL_DIR}/model.onnx" ] && [ -f "${MODEL_DIR}/tokenizer.json" ]; then
13+
echo "PIGuard model already present in ${MODEL_DIR}; nothing to do."
14+
exit 0
15+
fi
16+
17+
PIGUARD_REF="${PIGUARD_REF:-v1.0.0}"
18+
19+
echo "Downloading and exporting PIGuard model (${PIGUARD_REF})..."
20+
echo "Output directory: ${MODEL_DIR}"
21+
22+
# Run the export in a disposable Python container so no host Python/toolchain is
23+
# required. The guard repo's export_onnx.py downloads the model from HuggingFace
24+
# and converts it to ONNX.
25+
docker run --rm \
26+
-v "${MODEL_DIR}:/out" \
27+
-e HF_HOME=/tmp \
28+
python:3.12-slim bash -c "
29+
set -euo pipefail
30+
apt-get update >/dev/null
31+
apt-get install -y --no-install-recommends git curl ca-certificates >/dev/null
32+
git clone --depth 1 --branch '${PIGUARD_REF}' \
33+
https://github.com/BackendStack21/go-prompt-injection-guard.git /src
34+
cd /src
35+
pip install --no-cache-dir -r scripts/requirements.txt >/dev/null
36+
python scripts/export_onnx.py
37+
cp ~/.cache/piguard/onnx/* /out/
38+
"
39+
40+
echo "Done. Model files written to ${MODEL_DIR}:"
41+
ls -lh "${MODEL_DIR}"

docker/piguard/models/.gitkeep

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Placeholder so the empty models directory is tracked by git.
2+
# Real model files (*.onnx, tokenizer.json, config.json) are gitignored.

0 commit comments

Comments
 (0)