Skip to content

feat(seap): watch-folder-router — auto-route ingest.file.added.v1 by MIME (Bud 1) - #2535

Merged
POWERFULMOVES merged 1 commit into
mainfrom
feat/watch-folder-router
Aug 12, 2026
Merged

POWERFULMOVES merged 1 commit into
mainfrom
feat/watch-folder-router

Conversation

@POWERFULMOVES

@POWERFULMOVES POWERFULMOVES commented Aug 12, 2026

Copy link
Copy Markdown
Owner

SEAP ingestion Bud 1 — the first bud of the watch-folder constellation (SEAP_WATCHFOLDER_NATS_CONSTELLATION_2026-08-12, operator-directed).

What

A thin NATS router that consumes ingest.file.added.v1 and routes by mime_type, retiring the manual media-audio /analyze calls that every hand-driven ingest made:

  • audio/*, video/* → media-audio /analyze (transcription) → ingest.transcript.ready.v1
  • application/pdf + office docs → ingest.document.ready.v1 (pdf-ingest/langextract consume)
  • everything else → ingest.text.ready.v1 (extract-worker embeds)

MIME is trusted first; file extension is the fallback when MIME is generic (octet-stream).

Pattern

Follows the established templates: extract-worker (pure NATS consumer, no client HTTP) + voice-sampler (thin orchestrator, MinIO/media-audio wiring). No models, no GPU. :8125 health/metrics only. NATS default aligned to the standard authed URL. All three publish subjects already exist in nats-subjects.md.

Verified live

Built + started healthy on 5090 (NATS connected). Routing smoke test: synthetic PDF + text-plain ingest.file.added.v1 events routed correctly to document.ready and text.ready with the standard envelope — SMOKE PASS.

Constellation context (not in this PR)

  • Bud 2 transcript-harvester: consume the *.ready.v1 family → Hi-RAG + Open Notebook + JuiceFS + CGP
  • Bud 3 archon-harvest: decide what deserves an Archon work order

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a watch-folder router service for automatic file-ingestion routing.
    • Classifies files by type and sends audio/video for transcription.
    • Publishes transcript, document, and text-ready events with correlation metadata.
    • Added health and metrics endpoints for service monitoring.
    • Added deployment support, health checks, and configurable port exposure.
  • Documentation

    • Added setup, configuration, routing behavior, event format, and environment variable documentation.

…MIME (Bud 1)

The first bud of the SEAP watch-folder constellation
(SEAP_WATCHFOLDER_NATS_CONSTELLATION_2026-08-12). Consumes
ingest.file.added.v1 and routes by mime_type to the right analyzer,
retiring the manual media-audio /analyze calls every hand-driven ingest made:
  audio/* video/*  -> media-audio /analyze -> ingest.transcript.ready.v1
  pdf + office docs -> ingest.document.ready.v1 (pdf-ingest/langextract)
  else (text)       -> ingest.text.ready.v1 (extract-worker)

Thin NATS router (no models/GPU), :8125 health/metrics only. Follows the
extract-worker pure-consumer + voice-sampler MinIO patterns. NATS default
uses the standard authed URL (nats://nats:pmoves@...). All three subjects
already exist in the catalog.

Smoke-verified live: synthetic PDF + text ingest.file.added events routed to
document.ready and text.ready respectively (SMOKE PASS); container healthy,
NATS connected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added compose Compose files / service Dockerfiles docs Documentation services Service source under pmoves/services/ labels Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Wed Aug 12 16:29:53 UTC 2026

Services Checked

PMOVES.AI Docker Hardening Validation

[INFO] Checking: pmoves/docker-compose.hardened.yml

[INFO] Validating: hi-rag-gateway-v2
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: extract-worker
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: langextract
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: presign
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: render-webhook
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: retrieval-eval
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: pdf-ingest
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: jellyfin-bridge
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: invidious-companion-proxy
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: ffmpeg-whisper
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: media-video
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: media-audio
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: hi-rag-gateway-gpu
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: deepresearch
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: supaserch
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: publisher-discord
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: mesh-agent
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: nats-echo-req
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: nats-echo-res
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: comfy-watcher
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: grayjay-plugin-host
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: agent-zero
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: p7-room-orchestrator
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: archon
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: channel-monitor
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: pmoves-yt
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: notebook-sync
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: supabase_service_role_key
[WARN] No user directive
[WARN] No read_only directive
[WARN] No cap_drop: ["ALL"]
[WARN] No no-new-privileges
[WARN] No resource limits

[INFO] Validating: supabase_jwt_secret
[WARN] No user directive
[WARN] No read_only directive
[WARN] No cap_drop: ["ALL"]
[WARN] No no-new-privileges
[WARN] No resource limits

[INFO] Validating: p7_control_token
[WARN] No user directive
[WARN] No read_only directive
[WARN] No cap_drop: ["ALL"]
[WARN] No no-new-privileges
[WARN] No resource limits

======================================
Summary: 112 passed, 43 warnings, 0 errors

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a NATS-driven watch-folder-router service. The service classifies file events, transcribes audio and video through media-audio, publishes routed events, exposes health and metrics endpoints, and runs through the worker Compose configurations.

Changes

Watch-folder routing

Layer / File(s) Summary
File classification and event routing
pmoves/services/watch-folder-router/worker.py, pmoves/services/watch-folder-router/README.md
The router validates ingest.file.added.v1 events, classifies files by MIME type or extension, transcribes media, and publishes transcript, document, or text events with correlation metadata.
Runtime lifecycle and observability
pmoves/services/watch-folder-router/worker.py, pmoves/services/watch-folder-router/Dockerfile, pmoves/services/watch-folder-router/requirements.txt
The service manages NATS startup and shutdown, exposes /healthz and /metrics, and defines its container runtime and dependencies.
Worker deployment wiring
pmoves/docker-compose.yml, pmoves/docker-compose.workers.yml, pmoves/scripts/split_compose.py
The router is added to the workers profile with NATS and media-audio settings, network access, port 8125, health checks, and hardened runtime settings.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FileIngestion
  participant NATS
  participant WatchFolderRouter
  participant MediaAudio
  FileIngestion->>NATS: Publish ingest.file.added.v1
  NATS->>WatchFolderRouter: Deliver file event
  WatchFolderRouter->>WatchFolderRouter: Classify file
  WatchFolderRouter->>MediaAudio: Request media transcription
  MediaAudio-->>WatchFolderRouter: Return transcript
  WatchFolderRouter->>NATS: Publish routed event
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change and smoke test, but it omits the required Testing, Required Checks, and Review Coordination sections. Add the required template sections, document exact test commands and outputs, complete the required checklists, and record review coordination.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the watch-folder-router feature and its MIME-based routing of ingest.file.added.v1 events.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/watch-folder-router

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 290e1a0769

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +114 to +116
bucket = event.get("bucket")
key = event.get("key")
mime = event.get("mime_type", "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Unwrap the standard ingest envelope before routing

For every contract-compliant ingest.file.added.v1 message, the decoded object is an event envelope and the file fields are under event["payload"]; moreover, the registered payload exposes uri and kind, not top-level bucket, key, or mime_type. For example, pdf-ingest/app.py publishes exactly that envelope, so its events always hit this missing bucket/key rejection and no route runs. Unwrap the payload and parse the registered uri/kind contract, or introduce a separately versioned input contract.

AGENTS.md reference: pmoves/AGENTS.md:L34-L36

Useful? React with 👍 / 👎.



def _envelope(topic: str, payload: Dict[str, Any], src_event: Dict[str, Any]) -> bytes:
return json.dumps(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit the registered standard event envelope

When any route succeeds, _envelope() emits only topic, payload, correlation_id, source, and ts, while contracts/schemas/common/envelope.schema.json requires id and version as well. Consequently, consumers that validate the documented standard envelope will reject every event produced by this router; use services.common.events.envelope and validate the route payload before publishing.

AGENTS.md reference: pmoves/AGENTS.md:L219-L219

Useful? React with 👍 / 👎.

Comment on lines +151 to +153
elif route == "document":
# pdf-ingest / langextract own extraction; router only announces.
await _publish(SUBJECT_DOCUMENT, base, event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Process documents before announcing completion

When a PDF or Office document is classified here, the branch immediately publishes ingest.document.ready.v1 without calling pdf-ingest or langextract. Repo-wide inspection shows those services do not consume this subject—pdf-ingest publishes it only after downloading, extracting, and indexing—and the payload here also lacks the contract-required doc_id, uri, and chunk_count. Thus a watch-folder document is reported ready although no extraction or embedding occurred; invoke the document ingest path and publish its resulting completion event instead.

AGENTS.md reference: pmoves/AGENTS.md:L34-L36

Useful? React with 👍 / 👎.

Comment on lines +156 to +157
else:
await _publish(SUBJECT_TEXT, base, event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route text through a registered ingestion path

For plain-text inputs, this publishes ingest.text.ready.v1, but that topic is absent from contracts/topics.json and repo-wide search finds no subscriber for it; extract-worker accepts chunks through its HTTP /ingest endpoint instead. Therefore text files are silently declared ready without ever being extracted or embedded. Route them to the existing ingest API, or add a versioned contract and an actual consumer before publishing this subject.

AGENTS.md reference: pmoves/AGENTS.md:L31-L31

Useful? React with 👍 / 👎.

restart: unless-stopped
environment:
- NATS_URL=${NATS_URL:-nats://nats:4222}
- MEDIA_AUDIO_URL=${MEDIA_AUDIO_URL:-http://media-audio:8082}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make media-audio available to the worker overlay

Under the canonical overlay-up-workers path checked in pmoves/Makefile, Compose loads only the base, core, and workers overlays, while split_compose.py assigns media-audio exclusively to the media overlay. In that supported deployment, the configured media-audio hostname therefore has no running service, so every audio/video transcription request fails. Move the router alongside its analyzer or include/start the media dependency for the workers target.

AGENTS.md reference: AGENTS.md:L48-L50

Useful? React with 👍 / 👎.

@POWERFULMOVES

Copy link
Copy Markdown
Owner Author

Security-review note (automated scan flagged the NATS default as a hardcoded secret): acknowledged, proceeding without change — this is the documented PMOVES NATS-creds convention, verified against current code:

Ref: memory feedback_nats_creds_convention (re-verified 2026-08-12, not relied on blind — the 71-file prevalence + runtime injection are the actual evidence).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
pmoves/services/watch-folder-router/README.md (1)

11-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the required topology and service catalog files.

Add watch-folder-router, port 8125, /healthz, /metrics, and its subscribed and published subjects to .claude/context/services-catalog.md and .claude/context/nats-subjects.md. Update the Mermaid topology using pmoves/docs/AGENTS/PMOVES_AGENT_TOPOLOGY.md.

As per coding guidelines, “Document NATS event topology in .claude/context/nats-subjects.md and maintain services catalog with port assignments and health endpoints in .claude/context/services-catalog.md.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pmoves/services/watch-folder-router/README.md` around lines 11 - 16, Update
.claude/context/services-catalog.md with watch-folder-router, port 8125, and its
/healthz and /metrics endpoints; update .claude/context/nats-subjects.md with
its ingest.file.added.v1 subscription and MIME-based published subjects. Revise
the Mermaid topology following PMOVES_AGENT_TOPOLOGY.md to include the router
and its NATS/service connections.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pmoves/docker-compose.yml`:
- Around line 2284-2287: Replace the unauthenticated NATS fallback with the
tier-provided convention NATS_URL=${NATS_URL} in pmoves/docker-compose.yml lines
2284-2287 and regenerate the same corrected entry in
pmoves/docker-compose.workers.yml lines 97-100. Update
pmoves/services/watch-folder-router/README.md lines 37-41 to document that the
authenticated NATS URL comes from the tier environment.

In `@pmoves/services/watch-folder-router/requirements.txt`:
- Around line 2-3: Upgrade the FastAPI pin in requirements.txt to a release that
permits Starlette 1.0.1 or newer, then regenerate requirements.lock using the
dependency workflow consumed by the Docker image. Ensure the regenerated
lockfile resolves the compatible Starlette version and do not change h11 solely
for this issue.

In `@pmoves/services/watch-folder-router/worker.py`:
- Around line 84-207: Add concise docstrings to the undocumented functions in
the changed module, including _envelope, _transcribe, _handle_file_added,
_publish, lifespan, healthz, and metrics. Keep the existing behavior unchanged
and ensure at least 80% of the new functions have docstrings.
- Around line 114-120: Update the rejected-event warning in the worker’s
bucket/key validation block to stop logging the complete event payload. Log only
safe identifiers such as the presence or values of validated fields, or the
event’s field names, while preserving the rejection metric and early return.
- Around line 176-185: Ensure the router never completes startup without a
usable NATS subscription: update the lifespan connection flow in
pmoves/services/watch-folder-router/worker.py:176-185 to retry the initial
connection/subscription or re-raise the failure; update the /healthz handler at
pmoves/services/watch-folder-router/worker.py:193-202 to return an unhealthy
HTTP status when _nc is absent or closed; add depends_on with nats
service_healthy to the watch-folder-router service in
pmoves/docker-compose.yml:2283-2302 and regenerate the equivalent dependency in
pmoves/docker-compose.workers.yml:96-115.
- Around line 75-80: Update the MIME classification logic around the extension
fallback so that a non-empty mime value other than application/octet-stream
returns "text" before checking file extensions. Keep the extension-based media
and document routing only for empty MIME values or the generic octet-stream
MIME.
- Around line 106-120: Update _handle_file_added to require all
ingest.file.added.v1 fields defined by file-added.v1.schema.json, including
file_id, uri, and kind, before routing. Call
services.common.events.validate_payload() on the incoming event before
classification/publication, and validate each downstream payload immediately
before _publish.

---

Nitpick comments:
In `@pmoves/services/watch-folder-router/README.md`:
- Around line 11-16: Update .claude/context/services-catalog.md with
watch-folder-router, port 8125, and its /healthz and /metrics endpoints; update
.claude/context/nats-subjects.md with its ingest.file.added.v1 subscription and
MIME-based published subjects. Revise the Mermaid topology following
PMOVES_AGENT_TOPOLOGY.md to include the router and its NATS/service connections.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 638c9939-08c9-4864-b5d2-424866b4820c

📥 Commits

Reviewing files that changed from the base of the PR and between 4646f01 and 290e1a0.

⛔ Files ignored due to path filters (1)
  • pmoves/services/watch-folder-router/requirements.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • pmoves/docker-compose.workers.yml
  • pmoves/docker-compose.yml
  • pmoves/scripts/split_compose.py
  • pmoves/services/watch-folder-router/Dockerfile
  • pmoves/services/watch-folder-router/README.md
  • pmoves/services/watch-folder-router/requirements.txt
  • pmoves/services/watch-folder-router/worker.py

Comment thread pmoves/docker-compose.yml
Comment on lines +2284 to +2287
environment:
- NATS_URL=${NATS_URL:-nats://nats:4222}
- MEDIA_AUDIO_URL=${MEDIA_AUDIO_URL:-http://media-audio:8082}
- ROUTER_TRANSCRIBE_TIMEOUT_SEC=${ROUTER_TRANSCRIBE_TIMEOUT_SEC:-3600}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use the authenticated worker NATS configuration.

The unauthenticated fallback overrides the env_file value. The router then cannot connect to an authenticated NATS server. Use the repository worker NATS_URL=${NATS_URL} convention and document that the value comes from the tier environment.

  • pmoves/docker-compose.yml#L2284-L2287: remove the unauthenticated nats://nats:4222 fallback.
  • pmoves/docker-compose.workers.yml#L97-L100: regenerate the overlay with the same corrected NATS entry.
  • pmoves/services/watch-folder-router/README.md#L37-L41: document the authenticated tier-provided NATS URL instead of an unauthenticated default.

Based on learnings, worker service NATS configuration is intentionally sourced through tier env_file entries and uses NATS_URL=${NATS_URL}.

📍 Affects 3 files
  • pmoves/docker-compose.yml#L2284-L2287 (this comment)
  • pmoves/docker-compose.workers.yml#L97-L100
  • pmoves/services/watch-folder-router/README.md#L37-L41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pmoves/docker-compose.yml` around lines 2284 - 2287, Replace the
unauthenticated NATS fallback with the tier-provided convention
NATS_URL=${NATS_URL} in pmoves/docker-compose.yml lines 2284-2287 and regenerate
the same corrected entry in pmoves/docker-compose.workers.yml lines 97-100.
Update pmoves/services/watch-folder-router/README.md lines 37-41 to document
that the authenticated NATS URL comes from the tier environment.

Source: Learnings

Comment on lines +2 to +3
fastapi==0.114.2
uvicorn[standard]==0.30.6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a '^requirements\.lock$' pmoves/services/watch-folder-router
rg -n '^(fastapi|uvicorn|starlette|h11)==' \
  pmoves/services/watch-folder-router/requirements.lock \
  pmoves/services/watch-folder-router/requirements.txt

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 597


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- service files ---'
git ls-files pmoves/services/watch-folder-router

printf '%s\n' '--- dependency references and lock metadata ---'
rg -n -i 'requirements\.lock|pip-compile|uv lock|starlette|h11|fastapi|uvicorn' \
  pmoves/services/watch-folder-router \
  pmoves/Makefile pmoves/README* README.md 2>/dev/null || true

printf '%s\n' '--- dependency entries ---'
cat -n pmoves/services/watch-folder-router/requirements.txt
cat -n pmoves/services/watch-folder-router/requirements.lock

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 4852


🌐 Web query:

GHSA-86qp-5c8j-p5mr Starlette affected versions patched version

💡 Result:

The vulnerability identified as GHSA-86qp-5c8j-p5mr (also tracked as CVE-2026-48710) affects the Starlette framework by allowing a malformed HTTP Host header to poison the request.url.path, which can lead to the bypass of path-based security checks [1][2][3]. Affected versions include all versions of Starlette up to and including 1.0.0 [1][2][4]. The patched version is 1.0.1 or higher [2][3][5]. Upgrading to this version ensures that the Host header is validated against the grammar of RFC 9112 §3.2 and RFC 3986 §3.2.2 when constructing request.url, falling back to scope["server"] if the header is malformed [1][4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
import urllib.request

for project in ("fastapi", "starlette", "uvicorn"):
    with urllib.request.urlopen(f"https://pypi.org/pypi/{project}/json", timeout=10) as response:
        data = json.load(response)
    print(f"{project}: latest={data['info']['version']}")
    if project == "fastapi":
        for version in ("0.114.2", data["info"]["version"]):
            release = data["releases"].get(version, [])
            if not release:
                print(f"  {version}: unavailable")
                continue
            with urllib.request.urlopen(
                f"https://pypi.org/pypi/{project}/{version}/json", timeout=10
            ) as response:
                info = json.load(response)["info"]
            print(f"  {version} requires:")
            for requirement in info.get("requires_dist") or []:
                if requirement.lower().startswith("starlette"):
                    print(f"    {requirement}")
PY

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 2447


🌐 Web query:

FastAPI Starlette 1.0.1 compatibility requires starlette

💡 Result:

FastAPI supports Starlette 1.0.1 and later [1][2]. While FastAPI generally manages its dependencies automatically, using Starlette 1.0.1 or higher is explicitly recommended to resolve the high-severity security vulnerability CVE-2026-48710 (Host Header Validation Bypass) present in Starlette versions 1.0.0 and earlier [3][4]. If your project is using an older, pinned version of Starlette, you should manually update to at least 1.0.1 to address this security risk [3][4]. Modern versions of FastAPI (e.g., 0.115.x and later) are configured to pull in compatible Starlette 1.0+ versions automatically, but in environments with constrained or pinned dependencies, an explicit upgrade is necessary [3]. Key points regarding compatibility: - Security Recommendation: Upgrade to Starlette 1.0.1+ immediately to patch CVE-2026-48710, which can allow attackers to bypass path-based security middleware by crafting malformed Host headers [3][4]. - Versioning: FastAPI added support for Starlette 1.0.0+ in earlier releases [1][2] and continues to support the 1.x series [2][5]. - Best Practice: Avoid pinning Starlette versions unless absolutely necessary. Letting the package manager resolve the latest version ensures that security patches are applied automatically as they become available [6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- request URL and host-related logic ---'
rg -n -i 'host|trustedhost|request\.url|url\.path|middleware|path' \
  pmoves/services/watch-folder-router/worker.py \
  pmoves/services/watch-folder-router/README.md

printf '%s\n' '--- FastAPI metadata ---'
curl --fail --silent --show-error --insecure https://pypi.org/pypi/fastapi/json |
  python3 -c '
import json, sys
data=json.load(sys.stdin)
print("latest:", data["info"]["version"])
for version in ("0.114.2", "0.115.0", "0.115.12", data["info"]["version"]):
    if version not in data["releases"]:
        continue
    import urllib.request, ssl
    url=f"https://pypi.org/pypi/fastapi/{version}/json"
    with urllib.request.urlopen(url, context=ssl._create_unverified_context(), timeout=10) as response:
        info=json.load(response)["info"]
    print(version)
    for requirement in info.get("requires_dist") or []:
        if requirement.lower().startswith("starlette"):
            print(" ", requirement)
'

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 480


🌐 Web query:

site:github.com/fastapi/fastapi "starlette>=1.0.0" "fastapi"

💡 Result:

FastAPI provides full support for Starlette 1.0.0 and newer versions [1][2][3]. As of August 12, 2026, FastAPI supports Starlette version 1.0.0 and beyond, with current versions of FastAPI explicitly depending on Starlette versions that meet the starlette>=0.46.0 requirement [4]. The transition to supporting Starlette 1.0.0 was introduced earlier in 2026 [5][1], and subsequent releases have maintained compatibility with the Starlette 1.x.x series, including version 1.1.0 [3]. If you are encountering issues or dependency conflicts related to Starlette 1.0.0, ensure your FastAPI installation is up to date, as older versions of FastAPI (released prior to the formal adoption of Starlette 1.0.0 support) may strictly pin Starlette to versions lower than 1.0.0 [6][5]. If you are using a modern version of FastAPI, the pyproject.toml or requirements.txt should allow Starlette 1.0.0+ automatically [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline pmoves/services/watch-folder-router/worker.py --lang python
printf '%s\n' '--- route declarations and request URL usage ---'
rg -n -C 3 '`@app`\.|FastAPI\(|Request|request\.|url\.path|path-based|TrustedHost|middleware' \
  pmoves/services/watch-folder-router/worker.py

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1730


Upgrade FastAPI and regenerate requirements.lock.

fastapi==0.114.2 constrains Starlette to <0.39.0, so lockfile regeneration alone cannot remove starlette==0.38.6. Upgrade FastAPI to a release that permits starlette>=1.0.1, then regenerate the lockfile used by the Docker image. h11==0.16.0 is unrelated to this advisory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pmoves/services/watch-folder-router/requirements.txt` around lines 2 - 3,
Upgrade the FastAPI pin in requirements.txt to a release that permits Starlette
1.0.1 or newer, then regenerate requirements.lock using the dependency workflow
consumed by the Docker image. Ensure the regenerated lockfile resolves the
compatible Starlette version and do not change h11 solely for this issue.

Source: Linters/SAST tools

Comment on lines +75 to +80
# Fall back to extension when the MIME is generic (octet-stream).
ext = os.path.splitext(key or "")[1].lower()
if ext in (".m4a", ".mp3", ".wav", ".mp4", ".webm", ".mkv", ".mov", ".ogg", ".flac"):
return "media"
if ext in (".pdf", ".docx", ".doc", ".pptx", ".xlsx", ".rtf"):
return "document"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restrict extension fallback to generic MIME types.

Lines 75-80 apply the extension fallback for every non-media and non-document MIME type. A file declared as text/plain with an .mp3 suffix routes to media-audio. Return "text" before extension checks when mime is non-empty and is not application/octet-stream.

Proposed fix
     if mime.startswith(DOC_MIME_PREFIXES) or mime in DOC_MIME_EXACT:
         return "document"
-    # Fall back to extension when the MIME is generic (octet-stream).
+    if mime and mime != "application/octet-stream":
+        return "text"
+    # Fall back to extension when the MIME is generic (octet-stream).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Fall back to extension when the MIME is generic (octet-stream).
ext = os.path.splitext(key or "")[1].lower()
if ext in (".m4a", ".mp3", ".wav", ".mp4", ".webm", ".mkv", ".mov", ".ogg", ".flac"):
return "media"
if ext in (".pdf", ".docx", ".doc", ".pptx", ".xlsx", ".rtf"):
return "document"
# Fall back to extension when the MIME is generic (octet-stream).
ext = os.path.splitext(key or "")[1].lower()
if ext in (".m4a", ".mp3", ".wav", ".mp4", ".webm", ".mkv", ".mov", ".ogg", ".flac"):
return "media"
if ext in (".pdf", ".docx", ".doc", ".pptx", ".xlsx", ".rtf"):
return "document"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pmoves/services/watch-folder-router/worker.py` around lines 75 - 80, Update
the MIME classification logic around the extension fallback so that a non-empty
mime value other than application/octet-stream returns "text" before checking
file extensions. Keep the extension-based media and document routing only for
empty MIME values or the generic octet-stream MIME.

Comment on lines +84 to +207
def _envelope(topic: str, payload: Dict[str, Any], src_event: Dict[str, Any]) -> bytes:
return json.dumps(
{
"topic": topic,
"payload": payload,
"correlation_id": src_event.get("correlation_id") or src_event.get("file_id"),
"source": "watch-folder-router",
"ts": datetime.now(timezone.utc).isoformat(),
}
).encode()


def _transcribe(bucket: str, key: str) -> Dict[str, Any]:
resp = http.post(
f"{MEDIA_AUDIO_URL}/analyze",
json={"bucket": bucket, "key": key, "analysis_type": "transcription"},
timeout=DIARIZE_TIMEOUT,
)
resp.raise_for_status()
return resp.json()


async def _handle_file_added(msg) -> None:
try:
event = json.loads(msg.data.decode())
except Exception: # noqa: BLE001
logger.exception("unparseable ingest.file.added event")
routed_total.labels(route="unknown", status="rejected").inc()
return

bucket = event.get("bucket")
key = event.get("key")
mime = event.get("mime_type", "")
if not bucket or not key:
logger.warning("event missing bucket/key: %s", event)
routed_total.labels(route="unknown", status="rejected").inc()
return

route = _classify(mime, key)
base = {
"file_id": event.get("file_id"),
"bucket": bucket,
"key": key,
"mime_type": mime,
"namespace": event.get("namespace"),
"room_id": event.get("room_id"),
"persona": event.get("persona"),
"uploader": event.get("uploader"),
}

try:
if route == "media":
# Transcription is the slow step; run it off the event-loop thread.
body = await asyncio.to_thread(_transcribe, bucket, key)
if body.get("error"):
raise RuntimeError(f"media-audio error: {body['error']}")
text = body.get("text") or ""
payload = {
**base,
"text": text,
"chunks": body.get("chunks", []),
"model": body.get("model"),
"char_count": len(str(text)),
}
await _publish(SUBJECT_TRANSCRIPT, payload, event)
routed_total.labels(route="media", status="ok").inc()
logger.info("routed media %s -> transcript.ready (%d chars)", key, len(str(text)))
elif route == "document":
# pdf-ingest / langextract own extraction; router only announces.
await _publish(SUBJECT_DOCUMENT, base, event)
routed_total.labels(route="document", status="ok").inc()
logger.info("routed document %s -> document.ready", key)
else:
await _publish(SUBJECT_TEXT, base, event)
routed_total.labels(route="text", status="ok").inc()
logger.info("routed text %s -> text.ready", key)
except Exception: # noqa: BLE001
logger.exception("routing failed for %s (route=%s)", key, route)
routed_total.labels(route=route, status="error").inc()


async def _publish(topic: str, payload: Dict[str, Any], src_event: Dict[str, Any]) -> None:
if _nc is None:
logger.warning("NATS not connected; dropping %s for %s", topic, payload.get("key"))
return
await _nc.publish(topic, _envelope(topic, payload, src_event))


# ── Lifespan / app ──────────────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
global _nc
try:
import nats

_nc = await nats.connect(NATS_URL, max_reconnect_attempts=-1)
await _nc.subscribe(SUBJECT_IN, cb=_handle_file_added)
logger.info("NATS connected; subscribed %s", SUBJECT_IN)
except Exception: # noqa: BLE001
logger.exception("NATS connect failed — router is idle until NATS is reachable")
_nc = None
yield
if _nc is not None:
await _nc.drain()


app = FastAPI(title="watch-folder-router", lifespan=lifespan)


@app.get("/healthz")
async def healthz():
return {
"status": "healthy",
"service": "watch-folder-router",
"nats_connected": _nc is not None and not _nc.is_closed,
"media_audio_url": MEDIA_AUDIO_URL,
"subscribes": SUBJECT_IN,
"publishes": [SUBJECT_TRANSCRIPT, SUBJECT_DOCUMENT, SUBJECT_TEXT],
}


@app.get("/metrics")
async def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add function docstrings to meet the coverage requirement.

Only _classify has a function docstring. The other seven functions do not meet the required 80% docstring coverage for new Python code.

As per coding guidelines, “New Python code must maintain at least 80% docstring coverage.”

🧰 Tools
🪛 ast-grep (0.45.1)

[info] 84-92: use jsonify instead of json.dumps for JSON output
Context: json.dumps(
{
"topic": topic,
"payload": payload,
"correlation_id": src_event.get("correlation_id") or src_event.get("file_id"),
"source": "watch-folder-router",
"ts": datetime.now(timezone.utc).isoformat(),
}
)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pmoves/services/watch-folder-router/worker.py` around lines 84 - 207, Add
concise docstrings to the undocumented functions in the changed module,
including _envelope, _transcribe, _handle_file_added, _publish, lifespan,
healthz, and metrics. Keep the existing behavior unchanged and ensure at least
80% of the new functions have docstrings.

Source: Coding guidelines

Comment on lines +106 to +120
async def _handle_file_added(msg) -> None:
try:
event = json.loads(msg.data.decode())
except Exception: # noqa: BLE001
logger.exception("unparseable ingest.file.added event")
routed_total.labels(route="unknown", status="rejected").inc()
return

bucket = event.get("bucket")
key = event.get("key")
mime = event.get("mime_type", "")
if not bucket or not key:
logger.warning("event missing bucket/key: %s", event)
routed_total.labels(route="unknown", status="rejected").inc()
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline pmoves/services/common/events.py --items all
rg -n -C 4 --glob '*.py' \
  'services\.common\.events|validate.*event|ingest\.file\.added\.v1' pmoves/services

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 26718


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- worker structure ---'
ast-grep outline pmoves/services/watch-folder-router/worker.py --items all
printf '%s\n' '--- worker implementation ---'
cat -n pmoves/services/watch-folder-router/worker.py | sed -n '1,240p'

printf '%s\n' '--- shared event helper ---'
cat -n pmoves/services/common/events.py | sed -n '1,130p'

printf '%s\n' '--- relevant schemas and references ---'
find pmoves -type f \( -iname '*ingest*' -o -iname '*file*added*' -o -iname '*.json' \) -print | sort | head -200
rg -n -C 3 'file\.added|bucket|mime_type|checksum|size_bytes' pmoves --glob '*.json' --glob '*.py' | head -300

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ingest.file.added.v1 schema ---'
cat -n pmoves/contracts/schemas/ingest/file-added.v1.schema.json
printf '%s\n' '--- downstream schemas ---'
cat -n pmoves/contracts/schemas/ingest/document-ready.v1.schema.json
cat -n pmoves/contracts/schemas/ingest/transcript-ready.v1.schema.json
printf '%s\n' '--- topic registration ---'
sed -n '258,280p' pmoves/contracts/topics.json

printf '%s\n' '--- deterministic schema probe ---'
python3 - <<'PY'
import json
from pathlib import Path

try:
    from jsonschema import Draft202012Validator
except Exception as exc:
    print(f"jsonschema unavailable: {exc}")
    raise SystemExit(0)

root = Path("pmoves")
topics = json.loads((root / "contracts/topics.json").read_text())
schema = json.loads((root / "contracts" / topics["topics"]["ingest.file.added.v1"]["schema"]).read_text())
validator = Draft202012Validator(schema)

cases = {
    "minimal bucket/key": {"bucket": "assets", "key": "a.txt"},
    "valid-looking event": {
        "file_id": "f1", "bucket": "assets", "key": "a.txt",
        "mime_type": "text/plain", "namespace": "ns",
        "checksum": "abc", "size_bytes": 1, "meta": {},
    },
    "wrong optional types": {
        "bucket": "assets", "key": "a.txt", "mime_type": 7,
        "checksum": [], "size_bytes": "1", "meta": "bad",
    },
    "wrong required types": {"bucket": 4, "key": None},
}
for name, payload in cases.items():
    errors = sorted(validator.iter_errors(payload), key=lambda e: list(e.path))
    print(name, "VALID" if not errors else "INVALID")
    for error in errors:
        print(" ", list(error.path), error.message)
PY

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 4461


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- producers of ingest.file.added.v1 ---'
rg -n -C 10 'ingest\.file\.added\.v1' pmoves/services pmoves --glob '*.py' --glob '!pmoves/services/watch-folder-router/worker.py' | head -240

printf '%s\n' '--- all file-added samples and contract references ---'
rg -n -C 4 'file_id|\"uri\"|\"kind\"|\"bucket\"|\"key\"|\"mime_type\"' \
  pmoves/contracts/samples pmoves/services --glob '*.json' --glob '*.py' \
  | rg -B 4 -A 8 'file_id|\"uri\"|\"kind\"|\"bucket\"|\"key\"|\"mime_type\"' \
  | head -320

printf '%s\n' '--- worker publication and common helper usage ---'
rg -n -C 3 'def _publish|_envelope|validate_payload|from .*common\.events' \
  pmoves/services/watch-folder-router pmoves/services/common

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 43794


Validate ingest.file.added.v1 before routing and publishing.

The schema requires file_id, uri, and kind, but _handle_file_added checks only bucket and key. Align the worker with pmoves/contracts/schemas/ingest/file-added.v1.schema.json, then call services.common.events.validate_payload() before classification and publication. Validate downstream payloads before _publish as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pmoves/services/watch-folder-router/worker.py` around lines 106 - 120, Update
_handle_file_added to require all ingest.file.added.v1 fields defined by
file-added.v1.schema.json, including file_id, uri, and kind, before routing.
Call services.common.events.validate_payload() on the incoming event before
classification/publication, and validate each downstream payload immediately
before _publish.

Source: Coding guidelines

Comment on lines +114 to +120
bucket = event.get("bucket")
key = event.get("key")
mime = event.get("mime_type", "")
if not bucket or not key:
logger.warning("event missing bucket/key: %s", event)
routed_total.labels(route="unknown", status="rejected").inc()
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not log the complete rejected event.

Line 118 logs all unvalidated fields. The event can contain uploader and arbitrary metadata. Log a safe identifier or field names instead of the complete payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pmoves/services/watch-folder-router/worker.py` around lines 114 - 120, Update
the rejected-event warning in the worker’s bucket/key validation block to stop
logging the complete event payload. Log only safe identifiers such as the
presence or values of validated fields, or the event’s field names, while
preserving the rejection metric and early return.

Comment on lines +176 to +185
try:
import nats

_nc = await nats.connect(NATS_URL, max_reconnect_attempts=-1)
await _nc.subscribe(SUBJECT_IN, cb=_handle_file_added)
logger.info("NATS connected; subscribed %s", SUBJECT_IN)
except Exception: # noqa: BLE001
logger.exception("NATS connect failed — router is idle until NATS is reachable")
_nc = None
yield

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not accept traffic while the router has no NATS subscription.

When the initial NATS connection fails, the lifespan handler sets _nc to None and completes startup. _publish() then drops every routed event, while /healthz still returns HTTP 200 and "healthy". Retry the initial connection or fail startup. Report an unhealthy status while NATS is unavailable. Add a NATS health dependency in both Compose definitions.

  • pmoves/services/watch-folder-router/worker.py#L176-L185: retry the initial connection and subscription, or re-raise to fail startup.
  • pmoves/services/watch-folder-router/worker.py#L193-L202: return an unhealthy HTTP status when _nc is absent or closed.
  • pmoves/docker-compose.yml#L2283-L2302: add depends_on: nats: condition: service_healthy.
  • pmoves/docker-compose.workers.yml#L96-L115: regenerate the overlay with the same dependency.
📍 Affects 3 files
  • pmoves/services/watch-folder-router/worker.py#L176-L185 (this comment)
  • pmoves/services/watch-folder-router/worker.py#L193-L202
  • pmoves/docker-compose.yml#L2283-L2302
  • pmoves/docker-compose.workers.yml#L96-L115
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pmoves/services/watch-folder-router/worker.py` around lines 176 - 185, Ensure
the router never completes startup without a usable NATS subscription: update
the lifespan connection flow in
pmoves/services/watch-folder-router/worker.py:176-185 to retry the initial
connection/subscription or re-raise the failure; update the /healthz handler at
pmoves/services/watch-folder-router/worker.py:193-202 to return an unhealthy
HTTP status when _nc is absent or closed; add depends_on with nats
service_healthy to the watch-folder-router service in
pmoves/docker-compose.yml:2283-2302 and regenerate the equivalent dependency in
pmoves/docker-compose.workers.yml:96-115.

@POWERFULMOVES
POWERFULMOVES merged commit ad9b828 into main Aug 12, 2026
57 of 60 checks passed
@POWERFULMOVES
POWERFULMOVES deleted the feat/watch-folder-router branch August 12, 2026 18:09
POWERFULMOVES added a commit that referenced this pull request Aug 12, 2026
…ded, A0 bundle dispatched (signed) (#2536)

Signed CHIT trail (HMAC-SHA256, kid chit-signing-v01). Session: #2535
watch-folder-router (SEAP Bud 1), NATS accounts/leaf-topology trio merged
(#2492/93/96), #2532/33/34 admin-merged (9 GHCR builds unblocked), A0
review-refactor bundle dispatched (context 6RQ9v5PP) + harvested with caveats.

Co-authored-by: Mavis <Mavis@pmoves.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compose Compose files / service Dockerfiles docs Documentation services Service source under pmoves/services/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant