Skip to content

feat(org): shared LLM provider connections (cloud) - #219

Open
juanmichelini wants to merge 9 commits into
mainfrom
feat/org-provider-connections
Open

feat(org): shared LLM provider connections (cloud)#219
juanmichelini wants to merge 9 commits into
mainfrom
feat/org-provider-connections

Conversation

@juanmichelini

@juanmichelini juanmichelini commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

HUMAN:

  • A human has tested these changes.

Evidence:

image image

To test, go to

https://pr-795.staging.all-hands.dev/canvas/settings/llm
Then click add connection

  • select openhands in the combobox
  • pick any name you want
  • for api key you need your staging api key
image

Then create an LLM and select that provider connection.

image

AGENT:


Why

Org-level LLM profiles on cloud/SaaS currently carry credential material inline. Rotating a shared key means editing every profile that uses it. A provider connection is an org-level, named bundle of that credential material — an api_key plus an optional base_url — that multiple LLM profiles can reference by id, so rotating a shared key in one place updates every profile that points at it.

This is the cloud/enterprise counterpart to:

Both of those gated the feature off on cloud. This PR implements the cloud backend.

Summary

  • Adds an org-level provider-connections store (openhands/app_server/settings/provider_connections.py container model + org.provider_connections EncryptedJSON column, migration 151), exposed over a CRUD router at /api/organizations/{org_id}/provider-connections with SELECT ... FOR UPDATE serialization, EDIT_ORG_SETTINGS/VIEW_ORG_SETTINGS permissions, a 409 delete-ref guard, and read-at-use credential resolution at the profile-activation choke point (dangling reference → 422).

Issue Number

N/A — feature work; no tracking issue. Counterparts: OpenHands/software-agent-sdk#4492, OpenHands/OpenHands#16616.

Design

Mirrors the existing org LLM-profiles design rather than inventing a new one:

  • Storage: connections live in an EncryptedJSON blob on the org row (org.provider_connections), not a relational table. The column is the at-rest encryption boundary, so each connection's api_key rides in cleartext inside the encrypted envelope — the same contract llm_profiles already uses. Envelope shape: {connections: {<id>: ProviderConnection}}.
  • Concurrency: every mutation runs inside SELECT ... FOR UPDATE on the org row, so concurrent writes serialize instead of racing.
  • Permissions: CRUD requires EDIT_ORG_SETTINGS; listing requires VIEW_ORG_SETTINGS.
  • Referential integrity in code: deleting a connection still referenced by a profile returns 409. Both collections live on the same org row, so the FOR UPDATE lock makes the referrer check and the delete atomic (no TOCTOU window).
  • Resolution at activation: at the single profile-activation choke point, a linked profile's provider_connection_id is resolved into concrete credentials before the key is masked/snapshotted into the member's settings. A dangling reference returns 422. Resolution is read-at-use, so rotating a shared key takes effect the next time a linked profile is activated — nothing is pushed retroactively into running conversations.

What's included

  • openhands/app_server/settings/provider_connections.pyProviderConnections container model (create/update/delete/list, per-org limit, id validation, secret-safe summaries).
  • enterprise/server/routes/org_provider_connections.py — CRUD router at /api/organizations/{org_id}/provider-connections, mounted in saas_server.py.
  • enterprise/storage/org.py — new provider_connections EncryptedJSON column.
  • enterprise/migrations/versions/151_add_provider_connections_to_org.py — additive migration, no backfill (NULL reads back as empty). Chains off main's revision 150.
  • enterprise/server/routes/org_profiles.py_resolve_provider_connection wired into activate_profile.
  • Tests: 21 provider-connection cases (model invariants, CRUD, secret handling, delete ref-guard, resolution-at-activation incl. dangling to 422, env-limit handling) + 3 migration-151 cases + a migration-integrity regression test against the checked-in versions dir.

How to Test

Unit/integration tests (SQLite in-memory, real async sessions, encryption-on-persist verification):

cd enterprise
poetry install --with dev,test
PYTHONPATH=".:$PYTHONPATH" poetry run pytest \
  tests/unit/test_org_provider_connections.py \
  tests/unit/test_migration_151_add_provider_connections_to_org.py \
  tests/unit/test_org_profiles.py

Migration chain (requires a Postgres DSN in env; CI runs alembic upgrade head + downgrade/upgrade round-trip on the Apply migrations on test DB job):

cd enterprise && poetry run alembic history && poetry run alembic upgrade head

Manual API exercise against the published enterprise image (ghcr.io/openhands/enterprise-server:sha-<this pr>), with an org-admin session:

# create
curl -X POST "$HOST/api/organizations/$ORG_ID/provider-connections" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"display_name":"Shared OpenAI","provider":"openai","api_key":"sk-..."}'
# list (secrets never returned)
curl "$HOST/api/organizations/$ORG_ID/provider-connections" -H "Authorization: Bearer $TOKEN"
# rotate key — takes effect on next activation of any linked profile
curl -X PATCH "$HOST/api/organizations/$ORG_ID/provider-connections/$CONN_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"api_key":"sk-new..."}'
# delete guard: 409 while any profile references it
curl -X DELETE "$HOST/api/organizations/$ORG_ID/provider-connections/$CONN_ID" -H "Authorization: Bearer $TOKEN"

Video/Screenshots

Backend-only change (no UI); behavior is covered by the automated tests above.

Type

  • Feature

Notes

  • SDK pin is now a real release. An earlier revision of this PR temporarily git-pinned openhands-sdk / openhands-agent-server / openhands-tools to the unreleased merge commit of software-agent-sdk#4492 (73fabfd). That feature has shipped in SDK 1.43.0, and this PR now pins ==1.43.0 in pyproject.toml + uv.lock + both poetry.lock files. pillow stays at 12.3.0 because SDK 1.43.0 requires pillow>=12.3.0 at runtime.
  • Migrations: this PR adds migration 151 (chains off main's 150). The Apply migrations on test DB CI job runs the full chain on both DB drivers.
  • Follow-ups (separate PRs): Canvas frontend: expose the provider-connections UI on cloud (currently local-only).

This PR was created by an AI agent (OpenHands) on behalf of the user.


Enterprise server image for this PR:

ghcr.io/openhands/enterprise-server:sha-c21c869

Adds org-level shared LLM provider connections so multiple LLM profiles can
reference a single credential (api_key + optional base_url) by id. Rotating the
shared key in one place updates every profile that points at it.

Backend (enterprise app-server), mirroring the existing org LLM-profiles design:
- Store connections as an EncryptedJSON blob on the org row
  (org.provider_connections), NOT a relational table. The column is the at-rest
  encryption boundary; the api_key rides in cleartext inside the encrypted
  envelope, same contract as llm_profiles.
- ProviderConnections container model with create/update/delete/list, per-org
  limit, id validation, and secret-safe summaries.
- CRUD router (/api/organizations/{org_id}/provider-connections) mounted in
  saas_server. Mutations serialize via SELECT ... FOR UPDATE on the org row.
  CRUD requires EDIT_ORG_SETTINGS; listing requires VIEW_ORG_SETTINGS.
- Referential integrity enforced in code: deleting a connection still
  referenced by a profile returns 409; both collections share the org-row lock
  so the check+delete is atomic.
- Resolution wired at the profile-activation choke point: a linked profile's
  provider_connection_id is resolved into concrete credentials before the key
  is masked/snapshotted. Dangling reference returns 422. Read-at-use, so key  is masked/snapshotted. Dangling reference returnsation 150 adds the org.provider_connections column (no backfill; NULL
  reads back as emp  reads back as emp  reads back as emp  reads back as emp  reads back as to the
unreleased commit that adds LLM.provider_connection_id
(software-agent-sdk#4492, 73fabfd), since no published release contains it yet.
Pinned in both uv ([tool.uv.sources]) and poetry, including direct deps in
enterprise/pyproject.toml because Poetry resolves the openhands-ai path
dependency's PEP 621 pins. Bumped pillow 12.2.0 -> 12.3.0 to satisfy the
unreleased SDK. Revert to a normal vunreleased SDK. Revert to a normal vunreleased SDK. Revert to a norm munreleased SDK. Revert to a normal vunreleased SDK. Revert to a normal vunrelnd resolution-at-activation (incl. 422
on dangling reference).

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions github-actions Bot added the type: feat A new feature label Aug 20, 2026
@github-actions

Copy link
Copy Markdown

⚠️ This PR contains migrations. Please synchronize before merging to prevent conflicts.

openhands-agent and others added 3 commits August 21, 2026 00:41
The Docker image build ran 'poetry install --no-root' against the root
pyproject.toml + poetry.lock and failed with 'pyproject.toml changed
significantly since poetry.lock was last generated'. The SDK deps were switched
to a git rev in pyproject.toml (and uv.lock / enterprise/poetry.lock were
regenerated) but the root poetry.lock was not, so the enterprise-server image
never built. Regenerate it so the image builds and publishes its sha- tag.

Co-authored-by: openhands <openhands@all-hands.dev>
The enterprise stage runs 'poetry export --only main' (with hashes) then
'pip install -r requirements.txt'. With the SDK deps pinned to a git rev, pip
aborts: 'Can't verify hashes for these requirements because we don't have a way
to hash version control repositories'. openhands-sdk/agent-server/tools are
already installed in the base venv from the root 'poetry install', so strip
their software-agent-sdk git lines from requirements.txt, exactly as the
openhands-ai local path dep is already stripped.

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  enterprise
  saas_server.py 15-24, 60-66, 85, 171-177
  enterprise/migrations/versions
  151_add_provider_connections_to_org.py 33, 37
  enterprise/server/routes
  org_profiles.py 146-164, 169, 381-382
  org_provider_connections.py 87-90, 118-126, 130, 143-144, 152-155, 171-188, 203-205, 221-248, 267-295, 314-333
  enterprise/storage
  org.py
Project Total  

This report was generated by python-coverage-comment-action

@juanmichelini juanmichelini left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taste Rating: 🟡 Acceptable — the core design has good taste (mirroring llm_profiles instead of inventing new storage machinery), but there is one deploy-breaking bug and a pile of self-admitted temporary pins sitting between this PR and the merge button.

[CRITICAL ISSUES] (Must fix)

  • [enterprise/migrations/versions/150_add_provider_connections_to_org.py, Line 26] Migration revision collision: main already ships 150_add_daily_conversation_limit.py with revision: '150'. This PR adds a second revision 150, also with down_revision: '149'. Alembic will explode with a duplicate-revision / branched-head error the moment this rolls to any environment. Renumber to 151 with down_revision: '150'. The one CI job that would have caught this — "Apply migrations on test DB" — was skipped on this PR, which is exactly how this slipped through 4 failing checks.
  • [openhands/app_server/settings/provider_connections.py, Line 198] CI is red and mypy found a real bug: ProviderConnections.list() (line 195) shadows the builtin, so the -> list[...] annotations inside the class now refer to the method (Function ... not valid as a type). Rename list() to something like all() / values(), or you will keep tripping over builtins for the life of this module. Lint python, Lint enterprise python, check-package-versions, and check-sync are all failing — nothing with four red checks should describe itself as ready.
  • [pyproject.toml, enterprise/pyproject.toml, uv.lock, poetry.lock] Git-pinned to an unreleased SDK commit: the PR pins openhands-sdk / openhands-agent-server / openhands-tools to commit 73fabfd (merged 2026-08-19, ~2 days ago) in four manifests, plus a forced pillow 12.2.0 → 12.3.0 bump. Your own description says "before this can ship: swap the git pins back to a normal ==<version> pin." Correct. This PR is not mergeable until an SDK release containing #4492 is published and pinned properly. An unreleased commit ~2 days old fails any reasonable supply-chain bar for a credentials-handling feature, even if it is a sibling repo.

[IMPROVEMENT OPPORTUNITIES] (Should fix)

  • [enterprise/server/routes/org_profiles.py, Line 151] Bogus lazy import: the comment claims the import of _load_connections is lazy "to keep this module importable if the settings package layout shifts" and to avoid "a hard import cost on the hot non-linked path." There is no circular import here (org_provider_connections never imports org_profiles), and both routers are imported at server startup anyway. This is a comment justifying a workaround for a problem that doesn't exist — move it to the top of the file and delete the comment.
  • [enterprise/server/routes/org_provider_connections.py, Line 220] Function-level import: import uuid as _uuid inside the handler for no reason. Top of file. One line.
  • [openhands/app_server/settings/provider_connections.py, Line 70] Import-time configuration: MAX_CONNECTIONS_PER_ORG is frozen at module import, so MAX_PROVIDER_CONNECTIONS_PER_ORG set after startup is silently ignored, and tests must patch a module constant instead of flipping an env var. Read the env at call time, or accept that it's a constant and drop the env-var fiction.
  • [enterprise/server/routes/org_provider_connections.py, Line 46] Docstring lies about GET: the module docstring says "List/Get: VIEW_ORG_SETTINGS" but there is no single-connection GET endpoint. Either add the endpoint or fix the docstring — comments that describe non-behavior drift and mislead.

[TESTING GAPS]

  • The 19 unit tests are genuinely good — real async sessions, real HTTPX round trips, encryption-on-persist verification, the 409 ref-guard, the dangling-to-422 resolution. That's real coverage, not mock theater. But there is no migration test, and the migration is the thing that's broken. Back-to-back deployments (main's 150 + this 150) will fail at alembic upgrade — add an integration test that runs the migration chain head-to-head, or at minimum un-skip the "Apply migrations on test DB" job for PRs touching enterprise/migrations/.

[PR TEMPLATE] Per the repo's review guidelines (custom-codereview-guide.md): this PR does not follow .github/pull_request_template.md — missing Why, Issue Number, How to Test, Video/Screenshots, and Type. (An attempt to convert to draft was not permitted, so this is left as a comment per the guide.) For a backend PR publishing a GHCR image, How to Test with the exact curl commands against the published image would be genuinely useful, not bureaucratic.

[RISK ASSESSMENT]

  • ⚠️ Risk Assessment: 🔴 HIGH
    This PR (a) introduces a new at-rest store for API credentials (correctly using EncryptedJSON and org-row FOR UPDATE locking — good), (b) contains a migration that will break the next deploy via the duplicate alembic revision, and (c) temporarily depends on an unreleased, 2-day-old SDK commit pinned by SHA across four manifests.
    Recommendation: Do not auto-merge. Fix the migration revision, wait for an SDK release containing OpenHands/software-agent-sdk#4492, and get human sign-off on the credential-storage boundary (summaries/response models stay secret-free; expose_secrets only on the persistence path — verified, keep it that way).

VERDICT:
Needs rework: the design is sound and the tests are good, but the alembic collision is a deploy-breaker, CI is red from a real mypy bug, and the whole thing rides on a dependency the PR itself says must be replaced before merge.

KEY INSIGHT: Elegant design executed one revision number and one release too early — renumber the migration to 151, wait for an SDK release, and this becomes mergeable; as it stands, alembic upgrade fails on any database that has run main's revision 150.


Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.
  2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it's merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.


This review was prepared by an AI agent (OpenHands) on behalf of the user.

@juanmichelini
juanmichelini marked this pull request as ready for review August 21, 2026 13:17
@juanmichelini

Copy link
Copy Markdown
Contributor Author

@OpenHands please address the reviewer concerns

@openhands-ai

openhands-ai Bot commented Aug 21, 2026

Copy link
Copy Markdown

I'm on it! juanmichelini can track my progress at all-hands.dev

juanmichelini and others added 3 commits August 21, 2026 10:29
- Migration: renumber to 151 (main already owns revision 150), fixing the
  duplicate alembic revision that would break deploys; add a unit test
  pinning revision/down_revision plus an upgrade/downgrade op test, and
  assert the checked-in versions dir passes the integrity script from the
  root unit suite.
- Pins: replace the temporary git pin (73fabfd) with the published SDK
  1.43.0 release (contains software-agent-sdk#4492) in pyproject,
  uv.lock, and both poetry.lock files; keep pillow 12.3.0 (SDK 1.43.0
  requires pillow>=12.3.0 at runtime).
- Model: rename ProviderConnections.list() to all() to stop shadowing
  the builtin and unblock mypy.
- Limit: read MAX_PROVIDER_CONNECTIONS_PER_ORG at call time instead of
  freezing a module-level constant at import.
- Router: move the lazy _load_connections import in org_profiles and the
  function-level uuid import in org_provider_connections to the top of
  each module; fix the module docstring to stop claiming a single-object
  GET endpoint exists.

Co-authored-by: openhands <openhands@all-hands.dev>

Copy link
Copy Markdown
Contributor Author

@juanmichelini Thanks for the review — all concerns addressed in 096fa25 (pushed as 6e8c46d).

PR TEMPLATE

  • PR description restructured to the template: ## Why, ## Summary, ## Issue Number, ## How to Test (unit tests + alembic chain + manual curl exercise), ## Type. It's still a draft pending human review.

CRITICAL

  1. Alembic revision collision — renamed 150_add_provider_connections_to_org.py151_add_provider_connections_to_org.py (revision='151', down_revision='150'), chaining off main's 150_add_daily_conversation_limit. Added enterprise/tests/unit/test_migration_151_add_provider_connections_to_org.py (pins revision/down_revision, tests upgrade/downgrade ops) and test_real_enterprise_migrations_have_a_single_linear_chain in tests/unit/test_enterprise_migration_integrity.py so the checked-in versions dir is validated by the unit suite, not just CI. scripts/check_enterprise_migration_integrity.py passes; alembic history shows a single head at 151.
  2. ProviderConnections.list() shadowing builtin — renamed to all() (and all_summaries()). Confirmed this was the mypy blocker: mypy --config-file dev_config/python/mypy.ini openhands/app_server/settings/provider_connections.py now passes clean.
  3. Unreleased SDK pin — software-agent-sdk 1.43.0 is out and includes #4492 (verified 73fabfd is in the release's git history). All pins are now ==1.43.0 in pyproject.toml (PEP 621 + Poetry section), uv.lock, and both poetry.lock files; the direct git deps in enterprise/pyproject.toml and the TEMPORARY-pin note are gone. Kept pillow 12.3.0 because SDK 1.43.0 declares pillow>=12.3.0 at runtime — the PR body now reflects this.

SHOULD FIX

  • MAX_PROVIDER_CONNECTIONS_PER_ORG env var is now read at call time inside save() (no import-time constant), with tests for custom value, default (64), and invalid-value fallback.
  • Router docstring corrected: removed the claim of a single-object GET /{id} endpoint, and the permission model now documents List = VIEW_ORG_SETTINGS, CRUD = EDIT_ORG_SETTINGS.
  • Moved the lazy from server.routes.org_provider_connections import _load_connections (org_profiles.py) and the function-level import uuid (org_provider_connections.py) to the top of each module; module-level cycle check passes.

Verification

  • enterprise tests: test_org_provider_connections.py + test_migration_151_add_provider_connections_to_org.py + test_org_profiles.py + test_agent_profiles.py (97 tests) pass; full tests/unit/server (828) and tests/unit/storage (243) pass.
  • Root tests/unit/test_enterprise_migration_integrity.py (11) passes, including the new real-dir check.
  • Enterprise ruff (--config enterprise/dev_config/python/ruff.toml, v0.12.5) clean on all changed files; mypy clean on the changed files (remaining enterprise mypy errors are pre-existing in unrelated integration/slack files).
  • Lockfile consistency: uv lock --check and poetry check --lock (root + enterprise) pass, regenerated with the lockfiles' original tool versions (uv 0.12.0, Poetry 2.3.4).

This comment was created by an AI agent (OpenHands) on behalf of the user.

@openhands-ai

openhands-ai Bot commented Aug 21, 2026

Copy link
Copy Markdown

All of juanmichelini's review concerns on PR #219 have been addressed, committed (096fa25), and pushed (6e8c46d), with a resolution comment posted and the PR body updated to the required template.

Critical items:

  1. Alembic revision collision — migration renumbered 150151_add_provider_connections_to_org.py, chaining off main's 150_add_daily_conversation_limit. Added a migration test (revision pin + upgrade/downgrade ops) and a new regression test in tests/unit/test_enterprise_migration_integrity.py that validates the checked-in versions dir, so this failure mode is caught by unit tests, not just CI. check_enterprise_migration_integrity.py passes and alembic heads shows a single linear head at 151.
  2. list() builtin shadow — renamed to ProviderConnections.all() / all_summaries(). Confirmed this was the mypy blocker; root mypy on the file is now clean.
  3. Unreleased SDK git pin — software-agent-sdk 1.43.0 (released 2026-08-21) includes #4492 (verified 73fabfd is in its history), so all manifests now pin ==1.43.0: root pyproject.toml (PEP 621 + Poetry sections), uv.lock, root + enterprise poetry.lock (regenerated with the original tool versions — uv 0.12.0, Poetry 2.3.4). The temporary direct git deps in enterprise/pyproject.toml are removed. pillow stays at 12.3.0 since SDK 1.43.0 requires pillow>=12.3.0 at runtime — PR body updated accordingly.

Should-fix items: env-var limit now read at call time (with tests for custom/default/invalid values); router docstring fixed (removed the nonexistent single-object GET claim; documented List=VIEW / CRUD=EDIT); lazy imports in org_profiles.py and function-level uuid import moved to module top.

Verification: 97 targeted enterprise tests pass (provider connections, migration 151, org profiles, agent profiles), plus full tests/unit/server (828) and tests/unit/storage (243). Enterprise ruff clean on all changed files; mypy clean on changed files (remaining enterprise mypy errors are pre-existing in unrelated slack/jira integration files). uv lock --check and poetry check --lock pass.

One caveat: the provided GITHUB_TOKEN cannot read CI status checks on this repo ("Resource not accessible by integration"), so I couldn't watch the new CI run — worth confirming the previously failing lint/mypy jobs go green on 6e8c46d. The branch also received an upstream merge of main mid-session; I merged it cleanly and re-verified everything before pushing.

PR: #219

@juanmichelini

Copy link
Copy Markdown
Contributor Author

🟢 Good tasteApprove with one cleanup.

This is well-built. It doesn't invent a new pattern; it copies the proven llm_profiles design almost verbatim — same EncryptedJSON envelope on the org row, same SELECT … FOR UPDATE serialization, same degrade-to-empty-on-schema-drift, same expose_secrets serializer contract. Good taste is eliminating special cases by reusing an existing abstraction, and that's exactly what happened here. No function exceeds three levels of nesting, the data structure (a dict keyed by the connection id) matches the access pattern, and read-at-use resolution at the activation choke point is the correct call — rotating a shared key takes effect on next activation instead of being retroactively pushed into running conversations. The delete-ref guard and the delete share one FOR UPDATE lock, so there's no TOCTOU window. Real tests back it: SQLite + real async sessions asserting on persisted encrypted state, the 422 dangling path, and the 409 delete guard — not a pile of mocks.

Linus's Three Questions:

  1. Real problem? Yes — rotating a shared key across N inline profiles is a real operational pain.
  2. Simpler way? This is the simpler way (mirror the neighbor).
  3. What breaks? Nothing — unlinked profiles hit a byte-identical old path; migration is additive with NULL→empty.

[IMPROVEMENT OPPORTUNITIES]

  • [containers/app/Dockerfile, Line 153] Stale cruft from the transient git-pin: The sed pattern /software-agent-sdk/d matches nothing in the exported requirements — the packages are openhands-sdk / openhands-agent-server / openhands-tools from PyPI (verified in enterprise/poetry.lock: name = "openhands-sdk", version = "1.43.0", with hashes). There is no software-agent-sdk line to delete. The comment ("the openhands-sdk/agent-server/tools git deps … pip rejects VCS requirements") is also stale — this PR is the one that moved them off the git pin onto the ==1.43.0 PyPI release, so calling them "git deps" and justifying the strip with the VCS-hash rule no longer applies. This is dead code + a misleading comment left over from an earlier revision of this same PR. Either drop the /software-agent-sdk/d term (the -e and openhands-ai strips already cover the local path dep) or repoint it at the real package names if you still intend to keep already-installed SDK packages out of the hashed install. Harmless today (it's a no-op), but exactly the kind of "narrates change history" noise that misleads the next reader.

  • [openhands/app_server/settings/provider_connections.py, Line 195 & enterprise/server/routes/org_provider_connections.py, Line 137] Two implementations of "is the key set": ProviderConnections.summaries() computes api_key_set via has_real_api_key(conn.api_key), while the router's _to_response computes it via conn.api_key_value() is not None. They're semantically equivalent today, but they're two code paths with no mechanism to stay in sync — fix one and the other silently disagrees. Notably, summaries() is dead in the prod path: the LLM-profiles router calls profiles.summaries(...) directly, but this router bypasses it for _to_response. Pick one path (either have the router use summaries() like its sibling, or drop summaries() and keep _to_response) so there's a single source of truth.

  • [enterprise/server/routes/org_provider_connections.py, Line 279] Readability, not correctness: the model_copy(update={...}) with four conditional **({... if 'x' in fields else {}}) spreads works but reads like a puzzle. Building a plain updates: dict with four if statements first, then a single model_copy(update=updates), would be easier to follow. Pure taste; leave it if you prefer the expression form.

[STYLE NOTES]

  • [openhands/app_server/settings/provider_connections.py, Line 45] _get_max_connections_per_org has two near-identical warning/fallback branches (non-positive vs. non-int). One consolidated try/except (ValueError) with a single fallback would dedupe the logging. Trivial.

[TESTING GAPS]

None. The suite exercises real code paths (persist→decrypt round-trip, activation resolution, dangling→422, delete→409) and asserts on state, not mocked calls. The 64-iteration default-limit test is a touch slow but proves the real ceiling.

[DEPENDENCY CHANGES]

openhands-sdk / openhands-agent-server / openhands-tools bumped 1.42.1 → 1.43.0. First-party (same org), published 2026-08-21, hashes present in all three lockfiles — outside the 7-day rule for first-party but scrutinized: clean. pillow bumped 12.2.0 → 12.3.0 (third-party, published 2026-07-01, >7 days old) — required at runtime by SDK 1.43.0 (pillow>=12.3.0). No downgrades anywhere; the 287 deletions are all lockfile churn from the version bumps.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW
    Additive, non-backfill migration (NULL reads as empty); feature is off by default and only lights up for profiles that opt into provider_connection_id; unlinked profiles are byte-identical to the old path. Mirrors a proven, already-shipped design in a sibling codebase. No public API breakage.

VERDICT:
Worth merging — core logic is sound; address the Dockerfile sed/comment cleanup (and ideally the duplicated api_key_set path) either here or in a follow-up.

KEY INSIGHT:
The strength of this PR is that it adds a new capability by deliberately not designing anything new — it clones the adjacent llm_profiles contract end-to-end, which is why the risk is low and the review is short.


Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.
  2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it's merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.

This review was created by an AI agent (OpenHands) on behalf of the user.

Co-authored-by: openhands <openhands@all-hands.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants