Skip to content

test: integration tests for FileExecutor allowed_paths using Docker #2117

@bug-ops

Description

@bug-ops

Summary

Add Docker-based integration tests that verify FileExecutor file system access controls: allowed paths inside cwd, denied paths outside cwd, ~ path resolution, symlink boundaries, and path traversal attempts. Currently these behaviors are only testable in live agent sessions — CI has no automated coverage.

Relates to: #2115 (~ in allowed_paths silently blocks all file access)

Motivation

FileExecutor enforces allowed_paths config at runtime, but:

Proposed Solution

Use the testcontainers crate (already in the project for Qdrant) to spin up a containerized environment per test, then run FileExecutor (or a minimal zeph agent) against it.

Docker image

Build a test-only image zeph-test-fs that:

  • Contains a pre-baked directory structure:
    /workspace/          ← simulated cwd (allowed)
      project/
        src/main.rs
        Cargo.toml
      README.md
    /outside/            ← outside cwd (must be denied by default)
      secret.txt
    /tmp/allowed/        ← explicit extra allowed_path
      data.json
    /etc/passwd          ← must always be denied (path traversal target)
    
  • Has a non-root user zeph with limited permissions
  • Shares the zeph binary or library under test via volume mount or multi-stage build

The image should be built in CI as part of the existing Docker build pipeline and pushed to the project registry for reuse.

Test structure

New crate or test file: tests/integration/file_access.rs (behind #[cfg(feature = "integration")] or #[ignore]).

use testcontainers::{clients::Cli, images::generic::GenericImage};

/// FileExecutor allows reads within cwd.
#[tokio::test]
#[ignore = "requires Docker"]
async fn file_executor_allows_read_inside_cwd() { ... }

/// FileExecutor denies reads outside cwd when allowed_paths is empty.
#[tokio::test]
#[ignore = "requires Docker"]
async fn file_executor_denies_read_outside_cwd() { ... }

/// FileExecutor resolves ~ in allowed_paths correctly (regression for #2115).
#[tokio::test]
#[ignore = "requires Docker"]
async fn file_executor_tilde_in_allowed_paths_resolves() { ... }

/// FileExecutor blocks path traversal attempts (../../etc/passwd).
#[tokio::test]
#[ignore = "requires Docker"]
async fn file_executor_blocks_path_traversal() { ... }

/// FileExecutor does not follow symlinks outside allowed boundary.
#[tokio::test]
#[ignore = "requires Docker"]
async fn file_executor_symlink_escape_blocked() { ... }

/// FileExecutor allows an explicitly configured extra allowed_path outside cwd.
#[tokio::test]
#[ignore = "requires Docker"]
async fn file_executor_explicit_allowed_path_outside_cwd() { ... }

CI integration

Add a new GitHub Actions job integration-fs in .github/workflows/:

integration-fs:
  runs-on: ubuntu-latest
  needs: [build]          # reuse the already-built Docker image
  services:
    docker:
      image: docker:dind
  steps:
    - uses: actions/checkout@v4
    - name: Pull test image
      run: docker pull ghcr.io/bug-ops/zeph-test-fs:${{ github.sha }}
    - name: Run FS integration tests
      run: cargo nextest run --config-file .github/nextest.toml
             --workspace --features integration --lib --bins
             -E 'test(file_executor)'
      env:
        ZEPH_TEST_FS_IMAGE: ghcr.io/bug-ops/zeph-test-fs:${{ github.sha }}

The image tag is pinned to the commit SHA so tests always run against the matching binary.

Implementation Plan

Phase 1 — Docker image + fixture FS (1 PR)

  • Add docker/test-fs/Dockerfile with the fixture directory structure
  • Add build step to CI: build and push zeph-test-fs image
  • Add ZEPH_TEST_FS_IMAGE env var support to test harness

Phase 2 — Test harness + basic tests (1 PR)

  • Add tests/integration/file_access.rs
  • Implement ZephFsTestContainer helper (wraps testcontainers::GenericImage)
  • Write tests: allow-inside-cwd, deny-outside-cwd, explicit-allowed-path
  • Add integration feature flag to workspace

Phase 3 — Security tests (1 PR)

Acceptance Criteria

  • cargo nextest run --features integration -E 'test(file_executor)' passes in CI
  • Tilde expansion in allowed_paths is covered by a dedicated test (regression for fix(tools): FileExecutor does not expand ~ in allowed_paths, blocking all file access #2115)
  • Path traversal (../../etc/passwd) is denied and covered by a test
  • Symlink escape is denied and covered by a test
  • Tests are marked #[ignore] locally but run automatically in the integration-fs CI job
  • Docker image build is part of the existing CI pipeline (no extra build time on PRs that don't touch tools)
  • Test output is human-readable: clearly states which path was tested, what was expected, what happened

Notes

  • testcontainers is already in the dependency tree for Qdrant tests — reuse that pattern
  • Tests should clean up containers on panic (testcontainers handles this automatically)
  • Consider docker compose for multi-container scenarios (e.g., agent + mock LLM + FS container)
  • Long-term: the same Docker infrastructure can be extended for ShellExecutor sandbox tests

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions