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
229 changes: 229 additions & 0 deletions .github/workflows/dify-plugin-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
name: Dify Plugin E2E

on:
pull_request:
branches: [main]
paths:
- 'integrations/dify-plugin/**'
- 'tests/e2e/dify_plugin/**'
- 'sdks/sandbox/python/**'
- 'server/**'
push:
branches: [main]
paths:
- 'integrations/dify-plugin/**'
- 'tests/e2e/dify_plugin/**'
- 'sdks/sandbox/python/**'
- 'server/**'
- '.github/workflows/dify-plugin-e2e.yml'
workflow_dispatch:
# Allow manual trigger from GitHub Actions UI

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
DIFY_PORT: "5001"
# Use latest stable release tag (check https://github.com/langgenius/dify/releases)
DIFY_REF: "1.11.4"
DIFY_ADMIN_EMAIL: "admin@example.com"
DIFY_ADMIN_PASSWORD: "ChangeMe123!"
OPEN_SANDBOX_API_KEY: "opensandbox-e2e-key"

jobs:
dify-plugin-e2e:
name: Dify Plugin E2E Test
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install uv
run: pip install uv

# Cache Dify Docker images to speed up subsequent runs
- name: Cache Dify Docker images
uses: actions/cache@v4
id: dify-cache
with:
path: /tmp/dify-images
key: dify-images-${{ env.DIFY_REF }}-v1
restore-keys: |
dify-images-${{ env.DIFY_REF }}-

- name: Load cached Dify images
if: steps.dify-cache.outputs.cache-hit == 'true'
run: |
echo "Loading cached Dify images..."
for img in /tmp/dify-images/*.tar; do
if [ -f "$img" ]; then
docker load -i "$img" || true
fi
done
docker images | grep -E "langgenius|dify" || true

- name: Build execd image
run: |
docker build -f components/execd/Dockerfile -t opensandbox/execd:local .

- name: Create OpenSandbox config
run: |
cat > ~/.sandbox.toml <<EOF
[server]
host = "0.0.0.0"
port = 8080
log_level = "INFO"
api_key = "${{ env.OPEN_SANDBOX_API_KEY }}"

[runtime]
type = "docker"
execd_image = "opensandbox/execd:local"

[docker]
network_mode = "bridge"
EOF

- name: Install OpenSandbox server dependencies
working-directory: server
run: uv sync

- name: Start OpenSandbox server
working-directory: server
run: |
uv run python -m src.main > server.log 2>&1 &
echo "OPENSANDBOX_PID=$!" >> $GITHUB_ENV

- name: Wait for OpenSandbox server
run: |
for i in {1..30}; do
if curl -s http://localhost:8080/health | grep -q healthy; then
echo "OpenSandbox server is ready"
exit 0
fi
echo "Waiting for OpenSandbox server... ($i/30)"
sleep 2
done
echo "OpenSandbox server failed to start"
cat server/server.log
exit 1

- name: Prepare Dify docker-compose
working-directory: tests/e2e/dify_plugin
env:
DIFY_REF: ${{ env.DIFY_REF }}
run: |
python prepare_dify_compose.py
echo "Dify compose files ready"

- name: Pull Dify images
working-directory: tests/e2e/dify_plugin/.dify
run: |
docker compose pull --ignore-pull-failures || true
echo "Dify images pulled"

- name: Save Dify images to cache
if: steps.dify-cache.outputs.cache-hit != 'true'
run: |
mkdir -p /tmp/dify-images
# Save main Dify images for caching (versions from 1.11.4 docker-compose)
for img in \
"langgenius/dify-api:${{ env.DIFY_REF }}" \
"langgenius/dify-web:${{ env.DIFY_REF }}" \
"langgenius/dify-sandbox:0.2.12" \
"langgenius/dify-plugin-daemon:0.5.2-local"; do
name=$(echo "$img" | tr '/:' '_')
if docker image inspect "$img" >/dev/null 2>&1; then
echo "Saving $img..."
docker save "$img" -o "/tmp/dify-images/${name}.tar" || true
fi
done
ls -lh /tmp/dify-images/ || true

- name: Start Dify
working-directory: tests/e2e/dify_plugin/.dify
run: |
docker compose up -d
echo "Dify containers starting..."

- name: Wait for Dify API
run: |
for i in {1..90}; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${{ env.DIFY_PORT }}/console/api/ping 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo "Dify API is ready (HTTP 200)"
exit 0
fi
echo "Waiting for Dify API... ($i/90) HTTP=$HTTP_CODE"
sleep 5
done
echo "Dify API failed to start"
docker compose -f tests/e2e/dify_plugin/.dify/docker-compose.yaml logs
exit 1

- name: Install OpenSandbox Python SDK (local)
working-directory: sdks/sandbox/python
run: |
pip install -e .

- name: Install plugin dependencies
working-directory: integrations/dify-plugin/opensandbox
run: |
pip install -r requirements.txt

- name: Install e2e test dependencies
working-directory: tests/e2e/dify_plugin
run: |
pip install -r requirements.txt

- name: Run plugin unit tests
working-directory: integrations/dify-plugin/opensandbox
env:
OPENSANDBOX_BASE_URL: "http://localhost:8080"
OPENSANDBOX_API_KEY: ${{ env.OPEN_SANDBOX_API_KEY }}
PYTHONPATH: "."
run: |
python -m unittest discover -s tests -v

- name: Run E2E test
working-directory: tests/e2e/dify_plugin
env:
DIFY_CONSOLE_API_URL: "http://localhost:${{ env.DIFY_PORT }}"
# Plugin runs on host via remote debug, so localhost works
OPEN_SANDBOX_BASE_URL: "http://localhost:8080"
run: |
python run_e2e.py

- name: Upload OpenSandbox logs
if: always()
uses: actions/upload-artifact@v4
with:
name: opensandbox-server-log
path: server/server.log
retention-days: 5

- name: Collect Dify logs
if: always()
run: |
docker compose -f tests/e2e/dify_plugin/.dify/docker-compose.yaml logs > dify.log 2>&1 || true

- name: Upload Dify logs
if: always()
uses: actions/upload-artifact@v4
with:
name: dify-logs
path: dify.log
retention-days: 5

- name: Cleanup
if: always()
run: |
docker compose -f tests/e2e/dify_plugin/.dify/docker-compose.yaml down --volumes --remove-orphans || true
kill ${{ env.OPENSANDBOX_PID }} 2>/dev/null || true
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,15 @@ MANIFEST

# Virtual environments
venv/
.venv/
env/
ENV/
env.bak/
venv.bak/

# Dify test artifacts
.dify/

# Docker
*.pid
*.seed
Expand All @@ -214,6 +218,9 @@ Thumbs.db
.env.test.local
.env.production.local

# E2E test configs
*.e2e.toml

# API keys and secrets
secrets/
*.pem
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- Use feature branches (e.g., `feature/...`, `fix/...`) and keep PRs focused.
- PRs should include summary, testing status, and linked issues; follow the template in `CONTRIBUTING.md`.
- For major API or architectural changes, submit an OSEP (`oseps/`).
- **IMPORTANT: Before committing code**, always run `./scripts/add-license.sh` from the repo root to add Apache 2.0 license headers to new source files. This is required for CI to pass.

## Security & Configuration Tips
- Local server config lives in `~/.sandbox.toml` (copied from `server/example.config.toml`).
Expand Down
80 changes: 80 additions & 0 deletions integrations/dify-plugin/opensandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# OpenSandbox Dify Plugin (Tool)

This plugin lets Dify call a **self-hosted OpenSandbox server** to create, run, and
terminate sandboxes.

## Features (MVP)
- `sandbox_create`: create a sandbox and return its id
- `sandbox_run`: execute a command in an existing sandbox
- `sandbox_kill`: terminate a sandbox by id

## Requirements
- Python 3.12+
- Dify plugin runtime
- OpenSandbox server reachable by URL

## Local Testing (Dify docker-compose)

### 1) Start OpenSandbox Server
Run OpenSandbox locally with Docker runtime enabled and an API key.

Example config (adjust to your setup):
```toml
[server]
host = "0.0.0.0"
port = 8080
api_key = "your-open-sandbox-key"

[runtime]
type = "docker"
execd_image = "opensandbox/execd:v1.0.5"

[docker]
network_mode = "bridge"
```

### 2) Start Dify (official docker-compose)
Follow the official Dify self-hosted docker-compose guide to start a local Dify instance.

### 3) Enable Plugin Remote Debug in Dify UI
- Open Dify UI → **Plugins** → **Develop** (or **Debug**)
- Copy the **Remote Install URL** and **Remote Install Key**

Create `.env` in this plugin directory (do not commit it):
```bash
INSTALL_METHOD=remote
REMOTE_INSTALL_URL=debug.dify.ai:5003
REMOTE_INSTALL_KEY=your-debug-key
```

### 4) Run the Plugin
```bash
pip install -r requirements.txt
python -m main
```

### 5) Configure Provider Credentials in Dify
Set:
- **OpenSandbox base URL**: `http://localhost:8080`
- **OpenSandbox API Key**: `your-open-sandbox-key`

Then use the tools in a workflow:
1. `sandbox_create`
2. `sandbox_run`
3. `sandbox_kill`

## E2E Testing

Automated end-to-end tests are available in `tests/e2e/dify_plugin/`. These tests:
- Start OpenSandbox server and Dify
- Register the plugin via remote debugging
- Import and run a test workflow
- Verify sandbox operations work correctly

See `tests/e2e/dify_plugin/README.md` for details.

CI runs these tests automatically on changes to the plugin code. See `.github/workflows/dify-plugin-e2e.yml`.

## Notes
- The base URL should **not** include `/v1`.
- The plugin itself does **not** host OpenSandbox; it connects to your server.
1 change: 1 addition & 0 deletions integrations/dify-plugin/opensandbox/_assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions integrations/dify-plugin/opensandbox/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2026 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from dify_plugin import DifyPluginEnv, Plugin

plugin = Plugin(DifyPluginEnv())

if __name__ == "__main__":
plugin.run()
Loading
Loading