Skip to content

[AgentProfile][agent-server] active_agent_profile_id + /api/agent-profiles router + migration seed#3781

Merged
simonrosenberg merged 9 commits into
mainfrom
agent-profile-router
Jun 18, 2026
Merged

[AgentProfile][agent-server] active_agent_profile_id + /api/agent-profiles router + migration seed#3781
simonrosenberg merged 9 commits into
mainfrom
agent-profile-router

Conversation

@simonrosenberg

@simonrosenberg simonrosenberg commented Jun 17, 2026

Copy link
Copy Markdown
Member

HUMAN:

Phase 2 of the AgentProfile epic (#3713): serve the new store over HTTP and add the separate, pointer-only active-profile selector. Reviewed the seed mapping and the FK-on-delete wiring locally; behaviour matches the issue's creation-time-only contract.


AGENT:

Why

Epic #3713 (AgentProfiles) needs the merged AgentProfileStore (#3716) exposed over HTTP so the frontend/cloud can list, edit, and select named agent launch specs. Selecting an AgentProfile is creation-time-only: unlike the existing LLM /activate (which rewrites agent_settings.llm), activating an AgentProfile must only move a pointer and never mutate live agent_settings. This PR adds that surface and a one-time lazy migration so existing single-config users land with one profile already present.

Runs in parallel with #3717 (the resolver); it depends only on the merged store, not the resolver.

Summary

  • Active pointer: PersistedSettings.active_agent_profile_id: str | None = None — distinct from active_profile (the active LLM profile name). Additive with a default, so older settings files load with it None (no schema-version bump). Threaded through SettingsUpdatePayload + update(), the SDK SettingsResponse/SettingsUpdateRequest, and the settings_router GET/PATCH passthrough (PATCH can set or clear it, independently of active_profile).
  • /api/agent-profiles router (mirrors profiles_router.py): list / get / save / delete / rename + POST /{id}/activate that sets only active_agent_profile_id via settings_store.updateno agent_settings write. Activation is keyed on the stable id, so it survives renames. FK/store error mapping: ProfileReferenced/FileExistsError → 409, FileNotFoundError → 404, TimeoutError → 503, ValueError → 400, ProfileLimitExceeded → 409.
  • Lazy migration seed: first GET /api/agent-profiles on an empty store with a null pointer seeds exactly one default profile from the current agent_settings (OpenHands → references the active LLM profile, falling back to "default"; ACP → an ACP profile; mcp_server_refs=null = all) and sets the active pointer. Conservative — one profile, never one-per-LLM-profile.
  • FK guard on the LLM router: profiles_router delete/rename now go through delete_llm_profile / rename_llm_profile, so deleting an LLM profile an AgentProfile references returns 409 naming the referrers, and renaming cascades the llm_profile_ref.
  • Router mounted in api.py next to profiles_router.

Deferred: POST /{id}/materialize is a fast-follow after #3717 — it will load + decrypt the global mcp_config via decrypt_mcp_config_secrets, call the resolver's dry-run, and map DanglingMcpServerRef → 422. Leaving it out keeps this PR green and mergeable independently.

Issue Number

Closes #3719 (epic #3713). Parallel to #3717.

How to Test

Automated (run from repo root):

uv run ruff check <changed files>            # All checks passed
uv run ruff format --check <changed files>   # all formatted
uv run pyright <source + tests>              # 0 errors, 0 warnings
uv run pytest tests/agent_server/test_agent_profiles_router.py \
              tests/agent_server/test_profiles_router.py \
              tests/agent_server/test_settings_router.py -q   # 171 passed

Broader sweep (tests/agent_server tests/cross/test_check_persisted_settings_compat.py tests/sdk/profiles tests/sdk/llm/test_llm_profile_store.py tests/sdk/test_settings.py): 1477 passed. The single unrelated failure (test_conversation_service.py::TestAutoTitle::test_autotitle_sets_title_on_first_user_message) is pre-existing on main — those files are byte-identical to origin/main and the test fails there too; it is untouched by this PR.

Live end-to-end against the real app (create_app + FastAPI TestClient, isolated temp HOME/persistence dirs):

### GET /api/agent-profiles (first call → lazy seed) -> 200
{ "profiles": [ { "name": "default", "agent_kind": "openhands",
                  "llm_profile_ref": "default", "mcp_server_refs": null, "id": "66ff17f8-..." } ],
  "active_agent_profile_id": "66ff17f8-..." }              # seeded + pointer set

### POST /api/agent-profiles/my-agent -> 201
### GET  /api/agent-profiles/my-agent -> 200  (id e023c9d7-...)

### POST /api/agent-profiles/e023c9d7.../activate -> 200
active_agent_profile_id after activate: e023c9d7-... == e023c9d7-...  -> True
agent_settings unchanged by activate: True                 # pointer-only contract holds

after clear -> active_agent_profile_id: None | active_profile: keep-me   # independent set/clear

### DELETE /api/profiles/base-llm (referenced → 409) -> 409
{ "detail": "LLM profile is referenced by 1 agent profile(s): refs-base" }

Type

  • Feature

Notes


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:bfbe52a-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-bfbe52a-python \
  ghcr.io/openhands/agent-server:bfbe52a-python

All tags pushed for this build

ghcr.io/openhands/agent-server:bfbe52a-golang-amd64
ghcr.io/openhands/agent-server:bfbe52a5a6a03e8de757b3e700c55d0557058331-golang-amd64
ghcr.io/openhands/agent-server:agent-profile-router-golang-amd64
ghcr.io/openhands/agent-server:bfbe52a-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:bfbe52a-golang-arm64
ghcr.io/openhands/agent-server:bfbe52a5a6a03e8de757b3e700c55d0557058331-golang-arm64
ghcr.io/openhands/agent-server:agent-profile-router-golang-arm64
ghcr.io/openhands/agent-server:bfbe52a-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:bfbe52a-java-amd64
ghcr.io/openhands/agent-server:bfbe52a5a6a03e8de757b3e700c55d0557058331-java-amd64
ghcr.io/openhands/agent-server:agent-profile-router-java-amd64
ghcr.io/openhands/agent-server:bfbe52a-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:bfbe52a-java-arm64
ghcr.io/openhands/agent-server:bfbe52a5a6a03e8de757b3e700c55d0557058331-java-arm64
ghcr.io/openhands/agent-server:agent-profile-router-java-arm64
ghcr.io/openhands/agent-server:bfbe52a-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:bfbe52a-python-amd64
ghcr.io/openhands/agent-server:bfbe52a5a6a03e8de757b3e700c55d0557058331-python-amd64
ghcr.io/openhands/agent-server:agent-profile-router-python-amd64
ghcr.io/openhands/agent-server:bfbe52a-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:bfbe52a-python-arm64
ghcr.io/openhands/agent-server:bfbe52a5a6a03e8de757b3e700c55d0557058331-python-arm64
ghcr.io/openhands/agent-server:agent-profile-router-python-arm64
ghcr.io/openhands/agent-server:bfbe52a-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:bfbe52a-golang
ghcr.io/openhands/agent-server:bfbe52a5a6a03e8de757b3e700c55d0557058331-golang
ghcr.io/openhands/agent-server:agent-profile-router-golang
ghcr.io/openhands/agent-server:bfbe52a-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:bfbe52a-java
ghcr.io/openhands/agent-server:bfbe52a5a6a03e8de757b3e700c55d0557058331-java
ghcr.io/openhands/agent-server:agent-profile-router-java
ghcr.io/openhands/agent-server:bfbe52a-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:bfbe52a-python
ghcr.io/openhands/agent-server:bfbe52a5a6a03e8de757b3e700c55d0557058331-python
ghcr.io/openhands/agent-server:agent-profile-router-python
ghcr.io/openhands/agent-server:bfbe52a-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., bfbe52a-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., bfbe52a-python-amd64) are also available if needed

…files router + migration seed

Expose the AgentProfile store over HTTP and add a separate active pointer,
mirroring the LLM profiles router. AgentProfile activation is pointer-only:
unlike the LLM /activate it must not write agent_settings (creation-time-only
contract). Runs parallel to #3717; POST /{id}/materialize is a fast-follow
once the resolver lands.

- PersistedSettings.active_agent_profile_id (additive, defaults None; no
  schema bump) threaded through update(), SettingsResponse/Update, and the
  settings_router GET/PATCH passthrough.
- /api/agent-profiles router: CRUD + POST /{id}/activate (pointer only) with
  FK error mapping (ProfileReferenced/FileExists -> 409, FileNotFound -> 404,
  Timeout -> 503, ValueError -> 400).
- Lazy migration seed: first GET on an empty store seeds one default profile
  from agent_settings and sets the active pointer.
- LLM profiles_router delete/rename now enforce the agent-profile FK
  (delete referenced LLM profile -> 409 naming referrers; rename cascades).

Closes #3719

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@simonrosenberg simonrosenberg added the review-this This label triggers a PR review by OpenHands label Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

all-hands-bot commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   agent_profiles_router.py221995%134, 138, 165, 254, 299–300, 500, 540–541
   api.py2762690%109, 111–116, 118, 120, 122, 157, 169, 184, 190, 243, 248, 257–259, 503, 506, 510–512, 514, 521
   profiles_router.py156696%352–357
   settings_router.py125893%266, 268–269, 370, 372–373, 411, 416
openhands-agent-server/openhands/agent_server/persistence
   models.py1992686%280, 285, 323, 371, 406–412, 414, 416, 419, 423, 449, 466–467, 512, 516, 518, 547–550, 553
openhands-sdk/openhands/sdk/settings
   api_models.py33293%103, 105
TOTAL32584690978% 

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Taste Rating: Acceptable — the router shape is straightforward and the pointer-only activation contract is clear, but there are two correctness gaps around migration fidelity and encrypted secret round-trips that should be addressed before approval.

[CRITICAL ISSUES]

  • [openhands-agent-server/openhands/agent_server/agent_profiles_router.py, Line 249] Secret Handling: Saving a profile body that came from GET /api/agent-profiles/{name} with X-Expose-Secrets: encrypted will re-encrypt the already encrypted skills[].mcp_tools env/header values. A resolver that decrypts once then receives a Fernet token instead of the original secret, so ordinary edit/save round-trips can break MCP credentials.
  • [openhands-agent-server/openhands/agent_server/agent_profiles_router.py, Line 141] Migration Fidelity: The lazy seed does not preserve several overlapping fields from the current agent_settings (agent_context.skills, system_message_suffix, condenser, verification, and ACP acp_command/acp_args). When the resolver starts honoring active_agent_profile_id, existing single-config users can silently fall back to defaults rather than their configured launch behavior.

[TESTING GAPS]

  • Add a cipher-backed router test that exercises GET /api/agent-profiles/{name} with encrypted exposure followed by POST /api/agent-profiles/{name} and verifies the stored MCP secret decrypts exactly once to the original value.
  • Add lazy-seed tests with non-default OpenHands and ACP settings so the migration proves it preserves the profile fields the resolver will later use.

I ran the focused router tests locally: uv run pytest tests/agent_server/test_agent_profiles_router.py tests/agent_server/test_settings_router.py::test_patch_settings_active_agent_profile_id_independent -q → 29 passed.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    This adds a new persisted HTTP surface and a migration path for existing users. The endpoint structure is simple, but secret round-tripping and migration fidelity directly affect whether future profile materialization launches agents with the expected credentials and settings.

VERDICT:
Needs rework: The API shape is reasonable, but the encrypted edit/save path and the lazy seed need to preserve user data before this should be approved.

KEY INSIGHT:
The active pointer is deliberately lightweight, so the stored profile it points at must be a faithful and stable representation of the user's launch configuration.

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 generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

Review bot flagged two correctness gaps:

- Migration fidelity: the lazy seed now carries every cleanly-overlapping
  launch field from agent_settings (OpenHands: skills, system_message_suffix,
  condenser, verification subset; ACP: acp_command via shlex.join + acp_args),
  not just the LLM ref. Existing single-config users migrate faithfully once
  the resolver honors active_agent_profile_id.
- Secret round-trip: skills[].mcp_tools env/headers are decrypted (Fernet ->
  plaintext) on GET, save, and seed via a shared helper. The store masks/
  encrypts on save but has no symmetric load-time validator, so without this a
  GET(encrypted) -> POST edit double-encrypted the value. Plaintext now
  decrypts exactly once.

Tests: cipher-backed GET(encrypted) -> POST round-trip + encrypted-at-rest,
and seed-fidelity tests for non-default OpenHands and ACP settings.

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

Copy link
Copy Markdown
Member Author

Addressed the two findings in 97a4fd1:

Secret round-trip (save path): added _decrypt_profile_mcp_tools, applied on GET, save, and the seed. It decrypts any Fernet-token skills[].mcp_tools env/headers (via the public decrypt_str_with_cipher_or_keep, idempotent on plaintext) before the store re-encrypts, so a GET(encrypted)POST edit no longer double-encrypts. New tests: test_mcp_tools_secret_encrypted_roundtrip (decrypts exactly once to the original) and test_mcp_tools_secret_encrypted_at_rest.

Migration fidelity (seed): _build_seed_profile now carries every cleanly-overlapping launch field — OpenHands: skills, system_message_suffix, condenser, verification (secret-free subset); ACP: acp_command (via shlex.join) and acp_args — in addition to the existing scalars. New tests: test_seed_preserves_openhands_fields and test_seed_preserves_acp_fields.

materialize remains the only deferred piece (fast-follow after #3717).

@simonrosenberg simonrosenberg added review-this This label triggers a PR review by OpenHands and removed review-this This label triggers a PR review by OpenHands labels Jun 17, 2026

all-hands-bot commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Taste Rating: Acceptable - The API shape is straightforward and the test coverage is broad, but two identity/persistence paths can corrupt the new stable active-profile pointer.

[CRITICAL ISSUES]

  • save_agent_profile can replace an existing profile with a fresh UUID/revision when the request body omits those fields, leaving active_agent_profile_id pointing at a profile id that no longer exists.
  • The lazy migration seed is not idempotent under concurrent first requests, so it can also leave the active pointer referring to an overwritten default profile id.

[TESTING GAPS]

  • Add regression coverage for overwriting an active profile with a create-style body that omits id/revision, and for concurrent first GET /api/agent-profiles calls (or an equivalent locked/idempotent seed test).

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    This adds new agent-server endpoints and persistence migration behavior. The main risk is not broad security exposure, but durable settings/store inconsistency: once the pointer references a non-existent profile id, the UI/server can lose the active selection until manually repaired.

VERDICT:
Needs rework: Stable AgentProfile identity is the core invariant for pointer-only activation, so updates and lazy seeding must preserve the UUID that settings point at.

KEY INSIGHT:
The design is sound only if every write path treats the AgentProfile UUID as stable state, not as another defaultable request field.

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 generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

Review bot flagged two pointer-integrity gaps (active_agent_profile_id is keyed
on the stable id):

- save_agent_profile preserved nothing, so a create-style body (no id/revision)
  overwriting a namesake minted a fresh UUID and reset revision, dangling the
  active pointer. Now reuses the existing profile's id and bumps revision.
- The lazy seed wasn't idempotent under concurrent first requests. It now holds
  the store lock across the empty-check + save + pointer write (double-checked),
  so concurrent first GETs seed exactly once and the pointer always matches the
  persisted profile id.

Tests: overwrite-preserves-id-and-pointer + concurrent-first-list-seeds-once.

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

Copy link
Copy Markdown
Member Author

Addressed both id-stability findings in a48296e:

Overwrite mints a fresh id (Line 382): save_agent_profile now resolves the existing profile's (id, revision) via a new _existing_identity helper and, on overwrite, forces id to the existing value and bumps revision — even when a create-style body omits both. The active pointer (keyed on id) can no longer dangle. Test: test_overwrite_preserves_id_and_pointer.

Seed not idempotent under concurrency (Line 325): _seed_default_profile now holds the AgentProfileStore lock across the empty-check + save + pointer write, with a double-checked re-read inside the lock. Concurrent first GETs seed exactly once (the losers see a non-empty store and return), so the pointer always matches the single persisted profile id. Test: test_concurrent_first_list_seeds_once.

@simonrosenberg simonrosenberg added review-this This label triggers a PR review by OpenHands and removed review-this This label triggers a PR review by OpenHands labels Jun 17, 2026

all-hands-bot commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Acceptable — well-structured, correctly locked where it matters, and has excellent test coverage including a real concurrency test for the seed path. A few minor corrections before this merges.


[CRITICAL ISSUES]

  • [agent_profiles_router.py, Line 517] Wrong HTTP Status: (corrupted/mis-keyed settings file) is mapped to HTTP 409 Conflict. A server-side data integrity failure should be HTTP 500. The parallel handler in correctly returns 500 for the same scenario.

[IMPROVEMENT OPPORTUNITIES]

  • [api_models.py, Line 120] Missing Format Validation: on has no format constraint, while the sibling field has . A UUID pattern would reject clearly malformed inputs at the HTTP layer.

  • [models.py, Line 261] Comment Noise: is a diff-narrative comment that restates what the code already shows. It will drift as the surrounding code changes.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    New router, FK guard on existing delete/rename (could 409 callers that previously succeeded), and writes to the shared file. All changes are well-tested with real code paths. The pointer-only activation contract is correctly enforced. The lazy migration seed is guarded by the store lock with a double-check. Breaking risk is low — defaults to so existing settings files deserialize unchanged.

VERDICT:
Worth merging: one correctness fix (HTTP 500 vs 409), plus two optional polish items.

KEY INSIGHT:
The seed holding the store lock across empty-check + save + pointer write is the right design — that window is exactly where a concurrent first request would otherwise race.


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 file to your branch (or edit it if one already exists) with the trigger and the context the reviewer is missing. 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 generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

Comment thread openhands-agent-server/openhands/agent_server/agent_profiles_router.py Outdated
Comment thread openhands-agent-server/openhands/agent_server/persistence/models.py Outdated
Comment thread openhands-sdk/openhands/sdk/settings/api_models.py
…t trim

- activate: a corrupted/mis-keyed settings file (RuntimeError) is a server-side
  integrity failure → 500, not 409. Test added.
- SettingsUpdateRequest.active_agent_profile_id gains a UUID format pattern so
  malformed pointers are rejected at the HTTP layer (mirrors active_profile).
- Drop a diff-narrative comment in PersistedSettings.update().

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

Copy link
Copy Markdown
Member Author

All three addressed in eda4828:

  • activate HTTP 500 — the RuntimeError (corrupted/mis-keyed settings file) now returns 500, not 409, with a test (test_activate_settings_corruption_returns_500).
  • UUID format validationSettingsUpdateRequest.active_agent_profile_id now has a UUID pattern mirroring active_profile; malformed pointers 422 at the HTTP layer (test_patch_settings_rejects_malformed_active_agent_profile_id).
  • Comment trim — dropped the diff-narrative comment in PersistedSettings.update().

@simonrosenberg simonrosenberg added review-this This label triggers a PR review by OpenHands and removed review-this This label triggers a PR review by OpenHands labels Jun 17, 2026

all-hands-bot commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Acceptable — Solid implementation of a complex feature with one sharp edge.


Overview

This PR cleanly delivers Phase 2 of the AgentProfile epic: a full CRUD router for named agent launch specs, a pointer-only active_agent_profile_id threaded through the settings surface, a one-time lazy seed migration, and a FK guard on LLM profile delete/rename. The design is intentional and well-documented.

Standouts:

  • The concurrent seeding double-check (store._acquire_lock() + inner store.list() guard) is the right approach for a lazy singleton migration.
  • Activation is correctly pointer-only — no agent_settings mutation. The ActivateAgentProfileResponse.agent_settings_applied: bool = False field makes the contract self-documenting.
  • The FK guard on delete_llm_profile / rename_llm_profile names the blocking referrers in the 409, which is exactly what a caller needs to unblock.
  • Test coverage is comprehensive: 35 test functions including a concurrent seeding test with 8 threads and a full MCP-secrets round-trip test.

Issues

[IMPROVEMENT OPPORTUNITIES]

  • [agent_profiles_router.py, Line 386] Error detail strips all diagnostic information: detail=f"Invalid agent profile: {type(e).__name__}" tells the client only the exception class name ("ValidationError", "ValueError", "TypeError"), losing the actual field-level error messages from Pydantic and the value messages from the others. A client posting a bad profile body gets back zero actionable information. detail=str(e) (or at minimum f"Invalid agent profile: {e}") would give a structured, readable breakdown of which fields are wrong. The Pydantic ValidationError.__str__ is already client-safe.

  • [agent_profiles_router.py, Line 253] Cross-module private method call: store._acquire_lock() reaches into AgentProfileStore's internals from outside the module. This works today, but it's fragile — any refactor of how the store serializes its locking API would silently break this call site. If the locking-across-the-emptiness-check semantics are genuinely needed by callers, the store should expose them through a public API (store.acquire_lock() or a store.seed_once(profile, ...) helper). For now this is a style concern, but it will bite on the first internal refactor of AgentProfileStore.

[TESTING GAPS]

  • Missing coverage for stale-pointer + empty store: The seeding condition is not existing and settings.active_agent_profile_id is None. If the store is empty but the pointer is non-null (e.g. after a failed delete or direct-FS manipulation), GET /api/agent-profiles returns {"profiles": [], "active_agent_profile_id": "stale-uuid"} — no seed, no error. This is probably intentional (stale pointer = user state), but there is no test asserting this behavior, making it invisible to future readers and easy to accidentally change.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW
    All changes are additive: new router, new nullable field with backward-compatible default, FK guard that only adds a new 409 path to previously-silent success. Older settings files load cleanly with active_agent_profile_id=None (no schema-version bump needed). The only behavioral change on existing endpoints is the 409 guard on LLM profile delete/rename, which is tested. No breaking changes to existing API surface.

VERDICT:
Worth merging: The pointer-only activation contract, lazy seeding with lock, and FK guard are all correct. The error-message issue (line 386) is the only thing I'd fix before shipping — it will make production debugging significantly harder when clients post malformed profiles.

KEY INSIGHT:
The creation-time-only pointer design is the right call — keeping active_agent_profile_id strictly separate from active_profile prevents the resolver from having to unwind entangled live settings.


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. 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 generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

Comment thread openhands-agent-server/openhands/agent_server/agent_profiles_router.py Outdated
- save_agent_profile: surface field locations + error types on a ValidationError
  (actionable), but never the input/msg (a nested mcp_tools MCPConfig error
  embeds the input, which may carry secrets). Any other validation failure —
  notably SkillValidationError from a malformed mcp_tools, which is NOT a
  ValueError and previously escaped to a 500 leaking the secret in the
  traceback — is now caught and returned as a generic 422.
- Tests: 422 surfaces the offending field location; a malformed mcp_tools body
  never echoes its secret; empty store + non-null (stale) pointer is left as-is.

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

Copy link
Copy Markdown
Member Author

Addressed in 94c6c5c:

Error detail (line 386): save now returns a structured 422 with each failing field's loc + type on a ValidationError — actionable, and the client can see exactly which field is wrong. I deliberately omit input/msg: the Skill.mcp_tools validator wraps the nested MCPConfig error (which embeds the input dict, potentially MCP credentials) into its message, so str(e) would leak secrets. Fixing this surfaced a real bug — SkillValidationError is not a ValueError, so a malformed mcp_tools body previously escaped the except clause and 500'd with the secret in the traceback; it's now caught and returned as a generic 422. New tests: field-location surfaced + secret-never-echoed.

store._acquire_lock() (line 253): this follows the established convention — the merged FK module openhands/sdk/profiles/profile_refs.py (#3716) already calls store._acquire_lock() cross-module for exactly this scan-then-mutate pattern (its docstring documents the re-entrant nesting contract). Promoting it to a public API is reasonable but belongs with the store module rather than this router PR, and renaming now would churn the merged store + FK module while #3717 builds on them in parallel. Tracking as a follow-up; keeping the router consistent with the existing primitive for now.

@simonrosenberg simonrosenberg added review-this This label triggers a PR review by OpenHands and removed review-this This label triggers a PR review by OpenHands labels Jun 17, 2026

all-hands-bot commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Taste Rating: Acceptable — the router is much tighter after the previous fixes, and the focused tests pass, but the new id-keyed activation path still allows one durable identity invariant to be broken.

[CRITICAL ISSUES]

  • [openhands-agent-server/openhands/agent_server/agent_profiles_router.py, Line 414] Stable ID Uniqueness: Creating a new profile can reuse an existing profile's id from the request body. Because activation and active-pointer cleanup are keyed only by id, duplicate ids make the pointer ambiguous and let deleting one profile clear the active selection even while another profile with the same id still exists.

[TESTING GAPS]

  • Add a regression test that creates profile a, creates profile b with a's id, activates that id, then deletes a. The API should either reject the duplicate id or assign b a fresh id so the active pointer remains unambiguous.

I ran the focused router/settings coverage locally: uv run pytest tests/agent_server/test_agent_profiles_router.py tests/agent_server/test_profiles_router.py::test_delete_referenced_llm_profile_returns_409 tests/agent_server/test_profiles_router.py::test_rename_llm_profile_cascades_to_agent_refs tests/agent_server/test_settings_router.py::test_patch_settings_active_agent_profile_id_independent -q → 40 passed.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    This adds a new persisted HTTP surface and a new active pointer whose correctness depends on AgentProfile.id being a unique stable handle. Most paths preserve that invariant, but accepting duplicate ids on new names can leave the UI/server with ambiguous active-profile state.

VERDICT:
Needs rework: Do not approve until the save path enforces unique AgentProfile ids (or ignores client-supplied ids for new profile names) and covers the duplicate-id case.

KEY INSIGHT:
A pointer-only design is only safe if the pointed-to id is globally unique across the profile store, not just stable within a single profile file.

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 generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

Comment thread openhands-agent-server/openhands/agent_server/agent_profiles_router.py Outdated
The active pointer is keyed on AgentProfile.id, so ids must be globally unique,
not just stable per file. save now mints a server id on create (ignoring any
client-supplied id) and preserves it only on overwrite. Prevents a client from
creating two names with the same id, which made the pointer ambiguous (deleting
one could clear the active selection while a namesake id lives on).

Test: creating 'b' with 'a's id yields a distinct id; activating b then
deleting a leaves the pointer intact.

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

Copy link
Copy Markdown
Member Author

Fixed in 56b6023: save now mints a server-side id on create (ignoring any client-supplied id) and preserves it only on overwrite. The id is fully server-managed, so a client can't create two names sharing an id and the active pointer is never ambiguous. Test: creating b with a's id yields a distinct id; activating b then deleting a leaves the pointer intact.

@simonrosenberg simonrosenberg added review-this This label triggers a PR review by OpenHands and removed review-this This label triggers a PR review by OpenHands labels Jun 18, 2026

all-hands-bot commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟢 Good taste — the earlier blocking concerns are fully addressed in this commit.

Key fixes verified:

  • ID uniqueness: save_agent_profile now always mints a fresh uuid4() for new names, ignoring any client-supplied id. Overwriting a namesake keeps the existing id stable so active pointers survive edits. ✅
  • Regression test: test_create_mints_fresh_id_ignoring_client_id exercises the exact scenario from the prior review — creates profile a, tries to reuse a's id for profile b, deletes a, and confirms the active pointer to b is unambiguous. ✅
  • Seed race: Double-checked locking under store._acquire_lock() prevents concurrent first requests from seeding twice. ✅
  • MCP secret double-encrypt: _decrypt_profile_mcp_tools is called before both _seed_default_profile and save_agent_profile saves, preventing Fernet-token double-encryption on round-trips. ✅

All 10 prior review threads are resolved. Test coverage: 647-line test file covering CRUD, activation, seeding, FK guards, cipher round-trips, and error mapping. Broader suite (1477 tests) passes with one pre-existing unrelated failure on main that is untouched by this PR.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    New persisted HTTP surface with an active pointer keyed on UUID. The critical invariant (id-uniqueness across the profile store) is now enforced on all write paths and covered by a regression test. FK guards on LLM profile delete/rename add new behavior to an existing router but are well-exercised. The one-time lazy migration seeds exactly once under a lock. Suitable for merge once out of draft.

VERDICT:
Worth merging: The blocking ID-uniqueness issue is fixed and regressed; no new concerns found.

KEY INSIGHT:
Server-managed UUIDs (always uuid4() on first save, never trusted from the client) make the active pointer safe — the correctness argument is now fully encoded in both the implementation and the test suite.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

@simonrosenberg simonrosenberg marked this pull request as ready for review June 18, 2026 06:44
@simonrosenberg simonrosenberg self-assigned this Jun 18, 2026

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ QA Report: PASS

The new agent-profile HTTP surface works end-to-end in the running FastAPI app, including lazy seeding, independent pointer updates, pointer-only activation, rename/delete pointer behavior, and LLM-profile FK protection.

Does this PR achieve its stated goal?

Yes. I exercised the actual agent-server API with an isolated HOME/persistence directory using FastAPI TestClient: on main, /api/agent-profiles was absent and active_agent_profile_id could not be cleared; on this PR, first list seeded one default profile and set the active pointer, CRUD worked, activation set only active_agent_profile_id while leaving agent_settings unchanged, renames preserved the stable id/pointer, deletes cleared the active pointer, and deleting a referenced LLM profile returned 409 naming the referencing agent profile.

Phase Result
Environment Setup make build completed and installed the uv workspace environment.
CI Status 🟡 Queried GitHub checks: test/pre-commit/API compatibility checks were green; Agent Server build/coverage jobs were still in progress at query time.
Functional Verification ✅ Real API calls against the running FastAPI app matched the PR’s stated behavior.
Functional Verification

Test 1: Baseline on origin/main

Step 1 — Establish baseline without the PR:
Ran git checkout origin/main && uv run python /tmp/qa_agent_profiles_flow.py.
Relevant output:

{
  "GET /api/agent-profiles first": {"status": 404, "body": {"detail": "Not Found"}},
  "PATCH /api/settings clear agent pointer": {
    "status": 400,
    "body": {"detail": "At least one of agent_settings_diff, conversation_settings_diff, misc_settings_diff, or active_profile must be provided"}
  },
  "POST /api/agent-profiles/my-agent": {"status": 404, "body": {"detail": "Not Found"}},
  "POST /api/profiles/base-llm": {"status": 201},
  "DELETE /api/profiles/base-llm": {"status": 200}
}

This shows the new agent-profile HTTP router and settings pointer behavior did not exist on the base branch; the LLM profile delete path also allowed deletion in this baseline flow.

Step 2 — Apply the PR’s changes:
Checked out agent-profile-router at d8978d27161cce20a4990876008898b05527feb2.

Step 3 — Re-run with the PR in place:
Ran uv run python /tmp/qa_agent_profiles_flow.py.
Relevant output:

{
  "GET /api/agent-profiles first": {
    "status": 200,
    "body": {
      "active_agent_profile_id": "ff130889-316a-4c75-906d-aebeaa98251e",
      "profiles": [{"name": "default", "agent_kind": "openhands", "llm_profile_ref": "default", "mcp_server_refs": null}]
    }
  },
  "PATCH /api/settings set both pointers": {"status": 200, "body": {"active_profile": "keep-me", "active_agent_profile_id": "12345678-1234-1234-1234-1234567890ab"}},
  "PATCH /api/settings clear agent pointer": {"status": 200, "body": {"active_profile": "keep-me", "active_agent_profile_id": null}}
}

This confirms the lazy migration seed creates exactly one default profile and persists an active agent-profile pointer, and that active_agent_profile_id can be set/cleared independently from the existing active LLM profile pointer.

Test 2: Agent-profile CRUD and pointer-only activation

Step 1 — Baseline:
On origin/main, POST /api/agent-profiles/my-agent returned 404, so this user-facing API did not exist.

Step 2 — Apply the PR’s changes:
Used the PR branch/commit above.

Step 3 — Exercise the new behavior:
The same API flow created, fetched, activated, renamed, and deleted an agent profile:

{
  "POST /api/agent-profiles/my-agent": {"status": 201},
  "GET /api/agent-profiles/my-agent": {
    "status": 200,
    "body": {"profile": {"id": "c969c2c9-0b06-4e49-b5a5-6b0ecc9ef27b", "llm_profile_ref": "base-llm", "system_message_suffix": "qa-user-flow"}}
  },
  "POST /api/agent-profiles/{id}/activate": {
    "status": 200,
    "active_agent_profile_id_after": "c969c2c9-0b06-4e49-b5a5-6b0ecc9ef27b",
    "agent_settings_unchanged": true,
    "body": {"agent_settings_applied": false}
  },
  "POST /api/agent-profiles/my-agent/rename": {
    "status": 200,
    "renamed_id": "c969c2c9-0b06-4e49-b5a5-6b0ecc9ef27b",
    "active_pointer_after_rename": "c969c2c9-0b06-4e49-b5a5-6b0ecc9ef27b"
  },
  "DELETE /api/agent-profiles/renamed-agent": {"status": 200, "active_pointer_after_delete": null}
}

This confirms the active selection is id-based, activation is pointer-only (agent_settings_unchanged: true and agent_settings_applied: false), renames preserve the id/pointer, and deleting the active profile clears the pointer.

Test 3: LLM profile FK protection

Step 1 — Baseline:
On origin/main, the new agent-profile HTTP route was unavailable; creating and deleting an LLM profile through /api/profiles completed with 201 then 200.

Step 2 — Apply the PR’s changes:
Used the PR branch/commit above.

Step 3 — Exercise referenced LLM delete:
Created an LLM profile, created an agent profile referencing it, then attempted to delete the LLM profile:

{
  "POST /api/profiles/base-llm": {"status": 201},
  "POST /api/agent-profiles/refs-base": {"status": 201},
  "DELETE /api/profiles/base-llm": {
    "status": 409,
    "body": {"detail": "LLM profile is referenced by 1 agent profile(s): refs-base"}
  }
}

This confirms the FK guard works through the public API and names the referrer as claimed.

Issues Found

None.

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

Cosmetic consistency, no behavior change to status codes:
- _store_errors now maps the same set as profiles_router (TimeoutError -> 503,
  ValueError -> 400); FileNotFoundError/FileExistsError are handled inline per
  endpoint with clean, resource-specific messages (matching get/rename there).
- save's 422 uses FastAPI's request-validation shape (detail = list of error
  objects), trimmed to loc/type to stay secret-safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@simonrosenberg simonrosenberg added review-this This label triggers a PR review by OpenHands and removed review-this This label triggers a PR review by OpenHands labels Jun 18, 2026

all-hands-bot commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Acceptable — solid design, a few cleanup items worth addressing before merge.

Summary

Phase 2 of the AgentProfile epic is well-structured. The pointer-only activation contract is correctly enforced, concurrent seeding is properly guarded, the id-keyed pointer is a smart choice (survives renames), UUID validation happens at the HTTP boundary, and secret leakage is explicitly blocked from 422 responses. The FK guard on LLM profile delete/rename is a thoughtful addition. Tests are comprehensive and cover the interesting concurrency and round-trip cases.

Found a few items below worth addressing.


[IMPROVEMENT OPPORTUNITIES]

  • [agent_profiles_router.py, ~line 95] agent_settings_applied: bool = False — This field is always False in the response; the endpoint unconditionally constructs ActivateAgentProfileResponse without ever setting it to True. Per the PR description, POST /{id}/materialize is a deliberate fast-follow that isn't implemented here. Shipping a permanently-False field is misleading — API consumers could reasonably infer it has semantic meaning. Either add a comment like # Always False until materialize (#3717) lands to make the pre-wiring explicit, or defer this field until the endpoint that can actually set it arrives.

  • [agent_profiles_router.py, ~line 540] except (OSError, PermissionError)PermissionError is a subclass of OSError in Python's exception hierarchy, so the union is redundant. except OSError is sufficient.

  • [agent_profiles_router.py, save_agent_profile] There is a TOCTOU window between _existing_identity(store, name) (reads without the write lock) and the subsequent store.save(...). Two concurrent POSTs for the same new profile name both observe existing_id=None, both mint fresh UUIDs, and the second write overwrites the first — dangling any active_agent_profile_id pointer set to the first write's UUID. The seed path avoids this correctly (it holds store._acquire_lock() across check + save + pointer write). The save path should do the same, or id-minting should move inside the store. This is an unlikely race in practice, but the seed path already demonstrates the pattern.

  • [agent_profiles_router.py, list_agent_profiles] The emptiness check calls store.list() (deserialises full profiles) then store.list_summaries() separately for the response. Using list_summaries() for the emptiness check too avoids the double I/O and the unnecessary full-profile deserialisation.

[STYLE NOTES]

  • Excessive inline comments — many docstrings paraphrase the PR description or restate what the code structure already shows. The _seed_default_profile 8-line docstring and the _decrypt_profile_mcp_tools 18-line docstring are examples: they narrate the what instead of the why. Reserve comments for genuinely non-obvious invariants — the # settings store the command as a token list; the profile holds a single (re-parseable) string comment is a good example that earns its place.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW
    Changes are additive: new field with a safe default (None), new router that doesn't affect existing routes, FK guard that fails safe (rejects rather than silently deletes). No breaking changes to existing APIs. All net-new behavior is covered by tests.

VERDICT:
Worth merging — Core logic is sound and well-tested. Address the TOCTOU in save_agent_profile and the two minor nits before merge.

KEY INSIGHT:
The id-keyed active pointer is the right abstraction — it's the only reason rename and concurrent-overwrite semantics can be proven safe — but that guarantee is only complete when the save path holds the lock during the full identity-read + id-mint + write sequence, just as the seed path already does.


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. 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 generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

- save_agent_profile now holds the store lock across the identity read + id
  mint + write, so two concurrent creates of the same new name converge on one
  id instead of clobbering each other (mirrors the seed path). Regression test
  added.
- Comment agent_settings_applied as intentionally always-False (pointer-only;
  materialize #3717 is the path that resolves into settings).
- Trim the _seed_default_profile / _decrypt_profile_mcp_tools docstrings to the
  non-obvious why.

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

Copy link
Copy Markdown
Member Author

Addressed review 8 in bfbe52a:

  • TOCTOU in save_agent_profile (the pre-merge item): save now holds store._acquire_lock() across the identity read + id-mint + write, so two concurrent creates of the same new name converge on a single stable id rather than clobbering each other — same guard the seed path uses. Regression test test_concurrent_create_same_name_converges_on_one_id added.
  • agent_settings_applied: kept (a prior review valued it as a self-documenting contract marker) and annotated it as intentionally always-False — pointer-only activation; materialize ([AgentProfile][sdk] resolve_agent_profile(): collision-checked composition + resource-specific secret channels #3717) is the path that resolves a profile into settings.
  • Docstrings: trimmed _seed_default_profile and _decrypt_profile_mcp_tools to the non-obvious why.

Two intentionally kept, for consistency with the existing codebase (per the request to align with current conventions):

  • except (OSError, PermissionError): redundant in isolation, but this exact tuple is the convention in the sibling profiles_router.activate_profile and settings_router, so I'm matching it rather than diverging.
  • store.list() for the emptiness check: list() is filename-only (glob, no JSON parse), so it's cheaper than list_summaries() and deliberately counts unparseable files — that way a store with only a corrupt file isn't treated as empty and re-seeded.

@simonrosenberg simonrosenberg added review-this This label triggers a PR review by OpenHands and removed review-this This label triggers a PR review by OpenHands labels Jun 18, 2026

all-hands-bot commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟢 Good taste — the main blocking concern from the previous review (TOCTOU race in save_agent_profile) is fully resolved. The id-read + id-mint + store.save are now all executed under store._acquire_lock(), matching the pattern the seed path already used correctly. All prior critical/important threads are resolved.

Two minor items left from the last review remain unaddressed (noted inline). Neither is blocking — the core design is correct, the pointer-only activation contract is enforced, concurrency is properly guarded, secret handling is secure, and tests are thorough.

[IMPROVEMENT OPPORTUNITIES]

  • list_agent_profiles calls store.list() (full profile deserialisation) for the emptiness check and then store.list_summaries() separately for the response — double I/O, see inline.
  • except (OSError, PermissionError) in activate_agent_profilePermissionError is a subclass of OSError; the union is redundant, see inline.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW — fully additive: new nullable field with a safe default, new router that doesn't interfere with existing routes, FK guard that fails safe (rejects rather than silently deletes). No breaking changes to existing APIs.

VERDICT:
Worth merging — all blocking concerns addressed, comprehensive test coverage, solid evidence in the PR description.

KEY INSIGHT:
The id-keyed active pointer is the right abstraction; the TOCTOU fix in save_agent_profile closes the last gap where two concurrent creates could produce a dangling pointer.


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. 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 generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

@simonrosenberg

Copy link
Copy Markdown
Member Author

On the two remaining non-blocking suggestions — keeping both, for consistency with the existing codebase (and one is a factual miss):

  1. store.list() for the emptiness check: list() is filename-only — return [p.name for p in self.base_dir.glob('*.json')], no JSON read/parse. It's the cheaper call; list_summaries() is the one that deserialises. list() also counts unparseable files, so a store holding only a corrupt profile isn't treated as empty and re-seeded. So this is intentionally the lighter and safer choice.

  2. except (OSError, PermissionError): this exact tuple is the established convention — profiles_router.py:352, settings_router.py:266, settings_router.py:370. Narrowing only this one call site to except OSError would make it the outlier. Matching the siblings per the consistency goal.

Resolving these two threads; the review is already approved.

@simonrosenberg simonrosenberg merged commit b0be8a5 into main Jun 18, 2026
52 of 53 checks passed
@simonrosenberg simonrosenberg deleted the agent-profile-router branch June 18, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-this This label triggers a PR review by OpenHands

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[AgentProfile][agent-server] active_agent_profile_id + /api/agent-profiles router + migration seed

2 participants