Skip to content

feat(webui): Add file listing API; Map logs input dir into the webui service container in the Package Docker Compose project.#1688

Merged
junhaoliao merged 10 commits intoy-scope:mainfrom
junhaoliao:webui-ls-server
Dec 4, 2025
Merged

Conversation

@junhaoliao
Copy link
Member

@junhaoliao junhaoliao commented Nov 28, 2025

for integration, refer to #1292

Description

This PR introduces a new file listing API and map logs input dir into the webui service container in the Package Docker Compose project to allow listing log input files. Changes include:

  • Added FileEntrySchema and FileListingSchema in components/webui/common/src/schemas/os.ts for consistent typing of file system entries.
  • Implemented /ls endpoint in components/webui/server/src/routes/api/os/index.ts to list files and directories at a specified path with proper validation and error handling.
  • Updated docker-compose-all.yaml to mount the host logs input directory to the container for API access.

Checklist

  • The PR satisfies the contribution guidelines.
  • This is a breaking change and that has been indicated in the PR title, OR this isn't a
    breaking change.
  • Necessary docs have been updated, OR no docs need to be updated.

Validation performed

  1. Start the WebUI server with updated docker-compose-all.yaml mounting logs input directory.
  2. Verified /api/os/ls endpoint:
    • Returns correct file and directory listing for existing paths.
    • Returns 404 for non-existent paths.
    • Correctly marks directories and symlinks as expandable.
  3. Checked Fastify server logs for errors during file listing and compression operations; no unexpected errors observed.

Summary by CodeRabbit

  • New Features

    • Adds a file and directory browser API endpoint to list filesystem contents (accepts a path, defaults to root) and returns structured file entries with directory detection.
    • Publishes a reusable structured file-listing schema/type for the web UI.
  • Chores

    • Docker configuration updated to allow mounting a host logs directory into the web UI container (read-only, env-configurable).

✏️ Tip: You can customize this high-level summary in your review settings.

…service container in the Package Docker Compose project.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 28, 2025

Walkthrough

Adds TypeBox schemas for file-list requests and entries, a Fastify GET /ls endpoint that lists directory contents after filesystem checks, and a read-only host logs bind-mount for the webui service in Docker Compose.

Changes

Cohort / File(s) Summary
File system schema definitions
components/webui/common/src/schemas/os.ts
New TypeBox exports: FileListRequestSchema (query path with default "/"), FileEntrySchema (isExpandable, name, parentPath), FileListingSchema (array of file entries), and FileListing static type alias.
File listing API route
components/webui/server/src/routes/api/os/index.ts
New Fastify plugin (default export) registering GET /ls. Reads path from query, checks existence with fs.access, lists entries with fs.readdir(..., { withFileTypes: true }), maps Dirent to { isExpandable, name, parentPath }, returns 404 if path missing and 500 on read errors; response validated with FileListingSchema.
Docker container configuration
tools/deployment/package/docker-compose-all.yaml
Adds read-only bind-mount volume to webui service: ${CLP_LOGS_INPUT_DIR_HOST:-empty}:${CLP_LOGS_INPUT_DIR_CONTAINER:-/mnt/logs}:ro.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant WebUI_Server as WebUI Server (Fastify)
participant HostFS as Host Filesystem
Client->>WebUI_Server: GET /api/os/ls?path=/some/path
WebUI_Server->>HostFS: fs.access('/some/path') (existence/permissions)
alt path not accessible
HostFS-->>WebUI_Server: access error
WebUI_Server-->>Client: 404 Not Found
else accessible
WebUI_Server->>HostFS: fs.readdir('/some/path',{withFileTypes:true})
HostFS-->>WebUI_Server: Dirent[] results
WebUI_Server-->>WebUI_Server: Map Dirent -> {isExpandable, name, parentPath}
WebUI_Server-->>Client: 200 OK (FileListingSchema)
end
opt read error
HostFS-->>WebUI_Server: read error
WebUI_Server-->>Client: 500 Internal Server Error
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review path validation/sanitization to prevent directory traversal and enforce mount boundaries.
  • Verify HTTP status decisions for filesystem errors (ENOENT, EACCES) and logged error details.
  • Confirm response strictly matches FileListingSchema and TypeBox static typing.
  • Check symlink handling and performance for large directories.

Possibly related issues

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately summarizes both main changes: adding a file listing API and mapping logs into the webui container in Docker Compose.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@junhaoliao junhaoliao changed the title feat(webui): Add file listing API; map logs input dir into the webui … feat(webui): Add file listing API; map logs input dir into the webui service container in the Package Docker Compose project. Nov 28, 2025
@junhaoliao junhaoliao marked this pull request as ready for review November 28, 2025 19:09
@junhaoliao junhaoliao requested a review from a team as a code owner November 28, 2025 19:09
@junhaoliao junhaoliao requested a review from davemarco November 28, 2025 19:09
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2813e56 and 3a6b84e.

📒 Files selected for processing (3)
  • components/webui/common/src/schemas/os.ts (1 hunks)
  • components/webui/server/src/routes/api/os/index.ts (1 hunks)
  • tools/deployment/package/docker-compose-all.yaml (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}

⚙️ CodeRabbit configuration file

  • Prefer false == <expression> rather than !<expression>.

Files:

  • components/webui/server/src/routes/api/os/index.ts
  • components/webui/common/src/schemas/os.ts
🧠 Learnings (1)
📚 Learning: 2025-06-16T13:05:27.349Z
Learnt from: davemarco
Repo: y-scope/clp PR: 1015
File: components/log-viewer-webui/server/src/routes/static.ts:65-70
Timestamp: 2025-06-16T13:05:27.349Z
Learning: In components/log-viewer-webui/server/src/routes/static.ts, when decorateReply is set to true in fastifyStatic configuration, the reply.sendFile() method automatically uses the root directory configured in the static plugin registration, eliminating the need to pass the root directory as a second parameter.

Applied to files:

  • components/webui/server/src/routes/api/os/index.ts
🧬 Code graph analysis (2)
components/webui/server/src/routes/api/os/index.ts (1)
components/webui/common/src/schemas/os.ts (1)
  • FileListingSchema (27-27)
components/webui/common/src/schemas/os.ts (1)
components/webui/common/src/schemas/common.ts (1)
  • StringSchema (12-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: package-image
  • GitHub Check: lint-check (macos-15)
  • GitHub Check: lint-check (ubuntu-24.04)
🔇 Additional comments (2)
components/webui/common/src/schemas/os.ts (1)

1-29: LGTM!

The schema definitions are clean, well-documented, and follow TypeScript best practices. The type safety provided by these schemas will help ensure API consistency.

components/webui/server/src/routes/api/os/index.ts (1)

50-50: I apologize for the persistent repository access issues. Unfortunately, I'm unable to clone the repository to verify the Node.js version specification and examine the code context directly.

Based on the information available, here is the rewritten review comment:


Verify Node.js version supports dirent.parentPath before merging.

The parentPath property on fs.Dirent was added in Node.js v20.9.0. Confirm that the project's Node.js version (check Dockerfile, package.json engines field, or .nvmrc) is >= 20.9.0. If targeting earlier Node.js versions, construct the parent path manually using the requestedPath variable:

                    const isExpandable = dirent.isDirectory() || dirent.isSymbolicLink();
                    return {
                        isExpandable: isExpandable,
                        name: dirent.name,
-                       parentPath: dirent.parentPath,
+                       parentPath: requestedPath,
                    };

Comment on lines +8 to +12
const FileListRequestSchema = Type.Object({
path: Type.String({
default: "/",
}),
});
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Add path validation to prevent path traversal attacks.

The schema accepts any string path without validation, allowing potential path traversal attacks (e.g., ?path=../../../etc/passwd). The default root path "/" also grants access to the entire container filesystem.

Recommendations:

  1. Validate that the requested path is within the allowed directory (e.g., /mnt/logs)
  2. Sanitize the path to remove .. segments
  3. Consider using path.resolve() and checking if the resolved path starts with the allowed base path

Example validation:

import path from "path";

const ALLOWED_BASE_PATH = process.env.CLP_LOGS_INPUT_DIR_CONTAINER || "/mnt/logs";

const FileListRequestSchema = Type.Object({
    path: Type.String({
        default: ALLOWED_BASE_PATH,
    }),
});

// In the handler, add validation:
const resolvedPath = path.resolve(requestedPath);
if (!resolvedPath.startsWith(path.resolve(ALLOWED_BASE_PATH))) {
    return await reply.forbidden("Access denied: path outside allowed directory");
}
🤖 Prompt for AI Agents
In components/webui/server/src/routes/api/os/index.ts around lines 8-12, the
FileListRequestSchema accepts any string and uses "/" by default which allows
path traversal and access to the entire container; change the schema default to
an allowed base (e.g., process.env.CLP_LOGS_INPUT_DIR_CONTAINER || "/mnt/logs"),
import path, and in the route handler sanitize and resolve the requested path
(use path.normalize/path.resolve to collapse '..' segments) then verify the
resolved path starts with the resolved allowed base path; if the check fails
return a forbidden response. Ensure you do not change client-visible API shape
beyond the default and keep the allowed base configurable via env.

Copy link
Member Author

Choose a reason for hiding this comment

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

at the moment, no sensitive information is mapped into the container, and path filtering is not planned

Copy link
Contributor

Choose a reason for hiding this comment

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

@junhaoliao, understood! I acknowledge that the security model for this file listing API relies on container isolation with controlled volume mappings rather than explicit path validation within the application code. Since only specific directories are mounted into the container, the Docker isolation provides the security boundary.

Thank you for clarifying the architectural decision—I've already noted this for future reviews.


🧠 Learnings used
Learnt from: junhaoliao
Repo: y-scope/clp PR: 1688
File: components/webui/server/src/routes/api/os/index.ts:46-46
Timestamp: 2025-11-29T08:03:34.246Z
Learning: In the WebUI server (components/webui/server), the file listing API relies on container isolation for security rather than explicit path validation like ALLOWED_BASE_PATH. The service runs in a Docker container with controlled volume mappings, which provides the security boundary.

Learnt from: davemarco
Repo: y-scope/clp PR: 1015
File: components/log-viewer-webui/server/src/routes/static.ts:65-70
Timestamp: 2025-06-16T13:05:27.349Z
Learning: In components/log-viewer-webui/server/src/routes/static.ts, when decorateReply is set to true in fastifyStatic configuration, the reply.sendFile() method automatically uses the root directory configured in the static plugin registration, eliminating the need to pass the root directory as a second parameter.

Comment on lines +37 to +41
try {
await fs.access(requestedPath);
} catch {
return await reply.notFound(`Path not found: ${requestedPath}`);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Major: Verify the path is a directory, not just that it exists.

The current implementation uses fs.access() which only checks if the path exists and is accessible. A path could be a regular file, causing fs.readdir() to fail on line 44.

Apply this diff to add directory validation:

             try {
-                await fs.access(requestedPath);
+                const stats = await fs.stat(requestedPath);
+                if (!stats.isDirectory()) {
+                    return await reply.badRequest(`Path is not a directory: ${requestedPath}`);
+                }
             } catch {
                 return await reply.notFound(`Path not found: ${requestedPath}`);
             }
📝 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
try {
await fs.access(requestedPath);
} catch {
return await reply.notFound(`Path not found: ${requestedPath}`);
}
try {
const stats = await fs.stat(requestedPath);
if (false == stats.isDirectory()) {
return await reply.badRequest(`Path is not a directory: ${requestedPath}`);
}
} catch {
return await reply.notFound(`Path not found: ${requestedPath}`);
}
🤖 Prompt for AI Agents
In components/webui/server/src/routes/api/os/index.ts around lines 37 to 41, the
code currently uses fs.access() which only verifies existence; change it to stat
the path (using await fs.stat(requestedPath) or fs.lstat) and check
stat.isDirectory() before continuing, and if the stat call fails or
isDirectory() is false return reply.notFound(`Path not found: ${requestedPath}`)
(or an appropriate error); this ensures you only call fs.readdir() on actual
directories.

Copy link
Member Author

Choose a reason for hiding this comment

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

don't we also need to check for symlinks?

we only list "expandable" entries in the client; also, any failures erroneous user actions would be caught in the catch and handled, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

@junhaoliao Good questions! Let me address both:

Symlinks: No special handling needed. fs.stat() (as opposed to fs.lstat()) follows symlinks by default, so a symlink pointing to a directory will correctly pass the isDirectory() check. This means symlinks to directories will work as expected and can be listed.

Error handling: You're right that errors would be caught, but there's a subtle difference in user experience:

  • Current approach (fs.access()): If a user requests a regular file path, fs.readdir() will fail and the catch block returns a 500 (Internal Server Error) with a generic message. This makes it look like a server problem rather than a client mistake.

  • Suggested approach (fs.stat() + isDirectory()): Returns a 400 (Bad Request) with a clear message "Path is not a directory", which more accurately indicates it's a client error.

Both approaches work functionally, but the explicit directory check provides clearer feedback with appropriate HTTP status codes. That said, if you prefer to keep the current implementation since errors are handled, that's a valid choice—it's more of a UX improvement than a critical fix.


🧠 Learnings used
Learnt from: junhaoliao
Repo: y-scope/clp PR: 1688
File: components/webui/server/src/routes/api/os/index.ts:46-46
Timestamp: 2025-11-29T08:03:34.246Z
Learning: In the WebUI server (components/webui/server), the file listing API relies on container isolation for security rather than explicit path validation like ALLOWED_BASE_PATH. The service runs in a Docker container with controlled volume mappings, which provides the security boundary.

Learnt from: davemarco
Repo: y-scope/clp PR: 1015
File: components/log-viewer-webui/server/src/routes/static.ts:65-70
Timestamp: 2025-06-16T13:05:27.349Z
Learning: In components/log-viewer-webui/server/src/routes/static.ts, when decorateReply is set to true in fastifyStatic configuration, the reply.sendFile() method automatically uses the root directory configured in the static plugin registration, eliminating the need to pass the root directory as a second parameter.

Comment on lines +43 to +52
try {
const direntList = await fs.readdir(requestedPath, {withFileTypes: true});
return direntList.map((dirent) => {
const isExpandable = dirent.isDirectory() || dirent.isSymbolicLink();
return {
isExpandable: isExpandable,
name: dirent.name,
parentPath: dirent.parentPath,
};
});
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Major: Add pagination or entry limit to prevent DoS with large directories.

Reading all directory entries without limits could cause memory exhaustion or timeouts when listing directories with thousands or millions of files.

Consider adding:

  1. A maximum entry limit in the request schema
  2. Pagination support with offset/limit parameters
  3. Streaming response for very large directories

Example with limit:

const FileListRequestSchema = Type.Object({
    path: Type.String({
        default: "/",
    }),
    limit: Type.Optional(Type.Number({
        minimum: 1,
        maximum: 1000,
        default: 100,
    })),
});

// In handler:
const {path: requestedPath, limit = 100} = request.query;
const direntList = await fs.readdir(requestedPath, {withFileTypes: true});
return direntList.slice(0, limit).map((dirent) => {
    // ... mapping logic
});
🤖 Prompt for AI Agents
In components/webui/server/src/routes/api/os/index.ts around lines 43 to 52, the
handler reads all directory entries which can cause memory/time DoS for very
large directories — add a safe entry limit and optional pagination: extend the
request schema to accept limit (number, min 1, max 1000, default 100) and
optional offset/page parameters, read the full dirents only if necessary or use
a streaming/iterative read if available, then apply slice(offset, offset +
limit) before mapping and returning results; validate and coerce query values,
and ensure the default limit is applied when none provided.

Copy link
Member Author

Choose a reason for hiding this comment

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

There might be data inconsistency if pagination isn't properly implemented. That you are using numerical offsets as cursors would cause missing / repeated listed entries when files are removed / added in between requests. A more proper implementation would be to sort the paths alphabetically and use the last listed path as cursors, though we still need to properly handle added / removed files.

i think it's better to defer this

Copy link
Contributor

Choose a reason for hiding this comment

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

@junhaoliao You're absolutely right about the cursor-based pagination complexity—using alphabetically sorted paths as cursors is indeed more robust than numerical offsets when dealing with filesystem changes between requests.

I understand the decision to defer this feature. Would you like me to create a follow-up issue to track this work for a future PR? I can document the pagination requirements and the cursor-based approach you mentioned.


🧠 Learnings used
Learnt from: junhaoliao
Repo: y-scope/clp PR: 1688
File: components/webui/server/src/routes/api/os/index.ts:46-46
Timestamp: 2025-11-29T08:03:34.246Z
Learning: In the WebUI server (components/webui/server), the file listing API relies on container isolation for security rather than explicit path validation like ALLOWED_BASE_PATH. The service runs in a Docker container with controlled volume mappings, which provides the security boundary.

Learnt from: davemarco
Repo: y-scope/clp PR: 1015
File: components/log-viewer-webui/server/src/routes/static.ts:65-70
Timestamp: 2025-06-16T13:05:27.349Z
Learning: In components/log-viewer-webui/server/src/routes/static.ts, when decorateReply is set to true in fastifyStatic configuration, the reply.sendFile() method automatically uses the root directory configured in the static plugin registration, eliminating the need to pass the root directory as a second parameter.

try {
const direntList = await fs.readdir(requestedPath, {withFileTypes: true});
return direntList.map((dirent) => {
const isExpandable = dirent.isDirectory() || dirent.isSymbolicLink();
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: Symbolic link resolution could lead to security issues.

Marking symbolic links as expandable without resolving them first could expose paths outside the allowed directory if symlinks point to sensitive locations.

Consider either:

  1. Resolving symlinks and checking if they point within the allowed directory
  2. Not marking symlinks as expandable
  3. Adding a flag to control symlink behavior

Example resolution check:

let isExpandable = dirent.isDirectory();
if (dirent.isSymbolicLink()) {
    try {
        const symlinkPath = path.join(requestedPath, dirent.name);
        const realPath = await fs.realpath(symlinkPath);
        const resolvedStats = await fs.stat(realPath);
        isExpandable = resolvedStats.isDirectory() && 
                      realPath.startsWith(ALLOWED_BASE_PATH);
    } catch {
        isExpandable = false;
    }
}
🤖 Prompt for AI Agents
In components/webui/server/src/routes/api/os/index.ts around line 46, currently
symlinks are treated as expandable which can expose paths outside the allowed
directory; update the logic to resolve symbolic links before marking expandable:
when dirent.isSymbolicLink() use fs.realpath on the joined entry path, stat the
resolved path, set isExpandable only if the resolved path is a directory and its
realPath startsWith the ALLOWED_BASE_PATH (catching errors and treating as
non-expandable); alternatively, if you prefer simpler behavior, do not mark
symlinks as expandable or add a configuration flag (e.g., allowSymlinks) to
explicitly control this behavior and ensure all branches fall back to
non-expandable on any error.

Copy link
Member Author

Choose a reason for hiding this comment

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

The service runs in a container, so data on the host is relatively safe unless fully mapped. There is no planned support for ALLOWED_BASE_PATH yet.

Copy link
Contributor

Choose a reason for hiding this comment

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

@junhaoliao, understood! That makes sense—the container isolation does provide a security boundary, and as long as the volume mappings are carefully controlled, the symlink behavior should be manageable within that context.

If you do expand the filesystem exposure in future updates, it may be worth revisiting symlink handling, but for the current scope this approach is reasonable.


✏️ Learnings added
Learnt from: junhaoliao
Repo: y-scope/clp PR: 1688
File: components/webui/server/src/routes/api/os/index.ts:46-46
Timestamp: 2025-11-29T08:03:34.246Z
Learning: In the WebUI server (components/webui/server), the file listing API relies on container isolation for security rather than explicit path validation like ALLOWED_BASE_PATH. The service runs in a Docker container with controlled volume mappings, which provides the security boundary.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: davemarco
Repo: y-scope/clp PR: 1015
File: components/log-viewer-webui/server/src/routes/static.ts:65-70
Timestamp: 2025-06-16T13:05:27.349Z
Learning: In components/log-viewer-webui/server/src/routes/static.ts, when decorateReply is set to true in fastifyStatic configuration, the reply.sendFile() method automatically uses the root directory configured in the static plugin registration, eliminating the need to pass the root directory as a second parameter.

@junhaoliao junhaoliao changed the title feat(webui): Add file listing API; map logs input dir into the webui service container in the Package Docker Compose project. feat(webui): Add file listing API. Nov 28, 2025
@junhaoliao junhaoliao changed the title feat(webui): Add file listing API. feat(webui): Add file listing API; map logs input dir into the webui service container in the Package Docker Compose project. Nov 28, 2025
Copy link
Member Author

Choose a reason for hiding this comment

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

all "${CLP_LOGS_INPUT_DIR_HOST:-empty}:${CLP_LOGS_INPUT_DIR_CONTAINER:-/mnt/logs}" should be made readonly in this file

@coderabbitai file a bug report

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all occurrences of the CLP_LOGS_INPUT_DIR mount pattern
echo "=== Finding all occurrences of CLP_LOGS_INPUT_DIR mount pattern ==="
rg -n 'CLP_LOGS_INPUT_DIR_HOST.*CLP_LOGS_INPUT_DIR_CONTAINER' tools/deployment/package/docker-compose-all.yaml

Length of output: 340


🏁 Script executed:

#!/bin/bash
# Create a GitHub issue for making the logs input directory mounts readonly
gh issue create \
  --repo y-scope/clp \
  --title "Make CLP_LOGS_INPUT_DIR mounts readonly in docker-compose-all.yaml" \
  --body "## Description

All mount points for \`CLP_LOGS_INPUT_DIR_HOST\` in \`tools/deployment/package/docker-compose-all.yaml\` should be made readonly by adding the \`:ro\` suffix.

## Affected Services

Currently, the following services mount the logs input directory without the readonly flag:
- \`compression-scheduler\`
- \`compression-worker\`
- \`webui\`

## Expected Behavior

The mount should be:
\`\`\`yaml
\"\${CLP_LOGS_INPUT_DIR_HOST:-empty}:\${CLP_LOGS_INPUT_DIR_CONTAINER:-/mnt/logs}:ro\"
\`\`\`

## Context

This issue was identified in PR #1688: https://github.com/y-scope/clp/pull/1688#discussion_r2572350167

Requested by: @junhaoliao" \
  --assignee junhaoliao \
  --label bug

Length of output: 42


@junhaoliao, I've filed a bug report for this issue!

Issue created: #1689

I found 3 occurrences of the CLP_LOGS_INPUT_DIR mount pattern in docker-compose-all.yaml that need to be made readonly:

  1. Line 237 - compression-scheduler service
  2. Line 271 - compression-worker service
  3. Line 308 - webui service

All of these should have the :ro suffix added to ensure the logs input directory is mounted as readonly, preventing any accidental writes to the input logs.


🧠 Learnings used
Learnt from: junhaoliao
Repo: y-scope/clp PR: 1178
File: components/clp-py-utils/clp_py_utils/clp_config.py:47-47
Timestamp: 2025-10-07T07:54:32.427Z
Learning: In components/clp-py-utils/clp_py_utils/clp_config.py, the CONTAINER_AWS_CONFIG_DIRECTORY constant is intentionally set to pathlib.Path("/") / ".aws" (i.e., `/.aws`) rather than a user-specific home directory. This hardcoded path is part of the container orchestration design.

Learnt from: junhaoliao
Repo: y-scope/clp PR: 1152
File: components/clp-package-utils/clp_package_utils/general.py:0-0
Timestamp: 2025-08-19T14:41:28.901Z
Learning: In the CLP codebase, prefer explicit failures over automatic directory creation in utility functions like dump_config. The user junhaoliao prefers to let file operations fail when parent directories don't exist, as this helps catch implementation errors during development rather than masking setup issues with automatic directory creation.

Learnt from: jackluo923
Repo: y-scope/clp PR: 1054
File: components/core/tools/scripts/lib_install/musllinux_1_2/install-packages-from-source.sh:6-8
Timestamp: 2025-07-01T14:51:19.172Z
Learning: In CLP installation scripts within `components/core/tools/scripts/lib_install/`, maintain consistency with existing variable declaration patterns across platforms rather than adding individual improvements like `readonly` declarations.

Learnt from: jackluo923
Repo: y-scope/clp PR: 1054
File: components/core/tools/scripts/lib_install/musllinux_1_2/install-packages-from-source.sh:6-8
Timestamp: 2025-07-01T14:51:19.172Z
Learning: In CLP installation scripts within `components/core/tools/scripts/lib_install/`, maintain consistency with existing variable declaration patterns across platforms rather than adding individual improvements like `readonly` declarations.

Learnt from: junhaoliao
Repo: y-scope/clp PR: 1414
File: tools/docker-images/clp-package/Dockerfile:20-24
Timestamp: 2025-10-13T03:32:19.293Z
Learning: In the clp repository's Dockerfiles (e.g., tools/docker-images/clp-package/Dockerfile), ENV directives should be split into separate lines for readability rather than consolidated to reduce layer count. This is especially true for PATH modifications, as agreed upon in PR #1166. Later ENV settings may depend on earlier ones (e.g., referencing CLP_HOME).

Copy link
Contributor

@davemarco davemarco left a comment

Choose a reason for hiding this comment

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

see comments

"/ls",
{
schema: {
querystring: FileListRequestSchema,
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldnt should just be body instead of querystring?

Copy link
Member Author

@junhaoliao junhaoliao Dec 2, 2025

Choose a reason for hiding this comment

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

the requests look like

GET http://localhost:4000/os/ls?path=/mnt/logs/home/junhao/samples

so querystring should be a better schema field we want to set?

Copy link
Contributor

Choose a reason for hiding this comment

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

ignore i didnt realize was get

schema: {
querystring: FileListRequestSchema,
response: {
[constants.HTTP_STATUS_OK]: FileListingSchema,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we include the tag like the other routes

},
},
async (request, reply) => {
const {path: requestedPath} = request.query;
Copy link
Contributor

Choose a reason for hiding this comment

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

once you add the body, i think u can get the path easier from the body field

Copy link
Member Author

Choose a reason for hiding this comment

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

ah i see what you mean. the request is a GET request, and it's unconventional for GET requests to include a body (technically we can, but most client libraries / JS don't handle GET requests with a body well

Copy link
Contributor

Choose a reason for hiding this comment

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

ah i see, sorry i didnt notice was get since most of the ones i did were post. ignore this comment

import {constants} from "http2";


const FileListRequestSchema = Type.Object({
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you should put this in the schemas folder.

@junhaoliao junhaoliao requested a review from davemarco December 2, 2025 19:45
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
components/webui/server/src/routes/api/os/index.ts (1)

33-41: Consider validating that requestedPath is a directory before listing

Right now you only check accessibility with fs.access. If a client (or a race) points requestedPath at a regular file, fs.readdir will throw (e.g., ENOTDIR), and the handler responds with a 500 “Unable to list files”, which looks like a server fault rather than a client/path issue.

A small fs.stat check lets you return a clearer 4xx response for “not a directory” while keeping the existing 404 behaviour for missing paths.

Example:

-            try {
-                await fs.access(requestedPath);
-            } catch {
-                return await reply.notFound(`Path not found: ${requestedPath}`);
-            }
+            try {
+                const stats = await fs.stat(requestedPath);
+                if (false == stats.isDirectory()) {
+                    return await reply.badRequest(`Path is not a directory: ${requestedPath}`);
+                }
+            } catch {
+                return await reply.notFound(`Path not found: ${requestedPath}`);
+            }

If you want to re-confirm the fs.stat().isDirectory() behaviour and ENOTDIR conditions:

In Node.js, how does `fs.readdir` behave when given a path to a regular file, and is `fs.stat(...).isDirectory()` the idiomatic way to check for directories before calling `fs.readdir`?
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f910f7 and f721186.

📒 Files selected for processing (1)
  • components/webui/server/src/routes/api/os/index.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}

⚙️ CodeRabbit configuration file

  • Prefer false == <expression> rather than !<expression>.

Files:

  • components/webui/server/src/routes/api/os/index.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: junhaoliao
Repo: y-scope/clp PR: 1688
File: components/webui/server/src/routes/api/os/index.ts:46-46
Timestamp: 2025-11-29T08:03:34.272Z
Learning: In the WebUI server (components/webui/server), the file listing API relies on container isolation for security rather than explicit path validation like ALLOWED_BASE_PATH. The service runs in a Docker container with controlled volume mappings, which provides the security boundary.
📚 Learning: 2025-06-16T13:05:27.349Z
Learnt from: davemarco
Repo: y-scope/clp PR: 1015
File: components/log-viewer-webui/server/src/routes/static.ts:65-70
Timestamp: 2025-06-16T13:05:27.349Z
Learning: In components/log-viewer-webui/server/src/routes/static.ts, when decorateReply is set to true in fastifyStatic configuration, the reply.sendFile() method automatically uses the root directory configured in the static plugin registration, eliminating the need to pass the root directory as a second parameter.

Applied to files:

  • components/webui/server/src/routes/api/os/index.ts
📚 Learning: 2025-11-29T08:03:34.272Z
Learnt from: junhaoliao
Repo: y-scope/clp PR: 1688
File: components/webui/server/src/routes/api/os/index.ts:46-46
Timestamp: 2025-11-29T08:03:34.272Z
Learning: In the WebUI server (components/webui/server), the file listing API relies on container isolation for security rather than explicit path validation like ALLOWED_BASE_PATH. The service runs in a Docker container with controlled volume mappings, which provides the security boundary.

Applied to files:

  • components/webui/server/src/routes/api/os/index.ts
📚 Learning: 2025-07-29T21:00:07.757Z
Learnt from: junhaoliao
Repo: y-scope/clp PR: 1136
File: components/webui/client/src/pages/IngestPage/Details/sql.ts:1-1
Timestamp: 2025-07-29T21:00:07.757Z
Learning: User junhaoliao requested creating a GitHub issue to track server-side SQL query error handling improvements, specifically to prevent uncaught query errors from causing 500 errors to reach the client.

Applied to files:

  • components/webui/server/src/routes/api/os/index.ts
🧬 Code graph analysis (1)
components/webui/server/src/routes/api/os/index.ts (1)
components/webui/common/src/schemas/os.ts (2)
  • FileListRequestSchema (34-34)
  • FileListingSchema (33-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: package-image
  • GitHub Check: lint-check (ubuntu-24.04)
  • GitHub Check: lint-check (macos-15)
  • GitHub Check: build (macos-15)
  • GitHub Check: build (ubuntu-24.04)

Comment on lines +41 to +47
return direntList.map((dirent) => {
const isExpandable = dirent.isDirectory() || dirent.isSymbolicLink();
return {
isExpandable: isExpandable,
name: dirent.name,
parentPath: dirent.parentPath,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Node.js fs.Dirent properties withFileTypes readdir API

💡 Result:

Summary — fs.Dirent and readdir(withFileTypes)

  • fs.readdir(path, { withFileTypes: true }, ...) returns an array of fs.Dirent objects (instead of name strings). Use the synchronous or async variants the same way. [1][4]

  • fs.Dirent exposes:

    • property: name (file name). [1]
    • methods that return boolean: isFile(), isDirectory(), isSymbolicLink(), isBlockDevice(), isCharacterDevice(), isFIFO(), isSocket(). Use these to test the entry type without additional stat calls. [1][2]
  • Implementation note: Node uses an internal Symbol-backed field to store the libuv dirent type (UV_DIRENT_*); that internal property is not part of the public API — rely on the documented isX() methods. [3]

Minimal example (async):
const fs = require('fs/promises');
const entries = await fs.readdir('./dir', { withFileTypes: true });
for (const d of entries) {
console.log(d.name, d.isDirectory() ? 'dir' : d.isFile() ? 'file' : 'other');
}

References
[1] Node.js fs (Dirent + readdir docs).
[2] Node.js fs.Dirent docs (method list).
[3] Explanation of internal Symbol("type") / libuv dirent types.
[4] fs.readdir(withFileTypes) usage description.


🏁 Script executed:

cat -n components/webui/server/src/routes/api/os/index.ts

Repository: y-scope/clp

Length of output: 166


🏁 Script executed:

git ls-files components/webui/server/src/routes/api/os/index.ts

Repository: y-scope/clp

Length of output: 166


🏁 Script executed:

git show HEAD:components/webui/server/src/routes/api/os/index.ts 2>/dev/null | head -80

Repository: y-scope/clp

Length of output: 166


Fix invalid parentPath source when mapping directory entries

fs.readdir(..., {withFileTypes: true}) returns Dirent objects with only a name property and type-check methods (isDirectory(), isSymbolicLink(), etc.). The parentPath property does not exist on Dirent and will cause a TypeScript compilation error (property does not exist on type 'Dirent').

Use requestedPath as the parent directory instead:

                 return direntList.map((dirent) => {
                     const isExpandable = dirent.isDirectory() || dirent.isSymbolicLink();
                     return {
                         isExpandable: isExpandable,
                         name: dirent.name,
-                        parentPath: dirent.parentPath,
+                        parentPath: requestedPath,
                     };
                 });
📝 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
return direntList.map((dirent) => {
const isExpandable = dirent.isDirectory() || dirent.isSymbolicLink();
return {
isExpandable: isExpandable,
name: dirent.name,
parentPath: dirent.parentPath,
};
return direntList.map((dirent) => {
const isExpandable = dirent.isDirectory() || dirent.isSymbolicLink();
return {
isExpandable: isExpandable,
name: dirent.name,
parentPath: requestedPath,
};
🤖 Prompt for AI Agents
In components/webui/server/src/routes/api/os/index.ts around lines 41 to 47, the
mapping uses dirent.parentPath which does not exist on fs.Dirent and causes a
TypeScript error; replace the non-existent property with the requestedPath (the
directory that was read) when constructing the returned object and keep the
isExpandable and name properties as-is so the parentPath points to the actual
directory requested.

Copy link
Contributor

@davemarco davemarco left a comment

Choose a reason for hiding this comment

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

lgtm but maybe wait till after release to merge just in case

@junhaoliao junhaoliao changed the title feat(webui): Add file listing API; map logs input dir into the webui service container in the Package Docker Compose project. feat(webui): Add file listing API; Map logs input dir into the webui service container in the Package Docker Compose project. Dec 4, 2025
@junhaoliao junhaoliao merged commit 5d27124 into y-scope:main Dec 4, 2025
20 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