Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ As an alternative, it's also on [homebrew](https://brew.sh/):
$ brew install cagent
```

### Linux Quick Install

For Linux users, the easiest way to install is with our installation script:

```sh
curl -fsSL https://raw.githubusercontent.com/docker/cagent/main/scripts/install-linux.sh | bash
```

This script will:
- Automatically detect your architecture (amd64/arm64)
- Download the latest release
- Install to `/usr/local/bin` (or `~/.local/bin` if sudo is not available)
- Verify the installation

### Using binary releases

Finally, [Prebuilt binaries](https://github.com/docker/cagent/releases) for Windows,
Expand Down
199 changes: 199 additions & 0 deletions scripts/install-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
set -e

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Script info
REPO="docker/cagent"
BINARY_NAME="cagent"
INSTALL_DIR="/usr/local/bin"

# Print colored message
print_msg() {
local color=$1
shift
echo -e "${color}$@${NC}" >&2
Copy link
Contributor

Choose a reason for hiding this comment

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

Unquoted $@ could cause pathname expansion and word splitting

The line echo -e "${color}$@${NC}" has unquoted $@ which will cause:

  • Pathname expansion if messages contain glob characters (*, ?, [])
  • Multiple spaces collapsed to single spaces

For example: print_msg "$RED" "Error: File *.txt not found" could expand *.txt to actual filenames.

Recommendation: Use "$*" instead to properly join all arguments:

echo -e "${color}$*${NC}" >&2

This preserves the intended message without pathname expansion.

}

# Detect architecture
detect_arch() {
local arch
arch=$(uname -m)

case "$arch" in
x86_64|amd64)
echo "amd64"
;;
aarch64|arm64)
echo "arm64"
;;
*)
print_msg "$RED" "Error: Unsupported architecture: $arch"
print_msg "$YELLOW" "Supported architectures: x86_64 (amd64), aarch64 (arm64)"
exit 1
;;
esac
}

# Detect OS
detect_os() {
local os
os=$(uname -s)

case "$os" in
Linux)
echo "linux"
;;
Darwin)
print_msg "$RED" "Error: This script is for Linux only."
echo "" >&2
print_msg "$YELLOW" "For macOS, you have two options:"
print_msg "$YELLOW" " 1. Download Docker Desktop: https://www.docker.com/products/docker-desktop"
print_msg "$YELLOW" " (cagent is included starting with version 4.49.0)"
print_msg "$YELLOW" " 2. Use Homebrew: brew install cagent"
exit 1
;;
MINGW*|MSYS*|CYGWIN*)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: harmless, i don't think many windows users will try to run a bash script called install-linux

print_msg "$RED" "Error: This script is for Linux only."
echo "" >&2
print_msg "$YELLOW" "For Windows, download Docker Desktop: https://www.docker.com/products/docker-desktop"
print_msg "$YELLOW" "(cagent is included starting with version 4.49.0)"
exit 1
;;
*)
print_msg "$RED" "Error: Unsupported operating system: $os"
print_msg "$YELLOW" "This script is for Linux only."
print_msg "$YELLOW" "For other platforms, visit: https://github.com/$REPO"
exit 1
;;
esac
}

# Get latest release version
get_latest_version() {
print_msg "$BLUE" "Fetching latest version..."

# Try using gh CLI first if available
if command -v gh &> /dev/null; then
gh release list --repo "$REPO" --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null
Copy link
Contributor

Choose a reason for hiding this comment

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

i don't think we need both options here, i'd just go straight to the rest api unless there are other reasons for using gh

else
# Fall back to curl and parse JSON
curl -sSfL "https://api.github.com/repos/$REPO/releases/latest" |
Copy link
Contributor

Choose a reason for hiding this comment

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

Fragile JSON parsing could break or behave unexpectedly

The fallback version detection uses grep '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/' which is brittle and could break if:

  • GitHub changes JSON formatting/whitespace
  • Special characters appear in tag names
  • Response contains multiple tag_name fields
  • JSON is minified differently

Recommendation: Since the primary path already uses jq (via gh), consider checking for jq availability in the fallback:

if command -v gh &> /dev/null; then
    gh release list --repo "$REPO" --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null
elif command -v jq &> /dev/null; then
    curl -sSfL "https://api.github.com/repos/$REPO/releases/latest" | jq -r '.tag_name'
else
    # grep/sed fallback
    curl -sSfL "https://api.github.com/repos/$REPO/releases/latest" |
        grep '"tag_name"' |
        sed -E 's/.*"tag_name": *"([^"]+)".*/\1/'
fi

Or use Python/Node which are commonly available on Linux systems for more robust JSON parsing.

grep '"tag_name"' |
sed -E 's/.*"tag_name": *"([^"]+)".*/\1/'
fi
}

# Download and install
install_cagent() {
local os arch version

os=$(detect_os)
arch=$(detect_arch)
version=$(get_latest_version)

if [ -z "$version" ]; then
print_msg "$RED" "Error: Could not determine latest version"
exit 1
fi

print_msg "$GREEN" "Installing cagent $version for $os/$arch..."

local binary_name="${BINARY_NAME}-${os}-${arch}"
local download_url="https://github.com/$REPO/releases/download/$version/$binary_name"
Copy link
Contributor

Choose a reason for hiding this comment

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

Predictable temporary file name creates security vulnerability

The script uses /tmp/$BINARY_NAME-$$ where $$ is the process ID. Process IDs are predictable and sequential, allowing an attacker to:

  1. Pre-create the file with malicious content
  2. Create a symlink to redirect the download and overwrite sensitive files
  3. Exploit TOCTOU race conditions

Recommendation: Use mktemp for secure temporary file creation:

local tmp_file=$(mktemp)

mktemp creates files with unpredictable random names and secure permissions (0600), preventing these attacks.

local tmp_file="/tmp/$BINARY_NAME-$$"

# Download binary
print_msg "$BLUE" "Downloading from $download_url..."
Copy link
Contributor

Choose a reason for hiding this comment

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

CRITICAL: No checksum or signature verification of downloaded binary

The script downloads a binary from GitHub and immediately makes it executable without any cryptographic verification. Since this installation script is designed to be piped to bash (curl | bash), a compromised download or MITM attack could lead to arbitrary code execution.

Recommendation: Verify the downloaded binary's checksum or GPG signature before executing it. Many projects publish SHA256 checksums alongside releases. Example:

# Download checksum file
curl -fsSL "${download_url}.sha256" -o "${tmp_file}.sha256"

# Verify checksum
expected_sha=$(cat "${tmp_file}.sha256" | awk '{print $1}')
actual_sha=$(sha256sum "$tmp_file" | awk '{print $1}')
if [ "$expected_sha" != "$actual_sha" ]; then
    print_msg "$RED" "Error: Checksum verification failed"
    exit 1
fi

This is especially critical for security-sensitive installation scripts.

if ! curl -fsSL "$download_url" -o "$tmp_file"; then
print_msg "$RED" "Error: Failed to download binary"
exit 1
fi

# Make executable
chmod +x "$tmp_file"

Comment on lines +118 to +120
Copy link

Choose a reason for hiding this comment

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

We can do this with install later.

Suggested change
# Make executable
chmod +x "$tmp_file"

# Determine install directory
local target_dir="$INSTALL_DIR"
local use_sudo=true

# Check if we can write to /usr/local/bin
if [ ! -w "$INSTALL_DIR" ]; then
if ! command -v sudo &> /dev/null; then
# No sudo available, use user's local bin
target_dir="$HOME/.local/bin"
use_sudo=false
mkdir -p "$target_dir"
print_msg "$YELLOW" "Installing to $target_dir (sudo not available)"
else
print_msg "$YELLOW" "Installing to $target_dir (requires sudo)"
fi
else
use_sudo=false
fi

# Install binary
local target_path="$target_dir/$BINARY_NAME"
if [ "$use_sudo" = true ]; then
sudo mv "$tmp_file" "$target_path"
sudo chmod +x "$target_path"
Comment on lines +143 to +144
Copy link

Choose a reason for hiding this comment

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

Suggested change
sudo mv "$tmp_file" "$target_path"
sudo chmod +x "$target_path"
sudo install -m 755 "$tmp_file" "$target_path"

else
mv "$tmp_file" "$target_path"
Copy link

Choose a reason for hiding this comment

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

Suggested change
mv "$tmp_file" "$target_path"
install -m 755 "$tmp_file" "$target_path"

fi

print_msg "$GREEN" "✓ Successfully installed cagent to $target_path"

# Check if directory is in PATH
if [[ ":$PATH:" != *":$target_dir:"* ]]; then
print_msg "$YELLOW" "⚠ Warning: $target_dir is not in your PATH"
print_msg "$YELLOW" "Add it to your PATH by adding this to your ~/.bashrc or ~/.zshrc:"
print_msg "$YELLOW" " export PATH=\"$target_dir:\$PATH\""
fi

# Verify installation
print_msg "$BLUE" "Verifying installation..."
if command -v "$BINARY_NAME" &> /dev/null; then
local installed_version
Copy link
Contributor

Choose a reason for hiding this comment

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

Executes downloaded binary without prior checksum verification

The script runs "$BINARY_NAME" --version to verify installation, but executes the binary that was downloaded without any checksum verification (related to issue at line 112). If the download was compromised, this verification step itself executes malicious code.

Recommendation: Only execute the binary after cryptographic verification of its integrity. Move this verification step to occur after checksum validation is added.

installed_version=$("$BINARY_NAME" --version 2>&1 | head -n1)
print_msg "$GREEN" "✓ $installed_version"
else
print_msg "$YELLOW" "⚠ Installation complete, but 'cagent' not found in PATH"
print_msg "$YELLOW" "You may need to restart your shell or add $target_dir to PATH"
fi

# Print next steps
echo "" >&2
print_msg "$GREEN" "╔════════════════════════════════════════════════════════════╗"
print_msg "$GREEN" "║ cagent installation complete! ║"
print_msg "$GREEN" "╚════════════════════════════════════════════════════════════╝"
echo ""
print_msg "$BLUE" "Next steps:"
print_msg "$BLUE" " 1. Set your API keys (at least one required):"
print_msg "$BLUE" " export OPENAI_API_KEY=your_key # For OpenAI models"
print_msg "$BLUE" " export ANTHROPIC_API_KEY=your_key # For Anthropic models"
print_msg "$BLUE" " export GOOGLE_API_KEY=your_key # For Gemini models"
echo "" >&2
print_msg "$BLUE" " 2. Try it out:"
print_msg "$BLUE" " cagent run default \"What can you do?\""
echo "" >&2
print_msg "$BLUE" " 3. Learn more:"
print_msg "$BLUE" " https://github.com/$REPO"
echo "" >&2
}

# Main
main() {
print_msg "$GREEN" "╔════════════════════════════════════════════════════════════╗"
print_msg "$GREEN" "║ Docker cagent Installation Script ║"
print_msg "$GREEN" "╚════════════════════════════════════════════════════════════╝"
echo "" >&2

install_cagent
}

main