Skip to content

fix(guard): correct PIGuard score semantics and vendor JSON-faithful HTTP gateway - #85

Merged
jkyberneees merged 1 commit into
mainfrom
fix/piguard-memory-guard
Jul 22, 2026
Merged

fix(guard): correct PIGuard score semantics and vendor JSON-faithful HTTP gateway#85
jkyberneees merged 1 commit into
mainfrom
fix/piguard-memory-guard

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Problem

Reported against the Extended Memory flow with docker/config.restricted.json: the memory injection detector rejected every proposed fact — including innocuous ones — with scores pegged at 0.98–0.99.

Reproduced live against the actual PiGuard compose stack. Root cause is two independent bugs, each alone sufficient to reject everything (not the session taint system, which works differently and produces no scores).

Bug 1 — threshold applied to the wrong quantity (internal/guard/piguard.go)

resultFromResponse treated the sidecar score as an injection probability:

injected := r.Label == "INJECTION" || r.Score >= threshold

The PIGuard score is the confidence of the predicted label, whichever label that is — a confident BENIGN result also scores ~1.0 (confirmed against upstream README and live responses: "The user prefers tea over coffee." → BENIGN, score 0.99998). With threshold: 0.9, the Score >= threshold clause rejected virtually every response. The threshold now only gates INJECTION labels:

injected := r.Label == "INJECTION" && r.Score >= threshold

Regression coverage: the fake server in piguard_test.go now returns realistic high-confidence BENIGN scores (0.9997 — the old 0.1 masked the bug), plus a new TestResultFromResponse_ThresholdSemantics table test.

Bug 2 — gateway double-encodes odek's JSON (docker/)

The compose stack bridges odek → daemon via the upstream examples/http-gateway, which is a raw-text demo: it wraps the entire request body as the text. odek's client POSTs JSON {"text": "..."}, so the model scored the JSON wrapper instead of the content. Live results with the old gateway: "Deployments happen every Friday at 5pm." → INJECTION 0.992, "The user prefers dark mode in their IDE." → INJECTION 0.984.

Fix: vendored gateway docker/piguard-gateway/ that forwards daemon-protocol JSON (text/long/texts) verbatim to the Unix socket (raw-text fallback kept for curl). docker-compose.yml now builds it locally; its Dockerfile drops the amd64-pinned base digests so it builds natively on Apple Silicon (the upstream-pinned digests produce x86-64 binaries inside arm64-labeled images — rosetta error crash — a third, build-time issue documented in compose comments).

E2E verification (live stack, odek's exact JSON payloads)

Input Old gateway New gateway
8 benign facts / blog texts 3× INJECTION @ 0.98–0.99 8/8 BENIGN
4 real injections (ignore-instructions, DAN, ssh exfil, memory-poisoning) INJECTION 4/4 INJECTION @ 0.96–0.9999 — still rejected

/healthz, raw-text mode, and batch /raw verified working. go vet clean; go test ./internal/guard/... ./internal/memory/... all green.

Notes

  • docker/piguard/.gitignore: ignore modeling_*.py, an artifact the model export drops into models/.
  • Out of scope (documented in compose comments): the upstream daemon Dockerfile itself still pins amd64 base digests; on Apple Silicon build locally without the pins and tag piguard:local.

…HTTP gateway

Two independent bugs caused the PIGuard sidecar to reject virtually every
memory fact with scores pegged at 0.98-0.99, including innocuous content:

1. resultFromResponse treated the sidecar score as an injection probability
   (label == INJECTION || score >= threshold). The score is actually the
   confidence of the predicted label - a confident BENIGN result scores
   ~1.0 as well - so the threshold clause rejected almost everything at
   the default 0.9. The threshold now only gates INJECTION labels.

2. The docker-compose gateway (upstream examples/http-gateway, a raw-text
   demo) wrapped odek's JSON {"text":...} bodies as literal text, so the
   model scored the JSON wrapper instead of the content, flipping benign
   facts to INJECTION at 0.98-0.99. Replaced with a vendored gateway
   (docker/piguard-gateway) that forwards daemon-protocol JSON verbatim
   (with raw-text fallback for curl) and builds without amd64-pinned base
   digests (Apple Silicon safe).

Verified end-to-end against a live stack: 8/8 benign facts classified
BENIGN, 4/4 real injections classified INJECTION (still rejected), plus
regression tests for the threshold semantics.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek 58e0783 Commit Preview URL

Branch Preview URL
Jul 21 2026, 09:29 PM

@jkyberneees
jkyberneees merged commit 8275f7f into main Jul 22, 2026
8 checks passed
jkyberneees added a commit that referenced this pull request Jul 22, 2026
The upstream go-prompt-injection-guard Dockerfile pins its base images by
amd64-only sha256 digests, so building the remote git context on Apple
Silicon produces x86-64 binaries (piguard-server, libonnxruntime.so)
inside an arm64-labeled image and the container crash-loops with
"rosetta error: failed to open elf at /lib64/ld-linux-x86-64.so.2".

Vendor the daemon build in docker/piguard-daemon/Dockerfile: it compiles
the pinned upstream v1.0.0 source (git clone --branch v1.0.0) with
unpinned base image tags, which resolve to the host architecture. The
upstream Makefile is arch-aware and selects the aarch64 ONNX Runtime
when GOARCH=arm64, so the resulting image contains native binaries
(verified: ELF e_machine b7 = EM_AARCH64 for both the server binary and
libonnxruntime.so; container reaches healthy and correctly labels
BENIGN/INJECTION through the gateway).

This replaces the manual Apple Silicon workaround documented in PR #85.
jkyberneees added a commit that referenced this pull request Jul 22, 2026
… transport (#87)

Two follow-ups to the PIGuard false-positive investigation (#85):

1. Scan-rejected atoms no longer vanish. The guard scan in addAtom ran
   before quarantine routing and hard-dropped rejections, so any guard
   false positive silently destroyed a legitimate memory with no review
   path. Rejections now go to quarantine with a recorded reason
   ("scan_rejected: ..." alongside "tainted"), visible via
   odek memory extended quarantine. PromoteAtom skips the guard rescan:
   the human review is the approval, and a rescan would reject the same
   false positive again. The extractor no longer pre-scans atoms - the
   single scan gate is at persistence, so extraction and manual adds get
   identical quarantine-on-reject behavior.

2. guard.socket_path actually works now. The piguard client dialed the
   Unix socket but spoke HTTP, while the go-prompt-injection-guard daemon
   speaks newline-delimited JSON - every scan errored and fallback_to_local
   silently degraded to local-only scanning. Socket mode now forwards the
   request as one NDJSON line per call (matching the daemon protocol),
   surfaces {"error":...} daemon replies, and is covered by fake-daemon
   socket tests plus an E2E run against the real daemon container
   (BENIGN facts pass, injections rejected, batch/long work).
jkyberneees added a commit that referenced this pull request Jul 22, 2026
The upstream go-prompt-injection-guard Dockerfile pins its base images by
amd64-only sha256 digests, so building the remote git context on Apple
Silicon produces x86-64 binaries (piguard-server, libonnxruntime.so)
inside an arm64-labeled image and the container crash-loops with
"rosetta error: failed to open elf at /lib64/ld-linux-x86-64.so.2".

Vendor the daemon build in docker/piguard-daemon/Dockerfile: it compiles
the pinned upstream v1.0.0 source (git clone --branch v1.0.0) with
unpinned base image tags, which resolve to the host architecture. The
upstream Makefile is arch-aware and selects the aarch64 ONNX Runtime
when GOARCH=arm64, so the resulting image contains native binaries
(verified: ELF e_machine b7 = EM_AARCH64 for both the server binary and
libonnxruntime.so; container reaches healthy and correctly labels
BENIGN/INJECTION through the gateway).

This replaces the manual Apple Silicon workaround documented in PR #85.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant