feat: Add opt-in settings to enforce unique Agent and Agentic Memory Container names - #4808
Conversation
PR Reviewer Guide 🔍(Review updated until commit d4fb1b8)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to d4fb1b8 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit aa9b9ec
Suggestions up to commit 4f8f039
Suggestions up to commit bbbe096
Suggestions up to commit bbbe096
Suggestions up to commit 211fb54
|
|
Persistent review updated to latest commit 7bfddb8 |
7bfddb8 to
7732788
Compare
|
Persistent review updated to latest commit bbf285e |
Addresses reviewer feedback on opensearch-project#4808: the uniqueness flags were only gating register/create paths, so a rename via PUT /agents/{id} or PUT /memory_containers/{id} could bypass the check and produce two resources with the same name even while the setting was on. Extends both update transports with a uniqueness check, gated on the same per-resource flag as the create path: - plugins.ml_commons.agent_name_uniqueness_enabled - plugins.ml_commons.agentic_memory_name_uniqueness_enabled The check runs after access validation and before the put/update. Gate shape mirrors TransportUpdateModelGroupAction#updateModelGroup: StringUtils.isBlank(newName) || newName.equals(originalName) -> skip uniqueness search so it is skipped when (a) the flag is off, (b) the update omits a new name or provides a blank one, or (c) the new name equals the current name (idempotent PUT does not 409 against itself). On a duplicate, the 409 message cites the conflicting resource's ID to match model-group's error shape and help callers disambiguate which doc they collided with. Same best-effort semantics as the create path (documented in the PR description): two concurrent renames racing on the same target name can still both succeed. Unit tests (mocking client.search / sdkClient.searchDataObjectAsync): UpdateAgentTransportActionTests - uniquenessEnforced_renameToExistingName_rejected (asserts 409 + conflicting agent id in message + no update issued) - uniquenessEnforced_renameToUnusedName_allowed - uniquenessEnforced_sameNameNoOp_skipsSearch - uniquenessDisabled_renameSkipsSearch TransportUpdateMemoryContainerActionTests - same four scenarios, asserting the 409 cites the memory_container_id of the conflicting container. Integration tests extend RestMLAgentNameUniquenessIT and RestMLMemoryContainerNameUniquenessIT: - Existing rename-to-existing tests now also assert the 409 payload contains the conflicting resource ID. - Memory-container IT adds testRename_BlankName_NoOp_Accepted_WhenFlagOn (no agent counterpart: MLAgentUpdateInput.validate() already rejects blank names at parse time, so the case is unreachable there). Local results: 10/10 UpdateAgent UTs, 32/32 UpdateMemoryContainer UTs, 8/8 agent ITs, 9/9 memory-container ITs - all green. Signed-off-by: rithin-pullela-aws <rithinp@amazon.com>
bbf285e to
211fb54
Compare
|
Persistent review updated to latest commit 211fb54 |
Addresses reviewer feedback on opensearch-project#4808: the uniqueness flags were only gating register/create paths, so a rename via PUT /agents/{id} or PUT /memory_containers/{id} could bypass the check and produce two resources with the same name even while the setting was on. Extends both update transports with a uniqueness check, gated on the same per-resource flag as the create path: - plugins.ml_commons.agent_name_uniqueness_enabled - plugins.ml_commons.agentic_memory_name_uniqueness_enabled The check runs after access validation and before the put/update. Gate shape mirrors TransportUpdateModelGroupAction#updateModelGroup: StringUtils.isBlank(newName) || newName.equals(originalName) -> skip uniqueness search so it is skipped when (a) the flag is off, (b) the update omits a new name or provides a blank one, or (c) the new name equals the current name (idempotent PUT does not 409 against itself). The ~50-line search-plumbing block that was duplicated across the four transports (TransportRegisterAgent, UpdateAgent, TransportCreate/Update MemoryContainer) is extracted into NameUniquenessHelper, mirroring the MLModelGroupManager#validateUniqueModelGroupName shape: the helper runs the tenant-scoped exact-match search and hands back the raw SearchResponse, leaving each caller to format its own 409 body and short-circuit on blank/same names. Default-off behavior is unchanged - the feature-flag gate still short-circuits before any helper call. The 409 body intentionally does NOT echo the conflicting resource's document id: doing so would let a caller confirm the existence of resources they cannot otherwise see by probing names. Only the caller-supplied name is reflected back. Regression guards (assertFalse checks) are added to the unit and integration tests for both update paths. Signed-off-by: rithin-pullela-aws <rithinp@amazon.com>
211fb54 to
e4badd6
Compare
Addresses reviewer feedback on opensearch-project#4808: the uniqueness flags were only gating register/create paths, so a rename via PUT /agents/{id} or PUT /memory_containers/{id} could bypass the check and produce two resources with the same name even while the setting was on. Extends both update transports with a uniqueness check, gated on the same per-resource flag as the create path: - plugins.ml_commons.agent_name_uniqueness_enabled - plugins.ml_commons.agentic_memory_name_uniqueness_enabled The check runs after access validation and before the put/update. Gate shape mirrors TransportUpdateModelGroupAction#updateModelGroup: StringUtils.isBlank(newName) || newName.equals(originalName) -> skip uniqueness search so it is skipped when (a) the flag is off, (b) the update omits a new name or provides a blank one, or (c) the new name equals the current name (idempotent PUT does not 409 against itself). The ~50-line search-plumbing block that was duplicated across the four transports (TransportRegisterAgent, UpdateAgent, TransportCreate/Update MemoryContainer) is extracted into NameUniquenessHelper, mirroring the MLModelGroupManager#validateUniqueModelGroupName shape: the helper runs the tenant-scoped exact-match search and hands back the raw SearchResponse, leaving each caller to format its own 409 body and short-circuit on blank/same names. Default-off behavior is unchanged - the feature-flag gate still short-circuits before any helper call. The 409 body intentionally does NOT echo the conflicting resource's document id: doing so would let a caller confirm the existence of resources they cannot otherwise see by probing names. Only the caller-supplied name is reflected back. Regression guards (assertFalse checks) are added to the unit and integration tests for both update paths. Signed-off-by: rithin-pullela-aws <rithinp@amazon.com>
e4badd6 to
bbbe096
Compare
|
Persistent review updated to latest commit bbbe096 |
1 similar comment
|
Persistent review updated to latest commit bbbe096 |
|
Persistent review updated to latest commit 4f8f039 |
Addresses reviewer feedback on opensearch-project#4808: the uniqueness flags were only gating register/create paths, so a rename via PUT /agents/{id} or PUT /memory_containers/{id} could bypass the check and produce two resources with the same name even while the setting was on. Extends both update transports with a uniqueness check, gated on the same per-resource flag as the create path: - plugins.ml_commons.agent_name_uniqueness_enabled - plugins.ml_commons.agentic_memory_name_uniqueness_enabled The check runs after access validation and before the put/update. Gate shape mirrors TransportUpdateModelGroupAction#updateModelGroup: StringUtils.isBlank(newName) || newName.equals(originalName) -> skip uniqueness search so it is skipped when (a) the flag is off, (b) the update omits a new name or provides a blank one, or (c) the new name equals the current name (idempotent PUT does not 409 against itself). The ~50-line search-plumbing block that was duplicated across the four transports (TransportRegisterAgent, UpdateAgent, TransportCreate/Update MemoryContainer) is extracted into NameUniquenessHelper, mirroring the MLModelGroupManager#validateUniqueModelGroupName shape: the helper runs the tenant-scoped exact-match search and hands back the raw SearchResponse, leaving each caller to format its own 409 body and short-circuit on blank/same names. Default-off behavior is unchanged - the feature-flag gate still short-circuits before any helper call. The 409 body intentionally does NOT echo the conflicting resource's document id: doing so would let a caller confirm the existence of resources they cannot otherwise see by probing names. Only the caller-supplied name is reflected back. Regression guards (assertFalse checks) are added to the unit and integration tests for both update paths. Signed-off-by: rithin-pullela-aws <rithinp@amazon.com>
4f8f039 to
aa9b9ec
Compare
|
Persistent review updated to latest commit aa9b9ec |
…Container names Adds two dynamic cluster settings, both defaulting to false for backward compatibility: - plugins.ml_commons.agent_name_uniqueness_enabled - plugins.ml_commons.agentic_memory_name_uniqueness_enabled When enabled, the respective registration/creation path rejects a request whose name collides with an existing resource in the same tenant, returning HTTP 409 CONFLICT. When disabled, behavior is unchanged. The uniqueness check queries the resource index by name.keyword via the remote-metadata SDK, so it honors multi-tenancy. IndexNotFoundException is treated as "no duplicate possible" to support the first-ever registration. Tenant validation is hoisted above the uniqueness check in TransportRegisterAgentAction#doExecute: in multi-tenant mode a missing tenantId now fails fast with 403 before any metadata search runs, rather than leaking existence via a cross-tenant 409. For PLAN_EXECUTE_AND_REFLECT agents registered without an existing executor_agent_id, the internally derived "<name> (ReAct)" executor agent name is validated against the same tenant-aware search path, so the auto-created agent cannot silently collide with an existing name. The single-name check is extracted to checkAgentNameAvailable(name, tenantId) so both names share one implementation. Integration tests (RestMLAgentNameUniquenessIT, RestMLMemoryContainerNameUniquenessIT) cover end-to-end behavior against a running cluster for both flags: duplicates accepted when off, 409 when on, unique names always accepted, and dynamic flip without restart. These guard a regression class unit tests cannot catch (e.g. incorrect query field or malformed BoolQuery), since unit tests feed the transport action a synthetic SearchResponse rather than exercising the real search path. Signed-off-by: rithin-pullela-aws <rithinp@amazon.com>
Addresses reviewer feedback on opensearch-project#4808: the uniqueness flags were only gating register/create paths, so a rename via PUT /agents/{id} or PUT /memory_containers/{id} could bypass the check and produce two resources with the same name even while the setting was on. Extends both update transports with a uniqueness check, gated on the same per-resource flag as the create path: - plugins.ml_commons.agent_name_uniqueness_enabled - plugins.ml_commons.agentic_memory_name_uniqueness_enabled The check runs after access validation and before the put/update. Gate shape mirrors TransportUpdateModelGroupAction#updateModelGroup: StringUtils.isBlank(newName) || newName.equals(originalName) -> skip uniqueness search so it is skipped when (a) the flag is off, (b) the update omits a new name or provides a blank one, or (c) the new name equals the current name (idempotent PUT does not 409 against itself). The ~50-line search-plumbing block that was duplicated across the four transports (TransportRegisterAgent, UpdateAgent, TransportCreate/Update MemoryContainer) is extracted into NameUniquenessHelper, mirroring the MLModelGroupManager#validateUniqueModelGroupName shape: the helper runs the tenant-scoped exact-match search and hands back the raw SearchResponse, leaving each caller to format its own 409 body and short-circuit on blank/same names. Default-off behavior is unchanged - the feature-flag gate still short-circuits before any helper call. The 409 body intentionally does NOT echo the conflicting resource's document id: doing so would let a caller confirm the existence of resources they cannot otherwise see by probing names. Only the caller-supplied name is reflected back. Regression guards (assertFalse checks) are added to the unit and integration tests for both update paths. Signed-off-by: rithin-pullela-aws <rithinp@amazon.com>
- MLCreateMemoryContainerInput: reject null *or* blank name (was null-only), matching MLAgent's constructor-level check. Prevents the create path from accepting a whitespace-only name that would later look valid in search. - MLUpdateMemoryContainerInput: reject a non-null blank name at parse time. Null still means "no rename" (update's name field is optional), but a caller sending " " now fails fast with 400 instead of silently either 409'ing or overwriting the stored name with whitespace in continueUpdate. - TransportUpdateMemoryContainerAction.continueUpdate: tighten the guard from (newName != null) to StringUtils.isNotBlank(newName) as defense-in-depth behind the input-layer check. - UpdateAgentTransportAction: scope the uniqueness search by the retrieved agent's tenantId instead of the update input's, making the authoritative tenant source consistent with the memory-container path. - Standardize on org.apache.commons.lang3.StringUtils across the three name-blank checks so the predicate reads the same everywhere. - IT testRename_BlankName: was asserting 200 (a false green - it didn't read the doc back), now asserts 400 + reads the name back to confirm it wasn't mutated. testRename_ToUnusedName also reads back to confirm the rename actually persisted. - UTs for blank/empty name rejection on both inputs, plus a positive test that null name is still allowed on update. Signed-off-by: rithin-pullela-aws <rithinp@amazon.com>
aa9b9ec to
d4fb1b8
Compare
|
Persistent review updated to latest commit d4fb1b8 |
Description
Adds two opt-in, dynamic cluster settings, both default
falsefor backward compatibility:plugins.ml_commons.agent_name_uniqueness_enabledplugins.ml_commons.agentic_memory_name_uniqueness_enabledWhen enabled, registering an Agent or creating an Agentic Memory Container with a name that already exists in the same tenant is rejected with HTTP 409 CONFLICT. Default off preserves existing behavior.
Semantics: best-effort, not atomic. This matches the existing pattern in
MLModelGroupManager.validateUniqueModelGroupName(term query onname.keyword + tenantId, then unconditional index write) and inherits the same race window 1:1 — two concurrent registrations with the same name can both observe zero hits and both succeed. Closing the race would require a sidecar lock index +OpType.CREATEon a deterministic_id; that hardening should be applied uniformly across model groups, agents, and memory containers and is intentionally left for a follow-up.What this PR adds beyond the model-group precedent:
IllegalArgumentException.PLAN_EXECUTE_AND_REFLECTagents without a pre-existingexecutor_agent_id, the auto-created"<name> (ReAct)"executor agent's name is also validated so the derived agent can't collide with an existing one.IndexNotFoundExceptionfrom the search is treated as "no duplicate possible" so first-ever registrations on a fresh cluster work.Manual verification
Verified end-to-end on a single-node cluster (
./gradlew :opensearch-ml-plugin:run):_cluster/settings?include_defaults=trueasfalseRelated Issues
N/A
Check List
--signoff.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check here.