Skip to content

fix(agents): resolve or reject a deployment image at submit time [ASTD-532] - #1727

Draft
marcusds wants to merge 1 commit into
mainfrom
astd-532-reject-deployment-without-resolvable-image/mschwab
Draft

fix(agents): resolve or reject a deployment image at submit time [ASTD-532]#1727
marcusds wants to merge 1 commit into
mainfrom
astd-532-reject-deployment-without-resolvable-image/mschwab

Conversation

@marcusds

@marcusds marcusds commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

agents.deployments.default_image was only ever read by the deployment runner, so nothing upstream knew whether an image was required. A docker or k8s deployment submitted without one came back 201 with status pending and then failed on the next reconcile — a mistake that was knowable at submit time, reported asynchronously.

This refuses that request at the boundary, and exposes the resolved default so a client can tell "you must supply an image" from "one will be supplied for you".

Before and after, no default_image configured:

Before After
POST /deployments with no image, mode docker 201 pending, entity created, fails on reconcile 400, nothing created
GET /deployments/defaults {"default_image": ""}

An operator who has configured default_image is unaffected: the request still succeeds with an empty image, and the runner still applies the default. That path now works from Studio too, which previously refused to send it.

Related Issue

ASTD-532

Changes

  • api/v2/deployments.py: refuse a container-mode deployment when neither image nor deployments.default_image resolves. Placed beside the existing use_image_entrypoint guard, which validates the same class of thing on the same request.
  • api/v2/deployments.py: GET /deployments/defaults, returning the image a docker/k8s deployment gets when it omits one.
  • schema.py: DeploymentDefaults response model.
  • plugins/nemo-agents/openapi/openapi.yaml: regenerated.

Notes for reviewers

  • 400, not 422. The ticket proposed 422, but the guard two lines above this one — use_image_entrypoint requires deployment_mode 'docker' or 'k8s' — is a 400 for an equivalent mistake on the same endpoint. Two neighbouring guards disagreeing about the code for the same class of error is worse than either choice. Happy to move both to 422 if that is the preferred direction; I did not want to change existing behaviour by side effect.
  • Route ordering is load-bearing. /deployments/{name} already existed and matches "defaults", so declaring the new route after it would silently turn this into a lookup for a deployment nobody has. It is declared first, with a comment, and test_defaults_is_not_swallowed_by_the_deployment_name_route pins it by asserting the entity client is never called. Moving the route below {name} fails all three defaults tests.
  • Endpoint shape follows an existing precedent rather than inventing one: nemo-iron-swarm already serves GET /model-config-defaults for exactly this — server-side config a form needs in order to pre-fill.
  • The rejection covers the CLI and REST too, not only Studio. Both previously accepted a request that could not succeed.

Not included

  • Studio still has its own client-side rule (CreateDeploymentModal, "Container image is required for Docker and Kubernetes deployments"). Nothing consumes this endpoint yet; relaxing that rule to match the server is the client half and belongs on a Studio branch.
  • No Python SDK regeneration. make update-sdk produces no change for this endpoint — see Verification.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with documentation updates
  • Documentation only
  • Contributor tooling or automation
  • CI, build, or test infrastructure

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Documentation updated for user-visible behavior
  • Documentation not applicable — justification: the OpenAPI spec is regenerated and carries the endpoint's own description; no prose documents default_image today.

Verification

  • Pull request title follows the repository's Conventional Commit format
  • Every commit includes an appropriate Signed-off-by: trailer
  • uv run pre-commit run -a passes, or any blocked checks are identified below
  • Targeted tests pass, or tests are marked not applicable above
  • No secrets, API keys, or credentials are included

Targeted validation:

Command Result
uv run pytest plugins/nemo-agents/tests/unit/test_deployments_api.py 17 passed
uv run pytest plugins/nemo-agents/tests/unit 1530 passed; 2 failed, both test_port_allocation.py, which bind()s a socket and is blocked by the local sandbox — unrelated, and failing on main
uv run ruff check / ruff format --check on changed files All checks passed
uv run --frozen ty check on changed files All checks passed
make refresh-openapi endpoint and schema present, +48 lines in the plugin spec

Both new behaviours were checked against a deliberately broken build, so the tests are not decorative:

  • Removing the submit-time guard fails test_create_rejects_container_deployment_with_no_resolvable_image.
  • Moving /deployments/defaults below /deployments/{name} fails all three defaults tests.

Why there is no SDK change. AGENTS.md asks for make update-sdk when endpoints change, so I ran it. It regenerated 31 files as 1 insertion and 94 deletions, none of them this endpoint — because Stainless generates the Python SDK from the root openapi/openapi.yaml, and agents plugin endpoints are not in it:

grep -c "agents/v2/workspaces/{workspace}/deployments" openapi/openapi.yaml                 → 0
grep -c "agents/v2/workspaces/{workspace}/deployments" plugins/nemo-agents/openapi/openapi.yaml → 5

The SDK has never covered these endpoints, including the five that already existed. That deletion-only diff is pre-existing drift between the checked-in SDK and current Stainless output, and committing it here would remove unrelated content while adding nothing. I reverted it rather than fold it into this PR. It looks worth its own issue.

Blocked pre-commit hooks, both environmental and neither related to this change:

  • uv-lock — requires exactly uv 0.9.14 on PATH; this machine has 0.9.30. No dependency files are touched here.
  • helm-docs — the binary is not installed locally. No Helm files are touched.

make refresh-openapi also needs to run outside the macOS sandbox: uv panics there with the known system_configuration::dynamic_store crash noted in CLAUDE.md.

@github-actions github-actions Bot added the fix label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 38371/48948 78.4% 62.5%
Integration Tests 23059/46186 49.9% 22.6%

@marcusds
marcusds force-pushed the astd-532-reject-deployment-without-resolvable-image/mschwab branch from 0760592 to 219b6d7 Compare September 2, 2026 18:44
The runner resolves 'image or deployments.default_image' and fails the
deployment when neither is set. The API did not look at the image at all, so
a docker or k8s deployment submitted without one returned 201 with status
pending and only failed on the next reconcile — a mistake that was knowable
at submit time, reported asynchronously.

Refuse the request instead. Nothing is created, and the message names both
ways to satisfy it. An operator who has configured default_image is
unaffected: the request stays empty and the runner still applies the default.

The server is the only place that knows this, so it stays the only place that
decides it; clients surface the rejection rather than predicting it.

Also type a pre-existing resources={...} literal in this test module; ty
rejects it and would otherwise block committing alongside it.

Signed-off-by: mschwab <mschwab@nvidia.com>
@marcusds
marcusds force-pushed the astd-532-reject-deployment-without-resolvable-image/mschwab branch from 219b6d7 to f972b57 Compare September 2, 2026 19:25
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.

1 participant