Skip to content

fix(detect): restore secret + legacy-phrase coverage; strengthen HARD… #23

fix(detect): restore secret + legacy-phrase coverage; strengthen HARD…

fix(detect): restore secret + legacy-phrase coverage; strengthen HARD… #23

name: Sandbox Integration Tests
# MCP-34.5 / MCP-3236: Prove sandbox isolation works on Linux (Landlock LSM).
# ubuntu-latest == Ubuntu 24.04, kernel 6.8 — Landlock ABI ≥ 3 available.
# These tests are also covered by unit-tests.yml; this job surfaces them
# explicitly and adds the server-startup probe so CI shows dedicated evidence.
on:
push:
branches: ["*"]
paths:
- "internal/sandbox/**"
- "internal/upstream/core/sandbox*.go"
- "internal/security/scanner/**"
- "internal/upstream/core/**"
- ".github/workflows/sandbox-integration.yml"
pull_request:
branches: ["*"]
paths:
- "internal/sandbox/**"
- "internal/upstream/core/sandbox*.go"
- "internal/security/scanner/**"
- "internal/upstream/core/**"
- ".github/workflows/sandbox-integration.yml"
workflow_dispatch:
jobs:
sandbox-integration:
name: Sandbox Integration (Linux / Landlock)
runs-on: ubuntu-latest
env:
GO111MODULE: "on"
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.25"
cache: true
- name: Download dependencies
run: go mod download
# Confirm the kernel supports Landlock before running enforcement tests.
- name: Check Landlock availability
run: |
uname -r
if grep -qi landlock /proc/kallsyms 2>/dev/null || \
cat /proc/sys/kernel/landlock/abi 2>/dev/null | grep -q "[1-9]"; then
echo "Landlock available"
else
# ubuntu 24.04 exposes ABI via a prctl probe — let the Go test skip logic handle it
echo "Landlock probe inconclusive — Go tests will auto-skip if unavailable"
fi
# 1. sandbox package: Landlock enforcement (TestLandlockEnforcesFilesystemAllowlist),
# wrap/encode round-trip, rlimit constants.
- name: Run sandbox package tests
run: go test -v -race ./internal/sandbox/...
# 2. upstream/core: wrapWithSandbox full re-exec integration
# (TestSandboxWrapper_EndToEnd, TestSandboxWrapper_FailClosed, spec builders).
- name: Run upstream/core sandbox tests
run: go test -v -race -run "Sandbox|sandbox|buildSandbox" ./internal/upstream/core/...
# 3. scanner/engine: degradation under sandbox/none isolation mode
# (TestEngineResolveScannersSkipsDockerUnderSandbox, TestEngineEffectiveIsolationMode).
- name: Run scanner isolation-mode tests
run: go test -v -race -run "Sandbox|sandbox|IsolationMode|isolation" ./internal/security/scanner/...
# 4. Full sandbox + scanner test set with race detector.
- name: Run all sandbox-related tests (race)
run: |
go test -race \
./internal/sandbox/... \
./internal/upstream/core/... \
./internal/security/scanner/...
# 5. Build the binary (proves sandbox code compiles on linux/amd64).
- name: Build mcpproxy binary
run: go build -v -o mcpproxy ./cmd/mcpproxy
# 6. Server startup probe: start mcpproxy with isolation.mode=sandbox,
# verify it starts healthy, check the upstream list (no stdio servers
# configured so no wrapWithSandbox is called — this proves the binary
# starts cleanly under this config, not sandbox enforcement itself).
- name: Start mcpproxy with isolation.mode=sandbox (startup probe)
run: |
mkdir -p /tmp/mcp3236-ci
cat > /tmp/mcp3236-ci/mcp_config.json <<'EOF'
{
"listen": "127.0.0.1:19237",
"api_key": "qa-sandbox-ci-test",
"enable_web_ui": false,
"docker_isolation": { "mode": "sandbox" },
"mcpServers": []
}
EOF
MCPPROXY_DATA_DIR=/tmp/mcp3236-ci ./mcpproxy serve \
--config /tmp/mcp3236-ci/mcp_config.json \
--log-level=debug \
> /tmp/mcp3236-ci/server.log 2>&1 &
SERVER_PID=$!
echo "SERVER_PID=$SERVER_PID" >> "$GITHUB_ENV"
# Wait for server to be ready
for i in $(seq 1 20); do
if curl -sf -H "X-API-Key: qa-sandbox-ci-test" \
http://127.0.0.1:19237/api/v1/status > /dev/null 2>&1; then
echo "Server ready after ${i}s"
break
fi
sleep 1
done
- name: Verify server health under sandbox config
run: |
# Use the dedicated readiness endpoint (/readyz returns 200 once the
# server has completed startup) — structure-independent, unlike parsing
# the /api/v1/status JSON. The server serves HTTP before it's ready, so
# poll up to 30s.
READY=0
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:19237/readyz > /dev/null 2>&1; then
READY=1; echo "Server ready (/readyz 200) after ${i}s"; break
fi
sleep 1
done
echo "--- /readyz body ---"; curl -s http://127.0.0.1:19237/readyz || true; echo
echo "--- /api/v1/status ---"
curl -sf -H "X-API-Key: qa-sandbox-ci-test" http://127.0.0.1:19237/api/v1/status | python3 -m json.tool || true
if [ "$READY" != "1" ]; then
echo "ERROR: /readyz did not return 200 within 30s"
cat /tmp/mcp3236-ci/server.log
exit 1
fi
# Prove the server actually resolved SANDBOX mode (the global key is
# docker_isolation.mode — a wrong key silently falls back to "none",
# which would make this probe vacuous).
if ! grep -i "isolation_mode" /tmp/mcp3236-ci/server.log | grep -qi "sandbox"; then
echo "ERROR: server did not start in sandbox mode (expected isolation_mode=sandbox)"
grep -i "isolation_mode" /tmp/mcp3236-ci/server.log || echo "(no isolation_mode log line found)"
exit 1
fi
echo "Server healthy (/readyz) and confirmed isolation_mode=sandbox"
- name: macOS/non-Linux graceful-degrade probe (build check)
run: |
# Cross-compile for darwin to prove the no-op path compiles cleanly.
GOOS=darwin GOARCH=arm64 go build -o /dev/null ./internal/sandbox/... 2>&1 || true
GOOS=darwin GOARCH=arm64 go build -o /dev/null ./internal/upstream/core/ 2>&1 || true
echo "Cross-compile probe done (darwin build tags: sandbox_other.go path)"
- name: Stop server
if: always()
run: |
if [ -n "$SERVER_PID" ]; then kill "$SERVER_PID" 2>/dev/null || true; fi
cat /tmp/mcp3236-ci/server.log 2>/dev/null || true
- name: Upload server log
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sandbox-server-log
path: /tmp/mcp3236-ci/server.log
retention-days: 7