feat(host-agent): add read-only service logs endpoint for all services#806
Conversation
Add POST /v1/service/logs to the host agent that serves logs for any service including core services, bypassing the existing extension-only restriction. Uses Docker Compose label lookup for accurate container name resolution (handles open-webui -> dream-webui mismatch). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lightheartdevs
left a comment
There was a problem hiding this comment.
Audit: APPROVE
Clean read-only service logs endpoint. Security is solid: auth via check_auth(), SERVICE_ID_RE prevents injection, tail clamped 1-500, output truncated to 50KB, no shell=True.
The Docker Compose label-based container name resolution (_resolve_container_name()) is more robust than hardcoded naming conventions.
Minor notes (non-blocking):
_resolve_container_name()callsdocker ps --filter label=...per request — not cached. Under load, many short-lived processes. Consider caching resolved names with a short TTL.- When
result.returncode != 0and stderr does NOT contain "no such container", the error falls through without clear indication. Consider handling non-zero return codes more explicitly.
Depends on #804. Should merge after it.
Lightheartdevs
left a comment
There was a problem hiding this comment.
Revised Audit: REQUEST CHANGES — cross-container info disclosure + error handling
Withdrawing my earlier approval after deeper review.
Security (MEDIUM-HIGH): Cross-container log reading via missing project filter
_resolve_container_name() queries docker ps --filter label=com.docker.compose.service={id} but does NOT filter by com.docker.compose.project. On any machine running other Compose stacks, an authenticated user can read logs from ANY container with a matching Compose service label.
Fix: Add project filter:
["docker", "ps",
"--filter", f"label=com.docker.compose.service={service_id}",
"--filter", "label=com.docker.compose.project=dream-server",
"--format", "{{.Names}}"]Bug 1 (MEDIUM): Non-zero docker logs exit returns HTTP 200
Only "no such container" in stderr is handled. All other Docker errors (permission denied, daemon error) fall through and return 200 with error text served as "logs".
Fix:
if result.returncode != 0:
json_response(self, 500, {"error": f"docker logs failed: {result.stderr[:500]}"})
returnBug 2 (LOW-MEDIUM): Missing broad exception handler
The existing _handle_logs() has except Exception as exc:. The new _handle_service_logs only catches TimeoutExpired. Unhandled exceptions leak Python tracebacks.
Fix: Add except Exception as exc: matching the existing pattern.
Everything else confirmed solid: SERVICE_ID_RE blocks all injection chars, subprocess uses list args (no shell), check_auth uses timing-safe comparison, timeout=5 prevents hangs, text=True means truncation operates on characters not bytes, tail properly clamped 1-500.
…ervice logs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addressing review feedbackAll 3 issues fixed: Security (Cross-container log reading) — Fixed: Bug 1 (Non-zero docker logs returns 200) — Fixed: Bug 2 (Missing broad exception handler) — Fixed: |
Lightheartdevs
left a comment
There was a problem hiding this comment.
Re-audit: APPROVE — all 3 fixes verified
- Cross-container fix:
--filter label=com.docker.compose.project=dream-serveradded to_resolve_container_name()✅ - Non-zero exit handling: separate checks for "no such container" (200) vs other errors (500) ✅
- Broad exception handler added matching existing pattern ✅
CI all green. Depends on #804.
|
Note: The Rust dashboard-api rewrite (#821) merged. This PR only modifies |
What
POST /v1/service/logsto the host agent for read-only log access to ALL services (core + extensions)_resolve_container_name()using Docker Compose label lookup for accurate name resolution/v1/extension/logsendpoint is unchanged (backwards compatible)Why
/v1/extension/logsreturns 403 for core services (llama-server, open-webui, etc.) becausevalidate_service_id()blocks themdream-{service_id}) fails foropen-webuiwhose container isdream-webuiHow
POST /v1/service/logswith{"service_id": "...", "tail": N}— validates format only viaSERVICE_ID_RE, no core service restrictiondocker ps --filter label=com.docker.compose.service={service_id}returns actual container name. Falls back todream-{service_id}if lookup failsThree Pillars Impact
/v1/extension/logsuntouched — additive onlyModified Files
dream-server/bin/dream-host-agent.py—_resolve_container_name(),_handle_service_logs(), route indo_POSTTesting
Automated
Manual
curl -X POST -H "Auth..." -d '{"service_id":"open-webui","tail":50}' .../v1/service/logs— verify logs returnedtail: 0→ treated as 1;tail: 999→ treated as 500../etc) → 400/v1/extension/logsstill works for user extensionsReview
Platform Impact
Sequence
PR 4 of 7 (Phase 2 — shared infrastructure)