.PHONY: build clean test lint proto gen-cli-help integration integration-up integration-test integration-down BIN_DIR := bin PROTO_SRC := ../dify-agent/proto/dify/agent/stub/v1/agent_stub.proto PROTO_OUT := gen AGENT_CLI_HELP_JSON := ../dify-agent/src/dify_agent/layers/_agent_cli_help.json build: $(BIN_DIR)/shellctl $(BIN_DIR)/shellctl-sanitize-pty $(BIN_DIR)/shellctl-runner-exit $(BIN_DIR)/shellctl-runner $(BIN_DIR)/dify-agent gen-cli-help $(BIN_DIR)/shellctl: $(shell find cmd/shellctl internal -name '*.go') go build -o $@ ./cmd/shellctl $(BIN_DIR)/shellctl-sanitize-pty: $(shell find cmd/sanitize-pty internal/sanitize -name '*.go') go build -o $@ ./cmd/sanitize-pty $(BIN_DIR)/shellctl-runner-exit: $(shell find cmd/runner-exit internal/runner_exit -name '*.go') go build -o $@ ./cmd/runner-exit $(BIN_DIR)/shellctl-runner: $(shell find cmd/runner internal/landlock internal/cmdutil -name '*.go') go build -o $@ ./cmd/runner $(BIN_DIR)/dify-agent: $(shell find cmd/dify-agent-cli internal/agentcli internal/stubclient -name '*.go') $(shell find gen -name '*.go' 2>/dev/null) go build -o $@ ./cmd/dify-agent-cli # Regenerate the JSON help snapshot from the Go command tree. Runs as part of # `build` so the checked-in data never drifts from the CLI. gen-cli-help: $(BIN_DIR)/dify-agent $(BIN_DIR)/dify-agent __dump-cli-help > $(AGENT_CLI_HELP_JSON) test: lint go test ./... lint: golangci-lint run ./... proto: @mkdir -p $(PROTO_OUT) protoc \ --proto_path=../dify-agent/proto \ --go_out=$(PROTO_OUT) --go_opt=paths=source_relative \ --go-grpc_out=$(PROTO_OUT) --go-grpc_opt=paths=source_relative \ $(PROTO_SRC) clean: rm -rf $(BIN_DIR) # --- Integration tests --- # # Container name and host port are randomised per invocation to avoid # conflicts when multiple runs overlap or a previous run leaked a container. # # State is written to .integration-state so that separate make invocations # (integration-up, integration-test, integration-logs, integration-down) # can reference the same container. `make integration` runs the full # lifecycle in a single invocation with automatic cleanup. IMAGE_NAME := dify-agent-runtime:test STATE_FILE := .integration-state integration-up: @echo "Building runtime image..." docker build -t $(IMAGE_NAME) -f docker/Dockerfile . $(eval TEST_ID := $(shell printf '%05d' $$((RANDOM % 100000)))) $(eval CONTAINER_NAME := sandbox-rt-$(TEST_ID)) $(eval HOST_PORT := $(shell python3 -c 'import socket; s=socket.socket(); s.bind(("",0)); print(s.getsockname()[1]); s.close()')) $(eval AUTH_TOKEN := test-token-$(TEST_ID)) @echo "Starting runtime container $(CONTAINER_NAME) on port $(HOST_PORT)..." docker run -d --name $(CONTAINER_NAME) \ -p $(HOST_PORT):5004 \ -e SHELLCTL_AUTH_TOKEN=$(AUTH_TOKEN) \ $(IMAGE_NAME) @echo 'CONTAINER_NAME=$(CONTAINER_NAME)' > $(STATE_FILE) @echo 'HOST_PORT=$(HOST_PORT)' >> $(STATE_FILE) @echo 'AUTH_TOKEN=$(AUTH_TOKEN)' >> $(STATE_FILE) @echo "Waiting for runtime to be ready..." @for i in $$(seq 1 30); do \ if curl -sf http://localhost:$(HOST_PORT)/healthz > /dev/null 2>&1; then \ echo "Runtime is ready on port $(HOST_PORT)"; \ break; \ fi; \ if [ "$$i" -eq 30 ]; then \ echo "ERROR: runtime not ready after 60s" >&2; \ docker logs $(CONTAINER_NAME); \ docker rm -f $(CONTAINER_NAME) 2>/dev/null; \ rm -f $(STATE_FILE); \ exit 1; \ fi; \ sleep 2; \ done $(eval CONTAINER_NAME_NOISO := sandbox-rt-noiso-$(TEST_ID)) $(eval HOST_PORT_NOISO := $(shell python3 -c 'import socket; s=socket.socket(); s.bind(("",0)); print(s.getsockname()[1]); s.close()')) $(eval AUTH_TOKEN_NOISO := test-token-noiso-$(TEST_ID)) @echo "Starting no-isolation container $(CONTAINER_NAME_NOISO) on port $(HOST_PORT_NOISO)..." docker run -d --name $(CONTAINER_NAME_NOISO) \ -p $(HOST_PORT_NOISO):5004 \ -e SHELLCTL_AUTH_TOKEN=$(AUTH_TOKEN_NOISO) \ -e SHELLCTL_ENABLE_PATH_ISOLATION=false \ $(IMAGE_NAME) @echo 'CONTAINER_NAME_NOISO=$(CONTAINER_NAME_NOISO)' >> $(STATE_FILE) @echo 'HOST_PORT_NOISO=$(HOST_PORT_NOISO)' >> $(STATE_FILE) @echo 'AUTH_TOKEN_NOISO=$(AUTH_TOKEN_NOISO)' >> $(STATE_FILE) @for i in $$(seq 1 30); do \ if curl -sf http://localhost:$(HOST_PORT_NOISO)/healthz > /dev/null 2>&1; then \ echo "No-isolation runtime is ready on port $(HOST_PORT_NOISO)"; \ break; \ fi; \ if [ "$$i" -eq 30 ]; then \ echo "ERROR: no-isolation runtime not ready after 60s" >&2; \ docker logs $(CONTAINER_NAME_NOISO); \ docker rm -f $(CONTAINER_NAME_NOISO) 2>/dev/null; \ exit 1; \ fi; \ sleep 2; \ done integration-test: @test -f $(STATE_FILE) || { echo "ERROR: run 'make integration-up' first" >&2; exit 1; } @. ./$(STATE_FILE); \ SHELLCTL_GO_URL=http://localhost:$$HOST_PORT \ SHELLCTL_TEST_TOKEN=$$AUTH_TOKEN \ SHELLCTL_GO_URL_NO_ISOLATION=http://localhost:$$HOST_PORT_NOISO \ SHELLCTL_TEST_TOKEN_NO_ISOLATION=$$AUTH_TOKEN_NOISO \ go test -tags=integration -v -count=1 -timeout=300s ./tests/... integration-logs: @test -f $(STATE_FILE) || { echo "ERROR: no state file" >&2; exit 1; } @. ./$(STATE_FILE); docker logs $$CONTAINER_NAME integration-down: @if [ -f $(STATE_FILE) ]; then \ . ./$(STATE_FILE); \ docker rm -f $$CONTAINER_NAME 2>/dev/null || true; \ docker rm -f $$CONTAINER_NAME_NOISO 2>/dev/null || true; \ rm -f $(STATE_FILE); \ fi integration: @$(MAKE) integration-up @trap '$(MAKE) integration-down' EXIT; \ $(MAKE) integration-test