From 9dda3a020afdbe0fb965d175c5b3d55eec055cf7 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Tue, 23 Jun 2026 16:48:46 +0530 Subject: [PATCH 1/5] =?UTF-8?q?[FIX]=20Unblock=20OSS=20first-time=20setup?= =?UTF-8?q?=20=E2=80=94=20sample=20envs,=20host-gateway,=20ollama=20hint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend/sample.env: set INTERNAL_SERVICE_API_KEY=dev-internal-key-123 so workers' internal API calls don't 500 against backend middleware. Add TEMPORARY_REMOTE_STORAGE alongside PERMANENT_REMOTE_STORAGE. - workers/sample.env: mirror PERMANENT_REMOTE_STORAGE, TEMPORARY_REMOTE_STORAGE, REMOTE_PROMPT_STUDIO_FILE_PATH. Without these the executor / ide-callback workers crash at json.loads("") on first Prompt Studio index. - unstract/sdk1 file_storage/env_helper.py: raise FileStorageError with a clear message when the storage env var is unset or invalid JSON, instead of letting json.loads("") raise an inscrutable JSONDecodeError. - adapters/llm1 + embedding1 ollama.json: fix typo docker.host.internal -> host.docker.internal in the Base URL hint. - docker/docker-compose.yaml: extract a YAML anchor x-host-gateway and apply it (<<: *host_gateway) to backend, prompt-service, runner, celery-*, and every worker-* service. Before this only backend and prompt-service had extra_hosts, so adapter Test passed in prompt-service but real prompt execution from the worker pool failed with EAI_NONAME against host.docker.internal. - run-platform.sh: fail fast with actionable hint when `docker info` can't reach the daemon (missing docker group membership, etc.). Co-Authored-By: Claude Opus 4.7 Claude-Session: https://claude.ai/code/session_01Br691aZbhfcB4xdswrUjuw --- backend/sample.env | 5 +++- docker/docker-compose.yaml | 28 +++++++++++++++---- run-platform.sh | 8 ++++++ .../adapters/embedding1/static/ollama.json | 2 +- .../sdk1/adapters/llm1/static/ollama.json | 2 +- .../unstract/sdk1/file_storage/env_helper.py | 14 +++++++++- workers/sample.env | 6 ++++ 7 files changed, 55 insertions(+), 10 deletions(-) diff --git a/backend/sample.env b/backend/sample.env index 377016fdec..cb7c75dd3f 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -64,7 +64,9 @@ SESSION_EXPIRATION_TIME_IN_SECOND=7200 WEB_APP_ORIGIN_URL="http://frontend.unstract.localhost" # API keys for trusted services -INTERNAL_SERVICE_API_KEY= +# Workers send this as X-API-Key on internal calls to backend. +# Must match workers/sample.env INTERNAL_SERVICE_API_KEY; rotate in both for prod. +INTERNAL_SERVICE_API_KEY=dev-internal-key-123 # Unstract Core envs BUILTIN_FUNCTIONS_API_KEY= @@ -199,6 +201,7 @@ API_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_ur #Remote storage related envs PERMANENT_REMOTE_STORAGE='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' +TEMPORARY_REMOTE_STORAGE='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' REMOTE_PROMPT_STUDIO_FILE_PATH="unstract/prompt-studio-data" # Storage Provider for Tool registry diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index b7b49a7618..a07ab9a2fe 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -2,9 +2,16 @@ name: ${COMPOSE_PROJECT_NAME:-docker} include: - docker-compose-dev-essentials.yaml +# Reusable host-gateway mapping so containers can reach services on the host +# (e.g. host-installed Ollama at http://host.docker.internal:11434). +x-host-gateway: &host_gateway + extra_hosts: + - "host.docker.internal:host-gateway" + services: # Backend service backend: + <<: *host_gateway image: unstract/backend:${VERSION} container_name: unstract-backend restart: unless-stopped @@ -34,12 +41,10 @@ services: - traefik.enable=true - traefik.http.routers.backend.rule=Host(`frontend.unstract.localhost`) && (PathPrefix(`/api/v1`) || PathPrefix(`/deployment`) || PathPrefix(`/public`)) - traefik.http.services.backend.loadbalancer.server.port=8000 - extra_hosts: - # "host-gateway" is a special string that translates to host docker0 i/f IP. - - "host.docker.internal:host-gateway" # Celery worker for dashboard metrics processing worker-metrics: + <<: *host_gateway image: unstract/backend:${VERSION} container_name: unstract-worker-metrics restart: unless-stopped @@ -61,6 +66,7 @@ services: # Processes post-execution callbacks via InternalAPIClient (no Django). # Handles: ide_index_complete/error, ide_prompt_complete/error. worker-ide-callback: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-ide-callback restart: unless-stopped @@ -82,6 +88,7 @@ services: # Celery Flower celery-flower: + <<: *host_gateway image: unstract/backend:${VERSION} container_name: unstract-celery-flower restart: unless-stopped @@ -105,6 +112,7 @@ services: # Celery Beat celery-beat: + <<: *host_gateway image: unstract/backend:${VERSION} container_name: unstract-celery-beat restart: unless-stopped @@ -152,6 +160,7 @@ services: - traefik.enable=false prompt-service: + <<: *host_gateway image: unstract/prompt-service:${VERSION} container_name: unstract-prompt-service restart: unless-stopped @@ -166,9 +175,6 @@ services: - ../prompt-service/.env labels: - traefik.enable=false - extra_hosts: - # "host-gateway" is a special string that translates to host docker0 i/f IP. - - "host.docker.internal:host-gateway" x2text-service: image: unstract/x2text-service:${VERSION} @@ -184,6 +190,7 @@ services: - traefik.enable=false runner: + <<: *host_gateway image: unstract/runner:${VERSION} container_name: unstract-runner restart: unless-stopped @@ -206,6 +213,7 @@ services: # ==================================================================== worker-api-deployment-v2: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-api-deployment-v2 restart: unless-stopped @@ -238,6 +246,7 @@ services: - ${TOOL_REGISTRY_CONFIG_SRC_PATH}:/data/tool_registry_config worker-callback-v2: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-callback-v2 restart: unless-stopped @@ -264,6 +273,7 @@ services: - ${TOOL_REGISTRY_CONFIG_SRC_PATH}:/data/tool_registry_config worker-file-processing-v2: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-file-processing-v2 restart: unless-stopped @@ -316,6 +326,7 @@ services: - ${TOOL_REGISTRY_CONFIG_SRC_PATH}:/data/tool_registry_config worker-general-v2: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-general-v2 restart: unless-stopped @@ -343,6 +354,7 @@ services: - ${TOOL_REGISTRY_CONFIG_SRC_PATH}:/data/tool_registry_config worker-notification-v2: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-notification-v2 restart: unless-stopped @@ -391,6 +403,7 @@ services: - ${TOOL_REGISTRY_CONFIG_SRC_PATH}:/data/tool_registry_config worker-log-consumer-v2: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-log-consumer-v2 restart: unless-stopped @@ -440,6 +453,7 @@ services: - ${TOOL_REGISTRY_CONFIG_SRC_PATH}:/data/tool_registry_config worker-log-history-scheduler-v2: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-log-history-scheduler-v2 restart: unless-stopped @@ -463,6 +477,7 @@ services: - traefik.enable=false worker-scheduler-v2: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-scheduler-v2 restart: unless-stopped @@ -507,6 +522,7 @@ services: - ${TOOL_REGISTRY_CONFIG_SRC_PATH}:/data/tool_registry_config worker-executor-v2: + <<: *host_gateway image: unstract/worker-unified:${VERSION} container_name: unstract-worker-executor-v2 restart: unless-stopped diff --git a/run-platform.sh b/run-platform.sh index a2b793a131..f486f75305 100755 --- a/run-platform.sh +++ b/run-platform.sh @@ -33,6 +33,14 @@ check_dependencies() { echo "$red_text""docker not found. Exiting.""$default_text" exit 1 fi + if ! docker info >/dev/null 2>&1; then + echo "$red_text""Cannot connect to the Docker daemon.""$default_text" + echo " - Check group membership: getent group docker" + echo " - Add your user to it: sudo usermod -aG docker \$USER" + echo " - Activate in current shell: newgrp docker" + echo " - For new shells, a full desktop logout (not just terminal close) is required." + exit 1 + fi # For 'docker compose' vs 'docker-compose', see https://stackoverflow.com/a/66526176. docker compose >/dev/null 2>&1 if [ $? -eq 0 ]; then diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/ollama.json b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/ollama.json index c241e5e711..e5434a095e 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/ollama.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/ollama.json @@ -23,7 +23,7 @@ "type": "string", "title": "Base URL", "default": "", - "description": "Provide the base URL where Ollama server is running. Example: `http://docker.host.internal:11434` or `http://localhost:11434`" + "description": "Provide the base URL where Ollama server is running. Example: `http://host.docker.internal:11434` or `http://localhost:11434`" }, "max_retries": { "type": "number", diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json index 3c8a4a5f16..8f2b9db7a1 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/ollama.json @@ -23,7 +23,7 @@ "type": "string", "title": "Base URL", "default": "", - "description": "Provide the base URL where Ollama server is running. Example: http://docker.host.internal:11434 or http://localhost:11434" + "description": "Provide the base URL where Ollama server is running. Example: http://host.docker.internal:11434 or http://localhost:11434" }, "max_tokens": { "type": "number", diff --git a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py index 22b5210942..5fec370219 100644 --- a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py +++ b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py @@ -31,8 +31,20 @@ def get_storage(storage_type: StorageType, env_name: str) -> FileStorage: FileStorage: FIleStorage instance initialised using the provider and credentials configured in the env """ + raw = os.environ.get(env_name) + if not raw: + raise FileStorageError( + f"Required env var '{env_name}' is unset or empty. " + f"Expected JSON config of the form: {EnvHelper.ENV_CONFIG_FORMAT}" + ) + try: + file_storage_creds = json.loads(raw) + except json.JSONDecodeError as e: + raise FileStorageError( + f"Env var '{env_name}' is not valid JSON: {e}. " + f"Expected: {EnvHelper.ENV_CONFIG_FORMAT}" + ) from e try: - file_storage_creds = json.loads(os.environ.get(env_name, "")) provider = FileStorageProvider(file_storage_creds[CredentialKeyword.PROVIDER]) credentials = file_storage_creds.get(CredentialKeyword.CREDENTIALS, {}) if storage_type == StorageType.PERMANENT: diff --git a/workers/sample.env b/workers/sample.env index 4764fb0c5a..7ade3c7f57 100644 --- a/workers/sample.env +++ b/workers/sample.env @@ -316,6 +316,12 @@ UNSTRACT_RUNNER_API_BACKOFF_FACTOR=3 WORKFLOW_EXECUTION_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' API_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' +# Remote storage for Prompt Studio / IDE flows. Must match backend/sample.env. +# Required by executor and ide-callback workers; missing values crash json.loads in EnvHelper. +PERMANENT_REMOTE_STORAGE='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' +TEMPORARY_REMOTE_STORAGE='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' +REMOTE_PROMPT_STUDIO_FILE_PATH=unstract/prompt-studio-data + # File Execution Configuration WORKFLOW_EXECUTION_DIR_PREFIX=unstract/execution API_EXECUTION_DIR_PREFIX=unstract/api From bc4ad1f71ab948f97a96f11ac8667b8dd9525afb Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Tue, 23 Jun 2026 17:44:31 +0530 Subject: [PATCH 2/5] [FIX] Address greptile / coderabbit review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - env_helper.py: validate parsed JSON is a dict; normalize KeyError / TypeError / ValueError from provider construction into FileStorageError with the remediation message, so callers never see raw json/dict exceptions on a misconfigured env var. - docker-compose.yaml: extend the x-host-gateway anchor's docstring to warn that YAML merge does NOT concatenate lists — adding a sibling extra_hosts to a service shadows the anchor entry rather than appending. Future contributors must inline all entries instead. - run-platform.sh: branch the docker-daemon remediation hints by OS (Linux / macOS / Windows / other), so macOS users see the Docker-Desktop hint instead of irrelevant getent/usermod commands. Co-Authored-By: Claude Opus 4.7 Claude-Session: https://claude.ai/code/session_01Br691aZbhfcB4xdswrUjuw --- docker/docker-compose.yaml | 5 +++++ run-platform.sh | 22 +++++++++++++++---- .../unstract/sdk1/file_storage/env_helper.py | 14 +++++++++--- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index a07ab9a2fe..5884b88994 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -4,6 +4,11 @@ include: # Reusable host-gateway mapping so containers can reach services on the host # (e.g. host-installed Ollama at http://host.docker.internal:11434). +# NOTE: YAML merge (<<:) merges mappings shallowly and does NOT concatenate +# lists. If a service ever needs additional extra_hosts entries alongside +# this one, inline ALL entries under that service (including +# host.docker.internal:host-gateway) instead of using the anchor — a sibling +# extra_hosts key would silently shadow the anchor's list. x-host-gateway: &host_gateway extra_hosts: - "host.docker.internal:host-gateway" diff --git a/run-platform.sh b/run-platform.sh index f486f75305..40181806f8 100755 --- a/run-platform.sh +++ b/run-platform.sh @@ -35,10 +35,24 @@ check_dependencies() { fi if ! docker info >/dev/null 2>&1; then echo "$red_text""Cannot connect to the Docker daemon.""$default_text" - echo " - Check group membership: getent group docker" - echo " - Add your user to it: sudo usermod -aG docker \$USER" - echo " - Activate in current shell: newgrp docker" - echo " - For new shells, a full desktop logout (not just terminal close) is required." + case "$(uname -s)" in + Linux*) + echo " On Linux (daemon access via the 'docker' group):" + echo " - Check group membership: getent group docker" + echo " - Add your user to it: sudo usermod -aG docker \$USER" + echo " - Activate in current shell: newgrp docker" + echo " - For new shells, a full desktop logout (not just terminal close) is required." + ;; + Darwin*) + echo " On macOS: ensure Docker Desktop is running (whale icon in the menu bar)." + ;; + MINGW*|MSYS*|CYGWIN*) + echo " On Windows: ensure Docker Desktop is running and WSL integration is enabled if applicable." + ;; + *) + echo " Ensure the Docker daemon is running and your user can reach its socket." + ;; + esac exit 1 fi # For 'docker compose' vs 'docker-compose', see https://stackoverflow.com/a/66526176. diff --git a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py index 5fec370219..7c7e8ea076 100644 --- a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py +++ b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py @@ -44,6 +44,11 @@ def get_storage(storage_type: StorageType, env_name: str) -> FileStorage: f"Env var '{env_name}' is not valid JSON: {e}. " f"Expected: {EnvHelper.ENV_CONFIG_FORMAT}" ) from e + if not isinstance(file_storage_creds, dict): + raise FileStorageError( + f"Env var '{env_name}' must be a JSON object. " + f"Expected: {EnvHelper.ENV_CONFIG_FORMAT}" + ) try: provider = FileStorageProvider(file_storage_creds[CredentialKeyword.PROVIDER]) credentials = file_storage_creds.get(CredentialKeyword.CREDENTIALS, {}) @@ -56,9 +61,12 @@ def get_storage(storage_type: StorageType, env_name: str) -> FileStorage: else: raise NotImplementedError() return file_storage - except KeyError as e: - logger.error(f"Required credentials are missing in the env: {str(e)}") + except (KeyError, TypeError, ValueError) as e: + logger.error(f"Invalid storage configuration in env: {str(e)}") logger.error(f"The configuration format is {EnvHelper.ENV_CONFIG_FORMAT}") - raise e + raise FileStorageError( + f"Invalid storage configuration in env var '{env_name}': {e}. " + f"Expected: {EnvHelper.ENV_CONFIG_FORMAT}" + ) from e except FileStorageError as e: raise e From c76437edd27d3a2c9858e48f0bbf85af4861bebc Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 24 Jun 2026 10:32:52 +0530 Subject: [PATCH 3/5] [FIX] Address Jaseem's PR review nits - env_helper.py: narrow except (KeyError, ValueError) to the FileStorageProvider resolution only; constructor / fsspec exceptions now propagate untouched instead of being mislabeled as env config errors. - env_helper.py: drop dead `except FileStorageError: raise e` (inner try doesn't catch FileStorageError, and `raise e` would reset traceback). - workers/sample.env: refresh comment to match new FileStorageError behavior. - backend/sample.env: unquote REMOTE_PROMPT_STUDIO_FILE_PATH for consistency with workers/sample.env. Co-Authored-By: Claude Opus 4.7 Claude-Session: https://claude.ai/code/session_01Br691aZbhfcB4xdswrUjuw --- backend/sample.env | 2 +- .../unstract/sdk1/file_storage/env_helper.py | 21 +++++++------------ workers/sample.env | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/backend/sample.env b/backend/sample.env index cb7c75dd3f..743a31de8e 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -202,7 +202,7 @@ API_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_ur #Remote storage related envs PERMANENT_REMOTE_STORAGE='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' TEMPORARY_REMOTE_STORAGE='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' -REMOTE_PROMPT_STUDIO_FILE_PATH="unstract/prompt-studio-data" +REMOTE_PROMPT_STUDIO_FILE_PATH=unstract/prompt-studio-data # Storage Provider for Tool registry TOOL_REGISTRY_STORAGE_CREDENTIALS='{"provider":"local"}' diff --git a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py index 7c7e8ea076..3368faf218 100644 --- a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py +++ b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py @@ -51,22 +51,17 @@ def get_storage(storage_type: StorageType, env_name: str) -> FileStorage: ) try: provider = FileStorageProvider(file_storage_creds[CredentialKeyword.PROVIDER]) - credentials = file_storage_creds.get(CredentialKeyword.CREDENTIALS, {}) - if storage_type == StorageType.PERMANENT: - file_storage = PermanentFileStorage(provider=provider, **credentials) - elif storage_type == StorageType.SHARED_TEMPORARY: - file_storage = SharedTemporaryFileStorage( - provider=provider, **credentials - ) - else: - raise NotImplementedError() - return file_storage - except (KeyError, TypeError, ValueError) as e: + except (KeyError, ValueError) as e: logger.error(f"Invalid storage configuration in env: {str(e)}") logger.error(f"The configuration format is {EnvHelper.ENV_CONFIG_FORMAT}") raise FileStorageError( f"Invalid storage configuration in env var '{env_name}': {e}. " f"Expected: {EnvHelper.ENV_CONFIG_FORMAT}" ) from e - except FileStorageError as e: - raise e + credentials = file_storage_creds.get(CredentialKeyword.CREDENTIALS, {}) + if storage_type == StorageType.PERMANENT: + return PermanentFileStorage(provider=provider, **credentials) + elif storage_type == StorageType.SHARED_TEMPORARY: + return SharedTemporaryFileStorage(provider=provider, **credentials) + else: + raise NotImplementedError() diff --git a/workers/sample.env b/workers/sample.env index 7ade3c7f57..7d9a70721f 100644 --- a/workers/sample.env +++ b/workers/sample.env @@ -317,7 +317,7 @@ WORKFLOW_EXECUTION_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials" API_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' # Remote storage for Prompt Studio / IDE flows. Must match backend/sample.env. -# Required by executor and ide-callback workers; missing values crash json.loads in EnvHelper. +# Required by executor and ide-callback workers; missing/empty values raise FileStorageError in EnvHelper.get_storage(). PERMANENT_REMOTE_STORAGE='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' TEMPORARY_REMOTE_STORAGE='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' REMOTE_PROMPT_STUDIO_FILE_PATH=unstract/prompt-studio-data From 1ca86d103dfe85fbd3e51f086e621f080e4834b1 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:42:10 +0530 Subject: [PATCH 4/5] Update unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com> --- unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py index 3368faf218..16f6ab5ebf 100644 --- a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py +++ b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py @@ -59,6 +59,11 @@ def get_storage(storage_type: StorageType, env_name: str) -> FileStorage: f"Expected: {EnvHelper.ENV_CONFIG_FORMAT}" ) from e credentials = file_storage_creds.get(CredentialKeyword.CREDENTIALS, {}) + if not isinstance(credentials, dict): + raise FileStorageError( + f"Env var '{env_name}' field '{CredentialKeyword.CREDENTIALS}' must be a JSON object. " + f"Expected: {EnvHelper.ENV_CONFIG_FORMAT}" + ) if storage_type == StorageType.PERMANENT: return PermanentFileStorage(provider=provider, **credentials) elif storage_type == StorageType.SHARED_TEMPORARY: From f7403dfda6d77a805b9fbbf52834e7a049d2967a Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Wed, 24 Jun 2026 12:04:04 +0530 Subject: [PATCH 5/5] [FIX] Split long f-string in EnvHelper.get_storage to satisfy ruff E501 The coderabbit suggestion added an isinstance(credentials, dict) guard whose error message exceeded the 90-char line limit. Split the f-string across two adjacent string literals to keep behavior identical. Co-Authored-By: Claude Opus 4.7 Claude-Session: https://claude.ai/code/session_01Br691aZbhfcB4xdswrUjuw --- unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py index 16f6ab5ebf..830e7a1c34 100644 --- a/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py +++ b/unstract/sdk1/src/unstract/sdk1/file_storage/env_helper.py @@ -61,8 +61,8 @@ def get_storage(storage_type: StorageType, env_name: str) -> FileStorage: credentials = file_storage_creds.get(CredentialKeyword.CREDENTIALS, {}) if not isinstance(credentials, dict): raise FileStorageError( - f"Env var '{env_name}' field '{CredentialKeyword.CREDENTIALS}' must be a JSON object. " - f"Expected: {EnvHelper.ENV_CONFIG_FORMAT}" + f"Env var '{env_name}' field '{CredentialKeyword.CREDENTIALS}' " + f"must be a JSON object. Expected: {EnvHelper.ENV_CONFIG_FORMAT}" ) if storage_type == StorageType.PERMANENT: return PermanentFileStorage(provider=provider, **credentials)