Skip to content

fix(openapi): schema name collision hardening - #786

Merged
albcui merged 9 commits into
mainfrom
aalgo-352-openapi-schema-collision-hardening/albcui
Jul 20, 2026
Merged

fix(openapi): schema name collision hardening#786
albcui merged 9 commits into
mainfrom
aalgo-352-openapi-schema-collision-hardening/albcui

Conversation

@albcui

@albcui albcui commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary:

  • The issue is that when there's a name collision in the OpenAPI $ref namespace AND the actual schema differs, we accept the first one and throw away the rest. The result is an incorrect OpenAPI spec post merge. This PR hardens the OpenAPI spec processing to strictly error when collisions are detected. This revealed the following issues, which are fixed as part of this PR.
  • The name DeleteResponse and GenericSortField had collisions between Guardrails and Core Entities services.
    • Guardrails: drop the local DeleteResponse and GenericSortField, and re-use the ones in nmp-common
    • Core Entities Service: re-use GenericSortField from nmp-common. Re-named DeleteResponse to EntityDeleteResponse for now because it has an extra deleted_count field. However, ideally, we can leverage nmp-common's DeleteResponse as well.

Summary by CodeRabbit

  • New Features

    • Added updated_at / -updated_at sorting to multiple list commands (including workspaces, filesets, guardrail configs, and related resources).
    • Standardized list sorting to use a shared, entity-style sort contract.
    • Updated delete responses to return the deleted resource’s id (removing deleted_at).
  • Documentation

    • Updated CLI reference and OpenAPI specifications to reflect new --sort options and the updated delete response schema.
  • Bug Fixes

    • OpenAPI schema-name collisions now fail by default during spec generation.

@github-actions github-actions Bot added the fix label Jul 20, 2026
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 26384/34035 77.5% 61.8%
Integration Tests 15177/32660 46.5% 18.7%

@albcui
albcui force-pushed the aalgo-352-openapi-schema-collision-hardening/albcui branch from 6cf17d6 to e3246bd Compare July 20, 2026 19:49
@albcui
albcui marked this pull request as ready for review July 20, 2026 19:49
@albcui
albcui requested review from a team as code owners July 20, 2026 19:49
@albcui
albcui requested a review from JashG July 20, 2026 19:49
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change aligns shared entity delete and sort contracts across services, OpenAPI specifications, and CLI documentation. It also makes OpenAPI schema-collision handling fail by default and updates related generation logic and tests.

Changes

Entity API contract alignment

Layer / File(s) Summary
Backend response and sorting contracts
packages/nmp_common/..., services/core/entities/..., services/core/auth/..., services/guardrails/...
Shared DeleteResponse and GenericSortField replace local definitions; delete responses return id, and shared sort values are forwarded.
Delete response integration validation
services/core/*/tests/integration/*
Integration tests validate identifier-based delete responses and post-delete state.
Published OpenAPI schemas and endpoint references
openapi/*.yaml, openapi/ga/**/*.yaml
DeleteResponse requires id, GenericSortField gains sort variants, and ProjectSortField is removed.
CLI sort declarations and documentation
packages/nemo_platform_ext/..., docs/cli/reference.mdx
CLI sort literals and reference entries expose updated timestamp options.

Strict OpenAPI collision handling

Layer / File(s) Summary
Strict normalization and generation defaults
packages/nmp_common/..., script/..., plugins/nemo-customizer/pyproject.toml
Collision overrides are removed, plugin specs use one generation path, and strict collision configuration defaults to true.
Collision behavior tests
packages/nmp_common/tests/api/test_utils_openapi_spec.py
Tests assert that schema-name collisions raise ValueError by default.

Sequence Diagram(s)

sequenceDiagram
  participant process_plugin_specs
  participant apply_schema_fixes
  participant tweak_spec
  participant _normalize_refs_and_schema_keys
  process_plugin_specs->>apply_schema_fixes: process all plugin specs
  apply_schema_fixes->>tweak_spec: tweak specification
  tweak_spec->_normalize_refs_and_schema_keys: normalize schema names
  _normalize_refs_and_schema_keys-->>tweak_spec: raise ValueError on differing collision
Loading

Possibly related PRs

Suggested reviewers: maxdubrinsky

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: hardening OpenAPI schema collision handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aalgo-352-openapi-schema-collision-hardening/albcui

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/cli/reference.mdx`:
- Line 1163: Regenerate the CLI reference documentation using the make target
generate-cli-reference-docs, rather than editing the sort option entries
manually, and commit all resulting generated changes across the affected
occurrences.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9ddbc0c6-692e-454d-b6fc-2b6af207007f

📥 Commits

Reviewing files that changed from the base of the PR and between e3246bd and 0934f9d.

⛔ Files ignored due to path filters (3)
  • sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/files/filesets.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/guardrail/configs.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/prompts.py is excluded by !sdk/**
📒 Files selected for processing (4)
  • docs/cli/reference.mdx
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/files/filesets.py
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/guardrail/configs.py
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/prompts.py

Comment thread docs/cli/reference.mdx
Comment thread script/openapi_helper/plugin_config.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py (1)

458-461: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

DeleteResponse.id semantics differ from the IAM role-binding delete.

Here id is the resource's natural key (name / principal_id), matching the URL path parameter. In iam/endpoints.py, revoke_role_binding instead returns obj.id (the entity's surrogate UUID), not the path name. Same shared DeleteResponse.id field, two different semantics across services — confusing for SDK consumers expecting one meaning.

Also applies to: 921-924

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py`
around lines 458 - 461, Update the workspace deletion handler’s DeleteResponse
construction to return the deleted workspace entity’s surrogate UUID in id,
matching revoke_role_binding, instead of the path-based name. Preserve the
existing deletion message and response structure.
services/core/auth/src/nmp/core/auth/api/v2/iam/endpoints.py (1)

248-248: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Inconsistent id semantics vs. other DeleteResponse producers — see consolidated comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/auth/src/nmp/core/auth/api/v2/iam/endpoints.py` at line 248,
Update the DeleteResponse construction in this endpoint to match the established
id semantics used by the other DeleteResponse producers, rather than
unconditionally returning obj.id. Preserve the existing deletion flow and align
the response field with the consolidated convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@services/core/auth/src/nmp/core/auth/api/v2/iam/endpoints.py`:
- Line 248: Update the DeleteResponse construction in this endpoint to match the
established id semantics used by the other DeleteResponse producers, rather than
unconditionally returning obj.id. Preserve the existing deletion flow and align
the response field with the consolidated convention.

In `@services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py`:
- Around line 458-461: Update the workspace deletion handler’s DeleteResponse
construction to return the deleted workspace entity’s surrogate UUID in id,
matching revoke_role_binding, instead of the path-based name. Preserve the
existing deletion message and response structure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: a89ef811-639d-4666-87f6-c7c52c913885

📥 Commits

Reviewing files that changed from the base of the PR and between d7999aa and 6e35fb3.

⛔ Files ignored due to path filters (7)
  • sdk/python/nemo-platform/.nmpcontext/openapi.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/.nmpcontext/stainless.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/shared/delete_response.py is excluded by !sdk/**
  • sdk/python/nemo-platform/tests/api_resources/test_projects.py is excluded by !sdk/**
  • sdk/stainless.yaml is excluded by !sdk/**
📒 Files selected for processing (13)
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • packages/nmp_common/src/nmp/common/api/common.py
  • services/core/auth/src/nmp/core/auth/api/v2/iam/endpoints.py
  • services/core/auth/tests/integration/test_iam_role_bindings.py
  • services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py
  • services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py
  • services/core/entities/src/nmp/core/entities/api/v2/schemas.py
  • services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py
  • services/core/entities/tests/integration/test_generic_entities.py
  • services/core/entities/tests/integration/test_projects.py
  • services/core/entities/tests/integration/test_workspace_members.py
💤 Files with no reviewable changes (3)
  • services/core/auth/tests/integration/test_iam_role_bindings.py
  • services/core/entities/src/nmp/core/entities/api/v2/schemas.py
  • services/core/entities/tests/integration/test_workspace_members.py

albcui added 9 commits July 20, 2026 18:17
#737 fixed the Automodel/Unsloth backends, but two collisions remained across
the merged platform services and were silently collapsed by the schema-name
normalizer:

- DeleteResponse: core-entities' model (required id + deleted_count) collided
  with the shared nmp.common.api.common.DeleteResponse (id, deleted_at);
  guardrails had redefined the shared shape locally.
- GenericSortField: core-entities, guardrails, and common each defined a
  different enum under the same name.

The merge kept only the first-seen schema and repointed every $ref to it, so
delete/sort endpoints across services referenced the wrong contract in the
generated SDK.

Give each its true name (schema-name change only; JSON wire format unchanged):

- guardrails: drop the local DeleteResponse, import the byte-identical shared
  nmp.common.api.common.DeleteResponse.
- core-entities: DeleteResponse -> EntityDeleteResponse (keeps deleted_count),
  updating all four delete endpoints.
- core-entities GenericSortField -> WorkspaceSortField; guardrails
  GenericSortField -> GuardrailConfigSortField.

Regenerates openapi/openapi.yaml and the ga/ merged specs.

Refs AALGO-352.

Signed-off-by: Albert Cui <albcui@nvidia.com>
The differing-content collision gate added in #737 was opt-in: only the
nemo-customizer plugin spec enforced it, while platform/service specs stayed on
warn-and-collapse. That is exactly how the AALGO-352 collisions shipped
silently.

With those fixed (zero collisions across the platform, all services, and all
plugins), flip the default so any future collision fails spec generation loudly
instead of shipping a wrong SDK contract:

- tweak_spec, apply_schema_fixes, and PluginConfig.strict_schema_collisions
  default to True. The plugin "lenient" branch now passes
  strict_collisions=False explicitly (it previously relied on the old default).
- A spec can still opt out via strict_collisions=False /
  [tool.nemo.openapi].strict_schema_collisions = false.
- Generalize the collision error message (rename/dedupe or namespace) and
  invert the unit tests to pin the policy: default raises, opt-out warns.

Enforcement is at spec-generation time (make refresh-openapi / the manual
openapi-generator pre-commit hook). CI does not regenerate the spec today, so
this does not yet gate PRs on its own; adding a CI regen/drift check is a
follow-up.

Refs AALGO-352.

Signed-off-by: Albert Cui <albcui@nvidia.com>
Regenerate the vendored Python SDK (`make update-sdk`) to pick up the schema
renames from the AALGO-352 fix. Adds EntityDeleteResponse, WorkspaceSortField,
and GuardrailConfigSortField: the entities/projects/workspaces delete endpoints
now return EntityDeleteResponse, and the workspaces / guardrail-config list sort
params reference the new enums. The shared DeleteResponse / GenericSortField are
unchanged (still used by iam role-bindings, files, etc.).

The Stainless mapping report was warning-free; the three new models auto-mapped
correctly (entity_delete_response under $shared since it is returned by three
resources; the two sort fields under their own resources).

Also hand-update packages/nemo_platform_plugin/entities.py: its delete() /
delete_by_id() wrapper methods forward AsyncEntitiesResource.delete_entity_by_name,
whose return type is now EntityDeleteResponse, so retype those annotations. Regen
does not touch this hand-written file. (Pre-existing `parent: str | Omit` ty
diagnostics in that file are unrelated and left as-is.)

Refs AALGO-352.

Signed-off-by: Albert Cui <albcui@nvidia.com>
Signed-off-by: Albert Cui <albcui@nvidia.com>
Guardrails' config list defined its own GuardrailConfigSortField (created_at
only) in the legacy nmp.guardrails.app.common.common. GuardrailConfig is
entity-store-backed, so it can sort by any entity base field; point it at the
shared nmp.common.api.common.GenericSortField and retire the local enum.

Widen GenericSortField with updated_at — both guardrail configs and filesets
are entity-backed and the store already supports the column — and convert it
from (str, Enum) to StrEnum. A bare (str, Enum) member stringifies to
"GenericSortField.CREATED_AT_ASC" rather than "created_at", so passing the
member straight to the entity client produced a malformed sort and broke the
list request; StrEnum makes members stringify to their value. Guardrails now
passes sort.value, matching the filesets endpoint.

Regenerate the OpenAPI spec and Python SDK for the removed GuardrailConfigSortField
and widened GenericSortField.

Signed-off-by: Albert Cui <albcui@nvidia.com>
Signed-off-by: Albert Cui <albcui@nvidia.com>
Workspaces and projects each defined their own sort enum (WorkspaceSortField,
ProjectSortField) duplicating the shared nmp.common.api.common.GenericSortField.
Both are entity-store-backed and sort via getattr over real base columns, so
point them at the shared enum and delete the bespoke ones. ProjectSortField was
already identical (created_at, updated_at, name); workspaces additively gains
name-sorting (DBWorkspace.name is a real, unique column). This also retires
ProjectSortField's stale (str, Enum) base in favor of the shared StrEnum.

Regenerate the OpenAPI spec, Python SDK, and CLI for the removed schemas and the
widened workspace sort options.

Signed-off-by: Albert Cui <albcui@nvidia.com>
Entity/project/workspace delete endpoints returned EntityDeleteResponse
(message, id, deleted_count) while guardrails and IAM used the shared
nmp.common.api.common.DeleteResponse (message, id, deleted_at) — the name
collision the hardening pass resolved by keeping two models. Both extra
fields turned out to be write-only (read only by integration tests; no web
or SDK consumer), and deleted_count is ~always 1 (delete_entity_by_name
returns a hardcoded 1; the handler 404s on 0). So collapse to a single
DeleteResponse{message, id} with id required, and point all six delete
endpoints (entities, projects, workspaces, members, guardrails, IAM) plus
the plugin EntityClient at it. Drop EntityDeleteResponse, deleted_count,
and deleted_at.

Regenerate the OpenAPI spec and Python SDK; update the integration asserts
that checked the removed fields.

Signed-off-by: Albert Cui <albcui@nvidia.com>
The per-plugin strict_schema_collisions opt-out (warn-and-collapse) had no
users — every collision was fixed and no spec opts out — and it can never be
correct: collapsing two differing-content models into one name silently
repoints the other's $refs, shipping a wrong contract in the generated SDK.
It was migration scaffolding for the lenient->strict flip, now complete.

Remove it entirely: drop PluginConfig.strict_schema_collisions and the
strict_collisions parameter threaded through tweak_spec /
_normalize_refs_and_schema_keys / apply_schema_fixes, collapse the plugin
strict/lenient split to a single pass, and delete the customizer's now-
redundant pin plus the warn-and-collapse test. Collisions now always raise.

No schema or SDK change: the generated spec is byte-identical.

Signed-off-by: Albert Cui <albcui@nvidia.com>
@albcui
albcui force-pushed the aalgo-352-openapi-schema-collision-hardening/albcui branch from 0fbeb06 to a1a2fe8 Compare July 20, 2026 22:23
@albcui
albcui enabled auto-merge July 20, 2026 22:23
@albcui
albcui added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit 862b00f Jul 20, 2026
60 of 61 checks passed
@albcui
albcui deleted the aalgo-352-openapi-schema-collision-hardening/albcui branch July 20, 2026 22:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants