feat(voice-relay): NATS bridge for voice.agent.response.v1 (Gap 6) - #934
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new voice-relay FastAPI service that subscribes to Changes
Sequence Diagram(s)sequenceDiagram
participant Producer as Producer\n(agentzero.task.result.v1)
participant NATS as NATS Broker
participant VoiceRelay as voice-relay\nService
participant NATS_OUT as NATS Broker\n(voice.agent.response.v1)
participant Subscriber as Voice Subscriber
Producer->>NATS: Publish agentzero.task.result.v1
NATS->>VoiceRelay: Deliver message (INPUT_SUBJECT)
activate VoiceRelay
VoiceRelay->>VoiceRelay: Inspect meta.voice_mode
alt meta.voice_mode truthy
VoiceRelay->>VoiceRelay: Extract/transform text & metadata
VoiceRelay->>NATS_OUT: Publish voice.agent.response.v1
else filtered
VoiceRelay->>VoiceRelay: Increment filtered counter
end
deactivate VoiceRelay
NATS_OUT->>Subscriber: Deliver voice event
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Docker Hardening ValidationHardening Validation ReportValidated: Sat Mar 14 23:39:16 UTC 2026Services CheckedPMOVES.AI Docker Hardening Validation[INFO] Checking: pmoves/docker-compose.hardened.yml [INFO] Validating: hi-rag-gateway-v2 [INFO] Validating: extract-worker [INFO] Validating: langextract [INFO] Validating: presign [INFO] Validating: render-webhook [INFO] Validating: retrieval-eval [INFO] Validating: pdf-ingest [INFO] Validating: jellyfin-bridge [INFO] Validating: invidious-companion-proxy [INFO] Validating: ffmpeg-whisper [INFO] Validating: media-video [INFO] Validating: media-audio [INFO] Validating: hi-rag-gateway-v2-gpu [INFO] Validating: hi-rag-gateway-gpu [INFO] Validating: deepresearch [INFO] Validating: supaserch [INFO] Validating: publisher-discord [INFO] Validating: mesh-agent [INFO] Validating: nats-echo-req [INFO] Validating: nats-echo-res [INFO] Validating: publisher [INFO] Validating: analysis-echo [INFO] Validating: graph-linker [INFO] Validating: comfy-watcher [INFO] Validating: grayjay-plugin-host [INFO] Validating: agent-zero [INFO] Validating: archon [INFO] Validating: channel-monitor [INFO] Validating: pmoves-yt [INFO] Validating: notebook-sync [INFO] Validating: supabase_service_role_key [INFO] Validating: supabase_jwt_secret ====================================== |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.claude/context/services-catalog.md:
- Around line 236-254: The services catalog's quick-reference "All Service
Health Endpoints" list is missing the Voice Relay entry; add
"http://localhost:8121/healthz" under the quick-reference health endpoints so
the Voice Relay (port 8121, section titled "Voice Relay", NATS bridge) is
consistent with its full section and follows the catalog guideline to include
port assignments and health endpoints; update the quick-reference list where
other services' /healthz URLs are enumerated to include this entry.
In `@pmoves/contracts/topics.json`:
- Around line 6-8: The publisher list for the topic entry describing
voice.agent.response.v1 is incorrect (currently only "voice-relay"); update
pmoves/contracts/topics.json so the "publisher" field reflects all active
publishers (include the voice-relay publisher referenced by OUTPUT_SUBJECT in
pmoves/services/voice-relay/main.py plus the n8n publishers present in
pmoves/n8n-workflows/voice-platform-router.json and
pmoves/n8n/flows/voice_platform_router.json), or retire the n8n publishers in
this PR; locate the topic entry for voice.agent.response.v1 and either change
"publisher" to an array of all publisher identifiers or remove the n8n sources
if you are decommissioning them.
In `@pmoves/docs/architecture/voice-agent-response-relay.md`:
- Line 3: The document status line claims "Implemented (Option B — NATS relay)"
but the body still contains pre-implementation artifacts (references to
agent.task.completed.v1, "nats-py only", "python:3.12-slim", and a Next Steps
list saying to implement the relay); update the page so the body and examples
reflect the actual shipped service (remove or mark deprecated any
pre-implementation artifacts like agent.task.completed.v1 if not used, replace
"nats-py only" / "python:3.12-slim" with the real runtime/client choices, and
remove the "Next Steps" implementation checklist) or change the Status to
"Historical design" and add a short note summarizing what was actually shipped
and links to runbooks/smokes that prove implementation. Ensure references to the
relay protocol and client libraries in the doc match the live code and runbooks.
In `@pmoves/services/voice-relay/Dockerfile`:
- Around line 17-19: The Dockerfile hardcodes port 8121 in the HEALTHCHECK and
CMD (uvicorn main:app --port 8121) while main.py reads PORT from env; update the
Dockerfile so the runtime port comes from the PORT env var: change the CMD to a
shell form that expands $PORT (or use an ENTRYPOINT that runs uvicorn with
--port "$PORT") and update the HEALTHCHECK to also use the env var (e.g.,
shell-form curl/python check against http://localhost:$PORT/healthz) so both
uvicorn and the healthcheck honor the PORT environment variable; refer to the
HEALTHCHECK, CMD, uvicorn, main:app and PORT symbols when making the change.
In `@pmoves/services/voice-relay/main.py`:
- Line 98: The log currently prints user-facing text via logger.info("relayed
task_id=%s text=%s", data.get("task_id"), response_text[:80]) which may expose
PII; change the log to avoid emitting response_text and instead log only
metadata such as task/message identifiers (data.get("task_id"),
data.get("message_id") if available), the response length (len(response_text)),
and a non-reversible fingerprint (e.g., SHA256 or truncated hash) of
response_text; update the logger.info call to include those fields and remove
any plaintext response_text references, keeping the symbol names logger.info,
response_text, and data.get("task_id") to locate and modify the code.
- Around line 177-194: Replace the ad-hoc healthz and metrics handlers by
mounting the shared pmoves_health router: remove the healthz and metrics
endpoints (functions named healthz and metrics that reference generate_latest
and Response) and instead import the shared router symbol pmoves_health and call
app.include_router(pmoves_health) so the service uses the platform-standard
/healthz and /metrics behavior; also remove any local imports only used by the
old metrics handler (generate_latest/CONTENT_TYPE_LATEST/Response) once
pmoves_health is mounted.
- Around line 83-97: The code constructs voice_event and publishes it via
nc.publish to OUTPUT_SUBJECT without schema validation; call the shared
validator from services.common.events (e.g., validate_event or
validate_against_schema) before publishing to ensure the payload conforms to the
published event schema, handle validation errors by logging and
skipping/returning early, and only call await nc.publish(...) and RELAYED.inc()
after validation succeeds; keep the same variables (voice_event, _nc/nc,
OUTPUT_SUBJECT, RELAYED) and publish logic but gate it behind the schema
validation call.
- Line 25: The NATS_URL default embeds plaintext credentials in the NATS_URL
variable (set via os.getenv("NATS_URL", "nats://nats:pmoves@nats:4222")), which
violates secret-hardening rules; remove the credential-containing fallback and
instead load credentials via the central env/secret helper and *_FILE patterns
(or require NATS_URL to be provided). Replace the hardcoded default with a
neutral fallback (e.g. "nats://nats:4222" or no fallback) and use the project
secret loader functions (the repo's env helper that supports
NATS_USER/NATS_PASSWORD or NATS_URL_FILE) to construct the final URL, or fail
fast if no secret is present; update references to NATS_URL and os.getenv in
main.py accordingly.
- Around line 72-75: The filter currently bails out based only on
data.get("meta", {}).get("voice_mode"), which is incompatible with the upstream
agentzero.task.result.v1 payload that omits meta; update the gating in main.py
so it accepts voice-intended tasks by checking alternative locations and a
fallback: first check meta_voice = data.get("meta", {}).get("voice_mode"), then
check other likely fields on the payload (e.g. data.get("voice_mode"),
data.get("task", {}).get("voice_mode") or data.get("task", {}).get("type") ==
"voice") and only increment FILTERED and return if all checks indicate
non-voice; keep references to data, meta, FILTERED and the surrounding relay
logic so the filter is resilient to both old and new agentzero outputs.
- Around line 96-97: Replace core NATS publish/subscribe usage with JetStream
durable producers/consumers: create a JetStream context (e.g., js =
nc.jetstream()), replace nc.publish(OUTPUT_SUBJECT, ...) with
js.publish(NEW_OUTPUT_SUBJECT, payload) and replace nc.subscribe(...) blocks
(the subscription code referenced around lines 130-147) with js.subscribe(...,
durable="voice-relay-consumer", ack_wait=..., cb=..., or use js.pull_subscribe
and pull messages with fetch/ack). Ensure the subjects follow the
domain.entity.action.v1 pattern (rename OUTPUT_SUBJECT and any input subject to
that format), call msg.ack() (or ack each processed message) and handle
errors/retries so messages are durable across restarts, and keep RELAYED.inc()
after successful publish/ack.
- Around line 114-126: The callbacks _disconnected_cb and _closed_cb close over
loop variables nc and disconnect_event from an earlier iteration, causing
_mark_lost and the identity check (if _nc is nc) to operate on stale objects;
fix by changing those inner functions to accept nc and disconnect_event as
default parameters (e.g. async def _disconnected_cb(nc=nc,
disconnect_event=disconnect_event): and same for _closed_cb) so each closure
captures immutable bindings for that iteration before registering the callbacks
with the NATS client that calls _mark_lost.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b4815036-2b79-4652-a535-caeeb019ce3b
📒 Files selected for processing (9)
.claude/context/nats-subjects.md.claude/context/services-catalog.mdpmoves/contracts/topics.jsonpmoves/docker-compose.ymlpmoves/docs/architecture/voice-agent-response-relay.mdpmoves/services/voice-relay/Dockerfilepmoves/services/voice-relay/main.pypmoves/services/voice-relay/requirements.txtpmoves/tests/utils/service_catalog.py
Docker Hardening ValidationHardening Validation ReportValidated: Sat Mar 14 23:57:06 UTC 2026Services CheckedPMOVES.AI Docker Hardening Validation[INFO] Checking: pmoves/docker-compose.hardened.yml [INFO] Validating: hi-rag-gateway-v2 [INFO] Validating: extract-worker [INFO] Validating: langextract [INFO] Validating: presign [INFO] Validating: render-webhook [INFO] Validating: retrieval-eval [INFO] Validating: pdf-ingest [INFO] Validating: jellyfin-bridge [INFO] Validating: invidious-companion-proxy [INFO] Validating: ffmpeg-whisper [INFO] Validating: media-video [INFO] Validating: media-audio [INFO] Validating: hi-rag-gateway-v2-gpu [INFO] Validating: hi-rag-gateway-gpu [INFO] Validating: deepresearch [INFO] Validating: supaserch [INFO] Validating: publisher-discord [INFO] Validating: mesh-agent [INFO] Validating: nats-echo-req [INFO] Validating: nats-echo-res [INFO] Validating: publisher [INFO] Validating: analysis-echo [INFO] Validating: graph-linker [INFO] Validating: comfy-watcher [INFO] Validating: grayjay-plugin-host [INFO] Validating: agent-zero [INFO] Validating: archon [INFO] Validating: channel-monitor [INFO] Validating: pmoves-yt [INFO] Validating: notebook-sync [INFO] Validating: supabase_service_role_key [INFO] Validating: supabase_jwt_secret ====================================== |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (8)
pmoves/services/voice-relay/main.py (7)
179-196:⚠️ Potential issue | 🟠 MajorSwitch to shared
pmoves_healthrouter for/healthzand/metrics.Lines 179-196 implement ad-hoc health/metrics endpoints instead of the platform router.
As per coding guidelines, "Use FastAPI + uvicorn for API services and include the
pmoves_healthrouter for/healthzand/metricsendpoints".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/services/voice-relay/main.py` around lines 179 - 196, Remove the ad-hoc healthz and metrics endpoints (functions healthz and metrics) and instead mount the shared pmoves_health router; ensure the router exposes /healthz and /metrics and that any needed service info (INPUT_SUBJECT, OUTPUT_SUBJECT or nats connection state) is provided to the shared router via its expected configuration or dependency injection mechanism rather than local endpoint code, replacing the current uses of healthz and metrics with pmoves_health router registration.
115-127:⚠️ Potential issue | 🟠 MajorBind loop variables in callbacks to prevent stale-closure bugs.
Lines 117 and 119 capture loop-scoped
nc/disconnect_eventby reference; reconnect iterations can mutate behavior and leave_ncstale.Suggested patch
- def _mark_lost(reason: str) -> None: + def _mark_lost( + reason: str, + *, + nc_ref: NATS = nc, + disconnect_event_ref: asyncio.Event = disconnect_event, + ) -> None: global _nc - if _nc is nc: + if _nc is nc_ref: _nc = None - if not disconnect_event.is_set(): - disconnect_event.set() + if not disconnect_event_ref.is_set(): + disconnect_event_ref.set() logger.warning("nats connection lost: %s", reason)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/services/voice-relay/main.py` around lines 115 - 127, The callbacks _disconnected_cb and _closed_cb (and _mark_lost) close over loop-scoped variables nc and disconnect_event which can change across reconnects; fix by binding the current loop-specific values into the callbacks (e.g., capture current nc and disconnect_event as default parameters or local copies when defining _mark_lost/_disconnected_cb/_closed_cb) and use the bound variables when comparing/updating _nc and calling disconnect_event.set() so the callbacks always operate on the correct connection instance.
99-99:⚠️ Potential issue | 🟠 MajorAvoid logging response text content.
Line 99 logs user-facing text (
response_text[:80]), which risks leaking sensitive content.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/services/voice-relay/main.py` at line 99, The current logger.info call in main.py logs user-facing content via response_text[:80]; change it to avoid logging any plaintext response_text. Update the logging in the same scope where logger.info is called (reference symbols: logger.info, response_text, data.get("task_id")) to log only non-sensitive metadata such as the task_id and either the response length or a deterministic hash/hex digest of response_text (or a fixed "[REDACTED]" marker) instead of the actual text; ensure no substring of response_text is included in logs.
84-97:⚠️ Potential issue | 🟠 MajorValidate
voice_eventagainst schema before publish.Line 97 publishes directly without shared contract validation. Malformed events can leak to subscribers.
As per coding guidelines, "Validate payloads against schemas before publishing events using services/common/events.py".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/services/voice-relay/main.py` around lines 84 - 97, The code publishes voice_event directly to OUTPUT_SUBJECT without schema validation; update the publish path to validate voice_event against the shared event schema before sending. Import and call the shared validator (e.g., the validation function provided by services/common/events.py) to validate voice_event, handle validation errors by logging and skipping publish (or returning an error), and only call nc.publish(OUTPUT_SUBJECT, ...) when validation succeeds; reference the existing voice_event dict, nc/_nc and OUTPUT_SUBJECT to locate where to insert the validation.
25-25:⚠️ Potential issue | 🟠 MajorRemove plaintext credential fallback from
NATS_URL.Line 25 hardcodes credentials in a default URL. This is a secret-hardening regression.
As per coding guidelines, "Focus on PMOVES secret hardening conventions: - Prefer central env helpers and *_FILE secret loading paths. - Flag direct critical-secret reads and plaintext fallbacks."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/services/voice-relay/main.py` at line 25, The default NATS_URL constant currently includes plaintext credentials (NATS_URL = os.getenv("NATS_URL", "nats://nats:pmoves@nats:4222")), which must be removed: change how NATS_URL is resolved to avoid embedding credentials by delegating to the central env/secret loader (use the project's env helper or the *_FILE secret pattern) and/or separate variables (e.g., NATS_URL without credentials plus NATS_USER/NATS_PASSWORD or NATS_PASSWORD_FILE) so no plaintext fallback is present; update the resolution in main.py where NATS_URL is defined to read only the URL from the loader or require explicit provisioning, and remove the hardcoded "nats://nats:pmoves@nats:4222" default.
72-75:⚠️ Potential issue | 🔴 Critical
voice_modefilter is too narrow and will drop valid voice-intended tasks.Line 73 checks only
data["meta"]["voice_mode"]. Upstream task result payloads may not includemeta, so legitimate messages get filtered.Suggested patch
- meta = data.get("meta") or {} - if not meta.get("voice_mode"): + meta = data.get("meta") if isinstance(data.get("meta"), dict) else {} + metadata = data.get("metadata") if isinstance(data.get("metadata"), dict) else {} + task_obj = data.get("task") if isinstance(data.get("task"), dict) else {} + voice_mode = any( + ( + bool(meta.get("voice_mode")), + bool(metadata.get("voice_mode")), + bool(data.get("voice_mode")), + bool(task_obj.get("voice_mode")), + task_obj.get("type") == "voice", + ) + ) + if not voice_mode: FILTERED.inc() return🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/services/voice-relay/main.py` around lines 72 - 75, The current filter unconditionally drops any message lacking meta because it checks meta.get("voice_mode") on a possibly empty meta; change the logic so you only filter when meta is present and explicitly indicates voice_mode is false. Update the block around meta = data.get("meta") or {} to only call FILTERED.inc() and return when the meta object exists and meta.get("voice_mode") is falsy (i.e., when meta was provided and explicitly disables voice), leaving messages that have no meta untouched; reference meta, data, and FILTERED.inc() when making the change.
97-98:⚠️ Potential issue | 🟠 MajorUse JetStream instead of core NATS pub/sub for relay coordination.
Lines 97 and 148 use non-durable core publish/subscribe. This path is restart/drop prone for inter-service coordination.
As per coding guidelines, "Use NATS JetStream for inter-service coordination with subjects following the
domain.entity.action.v{n}pattern (e.g.,ingest.file.added.v1)".Also applies to: 147-149
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/services/voice-relay/main.py` around lines 97 - 98, The code currently uses core NATS publish (nc.publish) on OUTPUT_SUBJECT and then RELAYED.inc(), which is not durable; replace this with JetStream publishing: obtain a JetStreamContext (e.g., js = nc.jetstream()), ensure the subject follows the domain.entity.action.v1 pattern (rename OUTPUT_SUBJECT to something like "voice.relay.relayed.v1"), call await js.publish(subject, json.dumps(voice_event).encode("utf-8")) and only call RELAYED.inc() after a successful publish response; also replace corresponding core subscribe/publish usages around the other occurrence (the blocks referencing OUTPUT_SUBJECT and nc.publish/nc.subscribe) to use JetStream durable consumers/Publish API to ensure durability and restart resilience.pmoves/contracts/topics.json (1)
6-8:⚠️ Potential issue | 🟠 MajorVerify that
voice.agent.response.v1publisher mapping is complete.Line 7 currently declares only
"voice-relay". If n8n voice router flows still publishvoice.agent.response.v1, this mapping is incomplete.#!/bin/bash set -euo pipefail # Verify all current publishers of voice.agent.response.v1 rg -nP --type=json -C2 '"voice\.agent\.response\.v1"' pmoves/contracts/topics.json fd 'voice[-_]platform[-_]router\.json$' pmoves | xargs -r rg -nP -C2 'voice\.agent\.response\.v1' rg -nP -C2 'OUTPUT_SUBJECT|voice\.agent\.response\.v1' pmoves/services/voice-relay/main.pyAs per coding guidelines, "Define event schemas and topic mapping in
pmoves/contracts/directory with correspondingschemas/and topic mapping files".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/contracts/topics.json` around lines 6 - 8, The topics.json entry for topic "voice.agent.response.v1" currently lists only "voice-relay" as publisher; verify all actual publishers (run the provided rg/fd commands against pmoves to find n8n or router flows that emit voice.agent.response.v1) and update the "publisher" array for that topic to include every found publisher (e.g., add the n8n/router service names exactly as discovered), ensuring the publisher field remains an array of strings and the topic mapping matches the event producers you find.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pmoves/contracts/topics.json`:
- Around line 10-14: The topic "voice.cast.completed.v1" references a missing
schema at "schemas/voice/cast.completed.v1.schema.json"; add that JSON Schema
file under pmoves/contracts/schemas/voice/ named cast.completed.v1.schema.json
defining the event payload shape (required properties like e.g. eventId,
timestamp, deviceId, audioUrl or whatever fields your contract expects) and any
enums/types used by consumers, validate it with existing contract tests, and
ensure the "voice.cast.completed.v1" topic in topics.json points to this exact
schema filename.
In `@pmoves/services/voice-relay/main.py`:
- Around line 62-66: The blind except around JSON parsing should be narrowed to
the expected errors: catch json.JSONDecodeError and UnicodeDecodeError (e.g.,
around the data = json.loads(msg.data.decode("utf-8")) block) instead of except
Exception, increment ERRORS.inc() on those failures, and log the exception
details; apply the same change to the similar try/except near lines where
ERRORS.inc() is used (the second occurrence) so both
message-decoding/JSON-parsing blocks only handle those specific exceptions and
surface the error details for debugging.
In `@pmoves/tests/utils/service_catalog.py`:
- Around line 231-240: CAST_TTS_GATEWAY depends on flute-gateway but
FLUTE_GATEWAY is cataloged only under profile="tts" while compose runs it under
media/orchestration, so tests can skip the required dependency; fix by aligning
profiles/dep names: update the FLUTE_GATEWAY ServiceDefinition to include the
media/orchestration profile(s) (e.g., add "media" or "orchestration" to its
profile string) so it will be selected when CAST_TTS_GATEWAY (ServiceDefinition
CAST_TTS_GATEWAY) starts, or alternatively change CAST_TTS_GATEWAY.dependencies
to reference the correct service/profile used in compose; ensure you modify the
profile attribute on FLUTE_GATEWAY or the dependencies list on CAST_TTS_GATEWAY
accordingly.
---
Duplicate comments:
In `@pmoves/contracts/topics.json`:
- Around line 6-8: The topics.json entry for topic "voice.agent.response.v1"
currently lists only "voice-relay" as publisher; verify all actual publishers
(run the provided rg/fd commands against pmoves to find n8n or router flows that
emit voice.agent.response.v1) and update the "publisher" array for that topic to
include every found publisher (e.g., add the n8n/router service names exactly as
discovered), ensuring the publisher field remains an array of strings and the
topic mapping matches the event producers you find.
In `@pmoves/services/voice-relay/main.py`:
- Around line 179-196: Remove the ad-hoc healthz and metrics endpoints
(functions healthz and metrics) and instead mount the shared pmoves_health
router; ensure the router exposes /healthz and /metrics and that any needed
service info (INPUT_SUBJECT, OUTPUT_SUBJECT or nats connection state) is
provided to the shared router via its expected configuration or dependency
injection mechanism rather than local endpoint code, replacing the current uses
of healthz and metrics with pmoves_health router registration.
- Around line 115-127: The callbacks _disconnected_cb and _closed_cb (and
_mark_lost) close over loop-scoped variables nc and disconnect_event which can
change across reconnects; fix by binding the current loop-specific values into
the callbacks (e.g., capture current nc and disconnect_event as default
parameters or local copies when defining _mark_lost/_disconnected_cb/_closed_cb)
and use the bound variables when comparing/updating _nc and calling
disconnect_event.set() so the callbacks always operate on the correct connection
instance.
- Line 99: The current logger.info call in main.py logs user-facing content via
response_text[:80]; change it to avoid logging any plaintext response_text.
Update the logging in the same scope where logger.info is called (reference
symbols: logger.info, response_text, data.get("task_id")) to log only
non-sensitive metadata such as the task_id and either the response length or a
deterministic hash/hex digest of response_text (or a fixed "[REDACTED]" marker)
instead of the actual text; ensure no substring of response_text is included in
logs.
- Around line 84-97: The code publishes voice_event directly to OUTPUT_SUBJECT
without schema validation; update the publish path to validate voice_event
against the shared event schema before sending. Import and call the shared
validator (e.g., the validation function provided by services/common/events.py)
to validate voice_event, handle validation errors by logging and skipping
publish (or returning an error), and only call nc.publish(OUTPUT_SUBJECT, ...)
when validation succeeds; reference the existing voice_event dict, nc/_nc and
OUTPUT_SUBJECT to locate where to insert the validation.
- Line 25: The default NATS_URL constant currently includes plaintext
credentials (NATS_URL = os.getenv("NATS_URL", "nats://nats:pmoves@nats:4222")),
which must be removed: change how NATS_URL is resolved to avoid embedding
credentials by delegating to the central env/secret loader (use the project's
env helper or the *_FILE secret pattern) and/or separate variables (e.g.,
NATS_URL without credentials plus NATS_USER/NATS_PASSWORD or NATS_PASSWORD_FILE)
so no plaintext fallback is present; update the resolution in main.py where
NATS_URL is defined to read only the URL from the loader or require explicit
provisioning, and remove the hardcoded "nats://nats:pmoves@nats:4222" default.
- Around line 72-75: The current filter unconditionally drops any message
lacking meta because it checks meta.get("voice_mode") on a possibly empty meta;
change the logic so you only filter when meta is present and explicitly
indicates voice_mode is false. Update the block around meta = data.get("meta")
or {} to only call FILTERED.inc() and return when the meta object exists and
meta.get("voice_mode") is falsy (i.e., when meta was provided and explicitly
disables voice), leaving messages that have no meta untouched; reference meta,
data, and FILTERED.inc() when making the change.
- Around line 97-98: The code currently uses core NATS publish (nc.publish) on
OUTPUT_SUBJECT and then RELAYED.inc(), which is not durable; replace this with
JetStream publishing: obtain a JetStreamContext (e.g., js = nc.jetstream()),
ensure the subject follows the domain.entity.action.v1 pattern (rename
OUTPUT_SUBJECT to something like "voice.relay.relayed.v1"), call await
js.publish(subject, json.dumps(voice_event).encode("utf-8")) and only call
RELAYED.inc() after a successful publish response; also replace corresponding
core subscribe/publish usages around the other occurrence (the blocks
referencing OUTPUT_SUBJECT and nc.publish/nc.subscribe) to use JetStream durable
consumers/Publish API to ensure durability and restart resilience.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 82517e42-2b45-46db-9928-68ff85f88dad
📒 Files selected for processing (4)
pmoves/contracts/topics.jsonpmoves/services/voice-relay/main.pypmoves/services/voice-relay/requirements.txtpmoves/tests/utils/service_catalog.py
🚧 Files skipped from review as they are similar to previous changes (1)
- pmoves/services/voice-relay/requirements.txt
Docker Hardening ValidationHardening Validation ReportValidated: Sun Mar 15 00:15:49 UTC 2026Services CheckedPMOVES.AI Docker Hardening Validation[INFO] Checking: pmoves/docker-compose.hardened.yml [INFO] Validating: hi-rag-gateway-v2 [INFO] Validating: extract-worker [INFO] Validating: langextract [INFO] Validating: presign [INFO] Validating: render-webhook [INFO] Validating: retrieval-eval [INFO] Validating: pdf-ingest [INFO] Validating: jellyfin-bridge [INFO] Validating: invidious-companion-proxy [INFO] Validating: ffmpeg-whisper [INFO] Validating: media-video [INFO] Validating: media-audio [INFO] Validating: hi-rag-gateway-v2-gpu [INFO] Validating: hi-rag-gateway-gpu [INFO] Validating: deepresearch [INFO] Validating: supaserch [INFO] Validating: publisher-discord [INFO] Validating: mesh-agent [INFO] Validating: nats-echo-req [INFO] Validating: nats-echo-res [INFO] Validating: publisher [INFO] Validating: analysis-echo [INFO] Validating: graph-linker [INFO] Validating: comfy-watcher [INFO] Validating: grayjay-plugin-host [INFO] Validating: agent-zero [INFO] Validating: archon [INFO] Validating: channel-monitor [INFO] Validating: pmoves-yt [INFO] Validating: notebook-sync [INFO] Validating: supabase_service_role_key [INFO] Validating: supabase_jwt_secret ====================================== |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
pmoves/services/voice-relay/main.py (1)
117-129:⚠️ Potential issue | 🟠 MajorCallback closures still capture loop variables by reference.
The inner functions
_mark_lost,_disconnected_cb, and_closed_cbclose overncanddisconnect_eventfrom the enclosing loop scope. If the loop iterates before these callbacks fire, they'll reference stale objects. Bind them as default parameters.🔧 Proposed fix
- def _mark_lost(reason: str) -> None: + def _mark_lost( + reason: str, + *, + nc_ref: NATS = nc, + disconnect_event_ref: asyncio.Event = disconnect_event, + ) -> None: global _nc - if _nc is nc: + if _nc is nc_ref: _nc = None - if not disconnect_event.is_set(): - disconnect_event.set() + if not disconnect_event_ref.is_set(): + disconnect_event_ref.set() logger.warning("nats connection lost: %s", reason) - async def _disconnected_cb(): - _mark_lost("disconnected") + async def _disconnected_cb(nc_ref: NATS = nc, de_ref: asyncio.Event = disconnect_event): + _mark_lost("disconnected", nc_ref=nc_ref, disconnect_event_ref=de_ref) - async def _closed_cb(): - _mark_lost("closed") + async def _closed_cb(nc_ref: NATS = nc, de_ref: asyncio.Event = disconnect_event): + _mark_lost("closed", nc_ref=nc_ref, disconnect_event_ref=de_ref)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/services/voice-relay/main.py` around lines 117 - 129, The callbacks _mark_lost, _disconnected_cb, and _closed_cb are closing over loop variables nc and disconnect_event which can change before the callbacks run; fix by binding those loop values into the function signatures as default arguments (e.g. def _mark_lost(reason: str, _bound_nc=nc, _bound_event=disconnect_event) -> None and async def _disconnected_cb(_bound_nc=nc, _bound_event=disconnect_event): ...), then use the bound names inside and still update the module-level _nc appropriately (compare against _bound_nc when deciding to clear global _nc) so the callbacks reference the correct instances even if the loop continues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pmoves/contracts/schemas/voice/cast.completed.v1.schema.json`:
- Around line 1-42: The event schema requires device_id, device_name, audio_url,
text, and timestamp but current publish calls build payloads with keys like
device, group, audio_path and publish raw payloads from _publish_event(); update
the payload creation sites (the places that currently pass payloads with keys
'device', 'group', 'audio_path', 'text', 'voice', etc.) to produce payloads
matching the voice.cast.completed.v1 schema (rename device ->
device_id/device_name, audio_path -> audio_url, include text and timestamp
fields) and then modify _publish_event() to wrap and validate the payload with
envelope() from services/common/events.py before sending to NATS, handling
validation errors (log and bail or drop) so only schema-compliant messages are
published.
---
Duplicate comments:
In `@pmoves/services/voice-relay/main.py`:
- Around line 117-129: The callbacks _mark_lost, _disconnected_cb, and
_closed_cb are closing over loop variables nc and disconnect_event which can
change before the callbacks run; fix by binding those loop values into the
function signatures as default arguments (e.g. def _mark_lost(reason: str,
_bound_nc=nc, _bound_event=disconnect_event) -> None and async def
_disconnected_cb(_bound_nc=nc, _bound_event=disconnect_event): ...), then use
the bound names inside and still update the module-level _nc appropriately
(compare against _bound_nc when deciding to clear global _nc) so the callbacks
reference the correct instances even if the loop continues.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6ebf8df0-bafd-4791-a207-af57b6ee67f4
📒 Files selected for processing (4)
pmoves/contracts/schemas/voice/cast.completed.v1.schema.jsonpmoves/contracts/topics.jsonpmoves/images.yamlpmoves/services/voice-relay/main.py
🚧 Files skipped from review as they are similar to previous changes (1)
- pmoves/contracts/topics.json
Docker Hardening ValidationHardening Validation ReportValidated: Sun Mar 15 01:38:10 UTC 2026Services CheckedPMOVES.AI Docker Hardening Validation[INFO] Checking: pmoves/docker-compose.hardened.yml [INFO] Validating: hi-rag-gateway-v2 [INFO] Validating: extract-worker [INFO] Validating: langextract [INFO] Validating: presign [INFO] Validating: render-webhook [INFO] Validating: retrieval-eval [INFO] Validating: pdf-ingest [INFO] Validating: jellyfin-bridge [INFO] Validating: invidious-companion-proxy [INFO] Validating: ffmpeg-whisper [INFO] Validating: media-video [INFO] Validating: media-audio [INFO] Validating: hi-rag-gateway-v2-gpu [INFO] Validating: hi-rag-gateway-gpu [INFO] Validating: deepresearch [INFO] Validating: supaserch [INFO] Validating: publisher-discord [INFO] Validating: mesh-agent [INFO] Validating: nats-echo-req [INFO] Validating: nats-echo-res [INFO] Validating: publisher [INFO] Validating: analysis-echo [INFO] Validating: graph-linker [INFO] Validating: comfy-watcher [INFO] Validating: grayjay-plugin-host [INFO] Validating: agent-zero [INFO] Validating: archon [INFO] Validating: channel-monitor [INFO] Validating: pmoves-yt [INFO] Validating: notebook-sync [INFO] Validating: supabase_service_role_key [INFO] Validating: supabase_jwt_secret ====================================== |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Docker Hardening ValidationHardening Validation ReportValidated: Sun Mar 15 05:17:35 UTC 2026Services CheckedPMOVES.AI Docker Hardening Validation[INFO] Checking: pmoves/docker-compose.hardened.yml [INFO] Validating: hi-rag-gateway-v2 [INFO] Validating: extract-worker [INFO] Validating: langextract [INFO] Validating: presign [INFO] Validating: render-webhook [INFO] Validating: retrieval-eval [INFO] Validating: pdf-ingest [INFO] Validating: jellyfin-bridge [INFO] Validating: invidious-companion-proxy [INFO] Validating: ffmpeg-whisper [INFO] Validating: media-video [INFO] Validating: media-audio [INFO] Validating: hi-rag-gateway-v2-gpu [INFO] Validating: hi-rag-gateway-gpu [INFO] Validating: deepresearch [INFO] Validating: supaserch [INFO] Validating: publisher-discord [INFO] Validating: mesh-agent [INFO] Validating: nats-echo-req [INFO] Validating: nats-echo-res [INFO] Validating: publisher [INFO] Validating: analysis-echo [INFO] Validating: graph-linker [INFO] Validating: comfy-watcher [INFO] Validating: grayjay-plugin-host [INFO] Validating: agent-zero [INFO] Validating: archon [INFO] Validating: channel-monitor [INFO] Validating: pmoves-yt [INFO] Validating: notebook-sync [INFO] Validating: supabase_service_role_key [INFO] Validating: supabase_jwt_secret ====================================== |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
pmoves/services/cast-tts-gateway/service.py (1)
534-541: Consider validating payloads against schema before publishing.The event payload structure aligns with
cast.completed.v1.schema.json, but the service publishes directly viaself.nats_client.publish()without usingservices/common/events.pyfor schema validation. This applies to allvoice.cast.completed.v1publish calls (lines 534, 589, 670).As per coding guidelines: "Validate payloads against schemas before publishing events using services/common/events.py".
♻️ Suggested approach
Import and use the envelope helper for schema-validated publishing:
from services.common.events import envelope # Then in _publish_event or at call sites: env = envelope("voice.cast.completed.v1", payload, source="cast-tts-gateway") await self.nats_client.publish(subject, json.dumps(env).encode())🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/services/cast-tts-gateway/service.py` around lines 534 - 541, The code publishes "voice.cast.completed.v1" payloads directly via self.nats_client.publish (calls originating in service._publish_event and the call sites that emit voice.cast.completed.v1 at the three locations), so update the publish flow to validate against the schema by importing and using the envelope helper from services.common.events (envelope), wrapping the payload with envelope("voice.cast.completed.v1", payload, source="cast-tts-gateway") and then serializing that envelope before calling self.nats_client.publish; modify _publish_event (or each voice.cast.completed.v1 call site) to create and publish the schema-validated envelope instead of publishing the raw payload.pmoves/contracts/topics.json (1)
13-13: Minor inconsistency:publisherfield format varies between entries.
voice.agent.response.v1uses an array["voice-relay", "n8n-voice-platform-router"]whilevoice.cast.completed.v1uses a string"cast-tts-gateway". Consider using arrays consistently for easier programmatic parsing, even for single publishers.♻️ Suggested fix
"voice.cast.completed.v1": { "schema": "schemas/voice/cast.completed.v1.schema.json", "description": "Published when TTS audio has been successfully cast to a Chromecast/Google Home device.", - "publisher": "cast-tts-gateway", + "publisher": ["cast-tts-gateway"], "subscriber": ["publisher-discord", "cast-notebook-logger"] },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pmoves/contracts/topics.json` at line 13, The publisher field is inconsistent between topics (e.g., "voice.agent.response.v1" uses an array while "voice.cast.completed.v1" uses a string); update the "publisher" value for the topic "voice.cast.completed.v1" in topics.json to be an array (e.g., ["cast-tts-gateway"]) so all publisher fields use a consistent array format for programmatic parsing, and run any schema/validation (if present) against the file to confirm the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pmoves/services/cast-tts-gateway/service.py`:
- Around line 535-536: The object currently assigns both "device_id" and
"device_name" from result.get("device", "") which conflates a unique identifier
and a friendly name; update the construction so "device_id" reads
result.get("device_id", result.get("device", "")) and "device_name" reads
result.get("device_name", result.get("device", "")) (i.e., prefer explicit
device_id/device_name fields from result, fall back to legacy "device" if
needed), and add a short comment noting that if only "device" is present it will
populate both fields for compatibility.
---
Nitpick comments:
In `@pmoves/contracts/topics.json`:
- Line 13: The publisher field is inconsistent between topics (e.g.,
"voice.agent.response.v1" uses an array while "voice.cast.completed.v1" uses a
string); update the "publisher" value for the topic "voice.cast.completed.v1" in
topics.json to be an array (e.g., ["cast-tts-gateway"]) so all publisher fields
use a consistent array format for programmatic parsing, and run any
schema/validation (if present) against the file to confirm the change.
In `@pmoves/services/cast-tts-gateway/service.py`:
- Around line 534-541: The code publishes "voice.cast.completed.v1" payloads
directly via self.nats_client.publish (calls originating in
service._publish_event and the call sites that emit voice.cast.completed.v1 at
the three locations), so update the publish flow to validate against the schema
by importing and using the envelope helper from services.common.events
(envelope), wrapping the payload with envelope("voice.cast.completed.v1",
payload, source="cast-tts-gateway") and then serializing that envelope before
calling self.nats_client.publish; modify _publish_event (or each
voice.cast.completed.v1 call site) to create and publish the schema-validated
envelope instead of publishing the raw payload.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7642ff05-8fea-41ea-a557-3ca0ac58bff7
📒 Files selected for processing (27)
.claude/context/services-catalog.md.claude/context/submodules.md.gitmodulesPMOVES-BoTZPMOVES-BoTZ-check/config/tensorzero.local.models.tomlPMOVES-BoTZ-check/features/vl_sentinel/app_vl.pypmoves-cipher-mcppmoves-cipher-mcp/.gitignorepmoves-cipher-mcp/README.mdpmoves-cipher-mcp/cipher_mcp/__init__.pypmoves-cipher-mcp/cipher_mcp/client.pypmoves-cipher-mcp/cipher_mcp/server.pypmoves-cipher-mcp/cipher_mcp/tools.pypmoves-cipher-mcp/main.pypmoves-cipher-mcp/pmoves_announcer/__init__.pypmoves-cipher-mcp/pmoves_common/__init__.pypmoves-cipher-mcp/pmoves_health/__init__.pypmoves-cipher-mcp/pmoves_registry/__init__.pypmoves-cipher-mcp/pyproject.tomlpmoves/contracts/topics.jsonpmoves/docker-compose.ymlpmoves/docs/architecture/voice-agent-response-relay.mdpmoves/services/cast-tts-gateway/service.pypmoves/services/voice-relay/Dockerfilepmoves/services/voice-relay/main.pypmoves/services/voice-relay/requirements.txtpmoves/tests/utils/service_catalog.py
💤 Files with no reviewable changes (14)
- pmoves-cipher-mcp/.gitignore
- pmoves-cipher-mcp/cipher_mcp/server.py
- pmoves-cipher-mcp/README.md
- pmoves-cipher-mcp/cipher_mcp/client.py
- pmoves-cipher-mcp/pmoves_common/init.py
- pmoves-cipher-mcp/pmoves_registry/init.py
- PMOVES-BoTZ-check/config/tensorzero.local.models.toml
- pmoves-cipher-mcp/cipher_mcp/init.py
- pmoves-cipher-mcp/cipher_mcp/tools.py
- pmoves-cipher-mcp/main.py
- pmoves-cipher-mcp/pmoves_announcer/init.py
- pmoves-cipher-mcp/pmoves_health/init.py
- PMOVES-BoTZ-check/features/vl_sentinel/app_vl.py
- pmoves-cipher-mcp/pyproject.toml
✅ Files skipped from review due to trivial changes (2)
- PMOVES-BoTZ
- pmoves-cipher-mcp
🚧 Files skipped from review as they are similar to previous changes (6)
- .claude/context/services-catalog.md
- pmoves/tests/utils/service_catalog.py
- pmoves/docker-compose.yml
- pmoves/services/voice-relay/main.py
- pmoves/services/voice-relay/Dockerfile
- pmoves/services/voice-relay/requirements.txt
Docker Hardening ValidationHardening Validation ReportValidated: Sun Mar 15 05:29:44 UTC 2026Services CheckedPMOVES.AI Docker Hardening Validation[INFO] Checking: pmoves/docker-compose.hardened.yml [INFO] Validating: hi-rag-gateway-v2 [INFO] Validating: extract-worker [INFO] Validating: langextract [INFO] Validating: presign [INFO] Validating: render-webhook [INFO] Validating: retrieval-eval [INFO] Validating: pdf-ingest [INFO] Validating: jellyfin-bridge [INFO] Validating: invidious-companion-proxy [INFO] Validating: ffmpeg-whisper [INFO] Validating: media-video [INFO] Validating: media-audio [INFO] Validating: hi-rag-gateway-v2-gpu [INFO] Validating: hi-rag-gateway-gpu [INFO] Validating: deepresearch [INFO] Validating: supaserch [INFO] Validating: publisher-discord [INFO] Validating: mesh-agent [INFO] Validating: nats-echo-req [INFO] Validating: nats-echo-res [INFO] Validating: publisher [INFO] Validating: analysis-echo [INFO] Validating: graph-linker [INFO] Validating: comfy-watcher [INFO] Validating: grayjay-plugin-host [INFO] Validating: agent-zero [INFO] Validating: archon [INFO] Validating: channel-monitor [INFO] Validating: pmoves-yt [INFO] Validating: notebook-sync [INFO] Validating: supabase_service_role_key [INFO] Validating: supabase_jwt_secret ====================================== |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pmoves/services/cast-tts-gateway/service.py`:
- Around line 538-545: The payload for the "voice.cast.completed.v1" publish is
being sent directly; before calling _publish_event you must validate the event
payload using the shared validator in services/common/events.py (call the
validation function there, e.g., validate_event or validate_payload for
"voice.cast.completed.v1"), then only pass the validated payload to
_publish_event; update this call site (and the other "voice.cast.completed.v1"
publish sites in this file) to perform validation and handle/raise validation
errors appropriately before publishing.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 838b84ab-843b-4115-9495-94fd0d5787ab
📒 Files selected for processing (1)
pmoves/services/cast-tts-gateway/service.py
Docker Hardening ValidationHardening Validation ReportValidated: Sun Mar 15 05:38:49 UTC 2026Services CheckedPMOVES.AI Docker Hardening Validation[INFO] Checking: pmoves/docker-compose.hardened.yml [INFO] Validating: hi-rag-gateway-v2 [INFO] Validating: extract-worker [INFO] Validating: langextract [INFO] Validating: presign [INFO] Validating: render-webhook [INFO] Validating: retrieval-eval [INFO] Validating: pdf-ingest [INFO] Validating: jellyfin-bridge [INFO] Validating: invidious-companion-proxy [INFO] Validating: ffmpeg-whisper [INFO] Validating: media-video [INFO] Validating: media-audio [INFO] Validating: hi-rag-gateway-v2-gpu [INFO] Validating: hi-rag-gateway-gpu [INFO] Validating: deepresearch [INFO] Validating: supaserch [INFO] Validating: publisher-discord [INFO] Validating: mesh-agent [INFO] Validating: nats-echo-req [INFO] Validating: nats-echo-res [INFO] Validating: publisher [INFO] Validating: analysis-echo [INFO] Validating: graph-linker [INFO] Validating: comfy-watcher [INFO] Validating: grayjay-plugin-host [INFO] Validating: agent-zero [INFO] Validating: archon [INFO] Validating: channel-monitor [INFO] Validating: pmoves-yt [INFO] Validating: notebook-sync [INFO] Validating: supabase_service_role_key [INFO] Validating: supabase_jwt_secret ====================================== |
| _disconnected_cb, _closed_cb = _make_nats_callbacks(nc, disconnect_event) | ||
|
|
||
| try: | ||
| logger.info("connecting to NATS %s (backoff=%.1fs)", NATS_URL_REDACTED, backoff) |
Check failure
Code scanning / CodeQL
Clear-text logging of sensitive information High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, the fix is to ensure that values derived from secrets (like NATS_URL) are never logged directly. Instead of trying to perfectly sanitize the URL, the log should contain only non-sensitive metadata such as that a connection attempt is happening and possibly the configured input/output subjects, or a static label for which broker is being used.
The best minimal-impact fix here is:
- Keep
NATS_URLas-is for actual connection use. - Stop passing
NATS_URL_REDACTEDtologger.infoon line 167. - Replace that log message with one that does not include the URL or any derivative, e.g.,
"connecting to NATS (backoff=%.1fs)"or at most the subject names, which are already considered non-secret and are returned from the/healthzendpoint. - Since
NATS_URL_REDACTEDis no longer used, we can optionally remove its definition to avoid confusion, but that is not strictly necessary. To keep changes minimal and localized, I’ll leave the constant in place (in case other unseen code uses it) and only adjust the log statement in the shown snippet.
Concretely:
- In
pmoves/services/voice-relay/main.py, around line 167, change:logger.info("connecting to NATS %s (backoff=%.1fs)", NATS_URL_REDACTED, backoff)- to something like
logger.info("connecting to NATS (backoff=%.1fs)", backoff).
No new methods or imports are required; we are simply changing the log format and arguments.
| @@ -164,7 +164,7 @@ | ||
| _disconnected_cb, _closed_cb = _make_nats_callbacks(nc, disconnect_event) | ||
|
|
||
| try: | ||
| logger.info("connecting to NATS %s (backoff=%.1fs)", NATS_URL_REDACTED, backoff) | ||
| logger.info("connecting to NATS (backoff=%.1fs)", backoff) | ||
| await nc.connect( | ||
| servers=[NATS_URL], | ||
| connect_timeout=5, |
Bridges agentzero.task.result.v1 → voice.agent.response.v1 by filtering on meta.voice_mode. Closes Gap 6 from the voice pipeline analysis. - Python FastAPI service with /healthz and /metrics endpoints - Filters task results with voice_mode=true metadata flag - Port 8121, profiles: cast, media - Worker-tier hardened container with read_only filesystem Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add voice-relay to services-catalog.md (port 8121) - Register voice.agent.response.v1 relay subject in nats-subjects.md - Add publisher/subscriber metadata to topics.json - Add VOICE_RELAY ServiceDefinition to service_catalog.py - Update design doc status to Implemented Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Pin exact versions: nats-py==2.12.0, fastapi==0.129.0, uvicorn==0.40.0, prometheus_client==0.20.0 (aligned with PMOVES.YT and BoTZ pins) - Add connect_timeout=5 to NATS connect() to prevent indefinite hangs - Add debug log when filtering messages with empty response_text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add voice.cast.completed.v1 to topics.json (publisher: cast-tts-gateway, subscribers: publisher-discord, cast-notebook-logger) — gap from PR #932 - Add CAST_TTS_GATEWAY ServiceDefinition to service_catalog.py (port 8060, profile cast/media) — gap from PR #931 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… CI entry - Redact NATS credentials in log output (use NATS_URL_REDACTED) - Log response text_len instead of raw content to avoid PII in Loki - Create voice/cast.completed.v1.schema.json for topics.json reference - Add voice-relay as subscriber on agentzero.task.result.v1 - Register pmoves-voice-relay in images.yaml CI build matrix Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…catalog, update docs - Narrow bare `except Exception` to specific types (JSONDecodeError, UnicodeDecodeError for parsing; OSError, TimeoutError for NATS connect) - Make Dockerfile PORT configurable via ENV instead of hardcoded 8121 - List n8n-voice-platform-router as additional publisher in topics.json - Fix flute-gateway catalog profile from "tts" to "orchestration,media" - Add voice-relay + cast-tts-gateway to services-catalog health quick-ref - Rewrite architecture doc from proposal to shipped-service reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion, B023, health TODO, cast payloads - Use services.common.events.envelope() for schema-validated NATS publishing (widen Docker build context, add jsonschema dep, graceful import fallback) - Extract _make_nats_callbacks() factory to fix Ruff B023 loop-variable capture - Add TODO for shared pmoves_health router adoption - Align cast-tts-gateway voice.cast.completed.v1 payloads with schema (device_id/device_name/audio_url/text required fields, extras in meta) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CodeRabbit flagged that device_id and device_name were both set from result["device"]. Now prefer explicit device_id/device_name keys from the result dict, falling back to the legacy "device" name string. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e validation, JetStream rationale - voice-relay: use get_secret() from services.common.env for NATS_URL (supports Docker _FILE secret loading, addresses plaintext cred concern) - voice-relay: document core-NATS-vs-JetStream design decision inline - voice-relay: improve pmoves_health TODO with tracking issue reference - cast-tts-gateway: wrap _publish_event with envelope() schema validation - topics.json: normalize publisher field to array for consistency Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5b525a6 to
8a87a6e
Compare
Docker Hardening ValidationHardening Validation ReportValidated: Sun Mar 15 13:52:35 UTC 2026Services CheckedPMOVES.AI Docker Hardening Validation[INFO] Checking: pmoves/docker-compose.hardened.yml [INFO] Validating: hi-rag-gateway-v2 [INFO] Validating: extract-worker [INFO] Validating: langextract [INFO] Validating: presign [INFO] Validating: render-webhook [INFO] Validating: retrieval-eval [INFO] Validating: pdf-ingest [INFO] Validating: jellyfin-bridge [INFO] Validating: invidious-companion-proxy [INFO] Validating: ffmpeg-whisper [INFO] Validating: media-video [INFO] Validating: media-audio [INFO] Validating: hi-rag-gateway-v2-gpu [INFO] Validating: hi-rag-gateway-gpu [INFO] Validating: deepresearch [INFO] Validating: supaserch [INFO] Validating: publisher-discord [INFO] Validating: mesh-agent [INFO] Validating: nats-echo-req [INFO] Validating: nats-echo-res [INFO] Validating: publisher [INFO] Validating: analysis-echo [INFO] Validating: graph-linker [INFO] Validating: comfy-watcher [INFO] Validating: grayjay-plugin-host [INFO] Validating: agent-zero [INFO] Validating: archon [INFO] Validating: channel-monitor [INFO] Validating: pmoves-yt [INFO] Validating: notebook-sync [INFO] Validating: supabase_service_role_key [INFO] Validating: supabase_jwt_secret ====================================== |
Document the full pr-monitor-graphiti-chit FlOO$ pipeline in CLAUDE.md, wire existing hooks into settings.json, and resolve all remaining CodeRabbit threads from the #934-941 merge session. Changes: - CLAUDE.md: Add "PR Review & Merge Workflow" section with skill chain, usage guide, FlOO$ validation commands, and NATS subjects - CLAUDE.md: Fix skill pairing table (3-step → 4-step pipeline) - settings.json: Wire UserPromptSubmit hook for PR skill awareness - settings.json: Wire post-review-chit.sh to Skill PostToolUse - hooks/pr-skill-reminder.sh: New lightweight PR context reminder - .gitignore: Add runtime graphiti/CGP log patterns - Resolve 14 unresolved CodeRabbit threads on PRs #940 and #941 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: refresh CODEX_CLAUDE_PARITY_GAPS coverage report to 2026-03-13 - Coverage: 96.6% (down from 100%) - New commands added without parity updates: chit:review-sweep, chit:sign-trail, docs:reconcile, tac:review - Timestamp updated from 2026-02-28 to 2026-03-13 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(submodules): add PMOVES-a0-plugins plugin index + Agent Zero TAC tree **PMOVES-a0-plugins Submodule:** - Adds POWERFULMOVES/PMOVES-a0-plugins as submodule - Tracks PMOVES.AI-Edition-Hardened branch for security hardening - Enables curated plugin ecosystem for Agent Zero customizations - README includes PMOVES.AI-specific integration patterns: - TensorZero Gateway (port 3030/3000) for all LLM calls - NATS (nats://nats:pmoves@nats:4222) for event coordination - Hi-RAG v2 (port 8086/8087) for knowledge retrieval - Archon (port 8091) for prompt management - Security: non-root containers, healthchecks, metrics, CHIT **Agent Zero Customization TAC Tree:** - 50 checks across 9 phases for comprehensive review - 100% pass rate validates: - TensorZero integration and model naming format - Extension system proper use (23 extension points) - PMOVES.AI service leverage (not duplication) - Docker hardening (tier-agent-hardened anchor) - Observability (healthz/metrics endpoints, NATS heartbeat) - 4-tier context loading strategy - Plugin ecosystem integration - Subordinate agent model - Tools & prompts customization patterns Related: Agent Zero customization documentation and integration patterns * docs(submodule): update Agent Zero with integration guide and quickstart - PMOVES.AI_INTEGRATION.md: Complete service integration reference - QUICKSTART.md: 15-minute getting started guide This documentation provides: - Service connection details (TensorZero, NATS, Hi-RAG, Archon, Neo4j) - MCP API usage examples - Extension system guide (23 lifecycle hooks) - Security hardening patterns - Troubleshooting section Related: Agent Zero TAC tree (50/50 checks passing) Related: PMOVES-a0-plugins submodule initialization * feat(submodules): add PMOVES.Notes plugin to a0-plugins index **Plugin Added:** pmoves-notes-integration - Repository: https://github.com/POWERFULMOVES/a0-plugin-pmoves-notes - Branch: PMOVES.AI-Edition-Hardened **Plugin Features:** - Auto-save conversation summaries (message_loop_end extension) - Save reasoning traces to memory (monologue_end extension) - Manual tools: save_note, search_notes - NATS events: agent.notes.saved.v1, agent.notes.searched.v1 - Open Notebook integration (SurrealDB knowledge base) **TAC Review Results:** - Agent Zero Customization Review: 50/50 passing (100%) - All 9 phases validated successfully Related: Agent Zero integration documentation Related: PMOVES-a0-plugins submodule initialization * refactor(tac): simplify NATS regex pattern and update description - Simplify NATS subject pattern: agent\.task\.|agent\.subordinate\. → agent\.task|agent\.subordinate - Update description to explicitly state "across 9 phases (51 checks)" - Removes unnecessary escape before pipe operator - More robust pattern matching for NATS subjects Suggested by code review feedback - all 50 checks still passing. * fix(ui): apply PR review fixes to main - Fix playwright default port: 3100 → 4482 (matches docker-compose) - Fix base64url decoding in boot-jwt route (JWT uses -/_ instead of +/) - Add spawn error handler to with-env.mjs These fixes were applied to all UI Testing PRs (#908-#913). PR branches will rebase onto main to pick up these core fixes. * feat(ops): PR review skill chain integration + thread resolution Document the full pr-monitor-graphiti-chit FlOO$ pipeline in CLAUDE.md, wire existing hooks into settings.json, and resolve all remaining CodeRabbit threads from the #934-941 merge session. Changes: - CLAUDE.md: Add "PR Review & Merge Workflow" section with skill chain, usage guide, FlOO$ validation commands, and NATS subjects - CLAUDE.md: Fix skill pairing table (3-step → 4-step pipeline) - settings.json: Wire UserPromptSubmit hook for PR skill awareness - settings.json: Wire post-review-chit.sh to Skill PostToolUse - hooks/pr-skill-reminder.sh: New lightweight PR context reminder - .gitignore: Add runtime graphiti/CGP log patterns - Resolve 14 unresolved CodeRabbit threads on PRs #940 and #941 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Shaela Bello <slbello@uncg.edu> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
voice.agent.response.v1had subscribers (Flute-Gateway, cast-tts-gateway) but no publisher — voice pipeline dead-ended after Agent Zero task completionvoice-relayservice: lightweight NATS bridge that filtersagentzero.task.result.v1events withmeta.voice_mode=trueand republishes tovoice.agent.response.v1Changes
Commit 1: Service implementation
pmoves/services/voice-relay/main.pypmoves/services/voice-relay/requirements.txtpmoves/services/voice-relay/Dockerfilepmoves/docker-compose.ymlCommit 2: Registry & documentation
pmoves/contracts/topics.jsonpmoves/tests/utils/service_catalog.py.claude/context/services-catalog.md.claude/context/nats-subjects.mdpmoves/docs/architecture/voice-agent-response-relay.mdDesign
Port allocation
Port 8121 registered in all 3 catalogs:
docker-compose.yml(VOICE_RELAY_PORT).claude/context/services-catalog.mdpmoves/tests/utils/service_catalog.pyTest plan
python -m pytest pmoves/tests/test_port_conflicts.py -v— no port conflictsdocker compose --profile cast build voice-relay— image builds cleanlycurl -sf http://localhost:8121/healthz | jq .— health check respondscurl -sf http://localhost:8121/metrics | head -5— Prometheus metrics exposedagentzero.task.result.v1withmeta.voice_mode: true, verify relay tovoice.agent.response.v1🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores