📝 Description
Adds a session-scoped GPU-resident OCR framebuffer that decompresses and composites raw RDP bitmap tiles on-device for OCR, exact-change detection, and cache reuse. The enforcing Rust guard retains a trusted CPU shadow canvas needed to synthesize redaction PDUs and recover fail-closed sequence conflicts; it no longer materializes or transfers full BMP windows to the CUDA sidecar.
📣 User-facing impact
GPU-backed RDP data masking avoids full-window BMP encoding, transfer, decode, and redundant OCR work, reducing masking latency for unchanged or partially changed screens.
🔗 Related Issue
Linear DEP-102. Depends on #1717. Paired enterprise guard: hoophq/libhoop#109.
🚀 Type of Change
- 🐛 Bug fix (non-breaking change which fixes an issue)
- ✨ New feature (non-breaking change which adds functionality)
- 💥 Breaking change (fix or feature which causes existing functionality not to work as expected)
- 📚 Documentation update
- 🎨 Style/UI update
- ♻️ Code refactor
- ⚡ Performance improvement
- ✅ Test update
- 🔧 Build configuration change
- 🧹 Chore
📋 Changes Made
- Add a sequenced raw-RDP resident OCR transport with CUDA decompression/composition, dirty-region reuse, bounded per-session caches, and fail-closed sequence recovery.
- Move the enforcing Rust guard to libhoop, retain an API-compatible OSS passthrough stub, and fail release builds when enforcement is unavailable.
- Pin and validate the GPU OCR runtime, advertise the raw-RDP capability only when healthy, and require it before selecting agent-side RDP masking.
- Bound frame geometry, decoded work, result cardinality, redaction output, active detections, and terminal cancellation paths.
🧪 Testing
Test Configuration:
- Browser(s): N/A
- OS: macOS 26 arm64; Linux NVIDIA T4 GPU host
Tests performed:
-
Unit tests pass
-
Integration tests pass
-
Manual testing completed
-
go test ./gateway/broker ./gateway/rdp/... ./gateway/transport ./gateway/models: 7 packages passed; 1 package had no tests. -
cargo test --features require-enforcementinagentrs: 70 tests passed across 4 suites. -
OSS
guard_surfacepluscargo check: 2 tests passed and the stub build completed. -
pytest -q scripts/dev/ocr-poc/test_raw_image.py: 53 tests passed. -
Production CUDA image tests on the T4 host: 54 tests passed, including real CUDA RDP decoder parity and resident cache behavior.
📸 Screenshots (if applicable)
N/A.
✅ Checklist
- My code follows the style guidelines of this project
- I have performed a self-review of my own code
- I have commented my code, particularly in hard-to-understand areas
- My changes generate no new warnings
- New and existing unit tests pass locally with my changes
- I have checked my code and corrected any misspellings
📄 Additional Notes
Merge after #1717. Presidio remains the CPU/network policy stage. The trusted guard deliberately keeps CPU shadow compositing for redaction PDU synthesis and sequence recovery; removing it safely requires a GPU diff-reporting and pixel-export protocol. This change removes full-window BMP marshaling and sidecar host preprocessing, not every host bitmap operation.
Automated by MisterMal
Activity
21📋 API Changelog
API Changelog unknown vs. unknown
API Changes
GET /serverinfo
- removed the
alcatrazenum value from theredact_providerresponse property for the response status200
PR Summary by Qodo
Add GPU-resident RDP OCR processing
✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes
AI Description
-
• Decode and composite RDP bitmap tiles in GPU-resident session framebuffers. • Reuse unchanged OCR results while bounding caches, queues, and sensitive-data lifetimes. • Enforce guard availability and durable violation reporting across agent releases.
Diagram
-
sequenceDiagram participant RDP as RDP Server participant Agent as Rust Agent participant Guard as RDP Guard participant OCR as OCR Sidecar participant GPU as GPU Store participant Gateway as Gateway participant DB as Audit DB RDP->>Agent: Bitmap updates Agent->>Guard: Guarded stream Guard->>OCR: Sequenced RDP tiles OCR->>GPU: Decode and composite GPU-->>OCR: Cached OCR results OCR-->>Guard: Words and sequence Guard->>Gateway: Violation report Gateway->>DB: Atomic persistence DB-->>Gateway: Commit Gateway-->>Guard: Report acknowledgement
High-Level Assessment
-
The following are alternative approaches to this PR:
1. Send decoded RGBA dirty tiles
-
- ➕ Avoids implementing RDP bitmap decoding in CUDA.
- ➕ Keeps the sidecar protocol simpler.
- ➖ Retains CPU decoding and RGBA expansion costs.
- ➖ Transfers more data and weakens GPU-resident performance.
2. Embed CUDA OCR in the Rust guard
-
- ➕ Eliminates HTTP transport and cross-process sequencing.
- ➕ Shares lifecycle directly with enforcement.
- ➖ Requires complex Rust, CUDA, and ONNX integration.
- ➖ Couples transport enforcement to GPU runtime dependencies.
3. Maintain GPU state in the gateway
-
- ➕ Centralizes state and audit persistence.
- ➕ Avoids report acknowledgement machinery.
- ➖ Moves high-volume screen data away from GPU agents.
- ➖ Recreates gateway latency and memory pressure.
Recommendation: Keep the raw-RDP sidecar design. It preserves the existing process boundary while removing full-window encoding, transfer, and decode work, and the CPU shadow remains available for redaction PDU synthesis. The CUDA decoder parity and fail-closed sequence recovery deserve the deepest review.
-
Files changed (68) +7432 / -1217
-
Enhancement (19)
+4902 / -713-
gw.rs
Mark example relay frames as raw+4/-4
Mark example relay frames as raw
• Updates example frame construction for the control/data discriminator.proxy.rs
Integrate acknowledged guard events+242/-132
Integrate acknowledged guard events
• Uses the external gate, routes read-ahead through enforcement, and settles reports during teardown.rdp_proxy.rs
Harden RDP activation and credentials+283/-82
Harden RDP activation and credentials
• Pins guarded capabilities, preserves buffered bytes, and zeroizes credentials after CredSSP.session.rs
Add typed frame headers+55/-8
Add typed frame headers
• Uses the length word's high bit for frame kind and validates exact payload lengths.client.rs
Advertise guard capabilities safely+251/-51
Advertise guard capabilities safely
• Builds capabilities from the linked guard, tracks report acknowledgements, and joins connection tasks.control.rs
Define frame protocol v2+7/-0
Define frame protocol v2
• Adds capability constants for negotiated typed framing.message.rs
Encode explicitly typed controls+45/-4
Encode explicitly typed controls
• Sets and validates the control discriminator and rejects malformed control envelopes.message_types.rs
Add violation acknowledgement messages+12/-4
Add violation acknowledgement messages
• Defines serialization and parsing for persisted-report acknowledgements.proxy.rs
Bound reporting and relay tasks+259/-92
Bound reporting and relay tasks
• Retries stable report IDs, bounds response forwarding, owns child cancellation, and transfers zeroizing credentials.rdp_message_processor.rs
Harden multiplexed RDP sessions+367/-159
Harden multiplexed RDP sessions
• Negotiates typed frames, bounds inputs, isolates instances, resolves guards, and matches report acknowledgements.headers.go
Implement typed agent framing+81/-18
Implement typed agent framing
• Adds frame-kind encoding, exact validation, negotiated v2 controls, and acknowledgement constants.protocol_rdp.go
Bind sessions to agent and audit identities+18/-15
Bind sessions to agent and audit identities
• Propagates relay errors, records instance and database IDs, retains audit routes, and negotiates controls.session.go
Attach IDs to guardrail reports+3/-0
Attach IDs to guardrail reports
• Adds an optional report ID for deduplicating agent retries.rdp_guardrail.go
Persist guardrail evidence transactionally+73/-0
Persist guardrail evidence transactionally
• Locks sessions, deduplicates report IDs, and atomically appends audit data and detections.websocket.go
Harden agent routing and audit delivery+432/-126
Harden agent routing and audit delivery
• Bounds frames and workers, isolates instances, validates controls, persists reports, and acknowledges commits.bucket_rec.py
Expose cached detector sessions+11/-4
Expose cached detector sessions
• Separates detector lookup from CPU preprocessing for direct CUDA input binding.gpu_pipeline.py
Implement GPU-resident RDP OCR+1928/-0
Implement GPU-resident RDP OCR
• Adds CUDA decoding, compositing, hashing, OCR binding, bounded caches, sequencing, eviction, and scrubbing.raw_image.py
Define strict resident frame decoders+456/-0
Define strict resident frame decoders
• Validates bounded RGBA and RDP bodies, patches, geometry, bitmap depth, chunks, and wire versions.server_rapidocr.py
Expose resident GPU OCR APIs+375/-14
Expose resident GPU OCR APIs
• Adds raw and resident endpoints, serialized admission, health capabilities, reaping, release, and conflict handling.
Bug fix (4)
+507 / -234-
stream.rs
Bound RDP stream queues+165/-60
Bound RDP stream queues
• Uses poll-aware channels, preserves unread tails, chunks output, and enforces a strict byte limit.communicators.go
Serialize and bound agent writes+15/-1
Serialize and bound agent writes
• Protects WebSocket writes with a mutex and deadline.session.go
Isolate relay and teardown state+310/-166
Isolate relay and teardown state
• Makes admission nonblocking, fixes deadline accounting, scopes instances, and retains audit routes.irongw.go
Preserve post-handshake RDP connections+17/-7
Preserve post-handshake RDP connections
• Passes the durable session ID and clears the temporary TLS handshake deadline.
Refactor (7)
+295 / -27-
lib.rs
Provide a fail-closed OSS guard surface+241/-0
Provide a fail-closed OSS guard surface
• Implements an API-compatible passthrough stub that advertises no enforcement and rejects delegated guarded sessions.guard.rs
Define the agent guard boundary+41/-0
Define the agent guard boundary
• Re-exports the selected guard crate and rejects enforcement-required stub builds.lib.rs
Expose the extracted guard module+1/-1
Expose the extracted guard module
• Replaces the in-tree piigate export with the guard facade.main.rs
Wire the extracted guard into agentrs+1/-1
Wire the extracted guard into agentrs
• Replaces the local piigate module with the swappable guard facade.session.rs
Zeroize stored session credentials+5/-18
Zeroize stored session credentials
• Removes unnecessary cloning and switches session guard configuration to the external crate.types.rs
Simplify channels and track acknowledgements+5/-7
Simplify channels and track acknowledgements
• Stores one bounded sender per session and adds pending violation waiters.rdp.go
Adapt native RDP creation+1/-0
Adapt native RDP creation
• Supplies an empty durable ID for the native unguarded path.
Documentation (5)
+14 / -9-
0002-rdp-pii-guard-line-ocr-cache.md
Record the guard's move to libhoop+8/-3
Record the guard's move to libhoop
• Clarifies that historical agent guard paths now live in libhoop with an OSS stub.worker.go
Update benchmark references+4/-4
Update benchmark references
• Refers analyzer documentation to the external benchmark harness.piigate_metrics.go
Update guard metrics documentation+1/-1
Update guard metrics documentation
• Points agent-side redaction ownership to libhoop.server.py
Update benchmark terminology+1/-1
Update benchmark terminology
• Replaces references to the deleted benchmark with the external harness.index.js
Update canvas security references+0/-0
Update canvas security references
• Points the desktop-size contract to the extracted guard implementation.
Other (33)
+1714 / -234-
.env.sample
Document resident GPU OCR settings+13/-0
Document resident GPU OCR settings
• Documents resident-RDP transport and session, VRAM, TTL, and cache bounds.agent-ocr-build.yml
Track GPU OCR pipeline build inputs+3/-0
Track GPU OCR pipeline build inputs
• Adds bucket, raw transport, and GPU pipeline modules to OCR image path filters.pullrequest-release.yml
Build PR artifacts with paired libhoop+67/-0
Build PR artifacts with paired libhoop
• Resolves a paired libhoop branch and checks its pinned revision out for Rust artifact builds.pullrequest.yml
Test OSS and enterprise guard integrations+88/-0
Test OSS and enterprise guard integrations
• Adds dual-flavor Rust jobs, enforcement linkage validation, and a FreeRDP plumbing test.release.yml
Require libhoop in Rust release jobs+39/-0
Require libhoop in Rust release jobs
• Checks out private libhoop so enforcement-required releases cannot resolve to the OSS stub..gitignore
Align ignored artifacts with guard extraction+4/-10
Align ignored artifacts with guard extraction
• Ignores the OSS guard lockfile and removes exclusions for the deleted RDP benchmark.Dockerfile.agent-ocr
Keep CPU OCR on BMP transport+5/-0
Keep CPU OCR on BMP transport
• Packages shared transport helpers and explicitly selects BMP compatibility mode.Dockerfile.agent-ocr-gpu
Enable resident CUDA OCR by default+23/-11
Enable resident CUDA OCR by default
• Pins CuPy, packages the GPU pipeline, selects resident-RDP processing, and enforces one worker.Dockerfile.localbuild
Support swappable guard crates locally+45/-5
Support swappable guard crates locally
• Copies either guard flavor and supports enforcement-required Cargo builds without breaking Go mappings.Makefile
Add guard-aware builds and RDP tests+101/-7
Add guard-aware builds and RDP tests
• Adds non-destructive libhoop resolution, dual-flavor tests, enforcement-required releases, and FreeRDP validation.Cargo.toml
Define the OSS RDP guard crate+15/-0
Define the OSS RDP guard crate
• Creates a package-compatible path dependency for the OSS guard replacement.Cargo.lock
Resolve extracted guard dependencies+159/-67
Resolve extracted guard dependencies
• Adds hoop-rdp-guard and updates transitive dependencies after extracting the guard implementation.Cargo.toml
Depend on the swappable guard+18/-6
Depend on the swappable guard
• Adds the path-based guard crate and compile-time enforcement feature while removing moved dependencies.rdp_caps_probe.rs
Probe bitmap-only RDP negotiation+79/-23
Probe bitmap-only RDP negotiation
• Adds an empty-codec mode verifying the Fast-Path bitmap stream required by redaction.guard_surface.rs
Contract-test guard implementations+132/-0
Contract-test guard implementations
• Type-checks the guard seam and verifies delegated enforcement never silently disappears.docker-compose.gcp-gpu-agent.env.sample
Document production GPU limits+18/-2
Document production GPU limits
• Recommends one worker and documents resident framebuffer, VRAM, TTL, and cache sizing.deploy/docker-compose/docker-compose.gcp-gpu-agent.env.sample
docker-compose.gcp-gpu-agent.yml
Configure resident GPU OCR+18/-3
Configure resident GPU OCR
• Enables raw RDP GPU processing with bounded resident-store settings and single-process ownership.agent_capabilities_test.go
Test replacement capability isolation+59/-0
Test replacement capability isolation
• Verifies superseded sockets close and stale instances cannot affect replacements.headers_test.go
Test frame validation and negotiation+122/-0
Test frame validation and negotiation
• Covers malformed lengths, explicit kinds, and legacy-to-v2 transitions.session_bench_test.go
Benchmark nonblocking admission+3/-8
Benchmark nonblocking admission
• Updates slow-consumer expectations for per-session rejection instead of shared backpressure.session_queue_test.go
Expand relay lifecycle tests+202/-88
Expand relay lifecycle tests
• Tests byte bounds, deadlines, unread tails, shared-agent safety, instance routing, and audit retention.rdp_entity_detection_test.go
Test idempotent guardrail persistence+83/-0
Test idempotent guardrail persistence
• Verifies report retries create one audit entry and one detection set.irongw_test.go
Test TLS deadline cleanup+85/-0
Test TLS deadline cleanup
• Confirms reads continue after the temporary handshake deadline expires.agent_guardrails_test.go
Test bounded agent reports+271/-0
Test bounded agent reports
• Covers report limits, queue saturation, framing negotiation, malformed inputs, and acknowledgement ordering.Dockerfile.rapidocr
Package raw transport helpers+1/-1
Package raw transport helpers
• Includes bucket and raw image modules in the CPU PoC image.Dockerfile.rapidocr-gpu
Build resident GPU OCR runtime+31/-2
Build resident GPU OCR runtime
• Adds CuPy, packages the GPU pipeline, and prebuilds FP16 recognition models.device_policy.py
Enforce single-process ownership+21/-0
Enforce single-process ownership
• Rejects multi-worker startup when process-local GPU framebuffers are enabled.entrypoint-embedded.sh
Launch configured OCR workers+9/-1
Launch configured OCR workers
• Validates WEB_CONCURRENCY and passes it to Uvicorn.test_gpu_pipeline.py
Test resident GPU processing+0/-0
Test resident GPU processing
• Covers sequencing, eviction, cache reuse, scrubbing, decoder parity, compositing, and failure cleanup.test_raw_image.py
Test resident transport decoding+0/-0
Test resident transport decoding
• Covers body bounds, media types, geometry, patch budgets, depths, chunks, and malformed data.test_resolve_device.py
Test worker topology validation+0/-0
Test worker topology validation
• Verifies resident mode requires one process and validates positive worker counts.Dockerfile
Add an NLA-capable test server+0/-0
Add an NLA-capable test server
• Builds a FreeRDP shadow server for TLS, CredSSP, capability, and bitmap tests.start.sh
Serve deterministic RDP test content+0/-0
Serve deterministic RDP test content
• Starts a headless PII display, configures NLA credentials, and launches FreeRDP.
-
Code Review by Qodo
🐞 Bugs (0) 📘 Rule violations (2) 📎 Requirement gaps (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)
1. Header.Encode panics on overflow 📘 Rule violation ☼ Reliability ⭐ New
Description
Header.Encode terminates the process when a frame exceeds the wire-length limit instead of returning an error to its caller. A malformed or oversized frame can therefore crash the gateway rather than fail the affected operation safely.
Code
gateway/broker/headers.go[R83-84]+ if h.Len > headerLengthMask { + panic("frame payload exceeds wire limit")
Relevance
●●● StrongPanic in non-main helper on overflow is a clear reliability bug; team fixes such issues per rule 1807202.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
Rule 1807202 prohibits panic in non-main helpers. The newly added overflow branch in Header.Encode directly panics instead of returning a recoverable encoding error.
Rule 1807202: Do not terminate the process in library/command helpers; return errors to callergateway/broker/headers.go[79-94]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description `Header.Encode` panics when the payload length exceeds the wire limit, allowing a helper failure to terminate the gateway process. ## Issue Context Compliance rule 1807202 requires non-main helpers to return errors rather than panic. Update the encoding API and propagate or handle the resulting error at all callers. ## Fix Focus Areas - gateway/broker/headers.go[79-94] - gateway/broker/session.go[260-265]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
2. Proxy discards shutdown error ✓ Resolved 📘 Rule violation ☼ Reliability ⭐ New
Description
The client-to-server relay explicitly discards the asynchronous stream shutdown error without documenting why that failure is safe to ignore. Shutdown failures can consequently be treated as successful session closure with no error propagated to the proxy owner.
Evidence
Rule 1807206 requires I/O errors to be handled, propagated, or explicitly justified when ignored. The new relay executes let _ = server_wr.shutdown().await and provides no justification or error path.
Rule 1807206: Do not ignore errors from OS/exec/IO calls; handle or propagate themagentrs/src/proxy.rs[150-162]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The guarded proxy relay discards the result of `server_wr.shutdown().await`, silently swallowing an I/O failure. ## Issue Context Compliance rule 1807206 requires OS and I/O errors to be handled or propagated. Return the shutdown error through the relay outcome, or explicitly document why it is safe to ignore after ensuring cleanup remains correct. ## Fix Focus Areas - agentrs/src/proxy.rs[150-162]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
3. Graphics bypass enterprise guard ✗ Dismissed 🔗 Cross-repo conflict ⛨ Security ⭐ New
Description
The new activation interceptor forwards every frame for which libhoop's capability pinning returns None, even though None only means the frame is not the target capability PDU. A slow-path graphics update received before both capabilities are pinned therefore reaches the client before Gate::spawn, bypassing libhoop OCR and redaction.
Code
agentrs/src/rdp_proxy.rs[R765-768]+ client_framed + .write_all(patched.as_deref().unwrap_or(frame.as_ref())) + .await + .context("relay server activation PDU to client")?;
Relevance
●● ModerateComplex security/architectural gap in enterprise guard forwarding; no close precedent found either way.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The PR forwards the original server frame whenever libhoop returns no patched frame, and the enterprise library documents and implements None for any valid frame that is not Demand Active. The libhoop gate is only spawned afterward, so such bytes cannot be inspected or redacted.
agentrs/src/rdp_proxy.rs[732-768]agentrs/src/rdp_proxy.rs[185-207]agentrs/src/proxy.rs[133-143]External repo: hoophq/libhoop, rdp-guard/src/lib.rs [98-114]External repo: hoophq/libhoop, rdp-guard/src/piigate/capabilities.rs [156-168]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The guarded activation interceptor forwards non-capability server frames before the enterprise `Gate` exists. Because libhoop returns `Ok(None)` for any valid non-Demand-Active frame, a graphics update can bypass OCR and redaction. ## Issue Context Continue relaying activation control traffic required for negotiation, but classify server traffic during interception and fail closed or safely retain any graphics/update PDU until both capability frames are pinned and the gate is active. ## Fix Focus Areas - agentrs/src/rdp_proxy.rs[732-802] - agentrs/src/proxy.rs[138-143]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
View high (1)
4. Full snapshot sequence rollback ✓ Resolved 🐞 Bug ≡ Correctness
Description
GpuResidentStore.process accepts full snapshots without validating them against an existing session's expected sequence, allowing a delayed or replayed snapshot to replace newer framebuffer state and move the sequence backward. Subsequent valid incremental requests then fail with HTTP 409 after the newer state has already been discarded.
Code
scripts/dev/ocr-poc/gpu_pipeline.py[R1825-1829]+ if full_snapshot: + # Preserve the agent's globally monotonic sequence when state + # moves between Uvicorn workers. Resetting to zero can make stale, + # divergent worker framebuffers alias the same later sequence. + self._drop(session_key)
Relevance
●● ModerateThe rollback race is a substantive correctness concern, but no comparable historical precedent was found for this GPU snapshot sequencing design.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The full-snapshot branch drops and recreates an existing session without comparing the supplied sequence to state.sequence, while only incremental requests receive an equality check. The accepted sequence later becomes the session's new expected sequence, and the HTTP layer exposes resulting mismatches as 409 conflicts.
scripts/dev/ocr-poc/gpu_pipeline.py[1818-1839]scripts/dev/ocr-poc/gpu_pipeline.py[1893-1903]scripts/dev/ocr-poc/server_rapidocr.py[683-684]scripts/dev/ocr-poc/server_rapidocr.py[736-737]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description Full snapshots unconditionally replace existing resident state, even when their sequence precedes the session's expected sequence. Reject stale full snapshots before dropping the current framebuffer while still allowing a newer full snapshot to resynchronize missing state. ## Issue Context The accepted request sets `state.sequence = sequence + 1`, so accepting an older snapshot rolls sequence state backward and causes later legitimate incremental requests to conflict. ## Fix Focus Areas - scripts/dev/ocr-poc/gpu_pipeline.py[1818-1839] - scripts/dev/ocr-poc/gpu_pipeline.py[1893-1897] - scripts/dev/ocr-poc/test_gpu_pipeline.py[492-511]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
5. Write deadline is ineffective ✗ Dismissed 🐞 Bug ☼ Reliability ⭐ New
Description
sessionConnWrapper.Write checks the deadline only before calling SendRawDataToAgent, so a write started just before expiry can continue beyond the configured deadline. During the RDP handshake this defeats the two-second connection deadline because agentCommunicator.Send replaces it with its independent ten-second WebSocket deadline.
Code
gateway/broker/session.go[R622-625]+ if !deadline.IsZero() && !time.Now().UTC().Before(deadline) { + return 0, context.DeadlineExceeded + } + if err := s.session.SendRawDataToAgent(b); err != nil {
Relevance
●● ModerateSubtle semantic deadline-handling bug across files; no directly matching precedent found for this pattern.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The wrapper only compares the deadline before sending, while the invoked communicator applies a fresh fixed ten-second deadline to the underlying WebSocket. The RDP gateway deliberately installs a two-second deadline that remains active through handshakeAgentTLS, so this implementation does not enforce that handshake write bound.
gateway/broker/session.go[618-628]gateway/broker/communicators.go[12-36]gateway/rdp/irongw.go[378-400]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description `sessionConnWrapper.Write` only checks whether the write deadline has already expired. It does not apply that deadline to the actual agent WebSocket write, which instead receives the communicator's fixed ten-second timeout. ## Issue Context The RDP gateway sets a two-second deadline before reading the X224 response and performing the TLS handshake. A write beginning before that deadline can therefore block until the longer communicator timeout. ## Fix Focus Areas - gateway/broker/session.go[618-628] - gateway/broker/communicators.go[30-36] - gateway/rdp/irongw.go[378-400]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
6. Deadlines use non-UTC clock ✓ Resolved 📘 Rule violation ≡ Correctness
Description
New agent WebSocket and session write-deadline logic uses time.Now() rather than time.Now().UTC(). This violates the required UTC convention for gateway deadlines and deadline comparisons.
Code
gateway/broker/communicators.go[R33-35]+ if err := a.conn.SetWriteDeadline(time.Now().Add(agentWriteTimeout)); err != nil { + return err + }
Relevance
●●● StrongUTC deadline convention is a deterministic correctness fix, and repository history accepts closely related time normalization.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
agentCommunicator.Send calculates a WebSocket deadline with time.Now().Add(...), and sessionConnWrapper.Write compares its deadline against another non-UTC time.Now() value.
Rule 1807215: Use UTC time for persisted timestamps, expirations, and deadlinesgateway/broker/communicators.go[31-35]gateway/broker/session.go[619-624]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description New gateway deadline calculations and comparisons use local `time.Now()` values. ## Issue Context PR Compliance ID 1807215 requires gateway and agent deadline handling to consistently use `time.Now().UTC()`. ## Fix Focus Areas - gateway/broker/communicators.go[31-35] - gateway/broker/session.go[619-624]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
7. OCR env vars lack sample ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new OCR resident-pipeline environment variables are not mirrored in the root .env.sample. Missing keys include OCR_GPU_PREPROCESS, OCR_RESIDENT_MAX_SESSIONS, OCR_RESIDENT_VRAM_MB, OCR_RESIDENT_TTL_SECONDS, OCR_RESIDENT_CACHE_ENTRIES, OCR_RESIDENT_RECOGNITION_CACHE_ENTRIES, and RDP_OCR_TRANSPORT.
Code
scripts/dev/ocr-poc/server_rapidocr.py[R103-104]+GPU_PREPROCESS = os.environ.get("OCR_GPU_PREPROCESS", "") == "1" +resolve_worker_concurrency(os.environ.get("WEB_CONCURRENCY"), GPU_PREPROCESS)
Relevance
●●● StrongRepository history explicitly accepted mirroring newly introduced OCR environment variables in a sample environment file.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The Python server newly reads the GPU and resident-capacity variables, and the agent images introduce RDP_OCR_TRANSPORT; a repository-wide check of the root .env.sample shows none of these keys.
Rule 1800927: New env vars must be mirrored in .env.samplescripts/dev/ocr-poc/server_rapidocr.py[100-104]scripts/dev/ocr-poc/server_rapidocr.py[418-427]Dockerfile.agent-ocr-gpu[142-148].env.sample[1-109]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description New environment variables used by the resident OCR pipeline are absent from the repository-root `.env.sample`. ## Issue Context PR Compliance ID 1800927 requires every newly introduced environment-variable key in changed code or configuration to be mirrored exactly in `.env.sample`. ## Fix Focus Areas - scripts/dev/ocr-poc/server_rapidocr.py[103-104] - scripts/dev/ocr-poc/server_rapidocr.py[418-427] - Dockerfile.agent-ocr-gpu[142-148] - Dockerfile.agent-ocr[61-64] - .env.sample[1-109]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
View medium (2)
8. Persistence workflow lives in models ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
gateway/models/rdp_entity_detection.go now coordinates session locking, duplicate-report decisions, session updates, and detection insertion in one workflow. This business orchestration belongs under gateway/services, leaving models limited to entity and ORM definitions.
Code
gateway/models/rdp_entity_detection.go[R46-49]+ return DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var lockedSessionID string + lock := tx.Raw( + `SELECT id FROM private.sessions WHERE org_id = ? AND id = ? FOR UPDATE`,
Relevance
●●● StrongRepository history explicitly accepted moving business/domain workflow out of gateway/models into services.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The added model function starts a transaction and coordinates operations across the sessions and RDP detection tables, including the domain decision that duplicate report IDs are successful no-ops.
Rule 1800917: Keep business logic out of gateway/models and in gateway/servicesgateway/models/rdp_entity_detection.go[36-46]gateway/models/rdp_entity_detection.go[60-96]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description A multi-entity guardrail persistence and idempotency workflow has been added under `gateway/models`. ## Issue Context The workflow locks a session, checks domain-level report duplication, updates session audit data, and inserts detection records transactionally. PR Compliance ID 1800917 requires this orchestration to reside in `gateway/services` rather than model files. ## Fix Focus Areas - gateway/models/rdp_entity_detection.go[36-98] - gateway/services/[1-1] - gateway/transport/websocket.go[297-335]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
9. GuardrailsViolationAck constant is misplaced ✗ Dismissed 📘 Rule violation ≡ Correctness
Description
The new guardrails_violation_ack packet type is declared independently in gateway and agent modules, and the parser also compares a raw literal. Protocol packet identifiers must come from the permitted shared common/proto/... definitions.
Relevance
●● ModerateProtocol-constant policy supports the concern, but no close accepted or rejected precedent confirms this cross-module placement requirement.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The gateway introduces the packet type directly in gateway/broker/headers.go, while the Rust agent separately defines the same string and compares another raw copy during parsing instead of consuming a permitted shared protocol definition.
Rule 1800908: Use protocol packet type constants instead of raw string literalsgateway/broker/headers.go[34-36]agentrs/src/ws/message_types.rs[27-30]agentrs/src/ws/message_types.rs[52-55]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The new `guardrails_violation_ack` protocol packet identifier is declared outside the required shared protocol modules and is also used as a raw parser literal. ## Issue Context PR Compliance ID 1800908 requires packet type identifiers to be defined under `common/proto/...` and referenced through shared constants. ## Fix Focus Areas - gateway/broker/headers.go[34-36] - agentrs/src/ws/message_types.rs[27-30] - agentrs/src/ws/message_types.rs[52-55] - common/proto/agent/[1-1]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
10. PersistRDPGuardrailViolation uses global DB ⊘ Outdated 📘 Rule violation ⌂ Architecture
Description
The new model function performs its transaction through package-global DB without accepting a *gorm.DB parameter. This prevents callers from explicitly controlling the database or transaction context.
Code
gateway/models/rdp_entity_detection.go[46]+ return DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
Relevance
● WeakA closely matching global-DB dependency-injection suggestion was explicitly rejected in PR #1649.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The function signature has no database parameter, but its first database operation invokes DB.WithContext(ctx).Transaction, directly violating the required dependency-injection pattern.
Rule 1800914: Gateway model functions must accept *gorm.DB via parameter, not globalgateway/models/rdp_entity_detection.go[40-46]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description `PersistRDPGuardrailViolation` directly accesses the package-global `DB` while performing GORM operations. ## Issue Context PR Compliance ID 1800914 requires gateway model functions that perform database operations to accept and use a `*gorm.DB` parameter. ## Fix Focus Areas - gateway/models/rdp_entity_detection.go[40-46] - gateway/transport/websocket.go[297-335]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
11. HandleConnection lacks Swagger annotations 📘 Rule violation ⚙ Maintainability
Description
The /ws Gin handler is modified without an attached Swagger comment block or matching `@Router /ws [get]` directive. Consequently, the modified gateway HTTP/WebSocket entry point is not represented by the required API annotations.
Code
gateway/transport/websocket.go[R84-85]defer wsConn.Close() + wsConn.SetReadLimit(int64(maxAgentWebSocketMessageBytes))
Relevance
● WeakA closely matching Swagger requirement for a modified gateway handler was explicitly rejected in PR #1711.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
HandleConnection processes a Gin HTTP request and is registered as GET /ws, but its declaration has no preceding Swagger block while this PR changes its request-processing behavior.
Rule 1800918: Require Swagger annotations on new or modified gateway API handlersgateway/transport/websocket.go[64-85]gateway/api/server.go[1324-1324]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The modified `HandleConnection` Gin handler has no adjacent Swagger annotations, including no `@Router` directive. ## Issue Context The handler is registered as `GET /ws` in the gateway API server. PR Compliance ID 1800918 requires new or modified gateway API handlers to carry an attached Swagger block identifying their route and method. ## Fix Focus Areas - gateway/transport/websocket.go[64-85] - gateway/api/openapi/autogen/docs.go[1-1]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Context sources
63 rules✅ Cross-repo context — repo relationships
repo: hoophq/libhoop (branch: dep-102, sha: 7531aff1)Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agentPrevious reviews
Review updated until commit a4dc9db
Results up to commit 3b63d4c ⚖️ Balanced
🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)
1. Full snapshot sequence rollback ✓ Resolved 🐞 Bug ≡ Correctness
Description
GpuResidentStore.process accepts full snapshots without validating them against an existing session's expected sequence, allowing a delayed or replayed snapshot to replace newer framebuffer state and move the sequence backward. Subsequent valid incremental requests then fail with HTTP 409 after the newer state has already been discarded.
Code
scripts/dev/ocr-poc/gpu_pipeline.py[R1825-1829]+ if full_snapshot: + # Preserve the agent's globally monotonic sequence when state + # moves between Uvicorn workers. Resetting to zero can make stale, + # divergent worker framebuffers alias the same later sequence. + self._drop(session_key)
Relevance
●● ModerateThe rollback race is a substantive correctness concern, but no comparable historical precedent was found for this GPU snapshot sequencing design.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The full-snapshot branch drops and recreates an existing session without comparing the supplied sequence to state.sequence, while only incremental requests receive an equality check. The accepted sequence later becomes the session's new expected sequence, and the HTTP layer exposes resulting mismatches as 409 conflicts.
scripts/dev/ocr-poc/gpu_pipeline.py[1818-1839]scripts/dev/ocr-poc/gpu_pipeline.py[1893-1903]scripts/dev/ocr-poc/server_rapidocr.py[683-684]scripts/dev/ocr-poc/server_rapidocr.py[736-737]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description Full snapshots unconditionally replace existing resident state, even when their sequence precedes the session's expected sequence. Reject stale full snapshots before dropping the current framebuffer while still allowing a newer full snapshot to resynchronize missing state. ## Issue Context The accepted request sets `state.sequence = sequence + 1`, so accepting an older snapshot rolls sequence state backward and causes later legitimate incremental requests to conflict. ## Fix Focus Areas - scripts/dev/ocr-poc/gpu_pipeline.py[1818-1839] - scripts/dev/ocr-poc/gpu_pipeline.py[1893-1897] - scripts/dev/ocr-poc/test_gpu_pipeline.py[492-511]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
2. Deadlines use non-UTC clock ✓ Resolved 📘 Rule violation ≡ Correctness
Description
New agent WebSocket and session write-deadline logic uses time.Now() rather than time.Now().UTC(). This violates the required UTC convention for gateway deadlines and deadline comparisons.
Code
gateway/broker/communicators.go[R33-35]+ if err := a.conn.SetWriteDeadline(time.Now().Add(agentWriteTimeout)); err != nil { + return err + }
Relevance
●●● StrongUTC deadline convention is a deterministic correctness fix, and repository history accepts closely related time normalization.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
agentCommunicator.Send calculates a WebSocket deadline with time.Now().Add(...), and sessionConnWrapper.Write compares its deadline against another non-UTC time.Now() value.
Rule 1807215: Use UTC time for persisted timestamps, expirations, and deadlinesgateway/broker/communicators.go[31-35]gateway/broker/session.go[619-624]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description New gateway deadline calculations and comparisons use local `time.Now()` values. ## Issue Context PR Compliance ID 1807215 requires gateway and agent deadline handling to consistently use `time.Now().UTC()`. ## Fix Focus Areas - gateway/broker/communicators.go[31-35] - gateway/broker/session.go[619-624]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
3. OCR env vars lack sample ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new OCR resident-pipeline environment variables are not mirrored in the root .env.sample. Missing keys include OCR_GPU_PREPROCESS, OCR_RESIDENT_MAX_SESSIONS, OCR_RESIDENT_VRAM_MB, OCR_RESIDENT_TTL_SECONDS, OCR_RESIDENT_CACHE_ENTRIES, OCR_RESIDENT_RECOGNITION_CACHE_ENTRIES, and RDP_OCR_TRANSPORT.
Code
scripts/dev/ocr-poc/server_rapidocr.py[R103-104]+GPU_PREPROCESS = os.environ.get("OCR_GPU_PREPROCESS", "") == "1" +resolve_worker_concurrency(os.environ.get("WEB_CONCURRENCY"), GPU_PREPROCESS)
Relevance
●●● StrongRepository history explicitly accepted mirroring newly introduced OCR environment variables in a sample environment file.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The Python server newly reads the GPU and resident-capacity variables, and the agent images introduce RDP_OCR_TRANSPORT; a repository-wide check of the root .env.sample shows none of these keys.
Rule 1800927: New env vars must be mirrored in .env.samplescripts/dev/ocr-poc/server_rapidocr.py[100-104]scripts/dev/ocr-poc/server_rapidocr.py[418-427]Dockerfile.agent-ocr-gpu[142-148].env.sample[1-109]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description New environment variables used by the resident OCR pipeline are absent from the repository-root `.env.sample`. ## Issue Context PR Compliance ID 1800927 requires every newly introduced environment-variable key in changed code or configuration to be mirrored exactly in `.env.sample`. ## Fix Focus Areas - scripts/dev/ocr-poc/server_rapidocr.py[103-104] - scripts/dev/ocr-poc/server_rapidocr.py[418-427] - Dockerfile.agent-ocr-gpu[142-148] - Dockerfile.agent-ocr[61-64] - .env.sample[1-109]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
4. Persistence workflow lives in models ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
gateway/models/rdp_entity_detection.go now coordinates session locking, duplicate-report decisions, session updates, and detection insertion in one workflow. This business orchestration belongs under gateway/services, leaving models limited to entity and ORM definitions.
Code
gateway/models/rdp_entity_detection.go[R46-49]+ return DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var lockedSessionID string + lock := tx.Raw( + `SELECT id FROM private.sessions WHERE org_id = ? AND id = ? FOR UPDATE`,
Relevance
●●● StrongRepository history explicitly accepted moving business/domain workflow out of gateway/models into services.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The added model function starts a transaction and coordinates operations across the sessions and RDP detection tables, including the domain decision that duplicate report IDs are successful no-ops.
Rule 1800917: Keep business logic out of gateway/models and in gateway/servicesgateway/models/rdp_entity_detection.go[36-46]gateway/models/rdp_entity_detection.go[60-96]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description A multi-entity guardrail persistence and idempotency workflow has been added under `gateway/models`. ## Issue Context The workflow locks a session, checks domain-level report duplication, updates session audit data, and inserts detection records transactionally. PR Compliance ID 1800917 requires this orchestration to reside in `gateway/services` rather than model files. ## Fix Focus Areas - gateway/models/rdp_entity_detection.go[36-98] - gateway/services/[1-1] - gateway/transport/websocket.go[297-335]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
View medium (1)
5. GuardrailsViolationAck constant is misplaced ✗ Dismissed 📘 Rule violation ≡ Correctness
Description
The new guardrails_violation_ack packet type is declared independently in gateway and agent modules, and the parser also compares a raw literal. Protocol packet identifiers must come from the permitted shared common/proto/... definitions.
Relevance
●● ModerateProtocol-constant policy supports the concern, but no close accepted or rejected precedent confirms this cross-module placement requirement.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The gateway introduces the packet type directly in gateway/broker/headers.go, while the Rust agent separately defines the same string and compares another raw copy during parsing instead of consuming a permitted shared protocol definition.
Rule 1800908: Use protocol packet type constants instead of raw string literalsgateway/broker/headers.go[34-36]agentrs/src/ws/message_types.rs[27-30]agentrs/src/ws/message_types.rs[52-55]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The new `guardrails_violation_ack` protocol packet identifier is declared outside the required shared protocol modules and is also used as a raw parser literal. ## Issue Context PR Compliance ID 1800908 requires packet type identifiers to be defined under `common/proto/...` and referenced through shared constants. ## Fix Focus Areas - gateway/broker/headers.go[34-36] - agentrs/src/ws/message_types.rs[27-30] - agentrs/src/ws/message_types.rs[52-55] - common/proto/agent/[1-1]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
6. PersistRDPGuardrailViolation uses global DB ⊘ Outdated 📘 Rule violation ⌂ Architecture
Description
The new model function performs its transaction through package-global DB without accepting a *gorm.DB parameter. This prevents callers from explicitly controlling the database or transaction context.
Code
gateway/models/rdp_entity_detection.go[46]+ return DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
Relevance
● WeakA closely matching global-DB dependency-injection suggestion was explicitly rejected in PR #1649.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
The function signature has no database parameter, but its first database operation invokes DB.WithContext(ctx).Transaction, directly violating the required dependency-injection pattern.
Rule 1800914: Gateway model functions must accept *gorm.DB via parameter, not globalgateway/models/rdp_entity_detection.go[40-46]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description `PersistRDPGuardrailViolation` directly accesses the package-global `DB` while performing GORM operations. ## Issue Context PR Compliance ID 1800914 requires gateway model functions that perform database operations to accept and use a `*gorm.DB` parameter. ## Fix Focus Areas - gateway/models/rdp_entity_detection.go[40-46] - gateway/transport/websocket.go[297-335]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
7. HandleConnection lacks Swagger annotations 📘 Rule violation ⚙ Maintainability
Description
The /ws Gin handler is modified without an attached Swagger comment block or matching `@Router /ws [get]` directive. Consequently, the modified gateway HTTP/WebSocket entry point is not represented by the required API annotations.
Code
gateway/transport/websocket.go[R84-85]defer wsConn.Close() + wsConn.SetReadLimit(int64(maxAgentWebSocketMessageBytes))
Relevance
● WeakA closely matching Swagger requirement for a modified gateway handler was explicitly rejected in PR #1711.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
HandleConnection processes a Gin HTTP request and is registered as GET /ws, but its declaration has no preceding Swagger block while this PR changes its request-processing behavior.
Rule 1800918: Require Swagger annotations on new or modified gateway API handlersgateway/transport/websocket.go[64-85]gateway/api/server.go[1324-1324]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The modified `HandleConnection` Gin handler has no adjacent Swagger annotations, including no `@Router` directive. ## Issue Context The handler is registered as `GET /ws` in the gateway API server. PR Compliance ID 1800918 requires new or modified gateway API handlers to carry an attached Swagger block identifying their route and method. ## Fix Focus Areas - gateway/transport/websocket.go[64-85] - gateway/api/openapi/autogen/docs.go[1-1]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
✅ Build Completed with Success, Version=1719.0.0-ge0ad64d
✅ Build Completed with Success, Version=1719.0.0-g6603904
✅ Build Completed with Success, Version=1719.0.0-gdcf9497
Code review by qodo was updated up to the latest commit https://github.com/hoophq/hoop/commit/dcf9497bfd1a9f19abe0e6100fdb75bf5158f811
✅ Build Completed with Success, Version=1719.0.0-ga4dc9db
Migration Safety Analysis
No database migrations were changed in this PR. Safe to deploy to sandbox.