Skip to content

UN-3445 [MISC] backmerge main into feat — prompt-service collapse + UN-3735 .value fix#2181

Merged
muhammad-ali-e merged 24 commits into
feat/UN-3445-pg-queue-integrationfrom
backmerge/main-into-feat-20260714
Jul 15, 2026
Merged

UN-3445 [MISC] backmerge main into feat — prompt-service collapse + UN-3735 .value fix#2181
muhammad-ali-e merged 24 commits into
feat/UN-3445-pg-queue-integrationfrom
backmerge/main-into-feat-20260714

Conversation

@muhammad-ali-e

Copy link
Copy Markdown
Contributor

What

Backmerge main (23 commits) into the PG-queue integration branch feat/UN-3445-pg-queue-integration, keeping it current before the epic feat→main PR.

Notable incoming changes:

  • prompt-service → backend collapse (renames + prompt.Dockerfile removal)
  • UN-3735 destination-DB status .value fix (database_utils.py)
  • New backend test suites (api_v2, connector_v2, workflow_v2, usage_v2, …)
  • CI workflow updates

Conflicts (2 — both resolved deterministically)

File Resolution Why
workers/tests/test_legacy_executor_scaffold.py Accept main's deletion of test_no_flask_import Main (#2115) removed it as an obsolete Flask-era guard while de-flaking the suite — flask is not a workers dependency. feat had only wrapped the same reload-based test in a namespace snapshot. Net: −1 test, suite still collects (20 tests).
workers/uv.lock Take main's litellm 1.90.3 (from 1.85.1) Forward bump. uv lock --check confirms the lock is consistent with the merged manifest (268 packages, no update needed).

Everything else auto-merged (settings/base.py, docker-compose, tests/groups.yaml, rig/cli.py, root uv.lock, …).

Verification

  • uv lock --check (workers) — clean
  • py_compile + pytest --collect-only on the resolved test file — 20 tests collect
  • No conflict markers remain anywhere in the tree

🤖 Generated with Claude Code

jaseemjaskp and others added 24 commits June 26, 2026 18:13
GenericLoader's .center uses width/height: inherit, which collapsed to a
narrow column when used as LazyOutlet's content-area Suspense fallback (the
loader text wrapped one word per line beside the sidebar). Wrap the fallback in
a full-width, flex-centered .lazyOutletFallback box and pin .center to width:100%
so it renders centered at its natural size. The top-level loader in Router.jsx
is unaffected (it doesn't use this wrapper).
* UN-3185: Pin LLM profile form submit button as a sticky footer

In the prompt-studio Settings modal, the LLM-profile Add/Edit form put its
submit button (Update/Add) as the last element in normal flow inside a fixed
800px scroll column. When the form is tall — e.g. Advanced Settings expanded —
the button flowed past the modal's visible area and appeared to overflow.

- Make the button a sticky footer (position: sticky; bottom: 0) with a solid
  themed background so it stays pinned at the bottom of the scroll column and
  fields scroll beneath it.
- The form root's .settings-body-pad-top declared overflow-y: auto with no
  bounded height — a dead nested scroll context that would stop the sticky
  footer from pinning to the real scroller (.conn-modal-col). Override it back
  to visible for this form (scoped via .add-llm-profile-scroll-root).

Verified the sticky pinning against the real CSS rules; build green, biome clean.

* Apply suggestion from @greptile-apps[bot]

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Signed-off-by: Jaseem Jas <89440144+jaseemjaskp@users.noreply.github.com>

---------

Signed-off-by: Jaseem Jas <89440144+jaseemjaskp@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
)

* [MISC] Decommission prompt-service, old tools, and SDK1 prompt module (Phase 5)

Remove prompt-service source, Dockerfiles, and docker-compose entries.
Remove tools/classifier, tools/structure, tools/text_extractor directories.
Remove SDK1 prompt.py module and its tests.
Clean up PROMPT_HOST/PROMPT_PORT from backend settings, sample envs,
docker configs, and CI workflows. Remove prompt-service from uv-lock
scripts and production build workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* [MISC] Remove prompt-service from tox.ini env_list

The prompt-service directory was deleted in the prior commit but tox.ini
still referenced it, which would break CI test runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* UN-2888 [FIX] Add hook for setting default triad for invited users (#1877)

* [FIX] Add hook for setting default adapters for invited users

Add setup_default_adapters_for_user() hook to AuthenticationService
and call it from set_user_organization() when an invited user joins
an existing organization. This allows the cloud plugin to set up
default triad adapters (LLM, embedding, vector DB, x2text) for
invited users, fixing silent failures in API deployment creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update backend/account_v2/authentication_controller.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Signed-off-by: Praveen Kumar <praveen@zipstack.com>

* [FIX] Improve log message for setup_default_adapters_for_user

Address review comment: log user email and explain that default
adapters will not be set when the method is not implemented.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [MISC] Rename Default Triad to Default LLM Profile in UI

Update display label from "Default Triad" to "Default LLM Profile"
in the page heading and side navigation menu.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Praveen Kumar <praveen@zipstack.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Deepak K <89829542+Deepak-Kesavan@users.noreply.github.com>

* UN-3465 [FIX] Wrap set_user_organization in transaction.atomic (#1954)

* [FIX] Wrap set_user_organization in transaction.atomic

The new-org branch creates the org row, then calls frictionless onboarding
and the initial platform key. Failures mid-flow leave an orphan org with no
adapters or key, and subsequent logins skip onboarding entirely (gated on
new_organization). Atomic ensures the org rolls back on any failure so
retries get a clean fresh-org path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [MISC] Worktree skill — use --no-track to prevent accidental main pushes

Without --no-track, a later `git push -u origin <branch>` can be reported
by the server as also fast-forwarding main, landing commits on main.

* [FIX] Use logger.exception in authorization_callback

Preserves the traceback when the OAuth callback hits the safety-net
catch. Behaviour unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Athul <89829560+athul-rs@users.noreply.github.com>
Co-authored-by: vishnuszipstack <117254672+vishnuszipstack@users.noreply.github.com>

* UN-3386 [FEAT] Add Prompt Studio HITL change indicator plugin slot (#1930)

* UN-3386 [FEAT] Add Prompt Studio HITL change indicator plugin slot

Wires up the host-side hooks for the prompt-change-indicator plugin
(implementation lives in unstract-cloud): a dynamic-import slot in
the prompt card Header for the indicator button, and a route at
:orgName/review/readonly/:documentId for the read-only audit view.
Both gates fall through gracefully when the plugin is absent (OSS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* UN-3386 [FIX] Warn when ReadOnlyReviewPage loads without ReviewLayout

Addresses review feedback: the readonly route nests inside ReviewLayout
(manual-review plugin), so a deployment that ships prompt-change-indicator
without manual-review would silently fail to register the route. Log a
console.warn in that case to make the misconfiguration discoverable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* UN-3386 [FIX] Surface real plugin import errors in route loader

Bare catch in the prompt-change-indicator dynamic import was swallowing
syntax/runtime errors in the plugin file alongside the expected
"plugin missing in OSS" case. Detect the missing-module messages
explicitly and console.error anything else so a broken cloud plugin
no longer disables the readonly route silently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add a dedicated OpenAI-compatible LLM adapter (#1895)

* Add OpenAI-compatible LLM adapter

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review feedback for custom OpenAI adapter

* Fix import formatting after rebase

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address follow-up review comments for OpenAI-compatible adapter

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Refine OpenAI compatible adapter schema naming

* Reject empty model string in OpenAICompatibleLLMParameters

validate_model previously produced "custom_openai/" for an empty model,
surfacing as a confusing LiteLLM error at call time. Match the existing
GeminiLLMParameters.validate_model pattern: strip whitespace, raise
ValueError on empty input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert SCHEMA_PATH plumbing; rename schema to custom_openai.json

Addresses Ritwik's review feedback. The new BaseAdapter.SCHEMA_PATH
class variable and the conditional branch in get_json_schema() are
unnecessary: OpenAICompatibleLLMAdapter.get_provider() returns
"custom_openai", and the default path resolution already builds
…/llm1/static/{get_provider()}.json. Renaming the schema file lets
the default lookup find it and keeps the base class untouched, which
is the convention every other adapter follows.

- Rename openai_compatible.json -> custom_openai.json
- Drop SCHEMA_PATH class var and the if-None branch from BaseAdapter
- Drop SCHEMA_PATH override (and unused os/ClassVar imports) from
  OpenAICompatibleLLMAdapter
- Update test_openai_compatible_schema_is_loadable to read schema via
  get_json_schema() instead of touching SCHEMA_PATH directly

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Hari John Kuriakose <hari@zipstack.com>
Co-authored-by: Chandrasekharan M <chandrasekharan@zipstack.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Athul <athul@zipstack.com>
Co-authored-by: Athul <89829560+athul-rs@users.noreply.github.com>
Co-authored-by: vishnuszipstack <117254672+vishnuszipstack@users.noreply.github.com>

* ReverseMerge: V0.163.4 hotfix (#1980)

* [HOTFIX] Use importlib.util.find_spec for pluggable worker discovery (#1918)

* [FIX] Use importlib.util.find_spec for pluggable worker discovery

_verify_pluggable_worker_exists() previously checked for the literal file
`pluggable_worker/<name>/worker.py` on disk, which breaks when the plugin
has been compiled to a .so (Nuitka, Cython, or any C extension) — the
module is perfectly importable but the pre-check rejects it because only
the .py extension is considered.

Replace the filesystem check with importlib.util.find_spec(), which is
Python's standard way to ask "is this module resolvable by the import
system?". It honors every registered finder — source .py, compiled .so,
bytecode .pyc, namespace packages, zipimports — so the function now
matches what its docstring claims: verifying the module can be loaded,
not that a specific file extension is present.

Behavior is preserved for existing deployments:
- Images with no `pluggable_worker/<name>/` subpackage → find_spec
  raises ModuleNotFoundError (ImportError subclass) → returns False.
- Images with source .py → find_spec resolves the .py → returns True.
- Images with compiled .so → find_spec resolves the .so → returns True.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [FIX] Handle ValueError from find_spec in pluggable worker verification

Greptile-flagged edge case: importlib.util.find_spec() can raise
ValueError (not just ImportError) when sys.modules has a partially
initialised module entry with __spec__ = None from a prior failed import.
Broaden the except to catch both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [FIX] Resolve api-deployment worker directory from enum import path

worker.py:452 did worker_type.value.replace("-", "_") to derive the
on-disk dir name. All WorkerType enum values already use underscores,
so the replace was a no-op; for API_DEPLOYMENT whose dir is
"api-deployment" (hyphen), it resolved to "api_deployment" and the
os.path.exists() check failed. Boot then logged a spurious
"❌ Worker directory not found: /app/api_deployment" at ERROR level.

The task registration path (builder + celery autodiscover via
to_import_path) is unaffected, so this was purely log noise — but
noise at ERROR level that masks real failures in log scans.

Fix: derive the directory from the authoritative to_import_path()
which already handles the hyphen case (api_deployment -> api-deployment).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [HOTFIX] Add IAM Role / Instance Profile auth mode to AWS Bedrock adapter (#1944)

* [FEAT] Allow Bedrock to fall through to boto3's default credential chain

Match the S3/MinIO connector pattern: when AWS access keys are left blank
on the Bedrock LLM and embedding adapter forms, drop them from the kwargs
dict so boto3's default credential chain handles authentication. This
unlocks IAM role / instance profile / IRSA / AWS Profile scenarios on
hosts that already have ambient AWS credentials (e.g. EKS workers with
IRSA, EC2 with an instance profile).

- llm1/static/bedrock.json: clarify access-key descriptions to mention
  IRSA and instance profile (already non-required at v0.163.2 base).
- embedding1/static/bedrock.json: drop aws_access_key_id and
  aws_secret_access_key from top-level required; same description fix;
  expose aws_profile_name for parity with the LLM form.
- base1.py: AWSBedrockLLMParameters and AWSBedrockEmbeddingParameters
  now strip empty access-key values from the validated kwargs before
  returning, so empty strings don't override boto3's default chain.
  AWSBedrockEmbeddingParameters fields gain explicit None defaults
  and an aws_profile_name field.

Backward-compatible: existing adapters with access keys filled in
continue to work unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [FEAT] Add Authentication Type selector to Bedrock adapter form

Add an explicit `auth_type` selector with two options, making the auth
choice clear to users:

- "Access Keys" (default): existing flow, keys required
- "IAM Role / Instance Profile (on-prem AWS only)": no fields; relies on
  boto3's default credential chain (IRSA on EKS, task role on ECS,
  instance profile on EC2). Description on the selector explicitly notes
  this option is only for AWS-hosted Unstract deployments.

The form-only auth_type field is stripped before LiteLLM validation in
both AWSBedrockLLMParameters.validate() and AWSBedrockEmbeddingParameters.
validate(). Empty access keys continue to be stripped so boto3 falls
through to the default chain even when the access_keys arm is selected
without values (matches the S3/MinIO connector pattern).

Backward-compatible: legacy adapters without auth_type behave as
"Access Keys" mode (the default), and existing keys are forwarded
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [REVIEW] Address Bedrock auth_type review feedback

Fixes the P0/P1 issues raised by greptile-apps and jaseemjaskp on
PR #1944.

Behaviour fixes:
- Stale-key leak in IAM Role mode: switching an existing adapter from
  Access Keys to IAM Role would carry truthy stored access keys through
  the strip-empty-only loop, so boto3 silently authenticated with the
  old long-lived credentials instead of falling through to the host's
  IRSA / instance-profile identity. Both LLM and embedding paths were
  affected.
- Silent acceptance of unknown auth_type: a typo (e.g. "access_key") or
  a malformed payload from a non-UI client passed through the dict
  comprehension untouched, with no enum guard.
- Cross-field validation gap: explicit Access Keys mode with blank or
  whitespace-only values silently fell through to the default
  credential chain instead of surfacing the misconfiguration.

Implementation:
- Add a module-level _resolve_bedrock_aws_credentials helper used by
  both AWSBedrockLLMParameters.validate() and AWSBedrock
  EmbeddingParameters.validate(), so the auth-type contract is
  expressed once.
  - Validates auth_type against an allowlist (None | "access_keys" |
    "iam_role"); raises ValueError on anything else.
  - iam_role: unconditionally drops aws_access_key_id and
    aws_secret_access_key.
  - access_keys (explicit): requires non-blank values; raises ValueError
    if either is empty or whitespace-only.
  - Legacy (auth_type absent): retains the lenient strip behaviour so
    pre-PR adapter configurations continue to deserialise unchanged.
- Restore aws_region_name as required (no `= None` default) on
  AWSBedrockEmbeddingParameters; only credentials may legitimately be
  absent.
- Drop the orphan aws_profile_name field from
  embedding1/static/bedrock.json: it was added for parity with the LLM
  form but lives outside the auth_type oneOf and contradicts the
  selector's "no further input" semantics. The LLM form already had
  aws_profile_name pre-PR and is left alone for backwards compatibility.

Tests:
- New tests/test_bedrock_adapter.py covers 15 cases across LLM and
  embedding adapters: legacy-no-auth-type, explicit access_keys with
  valid/blank/whitespace keys, iam_role with stale/no keys, unknown
  auth_type rejection, cross-field validation, and preservation of
  unrelated params (model_id, aws_profile_name, region, thinking).

Skipped (P2 nice-to-have):
- Comment-scope clarification, MinIO reference rewording,
  validate-mutates-caller'\''s-dict, and the LLM form description nit
  about aws_profile_name visibility. These don'\''t change behaviour
  and can be addressed in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* [HOTFIX] Bump litellm to 1.83.10 from PyPI to clear CVE-2026-42208 (#1976)

Hotfix for cloud v0.159.3 (OSS v0.163.4). Customer scanner flagged
litellm 1.82.3 for CVE-2026-42208 (SQL injection in litellm proxy auth
path, affects 1.81.16-1.83.6). We do not use litellm.proxy, but
vulnerability scanners flag the installed package regardless of which
code path is reachable.

Bump to 1.83.10 — the exact version recommended by the upstream advisory
(v1.83.10-stable) and the smallest jump that clears the CVE range while
keeping python-dotenv==1.0.1 compatible (1.83.14 would force bumping
python-dotenv across 7+ pyproject.toml files). Only tiktoken needed to
move 0.9 -> 0.12 to satisfy litellm's pin.

Switch source back to PyPI now that the PyPI quarantine is over,
reversing the temporary fork in #1873.

Cohere embed timeout patch: verified that
litellm/llms/cohere/embed/handler.py is byte-identical between v1.82.3,
v1.83.10-stable, and v1.83.14-stable (the timeout-not-forwarded bug
fixed in #1848 is still present upstream — BerriAI/litellm#14635 remains
OPEN). Version guard bumped 1.82.3 -> 1.83.10; 6/6 patch tests pass on
the new version, confirming the monkey-patch still binds correctly.

Other cleanup from #1873:
- Drop git apt-install from worker-unified and tool Dockerfiles (no
  git-sourced deps remain in any uv.lock)
- Bump tool versions: structure 0.0.100 -> 0.0.101,
  classifier 0.0.79 -> 0.0.80, text_extractor 0.0.75 -> 0.0.76

Note on root uv.lock churn: the v0.163.4 root uv.lock had a pre-existing
corruption (banks v2.4.1 entry pointing at banks-2.2.0 wheel) that
blocked incremental resolution. Regenerated from scratch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [FIX] Align cohere patch docstring with version-guard semantics

Reviewer flagged that the docstring claimed the patch is "confirmed in
every release between 1.82.3 and 1.83.14-stable", but the guard at
_PATCHED_LITELLM_VERSION activates only on the exact pinned version. A
future maintainer reading the old text could reasonably expect bumping
to e.g. 1.83.11 to keep the fix active; in reality it silently turns
off.

Rewritten to reference _PATCHED_LITELLM_VERSION as the single source of
truth and to drop the rot-prone "as of 2026-05-20" calendar date.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* UN-3476 [FIX] Revert atomic wrap on set_user_organization (#1977)

The atomic wrap from #1954 uncommits the new org row when
frictionless_onboarding HTTP-calls the LLMW portal mid-transaction.
The portal runs on a separate DB session and under READ COMMITTED
cannot see the uncommitted row, so the call returns 400 and the
caller silently persists an adapter with an empty unstract_key.
Every new signup since 2026-05-19 09:47 UTC ships a broken
free-trial X2Text adapter (401 on first OCR).

Hotfix only — Phase 2 (UN-3476) restructures the function so the
atomic guarantee is reapplied around just the pure-DB writes, with
HTTP and non-DB side effects moved outside the transaction.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Restore text_extractor tool removed in Phase 5 decommission

The Phase 5 decommission commit removed classifier, structure,
text_extractor, and prompt-service. However, text_extractor is still
in active use by customers. This surgically restores only the
text_extractor tool while keeping the other decommissions in place.

- Restore tools/text_extractor/ directory (14 files from origin/main)
- Add tool-text_extractor back to docker-compose.build.yaml
- Add tool-text-extractor back to docker-tools-build-push.yaml workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Restore classifier tool removed in Phase 5 decommission

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unit-prompt-service group from test rig manifest

The prompt-service directory was deleted in the decommission PR, but
the test rig groups.yaml still referenced it, causing CI to fail with
"workdir does not exist" during validate and integration steps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove deleted prompt-service and structure tool refs from bump script

prompt-service/ and tools/structure/ are deleted by this PR, so
remove their variables, reset_file calls, and the entire
update_structure_tool_version function from bump_sdk_v0_version.sh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix stale references from decommissioned components

- Fix tool-text_extractor image name to tool-text-extractor in
  docker-compose.build.yaml to match CI, registry, and cloud naming
- Remove stale tool-structure from run-platform.sh ignore list
- Drop prompt-service from is_retryable_error docstring in retry_utils.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Trigger CI re-run

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Signed-off-by: Praveen Kumar <praveen@zipstack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Praveen Kumar <praveen@zipstack.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Deepak K <89829542+Deepak-Kesavan@users.noreply.github.com>
Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com>
Co-authored-by: Athul <89829560+athul-rs@users.noreply.github.com>
Co-authored-by: vishnuszipstack <117254672+vishnuszipstack@users.noreply.github.com>
Co-authored-by: jimmy <ziming_zhu2002@163.com>
Co-authored-by: Hari John Kuriakose <hari@zipstack.com>
Co-authored-by: Chandrasekharan M <chandrasekharan@zipstack.com>
Co-authored-by: Athul <athul@zipstack.com>
…+ Edit LLM Profile modal layout (#2128)

* fix(frontend): bundle all CSS into one file to fix lazy-load cascade order

Code-splitting (#2114) injects per-route CSS at navigation time, so
equal-specificity rules across components resolve in load order — breaking
the Edit LLM Profile form and Execution Logs header layout (UN-3185).
cssCodeSplit:false restores a single deterministic stylesheet; JS splitting
is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DsoHbMN7kTTWwcVC6NNkg

* perf(frontend): gzip static assets (css/js/fonts), not just html

nginx `gzip on` only compresses text/html by default, so JS/CSS shipped
uncompressed. Add gzip_types so the single CSS bundle (~341KB -> ~57KB) and
all JS chunks compress on the wire — offsets the cssCodeSplit:false upfront
cost and speeds every asset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DsoHbMN7kTTWwcVC6NNkg

* fix(frontend): clean up Edit LLM Profile modal padding & drop dead scroll classes

Remove settings-body-pad-top + add-llm-profile-scroll-root from the form root
(the shared overflow-y:auto added a nested scroll context that #2119 only
patched over). The sticky footer now pins directly to .conn-modal-col. Replace
the dead override with a :has()-scoped padding so .conn-modal-form-pad-left is
adjusted for this panel alone — sibling settings panels and the connector
dialog are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DsoHbMN7kTTWwcVC6NNkg

* perf(frontend): gzip_proxied any so assets compress behind the LB

nginx skips gzip for proxied requests by default (gzip_proxied off); the GKE
load balancer adds a Via header to every request, so static assets shipped
uncompressed despite gzip_types. Verified gzip works direct-to-pod.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DsoHbMN7kTTWwcVC6NNkg

* fix(frontend): remove inner scrollbar on Edit LLM Profile modal

The settings row is hard-fixed at 800px, forcing the form's column
(.conn-modal-col) to scroll once Advanced Settings expands. Let the row size to
the form so the modal grows instead. Scoped via :has to the LLM profile panel —
the other settings panels and the connector dialog share .conn-modal-row/-col
and are untouched (so the shared class stays; it is not unused).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DsoHbMN7kTTWwcVC6NNkg

* docs: tighten code comments in CSS/vite/nginx changes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DsoHbMN7kTTWwcVC6NNkg

* style: drop comments from AddLlmProfile.css changes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DsoHbMN7kTTWwcVC6NNkg

* fix: drop pre-compressed font types from gzip_types

WOFF2 (Brotli) and WOFF (zlib) are already compressed; re-gzipping wastes
CPU for no size gain. application/font-woff also never matches nginx's
mime.types (.woff -> font/woff), so it was a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DsoHbMN7kTTWwcVC6NNkg

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… detail + HITL blank page) (#2135)

* UN-3185 [FIX] Restore global Prism for prismjs add-ons (Prompt Studio detail + HITL blank page)

The Vite production build tree-shakes the bare `import "prismjs"` in
CombinedOutput.jsx (a side-effect-only import with no used bindings), so
nothing installs the global `Prism` that `prismjs/components/prism-json`
and the line-numbers plugin reference. Those add-ons then throw
`ReferenceError: Prism is not defined` at module evaluation, crashing the
Combined Output viewer shared by the Prompt Studio detail page and the
HITL / manual-review page (both render blank). The old CRA/webpack build
did not tree-shake it, so the regression only surfaces on the Vite build.

Add a dedicated prismSetup module that imports Prism core with a *used*
binding (survives tree-shaking) and pins it on globalThis, and import it
before the add-ons in CombinedOutput so the global is guaranteed present
by the time the add-on modules evaluate.

Verified against a production `vite build`: the emitted chunk now runs
`globalThis.Prism = <core>` immediately before `Prism.languages.json = ...`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3185 [FIX] Simplify globalThis guard in prismSetup (review)

Drop the always-true `typeof globalThis !== "undefined"` check — globalThis
is universally available in any ESM/Vite target. Addresses Greptile review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3185 [FIX] Install global Prism unconditionally + correct rationale (review)

Address PR review:
- silent-failure-hunter: a `!globalThis.Prism` guard could leave a different,
  pre-existing Prism in place, so the add-ons extend one instance while
  JsonView's highlightAll() reads another -> JSON silently unhighlighted.
  Assign unconditionally so the global is provably our core instance.
- comment-analyzer: the prior comment blamed evaluation order yet relied on it
  to justify the fix. Rewrite: relying on prismjs core's self-install is
  unreliable under code-splitting; the explicit globalThis assignment (from
  first-party code imported before the add-ons) is the deterministic fix.

Rebuilt: emitted chunk runs `globalThis.Prism = <core>` immediately before
`Prism.languages.json = ...`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3185 [FIX] Install global Prism eagerly at app entry (fixes HITL too)

Verified against the actual on-prem image (built with the manual-review
plugin): the per-component prismSetup import did NOT fix it. Because
manual-review's ResultEditor also imports `prismjs/components/prism-json`,
Rollup hoists that add-on into a SHARED lazy chunk (PdfViewer), separate
from CombinedOutput's chunk where prismSetup ran — with no ordering
guarantee between two lazy chunks, so the add-on still evaluated before the
global was installed. My local OSS build masked this: with no manual-review
plugin, prism-json wasn't shared and stayed in CombinedOutput's chunk.

Fix: import prismSetup EAGERLY from index.jsx so `globalThis.Prism` is
installed at bootstrap, before any lazy chunk (including the shared add-on
chunk) can load. Robust regardless of how Rollup hoists prism-json.

Reproduced the shared-chunk hoist locally (two independent lazy prism-json
importers) and confirmed: prism-json lands in its own lazy chunk while
globalThis.Prism stays in the eager entry <script type=module>, so the
global is always installed first. Fixes both the Prompt Studio detail page
and the HITL review page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…2034)

* UN-2265 Display platform version in the frontend profile page

Bake the build VERSION into the frontend image as
UNSTRACT_APPS_VERSION (same pattern as backend.Dockerfile), surface it
through the runtime config injected at container start, and show it as
a 'Platform Version' field on the profile page. Production CI already
passes *.args.VERSION to all images via docker bake, so published
images carry the real version on every deployment target; the field is
hidden when no version is available (e.g. local npm start).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* UN-2265 Address review: empty VERSION default, escape value in runtime config

- ARG VERSION defaults to empty so images built without a version hide
  the field instead of showing 'dev'
- Escape backslashes/quotes before embedding the version in the
  generated JS

* UN-2265 [FIX] Source platform version from runtime env, not baked image

RC->stable promotion re-tags images (it does not rebuild), so a version baked
into the frontend image at build time would stay frozen at the rc tag after
promotion. Drop the frontend.Dockerfile ARG/ENV baking and instead supply
UNSTRACT_APPS_VERSION from docker-compose using ${VERSION} -- the same value
that already tags the image, so the displayed version always matches what runs.

Addresses review from @ritwik-g / @ejhari (version must come dynamically from
compose in OSS and the Helm chart in Enterprise).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* [MISC] Add 'auto' version bump to OSS create-release

Adds an 'auto' choice (now the default) to create-release.yaml that picks
the OSS version bump from merged PR titles: a [FEAT]/[GATED-FEAT] PR merged
since the last release -> minor, otherwise patch. These are the only feature
PR types in the contribution guide, so this matches the documented SemVer
intent without any new labeling.

Details:
- 'auto' resolves in a new "Resolve auto bump" step (main mode only; hotfix
  lines stay patch-only). Fail-safe is always patch, so a compare/PR query
  hiccup never over-bumps the public version.
- Hotfix-mode input validation now accepts 'auto' (maps to patch).
- compute-version consumes the resolved bump; dry-run/final summaries show
  "auto -> minor/patch" for an explicit audit trail.
- 'auto' never selects major — that stays behind the confirm_major gate.

Validated by replaying the classifier over the last 13 real releases: 12/13
matched the human bump; the lone diff was a discretionary minor with no FEAT
PR (recoverable via manual minor override, which the notice points to).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [MISC] Address review: PR-read perms, patch fallback, truncation warning

- Add `pull-requests: read` to the job (the permissions block sets unlisted
  scopes to none, so `gh pr list` would 403 and 'auto' would silently always
  fall back to patch). [CodeRabbit]
- BUMP_TYPE falls back to 'patch' instead of 'auto' when resolved is empty,
  avoiding a latent "Unknown bump type: 'auto'" job failure. [Greptile]
- Surface a warning when the merged-PR query hits the 200-result cap instead
  of silently under-counting FEAT PRs. [Greptile]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [MISC] Harden auto-bump resolve step (review follow-ups)

From a multi-agent review of the auto-bump step:

- Guard the jq parses: malformed/non-array stdout now degrades to patch
  instead of crashing the job under set -e/pipefail (upholds the "never fail
  the release outright" invariant). Also validates TOTAL/FEAT_COUNT are numeric
  before arithmetic.
- Detect gh failure by exit status (if ! PR_JSON=$(...)) and surface captured
  stderr, so a permanent 403 (e.g. dropped pull-requests scope) is diagnosable
  rather than a cause-free warning that silently patches forever.
- Add '// empty' to the published_at lookup for consistency with get-latest,
  so a JSON null can't leak through as the literal "null".
- Only emit the truncation warning when no FEAT was found within the cap (the
  only case where truncation could change the outcome).
- Fix an inaccurate comment ("silently" -> "with a warning") and reword the
  self-referential permissions comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ailure (#2120)

* UN-3648: Mark API deployment execution ERROR on synchronous staging failure

When an API-deployment run failed synchronously at the "Staging files in
API storage" step (add_input_file_to_api_storage, before async dispatch),
the PENDING WorkflowExecution row was never marked ERROR, so the UI showed
the run as stuck/running forever and the real error wasn't surfaced.

Root cause: in api_v2/deployment_helper.py -> execute_workflow(), the
staging call sat outside the try/except. Only execute_workflow_async
failures were handled, and the DB row is marked ERROR by
execute_workflow_async internally -- a path a staging failure never reaches.

Fix:
- Move staging inside the try so synchronous failures are handled.
- In the except, explicitly call update_execution_err() to mark the row
  ERROR with the surfaced reason (the existing handler only did cleanup +
  built an error response, it never marked the DB row).

Add a regression test (sys.modules-stub style, no Django/DB) asserting a
staging exception marks the execution ERROR, releases the rate-limit slot,
and never reaches async dispatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* UN-3648: scope staging error-handling to pre-dispatch failures (review)

Address Greptile P1 + CodeRabbit Major on PR #2120:

- Give the synchronous staging call its own try/except instead of sharing the
  try with execute_workflow_async + post-dispatch processing. Error-marking
  (update_execution_err) now applies only to genuine pre-dispatch failures and
  can no longer overwrite an already-dispatched/completed execution's status.
- Isolate the update_execution_err call in its own try/except so a transient DB
  error while marking ERROR no longer skips slot release and storage cleanup
  (which were unconditional before this PR).
- Restore the dispatch/post-processing except to its original behaviour (slot
  release + storage cleanup only); dispatch failures are already marked ERROR
  internally by execute_workflow_async.

Add a regression test asserting cleanup still runs when update_execution_err
itself raises.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Drop ticket/incident references from in-code comments

Keep code comments focused on explaining the code; ticket and incident context
lives in the commit/PR history instead.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d mode) (#2132)

* UN-3584 [FEAT] Restrict LLM adapter creation to org admins (controlled mode)

Add a per-org 'restrict_llm_adapter_creation' setting (default off). When an
org admin enables it, only organization admins may create LLM adapters:
non-admin create requests are rejected with 403 and a contact-admin message.
Other adapter types and the default-off behavior are unchanged.

- account_v2: new BooleanField on Organization + migration
- tenant_account_v2: admin-only GET/PATCH organization/settings endpoint
- adapter_processor_v2: enforce the gate in AdapterInstanceViewSet.create
- frontend: admin-only toggle in Platform Settings

* UN-3584 [FIX] Gate LLM-restriction toggle to enterprise builds (hide in OSS)

Org admin roles / user management don't exist in OSS, so the controlled-mode
toggle must not render there. Probe for an enterprise-only plugin (absent in
OSS) and show the toggle only on enterprise/cloud builds, mirroring the
plugin-gating idiom in SideNavBar. Backend is already OSS-safe: the flag
defaults off and the create gate never fires.

* UN-3584 [FIX] Address PR review: service-account bypass, modified_by audit, extract gate

- Bypass the LLM-creation gate for service accounts (platform API-key
  sessions), consistent with how the rest of the permission layer treats
  them (Greptile P1).
- Set Organization.modified_by on the settings PATCH so the audit field
  isn't left stale (Greptile P2).
- Extract the controlled-mode check into _enforce_llm_creation_restriction
  to bring AdapterInstanceViewSet.create back under the cognitive-complexity
  limit (SonarQube).

* UN-3584 [FIX] Bind adapter creation to request org (prevent payload org override)

AdapterInstanceSerializer exposes organization via fields=__all__, and
DefaultOrganizationMixin.save only fills it when None — so a payload-supplied
organization would persist. Pass organization=UserContext.get_organization()
to serializer.save() so the row is bound to the same request-scoped org the
controlled-mode check evaluates, closing a per-org restriction bypass
(CodeRabbit security finding).
…mode) (#2145)

* UN-3584 [FEAT] Restrict LLM adapter creation to org admins (controlled mode)

Add a per-org 'restrict_llm_adapter_creation' setting (default off). When an
org admin enables it, only organization admins may create LLM adapters:
non-admin create requests are rejected with 403 and a contact-admin message.
Other adapter types and the default-off behavior are unchanged.

- account_v2: new BooleanField on Organization + migration
- tenant_account_v2: admin-only GET/PATCH organization/settings endpoint
- adapter_processor_v2: enforce the gate in AdapterInstanceViewSet.create
- frontend: admin-only toggle in Platform Settings

* UN-3584 [FIX] Gate LLM-restriction toggle to enterprise builds (hide in OSS)

Org admin roles / user management don't exist in OSS, so the controlled-mode
toggle must not render there. Probe for an enterprise-only plugin (absent in
OSS) and show the toggle only on enterprise/cloud builds, mirroring the
plugin-gating idiom in SideNavBar. Backend is already OSS-safe: the flag
defaults off and the create gate never fires.

* UN-3584 [FIX] Address PR review: service-account bypass, modified_by audit, extract gate

- Bypass the LLM-creation gate for service accounts (platform API-key
  sessions), consistent with how the rest of the permission layer treats
  them (Greptile P1).
- Set Organization.modified_by on the settings PATCH so the audit field
  isn't left stale (Greptile P2).
- Extract the controlled-mode check into _enforce_llm_creation_restriction
  to bring AdapterInstanceViewSet.create back under the cognitive-complexity
  limit (SonarQube).

* UN-3584 [FIX] Bind adapter creation to request org (prevent payload org override)

AdapterInstanceSerializer exposes organization via fields=__all__, and
DefaultOrganizationMixin.save only fills it when None — so a payload-supplied
organization would persist. Pass organization=UserContext.get_organization()
to serializer.save() so the row is bound to the same request-scoped org the
controlled-mode check evaluates, closing a per-org restriction bypass
(CodeRabbit security finding).

* UN-3585 [FEAT] Restrict connector creation to org admins (controlled mode)

* UN-3585 [FIX] Address PR review: split settings validation from mutation; correct org-binding comment

* UN-3585 [FIX] Hoist connector admin check before payload validation (fail-fast)
* UN-3586 [FEAT] Allow platform API key self-rotation via API

Operational automation (RLDatix) needs credential rotation through the API,
but the rotate endpoint was gated by IsOrganizationAdmin, which rejects
service accounts — so a platform API key could not rotate itself.

Add CanRotatePlatformApiKey for the rotate action: session callers still must
be org admins (may rotate any key in the org), while a platform API key
(bearer) may rotate ONLY its own key (pk == request.platform_api_key.id).
Cross-org access remains impossible (auth middleware + org-scoped queryset);
this only relaxes the intra-org gate to self-rotation. read keys still can't
POST (middleware), so only read_write/full_access keys reach rotate.

rotate returns the new key once via PlatformApiKeyDetailSerializer. No model
or migration change.

* UN-3586 [FIX] Move rotate self-check to has_object_permission (PR review)

Relocate the 'key may rotate only its own key' constraint from has_permission
(view-level, via view.kwargs[pk]) to has_object_permission, the idiomatic DRF
location for per-object access control — it receives the already-fetched obj.
has_permission stays as the coarse session-vs-key gate. Behavior is unchanged
(self-rotate 200, cross-key 403): this permission is only used by the rotate
detail action, which calls get_object() and so always triggers the
object-level check. (Greptile)

* UN-3586 [FIX] Allow key-based callers to rotate any key (drop self-only)

Empirically confirmed on staging that rotating a platform API key via the API
(bearer token) is blocked (403 'Only organization admins...') — the automation
path UN-3586 asks for. Enable it: a platform API key caller may rotate, same
as a session org admin. Drop the earlier self-only restriction (not a ticket
requirement) and the now-unneeded has_object_permission. Org scoping (auth
middleware + org-scoped queryset) still confines a key to its own org; read
keys still can't POST (middleware). rotate already returns the new key.

* UN-3586 [FIX] Require full_access key to rotate (close privilege escalation)

Greptile caught a real privilege escalation: after dropping self-only, a
read_write bearer key could rotate a full_access key and read its new secret
from the rotate response (rotate returns the new key), escalating read_write
-> full_access. Fix: key-based callers must present a full_access key to
rotate. read_write keys can no longer rotate; a full_access caller rotating
any key gains no privilege (already top tier) and matches what a session
admin can do. Session-admin rotate and org scoping unchanged.
)

* UN-3632 [FIX] Scope rig --fail-on-critical-gap to in-tier coverage so main runs green

The rig's unit/integration CI job had never passed on main. `--fail-on-critical-gap`
(passed only on the main push) failed the build on EVERY uncovered critical path,
including e2e-only paths run during the unit/integration tier and paths with no test
anywhere. Every group-level failure was already non-gating (optional groups, or exit
5 = no tests collected), so the gap gate was the sole cause of redness.

- Split critical-path gaps into in-scope vs out-of-scope (add `in_scope` to
  CriticalPathStatus; evaluate() already computed it). --fail-on-critical-gap now
  gates only on in-scope gaps — a declared in-tier covering group that didn't run
  green (real coverage regressed). Out-of-scope gaps (covered only by an unrun tier,
  or no declared coverage) are reported + logged but never gate that tier.
- Wire honest coverage that exists today: adapter-register-llm -> unit-sdk1. Gives
  the path teeth: if sdk1 regresses, it flips to an in-scope gap and fails.
- Drop unit-tool-registry group (component slated for removal; can't even collect).
- Delete 3 dead tests referencing removed code: core test_pandora_account.py
  (account_services removed), core test_pubsub_helper.py (LogHelper -> LogPublisher),
  platform-service test_auth_middleware.py (platform_service.main removed; also made
  live Postgres calls). unit-platform-service is green via its hermetic memory-leak
  test; unit-core has no valid tests left and skips as a placeholder.

New self-tests cover the in-scope/out-of-scope split at both evaluate() and cmd_run().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJqp7xMdd1kjLUKbvrJsq4

* test: prune dead rig groups/paths, park deprecated services, wire backend tests

Follow-up cleanup on the rig manifests (UN-3636):

- Park unit-runner and unit-prompt-service (commented out, not run by
  default) with a TODO to delete when those services are removed — both are
  being decommissioned; no value testing components on their way out.
- Drop the unit-tool-registry NOTE block entirely.
- Prune 6 unit-backend paths that collect zero tests (account_v2,
  api_deployment_v2, connector_v2, file_management, project,
  tenant_account_v2 — dirs missing or empty); optional skip-if-missing was
  hiding them and implying coverage that was never written.
- Wire two real backend tests into unit-backend:
    * middleware/test_exception.py — 5 hermetic tests, pass now.
    * prompt_studio/prompt_studio_core_v2/tests — pins the executor
      _handle_ide_index async path (no prompt-service coupling); runs once
      unit-backend is un-gated, skips safely until then.
- Comment out the tool-sandbox-exec critical path (TODO: remove with
  tool-registry/runner) — its covering group unit-runner is now parked.

`python -m tests.rig validate` → OK (13 groups, 9 critical paths).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* fix: make rig editable-install survive uv run re-sync; drop phantom Django setting

unit-core ran 0 tests / 2 collection errors (ModuleNotFoundError: No module
named 'unstract'). Root cause: _prepare_group_env did `uv pip install -e .`
for install_editable groups, but _pytest_command runs `uv run`, which
re-syncs the venv every call and wipes that install before pytest imports the
package — the same hazard the code already flagged for plugins.

Inject the package via `uv run --with-editable <workdir>` (survives the
re-sync, same mechanism as the RIG_PYTEST_PLUGINS `--with` specs) and drop the
wiped `uv pip install -e .`. unit-core now 27 passed, 0 errors.

Also remove DJANGO_SETTINGS_MODULE from the repo-root [tool.pytest.ini_options]:
it's a pytest-django option that warns "Unknown config option" for every
non-Django group, points at a non-existent module (backend.settings.test_cases),
and Django settings don't belong at the polyglot repo root. The rig injects it
per-group via groups.yaml env for unit-backend only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: make unit-backend collect + run django_db tests in the rig

The unit-backend group pointed at a non-existent settings module
(backend.settings.test_cases) and ran without pytest-django, so the
DB-backed tests errored at collection (Django uninitialised) and the
django_db tests had no test-DB lifecycle. Make the group actually runnable:

- Add pytest-django to the backend test group (bootstraps Django before
  collection; provides the test-DB + django_db fixtures).
- Point DJANGO_SETTINGS_MODULE at the existing backend.settings.test and
  inject the import-time-required settings via the group env — base.py reads
  them before any dotenv load, so they must exist in the process env, not a
  settings module. ENCRYPTION_KEY is an all-zero (valid, zero-entropy) Fernet
  placeholder, not a real secret.
- Set DB_SCHEMA=public: the app's fixed schema doesn't exist in the fresh
  test DB and tenancy is row-level, so migrations run in public.
- Drop workflow_manager/endpoint_v2/tests from the wired paths: its
  destination-connector tests import the enterprise `plugins` package,
  absent in OSS.
- Add the missing utils/file_storage{,/helpers}/__init__.py so those
  modules import as a package under pytest.
- Stop test_build_index_payload's sys.modules stubs from leaking into
  sibling collection: record + restore the originals once the helper is
  imported (a stubbed account_v2.models was breaking other modules'
  real imports).

unit-backend now collects clean; 126 passed, 4 skipped. The remaining 6
failures (usage_v2 helper stubs, dashboard_metrics cleanup tasks) are
pre-existing test bugs tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: fix two pre-existing backend test bugs exposed by the rig

dashboard_metrics: organization FK targets Organization's int PK, but the
tests passed a UUID string as organization_id, and verified rows through
the org-scoped default manager (empty without a UserContext). Create a
real Organization and read via _base_manager.

usage_v2: drop the fragile "stub usage_v2.models into sys.modules before
import" trick — under pytest-django Django imports the real module first,
so the stub never took and the helper hit the DB. Rebind the Usage symbol
the helper resolved instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: gate live connector integration tests behind credential env vars

The connector suite had 12 reds that needed real external services or
per-developer credentials no one has by default. Two were genuine bugs;
the rest are integration tests masquerading as unit tests.

Fixes (not credential-related):
- mariadb: assertion text drifted from the connector's actual message
  ("SSL SETTINGS", not "ssl-settings").
- sharepoint: skip test_json_schema_has_is_personal — is_personal is read
  from settings in code but was never exposed in json_schema.json (personal
  vs site is inferred from an empty site_url). Whether the schema should
  expose it is a product decision; tracked under UN-3414.

Gating (skipUnless, mirrors the existing SharePoint integration tests):
- filesystems (box, gdrive, minio, pcs, dropbox) already read creds from
  env; add the missing skip guard so they SKIP instead of failing.
- databases (mssql, mysql, postgresql, redshift, snowflake) had hardcoded
  personal creds (incl. a live-looking neon.tech URL and a Snowflake
  account) querying bespoke tables. Move creds to *_TEST_* env vars and
  skip unless provided, removing the secrets from the repo.

CI can run these by injecting the corresponding secrets as env vars in a
dedicated integration job; by default they skip cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: make unit-core and unit-connectors required rig groups

Both now run green and standalone (no external services; integration
tests skip cleanly when credentials are absent), so drop optional: true
to make them blocking merge gates per UN-3635.

unit-backend stays optional until the rig provisions a reachable DB_HOST
for it (UN-3636 follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* test: address PR review feedback on rig + connector test guards

- in_scope defaults False on CriticalPathStatus so a future evaluate()
  regression that forgets it under-gates (warning) rather than over-gates
  (spurious build block). [greptile]
- widen connector integration skip guards (redshift, snowflake, gdrive,
  minio, pcs) to require every env var the test hard-references, so a
  partially configured env skips cleanly instead of failing. [coderabbit]
- usage_v2 test_helper: swap Usage via an autouse monkeypatch fixture
  instead of a module-level rebind that leaks FakeUsage into later tests.
- build_index_payload test: evict the helper module from sys.modules after
  binding it, so later importers in the same process get a real copy.
- drop dead tox `runner` alias (its unit-runner group was removed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: provision infra for integration tier; split DB/credential tests out of unit

Make `requires_services` actually provision instead of being cosmetic. The rig
now brings up testcontainers infra (Postgres/MinIO) for any runnable group that
declares `requires_services`, and injects connection env into the group's pytest
subprocess (Postgres URL -> discrete DB_* vars; MinIO endpoint/creds). Previously
django_db tests fell back to the compose hostname `backend-db-1`, unreachable
from host-side pytest, so unit-backend had to be `optional`.

Reclassify infra-dependent tests by the rig's own tier taxonomy (unit = no
external services, integration = real infra but not the full platform):

- backend: split unit-backend into pure `unit-backend` (gates unit tier, no
  infra) and `integration-backend` (django_db tests: dashboard_metrics +
  prompt_studio_registry_v2; provisioned Postgres; gates integration tier).
- connectors: marker-based split (tests are interleaved within files). Credential
  + MinIO tests marked `@pytest.mark.integration`; `unit-connectors` runs
  `-m "not integration"`, new `integration-connectors` runs `-m "integration"`.
  test_minio actually runs against provisioned MinIO; external-credential tests
  skip. Skip-guard test_http_fs (was hitting a live URL unguarded in CI).

Both groups are non-optional and gate their tiers. Unit tier stays infra-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: address PR review — wire provisioned Redis, mark http_fs integration, cut rig complexity

- integration-backend declares requires_services: [postgres, redis] but the rig
  only injected Postgres/MinIO env, so Redis-backed tests bypassed the
  testcontainer and hit localhost:6379. Inject REDIS_HOST/PORT +
  CELERY_BROKER_BASE_URL from the provisioned endpoint (CodeRabbit).
- test_http_fs was skip-guarded but unmarked, so the connector marker split
  (-m "not integration") could still run it in the unit tier. Mark the module
  integration (CodeRabbit).
- Extract _inject_infra_env and _pytest_base_cmd to bring both functions under
  SonarCloud's cognitive-complexity threshold. NOSONAR the test's DB_PASSWORD
  placeholder (not a real credential).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: use hostname not literal IP in redis-wiring test (Sonar hotspot)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: mark local testcontainers MinIO http endpoint NOSONAR

The MinIO endpoint is a throwaway testcontainer with no TLS, so http is
expected. Suppress the SonarCloud insecure-protocol hotspot that otherwise
blocks the quality gate on new code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: stub UserDefaultAdapter so prompt-studio build-index tests run

The module stubs adapter_processor_v2.models to import PromptStudioHelper
without the full Django app, but only provided AdapterInstance. The helper
also imports UserDefaultAdapter, so the import failed and all 4 tests in the
module self-skipped via the _IMPORT_ERROR guard. Add the missing stub so the
tests actually execute.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: drop prompt-service from test compose overlay

Removed from the e2e test overlay; the platform brought up for e2e no longer
provisions a standalone prompt-service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: remove dead S3 smoke test and strip print() debug from connector tests

- test_miniofs: drop the permanently-skipped test_s3 (hardcoded AWS S3 smoke).
  MinIO/S3 is covered by test_minio (integration), the TestAccessFilteredS3
  unit tests, and the connectorkit registry check.
- database + filesystem tests: replace print()-in-loop debug with assertions
  (or drop redundant prints) so integration runs don't spam the logs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: switch unit-backend to marker-based selection; classify integration tests

unit-backend was a hand-kept file allowlist that had to grow with every new
test dir. Collect the whole backend tree instead and let markers decide:
tests needing live infra carry `@pytest.mark.integration` and are excluded
from the unit tier via `-m "not integration"`.

- register the `integration` marker in backend/pyproject.toml
- mark the two DB-bound suites (dashboard_metrics, prompt_studio_registry_v2)
  that were previously grouped by path only
- add a conftest marking the endpoint_v2 destination-connector subtree
  integration (uses django.test.TestCase -> needs Postgres). Kept out of the
  gating integration-backend group for now: 3 postgres destination tests are
  pre-existing failures and need a skip-guard/fix before they can gate.
- remove the dead SharePoint `test_json_schema_has_is_personal` skip: the
  schema never exposes `is_personal`, so the test only ever asserted-then-
  skipped; drop it rather than carry a permanent skip.

Verified: unit tier 116 passed / 50 deselected; integration-backend collects
its marked suites; rig self-tests 54 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: centralize DB-test marking; cover adapter-register-llm via integration API test

Replace the scattered integration-marking (per-file pytestmark, per-app
endpoint_v2 conftest) with a single backend/conftest.py hook that auto-marks
any Django TestCase/TransactionTestCase or django_db item as integration —
tests declare their DB need by how they're written, not a hand-kept marker.

Cover the adapter-register-llm critical path honestly: unit-sdk1 only
exercises the SDK provider classes, not the HTTP endpoint, so map it to a new
integration-backend APITestCase that POSTs /adapter/ (SDK context-window call
mocked, everything else real). Trim comments that would go stale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: complete DB-test centralization; move DB-writer tests to integration tier

Follow-up completing 6d61a56, which staged only the adapter API test and the
deleted per-dir conftest, leaving the rest of the batch uncommitted.

- backend/conftest.py: central pytest_collection_modifyitems hook auto-marks
  every Django TestCase/TransactionTestCase/django_db test as `integration`, so
  unit-backend (-m "not integration") and integration-backend (-m integration)
  are exact complements. Drops now-redundant per-app pytestmark in
  dashboard_metrics and prompt_studio_registry_v2.
- critical_paths.yaml: adapter-register-llm now covered_by integration-backend
  (real API test), reverting the earlier unit-sdk1 placeholder mapping.
- groups.yaml: integration-backend gains the adapter API test and the
  destination-connectors DB-writer tests (BE orchestration over the connector
  lib — superset of the connector-lib DB tests). Adds WORKFLOW_EXECUTION_DIR_PREFIX
  to the shared backend test env (ExecutionFileHandler builds paths from it).
- destination-connector postgres test: read DB_SCHEMA (was hardcoded "test"),
  use a lowercase table name (connector lowercases on read-back), drop the
  error-record case (hits a latent product edge: data=None serialized as the
  string 'None' into a jsonb column).
- Delete 7 connector-lib DB tests (databases/test_*_db.py) — superseded by the
  backend DB-writer tests; keep test_sql_safety.py, filesystems, connectorkit.
- rig cli.py / critical_paths.py: comment + docstring cleanups.

Verified (testcontainers Postgres/Redis): unit-backend 116 passed,
integration-backend 24 passed / 26 skipped (external-DB engines skip w/o creds),
unit-connectors 53 passed, rig validate OK (15 groups, 9 paths).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: switch integration-backend to marker-only selection; drop dead connector_v2 tests

- groups.yaml: integration-backend now selects paths ["."] with -m integration
  — the exact complement of unit-backend over the same tree. New backend DB
  tests auto-join the group with no manifest edit. Verified equivalent:
  collect-only shows the same 50/166 tests (116 deselected); full run against
  testcontainers Postgres/Redis gives the same 24 passed / 26 skipped as the
  hand-listed paths on CI.
- Delete backend/connector_v2/tests (connector_tests.py, conftest.py, fixture):
  written for the pre-v2 schema — the fixture loads removed models
  (account.org, account.user, project) so loaddata errors immediately, and the
  filename never matched pytest's test_*.py pattern, so these tests have not
  been collected anywhere. Same dead-test cleanup as the rest of this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* docs: concise backend test-contribution steps in tests/README.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* test: rewrite deployment_helper staging tests with patch.object

The previous version stubbed cross-app imports in sys.modules (only when
absent) — written for a bare no-Django env. Under the rig, pytest-django
boots Django and the real modules are already imported, so the stubs were
skipped and the tests called reset_mock() on real classes (AttributeError).

Same control-flow assertions, now via mock.patch.multiple on the imported
module: no sys.modules mutation, no import-order dependence, safe in a
shared pytest session. Stays in the unit tier (no DB touched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* test: drop sys.modules stub ceremony from build-index tests; subprocess flask check

- test_build_index_payload.py: import the real prompt_studio_helper (Django is
  loaded by the rig env) instead of installing ~30 stub modules in sys.modules
  and restoring them post-import. The clobber/restore dance was import-order
  dependent and silently skipped all tests when the stub surface drifted.
  Tests and assertions unchanged; patches now managed via ExitStack.
- test_legacy_executor_scaffold.py: test_no_flask_import now runs the check in
  a fresh subprocess. The in-place importlib.reload swapped class identities
  under sibling tests, and scanning sys.modules in-process fails if any other
  test imported flask.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* ci: move path filtering from trigger to job level for required-check compat

Trigger-level paths-ignore means docs/frontend/docker-only PRs never start
the workflow, so no check run is created and a required 'test' status check
would block those PRs forever. A 'changes' job now always runs and gates
'test' via needs + if; a skipped job still reports a check run (skipped =
passing for required checks), while any executed test failure still fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGcZHkM4jbUmrx4YNYewZK

* test: standardize pytest python_files across services; drop no-flask guard test

- python_files = [test_*.py, *_test.py, *_tests.py] in backend, workers, sdk1,
  connectors — one superset everywhere instead of three different configs.
  sdk1 loses the unused bare tests.py pattern (no such files). No currently
  existing file changes collection status (verified: zero *_tests.py in repo).
- Remove test_no_flask_import: flask is not a workers dependency, so a flask
  import in the exceptions module would already fail every test that imports
  it; the Flask source it guarded against (prompt-service) is decommissioned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* test: collect Django per-app tests.py files

Accepting the bare `tests.py` Django convention surfaces two real backend
suites (tenant_account_v2, account_v2 — 28 tests) that were written but
never collected. All pass in integration-backend (24→52 passed, skips
unchanged); unit-backend collection is untouched.

Bare `test.py` is deliberately NOT accepted: backend/backend/settings/test.py
is a Django settings module and would be imported as a test module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* ci: run unit and integration tiers as parallel jobs

The tiers share no state (separate runners, disjoint groups), so they run
as matrix legs; wall clock drops from tier-sum to max(tiers). Everything
needing both tiers' results moves to a new `report` job fed by per-tier
artifacts: report aggregation, coverage union (tier .coverage files are
suffixed so `coverage combine` can merge them), PR comment, and the
main-branch baseline.

Baseline writes were the one real conflict: parallel per-tier
`run --update-baseline` would race the cache save (first save wins, the
other tier's coverage is lost). Tier jobs now restore-only; the rig's
`report combine` gains `--update-baseline` as the single writer, refusing
the merge when any non-optional group is red or the baseline is corrupt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* ci: make report job propagate tier failures

Lets `report` serve as the single required branch-protection check: a
matrix job's result is success only when every leg passes, so either
tier failing turns report red too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* chore: tighten comments added in this PR

Trim verbose comments and drop PR-narrative phrasing that would go stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* ci: enforce --fail-on-critical-gap on PRs too

Critical-path regressions already gate PRs; the gap check was main-only
to avoid failing unrelated PRs on pre-existing debt. With main enforced
gap-free and PR CI running the merge ref, a gap failure on a PR can only
be one the PR introduces — so catch it before merge, not after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* ci: run tests on PRs against any base branch

`branches: [main]` filters by base branch, so stacked PRs (targeting a
feature branch) got no test runs at all until the stack reached main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* ci: tighten workflow comments; drop comment on removed trigger filter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* test: add per-test critical-path marker proof; stop 'empty' attesting coverage

Review feedback on the rig's coverage attestation: a covering group that
collected zero tests (pytest exit 5) counted as green for critical-path
coverage, so marker drift in a marker-only group like integration-backend
could bake false coverage into the baseline. Two changes close this:

- Coverage attestation now requires status == "pass"; "empty" stays
  non-failing for the build but proves nothing.
- New `proof: marker` per path in critical_paths.yaml: on top of a green
  covering group, the path needs >=1 passing test carrying
  @pytest.mark.critical_path("<id>") in that build. A rig-injected pytest
  plugin (PYTHONPATH + -p, junit_family=legacy) copies marker args into
  junit testcase properties; the rig reads back only passing testcases.
  Tests may stack multiple markers. Unknown marker ids fail the run so a
  typo can't silently attest nothing.

adapter-register-llm is flipped to proof: marker and its API test marked —
deleting that test now drops the path to regression instead of hiding
behind the rest of integration-backend staying green. Other paths stay
proof: group until their tests are backfilled and marked.

Verified: rig self-tests 64 passed; integration-backend via testcontainers
52 passed / 26 skipped / 1 xfailed with adapter-register-llm reported
covered via the junit marker property (under xdist).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* test: restore mock-only connector tests + error-path test; fail report on broken changes job

Address remaining PR review findings:

- Restore test_bigquery_db.py and test_mariadb.py: unlike the five
  live-DB smoke tests deleted alongside them, these are pure mock-based
  unit tests that ran green in CI and were the only coverage of BigQuery's
  Forbidden/NotFound exception mapping and MariaDB's SSL-param build +
  1045/2003 message mapping. They rejoin unit-connectors (unmarked).

- Restore test_insert_into_db_with_error_postgresql as strict xfail: it
  exercises a real shipping bug (get_sql_values_for_query serializes
  data=None as the literal string 'None', invalid JSON for the jsonb
  column) that deleting the test papered over. The xfail keeps the error
  path exercised in CI and flips loudly when the bug is fixed.

- ci-test.yaml: the report job now also needs `changes` and runs whenever
  changes did not succeed. Previously a failed changes job (checkout or
  paths-filter flake) skipped test -> skipped report -> the required check
  passed green with zero tests run. Skip remains only for a legitimate
  skip decided by a successful changes job (irrelevant paths / draft).

Verified: integration-backend via testcontainers reports the restored
error test as xfailed; unit-connectors 63 passed (+10 from the restored
files).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* refactor: address SonarCloud issues on rig code

- Extract per-path classification out of evaluate() to cut cognitive
  complexity (S3776)
- Drop unused config param from pytest_collection_modifyitems; pytest
  matches hook args by name (S1172)
- Use a real GroupManifest in the marker-proof validation test instead
  of a duck-typed stub (S5655)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
…for the AWS Marketplace redirect (#2152)

* LW-358 [FEAT] Add top-level LLMWhisperer marketplace landing route for the Tackle redirect

Tackle's post-purchase redirect is one static URL per listing and cannot
carry a per-buyer :orgName segment. The Unstract listings redirect to the
existing top-level /marketplace-landing, but LLMWhisperer had only the
org-scoped llm-whisperer/:orgName/marketplace-landing route — leaving the
LLMWhisperer listings with no valid redirect target, and the top-level
/marketplace-landing would fire the UNSTRACT claim for an LLMWhisperer
purchase.

Adds /llm-whisperer/marketplace-landing (outside RequireAuth, mirroring
the Unstract entries; the page handles its own auth and claims against
the portal backend). In OSS builds the plugin import resolves to the
optional-plugin stub and the route 404s harmlessly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* LW-358 [MISC] Address review: soften LLMWhisperer landing-route comment

Don't pin the concrete portal endpoint (lives in a separate gitignored plugin;
a rename can't be caught here) and lowercase 'portal backend' so it doesn't
read as a defined proper noun. Comment intent (don't consolidate the two
MarketplaceLandingPage entries) is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ng docker-py to 7.1.0 (#2155)

UN-3699 [FIX] Bump runner docker-py to 7.1.0 for requests>=2.32 compat

docker-py 6.1.3 is incompatible with requests>=2.32: its UnixHTTPAdapter
only overrides the old get_connection path, so requests 2.32+ (locked at
2.33.0 via unstract-core) routes the internal http+docker:// socket URL to
urllib3, raising URLSchemeUnknown -> DockerException at
DockerClient.from_env(). This breaks any runner image rebuilt from main on
the Docker container-client path (cloud/k8s is unaffected — it never calls
from_env).

Bump docker to 7.1.0, which handles requests>=2.32. Leaves the org-wide
requests==2.33.0 untouched (lowering it is unsatisfiable: workers and
x2text-service require >=2.33.0). Runner uses only stable high-level docker
APIs; websocket-client drops out as it is now an optional docker extra and
the runner uses no websocket features.

Verified: docker 7.1.0 + requests 2.33.0 -> DockerClient.from_env().ping()
== True against a real socket.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* UN-3632 [FIX] Scope rig --fail-on-critical-gap to in-tier coverage so main runs green

The rig's unit/integration CI job had never passed on main. `--fail-on-critical-gap`
(passed only on the main push) failed the build on EVERY uncovered critical path,
including e2e-only paths run during the unit/integration tier and paths with no test
anywhere. Every group-level failure was already non-gating (optional groups, or exit
5 = no tests collected), so the gap gate was the sole cause of redness.

- Split critical-path gaps into in-scope vs out-of-scope (add `in_scope` to
  CriticalPathStatus; evaluate() already computed it). --fail-on-critical-gap now
  gates only on in-scope gaps — a declared in-tier covering group that didn't run
  green (real coverage regressed). Out-of-scope gaps (covered only by an unrun tier,
  or no declared coverage) are reported + logged but never gate that tier.
- Wire honest coverage that exists today: adapter-register-llm -> unit-sdk1. Gives
  the path teeth: if sdk1 regresses, it flips to an in-scope gap and fails.
- Drop unit-tool-registry group (component slated for removal; can't even collect).
- Delete 3 dead tests referencing removed code: core test_pandora_account.py
  (account_services removed), core test_pubsub_helper.py (LogHelper -> LogPublisher),
  platform-service test_auth_middleware.py (platform_service.main removed; also made
  live Postgres calls). unit-platform-service is green via its hermetic memory-leak
  test; unit-core has no valid tests left and skips as a placeholder.

New self-tests cover the in-scope/out-of-scope split at both evaluate() and cmd_run().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJqp7xMdd1kjLUKbvrJsq4

* test: prune dead rig groups/paths, park deprecated services, wire backend tests

Follow-up cleanup on the rig manifests (UN-3636):

- Park unit-runner and unit-prompt-service (commented out, not run by
  default) with a TODO to delete when those services are removed — both are
  being decommissioned; no value testing components on their way out.
- Drop the unit-tool-registry NOTE block entirely.
- Prune 6 unit-backend paths that collect zero tests (account_v2,
  api_deployment_v2, connector_v2, file_management, project,
  tenant_account_v2 — dirs missing or empty); optional skip-if-missing was
  hiding them and implying coverage that was never written.
- Wire two real backend tests into unit-backend:
    * middleware/test_exception.py — 5 hermetic tests, pass now.
    * prompt_studio/prompt_studio_core_v2/tests — pins the executor
      _handle_ide_index async path (no prompt-service coupling); runs once
      unit-backend is un-gated, skips safely until then.
- Comment out the tool-sandbox-exec critical path (TODO: remove with
  tool-registry/runner) — its covering group unit-runner is now parked.

`python -m tests.rig validate` → OK (13 groups, 9 critical paths).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* fix: make rig editable-install survive uv run re-sync; drop phantom Django setting

unit-core ran 0 tests / 2 collection errors (ModuleNotFoundError: No module
named 'unstract'). Root cause: _prepare_group_env did `uv pip install -e .`
for install_editable groups, but _pytest_command runs `uv run`, which
re-syncs the venv every call and wipes that install before pytest imports the
package — the same hazard the code already flagged for plugins.

Inject the package via `uv run --with-editable <workdir>` (survives the
re-sync, same mechanism as the RIG_PYTEST_PLUGINS `--with` specs) and drop the
wiped `uv pip install -e .`. unit-core now 27 passed, 0 errors.

Also remove DJANGO_SETTINGS_MODULE from the repo-root [tool.pytest.ini_options]:
it's a pytest-django option that warns "Unknown config option" for every
non-Django group, points at a non-existent module (backend.settings.test_cases),
and Django settings don't belong at the polyglot repo root. The rig injects it
per-group via groups.yaml env for unit-backend only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: make unit-backend collect + run django_db tests in the rig

The unit-backend group pointed at a non-existent settings module
(backend.settings.test_cases) and ran without pytest-django, so the
DB-backed tests errored at collection (Django uninitialised) and the
django_db tests had no test-DB lifecycle. Make the group actually runnable:

- Add pytest-django to the backend test group (bootstraps Django before
  collection; provides the test-DB + django_db fixtures).
- Point DJANGO_SETTINGS_MODULE at the existing backend.settings.test and
  inject the import-time-required settings via the group env — base.py reads
  them before any dotenv load, so they must exist in the process env, not a
  settings module. ENCRYPTION_KEY is an all-zero (valid, zero-entropy) Fernet
  placeholder, not a real secret.
- Set DB_SCHEMA=public: the app's fixed schema doesn't exist in the fresh
  test DB and tenancy is row-level, so migrations run in public.
- Drop workflow_manager/endpoint_v2/tests from the wired paths: its
  destination-connector tests import the enterprise `plugins` package,
  absent in OSS.
- Add the missing utils/file_storage{,/helpers}/__init__.py so those
  modules import as a package under pytest.
- Stop test_build_index_payload's sys.modules stubs from leaking into
  sibling collection: record + restore the originals once the helper is
  imported (a stubbed account_v2.models was breaking other modules'
  real imports).

unit-backend now collects clean; 126 passed, 4 skipped. The remaining 6
failures (usage_v2 helper stubs, dashboard_metrics cleanup tasks) are
pre-existing test bugs tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: fix two pre-existing backend test bugs exposed by the rig

dashboard_metrics: organization FK targets Organization's int PK, but the
tests passed a UUID string as organization_id, and verified rows through
the org-scoped default manager (empty without a UserContext). Create a
real Organization and read via _base_manager.

usage_v2: drop the fragile "stub usage_v2.models into sys.modules before
import" trick — under pytest-django Django imports the real module first,
so the stub never took and the helper hit the DB. Rebind the Usage symbol
the helper resolved instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: gate live connector integration tests behind credential env vars

The connector suite had 12 reds that needed real external services or
per-developer credentials no one has by default. Two were genuine bugs;
the rest are integration tests masquerading as unit tests.

Fixes (not credential-related):
- mariadb: assertion text drifted from the connector's actual message
  ("SSL SETTINGS", not "ssl-settings").
- sharepoint: skip test_json_schema_has_is_personal — is_personal is read
  from settings in code but was never exposed in json_schema.json (personal
  vs site is inferred from an empty site_url). Whether the schema should
  expose it is a product decision; tracked under UN-3414.

Gating (skipUnless, mirrors the existing SharePoint integration tests):
- filesystems (box, gdrive, minio, pcs, dropbox) already read creds from
  env; add the missing skip guard so they SKIP instead of failing.
- databases (mssql, mysql, postgresql, redshift, snowflake) had hardcoded
  personal creds (incl. a live-looking neon.tech URL and a Snowflake
  account) querying bespoke tables. Move creds to *_TEST_* env vars and
  skip unless provided, removing the secrets from the repo.

CI can run these by injecting the corresponding secrets as env vars in a
dedicated integration job; by default they skip cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: make unit-core and unit-connectors required rig groups

Both now run green and standalone (no external services; integration
tests skip cleanly when credentials are absent), so drop optional: true
to make them blocking merge gates per UN-3635.

unit-backend stays optional until the rig provisions a reachable DB_HOST
for it (UN-3636 follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* test: address PR review feedback on rig + connector test guards

- in_scope defaults False on CriticalPathStatus so a future evaluate()
  regression that forgets it under-gates (warning) rather than over-gates
  (spurious build block). [greptile]
- widen connector integration skip guards (redshift, snowflake, gdrive,
  minio, pcs) to require every env var the test hard-references, so a
  partially configured env skips cleanly instead of failing. [coderabbit]
- usage_v2 test_helper: swap Usage via an autouse monkeypatch fixture
  instead of a module-level rebind that leaks FakeUsage into later tests.
- build_index_payload test: evict the helper module from sys.modules after
  binding it, so later importers in the same process get a real copy.
- drop dead tox `runner` alias (its unit-runner group was removed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: provision infra for integration tier; split DB/credential tests out of unit

Make `requires_services` actually provision instead of being cosmetic. The rig
now brings up testcontainers infra (Postgres/MinIO) for any runnable group that
declares `requires_services`, and injects connection env into the group's pytest
subprocess (Postgres URL -> discrete DB_* vars; MinIO endpoint/creds). Previously
django_db tests fell back to the compose hostname `backend-db-1`, unreachable
from host-side pytest, so unit-backend had to be `optional`.

Reclassify infra-dependent tests by the rig's own tier taxonomy (unit = no
external services, integration = real infra but not the full platform):

- backend: split unit-backend into pure `unit-backend` (gates unit tier, no
  infra) and `integration-backend` (django_db tests: dashboard_metrics +
  prompt_studio_registry_v2; provisioned Postgres; gates integration tier).
- connectors: marker-based split (tests are interleaved within files). Credential
  + MinIO tests marked `@pytest.mark.integration`; `unit-connectors` runs
  `-m "not integration"`, new `integration-connectors` runs `-m "integration"`.
  test_minio actually runs against provisioned MinIO; external-credential tests
  skip. Skip-guard test_http_fs (was hitting a live URL unguarded in CI).

Both groups are non-optional and gate their tiers. Unit tier stays infra-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: address PR review — wire provisioned Redis, mark http_fs integration, cut rig complexity

- integration-backend declares requires_services: [postgres, redis] but the rig
  only injected Postgres/MinIO env, so Redis-backed tests bypassed the
  testcontainer and hit localhost:6379. Inject REDIS_HOST/PORT +
  CELERY_BROKER_BASE_URL from the provisioned endpoint (CodeRabbit).
- test_http_fs was skip-guarded but unmarked, so the connector marker split
  (-m "not integration") could still run it in the unit tier. Mark the module
  integration (CodeRabbit).
- Extract _inject_infra_env and _pytest_base_cmd to bring both functions under
  SonarCloud's cognitive-complexity threshold. NOSONAR the test's DB_PASSWORD
  placeholder (not a real credential).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: use hostname not literal IP in redis-wiring test (Sonar hotspot)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: mark local testcontainers MinIO http endpoint NOSONAR

The MinIO endpoint is a throwaway testcontainer with no TLS, so http is
expected. Suppress the SonarCloud insecure-protocol hotspot that otherwise
blocks the quality gate on new code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: stub UserDefaultAdapter so prompt-studio build-index tests run

The module stubs adapter_processor_v2.models to import PromptStudioHelper
without the full Django app, but only provided AdapterInstance. The helper
also imports UserDefaultAdapter, so the import failed and all 4 tests in the
module self-skipped via the _IMPORT_ERROR guard. Add the missing stub so the
tests actually execute.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: drop prompt-service from test compose overlay

Removed from the e2e test overlay; the platform brought up for e2e no longer
provisions a standalone prompt-service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: remove dead S3 smoke test and strip print() debug from connector tests

- test_miniofs: drop the permanently-skipped test_s3 (hardcoded AWS S3 smoke).
  MinIO/S3 is covered by test_minio (integration), the TestAccessFilteredS3
  unit tests, and the connectorkit registry check.
- database + filesystem tests: replace print()-in-loop debug with assertions
  (or drop redundant prints) so integration runs don't spam the logs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: switch unit-backend to marker-based selection; classify integration tests

unit-backend was a hand-kept file allowlist that had to grow with every new
test dir. Collect the whole backend tree instead and let markers decide:
tests needing live infra carry `@pytest.mark.integration` and are excluded
from the unit tier via `-m "not integration"`.

- register the `integration` marker in backend/pyproject.toml
- mark the two DB-bound suites (dashboard_metrics, prompt_studio_registry_v2)
  that were previously grouped by path only
- add a conftest marking the endpoint_v2 destination-connector subtree
  integration (uses django.test.TestCase -> needs Postgres). Kept out of the
  gating integration-backend group for now: 3 postgres destination tests are
  pre-existing failures and need a skip-guard/fix before they can gate.
- remove the dead SharePoint `test_json_schema_has_is_personal` skip: the
  schema never exposes `is_personal`, so the test only ever asserted-then-
  skipped; drop it rather than carry a permanent skip.

Verified: unit tier 116 passed / 50 deselected; integration-backend collects
its marked suites; rig self-tests 54 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5HQX5CSoMR6RzHtXcfwJt

* test: centralize DB-test marking; cover adapter-register-llm via integration API test

Replace the scattered integration-marking (per-file pytestmark, per-app
endpoint_v2 conftest) with a single backend/conftest.py hook that auto-marks
any Django TestCase/TransactionTestCase or django_db item as integration —
tests declare their DB need by how they're written, not a hand-kept marker.

Cover the adapter-register-llm critical path honestly: unit-sdk1 only
exercises the SDK provider classes, not the HTTP endpoint, so map it to a new
integration-backend APITestCase that POSTs /adapter/ (SDK context-window call
mocked, everything else real). Trim comments that would go stale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: complete DB-test centralization; move DB-writer tests to integration tier

Follow-up completing 6d61a56, which staged only the adapter API test and the
deleted per-dir conftest, leaving the rest of the batch uncommitted.

- backend/conftest.py: central pytest_collection_modifyitems hook auto-marks
  every Django TestCase/TransactionTestCase/django_db test as `integration`, so
  unit-backend (-m "not integration") and integration-backend (-m integration)
  are exact complements. Drops now-redundant per-app pytestmark in
  dashboard_metrics and prompt_studio_registry_v2.
- critical_paths.yaml: adapter-register-llm now covered_by integration-backend
  (real API test), reverting the earlier unit-sdk1 placeholder mapping.
- groups.yaml: integration-backend gains the adapter API test and the
  destination-connectors DB-writer tests (BE orchestration over the connector
  lib — superset of the connector-lib DB tests). Adds WORKFLOW_EXECUTION_DIR_PREFIX
  to the shared backend test env (ExecutionFileHandler builds paths from it).
- destination-connector postgres test: read DB_SCHEMA (was hardcoded "test"),
  use a lowercase table name (connector lowercases on read-back), drop the
  error-record case (hits a latent product edge: data=None serialized as the
  string 'None' into a jsonb column).
- Delete 7 connector-lib DB tests (databases/test_*_db.py) — superseded by the
  backend DB-writer tests; keep test_sql_safety.py, filesystems, connectorkit.
- rig cli.py / critical_paths.py: comment + docstring cleanups.

Verified (testcontainers Postgres/Redis): unit-backend 116 passed,
integration-backend 24 passed / 26 skipped (external-DB engines skip w/o creds),
unit-connectors 53 passed, rig validate OK (15 groups, 9 paths).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: switch integration-backend to marker-only selection; drop dead connector_v2 tests

- groups.yaml: integration-backend now selects paths ["."] with -m integration
  — the exact complement of unit-backend over the same tree. New backend DB
  tests auto-join the group with no manifest edit. Verified equivalent:
  collect-only shows the same 50/166 tests (116 deselected); full run against
  testcontainers Postgres/Redis gives the same 24 passed / 26 skipped as the
  hand-listed paths on CI.
- Delete backend/connector_v2/tests (connector_tests.py, conftest.py, fixture):
  written for the pre-v2 schema — the fixture loads removed models
  (account.org, account.user, project) so loaddata errors immediately, and the
  filename never matched pytest's test_*.py pattern, so these tests have not
  been collected anywhere. Same dead-test cleanup as the rest of this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* docs: concise backend test-contribution steps in tests/README.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* test: rewrite deployment_helper staging tests with patch.object

The previous version stubbed cross-app imports in sys.modules (only when
absent) — written for a bare no-Django env. Under the rig, pytest-django
boots Django and the real modules are already imported, so the stubs were
skipped and the tests called reset_mock() on real classes (AttributeError).

Same control-flow assertions, now via mock.patch.multiple on the imported
module: no sys.modules mutation, no import-order dependence, safe in a
shared pytest session. Stays in the unit tier (no DB touched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* test: drop sys.modules stub ceremony from build-index tests; subprocess flask check

- test_build_index_payload.py: import the real prompt_studio_helper (Django is
  loaded by the rig env) instead of installing ~30 stub modules in sys.modules
  and restoring them post-import. The clobber/restore dance was import-order
  dependent and silently skipped all tests when the stub surface drifted.
  Tests and assertions unchanged; patches now managed via ExitStack.
- test_legacy_executor_scaffold.py: test_no_flask_import now runs the check in
  a fresh subprocess. The in-place importlib.reload swapped class identities
  under sibling tests, and scanning sys.modules in-process fails if any other
  test imported flask.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* ci: move path filtering from trigger to job level for required-check compat

Trigger-level paths-ignore means docs/frontend/docker-only PRs never start
the workflow, so no check run is created and a required 'test' status check
would block those PRs forever. A 'changes' job now always runs and gates
'test' via needs + if; a skipped job still reports a check run (skipped =
passing for required checks), while any executed test failure still fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGcZHkM4jbUmrx4YNYewZK

* ci: fold e2e tier into the image-build workflow (e2e-build-test.yaml)

ci-container-build.yaml built + booted the stack, checked nothing exited,
and threw it all away; ci-test-e2e.yaml had e2e plumbing but no images to
run against (VERSION never exported), so it never executed. Merge them:
build once, then let the rig boot its compose project against the just-
built SHA-tagged images and run the e2e tier. up -d --wait plus endpoint
readiness subsumes the old exited-container check.

VERSION joins tox passenv so the rig's compose subprocess can resolve the
${VERSION} image tags of the base compose file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGcZHkM4jbUmrx4YNYewZK

* ci: rename workflow to ci-e2e-build-test.yaml for ci-* consistency

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGcZHkM4jbUmrx4YNYewZK

* ci: pin external actions to commit SHAs in ci-e2e-build-test.yaml

Sonar/GHAS flag mutable tag refs — a moved tag can silently swap the
action code. Pinned to the newest release within each currently-used
major, so no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGcZHkM4jbUmrx4YNYewZK

* revert: align three files to main after merge

test_no_flask_import, the test_miniofs comment, and the groups.yaml Fernet
comment were intentionally dropped or shortened in #2115; keep main's version
rather than the e2e branch's re-additions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] cover auth-login critical path with an e2e test

Add the first real e2e critical-path test: OSS mock-login over real HTTP.

- tests/e2e/auth/test_login.py: POST form creds to /api/v1/login, assert 302 +
  sessionid cookie, confirm the session is usable via /api/v1/session; a
  negative test asserts bad creds don't redirect. Marked
  @pytest.mark.critical_path("auth-login").
- New non-optional e2e-login group (gates on failure) covering auth-login with
  proof: marker. Fix the path's stale entry (/api/v1/auth/login -> /api/v1/login).
- Shared authed_session fixture (login + org handshake, session-scoped) so
  future e2e tests don't re-implement auth.
- Extend the smoke gate to health-check every service, and fix _wait_ready:
  it probed the removed prompt-service (connection refused -> guaranteed 300s
  TimeoutError) and used wrong paths for runner/x2text. Both now use a single
  health_targets() source of truth.
- Run --fail-on-critical-gap on e2e PRs too (out-of-scope gaps stay warn-only);
  --update-baseline remains main-only. Matches the unit/integration lane.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [FIX] install requests in the rig's per-group pytest env

The e2e groups run pytest via `uv run --with ...`, whose ephemeral venv only
carries the specs the rig injects — it does not sync the root project's
`test-rig` dependency group where `requests` lives. So the e2e conftest's
`import requests` failed with ModuleNotFoundError, killing both e2e groups at
conftest load (pytest exit 4, no tests). Inject requests alongside the pytest
plugins so it survives the per-call venv re-sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [FIX] guard empty organizations list in authed_session

A fresh DB without seed data would make organizations[0] raise a bare
IndexError as a fixture error across every consumer. Assert first so the
failure names the cause.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [MISC] disable checkout credential persistence; tighten comments

- ci-e2e-build-test.yaml: persist-credentials: false on checkout (reports/
  is uploaded as an artifact; nothing after checkout needs the token).
- Trim verbose comments in the workflow header, RIG_PYTEST_PLUGINS, and
  _wait_ready to concise WHY-only form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] merge unit/integration/e2e into one workflow + report

Collapse ci-test.yaml (unit+integration matrix + report job) and
ci-e2e-build-test.yaml into a single `test` job that builds the service
images and runs every tier in one `rig run all` invocation. Because all
tiers are evaluated together, e2e coverage (auth-login) now shows in the
same PR comment as unit/integration instead of being invisible to it.

- One shared baseline cache (unstract-test-baseline-main-*); drops the
  ut/e2e namespacing that kept e2e coverage out of the posted report.
- Single sticky `test-results` comment, rendered by `rig run all` itself
  (write_summary emits combined-test-report.md).

Tradeoff (accepted for now): tiers run serially in one job, no path-filter
skip — a docs-only PR still builds images + runs e2e (as the old e2e
workflow already did). Revisit if wall-time bites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] rewrite ci-test.yaml as the single merged test job

Companion to the previous commit's workflow deletion: the merged single-job
workflow content (build images + rig run all + one sticky report) belongs in
ci-test.yaml. The prior commit only staged the ci-e2e-build-test.yaml removal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [FIX] run tiers separately in the merged job, not rig run all

`rig run all` picks one infra mode per invocation ("platform wins"), so with
e2e (compose) and integration (testcontainers) groups in one call it skips the
testcontainers Postgres integration-backend needs — the backend then resolves
the compose DB hostname, unreachable from host-side pytest (78 DNS errors →
integration-backend red → adapter-register-llm gap → build fails).

Run one rig invocation per tier instead (unit/integration on testcontainers,
e2e on compose; compose up only for e2e), then `report combine` unifies every
tier's junit into a single evaluation + PR comment. Both auth-login (e2e) and
adapter-register-llm (integration) now show covered in one report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] split merged test job into parallel tiers + report job

Revert the single-job merge. unit/integration run as parallel matrix legs on
testcontainers; e2e builds images and boots compose on its own runner. A report
job needs all three, merges every tier's junit into one critical-path
evaluation, and posts a single sticky PR comment.

Restores unit+integration parallelism and per-tier resource isolation the merged
job lost, while keeping the unified report. One baseline (report is sole
reader+writer) replaces the old per-lane baselines, killing the cross-lane
regression false-positive. e2e is not gated by the path filter — docker/**
changes must be e2e-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [MISC] fix stale workflow reference in compose test overlay

ci-e2e-build-test.yaml was folded into the e2e job of ci-test.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
…eware (#2160)

fix: preserve view function identity in platform-service auth middleware

`authentication_middleware` returned an unwrapped closure, so every
decorated view function's `__name__` was "wrapper". Flask derives an
endpoint name from `__name__`, which meant:

- Six routes carried a redundant `endpoint="..."` kwarg purely to dodge
  the resulting name collision.
- `/usage`, the one authenticated route without that kwarg, registered as
  endpoint `platform.wrapper`.
- Adding any new authenticated route without `endpoint=` would fail at
  import with "View function mapping is overwriting an existing endpoint
  function: platform.wrapper".

Add `functools.wraps` and drop the now-redundant `endpoint=` kwargs. URL
rules and methods are unchanged; only the internal Flask endpoint names
change (`platform.wrapper` -> `platform.usage`). Nothing in the repo
resolves routes via `url_for`, and callers reach platform-service over
HTTP by path, so this is not externally observable.

x2text-service's equivalent middleware already patched `wrapper.__name__`
by hand; this brings platform-service in line.

Also repair `tests/test_auth_middleware.py`, which imported
`unstract.platform_service.main` (removed) and `get_account_from_bearer_token`
(removed) and so broke collection of the entire platform-service suite. It
now covers `validate_bearer_token` against a mocked cursor.


Claude-Session: https://claude.ai/code/session_014gUmgQQ7xV73qn6kcB7mXG

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mistral was a first-class LLM adapter but had no embedding adapter,
despite shipping a real, popular embedding model (mistral-embed).
This adds a native Mistral embedding adapter that routes through
LiteLLM's `mistral/` provider, so cost calculation works out of the box.

- MistralEmbeddingParameters in base1.py (idempotent `mistral/` prefix,
  required-model validation)
- MistralEmbeddingAdapter + UI JSON schema, reusing the existing logo
- Registered in embedding1/__init__.py
- 28 unit tests mirroring the Gemini embedding coverage

Closes #2157

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com>
…st-4.7 models (#2165)

[FIX] Strip deprecated temperature for Claude Sonnet 5 and post-4.7 models

Configuring Claude Sonnet 5 via Azure AI Foundry (azure_ai adapter) failed
the connection test with "temperature is deprecated for this model". Anthropic
removed the sampling params (temperature/top_p/top_k) starting with Opus 4.7,
and every model since (Opus 4.8, Sonnet 5, Fable 5, Mythos 5) rejects them too.

The SDK1 sampling-param strip only recognized claude-opus-4-7, so for Sonnet 5
the strip never fired and Pydantic's default temperature=0.1 was forwarded to
Azure -> Anthropic -> 400.

Replace the single detection pattern with a maintained stem list covering all
sampling-deprecated Claude models, keeping the trailing-edge boundary that
rejects prefix collisions (claude-sonnet-50) and alpha continuations
(...-5verbose). Extend tests with positives, boundary negatives, and adapter
wiring for Sonnet 5 (including the reported azure_ai_foundry case).
…n collection (#2031)

* UN-2190 Auto-capture execution_id in API deployment Postman collection

Add a post-response script to the 'Process document' request that
stores message.execution_id into a collection variable, and point the
'Execution status' request's execution_id query param at that variable.
Users no longer copy-paste execution IDs between requests (mirrors the
LLMWhisperer collection's whisper_hash pattern).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* UN-2190 Address review: guard non-JSON responses, scope variable to API deployments

- Wrap pm.response.json() in try/catch so error pages (non-JSON) don't
  surface a Postman test error
- Move collection variables behind APIBase.get_collection_variables()
  so Pipeline collections (no status request) stay variable-free

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* UN-2190 Expose execution_id in the API execution response

The ExecutionResponse DTO carries execution_id but
APIExecutionResponseSerializer dropped it, so the Postman capture
script (and any API consumer) had to parse it out of status_api.
Add it as a first-class response field; the collection script's
message.execution_id lookup now matches the real payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address review comments on Postman collection scripts

- Capture script: add else branch that resets execution_id to the
  default sentinel and warns, so a stale id from a previous run is
  never silently reused when the execute response lacks the field.
- PostmanItem.event: use field(default_factory=list) instead of the
  None/[] tri-state; to_dict() now strips empty (falsy) event blocks,
  preserving existing external behavior (pipeline items omit "event").
- Constrain closed enums: EventItem.listen -> Literal["prerequest",
  "test"], ScriptItem.type -> Literal["text/javascript"].
- Document the capture script's response-shape coupling and that it is
  Postman's "test" hook.
- Drop unnecessary string forward-reference on get_collection_variables.
- constants: derive STATUS_EXEC_ID_VARIABLE from EXEC_ID_VARIABLE_NAME
  so the two can't drift.
- Add tests for the postman_collection package covering event
  strip/keep, pipeline vs api-deployment shape, shared-constant coupling
  across variable/URL/JS, and the unencoded {{execution_id}} status URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…2163)

* [MISC] Fix adapter-ops skill cost-tracking guidance and stale paths

The skill told adapter authors that `get_provider()` drives cost
calculation and pointed at
`platform-service/.../helper/cost_calculation.py` as the enforcement
site. That file no longer exists, and `provider` is now only a metadata
column on the usage row.

Cost is actually looked up from the prefixed model string that
`validate_model()` emits, via `litellm.cost_per_token()` in
`sdk1/audit.py` (LLM) and `sdk1/usage_handler.py` (embedding). Both call
sites swallow the exception and fall back to $0, so a miss is silent.

This gap let a branded OpenAI-compatible adapter pass the documented
check — returning a `get_provider()` that matches `litellm_provider`
exactly — while still billing $0, because `OpenAICompatibleLLMParameters`
prepends `custom_openai/` and no cost-map key uses that prefix.

Rewrite the guidance to verify the model prefix instead, add a base-class
decision table (native LiteLLM provider -> BaseChatCompletionParameters;
no priced models -> OpenAICompatibleLLMParameters), and swap the curl of
upstream JSON for a snippet that queries the pinned LiteLLM in the sdk1
venv.

Also repoint every documented script path from the skill's old directory
name (`unstract-adapter-extension`) to `adapter-ops`; all copy-pasteable
commands in SKILL.md were broken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015i26iF1GS3x55XHEjStUeP

* [MISC] Address review: pinned-env verify, JSON-safe stdout, MD058

- provider_capabilities.md: replace curl-to-litellm-main verification with
  the pinned sdk1-venv query, matching SKILL.md — upstream main can price
  models the pinned version doesn't, defeating the check.
- check_adapter_updates.py: send the "SDK1 Adapters Path" diagnostic to
  stderr so `--json` stdout is valid JSON for jq/automation.
- SKILL.md: blank line before the provider-names table (markdownlint MD058).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [MISC] Fix invalid --action in manage_models.py doc example

SKILL.md showed `--action add`, which argparse rejects (valid choices are
add-enum/remove-enum/set-default/…). Corrected to `add-enum` so the
documented command runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* [MISC] Bump litellm 1.85.1 -> 1.90.3 for MiniMax-M3 cost support

1.85.1 has no `minimax/MiniMax-M3` entry in litellm's cost map, so
native MiniMax-M3 usage bills $0. 1.90.0 added the model with its
long-context pricing tier (input/output/cache `_above_512k_tokens`),
letting the standard litellm cost path price it correctly.

1.90.3 is the settled patch on that minor. No new dependency floors and
no in-scope breaking changes to `completion`/`cost_per_token`/
`register_model`/`token_counter` between 1.85.1 and 1.90.3; the 1.90.0
`register_model` fix (preserves built-in cache pricing on override) is a
bonus. Re-locked all consuming services so the running images pick up
the new version rather than staying pinned at 1.85.1 transitively.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [MISC] Repoint cohere-embed timeout patch to litellm 1.90.3

The patch is version-guarded to the exact litellm pin; the 1.85.1 -> 1.90.3
bump left it skipped, so `_patched_async_embedding` was never defined and
its test failed to import. The bug is still present in 1.90.3 (cohere
`async_embedding`/`embedding` still don't forward `timeout` to
`client.post()`) and the copied bodies still match upstream, so only the
version constant needed updating.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tests (#2164)

* UN-3636 [DEV] cover api-deployment-auth as an integration critical path

The public deployment endpoint is guarded by a decorator rather than DRF
permission classes, so a regression silently opens it. Assert every rejection
lands before execute_workflow — no execution row, no dispatch — which makes the
path provable without workers.

Drop the decorative `entry` field from the critical-path registry; it was
render-only and had already drifted from the real routes.

Stop health-probing the runner in the e2e tier: container-based execution is
being retired in favour of in-worker execution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] cover workflow-author as an integration critical path

Creating a workflow implicitly materialises its source and destination
endpoints; nothing else in the product recreates them, so a workflow that loses
them is silently unconfigurable. Assert the endpoints appear, start
unconfigured, and accept configuration.

Authoring is synchronous, so this half of workflow-create-execute needs no
workers. The execute half stays a declared gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] cover api-deployment-provision as an integration critical path

Provisioning is the only place a deployment's API key is minted, and it is
returned exactly once — a regression is unrecoverable for the caller. Assert
the key is persisted, active, bound to the deployment, and listable.

Also assert the auth guard's accept path: without it, a guard that rejected
every request would satisfy the rejection cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] cover prompt-studio-author as an integration critical path

Composing a prompt is synchronous; only running it needs an LLM. Assert a
project is creatable before any adapter exists — a fresh org has none — and
that prompts bind to their project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] cover usage-aggregate-read as an integration critical path

Workers write usage rows, but the aggregation billed against them is a
synchronous read. Under-counting loses revenue and cross-org leakage is a
disclosure bug; the scoping lives in a default manager, so a stray switch to
the base manager would break it silently. Seed two orgs sharing a run_id and
assert only the caller's rows are summed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] cover connector-register-test as an integration critical path

Credential validation is the only point where the platform proves it can reach
external storage before a workflow depends on it; a version that always answered
"valid" would only surface deep inside an execution. Test against the rig's
MinIO rather than a mock, and assert registered credentials land encrypted.

Adds MinIO to integration-backend's services for that reason.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

* UN-3636 [DEV] address review: prove ciphertext at rest, tighten MinIO skip

- connector-register-test: assert the raw connector_metadata column is
  ciphertext (read past the decrypting field descriptor), so a regression to
  plaintext storage fails instead of passing on the decrypted round-trip.
- Require MINIO_SECRET_ACCESS_KEY in the skip guard — the setup accesses it
  unconditionally, so a partial env now skips cleanly instead of raising
  KeyError.
- usage-aggregate-read: clear the thread-local UserContext in tearDown so the
  org identifier can't leak into a later manager-filtered query.
- Drop an explanatory comment from groups.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UVfw7aAocC3KCJaEyZ5GU

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r tabs (#2030)

* UN-2742 Show profile name instead of LLM name in output tabs

Profiles encode user intent (same model can differ by LLMWhisperer
mode, chunking, etc.), so the profile name is the meaningful label
when comparing outputs. The LLM model name stays available as a
tooltip on each tab.

Applies to the shared tab strip used by the Output Analyzer and the
main combined-output view (JsonView), and the same pattern in
OutputForDocModal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* UN-2742: guarantee non-empty profile tab label and tooltip (review fix)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…N-3735 .value fix

Merge origin/main (23 commits) into the PG-queue integration branch.
Brings in the prompt-service→backend collapse, the UN-3735 destination
status `.value` fix, added backend test suites, and CI workflow updates.

Two conflicts, both resolved deterministically:
- workers/tests/test_legacy_executor_scaffold.py: accept main's removal of
  test_no_flask_import. Main's #2115 deleted it as an obsolete Flask-era
  guard (flask is not a workers dependency) while de-flaking the suite; feat
  had only wrapped the same reload-based test in a namespace snapshot. Net
  effect: -1 test, suite still collects.
- workers/uv.lock: take main's litellm 1.90.3 (forward bump from 1.85.1).
  `uv lock --check` confirms the lock is consistent with the merged manifest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60a50203-6254-48cb-b23d-4a341688684e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backmerge/main-into-feat-20260714

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@muhammad-ali-e
muhammad-ali-e merged commit 971d727 into feat/UN-3445-pg-queue-integration Jul 15, 2026
6 checks passed
@muhammad-ali-e
muhammad-ali-e deleted the backmerge/main-into-feat-20260714 branch July 15, 2026 16:24
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR backmerges 23 commits from main into the feat/UN-3445-pg-queue-integration branch, resolving two conflicts (deletion of an obsolete Flask guard test and a litellm lock bump) deterministically. The incoming changes span several independently landed features.

  • Org controlled-mode flags (restrict_llm_adapter_creation, restrict_connector_creation): two new boolean columns (migrations 0006/0007), enforced in AdapterInstanceViewSet and ConnectorInstanceViewSet with service-account bypass and explicit org-binding on save to close a payload-override path.
  • Platform API key rotation (CanRotatePlatformApiKey): a new permission restricts key-based rotation to full_access tier only, preventing a lower-tier key from reading a newly-rotated higher-tier secret; session callers still require admin.
  • Deployment staging failure fix (deployment_helper.py): pre-dispatch file-staging failures now mark the execution ERROR and release the rate-limit slot, instead of leaving the row stuck as PENDING; error-marking failure is isolated so cleanup always runs.
  • Postman collection improvements: collection variables and a post-response capture script auto-propagate execution_id from the execute response to the status request.
  • Prompt-service → backend collapse: PROMPT_HOST/PROMPT_PORT removed from ToolsUtils and ToolRuntimeVariable.

Confidence Score: 4/5

Safe to merge; all conflict resolutions are deterministic and all incoming logic changes carry their own test coverage.

The deployment staging fix, org controlled-mode flags, and key-rotation permission are well-isolated and each backed by new test suites. The two minor observations (xfail strict=True CI trap and non-JSON content-type coercion in organization_settings) are not blocking. The prompt-service variable removal is a clean deletion with no remaining callers. No data-loss or auth-bypass paths were found.

backend/workflow_manager/endpoint_v2/tests/destination-connectors/test_destination_connector_postgres.py (xfail strict marker) and backend/tenant_account_v2/views.py (content-type constraint on the settings endpoint).

Important Files Changed

Filename Overview
backend/tenant_account_v2/views.py Adds GET/PATCH organization_settings endpoint guarded by IsOrganizationAdmin; two-pass validation (check then mutate) is correct; update_fields correctly lists auto_now field.
backend/adapter_processor_v2/views.py Adds _enforce_llm_creation_restriction called after serializer validation (needs adapter_type from validated data); service-account bypass is correct; org is explicitly bound on save.
backend/connector_v2/views.py Adds _enforce_connector_creation_restriction called before serializer validation (no adapter type needed); org bound explicitly on save to prevent payload bypass.
backend/platform_api/permissions.py New CanRotatePlatformApiKey permission correctly restricts key-based rotation to full_access tier to prevent privilege escalation; session path delegates to IsOrganizationAdmin.
backend/api_v2/deployment_helper.py Wraps file-staging in a scoped try/except so a pre-dispatch failure marks the execution ERROR and cleans up; error-marking failure is also isolated so cleanup always runs.
backend/api_v2/postman_collection/dto.py Adds Postman collection variables and post-response script to auto-capture execution_id; {{execution_id}} variable syntax derived from constant to prevent drift; empty event blocks stripped in to_dict.
backend/workflow_manager/endpoint_v2/tests/destination-connectors/test_destination_connector_postgres.py Adds @pytest.mark.xfail(strict=True) to document a known None→jsonb bug; strict mode means CI breaks with XPASS if bug is fixed without removing the marker.
unstract/workflow-execution/src/unstract/workflow_execution/tools_utils.py Removes prompt_host/prompt_port env var reads as part of prompt-service → backend collapse; matching constants removed from ToolRuntimeVariable.
backend/account_v2/models.py Adds two boolean controlled-mode flags with default=False; migrations 0006/0007 are chained correctly.
workers/tests/test_legacy_executor_scaffold.py Removes test_no_flask_import as resolved conflict: main deleted the obsolete Flask-era guard; 20 tests remain collectible.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[API Deployment POST] --> B{Auth valid?}
    B -- No --> C[403/401/404]
    B -- Yes --> D[Create WorkflowExecution PENDING]
    D --> E{Stage files in API storage}
    E -- failure --> F[mark execution ERROR]
    F --> G[Release rate-limit slot]
    G --> H[Delete API storage dir]
    H --> I[Return ERROR response]
    E -- success --> J[execute_workflow_async]
    J -- dispatch error --> K[Release slot + cleanup]
    J -- success --> L[Return PENDING/COMPLETED response]

    subgraph Org Controlled Mode
        M[LLM Adapter Create] --> N{restrict_llm_adapter_creation?}
        N -- True + not admin --> O[403 PermissionDenied]
        N -- False or admin --> P[Save with org binding]
        Q[Connector Create] --> R{restrict_connector_creation?}
        R -- True + not admin --> S[403 PermissionDenied]
        R -- False or admin --> T[Save with org binding]
    end

    subgraph Key Rotation
        U[PATCH platform-api-key/rotate] --> V{Platform API key?}
        V -- Yes --> W{full_access?}
        W -- No --> X[403]
        W -- Yes --> Y[Rotate key]
        V -- No --> Z{Session admin?}
        Z -- No --> X
        Z -- Yes --> Y
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[API Deployment POST] --> B{Auth valid?}
    B -- No --> C[403/401/404]
    B -- Yes --> D[Create WorkflowExecution PENDING]
    D --> E{Stage files in API storage}
    E -- failure --> F[mark execution ERROR]
    F --> G[Release rate-limit slot]
    G --> H[Delete API storage dir]
    H --> I[Return ERROR response]
    E -- success --> J[execute_workflow_async]
    J -- dispatch error --> K[Release slot + cleanup]
    J -- success --> L[Return PENDING/COMPLETED response]

    subgraph Org Controlled Mode
        M[LLM Adapter Create] --> N{restrict_llm_adapter_creation?}
        N -- True + not admin --> O[403 PermissionDenied]
        N -- False or admin --> P[Save with org binding]
        Q[Connector Create] --> R{restrict_connector_creation?}
        R -- True + not admin --> S[403 PermissionDenied]
        R -- False or admin --> T[Save with org binding]
    end

    subgraph Key Rotation
        U[PATCH platform-api-key/rotate] --> V{Platform API key?}
        V -- Yes --> W{full_access?}
        W -- No --> X[403]
        W -- Yes --> Y[Rotate key]
        V -- No --> Z{Session admin?}
        Z -- No --> X
        Z -- Yes --> Y
    end
Loading

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
backend/workflow_manager/endpoint_v2/tests/destination-connectors/test_destination_connector_postgres.py:193-205
**`xfail(strict=True)` will break CI when the bug is fixed without touching this file**

`strict=True` means the test is expected to fail; if the underlying `None → SQL NULL` fix lands (removing the `'None'` literal serialisation), the test will unexpectedly pass and pytest reports it as `XPASS → FAILED`. The CI failure message will be "test_insert_into_db_with_error_postgresql unexpectedly passed", which is easy to misread. Consider adding the Jira/GH issue reference to the `reason` string so the next developer immediately knows which ticket to check and what to remove.

### Issue 2 of 2
backend/tenant_account_v2/views.py:66-114
**Boolean coercion fails silently for non-JSON content types**

`isinstance(value, bool)` correctly rejects integers and strings when the request body is JSON (DRF's JSON parser produces Python `bool`). However, if a caller sends the same body as `application/x-www-form-urlencoded` or `multipart/form-data`, DRF parses every value as a string (`"true"` / `"false"`), and the check returns a 400 with a message like `"restrict_llm_adapter_creation must be a boolean"`. The endpoint could add a `parser_classes = [JSONParser]` declaration to make the contract explicit and prevent the confusing error.

Reviews (1): Last reviewed commit: "UN-3445 [MISC] backmerge main into feat ..." | Re-trigger Greptile

Comment on lines 193 to 205
f"✅ Successfully inserted test data into PostgreSQL table: {self.test_table_name}"
)

@pytest.mark.xfail(
reason=(
"get_sql_values_for_query serializes data=None as the literal "
"string 'None', which is invalid JSON for the jsonb data column "
"(PR #2115 review). Remove this marker once None maps to SQL NULL."
),
strict=True,
)
def test_insert_into_db_with_error_postgresql(self) -> None:
"""Test insertion with error parameter into real PostgreSQL database."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 xfail(strict=True) will break CI when the bug is fixed without touching this file

strict=True means the test is expected to fail; if the underlying None → SQL NULL fix lands (removing the 'None' literal serialisation), the test will unexpectedly pass and pytest reports it as XPASS → FAILED. The CI failure message will be "test_insert_into_db_with_error_postgresql unexpectedly passed", which is easy to misread. Consider adding the Jira/GH issue reference to the reason string so the next developer immediately knows which ticket to check and what to remove.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/workflow_manager/endpoint_v2/tests/destination-connectors/test_destination_connector_postgres.py
Line: 193-205

Comment:
**`xfail(strict=True)` will break CI when the bug is fixed without touching this file**

`strict=True` means the test is expected to fail; if the underlying `None → SQL NULL` fix lands (removing the `'None'` literal serialisation), the test will unexpectedly pass and pytest reports it as `XPASS → FAILED`. The CI failure message will be "test_insert_into_db_with_error_postgresql unexpectedly passed", which is easy to misread. Consider adding the Jira/GH issue reference to the `reason` string so the next developer immediately knows which ticket to check and what to remove.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Comment on lines +66 to +114
@api_view(["GET", "PATCH"])
@permission_classes([IsAuthenticated, IsOrganizationAdmin])
def organization_settings(request: Request) -> Response:
"""Read or update org-level settings. Admin-only.

Exposes the controlled-mode flags in ``ORGANIZATION_SETTING_FLAGS``
(``restrict_llm_adapter_creation``, ``restrict_connector_creation``). GET
returns the current values; PATCH updates any subset of them.
"""
organization = UserContext.get_organization()
if not organization:
return Response(
status=status.HTTP_404_NOT_FOUND,
data={"message": "Org Not Found"},
)

if request.method == "PATCH":
provided = {
flag: request.data[flag]
for flag in ORGANIZATION_SETTING_FLAGS
if flag in request.data
}
if not provided:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": (
"Provide at least one of: "
f"{', '.join(ORGANIZATION_SETTING_FLAGS)}"
)
},
)
# Validate everything before mutating so a bad value in a later flag
# doesn't leave the in-memory org partially updated.
for flag, value in provided.items():
if not isinstance(value, bool):
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"message": f"{flag} must be a boolean"},
)
for flag, value in provided.items():
setattr(organization, flag, value)
organization.modified_by = request.user
organization.save(update_fields=[*provided, "modified_by", "modified_at"])

return Response(
status=status.HTTP_200_OK,
data={flag: getattr(organization, flag) for flag in ORGANIZATION_SETTING_FLAGS},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Boolean coercion fails silently for non-JSON content types

isinstance(value, bool) correctly rejects integers and strings when the request body is JSON (DRF's JSON parser produces Python bool). However, if a caller sends the same body as application/x-www-form-urlencoded or multipart/form-data, DRF parses every value as a string ("true" / "false"), and the check returns a 400 with a message like "restrict_llm_adapter_creation must be a boolean". The endpoint could add a parser_classes = [JSONParser] declaration to make the contract explicit and prevent the confusing error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/tenant_account_v2/views.py
Line: 66-114

Comment:
**Boolean coercion fails silently for non-JSON content types**

`isinstance(value, bool)` correctly rejects integers and strings when the request body is JSON (DRF's JSON parser produces Python `bool`). However, if a caller sends the same body as `application/x-www-form-urlencoded` or `multipart/form-data`, DRF parses every value as a string (`"true"` / `"false"`), and the check returns a 400 with a message like `"restrict_llm_adapter_creation must be a boolean"`. The endpoint could add a `parser_classes = [JSONParser]` declaration to make the contract explicit and prevent the confusing error.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants