Skip to content

fix(servers): make PATCH a field-scoped, revision-guarded write - #1722

Open
atirna wants to merge 1 commit into
agentic-community:mainfrom
atirna:fix/servers-patch-atomic-field-scoped-write
Open

fix(servers): make PATCH a field-scoped, revision-guarded write#1722
atirna wants to merge 1 commit into
agentic-community:mainfrom
atirna:fix/servers-patch-atomic-field-scoped-write

Conversation

@atirna

@atirna atirna commented Sep 5, 2026

Copy link
Copy Markdown

Summary

I changed PATCH /api/servers/{path} from a read-merge-full-card-write into a field-scoped, revision-guarded write, and moved the If-Match check into the repository update_one itself.

What was happening (issue #1716), on the MongoDB CE backend:

  • update_server_endpoint/patch_server_endpoint read the full card, merged the narrow patch, then DocumentDBServerRepository.update() persisted the whole merged card with an unconditional full-card $set
  • a card stored under the slash variant of its path (readable that way because get() falls back to the alternate _id) never matched the update filter, so a description-only PATCH came back 500 Failed to save server
  • any field written between the PATCH's read and its write — a rotated backend credential, an egress OAuth config, health status — was overwritten even though the patch never mentioned that field

What changed:

  • DocumentDBServerRepository.update() scopes its $set to updated_fields (plus updated_at) when given, tries the slash-variant _id on a miss the same way get() does, and takes an optional expected_updated_at that joins the _id in the same update_one filter — so the If-Match compare-and-set is one atomic database operation, not a client-side pre-check
  • PATCH passes the patch's field scope plus the revision it read; a guarded write that matches nothing is a 412, not a 500
  • PUT keeps its full-card replacement semantics, but its If-Match now also rides the repository write

Testing

  • before, on current main, against a real mongo:8.2: a description-only PATCH on a card stored under /legacy/ read fine but update_one matched 0 documents and the route returned 500 Failed to save server
  • before: a credential rotation landing between the PATCH's read and write was clobbered by the stale full-card $set (stored auth_credential_encrypted reverted to the pre-rotation value)
  • after: pytest tests/integration/test_server_patch_concurrency.py tests/unit/repositories/test_documentdb_server_repository.py tests/unit/api/test_server_routes_patch.py tests/unit/api/test_server_routes_put.py — the slash-variant card patches with 2xx, the concurrent credential survives, a stale If-Match fails atomically with nothing written, an identical replay is harmless, and clearing proxy_pass_url still $unsets the identity-url sidecar

The new integration test follows the existing test_agent_batch_repository.py pattern (real collection, skip when MongoDB is unreachable), so it runs in CI alongside the mongo:8.2 service the workflow already starts.

Fixes #1716

PATCH /api/servers/{path} read the full card, merged the client patch,
and persisted the whole merged card with an unconditional full-card
$set. Two problems (issue agentic-community#1716):

- a card stored under the slash variant of its path (readable that way
  because get() falls back to the alternate _id) never matched the
  update filter, so the route returned 500 "Failed to save server"
  for a description-only patch
- any field written concurrently between the read and the write (an
  egress OAuth config, a rotated credential, health status) was
  silently overwritten even though the patch never mentioned it

update() now scopes its $set to the fields the patch actually
supplies (plus updated_at), tries the slash-variant _id the same way
get() does, and accepts an expected_updated_at that joins the _id in
the same update_one filter, making the If-Match check an atomic
compare-and-set instead of a client-side pre-check. The PATCH route
passes the field scope and the revision it read; PUT keeps its
full-card replacement semantics but its If-Match now also moves into
the repository write.

Verified against a real MongoDB CE (mongo:8.2): the failing
description-only patch on a slash-variant card, the lost credential
update, and the stale If-Match race each reproduce on main and pass
with this change.
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.

PATCH /api/servers/{path} fails and uses non-atomic full-card write on MongoDB CE

1 participant