Allow accepting the CloudXR EULA non-interactively - #7380
Allow accepting the CloudXR EULA non-interactively#73802047767028-lang wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Isaac Lab Review Bot
The change adds the opt-in ISAACLAB_CXR_ACCEPT_EULA=1 environment variable and forwards its parsed value to the existing CloudXRLauncher(accept_eula=...) parameter, with a corresponding package changelog fragment.
- Design and architecture: The behavior is localized to the existing CloudXR launcher construction path and follows the adjacent environment-variable escape-hatch pattern. Launcher lifecycle, shutdown ownership, and auto-launch precedence remain unchanged. The environment-only control is a reasonable narrow scope for non-interactive deployments.
- API: No public Python symbols, signatures, defaults, or return types change. Unset values and values other than
1continue to passFalse, preserving the existing interactive EULA flow. The new user-visible environment variable is documented in the method and changelog. - Implementation: The exact
strip() == "1"parsing is deterministic and consistent with the siblingISAACLAB_CXR_SKIP_AUTOLAUNCHhandling. Existing early returns remain ahead of launcher construction, and the already-importedosmodule is reused without adding dependencies. The principal residual risk is the absence of automated regression coverage for the launcher path, but the implementation is sufficiently direct that this does not establish a pre-merge defect.
No blocking issues. No inline issue met the actionable-evidence threshold; the assessment above records the review feedback.
Automated review; human maintainers own approval decisions.
Greptile SummaryThe PR adds an opt-in environment variable that passes non-interactive NVIDIA CloudXR EULA acceptance to the teleoperation session lifecycle and documents it in a changelog fragment.
Confidence Score: 4/5The PR should not merge until the replay workflow’s independent CloudXR launcher also honors the new EULA acceptance setting. Headless replay runs bypass the changed lifecycle method and continue passing Files Needing Attention: source/isaaclab_teleop/isaaclab_teleop/session_lifecycle.py and scripts/environments/teleoperation/teleop_replay_agent.py Important Files Changed
Reviews (1): Last reviewed commit: "Allow accepting the CloudXR EULA non-int..." | Re-trigger Greptile |
| install_dir=str(Path.home() / ".cloudxr"), | ||
| env_config=self._cloudxr_env_file, | ||
| accept_eula=False, | ||
| accept_eula=os.environ.get(_CXR_ACCEPT_EULA_ENV, "").strip() == "1", |
There was a problem hiding this comment.
Replay bypasses EULA acceptance
When a headless replay run enables CloudXR auto-launch with ISAACLAB_CXR_ACCEPT_EULA=1, the replay agent uses its separate launcher with accept_eula=False and disables this lifecycle-managed path, causing the EULA prompt and RuntimeError that this setting is intended to prevent.
Knowledge Base Used: XR teleoperation
The NVIDIA CloudXR license is separate from the Omniverse one, and
`_ensure_cloudxr_runtime` hardcoded `accept_eula=False` when constructing the
`CloudXRLauncher`. With no acceptance marker present the launcher then prompts
on stdin, so any run without a terminal attached -- `nohup`, a container, CI --
dies with:
Accept NVIDIA CloudXR EULA? [y/N]: EULA not accepted. Exiting.
RuntimeError: CloudXR EULA was not accepted; cannot start the runtime
`isaacteleop` already supports accepting it up front: `CloudXRLauncher` takes an
`accept_eula` argument and registers an `--accept-eula` flag for exactly this
case. Isaac Lab simply never surfaced it.
Read `ISAACLAB_CXR_ACCEPT_EULA=1` and pass it through. The env-var form matches
the `ISAACLAB_CXR_SKIP_AUTOLAUNCH` escape hatch a few lines above, and mirrors
`OMNI_KIT_ACCEPT_EULA` for the Omniverse license. Unset or any other value keeps
the current interactive prompt, so existing behaviour is unchanged.
Verified on a headless multi-GPU workstation: with the acceptance marker at
`~/.cloudxr/run/eula_accepted` deleted, a `setsid nohup` teleop run now starts
the runtime and recreates the marker instead of aborting.
Signed-off-by: 2047767028-lang <2047767028@qq.com>
AI review commentGenerated by Codex (AI). @rwiltz Could you please confirm the intended CloudXR behavior for XR replay? Greptile correctly identified a real gap. teleop_replay_agent.py deliberately owns a separate, process-scoped CloudXRLauncher and disables the lifecycle-managed launcher, but that separate path still passes accept_eula=False. Consequently, ISAACLAB_CXR_ACCEPT_EULA=1 does not prevent the EULA prompt and RuntimeError for headless XR replay, despite the changelog saying the variable applies when teleop scripts auto-launch CloudXR. I recommend addressing this before merge by making _maybe_launch_cloudxr() honor the same environment variable, ideally through shared parsing logic. If replay is intentionally out of scope, the changelog should explicitly limit the promise to lifecycle-backed scripts. There is also an existing unit-test harness in source/isaaclab_teleop/test/test_cloudxr_lifecycle.py. At the exact PR head:
Please add focused cases for 1, whitespace-padded 1, unset, and non-1 values, while isolating both CloudXR environment variables. The new variable should also be documented alongside ISAACLAB_CXR_SKIP_AUTOLAUNCH in docs/source/features/isaac_teleop.rst. |
Review feedback from @AntoineRichard on three counts. **Replay was not covered.** `teleop_replay_agent._maybe_launch_cloudxr` owns a second, process-scoped `CloudXRLauncher` and passed `accept_eula=False` of its own, so `ISAACLAB_CXR_ACCEPT_EULA=1` did not prevent the prompt for headless XR replay even though its docstring states it mirrors the lifecycle gating. Both call sites now go through one `cloudxr_eula_accepted()` helper, exported from `isaaclab_teleop`, so the variable means the same thing wherever the runtime is started. **The existing suite was not isolated from the new variable.** With `ISAACLAB_CXR_ACCEPT_EULA=1` exported, `test_launches_with_correct_args` failed: it popped only `ISAACLAB_CXR_SKIP_AUTOLAUNCH` while still asserting `accept_eula=False`. It now clears both CloudXR variables. **Added focused coverage** in `TestCloudXREulaAcceptance`: an exact `1`, `1` padded with spaces and with tabs/newlines, six non-accepting values including the empty string and `1 1`, the unset case, and one asserting the two CloudXR variables do not read each other. Each case checks both the helper and the `accept_eula` value that reaches the launcher. Verified in the Isaac Lab conda environment: 33 passed with the variable unset and with each of `1`, ` 1 `, `0` and `yes` exported, so the suite no longer depends on the ambient environment. Also documents the variable in `docs/source/features/isaac_teleop.rst` next to `ISAACLAB_CXR_SKIP_AUTOLAUNCH`, including that acceptance is recorded in `~/.cloudxr/run/eula_accepted` and that it applies to the replay launcher too. Signed-off-by: 2047767028-lang <2047767028@qq.com>
|
Thanks @AntoineRichard — all three points were real, and the replay gap in particular was a Replay now shares the parsing. def cloudxr_eula_accepted() -> bool:
return os.environ.get(_CXR_ACCEPT_EULA_ENV, "").strip() == "1"exported from The test isolation bug is fixed. You are right that Added Run in the Isaac Lab conda environment, varying only the ambient variable:
(22 pre-existing plus 11 new parametrised cases; previously Documented in |
isaac-sim#7381 added _env_file_pins_gpu_index/_renderer_cuda_index at the same spot in session_lifecycle.py where this branch adds cloudxr_eula_accepted. Both blocks are pure additions and are kept; nothing else conflicted. No behaviour change: test_cloudxr_lifecycle.py passes (33 tests) with ISAACLAB_CXR_ACCEPT_EULA unset, 1 and 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SsU8ziGULaEBbtmvJSQZW
|
Merged Re-ran on the merged tree, varying only the ambient variable:
The other Kit-free suites in |
|
@AntoineRichard @rwiltz — this one is ready whenever you have a moment. The three review points from 8/28 are addressed in 7c48a40 (replay now shares the same parsing helper, the test isolation bug is fixed, and the variable is documented in One note on the red check: Happy to adjust anything if it would help. |
The docs offered ISAACLAB_CXR_ACCEPT_EULA as working "the same way
OMNI_KIT_ACCEPT_EULA works for the Omniverse license", but the parser only
took an exact 1, while Kit's gate is
os.environ.get("OMNI_KIT_ACCEPT_EULA", default="N").lower() in ["y", "yes", "1"]
and every use of that variable in this repository spells it Y or yes, never 1.
A user carrying that spelling over got ISAACLAB_CXR_ACCEPT_EULA=yes silently
ignored and the run aborted with the RuntimeError this variable exists to
avoid, with nothing to indicate the variable had been read and rejected.
Accept y, yes and 1 instead, case-insensitively. This matches Kit's set exactly
and also matches the CloudXR prompt itself, which takes y/yes, so the answer a
user would type at the prompt is the answer they can put in the variable.
Surrounding whitespace is still stripped and everything else -- 0, no, true,
unset -- still keeps the interactive prompt, so acceptance remains an explicit
opt-in.
AntoineRichard
left a comment
There was a problem hiding this comment.
I pushed a commit to this branch (e916ff0) rather than leaving it as a suggestion, since it touched three files together — see the commit message for the reasoning. Short version: the docs offer ISAACLAB_CXR_ACCEPT_EULA as working "the same way OMNI_KIT_ACCEPT_EULA works", but Kit's gate is
# isaacsim/kit/kit_app.py:19
if os.environ.get("OMNI_KIT_ACCEPT_EULA", default="N").lower() in ["y", "yes", "1"]:and every use of that variable in this repo spells it Y or yes, never 1 (docs/source/setup/installation/index.rst:478, the LEAPP pages, docs/source/experimental-features/rlinf_vla_posttraining.rst:78). So a user carrying that spelling over got ISAACLAB_CXR_ACCEPT_EULA=yes silently ignored and the run aborted with the exact RuntimeError this PR exists to remove. I ran every plausible value end-to-end through the real isaacteleop.cloudxr.runtime.check_eula and Kit's real check_eula with stdin closed; y/Y/yes/Yes/YES started under Kit and aborted under CloudXR. The commit accepts y, yes and 1 case-insensitively, which matches Kit's set exactly and also matches the CloudXR prompt itself (reply not in ("y", "yes")), so the answer a user would type at the prompt is the answer they can put in the variable. true/0/no/unset still keep the prompt. 39 tests pass.
Two smaller things left for you below. Nice fix overall — the isaacteleop side supports this cleanly and the gap was real.
| @@ -0,0 +1,10 @@ | |||
| Added | |||
There was a problem hiding this comment.
This should be pk-cloudxr-eula-noninteractive.minor.rst.
An unsuffixed <slug>.rst is a patch bump, but this PR adds cloudxr_eula_accepted to isaaclab_teleop.__all__, and docs/source/refs/contributing.rst:166 maps "minor bump (new public API)" to the .minor.rst suffix. The fragment's own section being Added points the same way.
Worth flagging that tools/changelog/cli.py check passes either way — it validates the filename pattern and the section headers, not whether the tier matches the change — so this one won't correct itself in CI.
There was a problem hiding this comment.
Renamed in 2508e10 — the fragment is now pk-cloudxr-eula-noninteractive.minor.rst.
You're right that the tier was wrong and that CI won't say so: tools/changelog/cli.py check --include-worktree reports ✓ All modified packages have valid changelog fragments. both before and after the rename, because it validates the filename pattern and the section headers, not whether the tier matches the change. The PR does add a name to isaaclab_teleop.__all__, and docs/source/refs/contributing.rst maps <slug>.minor.rst to "minor bump (new public API)", so .minor is the right tier.
| XrCameraFeedLayoutCfg, | ||
| ) | ||
| from .isaac_teleop_device import IsaacTeleopDevice, create_isaac_teleop_device | ||
| from .session_lifecycle import cloudxr_eula_accepted |
There was a problem hiding this comment.
This line is load-bearing and nothing tests it. teleop_replay_agent.py imports from isaaclab_teleop import cloudxr_eula_accepted, which only resolves because lazy_export() reads this stub — but test_cloudxr_lifecycle.py imports from isaaclab_teleop.session_lifecycle directly, so it never exercises the package-level name.
I confirmed the gap: deleting this line leaves all tests green while from isaaclab_teleop import cloudxr_eula_accepted raises ImportError: cannot import name 'cloudxr_eula_accepted' from 'isaaclab_teleop'. The replay path would break at runtime with a passing suite.
One assertion in TestCloudXREulaAcceptance closes it:
def test_exported_from_package_root(self):
"""The replay agent imports the helper from the package root, not the submodule."""
import isaaclab_teleop
assert isaaclab_teleop.cloudxr_eula_accepted is cloudxr_eula_acceptedThere was a problem hiding this comment.
Added in 2508e10, as your assertion.
I reproduced the gap before adding it rather than taking it on faith. Deleting from .session_lifecycle import cloudxr_eula_accepted from the stub and re-running the file:
1 failed, 39 passed in 2.09s
FAILED TestCloudXREulaAcceptance::test_exported_from_package_root
- AttributeError: No isaaclab_teleop attribute cloudxr_eula_accepted
The AttributeError comes out of lazy_loader/__init__.py:90, so lazy_export() genuinely has nothing to resolve the name with once that line is gone — the surviving "cloudxr_eula_accepted" entry in __all__ is not enough on its own. The other 39 tests stay green, which is the part that makes this worth covering: scripts/environments/teleoperation/teleop_replay_agent.py:1006 does from isaaclab_teleop import cloudxr_eula_accepted, so that deletion breaks the replay path at runtime with a fully passing suite.
With the line restored, 40 pass.
Two follow-ups from review. The fragment was an unsuffixed <slug>.rst, a patch bump, but this PR adds cloudxr_eula_accepted to isaaclab_teleop.__all__ and the stub that exports it. docs/source/refs/contributing.rst maps "minor bump (new public API)" to the .minor.rst suffix, and the fragment's own Added section says the same. CI does not catch this: tools/changelog/cli.py check validates the filename pattern and the section headers, not whether the tier matches the change. The line in __init__.pyi that exports the helper was load-bearing and untested. teleop_replay_agent.py imports it from the package root, which only resolves because lazy_export() reads that stub, while the suite imports from isaaclab_teleop.session_lifecycle directly and so never exercises the package-level name. Deleting the stub line leaves all 39 tests green while "from isaaclab_teleop import cloudxr_eula_accepted" raises, so the replay path would break at runtime with a passing suite. Confirmed by removing the line: only the new assertion fails, with "No isaaclab_teleop attribute cloudxr_eula_accepted" out of lazy_loader. 40 tests pass with it restored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SsU8ziGULaEBbtmvJSQZW
|
Thanks @AntoineRichard — both follow-ups are in 2508e10, and the parsing change in e916ff0 is a fix I should have made myself. I checked the premise rather than just taking the commit: Both follow-ups are answered in the threads above, with the reproduction for each. The suite is at 40 passing, |
|
@rwiltz @hougantc-nvda @kellyguo11 — this is approved by @AntoineRichard (2026-09-04) and now only needs a CODEOWNER review to unblock: Two notes for whoever picks it up:
|
|
run-ci |
Description
The NVIDIA CloudXR license is separate from the Omniverse one, and
TeleopSessionLifecycle._ensure_cloudxr_runtimehardcodesaccept_eula=Falsewhen itconstructs the
CloudXRLauncher. With no acceptance marker present the launcher then promptson stdin, so any run without a terminal attached —
setsid nohup, a container, CI — aborts:isaacteleopalready supports accepting it up front —CloudXRLaunchertakes anaccept_eulaargument and even registers an--accept-eulaflag whose help text reads"Accept the NVIDIA CloudXR EULA non-interactively (e.g. for CI or containers)". Isaac Lab
simply never surfaced it, so there is no way to reach it from
isaaclab teleop run,teleop_se3_agent.pyorrecord_demos.py.This reads
ISAACLAB_CXR_ACCEPT_EULA=1and passes it through. The env-var form matches theISAACLAB_CXR_SKIP_AUTOLAUNCHescape hatch a few lines above in the same method, and mirrorsOMNI_KIT_ACCEPT_EULAfor the Omniverse license. Unset — or any other value — keeps today'sinteractive prompt, so existing behaviour is unchanged and the license is still an explicit
opt-in.
Both places that launch the runtime share one
cloudxr_eula_accepted()helper — the sessionlifecycle and the process-scoped launcher in
teleop_replay_agent.py, which owns its ownCloudXRLauncher— so the variable means the same thing whichever script starts CloudXR.Validation. On a headless workstation, with the acceptance marker at
~/.cloudxr/run/eula_accepteddeleted first so the prompt path is actually exercised: asetsid nohupteleop run previously died with theRuntimeErrorabove; withISAACLAB_CXR_ACCEPT_EULA=1it starts the runtime, logsCloudXR runtime auto-launched,recreates the marker, and reaches
waiting for XR session.source/isaaclab_teleop/test/test_cloudxr_lifecycle.pygainsTestCloudXREulaAcceptance(exact
1, whitespace-padded1, six non-accepting values, unset, and independence fromISAACLAB_CXR_SKIP_AUTOLAUNCH), andtest_launches_with_correct_argsnow clears both CloudXRvariables so the suite no longer depends on the ambient environment. 33 pass with the variable
unset and with each of
1,1,0,yesexported.Type of change
Release backport
developScreenshots
Not applicable — terminal-only behaviour.
Checklist
pre-commitchecks with./isaaclab.sh --formatsource/<pkg>/changelog.d/for every touched package (do not editCHANGELOG.rstor bumpextension.toml— CI handles that)CONTRIBUTORS.mdor my name already exists thereNote on the unchecked box: I have left
CONTRIBUTORS.mdalone; let me know if you wouldlike me to add an entry.