Skip to content

feat(host-agent): add read-only service logs endpoint for all services#806

Merged
Lightheartdevs merged 2 commits intoLight-Heart-Labs:mainfrom
yasinBursali:feat/service-logs-agent
Apr 6, 2026
Merged

feat(host-agent): add read-only service logs endpoint for all services#806
Lightheartdevs merged 2 commits intoLight-Heart-Labs:mainfrom
yasinBursali:feat/service-logs-agent

Conversation

@yasinBursali
Copy link
Copy Markdown
Contributor

What

  • Add POST /v1/service/logs to the host agent for read-only log access to ALL services (core + extensions)
  • Add _resolve_container_name() using Docker Compose label lookup for accurate name resolution
  • Existing /v1/extension/logs endpoint is unchanged (backwards compatible)

Why

  • The existing /v1/extension/logs returns 403 for core services (llama-server, open-webui, etc.) because validate_service_id() blocks them
  • Users cannot view core service logs through the dashboard — the Terminal button opens but the fetch fails
  • Container name hardcoding (dream-{service_id}) fails for open-webui whose container is dream-webui

How

  • New endpoint: POST /v1/service/logs with {"service_id": "...", "tail": N} — validates format only via SERVICE_ID_RE, no core service restriction
  • Container resolution: docker ps --filter label=com.docker.compose.service={service_id} returns actual container name. Falls back to dream-{service_id} if lookup fails
  • Safety: Auth required, tail clamped to 1-500, output truncated to 50KB, read-only (no mutations)

Three Pillars Impact

  • Install Reliability: No installer code touched
  • Broad Compatibility: Standard Docker CLI commands, Compose V1+V2 label format identical
  • Extension Coherence: Existing /v1/extension/logs untouched — additive only

Modified Files

  • dream-server/bin/dream-host-agent.py_resolve_container_name(), _handle_service_logs(), route in do_POST

Testing

Automated

  • Python compile: PASS

Manual

  • curl -X POST -H "Auth..." -d '{"service_id":"open-webui","tail":50}' .../v1/service/logs — verify logs returned
  • Core service (llama-server) → 200 with logs (NOT 403)
  • Non-existent container → 200 + "Container is not running."
  • tail: 0 → treated as 1; tail: 999 → treated as 500
  • No auth → 401
  • Invalid service_id (../etc) → 400
  • Existing /v1/extension/logs still works for user extensions

Review

  • CG: ✅ APPROVED (clean — no warnings)
  • Compatibility: PASS (Docker label lookup, standard CLI)
  • Security: PASS (SERVICE_ID_RE prevents injection, auth enforced, read-only)

Platform Impact

  • macOS: Supported (host agent present)
  • Linux: Supported (host agent present)
  • Windows (WSL2): No host agent — dashboard-api fallback message (handled in follow-up PR)

Sequence

PR 4 of 7 (Phase 2 — shared infrastructure)

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>
Copy link
Copy Markdown
Collaborator

@Lightheartdevs Lightheartdevs left a comment

Choose a reason for hiding this comment

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

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() calls docker ps --filter label=... per request — not cached. Under load, many short-lived processes. Consider caching resolved names with a short TTL.
  • When result.returncode != 0 and 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.

Copy link
Copy Markdown
Collaborator

@Lightheartdevs Lightheartdevs left a comment

Choose a reason for hiding this comment

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

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]}"})
    return

Bug 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>
@yasinBursali
Copy link
Copy Markdown
Contributor Author

Addressing review feedback

All 3 issues fixed:

Security (Cross-container log reading) — Fixed:
Added --filter label=com.docker.compose.project=dream-server to _resolve_container_name(). Docker Compose stamps com.docker.compose.project=dream-server on all containers (from docker-compose.base.yml name: dream-server), so this scopes resolution to DreamServer containers only.

Bug 1 (Non-zero docker logs returns 200) — Fixed:
Added if result.returncode != 0: check after the "no such container" special case. Returns HTTP 500 with truncated stderr for Docker daemon errors, permission issues, etc.

Bug 2 (Missing broad exception handler) — Fixed:
Added except Exception as exc: after TimeoutExpired, matching the existing _handle_logs() pattern. Prevents unhandled exceptions from leaking Python tracebacks.

Copy link
Copy Markdown
Collaborator

@Lightheartdevs Lightheartdevs left a comment

Choose a reason for hiding this comment

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

Re-audit: APPROVE — all 3 fixes verified

  1. Cross-container fix: --filter label=com.docker.compose.project=dream-server added to _resolve_container_name()
  2. Non-zero exit handling: separate checks for "no such container" (200) vs other errors (500) ✅
  3. Broad exception handler added matching existing pattern ✅

CI all green. Depends on #804.

@Lightheartdevs
Copy link
Copy Markdown
Collaborator

Note: The Rust dashboard-api rewrite (#821) merged. This PR only modifies dream-host-agent.py which still exists — it should be unaffected. However, please rebase on latest main to confirm no conflicts.

@Lightheartdevs Lightheartdevs merged commit 502e711 into Light-Heart-Labs:main Apr 6, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants