diff --git a/dify-agent-runtime/docker/Dockerfile b/dify-agent-runtime/docker/Dockerfile index 798e36a8d41..ca058727e45 100644 --- a/dify-agent-runtime/docker/Dockerfile +++ b/dify-agent-runtime/docker/Dockerfile @@ -40,6 +40,7 @@ RUN apt-get update \ openssh-client \ procps \ ripgrep \ + tini \ tmux \ unzip \ xz-utils \ @@ -79,4 +80,5 @@ WORKDIR /home/dify EXPOSE 5004 +ENTRYPOINT ["/usr/bin/tini", "-g", "--"] CMD ["shellctl", "serve", "--listen", "0.0.0.0:5004"] diff --git a/dify-agent-runtime/docker/sync-e2b-template.sh b/dify-agent-runtime/docker/sync-e2b-template.sh new file mode 100755 index 00000000000..09a9558f98a --- /dev/null +++ b/dify-agent-runtime/docker/sync-e2b-template.sh @@ -0,0 +1,253 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly DEFAULT_TEMPLATE_NAME='dify-agent-local-sandbox' +readonly DEFAULT_IMAGE_REPOSITORY='langgenius/dify-agent-local-sandbox' +readonly DEFAULT_PLATFORM='linux/amd64' +readonly DEFAULT_E2B_CLI_VERSION='2.13.3' +readonly DEFAULT_WAIT_SECONDS='600' +readonly DEFAULT_POLL_SECONDS='10' +readonly DEFAULT_CPU_COUNT='2' +readonly DEFAULT_MEMORY_MB='1024' +readonly START_COMMAND='/usr/bin/env SHELLCTL_ENABLE_PATH_ISOLATION=true /usr/local/bin/shellctl serve --listen 0.0.0.0:5004' +readonly READY_COMMAND='curl -fsS http://localhost:5004/healthz' + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +usage() { + cat <&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +require_non_negative_integer() { + local name="$1" + local value="$2" + + [[ "${value}" =~ ^[0-9]+$ ]] || die "${name} must be a non-negative integer: ${value}" +} + +require_positive_integer() { + local name="$1" + local value="$2" + + require_non_negative_integer "${name}" "${value}" + (( value > 0 )) || die "${name} must be greater than zero" +} + +print_command() { + printf 'Command:' + printf ' %q' "$@" + printf '\n' +} + +source_ref='HEAD' +image_repository="${E2B_BASE_IMAGE_REPOSITORY:-${DEFAULT_IMAGE_REPOSITORY}}" +image_tag='' +template_name="${E2B_TEMPLATE_NAME:-${DEFAULT_TEMPLATE_NAME}}" +platform="${DEFAULT_PLATFORM}" +e2b_cli_version="${E2B_CLI_VERSION:-${DEFAULT_E2B_CLI_VERSION}}" +wait_seconds="${DEFAULT_WAIT_SECONDS}" +poll_seconds="${DEFAULT_POLL_SECONDS}" +cpu_count="${DEFAULT_CPU_COUNT}" +memory_mb="${DEFAULT_MEMORY_MB}" +dry_run=false +no_cache=false + +while (( $# > 0 )); do + case "$1" in + --source-ref) + (( $# >= 2 )) || die '--source-ref requires a value' + source_ref="$2" + shift 2 + ;; + --image-repository) + (( $# >= 2 )) || die '--image-repository requires a value' + image_repository="$2" + shift 2 + ;; + --image-tag) + (( $# >= 2 )) || die '--image-tag requires a value' + image_tag="$2" + shift 2 + ;; + --template-name) + (( $# >= 2 )) || die '--template-name requires a value' + template_name="$2" + shift 2 + ;; + --platform) + (( $# >= 2 )) || die '--platform requires a value' + platform="$2" + shift 2 + ;; + --wait-seconds) + (( $# >= 2 )) || die '--wait-seconds requires a value' + wait_seconds="$2" + shift 2 + ;; + --poll-seconds) + (( $# >= 2 )) || die '--poll-seconds requires a value' + poll_seconds="$2" + shift 2 + ;; + --cpu-count) + (( $# >= 2 )) || die '--cpu-count requires a value' + cpu_count="$2" + shift 2 + ;; + --memory-mb) + (( $# >= 2 )) || die '--memory-mb requires a value' + memory_mb="$2" + shift 2 + ;; + --no-cache) + no_cache=true + shift + ;; + --dry-run) + dry_run=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac +done + +[[ -n "${image_repository}" ]] || die 'image repository cannot be empty' +[[ -n "${template_name}" ]] || die 'template name cannot be empty' +[[ "${platform}" == */* ]] || die "platform must use OS/ARCH form: ${platform}" +require_non_negative_integer 'wait-seconds' "${wait_seconds}" +require_positive_integer 'poll-seconds' "${poll_seconds}" +require_positive_integer 'cpu-count' "${cpu_count}" +require_positive_integer 'memory-mb' "${memory_mb}" + +require_command git +require_command docker +require_command jq +require_command mktemp + +source_sha="$(git -C "${SCRIPT_DIR}" rev-parse --verify "${source_ref}^{commit}" 2>/dev/null)" \ + || die "cannot resolve source ref: ${source_ref}" +[[ -n "${image_tag}" ]] || image_tag="${source_sha}" + +image_ref="${image_repository}:${image_tag}" +platform_os="${platform%%/*}" +platform_arch="${platform#*/}" +deadline=$((SECONDS + wait_seconds)) +manifest_json='' +inspect_error='' + +while true; do + if manifest_json="$(docker buildx imagetools inspect --format '{{json .Manifest}}' "${image_ref}" 2>&1)"; then + break + fi + inspect_error="${manifest_json}" + manifest_json='' + + (( SECONDS < deadline )) || die "image is not available: ${image_ref}${inspect_error:+ (${inspect_error})}" + echo "Waiting for published image ${image_ref}..." >&2 + sleep "${poll_seconds}" +done + +platform_digest="$( + jq -r \ + --arg os "${platform_os}" \ + --arg arch "${platform_arch}" \ + '[.manifests[]? | select(.platform.os == $os and .platform.architecture == $arch) | .digest][0] // empty' \ + <<<"${manifest_json}" +)" +[[ "${platform_digest}" == sha256:* ]] \ + || die "image ${image_ref} does not contain platform ${platform}" + +resolved_image="${image_repository}@${platform_digest}" +image_json="$(docker buildx imagetools inspect --format '{{json .Image}}' "${resolved_image}")" \ + || die "cannot inspect resolved image: ${resolved_image}" + +actual_os="$(jq -r '.os // empty' <<<"${image_json}")" +actual_arch="$(jq -r '.architecture // empty' <<<"${image_json}")" +actual_revision="$(jq -r '.config.Labels["org.opencontainers.image.revision"] // empty' <<<"${image_json}")" + +[[ "${actual_os}/${actual_arch}" == "${platform}" ]] \ + || die "resolved image platform ${actual_os}/${actual_arch} does not match ${platform}" +[[ "${actual_revision}" == "${source_sha}" ]] \ + || die "image revision ${actual_revision:-} does not match source commit ${source_sha}" + +tmp_dir="$(mktemp -d)" +cleanup() { + rm -rf "${tmp_dir}" +} +trap cleanup EXIT + +dockerfile="${tmp_dir}/e2b.Dockerfile" +printf 'FROM %s\nUSER dify\nWORKDIR /home/dify\n' "${resolved_image}" > "${dockerfile}" + +e2b_command=( + npx --yes "@e2b/cli@${e2b_cli_version}" + template create "${template_name}" + --path "${tmp_dir}" + --dockerfile e2b.Dockerfile + --cmd "${START_COMMAND}" + --ready-cmd "${READY_COMMAND}" + --cpu-count "${cpu_count}" + --memory-mb "${memory_mb}" +) +if [[ "${no_cache}" == true ]]; then + e2b_command+=(--no-cache) +fi + +echo "Source commit: ${source_sha}" +echo "Published image: ${image_ref}" +echo "Resolved image: ${resolved_image} (${platform})" +echo "E2B Template: ${template_name}" +print_command "${e2b_command[@]}" + +if [[ "${dry_run}" == true ]]; then + echo 'Dry run; E2B Template was not changed.' + exit 0 +fi + +require_command npx +[[ -n "${E2B_API_KEY:-}" ]] || die 'E2B_API_KEY is required' + +"${e2b_command[@]}" +echo "E2B Template synchronized: ${template_name} <- ${resolved_image}"